ESM アジャイル事業部 開発者ブログ

永和システムマネジメント アジャイル事業部の開発者ブログです。

Apple M1で遊ぼう🍎

こんにちは。永和システムマネジメントの内角低め担当、はたけやまです。

仕事で使っているPCが、Intel Mac から Apple M1 を搭載した M1 Mac になりました。Apple M1 は Apple が開発した CPU で、64ビット ARM アーキテクチャ「AArch64」を採用しています。

せっかくの Apple M1 搭載機が手に入ったことですし、M1 Mac 上で ARM アセンブリプログラミングして遊んでみようと思います。

Hello World!!

まずは定番の Hello World を画面に出力する所から始めたいところですが、文字を画面に出力するだけの処理でもアセンブリプログラムには大仕事です。そこでもっと簡単な、終了ステータスに「86(ハロー)」を返すだけのプログラムを作成してみます。

とは言うものの、私もARM のアセンブリプログラミングには不慣れなので、何か参考となるサンプルが欲しいところです。こういう時はCのソースからアセンブリコードを出力するのがおすすめです。

以下のようなCのプログラムを用意して、

// hello.c
int main() {
    // 終了ステータスに「86(ハロー)」を返す
    return 86;
}

以下のコマンドを入力すると、アセンブリのソースファイルが hello.s という名前で出力されます。

$ gcc -S hello.c

出力されたアセンブリコードはこんな感じです(行番号を付与してます)

     1       .section    __TEXT,__text,regular,pure_instructions
     2      .build_version macos, 13, 0 sdk_version 13, 3
     3      .globl  _main                           ; -- Begin function main
     4      .p2align    2
     5  _main:                                  ; @main
     6      .cfi_startproc
     7  ; %bb.0:
     8      sub sp, sp, #16
     9      .cfi_def_cfa_offset 16
    10      str wzr, [sp, #12]
    11      mov w0, #86
    12      add sp, sp, #16
    13      ret
    14      .cfi_endproc
    15                                          ; -- End function
    16  .subsections_via_symbols

なんだか良く分からないアセンブリコードも出力されてますが、分からない部分は無視して雰囲気で読んでいきます。

  • 1行目:以下のコードがテキストセクション、実行可能な命令列であることを定義
  • 3行目:_main 関数が他のファイルから参照可能なグローバル関数であることを定義
  • 5行目: _main 関数がここからスタート
  • 8行目〜10行目:スタックフレームを確保後、何かを格納している?(今回は無視しても良さそう)
  • 11行目:w0 レジスタへ「86」を格納している。w0 レジスタへ格納した値が _main 関数の返り値として使われているっぽい
  • 12行目:スタックフレームを解放
  • 13行目:関数からリターン

出力されたアセンブリコードを参考にして、終了ステータスに「ハロー(86)」を返すプログラムを作成します。

// my-hello.S
// 終了ステータスに「ハロー(86)」を返す
    .section __TEXT,__text
    .globl _main
    .p2align 2
_main:
    mov w0, #86
    ret

アセンブリコードの拡張子は *.s でも *.S でもどちらでも大丈夫です。*.S だとプリプロセッサありで*.s だとプリプロセッサなしになります。今回は // でコメント行を記述したいので、プリプロセッサありの *.S を採用しています。

コードをビルドして実行します。終了ステータス「86(ハロー)」が返されていればOKです。

$ gcc my-hello.S
$ ./a.out
$ echo $?
86

【NOTE】AArch64のレジスタ

AArch64は0番目から30番目までの31個の汎用レジスタを持ちます。w0w30 のように w のプリフィックスをつけると32ビットレジスタとして扱われ、x0x30 のように x のプリフィックスをつけると64ビットレジスタとして扱われます。

また、特別な用途を持つ以下のレジスタも持ちます。

  • sp レジスタ:スタックポインタ
  • pc レジスタ:プログラムカウンタ
  • xzr wzr レジスタ:常に0を返すゼロレジスタ
  • (その他制御レジスタがいくつか)

関数の呼び出し

次はアセンブリ内で関数を呼び出してみます。Cで簡単な関数呼び出しのコードを書いて、出力されるアセンブリコードを見てみます。

// add.c
int add(int n, int m) {
    return n + m;
}
 
int main() {
    return add(10, 20);
}

出力されたアセンブリコードはこんな感じ。

     1       .section    __TEXT,__text,regular,pure_instructions
     2      .build_version macos, 13, 0 sdk_version 13, 3
     3      .globl  _add                            ; -- Begin function add
     4      .p2align    2
     5  _add:                                   ; @add
     6      .cfi_startproc
     7  ; %bb.0:
     8      sub sp, sp, #16
     9      .cfi_def_cfa_offset 16
    10      str w0, [sp, #12]
    11      str w1, [sp, #8]
    12      ldr w8, [sp, #12]
    13      ldr w9, [sp, #8]
    14      add w0, w8, w9
    15      add sp, sp, #16
    16      ret
    17      .cfi_endproc
    18                                          ; -- End function
    19      .globl  _main                           ; -- Begin function main
    20      .p2align    2
    21  _main:                                  ; @main
    22      .cfi_startproc
    23  ; %bb.0:
    24      sub sp, sp, #32
    25      .cfi_def_cfa_offset 32
    26      stp x29, x30, [sp, #16]             ; 16-byte Folded Spill
    27      add x29, sp, #16
    28      .cfi_def_cfa w29, 16
    29      .cfi_offset w30, -8
    30      .cfi_offset w29, -16
    31      stur    wzr, [x29, #-4]
    32      mov w0, #10
    33      mov w1, #20
    34      bl  _add
    35      ldp x29, x30, [sp, #16]             ; 16-byte Folded Reload
    36      add sp, sp, #32
    37      ret
    38      .cfi_endproc
    39                                          ; -- End function
    40  .subsections_via_symbols

今回もよく分からないところは読み飛ばして雰囲気で読んでいきます。

  • 5行目:_add 関数の開始位置
  • 8行目:16バイト分のスタックフレームを作成
  • 10行目:w0 レジスタに渡されたひとつ目の引数をスタックフレームに格納
  • 11行目:w1 レジスタに渡されたふたつ目の引数をスタックフレームに格納
  • 12行目:スタックフレームに格納されたひとつ目の引数の値を w8 レジスタにロード
  • 13行目:スタックフレームに格納されたふたつ目の引数の値を w9 レジスタにロード
  • 14行目:w8w9 を加算した結果を w0 レジスタへ格納。これが _add 関数の返り値となるっぽい
  • 15行目:スタックフレームを解放
  • 16行目:関数からリターン
  • 21行目:_main 関数の開始位置
  • 24行目:32バイト分のスタックフレームを作成
  • 26行目:呼び出し元のリンクレジスタ(x30)とフレームポインタ(x29)の値をスタックフレームへ退避
  • 27行目〜31行目:何かやってるぽいけど使われてなさげ。何かしらの定型コードか?
  • 32行目:w0 レジスタへ「10」をロード。ひとつ目の引数として使われてるっぽい
  • 33行目:w1 レジスタへ「20」をロード。ふたつ目の引数として使われてるっぽい
  • 34行目:_add 関数の呼び出し
  • 35行目:スタックフレームへ退避していた呼び出し元のリンクレジスタ(x30)とフレームポインタ(x29)の値を復帰
  • 36行目:スタックフレームを解放
  • 37行目:関数からリターン

これを参考に、同じ処理をアセンブリで書いてみます。

// my-add.S

    .section __TEXT,__text
    .globl _add
    .p2align 2
_add:
    // 返り値(w0) = 第一引数(w0) + 第二引数(w1)
    add w0, w0, w1
    ret

    .globl _main
_main:
    // リンクレジスタとフレームポインタの値をスタックへ退避
    sub sp, sp, #16
    stp x29, x30, [sp, #0]

    // w0 = _add(w0, w1)
    mov w0, #10
    mov w1, #20
    bl _add

    // リンクレジスタとフレームポインタの値をスタックから復元
    ldp x29, x30, [sp, #0]
    
    // return w0
    ret

ビルドして実行してみます。いい感じに動いてそうです。

$ gcc my-add.S     
$ ./a.out
$ echo $?
30

【NOTE】リンクレジスタとは?

リンクレジスタとは関数の呼び出し元に戻るためのアドレスを格納する特別なレジスタです。AArcht64 では x30 レジスタがリンクレジスタとして使われます。

bl 命令は関数呼び出し時に戻り先のアドレスをリンクレジスタへ格納し、ret 命令はリンクレジスタへ格納されたアドレスへを利用して呼び出し元へ戻ります。

関数の中で別の関数を呼び出す場合は「現在のリンクレジスタの値を退避 → 関数を呼び出し → 呼び出した関数から戻ってくる → リンクレジスタの値を復元」を行う必要があります。

条件分岐の書き方

次は条件分岐を行なってみます。Cで簡単な条件分岐のコードを書いて、出力されるアセンブリコードを見てみます。

// sum.c
// 1から10までの和を返す
int main() {
    int n = 10;
    int sum = 0;
    while (n > 0) {
        sum += n;
        n--;
    }
    return sum;
}

出力されたアセンブリコードはこちら。

 .section    __TEXT,__text,regular,pure_instructions
    .build_version macos, 13, 0 sdk_version 13, 3
    .globl  _main                           ; -- Begin function main
    .p2align    2
_main:                                  ; @main
    .cfi_startproc
; %bb.0:
    sub sp, sp, #16
    .cfi_def_cfa_offset 16
    str wzr, [sp, #12]
    mov w8, #10
    str w8, [sp, #8]
    str wzr, [sp, #4]
    b   LBB0_1
LBB0_1:                                 ; =>This Inner Loop Header: Depth=1
    ldr w8, [sp, #8]
    subs    w8, w8, #0
    cset    w8, le
    tbnz    w8, #0, LBB0_3
    b   LBB0_2
LBB0_2:                                 ;   in Loop: Header=BB0_1 Depth=1
    ldr w9, [sp, #8]
    ldr w8, [sp, #4]
    add w8, w8, w9
    str w8, [sp, #4]
    ldr w8, [sp, #8]
    subs    w8, w8, #1
    str w8, [sp, #8]
    b   LBB0_1
LBB0_3:
    ldr w0, [sp, #4]
    add sp, sp, #16
    ret
    .cfi_endproc
                                        ; -- End function
.subsections_via_symbols

ちょっと分かりづらいのですが、以下のような手順で条件分岐を行なっているようです。

  • subs wzr, w8, #0 で「w8レジスタの値から0を引いた結果」を NZCV レジスタという制御レジスタへ格納する
  • cset w9, eq で「w8 == 0 か否か」をw9レジスタへ格納する(真の場合は1が、偽の場合は0がセットされる)
  • tbnz w9, #0, BREAK で、w9 へ格納した値が1(真)の場合は BREAK へ飛ぶ

出力されたアセンブリを参考に、同じ処理を書いてみます。

// my-sum.S

    .section __TEXT,__text
    .p2align 2
    .globl _main
_main:
    mov w8, #10 // n = 10
    mov w0, #0  // sum = 0
LOOP:
    // n と 0 を比較して...
    subs wzr, w8, #0
    // n == 0 なら BREAK へ飛ぶ
    cset w9, eq
    tbnz w9, #0, BREAK
    // sum = sum + n
    add w0, w0, w8
    // n = n - 1
    sub w8, w8, #1
    b LOOP
BREAK:
    ret

ビルドして実行した結果はこちら。正しく動いてそうです。

$ gcc my-sum.S
$ ./a.out
$ echo $?
55

【NOTE】cmp 命令と beq 命令を使った条件分岐

先ほどは subs 命令と cset 命令と tbnz 命令を使って条件分岐を行いましたが、cmp 命令と beq 命令を使って条件分岐を行うこともできます。こちらの方が少し可読性が高いかも。

    .section __TEXT,__text
    .p2align 2
    .globl _main
_main:
    mov w8, #10 // n = 10
    mov w0, #0  // sum = 0
LOOP:
    // n と 0 を比較して...
    cmp w8, #0
    // n == 0 なら BREAK へ飛ぶ
    beq BREAK
    // sum = sum + n
    add w0, w0, w8
    // n = n - 1
    sub w8, w8, #1
    b LOOP
BREAK:
    ret

フィボナッチ数を計算

AArch64アセンブリの書き方がだいたい分かってきたので、フィボナッチ数を計算するプログラムを作成してみます。

// my-fib.S

    .section __TEXT,__text
    .p2align 2

// fib
// n番目のフィボナッチ数を返す
    .globl _fib
_fib:
    // スタックフレームの確保
    sub sp, sp, #32
    // リンクレジスタとフレームポインタを退避
    stp x29, x30, [sp, #16]

    // n をスタックへ保存
    str w0, [sp, #0]

    // n < 2 か否かを判定
    cmp w0, #2
    blt FIB_LT_2
    b FIB_GE_2
FIB_LT_2:
    // n < 2 の場合は n をそのまま返す
    b FIB_FIN
FIB_GE_2:
    // f(n-1) を計算
    ldr w0, [sp, #0]
    sub w0, w0, #1
    bl _fib

    // f(n-1) の結果をスタックへ保存
    str w0, [sp, #4]

    // f(n-2) を計算
    ldr w0, [sp, #0]
    sub w0, w0, #2
    bl _fib

    // f(n-1) + f(n-2) の結果を w0 へセット
    ldr w9, [sp, #4]
    add w0, w0, w9
FIB_FIN:
    // 退避していたリンクレジスタとスタックフレームを復帰
    ldp x29, x30, [sp, #16]
    // スタックフレームを解放
    add sp, sp, #32
    ret

// main
    .globl _main
_main:
    sub sp, sp, #16
    stp x29, x30, [sp, #0]

    // 10番目のフィボナッチ数を計算
    mov w0, #10
    bl _fib

    ldp x29, x30, [sp, #0]
    add sp, sp, #16
    ret

ビルドして実行してみます。終了ステータスに10番目のフィボナッチ数である「55」が返れば成功です。

$ gcc my-fib.S
$ ./a.out
$ echo $?
55

まとめ

以上、Cコンパイラの出力するアセンブリコードを頼りにAArch64アセンブリで簡単なプログラムを書いてみました。皆さんも今年の夏休みは海や山でAArch64アセンブリプログラムなんていかがですか?🍉

参考書籍

今回紹介した、Cコンパイラが出力したアセンブリコードを参照する手法は「大熱血!アセンブラ入門」が詳しいので、興味がある方はぜひご一読を🔥🔥🔥