1. 背景:
当模块与模块之间复用一些函数,但是函数实现又略有不同,不修改函数名时候的链接方式。
简单例子:
liba由a.c和ap.c组成,主程序由main.c和mp.c组成,其中ap.c和mp.c中有相同接口的print函数,但是其内部实现不同。期望结果为foo调用liba的print,main的print调用mp.c中的print。
a.c
#include "ap.h"
void foo ()
{
print ();
}
ap.c
#include <stdio.h>
void print ()
{
print ("Liba.\n");
}
main.c
extern void foo (void);
extern void print (void);
int main ()
{
foo ();
print ();
}
mp.c
#include <stdio.h>
void print ()
{
printf ("Main.\n");
}
2. 静态链接:
静态链接使用ar进行打包,将由汇编器生成的.o文件打包,生成的静态库其实就是一堆.o的集合,所有符号都尚未进行链接,只有在将静态库进行链接的时候,才会进行所有的.o和主程序进行链接。
同时注意,在上述例子中,静态库中的a.c中的foo将无法找到静态库中的print,因为生成静态库仅仅是对不同的.o的一个打包的过程,并没有将静态库中的每个.o进行关联分析,个人简单理解为对静态库的链接仅仅是将所有的.o进行链接,但是有一点区别:链接是与顺序有关的,当出现重复的符号时,主程序的符号弱出现在静态库之前,则将会覆盖静态库的符号,并没有报错,但是当库出现在主程序的符号之前时,会产生重定义的错误。编译器(GCC-4.8.3)应该是保证了主程序符号的有效性。因此看起来静态链接并不能产生期望的结果。
[@iZ25yrrem9lZ lib]$ hcc main.c -la mp.c -L./liba
/tmp/cclbjTuU.o: In function `print':
mp.c:(.text+0x0): multiple definition of `print'
./liba/liba.a(ap.o):ap.c:(.text+0x0): first defined here
collect2: 错误:ld 返回 1
[@iZ25yrrem9lZ lib]$ hcc main.c mp.c -la -L./liba
[@iZ25yrrem9lZ lib]$ ./a.out
Main
Main
[@iZ25yrrem9lZ lib]$
3. 动态链接:
3.1 动态链接、静态链接的区别:
动态链接库生成的方式不同于静态链接库,动态链接库并不是简单打包的过程,中间有一个链接的动作,但是和静态链接不同,静态链接会根据链接脚本进行重定位,最后生成的可执行文件中每个符号都有对应的有效地址。但是动态链接库是在程序运行过程中才进行加载(包括延迟加载),因此此时并没有相应的绝对地址,仍然是汇编器生成的相对地址。但是对于动态链接库中的符号,在动态链接库生成的时候,会对内部的符号进行处理,根据不同的选项生成延迟绑定的plt函数或者是内部函数(Bsymbolic选项)。
3.2 fPIC、GOT表:
GOT表为Global offset table(全局偏移表),当加载动态库的函数时遇到不确定的符号,将通过该表进行查找。
PIC为position independent code(位置无关代码)。
PLT为procedure linkage table(过程链接表),主要处理动态库函数在第一次使用时才进行绑定,其重定位本质仍是查找GOT,本文不展开叙述。
当编译过程中加上 -fPIC选项时,编译器会修改外部函数的属性(a.o为fPIC编译的代码,liba/a.o为没有fPIC代码,下图皆同),加了fPIC选项的未定义符号print为一个R_X86_64_PLT32的重定位类型。同时查看符号表,多出了一个GOT的段。
[@iZ25yrrem9lZ lib]$ readelf -r a.o liba/a.o
File: a.o
Relocation section '.rela.text' at offset 0x548 contains 1 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000005 000a00000004 R_X86_64_PLT32 0000000000000000 print - 4
Relocation section '.rela.eh_frame' at offset 0x560 contains 1 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000020 000200000002 R_X86_64_PC32 0000000000000000 .text + 0
File: liba/a.o
Relocation section '.rela.text' at offset 0x518 contains 1 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000005 000900000002 R_X86_64_PC32 0000000000000000 print - 4
Relocation section '.rela.eh_frame' at offset 0x530 contains 1 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000020 000200000002 R_X86_64_PC32 0000000000000000 .text + 0
[@iZ25yrrem9lZ lib]$ nm a.o liba/a.o
a.o:
U _GLOBAL_OFFSET_TABLE_
0000000000000000 T foo
U print
liba/a.o:
0000000000000000 T foo
U print
3.3 动态加载时候的几种符号查询:
动态链接对符号的查找主要有两种场景,即模块内部和模块外部。
在模块内部使用时,将采用相对跳转和调用。如下(使用了Bsymbolic),这里的print使用的是底下地址为598的print,是相对调用。
000000000000058c <foo>:
58c: 55 push %rbp
58d: 48 89 e5 mov %rsp,%rbp
590: e8 03 00 00 00 callq 598 <print>
595: c9 leaveq
596: c3 retq
597: 90 nop
0000000000000598 <print>:
598: 55 push %rbp
599: 48 89 e5 mov %rsp,%rbp
59c: 48 8d 3d 53 00 00 00 lea 0x53(%rip),%rdi # 5f6 <_fini+0xe>
5a3: e8 f8 fe ff ff callq 4a0 <puts@plt>
5a8: c9 leaveq
5a9: c3 retq
5aa: 90 nop
5ab: 90 nop
5ac: 90 nop
5ad: 90 nop
5ae: 90 nop
5af: 90 nop
在模块外部,使用的是间接访问,需要对GOT进行查询(这里因为延迟绑定的缘故使用的是plt),这里调用的不是下面5c8内部的print。
00000000000005bc <foo>:
5bc: 55 push %rbp
5bd: 48 89 e5 mov %rsp,%rbp
5c0: e8 13 ff ff ff callq 4d8 <print@plt>
5c5: c9 leaveq
5c6: c3 retq
5c7: 90 nop
00000000000005c8 <print>:
5c8: 55 push %rbp
5c9: 48 89 e5 mov %rsp,%rbp
5cc: 48 8d 3d 53 00 00 00 lea 0x53(%rip),%rdi # 626 <_fini+0xe>
5d3: e8 e0 fe ff ff callq 4b8 <puts@plt>
5d8: c9 leaveq
5d9: c3 retq
5da: 90 nop
5db: 90 nop
5dc: 90 nop
5dd: 90 nop
5de: 90 nop
5df: 90 nop
4d8位于.plt段,主要进行的还是对GOT的查询。
00000000000004d8 <print@plt>:
4d8: ff 25 ba 03 20 00 jmpq *0x2003ba(%rip) # 200898 <_GLOBAL_OFFSET_TABLE_+0x28>
4de: 68 02 00 00 00 pushq $0x2
4e3: e9 c0 ff ff ff jmpq 4a8 <_init+0x18>
当创建共享库的时候,默认都会当成全局符号进行处理。3.4 Bsymbolic选项:
打开这个选项以后,构建动态库的时候,动态库都会先选择库内的符号进行使用。
[@iZ25yrrem9lZ lib]$ gcc liba/a.c -fPIC -c -o a.o
[@iZ25yrrem9lZ lib]$ gcc liba/ap.c -fPIC -c -o ap.o
[@iZ25yrrem9lZ lib]$ gcc --shared -o liba.so a.o ap.o -Wl,-Bsymbolic
[@iZ25yrrem9lZ lib]$ gcc main.c mp.c -la -L. -Wl,-rpath=./
[@iZ25yrrem9lZ lib]$ ./a.out
Liba
Main
[@iZ25yrrem9lZ lib]$
参考资料:
1. 《程序员的自我修养-连接、装在与库》第七章、动态链接