ARM64 システムコール ABI

このページの目次

AArch64 の通常関数呼び出しと Linux システムコールは、どちらも x0 から引数を渡しますが、それらは同じ規約ではありません。通常関数は bl でジャンプし、AAPCS64 に従ってレジスタを割り当てます。システムコールでは、呼び出し番号を w8 に格納し svc #0 を実行し、カーネルがリクエストを処理します。svc の即値は Linux のシステムコール番号ではありません。

本稿では、Linux ネイティブの AArch64 ユーザー空間について扱います。AArch32 互換 ABI は含まれません。アプリケーションは、ターゲットプラットフォームのヘッダーファイルにある SYS_* または __NR_* を使用すべきであり、x86-64 の呼び出し番号を ARM64 に持ち込まないでください。

アプリケーションから見える規約を先に押さえる

用途AArch64 Linux 規約
カーネルへの移行svc #0
システムコール番号w8(x8 の下位 32 ビット)
最大 6 個の引数x0、x1、x2、x3、x4、x5
主要な戻り値x0
生のエラーx0 内の -errno。エラー範囲は -4095 から -1
libc 公共ラッパーのエラー通常 -1 を返し、スレッドローカル errno を設定する

同じ x0 は、エントリでは引数 1 を表し、出口では結果を表します。例えば write のエントリでは x0 はファイルディスクリプタですが、成功した出口では x0 は今回の書き込みバイト数です。C 関数 syscall() はすでに errno を処理しており、生のレジスタを直接返すだけのアセンブリではありません。syscall(2)

w0 への書き込みは上位 32 ビットをクリアします。幅は引数の型に合わせてください mov w0, #-1 x0 = 0x00000000ffffffff 32 ビットビューへの書き込み後の 64 ビットレジスタ値 sxtw x0, w0 x0 = 0xffffffffffffffff 符号付き 32 ビットの -1 を 64 ビットの -1 に拡張 カーネル引数が int の場合、低位を int として解釈します。long / ポインタの場合、完全な幅が必要です。 2つのビットパターンは、いくつかの int 引数に対して同じ効果を持ちますが、すべてのシステムコール引数で交換可能であることを意味しません。

w と x は同じレジスタの異なる幅のビュー

w レジスタへの書き込みは、対応する x レジスタの上位 32 ビットをクリアするため、mov w0, #-1 で得られる完全な x0 の値は 64 ビットの -1 ではありません。カーネル引数の型が int の場合、下位 32 ビットの全 1 はその型で -1 として解釈できます。インターフェースが符号付き 64 ビット数を要求する場合、符号拡張を省略することはできません。ポインタ、size_t、オフセット引数も、ターゲット ABI とインターフェース定義に従って準備する必要があり、一律に w レジスタにロードすることはできません。Arm AArch64 レジスタ

以下のビット演算モデルは、レジスタのビットパターンを示すだけで、カーネルを呼び出しません。また、戻り値を 32 ビットに切り捨てることで完全な 64 ビットの結果が隠蔽されることを同時に示しており、実際の生のエラー判定には x0 の幅を使用する必要があります。

MASK32 = (1 << 32) - 1
MASK64 = (1 << 64) - 1
w_minus_one = (-1) & MASK32
x_after_w_write = w_minus_one
x_after_sxtw = (w_minus_one | (~MASK32)) & MASK64
assert x_after_w_write == 0x00000000ffffffff
assert x_after_sxtw == 0xffffffffffffffff

def raw_error(bits):
    return (bits & MASK64) >= ((-4095) & MASK64)

assert raw_error(x_after_sxtw)
assert not raw_error(x_after_w_write)
assert raw_error((-22) & MASK64)
assert not raw_error(0x0000000100000000)
assert (0x0000000100000000 & MASK32) == 0

ここで x_after_w_write は 64 ビットのエラー範囲外にあるため、下位 32 ビットの 0xffffffff を見てシステムコールの失敗と判断することはできません。一方、同じ数がエラー範囲外だからといって、それが有効なアドレスであると宣言することもできません。アドレスかどうか、およびアクセス可能かどうかは、具体的な呼び出し結果とマップ条件によって決定されます。

同じ下位 32 ビットなのに、なぜ同じ戻り値ではないのか?

固定入力 -1 で拡張方式を切り替えると、16 進数のうち上位半分の部分だけが変化しますが、64 ビットの意味は 4294967295 から -1 に変わります。さらに -4096 を選択すると、符号拡張後に負になっても、[-4095, -1] の生のエラー符号化範囲には属しません。

図を準備しています
同じ下位 32 ビットなのに、なぜ同じ戻り値ではないのか?

w レジスタへの書き込みは上位 32 ビットをクリアし、符号拡張は符号ビットをコピーします。完全な 64 ビットが予約されたエラー範囲内に収まって初めて、-errno として解釈されます。

SVC は実際何を保存するのか

AArch64 EL0 → EL1:例外状態はハードウェアが保存し、汎用レジスタはカーネルが保存する ユーザーが svc #0 を実行 w8 はシステムコール番号、x0–x5 は引数 1 CPU が例外状態を記録 戻り PC → ELR_EL1;PSTATE → SPSR_EL1 2 同期例外ベクトルへ進入 EL1 スタックを選択;ESR_EL1 が SVC 例外を識別 3 Linux がレジスタを保存してディスパッチ pt_regs を形成し、トレースなどのチェックを行い、処理関数を呼び出す 4 Linux がコンテキストを復元して eret を実行 x0 に結果を持って戻り、ユーザー空間に戻る 5 図は EL1 カーネルでアーキテクチャメカニズムを説明しています。仮想化構成は、アプリケーションが使用する syscall ABI を変更しません。

EL0 から EL1 への移行を例にとると、ハードウェアは戻りアドレスとプロセッサ状態を保存し、ターゲット例外レベルのスタックを選択して例外ベクトルへ進入します。低い例外レベルからの AArch64 状態の同期例外スロットは VBAR_EL1 + 0x400 にあります。エントリコードは、例外の原因に応じて SVC、命令例外などを区別する必要があります。

ハードウェアは一度に x0–x30 をすべてメモリにプッシュしません。Linux のソフトウェアエントリは、必要な汎用レジスタ、ユーザースタックポインタなどを保存し、トレース、シグナル配信、および戻りパスの操作に使用できるコンテキストを形成します。「ハードウェアがすべての汎用レジスタを自動的に保存しない」という誤解を、「ハードウェアが何の状態も保存しない」と書き換えないでください。Arm 例外モデル

Linux v6.18 を読む際、entry.S の kernel_entry/kernel_exit から syscall.c の el0_svc_common および invoke_syscall へと追跡できます。エントリと退出には、トレース、シグナル、再スケジューリング、セキュリティ関連の作業も関与しており、いくつかのつなぎ合わせの擬似アセンブリで完全なカーネル実装を表すことはできません。エントリアセンブリ、システムコールディスパッチ

x86-64 と対照させる際は、同じ層で比較する

質問ネイティブ x86-64 Linuxネイティブ AArch64 Linux
引数 4r10(rcx を避けるため)x3
戻り位置がハードウェアによってどこに一時的に保存されるかrcxELR_EL1(EL1 を例とする)
プロセッサ状態がハードウェアによってどこに一時的に保存されるかr11SPSR_EL1(EL1 を例とする)
エントリメモリフレームの構築Linux ソフトウェアがレジスタを保存Linux ソフトウェアがレジスタを保存
例外からの復帰条件を満たせば SYSRET、それ以外は IRETERET

AArch64 には、x86 の SYSCALL が rcx/r11 を破壊するような同名の汎用レジスタの破壊は存在しませんが、それをもって「システムコールはすべてのレジスタ状態を保持する」と言うことはできません。特に拡張ベクトル状態には独立した ABI があり、SVE のシステムコール境界では高位ベクトル、述語レジスタなどについて明確な規約があり、SVE/SME 状態全体が不変であると仮定することはできません。Linux SVE ABI

パフォーマンスも表から導き出すことはできません。ページテーブルの切り替えの発生、カーネルの緩和策、CPU のモデル、周波数、および仮想化環境はすべてレイテンシに影響を与え、レジスタの配置がより整っているからといって、必ずしも速いわけではありません。

組み立て可能な完全な例

以下は、AArch64 Linux の write=64、exit=93 のみに依存します。短書き込みと EINTR を処理し、その他の失敗では終了します。ABI の観察に適していますが、汎用の非同期 I/O ループではありません。

.section .rodata
message:
    .ascii "hello\n"

.section .text
.global _start
_start:
    adrp x1, message
    add x1, x1, :lo12:message
    mov x2, #6
.Lwrite:
    mov x0, #1
    mov w8, #64
    svc #0
    cmn x0, #4              // -EINTR
    b.eq .Lwrite
    cmp x0, #0
    b.le .Lerror
    add x1, x1, x0
    subs x2, x2, x0
    b.ne .Lwrite
    mov x0, #0
    b .Lexit
.Lerror:
    mov x0, #1
.Lexit:
    mov w8, #93             // 生の exit で現在のスレッドを終了
    svc #0
.section .note.GNU-stack,"",%progbits

AArch64 Linux ではネイティブの as、ld でビルド可能です。他のアーキテクチャでは、クロスアセンブリとリンクのみが可能です:

clang --target=aarch64-linux-gnu -c syscall-arm64.S -o syscall-arm64.o
ld.lld -m aarch64elf -o syscall-arm64 syscall-arm64.o
readelf -h -l syscall-arm64

オブジェクトファイルを生成したからといって、ARM64 で実行検証されたわけではありません。実行するには、AArch64 Linux または正しく設定されたエミュレーション環境が必要です。この例には libc の起動コードがなく、exit は stdio バッファをフラッシュしません。

アセンブリのリーフ関数を C ABI と syscall ABI の両方に適合させる

前述の _start には通常の呼び出し元がありません。C からアセンブリラッパーを呼び出したい場合、関数は AAPCS64 に従って引数を受け取り戻す必要があり、かつ svc の前に Linux が必要とするレジスタを整える必要があります。以下の raw-write-arm64.S は、C の 3 つの引数を write にそのまま使用し、生の結果を直接返します。

.text
.global raw_write_arm64
.type raw_write_arm64, %function
raw_write_arm64:
    mov w8, #64
    svc #0
    ret
.size raw_write_arm64, .-raw_write_arm64
.section .note.GNU-stack,"",%progbits

対応する C の呼び出し元を raw-write-main.c として保存します:

extern long raw_write_arm64(int fd, const void *buffer, unsigned long size);
int main(void) {
    static const char message[] = "hello\n";
    const char *next = message;
    unsigned long left = sizeof message - 1;
    while (left != 0) {
        long result = raw_write_arm64(1, next, left);
        if (result == -4) continue;  /* ネイティブ Linux の EINTR */
        if (result <= 0) return 1;
        next += result;
        left -= (unsigned long)result;
    }
    return 0;
}

ここで C 呼び出しは、fd、buffer、size を x0–x2 に対応する引数位置に配置し、アセンブリは w8 を設定するだけです。bl で確立された戻りアドレスは x30 にあります。svc の例外状態は例外レベルレジスタに保存され、Linux がユーザー空間に戻った後、このリーフラッパーは ret で C の呼び出し元に復帰できます。

もし将来、ラッパーに bl ヘルパーを新たに追加する場合、独自の戻りアドレスを保存・復元する必要があり、かつ呼び出し先保存レジスタの規則に従う必要があります。AAPCS64 の公共インターフェースでは、SP は 16 バイト境界にアラインされている必要があります。8 バイト値のみをプッシュする書き方を模倣して、後続の呼び出しが間違ったスタックアラインメントで実行されないようにしてください。AAPCS64 サブルーチン呼び出し

clang --target=aarch64-linux-gnu -c raw-write-arm64.S -o raw-write-arm64.o
clang --target=aarch64-linux-gnu -ffreestanding -c raw-write-main.c -o raw-write-main.o
readelf -sW raw-write-arm64.o
readelf -rW raw-write-main.o

上記の 2 回のコンパイルには C 標準ライブラリのヘッダーファイルは不要で、他のアーキテクチャでシンボルと呼び出しリロケーションを確認するのに適しています。これらは C ランタイムのリンクを完了していません。実際のランタイムでは、AArch64 Linux でネイティブツールチェーンを使用して cc raw-write-main.c raw-write-arm64.S -o raw-write-demo を実行し、その後 ./raw-write-demo を実行します。予想される出力は hello です。

検証レベル何を示せるか何を代替できないか
クロスアセンブリ命令とリロケーション構文が許容されるカーネルが今回の引数を受け入れるかどうか
ELF ヘッダー / シンボル / リロケーションターゲットは AArch64 で、呼び出し参照が生成済みランタイム時のレジスタ内容が正しいこと
ネイティブまたはエミュレーション環境での実行現在のパスでの出力と終了ステータスすべてのエラーパス、実機でのパフォーマンス
ターゲット機での負荷・障害テストシグナル、短書き込みなどの条件下での動作他のカーネル構成における一般的な保証

CPU 機能とシステムコール番号は別の問題である

getcpu、mlock2 は ARM64 固有のシステムコールではありません。PR_PAC_RESET_KEYS は prctl の一種の操作であり、独立した syscall 番号ではありません。PAC、BTI、SVE、LSE が使用可能かどうかは、プロセッサの能力とカーネルが露出する ABI を組み合わせて判断する必要があります。

AArch64 Linux の C プログラムでは、システムヘッダーファイルのビット定義を使用し、HWCAP 定数を手書きでコピーしないでください:

#include <sys/auxv.h>
#include <asm/hwcap.h>

int has_lse_atomics(void)
{
    return (getauxval(AT_HWCAP) & HWCAP_ATOMICS) != 0;
}

HWCAP は、ユーザー空間で使用できる能力をカーネルが記述したものであり、CPU モデル文字列に基づいて推測するよりも信頼性が高いです。それはシステムコールが実装されたかどうかのリストではありません。Linux ELF HWCAP

LDXR/STXR は、ユーザー空間の原子アルゴリズムで使用される可能性のある命令ですが、カーネルに入るたびに必要というわけではありません。取得/解放セマンティクスを持つ LDAXR/STLXR と区別する必要があります。LSE は別の原子命令グループを提供します。原子性、メモリ順序、および待ち合わせと唤醒は 3 つの異なる問題であり、「x86 の lock プレフィックスがない」ことから NUMA 拡張性を導き出すことはできません。実際の同期がどのようにカーネルに入るかは、futex を参照してください。