前回は、Cコンパイラの吐き出した単純な大域変数へのメモリアクセスのコードを眺めてみました。今回は、大域変数でも構造体と配列へのアクセスを見てみます。今回も環境は、Raspberry Pi 3 model B+上のRaspbian OS、コンパイラはgcc 8.3.0 です。なお、生成されたアセンブリ言語コードの読みやすさを優先に、最適化オフ、デバッグ情報ありです。
※「ぐだぐだ低レベルプログラミング」投稿順indexはこちら
まずは例によって、C言語上の構造体変数(大域)の宣言部分です。ただし、この要素の並べ方はあまり良くない例です、意図的に。
//要素の並べ方がイマイチな構造体 struct g_s { char c1; short s1; int i1; int i2; } g_s1;
前回までで調べたとおり、初期値の無い大域変数なので、.bssセクションに実体メモリは確保されている筈。オブジェクトダンプで調べてみると、ちゃんと.bssセクション内の 0x00021048番地に置かれていました。
00021048 g O .bss 0000000c g_s1
この構造体に、これまたこれ見よがしに代入してみているのが以下の部分です。
g_s1.c1 = 0; g_s1.s1 = 0; g_s1.i1 = 0; g_s1.i2 = 0;
この部分がどのようなアセンブリ言語コードに展開されているのかobjdumpで見てみましょう。
g_s1.c1 = 0; 10498: e59f31ac ldr r3, [pc, #428] ; 1064c <main+0x1c8> 1049c: e3a02000 mov r2, #0 104a0: e5c32000 strb r2, [r3] g_s1.s1 = 0; 104a4: e59f31a0 ldr r3, [pc, #416] ; 1064c <main+0x1c8> 104a8: e3a02000 mov r2, #0 104ac: e1c320b2 strh r2, [r3, #2] g_s1.i1 = 0; 104b0: e59f3194 ldr r3, [pc, #404] ; 1064c <main+0x1c8> 104b4: e3a02000 mov r2, #0 104b8: e5832004 str r2, [r3, #4] g_s1.i2 = 0; 104bc: e59f3188 ldr r3, [pc, #392] ; 1064c <main+0x1c8> 104c0: e3a02000 mov r2, #0 104c4: e5832008 str r2, [r3, #8]
前回と同じですが、変数アドレスはテキストセグメント末尾のテーブルにおかれていました。まずそのアドレスをPC相対アドレシングでr3レジスタに持ってきています。
ldr r3, [pc, #428]
その後、mov r2, #0 などと代入すべき値をr2に準備した上で、最初のchar型要素に、
strb r2, [r3]
という形でバイトでストアしています。以下同様に格納していきます。第2のショート型要素には、
strh r2, [r3, #2]
という具合で、r3をベースレジスタとしてオフセット2を加えてアドレスとし、ハーフワード(16bit)でストアします。ここで気付くことは、
- 第1のchar型要素に対するオフセットは 0
- 第2のshort型要素に対するオフセットは2
ということです。1バイト隙間がある!
何も指定しなくてもコンパイラは構造体要素の大きさによってアライメントを調整し、ショート型は2バイトアラインにしてくれている、ということであります。ま、実際問題として、ミスアライメントを許さない(例外を発生する)RISC系統のマシン(勿論、Armもこれにふくまれます)であれば、この手の始末(パディング、アライメントを合わせるために構造体要素間のアドレスの隙間に詰め物をする)は大抵の場合、処理系が面倒をみてくれます。たとえ、ミスアライメントを許すようなマシンでも、根本的にミスアライメントは性能が落ちるので、そうしないオプションが優先になっている、メモリ量優先という場合にのみミスアライメントな変数の割り付けを許すというような処理系が多いのではないかと思います。実際には、使用する処理系のマニュアルにアライメントの制御について書かれている筈。
続いて、初期値を持つ大域な配列へのアクセスがどうなっているのか調べてみます。Cのコードではこんな感じ。
int ga[5] = {1, 2, 3, 4, 5};
初期値があるので、.dataセクションの中にメモリが確保されている筈。
00021030 g O .data 00000014 ga
ありました。.dataセクション内の0x00021030番地でした。さて、ここに5要素分、1要素4バイトなので合計20バイト、16進表現で0x14バイト分の領域に初期値が書かれているわけです。これにアクセスするCのコードのうちの1箇所、前回にも出てきた以下の関数呼び出しですが、
ga[i] = sub(ga[i-1], g_v2);
関数subの第一引数がgaへのアクセスでした。
for (i=1; i<argc; i++) { ~途中略~ if (i < 5) { ga[i] = sub(ga[i-1], g_v2);
この引数の読み取り部分について、どのようなアセンブリ言語コードが生成されているかというと、以下のようでした。
ldr r2, [pc, #316] ldr r2, [r2, r3, lsl #2]
最初のldr命令は、例によってPC相対で、配列gaのアドレスを配列からr2へ持ってくる操作です。ここでr2には配列の先頭番地が入っていますが、まだ配列の添え字部分の処理がのこっています。このシーケンスの直前で、C言語上、i-1にあたる添え字の値はレジスタr3に書き込まれていると思ってください。2つ目のldr命令では、r2(配列の先頭アドレス)に、r3(添え字の値)をlsl #2(論理左シフト2ビット=4倍、1要素は4バイトだから)したものを加えて得たアドレスから値をr2へ読み出しています。lsl #2の部分は、以前の回にも出てきた
フレキシブル第2オペランド
という記法であります。配列のアクセスは結構スッキリかける感じがします。
次回は、後回しにしてきたローカル変数のアクセスですかね。