BetaMao

ELF文件格式之动态链接

字数统计: 4.3k阅读时长: 20 min
2017/10/10 Share

总是看的时候懂,一转眼又忘了,另外为了加深理解,记录下ELF文件的动态链接部分~

动态链接优点多呀动态链接很灵活呀动态链接很神奇呀~

准备

写个小程序

这里首先写一个程序作为下面部分的演示:

1
2
3
4
5
6
7
8
9
10
11
//demo.c
#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 #查找定义工具

在源码根目录执行命令生成索引:

1
cscope -Rbq

使用时在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文件的启动过程开始吧,总体流程如下:

加载镜像

  1. 用户在bash下执行命令
  2. bash会进行fork()系统调用
  3. 子进程调用execve(),父进程等待子进程结束
  4. execve()->sys_execve()->do_execve()
  5. do_execve()先读取文件前128字节,接着调用search_binary_handle()匹配装载器
  6. ELF会接着调用load_elf_binary()装载它
  7. 此装载器执行的操作:
    判断文件有效性->
    寻找.interp段设置动态链接器路径->
    根据文件头做代码数据映射->
    初始化进程环境->
    指定程序入口地址(若为静态连接即文件中的入口地址,若为动态链接指向链接器)->
    函数返回
  8. 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 //将ebp清零,即代表此为初始栈

/*处理main需要的三个参数*/
popl %esi //弹出参数个数,即argc -> esi
movl %esp, %ecx //argv -> ecx


andl $0xfffffff0, %esp
pushl %eax //对齐与垃圾数据

pushl %esp //当前栈顶

pushl %edx //共享库终止函数的地址

#ifdef SHARED //对于动态可共享的使用这种方式,静态的含义一样,省略
call 1f //载入PIC寄存器
addl $_GLOBAL_OFFSET_TABLE_, %ebx

leal __libc_csu_fini@GOTOFF(%ebx), %eax
pushl %eax //__libc_csu_fini 即.fini
leal __libc_csu_init@GOTOFF(%ebx), %eax
pushl %eax //__libc_csu_init 即.init

pushl %ecx //argv
pushl %esi //argc

pushl main@GOT(%ebx) //main

call __libc_start_main@PLT //调用call __libc_start_main
#else
...................................................................
#endif
hlt //一般来说是不会执行这个的,除非exit崩溃了

接着是传入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),      //参数1-main
int argc, //参数2
char **argv, //参数3
#ifdef LIBC_START_MAIN_AUXVEC_ARG
ElfW(auxv_t) *auxvec,
#endif
__typeof (main) init, //参数4
void (*fini) (void), //参数5
void (*rtld_fini) (void), //参数6
void *stack_end) //参数7
{
int result; //存储main的返回值

__libc_multiple_libcs = &_dl_starting_up && !_dl_starting_up;

#ifndef SHARED
char **ev = &argv[argc + 1];

__environ = ev; //env
__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

/* Set up the stack checker's canary. */
uintptr_t stack_chk_guard = _dl_setup_stack_chk_guard (_dl_random); //建立stack canary,这里对每一个线程都创建了不同的canary
# 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, //建立canary值
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); //初始化libc

if (fini)
__cxa_atexit ((void (*) (void *)) fini, NULL, NULL); //注册程序里的析构器
.............................................................................
#endif
if (init)
(*init) (argc, argv, __environ MAIN_AUXVEC_PARAM); //程序初始化,即执行.init节区
.............................................................................
#ifndef SHARED
_dl_debug_initialize (0, LM_ID_BASE);
#endif
............................................................................
#else
result = main (argc, argv, __environ MAIN_AUXVEC_PARAM); //调用main函数
#endif
exit (result); //调用exit退出程序
}

上面执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 //若是共享库的,在链接时就执行了
/* For static executables, preinit happens right before init. */
{
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 ();

/* We do it this way to handle recursive calls to exit () made by
the functions registered with `atexit' and `on_exit'. We call
everyone on the list and use the status value in the last
exit (). */

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)
/* Don't free the last element in the chain, this is the statically
allocate element. */
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:~# readelf -S test 
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
.............................................................................................

看到.plt000003e0大小为40h、.got.plt00002000大小为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:~# objdump -j .text -d test

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内部,原来对scanfprintf的调用被转换为了对__isoc99_scanf@pltprintf@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:~# objdump -j .plt -d test

test: file format elf32-i386

Disassembly 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@pltprintf@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:~# readelf -x .got.plt test 

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:~# readelf -d test 

Dynamic section at offset 0xefc contains 26 entries:
Tag Type Name/Value
0x00000001 (NEEDED) Shared library: [libc.so.6] #依赖的共享库
0x00000005 (STRTAB) 0x25c #字符串表地址,即.dynstr
0x00000006 (SYMTAB) 0x1cc #符号表地址,即.dynsym
0x0000000a (STRSZ) 182 (bytes) #字符串表大小
0x0000000b (SYMENT) 16 (bytes) #符号表元素大小
0x00000002 (PLTRELSZ) 24 (bytes) #.rel.plt大小
0x00000014 (PLTREL) REL #
0x00000017 (JMPREL) 0x3a4 #代码重定位表地址,即.rel.plt
0x00000011 (REL) 0x364 #数据重定位表地址,即.rel.dyn
0x00000012 (RELSZ) 64 (bytes) #.rel.dyn大小
0x00000013 (RELENT) 8 (bytes)
0x6ffffffa (RELCOUNT) 4
0x00000000 (NULL) 0x0

查看.rel.plt的元素结构与内容:

1
2
3
4
typedef struct elf32_rel {
Elf32_Addr r_offset; //地址,其实是在.got.plt表中
Elf32_Word r_info; //type与index
} 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:~# readelf -r test 

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; //在.dynstr中的偏移
Elf32_Addr st_value; //符号值,根据类型不同可能是绝对值,地址什么的
Elf32_Word st_size; //符号大小
unsigned char st_info; //28Binding:4Type 绑定指示符号是局部全局弱符号,类型指示符号为对象函数节区文件等
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:~# readelf --dyn-syms  test 

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
//通过这个宏来获取类型ELF32_type等
#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

//通过这个宏来获取dynsym等节区地址
#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{
/* These first few members are part of the protocol with the debugger.
This is the same format used in SVR4. */

ElfW(Addr) l_addr; /* Difference between the address in the ELF
file and the addresses in memory. */ //加载基址
char *l_name; /* Absolute file name object was found in. */
ElfW(Dyn) *l_ld; /* Dynamic section of the shared object. */
struct link_map *l_next, *l_prev; /* Chain of loaded objects. */
}

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); //该函数在.rel.plt中的地址
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)]; //该函数在符号表中的地址
void *const rel_addr = (void *)(l->l_addr + reloc->r_offset); //该函数在.got表中的绝对地址
lookup_t result;
DL_FIXUP_VALUE_TYPE value;

/* Sanity check that we're really looking at a PLT relocation. */
assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT); //重定位类型断言,即0x7

/* Look up the target symbol. If the normal lookup rules are not
used don't look in the global scope. */
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)]);//计算获得glibc版本号
ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff;//计算获得printf符号在.gnu.version中对应hash表项的索引ndx,此表项放着printf符号对应的glibc版本号?? ;R_SYM宏计算需要重定位的符号所对应的符号表项索引
version = &l->l_versions[ndx];//计算获得printf符号对应的glibc版本号
if (version->hash == 0)
version = NULL; //`*(*(versym+index*2)&0x7fff <<4 +versionBase + 4)`
}

/* We need to keep the scope around so do some locking. This is
not necessary for objects which cannot be unloaded or when
we are not using any threads (yet). */
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, //关键部分,此函数是遍历link_map,根据符号名查找符号地址
version, ELF_RTYPE_CLASS_PLT, flags, NULL);

/* We are done with the global scope. */
if (!RTLD_SINGLE_THREAD_P)
THREAD_GSCOPE_RESET_FLAG ();

#ifdef RTLD_FINALIZE_FOREIGN_CALL
RTLD_FINALIZE_FOREIGN_CALL;
#endif

/* Currently result contains the base load address (or link map)
of the object that defines sym. Now add in the symbol
offset. */
value = DL_FIXUP_MAKE_VALUE (result, //得到符号正确地址
sym ? (LOOKUP_VALUE_ADDRESS (result)
+ sym->st_value) : 0);
}
else //若为非外部可见符号,则直接通过加载基址+符号偏移得到
{
/* We already found the symbol. The module (and therefore its load
address) is also known. */
value = DL_FIXUP_MAKE_VALUE (l, l->l_addr + sym->st_value);
result = l;
}

/* And now perhaps the relocation addend. */
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));

/* Finally, fix up the plt itself. */
if (__glibc_unlikely (GLRO(dl_bind_not)))
return value;

return elf_machine_fixup_plt (l, result, reloc, rel_addr, value); //修复.got.plt表中的值
}
CATALOG
  1. 1. 准备
    1. 1.1. 写个小程序
    2. 1.2. 源码阅读准备
  2. 2. ELF文件执行过程
    1. 2.1. 加载镜像
    2. 2.2. 动态链接
    3. 2.3. 初始化
    4. 2.4. 终止
  3. 3. 延迟绑定
  4. 4. 过程详解
    1. 4.1. 关键节区
    2. 4.2. _dl_runtime_resolve实现