前回までは,メモリ空間内での 複数のデータ(メモリ領域)の位置関係について学習してきた.
空間内のどの領域を(倉庫内のどの箱を)使うのか?
変数 int a の領域はココ, 変数 char b の領域はその隣, さらにその隣は空き領域で, 次は変数 double x, という感じだった.
今回は,個別のデータについて, メモリ領域内での記録形式を解剖しよう.
領域内のどのどこに(連続する複数個の箱のどれに) どのように(どんな順序・方向で)入れられるのか?
変数 int a(4 byte)の領域内の 1 byte 目は何?とか, 変数 double x(8 byte = 64 bit)の領域内の 1 bit 目は何?てな感じ.
なお,「メモリ空間」と「メモリ領域」の意味の微妙な違いに注意.
すでに知っているように, char 型のデータは,文字だけでなく整数を記録するためにも利用される. char 型の情報量は 1 byte = 8 bit なので, 28 = 256 種類の整数 0x00 〜 0xFF(0 ~ 255)を取り扱える. ただし,2通りの使い方があり, それぞれ表現できる数値の範囲が異なる:
これまでにも使って来た整数値としての char 型は,実は, signed char 型だった. (つまり,整数では,デフォルトで signed になる. 非負整数として使いたい場合に「unsigned」を明示せよ.)
なお,符号を気にしなければならないのは,数値として使う場合だけだ. 文字としての char 型の場合, 符号を気にする必要はない. (文字では,「signed」とか「unsigned」とか付けなくてよい.)
なお,負の整数は, 補数表現によって正の整数としてメモリに記録される. つまり,1 byte の場合,負の数 −x を正の数 256 − x で表現する. たとえば,−1 の補数は 256 − 1 = 255 = 0xFF, −2 の補数は 256 − 2 = 254 = 0xFE となる.
List 1 のプログラムを使って,このことを確かめてみよう.
main()
{
signed char sc = 0xFF; // char sc = 0xFF; でも同じ
unsigned char uc = 0xFF;
printf(" signed char : %4d\n", sc);
printf("unsigned char : %4d\n", uc);
}
メモリに記録されているデータは,どちらも同じ 0xFF であるにも関わらず, signed では −1, unsigned では 255 のように, 型によって異なる値をとることがわかるハズだ. 定数 0xFF を他のいろいろな数値に書き換えて実行してみよう. (ただし,8 bit 以内の値 0x00 〜 0xFF に.)
次に,int 型のデータは 4 byte の情報量を持ち, 4つの連続したアドレスのメモリ領域に記録される. つまり,int 型のデータは,メモリ領域内では, 4文字分の文字列と同様の形式で記録されていることになる.
なお,int のサイズについては, 実は 4 byte と決まっているわけではない. 古い処理系では 2 byte だったり, 新しい処理系では 8 byte だったりするかもしれない. 現在の主流が 4 byte というだけ. (つまり正式には,4 という定数ではなく, sizeof(int) で調べて使うべきだが, この授業では 4 としておく.)
また,int 型にも char 型と同様に, signed(符号つき)と unsigned(符号なし)があり, signed がデフォルト.
ただし,領域内でのバイトオーダ(byte order;1 byte 毎の記録順序) には2通りの方式があり, CPU の種類によって異なる方式が採用されている:
ここで,「上位/下位」は桁の順序を意味しており, 「先頭/末尾」は記録の順序(アドレス値の小/大)を意味する.
ちなみに,Intel 社の Core や AMD 社の Athron 等はリトルエンディアンを, IBM 社の PowerPC(Apple の旧 Mac 用)や Cell(Sony の PS3 用)等は ビッグエンディアンを採用している.
具体例:整数 2567(10進数)=00 00 0A 07(16進数)
自分が使っている PC のバイトオーダを List 2 のプログラムによって確かめてみよう.
main() { int data = 0x1234ABCD; // 0x12 が上位のバイト,0xCD が下位のバイト unsigned char *p; p = (unsigned char *)&data; /* 変数 data(int 型)のアドレス &data(int * 型)を 文字列(バイト列,unsigned char * 型)のアドレスとみなす */ printf("%X\n", data); printf(" %02X", p[0]); // 先頭のバイト printf(" %02X", p[1]); printf(" %02X", p[2]); printf(" %02X", p[3]); // 末尾のバイト printf("\n"); }
ポインタとキャストが出てきた. 忘れてしまった人は,復習すること.
また,List 2 の意味を理解するためには, List 1 とメモリマップについても理解している必要がある.
なお,メモリ内に記録されているバイナリデータ (binary data,ビット列,バイト列)を 「生(なま,raw)」の状態で取り出すには, List 2 のように,unsigned char 型へキャストする必要がある. もし,signed char を使ってしまうと, 補数表現によってデータの値が変化してしまう場合があることに注意しよう.
実行した結果より,その PC のバイトオーダは何だと判断できる?
最後に,double 型のデータは,8 byte = 64 bit の情報量をもち, 連続したメモリ領域に, IEEE 倍精度浮動小数点形式のビット列(2進数)として記録される.
IEEE 形式のビット列は次の手順によって生成される:
具体例:実数 -10.5
これを List 3 のプログラムによって確認しよう.
main() { double data = -10.5; unsigned char *p; int i; p = (unsigned char *)&data; printf("%f\n", data); for (i = 0; i < 8; i++) { printf(" %02X", p[i]); } printf("\n"); }
なお,この結果にも当然,バイトオーダが関係している.
main () { unsigned char data[4] = {0xCD, 0xAB, 0x34, 0x12}; unsigned int *p; ... printf("%08X\n", *p); }
$ ./int-2 ... 1234ABCD # リトルエンディアンの場合 または CDAB3412 # ビッグエンディアンの場合
$ ./xbo 16進数 > 1234ABCD 12 34 AB CD # PCがリトルエンディアンの場合,ビッグエンディアン形式で表示 または CD AB 34 12 # PCがビッグエンディアンの場合,リトルエンディアン形式で表示
余裕のある人は,List 3 の逆バージョンも作れるだろう.