Open12

今更ながらアセンブリに入門してみる。(自分用メモ)

くまさんくまさん

環境はwsl上のubuntu22.04LTSでやっていく。

x86_64ってやつになるのかな?

くまさんくまさん

とりあえずvimでhello.sファイルを作る。拡張子の.sはsourceのsっぽい?

くまさんくまさん

まずはexitするだけのプログラムを書いてみる。

exitはexitシステムコールを呼べば良さそう。

ただ、exitのシステムコール番号がわからないので調べる。

くまさんくまさん

Webで検索すれば出てくるとは思うけど、今回はCで書いたコードをディスアセンブル?して調べてみる。

とりあえず今回使うCのコード。

#include <stdlib.h>

int main(void)
{
    exit(0);
}
くまさんくまさん

このコードをアセンブリにしていきたい。gccのオプションでできるらしい。

gcc test.c -S

-Sをつけて実行すると、test.sが出力される。

test.sの中身は以下の通り

        .file   "test.c"
        .text
        .globl  main
        .type   main, @function
main:
.LFB6:
        .cfi_startproc
        endbr64
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        movl    $0, %edi
        call    exit@PLT
        .cfi_endproc
.LFE6:
        .size   main, .-main
        .ident  "GCC: (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0"
        .section        .note.GNU-stack,"",@progbits
        .section        .note.gnu.property,"a"
        .align 8
        .long   1f - 0f
        .long   4f - 1f
        .long   5
0:
        .string "GNU"
1:
        .align 8
        .long   0xc0000002
        .long   3f - 2f
2:
        .long   0x3
3:
        .align 8
4:

ただ、これだとexit@PLTというものを呼んでしまっていて、syscallするところが見れない。

これを解決する方法として、今回はstaticリンクで作成した実行ファイルを逆アセンブルすることにする。

くまさんくまさん
gcc --static test.c

これでいつものa.outが出力される。
今度はこれをobjdumpというコマンドを使って見てみる。

objdump -D a.out | less

このコマンドを実行すると、とても長いプログラムが表示される。標準ライブラリの関数も一緒に含まれてるらしい。

この中から、目的のexitシステムコールを呼んでる部分を探してみる。

普通にexitで検索をかけて見つかったそれっぽい部分のコードがこれ

00000000004466a0 <_exit>:
  4466a0:       f3 0f 1e fa             endbr64
  4466a4:       49 c7 c0 b8 ff ff ff    mov    $0xffffffffffffffb8,%r8
  4466ab:       be e7 00 00 00          mov    $0xe7,%esi
  4466b0:       ba 3c 00 00 00          mov    $0x3c,%edx
  4466b5:       eb 16                   jmp    4466cd <_exit+0x2d>
  4466b7:       66 0f 1f 84 00 00 00    nopw   0x0(%rax,%rax,1)
  4466be:       00 00
  4466c0:       89 d0                   mov    %edx,%eax
  4466c2:       0f 05                   syscall
  4466c4:       48 3d 00 f0 ff ff       cmp    $0xfffffffffffff000,%rax
  4466ca:       77 1c                   ja     4466e8 <_exit+0x48>
  4466cc:       f4                      hlt
  4466cd:       89 f0                   mov    %esi,%eax
  4466cf:       0f 05                   syscall
  4466d1:       48 3d 00 f0 ff ff       cmp    $0xfffffffffffff000,%rax
  4466d7:       76 e7                   jbe    4466c0 <_exit+0x20>
  4466d9:       f7 d8                   neg    %eax
  4466db:       64 41 89 00             mov    %eax,%fs:(%r8)
  4466df:       eb df                   jmp    4466c0 <_exit+0x20>
  4466e1:       0f 1f 80 00 00 00 00    nopl   0x0(%rax)
  4466e8:       f7 d8                   neg    %eax
  4466ea:       64 41 89 00             mov    %eax,%fs:(%r8)
  4466ee:       eb dc                   jmp    4466cc <_exit+0x2c>

これを見てると、0x3cをedxに入れて、edxをeaxに入れて(1回目の)syscallしてるっぽい。
ということはexitのシステムコールは0x3cっぽい?とりあえず試してみる。

くまさんくまさん

とりあえず最小限のコードを書いてみた。

main:
    mov rax, 0x3c
    syscall

これでコンパイルしてみる。

gcc hello.s

するとエラーが出る

hello.s: Assembler messages:
hello.s:2: Error: too many memory references for `mov'

原因はよくわからないけど、intel_syntaxをつけると解決できるっぽいのでやってみる。
さっきのコードの先頭の行に、以下を加える。

.intel_syntax noprefix

意味はいまいちよくわからないけど、これでもう一度コンパイルしてみる。
すると、今度は別のエラーが出る。

/usr/bin/ld: /usr/lib/gcc/x86_64-linux-gnu/11/../../../x86_64-linux-gnu/Scrt1.o: in function `_start':
(.text+0x1b): undefined reference to `main'
collect2: error: ld returned 1 exit status

mainが無いって言われる。けどさっきのエラーは解決したっぽいのでとりあえずは良さそう。

くまさんくまさん

mainがないと言われるので、コンパイルするときにエントリポイント?を指定してあげる。
とりあえずオブジェクトにコンパイルして、手動でリンクする。

gcc -c hello.s

これでhello.oが作成される。

試しにここでhello.oをdisasmしてみる。

objdump -D hello.o

するとこんな感じに出力される。

hello.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <main>:
   0:   48 c7 c0 3c 00 00 00    mov    $0x3c,%rax
   7:   0f 05                   syscall

良さそう。
これをldコマンドでエントリポイントを指定して実行ファイルにする。

ld -e main -o hello.bin hello.o

-eはエントリポイントのeらしい。
これを実行すると、エラーが出る。

ld: warning: cannot find entry symbol main; defaulting to 0000000000401000

これはmainにglobalがついてないことが原因っぽい。
さっきのコードを以下のように書き換える。

.intel_syntax noprefix
.global main

main:
    mov rax, 0x3c
    syscall

これでもう一度コンパイルし直す。

gcc -c hello.s
ld -e main -o hello.bin hello.o

今度はエラーが出ずに実行できた。ちゃんとhello.binも出力されてる。

試しにhello.binを実行してみたけど、何もされず正常に終了されてるっぽい。
これでとりあえずexitだけして終了するプログラムは完成。

次は標準出力に文字列を出力していく。

くまさんくまさん

標準出力に文字を出力するために、今回はwriteシステムコールを使う。(他に方法があるのかは知らないけど)

システムコールの番号は、このページで調べた。
https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md

今回はx86_64のコードを書いているので、writeは1番になる。
とりあえずさっきのexitと同じ感じでwriteも呼んでみる。

.intel_syntax noprefix
.global main

main:
    mov rax, 0x01
    syscall
    mov rax, 0x3c
    syscall

これでとりあえずwriteシステムコールは呼ばれるはず。
ただ、writeには引数を渡してあげる必要があるので、またさっきのページで調べる。

とりあえず簡単そうなfdとcountだけ設定してみる。

.intel_syntax noprefix
.global main

main:
    mov rax, 0x01
    mov rdi, 0x01
    mov rsi,
    mov rdx, 0x01
    syscall
    mov rax, 0x3c
    syscall

最初はとりあえずどちらも1に設定した。(fdの1はSTDOUT)

くまさんくまさん

引数のrsiには、出力したい文字列bufのアドレスを渡してあげる必要がある。

やりかたはよくわからないけど、こうしたらいけるっぽい。

.intel_syntax noprefix
.global main

main:
    mov rax, 0x01
    mov rdi, 0x01
    lea rsi, [str]
    mov rdx, 0x01
    syscall
    mov rax, 0x3c
    syscall

str:
    .ascii "c"

leaはLoad Effective Addressの略らしい。
C言語で言う&strみたいなものかな?

.asciiの他にも.asciizというものがあるらしい。
そっちだとc言語の文字列みたいに終端に0を追加してくれるらしい。
とりあえず今回は.asciiを使う。

これでコンパイルして実行してみると、無事cが一文字表示された。

そしたらstrHello, World!\nに変えて、countも14にしておく。
すると無事、Hello, World!と表示された。

くまさんくまさん

これでとりあえずコード自体は完成だけど、毎回コンパイルとリンクのコマンドを手動で実行するのは面倒なので、Makefileを書いてみる。

とりあえず書いたMakefileはこんな感じ。

NAME    = hello.bin
CC      = gcc
SRCS    = hello.s
OBJS    = $(SRCS:.s=.o)

$(NAME) : $(OBJS)
    $(LD) -e main -o $(NAME) $(OBJS)

clean   :
    $(RM) $(OBJS)

fclean  : clean
    $(RM) $(NAME)

re      : fclean $(NAME)

実行してみる。

$ make
as   -o hello.o hello.s
ld -e main -o hello.bin hello.o

ちゃんと実行された。