koturnの日記

転職したい社会人1年生の技術系日記

小さいHello Worldバイナリを作る

はじめに

先日は,少し不思議なHello Worldを紹介した. そこで思ったのが,この程度の小さいプログラムならば,gccは必要ないのではないかと思い至った. そこで,小さいHello Worldの実行ファイルを作ることにした.

方針

終了の仕方

先日の記事では,crt*.o ありきであったため,プログラマ視点でのプログマムのエントリポイントが main であったため,単に return 0; すればよかった. しかし,今回は crt*.o とのリンクは行わないため, return 0; に代わり, exit システムコールを呼び出す必要がある.

文字列データのアドレス

実行ファイルを生成するならば,"Hello World!\n" のアドレスも既知となる. したがって,ripから現在の実行中のアドレスを取得する必要はない.

また,前回と同様,文字列データは,コードの末尾に置くものとする.

セクションヘッダを削る

通常の実行ファイルは,

  1. ELFヘッダ
  2. プログラムヘッダ(複数)
  3. コードデータ
  4. セクション名テーブル
  5. セクションヘッダ(複数)

の5つを含むが 4.と5.は無くても良いので,以下の3つで実行ファイルを構成する.

  1. ELFヘッダ
  2. プログラムヘッダ(複数)
  3. コードデータ

セクションヘッダを削るため,$ 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でも同様の手法がとれるだろう.