在上一篇文章中,介绍了链接过程,本质上就是符号的解析、重定位。在上一步汇编的结果,生成一个可重定位目标文件。符号表、代码节、data节,还有重定位信息...。因为每个可重定位目标文件对应一个cpp,所以该cpp文件去调用其他cpp的函数,或者访问其他cpp的变量(非局部变量)时,汇编程序就会生成相关重定位信息。在链接过程中,就会根据这些重定位信息,去符号表找对应的符号的地址,调用的地方就会修改为符号的地址。
Chdy:编译原理——链接过程(上)zhuanlan.zhihu.com有了链接的过程,慢慢的就衍生出了静态链接、动态链接。程序中有很多重复的工作,比如:数学函数、显示等等功能。人们就想到用链接来复用这些高重复的功能。下面我们就来介绍静态链接和动态链接。
静态链接(.a.lib文件)
1.创建静态链接库
$gcc -c file1.c file2.c
$ar rcs mylib.a file1.o file2.o
2.使用静态链接库
$gcc -c main.c
$gcc -static -o output main.o ./mylib.a
3.链接过程
在静态链接过程中,链接器创建E、U、D三个集合。
- E组成可执行文件的所有目标文件的集合;
- U当前所有未解析的引用符号集合;
- D当前所有定义符号的集合;
开始E、U、D为空,首先扫描main.o,把它加入E,同时把引用符号(myFunc)加入U,main加入D。接着扫描mylib.a,将U中的所有符号与mylib.a中所有目标模块依次匹配,如发现file1.o中定义了myFunc,故file1.o加入到E,myFunc从U转移到D。在file1.o中发现还有未解析的符号printf,将其加到U。file2.o中没有匹配的符号,因而它将被丢弃。printf从C标准静态库(libc.a->printf.o指令中无需明显指出)获得匹配。到最后,如果U还有符号,则出现链接错误。
这里需要特别注意的是,链接器是根据命令行给出的顺序扫描。如果,符号的引用模块在后面,符号的定义在前面,就会出现链接错误。所以,好的做法,将静态库放到命令行最后。
4.与动态库比较的缺点:
- 因为静态库函数(如printf)被包含在每个进程的代码段中,对于并发运行上百个进程的系统,造成极大的主存资源浪费
- 因为静态库函数被合并到可执行目标中国,磁盘放着数千个可执行文件,造成磁盘空间浪费
- 静态库有新版本出现,须定期下载、重新编译和链接
动态链接(共享库 .so/.dll文件)
共享库包含目标模板的文件,每个模板包含有代码和数据。共享库,在磁盘和内存中只有一份备份,可以在装入时或运行时动态的被加载并链接。共享库升级时,被自动加载到内存,并和程序动态链接。可分模块,并独立的用不同的编程语言进行开发,第三方开发的共享库可作为程序插件,使程序功能易于扩展。
- 第一次加载运行时运行(load-time linking):通常由动态链接器(ld-linux.so)自动处理。标准C库(libc.so)通常按这种方式被链接
- 在已经开始运行后进行(run-time linking):通过调用dlopen()等接口实现。通常用于分发软件包、构建高性能Web服务器等。
1.创建动态链接库
$gcc -c file1.c file2.c
$gcc -shared -fPIC -o mylib.so file1.o file2.o
2.使用动态链接库
$gcc -c main.c
$gcc -o outfile main.o ./mylib.so
3.位置无关代码(PIC)
动态链接用到一个重要概念位置无关代码。GCC选项-fPIC指示生成PIC代码。要实现动态链接,必须生成PIC代码。共享库代码被加载的位置可以是不确定的,即使共享库代码长度发生变化,也不影响调用它的程序。引入PIC的目的,无需修改程序代码即可将共享库加载到任意地址运行。以下是应用。
特别注意:以下是可执行文件的内容,是通过工具objdump逆向形成,可执行文件只包括了,汇编指令所对应的二进制数。最左边的虚拟空间地址值和最右边的汇编指令,是工具生成,便于查看分析。
在链接过程中,可以通过重定位信息来重新定义调用其他模块函数的地址。但是,在可执行文件中,已经没有重定位信息了,要怎么链接呢?这时候,就引入了GOT(全局偏移表)来解决这个问题。GOT是一个指针数组(设置在.data节开始处,可读写),加载时动态链接器对GOT中各项进行重定位,填入所引用的地址。
call指令,跳转到804834cPLT[1]的首地址,接着跳到GOT[3](0x8049590的值所对应的地址0x8048352),即跳转到下一条指令。接着将常量0x0(标识ext函数)压栈。执行下一条指令,跳转到0x804833cPLT[0]。将0x8049588GOT[1]里面的内容压栈(4000a9f8动态链接器的标识信息)。执行下一条指令跳转到0x804958cGOT[2]里面的内容所对应的地址。GOT[2]为动态链接器延迟绑定代码的入口地址。开始执行动态链接,根据GOT[1]和ID确定ext地址并填入GOT[3],并转ext执行。以后调用ext,就不需要重复跳转了,因为GOT[3]已经修改为ext的地址了。