ELF: 揭秘可执行文件格式
- 📚 本书官网及作者联系方式
访问本书网站: Under The Hood Of Executables
联系作者: chessMan786
在第1章中,我们发现简单的 “Hello, World!”程序生成了一个出乎意料大的可执行文件。现在,让我们深入探讨使这一切成为可能的格式:可执行与可链接格式(ELF)。本章将探讨ELF文件(Linux 系统中可执行文件的标准二进制格式)的结构、用途及其复杂性。
剖析ELF文件
ELF 文件就像一个精心组织的容器,其中保存着我们程序的各种组件。让我们首先创建一个简单的程序,以便在本章中对其进行研究:
// example.c
#include <stdio.h>
const char message[] = "Hello from ELF!";
int initialized_var = 42;
int uninitialized_var;
void print_message() {
printf("%s\n", message);
}
int main() {
uninitialized_var = initialized_var;
print_message();
return 0;
}
让我们编译此程序,开始我们的探索:
$ gcc -g -o example example.c
ELF File Structure Overview
ELF文件由四个主要部分构成:
1、ELF文件头(ELF Header)
2、 程序头表(译者注:也称段表)(Program Header Table)
3、各种节(Sections)
4、节头表(Section Header Table)
下面是一个直观的示意图:
+-------------------+
| ELF文件头 |
+-------------------+
| 程序头表 |
+-------------------+
| |
| 节 |
| (.text, etc) |
| |
+-------------------+
| 节头表 |
+-------------------+
ELF 文件头: 文件的身份证
让我们详细研究一下 ELF 文件头:
$ readelf -h example
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: DYN (Shared object file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x1060
Start of program headers: 64 (bytes into file)
Start of section headers: 14816 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 13
Size of section headers: 64 (bytes)
Number of section headers: 31
Section header string table index: 30
让我们来分析一下这些字段:
1、魔数(译者注:计16字节)
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
- 前四个字节总是:0x7F(译者注:ASCII中的DEL控制字符,对应EI_MAG0), ‘E’(译者注:对应EI_MAG1), ‘L’(译者注:对应EI_MAG2), ‘F’(译者注:对应EI_MAG3)
- 以下字节指定了:
- 文件类别 (32/64位)(译者注:1为ELF32、2为ELF64,0为非法目标文件,对应第5个字节(EI_CLASS))
- 数据编码格式(译者注:大小端字节序,1为LSB,2为MSB,,对应第6个字节(EI_DATA))
- ELF版本号(译者注:对应第7个字节(EI_VERSION))
- 操作系统ABI(译者注:指的是目标操作系统ABI编号,对应第8个字节(EI_OSABI))
- ABI版本(译者注:进一步指定ABI的版本,用于区分ABI的部兼容版本,对应第9个字节(EI_ABIVERSION))
- 填充(译者注:保留并置为0,对应剩余7个字节(EI_PAD))
2、文件类型
Type: DYN (Shared object file)
常见类型包括:
EXEC
: 可执行文件DYN
: 共享目标文件(包括位置无关的可执行文件)REL
: 可重定位文件CORE
: 核心转储文件
3、处理器架构
Machine: Advanced Micro Devices X86-64
指定目标架构。其他常用值如下:
EM_386
: x86EM_ARM
: ARMEM_RISCV
: RISC-V
4、入口地址
Entry point address: 0x1060
程序执行的起始内存地址。它通常指向 _start
函数,而非 main
函数(我们将在第 4 章中探讨原因)。
Program Headers: Loading Instructions
程序头告诉系统如何创建进程映像。让我们来检查它们:
$ readelf -l example
Elf file type is DYN (Position-Independent Executable file)
Entry point 0x1060
There are 13 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040
0x00000000000002d8 0x00000000000002d8 R 0x8
INTERP 0x0000000000000318 0x0000000000000318 0x0000000000000318
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000898 0x0000000000000898 R 0x1000
LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000
0x0000000000000235 0x0000000000000235 R E 0x1000
LOAD 0x0000000000002000 0x0000000000002000 0x0000000000002000
0x00000000000001a4 0x00000000000001a4 R 0x1000
LOAD 0x0000000000002db8 0x0000000000003db8 0x0000000000003db8
0x0000000000000258 0x0000000000000260 RW 0x1000
关键程序头类型如下:
1、 PHDR
指向程序头表本身。
2、INTERP
指定动态链接器/解释器:
$ readelf -p .interp example
String dump of section '.interp':
[ 0] /lib64/ld-linux-x86-64.so.2
3、LOAD
描述即将加载到内存中的段:
- 只读段 (代码和常量)
- 可读可执行段 (代码)
- 可读可写段 (数据)
4、DYNAMIC
包含动态链接信息:
$ readelf -d example
Dynamic section at offset 0x2dc8 contains 27 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000c (INIT) 0x1000
0x000000000000000d (FINI) 0x11f8
[...]
节: 程序组件的位置
让我们全面了解一下各个节:
$ readelf -S example
There are 31 section headers, starting at offset 0x39f0:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000000318 00000318
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.gnu.property NOTE 0000000000000338 00000338
0000000000000030 0000000000000000 A 0 0 8
[...]
重要的节包括:
1、代码节
.text
: 可执行代码.init
: 初始化代码.fini
: 清理代码.plt
: 程序链接表(用于动态链接)(译者注:PLT)
2、数据节
.data
: 初始化数据.rodata
: 只读数据.bss
: 未初始化数据.got
: 全局偏移表(译者注:GOT)
3、符号和重定位部分
.symtab
: 符号表.strtab
: 字符串表.rela.text
: .text节的重定位.rela.data
: .data节的重定位
检查节的内容
让我们来看一些具体的节:
1、.text节中的代码
$ objdump -d -j .text example
...
0000000000001169 <print_message>:
1169: 55 push %rbp
116a: 48 89 e5 mov %rsp,%rbp
116d: 48 8d 05 94 0e 00 00 lea 0xe94(%rip),%rax
1174: 48 89 c7 mov %rax,%rdi
1177: e8 e4 fe ff ff call 1060 <puts@plt>
117c: 90 nop
117d: 5d pop %rbp
117e: c3 ret
...
2、.rodata节的只读数据
$ objdump -s -j .rodata example
Contents of section .rodata:
2000 01000200 48656c6c 6f206672 6f6d2045 ....Hello from E
2010 4c462100 LF!.
3、.data节的初始化数据
$ objdump -s -j .data example
Contents of section .data:
4000 2a000000 *...
符号表: 程序的字典
符号表将名称映射为地址:
$ readelf -s example
Symbol table '.dynsym' contains 7 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5 (2)
2: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
3: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterT[...]
4: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMC[...]
5: 0000000000000000 0 FUNC WEAK DEFAULT UND [...]@GLIBC_2.2.5 (2)
6: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __cxa_finalize@[...]
处理ELF文件
1、创建不同类型的ELF文件
可重定位的目标文件
$ gcc -c -o example.o example.c
$ file example.o
example.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
共享库
$ gcc -shared -fPIC -o libexample.so example.c
$ file libexample.so
libexample.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV)
静态可执行文件
$ gcc -static -o example_static example.c
$ file example_static
example_static: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux)
2、操作ELF文件
添加节
$ objcopy --add-section .mydata=myfile.txt example example_modified
剥离调试信息
$ strip --strip-debug example
提取节
$ objcopy -O binary --only-section=.text example text.bin
不同上下文中的ELF
1、可重定位文件 (.o)
- 包含适合链接的代码和数据
- 具有重定位信息,用于指明如何修改节内容
- 包括用于外部引用的符号表
2、可执行文件
- 包含可直接执行的代码和数据
- 包含描述如何创建进程映像的程序头
- 指定动态链接的解释器
3、共享对象 (.so)
- 可在运行时链接
- 位置无关代码(译者注:PIC)
- 可被多个进程共享
实践中的应用
1、调试
$ gdb example
(gdb) info files
Symbols from "/path/to/example".
Local exec file:
`/path/to/example', file type elf64-x86-64.
Entry point: 0x1060
0x0000000000000318 - 0x0000000000000334 is .interp
[...]
2、二进制分析
$ nm example
0000000000004010 B __bss_start
0000000000004010 b completed.8061
w __cxa_finalize@@GLIBC_2.2.5
0000000000004000 D __data_start
[...]
3、运行时分析
$ strace ./example
execve("./example", ["./example"], 0x7ffc9487d7e0 /* 66 vars */) = 0
brk(NULL) = 0x557a5a43c000
arch_prctl(ARCH_SET_FS, 0x557a5a43b540) = 0
[...]
安全方面的影响
1、ASLR (地址空间随机化)
$ cat /proc/sys/kernel/randomize_va_space
2
$ ./example
$ cat /proc/$(pgrep example)/maps
00400000-00401000 r-xp 00000000 08:01 1048578 /path/to/example
[...]
2、栈保护
$ readelf -s example | grep -i stack
8: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __stack_chk_fail@GLIBC_2.4
3、只读重定位( 译者注:RELRO)
$ readelf -d example | grep RELRO
0x000000000000001e (FLAGS) BIND_NOW RTLD_GLOBAL
小结
理解ELF格式对以下工作至关重要:
- 调试复杂问题
- 理解程序加载和执行的过程
- 实施安全措施
- 优化程序大小和性能
在下一章中,我们将深入研究ELF节,并探讨不同类型的代码和数据是如何在其中组织起来的。
延伸阅读
1、官方参考文献:
- System V ABI规范
- 工具接口标准 (TIS) ELF 规范
2、命令文档:
man elf
man objdump
man readelf
3、在线资源:
- Linux基金会文档
- ELF 工具链项目
如本文对你有些许帮助,欢迎大佬支持我一下(点赞+收藏+关注、关注公众号等),您的支持是我持续创作的竭动力
支持我的方式