x86-64 と x86 のシステムコール規約
システムコールには独自の呼び出し規約があり、C ABI ではなく各オペレーティングシステムが定めています。どの命令でカーネルに入るか、番号と引数をどこに置くか、エラーがどう返るかを説明します。
プログラムは通常、C ライブラリを通じてカーネルに到達します。write などの C ライブラリの関数が引数を所定の位置に移し、カーネルに入る命令を実行します。ライブラリの下では、システムごとに独自の規約があります。使う命令、システムコール番号を保持するレジスター、引数のレジスター、エラーの報告方法です。これはそのプラットフォームの C の呼び出し規約に似ていますが、アセンブリ、ランタイムライブラリ、サンドボックスを書く人にとって重要な細部が異なります。
| システム | 命令 | 番号 | 引数 | 結果とエラー |
|---|---|---|---|---|
| Linux x86-64 | syscall | RAX | RDI、RSI、RDX、R10、R8、R9 | RAX、-4095 から -1 は -errno |
| Linux x32 | syscall | RAX、ビット 30 をセット | RDI、RSI、RDX、R10、R8、R9 | RAX、-4095 から -1 は -errno |
| Linux i386 | int 0x80 または __kernel_vsyscall | EAX | EBX、ECX、EDX、ESI、EDI、EBP | EAX、-4095 から -1 は -errno |
| FreeBSD x86-64 | syscall | RAX | RDI、RSI、RDX、R10、R8、R9 | RAX、エラー時はキャリーフラグがセットされ、RAX に errno |
| FreeBSD i386 | int 0x80 | EAX | スタック上 | EAX、エラー時はキャリーフラグがセットされる |
| macOS x86-64 | syscall | RAX (クラスと番号) | RDI、RSI、RDX、R10、R8、R9 | RAX、エラー時はキャリーフラグがセットされる |
| Windows | 文書化されていない | 安定していない | 文書化されていない | 文書化されていない |
x86-64 の Linux
64 ビット Linux のプログラムは、RAX にシステムコール番号、RDI、RSI、RDX、R10、R8、R9 に最大 6 つの引数を置いて syscall を実行します。カーネルは結果を RAX に返し、-4095 から -1 までの値は符号を反転したエラー番号です。スタックでは何も渡しません。
引数レジスターは C の呼び出し規約のものと同じですが、4 番目だけは RCX ではなく R10 です。syscall 命令自体が RCX を戻りアドレスで、R11 をフラグで上書きするためです。この 2 つのレジスターの値は失われますが、カーネルは RAX 以外のほかのレジスターをすべて保存します。番号は Linux のシステムコール表にあり、write は 1 番です。
raw_syscall6 は、C の呼び出し規約の引数をシステムコールの引数のレジスターに移します。番号は RDI から RAX へ、各引数は 1 つずつ隣のレジスターへ、4 番目は RCX から R10 へ、6 番目はスタックから R9 へ移ります。raw_write はファイルディスクリプター 1 に対してシステムコール 1 番の write を実行します。syscall-x86-64.c
/* A system call on x86-64 Linux: the number in RAX, arguments in RDI,
RSI, RDX, R10, R8 and R9; the kernel returns in RAX and overwrites
RCX and R11. The six arguments of the C function arrive in the
registers of the C convention, which differ in the fourth. */
long raw_syscall6(long n, long a, long b, long c, long d, long e, long f)
{
register long r10 __asm__("r10") = d;
register long r8 __asm__("r8") = e;
register long r9 __asm__("r9") = f;
long ret;
__asm__ volatile ("syscall"
: "=a" (ret)
: "a" (n), "D" (a), "S" (b), "d" (c), "r" (r10), "r" (r8), "r" (r9)
: "rcx", "r11", "memory");
return ret;
}
/* write(1, buf, len): system call 1 on x86-64. */
long raw_write(const char *buf, unsigned long len)
{
long ret;
__asm__ volatile ("syscall"
: "=a" (ret)
: "a" (1L), "D" (1L), "S" (buf), "d" (len)
: "rcx", "r11", "memory");
return ret;
}
GCC 14.2.0 (Debian 14.2.0-19) x86_64-linux-gnu
AT&T 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -S syscall-x86-64.c
raw_syscall6:
movq %rdi, %rax
movq %r8, %r10
movq %rsi, %rdi
movq %r9, %r8
movq %rdx, %rsi
movq 8(%rsp), %r9
movq %rcx, %rdx
syscall
ret
raw_write:
movq %rdi, %r8
movl $1, %eax
movq %rsi, %rdx
movq %rax, %rdi
movq %r8, %rsi
syscall
retIntel 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -masm=intel -S syscall-x86-64.c
raw_syscall6:
mov rax, rdi
mov r10, r8
mov rdi, rsi
mov r8, r9
mov rsi, rdx
mov r9, QWORD PTR 8[rsp]
mov rdx, rcx
syscall
ret
raw_write:
mov r8, rdi
mov eax, 1
mov rdx, rsi
mov rdi, rax
mov rsi, r8
syscall
retClang 23.1.2 x86_64-linux-gnu
AT&T 構文 clang --target=x86_64-linux-gnu -O2 -fno-asynchronous-unwind-tables -S syscall-x86-64.c
raw_syscall6:
movq %r9, %r11
movq %rdi, %rax
movq 8(%rsp), %r9
movq %rsi, %rdi
movq %rdx, %rsi
movq %rcx, %rdx
movq %r8, %r10
movq %r11, %r8
syscall
retq
raw_write:
movq %rsi, %rdx
movq %rdi, %rsi
movl $1, %eax
movl $1, %edi
syscall
retqIntel 構文 clang --target=x86_64-linux-gnu -O2 -fno-asynchronous-unwind-tables -masm=intel -S syscall-x86-64.c
raw_syscall6:
mov r11, r9
mov rax, rdi
mov r9, qword ptr [rsp + 8]
mov rdi, rsi
mov rsi, rdx
mov rdx, rcx
mov r10, r8
mov r8, r11
syscall
ret
raw_write:
mov rdx, rsi
mov rsi, rdi
mov eax, 1
mov edi, 1
syscall
retLinux x32
x32 のプログラムは同じ命令とレジスターを使いますが、番号の表は独自のものです。カーネルは番号のビット 30、__X32_SYSCALL_BIT (0x40000000) で x32 のシステムコールを識別します。ほとんどのシステムコールは x86-64 と同じ番号で共有されています。引数にサイズの異なるポインターや long の値を含むシステムコールには、512 から始まる独自の番号があります。
i386 の Linux
32 ビット Linux のプログラムは、EAX に番号、EBX、ECX、EDX、ESI、EDI、EBP に最大 6 つの引数を置き、int 0x80 でカーネルに入ることができます。結果は EAX に返り、エラーは x86-64 と同じく -4095 から -1 です。C ライブラリはより高速な経路を優先します。カーネルがすべてのプロセスにマップする vDSO の関数 __kernel_vsyscall を呼び出し、この関数はプロセッサーが SYSENTER を備えていればそれで、なければ int 0x80 でカーネルに入ります。
write はシステムコール 4 番です。EBX は呼び出し先保存レジスターであり、呼び出し元の位置独立コードが使っているため、保存して復元する必要があります。syscall-i386.c
/* write(1, buf, len) on i386 Linux: system call 4 through INT 0x80,
the number in EAX and the arguments in EBX, ECX and EDX. The
braces give the instruction in AT&T and in Intel syntax. */
long raw_write(const char *buf, unsigned long len)
{
long ret;
__asm__ volatile ("int {$0x80|0x80}"
: "=a" (ret)
: "a" (4L), "b" (1L), "c" (buf), "d" (len)
: "memory");
return ret;
}
GCC 14.2.0 (Debian 14.2.0-19) i386-linux-gnu
AT&T 構文 gcc-14 -m32 -march=i686 -mtune=generic -O2 -fno-asynchronous-unwind-tables -S syscall-i386.c
raw_write:
pushl %ebx
movl $4, %eax
movl 8(%esp), %ecx
movl $1, %ebx
movl 12(%esp), %edx
int $0x80
popl %ebx
retIntel 構文 gcc-14 -m32 -march=i686 -mtune=generic -O2 -fno-asynchronous-unwind-tables -masm=intel -S syscall-i386.c
raw_write:
push ebx
mov eax, 4
mov ecx, DWORD PTR 8[esp]
mov ebx, 1
mov edx, DWORD PTR 12[esp]
int 0x80
pop ebx
retClang 23.1.2 i686-linux-gnu
AT&T 構文 clang --target=i686-linux-gnu -march=i686 -mtune=generic -O2 -fno-asynchronous-unwind-tables -S syscall-i386.c
raw_write:
pushl %ebx
movl 8(%esp), %ecx
movl 12(%esp), %edx
movl $4, %eax
movl $1, %ebx
int $128
popl %ebx
retlIntel 構文 clang --target=i686-linux-gnu -march=i686 -mtune=generic -O2 -fno-asynchronous-unwind-tables -masm=intel -S syscall-i386.c
raw_write:
push ebx
mov ecx, dword ptr [esp + 8]
mov edx, dword ptr [esp + 12]
mov eax, 4
mov ebx, 1
int 128
pop ebx
ret64 ビットのプログラムも int 0x80 を実行でき、その場合カーネルはその呼び出しを 32 ビットプログラムのものとして扱います。番号を i386 の表で引き、引数を EBX、ECX、EDX、ESI、EDI、EBP から、それぞれ 32 ビットずつ取ります。そのため、64 ビットのコードにコピーされた int 0x80 は、意図とは異なるシステムコールを、切り詰められたポインターで実行してしまいます。
FreeBSD
x86-64 の FreeBSD は Linux と同じレジスターを使います。番号は RAX、引数は RDI、RSI、RDX、R10、R8、R9 に置き、syscall でカーネルに入ります。エラーの報告方法は異なります。カーネルはキャリーフラグをセットして正のエラー番号を RAX に返し、C ライブラリのラッパーはキャリーフラグで分岐して errno を設定します。i386 では、FreeBSD の C ライブラリは C の呼び出し規約で呼ばれる関数の中で int 0x80 を実行し、カーネルは呼び出しが引数を置いたユーザースタックから引数を読み取ります。
macOS
x86-64 の macOS は syscall でカーネルに入り、FreeBSD や Linux と同じレジスターで引数を渡しますが、RAX の番号はビット 24 以上にクラスを持ちます。カーネルである XNU が BSD カーネルと Mach を組み合わせたものだからです。主なクラスは次のとおりです。
| クラス | 値 | システムコール |
|---|---|---|
| Mach | 1 | Mach トラップ (Mach への呼び出し) |
| Unix | 2 | write などの BSD システムコール |
| マシン依存 | 3 | x86 固有の呼び出し |
そのため、write (4 番) などの BSD システムコールは RAX に 0x2000004 を置いて実行します。FreeBSD と同じく、macOS もエラー時にキャリーフラグをセットします。
OpenBSD
OpenBSD 7.5 以降、カーネルは各システムコールを、そのシステムコールの命令がある C ライブラリ内の場所からしか受け付けません。動的リンカーが pinsyscalls でその場所をカーネルに伝え、ほかの場所からのシステムコールは SIGABRT でプログラムを終了させます。そのため、プログラムは C ライブラリを経由する必要があります。
Windows
Windows はシステムコール規約を文書化していません。プログラムは Windows API を呼び出し、その背後にあるネイティブシステムサービス、つまり Nt と Zw のルーチンには、ユーザーモードから、Microsoft が命令も番号も文書化していないシステムコールを通じて到達します。番号は Windows のリリースごとに変わります。たとえば各リリースの番号をまとめた表によれば、NtCreateUserProcess は Windows Vista から Windows 11 までの間に 6 つを超える番号を持ってきました。独自の番号で syscall を実行するコードは、別の Windows ビルドでは異なるシステムコールを実行するか、どのシステムコールも実行しない可能性があります。
出典
- System V Application Binary Interface, AMD64 Architecture Processor Supplement, version 1.0: コミット
e1ce098331da、2025-03-12 - Linux 7.3: arch/x86/entry/entry_64.S: コミット
c03c1a14507d、2026-10-18 - Linux 7.3: arch/x86/entry/entry_32.S: コミット
c03c1a14507d、2026-10-18 - Linux 7.3: arch/x86/include/uapi/asm/unistd.h: コミット
c03c1a14507d、2026-10-18 - FreeBSD 14.3: lib/libc/amd64/SYS.h: コミット
8c9ce319fef7、2025-06-06 - XNU xnu-12377.121.6: osfmk/mach/i386/syscall_sw.h: コミット
ac9718fb1af6、2026-06-17 - OpenBSD manual: pinsyscalls(2): 2026-09-25 の公開版
- Microsoft Learn: Using Nt and Zw versions of the native system services routines: コミット
110f60eaf2ac、2026-09-15 - Mateusz Jurczyk: Windows X86-64 System Call Table: 2026-09-25 の公開版