Linux x86-64 ABIがレジスタとスタックを使用して関数にパラメーターを渡す方法を理解していると思います(cf. 前のABIの説明 )。私が混乱しているのは、関数呼び出し全体で保持されると予想されるif/whatレジスタです。つまり、どのレジスタが破壊されないように保証されていますか?
以下に、レジスタの完全な表とドキュメントからの使用方法を示します[ PDF Link ]:
r12
、r13
、r14
、r15
、rbx
、rsp
、rbp
は、呼び出し先が保存するレジスタです-[関数呼び出し間で保持]列に[はい]があります。
ABIは、標準に準拠したソフトウェアが期待できることを指定します。主にコンパイラ、リンカー、その他の言語処理ソフトウェアの作成者向けに書かれています。これらの作成者は、同じ(または異なる)コンパイラーでコンパイルされたコードで適切に動作するコードをコンパイラーが生成することを望んでいます。それらはすべて、ルールのセットに同意する必要があります:呼び出し元から呼び出し先に渡される関数の正式な引数はどのように、関数の戻り値は呼び出し先から呼び出し元にどのように渡されるか、どのレジスターは呼び出し境界全体で保持/スクラッチ/未定義であるかなどオン。
たとえば、関数の生成されたアセンブリコードは、値を変更する前に保存されたレジスタの値を保存する必要があり、呼び出し元に戻る前にコードが保存された値を復元する必要があるというルールがあります。スクラッチレジスタの場合、生成されたコードはレジスタ値を保存および復元する必要はありません。必要に応じて行うことができますが、標準準拠のソフトウェアはこの動作に依存することはできません(実行する場合は標準準拠のソフトウェアではありません)。
アセンブリコードを記述している場合は、yoがこれらの同じルールを実行します(コンパイラーの役割を果たしています)。つまり、コードが呼び出し先保存レジスタを変更する場合、元のレジスタ値を保存および復元する命令を挿入する必要があります。アセンブリコードが外部関数を呼び出す場合、コードは標準に準拠した方法で引数を渡す必要があり、呼び出し先が戻るときに、保存されたレジスタ値が実際に保存されるという事実に依存する場合があります。
ルールは、標準に準拠したソフトウェアがどのように機能するかを定義します。ただし、これらのルールでnotを実行するコードを記述する(または生成する)ことは完全に合法です!コンパイラは、特定の状況下でルールに従う必要がないことを知っているため、これを常に行います。
たとえば、次のように宣言され、アドレスが取得されないfooという名前のC関数を考えます。
static foo(int x);
コンパイラは、コンパイル時に、この関数が現在コンパイル中のファイル内の他のコードによってのみ呼び出されることを100%確信しています。関数foo
は、静的であるという意味の定義が与えられているため、他の人が呼び出すことはできません。コンパイラーはコンパイル時にfoo
のすべての呼び出し元を知っているため、コンパイラーは必要な呼び出しシーケンスを自由に使用できます(呼び出しをまったく行わないこと、つまりfoo
の呼び出し元にfoo
のコードをインライン化することを含みます) 。
アセンブリコードの作成者として、これも行うことができます。つまり、標準に準拠するソフトウェアの期待を妨害または侵害しない限り、2つ以上のルーチンの間に「プライベートアグリーメント」を実装できます。
実験的アプローチ:GCCコードを逆アセンブルします
主に楽しみのためだけでなく、ABIが正しいことを理解したことをすばやく確認するためにも使用します。
すべてのレジスタをインラインアセンブルで上書きして、GCCに強制的に保存および復元させてみましょう。
main.c
_#include <inttypes.h>
uint64_t inc(uint64_t i) {
__asm__ __volatile__(
""
: "+m" (i)
:
: "rax",
"rbx",
"rcx",
"rdx",
"rsi",
"rdi",
"rbp",
"rsp",
"r8",
"r9",
"r10",
"r11",
"r12",
"r13",
"r14",
"r15",
"ymm0",
"ymm1",
"ymm2",
"ymm3",
"ymm4",
"ymm5",
"ymm6",
"ymm7",
"ymm8",
"ymm9",
"ymm10",
"ymm11",
"ymm12",
"ymm13",
"ymm14",
"ymm15"
);
return i + 1;
}
int main(int argc, char **argv) {
(void)argv;
return inc(argc);
}
_
コンパイルと逆アセンブル:
_ gcc -std=gnu99 -O3 -ggdb3 -Wall -Wextra -pedantic -o main.out main.c
objdump -d main.out
_
分解に含まれるもの:
_00000000000011a0 <inc>:
11a0: 55 Push %rbp
11a1: 48 89 e5 mov %rsp,%rbp
11a4: 41 57 Push %r15
11a6: 41 56 Push %r14
11a8: 41 55 Push %r13
11aa: 41 54 Push %r12
11ac: 53 Push %rbx
11ad: 48 83 ec 08 sub $0x8,%rsp
11b1: 48 89 7d d0 mov %rdi,-0x30(%rbp)
11b5: 48 8b 45 d0 mov -0x30(%rbp),%rax
11b9: 48 8d 65 d8 lea -0x28(%rbp),%rsp
11bd: 5b pop %rbx
11be: 41 5c pop %r12
11c0: 48 83 c0 01 add $0x1,%rax
11c4: 41 5d pop %r13
11c6: 41 5e pop %r14
11c8: 41 5f pop %r15
11ca: 5d pop %rbp
11cb: c3 retq
11cc: 0f 1f 40 00 nopl 0x0(%rax)
_
そのため、次のものがプッシュおよびポップされていることが明確にわかります。
_rbx
r12
r13
r14
r15
rbp
_
仕様から欠落しているのはrsp
だけですが、もちろんスタックは復元されると予想されます。議会を注意深く読むと、この場合に維持されていることが確認されます。
sub $0x8, %rsp
_:%rdi, -0x30(%rbp)
で_%rdi
_を保存するためにスタックに8バイトを割り当てます。これはインラインアセンブリ_+m
_制約に対して行われますlea -0x28(%rbp), %rsp
は_%rsp
_をsub
の前に復元します。つまり、_mov %rsp, %rbp
_の後に5ポップします%rsp
_に触れませんUbuntu 18.10、GCC 8.2.0でテスト済み。