理解了进程的描述和创建之后,自然会想到我们编写的可执行程序是如何作为一个进程工作的?这就涉及可执行文件的格式、编译、链接和装载等相关知识。
这里先提一个常见的名词“目标文件”,是指编译器生成的文件。“目标”指目标平台, 例如 x86 或 x86-64,它决定了编译器使用的机器指令集。目标文件一般也叫作 ABI(Application Binary Interface,应用程序二进制接口),目标文件和目标平台是二进制兼容的。二进制兼容 即指该目标文件已经是适应某一种 CPU 体系结构上的二进制指令。例如一个编译出来的x86-64目标文件是无法链接成ARM上的可执行文件的。
最古老的目标文件格式是 a.out,后来发展成 COFF,现在常用的有 PE (Windows)和 ELF(Linux)。
ELF(Executable and Linkable Format)即可执行的和可链接的格式, 是一个文件格式的标准。ELF 格式的文件用于存储 Linux 程序。ELF 是一 种对象文件的格式,用于定义不同类型的对象文件中都有什么内容、以什么样的格式放这些内容。ELF 首部会描绘整个文件的组织结构,它还包括很多节(sections,是在 ELF 文件里用以装载内容数据的最小容器),这些节有些是系统定义好的,有些是用户在文件中通过.section 命令自定义的,链接器会将多个输入目标文件中相同的节合并。
我们先来看一个例子,直观感受一下
test.c原文件内容如下:
#include
int g(int x)
{
return x + 3;
}
int f(int x)
{
return g(x);
}
int main()
{
return f(8) + 1;
}
以 ELF 格式为例,来看在可执行文件格式里的 3 种不同类型的目标文件。
(1)可重定位文件:这种文件一般是中间文件,还需要继续处理。由编译器和汇编器创建,一个源代码文件会生成一个可重定位文件。文件中保存着代码和适当的数据,用来和其他的目标文件一起来创建一个可执行文件或者动态链接库文件。在编译 Linux 内核时可能会注意到,每个内核源代码.c 文件都会生成一个同名的.o 文件,该文件即为可重定位目标文件,最后所有.o 文件会链接为一个文件,即 Linux 内核。另外,静态链接库文件实际上就是可重定位文件的打包,也是可重定位文件,一般以.a作为文件名后缀。
(2)可执行文件:一般由多个可重定位文件结合生成,是完成了所有重定位工作和符号解析的文件(动态链接库符号是在运行时解析的),文件中保存着一个用来执行的程序。重定位和符号解析会在链接部分详细介绍。
(3)动态链接库文件:也称为共享目标文件,是已经经过链接处理可以直接加载运行的库文件,是可以被可执行文件或其他动态链接库文件加载使用的库文件,例如标准 C 的库文件 libc.so。可以简单理解为没有主函数 main 的“可执行”文件,只有一堆函数可供其他程序调用。Linux 下动态链接库文件文件名后缀为.so 的文件,so 代表 shared object。
ELF 文件参与程序的链接(构建一个可执行程序)和程序的执行(加载可执行程序),所以可以从不同的角度来看待 ELF 格式的文件。
ELF文件的索引表。ELF文件的主体是各种节,典型的如代码节.text,还有描述这些节属性的信息(Program header table和Section header table),以及ELF文件的整体描述信息(ELF header),整体如图所示。
ELF Header在文件最开始描述了该文件的组织情况。ELF文件头会指出可执行文件是32位还是64位的,e_ident数组的第五个字节是1表示是32位,2表示是64位。ELF Header的其他部分主要说明了其他文件内容的位置、大小等信息。ELF Header长度为64字节,在/usr/include/elf.h文件中,可以看到其C语言格式的定义如下:
ELF表头首先会给出很多关于本ELF文件的属性信息,如上面提及到的3种ELF类型就是通过e_type来体现的。e_type的值1,2,3,4分别代表可重定位目标文件,可执行文件,共享目标文件和核心文件。如上面的ELF文件索引表所示,其中最重要的是段头表(Program header table)和节头表(Section header table)的位置。
段头表存储于文件的e_phoff(ELF header的字段,下同)位置,有e_phnum项内容,每项大小为e_phentsize字节。
节头表基本定义了整个ELF文件的组成,可以说是整个ELF就是由若干个节(Section)组成的。段只是对节区进行了重新组合,将连续的多个节区描述为一段连续区域,对应到一块连续的内存地址中。
节头表是由Section Header组成的表,包含了描述文件节区的信息,每个节区在表中都有一项,每一项给出诸如节区名称、节区大小这类信息。用于链接的目标文件必须包含节区头部表,其他目标文件有没有这个表都可以。每个节区头部结构的描述如下:
我们可以查看hello这个可执行文件的sections、指令及主要输出内容如下节区信息。前6列分别是[Nr]索引、Name 节名、Type 类型、Addr 虚拟地址、Off 偏移和Size 节大小。简单来说,该节描述了将可执行文件中起始位置位Off、大小为Size的一段数据加载到内存地址Addr。
(ps:hello.c文件中只是单纯的打印了一句"Hello World!")
我们以"[6].text"来理解每一节头的内容(一行是一个节的描述)。
段头(Program Header)表是和创建进程相关的,描述了连续的几个节在文件中的位置、大小以及它被放进内存后的位置和大小,告诉系统如何创建进程映像,可执行文件加载器就可以按这个说明将可执行文件搬到内存中。用来构造进程映像的目标文件必须具有段头表,可重定位文件不需要这个表。
我们可以查看生成的hello这个可执行文件的段头表,指令及主要输出内容如下段头表示例。
8列分别是Type类型、Offset文件偏移、VirtAddr虚拟地址、PhysAddr物理地址、FileSiz可执行文件中该区域的大小、MemSiz内存中该区域的大小、Flg属性标识和Align对齐方式。和节头表相似,该表描述了可执行文件中起始位置为Offset、大小为FileSiz的一段数据,加载到内存地址VirtAddr中。两者的虚拟地址信息是一致的,但节头表的Addr可以没有信息,可重定位目标文件的Addr就是全0。
我们以第一行为例进行说明,Type值为LOAD表示该段(Segment)需要加载到内存,Offset全0表示其内容为从可执行文件头开始共0x001e8(FileSiz)个字节,加载到虚拟地址0x08048000(VirtAddr)处,该段为可读(R)权限,4k(Align,0x1000)对齐。
再往下看为节与段的映射关系说明(Section to Segment mapping:),00即第一行描述的段,一共包括了.note、.gnu、.ABI-tag等多个节。
我们可以使用如下指令对ELF进行更多的研究实践。
(1)man elf:在Linux下输入此指令即可查看详细的格式定义。
(2)readelf:用于显示一个或多个elf格式的目标文件的信息,可以通过它的选项来控制显示哪些信息。
- -a 等价于 -h -l -S -s -r -d -V -A - I
- -h 显示elf文件开始的文件头信息
- -S 显示节头信息(如果有)
- -l 显示Program Header。(小写的L)
- -s 显示符号表段中的项(如果有)
- -r 显示可重定位段的信息
- -H 显示readelf所支持的命令行选项
(3)objdump:显示二进制文件信息,用于查看目标文件或者可执行的目标文件的构成的gcc工具,选项如下。
- -f 显示objfile中每个文件的整体头部摘要信息
- -h 显示目标文件各个section的头部摘要信息
- -r 显示文件的重定位入口。如果和-d或者-D一起使用,重定位部分以反汇编后的格式显示出来
- -s 显示指定section的完整内容。默认所有的非空section都会被显示
- -t 显示文件的符号表入口
- -x 显示所可用的头信息,包括符号表、重定位入口。-x等价于-a -f -h -r -t同时指定
(4)hexdump:用十六进制的数字来显示elf的内容。
以上内容为中科大软件学院《Linux操作系统分析》课后总结,感谢孟宁老师的倾心教授,老师讲的太好啦(^_^)
参考资料:《庖丁解牛Linux内核分析》 孟宁 编著