GNU/Linux ELF introduction — Di 03 Juni 2025

关于 GNU 工程

你好,世界!

GNU 是唯一一个为用户自由而开发的计算机系统。GNU 项目尊重计算机用户的自由。

你可能是一个 GNU 用户而你却不知道,因为大多数人错误地把 GNU 系统叫做 Linux,而 Linux 只是 GNU 系统的一个内核。

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 程序的 ELF 文件结构完整解析

一个极简的 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 可执行文件,那么 立伯乐 或许可以帮你。

让自由软件带你进入的美好自由世界!