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;
}
上述代码是foo函数的定义,常规操作,将其编译为.o文件,然后反汇编看一下:
[root@xxxx]# g++ -c foo.c
[root@xxxx]# objdump -d foo.o
foo.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <_Z3fooii>: #c++ name-mangling机制
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 89 7d fc mov %edi,-0x4(%rbp)
7: 89 75 f8 mov %esi,-0x8(%rbp)
a: 8b 55 fc mov -0x4(%rbp),%edx
d: 8b 45 f8 mov -0x8(%rbp),%eax
10: 01 d0 add %edx,%eax
12: 5d pop %rbp
13: c3 retq
查看foo.o的符号表:
[root@xxxx]# readelf -s foo.o
Symbol table '.symtab' contains 9 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS foo.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 2
4: 0000000000000000 0 SECTION LOCAL DEFAULT 3
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000000 0 SECTION LOCAL DEFAULT 6
7: 0000000000000000 0 SECTION LOCAL DEFAULT 4
8: 0000000000000000 20 FUNC GLOBAL DEFAULT 1 _Z3fooii
下面对两个.o文件进行链接,生成可执行文件,并查看其反汇编代码:
[root@xxxx]# g++ -o simpleApp simpleApp.o foo.o
[root@xxxx]# objdump -d simpleApp
simpleApp: file format elf64-x86-64
Disassembly of section .text:
0000000000400556 <main>:
400556: 55 push %rbp
400557: 48 89 e5 mov %rsp,%rbp
40055a: 48 83 ec 10 sub $0x10,%rsp
40055e: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
400565: c7 45 f8 01 00 00 00 movl $0x1,-0x8(%rbp)
40056c: 8b 55 f8 mov -0x8(%rbp),%edx
40056f: 8b 45 fc mov -0x4(%rbp),%eax
400572: 89 d6 mov %edx,%esi
400574: 89 c7 mov %eax,%edi
400576: e8 08 00 00 00 callq 400583 <_Z3fooii> #这里foo函数的具体位置确定了
40057b: 89 45 f4 mov %eax,-0xc(%rbp)
40057e: 8b 45 f4 mov -0xc(%rbp),%eax
400581: c9 leaveq
400582: c3 retq
0000000000400583 <_Z3fooii>:
400583: 55 push %rbp
400584: 48 89 e5 mov %rsp,%rbp
400587: 89 7d fc mov %edi,-0x4(%rbp)
40058a: 89 75 f8 mov %esi,-0x8(%rbp)
40058d: 8b 55 fc mov -0x4(%rbp),%edx
400590: 8b 45 f8 mov -0x8(%rbp),%eax
400593: 01 d0 add %edx,%eax
400595: 5d pop %rbp
400596: c3 retq
400597: 66 0f 1f 84 00 00 00 nopw 0x0(%rax,%rax,1)
40059e: 00 00
......#此处省略其它部分的代码
从上述代码中可以看出main函数中的foo函数地址被确定了。
总结一下,整个静态链接的过程就是将几个输入目标文件加工后合并成一个输出文件,可以分为两步:
- 空间与地址分配。在这个例子中链接器首先扫描simpleApp.o和foo.o文件,获得它们的各个段的长度、属性和位置,并且将输入目标文件中的符号表中所有的符号定义和符号引用收集起来,统一放到全局符号表。链接器将各个段合并,计算输出文件中各个段合并后的长度和位置,并建立映射关系。
- 符号解析与重定位。使用第一步中收集到的所有信息,读取输入文件中段的数据、重定位信息,并且进行符号解析与重定位、调整代码中的地址等。上述例子中合并完simpleApp.o和foo.o的.text段之后,实际上foo函数的位置已经确定,链接器需要修改main函数中关于foo函数引用的地址,这就是重定位。