システムコールのABIと呼び出し機構の内部
システムコールが「ただの関数より重い」本当の理由を、命令レベルで腑に落とせます。syscall命令のモード遷移、レジスタ規約、vDSO、seccompまで原理から押さえます。
- 1.x86-64ではsyscall命令がMSR(LSTAR/STAR)を使い、特権チェックなしでring0へ最短遷移し、戻りアドレスとフラグをrcx/r11に退避します。
- 2.番号はrax、引数はrdi/rsi/rdx/r10/r8/r9の順で渡し、rcxとr11は破壊される——C関数のABIとは規約が異なります。
- 3.vDSOはgettimeofday等をモード遷移なしで実行し、seccompはBPFで番号と引数を検査してsyscallを許可・拒否します。
システムコールはなぜ「ただの関数」ではないのか
ユーザープログラムから read() を呼ぶと、最終的に CPU の1つの命令——x86-64 なら syscall——が実行されます。これは通常の call 命令とは根本的に違います。call は同じ特権レベル内でスタックに戻りアドレスを積んで分岐するだけですが、syscall は 特権レベルそのものを ring3 から ring0 へ遷移 させ、実行をカーネルが指定した固定の入口へ飛ばします。
なぜ専用命令が要るのか。ユーザーコードがカーネルの任意のアドレスへ自由に飛べてしまえば保護が崩壊するからです。そこで CPU は「入口を1点に固定し、そこへ入るときだけ特権を上げる」という仕組みを命令レベルで提供します。この入口の住所や遷移時の挙動を定めるのが、本稿で扱う ABI(Application Binary Interface)と呼び出し機構です。前提となるモード分離はカーネルモードとユーザーモードを参照してください。
syscall 命令によるモード遷移の内部
x86-64 の syscall 命令は、割り込みディスクリプタテーブル(IDT)を引きません。代わりに MSR(モデル固有レジスタ) に事前登録された値を直接読みます。これが古い int 0x80(ソフトウェア割り込み)より高速な理由です。関係する MSR は次のとおりです。
| MSR | 保持する内容 | syscall 時の役割 |
|---|---|---|
| LSTAR | カーネル入口のRIP | 実行をこのアドレスへ分岐させる |
| STAR | カーネル/ユーザーのCSセレクタ | CSとSSを切り替えてring0化する |
| SFMASK | クリアすべきRFLAGSビット | IF等を落として割り込みを抑止 |
syscall 実行時、CPU は次を1命令でアトミックに行います。ユーザーの RIP を rcx へ、RFLAGS を r11 へ退避し、SFMASK に従って RFLAGS の指定ビットをクリアし、STAR から CS/SS をロードして ring0 へ移り、LSTAR の値を新しい RIP とします。重要なのは、この時点で スタックポインタは切り替わらない ことです。syscall は RSP を自動では保存しません。
int 0x80 経由の割り込みゲートは CPU が TSS からカーネルスタックをロードしますが、syscall はそれを行いません。Linux はカーネル入口(entry_SYSCALL_64)の冒頭で swapgs を使い、GS ベースが指す per-CPU 領域から自分のカーネルスタックポインタを取り出して RSP を張り替えます。ユーザーの RSP はその後レジスタ退避領域へ保存されます。この一連を誤ると、ユーザー制御下のスタックでカーネルが動く致命的な穴になります。
レジスタ規約:C の ABI とは別物
呼び出し規約は System V AMD64(通常の C 関数)と 意図的にずれています。混同が多いので正確に押さえます。
| 用途 | syscall ABI | C関数 (System V) |
|---|---|---|
| 番号/戻り値 | rax | rax(戻り値のみ) |
| 第1〜3引数 | rdi, rsi, rdx | rdi, rsi, rdx |
| 第4引数 | r10 | rcx |
| 第5,6引数 | r8, r9 | r8, r9 |
| 破壊される | rcx, r11 | 呼び出し先保存以外 |
差異の核心は2点です。第一に、第4引数が rcx ではなく r10 を使います。これは syscall が rcx を戻り RIP の退避に奪うためで、引数置き場として使えないからです。第二に、rcx と r11 は呼び出し後に必ず破壊されます。glibc のラッパーはこの差を吸収し、C の rcx 渡しを r10 へ詰め替えてから syscall を実行します。
カーネルは番号 rax を上限と照合し、sys_call_table という関数ポインタ配列を引いて実体(__x64_sys_read 等)へ分岐します。戻り値は再び rax に入りますが、-4095 から -1 の範囲はエラーコードを負で表す約束で、ラッパーがこれを errno に変換します。
ユーザー側(glibc read ラッパー)
mov rax, 0 ; __NR_read
mov r10, rcx ; C の第4引数規約 → syscall 規約へ詰め替え
syscall ; rcx←RIP, r11←RFLAGS, ring0 へ
cmp rax, -4095 ; 戻り値が負のエラー域か判定
jae __set_errno ; そうなら errno へ
vDSO:モード遷移を省く高速化
gettimeofday や clock_gettime のように 値を読むだけで副作用がない 操作にまでモード遷移コストを払うのは無駄です。そこでカーネルは vDSO(virtual Dynamic Shared Object) という小さな共有ライブラリを、各プロセスのアドレス空間へ自動でマップします。
仕組みはこうです。カーネルは時刻などのデータを書いた読み取り専用ページを公開し、vDSO 内の関数はそのページをユーザーモードのまま読みます。syscall 命令を1度も実行しないので、ring 遷移もスタック切替も発生しません。アプリは vDSO 内のシンボルを普通の関数として呼ぶだけで、内部で完結するか、必要なときだけ本物の syscall へフォールバックします。
歴史的に同目的の vsyscall は固定アドレスにコードを置いたため、ASLR を弱める攻撃面になりました。vDSO は ASLR でランダム配置され、AT_SYSINFO_EHDR という補助ベクタ経由でローダに位置を伝える設計です。現在 vsyscall は互換のためのエミュレーションに縮小され、実質 vDSO に置き換わっています。
ptrace と seccomp によるフィルタリング
カーネル入口に処理が集約されているため、syscall を1点で観測・制御 できます。これを使うのが ptrace と seccomp です。
ptrace はデバッガ(gdb や strace)が使う仕組みで、トレーサが対象プロセスの syscall 入口と出口で停止(PTRACE_SYSCALL)させ、レジスタを読み書きできます。strace が引数と戻り値を表示できるのは、まさにこの入口・出口フックでレジスタを覗いているからです。
seccomp(secure computing)はより軽量で、カーネル内で BPF プログラムを走らせて syscall を選別 します。seccomp-bpf モードでは、syscall 番号・アーキテクチャ・引数レジスタの値を入力として BPF が判定し、ALLOW/ERRNO(指定エラーで拒否)/KILL(プロセス殺害)/TRACE(ptrace へ委譲)などを返します。
BPF はレジスタ内の値(スカラ)しか直接見られません。ポインタが指すメモリの中身は、検査後にユーザー側スレッドが書き換える TOCTOU(time-of-check to time-of-use)競合が起き得るため、原則として参照先までは検査しません。文字列パスのような間接データに依存したフィルタは安全に書けない、という前提を外すと容易に回避されます。
seccomp はコンテナや名前空間と cgroupsによるサンドボックスの中核で、攻撃面を「使う syscall だけ」に絞ります。Docker のデフォルトプロファイルが多数の syscall を ERRNO で塞ぐのもこの機構です。
まとめ
syscall 命令は MSR(LSTAR/STAR/SFMASK) を引いて特権チェックなしで ring0 へ最短遷移し、戻り RIP/RFLAGS を rcx/r11 に退避します。スタック切替はソフトウェアの仕事で、Linux は swapgs で per-CPU 領域からカーネルスタックを張り替えます。レジスタ規約は番号 rax・引数 rdi/rsi/rdx/r10/r8/r9 で、第4引数が r10 になる点が C の ABI と決定的に違います。vDSO はモード遷移を省いて読み取り系を高速化し、seccomp/ptrace は集約された入口で syscall を選別・観測します。土台の概念はシステムコールとカーネルで確認できます。
OS Article
システムコールのABIと呼び出し機構の内部を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
システムコール
比較で見る軸
難易度: advanced / カテゴリ: OS / タグ数: 5
導入後に効く点
番号はrax、引数はrdi/rsi/rdx/r10/r8/r9の順で渡し、rcxとr11は破壊される——C関数のABIとは規約が異なります。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- OS
- タグ数
- 5
判断チェックリスト
- 自社の用途が「システムコール / ABI」に近いか確認する。
- 強みである「x86-64ではsyscall命令がMSR(LSTAR/STAR)を使い、特権チェックなしでring0へ最短遷移し、戻りアドレスとフラグをrcx/r11に退避します。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。