[メモ] avr-gcc アセンブラ / ABI 備忘録
avr-gcc 向けのアセンブラと ABI に関する雑多なメモ。随時追加する。
参考資料
公式
コミュニティ
用語メモ
AVR は直接関係無い。
prologue (プロローグ)
呼び出された関数 (callee) が最初に行う処理。関数内の処理が呼び出し元 (caller) に影響しないよう、これから値を破壊 (clobber) する見込みのレジスタの待避したり、ローカル変数領域の確保など。
epilogue (エピローグ)
呼び出された関数 (callee) が復帰する直前に行う処理。スタックフレームの解放、破壊したレジスタの復元など。
clobber (クロバー, クラバー)
関数呼び出しやアセンブラの文脈では、レジスタの値を「破壊」すること。trash とか smash とかと同じ?「clobbered registers」と言った場合、関数呼び出しやインラインアセンブラによって値が破壊される可能性のあるレジスタであり、すなわち呼び出し側で事前に待避が必要であることを意味する。
ABI
C 言語で適当に関数を作って avr-gcc -S
で生成されたアセンブラを眺めながらドキュメントを読むと理解しやすい。
レジスタの用途
r0
一時変数用のスクラッチレジスタ。アセンブラソースでは __temp_reg__
で参照される。
乗算命令 (mul
など) の計算結果の格納にも使われる。
r1
ゼロレジスタ。アセンブラソースでは __zero_reg__
で参照される。
ハードウェア的には他の汎用レジスタと同様に機能するが、基本的には常に 0x00 を格納しておいて、定数ゼロとして使用することになっている。
乗算命令の計算結果の格納にも使われるので、乗算の後はゼロクリアする必要がある。
手書きアセンブラで乗算命令を頻繁に使用する場合は r1:r0
以外をゼロレジスタとして使った方が命令数は少なくなる。
T フラグ
ステータスレジスタ (SREG
) 内にある。r0
と同様に一時変数として使用される。
Y (r29:r28
)
スタックフレーム内のローカル変数への間接アクセス用に使用される。
r8~r25
関数の中では固定用途ではないが、引数をレジスタで渡す際はこの範囲内で使用される。
関数呼び出し
呼び出し側 (caller) が待避しなければならないレジスタ (clobbered registers)
これらは callee によって破壊されるので、復帰後も caller で値を使用する場合は呼び出し前に待避しなければならない。
r0
(__temp_reg__
): 一時変数用レジスタr18–r25
: 汎用レジスタX
(r27:r26
),Z
(r31:r30
): ポインタT
フラグ:r0
と同様、ビット操作時の一時変数として使用
呼び出される側 (callee) が待避しなければならないレジスタ (saved registers)
caller はこれらはのレジスタが復帰後も呼び出し前の値を維持していることを期待するので、callee で使用する場合はその前に待避し、復帰前に復元しなければならない。
r1
(__zero_reg__
): 値の格納に使用した場合は復帰前までにゼロクリアすること。r2-r17
: 汎用レジスタY
(r29:r28
): ポインタ
スタックフレーム
スタックはアドレスの若番方向へ向かって積まれる。スタックの内容とスタックポインタ (SP
) はアライメントされない。
アドレス | サイズ [Bytes] | 内容 | 説明 |
---|---|---|---|
↑大 | ≧ 0 | 引数 | レジスタ渡しでは足りない場合に caller が積む |
0 または 2~3 | 復帰先アドレス | caller が積む。末尾呼び出しの場合は 0 のこともある | |
≧ 0 | 待避されたレジスタ (saved registers) |
必要に応じて callee が積む | |
↓小 | ≧ 0 | ローカル変数 | 必要に応じて callee が確保する |
clobbered registers は呼び出し元の責任で待避するので、呼び出し元のローカル変数領域に含まれる。
プロローグの処理
- saved registers のうち自身が使用 (破壊) するものをスタックに
push
して待避する。 SP
の値をY
レジスタにコピーし、ローカル変数領域のサイズ分を減算する。Y
レジスタの値をSP
に書き戻す。- レジスタ渡しされた引数を必要に応じてローカル変数領域にコピーする。
SP
を介した間接アクセスができないので、スタック内の引数やローカル変数へのアクセスは Y
レジスタを介して行う。
エピローグの処理
Y
レジスタにローカル変数領域のサイズを加算してSP
に書き戻す。- saved registers をスタックから
pop
して復元する。 - 戻り値をレジスタまたはスタックに配置する。
- 復帰する (
ret
)。
引数
可変長でない 16 バイトまでの引数は r25
から若番方向へ向かって確保したレジスタに配置し、それに収まらない分はスタックに積む。
- 全ての引数のサイズを偶数に切り上げる。
-
切り上げ後の引数のサイズの合計で 16 バイトまでに収まる分はレジスタに配置する。
r25
から始めて若番方向に向かって引数を配置する。- 奇数サイズのレジスタは若番側 (偶数番側) にアライメントする。
- 16 バイトから 1 バイトでも溢れたらその引数はスタックに積まれる。1 つの引数がレジスタとスタックに泣き別れたりはしない。
- 16 バイトから溢れた残りの引数は全てスタックに積む。
可変長引数 (varargs) の場合は全ての引数をスタックに積む。
戻り値
8 バイトまでの戻り値は r25
から若番方向に向かって確保したレジスタに配置する。
-
構造体でない戻り値は引数と同様、サイズを偶数に切り上げる。
- 例)
uint8_t
の値はr24
に配置する (r25
はパディング)。 - 例)
uint32_t
の値はr25:r22
に配置する (r25
が MSB 側、r22
が LSB 側)。
- 例)
-
構造体は 2 のべき乗に切り上げる。
- 例) 6 バイトの構造体は
r23:r18
に配置する (r25:r24
はパディング)。
- 例) 6 バイトの構造体は
文法
プリプロセッサ
ソースファイルの拡張子を大文字で .S
にすることにより、C/C++ と同様のプリプロセッサが使用でき、#define
、#ifdef
、#if
などのお馴染みのマクロが使える。
You can use the gnu C compiler driver to get other "CPP" style preprocessing by giving the input file a.S
suffix.
3.1 Preprocessing - Using as
#include
も使えるので、C/C++ 側と定数の定義を共通化できる。
つまづいたところ
オペランドに指定可能なレジスタが制限されている命令
カテゴリ | 命令 | 指定可能な範囲 |
---|---|---|
Immediate 系 | andi , cpi , ldi , ori , sbci , subi |
r16 -r31 |
Set Bit/Clear Bit | cbr , sbr , ser |
r16 -r31 |
直接メモリアクセス | lds |
r16 -r31 (※2) |
乗算 (※1) | muls |
r16 -r31 |
乗算 | mulsu , fmul , fmuls , fmulsu |
r16 -r23 |
※1) mul
は制限無いが、他の乗算命令には制限がある。
※2) AVRrc のみ制限あり。他のアーキテクチャでは制限なし。
ポインタレジスタと命令の使い分け
単に配列を舐めるのには X
~Z
のどれでも使えるが、配列を Read-Modify-Write したり構造体へアクセスするには displacement addressing ができる Y
、Z
の方が使いやすい。Y
は通常はローカル変数へのアクセスに使われる。
アドレッシング | X | Y, Z |
---|---|---|
Indirect | ld /st |
ld /st |
Post-increment | ld /st |
ld /st |
Pre-decrement | ld /st (未検証) |
ld /st (未検証) |
Displacement | 不可 | ldd /std |
ld rN, Z+
と ldd rN, Z+
は違う?
post-increment を期待して ldd rN, Z+
と書いたところ期待動作にならなかったが ld rN, Z+
だと期待動作になった。理由は分からない。深追いもできてない。
その他
addi rN, hoge
は無いがsubi rN, (-hoge)
で同じことができる。同様にadci
も無いのでsbci
を使う。