【しばらく編集不可モードで運営します】 編集(管理者用) | 差分 | 新規作成 | 一覧 | RSS | FrontPage | 検索 | 更新履歴

WhirlwindTutorialOnCreatingReallyTeensyElfExecutablesForLinux -

目次

Linux で動く極小 ELF 実行ファイルをつくる怒涛のチュートリアル (あるいは "Size Is Everything")


  She studied it carefully for about 15 minutes. Finally, she spoke. 
  "There's something written on here," she said, frowning, 
  "but it's really teensy." 
  (Dave Barry, "The Columnist's Caper")

もしあなたがプログラマで、膨れ上がるソフトウェアに嫌気が差しているなら、 この文章には効き目があるかもしれない。

この文書では、だぶつくバイト群を絞りあげてシンプルなプログラムに仕立てる方法を探求する。 (もちろん、より実用的な目的として ELF ファイル・フォーマットの Linux オペレーティング・システムの内部構造にふれるというのもある。 でも極小 ELF バイナリの作りかたについても何かしらわかってもらえると嬉しい。)

 
ここに書いてある説明やサンプルは、Intel 386 アーキテクチャで動く Linux 上の ELF バイナリ限定の話。その点に気をつけてほしい。 たぶん大事な部分は他の ELF 系 UNIX でも通用するはずだけれど、 私にはそう確約できるほど経験がない。

説明やサンプルにでてくるアセンブリのコードは Nasm で使うように書かれている。 (今回の用途に向いているだけでなく、Gas より前に x86 の文法を勉強した身には NASM の文法の方が AT&T 文法よりだいぶわかりやすい。) Nasm はフリーで入手できるし、とてもポータブル。 http://nasm.sourceforge.net/ を参照のこと。

もうひとつ。 アセンブリがさっぱりわからない人だと、この文書の一部についていくのはやや辛いかもしれない。


話を始めるべくプログラムを書こう。だいたいどんなプログラムでもいいけれど、単純なほどいい。 プログラムで何ができるかよりは、どれだけ小さな実行ファイルを作れるかを知りたいわけだからね。

というわけで、えらく簡単なプログラムを書いてみた。 何もせず OS に整数値を返すだけ。別に問題ないよね? 実際 Unix なら少なくとも二つはそんなプログラムがあるわけだし: true と false が。 0 と 1 は使われてしまったから、ここでは 42 を使うことにしよう。

最初のバージョンはこれ。

  /* tiny.c */
  int main(void) { return 42; }

コンパイルして実行できるのを確認。

  $ gcc -Wall tiny.c
  $ ./a.out ; echo $?
  42

で、どれだけでかいんだろう? 私のマシンだとこんなかんじ。

  $ wc -c a.out
     3998 a.out

(他のマシンだと少し違う結果になるはず。) まあ実際のところ、いまどきの水準でみればこれは結構ちいさい。 けれど必要以上にでかいのも確かだ。

まず最初にやるべきは実行ファイルの strip 。

  $ gcc -Wall -s tiny.c
  $ ./a.out ; echo $?
  42
  $ wc -c a.out
     2632 a.out

これでだいぶよくなった。次に最適化はどうだろう?

  $ gcc -Wall -s -O3 tiny.c
  $ wc -c a.out
     2616 a.out

効き目はあるけれどちょっとだけだな。まあそりゃそうだろう。 最適化するものがほとんど何もないんだから。

一文だけの C のプログラムを縮める方法がそうそうあるとは思えない。 ここらで C とはおさらばしてアセンブラを使うことにしよう。 うまくいけば C が勝手に持ち込んだ余計なオーバーヘッドを切り捨てることができるかもしれない。

というわけでバージョン 2 はこれ。やるのは main() から 42 を返すことだけ。 それはつまり関数が eax レジスタに 42 をセットして戻るということになる。

  ; tiny.asm
  BITS 32
  GLOBAL main
  SECTION .text
  main:
                mov     eax, 42
                ret

ビルドして動かしてみよう

  $ nasm -f elf tiny.asm
  $ gcc -Wall -s tiny.o
  $ ./a.out ; echo $?
  42

(アセンブリが難しいって言ったの誰だよ...) で、大きさはどのくらい?

  $ wc -c a.out
     2604 a.out

削れたのは 12 バイトばかりか。これで C の勝手なオーバーヘッドはぜんぶ、なの?

いや、問題は main() インターフェイスを使う多大なオーバーヘッドだ。 リンカは OS とのインターフェイスをくっつけてくれて、 そのインターフェイスが main() を呼びだす。これが不要なら、なんとか回避できないだろうか。

というわけで、回避できるかやってみよう。自分で _start ルーチンを定義する。

  ; tiny.asm
  BITS 32
  GLOBAL _start
  SECTION .text
  _start:
                mov     eax, 42
                ret

gcc はうまいことやってくれるだろうか。

  $ nasm -f elf tiny.asm
  $ gcc -Wall -s tiny.o
  tiny.o(.text+0x0): multiple definition of `_start'
  /usr/lib/crt1.o(.text+0x0): first defined here
  /usr/lib/crt1.o(.text+0x36): undefined reference to `main'

ダメだった。いや、本当はやってくれるはずなんだけれど、 やってほしいことを教える方法は知る必要がある。 gcc が -nostartfiles というオプションを認識するとうまくいく。 gcc の info によると:

    -nostartfiles
    Do not use the standard system startup files when linking. 
    The standard libraries are used normally. 

よし! これで大丈夫かな。

  $ nasm -f elf tiny.asm
  $ gcc -Wall -s -nostartfiles tiny.o
  $ ./a.out ; echo $?
  Segmentation fault
  139

うーむ。 gcc は文句を言わなくなったけれどプログラムが動かない。 何がまずいんだろう。

何がまずいかというと、_start を C の関数みたいに使って値を返すのがまずい。 実際、こいつはぜんぜん関数じゃない。 これは単なるシンボルで、リンカはここをプログラムのエントリ・ポイントにする。 プログラムを起動すると、これが直に呼ばれる。 このプログラムでスタックの先頭を見ることができるなら、そこには数字の 1 があるはずだ。 とりあえずアドレスではなさそうな値。で、このスタックの値が何かというと、 argc の値なわけ。 次には NULL 終端された argv の配列がきて、evnp がつづく。それでおしまい。 戻り先のアドレスはスタックのどこにもない。

じゃあ _start はどう終わればいいのだろう。そこで exit() ですよ! なにしろそのためにある関数なんだから。

えー、ちょっと嘘をつきました。ほんとは _exit() を呼ぶのが正しい。 (先頭のアンダースコアに注目。) exit() はプロセスの途中でやったタスクの後始末をするんだけれど、 今回そのタスクをすることはない。 ライブラリの起動コードをすっとばしてしまったからね。 そんなわけでライブラリの終了コードは飛ばして直に OS の終了処理に進む必要がある。

で、再挑戦。_exit() を呼ぶんだった。この関数は整数をひとつ引数にとる。 だからその数字をスタックに積み、関数を呼べばいい。 (_exit() が外部関数だという宣言も必要。) こんなアセンブリになる。

  ; tiny.asm
  BITS 32
  EXTERN _exit
  GLOBAL _start
  SECTION .text
  _start:
                push    dword 42
                call    _exit

ビルドして試すよ。

  $ nasm -f elf tiny.asm
  $ gcc -Wall -s -nostartfiles tiny.o
  $ ./a.out ; echo $?
  42

やっと動いた! で、大きさは?

  $ wc -c a.out
     1340 a.out

半分! 悪くない。悪くないけど、んー... gcc には何か他に変なオプションがないものだろうか。

あ、ひとつあった。ドキュメントで -nostartfiles の次にあるやつ。気になる。

    -nostdlib
    リンク時に標準システム・ライブラリを使用しません。
    指定されたライブラリだけがリンカに渡されます。

試してみてよさそうだ。

  $ gcc -Wall -s -nostdlib tiny.o
  tiny.o(.text+0x6): undefined reference to `_exit'

あらま。たしかに _exit() だって結局はライブラリ関数だからねえ... かわりになるものが必要だな。

いやでもちょっと、たかがプログラムを終えるのに libc の助けがいるっての?

否。いらない。もし上品ぶって可搬性についてどうこう言わないと腹をくくれば、 何ひとつリンクせずにプログラムを終えることはできる。 ただし、まずは Linux がどうやてシステムコールを呼ぶのかを知らないといけない。


他の OS と同様、Linux も動かしているプログラムが必要とする基本的な機能を システムコールとして用意している。ファイルを開く、読み書きする。 プロセスの終了も当然ある。

Linux のシステムコール呼び出しはある命令がインターフェイスになっている: int 0x80 だ。 すべてのシステムコールがこの割り込みを使う。 システムコールを呼ぶためには、 eax レジスタに呼び出すシステムコールの番号を入れておく必要がある。 他のレジスタは引数を(あれば)を渡すのに使う。 引数ひとつなら ebx を、 ふたつなら ebx と ecx を、 同様に edx, esi, edi を 3, 4, 5 番目の引数に使う。以下同じ。 システムコールから戻ると、eax が戻り値を持っている。 エラーがあったら eax は負の数になる。絶対値がエラーの種別をあらわす。

システムコールの番号は /usr/include/asm/unistd.h に一覧がある。 ちょっと覗けばわかるけど、exit のシステムコールは 1 番だ。 C の関数と同じく引数はひとつで、プロセスの戻り値。なので ebx をつかって引数をわたす。

これで次のバージョンをどう書くかはわかったと思う。もう外の関数は一切必要ない。

  ; tiny.asm
  BITS 32
  GLOBAL _start
  SECTION .text
  _start:
                mov     eax, 1
                mov     ebx, 42  
                int     0x80

いくぜ。

  $ nasm -f elf tiny.asm
  $ gcc -Wall -s -nostdlib tiny.o
  $ ./a.out ; echo $?
  42

キター! サイズは ?

  $ wc -c a.out
      372 a.out

これだよ、これでこそ小さいってもんだ! 前回のおよそ 1/4 になった。

で... 他に何か小さくできる事はないもんですかねえ。

命令を短かくするってのはどうだろう。

生成されたコードを眺めてみると、こんな風になる。

  00000000 B801000000        mov        eax, 1
  00000005 BB2A000000        mov        ebx, 42
  0000000A CD80              int        0x80

まず ebx 全体を初期化する必要はない。OS は下位バイトしか見ないから。 bl でいい。そうすれば 5 バイトだったのが 2 バイトで済む。

あとは eax に xor でゼロをセットして、1 バイトで済むインクリメント命令を使ってみる。 これでもう 2 バイト削れた。

  00000000 31C0              xor        eax, eax
  00000002 40                inc        eax
  00000003 B32A              mov        bl, 42
  00000005 CD80              int        0x80

このプログラムをこれ以上短くするのは無理と見てよさそうだ。

ついでに、もう gcc を使ってバイナリを作るのはやめよう。その機能を何も使わなくなっている。 自分でリンカ、 ld を呼ぶことにしよう。

  $ nasm -f elf tiny.asm
  $ ld -s tiny.o
  $ ./a.out ; echo $?
  42
  $ wc -c a.out
      368 a.out

4 バイト縮んだ。 (ちょっと! 5 バイト削ったじゃん? たしかに削ったんだけれど、 ELF フォーマットの整列の都合で 1 バイトはパディングされる。)

さて...これで終わりですかね。できる限りのことはしただろうか。

や、うむ。こいつのコードの長さは 7 バイトだ。 ELF は本当に 361 バイトもオーバーヘッドがあるんだろうか。 なんにしろ、ファイルの中味は何が入っているんだろう。

objdump で覗いてみよう。

  $ objdump -x a.out | less

出力はうげって感じだけれど、今は Sections の部分に集中しよう。

  Sections:
  Idx Name          Size      VMA       LMA       File off  Algn
    0 .text         00000007  08048080  08048080  00000080  2**4
                    CONTENTS, ALLOC, LOAD, READONLY, CODE
    1 .comment      0000001c  00000000  00000000  00000087  2**0
  
.text セクションはぜんぶで 7 バイト。指示したとおりだ。 マシン語の中味は完全に掌握したと考えていい。

でも別の、.comment というセクションは何だ。誰がこんなものをつけろと頼んだ? 28 バイトもある! .comment だか何だか知らないが、こいつは誓って無用の代物だろう...

.comment セクションはリストのオフセット 00000087 (十六進) から始まっている。 hexdump を使って中味を覗いてみるとこんなかんじ。

  00000080: 31C0 40B3 2ACD 8000 5468 6520 4E65 7477  1.@.*...The Netw
  00000090: 6964 6520 4173 7365 6D62 6C65 7220 302E  ide Assembler 0.
  000000A0: 3938 0000 2E73 796D 7461 6200 2E73 7472  98...symtab..str

おいおいおい。まさか Nasm が邪魔をするとは思わなかったよ。 gas でも使ってみるか。AT&T 文法よ、さようなら...

まったく。こんなんでどうよ。

  ; tiny.s
  .globl _start
  .text
  _start:
                xorl    %eax, %eax
                incl    %eax
                movb    $42, %bl
                int     $0x80

で、

  $ gcc -s -nostdlib tiny.s
  $ ./a.out ; echo $?
  42
  $ wc -c a.out
      368 a.out

変わんねえーー!

まあ、違いがあるにはあるんだけれど。また objdump を使ってみよう。

  Sections:
  Idx Name          Size      VMA       LMA       File off  Algn
    0 .text         00000007  08048074  08048074  00000074  2**2
                    CONTENTS, ALLOC, LOAD, READONLY, CODE
    1 .data         00000000  0804907c  0804907c  0000007c  2**2
                    CONTENTS, ALLOC, LOAD, DATA
    2 .bss          00000000  0804907c  0804907c  0000007c  2**2
                    ALLOC

コメントのセクションはなくなった。 かわりにありもしないデータを保存するための無駄セクションができてしまった。 このセクションは 0 バイト長のはずが、オーバーヘッドになっている。 ファイルサイズをでかくしていい正当な理由はない。

よし、こいつらが全てオーバーヘッドだとして、どうやって回避できるだろう。

この疑問に答えたいなら、いよいよ真のウィザードヘの道を歩みはじめることになる。 ELF フォーマットの中味を紐解く必要があるのだ。


i386 アーキテクチャ向けの ELF フォーマットについて書いた正式なドキュメントは ftp://tsx.mit.edu/pub/linux/packages/GCC/ELF.doc.tar.gz にある。 Postscript が面倒な人向けに テキストバージョンをつくっておく。 http://www.muppetlabs.com/~breadbox/software/ELF.txt この仕様は扱う範囲が大きいから、読むのが面倒な人がいても気持はわかる。 だいたい必要なことを以下にまとめてみた。

全ての ELF ファイルは ELF ヘッダという構造体から始まる。 この構造体は 52 バイトの大きさで、ファイルの中味に関する様々な情報を記している。 たとえば最初の 16 バイトは "identifier" といい、 マジックナンバーの署名(7F 45 4C 46)や 1 バイトのフラグ (32 ビットか 64ビットか、リトルエンディアンかビッグエンディアンか) などからなる。 ELF ヘッダのこの他のフィールドは次のようなものだ: 対象アーキテクチャ、ファイルは実行ファイルかオブジェクトファイルか共有ライブラリか。 プログラムの開始アドレス、あとはプログラムヘッダ表とセクションヘッダ表。

このふたつの表はファイルのどこにあってもいい。 ただ普通は前者が ELF ヘッダの直後、後者はファイルの終端近くにある。 二つの表には似たような役割がある。ファイルの内部がどんなコンポーネントに わかれているかを示しているのだ。違うのは、 セクションヘッダ表はファイルのどの位置に どのようなコンポーネントがあるのかに的をしぼっており、 対するプログラムヘッダ表はそのコンポーネントをメモリのどの位置にどうやってロードするかを 示している。 端的にいうと、セクションヘッダ表はコンパイラやリンカが使い、 プログラムヘッダ表はローダが使う。 オブジェクトファイルの場合、プログラムヘッダ表はオプション扱いだ。実際ついていることはない。 似たようなかんじで、実行ファイルの場合はセクションヘッダ表がオプション扱いになる。 ところがこいつは "かならず" ついている!

というわけで、これが最初の疑問の答えだ。 あのプログラムのオーバーヘッドのうち結構な部分が要りもしないセクションヘッダ表だった。 他のセクションのうちのいくつかも同様で、 プログラムのメモリ上での状態をつくるのに不要かもしれない。

次の疑問に移ろう。こいつらをどうやって排除すればいいのか?

やれやれ。やっとここまで来たというのに、 標準のツールはどれもセクションヘッダ表なしに何かしらのバイナリを作る機能がない。 そういうのが欲しいなら、自分で手をうつ必要がある。

これは別にバイナリエディタを起動してチマチマ手で十六進数をいじる、ということではない。 古き良き Nasm には素のバイナリを出力できるフォーマットがある。こいつを使えばいい。 あと必要なのは、空の ELF 実行ファイルのイメージデータだ。 それにあのプログラムを詰める。あのプログラムだけをね。他のものは要らない。

ELF の仕様、/usr/include/linux/elf.h、あとは出来合いの実行ファイルを見たりすれば、 空の 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      ehdrsize                        ;   e_ehsize
                dw      phdrsize                        ;   e_phentsize
                dw      1                               ;   e_phnum
                dw      0                               ;   e_shentsize
                dw      0                               ;   e_shnum
                dw      0                               ;   e_shstrndx
  
  ehdrsize      equ     $ - ehdr
  
  phdr:                                                 ; Elf32_Phdr
                dd      1                               ;   p_type
                dd      0                               ;   p_offset
                dd      $$                              ;   p_vaddr
                dd      $$                              ;   p_paddr
                dd      filesize                        ;   p_filesz
                dd      filesize                        ;   p_memsz
                dd      5                               ;   p_flags
                dd      0x1000                          ;   p_align
  
  phdrsize      equ     $ - phdr
  
  _start:
  
  ; your program here
  
  filesize      equ     $ - $$

このイメージデータの ELF ヘッダは、 ファイルが Intel 386 向けの実行ファイルであることを示している。 セクションヘッダ表はなく、プログラムヘッダ表がひとつある。 その表にはのエントリがひとつあって、 メモリの0x08048000 番地 (デフォルト) に ファイル全体を読み込むよう指示している。 (ELF ヘッダやプログラムヘッダ表をデータにもつファイルにすると、これは一般的な挙動だ。) それから、_start から実行を始めるようにいっている。 これはプログラムヘッダ表の直後に来る。 .data セグメントはない。 .bss セグメントも、 コメントもない。ただ最低限のものだけがある。

じゃあ、例の小さなプログラムを追加しよう。

  ; tiny.asm
                org     0x08048000
  
  ;
  ; (as above)
  ;
  
  _start:
            mov bl, 42 
            xor eax, eax 
            inc eax 
            int 0x80 
  filesize  equ $ - $$
  
で、試そう。

  $ nasm -f bin -o a.out tiny.asm
  $ chmod +x a.out
  $ ./a.out ; echo $?
  42

今まさにスクラッチから実行ファイルを作ることができた。どうよ。 あとサイズも見ておこう。

  $ wc -c a.out
       91 a.out

きゅうじゅういちバイト。前回の 4 分の 1、初回の "40" 分の 1 だ!

これ以上なにがあるだろう。今やどのバイトが何をするかぜんぶ説明できる。 実行ファイルの中味が何にあって何をするものなのか、すっかり把握している。 限界だ。もうこれ以上小さくすることはできない。

できるって?


さてと。ここらで一旦 ELF の仕様読みは一段落しよう。読んだ人は二つのことに気がついたはずだ。 1. ELF ファイルの個々の部分はどこに位置していてもいい (ELF ヘッダは例外。これはファイルの先頭に必要。) 2. ヘッダのフィールドのうちいくつかは使われていない。

特に、16 バイトの identifier の最後にならぶ 9 バイト分のゼロのことは考えてしまう。 こいつらは純粋なパディングで、ELF 標準が将来の拡張のために残しているものだ。 だから OS はここに何があっても気にしないはず。 しかもどうせメモリには全部読みこまれる。 で、例のプログラムは 7 バイトしかない ...

ELF ヘッダにコード入らんかな。

別にいいよね?

  ; tiny.asm
  
  BITS 32
  
                org     0x08048000
  
  ehdr:                                                 ; Elf32_Ehdr
                db      0x7F, "ELF"                     ;   e_ident
                db      1, 1, 1, 0
  _start:       mov     bl, 42
                xor     eax, eax
                inc     eax
                int     0x80
		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      ehdrsize                        ;   e_ehsize
                dw      phdrsize                        ;   e_phentsize
                dw      1                               ;   e_phnum
                dw      0                               ;   e_shentsize
                dw      0                               ;   e_shnum
                dw      0                               ;   e_shstrndx
  
  ehdrsize      equ     $ - ehdr
  
  phdr:                                                 ; Elf32_Phdr
                dd      1                               ;   p_type
                dd      0                               ;   p_offset
                dd      $$                              ;   p_vaddr
                dd      $$                              ;   p_paddr
                dd      filesize                        ;   p_filesz
                dd      filesize                        ;   p_memsz
                dd      5                               ;   p_flags
                dd      0x1000                          ;   p_align
  
  phdrsize      equ     $ - phdr
  
  filesize      equ     $ - $$

結局バイトはバイトってこと。

  $ nasm -f bin -o a.out tiny.asm
  $ chmod +x a.out
  $ ./a.out ; echo $?
  42
  $ wc -c a.out
       84 a.out

なかなかのもんでしょ。

これでもう下るところまで下ってきたなあ。 このファイルは ELF ヘッダとプログラムヘッダ表ひとつ分の長さぴったりしかない。 プログラムをメモリにロードして動かすには、どちらも絶対に必要なものだ。 もはや何も削るものはなくなった!

ただし...

もし、さっきコードにやったのと同じことをプログラムヘッダ表にもできたらどうだろう。 あれを ELF のヘッダに重ねる。できるかな。

そこで、まずプログラムを眺めてみよう。よく見ると ELF ヘッダの最後の 8 バイトは プログラムヘッダ表の最初の 8 バイトと類似している。 ある種の類似は "同じ" と言っていいかもしれない.

だから...

; tiny.asm

  
  BITS 32
  
                org     0x08048000
  
  ehdr:
                db      0x7F, "ELF"             ; e_ident
                db      1, 1, 1, 0
  _start:       mov     bl, 42
                xor     eax, eax
                inc     eax
                int     0x80
                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      ehdrsize                ; e_ehsize
                dw      phdrsize                ; e_phentsize
  phdr:         dd      1                       ; e_phnum       ; p_type
                                                ; e_shentsize
                dd      0                       ; e_shnum       ; p_offset
                                                ; e_shstrndx
  ehdrsize      equ     $ - ehdr
                dd      $$                                      ; p_vaddr
                dd      $$                                      ; p_paddr
                dd      filesize                                ; p_filesz
                dd      filesize                                ; p_memsz
                dd      5                                       ; p_flags
                dd      0x1000                                  ; p_align
  phdrsize      equ     $ - phdr
  
  filesize      equ     $ - $$

思ったとおり、Linux は 1 ビットくらいのみみっちいことは気にしなかった。

  $ nasm -f bin -o a.out tiny.asm
  $ chmod +x a.out
  $ ./a.out ; echo $?
  42
  $ wc -c a.out
       76 a.out

これで "本当に" もう降りるところまで降りたなあ。 もう構造体に重ねられる場所はない。なにせバイト列が一致しない。 最後まで来たってわけだ。

ただし、その、もし中味が一致するようにバイト列をいじれるなら話は別だけど...

実際のところ、Linux は構造体のフィールドのうちどれだけを見ているんだろう。 たとえば e_machine の値が 3 (intel 386 用を指す) だとかを見てるんだろうか。 決めうちしちゃってたりしないかな。

結論からいうと、この値はチェックされている。 でも他のフィールドの多くはびっくりするほど見事に無視されている。

つまりだ。 ELF ヘッダには意味のある値とない値がある。 最初の 4 バイトはマジックナンバーで、これが違うと Linux はファイルを使ってくれない。 次の e_field の 3 バイトはチェックしない。 これは結局 12 バイトの連続領域を好きにしていいってことだ。 e_type は 2 でないといけない。実行ファイルの意味になる。 e_machine は既に言ったとおり 3 にする。 e_version は、e_ident 内の他のバージョンと同じで、完全に無視される。 (現在 ELF 標準が 1 バージョンしかないことを考えると、これは理解できる。) e_entry は普通に考えて有意だ。プログムの開始位置が入っているんだから。 それに e_phoff はプログラムヘッダ表の正しい位置を入れる必要がある。 e_phnum も正しい表のエントリ数が必要。 e_flags はというと、Intel 系では未使用とドキュメントに書いてある。再利用は自由だね。 e_ehsize は ELF ヘッダが期待したサイズかを検証するのに使うんだけれど、 Linux は気にしちゃいない。e_phentsize も似た用途で、プログラムヘッダ表の検証用。 これはチェックしている。ただし 2.2 系カーネルの 2.2.17 以降。 2.2 系以前は無視してる。2.4.0 もそう。 ELF ヘッダの残りの部分はセクションヘッダ表に関するもので、実行ファイルだと出番はない。

それとプログラムヘッダ表の中味はどうだろう。 どれ、 p_type は 1 が入ってる。これはロード可能なセグメントだという意味。 p_offset は必須で、ロードのためのファイルのオフセットが入る。 同様に p_vaddr もロードするアドレスなので必要。 ただしロード先が 0x08048000 である必要はない。 0x00000000 以上 0x80000000 未満ならどこでもいい。ページには揃えること。 p_paddr は無視すると文書化されている。好きにしていい。 p_filesz はファイルのうちどれだけメモリにロードするかを表す。 p_memz はどれだけメモリセグメントが必要かをあらわす。このへんは割と意味がありそう。 p_flags はメモリセグメントのパーミッション。 読み込み可能 (4) でないと困る。読めない。あと実行可能 (1) も必要。 でないと中のコードを実行できない。他のビットは好きにできるけど、これらは必要だ。 最後に、p_align はメモリセグメントの整列の要件をあらわす。 このフィールドは主に位置独立なコードを再配置するのに使う。(共有ライブラリ用。) そして実行ファイルの場合、Linux はこれを無視する。

全体的に割と融通は効きそうだ。 特に、じっと睨むとわかるけれど、ELF ファイルの必須のフィールドは前半に集中している。 後半はほとんど書き換え放題。これを踏まえた上で、 二つの構造体をさっきより更に混ぜてみよう。

  ; tiny.asm
  
  BITS 32
  
                org     0x00200000
  
                db      0x7F, "ELF"             ; e_ident
                db      1, 1, 1, 0
  _start:
                mov     bl, 42
                xor     eax, eax
                inc     eax
                int     0x80
                db      0
                dw      2                       ; e_type
                dw      3                       ; e_machine
                dd      1                       ; e_version
                dd      _start                  ; e_entry
                dd      phdr - $$               ; e_phoff
  phdr:         dd      1                       ; e_shoff       ; p_type
                dd      0                       ; e_flags       ; p_offset
                dd      $$                      ; e_ehsize      ; p_vaddr
                                                ; e_phentsize
                dw      1                       ; e_phnum       ; p_paddr
                dw      0                       ; e_shentsize
                dd      filesize                ; e_shnum       ; p_filesz
                                                ; e_shstrndx
                dd      filesize                                ; p_memsz
                dd      5                                       ; p_flags
                dd      0x1000                                  ; p_align
  
  filesize      equ     $ - $$

見ればわかる(わかってほしい)ように、 プログラムヘッダ表の冒頭 20 バイトが ELF ヘッダ末尾の 12 バイトと重なっている。 このふたつがぴたりとはまった。実際のところ重なったと言えるのは 2 箇所だけだ。 一つ目は e_phnum フィールドで、 これはうまい具合に数すくない無視フィールドの p_paddr に重なった。 もう一つは e_phentsize フィールド。こちらは p_vaddr に重なった。 ここでは標準と違うロード先アドレスとして上半分からきた値の 0x0020 を使うことで 値を一致させることができた。

これで、本当に可搬性の衣を脱ぎすててしまった...

  $ nasm -f bin -o a.out tiny.asm
  $ chmod +x a.out
  $ ./a.out ; echo $?
  42
  $ wc -c a.out
       64 a.out

...それでも動く! しかもプログラムは 12 バイト短い。ぴったり予想どおり。

これで何もやりのこすことはない、わけでもないのはわかるよね ... プログラムヘッダ表を完全に ELF ヘッダに押しこめられればいい。 その聖杯を手にすることはできるだろうか ?

ただ、残りの 12 バイトを障害物でもかわすようにフィールドを微調整するのは無理がある。 残された可能性は、それを最初の 4 バイトの直後に置くことだ。 こうすると最初のプログラムヘッダ表はうまく e_ident の範囲におさまる。 でも残りの部分に問題はのこる。しばらく実験してみたが、この路線は不可能に思えてきた。

ところが、実はプログラムヘッダ表にはもう二つばかりよけてしまえるフィールドがあった。

前に p_memsz はメモリセグメントのために確保するメモリ量だと書いた。 その値が p_filesz より大きい場合、たしかにこの値は必要だ。 ところが向こうの方が大きいなら...

もうひとつ、あらゆる予想に反して、実は p_flags の実行ビットはなくても平気だった。 Linux が勝手にセットしてくれる。なぜこれが動くのかは正直よくわからない .... エントリポイントならいいのかもしれない。まあ何にせよ動く。

というわけで、この事実を踏まえたゲテモノファイルをつくりあげよう。

  ; tiny.asm
  
  BITS 32
  
                org     0x00001000
  
                db      0x7F, "ELF"             ; e_ident
                dd      1                                       ; p_type
                dd      0                                       ; p_offset
                dd      $$                                      ; p_vaddr 
                dw      2                       ; e_type        ; p_paddr
                dw      3                       ; e_machine
                dd      filesize                ; e_version     ; p_filesz
                dd      _start                  ; e_entry       ; p_memsz
                dd      4                       ; e_phoff       ; p_flags
  _start:
                mov     bl, 42                  ; e_shoff       ; p_align
                xor     eax, eax
                inc     eax                     ; e_flags
                int     0x80
                db      0
                dw      0x34                    ; e_ehsize
                dw      0x20                    ; e_phentsize
                dw      1                       ; e_phnum
                dw      0                       ; e_shentsize
                dw      0                       ; e_shnum
                dw      0                       ; e_shstrndx
  
  filesize      equ     $ - $$

p_flags が 5 から 4 になった。これでも平気と言ったとおり。 この 4 は同時に e_phoff の値になっている。 これはプログラムヘッダ表のオフセットになっている。まさに今置いた場所だ。 プログラム (があったの覚えてた?) は ELF ヘッダの下部に移し、 e_shoff フィールドから始まって e_flags フィールドのところで終わる。

ロード先アドレスが更に低い数字になったのに注目。できる限りのところまで引き下げてみた。 そのおかげで e_entry フィールドが妥当な小ささに収まっている。 これは都合がいい。といのはこれは p_memsz の値でもあるからだ。 (まあ仮想記憶があればほとんど問題にはならないけれど。 たぶんもとの値のままでも動くと思う。でも礼儀正しいに越したことはないよね。)

そして...

  $ nasm -f bin -o a.out tiny.asm
  $ chmod +x a.out
  $ ./a.out ; echo $?
  42
  $ wc -c a.out
       52 a.out

これでプログラムヘッダ表とプログラム自身を完全に ELF ヘッダに埋めこむことできた。 実行ファイルの大きさは ELF ヘッダの大きさとぴったり同じ! 大きくもないし小さくもない。しかもまだ、 Linux で文句なしに動くんだ。

さて、ようやく、真の可能な絶対最小に辿りついたというわけだ。疑問の余地はない。 でしょ。実際、これは完全な ELF ヘッダだ。(ちょっと不味いいじり方をしているけれど。) そうでもなければ Linux はかまってくれないよ。

本当 ?

いーや。最後にひとつ、とっておきのダーティ・トリックがある。

一見ファイルが ELF ヘッダのサイズより小さいのはまずそうだけれど、 Linux はうまくやってくれる。足りないバイトはゼロで埋まる。 例のやつの末尾には 7 つばかりゼロがある。こいつをファイルから取ってしまうのはどうだろう。

  ; tiny.asm
  
  BITS 32
  
                org     0x00001000
  
                db      0x7F, "ELF"             ; e_ident
                dd      1                                       ; p_type
                dd      0                                       ; p_offset
                dd      $$                                      ; p_vaddr 
                dw      2                       ; e_type        ; p_paddr
                dw      3                       ; e_machine
                dd      filesize                ; e_version     ; p_filesz
                dd      _start                  ; e_entry       ; p_memsz
                dd      4                       ; e_phoff       ; p_flags
  _start:
                mov     bl, 42                  ; e_shoff       ; p_align
                xor     eax, eax
                inc     eax                     ; e_flags
                int     0x80
                db      0
                dw      0x34                    ; e_ehsize
                dw      0x20                    ; e_phentsize
                db      1                       ; e_phnum
                                                ; e_shentsize
                                                ; e_shnum
                                                ; e_shstrndx
  
  filesize      equ     $ - $$

... これでも、信じがたいことだけど、動く実行ファイルができる。

  $ nasm -f bin -o a.out tiny.asm
  $ chmod +x a.out
  $ ./a.out ; echo $?
  42
  $ wc -c a.out
       45 a.out

ここまでだ。これでようやく、偽りなく限界の距離まできた。 このファイルの 45 バイト目をかわす抜け道はどこにもない。 この値はプログラムヘッダ表の大きさを指定し、ゼロではなく、 存在しなければならず、しかも ELF ヘッダの先頭から 45 バイト目に必要なのだ。 もうこれ以上できることはないと結論せざるを得ない。


この 45 バイトのファイルは、 標準のツールを使ってつくれる最小サイズの八分の一で、 純粋な C のコードから作れる最小サイズの五十分の一だ。 出来る限りのものを削ぎ落し、ほとんど不可能な二重化までした。

もちろん、ファイルの中の半数の値は ELF 標準に違反している。 Linux が吐きだしても不思議じゃない。プロセスIDなんて貰えそうにない。 そもそもこんなプログラムを作ろうなんて思わない。

一方で、実行ファイルのどのバイトも、責任と存在感を持っている。 これまで作ったプログラムのどれだけが、そんなことを言えるだろうか。


おしまい

コメント

(Too many spams ... embedded comments are not allowed now, sorry.)