ELFについて学んだ
とりあえず忘れないように書いておく。
Object file
main.c
int main(){ return 42; }
をcompile(gcc -c main.c
)してmain.o
を作る
readlefでELF header, section headerを見る * ELF header, program header, section headerの構造はここ
ELF Header
ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: REL (Relocatable file) Machine: Advanced Micro Devices X86-64 Version: 0x1 Entry point address: 0x0 Start of program headers: 0 (bytes into file) Start of section headers: 528 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 0 (bytes) Number of program headers: 0 Size of section headers: 64 (bytes) Number of section headers: 11 Section header string table index: 8
- object fileなのでprogram headerはない
- ファイルサイズは1232Byte, これは 528 (section headerのoffset) + 64 (section header entry size) * 11 (# of section header)に等しい。(section headerはファイル末尾に置かれる、実行時には必要ないのでstripが簡単なように。)
Section header
There are 11 section headers, starting at offset 0x210: 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 0000000000000035 0000000000000001 MS 0 0 1 [ 5] .note.GNU-stack PROGBITS 0000000000000000 00000080 0000000000000000 0000000000000000 0 0 1 [ 6] .eh_frame PROGBITS 0000000000000000 00000080 0000000000000038 0000000000000000 A 0 0 8 [ 7] .rela.eh_frame RELA 0000000000000000 000001a0 0000000000000018 0000000000000018 I 9 6 8 [ 8] .shstrtab STRTAB 0000000000000000 000001b8 0000000000000054 0000000000000000 0 0 1 [ 9] .symtab SYMTAB 0000000000000000 000000b8 00000000000000d8 0000000000000018 10 8 8 [10] .strtab STRTAB 0000000000000000 00000190 000000000000000d 0000000000000000 0 0 1
- Headerに書かれている通り、8番目が
.shstrtab
(Section Header STRing TABle)で、各セクションの名前が格納されている - 各section header entryの先頭4バイトがそのsectionの名前への
.shstrtab
の先頭からのoffset.shstrtab
のoffset + section headerの先頭4バイト = section名のoffset
.symtab
と.strtab
の関係も似ている.symtab
はsymbol table entryの配列- Entryの先頭4バイトが
.strtab
の先頭からシンボル名へのoffset
.eh_frame
についてはexceptionやdebug関連に使われるらしい。詳しくは知らない
Executable
gcc -o main main.c
としてexecutableを作る
ELF header
ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: EXEC (Executable file) Machine: Advanced Micro Devices X86-64 Version: 0x1 Entry point address: 0x4003e0 Start of program headers: 64 (bytes into file) Start of section headers: 6568 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 56 (bytes) Number of program headers: 9 Size of section headers: 64 (bytes) Number of section headers: 31 Section header string table index: 28
- Program headerができている
Shared library
gcc -c -fPIC foo.c gcc -c -fPIC bar.c gcc -shared -o libfoo.so
としてshared libraryを作る
foo.c
#include <stdio.h> extern int bar_1; extern int bar_2; int foo_with_init_1 = 1; double foo_with_init_2 = 2; int foo_wo_init_1; double foo_wo_init_2; void print(int arg){ printf("%lf", arg + bar_2 + foo_with_init_2); printf("%lf", arg + bar_2 + foo_with_init_2); printf("%lf", arg + bar_2 + foo_with_init_2); }
bar.c
int bar_1 = 1; int bar_2 = 2;
Relocation
printf
の呼び出しはfoo.o
では
3c: e8 00 00 00 00 callq 41 <print+0x41>
で、.soでは
842: e8 69 fe ff ff callq 6b0 <printf@plt>
になっている。これはfoo.o
の.rela.text
sectionで
000000000000003d R_X86_64_PLT32 printf-0x0000000000000004
と3d
に対するrelocationが指定されているから。これは.soでは
0000000000201018 R_X86_64_JUMP_SLOT printf@GLIBC_2.2.5
とされている。このoffsetは".got.plt" section内にある。
PLTとGOT
各プロセスからshared libraryをloadする際はrelocationによって書き換わる部分はWritableな必要があるが、なるべく共有する部分は増やしたい。 * PLT : 共有部分, read-only segment * GOT : プロセス固有部分, writable segment
ライブラリ関数を読んだ際の流れは(lazy symbol bindingの場合)
- func@PLTが呼ばれる
- func@PLTはGOTから飛び先を読む。初回はfunc@PLT内に戻ってくる
- func@PLTはGOT内の
PT_DYNAMIC
segmentのアドレスと、loaderのアドレスを取ってきてライブラリをloadする - ロードされたらGOT内のアドレスを書き換える。これによって2回目以降は直接目的の関数が呼ばれる
.interp section
上記のPT_DYNAMIC
segmentのアドレスと、loaderのアドレスはloader自身が書く。
これはプロセスが走り始める際(execve)にexecutableの.interp
section (INTERP
segment)で指定されるinterpreter(一般にはloader)がexecutableを引数として実行される。
これがexecutableとdynamic libraryの関連するsegmentを読み、適宜GOTの中身を書き換える。