このページの目次
Linux x86-64 システムコール ABI
手書きアセンブリ、インラインアセンブリ、seccompのデバッグ、または逆アセンブリの読解において、最も混乱しやすいのは以下の3つの異なるルールセットです。
- System V AMD64 C ABI:通常のユーザー空間関数における引数渡し、戻り値、およびレジスタの保存方法。
- Linux x86-64 syscall ABI:ユーザー空間がシステムコール番号と最大6つの引数をカーネルに渡す方法。
- CPUの
SYSCALL/SYSRET命令セマンティクス:特権レベルの切替時にハードウェアが実際にどのレジスタを変更するか。
本稿ではこれら3つの層を分離し、現在のLinux x86-64のエントリコードを基準に解説します。カーネルの実装は進化するため、ソースコードの断片をコピーする際は、特定バージョンの詳細を恒久的なABIとして扱うのではなく、バージョンを明記してください。
1. Linux syscall レジスタ規約
| 用途 | レジスタ |
|---|---|
| システムコール番号 | rax |
| 引数 1–3 | rdi, rsi, rdx |
| 引数 4–6 | r10, r8, r9 |
| 戻り値 | rax |
| ハードウェアによる破壊 | rcx, r11 |
典型的な呼び出し例:
mov $1, %rax # __NR_write
mov $1, %rdi # fd = STDOUT_FILENO
lea message(%rip), %rsi
mov $6, %rdx
syscall
Linuxカーネルは、負でない結果または -errno を直接返します。glibcなどのlibcラッパーは、-errno を -1 に変換し、スレッドローカルの errno を設定します。
なぜ第4引数は rcx ではなく r10 なのか
通常のSystem V関数では、第4の整数引数に rcx が使用されます。しかし、SYSCALL 命令はユーザー空間の戻りアドレスを rcx に書き込む必要があります。そのため、Linux syscall ABIでは第4引数が r10 に移動されています。
これが、インラインアセンブリで rcx と r11 のclobberを宣言しなければならない理由です。
static inline long
引数が4〜6個の場合、r10、r8、r9 を明示的にバインドする必要があります。本番コードでは、libcの呼び出しを優先すべきです。raw syscallは、キャンセルポイント、互換性ラッパー、およびlibcのエラー処理をバイパスします。
2. SYSCALL 命令の動作
64ビットモードで SYSCALL を実行すると、CPUは主に以下の処理を行います。
RCX <- 次のユーザー空間命令アドレス
R11 <- ユーザー空間の RFLAGS
RIP <- IA32_LSTAR
RFLAGS <- RFLAGS & ~IA32_FMASK
CS/SS <- IA32_STAR から派生したカーネルセグメントセレクタ
CPL <- 0
重要な結論は2つあります。
rcxとr11はハードウェアによって占有されるため、syscallを跨いで値を保存するために使用できません。SYSCALLはユーザー空間のrspを保存せず、自動的にカーネルスタックに切り替えることもありません。
RFLAGS は単に「変更されない」わけではありません。カーネルは IA32_FMASK でクリアするフラグビットを構成しており、Linuxのエントリコードは安全な状態を確立してから割り込みを管理します。
3. Linux entry_SYSCALL_64
現在のx86-64エントリの主要な順序は以下の通りです。
# arch/x86/entry/entry_64.S(セキュリティ強化と注釈は省略)
entry_SYSCALL_64:
swapgs
movq %rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2) # ユーザー rsp を一時保存
SWITCH_TO_KERNEL_CR3 scratch_reg=%rsp
movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp
pushq $__USER_DS
pushq PER_CPU_VAR(cpu_tss_rw + TSS_sp2)
pushq %r11
pushq $__USER_CS
pushq %rcx
pushq %rax
# struct pt_regs の構築を続行し、その後 do_syscall_64 を呼び出す
ここで言う tss.sp2 は、ハードウェアによって自動的にスタックが切り替えられる従来の sp0 フローではなく、Linuxが使用するper-CPUのスクラッチスロットです。順序も重要です。まずユーザー rsp を保存し、次にカーネルページテーブルとカーネルスタックに切り替え、最後にカーネルスタック上で struct pt_regs を構築します。
したがって、ユーザー空間のred zoneは、通常のsyscallエントリでのレジスタフレームによって上書きされません。フレームはカーネルスタック上にあり、「ユーザー rsp の下」にはありません。
4. 戻り値:SYSRET 高速パスと IRET フォールバック
システムコール終了後、Linuxは戻りコンテキストが SYSRET に適しているかどうかを確認します。アドレス、セグメントレジスタ、フラグビットなどの条件がすべて安全な場合にのみ高速パスが使用され、そうでない場合は IRET が使用されます。
これは単なるパフォーマンスの選択ではありません。AMDとIntelの SYSRET は、非canonicalアドレスなどの境界ケースで扱いにくい動作をするため、カーネルはユーザー制御可能な pt_regs を事前に検証する必要があります。
ユーザー空間から観察すると、両方の戻りパスは同じsyscall ABIを満たす必要があります。カーネルが必ず SYSRET を使用すると仮定してはいけません。
5. Red zone とシステムコール
System V AMD64 ABIでは、ユーザー rsp の下に128バイトのred zoneが定義されています。リーフ関数は、rsp を調整せずにこの領域を一時的に使用できます。
以下の3つのケースを区別する必要があります。
- 通常のsyscall/割り込みによるカーネル進入:Linuxはカーネルスタックに切り替わり、エントリフレームはユーザーred zoneに書き込みません。
- ユーザー空間へのシグナル配信:カーネルはシグナルフレームを構築する際に、red zoneのスペースを確保します。
- カーネルコード自体:カーネルは
-mno-red-zoneでコンパイルされるため、ユーザー空間ABIのred zoneに依存できません。
手書きのインラインアセンブリにおいて、"memory" clobberはコンパイラのメモリ再配置を制約するだけであり、syscallが特定の固定メモリ範囲のみアクセスすることを意味するものではありません。カーネルに渡されるポインタがどのバッファを指すかは、具体的なsyscallの定義によります。
6. vDSO は「ユーザー空間でsyscallを実行する」ものではない
vDSOは、プロセスのアドレス空間にマッピングされたカーネルの小さなELFコードです。一部のlibc APIはまずvDSOを呼び出し、カーネルが管理する読み取り専用データから結果を計算することで、実際の SYSCALL の実行を回避できます。
一般的なシンボルには以下が含まれます。
__vdso_clock_gettime__vdso_gettimeofday__vdso_time__vdso_getcpu- 新世代カーネルにおける
__vdso_getrandom
これらのシンボルが存在するか、リクエストを処理できるかは、アーキテクチャとカーネルバージョンに依存します。libcは、実際のsyscallへのフォールバックを準備しておく必要があります。
現在のプロセスのvDSOを確認するには:
# glibc環境では、実際の呼び出しが依然としてカーネルに入るかどうかを観察できる
7. int 0x80、sysenter、および64ビット syscall
これらは異なるエントリメカニズムであり、単一の固定「サイクル数」テーブルだけで比較することはできません。
| メカニズム | 主な使用シーン | エントリ設定 |
|---|---|---|
int 0x80 | 従来のi386 ABI;x86-64でも互換性エントリあり | IDT |
sysenter/sysexit | 32ビット高速システムコール | SYSENTER MSR |
syscall/sysret | ネイティブx86-64 Linux syscall ABI | STAR/LSTAR/FMASK MSR |
実際のレイテンシは、CPUマイクロアーキテクチャ、KPTI、緩和策、仮想化、周波数、およびカーネルバージョンの影響を受けます。パフォーマンスデータが必要な場合は、対象マシンで測定し、環境を報告してください。
古いCPU上での 50–70 cycles を、クロスプラットフォームの事実として扱わないでください。
8. 完全なアセンブリ例
.section .rodata
message:
.ascii "hello\n"
.section .text
.global _start
_start:
mov $1, %rax # __NR_write
mov $1, %rdi # stdout
lea message(%rip), %rsi
mov $6, %rdx
syscall
test %rax, %rax
js .error
xor %rdi, %rdi
jmp .exit
.error:
mov $1, %rdi
.exit:
mov $60, %rax # __NR_exit
syscall
ビルドと観察:
9. 権威ある資料
- Linux
entry_SYSCALL_64現在のソースコード - Linux x86 syscall 引数マッピング
- Linux man-pages
syscall(2) - x86-64 psABI
- Intel 64 and IA-32 Architectures Software Developer Manuals
推奨される読書順序は、まず syscall(2) でLinux ABIを確認し、次にプロセッサマニュアルで SYSCALL/SYSRET 命令のセマンティクスを確認し、最後にターゲットカーネルバージョンの entry_64.S と照合することです。これにより、CPUのルール、ユーザー空間のC ABI、および特定バージョンのカーネル実装を混同することがなくなります。