このページの目次

ARM64 システムコール ABI

カバー: svc 命令 → 引数レジスタ → x86-64 との違い → アトミック操作 (LDXR/STXR) → ARM64 固有の syscall → PAC/BTI 対象: ARM64 (AArch64), Linux 3.7+

概要

ARM64 (AArch64) のシステムコールは、svc #0 命令を使用して EL1(カーネルモード)に遷移します。x86-64 の syscall とは異なり、svc はレジスタの自動保存/復元を行いません。すべてのコンテキストスイッチはソフトウェアによって処理されます。ARM64 の syscall ABI はより直接的ですが、ソフトウェア側の対応が必要です。

レジスタ規約

# システムコール番号:
  x8 = syscall number (x86 の rax とは異なります!)

# 引数 (最大 6 個):
  x0 = arg1    x1 = arg2    x2 = arg3
  x3 = arg4    x4 = arg5    x5 = arg6

# 戻り値:
  x0 = 戻り値 (>=0: 成功, -errno: エラー)

# ARM64 の syscall はレジスタを破壊しません (x0 を除く)
#   → カーネルがすべてのレジスタを保存/復元します
#   x86 の rcx/r11 の破壊とは異なります

svc 命令の動作

flowchart TD
    SVC["svc #0 命令の実行"]

    SVC --> S1["① PSTATE を保存 → SPSR_EL1"]
    S1 --> S2["② 戻りアドレスを保存 → ELR_EL1<br/>(例外リンクレジスタ)"]
    S2 --> S3["③ CPU を EL1 (カーネルモード) に切り替え"]
    S3 --> S4["④ VBAR_EL1 + 0x400 にジャンプ<br/>(EL0 からの同期例外、64ビット)"]
    S4 --> S5["⑤ カーネル例外ベクタ → kernel_entry<br/>→ el0_svc ハンドラ"]

    classDef inst fill:#e3f2fd,stroke:#1565c0
    classDef step fill:#f3e5f5,stroke:#7b1fa2
    classDef done fill:#e8f5e9,stroke:#2e7d32
    class SVC inst
    class S1,S2,S3,S4 step
    class S5 done

カーネルエントリ: el0_svc

// arch/arm64/kernel/entry.S
el0_svc:
    // 1. すべてのユーザーレジスタをカーネルスタックに保存 (kernel_entry マクロ)
    kernel_entry 0

    // 2. x8 = syscall number を読み取る
    ldr x16, [tsk, #TSK_TI_FLAGS]
    tbnz x16, #TIF_SME, el0_sve_acc  // SME をチェック

    // 3. tracing かどうかをチェック (ptrace / audit)
    ldr x16, [tsk, #TSK_TI_FLAGS]
    tst x16, #_TIF_SYSCALL_WORK
    b.ne el0_svc_naked

    // 4. syscall ハンドラを呼び出す
    bl  el0_svc_common
      → invoke_syscall(regs->regs[8], regs->regs[0..5], ...)
      → sys_call_table[x8](x0, x1, x2, x3, x4, x5)

    // 5. regs->regs[0] = 戻り値
    // 6. kernel_exit 0 → すべてのレジスタを復元 → eret

x86-64 との主な違い

x86-64ARM64
syscall 命令syscallsvc #0
syscall nr レジスタraxx8
ハードウェア保存RCX←RIP, R11←RFLAGSELR_EL1, SPSR_EL1
スタック切り替え手動 (per-CPU から読み取り)手動 (SP_EL0 から読み取り)
戻り命令sysretqeret
破壊されるレジスタrcx, r11なし (すべて保存/復元)
引数レジスタrdi,rsi,rdx,r10,r8,r9x0,x1,x2,x3,x4,x5
典型的なレイテンシ~50-70c~30-50c

ARM64 固有の syscall

// ARM64 には x86 にない syscall がいくつかあります:

// getcpu (現在の CPU および NUMA ノードの取得):
getcpu(&cpu, &node, NULL);  // 通常 vDSO を介して実装され、実際の syscall は行われません

// メモリモデル関連の syscall:
mlock2(start, len, MLOCK_ONFAULT);  // x86 にもありますが、ARM での実装は異なります

// PAC (ポインター認証):
__NR_prctl PR_PAC_RESET_KEYS  // ARM64 で PAC キーをリセット

アトミック操作: LDXR / STXR

ARM64 はロック接頭辞 (x86 の LOCK CMPXCHG) に依存しません。
代わりに Load-Exclusive / Store-Exclusive ペアを使用します:

  1. LDXR x0, [x1]     // 読み取りおよび排他的アクセスとしてマーク
  2. ... x0 を変更 ...
  3. STXR w2, x0, [x1] // 書き込みを試みる → w2=0 で成功, w2=1 で失敗 (再試行)
  4. cbnz w2, retry    // 失敗 → 再試行

x86 の LOCK CMPXCHG との違い:
  バスロックが必要ないため、複数のアドレスで同時に排他的アクセスが可能
  大規模な NUMA システムでスケーラビリティに優れる

ユーザー空間での CPU 機能の取得

// ARM64: HWCAP フラグ (getauxval または /proc/self/auxv を介して)
#define HWCAP_FP       (1 << 0)   // 浮動小数点
#define HWCAP_ASIMD    (1 << 1)   // NEON/ASIMD
#define HWCAP_AES      (1 << 3)   // AES 命令
#define HWCAP_PMULL    (1 << 4)   // 多項式乗算 (GCM)
#define HWCAP_SHA1     (1 << 5)
#define HWCAP_SHA2     (1 << 6)
#define HWCAP_CRC32    (1 << 7)   // CRC32 命令
#define HWCAP_ATOMICS  (1 << 8)   // LSE アトミック操作 (ARMv8.1)
#define HWCAP_SVE      (1 << 22)  // スケーラブルベクトル拡張

unsigned long hwcap = getauxval(AT_HWCAP);
if (hwcap & HWCAP_AES) { /* AES 命令を使用 */ }

デバッグ

# システムコールテーブルの表示
cat /usr/include/asm-generic/unistd.h  # 共通テーブル
cat /usr/include/asm/unistd.h          # ARM64 固有

# strace によるトレース
strace -e trace=write,read ./a.out

# HWCAP
LD_SHOW_AUXV=1 /bin/true | grep HWCAP

参考

  • ソースコード⁠: arch/arm64/kernel/entry.S, arch/arm64/kernel/syscall.c
  • ARM64 ABI: AAPCS64 (Procedure Call Standard for ARM 64-bit)
  • LWN: "System calls on ARM64", "Pointer Authentication in the kernel"

キーワード: svc, ARM64, AArch64, ELR_EL1, SPSR_EL1, LDXR/STXR, LSE, PAC, HWCAP, NEON