kubo39's blog

ただの雑記です。

tinycr

これは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とか使ってみたけどだめだった) なんかいい方法ないですかね?