メモリ管理2:記録形式

前回は,複数のデータに対して, メモリ空間内での相互の位置関係について学習した.

空間内のどの領域を(倉庫内のどの箱を)使うのか?

変数 int a の領域はココ, 変数 char b の領域はその隣, さらにその隣は空き領域で, 次は変数 double c, という感じだった.

今回は,個別のデータに対して, それぞれのメモリ領域内での記録形式を解剖しよう.

領域内のどのどこに(連続する複数個の箱のどれに) どのように(どんな順序・方向で)入れられるのか?

変数 int a の領域(4 byte)内の 1 byte 目は何?とか, 変数 double b の領域(8 byte = 64 bit)内の 1 bit 目は何?てな感じ.

なお,「メモリ空間」と「メモリ領域」の意味の微妙な違いに注意.

教科書・参考書の該当範囲:なし

char型の記録形式

すでに知っている通り, char型のデータは,文字だけでなく整数を記録するためにも利用される. この char型の情報量(データサイズ)は 1 byte = 8 bit なので, 28 = 256 種類の整数 0x00 〜 0xFF(0 ~ 255)を取り扱える. さらに,charには2通りの使い方があり, それぞれ表現できる数値の範囲が異なる:

これまでにも使って来た整数値としての charは, 実は,signed char 型だった. つまり,整数では,デフォルトで signed になる. 非負整数として使いたい場合に「unsigned」を明示せよ.

なお,符号を気にしなければならないのは,数値として使う場合だけだ. 文字としての charの場合, 符号を気にする必要はない. 文字の場合,「signed」とか「unsigned」とか付けなくてよい.

そして,負の整数は, 補数表現によって正の整数としてメモリに記録される. つまり,1 byte の場合,負の数 −x を正の数 256 − x で表現する. たとえば,−1 の補数は 256 − 1 = 255 = 0xFF, −2 の補数は 256 − 2 = 254 = 0xFE となる. 次のプログラムを使って,このことを確かめてみよう.

ソース char.c:(char型の記録形式の確認)

#include <stdio.h>

int main(void)
{
	signed char	sc = 0xFF;	// char sc = 0xFF; でも同じ
	unsigned char	uc = 0xFF;

	printf("  signed char : %4d\n", sc);
	printf("unsigned char : %4d\n", uc);

	return (0);
}

このプログラムを実行すると, メモリに記録されているデータは, どちらも同じ 0xFF であるにも関わらず, signed char では −1, unsigned char では 255 のように, 型によって異なる値をとることがわかるハズだ. つまり,実体は同じであっても,表現が異なる.

さらに,定数 0xFF を他のいろいろな数値に書き換えて実行してみよう. ただし,当然だが,8 bit 以内の値 0x000xFF に.

int型の記録形式

次に,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進数)

ここで,16進数の2桁が 1 byte であることに注意しよう. 16進数1桁ずつではない.

自分が使っている PC のバイトオーダを 次のプログラムによって確かめてみよう.

ソース int.c:(int型の記録形式の確認)

#include <stdio.h>

int main(void)
{
	int		data = 0x1234ABCD;
	// 0x12 が上位のバイト,0xCD が下位のバイト
	unsigned char	*p;

	p = (unsigned char *)&data;
	/* 変数 data(int 型)のアドレス &data(int * 型)を
	文字列(バイト列,unsigned char * 型)のアドレスとみなす */

	// 4 byte のまま表示
	printf("%X\n", data);

	// 1 byte ずつ表示
	printf(" %02X", p[0]);	// 先頭のバイト
	printf(" %02X", p[1]);
	printf(" %02X", p[2]);
	printf(" %02X", p[3]);	// 末尾のバイト
	printf("\n");

	return (0);
}

ポインタとキャストが出てきた. 忘れてしまった人は, 前期の該当部分を復習すること.

また,配列のメモリマップについても理解している必要がある.

なお,メモリ内に記録されているバイナリデータ (binary data,ビット列,バイト列)を 「生(なま,raw)」の状態で取り出すには, このプログラムのように, データを unsigned char型へキャストする必要がある. もし,この時に char(つまり signed char)型を使ってしまうと, 補数表現によってデータの値が変化してしまう場合があることに注意しよう.

実行結果より,その PC のバイトオーダは何だと判断できるか?


double型の記録形式

最後に,double 型のデータは,8 byte = 64 bit の情報量をもち, 連続したメモリ領域に, IEEE 倍精度浮動小数点形式のビット列(2進数)として記録される.

IEEE は, 米国の電子情報工学系の学会であるが, JIS とか ISO とかと同様に,様々な工業規格を制定している.

IEEE 形式のビット列は次の手順によって生成される:

  1. 実数を (−1)S × 1.F × 2(E−1023) の形で表わす.
  2. 要するに,科学的表記法 M.F✕10N のバリエーションのひとつだ. IEEE 形式では,基数を 10 ではなく 2 とする等, コンピュータにとって好都合となるように工夫されている.
  3. 符号部 S を 1 bit, 指数部 E を 11 bit, 小数部 F を 52 bit の2進数として,それぞれ符号化(ビット列化)する.
  4. これらの符号化された SEF をこの順序で連結し, 64 bit の符号を作る.
  5. どんなデータの場合でも共通する -1,1,2,および 1023 については符号には含めない.

具体例:実数 -10.5

  1. −10.5 = −5.25 × 21 = −2.625 × 22 = −1.3125 × 23
    = (−1)1 × 1.3125 × 21026 − 1023
  2. S = 1(10進数) = 1(2進数,1 bit)
    E = 1026(10進数) = 100 0000 0010(2進数,11 bit)
    0.F = 0.3125(10進数) = 0.0101 0000 0000 ... 0000(2進数,52 bit)
  3. ビット列:1 100 0000 0010 0101 0000 0000 ... 0000(2進数,64 bit)
    = 0xC025 0000 0000 0000(16進数,8 byte)

int.c を改造し,確認してみよう:

$  cp  int.c  double.c
$  vim  double.c

ソース double.c:(double型の記録形式の確認)

#include <stdio.h>

int main(void)
{
	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");

	return (0);
}

実行結果は上の具体例の通りだろうか?

なお,この結果にも当然,バイトオーダが関係している.

さらに,特殊な実数データ 0.0,1.0/0.0,および 0.0/0.0 を data に代入した場合の実行結果についても調べてみよう.

なぜ,0.0 が特殊なデータなのか? 普通じゃねーの? いや,これは 1.F の形にならないので, IEEE 形式としては例外のデータということになる.

また,○/0.0 については,数学的に考察すればわかるハズだ. まさか,1/0=0 だとか 0/0=0 などと思ってはいないよな?

たとえば,1/0 = x と考えると,0*x = 1 だ. どんな数でもゼロ倍したら 0 になるハズなので, それが 1 になってしまうとは,x はふつうの数ではない. x の値は何だ?

0/0 についても同様に考えよう.


本日の課題

  1. int.c の実行結果から, 自分が使っているコンピュータのバイトオーダを調べよ.
  2. 実験室の PC と自宅等の PC では違う結果かもしれない. 機会があれば,それも試してみよう.
  3. int.c では, int型のデータを unsigned char型の配列とみなすことによって, バイトオーダを調べている. これとは逆のアプローチ... unsigned char 型の配列を int型のデータとみなすことによって, バイトオーダを調べるためのプログラム int-2.c を作成せよ.
  4. int型 16 進数の バイトオーダを変換するプログラム xbo.c を作成せよ.
  5. 互いにバイトオーダの異なるコンピュータ間でデータを共有/通信する場合, バイトオーダの変換処理が必要になる. 正しく変換しておかないと,たとえば,ある PC では 0x00 01 = 1 であった数値が, 別の PC では 0x01 00 = 256 と見なされてしまうことになる.

余裕のある人は,double.c の逆バージョンも作れるだろう.

提出:


(c) 2018, yanagawa@kushiro-ct.ac.jp