ELF文件结构描述
一个 ELF 文件通常有ELF文件头、段表、字符串表、符号表等组成,其总体结构如下图所示。不同的文件部分描述不同的文件内容:
- ELF文件头(ELF Header),它包含了描述整个文件的基本属性,如ELF文件版本、程序入口地址等。
- ELF 文件中的各个段(section)。段表(section header table)描述了 ELF 文件包含的所有段的信息,包含每个段的段名、段的长度、在文件中的偏移量等属性。
- 其他一些辅助结构,如字符串表,符号表等。
ELF Header
在ELF文件头中定义了ELF魔数、文件机器字节长度、数据存储方式、版本、运行平台、ABI版本、ELF重定位类型、硬件平台、硬件平台版本、入口地址、程序头入口和长度、段表的位置和长度及段的数量等。我们可以使用readelf
命令来详细查看ELF文件:
1 | $ readelf -h SimpleSection.o |
ELF文件头结构及相关常数被定义在/usr/include/elf.h
中,为了兼容各种平台ELF文件有32位版本和64位版本,分别叫做Elf32_Ehdr
、Elf64_Ehdr
。同时elf.h
使用typedef定义了一套自己的变量体系。
1 | /* Type for a 16-bit quantity. */ |
1 |
|
ELF魔数
我们可以看到readelf的输出中最前面的Magic
的16个字节刚好对应Elf64_Ehdr
的e_ident
这个成员。这16个字节用来标识ELF文件的平台属性,比如ELF字长、字节序、ELF文件版本等。
1 | Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 |
最开始的4个字节是所有ELF文件都必须相同的标识码,分别为0x7F
、0x45
、0x4c
、0x46
,第一个字节对应于ASCII字符里的DEL
控制符,后面三个字对应ELF这3个字母的ASCII码。这四个字节又被称为ELF文件的魔数。通过对魔数的判断可以确定文件的格式和类型。如果被执行的是Shell脚本或perl、python等解释型语言的脚本,那么它的第一行往往是#!/bin/sh
或#!/usr/bin/perl
或#!/usr/bin/python
,此时前两个字节#
和!
就构成了魔数,系统一旦判断到这两个字节,就对后面的字符串进行解析,以确定具体的解释程序路径。
接下来的一个字节是用来标识ELF的文件类的,0x01
、表示是32位,0x02
表示是64位;第6个字是字节序,规定该文件是大端还是小端。第7个字节规定ELF文件的主版本号,一般是1。后面9个字节ELF标准没有定义,一般填0。
ELF Section Header Table
ELF 段表保存了各个段的基本属性,是除了文件头之外最重要的结构,它描述了ELF中各个段的信息,如段名、段的大小、在文件中的偏移、读写权限等。编译器、链接器、装载器都是通过段表来定位和访问各个段的属性的。
我们可以使用readelf工具来查看段表。
1 | $ readelf -S SimpleSection.o |
段表的结构比较简单,它是一个以Elf64_Shdr
结构体为元素的数组。数组的元素个数等于段的个数,每个Elf64_Shdr
结构体对应一个段。Elf64_Shdr
又被称为段描述符(Section Descriptor)。对于SimpleSection.o
来说,段表就是有13个元素的数组,其中第一个元素是类型为NULL
的无效段描述。
1 | typedef struct |
段的类型
段的名字只是在链接和编译过程中有意义,但它并不能真正地表示段的类型。对于编译器和链接器来说,主要决定段的属性是段的类型(sh_type
)和段的标志位(sh_flags
)。
段的类型相关常量以SHT_
开头,常见的段类型如下表所示:
常量 | 值 | 含义 |
---|---|---|
SHT_NULL | 0 | 无效段 |
SHT_PROGBITS | 1 | 程序段。代码段、数据段都是这种类型。 |
SHT_SYMTAB | 2 | 符号表 |
SHT_STRTAB | 3 | 字符串表 |
SHT_RELA | 4 | 重定位表。该段包含了重定位信息。 |
SHT_HASH | 5 | 符号表的哈希表 |
SHT_DYNAMIC | 6 | 动态链接信息 |
SHT_NOTE | 7 | 提示性信息 |
SHT_NOBITS | 8 | 表示该段在文件中没有内容。如`.bss段 |
SHT_REL | 9 | 该段包含了重定位信息 |
SHT_SHLIB | 10 | 保留 |
SHT_DNYSYM | 11 | 动态链接的符号表 |
段标志位(sh_flag)
段标志位表示该段在进程虚拟地址空间中的属性。如是否可写、是否可执行等。相关常量以SHF_
开头。常见的段标志位如下表所示:
常量 | 值 | 含义 |
---|---|---|
SHF_WRITE | 1 | 表示该段在进程空间中可写 |
SHF_ALLOC | 2 | 表示该段在进程空间中需要分配空间。有些包含指示或控制信息的段不需要在进程空间中分配空间,就不会有这个标志。像代码段、数据段和.bss段都会有这个标志位。 |
SHF_EXECINSTR | 4 | 表示该段在进程空间中可以被执行,一般指代码段 |
段链接信息(sh_link、sh_info)
如果段的类型是与链接相关的(无论是动态链接还是静态链接),如重定位表、符号表、等,则sh_link
、sh_info
两个成员所包含的意义如下所示。其他类型的段,这两个成员没有意义。
sh_type | sh_link | sh_info |
---|---|---|
SHT_DYNAMIC | 该段所使用的字符串表在段表中的下标 | 0 |
SHT_HASH | 该段所使用的符号表在段表中的下标 | 0 |
SHT_REL | 该段所使用的相应符号表在段表中的下标 | 该重定位表所作用的段在段表中的下标 |
SHT_RELA | 该段所使用的相应符号表在段表中的下标 | 该重定位表所作用的段在段表中的下标 |
SHT_SYMTAB | 操作系统相关 | 操作系统相关 |
SHT_DYNSYM | 操作系统相关 | 操作系统相关 |
other | SHN_UNDEF | 0 |
重定位表
链接器在处理目标文件时须要对目标文件中某些部位进行重定位,即代码段和数据段中那些对绝对地址的引用的位置。对于每个需要重定位的代码段和数据段都会有一个相应的重定位表。一个重定位表同时也是ELF的一个段,那么这个段的类型就是SHT_REL
或者SHT_RELA
,它的sh_link
表示符号表的下标,它的sh_info
表示它作用于哪一个段。比如.rela.text
作用于.text
段,而.text
的下标为1,那么.rela.text
的sh_info
为1。
字符串表
ELF文件中用到了很多字符串,比如段名、变量名等。因为字符串的长度往往是不定的,所以用固定的结构来表示它比较困难。一种常见的做法是把字符串集中起来存放到一个表,然后使用字符串在表中的偏移来引用字符串。比如下述这个字符串表:
偏移 | +0 | +1 | +2 | +3 | +4 | +5 | +6 | +7 | +8 | +9 |
---|---|---|---|---|---|---|---|---|---|---|
+0 | \0 | h | e | l | l | o | w | o | r | l |
+10 | d | \0 | M | y | v | a | r | i | a | b |
+20 | l | e | \0 |
那么偏移与它们对应的字符串如下表所示:
偏移 | 字符串 |
---|---|
0 | 空字符串 |
1 | helloworld |
6 | world |
12 | Myvariable |
符号表
ELF文件中的符号表往往是文件中的一个段,段名一般叫.symtab
。符号表的结构是一个Elf64_Sym
结构(64位ELF文件)的数组,每个Elf64_Sym
结构对应一个符号。其结构定义如下:
1 | typedef struct { |
我们可以利用readelf
来查看ELF文件的符号。
1 | $ readelf -s SimpleSection.o |
readelf
的输出格式与上面描述的Elf64_Sym
的各个成员几乎一一对应,第一列Num表示符号表数组的下标,从0开始,共17个符号;第二列Value就是符号值,即st_value
;第三列Size为符号大小,即st_size
;第四列和第五列分别为符号类型和绑定信息;第六列Vis在C/C++中未使用,第七列Ndx即st_shndx
,表示该符号所属的段;最后一列即符号名称。对于另外的符号解释如下:
func1
和main
函数都是定义在SimpleSection.c
里面的,它们所在的位置都为代码段,所以Ndx为1,即SimpleSection.o
里面的.text
。他们是函数,所以类型是STT_FUNC
;它们是全局可见的,所以是STB_GLOBAL
;Size表示函数指令所占的字节数;Value表示函数相对于该段起始位置的偏移量。printf
这个符号在SimpleSection.c
里面被引用,但是没有被定义。所以它的Ndx是SHN_UNDEF
。(具体含义可参考ELF手册)global_init_var
是已初始化的全局变量,它被定义在.bss
段,即下标为3。global_uninit_var
是未初始化的全局变量,它是一个SHN_COMMON
类型的符号。static_var.1533
和static_var2.1534
是两个静态变量(符号修饰导致变量名变化),它们的绑定属性是STB_LOCAL
,即只是编译单元内部可见。STT_SECTION
类型的符号表示下标为Ndx的段的段名(符号名未显示)。SimpleSection.c
表示编译单元的源文件名。
参考资料
- Executable and Linkable Format
- 《程序员的自我修养——链接、装载与库》
- ELF man page
- 本文标题:ELF
- 创建时间:2023-09-18 00:02:27
- 本文链接:2023/09/18/ELF/
- 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!