读《程序员的自我修养》静态链接后,通过demo结合machO及x86_64架构解析静态链接过程
前言
关于静态链接的定义,及为什么要使用静态链接本文不再讨论,本文主要讨论如何实现静态链接,或者说静态链接是如何实现的。
首先考虑下,要实现静态链接需要解决哪些问题?比如:多个文件链接到一起使用什么规则?有外部引用的符号如何解决?等等。
问题分解:
- 如何合并多个目标文件(object file)
- 如何确定链接之后的段地址
如何确定哪些符号需要被重定向
链接规则
静态链接说的直白一点就是把多个文件(object file)合并为一个文件(excutable file),那么如何进行合并呢?
按需叠加
首先能想到的方案就是将输入的目标文件按照次序叠加起来,如图所示
这种方面在很多输入文件的情况下,输出文件将会有很多零散的段,比如一个APP可能会有近百个目标文件,每个文件都分别会有.text
.data
,那么最后输出文件将会有成百上千个零散的段,因为每个段都要有一定的对齐规则(下面会说明),这样会非常浪费空间。相似段合并
另一种方法是把具有相同性质的段合并到一起,如图
经过相似段的合并,极大限度的节约内存空间。下面让我们来验证下。
我们先来创建两个C的文件。分别为a.c 和 b.c
a.c代码如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16//引用外部符号
extern int shared;
//引用外部方法
void swap(int *a , int *b);
void test(){
printf("666");
}
int main(){
int a = 100;
//调用外部方法
swap(&a,&shared);
}b.c代码如下
1
2
3
4
5
int shared = 1;
void swap(int *a , int *b){
*a ^= *b ^= *a ^= *b;
}
下面我们来先把a.c编译为目标文件,关于目标文件和可执行文件的区别可以参考这里。
执行如下命令
1
clang -c a.c
此时会得到a.o文件,同理我们对b.c执行同样的操作。 此时我们将得到4个文件。
分别是:
- a.c(源文件)
- a.o(目标文件)
- b.c(源文件)
- b.o(目标文件)
接下来我们通过objdump -h a.o
来查看a.o中的内容。如图
关于各个段的含义,请参考machO
接下来把b.c同样通过objdump -h b.o
编译为目标文件。如图
然后进行连接clang a.o b.o -o ab
,文件夹下会多出ab的Unix可执行文件
。我们在使用objdump -h ab
,如图
我们看到链接之后的可执行文件,确实是采用了形似段合并的方式。
那么实现这种链接技术需要在链接之后怎么进行各个段的虚拟空间大小呢
段地址分配
我们先来解析下一些字段的含义
我们来分别获取a,b,ab的text段大小1
2
3a text(size) = 0x47
b text(size) = 0x2C
ab text(size) = 0x77
段空间分配的规则为 size(ab) = size(a)对齐 + size(b)。
我们把 a + b 也就是 0x47(16进制对齐为0x50) + 0x2C = 0x77。这里是需要最后一个目标文件的段是不需要对齐的,如果此时有a b c 3个目标文件,那么size(abc) = size(a)对齐 + size(b)对齐 + size(c)。
有兴趣的同学可以把链接顺序调换一下试试:clang b.o a.o -o ab
。
解析外部符号
还记得我们之前在a.c中的声明了一个外部变量和方法,那么在链接时,链接器是如何知道这两个符号要被替换(解析与重定位)呢?
重定位表与符号表
获取a.o的重定位表 otool -r a.o
获取a.o的符号表
计算方式为:重定位表地址 = 指令执行地址 + 指令本身的大小。
再根据重定位表的地址获得到 符号表的索引(下标)。
利用索引(下标)换算对应符号。
过程如图所示
关于shared 如何确定可以自己尝试计算,上图中 指令执行地址 + 指令本身的大小
的计算已经写出来了。
其他
其实在段地址确定之后,符号的地址也就可以确定了,我们可以借助machOView,来查看链接之后可执行文件。