ELF: Executable and Linking Format part2

PROGRAM LOADING

Introduction

这一篇介绍了目标文件信息和系统运行程序所需的动作。
可执行目标文件和共享目标文件都是静态的表示程序。为了执行这样的程序,系统使用这些文件来创建动态的程序表示,即进程映像。一个进程映像通过segment保存文本、数据、堆栈等。

Program Header

可执行目标文件和共享目标文件的程序头部表(program header table)是一个结构体数组,每个元素描述了一个segment,以及系统准备执行程序时所需的其他信息。目标文件的segment包含一个或多个section,后面会介绍。Program Header只对可执行目标文件和共享目标文件有意义。

1
2
3
4
5
6
7
8
9
10
11
// Program Header
typedef struct {
Elf32_Word p_type;
Elf32_Off p_offset;
Elf32_Addr p_vaddr;
Elf32_Addr p_paddr;
Elf32_Word p_filesz;
Elf32_Word p_memsz;
Elf32_Word p_flags;
Elf32_Word p_align;
} Elf32_Phdr;

p_type 说明了这个数组元素描述了一个什么种类的segment,以及如何解释这个数组元素的信息。Type取值和含义后面介绍。
p_offset 从文件开头到segment第一个字节的偏移。
p_vaddr segment第一个字节在内存中的虚拟地址。
p_paddr 某些系统上此成员保存segment的物理地址。由于System V忽略应用程序的物理地址,这个成员的取值在可执行目标文件和共享目标文件中没有定义。
p_filesz segment在文件映像中的字节数,可能为0。
p_memsz segment在内存映像中的字节数,可能为0。
p_flags 与segment相关的标志。后面介绍。
p_align 后面”程序加载”一节会介绍,可加载的进程segment其p_vaddr和p_offset必须是模页大小同余的。这个成员给出了segment在内存和文件中的对齐值。
有的表项描述了进程segment,其他的则描述了一些进程映像中并不包含的辅助信息。segment表项除了有明确规定的,可以采用任意的顺序。下面列出了segment类型的取值,其他的取值留作以后使用。
Segment Types,p_type

Name Value
PT_NULL 0
PT_LOAD 1
PT_DYNAMIC 2
PT_INTERP 3
PT_NOTE 4
PT_SHLIB 5
PT_PHDR 6
PT_LOPROC 0X70000000
PT_HIPROC 0X7FFFFFFF

PT_NULL 表示此数组元素未被使用,其他成员的值未定义。这个类型使程序头部表可以包含一些无关的表项。
PT_LOAD 表示这是一个可加载的segment,通过p_filesz和p_memsz描述。目标文件中的字节将被映射到内存segment的开始部分去。如果segment的内存大小(p_memsz)大于文件大小(p_filesz),在segment已初始化数据区后面多出来的部分将填充值为0的字节。文件大小可以小于内存大小。程序头部表中可加载的segment表项依据p_vaddr成员按升序排列。
PT_DYNAMIC 保存了动态链接的相关信息。参见后面”动态section”一节。
PT_INTERP 指定了一个以空字节结尾的字符串的位置和长度,这个字符串是需调用的解释器的路径。
PT_NOTE 指定了辅助信息的位置和大小。
PT_SHLIB 保留的,未定义语义。
PT_PHDR 如果有,指定了文件和内存映像中程序头部表的位置和大小。
PT_LOPROCPT_HIPROC此范围内的取值留作处理器相关的语义。

注:除非在其他地方有特殊的需求,所有程序头部的segment类型都是可选的,即文件的程序头部表可能只包含与其内容有关的程序头部。

Base Address

可执行目标文件和共享目标文件有一个基地址,这个地址是程序目标文件的内存映像的起始虚拟地址。基地址的一个作用是在动态链接时重定位程序的内存映像。
一个可执行目标文件或共享目标文件的基地址是在执行期间由三个值计算出来的:内存加载地址最大页尺寸程序的可加载segment的起始虚拟地址。程序头部中的虚拟地址可能并不代表程序内存映像中实际的虚拟地址。为了计算基地址,需要首先确定与PT_LOAD类型segmentp_vaddr对应的内存地址,然后截去地址的低位部分,使地址为最接近的最大页大小的整数倍的地址,这就是基地址。依据加载进内存的文件的类型,内存地址可能与p_vaddr的值匹配,也可能不匹配。
part1中介绍的.bss section是SHT_NOBITS型的,虽然它不占据文件空间,但对segment在内存中的映像还是有影响的。通常,这些未初始化的数据驻留在segment的尾部,因此,使得程序头部中的p_memszp_filesz大。

note section

有时厂商或系统构建者需要用特定的信息注释目标文件,程序利用这些 信息进行一致性和兼容性等检查。SHT_NOTE类型的section和PT_NOTE类型的程序头部就是用于这个目的。

Program Loading

系统创建或增补一个进程映像时,只是逻辑上的拷贝文件中的segment到虚拟内存中的segment。系统何时、是否在物理上访问这个文件,取决于程序的执行行为,比如系统加载等。执行进程的过程中,只有引用到逻辑页时才会请求一个物理页。进程中通常会有很多未引用的页,这种延迟物理读的方式就将这些未引用的页排除了,提高了系统的性能。在实际应用中,如果想实现这种方式,就必须使可执行目标文件和共享目标文件的segment映像在文件中的偏移及其虚拟地址是模页大小同余的。
Executable File

Program Header Segments

Member Text Data
p_type PT_LOAD PT_LOAD
p_offset 0x100 0x2bf00
p_vaddr 0x08048100 0x08074f00
p_paddr unspecified unspecified
p_filesz 0x2be00 0x4e00
p_memsz 0x2be00 0x5e24
p_flags PF_R+PF_X PF_R+PF_W+PF_X
p_align 0x1000 0x1000

例子中,.text段和.data段的文件偏移、虚拟地址都是模4KB同余的,但是有四个页面(依据页面大小和系统文件块大小)包含的不全是文本或数据。

  • 第一个文本页包含ELF头部,程序头部表和其他信息。
  • 最后一个text页包含一份.data段开始部分的拷贝。
  • 第一个data页包含一份.data段结尾部分的拷贝。
  • 最后一个data页可能包含与进程运行不相关的文件信息。

逻辑上,系统使每个段看起来都好像是完整和独立的,以便为它们所在的内存赋予访问权限。这就需要调整段的地址,以确保地址空间中的每个逻辑页有一组独立的权限。在上面的例子中,文件中保存文本结尾和数据开头的区域将被映射两次,一个虚拟地址对应文本,另一个不同的虚拟地址对应数据。
.data段的结尾处需要为未初始化的数据做特殊的处理,系统将这部分数据的值置为0。如果文件的最后一个data页包含其他信息,这些信息不会映射到逻辑内存页中,必须将内存页剩余部分的数据置为 0,这一点与可执行文件允许有未知的内容不同。系统是否需 要除去其他三个页面中的不纯信息(Impurities)未作规定。这个程序对应的内存映像如下图所示,假设页面大小是4KB(0x1000)。
Process Image Segment

可执行目标文件和共享目标文件加载 segment 时有所不同,通常可执行目标文件的segment包含的是绝对代码。为了使进程正确执行,segment必须驻留在可执行目标文件中指定的虚拟地址。 因此,系统不会改变p_vaddr的值,直接将它作为虚拟地址。
而共享目标文件的segment包含的是位置无关代码,这就意味着,虽然其虚拟地址在不同进程间是不同的,但却不会导致执行行为无效。尽管系统为每个进程单独选择虚拟地址,它仍然维护这些segment的相对位置。由于位置无关代码在segment中使用相对地址,内存虚拟地址间的偏移必须与文件中的虚拟地址的偏移保持一致。下图举了不同进程中共享目标虚拟地址分配的一个例子,展示了相对位置的不变性,也展示了如何计算基地址。
Example Shared Object Segment Address