1. ELF格式
ELF(Executable Linkable Format)格式是Linux中的可执行文件格式。在Linux中,可执行文件、目标文件、动态链接库(linux的.so)、静态链接库(linux下的.a文件)都是ELF格式的文件。
ELF文件标准里面把ELF格式文件分为了4类。可重定位文件(relocatable file)、可执行文件(executable file)、共享目标文件(shared object file)、核心转储文件(core dump file)。可以在bash中用file命令来查看相应的文件格式。
$ file a.o
a.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
$ file a.out
a.out: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 2.6.32, BuildID[sha1]=697f760e13fbf75e42a80bbfb459678d805d51cc, not stripped
$ file liba.so
liba.so: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, BuildID[sha1]=915c865179f6bb09fe6c36c3649e816fb986a1e1, not stripped
2. Section和Segment
正确理解"Section"和"Segment"的概念有助于我们理解目标文件和可执行文件。从本质上讲,"Section"和"Segment"都是ELF文件中的一小块区域。"Section"是相对于目标文件的一个概念,一个目标文件由ELF头和若干Section组成,如目标文件中的代码段、数据段、BSS段等。"Segment"是相对于可执行文件的一个概念。我们知道,可执行文件是由多个目标文件链接起来的,链接器会将目标文件中具有相同性质的段合并到一起,如将各个目标文件中的.text段合并成一个.text段,将各个.data段合并成一个.data段。此时,这些合并后的.text段、.data段以Section的形式存放在可执行文件中。那么为什么要在可执行文件中引入Segment的概念呢?因为直接将可执行文件按Section为划分单位映射到虚拟内存空间会浪费大量的内存空间,因为虚拟内存是按页对齐的。而操作系统在将可执行文件加载到虚拟内存空间时,往往只关心每个Section的权限(可读、可写、可执行),于是大佬们决定把具有相同权限的Section合并成一个Segment映射到虚拟内存空间。因此,Segment的概念实际上是从装载的角度重新划分了ELF的各个Section,链接器会尽量将具有相同权限的Section分配在同一个空间,这些Section被称作为一个Segment,操作系统正是按照Segment来将可执行文件映射到虚拟内存空间的,而不是按Section。
3. 目标文件
目标文件是ELF格式的文件,整个目标文件被分成多个具有一定长度的区域,每个区域被称为段,这里的段指的是一个Section。例如,.text段就是目标文件中用来存放程序源代码的地方,这些源代码被翻译成机器指令存放在代码段。.data段是用来存放初始化的全局变量和局部静态变量的地方。
$ objdump -h SimpleSection.o # 参数 -h 表示把ELF文件的各个段的基本信息打印出来
# size命令也可以用来查看ELF文件的代码段、数据段和BSS段的长度,-A指定运行模式为"System V compatibility mode"
$ size -A SimpleSection.o
# 代码段
$ objdump -s -d SimpleSection.o # -s 表示将所有段的内容以16进制方式打印,-d将所有包含指令的段反汇编
3.1 ELF头
在linux中,ELF文件的最前面是ELF文件头,文件头的信息在32位机器中是由Elf32_Ehdr结构体来描述的,在64位机器上用Elf64_Ehdr结构体来描述。
$ readelf -h SimpleSection.o # 查看ELF文件头
$ hexdump -x SimpleSection.o -n 64 # hexdump查看二进制文件,-x表示以双字节16进制显示,-n显示指定字节
上图是64位机器下ELF文件头,共64个字节,下面对其中的部分选项进行说明。
- e_ident: 前4个字节0x7f454c46是ELF文件的魔数。第5个字节0x02表示64位(0x01表示32位),第6个字节0x01表示小端(0x02表示大端),第7个字节是ELF文件的主版本号,一般是1。后面9个字节未定义,填0
- e_type: ELF文件类型。1 可重定位文件,2 可执行文件, 3 共享目标文件
- e_entry: ELF程序的入口虚拟地址,操作系统从这个地址开始执行进程的指令。可重定位文件一般没有入口地址,则这个值为0
- e_shoff: 段表在文件中的偏移
- e_ensize: ELF头本身的大小
- e_shentsize: 段表描述符的大小,一般等于sizeof(Elf64_Shdr)
- e_shnum: 段表描述符数量,这个值等于ELF文件中拥有的段的数量
- e_shstrndx: 段表字符串表所在段在段表中的下标
3.2 段表
段表描述了ELF文件中各个段的信息,例如段名、段的长度、在文件中的偏移、读写权限等。段表中的每一项用一个结构体来描述,在64位机器中是Elf64_Shdr(32机器中是Elf32_Shdr),段表本质上就是一个数组,数组中的每一项是一个Elf64_Shdr(32机器中是Elf32_Shdr)结构体。
- sh_name: 段名在段表字符串表.shstrtab中的偏移,段名是一个字符串,保存在名为.shstrtab的字符串表中
- sh_type: 段的类型。NULL 0 无效段; PROGBITS 1 代码段、数据段;SYMTAB 2 符号表;STRTAB 3 字符串表;RELA 4 重定位表
- sh_flags: 段的标志位,表示该段在进程虚拟地址空间中的属性。WRITE 1 可写;ALLOC 2 需要在进程空间中为该段分配空间;EXECINSTR 4 可执行
- sh_addr: 如果该段可以被加载,则为该段被加载后在进程地址空间中的虚拟地址
- sh_offset: 段偏移
- sh_size: 段的长度
- sh_link, sh_info: 段链接信息
$ readelf -S SimpleSection.o # 查看段表
3.3 重定位表
重定位表用来保存与重定位相关的信息,它在ELF文件中往往是一个或多个段,每个需要被重定位的ELF段都有一个对应的重定位表,如代码段.txt的重定位表为.rel.text。重定位表本质上是一个Elf64_Rel(32机器中是Elf32_Rel)结构的数组。Elf64_Rel的大小为16字节(Elf32_Rel为8字节),有两个成员变量:r_offset、r_info。
- r_offset: 重定位入口的偏移。
- r_info: 重定位入口的类型和符号。低32位表示重定位的入口类型,高32位表示重定位的入口在符号表中的下标(32位机器中分别为低8位,高24位)
$ objdump -r SimpleSection.o # 查看目标文件中需要重定位的符号
3.4 字符串表
字符串表在ELF文件中也以段的形式保存,用来存储ELF文件中的段名、变量名等。常见的字符串表有两个,.strtab: 用来存储普通的字符串;.shstrtab: 用来保存段表中用到的字符串。在字符串表中,所有的字符串连续保存在表中,用空字符(’\0’)隔开,字符串表的第一个字符为空字符。在ELF文件中只需要给出字符串在字符串表中的首地址的索引,就能够在字符串表中获取到这个字符串。
3.5 符号表
符号表是ELF文件中的一个段,段名一般叫.symtab
- st_name: 符号名在字符串表中的下标
- st_info: 符号类型和绑定信息。低4位表示符号类型(NOTYPE 0 未知符号;OBJECT 1 变量、数组;FUNC 2 函数;SECTION 3 段;FILE 4 文件名)。高4位表示符号绑定信息(LOCAL 0 局部符号;GLOBAL 1 全局符号;WEAK 2 弱引用)
- st_shndx: 符号所在段。若符号存在于本目标文件中, st_shndx表示符号所在段在段表中的下标。
- st_value: 符号相对应的值。在目标文件中且该符号不是"COMMON块",st_value表示该符号在对应段中的偏移;在可执行文件中,st_value表示符号的虚拟地址
- st_size: 符号大小
4. 程序头表
程序头表是ELF可执行文件中的一个段,用来保存"Segment"的信息。ELF目标文件中没有程序头表。程序头表本质上是一个数组,数组元素是一个Elf64_Phdr(32为机器为Elf32_Phdr)结构体。
- p_type: Segment的类型。LOAD 1; DYNAMIC ; INTERP
- p_offset: Segment在文件中的偏移
- p_vaddr: Segment的第一个字节在进程虚拟地址空间的起始位置
- p_paddr: Segment的物理装载地址,一般和p_vaddr相同
- p_filesz: Segment在ELF文件中所占用的长度
- p_memsz: Segment在进程虚拟地址空间中所占用的长度
- p_flags: Segment的权限。可读 R;可写 W;可执行 X
- p_align: Segment的对齐属性。按2的p_align次字节对齐
【参考资料】 《程序员的自我修养:链接、装载与库》、linux源码