C++程序动态链接
先看代码:
//simpleApp.cpp
int foo(int a, int b);
int main(){
int a = 0;
int b = 1;
int c;
c = foo(a, b);
return c;
}
[root@xxxx] g++ -c simpleApp.cpp #生成.o文件
[root@xxxx] g++ -o simpleApp simpleApp.cpp #生成可执行文件
/tmp/cc8NKLpJ.o: In function `main':
simpleApp.cpp:(.text+0x21): undefined reference to `foo(int, int)'
collect2: error: ld returned 1 exit status
可以看到,生成.o文件成功。生成可执行文件失败,错误提示为foo(int, int)未定义的引用。
使用objdump工具查看一下生成的.o文件的汇编代码:
[root@xxxx]# objdump -d simpleApp.o
simpleApp.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
8: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
f: c7 45 f8 01 00 00 00 movl $0x1,-0x8(%rbp)
16: 8b 55 f8 mov -0x8(%rbp),%edx
19: 8b 45 fc mov -0x4(%rbp),%eax
1c: 89 d6 mov %edx,%esi
1e: 89 c7 mov %eax,%edi
20: e8 00 00 00 00 callq 25 <main+0x25> #这是调用foo函数的地方,随便填了一个数
25: 89 45 f4 mov %eax,-0xc(%rbp)
28: 8b 45 f4 mov -0xc(%rbp),%eax
2b: c9 leaveq
2c: c3 retq
从汇编代码中可以看到,在main函数中调用foo函数的地方,编译器随意填了一个数。
再查看一下simpleApp.o的符号表:
[root@xxxx]# readelf -s simpleApp.o
Symbol table '.symtab' contains 10 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND #未使用
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS simpleApp.cpp #源文件名
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1 #某个段
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3 #某个段
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4 #某个段
5: 0000000000000000 0 SECTION LOCAL DEFAULT 6 #某个段
6: 0000000000000000 0 SECTION LOCAL DEFAULT 7 #某个段
7: 0000000000000000 0 SECTION LOCAL DEFAULT 5 #某个段
8: 0000000000000000 45 FUNC GLOBAL DEFAULT 1 main #main函数
9: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _Z3fooii #未定义的foo函数
从上述符号表中我们可以看到foo函数的符号被标记为UND,即未定义的符号。
//foo.c
int foo(int a, int b){
return a + b;
}
在simpleApp.cpp中引用了foo.c中的foo(int, int)函数。
上一次讲了静态链接,这次讲一下动态链接。首先将foo.c编译生成.so动态链接库。
[root@xxxx] g++ -shared -fpic -o foo.so foo.c
以动态链接的方式将simpleApp.o和foo.so链接起来生成可执行文件:
[root@xxxx] g++ -o simpleApp simpleApp.o foo.so
查看其汇编代码:
[root@xxxx]# objdump -d simpleApp
simpleApp: file format elf64-x86-64
Disassembly of section .plt:
......
0000000000400530 <.plt>:
400530: ff 35 d2 0a 20 00 pushq 0x200ad2(%rip) # 601008 <_GLOBAL_OFFSET_TABLE_+0x8>
400536: ff 25 d4 0a 20 00 jmpq *0x200ad4(%rip) # 601010 <_GLOBAL_OFFSET_TABLE_+0x10>
40053c: 0f 1f 40 00 nopl 0x0(%rax)
0000000000400540 <_Z3fooii@plt>:
400540: ff 25 d2 0a 20 00 jmpq *0x200ad2(%rip) # 601018 <_Z3fooii>
400546: 68 00 00 00 00 pushq $0x0
40054b: e9 e0 ff ff ff jmpq 400530 <.plt> #跳转.plt
Disassembly of section .text:
......
0000000000400636 <main>:
400636: 55 push %rbp
400637: 48 89 e5 mov %rsp,%rbp
40063a: 48 83 ec 10 sub $0x10,%rsp
40063e: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
400645: c7 45 f8 01 00 00 00 movl $0x1,-0x8(%rbp)
40064c: 8b 55 f8 mov -0x8(%rbp),%edx
40064f: 8b 45 fc mov -0x4(%rbp),%eax
400652: 89 d6 mov %edx,%esi
400654: 89 c7 mov %eax,%edi
400656: e8 e5 fe ff ff callq 400540 <_Z3fooii@plt>
40065b: 89 45 f4 mov %eax,-0xc(%rbp)
40065e: 8b 45 f4 mov -0xc(%rbp),%eax
400661: c9 leaveq
400662: c3 retq
400663: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
40066a: 00 00 00
40066d: 0f 1f 00 nopl (%rax)
......
从汇编代码中可以看出,main函数调用了_Z3fooii@plt函数,而_Z3fooii@plt实际上并不是真正的foo函数,只是一个foo函数的入口,因为真正的foo函数在动态链接库中。
这里采用了一种延迟绑定的机制,main第一次调用_Z3fooii@plt函数时,会先跳转到*0x200ad2(%rip)位置,这个位置是.got.plt段中保存真正foo函数地址的位置,然而第一次调用时,它并没有指向真正的foo函数,而是指向其之后的指令,跳转到.plt中的代码,之后跳转到动态链接器。于是会先跳转进入动态链接器执行,动态链接器找到真正的foo函数的地址,将其回填到.got.plt段中保存真正foo函数地址的位置。这样之后再次调用_Z3fooii@plt函数时,就可以直接跳转到真正的foo函数去执行了。
为了验证我们的想法,可以看一下.got.plt段中的内容:
[root@xxxx]# objdump -s simpleApp
simpleApp: file format elf64-x86-64
......
Contents of section .got.plt:
601000 d00d6000 00000000 00000000 00000000 ..`.............
601010 00000000 00000000 46054000 00000000 ........F.@.....
......
此时看到的.got.plt段中的内容是初始的内容,而0x601018的内容0x400546即指向跳转真正foo函数的指令的后一条指令,第一次调用结束后,这个位置存放的内容将会变成真正的foo函数地址。
0x00601000 GOT[0] addr of .dynamic
0x00601008 GOT[1] addr of reloc extry
0x00601010 GOT[2] addr of dynamic linker
0x00601018 GOT[3] foo()
为什么需要动态绑定?
使用延迟绑定的动机是对于一个像libc.so这样的动态链接库中包含很多函数,而一个应用程序往往只会使用到其中的小部分。把函数地址的解析推迟到它实际被调用的地方,能避免动态链接库在加载时对没有使用到的函数进行不必要的重定位。第一次调用过程运行时开销较大,但是其后的每次调用只会花费一条指令和一个间接的内存引用。