今更ながらアセンブリに入門してみる。(自分用メモ)
とりあえずHelloWorldを目標にする。
この動画を見てアセンブリ楽しそう!となったので、触ってみる。
環境は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システムコールを使う。(他に方法があるのかは知らないけど)
システムコールの番号は、このページで調べた。
今回は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が一文字表示された。
そしたらstr
をHello, 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
ちゃんと実行された。