ELF 文件结构与分析

 技术  Linux  ELF 󰈭 13152字

本文主要整理与学习了 ELF 文件的基本格式, 使用 readelf, gdb, objdump 等工具实际分析了 ELF 部分节区的字段细节, 但受于篇幅限制, 未对与 ELF 关联甚深的程序动静态链接、加载过程进行深入分析, 因此本文仅作为 ELF 基本结构的一个基本介绍与入门.

ELF 简介

可执行与可链接格式 ELF (Extensible Linking Format) 是一种用于可执行文件、目标代码、共享库和内核转储的通用标准文件格式, 主要在类Unix系统中使用. ELF 格式具有灵活性、可扩展性和跨平台性, 能在许多不同的硬件平台上被许多不同的操作系统所采用.

ELF 的主要使用场景如下:

  • 可执行文件; 当运行 Linux Shell 上的各种命令时(ls, grep) 时, 都是执行的 ELF 格式的文件. 其包含程序的机器代码和必要的元数据, 定义入口点、程序头表等执行所需信息, 加载器(loader)能直接将其映射到内存并执行.

  • 动态库/共享库(.so 文件); 共享库允许多个程序共享同一份代码,节省内存. 其支持动态链接,程序运行时才加载所需库

    • 动态库与共享库本质是两个名称的一个东西, 例如在 Windows 下的 .dll 动态链接库 Dynamic Link Library, 在 Linux/Unix 下的 .so 共享对象(Shared Object)
  • 目标文件(.o 文件); 目标文件是那些编译但未链接的中间文件, 其包含编译后的机器代码, 但不能直接执行, 其保存了符号表、重定位信息等待链接器处理, 链接器最终将多个.o文件合并为可执行文件或共享库; 目标文件允许分离编译,提高构建大型项目的效率

  • 静态库文件(.a 文件); 静态库本质上是多个目标文件(.o)的归档集合, 在链接时相关代码会被直接复制到最终的可执行文件中, 因此部署更加简单, 不依赖外部库

  • 内核模块(.ko 文件); 内核模块作为 ELF 文件包含了特殊的段和符号信息, 如 .modinfo 节包含模块元数据(作者、描述、版本等), .module_layout 存储内核版本和布局信息, .symtab.strtab包含大量内核特定符号等

  • 内核转储文件(coredump); 系统崩溃或程序异常终止时生成的调试信息, 记录程序崩溃时的内存状态、寄存器值等信息

ELF 文件基本结构

ELF 格式的文件可能既会参与程序链接又会参与程序执行, 根据使用过程的不同, 其被设计为同时拥有两种并行的视图:

  • 链接视图(Linking View): 主要用于链接器处理, 关注节(sections)

  • 执行视图(Execution View): 主要用于加载器处理, 关注段(segments)

ELF 文件的两种视图

总的来说, ELF 可以分为四大部分: ELF Header, ELF Program Header Table (或称Program Headers、程序头)、ELF Section Header Table (或称Section Headers、节头表)以及若干ELF Sections.

  • 尽管图中是按照 ELF 头, 程序头部表、节区、节区头部表的顺序排列的, 但实际上除了 ELF 头部表以外, 其它部分都没有严格的的顺序.
ELF 不同组成部分的索引关系
readelf 工具简介?

readelf 是一个强大的命令行工具, 用于分析 ELF 文件的内容. 通过 readelf, 你可以查看可执行文件、共享库、目标文件的内部结构细节.

readelf 的基本语法是: readelf [options] elffile, 常用选项为:

  • -h; 显示 ELF header 中的信息

  • -S; 列出文件的所有节, 显示 Section headers 的相关信息(Name, Type, Address 以及 Offset 等)

  • -l; 列出文件的程序头表以及所有段

  • -s; 显示文件的符号表信息, 包括函数名、全局变量等及其地址

  • -r; 显示重定位信息, 涉及动态链接过程

  • -d; 显示动态段的内容, 包括共享库依赖、符号表位置等.

  • -n; 显示文件中的注释段内容

  • -a; 显示文件的所有信息

  • -x <number or name>; 以 16 进制查看特定节的内容

更多的信息可以通过man查看 readelf 指令的说明.

ELF Header

ELF 头部的 ELF Header 给出了整个文件的组织情况, 使用 readelf -h 可以看到对应的内容. 使用 g++ 不加任何额外选项的编译一段简单的 C代码, 生成的可执行文件的头信息如下:

bash
 1$ readelf -h a.out
 2ELF Header:
 3  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
 4  Class:                             ELF64
 5  Data:                              2's complement, little endian
 6  Version:                           1 (current)
 7  OS/ABI:                            UNIX - System V
 8  ABI Version:                       0
 9  Type:                              DYN (Position-Independent Executable file)
10  Machine:                           Advanced Micro Devices X86-64
11  Version:                           0x1
12  Entry point address:               0x2210
13  Start of program headers:          64 (bytes into file)
14  Start of section headers:          55040 (bytes into file)
15  Flags:                             0x0
16  Size of this header:               64 (bytes)
17  Size of program headers:           56 (bytes)
18  Number of program headers:         13
19  Size of section headers:           64 (bytes)
20  Number of section headers:         32
21  Section header string table index: 31

其中比较重要的字段:

  • 魔数 Magic; 作为 ELF 文件的签名, 总是以固定的字节序列开始: 0x7F后跟ASCII码"ELF"(45 4c 46). 操作系统通过检查这个魔数来确认文件是ELF格式. 后面的字节包含了其他元信息如位宽、字节序等.
魔数 Magic Number

每种可执行文件的格式的开头几个字节都是很特殊的, 特别是开头4个字节, 通常被称为魔数(Magic Number). 通过对魔数的判断可以确定文件的格式和类型.

ELF的可执行文件格式的头4个字节为0x7F、e、l、f; Java的可执行文件格式的头4个字节为c、a、f、e; 如果被执行的是Shell脚本或perl、python等解释型语言的脚本, 那么它的第一行往往是#!/bin/sh#!/usr/bin/perl#!/usr/bin/python, 此时前两个字节#!就构成了魔数, 系统一旦判断到这两个字节, 就对后面的字符串进行解析, 以确定具体的解释程序路径(该特性称为 Shebang).

  • 文件类型 Type: ELF 文件主要有四种类型: 可重定位文件 ET_REL、可执行文件ET_EXEC, 共享目标文件ET_DYN、核心转储文件ET_CORE. 但是可以看到, 这里我们使用 g++ 生成的是一个 DYN 类型的文件, 括号里进一步说明这是一个 PIE 程序, 这不是一个传统的 EXEC 类型的可执行文件, 这是由于安全考虑的演进结果.

    • 问题背景: 传统上, 可执行文件是 EXEC 类型, 它们有固定的加载地址. 但随着安全威胁的增加, 特别是缓冲区溢出和返回导向编程(ROP)攻击, 操作系统引入了地址空间布局随机化(ASLR)作为防御机制. 但 ASLR 最初只能随机化共享库和堆栈的位置, 而传统 EXEC 类型的可执行文件主体依然加载在固定地址, 成为攻击者的目标. 为解决这个问题, PIE(位置无关可执行文件)被开发出来, 允许整个程序(包括代码段)被随机加载.

    • 工作原理: 当 g++ 生成 PIE 可执行文件时,其使用相对寻址而非绝对寻址, 编译所有代码为位置无关代码(PIC), 并将可执行文件标记为 DYN 类型

    • 程序加载: 操作系统加载器看到 DYN 类型的可执行文件时, 会将其当作共享库一样处理: 随机选择一个基地址, 然后相应调整所有内部引用. 这种行为很像动态库, 因此这些文件被标记为 DYN 类型, 尽管它们实际上是可执行文件.

    • 使用 g++ -no-pie 选项进行编译程序, 即可编译一个传统的 EXEC 类型的可执行文件.

  • 兼容性相关字段, 包括 Machine 目标架构, Class 类别, Data 数据编码等

  • 该ELF 文件结构相关字段, 包括程序头表、节头表的偏移量、长度与数量等

程序头表

如果程序头部表(Program Header Table)存在的话, 它会告诉系统如何创建进程. 用于生成进程的目标文件必须具有程序头部表, 但是重定位文件不需要这个表.

一个可执行文件(PIE)的程序头表示例如下:

bash
 1$ readelf -l a.out
 2
 3Elf file type is DYN (Position-Independent Executable file)
 4Entry point 0x21f0
 5There are 13 program headers, starting at offset 64
 6
 7Program Headers:
 8  Type           Offset             VirtAddr           PhysAddr
 9                 FileSiz            MemSiz              Flags  Align
10  PHDR           0x0000000000000040 0x0000000000000040 0x0000000000000040
11                 0x00000000000002d8 0x00000000000002d8  R      0x8
12  INTERP         0x0000000000000318 0x0000000000000318 0x0000000000000318
13                 0x000000000000001c 0x000000000000001c  R      0x1
14      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
15  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
16                 0x0000000000001278 0x0000000000001278  R      0x1000
17  LOAD           0x0000000000002000 0x0000000000002000 0x0000000000002000
18                 0x0000000000003579 0x0000000000003579  R E    0x1000
19  LOAD           0x0000000000006000 0x0000000000006000 0x0000000000006000
20                 0x0000000000001780 0x0000000000001780  R      0x1000
21  LOAD           0x0000000000007da0 0x0000000000008da0 0x0000000000008da0
22                 0x0000000000000358 0x00000000007a15f0  RW     0x1000
23  DYNAMIC        0x0000000000007db0 0x0000000000008db0 0x0000000000008db0
24                 0x0000000000000210 0x0000000000000210  RW     0x8
25  NOTE           0x0000000000000338 0x0000000000000338 0x0000000000000338
26                 0x0000000000000040 0x0000000000000040  R      0x8
27  NOTE           0x0000000000000378 0x0000000000000378 0x0000000000000378
28                 0x0000000000000044 0x0000000000000044  R      0x4
29  GNU_PROPERTY   0x0000000000000338 0x0000000000000338 0x0000000000000338
30                 0x0000000000000040 0x0000000000000040  R      0x8
31  GNU_EH_FRAME   0x0000000000006068 0x0000000000006068 0x0000000000006068
32                 0x0000000000000454 0x0000000000000454  R      0x4
33  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
34                 0x0000000000000000 0x0000000000000000  RW     0x10
35  GNU_RELRO      0x0000000000007da0 0x0000000000008da0 0x0000000000008da0
36                 0x0000000000000260 0x0000000000000260  R      0x1
37
38 Section to Segment mapping:
39  Segment Sections...
40   00
41   01     .interp
42   02     .interp .note.gnu.property .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt
43   03     .init .plt .text .fini
44   04     .rodata .eh_frame_hdr .eh_frame .gcc_except_table
45   05     .init_array .fini_array .dynamic .got .got.plt .data .bss
46   06     .dynamic
47   07     .note.gnu.property
48   08     .note.ABI-tag .note.gnu.build-id
49   09     .note.gnu.property
50   10     .eh_frame_hdr
51   11
52   12     .init_array .fini_array .dynamic .got

每个程序头对应的字段含义如下:

字段 说明
p_type 该字段为段的类型,或者表明了该结构的相关信息
p_offset 该字段给出了从文件开始到该段开头的第一个字节的偏移
p_vaddr 该字段给出了该段第一个字节在内存中的虚拟地址
p_paddr 该字段仅用于物理地址寻址相关的系统中
p_filesz 该字段给出了文件镜像中该段的大小,可能为0
p_memsz 该字段给出了内存镜像中该段的大小,可能为0
p_flags 该字段给出了与段相关的标记
p_align 该字段给出了段在文件以及内存中的对齐方式

段类型

段的类型 p_type 如下表所示.

需要注意, 程序头部表中的段类型又可以分为基本段和扩展段两大类:

  • 基本段 是ELF规范最初定义的标准段类型, 在所有符合ELF标准的系统中都被支持和识别. 这些段类型构成了ELF可执行文件的基础结构, 对于程序的加载和执行是必不可少的. 基本段类型通常具有较小的类型值(0-7), 并在原始ELF规范中明确定义.

  • 扩展段 是在ELF基本规范之外, 由特定操作系统、编译器或工具链引入的额外段类型. 这些段类型通常用于实现特定于平台的功能、安全特性或性能优化.

段类型 描述 用途
PT_NULL 0 未使用段 表示一个未使用的程序头部表项,系统会忽略该段
PT_LOAD 1 可加载段 定义需要从文件映射到内存中的段,包含代码(.text)和数据(.data)等
PT_DYNAMIC 2 动态链接信息段 包含动态链接器所需的信息,如共享库依赖、符号表位置等
PT_INTERP 3 解释器段 包含程序解释器路径的字符串,通常指向动态链接器,如"/lib/ld-linux.so.2"
PT_NOTE 4 附加信息段 存储辅助信息,如版本、供应商信息、ABI 兼容性说明等
PT_SHLIB 5 保留段 在当前 ELF 规范中未定义用途,保留供将来使用
PT_PHDR 6 程序头部表段 包含程序头部表自身在内存中的位置和大小信息
PT_TLS 7 线程局部存储段 包含线程局部变量的初始值和模板,用于线程专有数据
PT_GNU_EH_FRAME 0x6474e550 异常处理框架段 GNU 特有段类型,包含用于栈展开的异常处理信息
PT_GNU_STACK 0x6474e551 栈权限段 GNU 扩展,用于标记程序栈是否具有可执行权限
PT_GNU_RELRO 0x6474e552 只读重定位段 GNU 扩展,标记在初始化后应设为只读的段,增强安全性
PT_LOPROC 0x70000000 处理器特定段下限 为处理器特定语义保留的段类型范围的下界
PT_HIPROC 0x7fffffff 处理器特定段上限 为处理器特定语义保留的段类型范围的上界

段地址

ELF 文件中程序段的 p_vaddr 表示该段在虚拟内存中的预期加载地址, 但这个地址通常不是最终的实际加载地址, 而是需要与基地址(Base Address)结合计算.

在讨论一个可执行程序实际的虚拟地址之前, 需要首先明确其是否使用了 PIC 位置无关代码技术:

  • 对于那些使用绝对地址的代码, 程序在编译和链接时, 就假定它将在特定的虚拟地址空间中运行, 加载器也会尝试将程序段放置在它们指定的虚拟地址上.

  • 但对于可能被加载到任何位置的共享库或是 PIC/PIE 程序而言, 其完全避免了绝对地址引用, 而是通过相对寻址的方式来访问自己的代码和数据, 使用全局偏移表(GOT)和过程链接表(PLT)处理动态符号, 段和段之间维持着相对位置关系即可.

因此, 程序段在内存中的实际地址计算公式为:

text
1实际加载地址 = 基地址 + (p_vaddr - 最小可加载段的p_vaddr)
  • 基地址由动态加载器 (如 ld.so) 在加载时决定, 还会受到 ASLR、页对齐等机制的影响.

段标志位

当系统为可加载的段创建内存镜像时, 它会按照 p_flags 将段设置为对应的权限:

名称 含义
0x1 PF_X 可执行段(Executable segment)
0x2 PF_W 可写段(Writeable segment)
0x4 PF_R 可读段(Readable segment)

节区头表

节区头部表(Section Header Table)包含了描述文件节区的信息, 每个节区在表中都有一个表项, 会给出节区名称、节区大小等信息.

bash
 1$ readelf -S a.out
 2There are 31 section headers, starting at offset 0xc558:
 3
 4Section Headers:
 5  [Nr] Name              Type             Address           Offset
 6       Size              EntSize          Flags  Link  Info  Align
 7  [ 0]                   NULL             0000000000000000  00000000
 8       0000000000000000  0000000000000000           0     0     0
 9  [ 1] .interp           PROGBITS         0000000000000318  00000318
10       000000000000001c  0000000000000000   A       0     0     1
11  [ 2] .note.gnu.pr[...] NOTE             0000000000000338  00000338
12       0000000000000040  0000000000000000   A       0     0     8
13  [ 3] .note.ABI-tag     NOTE             0000000000000378  00000378
14       0000000000000020  0000000000000000   A       0     0     4
15  [ 4] .note.gnu.bu[...] NOTE             0000000000000398  00000398
16       0000000000000024  0000000000000000   A       0     0     4
17  [ 5] .gnu.hash         GNU_HASH         00000000000003c0  000003c0
18       0000000000000024  0000000000000000   A       6     0     8
19  [ 6] .dynsym           DYNSYM           00000000000003e8  000003e8
20       0000000000000360  0000000000000018   A       7     1     8
21  [ 7] .dynstr           STRTAB           0000000000000748  00000748
22       000000000000067a  0000000000000000   A       0     0     1
23  [ 8] .gnu.version      VERSYM           0000000000000dc2  00000dc2
24       0000000000000048  0000000000000002   A       6     0     2
25  [ 9] .gnu.version_r    VERNEED          0000000000000e10  00000e10
26       00000000000000f0  0000000000000000   A       7     4     8
27  [10] .rela.dyn         RELA             0000000000000f00  00000f00
28       00000000000000d8  0000000000000018   A       6     0     8
29  [11] .rela.plt         RELA             0000000000000fd8  00000fd8
30       00000000000002a0  0000000000000018  AI       6    24     8
31  [12] .init             PROGBITS         0000000000002000  00002000
32       000000000000001b  0000000000000000  AX       0     0     4
33  [13] .plt              PROGBITS         0000000000002020  00002020
34       00000000000001d0  0000000000000010  AX       0     0     16
35  [14] .text             PROGBITS         00000000000021f0  000021f0
36       000000000000337c  0000000000000000  AX       0     0     16
37  [15] .fini             PROGBITS         000000000000556c  0000556c
38       000000000000000d  0000000000000000  AX       0     0     4
39  [16] .rodata           PROGBITS         0000000000006000  00006000
40       0000000000000067  0000000000000000   A       0     0     8
41  [17] .eh_frame_hdr     PROGBITS         0000000000006068  00006068
42       0000000000000454  0000000000000000   A       0     0     4
43  [18] .eh_frame         PROGBITS         00000000000064c0  000064c0
44       00000000000011f8  0000000000000000   A       0     0     8
45  [19] .gcc_except_table PROGBITS         00000000000076b8  000076b8
46       00000000000000c8  0000000000000000   A       0     0     1
47  [20] .init_array       INIT_ARRAY       0000000000008da0  00007da0
48       0000000000000008  0000000000000008  WA       0     0     8
49  [21] .fini_array       FINI_ARRAY       0000000000008da8  00007da8
50       0000000000000008  0000000000000008  WA       0     0     8
51  [22] .dynamic          DYNAMIC          0000000000008db0  00007db0
52       0000000000000210  0000000000000010  WA       7     0     8
53  [23] .got              PROGBITS         0000000000008fc0  00007fc0
54       0000000000000028  0000000000000008  WA       0     0     8
55  [24] .got.plt          PROGBITS         0000000000008fe8  00007fe8
56       00000000000000f8  0000000000000008  WA       0     0     8
57  [25] .data             PROGBITS         00000000000090e0  000080e0
58       0000000000000018  0000000000000000  WA       0     0     8
59  [26] .bss              NOBITS           0000000000009100  000080f8
60       00000000007a1290  0000000000000000  WA       0     0     32
61  [27] .comment          PROGBITS         0000000000000000  000080f8
62       0000000000000036  0000000000000001  MS       0     0     1
63  [28] .symtab           SYMTAB           0000000000000000  00008130
64       0000000000001578  0000000000000018          29    10     8
65  [29] .strtab           STRTAB           0000000000000000  000096a8
66       0000000000002d84  0000000000000000           0     0     1
67  [30] .shstrtab         STRTAB           0000000000000000  0000c42c
68       0000000000000128  0000000000000000           0     0     1
69Key to Flags:
70  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
71  L (link order), O (extra OS processing required), G (group), T (TLS),
72  C (compressed), x (unknown), o (OS specific), E (exclude),
73  D (mbind), l (large), p (processor specific)

每个节区头对应的字段含义如下:

成员 说明
sh_name 节名称, 本质是一个数值, 作为节区头字符串表节区(Section Header String Table Section)的索引, 在该表中是一个以 NULL 结尾的字符串
sh_type 节区类型
sh_flags 每一bit代表不同的标志, 描述节是否可写, 可执行, 需要分配内存等属性
sh_addr 如果节区将出现在进程的内存映像中, 此成员给出节区的第一个字节应该在进程镜像中的位置. 否则, 此字段为 0
sh_offset 给出节区的第一个字节与文件开始处之间的偏移. SHT_NOBITS 类型的节区不占用文件的空间, 因此其 sh_offset 成员给出的是概念性的偏移
sh_size 此成员给出节区的字节大小. 除非节区的类型是 SHT_NOBITS, 否则该节占用文件中的 sh_size 字节. 类型为 SHT_NOBITS 的节区长度可能非零, 不过却不占用文件中的空间
sh_link 此成员给出节区头部表索引链接, 其具体的解释依赖于节区类型
sh_info 此成员给出附加信息, 其解释依赖于节区类型.
sh_addralign 某些节区的地址需要对齐. 目前它仅允许为 0 以及 2 的正整数幂数, 0 和 1 表示没有对齐约束.
sh_entsize 某些节区中存在具有固定大小的表项的表, 如符号表. 对于这类节区, 该成员给出每个表项的字节大小. 反之, 此成员取值为 0.

节区头部表中存在一些特殊的节区, 如:

  • SHN_UNDEF(0), 表示未定义的节区

  • SHN_LORESERVE(0xFF00), 表示保留区的下界, SHN_HIRESERVE(0xFFFF), 表示保留区的上界

    • 系统保留在 SHN_LORESERVESHN_HIRESERVE之间(包含边界)的索引值,这些值不在节头表中引用, 节头表不包含保留索引项

节区类型

名称 取值 说明
SHT_NULL 0 该类型节区是非活动的, 这种类型的节头中的其它成员取值无意义.
SHT_PROGBITS 1 该类型节区包含程序定义的信息, 它的格式和含义都由程序来决定.
SHT_SYMTAB 2 该类型节区包含一个符号表(SYMbol TABle). 目前目标文件对每种类型的节区都只能包含一个, 不过这个限制将来可能发生变化. 一般, SHT_SYMTAB 节区提供用于链接编辑(指 ld 而言)的符号, 尽管也可用来实现动态链接.
SHT_STRTAB 3 该类型节区包含字符串表(STRing TABle).
SHT_RELA 4 该类型节区包含显式指定位数的重定位项(RELocation entry with Addends), 例如, 32 位目标文件中的 Elf32_Rela 类型. 此外, 目标文件可能拥有多个重定位节区.
SHT_HASH 5 该类型节区包含符号哈希表(HASH table).
SHT_DYNAMIC 6 该类型节区包含动态链接的信息(DYNAMIC linking).
SHT_NOTE 7 该类型节区包含以某种方式标记文件的信息(NOTE).
SHT_NOBITS 8 该类型节区不占用文件的空间, 其它方面和SHT_PROGBITS相似. 尽管该类型节区不包含任何字节, 其对应的节头成员sh_offset 中还是会包含概念性的文件偏移.
SHT_REL 9 该类型节区包含重定位表项(RELocation entry without Addends), 不过并没有指定位数. 例如, 32位目标文件中的 Elf32_rel 类型. 目标文件中可以拥有多个重定位节区.
SHT_SHLIB 10 该类型此节区被保留, 不过其语义尚未被定义.
SHT_DYNSYM 11 作为一个完整的符号表, 它可能包含很多对动态链接而言不必要的符号. 因此, 目标文件也可以包含一个 SHT_DYNSYM 节区, 其中保存动态链接符号的一个最小集合, 以节省空间.
SHT_LOPROC 0X70000000 此值指定保留给处理器专用语义的下界(LOw PROCessor-specific semantics).
SHT_HIPROC 0X7FFFFFFF 此值指定保留给处理器专用语义的上界(HIgh PROCessor-specific semantics).
SHT_LOUSER 0X80000000 此值指定保留给应用程序的索引下界.
SHT_HIUSER 0X8FFFFFFF 此值指定保留给应用程序的索引上界.

节区标志位

节头中 sh_flags 字段的每一个比特位都可以给出其相应的标记信息, 其定义了对应的节区的内容是否可以被修改、被执行等信息.

名称 说明
SHF_WRITE 0x1 这种节包含了进程运行过程中可以被写的数据.
SHF_ALLOC 0x2 这种节在进程运行时占用内存. 对于不占用目标文件的内存镜像空间的某些控制节, 该属性处于关闭状态(off).
SHF_EXECINSTR 0x4 这种节包含可执行的机器指令(EXECutable INSTRuction).
SHF_MASKPROC 0xf0000000 所有在这个掩码中的比特位用于特定处理器语义.

当节区类型的不同的时候,sh_linksh_info 也会具有不同的含义:

sh_type sh_link sh_info
SHT_DYNAMIC 节区中使用的字符串表的节头索引 0
SHT_HASH 此哈希表所使用的符号表的节头索引 0
SHT_REL/SHT_RELA 与符号表相关的的节头索引 重定位应用到的节的节头索引
SHT_SYMTAB/SHT_DYNSYM 操作系统特定信息, Linux 中的 ELF 文件中该项指向符号表中符号所对应的字符串节区在 Section Header Table 中的偏移. 操作系统特定信息
other SHN_UNDEF 0

节区

节区部分包含在链接视图中要使用的大部分信息: 指令、数据、符号表、重定位信息等等.

下表罗列了一些重要的节区:

名称 类型 属性 含义
.bss SHT_NOBITS SHF_ALLOC + SHF_WRITE 包含将出现在程序的内存映像中的为初始化数据。根据定义,当程序开始执行,系统将把这些数据初始化为 0。此节区不占用文件空间。
.comment SHT_PROGBITS (无) 包含版本控制信息。
.data SHT_PROGBITS SHF_ALLOC + SHF_WRITE 这些节区包含初始化了的数据,将出现在程序的内存映像中。
.data1 SHT_PROGBITS SHF_ALLOC + SHF_WRITE 这些节区包含初始化了的数据,将出现在程序的内存映像中。
.debug SHT_PROGBITS (无) 此节区包含用于符号调试的信息。
.dynamic SHT_DYNAMIC SHF_ALLOC 位,是否 SHF_WRITE 位被设置取决于处理器。 此节区包含动态链接信息。节区的属性将包含 SHF_ALLOC 位。
.dynstr SHT_STRTAB SHF_ALLOC 此节区包含用于动态链接的字符串,大多数情况下这些字符串代表了与符号表项相关的名称。
.dynsym SHT_DYNSYM SHF_ALLOC 此节区包含了动态链接符号表。
.fini SHT_PROGBITS SHF_ALLOC + SHF_EXECINSTR 此节区包含了可执行的指令,是进程终止代码的一部分。程序正常退出时,系统将安排执行这里的代码。
.got SHT_PROGBITS 此节区包含全局偏移表。
.hash SHT_HASH SHF_ALLOC 此节区包含了一个符号哈希表。
.init SHT_PROGBITS SHF_ALLOC + SHF_EXECINSTR 此节区包含了可执行指令,是进程初始化代码的一部分。当程序开始执行时,系统要在开始调用主入口之前(通常指 C 语言的 main 函数)执行这些代码。
.interp SHT_PROGBITS 此节区包含程序解释器的路径名。如果程序包含一个可加载的段,段中包含此节区,那么节区的属性将包含 SHF_ALLOC 位,否则该位为 0。
.line SHT_PROGBITS (无) 此节区包含符号调试的行号信息,其中描述了源程序与机器指令之间的对应关系。其内容是未定义的。
.note SHT_NOTE (无) 此节区中包含注释信息,有独立的格式。
.plt SHT_PROGBITS 此节区包含过程链接表(procedure linkage table)。
.rel[name] SHT_REL 这些节区中包含了重定位信息。如果文件中包含可加载的段,段中有重定位内容,节区的属性将包含 SHF_ALLOC 位,否则该位置 0。传统上 name 根据重定位所适用的节区给定。例如 .text 节区的重定位节区名字将是:.rel.text 或者 .rela.text。
.rela[name] SHT_RELA 这些节区中包含了重定位信息。如果文件中包含可加载的段,段中有重定位内容,节区的属性将包含 SHF_ALLOC 位,否则该位置 0。
.rodata SHT_PROGBITS SHF_ALLOC 这些节区包含只读数据,这些数据通常参与进程映像的不可写段。
.rodata1 SHT_PROGBITS SHF_ALLOC 这些节区包含只读数据,这些数据通常参与进程映像的不可写段。
.shstrtab SHT_STRTAB 此节区包含节区名称。
.strtab SHT_STRTAB 此节区包含字符串,通常是代表与符号表项相关的名称。如果文件拥有一个可加载的段,段中包含符号串表,节区的属性将包含 SHF_ALLOC 位,否则该位为 0。
.symtab SHT_SYMTAB 此节区包含一个符号表。如果文件中包含一个可加载的段,并且该段中包含符号表,那么节区的属性中包含SHF_ALLOC 位,否则该位置为 0。
.text SHT_PROGBITS SHF_ALLOC + SHF_EXECINSTR 此节区包含程序的可执行指令。

段(Segments)是ELF文件在执行视图(Execution View)中的基本单位, 与链接视图中的节(Sections)不同, 段是程序加载到内存时实际使用的单位.

链接视图的节(Sections)提供了非常细粒度的程序组织方式, 这对编译器和链接器很有用. 但对操作系统加载器来说, 处理大量节会增加复杂性并降低效率. 段提供了一种更高层次的抽象, 将具有相似属性(如内存权限)的多个节组合在一起, 简化了加载过程.

一个简单的 ELF 可执行文件的内存布局如下:

Text
 1虚拟地址空间
 2+------------------+ 高地址
 3|      栈区域      |
 4|        ↓         |
 5|                  |
 6|        ↑         |
 7|      堆区域      |
 8+------------------+
 9|    未映射区域    |
10+------------------+
11|                  |
12|    数据段(rw-)   | ← 包含.data, .bss等节
13|                  |
14+------------------+
15|                  |
16|   代码段(r-x)    | ← 包含.text, .rodata等节
17|                  |
18+------------------+ 低地址

节区类型

字符串表节区

对于字符串表类型 SHT_STRTAB 来说, 其包含了以 NULL(0) 结尾的字符串序列, 对字符串的引用通常以字符串在表中的下标来给出, 例如节区头表的 sh_name 便是使用这种方式来给出自身的名字, 节区头表关联的是 .shstrtab 节(Section Header String Table).

符号表节区

对于符号表类型 SHT_SYMTAB, 其包含了用来定位、重定位程序中符号定义和引用的信息, 表项字段含义如下:

字段 说明
st_name 符号在字符串表中对应的索引. 如果该值非 0, 则它表示了给出符号名的字符串表索引, 否则符号表项没有名称. 注: 外部 C 符号在 C 语言和目标文件的符号表中具有相同的名称.
st_value 给出与符号相关联的数值, 具体取值依赖于上下文, 可能是一个正常的数值、一个地址等等.
st_size 给出对应符号所占用的大小. 如果符号没有大小或者大小未知, 则此成员为0.
st_info 给出符号的类型和绑定属性. 之后会给出若干取值和含义的绑定关系.
st_other 目前为0, 其含义没有被定义.
st_shndx 每个符号表项都以和其他节区间的关系给出定义, 此成员给出相关的节区头表索引

st_value

st_value 在不同的上下文下含义不同, 在文章 ELF文件基本结构 - CTF Wiki 中其说明:

  • 当符号对应为一个变量时, st_value 表明该变量在内存中的偏移.

    • 获取该符号对应的 st_shndx, 进而获取到相关的节区

    • 根据节区头元素可以获取节区的虚拟基地址和文件基地址

    • value-内存基虚拟地址=文件偏移-文件基地址

  • 当符号对应为一个函数时, st_value 表明该函数在文件中的偏移.

但这其实貌似是不完全对的. ELF 中不会区别对待变量或函数, st_value 的含义由以下的因素决定:

  • 文件类型(可重定位文件、可执行文件或共享对象文件)

  • 符号绑定类型(局部、全局、弱符号)

更加精确的结论:

  • 在可重定位文件(.o文件), 对于所有已定义的符号(无论是变量还是函数), st_value 都表示节区偏移量, 也就是相对于由 st_shndx 标识的节区起始位置的偏移量. 这一点对于变量和函数本质上是相同的.

  • 在可执行文件或共享对象文件中: 对于所有已定义的符号, st_value 都表示虚拟内存地址.

为了更好地理解 st_value, 我们使用一段 C 代码来演示:

c
 1#include <stdio.h>
 2
 3// 全局变量
 4int global_var = 42;
 5
 6// 静态全局变量
 7static int static_global = 100;
 8
 9// 函数声明
10void another_function(void);
11
12int main(void) {
13    int local_var = 10;
14    printf("全局变量: %d\n", global_var);
15    another_function();
16    return 0;
17}
18
19void another_function(void) {
20    printf("另一个函数被调用\n");
21}

这边我们主要关注三个符号: 全局变量 global_var, 静态全局变量 static_global 以及一个函数 another_function.

首先使用-c 选项编译出来 .o 目标文件, 并查看其符号表以及节区头表:

bash
 1$ gcc -c symbols.c -o symbols.o
 2
 3$ readelf -s symbols.o
 4
 5Symbol table '.symtab' contains 10 entries:
 6   Num:    Value          Size Type    Bind   Vis      Ndx Name
 7     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
 8     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS symbols.c
 9     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 .text
10     3: 0000000000000004     4 OBJECT  LOCAL  DEFAULT    3 static_global
11     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 .rodata
12     5: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    3 global_var
13     6: 0000000000000000    55 FUNC    GLOBAL DEFAULT    1 main
14     7: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND printf
15     8: 0000000000000037    22 FUNC    GLOBAL DEFAULT    1 another_function
16     9: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND puts
17
18$ readelf -S symbols.o
19There are 14 section headers, starting at offset 0x3d8:
20
21Section Headers:
22  [Nr] Name              Type             Address           Offset
23       Size              EntSize          Flags  Link  Info  Align
24  [ 0]                   NULL             0000000000000000  00000000
25       0000000000000000  0000000000000000           0     0     0
26  [ 1] .text             PROGBITS         0000000000000000  00000040
27       000000000000004d  0000000000000000  AX       0     0     1
28  [ 2] .rela.text        RELA             0000000000000000  000002a0
29       0000000000000090  0000000000000018   I      11     1     8
30  [ 3] .data             PROGBITS         0000000000000000  00000090
31       0000000000000008  0000000000000000  WA       0     0     4
32
33       ...

通过查看节区头表, 可以看到索引 Ndx 1, 3 分别对应.text代码段以及.data数据段, 此时st_value表示该符号在对应的分区的偏移量. 使用 objdump 进行验证:

bash
 1#################
 2## 查看数据段 ###
 3#################
 4$ objdump -s -j .data symbols.o
 5
 6symbols.o:     file format elf64-x86-64
 7
 8Contents of section .data:
 9 0000 2a000000 64000000                    *...d...
10
11
12#################
13## 查看代码段 ###
14#################
15# 直接查看 .text 不够直观
16$ objdump -s -j .text symbols.o
17
18# 反汇编查看代码段
19$ objdump -d symbols.o
20
21symbols.o:     file format elf64-x86-64
22
23
24Disassembly of section .text:
25
260000000000000000 <main>:
27   0:	55                   	push   %rbp
28   1:	48 89 e5             	mov    %rsp,%rbp
29   4:	48 83 ec 10          	sub    $0x10,%rsp
30   8:	c7 45 fc 0a 00 00 00 	movl   $0xa,-0x4(%rbp)
31   f:	8b 05 00 00 00 00    	mov    0x0(%rip),%eax        # 15 <main+0x15>
32  15:	89 c6                	mov    %eax,%esi
33  17:	48 8d 05 00 00 00 00 	lea    0x0(%rip),%rax        # 1e <main+0x1e>
34  1e:	48 89 c7             	mov    %rax,%rdi
35  21:	b8 00 00 00 00       	mov    $0x0,%eax
36  26:	e8 00 00 00 00       	call   2b <main+0x2b>
37  2b:	e8 00 00 00 00       	call   30 <main+0x30>
38  30:	b8 00 00 00 00       	mov    $0x0,%eax
39  35:	c9                   	leave
40  36:	c3                   	ret
41
420000000000000037 <another_function>:
43  37:	55                   	push   %rbp
44  38:	48 89 e5             	mov    %rsp,%rbp
45  3b:	48 8d 05 00 00 00 00 	lea    0x0(%rip),%rax        # 42 <another_function+0xb>
46  42:	48 89 c7             	mov    %rax,%rdi
47  45:	e8 00 00 00 00       	call   4a <another_function+0x13>
48  4a:	90                   	nop
49  4b:	5d                   	pop    %rbp
50  4c:	c3                   	ret

可以看到:

  • .data 中, 以小端序存放着 42 和 100 两个全局变量的值

  • .text 为汇编后的代码, 使用 objdump -d 查看反汇编后的代码, 可以看到在对应偏移量的位置为 another_function

至此, 目标文件的分析完成, 无论是函数还是变量, st_value 都是对应节区的偏移量.

下面分析可执行文件中的情况, 我们需要验证: st_value 表示虚拟地址的位置.

bash
 1# 省略了部分输出
 2
 3$ readelf -s symbols
 4
 5...
 6
 7Symbol table '.symtab' contains 28 entries:
 8   Num:    Value          Size Type    Bind   Vis      Ndx Name
 9     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
10     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS symbols.c
11     2: 0000000000004024     4 OBJECT  LOCAL  DEFAULT   24 static_global
12     3: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS
13    12: 0000000000001198     0 FUNC    GLOBAL HIDDEN    15 _fini
14    13: 0000000000001180    22 FUNC    GLOBAL DEFAULT   14 another_function
15    14: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND printf@GLIBC_2.2.5
16    15: 0000000000004020     4 OBJECT  GLOBAL DEFAULT   24 global_var
17    16: 0000000000004010     0 NOTYPE  GLOBAL DEFAULT   24 __data_start
18... 
19
20$ readelf -S symbols
21There are 30 section headers, starting at offset 0x3580:
22
23Section Headers:
24  [Nr] Name              Type             Address           Offset
25       Size              EntSize          Flags  Link  Info  Align
26  [14] .text             PROGBITS         0000000000001050  00001050
27       0000000000000146  0000000000000000  AX       0     0     16
28  [24] .data             PROGBITS         0000000000004010  00003010
29       0000000000000018  0000000000000000  WA       0     0     8

readelf 的结果可以看到, 相比于目标文件, 可执行文件的 st_value 的值已经变得大得多了, 尽管仍然通过 Ndx 指向数据段和代码段, 但此时该字段的含义已经变成了对应符号在运行时的虚拟地址的空间了. 我们使用 gdb 进行验证:

gdb
1$ gdb ./symbols
2GNU gdb (GDB) 15.2
3
4(gdb) info address another_function
5Symbol "another_function" is at 0x1180 in a file compiled without debugging.
6(gdb) info address global_var
7Symbol "global_var" is at 0x4020 in a file compiled without debugging.
8(gdb) info address static_global
9Symbol "static_global" is at 0x4024 in a file compiled without debugging.

gdb 的输出与符号表的输出是统一的, 以此验证通过!

st_info

st_info 中包含符号类型和绑定信息, 这里给出了控制它的值的方式具体信息如下:

c
1#define ELF32_ST_TYPE(i)    ((i)&0xf)                   # 低 4 位
2#define ELF32_ST_INFO(b, t) (((b)<<4) + ((t)&0xf))      # 高 4 位

符号类型如下:

名称 取值 说明
STT_NOTYPE 0 符号的类型没有定义.
STT_OBJECT 1 符号与某个数据对象相关, 比如一个变量、数组等等.
STT_FUNC 2 符号与某个函数或者其他可执行代码相关.
STT_SECTION 3 符号与某个节区相关. 这种类型的符号表项主要用于重定位, 通常具有 STB_LOCAL 绑定.
STT_FILE 4 一般情况下, 符号的名称给出了生成该目标文件相关的源文件的名称. 如果存在的话, 该符号具有 STB_LOCAL 绑定, 其节区索引是 SHN_ABS 且优先级比其他STB_LOCAL符号高.
STT_LOPROC~STT_HIPROC 13~15 保留用于特定处理器
  • 共享目标文件中的函数符号有比较特殊, 当另一个目标文件从共享目标文件中引用一个函数时, 链接器自动为被引用符号创建过程链接表(PLT)项. 共享目标中除了STT_FUNC , 其它符号将不会通过过程链接表自动被引用.
这段话如何理解? TODO

这段话是在解释共享目标文件中的函数符号如何被特殊处理。让我来帮你理解这个概念:

共享目标文件(通常是.so文件,类似于Windows中的DLL)中的函数符号有特殊处理机制,主要涉及到"过程链接表"(Procedure Linkage Table,简称PLT)的自动创建。

当我们分解这段话时:

  1. 当另一个目标文件从共享目标文件中引用一个函数时,链接器会自动为这个被引用的函数符号创建过程链接表(PLT)项。

  2. 这种自动创建PLT项的机制只适用于标记为STT_FUNC类型的符号(即函数符号)。

  3. 共享目标文件中的其他类型符号(比如数据对象STT_OBJECT)不会通过过程链接表自动被引用。

为什么需要这种特殊处理?这与动态链接的工作原理有关:

当程序运行时,共享库中的函数可能需要被延迟加载或重定位。过程链接表(PLT)与全局偏移表(GOT)一起工作,允许程序在运行时正确地调用共享库中的函数。当程序调用共享库中的函数时,它实际上是跳转到PLT中的一个条目,该条目再跳转到实际函数的地址。

这种机制的好处是:

  • 支持延迟绑定(lazy binding):函数地址只在第一次调用时解析
  • 允许共享库在内存中的不同位置加载
  • 使共享库能够被多个程序共享,同时每个程序可以有自己的函数地址引用

非函数符号(如全局变量)通常直接通过GOT访问,不需要PLT这一额外的间接层,因为数据访问模式与函数调用不同。

实际上,这是ELF(可执行与可链接格式)动态链接设计的一部分,专门针对函数调用进行了优化处理。

符号绑定的信息确定了符号的链接可见性以及其行为, 具体的取值如下:

名称 取值 说明
STB_LOCAL 0 表明该符号为局部符号, 在包含该符号定义的目标文件以外不可见. 相同名称的局部符号可以存在于多个文件中, 互不影响.
STB_GLOBAL 1 表明该符号为全局符号, 对所有将被组合在一起的目标文件都是可见的. 一个文件中对某个全局符号的定义将满足另一个文件对相同全局符号的未定义引用. 我们称初始化非零变量的全局符号为强符号, 只能定义一次.
STB_WEAK 2 弱符号与全局符号类似, 不过它们的定义优先级比较低.
STB_LOPROC ~STB_HIPROC 13 这个范围的取值是保留给处理器专用语义的.
  • 当链接器在链接多个可重定位目标文件时, 不允许定义多个相同名字的 STB_GLOBAL 符号. 但如果存在一个已定义的全局符号, 同名弱符号的存在不会引起错误. 链接器会优先选择全局定义, 忽略弱符号定义.

st_shndx

通常的 st_shndx 定义了符号所在节在节区头部表中的下标, 下面给出了一些特殊的取值:

  • SHN_ABS: 符号的取值具有绝对性, 不会因为重定位而发生变化.

  • SHN_COMMON: 符号标记了一个尚未分配的公共块. 符号的取值给出了对齐约束, 与节区的 sh_addralign 成员类似. 就是说, 链接编辑器将在地址位于 st_value 的倍数处为符号分配空间. 符号的大小给出了所需要的字节数.

  • SHN_UNDEF: 此索引值表示符号没有定义. 当链接编辑器将此目标文件与其他定义了该符号的目标文件进行组合时, 此文件中对该符号的引用将被链接到实际定义的位置.

符号名称的定位

对于一个符号表而言, 其所有的符号名称也是通过另一张字符串表来给出的. 与节区头表中的节区名称 sh_name 类似, 符号表表项的名称 st_name 也是一个数值索引, 其使用的字符串表根据符号表头的 sh_link 字段给出.

以上述的 C 代码为例, 为了简便, 编译出目标代码, 使用 readelf 查看对应的内容:

bash
 1# 查看所有的符号头表
 2$ readelf -S symbols.o
 3There are 14 section headers, starting at offset 0x408:
 4
 5Section Headers:
 6  [Nr] Name              Type             Address           Offset
 7       Size              EntSize          Flags  Link  Info  Align
 8
 9  ... 省略若干输出
10
11  [11] .symtab           SYMTAB           0000000000000000  00000168
12       00000000000000f0  0000000000000018          12     5     8
13  [12] .strtab           STRTAB           0000000000000000  00000258
14       0000000000000046  0000000000000000           0     0     1
15  [13] .shstrtab         STRTAB           0000000000000000  00000360
16       0000000000000074  0000000000000000           0     0     1
17
18# 查看 .symtab 关联的 12 号节区 .strtab
19$readelf -x 12 symbols.o
20
21Hex dump of section '.strtab':
22  0x00000000 0073796d 626f6c73 2e630073 74617469 .symbols.c.stati
23  0x00000010 635f676c 6f62616c 00676c6f 62616c5f c_global.global_
24  0x00000020 76617200 6d61696e 00707269 6e746600 var.main.printf.
25  0x00000030 616e6f74 6865725f 66756e63 74696f6e another_function
26  0x00000040 00707574 7300                       .puts.

通过符号表头的 Link 可以关联到 12 号节区表, 在 12 号节区内, 看到了所有符号的字符串名称.

但需要注意, 12 号节区中的内容是符号自身的字符串名字, 符号的值则由符号表内部的 Ndxst_value 给出.

哈希表节区

哈希表节区是用于存储哈希值的特殊节区. 这些节区主要用于以下几个目的:

  • 完整性验证:存储文件或特定节区的哈希值,用于验证文件是否被篡改

  • 数字签名:支持代码签名机制,以验证软件的来源和完整性

  • 安全启动:在嵌入式系统和安全启动过程中验证可执行文件

  • 符号查找加速:某些哈希表用于加速符号查找过程

.hash 是传统的哈希表节区, 使用 GNU 工具链则会以 .gnu.hash 替代, 其比传统的 .hash 更加高效, 占用的内存空间更小, 哈希函数也更加现代, 相关的内容留待后续有机会再学习:

参考:

数据相关节区

  • .bss; 未初始化的全局变量对应的节. 此节区不占用 ELF 文件空间, 但占用程序的内存映像中的空间. 当程序开始执行时, 系统将把这些数据初始化为 0.

    • bss 是 Block Started by Symbol 的简写, 说明该节区中单纯地说明了有哪些变量.
  • .data; 这些节区包含初始化了的数据, 会在程序的内存映像中出现.

  • .rodata; 这些节区包含只读数据,这些数据通常参与进程映像的不可写段。

代码相关节区

  • .init & .init_array; 此节区包含可执行指令, 是进程初始化代码的一部分. 程序开始执行时, 系统会在开始调用主程序入口(通常指 C 语言的 main 函数)前执行这些代码.

  • .text ;此节区包含程序的可执行指令

  • .fini & .fini_array; 此节区包含可执行的指令, 是进程终止代码的一部分. 程序正常退出时, 系统将执行这里的代码.

动态链接与重定位节区

ELF 文件中的动态链接相关节区主要包括:

  1. .dynamic; 核心动态链接信息节区, 包含了所有动态链接所需的信息结构. 它存储了各种动态链接条目, 如共享库路径、符号表位置等.

  2. .dynsym; 动态符号表, 包含了程序中所有需要动态链接的符号信息. 与.symtab不同, .dynsym只包含动态链接必需的符号.

  3. .dynstr; 动态字符串表, 存储了.dynsym中符号的名称字符串.

  4. .plt(Procedure Linkage Table)- 过程链接表, 用于支持延迟绑定(lazy binding)机制, 提高程序启动速度.

  5. .got(Global Offset Table)- 全局偏移表, 存储外部符号的地址. PLT会使用GOT进行符号解析.

  6. .got.plt; PLT专用的GOT表部分, 专门用于函数调用.

  7. .rela.dyn/.rel.dyn; 用于变量的重定位表.

  8. .rela.plt/.rel.plt; 用于函数的重定位表.

  9. .interp; 存储动态链接器(如/lib64/ld-linux-x86-64.so.2)的路径.

有关动态链接、PLT、GOT 相关的内容, 由于较为复杂, 参见后续博客. TODO

后记

在写知识类博客的时候, 总是难免地变成知识摘抄(从网络博客与 LLM 的知识库中), 或是技术说明书(罗列冗余无趣的字段详解).. 尽管断断续续从在网上写下第一个字到如今已经有 6 年左右了, 但始终无法很好地产出此类的知识文章; 相比之下, 各种技术工具的折腾记录反而更加容易些. 其原因本质上还是在于对于知识的理解不够到位, 却又希望输出好的文章: 对于初学者来说, 对新知识的学习必然是零碎的、片面的, 但对于博客创作者来说, 又希望对知识的介绍是系统的、引人入胜且条理清晰的, 我是不愿将笔记、草稿作为博客上传到互联网上的, 此类的内容不宜使用博客作为载体, 使用批注或 Obisidian 等管理工具或许更好, 但我又不愿将一份知识的学习拆分为两种; 尽管有言道学习最好的方式就是将它教给别人, 但当我真的学会后, 我是没有太大的动力去做科普、系统性的知识教程的, 这就导致了一些内容产出理念上的偏差..

不过总体来说, 我的博客还是个人学习的沉淀与输出, 他人倘若能有些许的受益就是其发表于互联网上的价值所在了. 毕竟, 目前内容农场大行其道, 各种图片过期、CSS 样式丑陋的博客也不在少数, 我自认本博客的审美不至于过于丑陋, 也并无任何的广告, 不滥用 LLM 生成内容, 一些地方也是会记录下我在学习、探索过程中的独到之处, 因此应当还是有一定价值的!

参考资料

参考阅读:

嗨! 这里是 rqdmap 的个人博客, 我正关注 GNU/Linux 桌面系统, Linux 内核 以及一切有趣的计算机技术! 希望我的内容能对你有所帮助~
如果你遇到了任何问题, 包括但不限于: 博客内容说明不清楚或错误; 样式版面混乱; 加密博客访问请求等问题, 请通过邮箱 rqdmap@gmail.com 联系我!
修改日志
  • Thu Feb 20 22:00:03 2025 +0800 ELF 文件结构与分析