呼び出し規約は、引数の配置、結果、保存される状態、スタックレイアウトに関するバイナリ契約です。Linux x86-64 プログラムは System V AMD64 関数 ABI を使用し、32 ビットプログラムは System V i386 ABI を使用します。Linux のシステムコールは別のエントリ ABI を使用するため、通常の関数呼び出しとして扱ってはいけません。
AMD64 関数呼び出し #
AMD64 ABI は各引数を最初に分類します。通常の整数とポインターを含む整数クラスの値には rdi、rsi、rdx、rcx、r8、r9 を使用します。SSE クラスの値には xmm0 から xmm7 を使用します。一方のクラスを使い切っても、もう一方を使い切ったことにはなりません。分類規則に従って割り当てられない引数は、スタック入力領域に置かれます。
集約型は 8 バイトクラスに分割されます。そのため、条件を満たす小さな構造体は複数の整数またはベクトルレジスターを占有できますが、メモリクラスの構造体はスタックで渡されます。メモリクラスの戻り値の場合、呼び出し元がストレージを割り当て、そのアドレスを隠し引数レジスター rdi で渡します。呼び出し先はそのアドレスを rax で返します。
整数クラスの戻り値は rax、続いて rdx を使用し、SSE クラスの結果は xmm0、続いて xmm1 を使用します。浮動小数点の float と double の結果は xmm0 を使用し、ABI で指定される long double などの x87 クラスは x87 スタックを使用します。
rbx、rbp、r12 から r15、rsp は呼び出し先保存です。呼び出し先がこれらの 1 つを変更した場合、ret 前に復元する必要があります。その他の汎用レジスターとすべてのベクトルレジスターは、基本 System V ABI では呼び出し元保存です。呼び出し元は呼び出し後に必要な生存値を保存します。
call の直前では、rsp は 16 バイト境界にアラインされています。8 バイトの戻りアドレスがプッシュされるため、通常の呼び出し先は rsp % 16 == 8 で入ります。スタック引数はアライメントに必要なパディングを伴って配置され、単純に 1 つずつプッシュされるわけではありません。ABI は現在の rsp より下に 128 バイトのレッドゾーンも予約します。リーフのユーザー空間関数は、通常のシグナル配送がそれを保持するため、スタックを調整せずに使用できます。カーネルコードと -mno-red-zone でビルドされたコードは、その領域に依存してはいけません。
可変長関数 #
可変長関数の固定引数には通常の分類アルゴリズムを使用します。呼び出し元はさらに %al を、使用したベクトル引数レジスター数の 0 から 8 までの上限に設定します。この用途で定義されるのは下位バイトだけです。そのため、%al を未指定のままにする手書きの printf 呼び出しは、表示される引数がすべて正しく見えても ABI に違反します。
va_start を使用する可変長関数の呼び出し先は、自身のフレームにレジスター保存領域を用意し、必要な引数レジスターを保存します。AMD64 の va_list は次を追跡します。
- 保存領域内の
gp_offsetとfp_offset。 - メモリで渡された引数用の
overflow_arg_area。 - 保存されたレジスター引数のベースである
reg_save_area。
va_arg は型分類とアライメントを適用しながら、該当する位置を進めます。デフォルト引数プロモーションが適用されるため、呼び出し元は必要に応じて float を double として、狭い整数型を int または unsigned int として渡します。
i386 関数呼び出し #
i386 System V ABI は、通常の引数をスタック入力領域に右から左の順で渡します。エントリ時には 0(%esp) に戻りアドレスがあり、4(%esp) に最初の引数があります。標準 C 規約では、戻り後に呼び出し元が引数を削除します。
ebx、esi、edi、ebp、esp は呼び出し先保存で、eax、ecx、edx は呼び出し元保存です。整数またはポインターの結果は eax を使用し、2 ワードの整数は edx:eax を使用できます。浮動小数点の結果は st(0) を使用します。構造体の戻り値規約は分類に依存し、隠し宛先ポインターを使用することがあります。
一般的なフレームポインターのプロローグは次のとおりです。
push ebp
mov ebp, esp
sub esp, local_sizeebp は本質的にフレームポインターではないため、最適化されたコードはこのシーケンスを省略できます。歴史的な i386 コードは 4 バイトのスタックアライメントを仮定していました。現在の GNU/Linux i386 ABI は呼び出し境界で 16 バイトのアライメントを要求し、コンパイラーはレガシー呼び出し元との相互運用時にエントリを再アラインできます。手書きアセンブリーは古い例からアライメントを推測せず、ツールチェーンが選択した ABI に従う必要があります。
Linux システムコール #
x86-64 Linux の syscall ABI は次のとおりです。
| 目的 | レジスター |
|---|---|
| システムコール番号 | rax |
| 引数 1–6 | rdi、rsi、rdx、r10、r8、r9 |
| 結果 | rax |
syscall によって無条件に破壊されるレジスター | rcx、r11 |
ハードウェアはユーザーの戻りアドレスを rcx に、ユーザーフラグを r11 に保存するため、4 番目の引数は AMD64 関数呼び出しとは異なり r10 を使用します。カーネルは rax に成功値または負のエラーを返します。-1 から -4095 までの値は -errno を符号化します。libc ラッパーはこのエラーを -1 に変換し、スレッドローカルな errno を設定します。
; x86-64 Linux: write(1, message, message_length)
mov eax, 1
mov edi, 1
lea rsi, [rel message]
mov edx, message_length
syscallint $0x80 を使用する i386 プロセスでは、eax が番号を保持し、引数には ebx、ecx、edx、esi、edi、ebp を使用し、eax が結果を保持します。i386 関数 ABI では ebx、esi、edi、ebp は呼び出し先保存ですが、これらのレジスターは syscall オペランドです。インラインエントリコードを含む関数は、呼び出し元に対する関数 ABI も引き続き保持する必要があります。
関数 ABI と syscall ABI の違い #
AMD64 関数呼び出しは 4 番目の引数に rcx を使用し、rbx、rbp、r12–r15 を保存します。x86-64 syscall は 4 番目の引数に r10 を使用し、rcx/r11 を明示的に破壊します。関数は宣言された API に従ってエラーを返しますが、生のカーネル境界は負のエラーコードを使用します。syscall 命令は特権レベルも変更しますが、それ自体では戻りアドレスをプッシュしたりスタックを切り替えたりしません。Linux のエントリコードが必要なカーネルスタックへの切り替えを行います。
アプリケーションコードは libc ラッパーを優先すべきです。ラッパーは両方の契約、該当する場合のエラー変換とキャンセル動作、vDSO の選択を実装します。インラインアセンブリーはレジスター、条件コード、メモリ効果をコンパイラーに記述する必要があり、主に C ライブラリとランタイムに適しています。
System V AMD64 psABI、syscall(2)、Linux の entry_64.S が、これらの契約に関する主な参照です。
コメント
GitHub で閲覧・コメントする(投稿にはログインが必要です)