ELFのオブジェクトファイル形式を生成する

2020-04-20
ELF64でQuineでは実行形式のELF64を直接出力して動かす方法がわかったんだけど、ELFにはリンク可能なオブジェクトファイルという形式もある

Executable and Linkable Format という名前なだけあって)。

オブジェクトファイルを出力できると分割コンパイルができるようになって便利だろう、ということで調べてみた。

オブジェクトファイルの構成

実行形式のELFは、ELFヘッダに加えてプログラムヘッダを出力し、コードとデータを出力するだけでよかった。

オブジェクトファイル形式ではコードやデータ以外にリンクに必要な情報として、どういうラベルが定義されているかや参照しているかという情報が必要になる。 それらを表現するために、

  • シンボルテーブル:どういうシンボルが定義されているか(または参照するか)
  • 文字列テーブル:シンボル名(やセクション名)
  • 再配置テーブル:外部のシンボルを参照する箇所の置き換え情報

そしてこれらのテーブルがELFファイルのどの範囲に出力されているか、というのをセクションヘッダで表している。

オブジェクトファイル形式の例

例えば定数を返すだけの関数:

int main() {
return 42;
}

をgccを使ってオブジェクトファイルにコンパイルして内容を見てみる。

セクションヘッダ

セクションヘッダを見るには readelf -S で、

$ gcc -c const.c
$ readelf -S const.o
There are 11 section headers, starting at offset 0x208:

Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
000000000000000b 0000000000000000 AX 0 0 1
[ 2] .data PROGBITS 0000000000000000 0000004b
0000000000000000 0000000000000000 WA 0 0 1
[ 3] .bss NOBITS 0000000000000000 0000004b
0000000000000000 0000000000000000 WA 0 0 1
[ 4] .comment PROGBITS 0000000000000000 0000004b
000000000000002a 0000000000000001 MS 0 0 1
[ 5] .note.GNU-stack PROGBITS 0000000000000000 00000075
0000000000000000 0000000000000000 0 0 1
[ 6] .eh_frame PROGBITS 0000000000000000 00000078
0000000000000038 0000000000000000 A 0 0 8
[ 7] .rela.eh_frame RELA 0000000000000000 00000198
0000000000000018 0000000000000018 I 8 6 8
[ 8] .symtab SYMTAB 0000000000000000 000000b0
00000000000000d8 0000000000000018 9 8 8
[ 9] .strtab STRTAB 0000000000000000 00000188
000000000000000e 0000000000000000 0 0 1
[10] .shstrtab STRTAB 0000000000000000 000001b0
0000000000000054 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)

この中で重要なのは、

  • セクション[1] .text がプログラムをコンパイルした結果のマシンコード
  • [2] .data が読み書き可能データ
  • [3] .bss が初期値なしの変数領域
  • [8] .symtabがシンボルテーブル
  • [9] .strtabが文字列テーブル
  • [10] .shstrtabはセクションヘッダ用の文字列テーブル

となる。

シンボルテーブル

シンボルテーブルは readelf -s で、

$ readelf -s const.o

Symbol table '.symtab' contains 9 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS const.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 2
4: 0000000000000000 0 SECTION LOCAL DEFAULT 3
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000000 0 SECTION LOCAL DEFAULT 6
7: 0000000000000000 0 SECTION LOCAL DEFAULT 4
8: 0000000000000000 11 FUNC GLOBAL DEFAULT 1 main

オブジェクトファイル内で定義されているグローバルな main 関数が8番に出力されている。

他、2〜7はセクションを指す。

実際のバイナリデータは、セクションヘッダの情報[8]のOffset0x00b0からSize0x00d8バイト分格納されていることがわかる。 形式はElf64_Sym構造体(EntSize0x18バイト)が個数分羅列されている。

文字列テーブル

文字列テーブルはシンボル名を保持するのに使用される。 構造は単に使用される文字列を連結しただけというもので、各文字列はヌルターミネートされている。

セクションテーブルのセクション[9] .strtabはOffsetが0x188でSizeが0x0eとなっているので、そこを見ると:

$ xxd const.o
...
00000180: 0063 6f6e 7374 2e63 .const.c
00000190: 006d 6169 6e00 .main.

シンボル1の const.c 、シンボル8の main の文字列が格納されている。 空文字列も先頭に配置されている。

シンボルテーブルからは文字列テーブル内の先頭からのオフセット値によって参照される。

例2: 外部参照がある場合

ソース内で定義されないラベルへの外部参照がある場合:

extern int putchar(int);

int main() {
putchar('A');
return 0;
}

再配置テーブルが必要になる。

セクションヘッダ:

$ gcc -c -fno-asynchronous-unwind-tables putchar.c
$ readelf -S putchar.o
...
[ 2] .rela.text RELA 0000000000000000 000001a0
0000000000000018 0000000000000018 I 7 1 8
...

再配置テーブル:

$ readelf -r putchar.o  # -r で再配置テーブル表示

Relocation section '.rela.text' at offset 0x1a0 contains 1 entry:
Offset Info Type Sym. Value Sym. Name + Addend
00000000000a 000900000004 R_X86_64_PLT32 0000000000000000 putchar - 4

Info4TypeR_X86_64_PLT32 を示していて、 9 は参照するシンボルのインデクスで putchar

$ readelf -s putchar.o
...
9: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND putchar

を表している。

Offset..0a はテキストセクション先頭から書き換え先のオフセット、 Addend-4 はアドレス解決時の加算値。

このへんType ごとの再配置の計算式が書いてある。

実際のバイナリデータの形式はElf64_Rela構造体

例3: データ参照の場合

文字列などのデータがある場合:

const char *get_message() {
return "Hello, world!\n";
}

外部参照じゃなくても再配置テーブルが生成される:

$ gcc -c -fno-asynchronous-unwind-tables message.c
$ readelf -r message.o

Relocation section '.rela.text' at offset 0x178 contains 1 entry:
Offset Info Type Sym. Value Sym. Name + Addend
000000000007 000500000002 R_X86_64_PC32 0000000000000000 .rodata - 4

これはプログラム(.text)とデータ(.rodata)でセクションが別なので同じオブジェクト内でもオブジェクトファイル生成時にはアドレスを解決させず、リンク時に行えるようにするためだと思う。

Info5 はシンボル番号:

$ readelf -s message.o
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5

で、ローカルセクション5= .rodata

$ readelf -S message.o
[ 5] .rodata PROGBITS 0000000000000000 0000004d
000000000000000f 0000000000000000 A 0 0 1

を指している。

オブジェクトファイルでHello world

てなことでオブジェクトファイルを生成するテストとして、Hello, world!を出力するファイルを生成してみる: (注意:x86-64のマシンコードを埋め込んでいるので、x86-64専用)

#include <elf.h>
#include <string.h>
#include <unistd.h>

int main() {
// マシンコード
unsigned char CODE[32] = {
0xba, 0x0f, 0x00, 0x00, 0x00, // 00: mov 15,%edx
0x48, 0x8d, 0x35, 0x00, 0x00, 0x00, 0x00, // 05: lea message(%rip),%rsi
0xbf, 0x01, 0x00, 0x00, 0x00, // 0c: mov $0x1,%edi
0xe8, 0x00, 0x00, 0x00, 0x00, // 11: callq write
0xb8, 0x00, 0x00, 0x00, 0x00, // 16: mov $0,%eax
0xc3, // 1b: retq
};

// 読み取り専用データ
char RODATA[16] = "Hello, world!\n";

// 文字列テーブル
char strtab[16] =
"\0"
"main\0"
"write\0";
int null_nameofs = 0;
int main_nameofs = null_nameofs + strlen("") + 1;
int write_nameofs = main_nameofs + strlen("main") + 1;

// シンボルテーブル
Elf64_Sym symtab[] = {
{ // UND
.st_name = null_nameofs,
.st_info = ELF64_ST_INFO(STB_LOCAL, STT_NOTYPE),
},
{ // Section .rodata
.st_name = null_nameofs,
.st_info = ELF64_ST_INFO(STB_LOCAL, STT_SECTION),
.st_shndx = 2, // .rodataセクション番号
},
{ // main
.st_name = main_nameofs,
.st_info = ELF64_ST_INFO(STB_GLOBAL, STT_NOTYPE),
.st_shndx = 1, // .textセクション番号
.st_value = 0, // .textセクション先頭からのオフセット
},
{ // write
.st_name = write_nameofs,
.st_info = ELF64_ST_INFO(STB_GLOBAL, STT_NOTYPE),
.st_shndx = 0, // 未定義UNDの番号
},
};

// 再配置テーブル
Elf64_Rela relatext[] = {
{ // call write の再配置
.r_offset = 0x12, // call write のアドレス部分のオフセット
.r_info = ELF64_R_INFO(3, R_X86_64_PLT32), // 3=writeのシンボル番号
.r_addend = -4, // -4=call命令で実際の計算に使用されるアドレスと配置位置の差
},
{ // message の再配置
.r_offset = 0x08, // lea message(%rip),%rsi のコードのアドレス部分のオフセット
.r_info = ELF64_R_INFO(1, R_X86_64_PC32), // 1=.rodataセクションを示すシンボル番号
.r_addend = 0 - 4, // 0=messageの.rodata中のオフセット、-4=lea命令で実際の計算に使用されるアドレスと配置位置の差
},
};

// セクションヘッダ用文字列テーブル
char shstrtab[64] =
"\0"
".text\0"
".rodata\0"
".rela.text\0"
".strtab\0"
".symtab\0"
".shstrtab\0";
int text_nameofs = null_nameofs + strlen("") + 1;
int rodata_nameofs = text_nameofs + strlen(".text") + 1;
int relatext_nameofs = rodata_nameofs + strlen(".rodata") + 1;
int strtab_nameofs = relatext_nameofs + strlen(".rela.text") + 1;
int symtab_nameofs = strtab_nameofs + strlen(".strtab") + 1;
int shstrtab_nameofs = symtab_nameofs + strlen(".symtab") + 1;

// 各項目の出力時のファイル中での位置とサイズ(セクションによっては8バイトアラインが必要かもしれない)
uintptr_t code_ofs = sizeof(Elf64_Ehdr);
size_t code_size = sizeof(CODE);
uintptr_t rodata_ofs = code_ofs + code_size;
size_t rodata_size = sizeof(RODATA);
uintptr_t strtab_ofs = rodata_ofs + rodata_size;
size_t strtab_size = sizeof(strtab);
uintptr_t symtab_ofs = strtab_ofs + strtab_size;
size_t symtab_size = sizeof(symtab);
uintptr_t relatext_ofs = symtab_ofs + symtab_size;
size_t relatext_size = sizeof(relatext);
uintptr_t shstrtab_ofs = relatext_ofs + relatext_size;
size_t shstrtab_size = sizeof(shstrtab);
uintptr_t sectionheader_ofs = shstrtab_ofs + shstrtab_size;

// セクションヘッダ
Elf64_Shdr section_headers[] = {
{ // NULL
.sh_name = null_nameofs,
.sh_type = SHT_NULL,
},
{ // .text
.sh_name = text_nameofs,
.sh_type = SHT_PROGBITS,
.sh_flags = SHF_ALLOC | SHF_EXECINSTR, // 確保+実行
.sh_addr = 0,
.sh_offset = code_ofs,
.sh_size = code_size,
.sh_link = 0,
.sh_info = 0,
.sh_addralign = 1,
.sh_entsize = 0,
},
{ // .rodata
.sh_name = rodata_nameofs,
.sh_type = SHT_PROGBITS,
.sh_flags = SHF_ALLOC, // 確保
.sh_addr = 0,
.sh_offset = rodata_ofs,
.sh_size = rodata_size,
.sh_link = 0,
.sh_info = 0,
.sh_addralign = 1,
.sh_entsize = 0,
},
{ // .strtab
.sh_name = strtab_nameofs,
.sh_type = SHT_STRTAB,
.sh_flags = 0,
.sh_addr = 0,
.sh_offset = strtab_ofs,
.sh_size = strtab_size,
.sh_link = 0,
.sh_info = 0,
.sh_addralign = 1,
.sh_entsize = 0,
},
{ // .symtab
.sh_name = symtab_nameofs,
.sh_type = SHT_SYMTAB,
.sh_flags = 0,
.sh_addr = 0,
.sh_offset = symtab_ofs,
.sh_size = symtab_size,
.sh_link = 3, // .strtabのセクション番号
.sh_info = 2, // ローカルシンボルの数
.sh_addralign = 8,
.sh_entsize = sizeof(Elf64_Sym),
},
{ // .rela.text
.sh_name = relatext_nameofs,
.sh_type = SHT_RELA,
.sh_flags = SHF_INFO_LINK,
.sh_addr = 0,
.sh_offset = relatext_ofs,
.sh_size = relatext_size,
.sh_link = 4, // .symtabのセクション番号
.sh_info = 1, // .textのセクション番号
.sh_addralign = 8,
.sh_entsize = sizeof(Elf64_Rela),
},
{ // .shstrtab
.sh_name = shstrtab_nameofs,
.sh_type = SHT_STRTAB,
.sh_flags = 0,
.sh_addr = 0,
.sh_offset = shstrtab_ofs,
.sh_size = shstrtab_size,
.sh_link = 0,
.sh_info = 0,
.sh_addralign = 1,
.sh_entsize = 0,
},
};

// ELFヘッダ
Elf64_Ehdr ehdr = {
.e_ident = { ELFMAG0, ELFMAG1, ELFMAG2 ,ELFMAG3,
ELFCLASS64, ELFDATA2LSB, EV_CURRENT, ELFOSABI_SYSV },
.e_type = ET_REL, // オブジェクトファイル
.e_machine = EM_X86_64,
.e_version = EV_CURRENT,
.e_entry = 0,
.e_phoff = 0,
.e_shoff = sectionheader_ofs, // セクションヘッダの開始位置
.e_flags = 0x0,
.e_ehsize = sizeof(Elf64_Ehdr),
.e_phentsize = sizeof(Elf64_Phdr),
.e_phnum = 0,
.e_shentsize = sizeof(Elf64_Shdr),
.e_shnum = sizeof(section_headers) / sizeof(*section_headers), // セクションヘッダのエントリ数
.e_shstrndx = sizeof(section_headers) / sizeof(*section_headers) - 1, // セクションヘッダ文字列テーブルインデクス(最後のセクション)
};

// 標準出力に出力
int fd = STDOUT_FILENO;
write(fd, &ehdr, sizeof(Elf64_Ehdr));
write(fd, CODE, sizeof(CODE));
write(fd, RODATA, sizeof(RODATA));
write(fd, strtab, sizeof(strtab));
write(fd, symtab, sizeof(symtab));
write(fd, relatext, sizeof(relatext));
write(fd, shstrtab, sizeof(shstrtab));
write(fd, section_headers, sizeof(section_headers));
close(fd);

return 0;
}

実行する:

$ gcc -o hello_obj hello_obj.c
$ ./hello_obj > hello.o # 実行してオブジェクトファイル生成
$ gcc -o hello hello.o # リンクして実行ファイルを生成
$ ./hello
Hello, world!

出力されたhello.o の中身:

# ELFヘッダ
00000000: 7f45 4c46 0201 0100 0000 0000 0000 0000 .ELF............
00000010: 0100 3e00 0100 0000 0000 0000 0000 0000 ..>.............
00000020: 0000 0000 0000 0000 5001 0000 0000 0000 ........P.......
00000030: 0000 0000 4000 3800 0000 4000 0700 0600 ....@.8...@.....
# .text
00000040: ba0f 0000 0048 8d35 0000 0000 bf01 0000 .....H.5........
00000050: 00e8 0000 0000 b800 0000 00c3 0000 0000 ................
# .rodata
00000060: 4865 6c6c 6f2c 2077 6f72 6c64 210a 0000 Hello, world!...
# .strtab
00000070: 006d 6169 6e00 7772 6974 6500 0000 0000 .main.write.....
# .symtab[4]
00000080: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000090: 0000 0000 0000 0000 0000 0000 0300 0200 ................
000000a0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000000b0: 0100 0000 1000 0100 0000 0000 0000 0000 ................
000000c0: 0000 0000 0000 0000 0600 0000 1000 0000 ................
000000d0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
# .rela.text[2]
000000e0: 1200 0000 0000 0000 0400 0000 0300 0000 ................
000000f0: fcff ffff ffff ffff 0800 0000 0000 0000 ................
00000100: 0200 0000 0100 0000 fcff ffff ffff ffff ................
# .shstrtab
00000110: 002e 7465 7874 002e 726f 6461 7461 002e ..text..rodata..
00000120: 7265 6c61 2e74 6578 7400 2e73 7472 7461 rela.text..strta
00000130: 6200 2e73 796d 7461 6200 2e73 6873 7472 b..symtab..shstr
00000140: 7461 6200 0000 0000 0000 0000 0000 0000 tab.............
# セクションヘッダ[7]
00000150: 0000 0000 0000 0000 0000 0000 0000 0000 # NULL
00000160: 0000 0000 0000 0000 0000 0000 0000 0000
00000170: 0000 0000 0000 0000 0000 0000 0000 0000
00000180: 0000 0000 0000 0000 0000 0000 0000 0000
00000190: 0100 0000 0100 0000 0600 0000 0000 0000 # .text
000001a0: 0000 0000 0000 0000 4000 0000 0000 0000
000001b0: 2000 0000 0000 0000 0000 0000 0000 0000
000001c0: 0100 0000 0000 0000 0000 0000 0000 0000
000001d0: 0700 0000 0100 0000 0200 0000 0000 0000 # .rodata
000001e0: 0000 0000 0000 0000 6000 0000 0000 0000
000001f0: 1000 0000 0000 0000 0000 0000 0000 0000
00000200: 0100 0000 0000 0000 0000 0000 0000 0000
00000210: 1a00 0000 0300 0000 0000 0000 0000 0000 # .strtab
00000220: 0000 0000 0000 0000 7000 0000 0000 0000
00000230: 1000 0000 0000 0000 0000 0000 0000 0000
00000240: 0100 0000 0000 0000 0000 0000 0000 0000
00000250: 2200 0000 0200 0000 0000 0000 0000 0000 # .symtab
00000260: 0000 0000 0000 0000 8000 0000 0000 0000
00000270: 6000 0000 0000 0000 0300 0000 0200 0000
00000280: 0800 0000 0000 0000 1800 0000 0000 0000
00000290: 0f00 0000 0400 0000 4000 0000 0000 0000 # .rela.text
000002a0: 0000 0000 0000 0000 e000 0000 0000 0000
000002b0: 3000 0000 0000 0000 0400 0000 0100 0000
000002c0: 0800 0000 0000 0000 1800 0000 0000 0000
000002d0: 2a00 0000 0300 0000 0000 0000 0000 0000 # .shstrtab
000002e0: 0000 0000 0000 0000 1001 0000 0000 0000
000002f0: 4000 0000 0000 0000 0000 0000 0000 0000
00000300: 0100 0000 0000 0000 0000 0000 0000 0000

参照