我们知道一个Linux程序包括程序代码和初始数据,那么这些程序二进制代码和初始数据在可执行程序文件中是怎么进行存储呢?这便是ELF文件格式要解决的问题。
一个Linux执行程序的内存结构粗略可划分为 代码段、数据段、BSS、堆、栈,如下图所示:
其中 BSS、堆、栈 中的内容是在程序运行过程中动态生成的, 而 代码段 和 数据段 内容是程序代码编译过程中生成的,因此,需要将 代码段 和 数据段 内容写入ELF文件,以便程序加载时可以从ELF文件读取数据,对内存空间的 代码段 和 数据段 进行初始化。对于 BSS、堆、栈 等,ELF文件中只需要保存内存使用规划数据即可,即内存起始地址、内存使用量等。上述只是一个简易粗略的内存使用情况划分,实际内存使用情况要比这个复杂,部分细小的数据段,比如 全局偏移表、过程链接表 等,我们暂时先放一放,后面会细说。
ELF除了要保存 代码段、数据段 的数据,以及各内存段的内存使用规划数据以外,还可能需要保存 程序版本信息、调试信息、动态链接符号表、动态链接的字符串 等等,而这些数据都被划分为一个个Section进行存储,然后在ELF文件中存放一个Section的索引表指向这些Seciton,以便进行定位。
ELF同时作为可执行程序的打包方式,为了简化可执行程序的加载流程,将程序加载过程中行为相近的Section规划到一段连续内存,并将其定义为一个Segment,以便整个Segment一起加载。同样,在ELF文件中存放一个Segment的索引表指向各个Segment,以便对它们进行定位。
综上所述,ELF文件的整体结构设计如下图所示:
1.ELF header,描述了程序版本等基本信息,以及Program header table 和 Section header table在ELF文件内的起始位置和大小;
2.Program header table,其实就是我们前面提到的Segment索引表;
3.Section header table,即前文提到的Setion索引表;
4..text\.rodata\.data,即前文提到的各个Setion,Section header table中记录了各个Setion的起始位置和大小,将一个或多个连续分布的Section划归为一个Segment,然后再Program header table中记录了各个Segment的起始位置和大小;
在了解了ELF文件整体结构设计后,我们来看看各个部分的数据结构是如何设计的。
ELF文件头(ELF header)
ELF头(ELF header)位于文件的开始位置。 它的主要目的是定位文件的其他部分。 文件头主要包含以下字段:
你可以在内核源码种找到表示ELF64 header的结构体 elf64_hdr
:
typedef struct elf64_hdr {
unsigned char e_ident[EI_NIDENT];
Elf64_Half e_type;
Elf64_Half e_machine;
Elf64_Word e_version;
Elf64_Addr e_entry;
Elf64_Off e_phoff;
Elf64_Off e_shoff;
Elf64_Word e_flags;
Elf64_Half e_ehsize;
Elf64_Half e_phentsize;
Elf64_Half e_phnum;
Elf64_Half e_shentsize;
Elf64_Half e_shnum;
Elf64_Half e_shstrndx;
} Elf64_Ehdr;
这个结构体定义在 elf.h
Section索引
所有的数据都存储在ELF文件的节(sections)中。 我们通过节头表中的索引(index)来确认节(sections)。 节头表表项包含以下字段:
而且,在linux内核中结构体 elf64_shdr
如下所示:
typedef struct elf64_shdr {
Elf64_Word sh_name;
Elf64_Word sh_type;
Elf64_Xword sh_flags;
Elf64_Addr sh_addr;
Elf64_Off sh_offset;
Elf64_Xword sh_size;
Elf64_Word sh_link;
Elf64_Word sh_info;
Elf64_Xword sh_addralign;
Elf64_Xword sh_entsize;
} Elf64_Shdr;
这个结构体定义在 elf.h
Segment索引
在可执行文件或者共享链接库中所有的节(sections)都被分为多个段(segments)。 程序头是一个结构的数组,每一个结构都表示一个段(segments)。 它的结构就像这样:
typedef struct elf64_phdr {
Elf64_Word p_type;
Elf64_Word p_flags;
Elf64_Off p_offset;
Elf64_Addr p_vaddr;
Elf64_Addr p_paddr;
Elf64_Xword p_filesz;
Elf64_Xword p_memsz;
Elf64_Xword p_align;
} Elf64_Phdr;
这个结构体定义在 elf.h.
综上所述,我们对ELF文件进行解析的过程大致如下:
1.读取位于ELF文件起始位置的ELF header数据结构,从中获取程序信息,以及Section索引表 和 Segment索引表 的起始位置和大小;
2.对于可执行程序ELF文件,读取Segment索引表,挨个遍历elf64_phdr 对象,从中获取该Segment的信息,以及在ELF文件中的起始位置和大小,以及对应的内存位置和内存大小。根据Segment索引将数据从ELF文件载入指定的内存中,即可完成程序载入过程;
3.对于其他类型ELF文件,读取Section索引表,挨个遍历elf64_shdr 对象,从中获取该Section的信息,及其在ELF文件中的起始位置和大小,以及对应的内存位置。根据需要,按照Section索引读取Section内容做相应处理;
我们可以用readelf工具读取ELF文件结构信息,以我们熟悉得mkdir程序为例,如下图:
使用 -h 参数可读取ELF header 结构体数据,可以看到文件类型是ELF64的可执行程序,程序入口内存地址是0x3700。
Program header起始地址在ELF文件中的偏移量是64bytes,单个Segment索引长度是56bytes,总共有13个Segment。
Section header起始地址在ELF文件中的偏移量是66112bytes,单个Section索引对象长度是64bytes,总共有31个。
使用-S参数可以打印Section header table所有内容,上述表头说明如下:
Name:该Section的名字;
Type:Section类型;
Address:该Section载入内存后的内存起始地址;
Offset:该Section对于数据在ELF文件中相对文件起始偏移量;
Size:该Section在程序内存中占用内存大小;
EntSize:该Section在ELF文件中数据大小;
Flags:*****
Link:*****
Info:*****
Align:数据对齐大小。
使用-l参数可以打印Program header table所有内容,上部表头说明如下:
Type:该Segment类型;
Offset:在ELF文件中的偏移位置;
VirtAddr:该Segment载入内存后的起始虚拟内存地址;
PhysAddr:该Segment载入内存后的起始物理内存地址;;
FileSiz:该Segment在ELF文件中大小;
MemSiz:该Segment载入内存后大小;
Flags:*****;
Align:数据对齐大小;
下部分显示了各个Segment包含了哪些Section
Linux系统加载ELF文件过程可参考这篇文章: ELF文件加载过程 - 知乎