认识 ELF 文件格式(三)
本篇文章我们介绍 ELF 文件中的程序头
一、程序头
程序头提供了 ELF 文件的段视图,segment 我们将其翻译成段。与节头提供的节视图不一样。节视图仅适用于静态链接。而段视图是在将 ELF 文件加载到进程并执行的时候,定位相关代码和数据,并确定加载到虚拟内存中的内容时,操作系统和动态链接器会用到段视图。
ELF 的段包含零个或多个节,实际上就是把多个节捆绑成单个段。段提供的可执行视图,只有 ELF 二进制文件才会用到他们,而非二进制文件,比如可重定位对象,则用不到他们。
如下则是程序头以及段和节之间的映射关系。
1 | readelf --wide --segments main |
我们随便找一个二进制文件,然后通过 readelf 查看对应的段和节之间的映射关系。如上,有 10 个程序头,并且通过 Section to Segment mapping
部分,我们发现 “段只不过是把节简单的绑定在一起” 而已。
如下是一个程序头的结构体定义,一个 ELF 文件有多个程序头,程序头如同数组一样排列,我们也将其称为程序头表。
1 | typedef struct |
接下来,我们来解析每一个字段的含义。
1. p_type 字段
标识了段的类型,常见的类型包括:PT_LOAD、PT_DYNAMIC、PT_INTERP
。
- PT_LOAD:此类型的段会在创建进程时加载到内存中,程序头的剩余部分描述了可加载块的大小和将其加载到的地址。通常至少有两个
PT_LOAD
类型的段,一个包含不可写数据节,另一个包含可写数据节。 PT_INTERP
:此段通常包含.interp
节,该节提供了加载二进制文件的解释器的名称。PT_DYNAMIC
:此段包含了.dynamic
节,该节告诉解释器如何解析二进制文件用于执行。
2. p_flags 字段
指定了段在运行时的访问权限,这里有 3 种重要的标志类型:PF_X、PF_W、PF_R
。
PF_X
标志:指定该段为可执行,一般对代码段设置此标志PF_W
标志:表示该段为可写,一般对可写数据段设置该位,代码段一般不设置该位。PF_R
标志:表示该段为可读,该属性在代码段和数据段都是正常情况。
3. p_offset 字段、p_vaddr 字段、p_paddr 字段、p_filesz 字段和 p_memsz 字段
其中 p_offset 字段:指定该段的起始文件偏移量,p_vaddr 字段:指定了加载的虚拟地址,p_filesz 字段:指定了段的大小。p_memsz 字段:指定了段在内存中的大小。
在某些操作系统上,可以使用 p_paddr 字段:指定段在物理内存的那个地址进行加载。在 Linux 操作系统中,该字段并未被使用且设置为零,因为操作系统在虚拟内存中执行二进制文件。
为什么要用 p_filesz 和 p_memsz 来指定段在文件中的大小和在内存中大小呢?
因为我们知道某些节只表明需要在内存中分配一些字节,而实际上并没有在二进制文件中占用这些字节。比如 .bss
节包含的初始化数据,肯定为零,所以实际上无须在二进制文件中包含这些零。但是。再将包含 .bss
节的段加载虚拟内存的时候,就应该分配 .bss
里面所有的字节。因此,p_memsz
很可能会大于 p_filesz
字段。这种情况下,链接器在加载二进制文件时,就会在段的末尾添加额外的字节,并且将其初始化为零。
4. p_align 字段
指定了段所需的内存对齐方式,以字节为单位。对齐值 0 或 1 表示不需要特定的对齐方式。如果 p_align 未设置为 0 或者 1,则其值必须是 2 的指数,并且 p_vaddr 必须等于 p_offset % p_align
。
二、小结
程序头我们介绍完了,因为段就是多个节的映射。只是看待的角度不同而已。
再将 ELF 二进制加载到进程中并且执行的时候,定位相关代码和数据并确定加载到虚拟内存中的内容时,操作系统和动态链接器就会使用到段视图。