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!