总是看的时候懂,一转眼又忘了,另外为了加深理解,记录下ELF文件的动态链接部分~
动态链接优点多呀动态链接很灵活呀动态链接很神奇呀~
准备 写个小程序 这里首先写一个程序作为下面部分的演示:
1 2 3 4 5 6 7 8 9 10 11 #include <stdio.h> void out () { char a[100 ]; scanf ("%s" ,a); printf ("%s" ,a); } int main () { out(); return 0 ; }
编译方式:
1 gcc -g -m32 -o test demo.c
源码阅读准备 为了方便阅读源代码,先下载源码与辅助工具:
1 2 apt-get source libc6-dev apt-get install cscope
在源码根目录执行命令生成索引:
使用时在vim里面使用
1 2 cs add /dir***/cscope.out cs find x var
其中x为查找类型:
s: 查找C语言符号,即查找函数名、宏、枚举值等出现的地方
g: 查找函数、宏、枚举等定义的位置,类似ctags所提供的功能
d: 查找本函数调用的函数
c: 查找调用本函数的函数
t: 查找指定的字符串
e: 查找egrep模式,相当于egrep功能,但查找速度快多了
f: 查找并打开文件,类似vim的find功能
i: 查找包含本文件的文件有的时候它并不能查找,可能需要通过grep等其他方法查找。
ELF文件执行过程 要理解动态链接,还是从ELF文件的启动过程开始吧,总体流程如下:
加载镜像
用户在bash下执行命令
bash会进行fork()系统调用
子进程调用execve(),父进程等待子进程结束
execve()->sys_execve()->do_execve()
do_execve()先读取文件前128字节,接着调用search_binary_handle()匹配装载器
ELF会接着调用load_elf_binary()装载它
此装载器执行的操作: 判断文件有效性-> 寻找.interp段设置动态链接器路径-> 根据文件头做代码数据映射-> 初始化进程环境-> 指定程序入口地址(若为静态连接即文件中的入口地址,若为动态链接指向链接器)-> 函数返回
sys_execve()返回到用户态时执行前面设置的地址处代码
动态链接 初始化 这里是最常见的情况,即默认编译选项时,它会为程序添加开始文件等,首先看_start它存在于glibc/sysdeps/i386/start.S:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 .text .globl _start .type _start,@function _start: xorl %ebp, %ebp popl %esi movl %esp, %ecx andl $0xfffffff0 , %esp pushl %eax pushl %esp pushl %edx #ifdef SHARED call 1f addl $_GLOBAL_OFFSET_TABLE_, %ebx leal __libc_csu_fini@GOTOFF(%ebx), %eax pushl %eax leal __libc_csu_init@GOTOFF(%ebx), %eax pushl %eax pushl %ecx pushl %esi pushl main@GOT(%ebx) call __libc_start_main@PLT #else ................................................................... #endif hlt
接着是传入7个参数,调用__libc_start_main,它在glib/csu/libc-start.c下面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 STATIC int LIBC_START_MAIN (int (*main)(int , char **, char ** MAIN_AUXVEC_DECL), int argc, char **argv, #ifdef LIBC_START_MAIN_AUXVEC_ARG ElfW(auxv_t ) *auxvec, #endif __typeof (main) init, void (*fini) (void ), void (*rtld_fini) (void ), void *stack_end) { int result; __libc_multiple_libcs = &_dl_starting_up && !_dl_starting_up; #ifndef SHARED char **ev = &argv[argc + 1 ]; __environ = ev; __libc_stack_end = stack_end; .................................................................................. # ifdef DL_SYSDEP_OSCHECK if (!__libc_multiple_libcs) { DL_SYSDEP_OSCHECK (__libc_fatal); } # endif apply_irel (); #ifndef __GNU__ __pthread_initialize_minimal (); #endif uintptr_t stack_chk_guard = _dl_setup_stack_chk_guard (_dl_random); # ifdef THREAD_SET_STACK_GUARD THREAD_SET_STACK_GUARD (stack_chk_guard); # else __stack_chk_guard = stack_chk_guard; # endif uintptr_t pointer_chk_guard = _dl_setup_pointer_guard (_dl_random, stack_chk_guard); # ifdef THREAD_SET_POINTER_GUARD THREAD_SET_POINTER_GUARD (pointer_chk_guard); # else __pointer_chk_guard_local = pointer_chk_guard; # endif ................................................................................ #endif if (__glibc_likely (rtld_fini != NULL )) __cxa_atexit ((void (*) (void *)) rtld_fini, NULL , NULL ); #ifndef SHARED __libc_init_first (argc, argv, __environ); if (fini) __cxa_atexit ((void (*) (void *)) fini, NULL , NULL ); ............................................................................. #endif if (init) (*init) (argc, argv, __environ MAIN_AUXVEC_PARAM); ............................................................................. #ifndef SHARED _dl_debug_initialize (0 , LM_ID_BASE); #endif ............................................................................ #else result = main (argc, argv, __environ MAIN_AUXVEC_PARAM); #endif exit (result); }
上面执init函数,它在csu/elf-init.c里面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void __libc_csu_init (int argc, char **argv, char **envp){ #ifndef LIBC_NONSHARED { const size_t size = __preinit_array_end - __preinit_array_start; size_t i; for (i = 0 ; i < size; i++) (*__preinit_array_start [i]) (argc, argv, envp); } #endif #ifndef NO_INITFINI _init (); #endif const size_t size = __init_array_end - __init_array_start; for (size_t i = 0 ; i < size; i++) (*__init_array_start [i]) (argc, argv, envp); }
初始化节区.init与下面终止时的.fini节区是由crti.o与ctrn.o以及每个编译单元的.init和.fini组合而成,那么反汇编test程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 root@kali:~# objdump -j .init -d test Disassembly of section .init: 000003bc <_init>: 3bc: 53 push %ebx 3bd: 83 ec 08 sub $0x8,%esp 3c0: e8 ab 00 00 00 call 470 <__x86.get_pc_thunk.bx> 3c5: 81 c3 3b 1c 00 00 add $0x1c3b,%ebx 3cb: 8b 83 f4 ff ff ff mov -0xc(%ebx),%eax 3d1: 85 c0 test %eax,%eax 3d3: 74 05 je 3da <_init+0x1e> 3d5: e8 4e 00 00 00 call 428 <__gmon_start__@plt> 3da: 83 c4 08 add $0x8,%esp 3dd: 5b pop %ebx 3de: c3 ret
由于此程序没有全局对象需要构造也没有显式定义函数在此执行,它只有一个gprof的函数调用
终止 而在exit里面,它在glibc/stdlib/exit.c里面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 void exit (int status) { __run_exit_handlers (status, &__exit_funcs, true , true ); } void attribute_hidden__run_exit_handlers (int status, struct exit_function_list **listp, bool run_list_atexit, bool run_dtors) { #ifndef SHARED if (&__call_tls_dtors != NULL ) #endif if (run_dtors) __call_tls_dtors (); while (*listp != NULL ){ struct exit_function_list *cur = *listp ; while (cur->idx > 0 ){ const struct exit_function *const f = &cur ->fns [--cur ->idx ]; switch (f->flavor) { void (*atfct) (void ); void (*onfct) (int status, void *arg); void (*cxafct) (void *arg, int status); case ef_free: case ef_us: break ; case ef_on: onfct = f->func.on.fn; #ifdef PTR_DEMANGLE PTR_DEMANGLE (onfct); #endif onfct (status, f->func.on.arg); break ; case ef_at: atfct = f->func.at; #ifdef PTR_DEMANGLE PTR_DEMANGLE (atfct); #endif atfct (); break ; case ef_cxa: cxafct = f->func.cxa.fn; #ifdef PTR_DEMANGLE PTR_DEMANGLE (cxafct); #endif cxafct (f->func.cxa.arg, status); break ; } } *listp = cur->next; if (*listp != NULL ) free (cur); } if (run_list_atexit) RUN_HOOK (__libc_atexit, ()); _exit (status); }
而_exit就是最简单的退出了,它存在于sysdeps/unix/sysv/linux/i386/_exit.S:
1 2 3 4 5 6 7 8 9 _exit: movl 4 (%esp), %ebx #ifdef __NR_exit_group movl $__NR_exit_group, %eax ENTER_KERNEL #endif movl $__NR_exit, %eax int $0x80 hlt
延迟绑定 一些优点导致普遍利用了延迟绑定技术,它使用了两个节:.got.plt与.plt,他们分别属于数据段与代码段,前者叫做全局偏移表(函数部分),后者叫做程序链接表,现在使用调试来说明:节区情况
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 root@kali:~ There are 35 section headers, starting at offset 0x2050: Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al ............................................................................................. [ 5] .dynsym DYNSYM 000001cc 0001cc 000090 10 A 6 1 4 [ 6] .dynstr STRTAB 0000025c 00025c 0000b6 00 A 0 0 1 [ 9] .rel.dyn REL 00000364 000364 000040 08 A 5 0 4 [10] .rel.plt REL 000003a4 0003a4 000018 08 AI 5 23 4 [12] .plt PROGBITS 000003e0 0003e0 000040 04 AX 0 0 16 [13] .plt.got PROGBITS 00000420 000420 000010 08 AX 0 0 8 [14] .text PROGBITS 00000430 000430 000222 00 AX 0 0 16 [21] .dynamic DYNAMIC 00001efc 000efc 0000f0 08 WA 6 0 4 [22] .got PROGBITS 00001fec 000fec 000014 04 WA 0 0 4 [23] .got.plt PROGBITS 00002000 001000 000018 04 WA 0 0 4 [32] .symtab SYMTAB 00000000 0017f4 0004b0 10 33 49 4 [33] .strtab STRTAB 00000000 001ca4 000267 00 0 0 1 .............................................................................................
看到.plt在000003e0大小为40h、.got.plt在00002000大小为14h。反编译text与.plt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 root@kali:~ Disassembly of section .text: ................................................................................... 0000056d <out>: 58c: 50 push %eax 58d: e8 7e fe ff ff call 410 <__isoc99_scanf@plt> 592: 83 c4 10 add $0x10 ,%esp 5a2: 50 push %eax 5a3: e8 48 fe ff ff call 3f0 <printf @plt> 5a8: 83 c4 10 add $0x10 ,%esp 000005b1 <main>: 5c2: e8 18 00 00 00 call 5df <__x86.get_pc_thunk.ax> 5c7: 05 39 1a 00 00 add $0x1a39 ,%eax 5cc: e8 9c ff ff ff call 56d <out> 5d1: b8 00 00 00 00 mov $0x0 ,%eax ...................................................................................
这里删除了无关内容,看到main里面对于out的调用是相对调用,而在out内部,原来对scanf和printf的调用被转换为了对__isoc99_scanf@plt和printf@plt的调用,再来看看.plt:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 ................................................................................... root@kali:~ test : file format elf32-i386Disassembly of section .plt: 000003e0 <.plt>: 3e0: ff b3 04 00 00 00 pushl 0x4(%ebx) 3e6: ff a3 08 00 00 00 jmp *0x8(%ebx) 3ec: 00 00 add %al,(%eax) ... 000003f0 <printf @plt>: 3f0: ff a3 0c 00 00 00 jmp *0xc(%ebx) 3f6: 68 00 00 00 00 push $0x0 3fb: e9 e0 ff ff ff jmp 3e0 <.plt> 00000400 <__libc_start_main@plt>: 400: ff a3 10 00 00 00 jmp *0x10(%ebx) 406: 68 08 00 00 00 push $0x8 40b: e9 d0 ff ff ff jmp 3e0 <.plt> 00000410 <__isoc99_scanf@plt>: 410: ff a3 14 00 00 00 jmp *0x14(%ebx) 416: 68 10 00 00 00 push $0x10 41b: e9 c0 ff ff ff jmp 3e0 <.plt> ...................................................................................
为了方便,从现在开始对.plt与.got.plt命名为plt与got吧,看到plt里面有四个条目,每个条目占用10h字节,里面有在out里被调用的__isoc99_scanf@plt和printf@plt,并且除了第一项其他三项格式一致,都是先jmp *0xxx(%ebx)若仔细观察,地址的增量为4字节,事实上%ebx此时对应着got表起始位置,它存储的是地址,所以每一项大小为4字节(32位系统),于是这里可以得出:
1 2 3 plt[1]->got[3] plt[2]->got[4] ............
另外每一项的第二条指令是push 0x**,这个数字在条目间按照8h递增,然后都是jmp 3e0,这个3e0就是plt[0]的地址了,再来看看got里面的内容:
1 2 3 4 5 6 root@kali:~ Hex dump of section '.got.plt' : NOTE: This section has relocations against it, but these have NOT been applied to this dump. 0x00002000 fc1e0000 00000000 00000000 f6030000 ................ 0x00002010 06040000 16040000 ........
emmmm,直接dump出来的没有显示为小端序,手动转换一下,发现got[0]为.dynamic的地址,从got[3]开始,它里面存的的是与之对应的plt条目的第二条代码的地址,那么到目前为止,说明plt项目的第一条指令似乎并没有实际的作用,它实际作用是push了一个index后就去执行plt[0]处的代码了,那看看plt[0]的代码呢,它是pushl 0x4(%ebx)后执行jmp *0x8(%ebx),意思就是pushl got[1];jmp *got[2],由上面可以看到,got[1],got[2]初始内容为0,但是程序执行到这里时绝对不可能还是这样,至少got[2]不会这样,否则会出现指针解引用异常,其实上面已经说了got[2]存的是_dl_runtime_resolve的地址,而got[1]其实存的是link_map的首地址,而这个过程是调用_dl_runtime_resolve(link_map,fun_index)来修正got表相应位置的值并执行它: 看到延迟绑定中上述过程只会发生在第一次,以后就不需要再次绑定了,plt条目的第一条指令会直接跳转到函数实际地址处。
过程详解 关键节区 上面对动态链接的过程有了大致的了解,现在来详细说明这个过程,补充几个上面没有提到的节区。还要从.dynamic节区开始,它的元素定义为:
1 2 3 4 5 6 7 typedef struct dynamic { Elf32_Sword d_tag; union { Elf32_Sword d_val; Elf32_Addr d_ptr; } d_un; } Elf32_Dyn;
不同d_tag类型值d_un含义不同,查看结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 root@kali:~ Dynamic section at offset 0xefc contains 26 entries: Tag Type Name/Value 0x00000001 (NEEDED) Shared library: [libc.so.6] 0x00000005 (STRTAB) 0x25c 0x00000006 (SYMTAB) 0x1cc 0x0000000a (STRSZ) 182 (bytes) 0x0000000b (SYMENT) 16 (bytes) 0x00000002 (PLTRELSZ) 24 (bytes) 0x00000014 (PLTREL) REL 0x00000017 (JMPREL) 0x3a4 0x00000011 (REL) 0x364 0x00000012 (RELSZ) 64 (bytes) 0x00000013 (RELENT) 8 (bytes) 0x6ffffffa (RELCOUNT) 4 0x00000000 (NULL) 0x0
查看.rel.plt的元素结构与内容:
1 2 3 4 typedef struct elf32_rel { Elf32_Addr r_offset; Elf32_Word r_info; } Elf32_Rel;
其中r_info含两种数据,通过宏获取:
1 2 3 #define ELF32_R_SYM(val) ((val) >> 8) #define ELF32_R_TYPE(val) ((val) & 0xff)
使用readelf查看程序的.rel.plt结构:
1 2 3 4 5 6 7 root@kali:~ Relocation section '.rel.plt' at offset 0x3a4 contains 3 entries: Offset Info Type Sym.Value Sym. Name 0000200c 00000207 R_386_JUMP_SLOT 00000000 printf @GLIBC_2.0 00002010 00000507 R_386_JUMP_SLOT 00000000 __libc_start_main@GLIBC_2.0 00002014 00000607 R_386_JUMP_SLOT 00000000 __isoc99_scanf@GLIBC_2.7
发现除了上述结构外还有Sym,Value和Sym.Name信息,他其实是通过.dynsym获取的,它的结构为:
1 2 3 4 5 6 7 8 typedef struct elf32_sym { Elf32_Word st_name; Elf32_Addr st_value; Elf32_Word st_size; unsigned char st_info; unsigned char st_other; Elf32_Half st_shndx; } Elf32_Sym;
本测试程序的节区如下,可以参照此数据手动运算,将会得到上面的值:
1 2 3 4 5 6 7 8 9 10 11 12 13 root@kali:~ Symbol table '.dynsym' contains 9 entries: Num: Value Size Type Bind Vis Ndx Name 0: 00000000 0 NOTYPE LOCAL DEFAULT UND 1: 00000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab 2: 00000000 0 FUNC GLOBAL DEFAULT UND printf @GLIBC_2.0 (2) 3: 00000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.1.3 (3) 4: 00000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__ 5: 00000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.0 (2) 6: 00000000 0 FUNC GLOBAL DEFAULT UND __isoc99_scanf@GLIBC_2.7 (4) 7: 00000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable 8: 0000066c 4 OBJECT GLOBAL DEFAULT 16 _IO_stdin_used
_dl_runtime_resolve实现 首先查看/sysdeps/x86_64/dl-trampoline.S:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 .text .globl _dl_runtime_resolve .type _dl_runtime_resolve, @function .align 16 cfi_startproc _dl_runtime_resolve: cfi_adjust_cfa_offset(16) subq $56,%rsp cfi_adjust_cfa_offset(56) movq %rax,(%rsp) ;保存寄存器 movq %rcx, 8(%rsp) movq %rdx, 16(%rsp) movq %rsi, 24(%rsp) movq %rdi, 32(%rsp) movq %r8, 40(%rsp) movq %r9, 48(%rsp) movq 64(%rsp), %rsi ;获取传入参数,即link_map与reg_offset movq 56(%rsp), %rdi call _dl_fixup ;调用了一个关键函数,它会查找符号地址并修正.got.plt表对应值 movq %rax, %r11 movq 48(%rsp), %r9 movq 40(%rsp), %r8 movq 32(%rsp), %rdi movq 24(%rsp), %rsi movq 16(%rsp), %rdx movq 8(%rsp), %rcx movq (%rsp), %rax addq $72, %rsp cfi_adjust_cfa_offset(-72) jmp *%r11 # Jump to function address. ;执行找到的函数 cfi_endproc .size _dl_runtime_resolve, .-_dl_runtime_resolve
现在跟入_dl_fixup查看,它在/elf/dl-runtime.c里面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 #define ElfW(type) _ElfW (Elf, __ELF_NATIVE_CLASS, type) #define _ElfW(e,w,t) _ElfW_1 (e, w, _##t) #define _ElfW_1(e,w,t) e##w##t #ifdef DL_RO_DYN_SECTION # define D_PTR(map, i) ((map)->i->d_un.d_ptr + (map)->l_addr) #else # define D_PTR(map, i) (map)->i->d_un.d_ptr #endif struct link_map { ElfW(Addr) l_addr; char *l_name; ElfW(Dyn) *l_ld; struct link_map *l_next , *l_prev ; } DL_FIXUP_VALUE_TYPE attribute_hidden __attribute ((noinline)) ARCH_FIXUP_ATTRIBUTE _dl_fixup ( #ifdef ELF_MACHINE_RUNTIME_FIXUP_ARGS ELF_MACHINE_RUNTIME_FIXUP_ARGS, #endif struct link_map *l, ElfW(Word) reloc_arg) { const ElfW (Sym) *const symtab = (const void *) D_PTR (l, l_info[DT_SYMTAB]); const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]); const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset); const ElfW (Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)]; void *const rel_addr = (void *)(l->l_addr + reloc->r_offset); lookup_t result; DL_FIXUP_VALUE_TYPE value; assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT); if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0 ) == 0 ) { const struct r_found_version *version = NULL ; if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL ) { const ElfW (Half) *vernum = (const void *) D_PTR (l, l_info[VERSYMIDX (DT_VERSYM)]); ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff ; version = &l->l_versions[ndx]; if (version->hash == 0 ) version = NULL ; } int flags = DL_LOOKUP_ADD_DEPENDENCY; if (!RTLD_SINGLE_THREAD_P) { THREAD_GSCOPE_SET_FLAG (); flags |= DL_LOOKUP_GSCOPE_LOCK; } #ifdef RTLD_ENABLE_FOREIGN_CALL RTLD_ENABLE_FOREIGN_CALL; #endif result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope, version, ELF_RTYPE_CLASS_PLT, flags, NULL ); if (!RTLD_SINGLE_THREAD_P) THREAD_GSCOPE_RESET_FLAG (); #ifdef RTLD_FINALIZE_FOREIGN_CALL RTLD_FINALIZE_FOREIGN_CALL; #endif value = DL_FIXUP_MAKE_VALUE (result, sym ? (LOOKUP_VALUE_ADDRESS (result) + sym->st_value) : 0 ); } else { value = DL_FIXUP_MAKE_VALUE (l, l->l_addr + sym->st_value); result = l; } value = elf_machine_plt_value (l, reloc, value); if (sym != NULL && __builtin_expect (ELFW(ST_TYPE) (sym->st_info) == STT_GNU_IFUNC, 0 )) value = elf_ifunc_invoke (DL_FIXUP_VALUE_ADDR (value)); if (__glibc_unlikely (GLRO(dl_bind_not))) return value; return elf_machine_fixup_plt (l, result, reloc, rel_addr, value); }