🙋‍♂️

assemblyでHelloWorld! (Mac)

4 min read

この記事

  • 42tokyoの課題でアセンブリによるプログラミングをする機会があったので、そのチュートリアルとしてHello, world!するところまでを記事にしようと思います。初学者なので勘違い等が含まれるかもしれません。
  • アセンブリ単体を扱った解説記事は多くあるので基礎的なことはそちらにまかせ、実際に書いて動かす過程での疑問点を調べ、纏めていきます。
  • HelloWorldする系の記事も沢山あるんですが、それぞれに疑問が残る部分があるのでこの記事ではそこに焦点を当てます。

基本戦略

  • MacOS x64 Intel 構文
  • nasmでコンパイルし、ldでリンクします
  • printfではなくwriteで標準出力します

準備

  1. nasmのインストール
    $> brew install nasm
    
  2. vim hello.s

書く

dataセクションを書く

各セクションについてはこちらを参照
今回はHello, world!という固定値があるのでそれを書きます。
ついでにその文字列長も用意しておきます。

hello.s
section .data
	msg db 'Hello, world!', 0x0a
	len equ $ - msg

msglenという定数を定義しました。

疑問点

この時点で色々浮かぶ疑問を一つずつ解決していきます!

  1. db is なに?

    • そもそも変数を作ってストレージを割り当てる際には以下の構文が必要となります。
      <変数名> <define-directive> <初期値> [, <初期値>...]
      
    • dbとは上の<define-directive>に指定できる物の一つで、1byteのストレージを割り当ててくれるようです。

    参照

  2. 1byteにHello, world!は入らなくない?

    • 1byte割り当てを1文字ずつ、連続して何度も行っているようです。(ASCIIコードに変換しながら)
    • ここは以下のようにも書き換えることができます。
      msg db "H","e",'l','l','o'," world !",0x0a
      
  3. 0x0a is なに?

    • 上の話と合わせて考えると簡単です。
      0x0aはASCIIコードの改行です!
  4. equ is なに?

    • equディレクティブは定数の定義に使用されます。
      num equ 42
      

    参照

  5. $ - 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

疑問点

  1. なぜ_main:_start: で解説するサイトもあるけど…

    • ここではエントリーポイントと呼ばれる、プログラムが最初に実行する場所を指定しています。決まった名前にするとエントリーポイントとして認識されるんですが、その名前は作る実行ファイルの形式[1]によって変わるみたいです。(最近のMacは_main:ですが、昔はstart:だったとか。。。)
    • 間違っている時はldをしたときに「_main:が無い」とエラーが出るのでそれに合わせて書き換えるのが確実だと思います。
    • もしくはld -e _start -o ...とすることでエントリーポイントを指定出来ます。
  2. syscallの部分をint 0x80で解説するサイトもあるけど…

    • intというのは割り込みシグナルを送る命令で、その際0x80番を指定するとシステムコールが発行されます。
    • つまりどちらも同じシステムコールの呼び出しで、int 0x80はx86とx86_64両方で使えて、syscallはx86_64でのみ使えます。今回はどちらでも構わないです。
    • 違いは下の項目

    参照

  3. 引数を入れるためのレジスタがサイトによって違う!

    • 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

    参照

  4. Macはシステムコールの番号に0x2000000が足されているのはなぜ?

    • UNIXのシステムコールと区別するためにそうされてるみたいです。探求してる方がこちらにいました。
    • 定義をみると2だけでなく1~4までそれぞれ意味がありそう。

コンパイル・実行する

$> 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:を指定するのと合わせたら、この辺が理由なのかなぁ…と予想しています。予想なので参考にはしないで下さい。

脚注
  1. elf32, elf64, Mach-O 64bit, ... ↩︎ ↩︎

  2. C言語でmain()の前に動くもの ↩︎