assemblyでHelloWorld! (Mac)
この記事
-
42tokyoの課題でアセンブリによるプログラミングをする機会があったので、そのチュートリアルとして
Hello, world!
するところまでを記事にしようと思います。初学者なので勘違い等が含まれるかもしれません。 - アセンブリ単体を扱った解説記事は多くあるので基礎的なことはそちらにまかせ、実際に書いて動かす過程での疑問点を調べ、纏めていきます。
- HelloWorldする系の記事も沢山あるんですが、それぞれに疑問が残る部分があるのでこの記事ではそこに焦点を当てます。
基本戦略
- MacOS x64 Intel 構文
-
nasm
でコンパイルし、ld
でリンクします -
printf
ではなくwrite
で標準出力します
準備
-
nasm
のインストール$> brew install nasm
vim hello.s
書く
dataセクションを書く
各セクションについてはこちらを参照
今回はHello, world!
という固定値があるのでそれを書きます。
ついでにその文字列長も用意しておきます。
hello.s
section .data
msg db 'Hello, world!', 0x0a
len equ $ - msg
msg
とlen
という定数を定義しました。
疑問点
この時点で色々浮かぶ疑問を一つずつ解決していきます!
-
db
is なに?- そもそも変数を作ってストレージを割り当てる際には以下の構文が必要となります。
<変数名> <define-directive> <初期値> [, <初期値>...]
-
db
とは上の<define-directive>
に指定できる物の一つで、1byteのストレージを割り当ててくれるようです。
- そもそも変数を作ってストレージを割り当てる際には以下の構文が必要となります。
-
1byteに
Hello, world!
は入らなくない?- 1byte割り当てを1文字ずつ、連続して何度も行っているようです。(ASCIIコードに変換しながら)
- ここは以下のようにも書き換えることができます。
msg db "H","e",'l','l','o'," world !",0x0a
-
0x0a
is なに?- 上の話と合わせて考えると簡単です。
0x0a
はASCIIコードの改行です!
- 上の話と合わせて考えると簡単です。
-
equ
is なに?-
equ
ディレクティブは定数の定義に使用されます。例num equ 42
-
-
$ - msg
is なに?- これは単純に引き算をしています。
-
$
というのは「ロケーションカウンタ」の現在値が入っていて、これが今はmsg
の直後を示しています。その数値からmsg
(文字列の先頭)の数値を引くとmsg
の文字列長が計算できます。 - 説明のために、C言語で同じ考え方を使って
strlen()
を書くとこんな感じint my_strlen(char *str) { char *dollar = str; while (*dollar != '\x00') dollar++; return ((int)(dollar - str)); // ポインタの差が文字列の長さ }
これでdataセクションの記述はすべて合点が行きました!
textセクションを書く
システムコールの「write」を呼び出し、Hello, world!
を標準出力します。
この辺のasmの解説は他サイトに溢れてるのでサラッとやります。
システムコール一覧
hello.s
section .data
msg db 'Hello, world!', 0x0a
len equ $ - msg
section .text
global _main ; '_main'を外部参照可能に
_main:
; SYSCALL: write(1, msg, len);
mov rax, 0x2000004 ; MacOSにおけるwrite関数の番号
mov rdi, 1 ; write()の第1引数。標準出力へ
mov rsi, msg ; write()第2引数
mov rdx, len ; write()第3引数
syscall
; SYSCALL: exit(0)
mov rax, 0x2000001 ; exit関数の番号
mov rdi, 0 ; 正常終了
syscall
疑問点
-
なぜ
_main:
?_start:
で解説するサイトもあるけど…- ここではエントリーポイントと呼ばれる、プログラムが最初に実行する場所を指定しています。決まった名前にするとエントリーポイントとして認識されるんですが、その名前は作る実行ファイルの形式[1]によって変わるみたいです。(最近のMacは
_main:
ですが、昔はstart:
だったとか。。。) - 間違っている時は
ld
をしたときに「_main:
が無い」とエラーが出るのでそれに合わせて書き換えるのが確実だと思います。 - もしくは
ld -e _start -o ...
とすることでエントリーポイントを指定出来ます。
- ここではエントリーポイントと呼ばれる、プログラムが最初に実行する場所を指定しています。決まった名前にするとエントリーポイントとして認識されるんですが、その名前は作る実行ファイルの形式[1]によって変わるみたいです。(最近のMacは
-
syscall
の部分をint 0x80
で解説するサイトもあるけど…-
int
というのは割り込みシグナルを送る命令で、その際0x80
番を指定するとシステムコールが発行されます。 - つまりどちらも同じシステムコールの呼び出しで、
int 0x80
はx86とx86_64両方で使えて、syscall
はx86_64でのみ使えます。今回はどちらでも構わないです。 - 違いは下の項目
-
-
引数を入れるためのレジスタがサイトによって違う!
-
int 0x80
を使ったときとsyscall
を使ったときで、引数のために使われるレジスタが変わります。
call call番号 第1引数 第2 第3 第4 第5 第6 返り値 int 0x80
eax ebx ecx edx esi edi ebp eax syscall
rax rdi rsi rdx r10 r8 r9 rax -
-
Macはシステムコールの番号に
0x2000000
が足されているのはなぜ?
コンパイル・実行する
$> nasm -f macho64 -o hello.o hello.s
$> ld -o hello hello.o -lSystem
$> ./hello
Hello, world!
-
nasm -f format
で実行ファイルの形式[1:1]を指定します。 -
ld
でリンクする時libSystem.dylib
を一緒にリンクします。-
libSystem.dylib
の中にあるlibc
が必要なだけなので、代わりに-lc
としても出来ます。なぜ
libc
が必要なのかはわかりませんでしたが、libc
の中にはcrt0.o
というスタートアップルーチン[2]が入っています。
これには_start:
ラベルが含まれているのでmacが_main:
を指定するのと合わせたら、この辺が理由なのかなぁ…と予想しています。予想なので参考にはしないで下さい。
-