M1 Mac でAssembly を動かす
息抜きで見ていた動画の内容をM1 Mac に適用した内容を紹介します。
ソースコード
以下にあげるのが、255より小さいフィボナッチ数列 fibonacci number を無限ループで表示するC言語のソースコードです。
※わざわざ無限ループを入れてるのは、ただそれに対応するアセンブリを紹介するためです。
C言語
#include <stdio.h>
int main(void) {
int x, y, z;
while (1) {
x = 0;
y = 1;
do {
printf("%d\n", x);
z = x + y;
x = y;
y = z;
} while (x < 255);
}
}
機械語の実行部分のみを表示するコマンド
% gcc -o fib fib.c
% otool -tv fib
上記のコマンドで表示されるのは、あくまでもコンパイルされたプログラムの実行部分だけなので、この出力結果の機械語だけで同じ処理を再現することはできません。
Cのプログラムからアセンブリを出力する
% gcc -S fib.c
アセンブリを実行する
% as -o fib.o fib.s
% gcc -o fib fib.o
% ./fib
オブジェクトファイルを作り
①とリンクして実行ファイルを作り
実行
私の環境では、ldのコマンドを使うと以下のようなエラーが出たので、色々設定するよりもこっちのコマンドを使うことにしました。
% ld -o fib fib.o
Undefined symbols for architecture arm64:
"_printf", referenced from:
_main in fib.o
ld: symbol(s) not found for architecture arm64
C言語とx86_64の機械語を比較する
この記事の冒頭で紹介した動画で説明されているアセンブラ言語 Assembly はx86(32bit)アーキテクチャとx86_64(64bit)アーキテクチャ用です。
それに対し、M1 Mac はarmアーキテクチャであるため、armアーキテクチャ用のアセンブラ言語を書く必要があります。
この記事の後半でarmアーキテクチャ用のアセンブリを解説しています。
因みに、x86アーキテクチャとx86_64アーキテクチャには書き方が微妙に違う2つの構文があります。
「違う構文」といっても流儀が違うという意味で、意味は同じです。
レジスタ名に%をつけるAT&T構文と、%をつけないIntel構文です。
この動画では%rbpや%rspなどと、レジスターに%をつけているのでAT&T構文を使っていることがわかります。
全体のx86の機械語
fib:
(TEXT, text) section
_main:
0000000100000f20 pushq %rbp
0000000100000f21 movq %rsp, %rbp
0000000100000f24 subq $0x20, %rsp
0000000100000f28 movl $0x0, -0x4(%rbp)
0000000100000f2f movl $0x0, -0x8(%rbp)
0000000100000f36 movl $0x1, -0xc(%rbp)
0000000100000f3d leaq 0x56(%rip), %rdi
0000000100000f44 movl -0x8(%rbp), %esi
0000000100000f47 movb $0x0, %al
0000000100000f49 callq 0x100000f78
0000000100000f4e movl -0x8(%rbp), %esi
0000000100000f51 addl -0xc(%rbp), %esi
0000000100000f54 movl %esi, -0x10(%rbp)
0000000100000f57 movl -0xc(%rbp), %esi
0000000100000f5a movl %esi, -0x8(%rbp)
0000000100000f5d movl -0x10(%rbp), %esi
0000000100000f60 movl %esi, -0xc(%rbp)
0000000100000f63 movl %eax, -0x14(%rbp)
0000000100000f66 cmpl $0xff, -0x8(%rbp)
0000000100000f6d jl 0x100000f3d
0000000100000f73 jmp 0x100000f2f
setup
pushq %rbp
movq %rsp %rbp
subq $0x20, %rsp
movl $0x0, -0x4(%rbp)
初めの数行は、ただのプログラムを実行するための準備であり、C言語で書いたソースコードに対応するところはないのだそう。
x = 0; y = 0;
movl $0x0, -0x8(%rbp)
movl $0x1, -0xc(%rbp)
movl(move long) は第一引数(1行目は0、2行目は1)を第二引数に渡されたレジスターに格納するコマンド。
rbp はスタックのベースポインターであり、そこからのオフセット量が0x8や0xc($$12_{10}$$)と記されている。
つまり、メモリ上にあるスタックのベースポインターから8バイトオフセットされた場所にxの初期値値(0)をおき、そこから4バイトオフセットされた場所にyの初期値(1)を置いたということになる。
printf
leaq 0x56(%rip), %rdi
movl -0x8(%rbp), %esi
movb $0x0, %al
callq 0x100000f78
leaq 0x56(%rip), %rdi: \nをセット
movl -0x8(%rbp), %esi: xをセット
なんかして(動画でもそう言ってた)
callq 0x100000f78: 呼び出し。0x100000f78はちょうど添付した機械語の行の下にある
z = x + y;
movl -0x8(%rbp), %esi
addl -0xc(%rbp), %esi
movl %esi, -0x10(%rbp)
rbpから8バイトオフセットされたメモリの値(xの値)をesiレジスターに格納
rbpから12バイトオフセットされたメモリの値(yの値)をesiレジスターに足す
その結果を、rbpから16バイトオフセットされたメモリ(z)に格納
x = y; y = x;
movl -0xc(%rbp), %esi
movl %esi, -0x8(%rbp)
movl -0x10(%rbp), %esi
movl %esi, -0xc(%rbp)
yの値を一度esiレジスターに格納してから、xへコピー
zの値を一度esiレジスターに格納してから、yへコピー
movl %eax, -0x14(%rbp)
この文は、デバッガーがデバッグ用にprintfの結果を格納しているだけのようだ。YouTube のコメントありがとう。
while (x < 255)
cmpl $0xff, -0x8(%rbp)
jl 0x100000f3d
cmpl(compare long): 第一引数(255)と第二引数(xの値)を比較する
jl(jump if less than): 比較結果が小さかったら、指定されたアドレス(printfの挙動が書かれた場所)にジャンプする
while(1)
jmp 0x100000f2f
jmp: この場所を通ったときは必ず指定された場所(x = 0の場所)にジャンプする
arm のAssemblyと比較する
全体像
fib:
(__TEXT,__text) section
_main:
0000000100003f30 sub sp, sp, #0x30
0000000100003f34 stp x29, x30, [sp, #0x20]
0000000100003f38 add x29, sp, #0x20
0000000100003f3c stur wzr, [x29, #-0x4]
0000000100003f40 b 0x100003f44
0000000100003f44 stur wzr, [x29, #-0x8]
0000000100003f48 mov w8, #0x1
0000000100003f4c stur w8, [x29, #-0xc]
0000000100003f50 b 0x100003f54
0000000100003f54 ldur w9, [x29, #-0x8]
0000000100003f58 mov x8, x9
0000000100003f5c mov x9, sp
0000000100003f60 str x8, [x9]
0000000100003f64 adrp x0, 0 ; 0x100003000
0000000100003f68 add x0, x0, #0xfb4 ; literal pool for: "%d\n"
0000000100003f6c bl 0x100003fa8 ; symbol stub for: _printf
0000000100003f70 ldur w8, [x29, #-0x8]
0000000100003f74 ldur w9, [x29, #-0xc]
0000000100003f78 add w8, w8, w9
0000000100003f7c str w8, [sp, #0x10]
0000000100003f80 ldur w8, [x29, #-0xc]
0000000100003f84 stur w8, [x29, #-0x8]
0000000100003f88 ldr w8, [sp, #0x10]
0000000100003f8c stur w8, [x29, #-0xc]
0000000100003f90 b 0x100003f94
0000000100003f94 ldur w8, [x29, #-0x8]
0000000100003f98 subs w8, w8, #0xff
0000000100003f9c b.lt 0x100003f54
0000000100003fa0 b 0x100003fa4
0000000100003fa4 b 0x100003f44
一見無駄なb(ジャンプ)のコマンドが所々に入っているように見えるが、C言語で書かれたプログラム1行(もしくは同じようなことを行うプログラム数行)ごとの区切りとなっていることが後々わかる。
setup
sub sp, sp, #0x30
stp x29, x30, [sp, #0x20]
add x29, sp, #0x20
stur wzr, [x29, #-0x4]
b 0x100003f44
基本的におこなっていることはプログラムを実行するためのセットアップなので、これから出てくる命令やレジスタだけを説明する。
sp:スタックポインターを表すレジスター。sub:引き算を行う命令
$$sp = sp - 48_{10}$$という意味x29レジスターに、スタックポインターから32バイトだけ後ろにオフセットされた(足し算された)場所をコピー
stp: レジスターからメモリにコピーする(ストアする)命令
x29レジスターに登録されている場所から4バイトだけ前にオフセットされたメモリに0をコピーしている
wzr: 常に0を表すレジスター(0レジスター)
b: 指定された場所(次の行の処理)にジャンプしている
x = 0; y = 1;
stur wzr, [x29, #-0x8]
mov w8, #0x1
stur w8, [x29, #-0xc]
b 0x100003f54
スタックベースから8バイト分オフセットされたメモリ(xの場所)に0をコピー
先ほどのセットアップの処理で、x29はスタックポインタの役割を果たすようになっている
stur: store unscaled register: 第一引数のレジスタの内容を第二引数のレジスタに保存する
mov: 第一引数のレジスターに、第二引数のレジスターの内容をコピーする命令
yに代入する値を保持している
スタックベースから12バイト分オフセットされたメモリ(yの場所)に1をコピー
printf("%d\n", x);
ldur w9, [x29, #-0x8]
mov x8, x9
mov x9, sp
str x8, [x9]
adrp x0, 0 ; 0x100003000
add x0, x0, #0xfb4 ; literal pool for: "%d\n"
bl 0x100003fa8 ; symbol stub for: _printf
xの値や表示するリテラルの値をレジスターに登録して、printfの命令たちがある場所へジャンプしている。結果的に、レジスターたちは以下の内容のように更新されているが以降の処理にはあまり影響を及ぼしていない。
w9: xの値の場所
x8, x9: ベースとなるスタックポインター
x0: "%d\n"のリテラル値が保存されている場所
z = x + y;
0000000100003f70 ldur w8, [x29, #-0x8]
0000000100003f74 ldur w9, [x29, #-0xc]
0000000100003f78 add w8, w8, w9
0000000100003f7c str w8, [sp, #0x10]
xを保持しているメモリの値をw8レジスターにロードする
yを保持しているメモリの値をw9レジスターにロードする
w8レジスターに、w8とw9を足した値を格納する
スタックポインターから16バイトオフセットされたメモリ(zの場所)に、w8レジスターの値(x + y)を格納している
x = y; y = z;
ldur w8, [x29, #-0xc]
stur w8, [x29, #-0x8]
ldr w8, [sp, #0x10]
stur w8, [x29, #-0xc]
b 0x100003f94
yの値を一度w8レジスターに格納してから、xへコピー
zの値を一度w8レジスターに格納してから、yへコピー
while (x < 255);
ldur w8, [x29, #-0x8]
subs w8, w8, #0xff
b.lt 0x100003f54
xの値をw8レジスターにコピーして、その値が255より小さければ指定された場所(printfの処理が始まる場所)にジャンプする。
while(1)
b 0x100003f44
この命令を実行することになるたび必ずx = 0にジャンプする。
因みに、某漫画ではこれを16進数で暗記している化け物が登場する。
私の最も好きなキャラクターで、彼の最初の登場は23巻。最終巻の4巻しか出てこなかったのが悔やまれる。(まぁプログラマーだから妥当な復活時期なのだが)
この記事が気に入ったらサポートをしてみませんか?