プログラミング
極小実行ファイル再考
The Teensy Executable Revisited (muppetlabs.com)
要約
この記事は、以前の「極小実行ファイル」に関するエッセイへの反論に応え、ELF実行ファイルのサイズを極限まで小さく保ちつつ、厳密なELF標準に準拠させる方法を探求しています。著者は、ELFヘッダーとプログラムヘッダーの重複利用や、未指定フィールドへのコード埋め込み、プログラムロードアドレスの調整といった巧妙なテクニックを駆使して、最終的に76バイトの実行ファイルを作成することに成功しました。これは、ELF仕様の柔軟性を最大限に活用しながら、最小限のコードで実行可能なファイルを実現する試みです。
全文翻訳
極小実行ファイル再考(または「地平線に集まる雷雲」)
何回か、私の元々のエッセイに対して「私が最終的に作成したものは、真にELF実行ファイルではない。むしろ、現在のLinuxカーネルがたまたまELF実行ファイルと誤解しているファイルだ」というコメントが寄せられました。これは的を射た意見です。あの45バイトのファイルは、ELF仕様の多くの要件に明らかに準拠していません。しかし、私を責めることができますか?何が可能かを知りながら、ELF仕様を窓から投げ捨てる直前の時点でどうしてやめることができたでしょう?しかし、これらの純粋主義者と私たち皆の中にある清教徒的な側面を満足させるために、私はこの続編を作成しました。
さて。私たちは実行ファイルを45バイトまで削りました。次に、可能な限り小さく保ちながら、公開された標準に厳密に準拠させたいと考えています。私たちが正しい道から逸れたのは、ELFヘッダーの「未使用」フィールドをいじり始めた時でした。ですから、その時点まで戻ってみましょう。
```
BITS 32
org 0x08048000
ehdr: ; Elf32_Ehdr
db 0x7F, "ELF", 1, 1, 1 ; e_ident
times 9 db 0
dw 2 ; e_type
dw 3 ; e_machine
dd 1 ; e_version
dd _start ; e_entry
dd phdr - $$ ; e_phoff
dd 0 ; e_shoff
dd 0 ; e_flags
dw ehdrsz ; e_ehsize
dw phdrsz ; e_phentsize
dw 1 ; e_phnum
dw 0 ; e_shentsize
dw 0 ; e_shnum
dw 0 ; e_shstrndx
ehdrsz equ $ - ehdr
phdr: ; Elf32_Phdr
dd 1 ; p_type
dd 0 ; p_offset
dd $$ ; p_vaddr
dd $$ ; p_paddr
dd filesz ; p_filesz
dd filesz ; p_memsz
dd 5 ; p_flags
dd 0x1000 ; p_align
phdrsz equ $ - phdr
_start:
xor eax, eax
inc eax
mov bl, 42
int 0x80
filesz equ $ - $$
```
これが91バイト版でした。では、これが最高のサイズとして行き詰まっているのでしょうか?いいえ、まったくそうではありません。ELFヘッダーとプログラムヘッダーテーブルを8バイト重複させても、何のルールも破っていません。ELF仕様は、ファイル内の異なるデータ構造の重複を明示的に許可しています。では、ここでそれを行いましょう。
```
; tiny.asm
BITS 32
org 0x08048000
ehdr:
db 0x7F, "ELF", 1, 1, 1 ; e_ident
times 9 db 0
dw 2 ; e_type
dw 3 ; e_machine
dd 1 ; e_version
dd _start ; e_entry
dd phdr - $$ ; e_phoff
dd 0 ; e_shoff
dd 0 ; e_flags
dw ehdrsz ; e_ehsize
dw phdrsz ; e_phentsize
phdr:
dd 1 ; e_phnum
; p_type
; e_shentsize
dd 0 ; e_shnum
; p_offset
; e_shstrndx
ehdrsz equ $ - ehdr
dd $$ ; p_vaddr
dd $$ ; p_paddr
dd filesz ; p_filesz
dd filesz ; p_memsz
dd 5 ; p_flags
dd 0x1000 ; p_align
phdrsz equ $ - phdr
_start:
xor eax, eax
inc eax
mov bl, 42
int 0x80
filesz equ $ - $$
```
これで83バイトになります。他に何ができるでしょうか?あまりないようです。絶望して、ELF仕様をもう一度読み返し、何かを探すかもしれません。初期レジスタ値について何か保証はありますか?edxレジスタだけです。そしてそれは、ゼロか、最終的なシャットダウンプロシージャのアドレスのいずれかを含むと書かれています。つまり、実際には何の保証もありません。探し続けましょう。
あっ、プログラムヘッダーテーブル構造のp_paddrフィールドです!Intelアーキテクチャに適用されない、または実行ファイルに適用されない—少なくとも私たちの実行ファイルには適用されない—他のすべてのヘッダーフィールドは、ELF仕様によりゼロに設定される必要があります。しかし、p_paddrフィールドについては、仕様はフィールドの内容が「未指定」であると述べています。ですから、結局のところ、私たちが遊べる4バイトがあります。それを使って何ができるでしょうか?もちろん、プログラムの一部を保持するために使います。もちろん、プログラム全体をそこに入れることはできないので、残りの部分に到達するためにjmp命令で4バイトのうち2バイトを無駄にする必要があります。しかし、それでも2バイトが残っており、プログラムの最初の命令はちょうど2バイト長です。
```
; tiny.asm
BITS 32
org 0x08048000
ehdr:
db 0x7F, "ELF", 1, 1, 1 ; e_ident
times 9 db 0
dw 2 ; e_type
dw 3 ; e_machine
dd 1 ; e_version
dd _start ; e_entry
dd phdr - $$ ; e_phoff
dd 0 ; e_shoff
dd 0 ; e_flags
dw ehdrsz ; e_ehsize
dw phdrsz ; e_phentsize
phdr:
dd 1 ; e_phnum
; p_type
; e_shentsize
dd 0 ; e_shnum
; p_offset
; e_shstrndx
ehdrsz equ $ - ehdr
dd $$ ; p_vaddr
_start: xor eax, eax ; p_paddr
jmp short part2
dd filesz ; p_filesz
dd filesz ; p_memsz
dd 5 ; p_flags
dd 0x1000 ; p_align
phdrsz equ $ - phdr
part2:
inc eax
mov bl, 42
int 0x80
filesz equ $ - $$
```
これで81バイトです。これで終わりでしょうか?p_paddrフィールドの次のフィールドはp_fileszフィールドです。もしjmp命令をそれと重複させることができれば、別の命令をそこに入れることができます。しかし残念ながら、そのフィールドの最初のバイトはファイル全体のサイズであり、これは賢明なジャンプではありません。そして残りのバイトはゼロです。そのアプローチはあまり有望に見えません。
p_paddrの前のフィールドはどうでしょうか?それはプログラムがロードされるアドレスです。ええ、私たちはデフォルト値の0x08048000を使う必要がないことはすでに知っています。少なくともアドレスはページアラインメントを保つ必要がありますが、2バイト命令をアドレスの上半分に収めることができるはずです。しかし、私たちのxorはそれには機能しません。これはリトルエンディアンであることを思い出してください。プログラムのバイトは現在次のとおりです。
```
31C0 xor eax, eax
EB10 jmp short part2
40 part2: inc eax
B32A mov bl, 42
CD80 int 0x80
```
xor命令はC0を最上位バイトとして残し、最上位ビットが設定されており、Linuxはそこにコードを置くことを好みません。(一般的に、32ビット実行ファイルでは、メモリの上半分は動的に割り当てられるアドレス、つまりヒープ、スタック、共有オブジェクトライブラリ用に予約されています。)
一方、「mov bl, 42」命令は、完全に許容できる最上位アドレスバイトを与えてくれます。そこで、ロードアドレスを0x2AB30000に変更し、少し再配置すると次のようになります。
```
; tiny.asm
BITS 32
org 0x2AB30000
ehdr:
db 0x7F, "ELF", 1, 1, 1 ; e_ident
times 9 db 0
dw 2 ; e_type
dw 3 ; e_machine
dd 1 ; e_version
dd _start ; e_entry
dd phdr - $$ ; e_phoff
dd 0 ; e_shoff
dd 0 ; e_flags
dw ehdrsz ; e_ehsize
dw phdrsz ; e_phentsize
phdr:
dd 1 ; e_phnum
; p_type
; e_shentsize
dd 0 ; e_shnum
; p_offset
; e_shstrndx
ehdrsz equ $ - ehdr
dw 0 ; p_vaddr
_start: mov bl, 42
xor eax, eax ; p_paddr
jmp short part2
dd filesz ; p_filesz
dd filesz ; p_memsz
dd 5 ; p_flags
dd 0x1000 ; p_align
phdrsz equ $ - phdr
part2:
inc eax
int 0x80
filesz equ $ - $$
```
これで79バイトになりました。もし「inc eax」命令のためにあと1バイトのスペースを見つけることができれば、p_addrフィールドのジャンプを「int 0x80」に置き換え、それによってプログラム全体を合法的にヘッダー内に収めることができます!たったあと1バイトだけ…
おそらく、プログラムの最後のバイトが次のフィールドにはみ出すようにするだけではどうでしょうか?それはp_fileszフィールドで、ファイル内のプログラムセグメントのサイズを指定します。プログラムの最後のバイトは0x80であり、これはファイルサイズとしては確かに大きすぎます。つまり、ファイルのサイズについて嘘をつくことはできますし、Linuxは見逃してくれるでしょう—しかし、ここでは真っ当な道を歩むことになっています。
しかし、p_fileszの次のフィールドはp_memszです。もしこれら2つのフィールドが逆だったら問題なかったでしょう。このフィールドについては以前にも見ました。プログラムをメモリにロードする際にどれだけのメモリを割り当てるかを指定します。このフィールドをファイルサイズよりも大きく設定することは全く問題ありません。しかし、このフィールドを使う方法があります。p_fileszを飛び越えてp_memszにジャンプすることができます。これを試すと、実際に先を行くことができるとわかります。
```
; tiny.asm
BITS 32
org 0x2AB30000
ehdr:
db 0x7F, "ELF", 1, 1, 1 ; e_ident
times 9 db 0
dw 2 ; e_type
dw 3 ; e_machine
dd 1 ; e_version
dd _start ; e_entry
dd phdr - $$ ; e_phoff
dd 0 ; e_shoff
dd 0 ; e_flags
dw ehdrsz ; e_ehsize
dw phdrsz ; e_phentsize
phdr:
dd 1 ; e_phnum
; p_type
; e_shentsize
dd 0 ; e_shnum
; p_offset
; e_shstrndx
ehdrsz equ $ - ehdr
dw 0 ; p_vaddr
_start: mov bl, 42
xor eax, eax ; p_paddr
jmp short part2
dd filesz ; p_filesz
part2: inc eax ; p_memsz
int 0x80
db 0
dd 7 ; p_flags
dd 0x1000 ; p_align
phdrsz equ $ - phdr
filesz equ $ - $$
```
そして76バイトに到達しました!このバージョンは