---
title: Linux x86-64 システムコール ABI：レジスタ、SYSCALL、RCX/R11
url: https://doc.liz6.com/ja/systems-programming/02-system-call-abi/01-x86-64-system-call-abi
locale: ja
area: systems-programming
tags:
- x86-64
- syscall
- ABI
- Linux
- assembly
- vDSO
- systems-programming
- system-call-abi
date: 2026-08-11
modified: 2026-08-11
description: CPU命令のセマンティクス、Linux syscall ABI、および現在のカーネル entry_SYSCALL_64 の3つの層からx86-64システムコールを解説し、r10引数、RCX/R11のclobber、スタック切替、red zone、SYSRET、vDSOについて重点的に説明します。
---

# Linux x86-64 システムコール ABI

手書きアセンブリ、インラインアセンブリ、seccompのデバッグ、または逆アセンブリの読解において、最も混乱しやすいのは以下の3つの異なるルールセットです。

1. **System V AMD64 C ABI**：通常のユーザー空間関数における引数渡し、戻り値、およびレジスタの保存方法。
2. **Linux x86-64 syscall ABI**：ユーザー空間がシステムコール番号と最大6つの引数をカーネルに渡す方法。
3. **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` |

典型的な呼び出し例：

```asm
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を宣言しなければならない理由です。

```c
static inline long raw_write(int fd, const void *buf, unsigned long count)
{
    register long rax __asm__("rax") = 1;   /* __NR_write */
    register long rdi __asm__("rdi") = fd;
    register long rsi __asm__("rsi") = (long)buf;
    register long rdx __asm__("rdx") = count;

    __asm__ volatile (
        "syscall"
        : "+a"(rax)
        : "D"(rdi), "S"(rsi), "d"(rdx)
        : "rcx", "r11", "memory"
    );
    return rax;
}
```

引数が4〜6個の場合、`r10`、`r8`、`r9` を明示的にバインドする必要があります。本番コードでは、libcの呼び出しを優先すべきです。raw syscallは、キャンセルポイント、互換性ラッパー、およびlibcのエラー処理をバイパスします。

## 2. `SYSCALL` 命令の動作

64ビットモードで `SYSCALL` を実行すると、CPUは主に以下の処理を行います。

```text
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エントリの主要な順序は以下の通りです。

```asm
# 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を確認するには：

```bash
grep '\[vdso\]' /proc/self/maps

# glibc環境では、実際の呼び出しが依然としてカーネルに入るかどうかを観察できる
strace -e trace=clock_gettime,gettimeofday /bin/date
```

## 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、緩和策、仮想化、周波数、およびカーネルバージョンの影響を受けます。パフォーマンスデータが必要な場合は、対象マシンで測定し、環境を報告してください。

```bash
perf stat -r 20 ./syscall-benchmark
```

古いCPU上での `50–70 cycles` を、クロスプラットフォームの事実として扱わないでください。

## 8. 完全なアセンブリ例

```asm
.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
```

ビルドと観察：

```bash
as -o syscall.o syscall.S
ld -o syscall syscall.o
strace ./syscall
objdump -drwC -Mintel ./syscall
```

## 9. 権威ある資料

- [Linux `entry_SYSCALL_64` 現在のソースコード](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/x86/entry/entry_64.S)
- [Linux x86 syscall 引数マッピング](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/x86/entry/syscalls/syscall_64.tbl)
- [Linux man-pages `syscall(2)`](https://man7.org/linux/man-pages/man2/syscall.2.html)
- [x86-64 psABI](https://gitlab.com/x86-psABIs/x86-64-ABI)
- [Intel 64 and IA-32 Architectures Software Developer Manuals](https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html)

推奨される読書順序は、まず `syscall(2)` でLinux ABIを確認し、次にプロセッサマニュアルで `SYSCALL/SYSRET` 命令のセマンティクスを確認し、最後にターゲットカーネルバージョンの `entry_64.S` と照合することです。これにより、CPUのルール、ユーザー空間のC ABI、および特定バージョンのカーネル実装を混同することがなくなります。
