Windows和Linux下的文件格式

现在PC平台上流行的可执行文件格式主要是 Windows 下的PE(Portable Executable)和 Linux 下的ELF(Executable Linkable Format),它们都是COFF(Common File Format)格式的变种。
COFF 是 Unix V3首先提出的规范,微软在此基础上制定了 PE 格式标准,并将它用于 Windows。后来 Unix V4 又在 COFF 的基础上引入了 ELF 格式,被 Linux 广泛使用。这也就是为什么 Windows 和 Linux 上的可执行文件如此相似的主要原因,因为它们都是源于同一种可执行文件格式 COFF。
从广义上讲,目标文件与可执行文件的存储格式几乎是一样的,我们可以将它们看成是同一种类型的文件,在 Windows 下,将它们统称为 PE 文件,在 Linux 下,将它们统称为 ELF文件。
另外,动态链接库(DLL,Dynamic Linking Library)(Windows 下的.dll和 Linux 下的.so)和静态链接库(Static Linking Library)(Windows 下的.lib和 Linux 下的.a)也是按照可执行文件的格式存储的。
静态链接库稍有不同,它是把多个目标文件捆绑在一起形成一个文件,再加上一些索引,你可以简单地把它理解为一个包含了很多目标文件的包。
其他不太常见的可执行文件格式还有 Intel/Microsoft 的 OMF(Object Module Format)、Unix a.out、MS-DOS .COM 等。
在 Linux 的 ELF 标准中,主要包含以下四类文件:
image.png

目标文件的组织形式

从整体上看,编译生成的目标文件被划分成了多个部分,每个部分叫做一个段(Section)。下图是 Linux GCC 生成的目标文件的格式:
image.png
段名大都以.作为前缀,表示这些名字是系统保留的。下面是对各个部分的说明:
image.png
除了这些系统保留的段名,应用程序也可以使用其它名字定义自己的段,比如可以在 ELF 文件中插入一个叫做music的段来保存 MP3 音乐。应用程序自定义的的段不建议使用.作为前缀,否则容易和系统保留段发生冲突。可执行文件的组织形式
可执行文件的组织形式和目标文件非常类似,也被划分成多个部分,如下图所示:
image.png
图中左半部分是可执行文件的结构:带阴影的是可执行文件增加的一些段,另外可执行文件删除了可重定位段(.rel.text.rel.data)以及段表(Section Table)。
总体来说,目标文件包含了10个左右的段,而可执行文件包含了将近30个左右的段,上面的两张图只列出了一些关键段,剩下的段都隐藏在“Other Data(其他数据)”。
图中右半部分是进程的地址空间
不同颜色的箭头表明了可执行文件应该被加载到地址空间的哪一个区域,可以发现,操作系统并不是为每个段都分配一个区域,而是将多个具有相同权限的段合并在一起,加载到同一个区域。
站在文件结构的角度,可执行文件包含了众多的段(Section),每个段都有不同的作用;站在加载和执行的角度,所有的段都是数据,操作系统只关心数据的权限,只要把相同权限的数据加载到同一个内存区域,程序就能正确执行。
常见的数据权限无外乎三种:只读(例如 .rodata 只读数据段)、读写(例如 .data 数据段)、读取和执行(例如 .text 代码段),我们将一块连续的、具有相同权限的数据称为一个Segment,一个 Segment 由多个权限相同的 Section 构成。
不巧的是,“Segment”也被翻译为“段”,但这里的段(Segment)是针对加载和执行的过程。
在 Linux 下,相信很多读者都遇到过一种叫做Segment fault(段错误)的错误,这种错误发生在程序执行期间,在编译和链接时无法检测,一般都是代码的权限不足导致的。例如:

  1. #include <stdio.h>
  2. char *str = "c.biancheng.net";
  3. int main(){
  4. str[1] = '@';
  5. return 0;
  6. }

程序执行到 6 行时就会出现“Segment fault(段错误)”,这是因为字符串 str 保存在地址空间的常量区,只能读取,不能写入,而修改字符串显然是越权操作。
在目标文件中,段表(Section Table)用来描述各个 Section 的信息,包括它的名字、长度、在文件中的偏移、读写权限等,通过段表可以详细地了解目标文件的结构。
而在可执行文件中,段表被删除了,取代它的是程序头表(Program Header Table);程序头表用来描述各个 Segment 的信息,包括它的类型、偏移、在进程虚拟地址空间中的起始地址、物理装载地址、长度、权限等。操作系统就是根据程序头表将可执行文件加载到内存,并为各个 Segment 分配内存空间、确定起止地址。
也就是说,可执行文件不再关注具体的文件结构,而是关注程序的加载和执行过程。
由于可执行文件在加载时实际上是被映射的虚拟地址空间,所以可执行文件很多时候又被叫做映像文件(Image)

段(Section)的合并

编译器生成的是目标文件,而我们最终需要的是可执行文件,链接(Linking)的作用就是将多个目标文件合并成一个可执行文件。
在链接过程中,链接器会将多个目标文件中的代码段、数据段、调试信息等合并成可执行文件中的一个段。段的合并仅仅是一个简单的叠加过程,如下图所示:
image.png
除了合并有用的段(例如代码段、数据段等),链接器还会删除多余的段(例如重定位段、段表等),增加其他段(例如程序头表等)