こんにちは。永和システムマネジメントの内角低め担当、はたけやまです。
仕事で使っている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個の汎用レジスタを持ちます。w0
や w30
のように w
のプリフィックスをつけると32ビットレジスタとして扱われ、x0
や x30
のように 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行目:
w8
とw9
を加算した結果を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コンパイラが出力したアセンブリコードを参照する手法は「大熱血!アセンブラ入門」が詳しいので、興味がある方はぜひご一読を🔥🔥🔥