1. 为什么要动态链接?
1.1 静态链接浪费内存和磁盘空间
静态链接的方式对于计算机内存和磁盘空间浪费非常严重,特别是多进程操作系统的情况下,静态链接极大的浪费了内存空间。在现在的Linux系统中,一个普通的程序会使用的C 语言静态库至少1MB以上。
1.2 静态链接程序的开发和发布不便
静态链接对程序的更新、部署、和发布也会带来很多麻烦。比如程序program1所使用的Lib.o 是由一个第三方厂商提供的,当该厂商更新了Lib.o的时候,那么program1的厂商就要拿到最新的Lib.o,然后将其与program1.o连接,将新的program1发布给用户。
要解决空间浪费 和 更新困难这两个问题最简单的办法就是把程序的模块相互分割开来,形成独立的文件,而不再将他们静态的链接到一起,简单的讲,就是不对那些组成程序的目标文件进行链接,等到程序运行的时候才进行链接,也就是说,把链接到这个过程推迟到了运行时再进行,这就是动态链接的基本思想。
1.3 动态链接从磁盘空间的角度来讲如何节省磁盘
.so 文件没有被链接到可执行程序里,那么多个进程都依赖于同一份.so文件,那样就大大节省了磁盘空间。
1.4 动态链接从从内存角度来讲如何节省内存
第一个进程把.so加载到了内存空间,后续的可执行文件,就不需要加载重复的内从到内存空间了,只是需要链接就可以了(只是代码段共享,数据段还是有独立的副本,因为只有.so中全局变量能被这个可执行文件使用,这个时候可执行程序会在bss段建立这个全局变量的副本)
1.4.1 装载时重定位
想要达到代码段共享,是没有那么简单的。
当同时有多个程序运行的时候,操作系统根据当时内存空闲情况,动态的分配一块大小合适的物理内存,给程序,所以程序被装载的地址是不确定的,系统在装载程序的时候,需要对程序的指令和数据中绝对地址的引用进行重定位,这个行为就叫做装载时重定位。
装载时重定位,是指在动态加载过程中,重定位 动态对象的地址,
其中-shared 关键字就能达到这个目的(达到了装载时重定位的目的),但是对于想要共享动态库的 指令部分是不能满足的。

1.4.2 地址无关代码
如何解决指令部分共享的问题?
在linux 下使用-fPIC来达到共享代码的目的。
对于现代机器来说,产生地址无关的代码并不麻烦,我们先来分析模块中各种类型的地址引用方式,这里我们把共享对象模块中的地址引用按照是否为跨模块分成两类,模块内部引用和模块外部引用,按照不同的引用方式又可以分为指令引用和数据访问,这样我们就得到了下面的四种引用方式:

情况1: 内部函数定义引出全局符号的介入,还是要按照模块外部处理的(这里都需要got.plt)
情况2: 全局变量的情况比较特殊,还是要按照模块外部处理,除了定义在模块内部的静态全局变量。(这里也需要got,同时在可执行文件里的bss段创建副本)
情况3,模块外部的函数调用,引入got.plt (procedure linkage table),是延迟绑定(Lazy Binding)的实现方式
情况4,外部数据访问,引入got(global offset table)
1.4.2.1 GOT
我们先来描述下什么是GOT
我们以模块间的数据访问为例子:
我们前面提到要使得代码地址无关,基本的思想就是把跟地址相关的部分放到数据段里面,很明显,这些其他模块的全局变量的地址是跟模块装载地址有关的,elf的做法是在数据段里面建立一个指向这些变量的指针数组,也被称为全局偏移表(GOT),当代码需要引用该全局变量时,可以通过GOT中相应的项间接引用。

1.4.2.2 共享模块的全局变量问题
对于第二种和第四种的情况这个涉及 .got段,引出了全局变量的处理问题,在bss段定义副本。
我们上面的情况中没有包含定义在模块内部的全局变量情况(只是有静态的全局变量),可能你的第一反应是这个不是很简单吗?跟模块内部定义的静态变量一样处理不就可以了吗?用上面的类型2的方式解决,但是有一种情况很特殊,当一个模块引用了 一个定义在共享对象中的全局变量的时候,比如 一个共享对象定义了一个全局的变量 global,而模块module.c中是这样引用的:
extern int global;
int foo(){
global = 1;
}
当编译器编译 module.c时,它无法根据这个上下文判断 global 是定义在同一个模块的其它目标文件还是定义在另外一个共享对象之中,即无法判断是否为跨模块间的调用
1.假设module.c是程序可执行文件的一部分,那么在这种情况下,由于程序主模块的代码并不是地址无关代码,也就是说代码不会使用这种类似于PIC的机制(可执行程序会用-fPIE),它引用这个全局变量的方式和普通数据访问的方式是一样的,编译器会产生这样的代码,
movl $0x1 , XXXXXXXX
XXXXXXXX 就是global的地址,由于可执行文件在运行时并不进行代码重定位,所以变量的地址必须在链接过程中确定下来,为了能够使链接过程正常进行,连接器会在创建可执行文件时,在它的".bss"段创建一个global 变量的副本,那么问题就很明显了,现在global 变量定义在原先的共享对象中,而在可执行文件的".bss"段还有一个副本。如果同一个变量同时存在于多个位置,这在程序实际的运行过程中肯定是不行的。
于是解决办法只有一个,那就是所有的使用这个变量的指令都指向位于可执行文件中的那个副本,elf共享库在编译时,默认把定义在模块内部的全局变量,当做定义在其他模块的的全局变量,前面的类型4,通过got 来实现变量的访问,当共享模块被装载时,如果某个全局变量在可执行文件中拥有副本,那么动态链接器就会把GOT中的相应地址指向该副本,这样该变量在运行时实际上最终只有一个实例。
如果变量在共享模块中被初始化,那么动态链接器还需要将该初始化值,复制到程序主模块中的变量副本去( 应该调用了赋值运算符重载函数),如果该全局变量在程序主模块中没有副本,那么GOT中的相应的地址就指向模块内部的该变量副本。
2.假设module.c是一个共享对象的一部分,那么GCC编译期在-fPIC的情况下,就会把对 global的调用按照跨模块 模式产生代码。原因也很简单,编译期无法确定对global的引用是跨模块的还是模块内部的,即使是模块内部的,按照上面1的结论,还是会产生跨模块的代码,因为global可能被可执行文件引用,从而使得共享模块中对global的引用要执行可执行文件中的global副本。
我们的代码示例如下:
pic.c int a =100; int b; void zhang(){ a = 80; b = 90; }
如果,没在zhang里使用 a,b,那么a,b是不会再重定位表里的。
main.c extern int a; extern int b; int main(){ a =800; b =900; }
这个例子中,在可执行程序里确实会直接用到这个,动态库里的全局变量,然后确实会在bss段建立副本,然后也会让加载到
的动态库的got段。里重新指定全局变量的地址,指定到可执行程序的定义位置,但是这种情况还是比较少见的,因为我们很少
用动态库里的全局变量:
我们常见的情况有两种
1.我们在可执行程序里定义了全局变量,然后和动态库里的全局变量冲突了
下面我们开始详细的分析第一种情况,我们有如下的code:
// pic.h void zhang();
// pic.c #include<stdio.h> int a =100; int b; void zhang(){ printf("aa value: %d,bb value: %d\n",a,b); printf("aa addr: %p,bb addr: %p\n",&a,&b); a = 80; b = 90; printf("a value: %d,b value: %d\n",a,b); printf("a addr: %p,b addr: %p\n",&a,&b); }
//编译动态库指令如下 gcc -shared -fPIC pic.c -o libpic.so
// main.c #include"pic.h" #include<stdio.h> int a=1000; int b; int main(){ printf("1main a addr is %p,main b addr %p\n",&a,&b); zhang(); printf("2main a value is %d,main b value %d\n",a,b); printf("3main a addr is %p,main b addr %p\n",&a,&b); return 0; }
// 编译这个可执行程序,关闭pie,默认是pie的 gcc -no-pie -g -O0 main.c -Wl,-rpath=./ -L./ -lpic -o main
-no-pie 为了关闭默认开启的pie ,不让操作系统去random.
首先说一下这个问题的输出:

我们先从这个输出中值的问题,分析,然后再从这个地址的角度,来分析。
我们程序的main函数开始执行时,动态库已经被装载了,装载已经结束了,装载之前,动态库是不知道可执行程序中是否会用到自己内部的全局变量的,所以它的got表,默认会放,全局变量在自己内存中的地址。
当动态库被加载的时候,动态链接器发现,自己的全局变量,在 可执行程序中有符号定义,链接器是不关心,你是定义冲突了,还是你是在用我的, 原则只有一个,就是我动态库的一定要和你可执行程序的统一,所以加载的时候,就会把got表中的
内容 进行修改,改成可执行程序中定义的变量地址。
这也解释了第一次打印的时候,还是可执行程序的值,但是在动态库里赋值以后,就把可执行程序的值改变了,后面可执行程序自己,打印的值也是,80,90.
然后打印地址是为了验证,可执行程序在装载以后,确实动态库里的变量,和可执行程序的变量地址都是相同的。
下面我们想实际验证下,在程序加载完之后,got表中的内容是否真的改变了。里面的值是否是可执行程序中的a,和b的地址。
在c++程序运行过程中,如何查看动态库中GOT表的内容?
gdb program_name
2. 在gdb中设置断点,可以是在某个函数调用之前或者在感兴趣的位置上设置断点: break function_name break line_number
3. 运行程序,直到断点处停止:run
4. 当程序停在断点处时,使用gdb的`info proc mappings`命令查看动态库的加载地址范围: info proc mappings
找到与动态库相关的内存区域,确定动态库的加载地址。 5. 使用gdb的`x`命令查看GOT表的内容,指定GOT表的起始地址和大小:前提是通过 readelf -S 找到 .got段在原动态库的地址(然后在进程的 虚拟地址空间中可以当做地址偏移来使用,因为原地址是以零地址为基准的)
x/nfu address
其中,`n`是要显示的内存单元的数量 10 ,`f`是显示格式 x 16进制显示,`u`是每个单元的大小,不写默认4字节(或者是w))),如果显示8字节用g,`address`是GOT表的起始地址。 例如,假设动态库被加载到地址`0x12340000`,并且GOT表的偏移是`0x100`,要查看GOT表的前10个指针(每个指针占8个字节),可以使用以下命令: x/10xg 0x12340100
1.查看.so 中got段的地址 通过 readelf -S ,以及.so中got段中的内容 readelf -r
readelf -S libpic.so 查看所有段,包括 got got.plt
对于段表中 Address的解释如下,来自于程序员的自我修养
section Address 段虚拟地址,如果该段可以被加载,则 sh_addr 为该段被加载后的,在进程空间中的虚拟地址(只是单存的编译后的地址,不是真正的地址,考虑到基准地址可能不是从零开始,以及pie的情况),否则 sh_addr 为零。
该段可以被加载,指定是可执行程序和动态库,包含动态库,不包含目标文件.o
readelf -S libpic.so
There are 28 section headers, starting at offset 0x1810:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .note.gnu.build-i NOTE 00000000000001c8 000001c8
0000000000000024 0000000000000000 A 0 0 4
[ 2] .gnu.hash GNU_HASH 00000000000001f0 000001f0
0000000000000044 0000000000000000 A 3 0 8
[ 3] .dynsym DYNSYM 0000000000000238 00000238
0000000000000150 0000000000000018 A 4 1 8
[ 4] .dynstr STRTAB 0000000000000388 00000388
000000000000009e 0000000000000000 A 0 0 1
[ 5] .gnu.version VERSYM 0000000000000426 00000426
000000000000001c 0000000000000002 A 3 0 2
[ 6] .gnu.version_r VERNEED 0000000000000448 00000448
这个.so 中的 Address列,到底怎么解读,和可执行程序,.o中的这个解读相同吗?
在`readelf -S`命令的输出中,`Address`列显示了各个段的内存地址。对于动态库(`so`文件)和可执行程序(可执行文件)以及目标文件(`o`文件),`Address`列的解读方式并不完全相同。
对于可执行程序和目标文件,`Address`列显示的是该段在虚拟内存中的加载地址(Load Address)。这表示段在内存中的实际运行位置。
一般.o文件中因为还没有被链接,分配地址,所以这个值一般都是0x00000000.
对于可执行目标程序来说,这个地址,可能是虚拟内存中的 加载地址,(如果没有开启pie的情况下,一般操作系统在gcc5.0编译器会默认开启pic,需要编译可执行程序的时候,手动的-no-pic 来关闭),如果开启了pie,那个这个地址相当于一个偏移了,因为这个地址,我们生成可执行程序来说,一般都是从零地址开始的,而开启了pie以后,会让操作系统把这个起始地址:radom 到 563a7a34d000-563a7a34e000 类似于这样的地址,所以我们相对于零地址的 这个地址,就可以当做一个偏移去使用,用最新的一个 radom起始地址加上原地址(Address),才是这个段在可执行文件中真正的地址。
而对于动态库,`Address`列显示的是加载地址(前提是这个动态库能从零地址加载它的各个段,但是动态库永远不可能从零地址开始加载它的各个段,不像可执行程序,有这个机会从零地址加载)所以这个地址,就相当于一个偏移来使用了,因为它是从零地址开始算的。
获取动态库在运行时的加载地址,需要使用其他方法,如在程序运行时使用调试器(如gdb)的`info proc mappings`命令来查看动态库加载到的内存地址范围。
我们的输出情况如下:

所以通过这个图,我们能看到 .got段相对于起始地址为0x0000的情况下,它的地址是 0x200fd0.
我们再来看下 .got 段中的内容

上面这个Offset还是比较形象的,而没有写成地址。这里用到了,-r来看 .got是个省劲的办法,也可以通过objdump看,.got段里详细的二进制数据。
这个截图能够说明 .got段的起始偏移地址 0x200fd0,和之前的图是吻合的,然后,.got段中前四个单元中的内容分别是:
_ITM_deregisterTMClone ,b, gmon_start,a,对应的值分别为 0,0x201030,0 ,0x201028,
那么我们看下,a,b的地址,是不是动态库中分配的地址,通过下图可以确定是动态库中,a,b的地址。也就是编译时指定的,
如果被可执行程序加载后,发现可执行程序里有对应的全局符号,则会指向可执行程序里,自己的a,b就不再使用了。

2.看下可执行程序中的 a.b的地址,以及运行起来后,ab的地址,以及ab地址和动态库中的got表关系

gcc -no-pie -g -O0 main.c -Wl,-rpath=./ -L./ -lpic -o main 使用这个指令编译的,关闭了pie
因为关闭了Pie,所以程序在运行起来也不会改变。
然后我们现在就用gdb调试下程序。

上面这个图,使用gdb查看当前进程的虚拟地址空间映射,从图中可以看到,我们的动态库被加载到了起始地址为
0x7ffff7bd3000,我们知道之前的got 表地址 0x200fd0,是针对于起始地址是零的情况,现在其实地址是0x7ffff7dd3000
那么.got 的地址应该是 0x7ffff7dd3000+0x200fd0 = 0x7ffff7dd3fd0
下面查看下got的这个起始地址,x/8xg 8代表查看8个单元,x代表16进制显示,g,代表每个单独的字节数

通过查看看到,a,和b的地址已经变成了可执行程序中的地址。验证了我们的猜想。
readelf -s libpic.so 查看所有的符号表,包括全局符号表和动态 符号表
nm (names) libpic.so 可以清晰的看到 符号在哪个段
readelf -r libpic.so 这个是查看重定位 表
一定要注意,如果没有引用这个符号,则一定不会在重定位表里。
readelf -s main 这个是查看 可执行文件的符号表,然后对于变量和函数来说,就代表变量或函数的虚拟地址,
在.o中 符号值代表在段中的偏移。
2.我们加载了多个动态库,动态库之间的全局变量冲突了,或者是同一个静态库被链接进了不同的动态库,然后不同的动态库被一个程序链接。导致冲突(这种更为常见)
下午要验证下这个,然后把之前的视频过滤下,记录相关内容。
在gdb调试中,查看变量的符号在全局里是否存在?
在C++程序运行过程中,可以通过使用调试工具(如gdb)来查看全局符号表的内容。调试工具可以提供对正在运行的程序的运行时信息的访问权限,包括全局符号表。 下面是在gdb中查看全局符号表的步骤: 1. 启动gdb调试器并加载正在运行的程序: gdb executable_file ````` 2. 运行程序: `````` run ````` 3. 使用gdb的`info variables`命令来查看全局符号表的内容: `````` info variables ````` 该命令会列出当前程序所有的全局变量以及符号表中的其他全局符号。
在gdb调试中,能否查看函数的符号是否存?
在 GDB 调试中,可以使用`info functions` 命令来查看当前可执行文件中的函数列表,从而判断函数符号是否存在。 在 GDB 命令提示符下,输入`info functions` 命令,GDB 将显示当前可执行文件中所有已加载的函数符号。函数符号将按照它们在代码中的定义顺序进行列出,并包括函数的名称、返回类型和参数列表。 如果要查找特定函数是否存在,可以使用 GDB 的`info functions` 命令的正则表达式参数,例如`info functions *my_function*`。这将只显示包含 "my_function" 字符串的函数符号,如果存在匹配的函数符号,则表示该函数存在。 通过`info functions` 命令,您可以快速检查函数符号的存在与否,以便在调试过程中进行相关的操作和调整。
gdb 中如何查看进程的虚拟地址空间分配
info proc mappings
对于第二种情况,我们来看下示例代码:
//pic.h void zhang();
// pic.c #include<stdio.h> int a =100; int b; void zhang(){ printf("aa value: %d,bb value: %d\n",a,b); printf("aa addr: %p,bb addr: %p\n",&a,&b); a = 80; b = 90; printf("a value: %d,b value: %d\n",a,b); printf("a addr: %p,b addr: %p\n",&a,&b); }
// pic1.h void zhang1();
// pic1.c #include<stdio.h> int a =110; int b; void zhang1(){ printf("zhang1 a value: %d,bb value: %d\n",a,b); printf("zhang1 a addr: %p,bb addr: %p\n",&a,&b); a = 180; b = 190; printf("zhang1 a value: %d,b value: %d\n",a,b); printf("zhang1 a addr: %p,b addr: %p\n",&a,&b); }
//main.c #include"pic.h" #include"pic1.h" #include<stdio.h> int main(){ zhang(); zhang1(); return 0; }
编译代码如下:
gcc -shared -fPIC pic.c -o libpic.so gcc -shared -fPIC pic1.c -o libpic1.so gcc -g -O0 -no-pie main.c -Wl,-rpath=./ -L./ -lpic1 -lpic -o main // 链接库的顺序,决定了使用哪个库里定义的变量的地址
输出内容如下:

通过这个输出,我们可以得到下面的结论:
动态库的加载顺序和编译程序的链接顺序有关,先链接到先加载。
动态库在编译时,会为自己定义的全局变量,生成相应的got段,指向自己变量的地址。
我们的加载顺序看下,编译指令:gcc -g -O0 -no-pie main.c -Wl,-rpath=./ -L./ -lpic1 -lpic -o main
我们先加载的pic1.so,也就是zhang1对应的动态库。
pic1.so 在被加载到进程的地址空间以后,动态链接器就会为它里面定义的 变量a和b分配,虚拟地址空间,根据它之前变量在动态库自身的偏移。
同时更新自己的.got段,指向正确的变量a,b地址。这个时候pic1.so库,就完成加载了。并且,a,b已经进入了全局符号表。
当pic.so在被加载到进程里,动态链接器同样会为它里面定义的 变量a和b分配空间,但是发现进程的全局符号表里已经有了a,b,则自己的.got
表更新成了,可执行程序中全局符号表里a和b的地址(此地址是pic1.so中变量a,b的地址),同时把自己的也放入全局符号表中。
所以最终两个库中的,.got都指向了第一个被加载的 .so中的变量a和b的地址。
下面来验证我们的结论是正确的。
而且注意对于全局变量是没有延迟加载的问题的。
我们先来看 pic.so和pic1.so中的.got段中的内容和地址:


pic1.so中的内容几乎和pic.so中相同,截图如下:
我们用gdb验证下,当程序启动以后,通过计算偏移地址,得到.got地址,然后打印.got地址中的内容,是不是都已经更新为第一个被加载的库中的变量a,b地址:
我们先验证下,我们在程序中真正使用的地址,是哪个库中的地址:先看程序中的打印信息

我们可以看到对应的a,地址是: 0x 7ffff7dd4028 b 地址是: 0x 7ffff7dd4030
我们先看下两个库中对于两个符号的地址是多少,然后在进程地址空间中加上这个偏移,看看加哪个库的基准地址,才能是上面的地址。
因为两个库,完全相同,所以地址偏移是相同的。我只是截取一个就可以了。

再截取下进程地址空间情况:

很明显a,b的地址是通过 pic1.so中的起始地址偏移, 0x 7ffff7bd3000 加上a,b在动态库pic1.so中的偏移地址,0x201028,0x201030
a 地址是: 0x 7ffff7dd4028 =0x 7ffff7bd3000 + 0x201028
b 地址是: 0x 7ffff7dd4030 =0x 7ffff7bd3000 + 0x201030
下面我们查看下,两个动态库加载完成以后,两个动态库中的.got表,里的变量地址是不是也都是指向了 pic1.so中的变量地址。
我们先来看下.got表在原始的,动态库中的地址,然后把这个地址作为偏移在 可执行程序的地址空间中进行查找。
因为两个库,完全相同,所以地址偏移是相同的。我只是截取一个就可以了。


上图验证了libpic.so的got段里的内容也是指向了,libpic1.so中的变量a.b
note:
即使是在编译的时候链接了动态库,但是没有调用动态库里的方法,那么动态库不会被加载到进程的虚拟地址空间。
对于静态局部变量,也是同样的遵从上面的原则,也是会放到.got段,然后看下下面的代码示例,简单的修改原来的代码如下:
我只是修改了 pic.cpp pic1.cpp 里面定义的全局变量,放到了 函数内部的静态局部变量
// pic.c #include<stdio.h> int b; void zhang(){ static int a =100; printf("aa value: %d,bb value: %d\n",a,b); printf("aa addr: %p,bb addr: %p\n",&a,&b); a = 80; b = 90; printf("a value: %d,b value: %d\n",a,b); printf("a addr: %p,b addr: %p\n",&a,&b); } // pic1.c #include<stdio.h> int b; void zhang1(){ static int a =200; printf("zhang1 a value: %d,bb value: %d\n",a,b); printf("zhang1 a addr: %p,bb addr: %p\n",&a,&b); a = 180; b = 190; printf("zhang1 a value: %d,b value: %d\n",a,b); printf("zhang1 a addr: %p,b addr: %p\n",&a,&b); }
程序的输出如下:
zhc@zhc-ThinkCentre-M930t-N000:~/TestC++/testdynamic$ ./main aa value: 100,bb value: 0 aa addr: 0x7f5e10458028,bb addr: 0x7f5e1065a030 a value: 80,b value: 90 a addr: 0x7f5e10458028,b addr: 0x7f5e1065a030 zhang1 a value: 200,bb value: 90 zhang1 a addr: 0x7f5e1065a028,bb addr: 0x7f5e1065a030 zhang1 a value: 180,b value: 190 zhang1 a addr: 0x7f5e1065a028,b addr: 0x7f5e1065a030
可以看出静态局部变量a的地址都是相同的,证明了两次修改的是同一个位置的这个全局变量。
1.4.2.3 got.plt
.got 段里存放全局变量,.got.plt 存放的是函数
可执行程序里也是存在 .got 和 got.plt的。
然后 .got 存的是全局变量,没有延迟加载的说法
.got 段,可执行程序中如果没有extern 动态库里的变量,则不会有 变量的内容
got.plt 存在延迟加载的问题,需要进一步。
因为 可执行程序一定会调用 动态库里的函数,所以可执行程序的.got.plt一定是有内容的,
然后因为延迟加载,当调用函数前,.got.plt里的地址,不是函数地址,当第一次调用完成以后,才会变成
函数的真正的地址。
测试函数代码如下:
// pic1.h void zhang1(); void pic1_init();
//pic1.c #include<stdio.h> void zhang(){ printf("pic1 zhang enter\n"); } void pic1_init(){ printf("pic1_init enter\n"); zhang(); }
// pic.h void zhang(); void pic_init();
// pic.c #include<stdio.h> void zhang(){ printf("pic zhang enter\n"); } void pic_init(){ printf("pic_init enter\n"); zhang(); }
#include"pic.h" #include"pic1.h" #include<stdio.h> int main(){ pic_init(); pic1_init(); zhang(); printf("after zhang addr is %p\n",zhang); return 0; }
编译指令如下:
gcc -shared -fPIC pic.c -o libpic.so gcc -shared -fPIC pic1.c -o libpic1.so gcc -g -O0 -no-pie main.c -Wl,-rpath=./ -L./ -lpic1 -lpic -o main // 链接库的顺序,决定了使用哪个库里定义的函数
程序的输出如下:

note:注意我们写了两个init函数的原因是为了,在里面调用zhang这个函数,否则zhang不在两个动态库的 重定位表里。
结论:
1.因为我们先链接的libpic1.so,所以main函数中调用了libpic1.so里的zhang.
在libpic.so中,调用zhang,执行的也是libpic1.so 的zhang.
2.因为我们在主函数里调用了zhang,pic_inti,pic1_init,所以这三个会放入got.plt表。
3.现在一共有三个got.plt表,main函数一个,pic1.so一个,pic.so一个。
main函数里没有zhang的定义,pic1.so和pic.so里都有定义,加载他俩后,都会在全局符号表里带入zhang这个符号,然后地址不同。
最后的指向情况如下图:

现在开始证明这个问题
1.证明这个地址zhang确实是libpic1.so中zhang的地址:

从图中看出zhang的地址是 0x7ffff7bd364a


最后看出这个地址是 0x000000000000064a+0x7ffff7bd3000= 0x7ffff7bd364a
2.证明上面的图标,也就是三个got.plt都指向同一个zhang的地址。


note.
这个图中要去掉那个printf打印,否则会把zhang搞到数据段里,暂时不清楚原因。
3.证明延迟加载
上面的图,已经证明了,程序执行完,zhang的地址确定,下面再执行zhang之前打断点,没有调用zhang的时候,可执行文件里plt表里的zhang,不是正确的地址。

1.4.2.4 全局符号介入问题
我们看下面的示例代码:
// pic.c static int a; extern int b; extern void ext(); void bar(){ a =1; b =2; } void foo(){ bar(); ext(); }
对上面的代码进行,编译生成动态库.
gcc -shared -fPIC pic.c -o libpic.so
然后我们用 readelf -r libpic.so 看下它的重定位表输出内容如下:

-
-
-
-
-
-
-
- 图 1
-
-
-
-
-
-
然后通过重定位表,我们知道,.rela.plt 是对.got.plt的重定位,而 .rela.dyn 是对数据段和.got段的重定位。
然后让我们一起来计算一下,如何进行重定位的。我们首先输出这个.so文件的各个段的内容
readelf -S libpic.so

-
-
-
-
-
-
-
- 图 2
-
-
-
-
-
-
我们看到 .got.plt段的偏移地址是 0x00001000, 地址是 0x00201000,这个偏移地址的意思是针对起始地址的偏移,因为目前看其实地址算作了0x00200000,而真正这个动态库被加载到进程的虚拟地址空间以后,可能是0x00300000.但是针对起始地址的偏移不会改变,这个也是我们能够找到 .got.plt段的主要原因。
然后我们再来计算一下 .got.plt 段中的内容都是什么。如下图:

-
-
-
-
-
-
-
- 图 3
-
-
-
-
-
-
图 3 就可以跟 图 1对应起来,图 1中的两个偏移 201018 201020对应。
首先在重定位表中,表示要重定位,.got.plt中的内容,对应的偏移是 201018 的函数标号 ext的地址,以及对应偏移是201020 的函数标号 bar的地址,在进行装载的时候,因为动态库的起始地址是不确定的,所以这两个地址也会跟随其实地址的变化而变化,但是最终一定是和.got.plt段中的具体下标是对应的,当动态链接器知道了函数的具体地址以后,就会根据重定位指示的位置,来更新.got.plt中的具体函数地址的内容,以达到重定位的目的。
通过以上分析,我们确定了第一种情况,和第三种情况,动态库的处理方式都是相同的,都是通过.got.plt的形式进行调用内部或者外部函数。
全局符号介入:优先级 按加载顺序,树的广度优先遍历
got,got.plt对于类对象的情况:
上测试代码,我们构造一种全局类对象冲突的情况,以及看看调用谁的构造,调用谁的拷贝构造,调用赋值运算符重载。
以及我在main函数里定义的全局变量的构造顺序和动态库里的比。
//pic.h #include<stdio.h> void zhang(); void pic_init(); class TestGlobal{ public: TestGlobal(){ printf("TestGlobal1 enter\n"); } ~TestGlobal(){ printf("~TestGlobal1 enter\n"); } TestGlobal(const TestGlobal&other){ printf("TestGlobal1 copy construct enter\n"); } TestGlobal& operator= (const TestGlobal&other){ printf("TestGlobal1 = enter\n"); } int a; };
//pic.cpp #include<stdio.h> #include"pic.h" TestGlobal zhanghuaichao; void zhang(){ printf("pic zhang enter\n"); } void pic_init(){ printf("pic_init enter\n"); TestGlobal zhc; zhanghuaichao = zhc; zhang(); }
//pic1.h #include<stdio.h> void zhang1(); void pic1_init(); class TestGlobal{ public: TestGlobal(){ printf("TestGlobal2 enter\n"); } ~TestGlobal(){ printf("~TestGlobal2 enter\n"); } TestGlobal(const TestGlobal&other){ printf("TestGlobal2 copy construct enter\n"); } TestGlobal& operator= (const TestGlobal&other){ printf("TestGlobal2 = enter\n"); } int a; };
//pic1.cpp #include<stdio.h> #include"pic1.h" TestGlobal zhanghuaichao; void zhang(){ printf("pic1 zhang enter\n"); } void pic1_init(){ printf("pic1_init enter\n"); TestGlobal zzzzz; zhanghuaichao =zzzzz; TestGlobal www(zzzzz); zhang(); }
#include"pic.h" void zhang1(); void pic1_init(); #include<stdio.h> class zyl{ public: zyl(){ printf("zyl enter\n"); } }; zyl testzyl; int main(){ printf("main enter\n"); pic_init(); pic1_init(); return 0; }
//编译代码指令如下: g++ -shared -fPIC pic.cpp -o libpic.so g++ -shared -fPIC pic1.cpp -o libpic1.so g++ -g -O0 -no-pie main.c -Wl,-rpath=./ -L./ -lpic1 -lpic -o main
输出结果如下:

结论:
1.静态加载的动态库里全局变量的初始化顺序,早于 可执行程序里定义的全局变量,可执行程序里全局变量早于main。
2.由于构造函数也是函数,C++里有类的概念,但是汇编里是没有类的概念。只有函数和变量。
而在 libpic1.so里有定义全局变量,然后有拷贝构造,有赋值运算符重载的调用,所以这三个函数,会进入.got.plt

而在libpic.so里有构造对象,有调用对象的赋值运算符重载,所以它的got.plt表里的内容指向了动态库pic1.so里的函数地址。
3. libpic.so里操作的变量zhanghuaichao的地址,就是libpic1.so里定义的变量的地址。