ELF形式の最小限のプログラム

libcのない世界からの記事Helloに触発されて、私も同様のことをすることにしました。 これをあからさまにしないために、私は次のタスクを自分で設定することにしました。 「ELF、hello!」のような単純な文字列を出力するプログラムを作成します。 実行可能ファイルでどのように表示されるかを理解します。 さて、途中で、100バイト以内に収まるようにしてください。



まず第一に、C ++の標準helloworld



#include <iostream> using namespace std; int main() { cout << "ELF, hello!\n"; return 0; }
      
      





コンパイルし、サイズを確認します。



 $ g++ test.cpp -static && ls -s -h a.out 1,3M a.out
      
      







いくら? 1.3 Mb? 12バイトの単一のメッセージを出力するには? うーん...さて、Cを試してみましょう。



 #include "stdio.h" int main() { printf("ELF, hello!\n"); return 0; }
      
      





また、コンパイルします。 コンパイル時に-staticオプションを指定しました-実行されるコード全体に興味があります。 動的コンパイルでは、サイズは確かに小さくなりますが、それでも私たちが望むほど大きくはありません。



 $ gcc test.c -static && ls -s -h a.out 568K a.out
      
      







半メガバイト以下。 ここに、STLの支払いがあります。 しかし、まだたくさんあります。 どうやら、アセンブラーの形で重い砲兵が不可欠です。 helloworldはasm上に、stdlibなしで記述します。 私はAT&T構文を好みます。



 .data str: .ascii "ELF, hello!" .byte 10 .text .global _start _start: movl $4, %eax movl $1, %ebx movl $str, %ecx movl $12, %edx int $0x80 movl $1, %eax movl $0, %ebx int $0x80
      
      







データセクションの2つのセクション-メッセージ(および新しい行に変換するための10)、コードセクション(.text)-80回目の割り込みを2回(レジスタの必要なパラメーターを使用して)呼び出し、メッセージを表示します。正しく完了するための2回目。



作成されたプログラムをコンパイルします(または翻訳してリンクします)。



 $ gcc easy.s -nostdlib && du -sb a.out 752 a.out
      
      







752バイト-これはすでに必要なものにはるかに近いです。 stripユーティリティでデバッグシンボルを削除します。



 $ strip a.out && du -sb a.out 476 a.out
      
      







より良いが、それでも十分ではない。 476バイトのファイルには何が含まれていますか? objdumpを使用してa.outを逆アセンブルします。



 $ objdump -D a.out a.out: file format elf32-i386 Disassembly of section .note.gnu.build-id: 08048094 <.note.gnu.build-id>: 8048094: 04 00 add $0x0,%al ... - ,     ... 80480b6: b6 08 mov $0x8,%dh Disassembly of section .text: 080480b8 <.text>: 80480b8: b8 04 00 00 00 mov $0x4,%eax 80480bd: bb 01 00 00 00 mov $0x1,%ebx 80480c2: b9 dc 90 04 08 mov $0x80490dc,%ecx 80480c7: ba 0c 00 00 00 mov $0xc,%edx 80480cc: cd 80 int $0x80 80480ce: b8 01 00 00 00 mov $0x1,%eax 80480d3: bb 00 00 00 00 mov $0x0,%ebx 80480d8: cd 80 int $0x80 Disassembly of section .data: 080490dc <.data>: 80490dc: 45 inc %ebp 80490dd: 4c dec %esp 80490de: 46 inc %esi 80490df: 2c 20 sub $0x20,%al 80490e1: 68 65 6c 6c 6f push $0x6f6c6c65 80490e6: 21 0a and %ecx,(%edx)
      
      







そのため、3つのセクションがありますが、2つしか書かれていません。 .textセクションにはコードがあります。 データセクション-12バイト形式のelfこんにちは(objdumpも逆アセンブル)。 .note.gnu.build-idセクションは他に何ですか? 注文しなかったため、大胆に削除しました。



 $ strip -R .note.gnu.build-id a.out && du -sb a.out 416 a.out
      
      







さらに60バイトが勝ちました。 悪くない。 コードを少し最適化してみましょう。 第一に、プログラムは原則として任意のコードで終了でき、必ずしもゼロで終了するわけではありません。 第二に、プログラムが起動すると、レジスタがリセットされます(ただし、実際のプログラムを作成するときにこれに依存するべきではありません。作成しているシステムのABIを確認してください)。

その結果、5バイトに変換されるmovl $ 4、%eaxの代わりに、2バイトに変換されるmovb $ 4、%alを使用できます。 第三に、最後の中断の後にコードに行を配置することで.dataセクションを取り除きます(とにかく、プログラムはそれ以上実行されません):



 .text .global _start _start: movb $4, %al movb $1, %bl movl $str, %ecx movb $12, %dl int $0x80 movb $1, %al int $0x80 str: .ascii "ELF, hello!" .byte 10
      
      







コンパイルし、余分な部分を削除して、サイズを確認します。



 $ gcc -nostdlib easy.s $ strip a.out $ strip -R .note.gnu.build-id a.out $ du -sb a.out 320 a.out
      
      







限界に達したようです。 320バイト-それ以上。 かどうか? これらの320バイトはどこから来たのですか? 私たちのコードは明らかに少ないです。 ただし、コードに加えて、バイナリにはELFヘッダーもあります。 そして、本当に最小限のプログラムを作成したい場合、ELF記述(たとえば、 ここ )を開き、ヘッダーを手動で作成する必要があります。



手動-これは16進エディターでは意味がありません。 リンカに対して、ファイルに何も割り当てる必要がないことを明確にするだけで、出力に記述したとおりに出力されます。 確かに、この場合、ファイルを開始する責任はすべて私たちにあります。

手動でコンパイルされたヘッダーを使用したプログラムの実装、私はこれを得ました:



  .set ofs, 0x10000 /* ofs -    */ /* ELF : */ .byte 0x7F .ascii "ELF" .long 0, 0, 0 /* ident */ .word 2 /* type */ .word 3 /* machine */ .long 0 /* version */ .long _start + ofs /* entry -    () */ .long phdr /* phoff -    (phdr) ( ) */ .long 0 /* shoff */ .long 0 /* flags */ .word 0 /* ehsize -  elf  */ .word phdrsize /* phentsize -  .  */ .word 1 /* phnum -  . . */ .word 0 /* shentsize */ .word 0 /* shnum */ .word 0 /* e_shstrndx */ /*   */ phdr: .long 1 /* type */ .long 0 /* offset */ .long ofs /* vaddr -      (  ) */ .long 0 /* paddr */ .long filesize /* filesz -     */ .long filesize /* memsz -     */ .long 5 /* pflags */ .long 0 /* palign */ .set phdrsize, . - phdr _start: /*   */ movb $4, %al movb $1, %bl movl $(str+ofs), %ecx movb $12, %dl int $0x80 movb $1, %al int $0x80 str: .ascii "ELF, hello!" .byte 10 .set filesize, .
      
      







また、プログラムオフセットを使用して手動で操作する必要があります。 簡略化すると、オフセットによって、プログラム内のコードとRAM内のコードのアドレス指定の違いを理解できます(実際、RAMにはまったく存在しませんが、それは別の話です)。 通常、リンカは必要な変位の決定に関与しますが、現在は私たち自身で作業しています。 ofsパラメーターにオフセットを配置しました。 オフセットサイズは、私の車で可能な限り最小(10,000)でした。 デフォルトでは8048000ですが、これは前提条件ではありません。



ELFヘッダー自体は、実際には1つのELFヘッダーではありません。 少なくとも2つ(エルフヘッダーとプログラムヘッダー)が必要です。 一般に、まだセクションヘッダーはありますが、スペースを節約するために使用することはありません。 経験的に、使用されるヘッダーフィールドは設定されています。 残りはゼロで埋められました。



プログラムをブロードキャストし、今度は手動でasおよびldを呼び出します。



 $ as w3test.s -o w3test.o $ ld -Ttext 0 --oformat binary -o w3test w3test.o $ du -sb w3test 115 w3test
      
      







115バイト! 元のバージョンの1万倍以下。 すべてが見えるでしょう。 実行するのに必要な最小限だけがあり、それ以上はありません。 そして、100バイトを克服する最初のタスクは失敗します。 ただし、これは制限ではありません! ヘッダーには未使用のバイトがあります。つまり、目的に合わせて使用​​できます。 残念ながら、コード自体はどのフィールドにも収まらず、大きすぎます。 しかし、線は収まります。



よく見ると、ELF IDの直後に、long型の3つの未使用フィールドがあります(各4バイト)。 これは、そこに線を入れることができることを意味します。 さらに、アスキー文字の形式で「ELF」がすでにあるため、行全体ではなく、最後の部分のみです。



さらに、phdヘッダーをelfの後ろではなく、ELFで使用される最後のバイトの直後に配置することにより、コードを短縮できます。 つまり、phdヘッダーはelfでわずかに階層化されますが、階層化されたフィールドはelfでは使用されないため、これは結果を引き起こしません。

同様に、プログラムをphdヘッダーに「レイヤー化」して配置できます(同じ理由で)。



結果は次のコードです。



  .set ofs, 0x10000 /* ofs -    */ /* ELF : 8*/ .byte 0x7F str: .ascii "ELF" .ascii ", hello!" .byte 10, 0, 0, 0 .word 2 /* type */ .word 3 /* machine */ .long 0 /* version */ .long _start+ofs /* entry -    () */ .long phdr /* phoff -    (phdr) ( ) */ .long 0 /* shoff */ .long 0 /* flags */ .word 0 /* ehsize -  elf  */ .word phdrsize /* phentsize -  .  */ /*   */ phdr: .long 1 /* type */ .long 0 /* offset */ .long ofs /* vaddr -      (  ) */ .long 0 /* paddr */ .long filesize /* filesz -     */ .long filesize /* memsz -     */ .long 5 /* pflags */ .set phdrsize, . - phdr + 4 _start: /*   */ movb $4, %al movb $1, %bl movl $(str+ofs), %ecx movb $12, %dl int $0x80 movb $1, %al int $0x80 .set filesize, .
      
      







ブロードキャストの後、サイズが89バイトのプログラムを取得します。 タスクが完了したと考えることができます。



最適化のアイデアもありました-elfヘッダー内にphdヘッダーをプッシュします。 しかし、10,000の最小変位では、構造の必要なフィールドが一致するように、そのようなパラメーターを選択することができなかったため、このアイデアは失敗しました。



PSコメントでは、61バイトのサイズでさらに最適化されたオプションが提案されました。このオプションでは、phdをelfに重ね合わせることができました。 -f binオプションを指定してnasm / yasmを使用してコンパイルしました。



 BITS 32; ORG 05430000h; DB 0x7F, "ELF"; DD 01h, 00h, $$; DW 02h, 03h; DD @main; DW @main - $$; @main: INC EBX; DB 05h; <-- ADD EAX, DD 04h; <-- LONG(04h) MOV ECX, @text; MOV DL, 12; INT 80h; AND EAX, 00010020h; XCHG EAX, EBX; INT 80h; @text: DB "ELF, hello!", 0Ah;
      
      







情報源



wikibooks.org -Linux上のCプログラマー向けアセンブラー

stackoverflow.com -20バイト未満の「Hello World」

muppetlabs.com -Linux用のTeensy ELF実行可能ファイル



All Articles