ELF文件格式
ELF可执行可链接格式,最初由UNIX系统实验室作为应用程序二进制接口的一部分制定,是COFF(common file format)的变种,相关定义在/usr/include/elf.h文件中。
ELF 文件类型
说明:linux和windows识别文件格式不同,linux是根据文件头的字段,windows直接根据文件名后缀。
主要分为三类:
- 可执行文件executable file:经过链接、可执行的目标文件,也被称为程序。
- 可重定位文(relocatable file):由源文件编译但是没有链接的目标文件,通常以.o作为拓展名。用于与其他文件链接构成可执行文件或动态链接库,通常是一段位置独立的代码(Position Independent code, PIC)
- 共享目标文件(shared object file):动态链接库文件,用于在链接的过程中与其他动态链接库或可重定位文件一起构建可执行文件。
除此之外,还存在 核心转储文件(core dump file)作为进程以外终止时地址空间的转储,也是ELF文件的一种。可以使用gdb工具读取辅助调试和查找程序崩溃的原因。
在内核中的定义(/include/uapi/linux/elf.h):
#define ET_REL 1
#define ET_EXEC 2
#define ET_DYN 3
#define ET_CORE 4
ELF文件格式
总体布局
ELF文件格式提供了两种视图,分别是链接视图和执行视图。链接视图是以节(section)为单位,执行视图是以段(segment)为单位。接视图就是在链接时用到的视图,而执行视图则是在执行时用到的视图。

可以使用readelf -l hello查看一个链接后的elf可执行文件,Section to Segment 的映射关系
Elf 文件类型为 EXEC (可执行文件)
Entry point 0x401cc0
There are 8 program headers, starting at offset 64
程序头:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x0000000000000488 0x0000000000000488 R 0x1000
LOAD 0x0000000000001000 0x0000000000401000 0x0000000000401000
0x000000000007fab1 0x000000000007fab1 R E 0x1000
LOAD 0x0000000000081000 0x0000000000481000 0x0000000000481000
0x0000000000026e0b 0x0000000000026e0b R 0x1000
LOAD 0x00000000000a8000 0x00000000004a9000 0x00000000004a9000
0x00000000000052f0 0x0000000000006b20 RW 0x1000
NOTE 0x0000000000000200 0x0000000000400200 0x0000000000400200
0x0000000000000044 0x0000000000000044 R 0x4
TLS 0x00000000000a8000 0x00000000004a9000 0x00000000004a9000
0x0000000000000020 0x0000000000000060 R 0x8
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x00000000000a8000 0x00000000004a9000 0x00000000004a9000
0x0000000000003000 0x0000000000003000 R 0x1
Section to Segment mapping:
段节...
00 .note.gnu.build-id .note.ABI-tag .rela.plt
01 .init .plt .text __libc_freeres_fn .fini
02 .rodata .eh_frame .gcc_except_table
03 .tdata .init_array .fini_array .data.rel.ro .got .got.plt .data __libc_subfreeres __libc_IO_vtables __libc_atexit .bss __libc_freeres_ptrs
04 .note.gnu.build-id .note.ABI-tag
05 .tdata .tbss
06
07 .tdata .init_array .fini_array .data.rel.ro .got
下面的段序号和上面程序头里的段一一对应。
文件结构
主要从链接的角度看elf文件。
elf头
描述文件的一些基本信息,文件头部存在魔术字符7f 45 4c 46即字符串"\177ELF",当文件被映射到内存时可以通过该字符串查找位置,dump内存。
readelf -h relocfile //查看文件头信息
ELF 头:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
类别: ELF64
数据: 2 补码,小端序 (little endian)
Version: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
类型: REL (可重定位文件)
系统架构: Advanced Micro Devices X86-64
版本: 0x1
入口点地址: 0x0
程序头起点: 0 (bytes into file)
Start of section headers: 816 (bytes into file)
标志: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 13
Section header string table index: 12
typedef struct elf32_hdr
{
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;
程序头表(program header table)
列举了所有有效的段(segments)和他们的属性(执行视图),链接视图是可选的,一般没有。
程序头是一个结构的数组,每一个结构都表示一个段(segments)。在可执行文件或者共享链接库中所有的节(sections)都被分为不同的几个段(segments)。
typedef struct elf32_phdr{
Elf32_Word p_type; /* Magic number and other info */
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;
程序头的索引地址(e_phoff)、段数量(e_phnum)、表项大小(e_phentsize)都是通过 ELF头部信息获取的。
可通过readelf -l 读取ELF程序头信息:
节头表(section header table)
一个ELF文件中到底有哪些具体的 sections,由包含在这个ELF文件中的 section head table(SHT)决定。每个section描述了这个段的信息,比如每个段的段名、段的长度、在文件中的偏移、读写权限及段的其它属性。
typedef struct elf32_shdr{
Elf32_Word sh_name; //节区名,名字是一个 NULL 结尾的字符串。
Elf32_Word sh_type; //为节区类型
Elf32_Word sh_flags; //节区标志
Elf32_Addr sh_addr; //节区的第一个字节应处的位置。否则,此字段为 0。
Elf32_Off sh_offset; //此成员的取值给出节区的第一个字节与文件头之间的偏移。
Elf32_Word sh_size; //此 成 员 给 出 节 区 的 长 度 ( 字 节 数 )。
Elf32_Word sh_link; //此成员给出节区头部表索引链接。其具体的解释依赖于节区类型。
Elf32_Word sh_info; //此成员给出附加信息,其解释依赖于节区类型。
Elf32_Word sh_addralign; //某些节区带有地址对齐约束.
Elf32_Word sh_entsize; //给出每个表项的长度字节数。
}Elf32_Shdr;
节区名存储在.shstrtab字符串表中,sh_name是表中偏移。
可通过readelf -S 读取sections' header:
There are 29 section headers, starting at offset 0xbf0f8:
节头:
[号] 名称 类型 地址 偏移量
大小 全体大小 旗标 链接 信息 对齐
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .note.gnu.bu[...] NOTE 0000000000400200 00000200
0000000000000024 0000000000000000 A 0 0 4
[ 2] .note.ABI-tag NOTE 0000000000400224 00000224
0000000000000020 0000000000000000 A 0 0 4
[ 3] .rela.plt RELA 0000000000400248 00000248
0000000000000240 0000000000000018 AI 0 18 8
[ 4] .init PROGBITS 0000000000401000 00001000
0000000000000017 0000000000000000 AX 0 0 4
[ 5] .plt PROGBITS 0000000000401018 00001018
00000000000000c0 0000000000000000 AX 0 0 8
[ 6] .text PROGBITS 00000000004010e0 000010e0
000000000007ef60 0000000000000000 AX 0 0 16
[ 7] __libc_freeres_fn PROGBITS 0000000000480040 00080040
0000000000000a66 0000000000000000 AX 0 0 16
[ 8] .fini PROGBITS 0000000000480aa8 00080aa8
0000000000000009 0000000000000000 AX 0 0 4
[ 9] .rodata PROGBITS 0000000000481000 00081000
000000000001c61c 0000000000000000 A 0 0 32
[10] .eh_frame PROGBITS 000000000049d620 0009d620
000000000000a71c 0000000000000000 A 0 0 8
[11] .gcc_except_table PROGBITS 00000000004a7d3c 000a7d3c
00000000000000cf 0000000000000000 A 0 0 1
[12] .tdata PROGBITS 00000000004a9000 000a8000
0000000000000020 0000000000000000 WAT 0 0 8
[13] .tbss NOBITS 00000000004a9020 000a8020
0000000000000040 0000000000000000 WAT 0 0 8
[14] .init_array INIT_ARRAY 00000000004a9020 000a8020
0000000000000010 0000000000000008 WA 0 0 8
[15] .fini_array FINI_ARRAY 00000000004a9030 000a8030
0000000000000010 0000000000000008 WA 0 0 8
[16] .data.rel.ro PROGBITS 00000000004a9040 000a8040
0000000000002ed4 0000000000000000 WA 0 0 32
[17] .got PROGBITS 00000000004abf18 000aaf18
00000000000000e0 0000000000000000 WA 0 0 8
[18] .got.plt PROGBITS 00000000004ac000 000ab000
00000000000000d8 0000000000000008 WA 0 0 8
[19] .data PROGBITS 00000000004ac0e0 000ab0e0
0000000000001a50 0000000000000000 WA 0 0 32
[20] __libc_subfreeres PROGBITS 00000000004adb30 000acb30
0000000000000048 0000000000000000 WA 0 0 8
[21] __libc_IO_vtables PROGBITS 00000000004adb80 000acb80
0000000000000768 0000000000000000 WA 0 0 32
[22] __libc_atexit PROGBITS 00000000004ae2e8 000ad2e8
0000000000000008 0000000000000000 WA 0 0 8
[23] .bss NOBITS 00000000004ae300 000ad2f0
0000000000001800 0000000000000000 WA 0 0 32
[24] __libc_freer[...] NOBITS 00000000004afb00 000ad2f0
0000000000000020 0000000000000000 WA 0 0 8
[25] .comment PROGBITS 0000000000000000 000ad2f0
0000000000000027 0000000000000001 MS 0 0 1
[26] .symtab SYMTAB 0000000000000000 000ad318
000000000000b3e8 0000000000000018 27 765 8
[27] .strtab STRTAB 0000000000000000 000b8700
00000000000068cf 0000000000000000 0 0 1
[28] .shstrtab STRTAB 0000000000000000 000befcf
0000000000000128 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
| sh_name | sh_type | description |
|---|---|---|
| .text | SHT_PROGBITS | 代码段,包含程序的可执行指令 |
| .data | SHT_PROGBITS | 包含初始化了的数据,将出现在程序的内存映像中 |
| .bss | SHT_NOBITS | 未初始化数据,因为只有符号所以 |
| .rodata | SHT_PROGBITS | 包含只读数据 |
| .comment | SHT_PROGBITS | 包含版本控制信息 |
| .eh_frame | SHT_PROGBITS | 它生成描述如何unwind 堆栈的表 |
| .debug | SHT_PROGBITS | 此节区包含用于符号调试的信息 |
| .dynsym | SHT_DYNSYM | 此节区包含了动态链接符号表 |
| .shstrtab | SHT_STRTAB | 存放section名,字符串表。Section Header String Table |
| .strtab | SHT_STRTAB | 字符串表 |
| .symtab | SHT_SYMTAB | 符号表 |
| .got | SHT_PROGBITS | 全局偏移表 |
| .plt | SHT_PROGBITS | 过程链接表 |
| .relname | SHT_REL | 包含了重定位信息,例如 .text 节区的重定位节区名字将是:.rel.text |
1).text 代码段
可以通过objdump -d 反汇编,查看ELF文件代码段内容。
2).strtab / .shstrtab 字符串表
在ELF文件中,会用到很多字符串,比如节名,变量名等。所以ELF将所有的字符串集中放到一个表里,每一个字符串以’\0’分隔,然后使用字符串在表中的偏移来引用字符串。这样在ELF中引用字符串只需要给出一个数组下标即可。字符串表在ELF也以段的形式保存, .shstrtab是专供section name的字符串表。
可以用一下命令查看:readelf -S xxx.o
Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .text PROGBITS 0000000000000000 00000040 0000000000000045 0000000000000000 AX 0 0 1 [ 2] .rela.text RELA 0000000000000000 00000658 0000000000000048 0000000000000018 12 1 8 [ 3] .data PROGBITS 0000000000000000 00000085 0000000000000000 0000000000000000 WA 0 0 1 … … [11] .shstrtab STRTAB 0000000000000000 00000108 0000000000000074 0000000000000000 0 0 1 [12] .symtab SYMTAB 0000000000000000 00000500 0000000000000138 0000000000000018 13 10 8 [13] .strtab STRTAB 0000000000000000 00000638 000000000000001e 0000000000000000 0 0 1
3).symtab 符号表
在链接的过程中需要把多个不同的目标文件合并在一起,不同的目标文件相互之间会引用变量和函数。在链接过程中,我们将函数和变量统称为符号,函数名和变量名就是符号名。
每个定义的符号都有一个相应的值,叫做符号值(Symbol Value),对于变量和函数,符号值就是它们的地址。
可以使用下面命令查看:readelf -s xxx.o

4).eh_frame / .eh_frame_hdr
在调试程序的时候经常需要进行堆栈回溯,早期使用通用寄存器(ebp)来保存每层函数调用的栈帧地址,但局限性很大。后来现代Linux操作系统在LSB(Linux Standard Base)标准中定义了一个.eh_frame section,用来描述如何去unwind the stack。gcc编译器默认打开,如果不想把.eh_frame section编入elf文件,可以通过gcc选项 -fno-asynchronous-unwind-tables 去除。
GAS(GCC Assembler)汇编编译器定义了一组伪指令来协助eh_frame生成调用栈信息CFI(Call Frame Information)。具体原理在后续《栈回溯》章节分析,这里不再阐述。
5)重定位表(.relname)
链接器在处理目标文件时,需要对目标文件中的某些部位进行重定位,即代码段和数据中中那些绝对地址引用的位置。对于每个需要重定位的代码段或数据段,都会有一个相应的重定位表。比如”.rel.text”就是针对”.text”的重定位表,”.rel.data”就是针对”.data”的重定位表。
GOT是全局偏移表( Global Offset Table),用于存储外部符号地址;PLT是程序链接表(Procedure Link Table),用于存储记录定位信息的额外代码。关于linux动态链接重定位原理,可以参考海枫写的《Linux动态链接中的PLT和GOT》。
相关命令
readelf -S relocfile //查看节头表objdump -x -s -d relocfile //查看.text汇编readelf -x .strtab relocfile //查看字符串表readelf -r file //查看重定位表
可执行文件的装载
当运行一个可执行文件时,首先需要将该文件和动态链接库装载到进程空间,形成一个进程镜像。
每个进程都有独立的虚拟地址空间,空间的布局由程序头(program header)决定,通过readelf -l file可以看到节到段的映射,以及段的地址空间布局。每一个段都包含一个或多个节,因为随着节数量的增多,在进行内存映射时就会大量浪费空间和资源。系统并不关心节的实际内容,而是不同节的读写、执行的权限。

浙公网安备 33010602011771号