このページの目次
x86-64 システムコール ABI
カバー範囲: syscall 命令 → 引数レジスタ規約 → red zone → vDSO → vsyscall → int 0x80 / sysenter との歴史的比較 対象: x86-64 (AMD64), Linux 3.x+
概要
x86-64 システムコールは syscall 命令(Intel 構文: syscall)を使用します。これは AMD64 で導入された高速システムコールメカニズムであり、32ビット x86 の3世代の進化(int 0x80 → sysenter/sysexit → syscall/sysret)に取って代わりました。呼び出し規約を理解することは、アセンブリの直接記述、インラインアセンブリ、または逆アセンブル結果における syscall 命令の解析に役立ちます。
レジスタ規約
# システムコール番号 (syscall number):
rax = システムコール番号 ( __NR_read = 0, __NR_write = 1, ...)
# 引数 (最大6個):
rdi = arg1 rsi = arg2 rdx = arg3
r10 = arg4 r8 = arg5 r9 = arg6
# 戻り値:
rax = 戻り値 (>=0: 成功, -errno: エラー → ユーザー空間の libc が errno に変換)
rdx = 2番目の戻り値 (限られた数のシステムコールのみ)
# 破壊されるレジスタ (Clobbered):
rcx = 保存された RIP (syscall 命令は RCX ← RIP を実行)
r11 = 保存された RFLAGS (syscall 命令は R11 ← RFLAGS を実行)
# 保持されるレジスタ (Preserved):
rbx, r12-r15, rbp, rsp
syscall 命令自体が rcx を戻りアドレスの保存に使用するため、4番目の引数として rcx の代わりに r10 が使用される点に注意してください。
syscall 命令の動作
syscall:
1. RCX ← RIP (ユーザー空間の戻りアドレスを保存)
2. R11 ← RFLAGS
3. RIP ← IA32_LSTAR (MSR 0xC0000082, entry_SYSCALL_64 を指す)
4. CS ← IA32_STAR[47:32]
5. SS ← IA32_STAR[47:32] + 8
6. Ring 0 (カーネル空間) に切り替え
→ カーネルは現在、カーネルスタックで実行中、IF は変更されない
sysretq (復帰):
1. RIP ← RCX (ユーザー空間の次の命令を復元)
2. RFLAGS ← R11 (フラグレジスタを復元、下位32ビット)
3. Ring 3 (ユーザー空間) に切り替え
重要: syscall はスタックを自動的に切り替えません。カーネルは entry_SYSCALL_64 内で swapgs と per-CPU kernel_stack の読み取りを通じて、手動でカーネルスタックに切り替える必要があります。
カーネルエントリ: entry_SYSCALL_64
# arch/x86/entry/entry_64.S
entry_SYSCALL_64:
swapgs # GS ベースをユーザー空間からカーネル空間の per-CPU 領域に切り替え
movq %rsp, PER_CPU(cpu_tss_rw + TSS_sp0) # ユーザースタックを TSS に保存
movq PER_CPU(pcpu_hot + X_top_of_stack), %rsp # カーネルスタックに切り替え
pushq $__USER_DS # iret フレームを構築 (SS)
pushq PER_CPU(cpu_tss_rw + TSS_sp0) # (RSP)
pushq %r11 # (RFLAGS)
pushq $__USER_CS # (CS)
pushq %rcx # (RIP)
# do_syscall_64(rdi=pt_regs, rax=nr) を呼び出し
call do_syscall_64
Red Zone: x86-64 ABI の落とし穴
x86-64 ABI: スタックポインタ (RSP) の下位 128 バイトは "red zone"
→ シグナルハンドラはこの 128 バイトにアクセスしてはならない
→ コンパイラは RSP を調整せずに、この領域をリーフ関数で使用できる
カーネルとの相互作用:
カーネル進入時: カーネルは RSP の下に iret フレームを配置する → red zone には触れない
ユーザー空間復帰時: ユーザー空間は red zone のデータを依存している可能性がある → カーネルはこれを破壊してはならない
シグナル処理: sigframe のインストール時には red zone を考慮する必要がある
→ シグナルスタックフレームは RSP-128 より下から開始される
vDSO: カーネルに入らないシステムコール
// arch/x86/entry/vdso/
// 一部のシステムコールは vDSO を介してユーザー空間で直接実行される:
// __vdso_clock_gettime(): TSC を読み取り、timekeeper 補正を適用 → システムコール不要!
// __vdso_getcpu(): per-CPU 変数を読み取り → システムコール不要!
// __vdso_time(): timekeeper キャッシュを読み取り → システムコール不要!
// __vdso_getrandom(): カーネルが管理する乱数プールを読み取り → システムコール不要!
// vsyscall (旧式、廃止予定):
// 0xffffffffff600000 に固定 → セキュリティリスク (固定アドレス = ROP の標的)
// 現在はデフォルトでエミュレートされる (ページフォルトによるシミュレーション) → 極めて低速 → vDSO を使用するべき
32-bit x86 との比較
int 0x80 sysenter syscall (x86-64)
アーキテクチャ x86 x86 (Pentium II 以降) x86-64 (AMD64)
戻りアドレスの保存 なし (ソフトウェア) ハードウェア: ECX ← EIP ハードウェア: RCX ← RIP
CPU コンテキストの切り替え IDT を介して MSR を介して MSR を介して
戻り値レジスタ eax eax rax
破壊されるレジスタ 少ない ecx, r11 rcx, r11
レイテンシ (サイクル) ~150 ~80 ~50-70
完全なシステムコールの例
# write(1, "hello\n", 6) の手書き — x86-64 Linux
movq $1, %rax # __NR_write = 1
movq $1, %rdi # fd = stdout
leaq msg(%rip), %rsi # buf
movq $6, %rdx # count
syscall
movq $60, %rax # __NR_exit = 60
xorq %rdi, %rdi # 終了コード = 0
syscall
msg: .ascii "hello\n"
参考
- ソースコード:
arch/x86/entry/entry_64.S,arch/x86/entry/vdso/,arch/x86/include/asm/syscall.h - ABI ドキュメント: System V AMD64 ABI, Linux x86-64 システムコールテーブル:
/usr/include/asm/unistd_64.h - LWN: "The vDSO and vsyscall", "Faster syscalls"
キーワード: syscall, sysret, entry_SYSCALL_64, red zone, vDSO, vsyscall, swapgs, calling convention