メモリ管理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」とか付けなくてよい.

なお,負の整数は, 補数表現によって, 正の整数としてメモリに記録される. この char型の 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)を持ちながら, 表面的には異なる値(表現,−1 or 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 用)等は ビッグエンディアンを採用していた.

さらに,最近の IoT 機器(スマホ,RasPi,Arduino,等)に多く採用されている ARM アーキテクチャな CPU では, バイトオーダを切替可能なバイエンディアン(bi-endian)となっている.

具体例:整数 2567(10進数)=00 00 0A 07(16進数)

ここで,16進数の2桁が 1 byteであることに注意しよう. 16進数1桁ずつではない. 上のリトルエンディアンの具体例を再確認しよう. ... 0A 07 → 70 A0 ... ではなく 07 0A ...

自分が使っている 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 のバイトオーダは何だと判断できるか?

実験室の PC と自宅等の PC では違う結果かもしれない. 機会があれば,それも試してみよう.

ちなみに,バイナリデータの存在について,これまでは気にしていませんでした. これまでは見かけ上,テキストデータだけを取り扱ってきました. しかし実は,書式付き入出力関数 printf("%d", ...) とか scanf("%lf") とかって, バイナリとテキストとの間でデータ形式を変換しているんですよ.

だったら,バイナリなんて考える必要ないべさ? いやいや,テキスト形式では情報量が大き過ぎて困るような場合, 例えば,通信や画像・音声処理の分野では, バイナリ形式が必要となっている.
記録形式と情報量って関係あるの? 整数 0〜255 の例:
  • バイナリ形式:255 = 0xFF → 1 byte
  • テキスト形式:"255" = {'2', '5', '5', '\0'} = {0x30, 0x32, 0x35, 0x35} → 4 byte
バイナリの方が少ない情報量で表現できる. 人間には読み書きしづらいけど.

double型の記録形式

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

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

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

  1. 実数を (−1)S × 1.F × 2(E−1023) の形で表わす.
    要するに,科学的表記法 M.F✕10N のバリエーションのひとつだ. IEEE 形式では,基数を 10 ではなく 2 とする等, コンピュータにとって好都合となるように工夫されている.
  2. 正負符号部 S を 1 bit, 指数部 E を 11 bit, 小数部 F を 52 bit の2進数として,それぞれ符号化(ビット列化)する.
  3. これらの符号化された SEF をこの順序で連結し, 64 桁の符号(ビット列)を作る.
    どんなデータの場合でも共通する 正負符号部の -1,整数部の 1,基数の 2, および指数部の 1023 については,このビット列には含めない. つまり,情報量を削減し,表現効率を高めている.
    なお,整数部を 1 に固定してしまうと,数値 0.0 を表現できないのでは? はい,それは特例として扱われます. IEEE 形式で表現し得る数値の最小値 1.0×2-1023 が 符号 0000 ... 0000 になるので,これを数値 0.0 とみなせば,好都合ですね.

具体例:実数 -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 はふつうの数ではない.

式 1.0/0.0 の値は何か? 答:Inf(infinity),無限大∞です.
式 0.0/0.0 の値は何だ? 答:NaN(not a number)だ. 日本語では「非数」と訳される場合もあるが, 決して「数に非ず(数 "number" ではない)」ということではなく, 「1個の数値としては定まらない(特定の数 "A number" ではない)」という意味ね.

練習問題

バイトオーダ調査プログラムの別バージョンを作成せよ.

上記の例題 int.c では, int型のデータを unsigned char型の配列とみなすことによって, バイトオーダを調べている. これとは逆のアプローチ... unsigned char 型の配列を int型のデータとみなすことによって, バイトオーダを調べるためのプログラム int-2.c を作成せよ.

ヒント:

...
int main(void)
{
	unsigned char	data[4] = {0xCD, 0xAB, 0x34, 0x12};
	unsigned int	*p;
	...
	printf("%08X\n", *p);
	...
}

実行例:

$ ./int-2
...
1234ABCD	# リトルエンディアンの場合
# または
CDAB3412	# ビッグエンディアンの場合

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


バイトオーダ変換プログラムを作成せよ.

互いにバイトオーダの異なるコンピュータ間でデータを共有/通信する場合, バイトオーダの変換処理が必要となる. もし,この変換が無いと,例えば,ある PC では 0x00 01 = 1 であった数値が, 別の PC では 0x01 00 = 256 と見なされてしまうことになる. そこで,int型 16 進数のバイトオーダを変換するプログラム xbo.c を作成せよ.

実行例:

$ ./xbo
16進数 > 1234ABCD
 12 34 AB CD	# PCがリトルエンディアンの場合,ビッグエンディアン形式で表示
# または
 CD AB 34 12	# PCがビッグエンディアンの場合,リトルエンディアン形式で表示

ヒント:16進数の入力には,scanf("%X", ...)


オーバフローを発生させよ.

上の説明の通り,どのデータ型でも,表現できる数値の範囲には制限がある. これをより深く理解するために...

数値の範囲制限を超えるとどうなるか? オーバフロー(overflow)を発生させるプログラム of.c を自由に作成し,実験してみよう.

例:

計算方法(使う数値や反復回数)については模索が必要かもしれない.
整数の場合,表示には,10進数だけでなく16進数を使えば, 仕組を理解し易くなるかもしれない. (逆効果の可能性もある.)
なお,16 = 24 = 0x10 ですね.

本日の課題

レポートを提出せよ.

質問 Q1〜Q5 に回答し,電子メールで提出せよ.

  • 注意事項: