hello worldなELFバイナリを出力するCのプログラム(の一番単純な奴)
こちらの記事(Binary HacksのHack #25の軽い補足)は、「インラインアセンブラをちょっとだけ使って、gccに小さなhello worldバイナリを出力させる」というお話でした。一方、小さいHello Worldが欲しかったら、gccにELF実行バイナリを出力させるのではなく、「自力でELFを吐くCのコードを書いてしまう」手もあります。ご利用のCPUのアセンブリ言語がわかるのでしたら、こっちの方法でHello Worldするのも悪くないですね。こちらの方法でしたら、「急にbrainf*ckのコンパイラが書きたくなった*1」などの非常によくあるシチュエーションにも応用が効きますしー。
ELF直書きって、なんかすごく難しいように思われていると思うんですが(いや、DSO吐いたりするのは実際面倒ですが)、hello worldくらいだったらなんてことないです。次の4つの処理を順に行えばいいだけです。
- ELFヘッダをwrite
- プログラムヘッダをwrite
- 文字列 "Hello World!\n" をwrite
- システムコールを使って文字列を出力するコードをwrite
以下にELFを吐くCプログラムの雛型的なものを示しますので、ELFゴルファーになる前の学習ネタなんかに使っていただければと。必ずしもC言語で書かなくてもいいんですが(そのほうがタイプ数は少ない)、最初はelf.hに沿ってコードを書く方がわかりやすい(ひともいる)かと。
基本方針
標準出力にELFなバイナリをwriteします。できあがったバイナリの全体をそのままメモリにロードします(カーネルにロードしてもらいます)。本当は、上記の3.と4.だけロードすればよいのですが、いろいろ面倒なんで全体を。ロードするアドレスは、LOAD_ADDRESS とします。単純に 0x00000000 でもいいですし、お好きな場所に城を建ててもいいです。
1. ELFヘッダ書き
/usr/include/elf.h に、Elf32_Ehdr という構造体があります。この構造体のメンバに適当な値をセットして、そのままwriteすればOKです。e_entryメンバ以外は、決まり文句です。e_entryには、上記4.の(ファイルオフセットではなくメモリ上での)アドレスを指定します。埋めてある定数についての詳細は、elf(5)のmanか、Binary Hacksか、Linkers&Loaders あたりを...。あーもちろん、BinaryHacks のukaiさんの記事が一番お薦めです(宣伝)。
void out_elf_header() { Elf32_Ehdr ehdr = { .e_ident = { ELFMAG0, ELFMAG1, ELFMAG2 ,ELFMAG3, ELFCLASS32, ELFDATA2LSB, EV_CURRENT, ELFOSABI_SYSV }, .e_type = ET_EXEC, .e_machine = EM_386, .e_version = EV_CURRENT, .e_entry = LOAD_ADDRESS + sizeof(Elf32_Ehdr) + sizeof(Elf32_Phdr) + STRING_LEN, .e_phoff = sizeof(Elf32_Ehdr), .e_shoff = 0, // dummy .e_flags = 0x0, .e_ehsize = sizeof(Elf32_Ehdr), .e_phentsize = sizeof(Elf32_Phdr), .e_phnum = 1, .e_shentsize = 0, // dummy .e_shnum = 0, .e_shstrndx = 0, // dummy }; write(1, &ehdr, sizeof(Elf32_Ehdr)); }
2. プログラムヘッダ書き
同じく Elf32_Phdr をwriteするだけです。最低限、PT_LOAD なヘッダを一つ書けばOKです*2。p_offsetは、ファイルのどこからメモリに貼るかのオフセット値です。全体を貼るので、0x0にします。埋める値の計算が面倒なのは、p_fileszとp_memszくらいでしょうか。1.〜4.の合計サイズを書きます。p_vaddrには、ファイルをメモリのどこにロードして欲しいかを書きます。カーネル様への指示ですね。それ以外は定型文です。なお、p_offset % p_align == 0 かつ p_vaddr % p_align == 0 でないとまずかったような気がするんですが詳しくはゴルファーの皆様のページでも見てください。
void out_program_header() { uintptr_t code_len = (uintptr_t)&end_ - (uintptr_t)&start_; Elf32_Phdr phdr = { .p_type = PT_LOAD, .p_offset = 0x0, .p_vaddr = LOAD_ADDRESS, .p_paddr = 0, // dummy .p_filesz = sizeof(Elf32_Ehdr) + sizeof(Elf32_Phdr) + STRING_LEN + code_len, .p_memsz = sizeof(Elf32_Ehdr) + sizeof(Elf32_Phdr) + STRING_LEN + code_len, /* BSSが欲しいならここを増やす */ .p_flags = PF_R | PF_X, .p_align = 0x1000, }; write(1, &phdr, sizeof(Elf32_Phdr)); }
3. 文字列書き
単に、write(1, "Hello World!\n", 13); するだけです。下記コードのように、4.のコードの中に.byte疑似命令で埋め込んでもいいです。.string という疑似命令で埋めてもいいです。ということは、1.〜3. 全部を、.byte や .string で埋めてもいいですが、そういう方向に行くならgcc(gas)でなくnasmを使う方が良いと思われます。
__asm__ ("rodata_: \r\n" ".byte 'h' \r\n" ".byte 'e' \r\n" (略) "start_: \r\n" "movl $4, %eax \r\n" // eax: system call number (__NR_write) "movl $1, %ebx \r\n" // ebx: fd (stdout)
4. コード書き
インラインアセンブラでいきなりコードを書き、GCCにコンパイルさせたあとのコードをwriteするコードもいっしょに書きます。GCC拡張である「&&でラベル参照」技を使うとラクにそういうことができます。
__asm__ ("start_: \r\n" "movl $4, %eax \r\n" // eax: system call number (__NR_write) "movl $1, %ebx \r\n" // ebx: fd (stdout) "movl $" ECX ", %ecx \r\n" // ecx: addr "movl $13, %edx \r\n" // edx: len "int $0x80 \r\n" "movl $1, %eax \r\n" // eax: system call number (__NR_exit) "movl $0, %ebx \r\n" // ebx: exit code "int $0x80 \r\n" "end_: "); extern char *start_, *end_;
これを用意して、
void write_code() { const uintptr_t code_len = (uintptr_t)&end_ - (uintptr_t)&start_; write(1, &rodata_, code_len); }
これだけです*3。文字列を出力して(write)、exitします。なお、すこしでも短いバイナリが欲しい場合には、movlとかアリエえませんので、適当に改変してください。まずは116? 111? バイトくらいに改造するのがよいと思います。
ソースコード全体
131バイトのバイナリを生成するコードです。あれー、さっきより1バイト少ないな...まぁいいか。
#include <elf.h> #include <unistd.h> // write #define PAGE_ALIGN(adr) ((adr) & ~(0x1000 - 1)) // 16進下3桁を切り捨てるだけ #define LOAD_ADDRESS PAGE_ALIGN(0x12345678) // 0x12345000にロード #define STRING_LEN 13 #define TO_STR(s) TO_STR_(s) #define TO_STR_(s) #s #define ECX \ TO_STR(LOAD_ADDRESS + 52 + 32) // LOAD_ADDRESS + sizeof(Elf32_Ehdr) + sizeof(Elf32_Phdr) __asm__ ("start_: \r\n" "movl $4, %eax \r\n" // eax: system call number (__NR_write) "movl $1, %ebx \r\n" // ebx: fd (stdout) "movl $" ECX ", %ecx \r\n" // ecx: addr "movl $13, %edx \r\n" // edx: len "int $0x80 \r\n" "movl $1, %eax \r\n" // eax: system call number (__NR_exit) "movl $0, %ebx \r\n" // ebx: exit code "int $0x80 \r\n" "end_: "); extern char *start_, *end_; void out_elf_header() { Elf32_Ehdr ehdr = { .e_ident = { ELFMAG0, ELFMAG1, ELFMAG2 ,ELFMAG3, ELFCLASS32, ELFDATA2LSB, EV_CURRENT, ELFOSABI_SYSV }, .e_type = ET_EXEC, .e_machine = EM_386, .e_version = EV_CURRENT, .e_entry = LOAD_ADDRESS + sizeof(Elf32_Ehdr) + sizeof(Elf32_Phdr) + STRING_LEN, .e_phoff = sizeof(Elf32_Ehdr), .e_shoff = 0, // dummy .e_flags = 0x0, .e_ehsize = sizeof(Elf32_Ehdr), .e_phentsize = sizeof(Elf32_Phdr), .e_phnum = 1, .e_shentsize = 0, // dummy .e_shnum = 0, .e_shstrndx = 0, // dummy }; write(1, &ehdr, sizeof(Elf32_Ehdr)); } void out_program_header() { uintptr_t code_len = (uintptr_t)&end_ - (uintptr_t)&start_; Elf32_Phdr phdr = { .p_type = PT_LOAD, .p_offset = 0x0, .p_vaddr = LOAD_ADDRESS, .p_paddr = 0, // dummy .p_filesz = sizeof(Elf32_Ehdr) + sizeof(Elf32_Phdr) + STRING_LEN + code_len, .p_memsz = sizeof(Elf32_Ehdr) + sizeof(Elf32_Phdr) + STRING_LEN + code_len, .p_flags = PF_R | PF_X, .p_align = 0x1000, }; write(1, &phdr, sizeof(Elf32_Phdr)); } void out_code() { uintptr_t code_len = (uintptr_t)&end_ - (uintptr_t)&start_; write(1, &start_, code_len); } int main() { out_elf_header(); out_program_header(); write(1, "hello world!\n", 13); out_code(); return 0; }
実行例
% gcc -m32 -Wall hello_world_elfout.c % ./a.out > elf % wc -c elf 131 elf % chmod +x elf % ./elf hello world!