HN 日本語サマリー

← 一覧へ戻る
プログラミング

極小実行ファイル再考

The Teensy Executable Revisited (muppetlabs.com)

45 pointsby ankitg122 コメント

要約

この記事は、以前の「極小実行ファイル」に関するエッセイへの反論に応え、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バイトに到達しました!このバージョンは