web-dev-qa-db-ja.com

Linux x86-64関数呼び出しで保存されるレジスタ

Linux x86-64 ABIがレジスタとスタックを使用して関数にパラメーターを渡す方法を理解していると思います(cf. 前のABIの説明 )。私が混乱しているのは、関数呼び出し全体で保持されると予想されるif/whatレジスタです。つまり、どのレジスタが破壊されないように保証されていますか?

37
boneheadgeek

以下に、レジスタの完全な表とドキュメントからの使用方法を示します[ PDF Link ]:

table from docs

r12r13r14r15rbxrsprbpは、呼び出し先が保存するレジスタです-[関数呼び出し間で保持]列に[はい]があります。

64
Carl Norum

ABIは、標準に準拠したソフトウェアが期待できることを指定します。主にコンパイラ、リンカー、その他の言語処理ソフトウェアの作成者向けに書かれています。これらの作成者は、同じ(または異なる)コンパイラーでコンパイルされたコードで適切に動作するコードをコンパイラーが生成することを望んでいます。それらはすべて、ルールのセットに同意する必要があります:呼び出し元から呼び出し先に渡される関数の正式な引数はどのように、関数の戻り値は呼び出し先から呼び出し元にどのように渡されるか、どのレジスターは呼び出し境界全体で保持/スクラッチ/未定義であるかなどオン。

たとえば、関数の生成されたアセンブリコードは、値を変更する前に保存されたレジスタの値を保存する必要があり、呼び出し元に戻る前にコードが保存された値を復元する必要があるというルールがあります。スクラッチレジスタの場合、生成されたコードはレジスタ値を保存および復元する必要はありません。必要に応じて行うことができますが、標準準拠のソフトウェアはこの動作に依存することはできません(実行する場合は標準準拠のソフトウェアではありません)。

アセンブリコードを記述している場合は、yoがこれらの同じルールを実行します(コンパイラーの役割を果たしています)。つまり、コードが呼び出し先保存レジスタを変更する場合、元のレジスタ値を保存および復元する命令を挿入する必要があります。アセンブリコードが外部関数を呼び出す場合、コードは標準に準拠した方法で引数を渡す必要があり、呼び出し先が戻るときに、保存されたレジスタ値が実際に保存されるという事実に依存する場合があります。

ルールは、標準に準拠したソフトウェアがどのように機能するかを定義します。ただし、これらのルールでnotを実行するコードを記述する(または生成する)ことは完全に合法です!コンパイラは、特定の状況下でルールに従う必要がないことを知っているため、これを常に行います。

たとえば、次のように宣言され、アドレスが取得されないfooという名前のC関数を考えます。

static foo(int x);

コンパイラは、コンパイル時に、この関数が現在コンパイル中のファイル内の他のコードによってのみ呼び出されることを100%確信しています。関数fooは、静的であるという意味の定義が与えられているため、他の人が呼び出すことはできません。コンパイラーはコンパイル時にfooのすべての呼び出し元を知っているため、コンパイラーは必要な呼び出しシーケンスを自由に使用できます(呼び出しをまったく行わないこと、つまりfooの呼び出し元にfooのコードをインライン化することを含みます) 。

アセンブリコードの作成者として、これも行うことができます。つまり、標準に準拠するソフトウェアの期待を妨害または侵害しない限り、2つ以上のルーチンの間に「プライベートアグリーメント」を実装できます。

4
Jeff N

実験的アプローチ: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);
}
_

GitHubアップストリーム

コンパイルと逆アセンブル:

_ 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ポップします
  • 6つのプッシュと6つの対応するポップがあります
  • 他の指示は_%rsp_に触れません

Ubuntu 18.10、GCC 8.2.0でテスト済み。