ELF64ファイルを生成してHello World

2019-01-28

ELFを自分で生成する方法を調べたところ、 hello worldなELFバイナリを出力するCのプログラム(の一番単純な奴) - memologue が非常に参考になった。 ただ32ビット版なので64ビット版にしたい。

ELF64ビット版への変更

ELF64ビット版にするには、

  • 構造体: 32 から 64 に(Elf64_Ehdrなど)。
  • e_machineEM_X86_64
  • システムコール: int $0x80syscall に変更。
  • システムコール番号: write1 に、 exit60 にする。
  • システムコールに渡すレジスタ: rdi, rsi, rdx の順に。
  • アセンブラ: movlmov に。

で一応動いた。

必要なプログラム部分だけを読み込ませたい

生成しているELFを見ると、ロードするプログラムはプログラムヘッダの p_offset = 0x0 なのでファイルの先頭から、 サイズは p_filesz = sizeof(Elf32_Ehdr) + sizeof(Elf32_Phdr) + STRING_LEN + code_len なので出力した内容全体を指すことになる。 でもヘッダ部分は実際の実行には必要ないはずだからメモリに読み込む必要ないんじゃない? 実際のプログラムの部分だけを指定したいよね、と思って試してみた:

  • ファイル中のプログラムの開始位置を示す p_offsetsizeof(Elf32_Ehdr) + sizeof(Elf32_Phdr) に、
  • サイズを示す p_fileszp_memszSTRING_LEN + code_len に、
  • エントリポイント e_entryLOAD_ADDRESS + STRING_LEN に、

こうすればELFのヘッダを除いたプログラムの部分だけを LOAD_ADDRESS に読み込み動いてくれるはず…と思ったが segmentation fault が起きてしまう。 あれこれ試してなんとなく理解したのは、ファイル中の位置と読み込み先のメモリのアドレスのページ内の位置が合っている必要がある、ということらしい。

Program Header (Linker and Libraries Guide)

p_align: Loadable process segments must have congruent values for p_vaddr and p_offset, modulo the page size. This member gives the value to which the segments are aligned in memory and in the file. Values 0 and 1 mean no alignment is required. Otherwise, p_align should be a positive, integral power of 2, and p_vaddr should equal p_offset, modulo p_align. See “Program Loading (Processor-Specific)”.

ロード可能なプロセスセグメントは、ページサイズを法として、p_vaddrとp_offsetに一致する値を持たなければなりません。 このメンバは、セグメントがメモリ内およびファイル内で整列している値を示します。 値0および1は、配置が不要であることを意味します。 それ以外の場合、p_alignは2の正の整数乗でなければならず、p_vaddrはp_alignを法としたp_offsetと等しくなければなりません。 「プログラムのロード(プロセッサ固有)」を参照してください。

ということで上の変更に加えて、 LOAD_ADDRESS4096 の余りを 64 + 56= sizeof(Elf32_Ehdr) + sizeof(Elf32_Phdr))に合わせる必要がある (もしくはELFヘッダとプログラムヘッダの後に4096バイトまでパディングを入れる)。

ちなみに p_align0x1000 から 0x10 とかに変えても特に効果はなさそうだった。

なんと不便な、またロード先のアドレスを切りよくしようとするとファイルサイズが、ファイル上で詰めようとすると実行時のメモリが無駄になってしまうのがなんとも…。

ソース

#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 MSG TO_STR(LOAD_ADDRESS + 64 + 56) // LOAD_ADDRESS + sizeof(Elf64_Ehdr) + sizeof(Elf64_Phdr)

__asm__ (
"start_:\n"
" mov $1, %eax\n" // eax: system call number (__NR_write)
" mov $1, %edi\n" // edi: fd (stdout)
" mov $" MSG ", %rsi\n" // rsi: addr
" mov $13, %rdx\n" // rdx: len
" syscall\n"
" mov $60, %eax\n" // eax: system call number (__NR_exit)
" mov $0, %edi\n" // edi: exit code
" syscall\n"
"end_:\n"
);
extern char *start_, *end_;

void out_elf_header() {
Elf64_Ehdr ehdr = {
.e_ident = { ELFMAG0, ELFMAG1, ELFMAG2 ,ELFMAG3,
ELFCLASS64, ELFDATA2LSB, EV_CURRENT, ELFOSABI_SYSV },
.e_type = ET_EXEC,
.e_machine = EM_X86_64,
.e_version = EV_CURRENT,
.e_entry = LOAD_ADDRESS + sizeof(Elf64_Ehdr) + sizeof(Elf64_Phdr) + STRING_LEN,
.e_phoff = sizeof(Elf64_Ehdr),
.e_shoff = 0, // dummy
.e_flags = 0x0,
.e_ehsize = sizeof(Elf64_Ehdr),
.e_phentsize = sizeof(Elf64_Phdr),
.e_phnum = 1,
.e_shentsize = 0, // dummy
.e_shnum = 0,
.e_shstrndx = 0, // dummy
};

write(1, &ehdr, sizeof(Elf64_Ehdr));
}

void out_program_header() {
uintptr_t code_len = (uintptr_t)&end_ - (uintptr_t)&start_;
Elf64_Phdr phdr = {
.p_type = PT_LOAD,
.p_offset = 0x0,
.p_vaddr = LOAD_ADDRESS,
.p_paddr = 0, // dummy
.p_filesz = sizeof(Elf64_Ehdr) + sizeof(Elf64_Phdr) + STRING_LEN + code_len,
.p_memsz = sizeof(Elf64_Ehdr) + sizeof(Elf64_Phdr) + STRING_LEN + code_len,
.p_flags = PF_R | PF_X,
.p_align = 0x1000,
};

write(1, &phdr, sizeof(Elf64_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 hello.c
$ ./a.out > hello
$ chmod 755 hello
$ ./hello
hello world!

関連記事