操作系统——ELF文件格式(九)
操作系统——ELF文件格式(九)
2020-09-21 18:02:00 hawk
概述
因为后面我们实现的内核主要通过c语言进行编写,其通过gcc编编译器产生的文件格式就是elf文件格式。因此这里我们将会简单介绍一下elf文件格式。这一节也是比较枯燥的知识点。如果不感兴趣或者已经有基础的可以直接跳过,之后用到的时候在会看。
ELF文件
elf简介
首先概括的介绍一下elf文件吧。ELF指的是Executable and Linkable Format,即可执行链接格式。最初是由UNIX系统实验室开发的应用程序二进制借口。工具接口标准委员会选择了它作为IA32体系结构上不同操作系统之间的可移植二进制文件格式。
elf目标文件
实际上ELF目标文件有多种类型,包括待重定位文件(Relocatable file)、共享目标文件(shared object file)以及可执行文件(executable file)。下表简单总结了一下相关的信息
ELF目标文件类型 | 描述 |
待重定位文件(relocatable file) |
待重定位文件就是常说的目标文件,属于源文件编译后但未完成链接的半成品。其往往被用于与其他目标文件合并链接,以构建出二进制可执行文件或者动态链接库。 实际上如果该目标文件中引用了其他外部文件中定义的符号,在编译阶段只能先标识出一个符号名,具体地址不能确定。等待链接的过程中将外部文件重定位后才能确定引用的符号地址 |
共享目标文件(shared object file) | 即动态链接库。在可执行文件被加载的过程中被动态链接,成为程序代码的一部分 |
可执行文件(executable file) | 经过编译、链接后,可以直接运行的程序文件 |
段(segment)和节(section)
程序中最重要的部分就是段(segment)和节(section),他们才是真正的程序体,是真真切切的程序资源。当然,程序中的段和节的信息同样是通过header来描述的——程序头是program header;节头是section header。一个程序中的段和节的数量和大小往往是不固定的,因此程序往往需要专门的数据结构来记录和描述这些数据,即程序头表(program header table)和节头表(section header table),类似于前面的GDT和页表等,其相当于数组,而里面的数组元素分别就是程序头和节头。(需要特别支出,虽然为了方便是一起介绍的,但是程序头表和节头表是分别存储的,也就是程序头表存储程序头,节头表存储节头,不会混合存储,这一点需要搞清楚)。
在表中,实际上每一个元素通称为条目,即entry。一个条目代表一个段或一个节的头部描述信息。当然,由于程序中段和节的数量是不固定的,则其程序头表和节头表自然大小也就不确定。而考虑到其存储的顺序,存储在后边的表的起始地址也不会固定。但是我们需要在开始的时候就可以直接访问,从而获取程序文件的相关信息,因此我们需要在固定的位置,用一个固定的结构的数据来描述程序头表和节头表的大小以及位置信息——即ELF header。实际上这部分数据位于文件的最开始的部分,并且其具有固定的大小。
因此,实际上ELF文件格式大体上仍然可以分为文件头和文件体两部分。ELF header从“全局上”给出程序文件的组织结构,概要出程序中其他头表的位置大小等信息——如程序头表的大小及位置、节头表的大小及位置等。然后各个段和节的位置、大小等信息再分别从具体的程序头表和节头表中予以说明。需要说明的是,无论是待重定位文件,还是可执行文件中,文件最开头的部分必须都是elf Header。然后才是其他的各个部分。
ELF Header
首先简单介绍一下ELF header中的数据类型(我们这里仅仅介绍32位的,因为我们前面也默认都是在32位环境下),如下表所示
数据类型名称 | 字节大小 | 对齐 | 意义 |
Elf32_Half | 2 | 2 | 无符号中等大小的整数 |
Elf32_Word | 4 | 4 | 无符号大整数 |
Elf32_Addr | 4 | 4 | 无符号程序运行地址 |
Elf32_Off | 4 | 4 | 无符号文件偏移量 |
前面介绍了一下会用到的数据类型,下面则是具体的ELF header的定义,如下所示
#define EI_NIDENT (16)
typedef struct { unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */ Elf32_Half e_type; /* Object file type */ Elf32_Half e_machine; /* Architecture */ Elf32_Word e_version; /* Object file version */ Elf32_Addr e_entry; /* Entry point virtual address */ Elf32_Off e_phoff; /* Program header table file offset */ Elf32_Off e_shoff; /* Section header table file offset */ Elf32_Word e_flags; /* Processor-specific flags */ Elf32_Half e_ehsize; /* ELF header size in bytes */ Elf32_Half e_phentsize; /* Program header table entry size */ Elf32_Half e_phnum; /* Program header table entry count */ Elf32_Half e_shentsize; /* Section header table entry size */ Elf32_Half e_shnum; /* Section header table entry count */ Elf32_Half e_shstrndx; /* Section header string table index */ } Elf32_Ehdr;
1. e_ident字段。下面我们分别说明一下每一个字段的含义,首先是e_ident字段,用来表示elf字符等信息,具体布局如下所示
e_ident数组成员 | 意义 |
e_ident[0] = 0x7f | 固定的ELF文件的4位魔数,表明该文件是elf文件 |
e_ident[1] = 'E' | |
e_ident[2] = 'L' | |
e_ident[3] = 'F' | |
e_ident[4] |
用来表示elf文件类型 0表示文件是不可识别类型 1表示文件是32位elf格式文件 2表示文件是64位elf格式文件 |
e_ident[5] |
用来指定编码格式 0表示非法编码格式 1表示小端字节序,即LSB 2表示大端字节序,即MSB |
e_ident[6] |
elf头的版本信息,默认为1 0表示非法版本 1表示当前版本 |
e_ident[7~15] | 暂且无用,保留,均初始化为0 |
2. e_type字段。其占用2字节,用来制定elf目标文件的类型。对应的值如下表所示
elf目标文件类型 | 取值 | 意义 |
ET_NONE | 0 | 未知目标文件格式,忽略 |
ET_REL | 1 | 可重定位文件 |
ET_EXEC | 2 | 可执行文件 |
ET_DYN | 3 | 动态共享目标文件 |
ET_CORE | 4 | core文件,即程序崩溃时其内存映像的转储格式 |
ET_LOPROC | 0xff00 | 特定处理器文件的拓展下边界 |
ET_HIPROC | 0xffff | 特定处理器文件的拓展上边界 |
前5个文件形式还比较正常,而实际上最后两个的文件类型和前面的差异很大,是因为他们是与硬件相关的参数。
3. e_machine字段,占用2字节。其用来描述elf目标文件的体系结构类型。可能的取值如下所示
体系结构类型 | 取值 | 意义 |
EM_NONE | 0 | 未指定 |
EM_M32 | 1 | AT&T WE 32100 |
EM_SPARC | 2 | SPARC |
EM_386 | 3 | Intel 80386 |
EM_68K | 4 | Motorola 68000 |
EM_88K | 5 | Motorola 88000 |
EM_860 | 7 | Intel 80860 |
EM_MIPS | 8 | MIPS RS3000 |
4. e_version字段,占用4字节,用来表示版本信息
5. e_entry字段,占用4字节,用来指明操作系统运行该程序时,将控制权转移到的虚拟地址上
6. e_phoff字段,占用4字节,用来指明程序头表(Program Header table)在文件内的字节偏移量。如果没有程序头表,该值为0
7. e_shoff字段,占用4字节,用来指明节头表(section Header table)在文件内的字节偏移量。如果没有节头表,该值为0
8. e_flags字段,占用4字节,用来指明与处理其相关的标志。
9. e_ehsize字段,占用2字节,用来指明Elf Header的字节大小
10. e_phentsize字段,占用2字节,用来指明程序头表(Program Header table)中每个条目(entry)的字节大小。即每个用来描述段信息的数据结构的字节大小,该结构是后面要介绍的sruct Elf32_Phdr
11. e_phnum字段,占用2字节,用来指明程序头表中条目的数量。实际上就是节的个数。
12. e_shentsize字段,占用2字节,用来指节头表(section header table)中每个条目(entry)的字节大小,即每个用来描述节信息的数据结构的字节大小
13. e_shnum字段,占用2字节,用来指明节头表中条目的数量。实际上就是节的个数
14. e_shstrndx字段,占用2字节,用来指明string name table在节头表中的索引index
基本上上面就是对应的头文件数据结构各个字段对应的值。
程序头表
下面简单介绍一下程序头表相关的数据结构,其中的条目数据结构定义如下
typedef struct { Elf32_Word p_type; /* Segment type */ Elf32_Off p_offset; /* Segment file offset */ Elf32_Addr p_vaddr; /* Segment virtual address */ Elf32_Addr p_paddr; /* Segment physical address */ Elf32_Word p_filesz; /* Segment size in file */ Elf32_Word p_memsz; /* Segment size in memory */ Elf32_Word p_flags; /* Segment flags */ Elf32_Word p_align; /* Segment alignment */ } Elf32_Phdr;
同样的,我们简单介绍一下程序头表中条目的数据结构的各个字段
1. p_type字段,占用4字节。用来指明程序中该段的类型,主要类型如下表所示
类型 | 取值 | 说明 |
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 |
2. p_offset字段,占用4字节,用来指明本段在文件内的起始偏移字节
3. p_vaddr字段,占用4字节。用来指明本段在内存中的起始虚拟地址
4. p_paddr字段,占用4字节。仅用于与物理地址相关的系统中
5. p_filesz字段,占用4字节。用来指明本段在文件中的大小
6. p_memsz字段,占用4字节。用来指明本段在内存中的大小。
7. p_flags字段,占用4字节。用来指明与本段相关的标志,具体取值如下所示
类型 | 取值 | 说明 |
PF_X | 1 | 本段具有可执行权限 |
PF_W | 2 | 本段具有可写权限 |
PF_R | 4 | 本段具有可读权限 |
PF_MASKOS | 0x0ff00000 | 本段与操作系统相关 |
PF_MASKPROC | 0xf0000000 | 本段与处理器相关 |
8. p_align字段,占用4字节,用来指明本段在文件和内存中的对其方式。如果值为0或1,则表明不对齐;否则p_align应该是2的幂次数
这样子,我们就简单介绍完毕了elf header和program header等数据结构。实际上在链接完毕后,程序运行的代码、数据等资源就在段中了。剩下诸如section等细节,如果感兴趣的话可以自行了解。