昨日の続き
環境は FreeBSD 6.1 (AMD64) で、試したのは libc なしの Hello, world.
そういえば 0 をセットするなら xor を使うと小さくなるなんて話があった事を思い出したのでやってみる。1の作り方はxorで0にしてincすると1byte小さくなるようだ。もっと賢い方法があるかもしれないが。
#include <sys/syscall.h> .section .rodata .HELLO: .ascii "Hello, world.\n" .globl _start .section .init _start: movq $SYS_write, %rax xorq %rdi,%rdi inc %rdi movq $.HELLO,%rsi movq $14,%rdx syscall xorq %rdi,%rdi xorq %rax, %rax inc %rax syscall
システムコール番号は _exit が 1 で、 write は 4。
これを strip (.bss, .data, .text も削る)、すると 512 byte となる。昨日は .text を削り忘れていた。
Linux と FreeBSD の違いはあるが、本の例で488byte。64bit 用だとアドレスが入る所1か所で4byte大きくなり、ELFヘッダで 12 byte、プログラムヘッダの1エントリで12byte(これが2つ分)、セクションヘッダ1つで8byte(これ4つ)大きくなる計算だ。まあ妥当な所か。こっちはアセンブラで書いてる分得している所もあるだろう。
ちょっとずるい気もするが、xorq, movq, でなく xorl, movl を使い、レジスタも eax, edi, esi, edx を使うとさらに縮む。さらに %eax に mov する所を %el にするとまた縮む。これはやって良い事なのか?ともあれプログラムがexecve された時にレジスタが0クリアされるならば動くだろう。確認しろって?
という事で、execve(2) の実装を調べた。これは、引数をコピーインして kern_execve を呼ぶ。結局たどり着くのは do_execve (sys/kern/kern_exec.c)、exec_setregs で起動されるプログラムに渡す値がセットされる。この関数は当然アーキテクチャ依存で、AMD64の場合 sys/amd64/amd64/machdep.c にある。ここでスレッドのレジスタを格納する構造体を一旦 bzero でクリアしているので、多くの汎用レジスタは 0 になる。勿論 rip はエントリポイントになるし、 rsp はスタックポインタだ。あと、rdi は引数 argv を指す。
という事で、プログラムの各セクションが0xffffffff 以下にロードされていたからrdi, rsi の下位32bitだけ、また0クリアのおかげで rax は下位8bitセットするだけで動いた、という訳だったようだ。
命令プレフィックスが付くので xor でクリアするなら下手に16bitにするより32bit命令の方が短くなる、とか面白いね。即値はたぶん小さいビット数の方が得をするかな?がんばるなら命令セットマニュアルは必携だろう。もうがんばらない予定。
一応今までがんばった結果はこれ。504byteだった。
.section .rodata .L .ascii "Hello, world.\n" .globl _start .section .init _start: mov $4,%al xor %edi,%edi inc %edi mov $1,%dl mov $.L,%esi mov $14,%edx syscall xor %edi,%edi mov $1,%al syscall