AArch64 (Arm v8) についてのメモ (レジスタ,関数呼び出し,システムコール)

AArch64→x86-64のバイナリ変換について調べていときのAArch64のメモ

Instruction Set

Arm v8は3つの命令セットをサポートしている。

  • A32 (=ARM)
  • T32 (=Thumb2)
  • A64

A32とT32はどちらも32bitで、これらをまとめてAArch32という。 A32とT32はMOV PC, LDR PCなどの特別な命令でモードをが切り替わる。

A64は64bitの命令セットでAArch32に対してAArch64という。

Registers

GP registers

  • r0~r31: GPR
  • w0~w31: 32bitとして利用する場合の名称
  • x0~x32: 64bitとして利用する場合の名称

Dedicated registers

  • r29: frame pointer(fp)
  • r30: link register (lr): リターンアドレス専用
  • r31: ゼロレジスタ(xzr/wzr)として使われる

Mnemonic

ldrはLoad registerの略。 [w1, 12]w1+12の意味。

ldr w0,[w1,12] 

Calling convention

aapcs64ではこのように定められている。

https://github.com/ARM-software/abi-aa/blob/main/aapcs64/aapcs64.rst#general-purpose-registers

parameter passing/return valueにつかわれるレジスタはデータ型で決まる (aapcs64, 6.9節)

以下のCプログラムをコンパイルして眺めてみる。

godbolt.org

struct S {
    char c[3000];
};

static S s;

S foo() {
    return s;
}
foo():                                // @foo()
        stp     x29, x30, [sp, #-16]!           // 16-byte Folded Spill
        mov     x29, sp
        mov     x0, x8
        mov     x2, #3000                       // =0xbb8
        adrp    x1, _ZL1s
        add     x1, x1, :lo12:_ZL1s
        bl      memcpy
        ldp     x29, x30, [sp], #16             // 16-byte Folded Reload
        ret

関数プロローグ

pc(x30)とfp(x29)の退避が行われる。

もう一つ重要なのは、fooは無引数なのにx8をパラメータとして受け取っていること。 fooはサイズが大きな構造体を返す関数であるため、callerはメモリ領域を確保してx0を介してcalleeにそのメモリアドレスを渡す。 そして、calleeがそのアドレスに結果を書き込む。

aapcs64より:

the caller shall reserve a block of memory of sufficient size and alignment to hold the result. The address of the memory block shall be passed as an additional argument to the function in x8. The callee may modify the result memory block at any point during the execution of the subroutine

本体

aarchにはcall命令はないので、そのかわりにbl (branch and linkの略かな?)を使う。 memcpyの戻り値はアドレス(8byte)なのでr0で受け渡しする。

void* memcpy (void *dstpp, const void *srcpp, size_t len);

関数エピローグ

pcとfpを復元する

System call (Linux)

glibcの実装を見てみる

/* syscall (int nr, ...)

   AArch64 system calls take between 0 and 7 arguments. On entry here nr
   is in w0 and any other system call arguments are in register x1..x7.

   For kernel entry we need to move the system call nr to x8 then
   load the remaining arguments to register. */

ENTRY (syscall)
    uxtw    x8, w0
    mov x0, x1
    mov x1, x2
    mov x2, x3
    mov x3, x4
    mov x4, x5
    mov x5, x6
    mov x6, x7
    svc 0x0
    cmn x0, #4095
    b.cs    1f
    RET
1:
    b   SYSCALL_ERROR
PSEUDO_END (syscall)

https://github.com/bminor/glibc/blob/ad05a42370fa09062ff2b450fb69905d9f407643/sysdeps/unix/sysv/linux/aarch64/syscall.S

aarchではsvc命令でシステムコールを発行する。(x64でいうsyscall) Linuxではx0~x7にシステムコールの引数。x8にシステムコールの番号を入れてからsvc 0で呼び出すことがわかる。 (SVC命令は即値を取れるが、どうやらLinuxでは使われてないっぽい。) Linuxカーネル側でも確かにレジスタがこのように使われていることが確認できる。

Chromium OS Docs - Linux System Call Table b.csはbranch if carry set (carry=1)の意味。

典型的なユーザーモード(EL0)からカーネルモード(E1)のSVC命令直後は以下のように処理が進む。

References