Linux システムコール

最終更新: 2024-07-15 08:53:17 +0800 +0800
原文は英語で書かれています。あなたはAI翻訳版を読んでいます。

Linux システムコールは、プロセスとカーネルの間にある、アーキテクチャ固有のバイナリインターフェースを形成します。アプリケーションコードは通常、代わりに C ライブラリ API を対象にします。これは、ライブラリが型、feature-test マクロ、互換性動作、およびマシン固有のエントリシーケンスを提供するためです。

ライブラリ API とカーネル ABI #

open(path, flags) のようなソースレベルの呼び出しは、open という名前のカーネルシステムコールを実行することを保証するものではありません。glibc はこれを openat で実装したり、キャンセルチェックを追加したり、時間幅互換のエントリポイントを選択したりできます。clock_gettime のような関数は、カーネルに入らずに vDSO 内で実行できます。逆に Linux は、古い C ライブラリがラッパーを提供していなかった gettid のようなシステムコールを公開しています。

カーネル ABI は次の要素で構成されます。

  • プロセスのアーキテクチャおよび ABI に対応するシステムコール番号;
  • レジスタで表現される引数。一部の ABI では、引数の一部がスタック上に置かれます;
  • syscall、int $0x80、svc、または ecall のようなエントリ命令; および
  • 成功およびエラーを表す生の戻り規約。

<sys/syscall.h> の SYS_name 定数は、対象ビルド ABI における番号を識別します。これらはアーキテクチャ間で安定していません。x86-64 の write は 1 ですが、i386 の write は 4 です。Compat および x32 のプロセスは、同じカーネル上でも別のテーブルを使用できます。カーネルは、その ABI の syscall table を通じて番号をディスパッチし、通常は SYSCALL_DEFINE* マクロで宣言された実装を呼び出します。

レジスタ、結果、および errno #

一般的な Linux x86 エントリ ABI の 2 つは、対応関係を次のように示します。

ABIエントリ番号引数 1~6結果
x86-64syscallraxrdi, rsi, rdx, r10, r8, r9rax
i386int $0x80eaxebx, ecx, edx, esi, edi, ebpeax

x86-64 の命令は rcx と r11 を上書きします。そのため、4 番目の引数には、C 関数 ABI の rcx ではなく r10 を使用します。ほかのアーキテクチャには、異なるアラインメント規則やレジスタペア規則があります。たとえば、一部の 32 ビット ABI では、64 ビット引数を分割してアラインメントします。

x86 では、カーネルは結果レジスタの -1 から -4095 までの範囲にある負の値としてエラーを報告します。libc ラッパーは、その生の結果を -1 に変換し、正のエラー番号をスレッドローカルな errno に格納します。エラーインジケーターを別途持つアーキテクチャでは、libc ラッパーがその差異を正規化します。したがって、コードは呼び出し成功後に errno を読み取るのではなく、文書化されたラッパーの戻り値を検査する必要があります。

syscall の直接使用 #

glibc は、通常のラッパーが存在しないエントリに対して間接ラッパーを提供します。

c
#define _GNU_SOURCE
#include <sys/syscall.h>
#include <unistd.h>

long tid = syscall(SYS_gettid);
if (tid == -1) {
    /* errno describes the failure. */
}

プロトタイプは long syscall(long number, ...); です。この関数はアーキテクチャのエントリシーケンスを実行し、カーネルエラーを errno に変換しますが、可変長引数であるため型チェックは行われません。値を正確な ABI 幅にキャストし、64 ビット引数を分割する場合はアーキテクチャ固有の規則に従ってください。実行中のカーネルがその番号を実装していない場合、新しい呼び出しは ENOSYS を返すこともあります。

通常の libc ラッパーが存在する場合は、それを優先してください。直接使用すると、次の機能を迂回する可能性があるためです。

  • libc が実装する構造体および時間幅の互換性;
  • ラッパーが定義する、古いシステムコールへのフォールバック;
  • POSIX スレッドキャンセルの動作; および
  • vDSO による高速化。

インラインアセンブリを生で使用すると、レジスタ制約、破壊されるレジスタやメモリ、メモリ順序付け、およびエラー変換に関する危険が加わります。C ライブラリまたは別の ABI レベルのランタイムを実装する場合に限り、使用が正当化されます。

アーキテクチャとバージョンの移植性 #

カーネルシステムコールは、より高レベルの標準がライブラリの動作も定義していない限り、Linux 固有です。コンパイル時の SYS_name の利用可能性が証明するのは、ヘッダーが ABI を記述していることだけであり、実行中のカーネルがその呼び出しを実装していることではありません。呼び出しを実行して ENOSYS を処理することで確認し、存在する場合は意味的に正しいフォールバックを使用してください。

別のアーキテクチャから番号をコピーしたり、クロスコンパイルされたプログラムにホスト用のテーブルをハードコードしたりしないでください。境界を通過する構造体も、選択した ABI のフィールド幅、アラインメント、エンディアン、および UAPI 定義に依存します。Seccomp と audit が同じ理由でアーキテクチャトークンと番号の両方を識別するのは、このためです。

再起動、キャンセル、トレース、および Seccomp #

シグナルはブロッキング呼び出しを中断できます。ユーザー空間へ戻る間、カーネルは内部の -ERESTART* 結果を使用して呼び出しを再発行するかどうかを判断します。ユーザー空間からは通常、呼び出しが完了した状態、または -1/EINTR のいずれかが見えます。SA_RESTART、対象のシステムコール、およびデータがすでに転送されたかどうかによって結果が決まります。アプリケーションは、操作の API が再試行を安全にしている場合に限り EINTR を再試行し、再試行前に正の短い結果を処理する必要があります。

POSIX スレッドキャンセルは、カーネルシステムコールの特性ではなく libc の特性です。キャンセルポイント用の glibc ラッパーは、ブロッキング操作の前後でキャンセルを調整します。syscall() を直接呼び出すと、その調整を迂回する可能性があります。

strace と ptrace は、カーネルへのエントリと終了、引数、および生の結果を監視します。vDSO だけで完全に処理される呼び出しは、syscall stop を生成しません。トレーサーは番号、引数、または結果を変更することもできるため、トレースされた動作は独立したセキュリティ境界ではありません。

Seccomp BPF は、カーネルで実行される前に、システムコールのアーキテクチャ、番号、および生の引数値を評価します。番号空間は重複するため、堅牢な allowlist は番号だけでなくアーキテクチャトークンも検査します。フィルターはポインター引数をデリファレンスできません。また、libc のアップグレードによって、API 名から示唆されるものとは異なる基盤システムコールが選択される可能性があります。したがって Seccomp はカーネルの攻撃対象領域を最小化しますが、完全なサンドボックスを構成するものではありません。

アップストリームの syscall(2) ページにはエントリおよびレジスタの規約が記載されており、カーネルの seccomp documentation ではフィルタリングのセマンティクスが定義されています。

コメント

GitHub で閲覧・コメントする(投稿にはログインが必要です)