ELF文件格式之动态链接

总是看的时候懂,一转眼又忘了,另外为了加深理解,记录下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表中的值
}