总结摘要
ELF 文件即 Linux 下的可执行文件
ELF 文件初识
ELF 文件即 Linux 下的可执行文件,ELF 是 Executable and Linkable Format 的缩写。它是一个标准的二进制文件格式,用于存储可执行文件、目标代码、共享库和核心转储等。
使用 Linux 内置命令 file 可以轻松查看文件类型:
1
2
3
4
5
| $ file /bin/ls
/bin/ls: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically
linked, interpreter /lib64/ld-linux-x86-64.so.2,
BuildID[sha1]=d1f6561268de19201ceee260d3a4f6662e1e70dd, for GNU/Linux 4.4.0,
stripped
|
ELF 64-bit LSB pie executable 部分表示这是一个 64 位的 ELF 文件,LSB 表示低位字节序,pie 表示位置无关可执行文件(Position Independent Executable)。x86-64 表示这是一个 amd64(x86-64) 架构的可执行文件。dynamically linked 表示这个可执行文件是动态链接的,意味着它依赖于共享库。interpreter /lib64/ld-linux-x86-64.so.2 表示这个可执行文件使用的动态链接器是 /lib64/ld-linux-x86-64.so.2。BuildID[sha1]=d1f6561268de19201ceee260d3a4f6662e1e70dd 是一个唯一的标识符,用于标识这个可执行文件的构建版本。for GNU/Linux 4.4.0 表示这个可执行文件是为 GNU/Linux 4.4.0 版本构建的。stripped 表示这个可执行文件已经被剥离了调试信息。
当 ELF 文件被赋予可执行属性时,直接在命令行中输入 ELF 文件路径即可执行该文件:
1
2
3
4
5
6
7
8
9
10
| # 可执行文件在当期目录下
$ ./binary_file
# 可执行文件在其他目录下(使用绝对路径)
$ /bin/binary_file
# 可执行文件在其他目录下(使用相对路径)
$ ../path/to/binary_file
# 可执行文件在环境变量 PATH 中
$ binary_file
|
二进制文件的编译过程
当我们编写一个 C 语言程序并使用 gcc 编译时,gcc 会将源代码编译成目标文件(.o 文件),然后链接成可执行文件。这个过程可以分为以下几个步骤:
1
2
3
4
5
6
7
| // hello.c
#include <stdio.h>
int main() {
printf("Hello, World!\n");
return 0;
}
|
- 预处理:将源代码中的宏定义、头文件等进行处理,生成中间文件。
1
2
3
4
5
6
7
8
9
10
11
| $ gcc -E hello.c -o hello.i
$ cat hello.i
# 这里预处理后的代码有 800 多行
# 这里只截取 printf 函数的部分
extern int printf (const char *__restrict __format, ...);
# ...
int main() {
printf("Hello, World!\n");
return 0;
}
|
可以看到预处理会将 hello.c 中的头文件包含和宏定义展开,生成一个中间文件 hello.i。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| $ gcc -S hello.i -o hello.s
$ cat hello.s
.file "hello.c"
.text
.section .rodata
.LC0:
.string "Hello, World!"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
leaq .LC0(%rip), %rax
movq %rax, %rdi
call puts@PLT
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (GNU) 15.1.1 20250425"
.section .note.GNU-stack,"",@progbits
|
- 汇编:将汇编代码转换为机器码,生成目标文件(.o 文件)。
1
2
3
4
5
6
7
8
9
10
| $ gcc -c hello.s -o hello.o
$ file hello.o
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
$ chmod +x ./hello.o
'./hello.o' 的模式已由 0644 (rw-r--r--) 更改为 0755 (rwxr-xr-x)
$ ./hello.o
zsh: 可执行文件格式错误: ./hello.o
|
程序在这一步执行会报错是因为 ELF 64-bit LSB relocatable 表示这是一个可重定位文件(relocatable file),它不能直接执行。可重定位文件是编译器生成的中间文件,包含了机器码和符号信息,但还没有链接成可执行文件。
1
2
3
4
5
6
7
| $ gcc hello.o -o hello
$ file hello
hello: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=fff430a3b4def9f6c9b6b37917f6e7a44e1fedd5, for GNU/Linux 4.4.0, not stripped
$ ./hello
Hello, World!
|
dynamically linked 表示这个可执行文件是动态链接的,意味着它依赖于共享库。
同样的静态链接的可执行文件也可以通过 gcc -static 命令生成:
1
2
3
4
| $ gcc -static hello.o -o hello_static
$ file hello_static
hello_static: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=86962ce4b2b5f4686ce66e7b8c2dd33a9b61410a, for GNU/Linux 4.4.0, not stripped
|
statically linked 表示这个可执行文件是静态链接的,意味着它不依赖于共享库。
ELF 文件结构
通常 ELF 文件结构如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| +------------------------------+
| ELF HEADER |
+------------------------------+
| PROGRAM HEADER TABLE |
+------------------------------+
| SECTION 1 |
+------------------------------+
| ... |
+------------------------------+
| SECTION 2 |
+------------------------------+
| ... |
+------------------------------+
| SECTION HEADER TABLE |
+------------------------------+
|
简单的 ELF 文件结构通常至少包括以下几个部分:
1
2
3
4
5
6
7
8
9
| +-------------------------------------------+
| FILE HEADER (ELF HEADER) |
+-------------------------------------------+
| .text SECTION (代码段) |
+-------------------------------------------+
| .data SECTION (数据段) |
+-------------------------------------------+
| .bss SECTION (未初始化数据段) |
+-------------------------------------------+
|
以下收集了一些常见的 ELF 文件结构的部分:
| 序号 | 结构/段名 | 中文名称/类型 | 描述 |
|---|
| 1 | ELF HEADER | ELF文件头 | 文件头,包含ELF文件的基本信息,如类型、架构、入口点等。 |
| 2 | PROGRAM HEADER TABLE | 程序头表 | 描述可执行文件各个段(Segment)的信息,如内存地址、大小、权限等。 |
| 3 | SECTION HEADER TABLE | 节头表 | 描述文件中各个节(Section)的信息,如名称、类型、大小等。 |
| 4 | .interp SECTION | 解释器节 | 包含动态链接器(解释器)的路径。 |
| 5 | .text SECTION | 代码段 | 存放程序的机器指令(可执行代码)。 |
| 6 | .rodata SECTION | 只读数据段 | 存放只读数据(如常量字符串等)。 |
| 7 | .data SECTION | 已初始化数据段 | 存放已初始化的全局变量和静态变量。 |
| 8 | .bss SECTION | 未初始化数据段 | 存放未初始化的全局变量和静态变量,运行时会被初始化为0。 |
| 9 | .tdata SECTION | 线程数据段 | 存放线程局部变量的初始值(TLS)。 |
| 10 | .tbss SECTION | 线程未初始化数据段 | 存放线程局部变量的未初始化值(TLS),运行时会被初始化为0。 |
| 11 | .jcr SECTION | Java类注册表 | 用于Java相关的运行时(很少见,一般可略)。 |
| 12 | .ctors SECTION | 构造函数数组 | 存放程序启动时需要调用的构造函数指针数组。 |
| 13 | .dtors SECTION | 析构函数数组 | 存放程序结束时需要调用的析构函数指针数组。 |
| 14 | .preinit_array SECTION | 预初始化数组 | 存放在构造函数前执行的初始化函数数组。 |
| 15 | .init_array SECTION | 初始化数组 | 存放程序启动时需要调用的初始化函数数组。 |
| 16 | .fini_array SECTION | 终止数组 | 存放程序结束时需要调用的终止(清理)函数数组。 |
| 17 | .init SECTION | 初始化代码段 | 存放启动时执行的初始化代码。 |
| 18 | .fini SECTION | 终止代码段 | 存放退出时执行的清理代码。 |
| 19 | .plt SECTION | 过程链接表 | 用于动态链接时的函数调用跳转表(Procedure Linkage Table)。 |
| 20 | .got SECTION | 全局偏移表 | 动态链接时全局变量地址表(Global Offset Table)。 |
| 21 | .plt.got SECTION | 过程链接表/全局偏移表合并 | 某些架构下的混合段。 |
| 22 | .got.plt SECTION | 全局偏移表/过程链接表合并 | 某些架构下的混合段。 |
| 23 | .dynamic SECTION | 动态节 | 存放动态链接所需的信息,如依赖库、符号表地址等。 |
| 24 | .hash SECTION | 哈希表段 | 存放符号表的哈希索引,加速符号查找。 |
| 25 | .symtab SECTION | 符号表 | 存放符号信息(如函数、变量名及其地址等),主要用于静态链接和调试。 |
| 26 | .dynsym SECTION | 动态符号表 | 用于动态链接的符号表,只包含动态链接需要的符号。 |
| 27 | .strtab SECTION | 字符串表 | 存放符号表对应的字符串(如符号名等)。 |
| 28 | .dynstr SECTION | 动态字符串表 | 动态链接用的符号名字符串表。 |
| 29 | .rel.text SECTION / .rela.text | 代码段重定位信息 | 代码段中需要重定位的地址和符号信息。 |
| 30 | .rel.data SECTION / .rela.data | 数据段重定位信息 | 数据段中需要重定位的地址和符号信息。 |
| 31 | .rel.dyn SECTION / .rela.dyn | 动态段重定位信息 | 用于动态链接时的重定位信息。 |
| 32 | .debug SECTION | 调试信息段 | 存放调试信息,如符号、源代码行号等,仅用于调试。 |
| 33 | .note SECTION | 注释段 | 存放额外信息,如编译器、平台、版本等。 |
可以使用 readelf 命令查看 ELF 文件的详细结构信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| $ readelf -WS /bin/ls
There are 28 section headers, starting at offset 0x223c0:
节头:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 0] NULL 0000000000000000 000000 000000 00 0 0 0
[ 1] .note.gnu.property NOTE 0000000000000350 000350 000050 00 A 0 0 8
[ 2] .note.gnu.build-id NOTE 00000000000003a0 0003a0 000024 00 A 0 0 4
[ 3] .interp PROGBITS 00000000000003c4 0003c4 00001c 00 A 0 0 1
[ 4] .gnu.hash GNU_HASH 00000000000003e0 0003e0 00004c 00 A 5 0 8
[ 5] .dynsym DYNSYM 0000000000000430 000430 000be8 18 A 6 1 8
[ 6] .dynstr STRTAB 0000000000001018 001018 0005f3 00 A 0 0 1
[ 7] .gnu.version VERSYM 000000000000160c 00160c 0000fe 02 A 5 0 2
[ 8] .gnu.version_r VERNEED 0000000000001710 001710 0000d0 00 A 6 1 8
[ 9] .rela.dyn RELA 00000000000017e0 0017e0 000b10 18 A 5 0 8
[10] .relr.dyn RELR 00000000000022f0 0022f0 000050 08 A 0 0 8
[11] .init PROGBITS 0000000000003000 003000 00001b 00 AX 0 0 4
[12] .text PROGBITS 0000000000003040 003040 015893 00 AX 0 0 64
[13] .fini PROGBITS 00000000000188d4 0188d4 00000d 00 AX 0 0 4
[14] .rodata PROGBITS 0000000000019000 019000 005270 00 A 0 0 32
[15] .eh_frame_hdr PROGBITS 000000000001e270 01e270 0005dc 00 A 0 0 4
[16] .eh_frame PROGBITS 000000000001e850 01e850 001a50 00 A 0 0 8
[17] .note.ABI-tag NOTE 00000000000202a0 0202a0 000020 00 A 0 0 4
[18] .init_array INIT_ARRAY 0000000000021fd0 020fd0 000008 08 WA 0 0 8
[19] .fini_array FINI_ARRAY 0000000000021fd8 020fd8 000008 08 WA 0 0 8
[20] .data.rel.ro PROGBITS 0000000000021fe0 020fe0 000a50 00 WA 0 0 32
[21] .dynamic DYNAMIC 0000000000022a30 021a30 0001f0 10 WA 6 0 8
[22] .got PROGBITS 0000000000022c20 021c20 0003c8 08 WA 0 0 8
[23] .data PROGBITS 0000000000023000 022000 000278 00 WA 0 0 32
[24] .bss NOBITS 0000000000023280 022278 001300 00 WA 0 0 32
[25] .comment PROGBITS 0000000000000000 022278 00001b 01 MS 0 0 1
[26] .gnu_debuglink PROGBITS 0000000000000000 022294 000010 00 0 0 4
[27] .shstrtab STRTAB 0000000000000000 0222a4 000119 00 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),
D (mbind), l (large), p (processor specific)
|
PROGRAM HEADER TABLE 指示的是段(segment),而 SECTION HEADER TABLE 指示的是节(section)。两者的区别在于:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| ELF 文件
+-------------------------------------------------------------+
| ELF Header | Program Header Table | Section 1 | Section 2 | ...
+-------------------------------------------------------------+
Program Header Table (段) --> 用于运行时加载
|
|--- Segment 1: 可能覆盖 Section 1 + Section 2
|--- Segment 2: 可能只覆盖 Section 3
Section Header Table (节) --> 用于链接/调试
|
|--- Section 1: .text
|--- Section 2: .data
|--- Section 3: .bss
|--- Section 4: .symtab
...
|
segment 揭示了程序在内存中的布局,而 section 则揭示了程序的逻辑结构。
segment 会装载多个权限相同的 section,从而在内存中形成一个连续的区域,节约资源和进行更好的权限控制
静态链接和动态链接
C 语言编译器是如何将多个文件联合编译出来一个可执行文件呢?
答案是通过链接(linker), 那么如何组织程序的链接结构?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // main.c
#include <stdio.h>
extern int add(int a, int b);
int main() {
int a = 1;
int b = 2;
int result = add(a, b);
printf("The result of adding %d and %d is %d\n", a, b, result);
}
// func.c
int add(int a, int b) {
return a + b;
}
|
1
2
3
4
| $ gcc main.c func.c -o main
$ ./main
The result of adding 1 and 2 is 3
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| 目标文件1 目标文件2
+-----------+ +-----------+
| .text | | .text |
+-----------+ +-----------+
| .data | | .data |
+-----------+ +-----------+
| .bss | | .bss |
+-----------+ +-----------+
\ /
\ /
\ /
\ /
\ /
\ /
\ /
\ /
\ /
\ /
\ /
+-------------------+
| 链接器(ld) |
+-------------------+
|
v
+---------------------------+
| .text (合并后) |
+---------------------------+
| .data (合并后) |
+---------------------------+
| .bss (合并后) |
+---------------------------+
|
两个程序中相同的 section 会被合并成一个 section, 从而更好的利用资源和内存空间
系统拥有许多的库文件(类似 C 语言的标准库), 通过在运行时链接还是编译时链接分为动态链接和静态链接
| 序号 | 动态链接(Dynamic Linking) | 静态链接(Static Linking) | 说明对比 |
|---|
| 1 | 在运行时完成链接 | 在编译时完成链接 | 链接时机 |
| 2 | 使用共享库(.so, .dll) | 使用静态库(.a, .lib) | 所用库类型 |
| 3 | 多个程序可共享内存中的库代码 | 每个程序独立拥有一份库代码和数据 | 内存/代码复用 |
| 4 | 运行时加载库(延迟加载可能) | 编译时加载库(不可延迟) | 加载方式 |
| 5 | 可通过升级库文件直接影响所有相关程序 | 升级库文件需重新编译所有相关程序 | 升级与维护性 |
| 6 | 节省磁盘空间(库只存一份) | 占用更多磁盘空间(每个程序都包含库) | 存储占用 |
| 7 | 程序启动和运行时依赖外部库存在 | 程序本身独立,无需外部库 | 运行依赖 |
| 8 | 兼容性较弱(库版本不一致易出问题) | 兼容性较强(所有依赖已固定) | 兼容性 |
| 9 | 运行时调用库函数,性能略低于静态链接 | 库代码已集成,运行时性能更高 | 性能对比 |
| 10 | 可实现库的动态升级/热更新 | 需重新编译和部署,无法动态升级 | 维护方式 |
| 11 | 适用于大型、需频繁升级的应用 | 适用于小型、发行版程序或嵌入式开发 | 典型使用场景 |
字节序
- 低位字节序(Little Endian)/ 小端序 :低位字节存储在低地址,高位字节存储在高地址。
1
2
3
4
5
| 1234 在内存中的存储方式:
\x01\x02\x03\x04
"abcd" 字符串在内存中的存储方式:
\x61\x62\x63\x64
|
gdb 查看
1
2
3
4
| pwndbg> x/1gx 0x7fffffffdfc0
0x7fffffffdfc0: 0x0000000064636261
^ ^
高地址 低地址
|
- 高位字节序(Big Endian)/ 大端序 :高位字节存储在低地址,低位字节存储在高地址。
1
2
| 1234 在内存中的存储方式:
\x04\x03\x02\x01
|