ぐだぐだ低レベルプログラミング(13) 変数アクセスのコードを眺めてみれば

第10回でgccがメモリアクセスするのにどのようなコードを吐き出しているのか確認しようと考えたものの、第10回から第11回第12回とずっと回り道をしてしまいました。今回ようやくcコンパイラの吐き出すメモリアクセスのコードの確認に入ります。例によって環境は ラズパイ上のRaspbian, コンパイラはgcc  (Raspbian 8.3.0-6+rpi1) 8.3.0 です。

※「ぐだぐだ低レベルプログラミング」投稿順indexはこちら

なお、分かり易くするため、最適化オフでデバッグ情報あり、でコンパイルしております。

さて最初に見てみるのは、初期値を与えていない単純な大域変数 g_v1 です。変数の宣言は以下のようです。

int g_v1;

前回見てきたように、初期値を与えない大域変数は .bssセクションに置かれる筈です。実際のリンク後の実行可能オブジェクトのダンプを見てみます。

00021054 g O .bss 00000004 g_v1

.bss セクション内のアドレス 0x00021054番地からの4バイトがこの大域変数 g_v1に割り当てられていることが分かります。この番地0x00021054という数字がプログラムのどこかにある筈です。調べてみると、プログラム本体の機械語命令コードを収めてある .text セクションの末尾にそれはありました。

.L8:
	.word	g_s1
	.word	g_v1
	.word	ga
	.word	g_v2

.L8というローカルシンボルの後にワードで定数が定義されており、そこに今回定義した大域変数の名前(シンボル)が並んでいます。それぞれのお名前シンボルには実体変数をおくべきメモリ番地が割り当てられている。するとこの.L8を起点とする一種の変数名テーブルから各変数のアドレスを引き出せる筈。実際に以下のコードがどのように展開されているか見てみます。

 g_v1 = atoi(argv[i]);

ライブラリ関数 atoi を呼び出して得た結果を g_v1に代入しています。

 	bl	atoi
	mov	r2, r0
	ldr	r3, .L8+4
	str	r2, [r3]

blはサブルーチンコール命令です。ライブラリ関数 atoi を呼び出しているように見えます。しかしこれは、Cのソースをコンパイルしてアセンブラソースにするところで止めているため、分かり易い静的リンク式の「仮の姿」です。通常cのライブラリは動的リンクされるので、直接atoiを呼び出すことはなく、.pltという セクション中のコードをへて間接的に呼び出す形になります。呼び出し方にちょっとテクが入っているだけでやることは一緒なので、atoiを呼び出すときにr0に入っていた値(文字列へのポインタ)から数値変換された値がr0に戻ってきます。ここでは、mov r2, r0で一旦r2に入れた後(最適化オフです)、ldr命令でr3レジスタに.L8+4なる値をロードしています。ここで、r3にg_v1変数のアドレスを得ているわけです。ただし、ldr 命令も実は見やすい「仮の姿」です。実際のコードを見てみれば、

ldr r3, [pc, #340]

などと、PC相対でメモリアクセスしていました。アドレステーブルは.textセクションの末尾におかれているので、命令コードの番地を保持しているPCから相対番地でアクセスしやすいからだと思います。そして最後の

str r2, [r3]

が、r2(先ほど変換した結果をいれてある)をr3が指すメモリ番地へ格納する命令ですね。movやldrでは、左のオペランドへ右のオペランドから転送、という形で右から左ですが、strは、左のオペランドから右のオペランドと方向が変わります。説明するとかなり長くなりましたが、.bssにおかれた単純変数へのアクセスは特に難しいこともないと思います。

あまりかわり映えしないのですが、.dataにおかれた初期値ありの単純変数へのアクセスも一応確認しておきたいと思います。C言語のコード上はこんな感じ。

int g_v2 = 333;

リンク後の実行ファイルで確認すると、.dataセクションの中のアドレスに置かれていることが分かります。

0002102c g O .data 00000004 g_v2

ただこれだけだと.bssと変わりがないですが、アセンブラソースを眺めてみれば.dataセクションの中に、初期値のある実体が宣言されていることが分かります。

 	.global	g_v2
	.data
	.align	2
	.type	g_v2, %object
	.size	g_v2, 4
g_v2:
	.word	333

ちゃんと、初期値として333が与えられたメモリがg_v2なるラベルの場所に置かれていることが確認できました。このg_v2を読みだしているのが、

 ga[i] = sub(ga[i-1], g_v2);

という部分で、subという名前の関数の第2の引数としてg_v2を与えているところです。先ほどの g_v1では、.L8+4でアドレスをひきだしていたので、上の方のテーブルから、g_v2では .L8+12でアドレスを引き出している筈。

 	ldr	r3, .L8+12
	ldr	r3, [r3]
	mov	r1, r3
	mov	r0, r2
	bl	sub

最初のldr命令で、r3レジスタに.L8+12からg_v2のメモリアドレスを取り出してきます。g_v1と同様、実際にはPC相対メモリアドレシングの命令に展開されます。そして、次のldr命令でr3レジスタの指すメモリアドレス(当然初期値333が格納されている)から読み出して、r3レジスタに代入しています。そしてmov命令でr3の内容をr1に入れています。転送のステップが、まどろっこしいのは最適化していない為でしょう。EABIの規約でr1は関数呼び出し時に第2の引数をいれるべき場所です。その次でr0にr2をmovしているのはこのコードの前に用意してあった第1の引数の値を規約で第1引数を入れるべきr0レジスタに移しているだけです。これで引数の準備が出来たので、bl命令で関数subを読み出します。なお、このsubは同じソースファイル内で定義されている関数なので、先ほどのライブラリ関数atoiのように、.pltセクションを経由するようなことはなく、直接呼出しのコードが生成されます。

次回は、構造体とか配列のアクセスを見ていきたいと思います。

ぐだぐだ低レベルプログラミング(12) オブジェクトファイルその2 へ戻る

ぐだぐだ低レベルプログラミング(14) 変数アクセスのコードを眺めてみれば2 へ進む