05 月 27 日(水)1-2h

メモリマップ

数値や文字などのデータは,プログラムの実行中には, コンピュータのメモリの内部に記録されている. 今回は,データがメモリ内のどこに/どのように記録されるのか? メモリマップ(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 進数として表示している.

なぜ,わざわざ 16 進数で? 各データのメモリ使用量が 4バイトや 8バイトなので, 2n を単位として数える方が好都合だからだ. 16 進数と 2 進数 についても確認しておこう.
List 1. メモリマップのテスト mm-1.c
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 のアドレス...
}								
アドレス演算子「&」については,すでに, 変数にデータを入力するために scanf() とともに, 何度も使われてきた. (しかし,printf() とともに使ったのは今回が初めて.) 「あー,つまり,scanf ってのは,データを変数に代入するというよりも, 指定のアドレスのメモリ領域に記録するってことなんだー」と気付くところだ.

実行例:

$  ./mm-1
(アドレス) : (変数名)
0xBFB7DF3F : a
0xBFB7DF38 : i
0xBFB7DF30 : x
...

ここで,アドレスは時と場合と処理系によって異なる. (異なる結果になっていても気にしなくてよい.)

ちなみに,複数回実行すると,アドレスが微妙に変化する. (変化しない場合もある.) これで,アドレスの決定は,プログラム側ではなく, 処理系(OS)側によってなされていることが確認できる. (この授業では,アドレス変化の規則性などについては,気にしなくてよい.)

なお,アドレスの数値はゴミではない. 勘違いしないこと. (「意味不明な値=ゴミ」とは限らない.)

ゴミとは初期化されていない変数の値のことなんだが, ここでは,初期化しているし, 変数の「値」ではなく,変数の「アドレス」を表示しているし.

次に,アドレスの順序を整えてみよう:

$  ./mm-1 | sort
(アドレス) : (変数名)
0xBFD41CE0 : x
0xBFD41CE8 : i
0xBFD41CEF : a

...

実行結果を,画面表示する前に, sort コマンドで昇順(小→大)に並べ替えただけ.

なお,宣言の順序とアドレスの順序とが異なっても気にしなくてよい. 上述の通り,処理系依存なので.

この実行結果は,Table 1 のようなメモリマップの状態を意味している. なお,アドレス範囲の末尾は,各変数のデータサイズ(バイト数)から算出される.

char は1バイト, int は4バイト, double は8バイト.
Table 1. メモリマップ表
  アドレス範囲    変数名  
0x BFD4 1CE0 〜 1CE7x
0x BFD4 1CE8 〜 1CEBi
0x BFD4 1CEC 〜 1CEE空き
0x BFD4 1CEFa
空き領域の存在に注意しよう. 「メモリが隙間なく利用される」とは限らない.

ところで,今回のプログラムでは,アドレスを表示しているが, これは,メモリマップについて学習するためだけの措置だ. 実用的なプログラミングでは, アドレスを表示するようなことは,ほとんどない. 具体的なアドレスは,ユーザが知らなくても, コンピュータさえ知っていれば充分だ.

ユーザに通知するのは, 異常動作通知「0xXXXX 番地でメモリ保護例外が発生しました」等, 非常に特殊な場合だけ. しかも,普通のユーザにとっては,そんなこと通知されても, どーしよーもないし.

しかし,C言語のプログラマたる者は, 常に正常動作するプログラムを作成するために, 脳内にメモリマップをイメージする必要がある. 後で試すように,メモリ内容は簡単に破壊可能だからだ.

書き換えたつもりのないデータが, プログラムの不具合(== プログラマの不注意)によって, 簡単に書き換えられてしまう.

配列のメモリマップ

メモリマップのイメージが特に重要になるのは, 配列を使う場合だ. List 2 を実行してみよう.

List 2. 配列のメモリマップのテスト mm-2.c
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 を実行してみよう.

List 3. メモリ破壊の実験 mm-3.c
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
書き換わる変数は a[0] とは限らない.

疑問:

原因を推理しよう.

要素番号を変えて実行し, どうなるか試してみるとよい. b[3] = 99999b[5] = 99999 とか, b[-2] = 99999b[10000] = 99999 等についても.

もし,何も表示されない場合,sort を使わずに実行してみよう. 実行時エラー(Bus error や Segmentation fault)で異常終了したことがわかる.


本日の課題

  1. mm-2 の実行結果について,Table 1 のようなメモリマップ表を作成せよ.
  2. 空き領域も明示せよ. 表の罫線は不要.

    なお,当たり前だが,このページに載っている実行結果ではなく, 自分の実行結果を書くんですよー.

  3. mm-3 の不具合について,原因を推理せよ.
  4. なぜ,b[4] に書き込んだデータが他の変数に書き込まれてしまうのか? また,そもそもなぜ,存在しないハズの変数 b[4] に書き込めてしまうのか? など.

    アドバイス:メモリマップを使って考えよう. 面倒がらずに List 3 を改造し,各変数のメモリアドレスも表示してみるとよい. そして,b[4] のアドレスも予想すること.

レポート提出
  • 提出方法: 電子メール
    • 宛先:yanagawa@kushiro-ct.ac.jp
    • 件名:c-0527
  • 提出期限:05月29日(金)17:00
  • 提出内容(本文):
    • 学年学科,出席番号,氏名
    • 問1のメモリマップ表
    • 問2の考察(原因の推理)
    • (疑問)
注意事項: 以下の点についても厳しくチェックする:

  • 考察の文章の的確さ(論理,文法,誤字脱字,3C,など)

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