1、为什么要动态链接
1.1 空间浪费
对于静态链接来说,在程序运行之前,会将程序所需的所有模块编译、链接成一个可执行文件。这种情况下,如果 Program1 和 Program2 都需要用到 Lib.o 模块,那么,内存中和磁盘中实际上就存在了两份Lib.o的代码。当共享的模块基数变得很大时,空间浪费无法想象。
1.2 更新困难
动态链接对程序的更新、部署和发布也会带来很多麻烦。比如 Program1 所使用的 Lib.o 是由一个第三方厂商提供的,当该厂商更新了 Lib.o 的时候,那么 Program1 的厂商就需要拿到最新版的 Lib.o,然后将其与 Program.o 链接后,将新的 Program1 整个发布给用户。这样做的缺点很明显,即一旦程序中有任何模块更新,整个程序就要重新链接、发布给用户。
1.3 动态链接
要解决空间浪费和更新困难这两个问题最简单的办法就是把程序的模块相互分割开来,形成独立的文件,而不再将它们静态地链接在一起。简单地讲,就是不对那些组成程序地目标文件进行链接,等到程序要运行时才进行链接。也就是说,把链接这个过程推迟到了运行时再进行,这就是动态链接(Dynamic Linking)的基本思想。
2、简单的动态链接例子
2.1 简单例子
我们先实现一个最简单的动态链接库的例子,感受一下。
program1.c文件的内容:
c
#include "Lib.h"
int main()
{
foobar(1);
return 0;
}
program2.c文件的内容:
c
#include "Lib.h"
int main()
{
foobar(2);
return 0;
}
Lib.h文件的内容:
c
#ifndef LIB_H
#define LIB_H
void foobar(int i);
#endif
Lib.c文件的内容:
c
#include <stdio.h>
void foobar(int i)
{
printf("Printing from Lib.so %d\n", i);
}
program1.c 和 program2.c 都调用了 Lib.c 里面的 foobar 函数。为了在内存中加载一次 Lib.c,使 program1 和 program2 共享。我们可以将 Lib.c 编译成共享对象(动态库)。
这里需要强调一下,这里所谓的共享,并不是共享整个Lib.c的内容,而是特指共享它的代码部分。 对于Lib.c中的数据部分,每个进程都需要一份自己的拷贝,因为它们可能需要独立地修改Lib.c中的数据。
先将Lib.c编译成共享对象:
bash
gcc -fPIC -shared -o Lib.so Lib.c
-shared表示的是产生共享对象。 -fPIC的含义暂时先不用管,待会儿再说。
现在,我们来分别编译Program1和Program2:
bash
gcc -o Program1 Program1.c ./Lib.so
gcc -o Program2 Program2.c ./Lib.so
现在执行./Program1就可以执行,并看到如下输出:
Attention :注意上一步骤中,我们使用了 ./Lib.so,来指定编译链接时搜索库的路径、装载时指定的动态库搜索路径。
现代链接器在处理动态库时将 链接时路径(Link-time path)和 运行时路径(Run-time path)分开
实际上,这一步骤可以拆解成以下:
bash
liangjie@liangjie-virtual-machine:~/Desktop/cfp$ gcc -fPIC -shared -o libtest.so Lib.c
liangjie@liangjie-virtual-machine:~/Desktop/cfp$ gcc -o Program1 Program1.c -L./ -ltest -Wl,-rpath,./
liangjie@liangjie-virtual-machine:~/Desktop/cfp$ ./Program1
Printing from Lib.so 1
-
-Ldir:制定链接时搜索库的路径。比如你自己的库,可以用它制定目录,不然链接器将只在标准库的目录找。这个dir就是目录的名称
-
-Wl,option:此选项传递 option 给链接程序,指定运行时动态库路径,链接程序将动态库的路径包含在可执行文件中;如果 option 中间有逗号, 就将 option 分成多个选项, 然后传递给会链接程序
可以使用 readelf 查看 dynamic 段,其中会有可执行文件依赖的动态库(NEEDED)以及动态库的运行时路径(RUNPATH):
bash
liangjie@liangjie-virtual-machine:~/Desktop/cfp$ readelf -d Program1
Dynamic section at offset 0x2da8 contains 29 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libtest.so]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000001d (RUNPATH) Library runpath: [./]
0x000000000000000c (INIT) 0x1000
0x000000000000000d (FINI) 0x1164
0x0000000000000019 (INIT_ARRAY) 0x3d98
0x000000000000001b (INIT_ARRAYSZ) 8 (bytes)
0x000000000000001a (FINI_ARRAY) 0x3da0
0x000000000000001c (FINI_ARRAYSZ) 8 (bytes)
0x000000006ffffef5 (GNU_HASH) 0x3b0
0x0000000000000005 (STRTAB) 0x480
0x0000000000000006 (SYMTAB) 0x3d8
0x000000000000000a (STRSZ) 157 (bytes)
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000015 (DEBUG) 0x0
0x0000000000000003 (PLTGOT) 0x3fb8
0x0000000000000002 (PLTRELSZ) 24 (bytes)
0x0000000000000014 (PLTREL) RELA
0x0000000000000017 (JMPREL) 0x620
0x0000000000000007 (RELA) 0x560
0x0000000000000008 (RELASZ) 192 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x000000000000001e (FLAGS) BIND_NOW
0x000000006ffffffb (FLAGS_1) Flags: NOW PIE
0x000000006ffffffe (VERNEED) 0x530
0x000000006fffffff (VERNEEDNUM) 1
0x000000006ffffff0 (VERSYM) 0x51e
0x000000006ffffff9 (RELACOUNT) 3
0x0000000000000000 (NULL) 0x0
指定运行时动态库路径常见方法:
(1)gcc参数指定 -Wl,-rpath = ${LD_PATH}
(2)配置文件 /etc/ld.so.conf 文件中添加库的搜索路径
(3)设置环境变量 export LD_LIBRARY_PATH=${LD_PATH}
执行 Program1 时,操作系统会首先在我们的虚拟进程空间中加载进一个动态链接器,动态链接器帮我们完成链接任务,然后我们的程序就开始执行了。
解析:
Lib.c 被编译成 libtest.so 共享对象文件,Program1.c 被编译成 Program1.o 后,链接成可执行程序 Program1。
上图中有一个步骤与静态链接不一样,那就是 Program1.o 被链接成可执行文件这一步,在静态链接中,这一步链接过程会把 Program1.o和 Lib.o 链接到一起,并且输出可执行文件 Program1。但在这里 Lib.o 没有被链接进来,链接的输入目标文件只有 Program1.o (当然还有C语言运行库,我们这里暂时忽略),但是从前面的命令行中我们看到,Lib.so也参与了链接过程这是怎么回事呢?
让我们回到动态链接的机制上来,当程序模块 Program1.c 被编译成 Program1.o 时,编译器还不知道 foobar() 函数的地址。当连接器将 Program1.o 链接成可执行文件时,这时候连接器必须确定 Program1.o 所引用的 foobar() 函数的性质。如果 foobar() 是一个定义在其静态目标模块中的函数,那么链接器将会按照静态链接的规则,将 Program1.o 中的 foobar 地址引用重定位;如果 foobar() 是定义在某个动态共享对象中的函数,那么链接器就会将这个符号的引用标记为一个动态链接的符号,不对它进行地址重定位, 而是在装载的时候再进行重定位。
可以使用 readelf 解析出 Program1 中的符号表:其中 .dynsym 为动态符号表,包含于 .symtab 全局符号表中
bash
liangjie@liangjie-virtual-machine:~/Desktop/cfp$ readelf -s Program1
Symbol table '.dynsym' contains 7 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND _[...]@GLIBC_2.34 (2)
2: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterT[...]
3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND foobar
4: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
5: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMC[...]
6: 0000000000000000 0 FUNC WEAK DEFAULT UND [...]@GLIBC_2.2.5 (3)
Symbol table '.symtab' contains 36 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS Scrt1.o
2: 000000000000038c 32 OBJECT LOCAL DEFAULT 4 __abi_tag
3: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c
4: 0000000000001090 0 FUNC LOCAL DEFAULT 16 deregister_tm_clones
5: 00000000000010c0 0 FUNC LOCAL DEFAULT 16 register_tm_clones
6: 0000000000001100 0 FUNC LOCAL DEFAULT 16 __do_global_dtors_aux
7: 0000000000004010 1 OBJECT LOCAL DEFAULT 26 completed.0
8: 0000000000003db0 0 OBJECT LOCAL DEFAULT 22 __do_global_dtor[...]
9: 0000000000001140 0 FUNC LOCAL DEFAULT 16 frame_dummy
10: 0000000000003da8 0 OBJECT LOCAL DEFAULT 21 __frame_dummy_in[...]
11: 0000000000000000 0 FILE LOCAL DEFAULT ABS Program1.c
12: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c
13: 00000000000020e0 0 OBJECT LOCAL DEFAULT 20 __FRAME_END__
14: 0000000000000000 0 FILE LOCAL DEFAULT ABS
15: 0000000000003db8 0 OBJECT LOCAL DEFAULT 23 _DYNAMIC
16: 0000000000002004 0 NOTYPE LOCAL DEFAULT 19 __GNU_EH_FRAME_HDR
17: 0000000000003fb8 0 OBJECT LOCAL DEFAULT 24 _GLOBAL_OFFSET_TABLE_
18: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_mai[...]
19: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterT[...]
20: 0000000000004000 0 NOTYPE WEAK DEFAULT 25 data_start
21: 0000000000004010 0 NOTYPE GLOBAL DEFAULT 25 _edata
22: 0000000000001164 0 FUNC GLOBAL HIDDEN 17 _fini
23: 0000000000000000 0 FUNC GLOBAL DEFAULT UND foobar
24: 0000000000004000 0 NOTYPE GLOBAL DEFAULT 25 __data_start
25: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
26: 0000000000004008 0 OBJECT GLOBAL HIDDEN 25 __dso_handle
27: 0000000000002000 4 OBJECT GLOBAL DEFAULT 18 _IO_stdin_used
28: 0000000000004018 0 NOTYPE GLOBAL DEFAULT 26 _end
29: 0000000000001060 38 FUNC GLOBAL DEFAULT 16 _start
30: 0000000000004010 0 NOTYPE GLOBAL DEFAULT 26 __bss_start
31: 0000000000001149 25 FUNC GLOBAL DEFAULT 16 main
32: 0000000000004010 0 OBJECT GLOBAL HIDDEN 25 __TMC_END__
33: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMC[...]
34: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@G[...]
35: 0000000000001000 0 FUNC GLOBAL HIDDEN 12 _init
那么链接器如何知道 foobar 的引用是一个静态符号还是一个动态符号呢?这就是为什么在编译的时候要用到 Lib.so 的原因。Lib.so 中保存了完整的符号信息,把 Lib.so 作为链接的输入文件之一,链接器在解析符号时就可以知道 foobar 是一个定义在 Lib.so 的动态符号,这样链接器就可以对 foobar 的引用做特殊的处理,使它成为一个动态符号的引用。
关于模块
在静态链接时,整个程序最终只有一个可执行文件,它是一个不可以分割的整体;但是在动态链接下,一个程序被分成了若干个文件,有程序的主要部分,
即可执行文件(Program1)和程序所依赖的共享对象(Lib.so),很多时候,我们也把这部分称为模块,
即动态链接下的可执行文件和共享对象都可以看作是程序的一个模块
2.2 动态链接程序运行时地址空间分布
对于静态链接的可执行文件来说,整个进程只有一个文件要被映射,即可执行文件。而对于动态链接,除了可执行文件,还有它所依赖的共享目标文件。
还是以上面的 Program1 为例,对 Lib.c 稍作修改:
c
#include <stdio.h>
void foobar(int i)
{
printf("Printing from Lib.so %d\n", i);
sleep(-1);
}
然后就可以查看进程的虚拟地址空间分布:
bash
liangjie@liangjie-virtual-machine:~/Desktop/cfp$ ./Program1 &
[1] 4801
Printing from Lib.so 1
liangjie@liangjie-virtual-machine:~/Desktop/cfp$ cat /proc/4801/maps
5636a1ef2000-5636a1ef3000 r--p 00000000 08:03 2883771 /home/liangjie/Desktop/cfp/Program1
5636a1ef3000-5636a1ef4000 r-xp 00001000 08:03 2883771 /home/liangjie/Desktop/cfp/Program1
5636a1ef4000-5636a1ef5000 r--p 00002000 08:03 2883771 /home/liangjie/Desktop/cfp/Program1
5636a1ef5000-5636a1ef6000 r--p 00002000 08:03 2883771 /home/liangjie/Desktop/cfp/Program1
5636a1ef6000-5636a1ef7000 rw-p 00003000 08:03 2883771 /home/liangjie/Desktop/cfp/Program1
5636a1f8f000-5636a1fb0000 rw-p 00000000 00:00 0 [heap]
7fde22c00000-7fde22c28000 r--p 00000000 08:03 4988401 /usr/lib/x86_64-linux-gnu/libc.so.6
7fde22c28000-7fde22dbd000 r-xp 00028000 08:03 4988401 /usr/lib/x86_64-linux-gnu/libc.so.6
7fde22dbd000-7fde22e15000 r--p 001bd000 08:03 4988401 /usr/lib/x86_64-linux-gnu/libc.so.6
7fde22e15000-7fde22e19000 r--p 00214000 08:03 4988401 /usr/lib/x86_64-linux-gnu/libc.so.6
7fde22e19000-7fde22e1b000 rw-p 00218000 08:03 4988401 /usr/lib/x86_64-linux-gnu/libc.so.6
7fde22e1b000-7fde22e28000 rw-p 00000000 00:00 0
7fde22ea3000-7fde22ea6000 rw-p 00000000 00:00 0
7fde22eb5000-7fde22eb6000 r--p 00000000 08:03 2883703 /home/liangjie/Desktop/cfp/Lib.so
7fde22eb6000-7fde22eb7000 r-xp 00001000 08:03 2883703 /home/liangjie/Desktop/cfp/Lib.so
7fde22eb7000-7fde22eb8000 r--p 00002000 08:03 2883703 /home/liangjie/Desktop/cfp/Lib.so
7fde22eb8000-7fde22eb9000 r--p 00002000 08:03 2883703 /home/liangjie/Desktop/cfp/Lib.so
7fde22eb9000-7fde22eba000 rw-p 00003000 08:03 2883703 /home/liangjie/Desktop/cfp/Lib.so
7fde22eba000-7fde22ebc000 rw-p 00000000 00:00 0
7fde22ebc000-7fde22ebe000 r--p 00000000 08:03 4988059 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7fde22ebe000-7fde22ee8000 r-xp 00002000 08:03 4988059 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7fde22ee8000-7fde22ef3000 r--p 0002c000 08:03 4988059 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7fde22ef4000-7fde22ef6000 r--p 00037000 08:03 4988059 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7fde22ef6000-7fde22ef8000 rw-p 00039000 08:03 4988059 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7ffe759f3000-7ffe75a14000 rw-p 00000000 00:00 0 [stack]
7ffe75b22000-7ffe75b26000 r--p 00000000 00:00 0 [vvar]
7ffe75b26000-7ffe75b28000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]
我们可以看到,整个进程虚拟地址空间中,相比与静态链接多了几个文件的映射。Lib.so 和 Program1 一样,它们都是被操作系统用同样的方法映射至进程的虚拟地址空间,只是它们占据的虚拟地址和长度不同。Program1 除了使用 Lib.so之外,它还用到了动态链接形式的C语言运行库 libc.so.6。另外还有一个值得关注的共享对象就是 ld-linux-x86-64.so.2,它实际上是Linux下的动态链接器。动态链接器与普通共享对象一样被映射到了进程的地址空间,在系统开始运行 Program1 之前(这时候已经完成了装载),首先会把控制权交给动态链接器,由它完成所有的动态链接工作,完成之后再把控制权交给 Program1,然后 Program1 程序开始执行。
3、地址无关代码
3.1 固定装载地址的困扰
关于共享目标文件在内存中的地址分配,主要有两种解决方案,分别是:
- 静态共享库(Static Shared Library)(地址固定)
- 动态共享库(Dynamic Shared Libary)(地址不固定)
静态共享库
静态共享库的做法是将程序的各个模块统一交给操作系统进行管理,操作系统在某个特定的地址划分出一些地址块,为那些已知的模块预留足够的空间。因为这个地址对于不同的应用程序来说,都是固定的,所以称之为静态。
但是静态共享库的目标地址会导致地址冲突、升级等问题。
动态共享库
采用动态共享库的方式,也称为装载时重定位(Load Time Relocation)。其基本思路是:在链接时,对所有绝对地址的引用都不作重定位,而把这一步推迟到装载时再完成。一旦模块装载地址确定,即目标地址确定,那么系统就对程序中所有的绝对地址引用进行重定位。
3.2 装载时重定位
采用动态共享库的方式,也称为装载时重定位(Load Time Relocation)。其基本思路是:在链接时,对所有绝对地址的引用都不作重定位,而把这一步推迟到装载时再完成。一旦模块装载地址确定,即目标地址确定,那么系统就对程序中所有的绝对地址引用进行重定位。
我们前面在静态链接时提到过重定位,那是的重定位叫做链接时重定位(Link Time Relocation),而现在这种情况经常被称为装载时重定位(Load Time Relocation)。在windows中,又叫基址重置(Rebasing),区别于静态链接的链接时重定位-link time relocation
但是这种方式也存在一些问题。比如,动态链接模块被装载映射至虚拟空间后,指令部分是在多个进程间共享的,由于装载时重定位的方法需要修改指令,所以没有办法做到同一份指令被多个进程共享,因为指令被重定位后对于每个进程来说都是不同的。
Attention:关于上面一句话的理解
共享对象也就是动态链接库在被装载到物理内存后,始终是只有一份的,不管有多少个进程使用它。但是对于每一个进程,共享对象会映射一次到虚拟地址空间,也就是每个进程空间都有一份共享对象的映射,此时,对于不同的进程,映射的地址(基址)是不一样的(大部分情况下)。紧接着,进行装载时重定位。装载时重定位由动态链接器完成,动态链接器会被一起映射到进程空间中。它根据共享对象在虚拟内存空间中的地址修改在物理内存中的共享对象中的指令,为什么会修改指令,原因在于绝对地址访问(如模块内的变量访问)是直接用mov指令完成的,也就是直接将地址打入寄存器,所以,此时的重定位会直接修改指令。进一步,共享对象中修改的指令是根据共享对象被映射到虚拟空间中的地址(基址)决定的,而每个进程对共享对象的映射不可能都是在相同地址。所以也就无法完成这一部分代码的共享
虽然,动态链接库中的代码是共享的,但是其中的可修改数据部分对于不同进程来说是由多个副本的,所以它们可以采用装载时重定位的方法来解决。基于此,一种名为地址无关代码的技术被提出以克服这个问题。
Linux 和 GCC 支持这种装载时重定位的方法,我们前面在产生共享对象时,时殷弘了两个 GCC 参数"-shared"和"-fPIC",如果只使用"-shared",那么输出共享对象就是使用了装载时重定位的方法。
3.3 地址无关码
基本思路是把指令中那些需要被修改的部分分离出来,跟数据部分放到一起,这样,剩下的指令就可以保持不变,而数据部分在每个进程中拥有一个副本。ELF 针对各种可能的访问类型(模块内部指令调用、模块内部数据访问、模块间指令调用、模块间数据访问),实现了对应地址引用方式,从而实现了PIC(Position-independent Code)。
共享对象模块中的地址引用按照是否为跨模块分为两类:模块内部引用、模块外部引用。按照不同的引用方式又可分为:指令引用、数据引用。以如下代码为例,可得出如下四种类型:
c
/*
* pic.c
*/
static int a;
extern int b;
extern void ext();
void bar()
{
a = 1;
b = 2;
}
void foo()
{
bar();
ext();
}
bash
liangjie@liangjie-virtual-machine:~/Desktop/cfp$ gcc -fPIC -shared -o libpic.so pic.c
类型1:模块内部的函数调用
由于被调用的函数与调用者都处于同一模块,它们之间的相对位置是固定的。对于现代的系统来说,模块内部的调用都可以是相对地址调用,或者是基于寄存器的相对调用,所以对于这种指令是不需要重定位的。
bash
liangjie@liangjie-virtual-machine:~/Desktop/cfp$ objdump -d libpic.so
......
0000000000001070 <bar@plt>:
1070: f3 0f 1e fa endbr64
1074: f2 ff 25 a5 2f 00 00 bnd jmp *0x2fa5(%rip) # 4020 <bar+0x2ee7>
107b: 0f 1f 44 00 00
......
000000000000115b <foo>:
115b: f3 0f 1e fa endbr64
115f: 55 push %rbp
1160: 48 89 e5 mov %rsp,%rbp
1163: b8 00 00 00 00 mov $0x0,%eax
1168: e8 03 ff ff ff call 1070 <bar@plt>
116d: b8 00 00 00 00 mov $0x0,%eax
1172: e8 e9 fe ff ff call 1060 <ext@plt>
1177: 90 nop
1178: 5d pop %rbp
1179: c3 ret
......
foo 中对 bar 的调用的那条指令实际上是一条相对地址调用指令。只要 bar 和 foo 的相对位置不变,这条指令是地址无关的。即无论模块被装载到哪个位置,这条指令都是有效的,这种相对地址的方式对于 jmp 指令也有效。
注:这里面的关于 "< bar@plt >",在后面的 PLT 章节会去详细讲解,这里就把它理解成 < bar > 就行了
类型2:模块内部的数据访问,如模块中定义的全局变量、静态变量
一个模块前面一般是若干个页的代码,后面紧跟着若干个页的数据,这些页之间的相对位置是固定的,即任何一条指令与它需要访问的模块内部数据之间的相对位置是固定的,所以只需要相对于当前指令加上固定的偏移量就可以访问模块内部数据了。
反汇编 libpic.so
bash
liangjie@liangjie-virtual-machine:~/Desktop/cfp$ objdump -d libpic.so
bash
0000000000001139 <bar>:
1139: f3 0f 1e fa endbr64
113d: 55 push %rbp
113e: 48 89 e5 mov %rsp,%rbp
1141: c7 05 e9 2e 00 00 01 movl $0x1,0x2ee9(%rip) # 4034 <a>
1148: 00 00 00
114b: 48 8b 05 86 2e 00 00 mov 0x2e86(%rip),%rax # 3fd8 <b>
1152: c7 00 02 00 00 00 movl $0x2,(%rax)
1158: 90 nop
1159: 5d pop %rbp
115a: c3 ret
以访问 a 变量为例:
bash
1141: c7 05 e9 2e 00 00 01 movl $0x1,0x2ee9(%rip) # 4034 <a>
%rip 寄存器保存的是下一条指令的地址"0x114b"(因为这是个相对地址,所以用引号扩住)
a 的访问地址为(这里是基于模块 装载地址为 0 来计算的):0x2ee9(固定偏移量) + 0x114b(当前指令即 PC 值) = 0x4034
固定偏移量 0x2ee9 是模块 libpic.so 在编译时就算好的
我们使用 readelf -S 查看 libpic.so 中各个 section 的地址,发现 0x4034 刚好在 .bss 段,符合未初始化的静态变量在 .bss 段事实。
bash
liangjie@liangjie-virtual-machine:~/Desktop/cfp$ readelf -S libpic.so
There are 27 section headers, starting at offset 0x3568:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
......
[21] .data PROGBITS 0000000000004028 00003028
0000000000000008 0000000000000000 WA 0 0 8
[22] .bss NOBITS 0000000000004030 00003030
0000000000000008 0000000000000000 WA 0 0 4
......
当然了,模块 libpic.so 的装载地址肯定不是0。在实际装载时,会确定模块的装载地址,那么变量 a 的访问地址为:
bash
装载地址 + 0x4034
类型3:模块间数据访问
模块间的数据访问比模块内部稍微麻烦一些,因为模块间的数据访问目标地址要等到装载时才决定。此时,动态链接需要使用代码无关地址技术,其基本思想是把地址相关的部分放到数据段。ELF 的实现方法是:在数据段中建立一个指向这些变量的指针数组,也称为全局偏移表(Global Offset Table,GOT),当代码需要引用该全局变量时,可以通过 GOT 中相对应的项间接引用。过程示意图如下所示
当指令中需要访问变量 b 时,程序会先找到 GOT,然后根据 GOT 中变量所对应的项找到变量的目标地址。每个变量都对应一个4字节的地址,链接器在装载模块的时候会查找每个变量所在的地址,然后填充GOT中的各个项,以确保每个指针所指向的地址正确。由于 GOT 本身是放在数据段的,所以它可以在模块装载时被修改,并且每个进程都可以由独立的副本,相互不受影响。
我们回顾刚才函数 bar()的反汇编代码。为访问变量 b ,我们程序首先计算出变量 b 在 got 中的位置,即
0x1152(%rip,也就是 PC 值) + 0x2e86 (固定偏移)= 0x3fd8
然后使用寄存器间接寻址方式给变量 b 赋值2。
bash
0000000000001139 <bar>:
1139: f3 0f 1e fa endbr64
113d: 55 push %rbp
113e: 48 89 e5 mov %rsp,%rbp
1141: c7 05 e9 2e 00 00 01 movl $0x1,0x2ee9(%rip) # 4034 <a>
1148: 00 00 00
114b: 48 8b 05 86 2e 00 00 mov 0x2e86(%rip),%rax # 3fd8 <b>
1152: c7 00 02 00 00 00 movl $0x2,(%rax)
1158: 90 nop
1159: 5d pop %rbp
115a: c3 ret
我们可以用 objdump 来查看 got 表位置:
bash
liangjie@liangjie-virtual-machine:~/Desktop/cfp$ objdump -h libpic.so
......
18 .got 00000028 0000000000003fd8 0000000000003fd8 00002fd8 2**3
CONTENTS, ALLOC, LOAD, DATA
19 .got.plt 00000028 0000000000004000 0000000000004000 00003000 2**3
CONTENTS, ALLOC, LOAD, DATA
......
可以看到 got 在文件中的偏移是 0x3fd8,我们再来看看 libpic.so 的需要在动态链接时的重定位项:
bash
liangjie@liangjie-virtual-machine:~/Desktop/cfp$ objdump -R libpic.so
......
0000000000003fd8 R_X86_64_GLOB_DAT b
.....
这里的 R_X86_64_GLOB_DAT 含义:一旦知道变量 b 的运行时地址,就把它放入 0x3fd8 处。
可以看到变量 b 的地址需要重定位,它的地址位于 0x3fd8,也就是 got 中偏移0,相当于是 GOT 中的第一项(每4字节一项)。这也就有上面反汇编中的对 b 赋值语句:
- 将 0x2e86 + PC 的值(就是变量 b 在 got 表中的地址)写到寄存器 rax 中
- 将立即数 2,赋值给 rax 寄存器中地址指向的值(也就是变量 b)
bash
......
114b: 48 8b 05 86 2e 00 00 mov 0x2e86(%rip),%rax # 3fd8 <b>
1152: c7 00 02 00 00 00 movl $0x2,(%rax)
......
类型4:模块间调用、跳转
对于模块间函数调用,同样可以采用类型3的方法来解决。与上面的类型有所不同的是,GOT中响应的项保存的是目标函数的地址,当模块需要调用目标函数时,可以通过GOT中的项进行间接跳转。
总结:
指令中的有些地址要在装载时才能确定,也就是不同的进程可能有不同的地址。
之前我们已经解释过,共享对象的数据段,是每个进程一份的。而数据段和代码段的相对位置又是确定的。
由此,我们就可以在数据段中建立一个指针数组,称其为GOT,global offset table。里面存放跨模块的数据的地址,当然,可以在装载时动态填入。然后,共享对象指令中对跨模块数据的访问,可以通过GOT中的指针间接访问。
这样的好处是,指令中的地址就从跨模块数据的地址,变成了got中指针的地址,而这个地址是相对代码段确定的。
以上,就是动态链接最最最核心的思想
3.5 共享模块全局变量问题
共享对象代码段中,对模块内全局数据的访问,也是通过 got 实现的。 既然是模块内,为啥不用相对地址呢?
因为其他的模块可能会使用全局数据。比如 module.c 中这样的代码:
c
extern int global;
int foo()
{
global = 1;
}
int main()
{
foo();
return 0;
}
我们对 module.c 进行编译,将他编译成一个目标文件 module.o
bash
liang@liang-virtual-machine:~/cfp$ gcc -c -fno-stack-protector module.c
随后使用 ld 对其进行链接,链接对象是一个动态库,且动态库中定义了全局变量 global,如下
bash
liang@liang-virtual-machine:~/cfp$ gcc -fPIC -shared -o libtest.so libtest.c
liang@liang-virtual-machine:~/cfp$ cat ./libtest.c
int global = 4;
int add(int a, int b)
{
global = a + b;
return global;
}
liang@liang-virtual-machine:~/cfp$
使用 ld 对其进行链接,链接成可执行文件 module
bash
liang@liang-virtual-machine:~/cfp$ ld -e main module.o -o module ./libtest.so
liang@liang-virtual-machine:~/cfp$
在 module.c 这个代码中,对 global 进行了赋值,既然是赋值,肯定需要 global 的地址,但是编译时 (这里只进行了编译,没有链接),gcc 并不知道它在共享对象中定义了。因此,gcc会在 bss 段中定义 global,也就是说,在编译 module.c 时,就为 global 分配了虚拟内存地址。这样,如果加载共享模块 libtest.so 后,加载的模块中(数据段)也有该变量的副本,肯定会产生矛盾。
既然有可能出现这种情况,干脆,让共享对象访问自身的全局变量时,也通过 got 的方式,就避免了进程中存在多个 global 的可能,即在装载时,将 global 的虚拟内存地址存入共享变量中的 got 中。
- 这样,如果运行时动态加载的时候,发现可执行文件中也有该变量,则会统一在 GOT 表中重定位填充为可执行文件 bss 段中该变量副本的地址。
- 如果在共享库中对该变量进行了初始化,动态装载器还得负责将初始化的值拷贝到可执行文件bss中该变量的副本位置。
- 如果可执行文件中没有该变量,则 GOT 表中重定位后,指向自己模块内的该变量。这样就意味着对模块内的变量访问,也采用了 GOT 表。也就是说,对于共享库中的全局对象,无论是否是内部的,还是无法决定是否是内部的,都得作为外部模块访问那样,使用 GOT 表进行访问。
我们可以使用 objdump 工具验证上述结论(共享对象访问自身的全局变量时,也是通过 got 的方式):
可以看到,got 表的范围为 0x200fd0 - 0x201000,
bash
liang@liang-virtual-machine:~/cfp$ objdump -h libtest.so
......
18 .got 00000030 0000000000200fd0 0000000000200fd0 00000fd0 2**3
CONTENTS, ALLOC, LOAD, DATA
19 .got.plt 00000018 0000000000201000 0000000000201000 00001000 2**3
CONTENTS, ALLOC, LOAD, DATA
......
我们再查看 动态可重定位表,我们发现需要重定位项 global,需要修复的地址刚好是 got 范围内,且刚好是 got[1] 条目1(.got section的大小为0x30------即.got中的条目个数为6(.got的每个条目占8字节))。
bash
liang@liang-virtual-machine:~/cfp$ objdump -R libtest.so
libtest.so: file format elf64-x86-64
DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
0000000000200e28 R_X86_64_RELATIVE *ABS*+0x0000000000000660
0000000000200e30 R_X86_64_RELATIVE *ABS*+0x0000000000000620
0000000000201018 R_X86_64_RELATIVE *ABS*+0x0000000000201018
0000000000200fd0 R_X86_64_GLOB_DAT _ITM_deregisterTMCloneTable
0000000000200fd8 R_X86_64_GLOB_DAT global@@Base
0000000000200fe0 R_X86_64_GLOB_DAT __gmon_start__
0000000000200fe8 R_X86_64_GLOB_DAT _Jv_RegisterClasses
0000000000200ff0 R_X86_64_GLOB_DAT _ITM_registerTMCloneTable
0000000000200ff8 R_X86_64_GLOB_DAT __cxa_finalize@GLIBC_2.2.5
R_X86_64_GLOB_DAT的含义:一旦知道 global 的运行时地址,就把它放入 0x200fd8 处。
我们还可以反汇编 add 函数代码,看看是如何访问 global 变量的:
bash
0000000000000690 <add>:
690: 55 push %rbp
691: 48 89 e5 mov %rsp,%rbp
694: 89 7d fc mov %edi,-0x4(%rbp)
697: 89 75 f8 mov %esi,-0x8(%rbp)
69a: 8b 55 fc mov -0x4(%rbp),%edx
69d: 8b 45 f8 mov -0x8(%rbp),%eax
6a0: 01 c2 add %eax,%edx
6a2: 48 8b 05 2f 09 20 00 mov 0x20092f(%rip),%rax # 200fd8 <_DYNAMIC+0x198>
6a9: 89 10 mov %edx,(%rax)
6ab: 48 8b 05 26 09 20 00 mov 0x200926(%rip),%rax # 200fd8 <_DYNAMIC+0x198>
6b2: 8b 00 mov (%rax),%eax
6b4: 5d pop %rbp
6b5: c3 retq
由上可见,对于 global 变量的访问,实际上是访问地址 0x200fd8 中的地址所指向的值
3.6 数据段地址无关性
通过上面的方法,我们能保证共享对象中的代码部分地址无关,但是数据部分是不是也有绝对地址引用的问题呢?
这里我们还是用上面地址无关码的例子,稍加改动:
c
static int a;
extern int b;
extern void ext();
static int*p=&a;
void bar()
{
a = 1;
b = 2;
b = *p;
}
void foo()
{
bar();
ext();
}
上面的地址无关码的例子里面加了这样一段代码的话
c
static int*p=&a;
那么指针 p 指向就是一个绝对地址,它指向变量 a,而变量 a 的地址会随着共享对象的装载地址改变而改变。那么有什么办法解决这个问题呢?
对于数据段来说,它在每个进程都有一份独立的副本,所以并不担心被进程改变。从这点来看,我们可以选择装载时重定位的方法来解决数据段中绝对地址引用问题。对于共享对象来说,如果数据段中有绝对地址引用,那么编译器和链接器就会产生一个重定位表(叫做rela.dyn),这个重定位表里面包含了 "R_X86_64_RELATIVE" 类型的重定位入口,用于解决上述问题。当动态链接器装载共享对象时,如果发现该共享对象有这样的重定位入口(动态链接重定位表),那么动态链接器就会对该共享对象进行重定位。
通过 objdump 工具得到共享目标文件的动态重定位表,如下:
bash
liangjie@liangjie-virtual-machine:~/Desktop/cfp$ objdump -R libpic.so
libpic.so: file format elf64-x86-64
DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
0000000000003e48 R_X86_64_RELATIVE *ABS*+0x0000000000001130
0000000000003e50 R_X86_64_RELATIVE *ABS*+0x00000000000010f0
0000000000004028 R_X86_64_RELATIVE *ABS*+0x0000000000004028
0000000000004030 R_X86_64_RELATIVE *ABS*+0x000000000000403c
0000000000003fd8 R_X86_64_GLOB_DAT b
0000000000003fe0 R_X86_64_GLOB_DAT __cxa_finalize
0000000000003fe8 R_X86_64_GLOB_DAT _ITM_registerTMCloneTable
0000000000003ff0 R_X86_64_GLOB_DAT _ITM_deregisterTMCloneTable
0000000000003ff8 R_X86_64_GLOB_DAT __gmon_start__
0000000000004018 R_X86_64_JUMP_SLOT ext
0000000000004020 R_X86_64_JUMP_SLOT bar
查看 section 信息,我们发现一个重定位项:
bash
0000000000004030 R_X86_64_RELATIVE *ABS*+0x000000000000403c
根据下面的段表 信息以及需要重定位的符号地址 ,可以判断出这一项需要重定位地址 位于 .data 段。根据动态重定位表中的 VALUE 值,可以知道重定位符号需要被修复成目的值为:0x403c
bash
liangjie@liangjie-virtual-machine:~/Desktop/cfp$ readelf -S libsta.so
There are 24 section headers, starting at offset 0x34c0:
Section Headers:
......
[19] .got PROGBITS 0000000000003fd8 00002fd8
0000000000000028 0000000000000008 WA 0 0 8
[20] .got.plt PROGBITS 0000000000004000 00003000
0000000000000028 0000000000000008 WA 0 0 8
[21] .data PROGBITS 0000000000004028 00003028
0000000000000010 0000000000000000 WA 0 0 8
[22] .bss NOBITS 0000000000004038 00003038
0000000000000008 0000000000000000 WA 0 0 4
......
反汇编 libpic.so ,我们看到,0x403c,刚好是变量 a 的地址,这刚好与代码想要表达的意思相符。
bash
0000000000001139 <bar>:
1139: f3 0f 1e fa endbr64
113d: 55 push %rbp
113e: 48 89 e5 mov %rsp,%rbp
1141: c7 05 f1 2e 00 00 01 movl $0x1,0x2ef1(%rip) # 403c <a>
1148: 00 00 00
114b: 48 8b 05 86 2e 00 00 mov 0x2e86(%rip),%rax # 3fd8 <b>
1152: c7 00 02 00 00 00 movl $0x2,(%rax)
1158: 48 8b 05 d1 2e 00 00 mov 0x2ed1(%rip),%rax # 4030 <p>
115f: 8b 10 mov (%rax),%edx
1161: 48 8b 05 70 2e 00 00 mov 0x2e70(%rip),%rax # 3fd8 <b>
1168: 89 10 mov %edx,(%rax)
116a: 90 nop
116b: 5d pop %rbp
116c: c3 ret
实际上,我们甚至可以让代码段也使用这种装载时重定位的方法,而不使用地址无关代码。从前面的例子中我们看到,我们在编译共享对象时使用了"-PIC"参数,这个参数表示产生地址无关的代码段。如果我们不使用这个参数来产生共享对象又会怎么样呢?
bash
$gcc -shared pic. c -o pic. so
上面这个命令就会产生一个不使用地址无关代码而使用装载时重定位的共享对象。但正如我们前面分析过的一样,如果代码不是地址无关的,它就不能被多个进程之间共享,于是也就失去了节省内存的优点。但是装载时重定位的共享对象的运行速度要比使用地址无关代码的共享对象快,因为它省去了地址无关代码中每次访问全局数据和函数时需要做一次计算当前地址以及间接地址寻址的过程。
对于可执行文件来说,默认情况下,如果可执行文件是动态链接的,那么 GCC 会使用 PIC 的方法来产生可执行文件的代码段部分,以便于不同的进程能够共享代码段,节省内存。所以我们可以看到,动态链接的可执行文件中存在"got"这样的段。
4、PLT
4.1 PLT
在之前的《静态链接与动态链接》中,我们介绍了这两者的优缺点:动态链接的缺点主要就是动态链接的程序执行速度会比静态链接的程度略慢一些。
原因就在于动态链接的可执行程序对于模块间的变量以及函数访问,都需要通过 GOT 表进行间接跳转。如此一来,程序的运行速度肯定会有所减慢。
另一个很重要的原因就是动态链接的链接工作是在程序运行时来完成的,即程序开始执行前动态链接器会去寻找并且装载程序所需的动态共享对象,然后完成一系列的符号重定位操作。这部分动作肯定会减慢程序的启动速度。
针对这种情况(链接工作是在程序运行时来完成的),一种称为"延迟绑定(Lazy Binding)"的解决办法出现了。延迟绑定的核心思想就是在程序启动时并不完成所有模块间函数调用的符号重定位操作,只有当目标程序需要调用某个模块外函数时才进行地址绑定(即符号查找、符号重定位)。
要实现以上的目标,ELF文件采用了 PLT(PProcedure Linkage Table) 的结构,这种结构内包含了一些很精妙的指令序列,这也是接下来所要讲解的内容。
PTL 原理:当调用外部模块的函数时,通过 PTL 新增加的一层间接跳转。调用函数并不直接通过 GOT 跳转,而是通过一个叫作 PLT 项的结构来进行跳转。每个外部函数在 PLT 中都有一个相应的项
4.2 大体逻辑思考
在讲解 PLT 具体细节之前,我们可以从自顶向下的角度来思考一下如何完成这一项工作。假设目标程序需要调用某个动态共享对象 liba.so内的函数foo(),那么第一次调用该函数的时候,动态链接器就需要一个寻找 foo 函数地址的查找函数来完成绑定的工作。
那么这个查找函数需要哪些信息呢?首先要知道绑定行为发生在哪个模块内(目标程序主模块内),其次我们要知道具体要绑定哪个函数(foo()函数)。在 Glibc 中,这个查找函数的名字就叫做 _dl_runtime_resolve()。把这个过程用伪代码描述出来,就如以下所示:
bash
void DSOFunction@plt()
{
if (DSOFunction@got[index] != RELOCATED) {
//如果该函数是第一次调用,GOT表内还没有该函数的地址
让查找函数根据模块ID和被调用函数的ID来获取被调用函数的地址
并且填入GOT的对应表项之中
DSOFunction@got[index] = RELOCATED;
}
else{
//GOT表内已经有了该函数地址,直接跳转到该函数地址
jmp *DSOFunction@got[index];
}
}
这一段伪代码就是 PLT 结构之中的模块外函数的对应表项。将伪代码整理一下,我们就可以得到汇编语言级别的 PLT 表项的内容,如下所示:
c
foo@plt
jmp *(foo@got)
push n
push moduleID
jmp _dl_runtime_resolve
第一条指令就是跳转到 foo() 函数所对应的 GOT 表项,如果该 GOT 表项已经被绑定好了,那就可以直接跳转到正确的函数地址。如果是第一次调用该函数,其 GOT 表项内的内容是第二条指令"push n"的地址,这一步就实现伪代码中的 if 判断。
第二条指令就是将 foo() 函数所对应的函数 ID 压入栈内,这个 ID 是 foo 函数在重定位表中的下标。
第三条指令就是将该模块的 ID 压入栈中,
第四条指令就是跳转到我们上文所说的查找函数_dl_runtime_resolve()。_dl_runtime_resolve()进行一系列查找之后,会将 foo() 函数的绝对地址填入 GOT 的对应表项中,然后将控制流转到 foo() 函数上。
一旦 foo() 函数地址被成功绑定,之后再次调用 foo() 在 PLT 的表项,就是直接通过 GOT 表项跳转到正确的地址上。以上就是 GOT 和 PLT 出现的大体逻辑。接下来讲解具体的工作流程。
4.3 GOT 与 PLT
ELF 文件将 got 分为两部分,分别是 .got 和 .got.plt,前者用于储存全局变量,后者用于保存 DSO 中的函数引用地址。
这里要说明一点:PLT 位于可执行程序的代码段,是可读不可写的;而 GOT 位于可执行程序的数据段,是可读可写的。另外 .got.plt 还有一个特别之处在于它的前三项都是有特定含义的,含义分别如下所示:
- 第一项保存了.dynamic段的地址,这其中描述了本模块动态链接的相关信息
- 第二项保存本模块的 ID
- 第三项保存了_dl_runtime_resolve的地址
我们还是以 libpic.so 为例,弄清 .got.plt 段的含义:
bash
liang@liang-virtual-machine:~/cfp$ objdump -s -d libpic.so
......
Contents of section .got.plt:
201000 100e2000 00000000 00000000 00000000 .. .............
201010 00000000 00000000 e6050000 00000000 ................
201020 f6050000 00000000 ........
......
liang@liang-virtual-machine:~/cfp$ readelf -S libpic.so
......
[19] .dynamic DYNAMIC 0000000000200e10 00000e10
00000000000001c0 0000000000000010 WA 4 0 8
......
[10] .plt PROGBITS 00000000000005d0 000005d0
0000000000000030 0000000000000010 AX 0 0 16
[11] .plt.got PROGBITS 0000000000000600 00000600
0000000000000010 0000000000000000 AX 0 0 8
......
liang@liang-virtual-machine:~/cfp$ objdump -S libpic.so
libpic.so: file format elf64-x86-64
Disassembly of section .plt:
00000000000005d0 <bar@plt-0x10>:
5d0: ff 35 32 0a 20 00 pushq 0x200a32(%rip) # 201008 <_GLOBAL_OFFSET_TABLE_+0x8>
5d6: ff 25 34 0a 20 00 jmpq *0x200a34(%rip) # 201010 <_GLOBAL_OFFSET_TABLE_+0x10>
5dc: 0f 1f 40 00 nopl 0x0(%rax)
00000000000005e0 <bar@plt>:
5e0: ff 25 32 0a 20 00 jmpq *0x200a32(%rip) # 201018 <_GLOBAL_OFFSET_TABLE_+0x18>
5e6: 68 00 00 00 00 pushq $0x0
5eb: e9 e0 ff ff ff jmpq 5d0 <_init+0x20>
00000000000005f0 <ext@plt>:
5f0: ff 25 2a 0a 20 00 jmpq *0x200a2a(%rip) # 201020 <_GLOBAL_OFFSET_TABLE_+0x20>
5f6: 68 01 00 00 00 pushq $0x1
5fb: e9 d0 ff ff ff jmpq 5d0 <_init+0x20>
......
64 位系统中地址长度是 64 比特,也就是 8 字节。按 8 字节一项并调整字节序后可得 .got.plt 的内容是
第几项 | 地址 | 内容 | 备注 |
---|---|---|---|
0 | 0x201000 | 0x0000000000200e10 | .dynamic 段地址 |
1 | 0x201008 | 0x0000000000000000 | 本镜像的link_map数据结构地址,未运行无法确定,故以全 0 填充 |
2 | 0x201010 | 0x0000000000000000 | _dl_runtime_resolve 函数地址,未运行无法确定,故以全 0 填充 |
3 | 0x201018 | 0x000000000000005e6 | bar 对应的 .got.plt 表项,内容是 bar 的 PLT 表项地址加 6 |
4 | 0x201020 | 0x000000000000005f6 | ext 对应的 .got.plt 表项,内容是 ext 的 PLT 表项地址加 6 |
bash
00000000000005e0 <bar@plt>:
5e0: ff 25 32 0a 20 00 jmpq *0x200a32(%rip) # 201018 <_GLOBAL_OFFSET_TABLE_+0x18>
5e6: 68 00 00 00 00 pushq $0x0
5eb: e9 e0 ff ff ff jmpq 5d0 <_init+0x20>
看到它跳转到了 0x200a32(%rip) 指向的地址,0x200a32(%rip) 的内容在反汇编结果的注释中给出了,是 0x201018 。0x201018 正是 bar 函数的 .got.plt 表项的地址,其内容是 0x00000000000005e6,这个地址实际上是 bar 的 PLT 表项地址加 6。可见 5e0 处的 jmpq 指令实际上跳到了 0x5e6 处,相当于没有跳转。0x5e6 处的 pushq 指令将 0x00 压栈,可以理解为接下来要调用的函数的参数。接着 0x5eb 处的 jmpq 指令跳转到了 0x5d0 即 PLT 表的第 0 项
bash
00000000000005d0 <bar@plt-0x10>:
5d0: ff 35 32 0a 20 00 pushq 0x200a32(%rip) # 201008 <_GLOBAL_OFFSET_TABLE_+0x8>
5d6: ff 25 34 0a 20 00 jmpq *0x200a34(%rip) # 201010 <_GLOBAL_OFFSET_TABLE_+0x10>
5dc: 0f 1f 40 00 nopl 0x0(%rax)
先是把 0x201008 即 .got.plt 表的第 1 项压栈,接着跳转到 201010 即 .got.plt 表的第 2 项亦即 _dl_runtime_resolve 函数,解析 bar 函数真正的地址。之后会执行 bar,并将 bar 函数真正的地址写到 bar 对应的 .got.plt 表项中。这样下次调用 bar 数时 0x5e0 处的 jmpq 指令会直接跳转到 bar 函数真正的地址,不用再调用 _dl_runtime_resolve。
4.4 .plt、.plt.got、.got 和 .got.plt 之间的区别
通过上一小节的分析:
section | 所在 segment | section 属性 | 用途 |
---|---|---|---|
.plt | 代码段 | RE(可读,可执行) | .plt section 实际就是通常所说的过程链接表(Procedure Linkage Table, PLT) |
.plt.got | 代码段 | RE | .plt.got section 用于存放 __cxa_finalize 函数对应的 PLT 条目 |
.got | 数据段 | RW(可读,可写) | .got section 中可以用于存放全局变量的地址;.got section 中也可以用于存放不需要延迟绑定的函数的地址 |
.got.plt | 数据段 | RW | .got.plt section 用于存放需要延迟绑定的函数的地址 |
5、动态链接相关结构
5.1 ".interp"段
动态链接器的位置由 ELF 可执行文件决定。在动态链接的 ELF 可执行文件中,有一个专门的段叫做".interp"段。
".interp"段的内容很简单,里面保存的就是一个字符串,这个字符串就是可执行文件所需要的动态链接器路径。
动态链接器在Linux下是Glibc的一部分,也就是属于系统库级别。
bash
liangjie@liangjie-virtual-machine:~/Desktop/cfp$ objdump -s Program1
Program1: file format elf64-x86-64
Contents of section .interp:
0318 2f6c6962 36342f6c 642d6c69 6e75782d /lib64/ld-linux-
0328 7838362d 36342e73 6f2e3200 x86-64.so.2.
Contents of section .note.gnu.property:
0338 04000000 20000000 05000000 474e5500 .... .......GNU.
0348 020000c0 04000000 03000000 00000000 ................
0358 028000c0 04000000 01000000 00000000 ................
Contents of section .note.gnu.build-id:
0368 04000000 14000000 03000000 474e5500 ............GNU.
0378 e24d7e65 dfac3356 97a6b11b d2524780 .M~e..3V.....RG.
0388 e12f526d ./Rm
Contents of section .note.ABI-tag:
038c 04000000 10000000 01000000 474e5500 ............GNU.
039c 00000000 03000000 02000000 00000000 ................
Contents of section .gnu.hash:
03b0 02000000 06000000 01000000 06000000 ................
03c0 00008100 00000000 06000000 00000000 ................
03d0 d165ce6d .e.m
Contents of section .dynsym:
03d8 00000000 00000000 00000000 00000000 ................
03e8 00000000 00000000 5c000000 12000000 ........\.......
03f8 00000000 00000000 00000000 00000000 ................
0408 01000000 20000000 00000000 00000000 .... ...........
0418 00000000 00000000 46000000 12000000 ........F.......
0428 00000000 00000000 00000000 00000000 ................
0438 1d000000 20000000 00000000 00000000 .... ...........
0448 00000000 00000000 2c000000 20000000 ........,... ...
0458 00000000 00000000 00000000 00000000 ................
0468 4d000000 22000000 00000000 00000000 M..."...........
0478 00000000 00000000 ........
......
......
5.2 ".dynamic"段
类似于".interp"这样的段,ELF中还有几个段也是专门用于动态链接的,比如 ".dynamic" 段和 ".dynsym"段等。要了解动态链接器如何完成链接过程,跟前面一样,从了解ELF文件中跟动态链接相关的结构入手将会是一个很好的途径。ELF文件中跟动态链接相关的段有好几个,相互之间的关系也比较复杂,我们先从 ".dynamic" 段入手
动态链接ELF中最重要的结构应该是" .dynamic"段,这个段里面保存了动态链接器所需要的基本信息,比如依赖于哪些共享对象、动态链接符号表的位置、动态链接重定位表的位置、共享对象初始化代码的地址等。" .dynamic"段的结构很经典,就是我们已经碰到过的ELF中眼熟的结构数组,结构定义在"elf.h"中
c
typedef struct {
Elf32_Sword d_tag;
union {
Elf32_Word d_val;
Elf32_Addr d_ptr;
} d_un;
} Elf32_Dyn;
//常见类型值
#define DT_NULL 0 /* Marks end of dynamic section */
#define DT_NEEDED 1 /* Name of needed library */
#define DT_HASH 4 /* Address of symbol hash table */
#define DT_STRTAB 5 /* Address of string table */
#define DT_SYMTAB 6 /* Address of symbol table */
#define DT_RELA 7 /* Address of Rela relocs */
#define DT_RELAENT 9 /* Size of one Rela reloc */
#define DT_STRSZ 10 /* Size of string table */
#define DT_INIT 12 /* Address of init function */
#define DT_FINI 13 /* Address of termination function */
#define DT_SONAME 14 /* Name of shared object */
#define DT_RPATH 15 /* Library search path (deprecated) */
#define DT_REL 17 /* Address of Rel relocs */
#define DT_RELENT 19 /* Size of one Rel reloc */
Elf32_Dyn 结构由一个类型值加上一个附加的数值或指针,对于不同类型,后面附加的数值或者指针有着不同含义。我们这里列举几个比较常见的类型值(这些值都是定义在"elf.h"里面的宏),如表7-2所示:
d_tag 类型 | d_un 的含义 |
---|---|
DT_SYMTAB | 动态连接符号表的地址,d_ptr 表示 ".dynsym" 的地址 |
DT_STRTAB | 动态链接字符串表地址,d_ptr 表示 ".dynstr" 的地址 |
DT_STRSZ | 动态链接字符串表大小,d_val 表示大小 |
DT_HASH | 动态链接哈希表地址,d_ptr 表示".hash"地址 |
DT_SONAME | 本共享对象的"SO-NAME",我们在后面会介绍"SO-NAME" |
DT_INIT | 初始化代码地址 |
DT_FINI | 结束代码地址 |
DT_NEEDED | 依赖的共享对象文件,d_ptr 表示所依赖的共享对象文件名 |
DT_REL | 动态链接重定位表入口 |
DT_RELENT | 动态重读位表入口数量 |
.dynamic 段可以看成是动态链接下ELF文件的"文件头",只是我们前面看到的 ELF 文件头中保存的是静态链接时相关的内容,比如静态链接时用到的符号表、重定位表等,这里换成了动态链接下所使用的相应信息了。使用 readelf 查看".dynamic"段的内容
bash
liangjie@liangjie-virtual-machine:~/Desktop/cfp$ readelf -d Lib.so
Dynamic section at offset 0x2e20 contains 24 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000c (INIT) 0x1000
0x000000000000000d (FINI) 0x1174
0x0000000000000019 (INIT_ARRAY) 0x3e10
0x000000000000001b (INIT_ARRAYSZ) 8 (bytes)
0x000000000000001a (FINI_ARRAY) 0x3e18
0x000000000000001c (FINI_ARRAYSZ) 8 (bytes)
0x000000006ffffef5 (GNU_HASH) 0x2f0
0x0000000000000005 (STRTAB) 0x3d8
0x0000000000000006 (SYMTAB) 0x318
0x000000000000000a (STRSZ) 127 (bytes)
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000003 (PLTGOT) 0x4000
0x0000000000000002 (PLTRELSZ) 48 (bytes)
0x0000000000000014 (PLTREL) RELA
0x0000000000000017 (JMPREL) 0x530
0x0000000000000007 (RELA) 0x488
0x0000000000000008 (RELASZ) 168 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x000000006ffffffe (VERNEED) 0x468
0x000000006fffffff (VERNEEDNUM) 1
0x000000006ffffff0 (VERSYM) 0x458
0x000000006ffffff9 (RELACOUNT) 3
0x0000000000000000 (NULL) 0x0
Linux还提供了ldd命令查看一个程序主模块或一个共享库依赖于哪些共享库:
bash
liangjie@liangjie-virtual-machine:~/Desktop/cfp$ ldd Program1
linux-vdso.so.1 (0x00007ffdf2b5e000)
./Lib.so (0x00007f13d8c69000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f13d8a00000)
/lib64/ld-linux-x86-64.so.2 (0x00007f13d8c75000)
5.3 动态符号表
动态符号表,段名通常叫做 .dynsym,用于表示模块之间的符号导入导出关系。.dynsym 只保存了与动态链接相关的符号,.symtab 中往往保存了所有符号,包括 .dynsym 中的符号。一般动态链接的模块同时拥有 .dynsym 和 .symtab 两个表。
与 .symtab 类似,动态符号表也需要一些辅助的表,比如动态符号字符串表 .dynstr。 由于动态链接在程序运行时查找符号,为了加快符号的查找过程,往往还有辅助的符号哈希表. hash。
我们可以使用 readelf 查看ELF文件的动态符号表及它的哈希表:
bash
liangjie@liangjie-virtual-machine:~/Desktop/cfp$ readelf -sD Lib.so
Symbol table for image contains 7 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterT[...]
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND [...]@GLIBC_2.2.5 (2)
3: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
4: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMC[...]
5: 0000000000000000 0 FUNC WEAK DEFAULT UND [...]@GLIBC_2.2.5 (2)
6: 0000000000001119 43 FUNC GLOBAL DEFAULT 14 foobar
动态链接符号表的结构与静态链接的符号表几乎一样。
5.3 动态链接重定位表
在静态链接中,目标文件里面包含有专门用于表示重定位信息的重定位表,比如 ".rel.text" 表示的是代码段的重定位表,".rel.data" 是数据段的重定位表。在动态链接中,也有重定位表:
- ".rela.dyn" 是对数据引用的修正,他所修正的位置位于 ".got" 以及数据段
- 而 ".rela.plt" 是对函数引用的修正,他所修正的位置位于 ".got.plt"
共享对象需要重定位的主要原因是导入符号的存在。
动态链接下,无论是可执行文件或共享对象,一旦它依赖于其他共享对象,也就是说有导入的符号,那么它的代码或数据中就会有对于导入符号的引用。在编译时这些导入符号地址未知。在静态连接中,这些未知的地址引用在最终链接时被修正。但是在动态链接中,导入符号的地址在运行时才确定,所以需要在运行时将这些导入符号引用修正,即需要重定位。
可以使用 readelf 或者 objdump 查看重定位表中的信息:
bash
liangjie@liangjie-virtual-machine:~/Desktop/cfp$ readelf -r Program1
Relocation section '.rela.dyn' at offset 0x558 contains 8 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000003da8 000000000008 R_X86_64_RELATIVE 1140
000000003db0 000000000008 R_X86_64_RELATIVE 1100
000000004008 000000000008 R_X86_64_RELATIVE 4008
000000003fd8 000100000006 R_X86_64_GLOB_DAT 0000000000000000 __libc_start_main@GLIBC_2.34 + 0
000000003fe0 000200000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_deregisterTM[...] + 0
000000003fe8 000400000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
000000003ff0 000500000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_registerTMCl[...] + 0
000000003ff8 000600000006 R_X86_64_GLOB_DAT 0000000000000000 __cxa_finalize@GLIBC_2.2.5 + 0
Relocation section '.rela.plt' at offset 0x618 contains 1 entry:
Offset Info Type Sym. Value Sym. Name + Addend
000000003fd0 000300000007 R_X86_64_JUMP_SLO 0000000000000000 foobar + 0
liangjie@liangjie-virtual-machine:~
liangjie@liangjie-virtual-machine:~/Desktop/cfp$ objdump -R Program1
Program1: file format elf64-x86-64
DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
0000000000003da8 R_X86_64_RELATIVE *ABS*+0x0000000000001140
0000000000003db0 R_X86_64_RELATIVE *ABS*+0x0000000000001100
0000000000004008 R_X86_64_RELATIVE *ABS*+0x0000000000004008
0000000000003fd8 R_X86_64_GLOB_DAT __libc_start_main@GLIBC_2.34
0000000000003fe0 R_X86_64_GLOB_DAT _ITM_deregisterTMCloneTable@Base
0000000000003fe8 R_X86_64_GLOB_DAT __gmon_start__@Base
0000000000003ff0 R_X86_64_GLOB_DAT _ITM_registerTMCloneTable@Base
0000000000003ff8 R_X86_64_GLOB_DAT __cxa_finalize@GLIBC_2.2.5
0000000000003fd0 R_X86_64_JUMP_SLOT foobar@Base
- 我们看到有几种重定位入口类型:R_X86_64_RELATIVE、R_X86_64_GLOB_DAT、R_X86_64_JUMP_SLOT。不同的重定位类型表示重定位时有不同的地址计算方法
- 其中 R_X86_64_GLOB_DAT、R_X86_64_JUMP_SLOT 这两种类型表示,被修正的位置只需要直接填入符号的地址即可