编译器用于为源程序文件产生对应的二进制代码和数据目标文件。
链接程序用于对相关的所有目标文件进行组合处理,形成一个可被内核加载的目标文件(即可执行文件)。
上述涉及的两种目标文件统一称为目标文件。本文主要分析目标文件的格式及其组成。并简要分析目标文件的链接过程。
1 目标文件格式
GNU gcc 或 gas 编译输出的目标模块文件和链接程序所生成的可执行文件都是用了 UNIX 传统的 a.out 格式。这是一种被称为汇编与链接输出的目标文件格式。
![](https://i-blog.csdnimg.cn/blog_migrate/3dc0c7c1fe9fa2678c4eaa2e81861503.png)
1.1 执行头部分(exec header)
执行头部分中含有一些参数(exec 结构)是有关目标文件的整体结构信息。例如代码和数据区的长度、未初始化数据区的长度、对应源程序文件以及目标文件创建时间等。对于可执行文件,内核借助这些信息将该文件加载到内核;对于非可执行目标文件,连接程序借助这些信息将目标文件链接组合成一个大的模块或可执行文件。
目标文件的头中包含一个长度为 32 个字节的 exec 数据结构,通常称为文件头结构或执行头结构,其具体定义如下:
struct exec{
unsigned long a_magic, //执行文件魔数,使用 N_MAGIC 等宏访问
unsigned a_text , //代码段长度 byte
unsigned a_data, //数据区长度 byte
unsigned a_bss, //文件中未初始化数据区长度
unsigned a_syms, //文件中符号表长度
unsigned a_entry, //执行地址入口
unsigned a_trsize, //代码重定位信息长度
unsigned a_drsize //数据重定位信息长度
};
其中魔数的值可以将a.out 文件分成以下几种类型;
- OMAGIC(old magic) 它指明文件是目标文件或者是不纯的可执行文件,其魔数是 0x107。其正文区紧跟此结构头,即从32个字节开始。
- ZMAGIC 指明文件为需求分页处理(demang-paging 即按需加载 load on demand)的可执行文件,其魔数 0x10b。其正文从1024个字节开始,头与正文之间的预留区初始化为0。
其中 a_bss 所指定长度的 bss 区,在 a.out 文件中实际不占用空间,因为 bss 区数据值为 0,而内存申请时也被初始化为 0,故无需再 a.out 中保存。
a_entry 字段指定了程序代码开始执行的位置,对于非可执行目标文件为 0。a_trsize 和 a_drsize 对于可执行文件并不需要,用于链接生成可执行文件时使用。a_syms 用于需要保留符号信息,主要用于调试。
1.2 重定位信息部分
这些部分包含了供链接程序使用的记录数据,在组合目标模块时用于定位代码段(数据段)中的指针或地址。重定位项的主要功能有两方面:一是当代码段被重定位到一个不同的基地址处时,重定位项则用于之处需要修改的地方;二是在模块文件中存在对未定义符号引用时,当此未定义符号最终被定义时链接程序可以使用相应的重定位项对符号的值进行修正。其重定位记录项构成如下:
struct relocation_info
{
int r_address; //段内需要重定位的地址
unsigned int r_symbolnum:24; //含义与r_extern 有关。指定符号表中一个符号或者一个段
unsigned int r_pcrel:1; //1 bit, PC相关标志
unsigned int r_length:2; //2 bits 指定要被重定位字段的长度(2的次方)
unsigned int r_extern:1; //外部标识位,1--以符号的值重定位,0--以段的地址重定位
unsigned int r_pad:4; //填充位
};
由此重定位记录项的结构可以看出,每个记录项含有模块文件代码区(代码段)和数据区(数据段)中需要需要重定位的地址(此地址长度为 4 个字节,r_address) ,以及规定如何具体进行重定位操作的信息。
- r_address 指此可重定位项从所在代码段或数据段开始地址算起的偏移值。
- r_length 表示所指定的重定位项从 r_address 其所占字节数, 可能为1、2、4、8。
- r_extern 控制着 r_sysmbolnum 的含义,当r_extern = 0 时,表示对代码段重定位,r_sysmbolnum 字段用来指定该重定位项具体位于哪个代码段(数据段);当 r_extern = 1 时,则表示该重定位项是对一个外部符号的引用,此时 r_sysmbolnum 指定目标文件中符号表中的一个符号,需要使用符号的值进行重定位。
1.3 符号表和字符串部分
符号表同样含有供链接程序使用的记录数据。这些记录数据保存着模块文件中定义的全局符号以及需要从其他模块文件中输入的符号,或者是链接器定义的符号,用于在模块文件之间对命名的变量和符号之间的交叉引用。字符串标中含有与符号名相对应的符号名。用于调试程序,与链接过程无关,例如行号、源代码、局部符号以及数据结构描述详细信息等。
符号表记录项的结构如下:
struct nlist
{
union{
char *n_name; //字符串指针
struct nlist *n_next; //或者是指向另一个符号项结构的指针
long n_strx; //或者是符号名称在字符串表中的字节偏移值
}n_un;
unsigned char n_type; //该字节分成3个字段
char n_other; //通常不用
short n_desc;
unsigned long n_value; //符号值
};
由于 GNU gcc 编译器允许任意长度的标识符,因此标识符字符串都位于符号表后的字符串表中。类型字段 n_type 指明了符号的类型,该字段的最后一个 bit 用于指明符号是否是外部的(全局的)。如果该位为 1 的话,那么说明该符号是一个全局符号。链接程序并不需要局部符号信息,但可供调试程序使用。n_type 字段的其余 bit 用来指明符号类型。符号的主要类型包括:
- text、data 或 bbs 指明是本模块文件中定义的符号,此时符号的值是模块中改符号的可重定位地址。
- abs 指明符号是一个绝对的(固定的)不可重定位的符号,符号的值就是该固定值。
- undef 指明是一个本模块文件中未定义的符号。此时符号的值通常是 0。
2 a.out 可执行文件在进程逻辑地址空间的分布
Linux 0.11 系统中进程的逻辑空间大小是 64 MB。对于 ZMAGIC 类型的 a.out 执行文件,它的代码区长度是内存页面的整数倍。由于 Linux 内核使用按需分页(demand-paging)技术,即在一页代码实际使用的时候才被加载到物理页面中,而在进行加载操作的 fs/execve()函数中仅仅为其设置了分页机制的页目录项和页表项,因此按需分页技术可以加快程序的加载速度。
![](https://i-blog.csdnimg.cn/blog_migrate/f2160866bb07fb47e0439b9d273ce995.png)
3 链接程序输出
链接程序对输入的一个或多个模块文件以及相关库函数模块进行处理,最终生成相应的二进制文件或是一个所有模块组合而成的大模块文件。在这个过程中,大致分为以下几个步骤:
- 链接程序的首要任务时给执行文件(或输出的模块文件)进行存储空间分配操作。一旦存储位置确定,链接程序就可以继续执行符号绑定和代码修正操作。
-
第二个任务是把所有模块中相同类型的段组合连接在一起,在输出文件中形成一个相应类型的汇总段。
段合并的一般顺序是先正文段、数据段、未初始化段bss。其中段开始位置为页对齐,段内为字对齐。
4 可执行文件的加载
Linux 系统加载一个可执行文件时,linux 会根据文件头部结构中的信息首先判断文件是否是一个合适的可执行文件,即其魔数是否为 ZMAGIC。然后系统在用户态堆栈顶部为程序设置环境和命令行上输入参数信息并为其构建一个任务数据结构。接着在设置了一些相关寄存器值后利用堆栈返回技术去执行程序。执行程序映像文件中的代码和数据将会在实际执行到或用到时利用需求加载技术(load on demand)动态加载到内存中。
linux 系统内核加载时,内核是由引导启动程序利用 ROM BIOS 中断调用加载到内存中,因此编译产生的内核各模块中的执行头结构部分需要去掉。