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

JosephHalfmoon

前回までで大域変数について、単純な変数、初期値のあるもの無いもの、配列、構造体とコンパイラの吐き出したメモリアクセスのコードを一通り調べてみました。今回は、関数の中のローカル変数、引数について調べてみます。前回、前々回と同じく、環境はRaspberry Pi 3 model B+上のRaspbian OS、コンパイラはgcc 8.3.0 です。なお、生成されたアセンブリ言語コードの読みやすさを優先に、最適化オフ、デバッグ情報ありです。

まずは、元のC言語のコードから。意味無しの関数であります。引数が2つ、戻り値あり、ローカル変数1つ、いずれもint型です。

int sub(int a, int b) {
    int c;
    c = a * 3 + b;
    c >>= 1;
    return c;
}

短い関数なので、.textセクション内のobjdumpによる逆アセンブルリストをそのまま掲載してしまいます。

00000000 <sub>:
   0:	e52db004 	push	{fp}		; (str fp, [sp, #-4]!)
   4:	e28db000 	add	fp, sp, #0
   8:	e24dd014 	sub	sp, sp, #20
   c:	e50b0010 	str	r0, [fp, #-16]
  10:	e50b1014 	str	r1, [fp, #-20]	; 0xffffffec
  14:	e51b2010 	ldr	r2, [fp, #-16]
  18:	e1a03002 	mov	r3, r2
  1c:	e1a03083 	lsl	r3, r3, #1
  20:	e0833002 	add	r3, r3, r2
  24:	e51b2014 	ldr	r2, [fp, #-20]	; 0xffffffec
  28:	e0823003 	add	r3, r2, r3
  2c:	e50b3008 	str	r3, [fp, #-8]
  30:	e51b3008 	ldr	r3, [fp, #-8]
  34:	e1a030c3 	asr	r3, r3, #1
  38:	e50b3008 	str	r3, [fp, #-8]
  3c:	e51b3008 	ldr	r3, [fp, #-8]
  40:	e1a00003 	mov	r0, r3
  44:	e28bd000 	add	sp, fp, #0
  48:	e49db004 	pop	{fp}		; (ldr fp, [sp], #4)
  4c:	e12fff1e 	bx	lr

まず最初に fpという名のレジスタを退避しています。fp = frame pointerの略だと思います。スタック上のローカル変数を指すためのベースとなるもの。Armの場合、汎用レジスタの中の一本 r11 を fp としてつかう「お約束」になっているので、r11=fp と表示されています。関数に入ってきた時点では、r11にこのsub関数を呼び出した元の関数が使っていたフレームポインタの値が記録されている筈なので、まずは退避しておくのだと思います。その上で、sp(Stack pointer)の値をfpにコピーするのと等しい以下の操作をしてfpを設定しています。Armにおける sp は汎用レジスタとしては r13であるのですが、r13,r14,r15はハードウエアによってキメウチの動作があり、モードによっては実体レジスタがスイッチされたりすることもあるので、「汎用レジスタ」として「普通の操作」に使うことはお勧めできないです。spの操作の中で、spに何か足してスタック上のアドレスを作るというのは「良くやる手」なので、spの操作として以下のコードはOKと思われます。

add fp, sp, #0

その次に自分自身から20を引いてスタック上にローカル変数の置き場所(スタックフレーム)を確保しています。

sub sp, sp, #20

その次の以下のコードは、最適化していないのでとても非効率ですが、レジスタ渡しの関数引数を、先に確保したスタックフレーム内に一端保存しているようです。

str r0, [fp, #-16]
str r1, [fp, #-20]

ArmのEABIのお約束で、関数に渡される第1の引数はr0、第2の引数はr1に渡されることになっています。fpを基準にしてそこから16番地分下の位置に第1引数、20番地分下に第2引数を格納しています。さきほど20番地分確保しているので、一番下に鎮座することになります。その次の7命令は、Cで書けば以下の1行のコードに相当します。

c = a * 3 + b;

繰り返しますが、最適化していないので、非常にまどろっこしいコードになっていますが、そのお陰で部分的に取り出しても容易に理解できるし、最適化コードでよくある消えてしまった変数とか、処理などもありません。

ldr r2, [fp, #-16] <=第1の引数であるaをスタックフレームからロード
mov r3, r2 <=aの値をr3にコピー
lsl r3, r3, #1 <=r3にコピーしたaの値を1ビット左論理シフト=2倍つまりa*2
add r3, r3, r2 <=r3に入っている a*2 の値の値にaの値を足す=3倍つまりa*3
ldr r2, [fp, #-20] <=第2の引数であるbをスタックフレームからロード
add r3, r2, r3<=r3に入っているa*3とr2に入っているbを足したものをr3へ
str r3, [fp, #-8]<=r3に入っている a*3+bの値をスタックフレームのfpより8番地下へストア

とてもまどろっこしいですが、ちゃんとa*3+bが計算されスタックフレームに書き戻されました。そしてこの書き戻しの先の番地、fpから8番地分下という場所が、ローカル変数cが鎮座している場所のようです。

大分長くなってしまったので、c >>=1;のところは飛ばさせてもらいます。最後の return c; の相当部分を見れば、ちゃんとローカル変数 c の値を関数の戻り値として返していることが分かります。

ldr r3, [fp, #-8]<=スタックフレーム上の変数cの場所から値を読み出す
mov r0, r3<=読みだした値をr0(戻り値を入れるお約束のレジスタ)にコピー
add sp, fp, #0<=fpの値(元のspの値)をspに戻す
pop {fp}<=さらにスタック上からfpの旧値を復元
bx lr<=リンクレジスタ(r14でもある)に格納されている戻り番地に分岐

ちゃんと計算結果を戻してました(当たり前か)

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

ぐだぐだ低レベルプログラミング(16) Arm Neonを使ってみる1