ELF
Table of Contents
1. ELF
1.1. ELF Format
ELF 格式的主要结构为:
- ELF Header (ehdr)
- Program Header (phdr)
- segments / sections (content)
- Section Header (shdr)
elf 文件在磁盘上的布局为:
+-------------------------------+ | ELF File Header | +-------------------------------+ | Program Header for segment #1 | +-------------------------------+ | Program Header for segment #2 | +-------------------------------+ | ... | +-------------------------------+ | Contents (Byte Stream) | | ... | +-------------------------------+ | Section Header for section #1 | +-------------------------------+ | Section Header for section #2 | +-------------------------------+ | ... | +-------------------------------+ | ".shstrtab" section | +-------------------------------+ | ".symtab" section | +-------------------------------+ | ".strtab" section | +-------------------------------+
elf 同时需要为链接 (static linker) 和加载 (kernel 以及 runtime/dynamic linker) 服务, 而两者关注的内容并不相同:
- 链接过程关注 section header 和 section
- 加载过程关注 segment (prgram) header 和 segment, 这些内容需要加载到内存
section 和 segment 实际上对应的都是同一块数据:
+-----------------+ +----| elf file header |----+ | +-----------------+ | v v +-----------------+ +-----------------+ | program headers | | section headers | +-----------------+ +-----------------+ || || || || || || || +------------------------+ || +--> | data (segction/segment)|<--+ +------------------------+
1.2. ELF Section
1.2.1. overview
section 的格式:
typedef struct elf32_shdr { Elf32_Word sh_name; /* 相对于 .shstrtab 的 offset */ Elf32_Word sh_type; Elf32_Word sh_flags; Elf32_Addr sh_addr; /* memory address */ Elf32_Off sh_offset; /* file offset */ Elf32_Word sh_size; Elf32_Word sh_link; Elf32_Word sh_info; Elf32_Word sh_addralign; Elf32_Word sh_entsize; } Elf32_Shdr;
1.2.2. init/finit
1.2.2.1. init
init section 保存的是 init 时要执行的函数 (而不是函数指针), 但现在看来 init section 应该慎重使用, 各种平台和编译器 (linker?) 对它的支持都不太相同. 例如:
- glibc 定义了自己的 .init section, 代码再修改 .init 的话会有 segmentation fault.
- arm 没有定义 .init section, 代码可以自由修改, 但 dynamic 里并没有 DT_INIT 项, 所以 .init 不会被调用
- arm 通过 -Wl,-init 可以把 DT_INIT 指向 init 函数, 但它并没有把 init 函数放在 .init section 中
1.2.2.1.1. sample code
#include <stdio.h> __attribute__((section (".init"))) void test() { printf("hello world!\n"); } int main(int argc, char *argv[]) { printf("main: %p\n", &main); return 0; }
1.2.2.1.2. arm_linux_androideabi-gcc
$> ./a.out main: 0xb662e3b4 $> arm-linux-androideabi-readelf -a ./a.out Dynamic section at offset 0xec8 contains 28 entries: Tag Type Name/Value 0x00000003 (PLTGOT) 0x1fe4 0x00000002 (PLTRELSZ) 32 (bytes) 0x00000017 (JMPREL) 0x340 0x00000014 (PLTREL) REL 0x00000011 (REL) 0x318 0x00000012 (RELSZ) 40 (bytes) 0x00000013 (RELENT) 8 (bytes) 0x6ffffffa (RELCOUNT) 5 0x00000015 (DEBUG) 0x0 0x00000006 (SYMTAB) 0x1e0 0x0000000b (SYMENT) 16 (bytes) 0x00000005 (STRTAB) 0x260 0x0000000a (STRSZ) 84 (bytes) 0x00000004 (HASH) 0x2b4 0x00000001 (NEEDED) Shared library: [libc.so] 0x00000001 (NEEDED) Shared library: [libdl.so] 0x0000001a (FINI_ARRAY) 0x1ea8 0x0000001c (FINI_ARRAYSZ) 8 (bytes) 0x00000019 (INIT_ARRAY) 0x1eb0 0x0000001b (INIT_ARRAYSZ) 16 (bytes) 0x00000020 (PREINIT_ARRAY) 0x1ec0 0x00000021 (PREINIT_ARRAYSZ) 0x8 0x0000001e (FLAGS) BIND_NOW 0x6ffffffb (FLAGS_1) Flags: NOW 0x6ffffff0 (VERSYM) 0x2e8 0x6ffffffe (VERNEED) 0x2f8 0x6fffffff (VERNEEDNUM) 1 0x00000000 (NULL) 0x0 // 并不包含 DT_INIT, 所以 .init section 中的函数不会被 loader 调用
1.2.2.1.3. gcc on x86_64
gcc on x86_64 本身已经使用了 .init section 来做某种初始化动作, 应用自己再设置 .init 会有问题
$> ./a.out hello world! Segmentation fault (core dumped) $> objdump -D ./a.out Disassembly of section .init: 0000000000400400 <_init>: 400400: 48 83 ec 08 sub $0x8,%rsp 400404: 48 8b 05 ed 0b 20 00 mov 0x200bed(%rip),%rax # 600ff8 <__gmon_start__> 40040b: 48 85 c0 test %rax,%rax 40040e: 74 02 je 400412 <test> 400410: ff d0 callq *%rax 0000000000400412 <test>: 400412: 55 push %rbp 400413: 48 89 e5 mov %rsp,%rbp 400416: bf f4 05 40 00 mov $0x4005f4,%edi 40041b: e8 20 00 00 00 callq 400440 <puts@plt> 400420: 90 nop 400421: 5d pop %rbp 400422: c3 retq 400423: 48 83 c4 08 add $0x8,%rsp 400427: c3 retq
1.2.2.2. finit
1.2.2.3. init_array
init_array section 中保存的是函数指针.
1.2.2.3.1. 使用 init_array
#include <stdio.h> void test() { printf("hello world!\n"); } void (*x) () __attribute__((section (".init_array"))) = test; void (*y) () __attribute__((section (".init_array"))) = test; int main(int argc, char *argv[]) { printf("main: %p, %p\n", &main, &test); return 0; }
$> arm-linux-androideabi-gcc test.c -fPIE -pie -O3 -g3 $> ./a.out hello world! hello world! main: 0xb42933b4, 0xb4293490
$> arm-linux-androideabi-objdump -D a.out Disassembly of section .init_array: 00001ea8 <__INIT_ARRAY__>: 1ea8: ffffffff ; <UNDEFINED> instruction: 0xffffffff 00001eac <y>: 1eac: 00000490 muleq r0, r0, r4 00001eb0 <x>: 1eb0: 00000490 muleq r0, r0, r4 1eb4: 00000000 andeq r0, r0, r0 ... 00000490 <test>: 490: e59f0004 ldr r0, [pc, #4] ; 49c <test+0xc> 494: e08f0000 add r0, pc, r0 498: eaffffbf b 39c <puts@plt> 49c: 00000004 andeq r0, r0, r4
另外, 使用 PIE 后 init_array 中的数据 (函数指针) 需要被 linker 重定位, init_array 所处的 PT_LOAD segment 与 got, data, bss 等相同, 是可写的.
1.2.2.3.2. constructor
gcc 的 constructor attribute 是通过 init_array 实现的
#include <stdio.h> __attribute__((constructor)) void test() { printf("hello world!\n"); } __attribute__((constructor)) void test2 () { printf("hello world2!\n"); } int main(int argc, char *argv[]) { printf("main: %p, %p\n", &main, &test); return 0; }
$> ./a.out hello world! hello world2! main: 0xba82c3c4, 0xba82c3b4 $> arm-linux-androideabi-objdump -D ./a.out Disassembly of section .init_array: 00001ea8 <__INIT_ARRAY__>: 1ea8: ffffffff ; <UNDEFINED> instruction: 0xffffffff 1eac: 000003b4 ; <UNDEFINED> instruction: 0x000003b4 1eb0: 000003f8 strdeq r0, [r0], -r8 1eb4: 00000000 andeq r0, r0, r0 000003f8 <test2>: 3f8: e59f0004 ldr r0, [pc, #4] ; 404 <test2+0xc> 3fc: e08f0000 add r0, pc, r0 400: eaffffe5 b 39c <puts@plt> 404: 000000cc andeq r0, r0, ip, asr #1
- Backlinks
GCC Attribute (GCC Attribute > constructor): constructor
1.2.2.3.3. init_array 如何被调用
- so 中的 init_array 需要依赖 runtime linker linker_so.call_constructors
主程序的 init_array 由 startfiles (crt0.o) 提供的 _start 去调用, 所以使用了 nostartfiles 编译的程序无法通过 init_array 完成 constructor 的动作 crt0.o
#0 0x000055555555520c in A::A() () #1 0x00005555555551d8 in __static_initialization_and_destruction_0(int, int) () #2 0x000055555555520a in _GLOBAL__sub_I_a () #3 0x000055555555527d in __libc_csu_init () #4 0x00007ffff7dcf040 in __libc_start_main (main=0x555555555180 <main>, argc=1, argv=0x7fffffffc868, init=0x555555555230 <__libc_csu_init>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffc858) at ../csu/libc-start.c:264 #5 0x000055555555508e in _start () (gdb)
__libc_csu_init 的实现与下面的代码基本相同的:
void call_init_array() { typedef void (*FP)(); extern FP __init_array_start; extern FP __init_array_end; for (FP *p = &__init_array_start; p < &__init_array_end; p++) { (*p)(); } }
即它会依赖 __init_array_start 和 __init_array_end 符号, 如果在 linker script 中修改了这两个符号, 就可以影响 init_array 的执行
Backlinks
crt0.o (Bare Metal > crt > crt0.o): 1. 初始化 GP 2. 清空 BSS 3. 通过 atexit 注册 _libc_fini_array, 以便在程序结束时调用 finit_array 4. 通过 _libc_init_array 以调用 init_array 5. 调用 main 6. exit
1.2.2.4. finit_array
Backlinks
crt0.o (Bare Metal > crt > crt0.o): 1. 初始化 GP 2. 清空 BSS 3. 通过 atexit 注册 _libc_fini_array, 以便在程序结束时调用 finit_array 4. 通过 _libc_init_array 以调用 init_array 5. 调用 main 6. exit
1.2.3. text
1.2.4. data
1.2.5. rodata
1.2.6. bss
1.2.7. static linker 相关
1.2.7.1. symtab
symtab 与 dynsym 都是 symbol table, 格式相同:
struct Elf32_Sym { Elf32_Word st_name; // Symbol name (index into string table) Elf32_Addr st_value; // Value or address associated with the symbol Elf32_Word st_size; // Size of the symbol unsigned char st_info; // Symbol's type and binding attributes unsigned char st_other; // Must be zero; reserved Elf32_Half st_shndx; // Which section (header table index) it's defined in };
elf 中和 symbol 相关的有四个 section:
- dynsym
- dynstr
- symtab
- strtab
其中 dynsym 和 dynstr 在运行阶段给 loader 使用, symtab 和 strtab 是链接阶段给 linker 使用的, 并且包含一些 debug 信息给 debugger 使用.
symtab 中引用的名字位于 strtab 中, 而 dynsym 中引用的名字位于 dynstr 中.
strip 命令默认会删除掉 symtab 和 strtab 以及 dwarf 相关的 section. 但不会影响 dynsym 和 dynstr, 所以:
- 被 strip 了的 so 不影响它的 dynamic linking, 也不影响 gdb 看到它定义的 global symbol, 因为它们都定义在 dynsym 中.
被 strip 了的 relocatable file 无法被 linker 使用, 例如:
$gcc -c test.c $strip test.o $gcc main.c test.o main.c:(.text+0x15): undefined reference to `foo'
被 strip 了的 executable file 通过 gdb 无法看到那些非 so 中定义的 symbol, 例如:
$gcc -shared -fPIC -o libtest.so $gcc main.c -L. -ltest $strip /a.out $gdb /a.out (gdb) b foo Breakpoint 1 at 0x400500 (gdb) b main Function "main" not defined.
dynsym 中既包含自己定义的符号, 也包括自己引用的其他 so 中的符号, 例如:
Symbol table '.dynsym' contains 13 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 00000000000004c8 0 SECTION LOCAL DEFAULT 9 2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5 (2) 3: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__ 4: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _Jv_RegisterClasses 5: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2.5 (2) 6: 000000000020101c 0 NOTYPE GLOBAL DEFAULT ABS _edata 7: 0000000000201018 4 OBJECT GLOBAL DEFAULT 22 x 8: 00000000000005dc 31 FUNC GLOBAL DEFAULT 11 foo 9: 0000000000201030 0 NOTYPE GLOBAL DEFAULT ABS _end 10: 000000000020101c 0 NOTYPE GLOBAL DEFAULT ABS __bss_start 11: 00000000000004c8 0 FUNC GLOBAL DEFAULT 9 _init 12: 0000000000000638 0 FUNC GLOBAL DEFAULT 12 _fini
其中 foo,x 等是 libfoo.so 中自己定义的符号, 其它 so 要调用 foo 时, rtld 可以通过 libfoo.so 的 dynsym 中的 foo 对应的表项获得 foo 函数真正的地址.
而 puts 的 Ndx 为 UND, 表示 libfoo.so 中引用了 puts, 但本身没有它的定义, libfoo.so 调用 puts 时, rtld 通过 rela.plt 会引用这个 dynsym 中的 puts 对应的表项, 以便知道 rtld 需要动态加载哪个符号.
即, 符号表会包含所有定义或引用的符号.
1.2.7.2. strtab
strtab 与 dynstr 都是 string table, 格式相同. 它们包含是多个以 \0 结尾的字符串. 例如:
$> readelf -a ./a.out ... [ 4] .dynstr STRTAB 00000270 000270 00005d 00 A 0 0 1 ... $> od -c ./a.out +0x270 // with space stripped 0001160 \0__libc_init\0LIB 0001200 C\0libc.so\0__cxa_ 0001220 atexit\0printf\0x\0 0001240 y\0_edata\0_end\0__ 0001260 bss_start\0libfoo 0001300 .so\0libdl.so\0 $> readelf -p .dynstr ./a.out String dump of section '.dynstr': [ 1] __libc_init [ d] LIBC [ 12] libc.so [ 1a] __cxa_atexit [ 27] printf [ 2e] x [ 30] y [ 32] _edata [ 39] _end [ 3e] __bss_start [ 4a] libfoo.so [ 54] libdl.so
1.2.7.3. rel.text
rel.text 存在于 object 文件中, 用来保存 static linker 需要的重定位信息, 例如
Relocation section '.rela.text' at offset 0xd8 contains 1 entry: Offset Info Type Sym. Value Sym. Name + Addend 00000000000a 000400000002 R_X86_64_PC32 0000000000000000 a - 4
BFD 操作 reloc 相关的功能就是在操作 rel.text
`objdump -r` 也是通过 rel.text 来显示 reloc 信息
1.2.8. dynamic linker 相关
1.2.8.1. .dynamic section
dynamic linker 需要的所有信息都需要在运行时通过 dynamic section 得到, 它主要是通过 DT_XXX 指出动态链接需要的其它 section 的位置信息.
struct Elf32_Dyn { Elf32_Sword d_tag; // Type of dynamic table entry. union { Elf32_Word d_val; // Integer value of entry. Elf32_Addr d_ptr; // Pointer value of entry. } d_un; };
d_tag | tag_value | d_un | for exectuable | for so |
---|---|---|---|---|
DT_NULL | 0 | Ignored | Mandatory | Mandatory |
DT_NEEDED | 1 | d_val | Optional | Optional |
DT_PLTRELSZ | 2 | d_val | Optional | Optional |
DT_PLTGOT | 3 | d_ptr | Optional | Optional |
DT_HASH | 4 | d_ptr | Mandatory | Mandatory |
DT_STRTAB | 5 | d_ptr | Mandatory | Mandatory |
DT_SYMTAB | 6 | d_ptr | Mandatory | Mandatory |
DT_RELA | 7 | d_ptr | Mandatory | Optional |
DT_RELASZ | 8 | d_val | Mandatory | Optional |
DT_RELAENT | 9 | d_val | Mandatory | Optional |
DT_STRSZ | 10 | d_val | Mandatory | Mandatory |
DT_SYMENT | 11 | d_val | Mandatory | Mandatory |
DT_INIT | 12 | d_ptr | Optional | Optional |
DT_FINI | 13 | d_ptr | Optional | Optional |
DT_SONAME | 14 | d_val | Ignored | Optional |
DT_RPATH | 15 | d_val | Optional | Optional |
DT_REL | 17 | d_ptr | Mandatory | Optional |
DT_RELSZ | 18 | d_val | Mandatory | Optional |
DT_RELENT | 19 | d_val | Mandatory | Optional |
DT_JMPREL | 23 | d_ptr | Optional | Optional |
DT_BIND_NOW | 24 | Ignored | Optional | Optional |
DT_INIT_ARRAY | 25 | d_ptr | Optional | Optional |
DT_FINI_ARRAY | 26 | d_ptr | Optional | Optional |
DT_INIT_ARRAYSZ | 27 | d_val | Optional | Optional |
DT_FINI_ARRAYSZ | 28 | d_val | Optional | Optional |
DT_RUNPATH | 29 | d_val | Optional | Optional |
DT_FLAGS | 30 | d_val | Optional | Optional |
DT_PREINIT_ARRAY | 32 | d_ptr | Optional | Ignored |
DT_PREINIT_ARRAYSZ | 33 | d_val | Optional | Ignored |
… |
$> readelf -a ./a.out [16] .dynamic DYNAMIC 00001ec4 000ec4 000110 08 WA 4 0 4 Dynamic section at offset 0xec4 contains 29 entries: Tag Type Name/Value 0x00000003 (PLTGOT) 0x1fe8 0x00000002 (PLTRELSZ) 24 (bytes) 0x00000017 (JMPREL) 0x328 0x00000014 (PLTREL) REL 0x00000011 (REL) 0x300 0x00000012 (RELSZ) 40 (bytes) 0x00000013 (RELENT) 8 (bytes) 0x6ffffffa (RELCOUNT) 5 0x00000015 (DEBUG) 0x0 $> od -x ./a.out +0xec4 0007304 0003 0000 1fe8 0000 0002 0000 0018 0000 0007324 0017 0000 0328 0000 0014 0000 0011 0000 0007344 0011 0000 0300 0000 0012 0000 0028 0000 0007364 0013 0000 0008 0000 fffa 6fff 0005 0000 0007404 0015 0000 0000 0000 0006 0000 01e0 0000 0007424 000b 0000 0010 0000 0005 0000 0250 0000 0007444 000a 0000 004d 0000 0004 0000 02a0 0000 0007464 0001 0000 0012 0000 0001 0000 0044 0000 0007504 000c 0000 0420 0000 001a 0000 1ea4 0000
其中需要注意的是:
- DT_SYMTAB 和 DT_STRTAB 指向的是实际是 .dynsym 和 .dynstr (而不是 .symtab 和 .strtab), 因为 .dynamic 是给 linker 用的, 它只需要关注 .dynsym/.dynstr
- DT_NEEDED, DT_RUNPATH, DT_SONAME 的 d_val 是对应 .dynstr 的 offset, 而不是对应 .dynsym 的 index.
- DT_REL{A} 对应的实际上是 .rel{a}.dyn, DT_JMPREL{A} 对应的是 .rel{a}.plt
1.2.8.1.1. DT_SYMBOLIC
symbolic 会在一定程序上阻塞 so 上全局符号的 interposition
$> cat foo.c void bar() { printf("%s\n", "bar"); } void foo() { printf("%s\n", "foo"); bar(); } $> cat main.c #include <stdio.h> extern void foo(); void bar() { printf("%s\n", "bar from main"); } int main(int argc, char *argv[]) { foo(); return 0; } $> gcc -shared -fPIC -O0 -g3 foo.c -o libfoo.so -Wl,-Bsymbolic # -Bsymbolc 导致链接 libfoo.so 时优先从 libfoo.so 本身查找 bar, 生成 `call bar`, # 而不是生成 `call bar@plt`, 然后让 rtld 在运行时在所有 so(及 elf) 中查找 $> gcc main.c libfoo.so $> LD_LIBRARY_PATH=. ./a.out foo bar $> gcc -shared -fPIC -O0 -g3 foo.c -o libfoo.so $> LD_LIBRARY_PATH=. ./a.out foo bar from main
比较两者的代码:
$> gcc -shared -O0 -g3 -fPIC foo.c -o libfoo.so -Wl,-Bsymbolic $> objdump -D libfoo.so 000000000000063d <foo>: 63d: 55 push %rbp 63e: 48 89 e5 mov %rsp,%rbp 641: 48 8d 3d 21 00 00 00 lea 0x21(%rip),%rdi # 669 <_fini+0xd> 648: e8 e3 fe ff ff callq 530 <puts@plt> 64d: b8 00 00 00 00 mov $0x0,%eax 652: e8 d3 ff ff ff callq 62a <bar> <------ 657: 90 nop 658: 5d pop %rbp 659: c3 retq $> gcc -shared -O0 -g3 -fPIC foo.c -o libfoo.so $> objdump -D libfoo.so 000000000000065d <foo>: 65d: 55 push %rbp 65e: 48 89 e5 mov %rsp,%rbp 661: 48 8d 3d 21 00 00 00 lea 0x21(%rip),%rdi # 689 <_fini+0xd> 668: e8 d3 fe ff ff callq 540 <puts@plt> 66d: b8 00 00 00 00 mov $0x0,%eax 672: e8 d9 fe ff ff callq 550 <bar@plt> <---- 通过 plt 查找, loader 会负责将 main 中的 bar 写到 got.plt 中 677: 90 nop 678: 5d pop %rbp 679: c3 retq
实际上即使没有针对 symbolic 生成 `call bar` 形式的代码, rtld 在处理 DT_SYMBOLIC 时, 也会优先从当前 so 中查找符号, 保证在有 DT_SYMBOLIC 时将 libfoo.so 中的 bar 写到 got.plt 中以满足 symbolic 的要求.
Bsymbolic 的一个使用场景是 rtld 本身: 做为一个 DSO, 它会使用 Bsymbolic 保证在完成自身的动态链接之前也可以正确的进行函数调用, 因为 Bsymbolic 可以避免对函数进行重定位.
通过控制函数的 visibility 也可以达到相同的效果 (例如使用 `protected` visibility), 但由于 rtld 需要调用 libc.a 中的代码, 这部分公共代码是不能修改 visibility 的
- Backlinks
GCC Attribute (GCC Attribute > visibility): 其中 `protected` visibility 功能与 DT_SYMBOLIC 类似
1.2.8.2. interp
const char interp_section[] __attribute__((section(".interp"))) = "/path/to/dynamic/linker";
1.2.8.3. got
1.2.8.3.1. got 的格式
got 的格式类似于 int[], 数组中保存的符号没有经过 patch 的地址, 例如:
$> readelf -a ./a.out ... vaddr offset [17] .got PROGBITS 00001fd4 000fd4 00002c 00 WA 0 0 4 $> arm-linux-androideabi-objdump -D ./a.out Disassembly of section .got: 00001fd4 <_GLOBAL_OFFSET_TABLE_-0x14>: 1fd4: 00001ebc ; <UNDEFINED> instruction: 0x00001ebc 1fd8: 00001eac andeq r1, r0, ip, lsr #29 1fdc: 00001ea4 andeq r1, r0, r4, lsr #29 1fe0: 00001eb4 ; <UNDEFINED> instruction: 0x00001eb4 1fe4: 00000454 andeq r0, r0, r4, asr r4 00001fe8 <_GLOBAL_OFFSET_TABLE_>: ... 1ff4: 00000374 andeq r0, r0, r4, ror r3 1ff8: 00000374 andeq r0, r0, r4, ror r3 1ffc: 00000374 andeq r0, r0, r4, ror r3 $> od -x ./a.out +0xfd4 0007724 1ebc 0000 1eac 0000 1ea4 0000 1eb4 0000 0007744 0454 0000 0000 0000 0000 0000 0000 0000 0007764 0374 0000 0374 0000 0374 0000 000c 0000 0010004 0000 0000 4700 4343 203a 4728 554e 2029
1.2.8.3.2. 全局变量与 got (arm)
~/source/sharklj1@tj02433pcu> d debug exec out/target/product/sp9850j_1h10/symbols/system/bin/linker /system/bin/linker Remote debugging from host 127.0.0.1 __dl__start () at bionic/linker/arch/arm/begin.S:32 32 mov r0, sp (gdb) b __dl___linker_init Breakpoint 1 at 0xaaab6e38: __dl___linker_init. (2 locations) (gdb) c Continuing. Breakpoint 1, __linker_init (raw_args=0xfffefa80) at bionic/linker/linker.cpp:4400 4400 KernelArgumentBlock args(raw_args); (gdb) n 4402 ElfW(Addr) linker_addr = args.getauxval(AT_BASE); (gdb) 4403 ElfW(Addr) entry_point = args.getauxval(AT_ENTRY); (gdb) 4404 ElfW(Ehdr)* elf_hdr = reinterpret_cast<ElfW(Ehdr)*>(linker_addr); (gdb) 4405 ElfW(Phdr)* phdr = reinterpret_cast<ElfW(Phdr)*>(linker_addr + elf_hdr->e_phoff); (gdb) 4407 soinfo linker_so(nullptr, nullptr, nullptr, 0, 0); (gdb) 4416 if (reinterpret_cast<ElfW(Addr)>(&_start) == entry_point) { (gdb) ni 0xf7787e80 4416 if (reinterpret_cast<ElfW(Addr)>(&_start) == entry_point) { (gdb) disass Dump of assembler code for function __linker_init(void*): ... 0xf7787e7e <+82>: ldr r1, [sp, #544] ; 0x220 => 0xf7787e80 <+84>: ldr r2, [pc, #316] ; (0xf7787fc0 <__linker_init(void*)+404>) 0xf7787e82 <+86>: add r2, pc 0xf7787e84 <+88>: ldr r2, [r2, #0] 0xf7787e86 <+90>: cmp r2, r1 (gdb) p /x $r2 $1 = 0x62f6a // 0x62f6a 存储在 [pc,#316] 处, 这里是 __dl___linker_init 函数的 literal pool 区域, 表示 pc 与 GOT[__dl__start] 的偏移量 // 所以后面通过 pc+0x62f6a (0xf77eadf0) 可以得到 GOT[__dl__start] (gdb) ni 0xf7787e84 4416 if (reinterpret_cast<ElfW(Addr)>(&_start) == entry_point) { (gdb) p /x $r2 $3 = 0xf77eadf0 (gdb) x 0xf77eadf0 0xf77eadf0: 0x000028fc // 由于 linker 做为 interp map 在 0xf777b000, 所以 GOT 在原 elf 中的位置是 0xf77eadf0 - 0xf777b000 = 0x6fdf0 // Disassembly of section .got: // 0006fddc <__dl__GLOBAL_OFFSET_TABLE_-0x218>: // 6fddc: 000715b8 ; <UNDEFINED> instruction: 0x000715b8 // 6fde0: 000711d0 ldrdeq r1, [r7], -r0 // 6fde4: 000716bc ; <UNDEFINED> instruction: 0x000716bc // 6fde8: 0006eaf4 strdeq lr, [r6], -r4 // 6fdec: 00070174 andeq r0, r7, r4, ror r1 // 6fdf0: 000028fc strdeq r2, [r0], -ip <--- 28fc
1.2.8.3.3. 全局变量与 got (x86)
/* main.c */ extern void foo(); extern void bar(); int main(int argc, char *argv[]) { foo(); bar(); return 0; } /* foo.c */ int x = 0xab; int foo() { printf("foo\n"); x = 0xcd; } /* bar.c */ extern int x; int bar() { printf("bar\n"); x = 0x12; }
$ gdb ./a.out (gdb) b main Breakpoint 1 at 0x400648 (gdb) r Starting program: /home/sunway/a.out Breakpoint 1, 0x0000000000400648 in main () (gdb) disass foo Dump of assembler code for function foo: 0x00007ffff7bd85dc <+0>: push %rbp 0x00007ffff7bd85dd <+1>: mov %rsp,%rbp 0x00007ffff7bd85e0 <+4>: lea 0x5f(%rip),%rdi # 0x7ffff7bd8646 0x00007ffff7bd85e7 <+11>: callq 0x7ffff7bd84f0 <puts@plt> 0x00007ffff7bd85ec <+16>: mov 0x2009d5(%rip),%rax # 0x7ffff7dd8fc8 0x00007ffff7bd85f3 <+23>: movl $0xcd,(%rax) 0x00007ffff7bd85f9 <+29>: pop %rbp 0x00007ffff7bd85fa <+30>: retq End of assembler dump. 0x7ffff7dd8fc8 这个地址实际上位于 foo.so 的 .got section. 因为: pmap 显示 libfoo.so 被映射到 0x00007ffff7bd8000: 00007ffff7bd8000 4K r-x-- /home/sunway/libfoo.so 00007ffff7bd9000 2044K ----- /home/sunway/libfoo.so 00007ffff7dd8000 4K r---- /home/sunway/libfoo.so 00007ffff7dd9000 4K rw--- /home/sunway/libfoo.so 0x7ffff7dd8fc8 - 0x00007ffff7bd8000 = 0x200fc8 然后通过 readelf -S libfoo.so | grep " .got " 结果为: [20] .got PROGBITS 0000000000200fc8 00000fc8 所以 0x7ffff7dd8fc8 实际就是 libfoo.so 被加载后的 GOT[0] (gdb) x /xg 0x7ffff7dd8fc8 0x7ffff7dd8fc8: 0x00007ffff7dd9018 0x00007ffff7dd9018 这个地址是 x 变量的实际地址, 去掉 libfoo.so 映射的首地址 00007ffff7bd8000 后为 0x201018, 通过 readelf -S libfoo.so: [22] .data PROGBITS 0000000000201010 00001010 000000000000000c 0000000000000000 WA 0 0 8 [23] .bss NOBITS 0000000000201020 0000101c 0000000000000010 0000000000000000 WA 0 0 8 可以确定这个地址位于 .data section. (gdb) x 0x00007ffff7dd9018 0x7ffff7dd9018 <x>: 0x00000000000000ab 所以真正的 x 变量位于 libfoo.so 的 .data 中, 且其地址被写在 libfoo.so 的 GOT[0] 中, libfoo.so 中对 x 的引用都通过 GOT[0] 进行. 同理, 观察 libbar.so 对 x 的引用: (gdb) disass bar Dump of assembler code for function bar: 0x00007ffff79d65dc <+0>: push %rbp 0x00007ffff79d65dd <+1>: mov %rsp,%rbp 0x00007ffff79d65e0 <+4>: lea 0x5f(%rip),%rdi # 0x7ffff79d6646 0x00007ffff79d65e7 <+11>: callq 0x7ffff79d64f0 <puts@plt> 0x00007ffff79d65ec <+16>: mov 0x2009d5(%rip),%rax # 0x7ffff7bd6fc8 0x00007ffff79d65f3 <+23>: movl $0x12,(%rax) 0x00007ffff79d65f9 <+29>: pop %rbp 0x00007ffff79d65fa <+30>: retq End of assembler dump. 0x7ffff7bd6fc8 这个地址位于 libbar.so 的 GOT[0] (gdb) x /gx 0x7ffff7bd6fc8 0x7ffff7bd6fc8: 0x00007ffff7dd9018 可见 libbar.so 的 GOT[0] 与 libfoo.so 的 GOT[0] 均指向 libfoo.so 的 .data section 中 x 的实际地址.
在使用 GOT 表时, so 的代码并不需要 `GOT 基址` 这样的数据, 因为 GOT 与代码的距离的在静态链接时就是可以确定的:
以 objdump -d libfoo.so 为例:
00000000000005dc <foo>: 5dc: 55 push %rbp 5dd: 48 89 e5 mov %rsp,%rbp 5e0: 48 8d 3d 5f 00 00 00 lea 0x5f(%rip),%rdi # 646 <_fini+0xe> 5e7: e8 04 ff ff ff callq 4f0 <puts@plt> 5ec: 48 8b 05 d5 09 20 00 mov 0x2009d5(%rip),%rax # 200fc8 <_DYNAMIC+0x180> 5f3: c7 00 cd 00 00 00 movl $0xcd,(%rax) 5f9: 5d pop %rbp 5fa: c3 retq
这里的 0x2009d5 就是代码与 GOT 的偏移量, 当 so 被静态链接时, so 基址为 0, rip 为 5f3, 所以 GOT 位于 0x5f3 + 0x2009d5 = 0x200fc8, 当 so 被加载后,假设加载到 0x7ffff7bd8000, 则运行时 rip 会变为 0x7ffff7bd8000 + 0x5f3, 所以运行时 GOT 计算的结果为: 0x200fc8 + 0x7ffff7bd8000 = 0x7ffff7dd8fc8
1.2.8.4. plt
1.2.8.4.1. 函数与 plt
/* gcc -shared -fPIC -o libfoo.so foo.c */ /* gcc -shared -fPIC -o libbar.so bar.c */ /* gcc main.c -L. -lfoo -lbar */ /* main.c */ extern void foo(); extern void bar(); int main(int argc, char *argv[]) { foo(); bar(); return 0; } /* foo.c */ int foo() { printf("foo\n"); } /* bar.c */ int bar() { printf("bar\n"); }
$ LD_LIBRARY_PATH=. gdb a.out (gdb) b main (gdb) r (gdb) disass Dump of assembler code for function main: 0x0000000000400644 <+0>: push %rbp 0x0000000000400645 <+1>: mov %rsp,%rbp 0x0000000000400648 <+4>: sub $0x10,%rsp 0x000000000040064c <+8>: mov %edi,-0x4(%rbp) 0x000000000040064f <+11>: mov %rsi,-0x10(%rbp) => 0x0000000000400653 <+15>: mov $0x0,%eax 0x0000000000400658 <+20>: callq 0x400550 <foo@plt> 0x000000000040065d <+25>: mov $0x0,%eax 0x0000000000400662 <+30>: callq 0x400530 <bar@plt> 0x0000000000400667 <+35>: mov $0x0,%eax 0x000000000040066c <+40>: leaveq 0x000000000040066d <+41>: retq End of assembler dump. foo 函数显示的地址为 0x400550, 这个并不是 foo.c 中定义的那个真正的 foo 函数的地址, 而只是 .plt 表中的地址: 这个 plt 表项位于 a.out 的 第一个 LOAD segment 中 (类型为 RE) 实际上, a.out 的 .plt 中一共有四项, 分别为 plt0 (__ld_runtime_resolve 的代码), bar@plt, __libc_start_main@plt 以及 foo@plt 比如: 0x400520 是 .plt 的起始地址: ~@sunway-work> readelf -S ./a.out |grep " .plt" [12] .plt PROGBITS 0000000000400520 00000520 (gdb) x /10g 0x400520 0x400520: 0x25ff00200aca35ff 0x00401f0f00200acc 0x400530 <bar@plt>: 0x006800200aca25ff 0xffffffe0e9000000 0x400540 <__libc_start_main@plt>: 0x016800200ac225ff 0xffffffd0e9000000 0x400550 <foo@plt>: 0x026800200aba25ff 0xffffffc0e9000000 0x400560 <_start>: 0x89485ed18949ed31 0x495450f0e48348e2 看一下 foo@plt (PLT[2]) 的具体代码: (gdb) disass 0x400550 Dump of assembler code for function foo@plt: 0x0000000000400550 <+0>: jmpq *0x200aba(%rip) # 0x601010 <[email protected]> 0x0000000000400556 <+6>: pushq $0x2 0x000000000040055b <+11>: jmpq 0x400520 End of assembler dump. 0x601010 对应于 .got.plt section 中的一项: (gdb) x 0x601010 0x601010 <[email protected]>: 0x0000000000400556 当 .got.plt 表项第一次被使用时 (未经过 rtld 解析过), 其值均为相应的 plt 指令的下一条指令地址, 在这 里即 pushq $0x2 指令的地址, rtld 解析过后会修改 .got.plt 表项的值为真正的地址. pushq $0x2 指令将 0x2 入栈, 这里的 0x2 指的实际上是 foo 函数在 a.out 的 .rela.plt section 中的索引, .rela.plt 的内容为: Relocation section '.rela.plt' at offset 0x4b8 contains 3 entries: Offset Info Type Sym. Value Sym. Name + Addend 000000601000 000100000007 R_X86_64_JUMP_SLO 0000000000000000 bar + 0 000000601008 000200000007 R_X86_64_JUMP_SLO 0000000000000000 __libc_start_main + 0 000000601010 000400000007 R_X86_64_JUMP_SLO 0000000000000000 foo + 0 rtld 通过 .rela.plt[2] 就可以解析到 dynsym 中对应的符号信息 (foo) 以及要写回的 .got.plt 表的位置 (0x601010) jmpq 0x400520 是跳转到 rtld (__ld_runtime_resolve) 函数去动态加载 foo 函数. 0x400520 实际上就是 PLT[0] (gdb) disass 0x400520,0x400530 Dump of assembler code from 0x400520 to 0x400530: 0x0000000000400520: pushq 0x200aca(%rip) # 0x600ff0 <_GLOBAL_OFFSET_TABLE_+8> 0x0000000000400526: jmpq *0x200acc(%rip) # 0x600ff8 <_GLOBAL_OFFSET_TABLE_+16> 0x000000000040052c: nopl 0x0(%rax) End of assembler dump. 0x600ff8 即 .got.plt[16] 保存着 __ld_runtime_resolve 函数的地址: (gdb) x 0x600ff8 0x600ff8 <_GLOBAL_OFFSET_TABLE_+16>: 0x00007ffff7def200 (gdb) disass 0x00007ffff7def200 Dump of assembler code for function _dl_runtime_resolve: 0x00007ffff7def200 <+0>: sub $0x38,%rsp 0x00007ffff7def204 <+4>: mov %rax,(%rsp) 0x00007ffff7def208 <+8>: mov %rcx,0x8(%rsp) 0x00007ffff7def20d <+13>: mov %rdx,0x10(%rsp) 0x00007ffff7def212 <+18>: mov %rsi,0x18(%rsp) 0x00007ffff7def217 <+23>: mov %rdi,0x20(%rsp) 0x00007ffff7def21c <+28>: mov %r8,0x28(%rsp) 0x00007ffff7def221 <+33>: mov %r9,0x30(%rsp) 0x00007ffff7def226 <+38>: mov 0x40(%rsp),%rsi 0x00007ffff7def22b <+43>: mov 0x38(%rsp),%rdi 0x00007ffff7def230 <+48>: callq 0x7ffff7de8680 <_dl_fixup> 0x00007ffff7def235 <+53>: mov %rax,%r11 0x00007ffff7def238 <+56>: mov 0x30(%rsp),%r9 0x00007ffff7def23d <+61>: mov 0x28(%rsp),%r8 0x00007ffff7def242 <+66>: mov 0x20(%rsp),%rdi 0x00007ffff7def247 <+71>: mov 0x18(%rsp),%rsi 0x00007ffff7def24c <+76>: mov 0x10(%rsp),%rdx 0x00007ffff7def251 <+81>: mov 0x8(%rsp),%rcx 0x00007ffff7def256 <+86>: mov (%rsp),%rax 0x00007ffff7def25a <+90>: add $0x48,%rsp 0x00007ffff7def25e <+94>: jmpq *%r11 End of assembler dump. (gdb) n foo 7 bar(); foo 执行完毕, 此时再看一次 foo@plt 相关的信息: (gdb) disass 0x400550 Dump of assembler code for function foo@plt: 0x0000000000400550 <+0>: jmpq *0x200aba(%rip) # 0x601010 <[email protected]> 0x0000000000400556 <+6>: pushq $0x2 0x000000000040055b <+11>: jmpq 0x400520 End of assembler dump. .plt[2] 的内容没有变化, 再看相应的 .got.plt 表项: (gdb) x 0x601010 0x601010 <[email protected]>: 0x00007ffff7bd85ac 而执行前的结果为: (gdb) x 0x601010 0x601010 <[email protected]>: 0x0000000000400556 可见, __ld_runtime_resolve 已经将 [email protected] 的值修改了, 这个 0x00007ffff7bd85ac 实际上就是 libfoo.so 中 foo 函数的真正地址: (gdb) disass 0x00007ffff7bd85ac Dump of assembler code for function foo: 0x00007ffff7bd85ac <+0>: push %rbp 0x00007ffff7bd85ad <+1>: mov %rsp,%rbp 0x00007ffff7bd85b0 <+4>: lea 0x4f(%rip),%rdi # 0x7ffff7bd8606 0x00007ffff7bd85b7 <+11>: callq 0x7ffff7bd84c0 <puts@plt> 0x00007ffff7bd85bc <+16>: pop %rbp 0x00007ffff7bd85bd <+17>: retq End of assembler dump.
1.2.8.4.2. 总结
每个 so 均有其 PLT 表, 当使用 PIE 时所有非 static 的函数均通过 PLT 表引用.
一个动态加载的函数符号的解析需要涉及到以下的 elf section:
- .plt
- .got.plt
- .rela.plt
- 其他 so 的 .dynsym 和 .dynstr
plt[0] 保存着 __ld_runtime_resolve 的地址, 进行符号的动态加载.
.got.plt 与 .got 类似, 都是需要被 rtld 修改的, 不同的是 .got 主要是为了定位全局变量, 而 .got.plt 是为了定位函数.
这个 section 被加载到类型为 RW 的第二个 LOAD segment, 而 .plt 和 .rela.plt 是不变的, 所以它们被加载到类型 RE 的第一个 LOAD segment, 即:
Section to Segment mapping: Segment Sections... 00 01 .interp 02 .interp ... .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .rodata ... ~~~~~~~~~~ ~~~~~ 03 .ctors .dtors .jcr .dynamic .got .got.plt .data .bss ~~~~~~~~ 04 .dynamic ...
__ld_runtime_resolve 通过 .rela.plt 获得要加载的符号的名字和要改写的 .got.plt 项的地址.
最终 __ld_runtime_resolve 需要查找其它 so 的 .dynsym 和 .dynstr 来确定函数真正的地址.
1.2.8.4.3. no-plt
arm 有一个编译选项是 `-mno-plt`, 相当于对 plt 进行 inline, 可以避免一次跳转
foo: stp x29, x30, [sp, -32]! mov x29, sp str w0, [sp, 28] # 下面三行相当于 bar@plt, 直接被 inline 在这里了 adrp x0, :got:bar ldr x0, [x0, #:got_lo12:bar] blr x0 nop ldp x29, x30, [sp], 32 ret
1.2.8.5. dynsym
1.2.8.6. dynstr
1.2.8.7. got.plt
got.plt 与 got 类似, 但它保存的是函数的地址 (而不是全局变量的地址). plt 需要用到 got.plt
1.2.8.8. rel.dyn
rel.dyn 是用来指示哪些需要重定位的, 重定位一般是修改 GOT 表 (但也有例外, 比如后面的 rela.dyn, 重定位修改的是.data 部分的值), 通过 rel.dyn, loader 才知道 GOT 表中的各个项需要如何修改
rel.dyn 的格式为:
struct Elf32_Rel { Elf32_Addr r_offset; Elf32_Word r_info; }
其中:
- r_offset 是需要 patch 的 vaddr, 一般位于 got 表中
- r_info 是一个 int, 包含了 rel 的类型和 dynsym 的索引
1.2.8.8.1. 例子
/* test.c */ #include <stdio.h> extern int x; extern int y; int main(int argc, char *argv[]) { printf("main: %x\n", &x, &y); return 0; } /* foo.c */ int x = 10; int y = 11;
$> arm-linux-androideabi-gcc foo.c -fPIC -shared -o libfoo.so -g3 -O0 $> arm-linux-androideabi-gcc test.c -fPIE -pie -O0 -g3 -L. -lfoo $> arm-linux-androideabi-readelf -a ./a.out Relocation section '.rel.dyn' at offset 0x33c contains 7 entries: Offset Info Type Sym.Value Sym. Name 00001fcc 00000017 R_ARM_RELATIVE 00001fd0 00000017 R_ARM_RELATIVE 00001fd4 00000017 R_ARM_RELATIVE 00001fd8 00000017 R_ARM_RELATIVE 00001fdc 00000017 R_ARM_RELATIVE 00001fe0 00000415 R_ARM_GLOB_DAT 00000000 x 00001fe4 00000515 R_ARM_GLOB_DAT 00000000 y 其中: 0x1fe0 位于 GOT 表中, 它存储的值是 x 的地址; 0x415 << 8 = 4, 代表 x 在 dynsym 中的索引. 0x415 & 0xff = 0x15, 代表 rel 的类型: R_ARM_GLOB_DAT Symbol table '.dynsym' contains 9 entries: Num: Value Size Type Bind Vis Ndx Name 0: 00000000 0 NOTYPE LOCAL DEFAULT UND 1: 00000000 0 FUNC GLOBAL DEFAULT UND __libc_init@LIBC (2) 2: 00000000 0 FUNC GLOBAL DEFAULT UND __cxa_atexit@LIBC (2) 3: 00000000 0 FUNC GLOBAL DEFAULT UND printf@LIBC (2) 4: 00000000 0 OBJECT GLOBAL DEFAULT UND x 5: 00000000 0 OBJECT GLOBAL DEFAULT UND y 6: 00002000 0 NOTYPE GLOBAL DEFAULT ABS _edata 7: 00002004 0 NOTYPE GLOBAL DEFAULT ABS _end 8: 00002000 0 NOTYPE GLOBAL DEFAULT ABS __bss_start
这样 loader 在工作时, 就可以从 rel.dyn 和 dynsym (以及 dynstr) 中获得需要重定位的项的信息:
- 符号的名字
- 重定位类型
- 定位完成后需要 patch 的地址 (GOT 表项)
1.2.8.9. rel.plt
rel.plt 与 got.plt 的关系, 类似于 rel.dyn 与 got 的关系:
rel.dyn 用来索引 got, 以便对数据进行重定位. rel.plt 用来索引 got.plt, 以便对函数进行重定位.
rel.plt 的格式与 rel.dyn 是一样的, 都是 Elf32_Rel, 它的 r_offset 指示的 vaddr 位于 got.plt 表中
通过 r_info 中的 relocation type, 可以区分出 rel.plt 与 rel.dyn
$> cat main.c extern void foo(); int main(int argc, char *argv[]) { foo(); return 0; } $> arm-linux-androideabi-gcc main.c -fPIE -pie -L. -lfoo $> arm-linux-androideabi-readelf -a ./a.out Relocation section '.rel.plt' at offset 0x330 contains 3 entries: Offset Info Type Sym.Value Sym. Name 00001ff4 00000116 R_ARM_JUMP_SLOT 00000000 __libc_init@LIBC 00001ff8 00000216 R_ARM_JUMP_SLOT 00000000 __cxa_atexit@LIBC 00001ffc 00000316 R_ARM_JUMP_SLOT 00000000 foo ~~~~~~~~ $> arm-linux-androideabi-objdum -D ./a.out 00001fe8 <_GLOBAL_OFFSET_TABLE_>: ... 1ff4: 00000348 andeq r0, r0, r8, asr #6 1ff8: 00000348 andeq r0, r0, r8, asr #6 1ffc: 00000348 andeq r0, r0, r8, asr #6 ;; 所以 linker 将来需要修改 1ffc 处的值为重定位后的 plt[0] 的地址 (0x348 是没有经过重定位的 plt[0]) 00000374 <foo@plt>: 374: e28fc600 add ip, pc, #0, 12 <--- ip = pc + 0<<12 = 37c 378: e28cca01 add ip, ip, #4096 ; 0x1000 <--- ip = 37c + 0x10000 = 0x137c 37c: e5bcfc80 ldr pc, [ip, #3200]! ; 0xc80 <--- pc= [0x137c+0xc80] = [0x1ffc] = 0x348 ;; 0x348 实际就是 plt[0], 即 linker 的 resolver. 00000348 <__libc_init@plt-0x14>: 348: e52de004 push {lr} ; (str lr, [sp, #-4]!) 34c: e59fe004 ldr lr, [pc, #4] ; 358 <note_end+0x178> 350: e08fe00e add lr, pc, lr 354: e5bef008 ldr pc, [lr, #8]! 358: 00001c90 muleq r0, r0, ip
1.2.8.10. rela.dyn
rela.dyn 与 rel.dyn 基本是一样的, 唯一的不同的 rela 比 rel 多了一个 addend 成员.
struct Elf32_Rela { Elf32_Addr r_offset; // Location (file byte offset, or program virtual addr) Elf32_Word r_info; // Symbol table index and type of relocation to apply Elf32_Sword r_addend; // Compute value for relocatable field by adding this }
新的平台如 arm64, x86_64 都使用 rela 代替了 rel. 因为使用 rel 需要比 rela 多访问一次内存.
1.2.8.10.1. rela on arm64
$> cat foo.c int xxx[4]; $> aarch64-linux-gnu-gcc foo.c -shared -fPIC -o libfoo.so -O0 -g3 $> cat main.c #include <stdio.h> extern int xxx[4]; int *j = xxx + 3; int *k = xxx; int main(int argc, char *argv[]) { printf("%p %p\n", j, k); return 0; } $> aarch64-linux-gnu-gcc main.c -fPIE -pie -O0 -g3 libfoo.so $> readelf -a ./a.out Relocation section '.rela.dyn' at offset 0x638 contains 13 entries: Offset Info Type Sym. Value Sym. Name + Addend ... 000000010ce8 001400000401 R_AARCH64_GLOB_DA 0000000000000000 _ITM_registerTMCloneTa + 0 000000010d40 000a00000101 R_AARCH64_ABS64 0000000000000000 xxx + c 000000010d48 000a00000101 R_AARCH64_ABS64 0000000000000000 xxx + 0 ;; 其中 10d40 位于 .data, 它存储的值是 j 的地址 (即 xxx + 3*4 = xxx + ;; c), 在 rela 中, c 会作为 addend 保存在 rela entry 中
1.2.8.10.2. rel on arm32
$> arm-linux-androideabi-gcc foo.c -fPIC -shared -o libfoo.so -g3 -O0 $> arm-linux-androideabi-gcc main.c -fPIE -pie -L. -lfoo -O0 -g3 $> readelf -a ./a.out Relocation section '.rel.dyn' at offset 0x324 contains 7 entries: Offset Info Type Sym.Value Sym. Name ... 00002000 00000402 R_ARM_ABS32 00000000 xxx 00002004 00000402 R_ARM_ABS32 00000000 xxx ;; 在 rel 中, i,j 对应的 entry 都和 xxx 符号有关, 由于没有 addend 值, 如 ;; 何确定 i, j 相对 xxx 的偏移量? 实际上, 这个偏移量是做为初值保存在 .data 中的 $> arm-linux-androideabi-objdump -D ./a.out 00001fe8 <_GLOBAL_OFFSET_TABLE_>: ... 1ff4: 00000374 andeq r0, r0, r4, ror r3 1ff8: 00000374 andeq r0, r0, r4, ror r3 1ffc: 00000374 andeq r0, r0, r4, ror r3 Disassembly of section .data: 00002000 <j>: 2000: 0000000c andeq r0, r0, ip ~~~~~~~~ 00002004 <k>: 2004: 00000000 andeq r0, r0, r0 ~~~~~~~~ ;; GOT 中对于 i,j 的初值是无用的, 刚好可以用来保存 addend, 但 linker ;; 显然需要多一次内存访问.
1.2.8.10.3. note
从前面 rel 及 rela 的实现方式可以看到, loader 可以通过重定位的方式给全局变量赋初值, 但仅限于 symbol_addr + addend 的方式, 所以考虑下面的代码能否编译通过:
/* 1. false */ extern int x; int y = x; /* 2. true */ extern int x; int *y = &x + 1; /* 3. true */ extern int x; int y = (int)&x; /* 4. false */ extern int x; int y = (int)(&x) + int(&x);
1.2.8.11. rela.plt
1.2.8.12. data.rel.ro
1.2.9. 其它
1.2.9.1. eh_frame
1.2.9.2. hash
find_symbol_by_name 使用 .hash section 来查找 symbol
https://blogs.oracle.com/ali/gnu-hash-elf-sections http://www.sco.com/developers/gabi/latest/ch5.dynamic.html#hash
1.2.9.2.1. .hash section 的格式
.hash 使用链表来解决 hash 冲突
nbucket nchain bucket[0] . . . bucket[nbucket-1] chain[0] . . . chain[nchain-1]
1.2.9.2.2. hash 函数
unsigned long elf_Hash(const unsigned char *name) { unsigned long h = 0, g; while (*name) { h = (h << 4) + *name++; if (g = h & 0xf0000000) h ^= g >> 24; h &= ~g; } return h; } int main(int argc, char *argv[]) { printf("%x\n", elf_Hash(argv[1])); return 0; }
1.2.9.2.3. example
libfoo.so:
int xxx = 0; void zzz() { }
$> arm-linux-androideabi-gcc -O0 -g3 foo.c -shared -fPIC -o libfoo.so $> readelf -a ./libfoo.so [ 4] .hash HASH 00000208 000208 000034 04 A 2 0 4 ... Symbol table '.dynsym' contains 8 entries: Num: Value Size Type Bind Vis Ndx Name 0: 00000000 0 NOTYPE LOCAL DEFAULT UND 1: 00000000 0 FUNC GLOBAL DEFAULT UND __cxa_finalize@LIBC (2) 2: 00000000 0 FUNC GLOBAL DEFAULT UND __cxa_atexit@LIBC (2) 3: 00002004 4 OBJECT GLOBAL DEFAULT 17 xxx 4: 00000308 20 FUNC GLOBAL DEFAULT 11 zzz 5: 00002004 0 NOTYPE GLOBAL DEFAULT ABS _edata 6: 00002008 0 NOTYPE GLOBAL DEFAULT ABS _end 7: 00002004 0 NOTYPE GLOBAL DEFAULT ABS __bss_start $> od -x ./libfoo.so +0x208|more 0001010 0003 0000 0008 0000 0005 0000 0006 0000 0001030 0007 0000 0000 0000 0000 0000 0000 0000 0001050 0000 0000 0003 0000 0004 0000 0002 0000 0001070 0001 0000 0000 0002 0002 0001 0001 0001 0001110 0001 0001 0001 0001 0001 0001 776f 08d6 0001130 0014 0000 0000 0000 001d 0000 0000 0000 0001150 0001 0001 0015 0000 0010 0000 0000 0000 0001170 0d63 0005 0000 0002 0010 0000 0000 0000 0001210 1ed8 0000 0017 0000 1ff8 0000 0216 0000
按 .hash 的格式解析后为:
- nbucket: 3
- nchain: 8
- bucket[0] = 5
- bucket[1] = 6
- bucket[2] = 7
- chain[0] = 0
- chain[1] = 0
- chain[2] = 0
- chain[3] = 0
- chain[4] = 3
- chain[5] = 4
- chain[6] = 2
- chain[7] = 1
以查找 __bss_start 为例:
- elfhash __bss_start 为 0x90ff134
- bucket 索引为 0x90ff134 % nbucket(3) = 2
- 检查 bucket[2] = dynsym[7] = __bss_start, 找到
以查找一个不存在的 hello 为例:
- elfhash hello = 0x6ec32f % 3 = 1
- bucket[1] = 6, dynsym[6] = _end, 不相等
- chain[6] = 2, dynsym[2] = __cxa_atexit@LIBC (2), 不相等
- chain[2] = 0, 查找结束, 因为 0 是一个占位符, dynsym[0] 不包含有效值
1.2.9.2.4. 关于 bucket 和 chain:
bucket[x] 相当于所有 hash 为 x 的符号组成的链表的头, 而 chain[bucket[x]], chain[chain[bucket[x]]]…构成链表.
所以, dynsym 中的符号构成以下几条链表:
[5,4,3], [6,2] [7,1]
1.2.9.3. gnu.hash
1.2.9.4. gnu.version
gnu.version 是对 dynsym 的补充, 表示每个 dynsym 的符号的 version
typedef Elf32_Half Elf32_Versym;
$> cat test.c void foo() { printf("foo from LIBTEST_V1.0\n"); } $> cat v.map LIBTEST_V1.0 { global: foo; local: *; }; $> gcc -shared test.c -o libtest.so -Wl,--version-script,v.map $> readelf -a ./libtest.so Symbol table '.dynsym' contains 8 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab 2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5 (3) 3: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__ 4: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable 5: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2.5 (3) 6: 0000000000000000 0 OBJECT GLOBAL DEFAULT ABS LIBTEST_V1.0 7: 00000000000005ba 19 FUNC GLOBAL DEFAULT 13 foo@@LIBTEST_V1.0 Version symbols section '.gnu.version' contains 8 entries: Addr: 0000000000000364 Offset: 0x000364 Link: 3 (.dynsym) 000: 0 (*local*) 0 (*local*) 3 (GLIBC_2.2.5) 0 (*local*) 004: 0 (*local*) 3 (GLIBC_2.2.5) 2 (LIBTEST_V1.0) 2 (LIBTEST_V1.0) $> od -x libtest.so +0x364 0001544 0000 0000 0003 0000 0000 0003 0002 0002 ...
1.2.9.5. gnu.version_d
version definition
typedef struct { Elf32_Half vd_version; Elf32_Half vd_flags; Elf32_Half vd_ndx; Elf32_Half vd_cnt; Elf32_Word vd_hash; Elf32_Word vd_aux; Elf32_Word vd_next; } Elf32_Verdef;
Version definition section '.gnu.version_d' contains 2 entries: Addr: 0x0000000000000378 Offset: 0x000378 Link: 4 (.dynstr) 000000: Rev: 1 Flags: BASE Index: 1 Cnt: 1 Name: libtest.so 0x001c: Rev: 1 Flags: none Index: 2 Cnt: 1 Name: LIBTEST_V1.0 Version definition past end of section
1.2.9.6. gnu.version_r
version needs
typedef struct { Elf32_Half vn_version; Elf32_Half vn_cnt; Elf32_Word vn_file; Elf32_Word vn_aux; Elf32_Word vn_next; } Elf32_Verneed;
$> readelf -a libtest.so // libtest.so 需要 GLIBC_2.2.5 版本的 libc.so.6 Version needs section '.gnu.version_r' contains 1 entries: Addr: 0x00000000000003b0 Offset: 0x0003b0 Link: 4 (.dynstr) 000000: Version: 1 File: libc.so.6 Cnt: 1 0x0010: Name: GLIBC_2.2.5 Flags: none Version: 3 $> gcc main.c ./libtest.so $> readelf -a ./a.out // a.out 需要 LIBTEST_V1.0 的 ./libtest.so Version needs section '.gnu.version_r' contains 2 entries: Addr: 0x0000000000000438 Offset: 0x000438 Link: 6 (.dynstr) 000000: Version: 1 File: ./libtest.so Cnt: 1 0x0010: Name: LIBTEST_V1.0 Flags: none Version: 3 0x0020: Version: 1 File: libc.so.6 Cnt: 1 0x0030: Name: GLIBC_2.2.5 Flags: none Version: 2
1.3. ELF Segment
segment 的格式为:
typedef struct { Elf32_Word p_type; Elf32_Off p_offset; Elf32_Addr p_vaddr; Elf32_Addr p_paddr; Elf32_Word p_filesz; Elf32_Word p_memsz; Elf32_Word p_flags; Elf32_Word p_align; } Elf32_Phdr;
与 readelf 的输出相对应:
Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align RISCV_ATTRIBUT 0x001063 0x0000000000000000 0x0000000000000000 0x000033 0x000000 R 0x1 LOAD 0x000000 0x0000000000000000 0x0000000000000000 0x000424 0x000424 R E 0x1000 LOAD 0x000ed0 0x0000000000001ed0 0x0000000000001ed0 0x000168 0x000170 RW 0x1000 DYNAMIC 0x000ee0 0x0000000000001ee0 0x0000000000001ee0 0x000120 0x000120 RW 0x8 NOTE 0x0001c8 0x00000000000001c8 0x00000000000001c8 0x000024 0x000024 R 0x4 GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW 0x10 GNU_RELRO 0x000ed0 0x0000000000001ed0 0x0000000000001ed0 0x000130 0x000130 R 0x1
1.3.1. PT_INTERP
PT_INTERP 指向 interp section, 包括 interpreter 的路径, 例如 /lib/ld-linux-x86-64.so.2.
只有 executable file 才有这一项.
1.3.2. PT_NOTE
PT_NOTE 和 coredump 有关
1.3.3. PT_LOAD
一个 executable file 或 shared object file 一般会包含两个 LOAD 类型的 segment: 例如:
,-—
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000 |
0x00000000000006ac 0x00000000000006ac R E 200000 |
LOAD 0x0000000000000e28 0x0000000000200e28 0x0000000000200e28 |
0x00000000000001f4 0x0000000000000208 RW 200000 |
`-—
LOAD 类型的 segment 表示这个 segment 是需要从 elf 文件中映射进来的, 具体的, 第一个 LOAD segment 是 RE (只读可执行的), 主要包括 elf 中的一些代码段 (例如 text, init)和一些只读数据段 (例如 rodata, dynsym) 等.第二个 LOAD segment 是 RW, 包括一些可写的数据段(例如 got, data, bss, dynamic 等).
PT_LOAD 是 elf 加载的第一步: kernel 或 rtld 负责将所有 PT_LOAD 类型的数据 map 进来
1.3.4. PT_DYNAMIC
PT_DYNAMIC 是给 loader 准备的, 是 loader 工作的起点. PT_DYNAMIC 指向 dynamic section (例如前面的 0x000ee0 就是 .dynamic section 的 offset), loader 需要 dynamic section 中的数据来找到 rela, dynsym 等数据.
需要注意的是 dynamic section 以及它包含的各个 DT_XXX 在前面已经通过 PT_LOAD 加载到内存中了, 所以 loader 可以放心的访问所有的 dynamic 信息, dynamic 信息是特意给 loader 准备的.
1.3.5. PT_GNU_RELRO
GNU_RELRO 要在 link relocations 完成后把一些 section 用 mprotect 修改为只读. 这些 section 主要包括: .init_array, .fini_array 以及 .got.
若使用了 RTLD_LAZY, 则不包括 .got.plt, 因为 LAZY 要求能在运行时修改 .got.plt
$> cat test.c int main(int argc, char *argv[]) { sleep(1000); return 0; } $> gcc test.c -O0 -g3 -Wl,-z,relro -no-pie $> readelf -a ./a.out Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040 0x00000000000001f8 0x00000000000001f8 R E 0x8 INTERP 0x0000000000000238 0x0000000000400238 0x0000000000400238 0x000000000000001c 0x000000000000001c R 0x1 [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2] LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000 0x00000000000006ec 0x00000000000006ec R E 0x200000 LOAD 0x0000000000000e08 0x0000000000600e08 0x0000000000600e08 0x0000000000000228 0x0000000000000230 RW 0x200000 DYNAMIC 0x0000000000000e18 0x0000000000600e18 0x0000000000600e18 0x00000000000001d0 0x00000000000001d0 RW 0x8 ... GNU_RELRO 0x0000000000000e08 0x0000000000600e08 0x0000000000600e08 0x00000000000001f8 0x00000000000001f8 R 0x1 ~~~~~~~~~ Section to Segment mapping: Segment Sections... 00 01 .interp 02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame 03 .init_array .fini_array .dynamic .got .got.plt .data .bss ... 08 .init_array .fini_array .dynamic .got ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Section Headers: ... [21] .got PROGBITS 0000000000600fe8 00000fe8 0000000000000018 0000000000000008 WA 0 0 8 [22] .got.plt PROGBITS 0000000000601000 00001000 ~~~~~~~~~~~~~~~~ 0000000000000020 0000000000000008 WA 0 0 8 [23] .data PROGBITS 0000000000601020 00001020 $> strace ./a.out arch_prctl(ARCH_SET_FS, 0x7ff1d5243440) = 0 mprotect(0x7ff1d5056000, 16384, PROT_READ) = 0 mprotect(0x600000, 4096, PROT_READ) = 0 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ mprotect(0x7ff1d5283000, 4096, PROT_READ) = 0 munmap(0x7ff1d5244000, 255800) = 0 // 0x600000 是 GNU_RELRO 的地址 (0x600e08) 按 PAGE_SIZE round 后的结果, 所以实际 protect 的区域为 0x600000 ~ 0x601000, 而 0x601000 刚好是 .got.plt 的地址, 所以编译器已经考虑到 mprotect 需要 PAGE_SIZE 对齐的要 求. $> cat test.c int main(int argc, char *argv[]) { sleep(1000); return 0; } $> gcc test.c -O0 -g3 -Wl,-z,relro -z now $> readelf -a a.out GNU_RELRO 0x0000000000000de0 0x0000000000200de0 0x0000000000200de0 0x0000000000000220 0x0000000000000220 R 0x1 03 .init_array .fini_array .dynamic .got .data .bss ... 08 .init_array .fini_array .dynamic .got // 0x200de0 + 0x220 = 0x201000, 这个地址刚好是 .data 的首地址, 所以使 // 用 -z now 后 plt 使用的 got 在包含在 relro 之内了
Backlinks
BFD Tutorial (BFD Tutorial > Overview): bfd 支持不同平台上的不同的 object 文件格式, 包括 elf, a.out, coff 等.
objcopy (binutils > usage > objcopy > 生成 bin): 第一个 PT_LOAD 从 0 开始 (而不是 0x318, .interp), 因为它包含了 elf 文件开头的信 息, 例如 `ELF` magic number 和 elf 的 program header, 参考 ELF