本文主要阐述 Linux 的目标文件(有可重定位目标文件
、可执行目标文件
和共享目标文件三种形式),并把重点放在其格式和案例分析上。
注:一般情况下,我们说的目标文件专指可重定位目标文件,而可执行文件专指可执行目标文件,但在本文中,为了使概念更加清晰,我们会使用这两种文件的全称。
Linux 目标文件
按照《程序员的自我修养——链接、装载与库》一书第 3.1 节的说法,
从广义上看,目标文件与可执行文件的格式几乎是一模一样的,所以我们可以广义地将目标文件与可执行文件看成是一种类型的文件。
即把它们统称为 ELF(Executable Linkable Format)文件。
实际上,可重定位目标文件跟可执行目标文件还是有区别的。目标文件有三种形式:
- 可重定位目标文件(
.o
文件),包含二进制代码和数据,其形式可以在编译时与其他可重定位目标文件合并起来,创建一个可执行目标文件; - 可执行目标文件包含二进制代码和数据,其形式可以被直接拷贝到存储器并执行。
- [仅为完整性,本文不阐述]共享目标文件是一种特殊类型的可重定位目标文件,可以在加载或者运行时被动态地加载到存储器并链接。
我们先来看一下可重定位目标文件和可执行目标文件生成的过程。
可重定位目标文件和可执行目标文件生成的过程
源文件经过以下几步生成可重定位目标文件和可执行目标文件:
- 预处理(preprocessor):对 #include、#define、#ifdef/#endif、#ifndef/#endif 等宏进行处理
- 编译(compiler):将源码编译为汇编代码
- 汇编(assembler):将汇编代码汇编为可重定位目标代码(文件)
- 链接(linker):将可重定位目标代码链接为可执行目标文件
整个过程可用图表示如下:
图片来源
接下来,我们分开对可重定位目标文件与可执行目标文件进行阐述。《深入理解计算机系统》一书第 7 章“链接”和《程序员的自我修养——链接、装载与库》一书第 3 章“目标文件里有什么”将作为主要参考资料。
可重定位目标文件
可重定位目标文件格式
可重定位目标文件的典型格式如下图所示:
图片来源:《深入理解计算机系统》
其中,
ELF Header
: 包含了描述整个文件的基本属性,如 ELF 文件版本、目标机器型号、程序入口地址等。.text
:已编译程序机器(二进制)代码。.rodata
:只读数据,如 printf 语句中的格式串和开关语句的跳转表。.data
:已初始化的全局(静态)变量或静态变量。但如果我们对全局变量或静态变量赋值为 0,那它会被放到 .bss 段。 (注意全局变量和全局静态变量的区别:作用域。).bss
:未初始化的全局变量或静态变量。该段不占用可重定位目标文件的实际空间,但当它被加载到内存中时,该段内的变量是要占用内存的。.symtab
:一个符号表,它存放在程序中定义和使用的函数和全局变量的信息,但不包含局部变量的条目。.rel.text
:一个 .text 节中位置的列表,当链接器把这个可重定位文件和其他文件结合时,需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。另一方面,调用本地函数的指令不用修改。注意,可执行目标文件中并不需要重定位信息,因此通常省略,除非用户显式地指示链接器包含这些信息。.ret.data
:被模块引用或定义的任何全局变量的重定位信息。一般而言,任何已初始化的全局变量,如果它的初始值是一个全局变量地址或者外部定义函数的地址,都需要被修改。.debug
:一个调试符号表,其条目是程序中定义的局部变量和类型定义、程序中定义和引用的全局变量,以及原始的 C/C++ 源文件。只有以 -g 选项调用编译驱动程序时才会得到这个表。.line
:原始 C/C++ 源程序中的行号和 .text 节中机器指令之间的映射。只有以 -g 选项调用编译驱动程序时才会得到这个表。.strtab
:一个字符串表,其内容包括 .symtab 和 .debug 节中的符号表,以及节头部中的节名字。字符串表就是以 null 结尾的字符串序列。Section Header Table
:描述了 ELF 文件包含的所有段的信息,如每个段的段名、段的长度、在文件中的偏移、读写权限及段的其他属性。
案例分析
接下来,我们利用《程序员的自我修养——链接、装载与库》一书第 3 章提供的一个样例来分析下这些段。
编译样例
样例如下:
1 | /************************************* |
编译该程序可得可重定位目标文件 SimpleSection.o:
1 | $ gcc -c SimpleSection.c |
查看 ELF Header
我们可以查看该目标文件的 ELF Header:
1 | $ readelf -h SimpleSection.o |
并得到如下结果:
我们可以看到,ELF Header 定义了诸多该目标文件的属性。关于各个属性详细信息请参考《程序员的自我修养——链接、装载与库》一书第 3.4.1 节“文件头”。
查看 Section Header Table
我们还可以继续查看一下该目标文件的 Section Header Table:
1 | $ readelf -S SimpleSection.o |
并得到如下结果:
结合该图和上边关于 ELF Header 的图,我们可以将 SimpleSection.o 的 Section Header Table 及所有段的位置和长度画出来:
正如我们在前边提到的,Section Header Table 描述了 ELF 文件包含的所有段的信息,如每个段的段名、段的长度、在文件中的偏移、读写权限及段的其他属性。具体地,它是利用了Elf32_Shdr (也称为段描述符)
这样一个结构体来描述每一段的属性,该结构体长度为 40 个字节。在 SimpleSection.o 文件中总共有 13 个段,也就需要 13 个Elf32_Shdr
来描述它们,总共长度为 13 * 40 = 0x208 个字节。另外,Section Header Table 的第一个元素是无效的Elf32_Shdr
,它的类型为 NULL。
整个 SimpleSection.o 文件的长度为 1336 个字节,这跟我们用du
命令查看的结果是一致的:
关于 Section Header Table 详细信息请参考《程序员的自我修养——链接、装载与库》一书第 3.4.2 节“段表”。
可执行目标文件
可执行目标文件格式
可执行目标文件的典型格式如下图所示:
图片来源:《深入理解计算机系统》
案例分析
我们还接着可重定位文件的例子继续分析。
静态链接
我们先将 SimpleSection.o 文件静态链接成可执行文件 SimpleSection.static:
1 | $ gcc -static SimpleSection.o -o SimpleSection.static |
再查看下 SimpleSection.static 文件的 Section Header Table:
1 | $ readelf -S SimpleSection.static |
并得到如下结果:
具体分析过程跟“可重定位目标文件”部分相同。
动态链接
我们先将 SimpleSection.o 文件动态链接成可执行文件 SimpleSection.dynamic:
1 | $ gcc SimpleSection.o -o SimpleSection.dynamic |
再查看下 SimpleSection.dynamic 文件的 Section Header Table:
1 | $ readelf -S SimpleSection.dynamic |
并得到如下结果:
重定位段信息
值得注意的是上图(动态链接)中的两个属性为重定位表的段.rel.dyn
和.rel.plt
。它们跟.rel.text
或.rel.data
并不一样。
我们先用readelf -r
命令来查看一下可重定位目标文件和可执行目标文件中的可重定位段的区别:
可重定位目标文件
1
$ readelf -r SimpleSection.o
并得到结果如下:
可执行目标文件
1
$ readelf -r SimpleSection.dynamic
并得到结果如下:
比较上边两图,我们(从Offset
属性)可以知道:
.rel.text
属于普通重定位段,由编译器编译产生,存在于可重定位目标文件内;用于最终可执行目标文件或者动态库的重定位。.rel.dyn
和.rel.plt
属于动态重定位段,由链接器产生,存在于可执行目标文件或者动态库内;借助这两个段可动态修改对应的.got
和.got.plt
段,从而实现运行时重定位。.rel.dyn
对应.got
段;.rel.plt
对应.got.plt
。
更详细可参考博文 Linux 下 ELF 重定位理解。
后记
本来的打算是先阐述目标文件,再说明如何将可执行文件加载到进程虚拟地址空间,从而达到叙述虚拟地址空间的目的。很明显,我失误了。光是目标文件这么少的知识点我都要写这么长的博文,再加上加载和虚拟地址空间,可以预计博文将会很长。那时,不但读者受不了,自己也受不了…所以还是将大的主题继续拆分。关于加载和虚拟地址空间请见下一博文。