大家好,欢迎来到【程序员的自我修养】专栏。正如其专栏名,本专栏主要分享学习《程序员的自我修养——链接、装载与库》的知识点以及结合自己的工作经验以及思考。编译原理相关知识本身就比较有难度,我会尽自己最大的努力,争取深入浅出。若你希望与一群志同道合的朋友一起学习,也希望加入到我们的学习群中。文末有加入方式。
通过上一篇【程序员的自我修养08】精华!!!动态库的由来及其实现原理,大致介绍了动态链接的历史背景,以及通过示例进一步了解当今动态库的实现过程。由于篇幅有限,所以针对一些特殊场景和动态库链接的过程没有进一步展开描述。本文主要作为上文内容的补充。
针对下面的示例,我们进行思考:
//main.c
#include<stdio.h>
extern int g_shared_num;
int main()
{
g_shared_num = 1;
printf("g_shared_num = %d\n",g_shared_num);
return 0;
}
//shared.c
int g_shared_num = 0;
编译&执行:
yihua@ubuntu:~/test/globaldata$ gcc -shared -fPIC shared.c -o libshared.so
yihua@ubuntu:~/test/globaldata$ gcc main.c -lshared -L. -o main
yihua@ubuntu:~/test/globaldata$ export LD_LIBRARY_PATH=./
yihua@ubuntu:~/test/globaldata$ ./main
g_shared_num = 1
yihua@ubuntu:~/test/globaldata$
分析:
在动态库libshared.so
中,定义了一个全局变量g_shared_num
,主程序并对其进行引用。由于main
在运行阶段不会进行动态重定位的操作,因此在编译阶段就需要确认g_shared_num
,于是乎,在main
程序的.bss
段创建一个副本。如下:
000000000000075a <main>:
75a: 55 push %rbp
75b: 48 89 e5 mov %rsp,%rbp
75e: 8b 05 ac 08 20 00 mov 0x2008ac(%rip),%eax # 201010 <g_shared_num>
764: 89 c6 mov %eax,%esi
766: 48 8d 3d 97 00 00 00 lea 0x97(%rip),%rdi # 804 <_IO_stdin_used+0x4>
76d: b8 00 00 00 00 mov $0x0,%eax
772: e8 b9 fe ff ff callq 630 <printf@plt>
777: b8 00 00 00 00 mov $0x0,%eax
77c: 5d pop %rbp
77d: c3 retq
77e: 66 90 xchg %ax,%ax
yihua@ubuntu:~/test/globaldata$ nm main | grep g_shared_num
0000000000201010 B g_shared_num
由上可知,main
的汇编语句中,对g_shared_num
的引用为相对地址引用。这就存在一个问题,main
和libshared.so
中都有g_shared_num
的副本,那么在运行时,采用哪一个呢?
当前的解决方式为:ELF共享库在编译时,默认把定义在模块内部的全局变量当作定义在其它模块的全局变量,也就是通过GOT来实现变量的访问。
思考:如下代码,最终输出什么呢?
//main.c
extern int shared1();
extern int shared2();
int main()
{
shared1();
shared2();
return 0;
}
//shared-1.c
#include<stdio.h>
int g_shared_num = 1;
int shared1()
{
printf("shared-1 g_shared_num = %d\n",g_shared_num);
return 0;
}
//shared-2.c
#include<stdio.h>
int g_shared_num = 2;
int shared2()
{
printf("shared-2 g_shared_num = %d\n",g_shared_num);
return 0;
}
编译:
yihua@ubuntu:~/test/globaldata$ gcc -fPIC -shared shared-1.c -o libshared1.so
yihua@ubuntu:~/test/globaldata$ gcc -fPIC -shared shared-2.c -o libshared2.so
yihua@ubuntu:~/test/globaldata$ gcc main.c -o main -L. -lshared1 -lshared2
yihua@ubuntu:~/test/globaldata$ ./main
??????????????????
可以思考一下为什么?或更换一下编译指令gcc main.c -o main -L. -lshared2 -lshared1
。再看看输出。
如下代码:
//test.c
static int a;
static int* p = &a
分析:
我们知道上述代码中,指针p
的地址是一个绝对地址,它指向变量a
。而a
会随着加载地址的不同而导致其虚拟地址变化。这就导致对指针p
引用的代码段也需要发生变化。这就会导致共享库中代码段无法在多个进程中共享。
当前的处理方式:对于共享对象来说,如果数据段中有绝对地址引用,那么编译器和链接器就会产生一个重定位表,在动态链接时,再重定位修复。如下:
如图可知,动态库中明确描述了符号p
在动态链接时,重定位为a
的地址。
其实动态链接的过程可以分为三步骤:启动动态链接器本身、装载所需要的共享对象、重定位和初始化。
至于动态链接器具体如何完成这一过程,不再赘述。
这里面有一个问题:若可执行程序依赖liba.so
,libb.so
,若两个共享库中具备相同的符号,按照上述的加载流程,岂不是会造成全局符号表中有两个相同的符号。该场景即为上面的思考问题。
在linux下的动态链接器处理规则:当一个符号需要被加入到全局符号表时,如果相同的符号名已经存在,则后加入的符号被忽略。这样一个共享对象里面的全局符号被另一个共享对象的全局符号覆盖的现象又被称为共享对象全局符号介入。
.init
段,那么动态链接器会执行.init
中的代码,用以实现共享对象特有的初始化过程。注:若进程的可执行文件也有.init
段,那么动态链接器不会执行它,因为可执行文件的.init
和.finit
由程序初始化代码负责执行。
Entry point address
并开始执行。本文是对上篇文章的补充,讲解了共享模块中全局变量的处理方式,以及如何实现共享模块中数据段的地址无关性。
再进一步讲解了动态链接的过程,其中可能会遇到的问题:共享对象全局符号介入。实际上,我在实际工作中已经遇到过了,并做过分享:坑惨啦!!!——符号冲突案例分析
若我的内容对您有所帮助,还请关注我的公众号。不定期分享干活,剖析案例,也可以一起讨论分享。
我的宗旨:
踩完您工作中的所有坑并分享给您,让你的工作无bug,人生尽是坦途。