你好,世界!
GNU 是唯一一个为用户自由而开发的计算机系统。GNU 项目尊重计算机用户的自由。
你可能是一个 GNU 用户而你却不知道,因为大多数人错误地把 GNU 系统叫做 Linux,而 Linux 只是 GNU 系统的一个内核。
GNU 操作系统为所有人提供一整套研究软件源代码和进行编译的系统及其以 自由软件许可证 发布的源代码,让用户可以自主构建二进制可执行文件。
GNU 可执行文件格式通常称为 ELF,即 Executable and Linkable Format。它是 Unix/Linux 系统和 GNU 工具链的标准二进制文件格式,此文件并无固定扩展名,但常见
均可用此格式。
ELF 格式有多种关键特性使之被广泛使用。它是跨平台的格式,支持多种 CPU 架构,包括 x86, ARM, RISC-V 等;它支持位置无关代码 PIC,可通过 .so 共享库实现运行时加载;它容易调试,可包含 DWARF 格式的调试数据;它的结构模块化,代码、数据、调试信息分节存储,清晰简单。
ELF 是理解 GNU、Linux 程序运行的基础,掌握它有助于分析二进制文件、优化性能或调试复杂问题。
GNU 有多种工具链支持 ELF 格式。编译工具 'gcc' 默认生成 ELF 格式的可执行文件;分析工具 'readelf' 可查看 ELF 头信息;使用 'objdump' 可反汇编 ELF 文件的代码段;而链接器 `ld` 可以控制 ELF 文件生成。
ELF 文件的核心结构包含三个主要部分:
我们用命令 'file file-name' 可以查看 ELF 文件的基本情况。例如 'bash' 命令:
file /gnu/store/c1d6ips673iylyfayz1q3kyy89w6bv4m-coreutils-9.1/bin/ls
会输出:
/gnu/store/c1d6ips673iylyfayz1q3kyy89w6bv4m-coreutils-9.1/bin/ls: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /gnu/store/hw6g2kjayxnqi8rwpnmpraalxi0djkxc-glibc-2.39/lib/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, stripped
说明:这是一个64位程序,动态链接到 `ld-linux-x86-64.so.2`,移除了调试符号。
下面我们看下一个更为详细的例子。
一个极简的 hello.c 代码:
#include <stdio.h>
int main() {
printf("Hello World
");
return 0;
}
我们用 gcc 编译之并生成可执行文件:
gcc hello.c -o hello
然后,通过 `readelf` 工具逐层分析。先看文件头:
readelf -h hello
文件头输出以下内容:
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: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x401040
Start of program headers: 64 (bytes into file)
Start of section headers: 13448 (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: 30
Section header string table index: 29
再看程序头:
readelf -l hello
程序头输出以下内容:
Elf file type is EXEC (Executable file)
Entry point 0x401040
There are 13 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040
0x00000000000002d8 0x00000000000002d8 R 0x8
INTERP 0x0000000000000318 0x0000000000400318 0x0000000000400318
0x0000000000000050 0x0000000000000050 R 0x1
[Requesting program interpreter: /gnu/store/hw6g2kjayxnqi8rwpnmpraalxi0djkxc-glibc-2.39/lib/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x00000000000005e8 0x00000000000005e8 R 0x1000
LOAD 0x0000000000001000 0x0000000000401000 0x0000000000401000
0x0000000000000145 0x0000000000000145 R E 0x1000
LOAD 0x0000000000002000 0x0000000000402000 0x0000000000402000
0x00000000000000e4 0x00000000000000e4 R 0x1000
LOAD 0x0000000000002dc8 0x0000000000403dc8 0x0000000000403dc8
0x0000000000000250 0x0000000000000258 RW 0x1000
DYNAMIC 0x0000000000002dd8 0x0000000000403dd8 0x0000000000403dd8
0x0000000000000200 0x0000000000000200 RW 0x8
NOTE 0x0000000000000368 0x0000000000400368 0x0000000000400368
0x0000000000000040 0x0000000000000040 R 0x8
NOTE 0x00000000000003a8 0x00000000004003a8 0x00000000004003a8
0x0000000000000020 0x0000000000000020 R 0x4
GNU_PROPERTY 0x0000000000000368 0x0000000000400368 0x0000000000400368
0x0000000000000040 0x0000000000000040 R 0x8
GNU_EH_FRAME 0x0000000000002010 0x0000000000402010 0x0000000000402010
0x000000000000002c 0x000000000000002c R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x0000000000002dc8 0x0000000000403dc8 0x0000000000403dc8
0x0000000000000238 0x0000000000000238 R 0x1
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.gnu.property .note.ABI-tag .hash .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt
03 .init .plt .text .fini
04 .rodata .eh_frame_hdr .eh_frame
05 .init_array .fini_array .dynamic .got .got.plt .data .bss
06 .dynamic
07 .note.gnu.property
08 .note.ABI-tag
09 .note.gnu.property
10 .eh_frame_hdr
11
12 .init_array .fini_array .dynamic .got
再看节区头:
readelf -S hello
节区头输出以下内容:
There are 30 section headers, starting at offset 0x3488:
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 0000000000400318 00000318
0000000000000050 0000000000000000 A 0 0 1
[ 2] .note.gnu.pr[...] NOTE 0000000000400368 00000368
0000000000000040 0000000000000000 A 0 0 8
[ 3] .note.ABI-tag NOTE 00000000004003a8 000003a8
0000000000000020 0000000000000000 A 0 0 4
[ 4] .hash HASH 00000000004003c8 000003c8
0000000000000024 0000000000000004 A 6 0 8
[ 5] .gnu.hash GNU_HASH 00000000004003f0 000003f0
000000000000001c 0000000000000000 A 6 0 8
[ 6] .dynsym DYNSYM 0000000000400410 00000410
0000000000000060 0000000000000018 A 7 1 8
[ 7] .dynstr STRTAB 0000000000400470 00000470
00000000000000f4 0000000000000000 A 0 0 1
[ 8] .gnu.version VERSYM 0000000000400564 00000564
0000000000000008 0000000000000002 A 6 0 2
[ 9] .gnu.version_r VERNEED 0000000000400570 00000570
0000000000000030 0000000000000000 A 7 1 8
[10] .rela.dyn RELA 00000000004005a0 000005a0
0000000000000030 0000000000000018 A 6 0 8
[11] .rela.plt RELA 00000000004005d0 000005d0
0000000000000018 0000000000000018 AI 6 23 8
[12] .init PROGBITS 0000000000401000 00001000
0000000000000017 0000000000000000 AX 0 0 4
[13] .plt PROGBITS 0000000000401020 00001020
0000000000000020 0000000000000010 AX 0 0 16
[14] .text PROGBITS 0000000000401040 00001040 # 机器指令
00000000000000fb 0000000000000000 AX 0 0 16
[15] .fini PROGBITS 000000000040113c 0000113c
0000000000000009 0000000000000000 AX 0 0 4
[16] .rodata PROGBITS 0000000000402000 00002000 # 字符串常量
0000000000000010 0000000000000000 A 0 0 4
[17] .eh_frame_hdr PROGBITS 0000000000402010 00002010
000000000000002c 0000000000000000 A 0 0 4
[18] .eh_frame PROGBITS 0000000000402040 00002040
00000000000000a4 0000000000000000 A 0 0 8
[19] .init_array INIT_ARRAY 0000000000403dc8 00002dc8
0000000000000008 0000000000000008 WA 0 0 8
[20] .fini_array FINI_ARRAY 0000000000403dd0 00002dd0
0000000000000008 0000000000000008 WA 0 0 8
[21] .dynamic DYNAMIC 0000000000403dd8 00002dd8
0000000000000200 0000000000000010 WA 7 0 8
[22] .got PROGBITS 0000000000403fd8 00002fd8
0000000000000010 0000000000000008 WA 0 0 8
[23] .got.plt PROGBITS 0000000000403fe8 00002fe8
0000000000000020 0000000000000008 WA 0 0 8
[24] .data PROGBITS 0000000000404008 00003008 # 初始化全局变量
0000000000000010 0000000000000000 WA 0 0 8
[25] .bss NOBITS 0000000000404018 00003018 # 未初始化全局变量
0000000000000008 0000000000000000 WA 0 0 1
[26] .comment PROGBITS 0000000000000000 00003018
0000000000000024 0000000000000001 MS 0 0 1
[27] .symtab SYMTAB 0000000000000000 00003040
0000000000000240 0000000000000018 28 8 8
[28] .strtab STRTAB 0000000000000000 00003280
00000000000000ff 0000000000000000 0 0 1
[29] .shstrtab STRTAB 0000000000000000 0000337f
0000000000000103 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),
D (mbind), l (large), p (processor specific)
然后,我们再看看依赖库:
ldd hello
输出以下内容:
linux-vdso.so.1 (0x00007fedbd8b1000)
libgcc_s.so.1 => /gnu/store/22x2qpifqxpj8fy5b94qfbw5y53gkjj7-gcc-15.1.0-lib/lib/libgcc_s.so.1 (0x00007fedbd87e000)
libc.so.6 => /gnu/store/hw6g2kjayxnqi8rwpnmpraalxi0djkxc-glibc-2.39/lib/libc.so.6 (0x00007fedbd6a0000)
/gnu/store/hw6g2kjayxnqi8rwpnmpraalxi0djkxc-glibc-2.39/lib/ld-linux-x86-64.so.2 (0x00007fedbd8b3000)
然后,我们还可用看动态符号表:
readelf --dyn-syms hello
输出以下内容:
Symbol table '.dynsym' contains 4 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND _[...]@GLIBC_2.34 (2)
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5 (3)
3: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
我们甚至可以提取 `.rodata` 中的字符串:
readelf -p .rodata hello
输出以下内容:
String dump of section '.rodata':
[ 4] Hello World
运行程序时,操作系统按以下方式加载 ELF:
0x400000 +-------------------+
| ELF Header |
| Program Headers |
| .text (代码段) | # main() 在这里
| .rodata ("Hello") |
+-------------------+
0x600000 +-------------------+
| .data (全局变量) |
| .bss (未初始化数据) |
| 动态链接信息 |
+-------------------+
我们看到 GNU 可执行文件的分层结构:ELF = Header + Program Headers(运行时) + Section Headers(链接时);其代码与数据的分离:`.text`(代码)只读,`.data`/`.bss`(数据)可写;其动态链接的详情:通过 `INTERP` 和 `.dynamic` 节实现延迟绑定。实际上,最小 Hello World 的 ELF 也仍有约 30 个节区(可通过 `gcc -Wl,--verbose` 查看默认链接脚本,当然可用手动再精简)。
学习了以上的 ELF 结构和分析过程,我们就能够打开 ELF 文件并对之进行手动改造,让它适应自己的需求。
如果你希望自己能够为自由软件作出贡献,并了解更多 GNU 可执行文件,那么 立伯乐 或许可以帮你。
让自由软件带你进入的美好自由世界!