前面一个篇章介绍了ELF文件格式,目前存在的问题是当我们有两个目标文件的时候编译器如何将它们链接起来,然后符号怎么样转化为最终的虚拟地址,而这就是静态链接
- 静态链接:指把要调用的函数或者过程链接到可执行文件中,成为可执行文件的一部分
- 动态链接:所调用的函数代码并没有被拷贝到应用程序的可执行文件中去,而是仅仅在其中加入了所调用函数的描述信息
空间与地址分配
例如有如下两个c源程序
//1.c
extern int shared;
int main(){
int a = 100;
swap(&a, &shared);
}
//2.c
int shared = 1;
void swap(int *a, int *b){
*a ^= *b ^= *a ^= *b;
}
当使用gcc -c 1.c 2.c的时候分别生成了对应的目标文件,也就是说我们最后要生成一个最终的可执行文件,那么这就有第一个问题对于多个输入目标文件,链接器如何将它们各个段合并到输出文件,有两种方式:按序叠加和相似段合并
按序叠加的方案很简单,就是直接将各个目标文件依次合并,例如如下图
这样做的缺点是输出文件会有很多零散的段,这种做法浪费空间,因为操作系统要求每一个段有一定的地址和空间对其,例如一个页的大小如果为4096字节,那么即使这个段只有1字节那么也要浪费4096字节的空间
相似段合并也很好理解,例如1.c中的.text段和2.c中的.text段合并到最终目标文件的.text段其他段也类似,就如下图
这里也有一个问题,.bss段存放的是未初始化的变量在目标文件中是不占空间的,但是在链接成可执行文件的时候,要将.bss段合并并且分配虚拟空间,注意这里提到的虚拟空间是指操作系统上的虚拟地址空间,通过这个虚拟地址操作系统可以通过地址转换成最终的物理空间,而不是像之前所述的在ELF文件中的地址
现在的链接器基本都采用相似段合并的方法,使用这种方法的链接器一般都使用两步链接的方法:
- 空间与地址分配,扫描所有的输入目标文件,斌并且获取各个段的长度、属性和位置,并且将输入目标文件中的符号表中的符号定义和符号引用收集起来,统一放到一个全局符号表
- 符合解析和重定位,使用第一步收集到的信息,读取输入文件中段的数据、重定位信息,并且进行符号解析与重定位、调整代码中的地址等
例如上面的两个目标文件连接之后,查看各个段的地址
那么根据上面的一些地址,在最终的可执行文件中,LMA便是虚拟地址,可以画出下面的各个段合并的示意图
当完成相似段合并之后,第一个步骤还有很重要的任务,计算各个符号的虚拟地址,在1.c中具有一个全局符号main,而2.c中具有两个全局符号swap和shared,使用readelf查看main相对于1.o的偏移量为0,而12可执行文件的.text段的虚拟开始地址是0x004000e8,所以main符号的虚拟地址是0x004000e8,同样的可以计算其他的虚拟地址,这点可以查看12可执行文件的符号表证实
符号解析与重定位
在完成空间和地址的分配步骤之后,链接器就进入到了符号解析与重定位的步骤,这也是核心的内容,要搞清楚地是如何将符号进行解析与将符号正确的地址进行重定位
当程序源码文件变成目标文件的时候,例如1.c编译成目标文件的时候,编译器并不知道shared和swap的地址,因为它们定义在其他目标文件中,所以编译器暂时的把地址0看做是这些符号的地址
经过编译器符号解析与重定位之后获取到了符号真实的地址,在将它填充到最后的可执行文件中
这里还涉及到指令修正问题,指令后面的地址如何计算这个不重点介绍
那么链接器是如何知道哪些指令是需要被调整的呢?然后这些指令的哪些部分需要被调整?怎么被调整?这就是需要一个重定位表。重定位表也叫重定位段其实就是一个段,如果.data有要被重定位的地方那么会有一个相应的叫.rel.text的段保存了代码段的重定位表,同样的如果.text有需要重定位的地方,那么相应的会有一个.rel.text的段
同样的在elf.h头文件下同样存在一个结构Elf64_Rel存储着重定位表的各个字段
typedef struct
{
Elf64_Addr r_offset; /* Address */
Elf64_Xword r_info; /* Relocation type and symbol index */
} Elf64_Rel;