はじめに
先日は,少し不思議なHello Worldを紹介した. そこで思ったのが,この程度の小さいプログラムならば,gccは必要ないのではないかと思い至った. そこで,小さいHello Worldの実行ファイルを作ることにした.
方針
終了の仕方
先日の記事では,crt*.o
ありきであったため,プログラマ視点でのプログマムのエントリポイントが main
であったため,単に return 0;
すればよかった.
しかし,今回は crt*.o
とのリンクは行わないため, return 0;
に代わり, exit
システムコールを呼び出す必要がある.
文字列データのアドレス
実行ファイルを生成するならば,"Hello World!\n"
のアドレスも既知となる.
したがって,ripから現在の実行中のアドレスを取得する必要はない.
また,前回と同様,文字列データは,コードの末尾に置くものとする.
セクションヘッダを削る
通常の実行ファイルは,
- ELFヘッダ
- プログラムヘッダ(複数)
- コードデータ
- セクション名テーブル
- セクションヘッダ(複数)
の5つを含むが 4.と5.は無くても良いので,以下の3つで実行ファイルを構成する.
- ELFヘッダ
- プログラムヘッダ(複数)
- コードデータ
セクションヘッダを削るため,$ objdump -d
が効かなくなるが,まぁ良しとしよう.
実行ファイルの生成
上記の方針を踏まえ,実行ファイルを作るプログラムを書く. C言語で書く必要は無いのだが,ELFヘッダ,プログラムヘッダの構造体を利用することが可能なため,意外とCで書くのが楽になる. ただし,前回と同様,x64向けのプログラムを作る.
生成する実行ファイル名は a.out
で固定しており,また,生成後に実行権限を付与し,実行するようにしてある.
これで,サイズにして171 bytesのHello Worldプログラムを作成できる.
コードの説明は特にしないが,構造体のメンバを見れば,どこで指定しているかは容易にわかる.
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/stat.h> #include <elf.h> #define BASE_ADDR 0x08000000 #define N_PROGRAM_HEADER 1 #define N_SECTION_HEADER 0 #define HEADER_SIZE (sizeof(Elf64_Ehdr) + sizeof(Elf64_Phdr) * N_PROGRAM_HEADER) int main(void) { unsigned int str_addr = BASE_ADDR + HEADER_SIZE + 37; Elf64_Ehdr ehdr; Elf64_Phdr phdr; char code[] = // mov $0x01,%rax # System call number (write) "\x48\xc7\xc0\x01\x00\x00\x00" // mov $0x0d,%edx # Third argument "\xba\x0d\x00\x00\x00" // mov $0x********,%rsi # Second argument "\x48\xc7\xc6\x00\x00\x00\x00" // mov $0x01,%edi # First argument "\xbf\x01\x00\x00\x00" // syscall "\x0f\x05" // mov $0x3c,%rax # System call number (exit) "\x48\xc7\xc0\x3c\x00\x00\x00" // xor %edi,%edi # First argument "\x31\xff" // syscall "\x0f\x05" // String data "Hello World!\n"; FILE *f = fopen("a.out", "wb"); if (f == NULL) { fputs("Failed to open a.out\n", stderr); return EXIT_FAILURE; } // ELF header ehdr.e_ident[EI_MAG0] = ELFMAG0; ehdr.e_ident[EI_MAG1] = ELFMAG1; ehdr.e_ident[EI_MAG2] = ELFMAG2; ehdr.e_ident[EI_MAG3] = ELFMAG3; ehdr.e_ident[EI_CLASS] = ELFCLASS64; ehdr.e_ident[EI_DATA] = ELFDATA2LSB; ehdr.e_ident[EI_VERSION] = EV_CURRENT; ehdr.e_ident[EI_OSABI] = ELFOSABI_LINUX; ehdr.e_ident[EI_ABIVERSION] = 0x00; ehdr.e_ident[EI_PAD] = 0x00; ehdr.e_type = ET_EXEC; ehdr.e_machine = EM_X86_64; ehdr.e_version = EV_CURRENT; ehdr.e_entry = BASE_ADDR + HEADER_SIZE; ehdr.e_phoff = sizeof(Elf64_Ehdr); ehdr.e_shoff = 0; ehdr.e_flags = 0x00000000; ehdr.e_ehsize = sizeof(Elf64_Ehdr); ehdr.e_phentsize = sizeof(Elf64_Phdr); ehdr.e_phnum = N_PROGRAM_HEADER; ehdr.e_shentsize = 0; ehdr.e_shnum = N_SECTION_HEADER; ehdr.e_shstrndx = SHN_UNDEF; fwrite(&ehdr, sizeof(ehdr), 1, f); // Program header phdr.p_type = PT_LOAD; phdr.p_flags = PF_R | PF_X; phdr.p_offset = 0x000000000000000000; phdr.p_vaddr = BASE_ADDR; phdr.p_paddr = BASE_ADDR; phdr.p_filesz = HEADER_SIZE + sizeof(code); phdr.p_memsz = HEADER_SIZE + sizeof(code); phdr.p_align = 0x0000000000000100; fwrite(&phdr, sizeof(phdr), 1, f); // Put string data address into code memcpy(code + 15, &str_addr, sizeof(str_addr)); // Write .text section fwrite(code, 1, sizeof(code), f); fclose(f); f = NULL; // Show file size printf("Size of a.out: %lu bytes\n", HEADER_SIZE + sizeof(code)); fflush(stdout); // Give execution permission chmod("a.out", 0755); // Execute created binary system("./a.out"); return EXIT_SUCCESS; }
コンパイルと実行結果は以下の通り.
$ gcc -O2 gen_hello_x64.c -o gen_hello_x64 $ ./gen_hello_x64 Size of a.out: 171 bytes Hello World!
また,逆コンパイル結果は以下の通り. 前述した通り,セクションヘッダが無いため,プログラムデータ本体を見ることができない.
$ objdump -d a.out a.out: file format elf64-x86-64
おまけ: セクションヘッダを付ける
丁寧にセクションヘッダも付けるとしたら,プログラムは以下のようになる.
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/stat.h> #include <elf.h> #define BASE_ADDR 0x08000000 #define N_PROGRAM_HEADER 1 #define N_SECTION_HEADER 3 #define HEADER_SIZE (sizeof(Elf64_Ehdr) + sizeof(Elf64_Phdr) * N_PROGRAM_HEADER) #define FOOTER_SIZE (sizeof(Elf64_Shdr) * N_SECTION_HEADER) const char SH_STR_TBL[] = "\0.text\0.shstrtbl"; int main(void) { unsigned int str_addr = BASE_ADDR + HEADER_SIZE + 37; Elf64_Ehdr ehdr; Elf64_Phdr phdr; Elf64_Shdr shdr; char code[] = /* mov $0x01,%rax # System call number (write) */ "\x48\xc7\xc0\x01\x00\x00\x00" /* mov $0x0d,%edx # Third argument */ "\xba\x0d\x00\x00\x00" /* mov $0x********,%rsi # Second argument */ "\x48\xc7\xc6\x00\x00\x00\x00" /* mov $0x01,%edi # First argument */ "\xbf\x01\x00\x00\x00" /* syscall */ "\x0f\x05" /* mov $0x3c,%rax # System call number (exit) */ "\x48\xc7\xc0\x3c\x00\x00\x00" /* xor %edi,%edi # First argument */ "\x31\xff" /* syscall */ "\x0f\x05" /* String data */ "Hello World!\n"; FILE *f = fopen("a.out", "wb"); if (f == NULL) { fputs("Failed to open a.out\n", stderr); return EXIT_FAILURE; } /* ELF header */ ehdr.e_ident[EI_MAG0] = ELFMAG0; ehdr.e_ident[EI_MAG1] = ELFMAG1; ehdr.e_ident[EI_MAG2] = ELFMAG2; ehdr.e_ident[EI_MAG3] = ELFMAG3; ehdr.e_ident[EI_CLASS] = ELFCLASS64; ehdr.e_ident[EI_DATA] = ELFDATA2LSB; ehdr.e_ident[EI_VERSION] = EV_CURRENT; ehdr.e_ident[EI_OSABI] = ELFOSABI_LINUX; ehdr.e_ident[EI_ABIVERSION] = 0x00; ehdr.e_ident[EI_PAD] = 0x00; ehdr.e_type = ET_EXEC; ehdr.e_machine = EM_X86_64; ehdr.e_version = EV_CURRENT; ehdr.e_entry = BASE_ADDR + HEADER_SIZE; ehdr.e_phoff = sizeof(Elf64_Ehdr); ehdr.e_shoff = HEADER_SIZE + sizeof(code) + sizeof(SH_STR_TBL); ehdr.e_flags = 0x00000000; ehdr.e_ehsize = sizeof(Elf64_Ehdr); ehdr.e_phentsize = sizeof(Elf64_Phdr); ehdr.e_phnum = N_PROGRAM_HEADER; ehdr.e_shentsize = sizeof(Elf64_Shdr); ehdr.e_shnum = N_SECTION_HEADER; ehdr.e_shstrndx = 1; fwrite(&ehdr, sizeof(ehdr), 1, f); /* Program header */ phdr.p_type = PT_LOAD; phdr.p_flags = PF_R | PF_X; phdr.p_offset = 0x000000000000000000; phdr.p_vaddr = BASE_ADDR; phdr.p_paddr = BASE_ADDR; phdr.p_filesz = HEADER_SIZE + sizeof(code) + sizeof(SH_STR_TBL) + FOOTER_SIZE; phdr.p_memsz = HEADER_SIZE + sizeof(code) + sizeof(SH_STR_TBL) + FOOTER_SIZE; phdr.p_align = 0x0000000000000100; fwrite(&phdr, sizeof(phdr), 1, f); /* Put string data address into code */ memcpy(code + 15, &str_addr, sizeof(str_addr)); /* Write .text section */ fwrite(code, 1, sizeof(code), f); /* Write section header names */ fwrite(SH_STR_TBL, 1, sizeof(SH_STR_TBL), f); /* First section header */ shdr.sh_name = 0; shdr.sh_type = SHT_NULL; shdr.sh_flags = 0x0000000000000000; shdr.sh_addr = 0x0000000000000000; shdr.sh_offset = 0x0000000000000000; shdr.sh_size = 0x0000000000000000; shdr.sh_link = 0x00000000; shdr.sh_info = 0x00000000; shdr.sh_addralign = 0x0000000000000000; shdr.sh_entsize = 0x0000000000000000; fwrite(&shdr, sizeof(shdr), 1, f); /* Second section header (.shstrtbl) */ shdr.sh_name = 7; shdr.sh_type = SHT_STRTAB; shdr.sh_flags = 0x0000000000000000; shdr.sh_addr = 0x0000000000000000; shdr.sh_offset = HEADER_SIZE + sizeof(code); shdr.sh_size = sizeof(SH_STR_TBL); shdr.sh_link = 0x00000000; shdr.sh_info = 0x00000000; shdr.sh_addralign = 0x0000000000000001; shdr.sh_entsize = 0x0000000000000000; fwrite(&shdr, sizeof(shdr), 1, f); /* Third section header (.text) */ shdr.sh_name = 1; shdr.sh_type = SHT_PROGBITS; shdr.sh_flags = SHF_EXECINSTR | SHF_ALLOC; shdr.sh_addr = BASE_ADDR + HEADER_SIZE; shdr.sh_offset = HEADER_SIZE; shdr.sh_size = sizeof(code); shdr.sh_link = 0x00000000; shdr.sh_info = 0x00000000; shdr.sh_addralign = 0x0000000000000004; shdr.sh_entsize = 0x0000000000000000; fwrite(&shdr, sizeof(shdr), 1, f); fclose(f); f = NULL; /* Show file size */ printf("Size of a.out: %lu bytes\n", HEADER_SIZE + sizeof(code) + sizeof(SH_STR_TBL) + FOOTER_SIZE); fflush(stdout); /* Give execution permission */ chmod("a.out", 0755); /* Execute created binary */ system("./a.out"); return EXIT_SUCCESS; }
上記のプログラムから a.out
を生成し,objdump
で逆アセンブルしてみると,以下のようになる.
$ gcc -O2 gen_hello_x64.c -o gen_hello_x64 $ ./gen_hello_x64 Size of a.out: 380 bytes Hello World! $ objdump -d a.out a.out: ファイル形式 elf64-x86-64 セクション .text の逆アセンブル: 0000000008000078 <.text>: 8000078: 48 c7 c0 01 00 00 00 mov $0x1,%rax 800007f: ba 0d 00 00 00 mov $0xd,%edx 8000084: 48 c7 c6 9d 00 00 08 mov $0x800009d,%rsi 800008b: bf 01 00 00 00 mov $0x1,%edi 8000090: 0f 05 syscall 8000092: 48 c7 c0 3c 00 00 00 mov $0x3c,%rax 8000099: 31 ff xor %edi,%edi 800009b: 0f 05 syscall 800009d: 48 rex.W 800009e: 65 gs 800009f: 6c insb (%dx),%es:(%rdi) 80000a0: 6c insb (%dx),%es:(%rdi) 80000a1: 6f outsl %ds:(%rsi),(%dx) 80000a2: 20 57 6f and %dl,0x6f(%rdi) 80000a5: 72 6c jb 0x8000113 80000a7: 64 21 0a and %ecx,%fs:(%rdx) ...
無事に .text セクションの中身がわかるようになった.
Hello World!
の部分については無理に解釈されているため,でたらめなニーモニックが出力されているが,気にしなくてもよい.
まとめ
実行ファイルにはELFヘッダとプログラムヘッダが必須となるが,セクションヘッダは無くてもよい. ヘッダでエントリポイントを指定し,その位置から機械語を配置することで,実行ファイルができる.
なお,今回の記事の実行ファイル生成プログラムは以下のリポジトリにある.
また,工夫次第ではもっと小さい実行ファイルを作れるようだ. 以下の記事では終了ステータスを返すだけのx86プログラムだが,x64でも同様の手法がとれるだろう.