数値や文字などのデータは,プログラムの実行中には, コンピュータのメモリの内部に記録されている. 今回は,データがメモリ内のどこに/どのように記録されるのか? メモリマップ(memory map)について理解しよう.
まず,メモリ内にデータがどんな形式で記録されるのか? 概要を理解しよう.
プログラムが取り扱うすべての種類のデータは, メモリ(RAM)の中に数値(2進数)として記録されている. 各データ型のメモリ内での記録形式は次の通りである:
メモリ内でのデータの実体は,型によらず,すべて2進整数である. ただし,人間は2進数を取り扱うのが苦手なので, Cではそれを 10 進数や 16 進数や文字として表現し, ソースファイルに記述したり画面に表示できるようになっている.
ここで,2種類の表現方法... 記録形式(メモリ内部でのデータの「実体」)と 表示形式(画面上でのデータの「外見」) との間の違いに注意しよう.
たとえば,型変換(double)65 では, 型を整数から実数へ変えているので, 記録形式も表示形式も変わる.65.0 等に. これだと,記録と表示の間に,何か関連があるように見えるかもしれないが...
一方,printf("%c", 65) では, 整数 65 を画面上で文字 'A' に見せかけるだけであり, メモリ内の整数 65 を文字 'A' に書き換えているわけではない. そもそも,メモリ内には数値しか記録できないわけで... 記録形式は変わらず,65 のままであり,表示形式だけが A に変わる.
とにかく,見かけ(画面上の情報)にダマされるな!! 中身(メモリ内の情報)を想像するんだ.
さて,ひとつのプログラムの内部では, 一般に,複数個のデータを利用することになるだろう. そこで,どのデータがメモリ内のどの位置に記録されるのか?具体的に理解しよう.
なお,ここからは,利用可能なメモリの全体をメモリ空間, その内の一部分をメモリ領域と呼んで区別しよう. メモリ空間には, 1 byte 毎に番号(メモリアドレス;memory address)が 連続的に割り振られている. メモリ領域は, アドレス範囲(先頭アドレスとデータサイズ, または,先頭アドレスと末尾アドレス)により特定されることになる.
想像してみよう. 同サイズのダンボール箱がたくさん,倉庫に収納されている. また,各箱には通し番号が付けられ,番号順に並べられている. これをメモリに例えると...
変数データについては, アドレス演算子「&」で先頭アドレスを調べられる. List 1 のプログラムでは, さまざまな変数のアドレスを 16 進数として表示している.
main() { char a = '0'; int i = 0; double x = 0.0; printf("(アドレス) : (変数名)\n"); printf("0x%08X : a\n", &a); // 変数 a のアドレス &a を表示 printf("0x%08X : i\n", &i); // 変数 i のアドレス... printf("0x%08X : x\n", &x); // 変数 x のアドレス... }
実行例:
$ ./mm-1 (アドレス) : (変数名) 0xBFB7DF3F : a 0xBFB7DF38 : i 0xBFB7DF30 : x ...
ここで,アドレスは時と場合と処理系によって異なる. (異なる結果になっていても気にしなくてよい.)
ちなみに,複数回実行すると,アドレスが微妙に変化する. (変化しない場合もある.) これで,アドレスの決定は,プログラム側ではなく, 処理系(OS)側によってなされていることが確認できる. (この授業では,アドレス変化の規則性などについては,気にしなくてよい.)
なお,アドレスの数値はゴミではない. 勘違いしないこと. (「意味不明な値=ゴミ」とは限らない.)
ゴミとは初期化されていない変数の値のことなんだが, ここでは,初期化しているし, 変数の「値」ではなく,変数の「アドレス」を表示しているし.
次に,アドレスの順序を整えてみよう:
$ ./mm-1 | sort (アドレス) : (変数名) 0xBFD41CE0 : x 0xBFD41CE8 : i 0xBFD41CEF : a ...
実行結果を,画面表示する前に, sort コマンドで昇順(小→大)に並べ替えただけ.
なお,宣言の順序とアドレスの順序とが異なっても気にしなくてよい. 上述の通り,処理系依存なので.
この実行結果は,Table 1 のようなメモリマップの状態を意味している. なお,アドレス範囲の末尾は,各変数のデータサイズ(バイト数)から算出される.
アドレス範囲 | 変数名 |
---|---|
0x BFD4 1CE0 〜 1CE7 | x |
0x BFD4 1CE8 〜 1CEB | i |
0x BFD4 1CEC 〜 1CEE | 空き |
0x BFD4 1CEF | a |
ところで,今回のプログラムでは,アドレスを表示しているが, これは,メモリマップについて学習するためだけの措置だ. 実用的なプログラミングでは, アドレスを表示するようなことは,ほとんどない. 具体的なアドレスは,ユーザが知らなくても, コンピュータさえ知っていれば充分だ.
しかし,C言語のプログラマたる者は, 常に正常動作するプログラムを作成するために, 脳内にメモリマップをイメージする必要がある. 後で試すように,メモリ内容は簡単に破壊可能だからだ.
メモリマップのイメージが特に重要になるのは, 配列を使う場合だ. List 2 を実行してみよう.
main() { char a[3]; int b[3]; double c[3]; int i; for (i = 0; i < 3; i++) { printf("0x%08X : a[%d]\n", &a[i], i); printf("0x%08X : b[%d]\n", &b[i], i); printf("0x%08X : c[%d]\n", &c[i], i); } printf("0x%08X : i\n", &i); }
$ ./mm-2 | sort 0xBFC423E4 : i 0xBFC423E8 : c[0] 0xBFC423F0 : c[1] 0xBFC423F8 : c[2] 0xBFC42400 : b[0] 0xBFC42404 : b[1] 0xBFC42408 : b[2] 0xBFC4240D : a[0] 0xBFC4240E : a[1] 0xBFC4240F : a[2] ...
実行結果をよく観察し, Table 1 のようなメモリマップ表を作成してみよう. 同じ配列の配列要素は要素番号の順に隙間なく連続的に 配置されている,ということがわかるハズだ. (異なる配列の間には隙間ができる場合もある.)
C言語では,プログラマの権力は絶大であり, メモリ内容を破壊し, プログラムを異常動作させることも簡単にできてしまう... 当たり前だが,わざとやってはいけません. しかし,意図せずとも,わずかな不注意によって, 重大な欠陥が混入してしまうこともよくある.
したがって,一見うまく動作しているとしても, 本当に正しく動作しているとは限らない. メモリ関連のありがちなバグ(bug;不具合の原因)を体験してみよう.
メモリ関連のバグは,特に,配列を使用している場合に遭遇しやすい. List 3 を実行してみよう.
main()
{
int a[3] = { 10, 11, 12 };
int b[3] = { 20, 21, 22 };
int c[3] = { 30, 31, 32 };
int i;
// b[4] = 99999; // バグなコード
for (i = 0; i < 3; i++) {
printf("a[%d] = %d\n", i, a[i]);
printf("b[%d] = %d\n", i, b[i]);
printf("c[%d] = %d\n", i, c[i]);
}
}
このまま,コンパイルし,実行:
$ ./mm-3 | sort a[0] = 10 a[1] = 11 a[2] = 12 b[0] = 20 b[1] = 21 b[2] = 22 c[0] = 30 c[1] = 31 c[2] = 32
これは,いたって普通の動作だ.
では次に, 8行目のコメント化されているコード(// b[4] = 9999;)を 有効化(行頭のコメント記号「//」を削除)し, コンパイルし,実行:
$ ./mm-3 | sort
a[0] = 99999 # えっ!?
a[1] = 11
a[2] = 12
b[0] = 20
b[1] = 21
b[2] = 22
c[0] = 30
c[1] = 31
c[2] = 32
疑問:
原因を推理しよう.
要素番号を変えて実行し, どうなるか試してみるとよい. b[3] = 99999, b[5] = 99999 とか, b[-2] = 99999 や b[10000] = 99999 等についても.
もし,何も表示されない場合,sort を使わずに実行してみよう. 実行時エラー(Bus error や Segmentation fault)で異常終了したことがわかる.
注意事項:
なお,当たり前だが,このページに載っている実行結果ではなく, 自分の実行結果を書くんですよー.
なぜ,b[4] に書き込んだデータが他の変数に書き込まれてしまうのか? また,そもそもなぜ,存在しないハズの変数 b[4] に書き込めてしまうのか? など.
アドバイス:メモリマップを使って考えよう. 面倒がらずに List 3 を改造し,各変数のメモリアドレスも表示してみるとよい. そして,b[4] のアドレスも予想すること.