ELF文件是什么?
ELF是Executable and Linkable Format的缩写,字面上看就是可执行和可连接文件。在Linux下可重定位文件(.o)、可执行文件、共享目标文件(.so)、核心转储问文件(core dump) 都是使用ELF文件格式。
ELF 通常由编译器或者连接器产生,并且是二进制格式,使用一些工具可以更好的观察它的结构,如readelf、objdump
ELF由什么组成
ELF文件由ELF头(ELF header)、程序头表(Program header table)、节(Section)和节头表(Section header table)4个部分组成。
除了ELF头,ELF文件不一定包含其他部分,且位置分布也不一定和上图一致。
分析ELF前置准备
int printf(const char *, ...);
int global_init_var = 84;
int global_uninit_var;
void func1(int i){
printf("func1 %d", i);
}
int main(){
static int static_var = 5;
static int static_var2;
int a = 1;
int b;
func1(11 + 22 + 33 + a - b);
return a;
}
后续会使用上面的代码产生的可重定位文件进行ELF文件的分析,产生可重定位文件的方式是gcc -c
ELF Header
ELF头的结构定义在elf.h的ELF32_Ehdr/ELF64_Ehdr中,这边以Elf64_Ehdr为例子
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
Elf64_Half e_type; /* Object file type */
Elf64_Half e_machine; /* Architecture */
Elf64_Word e_version; /* Object file version */
Elf64_Addr e_entry; /* Entry point virtual address */
Elf64_Off e_phoff; /* Program header table file offset */
Elf64_Off e_shoff; /* Section header table file offset */
Elf64_Word e_flags; /* Processor-specific flags */
Elf64_Half e_ehsize; /* ELF header size in bytes */
Elf64_Half e_phentsize; /* Program header table entry size */
Elf64_Half e_phnum; /* Program header table entry count */
Elf64_Half e_shentsize; /* Section header table entry size */
Elf64_Half e_shnum; /* Section header table entry count */
Elf64_Half e_shstrndx; /* Section header string table index */
} Elf64_Ehdr;
使用readelf对之前的.o文件其进行分析
>> readelf -h main.o
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 1048 (bytes into file)
Flags: 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
将上述输出与Elf64_Ehdr结构进行对应
字段 | 对应Elf64_Ehdr的结构 | 描述 |
---|---|---|
Magic | e_ident | 前4个字节固定是 7F 45 4C 46; 45 4C 46 是ELF的ascii码; |
Class | e_ident[4] | 01为32位,02为64位 |
Data | e_ident[5] | 01 是小端序, 02 是大端序 |
Version | e_ident[6] | 表示ELF的版本号,不过一般都为1,因为ELF1.2版本后到目前都没有更新 |
OS/ABI | e_ident[7] | 表示使用的ABI类型,不过一般情况下是0, 也就是UNIX - System V |
ABI VERSION | e_ident[8] | 表示使用的ABI的版本,一般情况下是0 |
Type | e_type | 表示ELF的文件类型,ET_REL(1)为可重定位文件,一般是.o文件;ET_EXEC(2)为可执行文件;ET_DYN(3)一般为.so文件; ET_CORE(4) 为core file,也就是core dump 产生的文件 |
Machine | e_machine | 常量定义也在elf.h中,数量得有点多,其中EM_x86_64 为62 |
Version | e_version | 意义和上面的Version一致 |
Entry point address | e_entry | 表示程序执行的入口地址,因为这边是.o文件所以该值为0 |
Start of program headers | e_phoff | 表示Program Header的入口偏移量 |
Start of section headers | e_shoff | 表示Section Header的入口偏移量 |
Flags | e_flags | 表示ELF文件相关的特定处理器的flag |
Size of this header | e_ehsize | 表示ELF Header大小, 当前header的大小就是64 字节 |
Size of program headers | e_phentsize | 表示Program Header大小 |
Size of section headers | 实际上这里指的是Elf64_Shdr这个结构的大小 | |
Number of section headers | e_shnum | 表示Section Header的个数 |
Section header string table index | e_shstrndx | 段表字符串表所在段在段表中的下标 |
这边还可以使用hexdump 来查看.o文件的前64个字节来对比readelf的输出
>>hexdump -C -n 64 main.o
00000000 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 |.ELF............|
00000010 01 00 3e 00 01 00 00 00 00 00 00 00 00 00 00 00 |..>.............|
00000020 00 00 00 00 00 00 00 00 18 04 00 00 00 00 00 00 |................|
00000030 00 00 00 00 40 00 00 00 00 00 40 00 0d 00 0c 00 |....@.....@.....|
00000040
段表Section Header Table
ELF中包含各种各样的段,而段表就是描述这些段的基本属性的结构,如段名、段长、段在文件中的偏移、读写权限等。而段表所在的位置由Elf64_Ehdr中的e_shoff来决定。换句话说编译器、加载器、链接器等都是依靠ELF文件的段表来访问各个段的。
这边先看段表的结构,实际上段表是一个以Elf64_Shdr为元素的数组,而Elf64_Shdr结构如下:
typedef struct
{
Elf64_Word sh_name; // section_name段名,实际上名字存储在字符串表'.shstrtab'中,这边存的是段名在字符串表中的下标
Elf64_Word sh_type; //section type 段的类型,具体枚举后续会有
Elf64_Xword sh_flags; //section flag 段的标志位,具体枚举后续会有
Elf64_Addr sh_addr; //section addr 段的虚拟地址,如果该段可被加载,则会被加载到地址空间的对应虚拟地址中
Elf64_Off sh_offset; // section offset 段在文件中的偏移
Elf64_Xword sh_size; // section size 段的长度
Elf64_Word sh_link; // section Link 段的链接信息
Elf64_Word sh_info; // section Info 段的信息
Elf64_Xword sh_addralign; // Section alignment 段对齐长度,某些段会对地址对齐有要求
Elf64_Xword sh_entsize; // Entry size if section holds table 某些段会有固定大小的项,这些项所占用的大小是一样的。如果为0,则不没有包含这种项
} Elf64_Shdr;
section type
段的类型相关常量都是以SHT_开头,以下是常用的段以及对应的描述
常量 | 值 | 描述 |
---|---|---|
SHT_NULL | 0 | 无效 |
SHT_PROGBITS | 1 | 代码段、数据段都是这个类型 |
SHT_SYMTAB | 2 | 该段内容是符号表 |
SHT_STRTAB | 3 | 该段内容是字符串表 |
SHT_RELA | 4 | 重定位表,该段包含重定位信息 |
SHT_HASH | 5 | 符号表的哈希表 |
SHT_DYNMIC | 6 | 动态链接信息表 |
SHT_NOTE | 7 | 提示性信息 |
SHT_NOBITS | 8 | 表示该段在文件中不存在内容,.bss段就是该类型的 |
SHT_REL | 9 | 重定位表,该段包含重定位信息 |
SHT_SHLIB | 10 | 保留 |
SHT_DNYSYM | 11 | 动态链接符号表 |
还有很多段类型的常量没展现, 具体可以看elf.h;SHT_REL(Relocation entries, no addends) 和 SHT_RELA(Relocation entries with addends) 都是重定位表,但有少许区别,对于静态编译的重定位而言,重定位地址总时相对于某个参照物进行偏移,而SHT_REL 将偏移放在了需要重定位的内存地址处,而SHT_RELA 将偏移放在了重定位表中,而重定位内存处则填0.
section flag
段的标志位相关常量是以SHF_开头,以下是常用的段的标志位
常量 | 值 | 描述 | readelf 的标志 |
---|---|---|---|
SHF_WRITE | 1<< 0 | 表示该段可写入 | W |
SHF_ALLOC | 1 << 1 | 表示该段在执行时需要分配空间,如代码段、数据段、.bss段都会有这个标志 | A |
SHF_EXECINSTR | 1 << 2 | 表示该段可以被执行, 一般是指代码段 | X |
SHF_MERGE | 1 << 4 | 表示可以合并以消除重复的数据的section | M |
SHF_STRINGS | 1 << 5 | 标识由NULL结尾的字符串组成的section,每个字符串的大小在section header的sh_entsize指定 | S |
SHF_INFO_LINK | 1 << 6 | 表示字段sh_info中包含section headers的某个section的下标 | I |
SHF_LINK_ORDER | 1 << 7 | 表示链接器添加了特殊的排序要求 | L |
SHF_OS_NONCONFORMING | 1 << 8 | 表示section需要链接标准之外的基于特定操作系统的处理 | O |
SHF_GROUP | 1 << 9 | 表示section 是section Group的成员,该标志只能为包含在可重定位对象中的section设置 | G |
SHF_TLS | 1 << 10 | 表示sction包含线程本地存储 | T |
SHF_COMPRESSED | 1 << 11 | 包含压缩数据,不能与SHF_ALLOC一起使用 | C |
还有一些其他的标志没展现,具体可以看elf.h。
section link && section info
如果段的类型是与连接相关的,那么sh_link、sh_info这两个成员就会有一些意义。
sh_type | sh_link | sh_info |
---|---|---|
SHT_DYNAMIC | 该段所使用的字符串表在段表的下标 | 0 |
SHT_HASH | 该段所使用的符号表在段表的下标 | 0 |
SHT_REL/SHT_RELA | 该段所使用的符号表在段表的下标 | 重定位表作用的表在段表中的下标 |
SHT_SYMTAB/SHT_DYNSY | 操作系统相关 | 操作系统相关 |
other | SHN_UNDEF | 0 |
分析目标文件的section header Table
这边可以使用objdump 或者readelf来观察段表的结构
>> objdump -h main.o
main.o: file format elf64-x86-64
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000048 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000008 0000000000000000 0000000000000000 00000088 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000008 0000000000000000 0000000000000000 00000090 2**2
ALLOC
3 .rodata 00000009 0000000000000000 0000000000000000 00000090 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .comment 0000001d 0000000000000000 0000000000000000 00000099 2**0
CONTENTS, READONLY
5 .note.GNU-stack 00000000 0000000000000000 0000000000000000 000000b6 2**0
CONTENTS, READONLY
6 .eh_frame 00000058 0000000000000000 0000000000000000 000000b8 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
>> readelf -S main.o
There are 13 section headers, starting at offset 0x418:
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
0000000000000048 0000000000000000 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 00000338
0000000000000048 0000000000000018 I 10 1 8
[ 3] .data PROGBITS 0000000000000000 00000088
0000000000000008 0000000000000000 WA 0 0 4
[ 4] .bss NOBITS 0000000000000000 00000090
0000000000000008 0000000000000000 WA 0 0 4
[ 5] .rodata PROGBITS 0000000000000000 00000090
0000000000000009 0000000000000000 A 0 0 1
[ 6] .comment PROGBITS 0000000000000000 00000099
000000000000001d 0000000000000001 MS 0 0 1
[ 7] .note.GNU-stack PROGBITS 0000000000000000 000000b6
0000000000000000 0000000000000000 0 0 1
[ 8] .eh_frame PROGBITS 0000000000000000 000000b8
0000000000000058 0000000000000000 A 0 0 8
[ 9] .rela.eh_frame RELA 0000000000000000 00000380
0000000000000030 0000000000000018 I 10 8 8
[10] .symtab SYMTAB 0000000000000000 00000110
0000000000000198 0000000000000018 11 11 8
[11] .strtab STRTAB 0000000000000000 000002a8
000000000000008c 0000000000000000 0 0 1
[12] .shstrtab STRTAB 0000000000000000 000003b0
0000000000000061 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)
可以看到readelf 输出的内容会比objdump更多,因为objdump会省略一些辅助性的段。在前文elf header中可以看到main.o中的Number of section headers确实是13,与readelf输出段的个数匹配,不过ELF段表的第一个元素是无效的描述符,所以真正有效的描述符只有12个
name | Type | flag | 描述 |
---|---|---|---|
.text | PROGBITS | SHF_ALLOC && SHF_EXECINSTR | 代码段,加载时需要分配空间且可以被执行 |
.rela.text | RELA | SHF_INFO_LINK | 代码段的重定位段,sh_link字段包含符号表在段表的下标,sh_info可以确定作用于.text |
.data | PROGBITS | SHF_WRITE && SHF_ALLOC | 数据段,可写入且加载时需要分配空间 |
.bss | NOBITS | SHF_WRITE && SHF_ALLOC | Block Started by Symbol,存储没有初始化或者初始化为0的全局变量,文件中不存在内容,但加载后可写入且需要分配空间 |
.rodata | PROGBITS | SHF_ALLOC | 只读数据段存储常量数据,比如程序中定义为const的全局变量,#define定义的常量。 加载后需要分配空间 |
.comment | PROGBITS | SHF_MERGE && SHF_STRINGS | 注释信息段,在连接的时候可以合并该段,并且段中存储的是以NULL为结尾的字符串 |
.note.GNU-stack | PROGBITS | 堆栈提示段 | |
.eh_frame | PROGBITS | SHF_ALLOC | 这个段在GCC生成处理异常的代码时,描述如何展开堆栈 。加载时需要分配空间 |
.rela.eh_frame | RELA | SHF_INFO_LINK | .eh_frame的重定位段,由sh_info可以确定作用于.eh_frame |
.symtab | SYMTAB | 符号表 | |
.strtab | STRTAB | 字符串表 | |
.shstrtab | STRTAB | 段表字符串表,通常存储段名 |
重定位表
重定位表表项的结构回在静态连接中总结
字符串表
因为字符串的长度一般都不固定,所以常见的做法是存放到统一的表里
让偏移和字符串可以一一对应
这样在ELF文件中只需要引用一个字符串表的偏移就可以了。常见的字符串表就是.strtab(字符串表) 和 .shstrtab(段表字符串表)。字符串表用来保存普通的字符串,比如符号名字等。而段表字符串表用来保存段表中会用到的字符串,比如段表名字等。
回到最开头的mian.o文件中,在ELF Header里由一个e_shstrndx,这个变量是段表字符串表在段表中的下标,即.shstrtab在段表中的下标,可以对比ELF headr的输出和readelf -S 的输出,发现e_shstrndx确实是12。
符号表
.symtab 是由 Elf32_Sym/Elf64_Sym 够成的一个数组,其定义如下:
typedef struct
{
Elf64_Word st_name; // 符号名字在字符串表中的下标
unsigned char st_info; // 符号类型和绑定信息
unsigned char st_other; // 貌似没用
Elf64_Section st_shndx; // 符号所在段,但部分特殊符号意义稍微特殊一点
Elf64_Addr st_value; // 符号值,不同符号这个字段的意义不一致
Elf64_Xword st_size; // 符号大小,如果为0则大小为未知
} Elf64_Sym;
st_info
st_info 的低4位用来代表符号类型,高28位代表符号绑定信息。
绑定信息的部分宏定义如下:
宏定义 | 值 | 描述 |
---|---|---|
STB_LOCAL | 0 | 局部符号,目标文件之外不可见 |
STB_GLOBAL | 1 | 全局符号,所有目标文件都可见 |
STB_WEAK | 2 | 弱符号,类似于全局符号,但定义具有较低优先级 |
符号类型的部分宏定义如下
宏定义 | 值 | 描述 |
---|---|---|
STT_NOTYPE | 0 | 未知类型 |
STT_OBJECT | 1 | 符号是个数据对象,比如变量、数组等 |
STT_FUNC | 2 | 符号是个函数 |
STT_SECTION | 3 | 表明该符号是一个段,这个符号必须是STB_LOCA的 |
STT_FILE | 4 | 该符号是文件名字,一般是该目标文件对应的源文件名, 一定是STB_LOCAL类型,且st_shndx一定是SHN_ABS |
STT_COMMON | 5 | 该符号是一个未初始化的公共块,处理方式会与 STB_LOCAL 相同 |
st_shndx
符号如果定义在本文件中,则该字段就是 所在段 在 段表中的下标。如果不是定义在目标文件以及对于有些特殊的符号,则是按以下定义来。
宏定义 | 值 | 描述 |
---|---|---|
SHN_UNDEF | 0 | 符号未定义,这个符号在该目标文件中有引用,但定义不在该目标文件中 |
SHN_ABS | 1 | 表示该符号包含一个绝对的值,如文件名称这种符号 |
SHN_COMMON | 2 | 表示该符号属于未初始化的公共块,比如mina.o中定义的gloabal_uninit_var这个变量 |
还有一些特殊定义没展现。 |
st_value
这个变量对于不同情况有不同的定义:
- 如果符号在目标文件中,且符号的st_shndx不是SHN_COMMON,则st_value表示符号在段中的偏移,最常见的就是全局变量的符号
- 如果符号在目标文件中,且符号的st_shndx是SHN_COMMON,则st_value是对齐属性
- 如果符号在可执行文件中,则st_value表示符号的虚拟地址,这个对于动态连接十分有用。
分析main.o的符号表
>>readelf -s main.o
Symbol table '.symtab' contains 17 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS main.cpp
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000004 4 OBJECT LOCAL DEFAULT 3 _ZZ4mainE10static_var
7: 0000000000000004 4 OBJECT LOCAL DEFAULT 4 _ZZ4mainE11static_var2
8: 0000000000000000 0 SECTION LOCAL DEFAULT 7
9: 0000000000000000 0 SECTION LOCAL DEFAULT 8
10: 0000000000000000 0 SECTION LOCAL DEFAULT 6
11: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 global_init_var
12: 0000000000000000 4 OBJECT GLOBAL DEFAULT 4 global_uninit_var
13: 0000000000000000 36 FUNC GLOBAL DEFAULT 1 _Z5func1i
14: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_
15: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _Z6printfPKcz
16: 0000000000000024 36 FUNC GLOBAL DEFAULT 1 main
>>>objdump -t main.o
main.o: file format elf64-x86-64
SYMBOL TABLE:
0000000000000000 l df *ABS* 0000000000000000 main.cpp
0000000000000000 l d .text 0000000000000000 .text
0000000000000000 l d .data 0000000000000000 .data
0000000000000000 l d .bss 0000000000000000 .bss
0000000000000000 l d .rodata 0000000000000000 .rodata
0000000000000004 l O .data 0000000000000004 _ZZ4mainE10static_var
0000000000000004 l O .bss 0000000000000004 _ZZ4mainE11static_var2
0000000000000000 l d .note.GNU-stack 0000000000000000 .note.GNU-stack
0000000000000000 l d .eh_frame 0000000000000000 .eh_frame
0000000000000000 l d .comment 0000000000000000 .comment
0000000000000000 g O .data 0000000000000004 global_init_var
0000000000000000 g O .bss 0000000000000004 global_uninit_var
0000000000000000 g F .text 0000000000000024 _Z5func1i
0000000000000000 *UND* 0000000000000000 _GLOBAL_OFFSET_TABLE_
0000000000000000 *UND* 0000000000000000 _Z6printfPKcz
0000000000000024 g F .text 0000000000000024 main
符号表的第一项是无效的,所以main.o中16个有效符号,然后Name被readelf直接读取字符串表给翻译出来出来了。
符号下标 | 描述 |
---|---|
1 | 该符号是文件名字, 内容是绝对的值,符号名字是main.cpp |
6 | 该符号是个数据对象,是个本地符号,定义在该目标文件中,所在段的下标是3,也就是.data段,名字是_ZZ4mainE10static_var,长度是4,在段中的偏移是4 |
7 | 该符号是个数据对象,是个本地符号,定义在该目标文件中,所在段的下标是4,也就是.bss段,名字是_ZZ4mainE11static_var2, 长度是4,在段中的偏移是4 |
11 | 该符号是个数据对象,是个全局符号,定义在该目标文件中,所在段的下标是3,也就是.data段,名字是global_init_var, 长度是4, 在段中偏移是0 |
12 | 该符号是个数据对象,是个全局符号,定义在该目标文件中,所在段的下标是4,也就是.bss段,名字是global_uninit_var, 长度是4, 在段中偏移是0 |
13 | 该符号是个函数对象,是个全局符号,所在段的下标是1,也就是.textt段,名字是_Z5func1i, 长度是36 |
14 | 该符号类型未定义,是个全局符号,在该文件中只是引用,名字是_GLOBAL_OFFSET_TABLE_,实际上这个是一个由编译器生成的符号,用于定位全局地址无关的变量的真实地址,素材GOT |
15 | 该符号是个未定义,是个全局符号,在该文件中只是引用,名字是_Z6printfPKcz,实际上这个是printf函数,文件中确实对printf没有进行定义 |
16 | 该符号是个函数对象,是个全局符号,定义在该目标文件中,所在段的下标是1,也就是.text段,函数名字是main,长度36,在段中偏移是0X24 |
对于其他Type是section的符号,实际上都是对应Ndx的段的段名,readelf没有显示,但objdump 可以看到这些符号的名字。
然后通过objdump 可以查看代码段,对上述处于代码段的符号偏移进行验证
>>> objdump -d main.o
main.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <_Z5func1i>:
......
23: c3 retq
0000000000000024 <main>:
......
47: c3 retq
可以看到_Z5func1i从0X00开始长度为36(0X24), 从0x24开始是main函数的内容,长度为36(0x2f)。
然后可以通过hexdump来对数据段的内容进行验证:
>> hexdump -n 8 -s 0x88 main.o
0000088 0054 0000 0005 0000
0000090
数据段在文件中偏移为0x88,长度为8, 前4个字节是global_init_var,内容是采用小端序存储,其值为0x00000054,十进制下位84.第4到7个字节为_ZZ4mainE10static_var,内容是0X00000005.
总结
这边较为深入的介绍了ELF Header 、section Header 、以及secton的结构,并使用了《程序员的自我修养-链接、装载与库》这本书上的例子作为实验的内容,详细展现了目标文件中的这些信息应该如何解读。
在知道了目标文件中的结构后,接下来的问题就是如何在链接的时候将他们组合起来,形成一个可执行文件或者是.so文件。
引用
- 程序员的自我修养-链接、装载与库.pdf
- https://sp4n9x.github.io/2021/05/27/ELF_FileFormat_Analysis/ ELF文件格式分析
- https://blog.csdn.net/wyzworld/article/details/114805643 ELF文件详解
- https://zhuanlan.zhihu.com/p/286088470
- http://nicephil.blinkenshell.org/my_book/ch04.html
- https://docs.oracle.com/cd/E19683-01/816-1386/chapter6-79797/index.html