转自:博客1, http://chuquan.me/2018/06/03/linking-static-linking-dynamic-linking/
1.为什么需要重定位?
执行函数:
程序的运行过程就是CPU不断的从内存中取出指令然后执行的过程。
假设对g_num做++操作,那么汇编指令:
call 0x4004fd #地址为函数add在内存中的地址,调用时会跳转到0x4004fd来执行指令函数add对应的机器指令 mov 0x400fda %eax #将0x400fda地址的内容放到eax寄存器中,g_num的地址就是0x400fda add $0x1 %eax #将寄存器内容+1
那么地址从哪里获取?确定程序运行时地址的过程就是 重定位(Relocation)。
2.编译为 .o文件的过程
目标文件.o包括:
- 数据段:全局变量的定义
- 代码段:所有函数的定义
- 符号表:对外部变量的引用
编译过程可以确定:
- 定义在该源文件中函数的内存地址
- 定义在该源文件中全局变量的内存地址
//都是针对本文件内部的相对地址
而如果针对引用其他文件中定义的变量或函数,那么编译时就将调用的地址设置为空,并且生成一条记录,之后的任务交给链接器。
例如:
// a.c extern int shared;//引用了其他文件中定义的全局变量 int main() { int a = 100; swap(&a, &shared);//引用了其他文件中定义的函数 }
// b.c int shared = 1; void swap(int *a, int *b) { *a ^= *b ^= *a ^= *b; }
那么在编译为汇编文件.i时,调用其他文件中的函数,汇编指令为空地址,表示不确定:
call 0x000000
并且生成一条记录放在.rel.text中,对外部定义的全局变量的使用,放在.rel.text中,链接器根据上述两者内容填写空白位置。
.o文件内容
对外部函数的引用放在.rel.text中,对外部全局变量的引用放在.rel.data段中。
编译过程确定了该源文件中函数和变量的相对地址,以及引用其他文件的相对地址,需要链接时确定的信息放在.rel.data/.rel.text中,以修正地址。
3.链接器的工作
3.1 重定位第一个过程:链接合并段
链接器会将所有需要的目标文件.o合并,即将对应段合并,这样就能确定相对地址,以更新目标文件中的相对地址。
上图中,蓝色部分表示为代码段,依次合并;
黄色是数据段,主要观察数据段的相对地址变化,目标文件2经过合并更新相对地址后是6,目标文件3经过合并更新相对地址后是8.
相对地址 + offset(偏移) = 最终内存地址
在合并时会产生偏移。段的偏移只有在链接的时候才能确定,因为连接时才会发生段的合并,确定最终地址。编译过程是无法确定的。
3.2 引用符号重定位
已经确定了相对地址和偏移,确定了变量和函数的最终内存地址,所以就可以更新.rel.data/.ref.text的最终值。
其中rel是Relocation Table的缩写,保存着需要重定位的符号,即重定位表。
4.#include的所有都会被包含?
可以认为在静态编译时,形成可执行文件时,会将头文件中的函数也一并有个副本,进行段合并,在代码段合并,共同生成可执行文件。
即每生成一个可执行文件,就会有一份函数的拷贝,在调用时,比如说scanf这样,直接call 最终内存地址即可。