ASM-ARM64

ここではAArch64のプロシージャ呼び出し規約(AAPCS64)における、整数・ポインタ引数の基本を扱う。macOSにはAAPCS64を基にした固有規則もあるが、この範囲では共通だ。 第1〜8引数はx0〜x7(32ビットの値ならw0〜w7)に渡り、9個目以降は通常スタック側に渡る。 浮動小数点値やベクトル値は主にv0〜v7を使う。構造体や可変長引数には別の規則があり、「ARM64なら必ず8個」とは限らない。

Cのコードで確認

#include <stdio.h>

// 引数が 8 個ある関数
int add_all(int a, int b, int c, int d, int e, int f, int g, int h) {
    int sum = a + b + c + d + e + f + g + h;
    return sum; // breakpoint
}

int main(void) {
    int result = add_all(1, 2, 3, 4, 5, 6, 7, 8);
    printf("Result: %d\n", result);
    return 0;
}

macOSではApple Clangで最適化をオフ、デバッグ情報をオンにしてコンパイルする。 環境によってはgccがclangの互換名だが、ここではclangを使う。

$ clang -g -O0 demo.c -o demo

デバッガにはmacOS標準のLLDBを使う。

$ lldb ./demo

(lldb) b add_all
(lldb) r
(lldb) b add_all
Breakpoint 1: where = demo`add_all + 36 at demo.c:5:15, address = 0x0000000100000484
(lldb) r
Process 5137 launched: '/path/to/demo' (arm64)
Process 5137 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x0000000100000484 demo`add_all(a=1, b=2, c=3, d=4, e=5, f=6, g=7, h=8) at demo.c:5:15
   2
   3    // 引数が 8 個ある関数
   4    int add_all(int a, int b, int c, int d, int e, int f, int g, int h) {
-> 5        int sum = a + b + c + d + e + f + g + h;
   6        return sum; // breakpoint
   7    }
   8
Target 0: (demo) stopped.

ブレークポイントを置き、disassembleで生成された命令を確認する。Apple SiliconはAArch64なので、命令はx86-64と異なる。

Apple Silicon向けのコンパイル例。-O0では、引数をいったんスタックへ退避する。

(lldb) disassemble
demo`add_all:
    0x100000460 <+0>:   sub    sp, sp, #0x30
    0x100000464 <+4>:   str    w0, [sp, #0x2c]
    0x100000468 <+8>:   str    w1, [sp, #0x28]
    0x10000046c <+12>:  str    w2, [sp, #0x24]
    0x100000470 <+16>:  str    w3, [sp, #0x20]
    0x100000474 <+20>:  str    w4, [sp, #0x1c]
    0x100000478 <+24>:  str    w5, [sp, #0x18]
    0x10000047c <+28>:  str    w6, [sp, #0x14]
    0x100000480 <+32>:  str    w7, [sp, #0x10]
->  0x100000484 <+36>:  ldr    w8, [sp, #0x2c]
    0x100000488 <+40>:  ldr    w9, [sp, #0x28]
    0x10000048c <+44>:  add    w8, w8, w9
    0x100000490 <+48>:  ldr    w9, [sp, #0x24]
    0x100000494 <+52>:  add    w8, w8, w9
    0x100000498 <+56>:  ldr    w9, [sp, #0x20]
    0x10000049c <+60>:  add    w8, w8, w9
    0x1000004a0 <+64>:  ldr    w9, [sp, #0x1c]
    0x1000004a4 <+68>:  add    w8, w8, w9
    0x1000004a8 <+72>:  ldr    w9, [sp, #0x18]
    0x1000004ac <+76>:  add    w8, w8, w9
    0x1000004b0 <+80>:  ldr    w9, [sp, #0x14]
    0x1000004b4 <+84>:  add    w8, w8, w9
    0x1000004b8 <+88>:  ldr    w9, [sp, #0x10]
    0x1000004bc <+92>:  add    w8, w8, w9
    0x1000004c0 <+96>:  str    w8, [sp, #0xc]
    0x1000004c4 <+100>: ldr    w0, [sp, #0xc]
    0x1000004c8 <+104>: add    sp, sp, #0x30
    0x1000004cc <+108>: ret

メモリ確保の話

(高)  0x100000000  [ 誰かが使っている領域 ]
 ^                 [ 誰かが使っている領域 ]
 |    0x00001030   SP -----> [ 呼び出し元の位置 ]  ← 高い
 |                           | (引き算で移動)
 |                           v
(低)  0x00001000   SP -----> [ 新しい位置 ]        ← 低い

変数用の領域を確保するため、sp(stack pointer)を引き下げ、ここまでスタックを使うと示す。

sub sp, sp, #0x30

subはsubtract(引き算)を表す。 sub [命令先(Destination Register)], [元データ(Source Register)], [即値(Immediate)]なので、ここではsp = sp - 48(0x30)と同じ意味だ。 a〜hとsumの9個のintは合計36B。スタック領域はABIの整列条件も満たす必要がある。

なぜ48B確保するのか

Q. #0x30(48B)確保したのに36Bしか使っていない?

A. AAPCS64では、スタックポインタspを16バイト境界に保つ。 この例では、36Bのローカル領域を16の倍数に切り上げ、48B(36Bのデータ+12Bの余白)を確保する。48BはARM64の一般則ではなく、このコンパイル例のフレーム配置による。 なお、AArch64の非整列アクセスがすべてAlignment Faultになるわけではない。spの整列条件とスタックフレームのサイズを混同しないことがポイントだ。

データの書き込み

str w0,[sp, #0x2c]
str w1,[sp, #0x28]
str w2,[sp, #0x24]
...

strはstore(書き込み)を表す。 str [元データとなるレジスタ(Source Register)], [書き込み先]で、書き込み先にはアドレッシングモードを使う。 [基準となるアドレス(Base Register), 基準からの距離(Immediate Offset)]の形式だ。

str w0, [sp, #0x2c]なら、sp + 0x2c(44)のアドレスにw0の32ビット(4B)を保存する。intが4Bなので、オフセットも4ずつ移動する。

データの読み込み

w0〜w7をスタックに書き込んだら、次に計算する。

ldr w8, [sp, #0x2c]
ldr w9, [sp, #0x28]

ldrはLoad(読み込み)で、strの逆だ。 ldr [書き込み先のレジスタ(Destination Register)], [読み込み元]の形式だ。読み込みにもベースとオフセットを使う。

計算する

add w8, w8, w9

addは足し算だ。 add [演算結果の格納先レジスタ(Destination Register)], [加える値のレジスタ1(First Source Register)], [加える値のレジスタ2(Second Source Register)]の形式だ。 w8 = w8 + w9と同じなので、以降はw9にspから値をロードし、w8に加える作業が続く。

返り値

AArch64では、32ビット整数の返り値をw0(64ビット値ならx0)で返す。計算後、str w8, [sp, #0xc]でsumに移し、ldr w0, [sp, #0xc]でw0に戻す。 add sp, sp, #0x30でスタック領域を解放し、retで呼び出し元へ戻る。w0の値が返り値になる。

参考資料