これはCrystal Advent Calendar2015の17日目らしいです。ひ、日付? な、なんのことです…!?
だいぶ昔に極小バイナリ作成をやっていたのですが、あれからいくばくか進捗がありまして現在146バイトとなっています。
https://github.com/kubo39/tinycr
せっかくなので解説をしてみようと思います。
main is usually a function: 151-byte static Linux binary in Rust
とか
Nim binary size from 160 KB to 150 Bytes · HookRace - a Nim blog for now
とかでもやられてて特に目新しいものもないのですが。
$ ./build.sh Crystal 0.13.0 [60777f3] (Mon Mar 7 17:12:47 UTC 2016) + crystal build hello.cr --emit obj --prelude=empty --release + ld hello.o -o hello -s --static -nostdlib --gc-sections -T script.ld + objcopy -j combined -O binary hello hello.bin + nasm -f bin -o tinybin -D entry=0x400070 elf.s + chmod +x tinybin + hexdump -C tinybin 00000000 7f 45 4c 46 02 01 01 00 48 65 6c 6c 6f 21 0a 00 |.ELF....Hello!..| 00000010 02 00 3e 00 01 00 00 00 70 00 40 00 00 00 00 00 |..>.....p.@.....| 00000020 38 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |8...............| 00000030 00 00 00 00 38 00 38 00 01 00 00 00 07 00 00 00 |....8.8.........| 00000040 00 00 00 00 00 00 00 00 00 00 40 00 00 00 00 00 |..........@.....| 00000050 00 00 40 00 00 00 00 00 92 00 00 00 00 00 00 00 |..@.............| 00000060 92 00 00 00 00 00 00 00 00 10 00 00 00 00 00 00 |................| 00000070 b8 01 00 00 00 bf 01 00 00 00 be 08 00 40 00 ba |.............@..| 00000080 07 00 00 00 0f 05 b8 3c 00 00 00 31 ff 0f 05 31 |.......<...1...1| 00000090 c0 c3 |..| 00000092 + wc -c 146 + ./tinybin Hello!
それでは1行目からみてみましょう。
crystal build hello.cr --emit obj --prelude=empty --release
ここでは crystal build
コマンドを使ってオブジェクトファイルを生成しています。 --prelude=empty
を指定することで外部のファイルに依存しない形にしています。ついでに --release
ビルドでインライン化を狙います。蛇足となりますが、crystalは関数に @[Naked]
や @[AlwaysInline]
を指定することができますが、自分の手元で試したところreleaseビルドでなければインライン化されませんでした。
hello.crは以下のようなコードです。
def syswrite dst = 0_u64 write = 1_u64 asm("syscall" : "={rax}"(dst) : "{rax}"(write), "{rdi}"(1), "{rsi}"(0x400008), "{rdx}"(7) : "rcx", "r11", "memory" : "volatile") end def sysexit dst = 0_u64 exit = 60_u64 asm("syscall" : "={rax}"(dst) : "{rax}"(exit), "{rdi}"(0) : "rcx", "r11", "memory" : "volatile") end def __main syswrite; sysexit end __main
GCC拡張構文を使ってインラインアセンブラを書いているだけのコードです。 0x400008
という数値が不気味な感じですが、これは後に触れます。
ld hello.o -o hello -s --static -nostdlib --gc-sections -T script.ld
リンカを使ってオブジェクトファイルからバイナリを生成します。各オプションに関しては -s
がstripでシンボル情報を削除する、 -static
が単一の静的バイナリにする、 -nostdlib
がlibcに依存しないバイナリを生成する、 --gc-section
が他から参照されないセクションを削除するといった意味です。またここではリンク時にかませるリンカスクリプトを用意していて以下のような内容です。
ENTRY(main) SECTIONS { . = 0x400070; combined . : AT(0x400070) ALIGN(1) SUBALIGN(1) { *(.text*) } }
ENTRY(main)
はプログラムの開始をシンボル main
とすることと、 combined
というセクションをアドレス 0x400070
を始点として作成しています。中身は .text
だけです。
objcopy -j combined -O binary hello hello.bin
objcopy コマンドを使って combined セクションだけを切り出しています。この時点で hello.bin はただのデータであり実行することはできません。
nasm -f bin -o tinybin -D entry=0x400070 elf.s
nasm を使ってアセンブラのファイル elf.s
から実行バイナリを生成します。 -D entry=0x400070
を指定してエントリポイントを教えてやります。
elf.s はこのようなファイルです。Rustで似たようなことをやってる人がいたのでそれをもってきています。
;; Custom ELF header for the binary. ;; Taken from https://github.com/kmcallister/tiny-rust-demo ;; Inspired by http://www.muppetlabs.com/~breadbox/software/tiny/teensy.html bits 64 org 0x00400000 ehdr: db 0x7f, "ELF" ; magic db 2, 1, 1, 0 ; 64-bits, little endian, version 1 db "Hello!" , 0x0A, 0 dw 2 ; e_type = executable dw 0x3e ; e_machine = x86-64 dd 1 ; e_version dq entry ; e_entry dq phdr - $$ ; e_phoff dq 0 ; e_shoff dd 0 ; e_flags dw ehdrsize ; e_ehsize dw phdrsize ; e_phentsize ehdrsize equ $ - ehdr phdr: dd 1 ; p_type & (e_phnum + e_shentsize + e_shnum + e_shstrndx) dd 7 ; p_flags = rwx dq 0 ; p_offset dq $$, $$ ; p_vaddr, p_paddr dq filesize ; p_filesz dq filesize ; p_memsz dq 0x1000 ; p_align phdrsize equ $ - phdr incbin "hello.bin" filesize equ $ - ehdr ;; vim: ft=tasm
元のやつからは dd 1; p_type & (e_phnum + e_shentsize + e_shnum + e_shstrndx)
の部分でehdrとphdrの一部をオーバーラップさせています。これで8バイト削減できる計算です。
部分的なところでとくに大事なところをみていくと、 org 0x00400000
はこのプログラムの先頭のアドレスを指しています。このアドレスというのは実際どこでもよいのですが、 0x00100000
より前はカーネルによって予約されている領域なので使うことができません。
db "Hello!" , 0x0A, 0
は出力部分の文字列をpadding領域に格納するtrickです。これで上の 0x400008
の数値の謎が解けました。
incbin "hello.bin"
はobjcopyで切り出したデータ部を接合するところです。これで ELFヘッダ+データ部 となるバイナリが生成できました。
おまけ
さて、このバイナリは完全に無駄なものを排除できたわけではありません。
objdump -dr
で元のオブジェクトファイルをみると、
$ objdump -dr hello.o hello.o: ファイル形式 elf64-x86-64 セクション .text の逆アセンブル: 0000000000000000 <main>: 0: b8 01 00 00 00 mov $0x1,%eax 5: bf 01 00 00 00 mov $0x1,%edi a: be 08 00 40 00 mov $0x400008,%esi f: ba 07 00 00 00 mov $0x7,%edx 14: 0f 05 syscall 16: b8 3c 00 00 00 mov $0x3c,%eax 1b: 31 ff xor %edi,%edi 1d: 0f 05 syscall 1f: 31 c0 xor %eax,%eax 21: c3 retq
これを見るとわかると思いますが、最後の3バイト(xor命令のところで2つ、retq命令のところで1つ)ほど余計なものが入ってしまっています。 これは LLVMのintrinsics unreachableを使えば削除できる気がしますが、自分でいろいろ試したところうまくいきませんでした。(NoReturnとか使ってみたけどだめだった) なんかいい方法ないですかね?