C语言程序的编译和链接过程
1.程序的编译
一般而言,大多数编译系统都提供编译驱动程序(complier driver),根据用户需求调用语言预处理器,编译器,汇编器和链接器.例如有如下历程:
//main.c
void swap();
int buf[2]={1, 2};
int main()
{
swap();
return 0;
}
//swap.c
int *bufp0 = &buf[0]
int *bufp1;
void swap()
{
int temp;
bufp1 = &buf[1];
temp = *bufp0;
*bufp0 = *bufp1;
*bufp1 = temp;
}
驱动程序首先运行C预处理器(cpp),它将C的源程序main.c翻译成一个ASCII码的中间文件main.i.接下来,驱动程序运行C编译器(ccl),将main.i翻译成一个ASCII汇编语言文件main.s.然后,驱动程序运行汇编器(as),它将main.s翻译成一个可重定位的目标文件main.o.具体过程如下图所示:
2.链接
链接就是将不同部分的代码和数据收集和组合成为一个单一文件的过程,这个文件可被加载或拷贝到存储器执行.
链接可以执行与编译时(源代码被翻译成机器代码时),也可以执行与加载时(在程序被加载器加载到存储器并执行时),甚至执行与运行时,由应用程序来执行.在现代系统中,链接是由链接器自动执行的.
链接器分为:静态链接器和动态链接器两种.
2.1.静态链接器
静态链接器以一组可重定位目标文件和命令行参数作为输入,生成一个完全链接的可以加载和运行的可执行目标文件作为输出.
静态链接器主要完成两个任务:
1>符号解析:目标文件定义和引用符号.符号解析的目的在于将每个符号引用和一个符号定义联系起来.
2>重定位:编译器和汇编器生成从地址零开始的代码和数据节.链接器通过把每个符号定义和一个存储器位置联系起来,然后修改所有对这些符号的引用,使得他们执行这个存储位置,从而重定位这些节.
目标文件:
目标文件有三种形式:
1>可重定位的目标文件:
包含二进制代码和数据,其形式可以再编译时与其他可定位目标文件合并起来,创建一个可执行目标文件.
2>可执行目标文件:
包含二进制代码和数据,其形式可以被直接拷贝到存储器并执行.
3>共享目标文件:
一种特殊的可重定位目标文件,可以再加载或运行时,被动态地夹在到存储器并执行.
编译器和汇编器生成可重定位目标文件(包括共享目标文件),链接器生成可执行目标文件.
可重定位目标文件:
EF头L以一个16字节的序列开始,这个序列描述了字的大小和生成该文件的系统字节顺序.ELF头剩下的部分包含帮助链接器解析和解释目标文件的信息.其中包括ELF头的大小,目标文件的类型(比如,可重定位,可执行,共享目标文件),机器类型,节头部表的文件偏移,以及节头部表中的表目大小和数量.不同节的位置和大小是节头部表描述的,其中目标文件中的每个节都有一个固定大小的表目.ELF格式的可重定位目标文件结构如下图:
.text:已编译程序的机器代码
.rodata:只读数据
.data:已初始化的全局C变量
.bss:未初始化的全局C变量.在目标文件中这个节不占实际空间,仅是一个占位符.
.sysmtab:一个符号表,存放在程序中被定义和引用的函数和全局变量的信息.
.rel.text:当链接器把这个目标文件和其他文件结合时,.text节中的许多位置都需要修改.一般而言,任何调用外部函数或者引用全局变量的指令都要修改.另一个方面,调用本地函数的指令则不需要修改.
.rel.data:被模块定义或引用的任何全局变量的信息.
.debug:一个调试符号表
.line:原始C源程序中的行号和.text节中机器指令之间的映射.
.strtab:一个字符串表,其中内容包括.symtab和.debug节中的符号表,以及节头部中的节名字.
符号和符号表
每个可重定位目标模块m都有一个符号表,它包含m所定义和引用的符号的信息.在链接器上下文中,有三种不同的符号:
1>由m定义并能被其他模块引用的全局符号.全局链接器符号对应于非静态的C函数以及被定义为不带C的static属性的全局变量.
2>由其他模块定义并被模块m引用的全局符号.这些符号成为外部符号,对应于定义在其他模块中的C函数和变量.
3>只被模块m定义和引用的本地符号.有的本地符号链接器符号对应于带static属性的C函数和全局变量.这些符号在模块m中的任何地方都可见,但是不能被其他模块引用.目标文件中对应于模块m的节和相应的源文件的名字也能获得本地符号.
符号表式有汇编器构造的,使用编译器输出到汇编语言.s文件中的符号.sysmab节中包含ELF符号表.这张符号表包含一个关于表目的数组.表目的格式如下:
typedef struct{
int name; //string table offset
int value; //section offset, or VM address
int size; //object size in bytes
char type:4, //data, func, section, or src file
binding:4; //local or global
char reserved; //unused
char section; //section header index, ABS, UNDEF, or COMMON
}Elf_Symbol;
2.1.1符号解析
链接器解析符号引用的方法是将每个引用和它输入的可重定位目标文件按的符号表中的一个确定的符号定义联系起来.
对于那些和引用定义在相同模块的本地符号的引用,符号解析式非常简单明了的.编译器只允许每个模块中的每个本地符号只有一个定义.编译器还确保静态本地变量,它们会有本地链接器符号,拥有唯一的名字.
对于全局符号的引用解析,当编译器遇到一个不是在当前模块中定义的符号(变量或函数名)时,它会假设该符号式在其他某个模块中定义的,生成一个链接器符号表表目,并把它交给链接器处理.如果链接器在它的任何输入模块中都找不到这个被引用的符号,它就输出一条错误信息并终止.
在编译时,编译器输出的每个全局符号给汇编器,或者是强,或者是弱,而汇编器把这个信息隐含地编码在可重定位目标文件的符号表中.函数和以初始化的全局变量是强符号,未初始化的全局变量是弱符号.
根据符号的强弱,有如下规则:
1>不允许有多个强符号
2>如果有一个强符号和多个弱符号,则选择强符号
3>如果有多个弱符号,则任选一个弱符号
与静态库链接
所有编译系统都提供一种机制,将所有相关的目标模块打包为一个单独的文件,称为静态库,它可以用做链接器的输入.当链接器构造一个输出的可执行文件时,它只拷贝静态库里被应用程序引用的目标模块.
在unix系统中,静态库以一种称为存档的特殊文件格式存放在磁盘中.存档文件是一组连接起来的可重定位目标文件的集合,有一个头部描述每个成员目标文件的大小和位置.
链接器如何使用静态库来解析引用
在符号解析阶段,链接器从左到右按照它们在编译驱动程序命令行上出现的相同顺序来扫描可重定位目标文件和存档文件.在这次扫描中,链接器位置一个可重定位目标文件集合E,这个集合中的文件会被合并起来形成可执行文件,和一个未解析的符号集合U,以及一个在前面输入文件中已定义的符号结合D.初始时,E,U,D都是空的.
1>对于命令行上的每个输入文件f,链接器会判断f是一个目标文件还是一个存档文件.如果是一个目标文件,那么链接器把f添加到E,修改U和D来反映f中的符号定义和引用,并继续下一个输入文件.
2>如果f是一个存档文件,那么链接器就尝试匹配U中未解析的符号由存档文件成员定义的符号.如果某个存档文件成员m,定义了一个符号来解析U中的一个引用,那么就将m加到E中,并且链接器修改U和D来反映m中的符号定义和引用.对存档文件中的所有成员目标文件都反复进行这个过程,知道U和D都不再发生变化.在此时,任何不包含在E中的成员目标文件都会被丢弃,而链接器将继续到下一个输入文件.
3>如果当链接器完成对输入命令行的扫描后,U是非空的,那么链接器就会输出一个错误并终止.否则,它会合并重定位E中的目标文件,从而构建输出的可执行文件.
这种方式,导致了在输入命令时要考虑到,静态库和目标文件的位置,库文件放在目标文件的后面,如果库文件之间有引用关系,则被引用的库放在后面.
2.1.2重定位
当链接器完成了符号解析这一步时,它就把代码中的每个符号引用和确定的一个符号定义(也就是,它的一个输入目标模块中的一个符号表表目)联系起来.此时,链接器就知道它的输入目标模块中的代码节和数据解的确切大小.然后就开始重定位步骤.重定位由两步组成:
1>重定位节和符号定义:
在这一步中,链接器将所有相同类型的节合并为一个新的聚合节.然后,链接器将运行时存储器地址赋值给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号.当这一步完成时,程序中的每个指令和全局变量都一个唯一的运行时存储器地址.
2>重定位节中的符号引用:
在这一步中,链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址.为了执行这一步,链接器依赖于称为重定位表目的可重定位目标模块中的数据结构.
重定位表目:
当汇编器生成一个目标模块时,它并不知道数据和代码最终将存放在存储器中的什么位置.它也不知道这个模块引用的任何外部定义的函数或者全局变量的位置.所以,无论何时汇编器遇到对最终位置未知的目标引用,它就会生成一个重定位表目,告诉链接器在将目标文件合并为可执行文件时,如何修改这个引用.代码的重定位表目放在.rel.text中.已初始化数据的重定位表目放在rel.data中.
ELF重定位表目的格式如下:
typedef struct{
int offset; //offset of the reference to relocate
int symbol:24, //symbol the reference point to
type:8; //relocation type
} Elf32_Rel;
ELF定义了11中不同的重定位类型,其中最基本的两种重定位类型是:R_386_PC32(重定位一个使用32PC相关的地址引用)和R_386_32(重定位一个使用32位绝对地址的引用).
2.2.动态链接器
共享库是一个目标模块,在运行时,可以加载到任意的存储器地址,并在存储器中和一个程序链接起来.这个过程称为动态链接,是由动态链接器完成的.
共享库的共享在两个方面有所不同.首先,在任何给定的文件系统中,对于一个库只有一个.so文件.所有引用该库德可执行目标文件共享这个.so文件中的代码和数据,而不是像静态库德内容那样被拷贝和嵌入到引用它们的可执行的文件中.其次,在存储器中,一个共享库的.text节只有一个副本可以被不同的正在运行的进程共享.