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
Mnemonic
ldrはLoad registerの略。
[w1, 12]
はw1+12
の意味。
ldr w0,[w1,12]
Calling convention
aapcs64ではこのように定められている。
parameter passing/return valueにつかわれるレジスタはデータ型で決まる (aapcs64, 6.9節)
例
以下のCプログラムをコンパイルして眺めてみる。
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)
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命令直後は以下のように処理が進む。
- SVCによって権限昇格。(EL0->EL1)
- ESR_EL1(syndrome register)にはSVCによって例外が起きたことを表す値が格納される
- VBAR_EL1レジスタで指定された例外ベクタテーブルを参照する
- 例外ベクタテーブルの固定オフセット(IRQとFIQで異なる)に置かれたアドレスを例外ハンドラとして呼び出す。
- 例外ハンドラ中のsyscall(svc)ハンドラを呼び出す
References
- https://armkeil.blob.core.windows.net/developer/Files/pdf/graphics-and-multimedia/ARMv8_InstructionSetOverview.pdf
- https://wiki.cdot.senecacollege.ca/wiki/AArch64_Register_and_Instruction_Quick_Start
- Arm A64 Instruction Set Architecture
- https://zenn.dev/hidenori3/articles/c9053a76be641c
- aapcs64: https://github.com/ARM-software/abi-aa/blob/main/aapcs64/aapcs64.rst#the-base-procedure-call-standard
- aaelf64: https://github.com/ARM-software/abi-aa/releases/download/2023Q3/aaelf64.pdf
- https://stackoverflow.com/questions/76567156/how-does-arm-svc-instruction-works
- https://developer.arm.com/documentation/100933/0100/AArch64-exception-vector-table