LoginSignup
42
36

More than 5 years have passed since last update.

Macでx86アセンブリ入門してみた

Last updated at Posted at 2015-08-10

Xcodeでx86アセンブリを書くの続き。

いくつかプログラムを書いてみたので忘れないうちにメモ。

最近はIntelシンタックスで書くのが主流みたいだけど、gcc -S とかそういうツールでAT&Tシンタックスをよく目にするし、movlやmovbなどの区別を考えながら書いたほうが初学者には取っ付きやすそうということで、AT&Tシンタックスを選択した。

①exitするだけのコマンド

.global _main

.text
_main:
    pushl  $0
    movl   $1, %eax
    pushl  %eax # espを4下げるためにダミーでスタックに4バイトぶん積む
    int    $0x80

まず、システムコールを呼ぶには、まずスタックにシステムコールの引数(exitの場合は終了ステータス)をpushし、eaxにシステムコール番号を入れて、スタックに4バイト積んで、それからint命令に0x80を渡す。

pushl %eax のところは実は subl $4, %esp でも別に良い。espを4下げる理由はおそらく、システムコールがcall命令によって呼ばれることが一般的で、call命令は戻り先アドレスをスタックにpushするため、それに合わせたらしい。

また、竹迫さんの記事にもあるように、システムコールの引数をスタックに積むのはFreeBSD由来の流儀で、Linuxの場合はレジスタで渡すらしい。

②コマンドライン引数の数を終了ステータスにする

.global _main

.text
_main:
    movl   4(%esp), %eax # eaxにesp+4のアドレスの値を入れる
    subl   $1, %eax # 起動コマンドを含まない引数の数を取りたいので1引く
    pushl  %eax # 終了ステータス
    movl   $1, %eax
    pushl  %eax
    int    $0x80

まず、 4(%esp) はespに4を足したアドレスに格納されている値のこと。

mainが実行されるときにesp+4の位置にCでいうところのargcが入っているので、それから1を引いたものをpushした上でexitのシステムコールを呼んでいる。

③Hello World!

ハローワールドを出力してみる。

.global _main

.data
str:
    .ascii  "Hello, World!\n"
    .set str_len, . - str

.text
_main:
    pushl  $str_len
    pushl  $str
    pushl  $1
    movl   $4, %eax # write(1, str, str_len)
    pushl  %eax
    int    $0x80

    pushl  $0
    movl   $1, %eax
    pushl  %eax
    int    $0x80

writeシステムコールは4番だそうな。というのはMacのオープンソース関係のページにコードがあるのでそこから調べるらしい。

writeの引数には、ファイルディスクリプタ、出力したい文字列の先頭のアドレス、文字列の長さを渡す。定数文字の長さを得るたのイディオムが .set str_len, . - str というやつ。ドットはおそらく現在地のアドレスで、そこからstrというラベルのアドレスを引くことで長さを得ている。

.asciiは、後ろにヌル文字を付けない文字のこと。↓に詳しい。

④実行されている自分自身のコマンド名を出力するコマンド

argv[0]をprintする。

まずはこんな感じで書いてみたらとりあえず動いた。

.global _main

.text
strlen: # eaxに入っているアドレスが指すNULL終端文字列の長さをeaxに入れて返す
    movl    $0, %ebx
    subl    $1, %eax
loop:
    addl    $1, %ebx
    addl    $1, %eax
    cmpb    $0, (%eax)
    jne     loop
    subl    $1, %ebx
    movl    %ebx, %eax
    ret

_main:
    movl    8(%ebp), %eax # ebp+8のアドレスからargv[0]が始まる
    call    strlen
    pushl   %eax      # 出力する文字列の長さ
    pushl   8(%ebp)   # 出力する文字列 argv[0]
    pushl   $1        # ファイルディスクリプタ
    movl    $4, %eax  # writeシステムコール番号
    pushl   %eax      # 4バイト下げる
    int     $0x80     # システムコール

    movl    4(%esp), %eax
    subl    $1, %eax
    pushl   %eax
    movl    $1, %eax
    pushl   %eax
    int     $0x80

call strlenpushl %eip; jmp strlen と同じことらしい。

関数をきちんと定義する

上ので動くには動くが、調べてみると、色々規約を守っていないことがわかった。

関数定義の基本形

関数呼び出しを行う場合には、必ず呼び出した時のレジスタの値と関数処理が終了した時のレジスタの値を一致させる必要があります。
関数に入った(call命令を実行した)時点で、スタックの先頭には関数から帰るべきアドレス(命令ポインタ)が格納されています。
espが変更されては、関数から復帰する際に戻るべき場所が判らなくなってしまいますので、必ずレジスタを復元する処理が必要となります。

一般的に、関数処理時のレジスタルールは以下のものがあります。
* 関数に入る時と出る時で変化してはいけないレジスタ: EBP, ESP, EDI, ESI, EBX
* 戻り値を格納するレジスタ: EAX

これに気をつけて書いてみると、このようになった。

.global _main

.text
strlen: # 第一引数に渡されたアドレスが指すNULL終端文字列の長さをeaxに入れて返す
    push    %ebp        # ベースポインタをスタックに保存
    movl    %esp, %ebp  # 現時点でのスタックポインタの位置をベースポインタに設定

    movl    8(%ebp), %edx # 第一引数で渡されたアドレスをedxに入れる
    movl    $0, %eax    # 返り値となるeaxにゼロを入れる(これをインクリメントしていく)
    subl    $1, %edx    # edxから1引いておく(ループの数合わせのため)
loop:
    addl    $1, %eax    # eaxをインクリメント(文字数+1)
    addl    $1, %edx    # edxをインクリメント(アドレスを1進める)
    cmpb    $0, (%edx)  # edxの指している値をゼロ(NULL文字)と比較
    jne     loop        # もしNULL文字でなければ、loopまで戻る
    subl    $1, %eax    # ループから抜けたらeaxをデクリメント(ループの数合わせのため)

    movl    %ebp, %esp  # スタックポインタを復元する
    popl    %ebp        # ベースポインタを復元する
    ret

_main:
    pushl   8(%ebp)     # ebp+8のアドレスargv[0]をstrlenの第一引数に突っ込む
    call    strlen
    pushl   %eax        # 出力する文字列の長さ
    pushl   8(%ebp)     # 出力する文字列 argv[0]
    pushl   $1          # ファイルディスクリプタ
    movl    $4, %eax    # writeシステムコール番号
    pushl   %eax        # 4バイト下げる
    int     $0x80       # システムコール

    movl    4(%esp), %eax
    subl    $1, %eax
    pushl   %eax
    movl    $1, %eax
    pushl   %eax
    int     $0x80

リファクタリング

「ループの数合わせのため」と書いてあるところが嫌な感じなので、strlenをリファクタリングしてみた。

strlen: # 第一引数に渡されたアドレスが指すNULL終端文字列の長さをeaxに入れて返す
    push    %ebp        # ベースポインタをスタックに保存
    movl    %esp, %ebp  # 現時点でのスタックポインタの位置をベースポインタに設定

    movl    8(%ebp), %edx # 第一引数で渡されたアドレスをedxに入れる
    movl    $0, %eax    # 返り値となるeaxにゼロを入れる(これをインクリメントしていく)
loop:
    cmpb    $0, (%edx)  # edxの指している値をゼロ(NULL文字)と比較
    je      loop_break  # NULL文字ならbreak
    addl    $1, %eax    # eaxをインクリメント(文字数+1)
    addl    $1, %edx    # edxをインクリメント(アドレスを1進める)
    jmp     loop        # breakしなかった場合はループの先頭まで戻る
loop_break:
    leave               # movl %ebp, %esp; popl %ebp と同じ
    ret

元のが do {...} while (...) で、新しいのが while (...) {...} に近い。

⑤引数の数を文字列として出力するコマンド

puts(itoa(argc)) みたいなことをする。

.global _main

.text

putchar: # 第一引数で与えられた文字(アドレスではなくASCII文字そのもの)を出力する
    pushl   %ebp
    movl    %esp, %ebp  # お決まりの2行

    movl    %ebp, %edx
    addl    $8, %edx    # edxにebp+8を入れる(第一引数を指すアドレス)

    pushl   $1
    pushl   %edx        # edxをwrite
    pushl   $1
    movl    $4, %eax
    pushl   %eax
    int     $0x80

    addl    $16, %esp   # この関数内でpushした数x4をespに足す(スタックを元に戻す)
    leave
    ret

print_digit: # 第一引数で与えられた0から9までの数字を文字列として出力する
    pushl   %ebp
    movl    %esp, %ebp  # お決まりの2行

    movl    8(%ebp), %edx # 第一引数の値(数字)をedxに入れる
    addl    $0x30, %edx # edxに0x30を足す(数字をASCII文字に変換)
    pushl   %edx
    call    putchar

    addl    $4, %esp    # この関数内でpushした数x4をespに足す(スタックを元に戻す)
    leave
    ret

print_digits: # 第一引数で与えられた整数を文字列として出力する
    pushl   %ebp
    movl    %esp, %ebp  # お決まりの2行

    movl    8(%ebp), %eax # 第一引数の値をeaxに入れる
    pushl   %ebx        # この関数内でebxの値が変わるので退避させる
    pushl   $123        # 0-9ではない適当な値(番兵)をスタックに積む

loop_push_digits:
    movl    $10, %ebx   # 割る数(10)
    movl    $0, %edx    # divlを呼ぶ前にedxをゼロにする
    divl    %ebx        # 商と余りを同時に求める eax = div(eax, ebx), edx = mod(eax, ebx)
    pushl   %edx        # 10で割った余りをスタックに積む
    cmpl    $0, %eax    # eaxを10で割った商とゼロを比較
    jne     loop_push_digits # ゼロでなければ再度ループ

loop_pop_digits:
    cmpl    $123, (%esp) # スタックの先頭を番兵と比較
    je      loop_pop_digits_break # 番兵と同じであればループを抜ける

    call    print_digit # スタックの先頭に入っている数字を出力
    popl    %eax        # 出力した数字はもう不要なのでpop
    jmp     loop_pop_digits

loop_pop_digits_break:
    popl    %eax        # 番兵をpop
    popl    %ebx        # 保存しておいたebxをpop
    leave
    ret

_main:
    pushl   4(%ebp)
    call    print_digits

    movl    $0, %eax
    movl    $1, %eax
    pushl   %eax
    int     $0x80

いちいち解説するのが面倒な分量になってきたので割愛。argcを出力してくれる。

$ ./main 1 2 3 4 5 6 7 8 9 10
11

divlは商と余りを一気に計算してくれて、レジスタも3つ使うというのはおもしろかった。

まとめと感想

Macでx86アセンブリを書いて、システムコールを呼んだり関数を作ったりするところまでできた。

事前調査を1週間ぐらいやっていたとは言え、手を動かしたのは1日程度でこのぐらいまでできたのはだいぶ満足。Xcodeでやったのがだいぶ良かった。メモリを見られたり、好きなところにブレークポイントを置けるのはかなり助かる。

x86で調べるとLinuxの情報が出てくることが多くてMacでは通用しなくて戸惑った。x86 FreeBSDと調べると解決することが多かった。その他、nasmの情報や、GASのAT&TとIntelの情報が混在していてだいぶ調べにくかった。

その他参考リンク

42
36
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
42
36