ELF的英文全称是The Executable and Linking Format,最初是由UNIX系统实验室开发、发布的ABI(Application Binary Interface)接口的一部分,也是Linux的主要可执行文件格式。
ELF文件种类:
- 可执行文件(.out):Executable File,包含代码和数据,是可以直接运行的程序。其代码和数据都有固定的地址 (或相对于基地址的偏移 ),系统可根据这些地址信息把程序加载到内存执行。
- 可重定位文件(.o文件):Relocatable File,包含基础代码和数据,但它的代码及数据都没有指定绝对地址,因此它适合于与其他目标文件链接来创建可执行文件或者共享目标文件。
- 共享目标文件(.so):Shared Object File,也称动态库文件,包含了代码和数据,这些数据是在链接时被链接器(ld)和运行时动态链接器(ld.so.l、libc.so.l、ld-linux.so.l)使用的。
ELF头部格式:
我们可以使用man 5 elf命令,查看第5页相关格式的定义和说明。
下面是我摘取并适当处理的64位ELF头格式相关定义:
typedef uint64_t Elf64_Addr;
typedef uint64_t Elf64_Off;
#define EI_NIDENT 16
typedef struct {
unsigned char e_ident[EI_NIDENT]; // 最开头是16个字节的e_ident, 其中包含用以表示ELF文件的字符,以及其他一些与机器无关的信息。开头的4个字节值固定不变,为0x7f和ELF三个字符。
uint16_t e_type; // 该文件的类型 2字节
uint16_t e_machine; // 该程序需要的体系架构 2字节
uint32_t e_version; // 文件的版本 4字节
Elf64_Addr e_entry; // 程序的入口地址 8字节
Elf64_Off e_phoff; // Program header table 在文件中的偏移量 8字节
Elf64_Off e_shoff; // Section header table 在文件中的偏移量 8字节
uint32_t e_flags; // 对IA32而言,此项为0。 4字节
uint16_t e_ehsize; // 表示ELF header大小 2字节
uint16_t e_phentsize; // 表示Program header table中每一个条目的大小 2字节
uint16_t e_phnum; // 表示Program header table中有多少个条目 2字节
uint16_t e_shentsize; // 表示Section header table中的每一个条目的大小 2字节
uint16_t e_shnum; // 表示Section header table中有多少个条目 2字节
uint16_t e_shstrndx; // 包含节名称的字符串是第几个节 2字节
} Elf64_Ehdr;
关于标识符e_ident 16个字节的值含义:
名称 位置 说明
EI_MAG0 0 文件标识(0x7f)
EI_MAG1 1 文件标识(E)
EI_MAG2 2 文件标识(L)
EI_MAG3 3 文件标识(F)
EI_CLASS 4 文件类,取值:0-非法,1-32位,2-64位
EI_DATA 5 数据编码,取值:0-非法,1-小端,2-大端
EI_VERSION 6 ELF头部版本
EI_PAD 7~15 补齐字节,一般为0
关于文件类型e_type取值:
0 NONE (未知目标文件格式)
1 REL (可重定位文件)
2 EXEC (可执行文件)
3 DYN (共享目标文件)
4 CORE (转储格式)
关于体系架构e_machine取值:
0 No machine
2 SPARC
3 Intel 80386
8 MIPS I Architecture
0x14 PowerPC
0x28 Advanced RISC Machines ARM
0x3e Advanced Micro Devices X86-64
关于文件版本e_version取值:
0 NONE (非法版本)
1 CURRENT (当前版本)
下面我们实际生成一个ELF64位的可执行文件,来实际分析其头部格式。
#include
#include
int main(int argc, char* argv[])
{
printf("hello\n");
return 0;
}
编译:
[develop@localhost test]$ gcc -o test test.c
使用readelf工具查看头部格式:
[develop@localhost test]$ readelf -h test
ELF 头:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
类别: ELF64
数据: 2 补码,小端序 (little endian)
版本: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
类型: EXEC (可执行文件)
系统架构: Advanced Micro Devices X86-64
版本: 0x1
入口点地址: 0x400430
程序头起点: 64 (bytes into file)
Start of section headers: 6496 (bytes into file)
标志: 0x0
本头的大小: 64 (字节)
程序头大小: 56 (字节)
Number of program headers: 9
节头大小: 64 (字节)
节头数量: 31
字符串表索引节头: 30
使用od工具查看二进制内容,根据ELF头格式解析(od用法太复杂,也可以用hexdump -C test -n 64):
[develop@localhost test]$ od -A x -t x1 test -N 64 -w16
000000 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
=》7f 45 4c 46为固定的魔法字节,02表示64位,第1个01表示小端序,第2个01表示ELF头版本。
000010 02 00 3e 00 01 00 00 00 30 04 40 00 00 00 00 00
=》02 00表示文件类型为EXE可执行,3e 00表示处理器体系结构为x86-64,01 00 00 00表示文件版本,30 04 40 00 00 00 00 00表示程序入口地址。
000020 40 00 00 00 00 00 00 00 60 19 00 00 00 00 00 00
=》40 00 00 00 00 00 00 00表示程序头偏移位置,60 19 00 00 00 00 00 00表示节头表偏移位置。
000030 00 00 00 00 40 00 38 00 09 00 40 00 1f 00 1e 00
=》00 00 00 00表示标志位,40 00表示本ELF头的大小,38 00表示每个程序头的大小,09 00表示程序头数量,40 00表示每个节头的大小,1f 00表示节头数量,1e 00表示字符串节头表索引号。
链接视图与执行视图:
现在我们已经对ELF头格式有了基本了解,接下来要继续看程序头和节头了。
先看一张图:
上图左边为链接视图,主要指前面ELF头部的节头表定义的节区分布,它指明了目标代码文件的内容布局。
上图右边为执行视图,主要指前面ELF头部的程序头表定义的段区分布,它指明了程序运行时的内存布局。
链接视图由节区组成,执行视图由要由段区组成,我们平时在进行程序构建的时候理解的.text、.bss、.data段,这些都是section,也就节区的概念,这些节区通过section headers table进行组织与重定位。而program headers table则组织了程序运行时的内存布局,它的真实内容由section填充,这里借用网络上的一张图来粗略地表达这层关系:
程序头格式:
继续摘取程序头格式定义:
typedef struct {
uint32_t p_type; // 当前Program header所描述的段的类型
uint32_t p_flags; // 与段相关的标志
Elf64_Off p_offset; // 段的第一个字节在文件中的偏移
Elf64_Addr p_vaddr; // 段的第一个字节在内存中的虚拟地址
Elf64_Addr p_paddr; // 在物理内存定位相关的系统中,此项是为物理地址保留
uint64_t p_filesz; // 段在文件中的长度
uint64_t p_memsz; // 段在内存中的长度
uint64_t p_align; // 根据此项值来确定段在文件及内存中如何对齐
} Elf64_Phdr;
程序头定义了可执行文件在装入内存后的段内存布局,每个程序头对应进程的一个内存段,程序头对于目标文件不是必须的,而且用gcc编译的.o文件确实没有程序头。
p_type取值:
0 PT_NULL 无意义,可忽略。
1 PT_LOAD 可加载段。段数据由文件映射到内存,如果 p_memsz 大于 p_filesz,则额外部分填充为 0。
2 PT_DYNAMIC 动态段。包含动态链接所需的信息。
3 PT_INTERP 本段包含一个路径字符串,该路径存放解释器。
4 PT_NOTE 注释段,包含一些辅助信息。
5 PT_SHLIB 保留的段类型,暂不关心。
6 PT_PHDR 程序头段。指明程序头表在文件和内存映像中的位置和大小。
p_flags取值:
1 PF_X 内存可执行。
2 PF_W 内存可写。
4 PF_R 内存可读。
使用readelf工具查看样例程序的程序头:
[develop@localhost test]$ readelf -l test
Elf 文件类型为 EXEC (可执行文件)
入口点 0x400430
共有 9 个程序头,开始于偏移量64
程序头:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040
0x00000000000001f8 0x00000000000001f8 R E 8
INTERP 0x0000000000000238 0x0000000000400238 0x0000000000400238
0x000000000000001c 0x000000000000001c R 1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x0000000000000704 0x0000000000000704 R E 200000
LOAD 0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
0x000000000000021c 0x0000000000000220 RW 200000
DYNAMIC 0x0000000000000e28 0x0000000000600e28 0x0000000000600e28
0x00000000000001d0 0x00000000000001d0 RW 8
NOTE 0x0000000000000254 0x0000000000400254 0x0000000000400254
0x0000000000000044 0x0000000000000044 R 4
GNU_EH_FRAME 0x00000000000005d8 0x00000000004005d8 0x00000000004005d8
0x0000000000000034 0x0000000000000034 R 4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 10
GNU_RELRO 0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
0x00000000000001f0 0x00000000000001f0 R 1
Section to Segment mapping:
段节...
00
01 .interp
02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame
03 .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss
04 .dynamic
05 .note.ABI-tag .note.gnu.build-id
06 .eh_frame_hdr
07
08 .init_array .fini_array .jcr .dynamic .got
节头格式:
摘取节头格式定义:
typedef struct {
uint32_t sh_name; // 节区名称相对于字符串表的位置偏移
uint32_t sh_type; // 节区类型
uint64_t sh_flags; // 节区标志位集合
Elf64_Addr sh_addr; // 节区装入内存的地址
Elf64_Off sh_offset; // 节区相对于文件的位置偏移
uint64_t sh_size; // 节区内容大小
uint32_t sh_link; // 指定链接的节索引,与具体的节有关
uint32_t sh_info; // 指定附加信息
uint64_t sh_addralign; // 节装入内存的地址对齐要求
uint64_t sh_entsize; // 指定某些节的固定表大小,与具体的节有关
} Elf64_Shdr;
节头定义了目标文件的代码及数据在文件中的位置。
sh_type取值:
0 SHT_NULL 无对应节区,该节其他字段取值无意义
1 SHT_PROGBITS 程序数据
2 SHT_SYMTAB 符号表
3 SHT_STRTAB 字符串表
4 SHT_RELA 带附加的重定位项
5 SHT_HASH 符号哈希表
6 SHT_DYNAMIC 动态链接信息
7 SHT_NOTE 提示性信息
8 SHT_NOBITS 无数据程序空间(bss)
9 SHT_REL 无附加的重定位项
10 SHT_SHLIB 保留
11 SHT_DYNSYM 动态链接符号表
14 SHT_INIT_ARRAY 构造函数数组
15 SHT_FINI_ARRAY 析构函数数组
16 SHT_PREINIT_ARRAY
17 SHT_GROUP
18 SHT_SYMTAB_SHNDX
19 SHT_NUM
0x70000000 SHT_LOPROC
0x7fffffff SHT_HIPROC
0x80000000 SHT_LOUSER
0x8fffffff SHT_HIUSER
sh_flags取值:
1 SHF_WRITE
2 SHF_ALLOC
4 SHF_EXECINSTR
8 SHF_MASKPROC
使用readelf工具查看样例程序的节区表:
[develop@localhost test]$ readelf -S test
共有 31 个节头,从偏移量 0x1960 开始:
节头:
[号] 名称 类型 地址 偏移量
大小 全体大小 旗标 链接 信息 对齐
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000400238 00000238
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.ABI-tag NOTE 0000000000400254 00000254
0000000000000020 0000000000000000 A 0 0 4
[ 3] .note.gnu.build-i NOTE 0000000000400274 00000274
0000000000000024 0000000000000000 A 0 0 4
[ 4] .gnu.hash GNU_HASH 0000000000400298 00000298
000000000000001c 0000000000000000 A 5 0 8
[ 5] .dynsym DYNSYM 00000000004002b8 000002b8
0000000000000060 0000000000000018 A 6 1 8
[ 6] .dynstr STRTAB 0000000000400318 00000318
000000000000003d 0000000000000000 A 0 0 1
[ 7] .gnu.version VERSYM 0000000000400356 00000356
0000000000000008 0000000000000002 A 5 0 2
[ 8] .gnu.version_r VERNEED 0000000000400360 00000360
0000000000000020 0000000000000000 A 6 1 8
[ 9] .rela.dyn RELA 0000000000400380 00000380
0000000000000018 0000000000000018 A 5 0 8
[10] .rela.plt RELA 0000000000400398 00000398
0000000000000030 0000000000000018 AI 5 24 8
[11] .init PROGBITS 00000000004003c8 000003c8
000000000000001a 0000000000000000 AX 0 0 4
[12] .plt PROGBITS 00000000004003f0 000003f0
0000000000000030 0000000000000010 AX 0 0 16
[13] .plt.got PROGBITS 0000000000400420 00000420
0000000000000008 0000000000000000 AX 0 0 8
[14] .text PROGBITS 0000000000400430 00000430
0000000000000182 0000000000000000 AX 0 0 16
[15] .fini PROGBITS 00000000004005b4 000005b4
0000000000000009 0000000000000000 AX 0 0 4
[16] .rodata PROGBITS 00000000004005c0 000005c0
0000000000000016 0000000000000000 A 0 0 8
[17] .eh_frame_hdr PROGBITS 00000000004005d8 000005d8
0000000000000034 0000000000000000 A 0 0 4
[18] .eh_frame PROGBITS 0000000000400610 00000610
00000000000000f4 0000000000000000 A 0 0 8
[19] .init_array INIT_ARRAY 0000000000600e10 00000e10
0000000000000008 0000000000000008 WA 0 0 8
[20] .fini_array FINI_ARRAY 0000000000600e18 00000e18
0000000000000008 0000000000000008 WA 0 0 8
[21] .jcr PROGBITS 0000000000600e20 00000e20
0000000000000008 0000000000000000 WA 0 0 8
[22] .dynamic DYNAMIC 0000000000600e28 00000e28
00000000000001d0 0000000000000010 WA 6 0 8
[23] .got PROGBITS 0000000000600ff8 00000ff8
0000000000000008 0000000000000008 WA 0 0 8
[24] .got.plt PROGBITS 0000000000601000 00001000
0000000000000028 0000000000000008 WA 0 0 8
[25] .data PROGBITS 0000000000601028 00001028
0000000000000004 0000000000000000 WA 0 0 1
[26] .bss NOBITS 000000000060102c 0000102c
0000000000000004 0000000000000000 WA 0 0 1
[27] .comment PROGBITS 0000000000000000 0000102c
000000000000005a 0000000000000001 MS 0 0 1
[28] .symtab SYMTAB 0000000000000000 00001088
0000000000000600 0000000000000018 29 47 8
[29] .strtab STRTAB 0000000000000000 00001688
00000000000001c9 0000000000000000 0 0 1
[30] .shstrtab STRTAB 0000000000000000 00001851
000000000000010c 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)
最后:
由于ELF非常复杂,关于具体的节区内容的进一步展示,后续再补充。