06 月 14 日(水)3-4h

数値データの記録形式

前回までは,メモリ空間内での 複数のデータ(メモリ領域)の位置関係について学習してきた.

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

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

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

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

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

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


char 型の記録形式

すでに知っているように, 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 のプログラムを使って,このことを確かめてみよう.

List 1. 補数表現の確認 char.c
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 型の記録形式

次に,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 のプログラムによって確かめてみよう.

List 2. int 型の記録形式の確認 int.c
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 型の記録形式

最後に,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 の符号を作る.

具体例:実数 -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)

これを List 3 のプログラムによって確認しよう.

List 3. double 型の記録形式の確認 double.c
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");
}
List 3 の処理内容は List 2 とほぼ同じ. 型が異なるだけ.

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


本日の課題

  1. List 2 の実行結果から, 自分が使っているコンピュータのバイトオーダを調べよ.
  2. 実験室の PC と自宅等の PC では違う結果かもしれない. 試してみよう.
  3. List 2 では, int 型のデータを unsigned char 型の配列とみなすことによって, バイトオーダを調べている. これとは逆のアプローチ (unsigned char 型の配列を int 型のデータとみなすこと) によって, バイトオーダを調べるためのプログラム int-2.c を作成せよ.
    • ヒント:
    • main ()
      {
      	unsigned char data[4] = {0xCD, 0xAB, 0x34, 0x12};
      	unsigned int *p;
      	...
      	printf("%08X\n", *p);
      }
      
    • 実行結果:
    • $ ./int-2
      ...
      1234ABCD	# リトルエンディアンの場合
      または
      CDAB3412	# ビッグエンディアンの場合
      
  4. int 型 16 進数のバイトオーダを変換するプログラム xbo.c を作成せよ.
    • 実行例:
    • $  ./xbo
      16進数 > 1234ABCD
       12 34 AB CD	# PCがリトルエンディアンの場合,ビッグエンディアン形式で表示
      または
       CD AB 34 12	# PCがビッグエンディアンの場合,リトルエンディアン形式で表示
      
    • ヒント:16進数の入力には,scanf("%X", ...)
    • 互いにバイトオーダの異なるコンピュータ間でデータを共有/通信する場合, バイトオーダの変換処理が必要になる. 正しく変換しておかないと,たとえば,ある PC では 0x00 01 = 1 であった数値が, 別の PC では 0x01 00 = 256 と見なされてしまうことになる.

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

レポート提出
  • 提出方法: 電子メール
    • 宛先:yanagawa@kushiro-ct.ac.jp
    • 件名:c-0614
  • 提出期限:06月19日(月)17:00
  • 提出内容(本文):
    • 学年学科,出席番号,氏名
    • 問1の実行結果とバイトオーダ(何エンディアン?(その判断理由は?))
    • 問2のソースコードと実行結果
    • 問3のソースコードと実行結果
    • (疑問)
注意事項: 以下の点についても厳しくチェックする:

  • ソースコードの美しさ(インデント,処理手順のわかり易さ,など)

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