Hello GNU Assembler World

何年か前に「はじめて読む486」を読んだはずなんですけど、最近アセンブラ読もうとしたら全然読めなかったので、一から復習することにしました。アメリカ人が重要なタスクに取り組んでて凄い!と書いた直後にアセンブラの復習とかシュールなんですけど、まぁ気になったら次に進めないですし仕方ないですね。コードの動作確認は32bitのCentOSで行っています。手元の64bitOSだとググって出てくる情報と色々違うので、面倒でやめました。

Hello World

まずはHello Worldです。少しググったところ以下のような感じでした。

# helloworld.s
.data
msg:     .ascii "hello gas world\n"
msg_end: .equ   len, msg_end-msg
.globl main
main:
        movl $4, %eax    # sys_write
        movl $1, %ebx    # stdout
        movl $msg, %ecx
        movl $len, %edx
        int $0x80        # system call
        ret
$ gcc helloworld.s 
$ ./a.out
hello gas world

int $0x80がシステムコールの呼び出しで、呼び出し時のパラメータをせっせと各レジスタに積んでると言うところですね。高級言語的に書くと、system_call(4, 1, $msg, $len)ですね。なおシステムコールの一覧は/usr/include/asm/unistd.hで見れるんですが、各システムコールがどんなパラメータ取るのかの1次情報がどこにあるのかは分かりませんでした。一応このページを見ると色々書いてます。

あとHello Worldはちょっと複雑なので、ただ単に起動して終了するだけの以下のコードから始めるのが良いかなと思いました。

# exit.s
.globl main
main:
        movl $1, %eax  # sys_exit
        movl $0, %ebx  # 終了コード
        int  $0x80
        ret
$ gcc -t exit.s
$ ./a.out
$ echo $?
0

エントリポイント

色々ググっていて.globlで指定するエントリポイントに_startが使われている場合がある事に気がつきました。ところがexit.sのmainを_startに書き換えると、リンクに失敗します。

# exit2.s
.globl _start
_start:
        movl $1, %eax  # sys_exit
        movl $0, %ebx  # 終了コード
        int  $0x80
        ret
$ gcc exit2.s
/tmp/cck5d6xx.o: In function `_start':
(.text+0x0): multiple definition of `_start'
/usr/lib/gcc/i686-redhat-linux/4.4.7/../../../crt1.o:(.text+0x0): first defined here
/usr/lib/gcc/i686-redhat-linux/4.4.7/../../../crt1.o: In function `_start':
(.text+0x18): undefined reference to `main'
collect2: ld はステータス 1 で終了しました

crt1.oに_startが定義されてるよ、とgccが文句を言っています。crt1.oって何?と思ってverboseオプションを付けて再度トライしてみると以下のような出力が得られます。数行のアセンブラを書いただけでもデフォルトで色々リンクされるんですね。知りませんでした。で、その中のcrt1に_startが定義されています。

$ gcc -t exit2.s
/usr/bin/ld: mode elf_i386
/usr/lib/gcc/i686-redhat-linux/4.4.7/../../../crt1.o
/usr/lib/gcc/i686-redhat-linux/4.4.7/../../../crti.o
/usr/lib/gcc/i686-redhat-linux/4.4.7/crtbegin.o
/tmp/cc4GjdFH.o
-lgcc_s (/usr/lib/gcc/i686-redhat-linux/4.4.7/libgcc_s.so)
/lib/libc.so.6
(/usr/lib/libc_nonshared.a)elf-init.oS
/lib/ld-linux.so.2
-lgcc_s (/usr/lib/gcc/i686-redhat-linux/4.4.7/libgcc_s.so)
/usr/lib/gcc/i686-redhat-linux/4.4.7/crtend.o
/usr/lib/gcc/i686-redhat-linux/4.4.7/../../../crtn.o
/tmp/cc4GjdFH.o: In function `_start':
(.text+0x0): multiple definition of `_start'
/usr/lib/gcc/i686-redhat-linux/4.4.7/../../../crt1.o:(.text+0x0): first defined here
/usr/lib/gcc/i686-redhat-linux/4.4.7/../../../crt1.o: In function `_start':
(.text+0x18): undefined reference to `main'
/usr/bin/ld: link errors found, deleting executable `a.out'
collect2: ld はステータス 1 で終了しました

ちなみにデフォルトで色々リンクされるのを、抑制するnostdlibというオプションがgccにはあります。これを指定すると、余計なものが全くリンクされずに意図通りに動作します。

$ gcc -t exit2.s -nostdlib
/usr/bin/ld: mode elf_i386
/tmp/ccfmbGAi.o
$ ./a.out
$ echo $?
0

それでもcrt1.oが何なのか気になったので調べてみたところ、これはC RunTimeの略でglibcの中に該当するコードがありました。i386アーキテクチャだとglibcのsysdeps/i386/start.Sに_startが定義されています。何をやってるかって言うと、色々初期化っぽい事をしてからmain関数を呼び出しているだけです。C言語のmain関数ってこれ由来だった訳ですね。ちなみにエントリポイントはgccの-eオプションで任意の関数名に変更できます。適当に中二病っぽい名前のエントリポイントを試しておきました。

アドレッシングモード

Cで適当なコード書いてアセンブラ吐かせて実験していると、アドレッシングモードとか言うのが沢山ある事に気がつきます。とりあえず色々見かけたのを列挙してみたコードを貼付けておきます。要はメモリとかレジスタへのアクセス方法なんですけど、ややこしいです。僕はindexedアドレッシングモードの見た目がイカつくて、理解するのに時間がかかりました。

.data
arr:
  .long  3,67,34,228,45,75,54,34,44,33,22,11,66,0
.data
arr:
  .long  3,67,34,228,45,75,54,34,44,33,22,11,66,0

.globl main
main:
    movl $10, %ebx            # [immediate]
    movl %esp, %ebx           # [direct]
    movl (%esp), %ebx         # [indirect]

    movl arr, %ebx            # アドレッシングモード名不明 arrの先頭
    movl arr+4, %ebx          # 同上 arrの2つ目の要素
    movl arr+8, %ebx          # 同上 arrの3つ目の要素

    movl $0, %ecx
    movl arr(,%ecx,4), %ebx   # [indexed] arrの先頭
    movl $1, %ecx
    movl arr(,%ecx,4), %ebx   # [indexed] arrの2つ目
    movl $2, %ecx
    movl arr(,%ecx,4), %ebx   # [indexed] arrの3つ目

    pushl $10
    pushl $20
    pushl $30
    movl 0(%esp), %ebx        # [base pointer] スタックの一番目
    movl 4(%esp), %ebx        # [base pointer] スタックの二番目
    movl 8(%esp), %ebx        # [base pointer] スタックの三番目

    movl $3, %ecx
    movl $4, %edx
    leal (%ecx, %edx), %ebx   # [多分indexed] ebx=ecx+edx (3+4=7)

    # sys_exit
    movl $1, %eax
    int $0x80

サブルーチンの呼び出し

次はサブルーチンの呼び出しの仕組みを抑えたいということで、次のようなCのコードをコンパイルしてみます。

int add(int a, int b) {
    return a+b;
}
int main() {
    return add(10, 15);
}

そうすると以下のようなアセンブラが得られます。add以下のコードに注目すると、古いベースポインタをスタックに退避して処理を行い、処理が終わったらベースポインタを元に戻しています。そうする事で、呼び出し元とサブルーチンの間で、スタックを使った引数の受け渡しを行えるわけですね。戻り値はどうやらeaxレジスタに格納するのが標準的な挙動のようです。

.file   "add.c"
        .text
.globl add
        .type   add, @function
add:
        pushl   %ebp               # 古いベースポインタをスタックに退避
        movl    %esp, %ebp         # 現在のスタックの先頭をベースポインタに設定 (このサブルーチン内のベースポインタはこの値)
        movl    12(%ebp), %eax     
        movl    8(%ebp), %edx
        leal    (%edx,%eax), %eax
        popl    %ebp               # 処理が終わったら退避してたベースポインタを再設定
        ret
        .size   add, .-add
.globl main
        .type   main, @function
main:
        pushl   %ebp
        movl    %esp, %ebp
        subl    $8, %esp
        movl    $15, 4(%esp)
        movl    $10, (%esp)
        call    add
        leave
        ret
        .size   main, .-main
        .ident  "GCC: (GNU) 4.4.7 20120313 (Red Hat 4.4.7-3)"
        .section        .note.GNU-stack,"",@progbits

まぁそういうわけでアセンブラ書けないけど、頑張れば読める感じには復習できた気でいます。いつかアセンブラ解読して実作業が進む体験を出来る日を夢見て精進しきたいですね。あと中々これはという資料がオンラインで探せなかったのですが、下記のページが凄く役に立ちました。ありがとうございます。

1個目のリンクを読んでから2個目のPDFを頑張って読んだら、かなり理解は進みそうです。(僕は4章までしか読んでません)

Leave a Reply

Your email address will not be published. Required fields are marked *