目录
一.源文件到目标文件的转化过程
二.目标文件
三.链接器的主要任务
四.例题解析
一.源文件到目标文件的转化过程
(1)过程
预处理—编译—汇编—链接
(2)图示及解析
- 图示如下
预处理阶段:预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。结果就得到另外一个C程序,通常是以.i作为文件扩展名。即.c文件转化为.i文件的过程。
编译阶段:编译器(ccl)将文本文件.i翻译成文本文件.s,它包含一个汇编语言程序。即.i文件转化为.s文件的过程。
汇编阶段:汇编器(as)将.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序 的格式,并将结果保存在目标文件.o中,.o文件是一个二进制文件。即.s文件转化为.o文件的过程。
链接阶段:链接器(ld)将.o文件及一些必要的系统目标文件组合起来,创建一个可执行目标文件 ,可以被加载到内存中,由系统执行。
二.目标文件
1.目标文件的三种形式
- 可重定位目标文件。包含二进制代码和数据,其形式可以在编译时与其他可重定位目标文件合并起来,创建一个可执行目标文件。
- 可执行目标文件。包含二进制代码和数据,其形式可以被直接复制到内存并执行。
- 共享目标文件。一种特殊类型的可重定位目标文件,可以在加载或者运行时被动态地加载进内存并链接。
- 编译器和汇编器生成可重定位目标文件(包括共享目标文件)。
- 链接器生成可执行目标文件。
2.可重定位目标文件
- 上图为典型的ELF可重定位目标文件,夹在ELF头和节头部表之间的都是节,一个典型的ELF可重定位目标文件包含下面几个节:
- .text:已编译程序的机器代码。
- .rodata:只读数据,比如printf语句中的格式串和开关语句的跳转表。
- .data:已初始化的全局和静态C变量。局部C变量在运行时被保存在栈中,既不出现在.data节中,也不出现在.bss节中。
- .bss:未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量。
- .symtab:一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。
- .rel.text:一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。
- .rel.data:被模块引用或定义的所有全局变量的重定位信息。
- .debug:一个调试符号表,其条目是程序中定义的局部变量和类型定义,程序中定义和引用的全局变量,以及原始的C源文件。
- .line:原始C源程序中的行号和.text节中机器指令之间的映射。只有以-g选项调用编译器驱动程序时,才会得到这张表。
- .strtab:一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部中的节名字。字符串表就是以null结尾的字符串系列。
三.链接器的主要任务
- 为了构造可执行文件,链接器必须完成两个主要任务:符号解析、重定位。
1.符号解析
- 目标文件定义和引用符号,每个符号对应于一个函数、一个全局变量或一个静态变量(即C语言中任何以static属性声明的变量)。符号解析的目的是将每个符号引用 正好和一个符号定义 关联起来。
(1)符号和符号表
- 每个可重定位目标模块m都有一个符号表,它包含m定义和引用的符号信息。在链接器的上下文中,有三种不同的符号:
- 由模块m定义并能被其他模块引用的全局符号。 全局链接器符号对应于非静态C函数 和全局变量。
- 由其他模块定义并被模块m引用的全局符号。 其称为外部符号,对应于其他模块中定义的非静态C函数和全局变量。
- 只被模块m定义和引用的局部符号。 对应于带static属性的C函数和全局变量,这些符号在模块m中的任何位置都可见,但不能被其他模块引用。
(2)全局符号的强弱性
强符号:函数名和已初始化的全局变量名。
弱符号:未初始化的全局变量名。
(3)链接器对符号解析的规则
Rule1:不允许有多个同名的强符号
- 强符号只能被定义一次,否则链接错误。
Rule2:如果有一个强符号和多个弱符号同名,那么选择强符号。
- 对弱符号的引用被解析为其强符号的定义。
Rule3:如果有多个弱符号同名,那么从这些弱符号中任意选择一个。
- 使用gcc -fno-common链接时,会告诉链接器在遇到多个弱定义的全局符号时输出一条警告信息。
(4)多重定义全局符号的问题
- 尽量避免使用全局变量
- 一定需要用的话,请按以下规则使用
尽量使用本地变量(static)
全局变量要赋初值
外部全局变量要使用extern
2.重定位
- 编译器和汇编器生成从地址0开始的代码和数据节。 链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使它们指向这个内存位置。链接器使用汇编器产生的重定位条目的详细指令,不加甄别地执行这样地重定位。
重定位的过程
- 合并相同的节
– 将集合E的所有目标模块中相同的节合并成新节,例如所有.text节合并作为可执行文件中的.text节 - 对定义符号进行重定位(确定地址)
– 确定新节中所有定义符号在虚拟地址空间中的地址
– 完成这一步后,每条指令和每个全局或局部变量都可确定地址 - 对引用符号进行重定位(确定地址)
– 修改.text和.data节中对每个符号的引用(地址),需要用到在.rel.data和.rel.text节中保存的重定位信息。
四.例题解析
1.
解析:选B。xp是符号的定义,x是符号的引用。该题主要是考查符号的定义和引用。
解析:选A。该题中变量x在m1.c中为强符号,在m2.c中为弱符号。在调用p1函数后,x处原来存放的100被替换,-1.0的double类型表示为1 0111 1111 111 00…0,十六进制表示为BFF0 0000 0000 0000。因为x、y和z都是初始化变量,同在.data节中,链接后空间被分配在一起,x占4B,随后y和z各占2B。因为IA-32为小端方式,所以,x的机器数为全0,y的机器数也为全0,z的机器数为BFF0H。执行printf函数后x=0, z=-(214+24)=-16400。
解析:选D。重定位最后一步是对引用处的地址进行重定位,重定位的方式有多种,只有绝对地址方式才是将引用处的地址修改为与之关联(绑定)的定义处的首地址,而对于其他重定位方式,就不一定是这样,例如,对于PC相对地址方式,引用处填写的是一个相对地址。
解析:选D。A、因为“int *bufp1 = &buf[1];”是一个声明,也即是对变量bufp1的数据类型的定义和初始化,因此这个需要重定位的初始化值将被存储在.date节中,因而重定位条目在.rel.data节中,并且是绑定buf的一个引用,即引用buf的一个重定位条目。 B、因为buf有2个数组元素,每个元素占4B,因此bufp1的地址为0x8048930+8=0x8048938,重定位时与引用绑定的符号是buf,即绑定的是&buf[0],而真正赋给bufp1的是&buf[1],引用的地址和绑定的地址相差4,所以重定位前的内容为十六进制数04 00 00 00。 C、可执行文件已经进行了重定位,所以,bufp1所在的地址0x8048938处,应该是重定位后的值,显然应该是buf[1]的地址。重定位时通过初始值加上buf的值得到,即4+0x8048930=0x8048934,小端方式下,4个字节分别为34H、89H、04H、08H。 D、在重定位条目中只有对buf的引用,没有对bufp1的引用,这里bufp1是一个定义。