このページの目次

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 0x80sysenter/sysexitsyscall/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