ELF 文件 (Executable Linkable Format) 是一种文件存储格式. Linux 下的目标文件和可执行文件都按照该格式进行存储, 有必要做个总结.
概要
本文主要记录总结 32 位的 Intel x86 平台下的 ELF 文件结构. ELF 文件以 Section 的形式进行存储. 代码编译后的指令放在代码段(Code Section), 全局变量和局部静态变量放到数据段(Data Section). 文件以一个 "文件头" 开始, 记录了整个文件的属性信息.
未链接的目标文件结构
- SimpleSection.c
- int printf(const char* format, ...);
- int global_init_var = 84;
- int global_uniit_var;
- void func1(int i)
- {
- printf("%d\n", i);
- }
- int main(void)
- {
- static int static_var = 85;
- static int static_var2;
- int a = 1;
- int b;
- func1(static_var + static_var2 + a + b);
- return a;
- }
对于上面的一段 c 代码将其编译但是不链接. gcc -c -m32 SimpleSection.c( -c 表示只编译不链接,-m32 表示生成 32 位的汇编)得到 SimpleSection.o. 可以用 objdump 或 readelf 命令查看目标文件的结构和内容.
ELF 文件头
可以用 readelf -h 查看文件头信息. 执行 readelf -h SimpleSection.o 后:
- root@DESKTOP-2A432QS:~/c# readelf -h SimpleSection.o
- ELF Header:
- Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
- Class: ELF32
- Data: 2's complement, little endian
- Version: 1 (current)
- OS/ABI: UNIX - System V
- ABI Version: 0
- Type: REL (Relocatable file)
- Machine: Intel 80386
- Version: 0x1
- Entry point address: 0x0
- Start of program headers: 0 (bytes into file)
- Start of section headers: 832 (bytes into file)
- Flags: 0x0
- Size of this header: 52 (bytes)
- Size of program headers: 0 (bytes)
- Number of program headers: 0
- Size of section headers: 40 (bytes)
- Number of section headers: 13
- Section header string table index: 10
程序头包含了很多重要的信息, 每个字段的含义可参考 ELF 结构文档. 主要看下:
Entry point address: 程序的入口地址, 这是没有链接的目标文件所以值是 0x00
Start of section headers: 段表开始位置的首字节
Size of section headers: 段表的长度(字节为单位)
Number of section headers: 段表中项数, 也就是有多少段
Start of program headers: 程序头的其实位置(对于可执行文件重要, 现在为 0)
Size of program headers: 程序头大小(对于可执行文件重要, 现在为 0)
Number of program headers: 程序头中的项数, 也就是多少 Segment(和 Section 有区别, 后面介绍)
Size of this header: 当前 ELF 文件头的大小, 这里是 52 字节
段表及段(Section)
段表
ELF 文件由各种各样的段组成, 段表就是保存各个段信息的结构, 以数组形式存放. 段表的起始位置, 长度, 项数分别由 ELF 文件头中的 Start of section headers,Size of section headers,Number of section headers 指出. 使用 readelf -S SimpleSection.o 查看 SimpleSection.o 的段表如下:
- There are 13 section headers, starting at offset 0x340:
- Section Headers:
- [Nr] Name Type Addr Off Size ES Flg Lk Inf Al
- [ 0] NULL 00000000 000000 000000 00 0 0 0
- [ 1] .text PROGBITS 00000000 000034 000062 00 AX 0 0 1
- [ 2] .rel.text REL 00000000 0002a8 000028 08 I 11 1 4
- [ 3] .data PROGBITS 00000000 000098 000008 00 WA 0 0 4
- [ 4] .bss NOBITS 00000000 0000a0 000004 00 WA 0 0 4
- [ 5] .rodata PROGBITS 00000000 0000a0 000004 00 A 0 0 1
- [ 6] .comment PROGBITS 00000000 0000a4 000036 01 MS 0 0 1
- [ 7] .note.GNU-stack PROGBITS 00000000 0000da 000000 00 0 0 1
- [ 8] .eh_frame PROGBITS 00000000 0000dc 000064 00 A 0 0 4
- [ 9] .rel.eh_frame REL 00000000 0002d0 000010 08 I 11 8 4
- [10] .shstrtab STRTAB 00000000 0002e0 00005f 00 0 0 1
- [11] .symtab SYMTAB 00000000 000140 000100 10 12 11 4
- [12] .strtab STRTAB 00000000 000240 000065 00 0 0 1
- Key to Flags:
- W (write), A (alloc), X (execute), M (merge), S (strings)
- I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
- O (extra OS processing required) o (OS specific), p (processor specific)
总共有 13 个 Section, 重点关注. text, .data, .rodata, .symtab, .rel.text 段.
代码段
.text 段保存代码编译后的指令, 可以用 objdump -s -d SimpleSection.o 查看 SimpleSection.o 代码段的内容.
- SimpleSection.o: file format elf32-i386
- Contents of section .text:
- 0000 5589e583 ec0883ec 08ff7508 68000000 U.........u.h...
- 0010 00e8fcff ffff83c4 1090c9c3 8d4c2404 .............L$.
- 0020 83e4f0ff 71fc5589 e55183ec 14c745f0 ....q.U..Q....E.
- 0030 01000000 8b150400 0000a100 00000001 ................
- 0040 c28b45f0 01c28b45 f401d083 ec0c50e8 ..E....E......P.
- 0050 fcffffff 83c4108b 45f08b4d fcc98d61 ........E..M...a
- 0060 fcc3 ..
... 省略
- Disassembly of section .text:
- 00000000 <func1>:
- 0: 55 push %ebp
- 1: 89 e5 mov %esp,%ebp
- 3: 83 ec 08 sub $0x8,%esp
- 6: 83 ec 08 sub $0x8,%esp
- 9: ff 75 08 pushl 0x8(%ebp)
- c: 68 00 00 00 00 push $0x0
- 11: e8 fc ff ff ff call 12 <func1+0x12>
- 16: 83 c4 10 add $0x10,%esp
- 19: 90 nop
- 1a: c9 leave
- 1b: c3 ret
- 0000001c <main>:
- 1c: 8d 4c 24 04 lea 0x4(%esp),%ecx
- 20: 83 e4 f0 and $0xfffffff0,%esp
- 23: ff 71 fc pushl -0x4(%ecx)
- 26: 55 push %ebp
- 27: 89 e5 mov %esp,%ebp
- 29: 51 push %ecx
- 2a: 83 ec 14 sub $0x14,%esp
- 2d: c7 45 f0 01 00 00 00 movl $0x1,-0x10(%ebp)
- 34: 8b 15 04 00 00 00 mov 0x4,%edx
- 3a: a1 00 00 00 00 mov 0x0,%eax
- 3f: 01 c2 add %eax,%edx
- 41: 8b 45 f0 mov -0x10(%ebp),%eax
- 44: 01 c2 add %eax,%edx
- 46: 8b 45 f4 mov -0xc(%ebp),%eax
- 49: 01 d0 add %edx,%eax
- 4b: 83 ec 0c sub $0xc,%esp
- 4e: 50 push %eax
- 4f: e8 fc ff ff ff call 50 <main+0x34>
- 54: 83 c4 10 add $0x10,%esp
- 57: 8b 45 f0 mov -0x10(%ebp),%eax
- 5a: 8b 4d fc mov -0x4(%ebp),%ecx
- 5d: c9 leave
- 5e: 8d 61 fc lea -0x4(%ecx),%esp
- 61: c3 ret
可以看到. text 段里保存的正是 func1()和 main()的指令.
数据段和只读数据段
.data 段保存的是已经初始化了的全局静态变量和局部静态变量. 前面 SimpleSection.c 中的 global_init_varabal 和 static_var 正是这样的变量. 使用 objdump -x -s -d SimpleSection.o 查看:
- Contents of section .data:
- 0000 54000000 55000000 T...U...
- Contents of section .rodata:
- 0000 25640a00 %d..
最左边的 0000 是偏移, 不用看, 后面跟着的 0x00000054 和 0x00000055 正是 global_init_varabal 和 static_var 的初始值.
.rodata 段存放的是只读数据, 包括只读变量(const 修饰的变量和字符串常量), 这个例子中保存了 "%d\n" 正是调用 printf 的时候使用的字符常量.
符号表段
符号表段一般叫做. symtab, 以数组结构保存符号信息(函数和变量), 对于函数和变量符号值就是它们的地址. 主要关注两类符号:
定义在目标文件中的全局符号, 可以被其他目标文件引用, 比如 SimpleSction.o 里面的 func1, main 和 global_init_var.
在本目标文件中引用的全局符号, 却没有定义在本目标文件, 比如 pritnf.
可以用 readelf -s SimpleSection.o 查看 SimpleSection.o 的符号:
- Symbol table '.symtab' contains 16 entries:
- Num: Value Size Type Bind Vis Ndx Name
- 0: 00000000 0 NOTYPE LOCAL DEFAULT UND
- 1: 00000000 0 FILE LOCAL DEFAULT ABS SimpleSection.c
- 2: 00000000 0 SECTION LOCAL DEFAULT 1
- 3: 00000000 0 SECTION LOCAL DEFAULT 3
- 4: 00000000 0 SECTION LOCAL DEFAULT 4
- 5: 00000000 0 SECTION LOCAL DEFAULT 5
- 6: 00000004 4 OBJECT LOCAL DEFAULT 3 static_var.1488
- 7: 00000000 4 OBJECT LOCAL DEFAULT 4 static_var2.1489
- 8: 00000000 0 SECTION LOCAL DEFAULT 7
- 9: 00000000 0 SECTION LOCAL DEFAULT 8
- 10: 00000000 0 SECTION LOCAL DEFAULT 6
- 11: 00000000 4 OBJECT GLOBAL DEFAULT 3 global_init_var
- 12: 00000004 4 OBJECT GLOBAL DEFAULT COM global_uniit_var
- 13: 00000000 28 FUNC GLOBAL DEFAULT 1 func1
- 14: 00000000 0 NOTYPE GLOBAL DEFAULT UND printf
- 15: 0000001c 70 FUNC GLOBAL DEFAULT 1 main
可以看到:
func1 和 main 的 Ndx 对应的值是 1, 表示在. text 段(.text 段在段表中的索引是 1), 类型是 FUNC,value 分别是 0x00000000 和 0x0000001c, 表明这两个函数指令字节码的首字节分别在. text 段的 0x00000000 和 0x0000001c 偏移处.
printf 的 Ndx 是 UND, 表明这个符号没有在 SimpleSection.o 中定义, 仅仅是被引用.
global_init_var 和 static_var.1488 两个符号的 Ndx 都是 3, 说明他们被定义在数据段, value 分别是 0x00000000 和 0x00000004, 表示这个符号的位置在数据段的 0x00000000 和 0x00000004 偏移处, 翻看上一节
- Contents of section .data:
- 0000 54000000 55000000 T...U...
数据段 0x00000000 和 0x00000004 偏移处保存的正是 global_init_var 和 static_var 这两个变量.
重定位表段
重定位表也是一个段, 用于描述在重定位时链接器如何修改相应段里的内容. 对于. text 段, 对应的重定位表是. rel.text 表. 使用 objdump -r SimpleSection.o 查看重定位表.
- SimpleSection.o: file format elf32-i386
- RELOCATION RECORDS FOR [.text]:
- OFFSET TYPE VALUE
- 0000000d R_386_32 .rodata
- 00000012 R_386_PC32 printf
- 00000036 R_386_32 .data
- 0000003b R_386_32 .bss
- 00000050 R_386_PC32 func1
printf 对应的那行的 OFFSET 为 0x00000012, 表明. text 段的 0x00000012 偏移处需要修改. 我们 objdump -s -d SimpleSection.o 查看代码段的 0x00000012 偏移, 发现是 "fc ff ff ff" 是 call 指令的操作数.
- 00000000 <func1>:
- 0: 55 push %ebp
- 1: 89 e5 mov %esp,%ebp
- 3: 83 ec 08 sub $0x8,%esp
- 6: 83 ec 08 sub $0x8,%esp
- 9: ff 75 08 pushl 0x8(%ebp)
- c: 68 00 00 00 00 push $0x0
- 11: e8 fc ff ff ff call 12 <func1+0x12>
- 16: 83 c4 10 add $0x10,%esp
- 19: 90 nop
- 1a: c9 leave
- 1b: c3 ret
也就是说, 在没有重定位前 call 指令的操作 "fc ff ff ff" 是无效的, 需要在重定位过程中进行修正. func1 那行也同理.
总结
ELF 文件结构可以用下面的图表示:
可执行程序结构
和未链接的 ELF 文件结构一样, 只不过引入了 Segment 的概念 (注意和 Section 进行区分).Segment 本质上是从装载的角度重新划分了 ELF 的各个段. 目标文件链接成可执行文件时, 链接器会尽可能把相同权限属性的段(Section) 分配到同一 Segment.Segment 结构的起始位置, 项数, 大小分别由 ELF 头中的 Size of program headers,Number of program headers, Size of this header 字段指定.
参考资料:
《程序员的自我修养》第 3,6 章
ELF 结构文档
来源: https://www.cnblogs.com/gatsby123/p/9750187.html