什么是静态链接
代码经过编译生成目标文件后的下一步是将多个目标文件链接成一个可以执行文件。
将多个目标文件链接成一个可执行文件的过程称为静态链接。
目标文件对于外部符号的处理
单个源文件编译中当引用到外部文件的变量或者函数时(这些外部函数与变量也称为外部符号),会暂时将引用到地址以伪地址代替,等待链接时将真正引用的地址替换上。
以两个文件hello.c和world.c为例子。hello.c引用了两个外部变量,一个为系统标准输出函数printf,一个为全局变量globalInt。
//hello.c
#include <stdio.h>
extern int globalInt ;
int main()
{
int b = doubleNumber(globalInt);
return b;
}
//world.c
int globalInt = 4;
int doubleNumber(int a)
{
return a * a;
}
使用-c参数编译
gcc -c hello.c world.c
hello.c 与world.c 并分别输出目标文件,hello.o和world.o
再将hello.o反编译成汇编代码objdump -d hello.o
将world.o反编译成汇编代码objdump -d world.o
hello.o: 文件格式 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: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # e <main+0xe>
e: 89 c7 mov %eax,%edi
10: b8 00 00 00 00 mov $0x0,%eax
15: e8 00 00 00 00 callq 1a <main+0x1a>
1a: 89 45 fc mov %eax,-0x4(%rbp)
1d: 8b 45 fc mov -0x4(%rbp),%eax
20: c9 leaveq
21: c3 retq
这里需要留意两个行命令
1. 8: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # e <main+0xe>
这条指令的作用是将全局变量globalInt传入doubleNumber作为参数。命令为将相对rip位置为0的内存区域,传送至eax中。 其中8b05为mov的命令码,00000000为地址。
2. 15: e8 00 00 00 00 callq 1a <main+0x1a>
这条指令的作用调用doubleNumber函数,即进行函数跳转。e8是call指令码,00000000是跳转地址
这里可以看到,无论globalInt的值还是doubleNumber函数肯定不会为0,此处编译器使用0时作为伪地址暂时填充。
world.o: 文件格式 elf64-x86-64
Disassembly of section .text:
0000000000000000 <doubleNumber>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 89 7d fc mov %edi,-0x4(%rbp)
7: 8b 45 fc mov -0x4(%rbp),%eax
a: 0f af 45 fc imul -0x4(%rbp),%eax
e: 5d pop %rbp
f: c3 retq
地址与空间的分配策略
从上一篇文件说到,每个目标文件生成后,都会拥有自己的代码段,数据段,那么当他们进行链接后,生成的可执行文件的段结构是什么样的?
目前主流的方法是将性质相同的段进行合并,如不同目标文件的代码段,数据段都会合成同一个代码、数据段。
第一步 文件混合
合并各个目标文件中相同的段,并且将所有目标文件的符号表中的符号统一放置到全局的符号表中。
第二步 符号解析与重定位
读取段中的数据,重定位信息,调整代码中的地址。
使用ld连接器将hello.o 和 world.o进行链接
ld hello.o world.o -e main -o helloworld
使用objdump -h hello.o
查看链接前hello.o的各个段的属性
Idx Name Size VMA LMA File off Algn
0 .text 00000022 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000000 0000000000000000 0000000000000000 00000062 2**0
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 0000000000000000 0000000000000000 00000062 2**0
ALLOC
3 .comment 00000035 0000000000000000 0000000000000000 00000062 2**0
CONTENTS, READONLY
4 .note.GNU-stack 00000000 0000000000000000 0000000000000000 00000097 2**0
CONTENTS, READONLY
5 .eh_frame 00000038 0000000000000000 0000000000000000 00000098 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
使用objdump -h world.o
查看链接前world.o的各个段的属性
Idx Name Size VMA LMA File off Algn
0 .text 00000010 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data 00000004 0000000000000000 0000000000000000 00000050 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 0000000000000000 0000000000000000 00000054 2**0
ALLOC
3 .comment 00000035 0000000000000000 0000000000000000 00000054 2**0
CONTENTS, READONLY
4 .note.GNU-stack 00000000 0000000000000000 0000000000000000 00000089 2**0
CONTENTS, READONLY
5 .eh_frame 00000038 0000000000000000 0000000000000000 00000090 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
使用objdump -h helloworld
查看链接后helloworld的各个段的属性
Idx Name Size VMA LMA File off Algn
0 .text 00000032 00000000004000e8 00000000004000e8 000000e8 2**0
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .eh_frame 00000058 0000000000400120 0000000000400120 00000120 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .data 00000004 0000000000600178 0000000000600178 00000178 2**2
CONTENTS, ALLOC, LOAD, DATA
3 .comment 00000034 0000000000000000 0000000000000000 0000017c 2**0
CONTENTS, READONLY
可以看到链接后的helloworld文件.text段大小为0x32,为hello.o和world.o的.text大小之和。相同还有.data段也为为hello.o和world.o的.data段大小之和。
如上图看到的,其实这里的地址的分配这里有两层意思,第一层指的是链接后文件的存放的磁盘大小,第二层指的是程序装载到内存后,所占的虚拟内存区域的大小。
物理的磁盘的大小可以从File Off列和Size列共同计算得出某段的起始偏移和大小,加载后段所以在的虚拟内存偏移可以从VMA(Virtual Memory Address)计算得出。
符号解析
刚才说到,目标文件在编译时,会将外部符号的引用地址使用伪地址进行填充,那么当多个目标文件进行链接时,会符号进行解析。
再对hello.o和world.o的链接后的helloworld进行反编译
objdump -d helloworld
helloworld: 文件格式 elf64-x86-64
Disassembly of section .text:
00000000004000e8 <main>:
4000e8: 55 push %rbp
4000e9: 48 89 e5 mov %rsp,%rbp
4000ec: 48 83 ec 10 sub $0x10,%rsp
4000f0: 8b 05 82 00 20 00 mov 0x200082(%rip),%eax # 600178 <globalInt>
4000f6: 89 c7 mov %eax,%edi
4000f8: b8 00 00 00 00 mov $0x0,%eax
4000fd: e8 08 00 00 00 callq 40010a <doubleNumber>
400102: 89 45 fc mov %eax,-0x4(%rbp)
400105: 8b 45 fc mov -0x4(%rbp),%eax
400108: c9 leaveq
400109: c3 retq
000000000040010a <doubleNumber>:
40010a: 55 push %rbp
40010b: 48 89 e5 mov %rsp,%rbp
40010e: 89 7d fc mov %edi,-0x4(%rbp)
400111: 8b 45 fc mov -0x4(%rbp),%eax
400114: 0f af 45 fc imul -0x4(%rbp),%eax
400118: 5d pop %rbp
400119: c3 retq
再次看文件开头时指出的两处命令,如今已经指明了具体的值。
4000f0: 8b 05 82 00 20 00 mov 0x200082(%rip),%eax # 600178 <globalInt>
指出globalInt的位置为0x600178。
4000fd: e8 08 00 00 00 callq 40010a <doubleNumber>
调用callq最终跳转的地址为0x040010a,也就是doubleNumber函数的位置。
用图直观一点表示
重定向符号
上面可以看到链接后生成的文件,最终的地址得到正确的修改,指向了正确的函数或者全局变量的地址。那符号是如何找到重定向到正确的地址呢?
正常有三步骤
1.从重定义表获取需重定向的符号
当生成目标文件时,如果有使用到外部定义的符号时,会将符号写入到对应的重定位段中。比如在text段中遇到外部符号,写入到.rel.text中。
查看hello.o的重定位段
objdump -r hello.o
hello.o: 文件格式 elf64-x86-64
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
000000000000000a R_X86_64_PC32 globalInt-0x0000000000000004
0000000000000016 R_X86_64_PC32 doubleNumber-0x0000000000000004
2.从全局符号表中找到符号
目标文件链接成一个文件后,符号表都统一写入到全局符号表中,从中可以找到对应的符号相应的具体的地址
3.修正指令
根据指令来进行修正
小结
静态链接的大体过程可以分为两大阶段,1.目标文件的合并,2.符号的解析与重定向