从.c文件到 可执行 文件需要经历的过程
static linking
静态链接器的输入是一组可重定向文件,输出一个全链接的可执行目标文件。
目标文件有三种形式:
1. 可重定向目标文件:包含二进制代码和数据,可以在编译时和其他可重定向目标文件一起,得到可执行文件。
2. 可执行目标文件:包含二进制代码和数据,可以直接拷贝进内存进行执行。
3. 共享目标文件:一种特殊的重定向文件,可以被加载进内存,并在加载和运行期间进行动态链接。
编译器和汇编器产生可重定向目标文件(包括共享目标文件)
链接器产生可执行文件。
链接器的主要任务:
1. 符号解析
2. 重定向
ELF(Executable and Linkable format)
ELF header: 16 bytes 序列 描述字长和系统中产生字节的顺序(大端还是小端)。其他的信息还有header的大小,目标文件的类型,机器类型,section header table的偏移及其中包含的entries的数量和大小。
对于每个section, section header table 包含一个固定大小的entry, 其中包含相应section位置和大小的信息。
下面给出一段代码和相应的ELF.
#include<stdio.h>
#include "vector.h"
int x[2] = {1,2};
int y[2] = {3,4};
int z[2];
int main(){
addvec(x,y,z,2);
printf("z = [%d,%d]\n",z[0],z[1]);
return 0;
}
.text: 编译程序的机器码
.rodata: 只读数据。switch的跳转表之类。
.data: 初始化的全局变量和静态变量。
.bss: 未初始化的全局数据,没有实际的空间分配,只起占位作用。
.symtab: 符号表,包含函数和全局变量的信息。
.rel.text: .text中需要在重定向时修改的位置信息。例如,调用外部函数或引用的指令,都需要在重定向时写入相应函数的地址。
rel.data: 例如,一个初始化的全局变量引用一个外部的地址或外部的函数,都需要在重定向时修改。
.debug: 一个进行调试时使用的表,存放局部变量和typedef, 全局变量,程序中引用的变量,原始的c代码的文件。
.line: 原始C代码到机器码的映射。
符号表和符号
连接器的上下文中包含三种符号:
在模块中定义,可以被其他模块使用的全局符号。包含nonstatic C function 和不带static的全局变量
在模块中引用,但是在外部模块中定义的符号。通常是带extern关键字的函数或变量。
符号解析
符号解析分两种,一种是局部符号解析,只需在对应的模块中进行;另一种是全局符号的解析。例如上面函数用到的printf()就属于全局符号。这些符号往往是undef的类型。因为编译器在产生符号表的过程中如果遇到相应模块中未定义的符号,就会假设该符号在别的模块中定义,并把解析的任务交给链接器进行。
在解析全局符号的过程中,可能会遇到符号在多个目标文件中定义的问题,链接器的处理遵循下面的准则:
rule 1: multiple strong symbols are not allowed.
rule 2: given a strong symbol and multiple weak symbols, choose the strong symbol.
rule 3: given multiple weak symbols, choose any of the weak symbols.
下面举例阐明rule 1:
//fool.c
int main(){
return 0;
}
//bar1.c
int main(){
return 0;
}
编译结果如下:
下面是rule2的例子:
//foo2.c
#include<stdio.h>
void foo(void);
int x = 15213;
int main(){
f();
printf("x = %d\n",x);
return 0;
}
//bar2.c
int x;
void f(){
x = 15212;
}
运行结果如下:
rule 3举例:
//foo3.c
#include<stdio.h>
void f(void);
int x = 15213;
int y = 15212;
int main(){
f();
printf("x = 0x%x y = 0x%x \n",x,y);
return 0;
}
//bar3.c
double x;
void f(){
x = -0.0;
}
运行结果如下:
解释:在foo3.c符号表中,x 和 y是.data section中连续的两个变量。在调用f()的时候,由于有rule3, 所以操作的变量是foo3.c中的x, 而非bar3.c中的x. 因此,复制会导致将x 和 y的数据域覆盖。
静态链接库
静态链接库的实现如下:
将每一个函数都分别编译成各自名字命名的.o文件,将所有的.o文件打包,放入一个archive的文件中,就是所谓的静态链接库.a形式。
archive中包含各个函数的信息,以便调用时能快速找到相应的模块。
使用静态库进行解析的过程
在编译完main函数后,符号表中会出现很多undef的符号,记为U. D 表示已经定义的符号, E表示一组可重定向的文件, 是链接时需要merge的静态库文件。
执行的顺序如下:
按照给出的静态库的顺序扫描;
扫描一个静态库,看有没有匹配U中的符号,如果匹配, 则将该符号放入D中,修改相关信息。将链接的该块的无法解析的符号加入U中。
这样存在的最大问题是循环依赖。因此链接的顺序是很重要的。