前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >[操作系统] ELF文件从形成到加载轮廓

[操作系统] ELF文件从形成到加载轮廓

作者头像
DevKevin
发布2025-03-08 09:21:32
发布2025-03-08 09:21:32
4000
代码可运行
举报
文章被收录于专栏:Base_CDNKevinBase_CDNKevin
运行总次数:0
代码可运行

目标文件

编译和链接这两个步骤,在Windows下被IDE封装的很完美,我们一般是使用一键编译并运行,但是当链接出错的话我们就束手无措了。在Linux下有gcc/g++编译器,可以直接展示出编译链接的过程。

在软件开发中,编译是将程序的源代码(通常是人类可读的高级语言,如 C/C++)翻译成 CPU 能够直接执行的机器代码(二进制代码)。通过这一步骤,源文件被转换为目标文件,为后续的链接奠定基础。

编译过程与目标文件的生成

以一个简单的例子为例:假设我们有一个源文件 hello.c,其内容如下:

代码语言:javascript
代码运行次数:0
复制
// hello.c
#include <stdio.h>
int main() {
    printf("hello world!\n");
    return 0;
}

使用 gcc 编译器,我们可以通过以下命令编译该源文件:

代码语言:javascript
代码运行次数:0
复制
$ gcc -c hello.c

编译完成后,生成一个扩展名为 .o 的文件(例如 hello.o),被称为目标文件(Object File)。我们可以通过以下命令查看生成的文件:

代码语言:javascript
代码运行次数:0
复制
$ ls
hello.c  hello.o
目标文件的特性:
  • 目标文件是二进制文件,通常采用 ELF(Executable and Linkable Format)格式(在 Linux/x86_64 系统中)。
  • 使用 file 命令可以检查目标文件的类型,例如:
代码语言:javascript
代码运行次数:0
复制
$ file hello.o
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
  • ELF 是一种通用的文件格式,用于封装二进制代码、数据和符号信息,是 Linux 系统中目标文件、可执行文件和共享库的标准格式。
  • 目标文件包含编译后的机器代码,但还未与库文件或其他目标文件链接,因此不能直接运行。

通过编译过程,我们可以生成目标文件,并了解 ELF 格式作为二进制文件封装的重要作用。

如果我们修改了一个源文件,那么我们只需要单独编译这一个文啊进,而不是重新编译整个工会测过,将目标文件编译后重新链接即可。

ELF文件:编译与链接基础

为了全面理解编译和链接的细节,我们需要深入了解 ELF(Executable and Linkable Format)文件格式。ELF 是一种通用的二进制文件格式,在 Linux 系统中广泛用于目标文件、可执行文件、共享库以及内核转储等。以下是 ELF 文件的四种主要类型及其特点:

ELF 文件的四种类型
  1. 可重定位文件(Relocatable File)
    • .o 文件(目标文件)。
    • 包含适合与其他目标文件链接,以创建可执行文件或共享目标文件的代码和数据。
    • 这些文件是在编译阶段生成的,通常通过 gcc -c 命令生成,尚未进行最终的地址解析和链接。
  2. 可执行文件(Executable File)
    • 即可直接运行的程序文件(如 a.out 或其他二进制可执行文件)。
    • 通过链接器将多个目标文件和库文件组合后生成,包含完整的机器代码和数据,可由操作系统加载并执行。
  3. 共享目标文件(Shared Object File)
    • .so 文件(动态库)。
    • 可以在运行时由多个程序共享加载,节省内存空间,但需要确保运行环境中有正确的库文件支持。
  4. 内核转储(Core Dumps)
    • 用于存储当前进程的执行上下文,通常在进程因信号(如段错误)触发时生成。
    • 这些文件可用于调试,分析程序崩溃的原因。
ELF 文件的结构组成

一个 ELF 文件由以下四个主要部分组成:

  1. ELF 头(ELF Header)
    • 位于文件开头,描述文件的主要特性,如目标架构(例如 x86-64)、文件类型(可重定位、可执行等)以及版本信息。
    • 其主要作用是定位文件的其他部分,为解析文件提供基础。
  2. 程序头表(Program Header Table)
    • 列出文件中的所有有效段(Segments)及其属性。
    • 记录每个段的起始位置、偏移量和长度,因为这些段在二进制文件中紧密排列,程序头表提供必要的描述信息以区分和加载这些段。
    • 主要用于可执行文件和共享库,在加载时由操作系统或动态链接器使用。
  3. 节头表(Section Header Table)
    • 包含对文件中的节(Sections)的描述,记录每个节的类型、位置、大小等信息。
    • 节是 ELF 文件的基本组成单位,用于组织和存储不同的数据和代码。
  4. 节(Sections)
    • ELF 文件中的基本数据单元,包含特定类型的信息。各种数据和代码存储在不同的节中,常见节包括:
      • 代码节(.text):保存机器指令,是程序的主要执行部分。
      • 数据节(.data):保存已初始化的全局变量和局部静态变量。
      • 其他节如 .bss(未初始化的全局变量和静态变量)、.rodata(只读数据,如字符串字面量)等,具体取决于文件类型和编译选项。

ELF从形成到加载轮廓

ELF 文件形成可执行文件

ELF(Executable and Linkable Format)文件是 Linux 系统中编译和链接的核心格式。为了生成可执行文件,涉及以下两个主要步骤,并补充相关的知识点:

Step-1:将多份 C/C++ 源代码翻译成目标 .o 文件
  • 编译过程:通过编译器(如 gccg++)将 C/C++ 源代码(.c.cpp 文件)翻译成目标文件(.o 文件)。编译器会执行以下几个阶段:
    1. 预处理(Preprocessing):处理 #include、宏定义和条件编译指令,生成预处理文件(.i 文件)。
    2. 编译(Compilation):将预处理后的代码转换为汇编代码(.s 文件),生成特定架构的机器指令。
    3. 汇编(Assembly):将汇编代码转换为二进制目标文件(.o 文件),格式为 ELF 可重定位文件。
  • 命令示例:使用 gcc -c 编译源文件,例如:
代码语言:javascript
代码运行次数:0
复制
$ gcc -c source1.c -o source1.o
$ gcc -c source2.c -o source2.o
  • 目标文件的特性
    • 目标文件是二进制文件,采用 ELF 格式,类型为可重定位文件(Relocatable File)
    • 包含适合链接的代码(.text Section)、数据(.data.bss Section)、符号表(.symtab)和重定位信息(.rela),但地址尚未最终确定(符号引用未解析)。
    • 目标文件不能直接执行,需通过链接器进一步处理。
  • 知识点扩展
    • 编译器会根据目标架构(如 x86-64)生成对应的机器代码。
    • 如果源代码包含外部函数或变量引用(未定义符号),目标文件会记录这些符号的重定位信息,供链接器解析。
    • 使用 gcc -Wall 可启用警告选项,gcc -g 可生成调试信息(.debug Section),便于调试。
Step-2:将多份 .o 文件的 Section 进行合并
  • 链接过程:在链接阶段,链接器(如 ld)将多个目标文件(.o 文件)的各个 Section 合并,并可能与库文件(如静态库 .a 或动态库 .so)结合,生成最终的可执行文件(.out 或指定名称)。
  • Section 合并细节
    1. 链接器读取每个目标文件的节头表(Section Header Table),识别 .text(代码)、.data(初始化数据)、.bss(未初始化数据)、.rodata(只读数据)等 Section。
    2. 根据 Section 的属性(如可读、可写、可执行)和逻辑关系,合并这些 Section,形成连续的内存布局。
    3. 解析符号表(.symtab)和重定位表(.rela),解决未定义符号(如函数或变量的引用),确保所有地址引用正确。
    4. 如果使用动态链接,还会处理动态符号表(.dynsym)和全局偏移表/过程链接表(.got.plt),为运行时加载动态库做准备。
  • 命令示例:生成可执行文件:
代码语言:javascript
代码运行次数:0
复制
$ gcc source1.o source2.o -o program

或直接从源文件生成:

代码语言:javascript
代码运行次数:0
复制
$ gcc source1.c source2.c -o program
  • 注意事项
    • 实际合并是在链接时进行的,但并非简单地将 Section 逐一拼接。链接器还会处理符号解析、地址分配、库文件的合并等复杂操作。
    • 静态链接会将静态库(.a)内容直接嵌入可执行文件;动态链接则引用动态库(.so),仅记录加载信息,运行时由动态链接器(如 /lib64/ld-linux-x86-64.so.2)加载。
    • 链接阶段可能出现错误,如“undefined reference”(未定义引用),通常因缺少库文件或符号定义不一致引起。
  • 知识点扩展
    • 链接器会优化空间利用率,将小块 Section 合并成较大的连续块,减少内存页面碎片(页面大小通常为 4KB)。
    • 如果目标文件包含调试信息,链接器会保留 .debug Section,便于使用 gdb 调试。
    • 链接器支持脚本(如 ld 的 linker script),可自定义内存布局和 Section 合并规则。

ELF 可执行文件加载

当生成的 ELF 可执行文件加载到内存中时,操作系统会根据其结构完成对ELF中不同的Section的合并,形成segment。

Section 合并为 Segment
  • Section 与 Segment 的关系
    • ELF 文件中的 Section(如 .text.data.rodata)是链接视图的逻辑单元,描述文件内容的组织方式。
    • 在加载时,操作系统根据 Section 的属性(如可读、可写、可执行)和程序头表(Program Header Table)中的信息,将具有相同属性的 Section 合并成Segment(段),作为执行视图的物理加载单元。
  • 合并原则
    • 相同属性:如可读(R)、可写(W)、可执行(E)等。
    • 空间分配:需要加载时申请内存空间,合并后形成连续的内存区域。
    • 权限控制:合并后的 Segment 可定义为只读段(如包含 .text.rodata)、可读写段(如包含 .data.bss)或可执行段。
  • 加载效率
    • 内存中的存储和磁盘存储类似,也是以4KB为单位进行存储,所以在合并原则的约束下,类似的Section可以合并,从而减少内存浪费。
    • 合并减少页面碎片(页面大小通常为 4KB),提高内存使用效率。例如:
      • 如果 .text 部分为 4097 字节,.init 部分为 512 字节,未合并时需 3 个页面(4096 × 3 = 12288 字节);合并后可能仅需 2 个页面(4096 × 2 = 8192 字节)。
    • 操作系统将 Segment 映射到虚拟内存,使用分页机制管理物理内存,提高加载和执行性能。
  • 知识点扩展
    • 合并方式已在 ELF 文件生成时通过**程序头表(Program header table)**确定,程序头表记录了每个 Segment 的起始地址、长度、权限和文件偏移等信息。
    • 动态链接的 Segment 可能包含 .dynamic.got.plt Section,用于运行时解析共享库符号。
查看可执行程序的 Section 和 Segment
  • 查看 Section(节头表):使用 readelf -S 命令。例如:
代码语言:javascript
代码运行次数:0
复制
$ readelf -S a.out

输出显示可执行文件中的各个 Section,如 .text(代码)、.data(初始化数据)、.rodata(只读数据)、.bss(未初始化数据)等,及其属性(地址、偏移、大小、权限等)。

  • 查看 Segment(程序头表):使用 readelf -l 命令。例如:
代码语言:javascript
代码运行次数:0
复制
$ readelf -l a.out

输出显示程序头表中的 Segment 信息,包括类型(如 LOADDYNAMIC)、虚拟地址、文件偏移、文件大小、内存大小、权限(R/W/E)和对齐方式。

代码语言:javascript
代码运行次数:0
复制
- `LOAD` 段:需要加载到内存的代码和数据段,可能包含 `.text`、`.data` 等 Section。  
- `DYNAMIC` 段:用于动态链接,包含动态库加载信息。  
- `GNU_STACK` 段:指定栈的权限(通常可读写)。
查看可执行程序的 Section:
代码语言:javascript
代码运行次数:0
复制
$ readelf -S a.out
代码语言:javascript
代码运行次数:0
复制
There are 30 section headers, starting at offset 0x1a50:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000  # 空 Section,用于占位
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         0000000000400238  00000238  # 程序解释器路径(动态链接器)
       000000000000001c  0000000000000000   A       0     0     1
  [ 2] .note.ABI-tag     NOTE             0000000000400254  00000254  # ABI 版本信息
       0000000000000020  0000000000000000   A       0     0     4
  [ 3] .note.gnu.build-i NOTE             0000000000400274  00000274  # 构建 ID,唯一标识可执行文件
       0000000000000024  0000000000000000   A       0     0     4
  [ 4] .gnu.hash         GNU_HASH         0000000000400298  00000298  # GNU 哈希表,加速符号查找
       0000000000000024  0000000000000000   A       5     0     8
  [ 5] .dynsym           DYNSYM           00000000004002c0  000002c0  # 动态符号表
       00000000000000f0  0000000000000018   A       6     1     8
  [ 6] .dynstr           STRTAB           00000000004003b0  000003b0  # 动态字符串表(符号名称)
       000000000000008b  0000000000000000   A       0     0     1
  [ 7] .gnu.version      VERSYM           000000000040043c  0000043c  # 符号版本信息
       0000000000000014  0000000000000002   A       5     0     2
  [ 8] .gnu.version_r    VERNEED          0000000000400450  00000450  # 符号版本需求信息
       0000000000000020  0000000000000000   A       6     1     8
  [ 9] .rela.dyn         RELA             0000000000400470  00000470  # 动态重定位表
       0000000000000030  0000000000000018   A       5     0     8
  [10] .rela.plt         RELA             00000000004004a0  000004a0  # PLT(过程链接表)重定位表
       00000000000000c0  0000000000000018  AI       5    23     8
  [11] .init             PROGBITS         0000000000400560  00000560  # 程序初始化代码
       000000000000001a  0000000000000000  AX       0     0     4
  [12] .plt              PROGBITS         0000000000400580  00000580  # 过程链接表(PLT)
       0000000000000090  0000000000000010  AX       0     0     16
  [13] .text             PROGBITS         0000000000400610  00000610  # 程序代码段
       00000000000001e2  0000000000000000  AX       0     0     16
  [14] .fini             PROGBITS         00000000004007f4  000007f4  # 程序终止代码
       0000000000000009  0000000000000000  AX       0     0     4
  [15] .rodata           PROGBITS         0000000000400800  00000800  # 只读数据段
       0000000000000024  0000000000000000   A       0     0     8
  [16] .eh_frame_hdr     PROGBITS         0000000000400824  00000824  # 异常处理框架头
       0000000000000034  0000000000000000   A       0     0     4
  [17] .eh_frame         PROGBITS         0000000000400858  00000858  # 异常处理框架数据
       00000000000000f4  0000000000000000   A       0     0     8
  [18] .init_array       INIT_ARRAY       0000000000600de0  00000de0  # 初始化函数指针数组
       0000000000000008  0000000000000008  WA       0     0     8
  [19] .fini_array       FINI_ARRAY       0000000000600de8  00000de8  # 终止函数指针数组
       0000000000000008  0000000000000008  WA       0     0     8
  [20] .jcr              PROGBITS         0000000000600df0  00000df0  # Java 类注册信息
       0000000000000008  0000000000000000  WA       0     0     8
  [21] .dynamic          DYNAMIC          0000000000600df8  00000df8  # 动态链接信息
       0000000000000200  0000000000000010  WA       6     0     8
  [22] .got              PROGBITS         0000000000600ff8  00000ff8  # 全局偏移表(GOT)
       0000000000000008  0000000000000008  WA       0     0     8
  [23] .got.plt          PROGBITS         0000000000601000  00001000  # PLT 相关的 GOT
       0000000000000058  0000000000000008  WA       0     0     8
  [24] .data             PROGBITS         0000000000601058  00001058  # 数据段
       0000000000000004  0000000000000000  WA       0     0     1
  [25] .bss              NOBITS           0000000000601060  0000105c  # 未初始化数据段
       0000000000000010  0000000000000000  WA       0     0     16
  [26] .comment          PROGBITS         0000000000000000  0000105c  # 编译器注释信息
       000000000000002d  0000000000000001  MS       0     0     1
  [27] .symtab           SYMTAB           0000000000000000  00001090  # 符号表
       0000000000000678  0000000000000018          28    46     8
  [28] .strtab           STRTAB           0000000000000000  00001708  # 字符串表(符号名称)
       000000000000023f  0000000000000000           0     0     1
  [29] .shstrtab         STRTAB           0000000000000000  00001947  # Section 名称字符串表
       0000000000000108  0000000000000000           0     0     1

.text:

  • 存储程序的代码段,即编译后的机器指令。
  • 包括函数、主程序、库函数等所有可执行代码。

.data

  • 存储已初始化的全局变量,存储数据段。

.bss(better save space):

  • 为初始化的全局变量不会在data,而是在bss中记录有多少个变量,因为所有的全局变量都是未知的,没有初始化,过于臃肿。
  • 在运行的时候会从bss读取,初始化为0。这就是为什么为初始化的变量会自动初始化为0的原因。
查看 Section 合并的 Segment:
代码语言:javascript
代码运行次数:0
复制
$ readelf -l a.out
代码语言:javascript
代码运行次数:0
复制
Elf file type is EXEC (Executable file)
Entry point 0x4003e0  # 程序入口地址
There are 9 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040  # 程序头表信息
                 0x00000000000001f8 0x00000000000001f8  R E    8
  INTERP         0x0000000000000238 0x0000000000400238 0x0000000000400238  # 程序解释器路径
                 0x000000000000001c 0x000000000000001c  R      1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]  # 动态链接器路径
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000  # 可加载段(代码段)
                 0x0000000000000744 0x0000000000000744  R E    200000
  LOAD           0x0000000000000e10 0x0000000000600e10 0x0000000000600e10  # 可加载段(数据段)
                 0x0000000000000218 0x0000000000000220  RW     200000
  DYNAMIC        0x0000000000000e28 0x0000000000600e28 0x0000000000600e28  # 动态链接信息
                 0x00000000000001d0 0x00000000000001d0  RW     8
  NOTE           0x0000000000000254 0x0000000000400254 0x0000000000400254  # 注释信息(ABI、构建 ID)
                 0x0000000000000044 0x0000000000000044  R      4
  GNU_EH_FRAME   0x00000000000005a0 0x00000000004005a0 0x00000000004005a0  # 异常处理框架信息
                 0x000000000000004c 0x000000000000004c  R      4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000  # 栈权限(RW,不可执行)
                 0x0000000000000000 0x0000000000000000  RW     10
  GNU_RELRO      0x0000000000000e10 0x0000000000600e10 0x0000000000600e10  # 重定位只读段
                 0x00000000000001f0 0x00000000000001f0  R      1

 Section to Segment mapping:
  Segment Sections...
   00     
   01     .interp  # 程序解释器路径
   02     .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame  # 代码段
   03     .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss  # 数据段
   04     .dynamic  # 动态链接信息
   05     .note.ABI-tag .note.gnu.build-id  # 注释信息
   06     .eh_frame_hdr  # 异常处理框架信息
   07     
   08     .init_array .fini_array .jcr .dynamic .got  # 初始化相关段
Section 合并为 Segment 的原因与意义:
  • 减少页面碎片:未合并时,小块 Section 可能分散占用多个内存页面,导致浪费(页面大小为 4KB 的整数倍)。合并后,连续的 Segment 减少页面使用,优化内存效率。
  • 权限管理和安全:不同 Segment 可定义不同的访问权限(如 .text 为可执行只读,.data 为可读写),由操作系统通过内存保护机制(如 MMU)实现,提高程序安全性。
  • 性能优化:连续的 Segment 加载更快,减少虚拟内存映射和页面故障(page fault)。
  • 知识点扩展
    • 现代操作系统使用虚拟内存管理单元(MMU)将 Segment 映射到物理内存,并通过页表实现地址转换。
    • 如果程序使用动态库,加载时动态链接器(如 ld-linux.so)会解析 .dynamic.got.plt Section,加载共享库并绑定符号。

链接视图与执行视图:节头表与程序头表的区别与应用
  1. 链接视图(Linking View)
    • 对应**节头表(Section Header Table)**。
    • 提供细粒度的文件结构,适合静态链接分析。链接器根据节头表合并 Section,生成优化后的 Segment。
    • 主要内容
      • 每个 Section 有自己的属性(如类型、地址、偏移、大小、权限),由节头表描述。
      • Section 内容紧密排列在文件中,但不一定连续(地址可能虚拟分配)
    • 常见 Section 及其作用
      • .text:保存机器指令,是程序执行的核心,权限通常为可执行只读。
      • .data:保存已初始化的全局变量和局部静态变量,权限为可读写。
      • .rodata:保存只读数据(如字符串字面量),只能存在于只读段(通常与 .text 合并)。
      • .bss:为未初始化的全局变量和局部静态变量预留空间,实际数据在运行时初始化,权限为可读写。
      • .symtab:符号表,记录函数名、变量名与代码或数据的对应关系,用于链接阶段解析符号引用。
      • .rela:重定位表,记录需要调整地址的符号引用位置,链接器根据此表修正地址。
      • .debug:调试信息,包含源代码行号、变量名和类型等,供调试工具(如 gdb)使用。
      • .got.plt:全局偏移表和过程链接表,用于动态链接,保存共享库函数的间接引用地址,运行时由动态链接器修改。
    • 查看方法:使用 readelf -S 命令查看目标文件(如 hello.o)或可执行文件(如 a.out)的节头表。
  2. 执行视图(Execution View)
    • 对应**程序头表(Program Header Table)**。
    • 指导操作系统加载可执行文件,完成进程内存的初始化,服务于运行时的内存加载和初始化
    • 告诉操作系统哪些模块可以被加载进内存。
    • 加载进内存之后哪些分段是可读可写,哪些分段是只读,哪些分段是可执行的。
    • 包含 Segment 信息(如 LOADDYNAMICGNU_STACK),描述每个 Segment 的虚拟地址、文件偏移、大小、权限和对齐方式。
    • 一个可执行程序的ELF文件中一定会有**Program Header Table**
    • 典型 Segment 类型
      • LOAD:需要加载到内存的代码和数据段,包含 .text.data.rodata 等 Section。
      • DYNAMIC:动态链接信息,包含共享库依赖和符号解析数据。
      • GNU_STACK:栈段的权限设置(通常可读写)。
      • GNU_RELRO:只读重定位段,保护动态链接后的数据免受修改。
    • 每个程序头表项(Program Header Entry)描述一个 Segment 的属性,包括:
      • 类型(如 LOAD:需要加载到内存的代码或数据段,DYNAMIC:动态链接信息)。
      • 虚拟地址(加载到内存时的起始地址)。
      • 文件偏移(在文件中的起始位置)。
      • 文件大小和内存大小(可能不同,如 .bss 段在内存中扩展)。
      • 权限标志(如可读 R、可写 W、可执行 E)。
      • 对齐方式(确保内存页面对齐,通常为 4KB 的倍数)。
    • 查看方法:使用 readelf -l 命令查看可执行文件的程序头表。
      • 当文件读取到内存中的时候,操作系统通过程序头表加载 Segment 到虚拟内存,结合分页机制映射到物理内存,通过读取到的Segment的内容权限对页表进行设置对应的权限,所以一个进程在启动的时候就可以可以知道什么区域是什么权限。
      • 动态链接的程序在加载时,动态链接器(如 /lib64/ld-linux-x86-64.so.2)解析 .dynamic.got.plt,加载共享库并绑定符号,确保程序运行时能访问外部函数。

总结

  • 节头表用于链接阶段,提供 Section 级别的详细信息,服务于链接器合并和优化。
  • 程序头表用于执行阶段,指导操作系统加载和初始化内存中的 Segment,服务于程序运行。

二者说白了就是,一个在链接时用,一个在运行时用。 执行命令查看的内容在3.2.2中已展示。

符号表
.symtab 符号表的基本概念
  • 定义.symtab 是 ELF 文件中的一个重要 Section(节),称为符号表(Symbol Table)。它是存储程序中符号(函数名、变量名等)及其相关信息的表格,用于描述源码中的标识符(如函数、变量)与目标文件或可执行文件中代码和数据的对应关系。
  • 位置.symtab 通常位于目标文件(.o)或可执行文件(.out)中,属于链接视图(Linking View)的部分,存储在节头表(Section Header Table)中描述的 Section 中。
  • 作用
    • 在编译和链接阶段,符号表帮助编译器和链接器解析、跟踪和绑定源码中的符号(如函数名 main、变量 label),确保程序的正确连接和地址分配。
    • 在调试阶段,符号表为调试工具(如 gdb)提供符号信息,映射源码中的标识符到内存地址,便于定位和分析。
    • 在生产环境中,可以通过编译选项(如 gcc -s)去除 .symtab,减小文件大小,但会失去调试能力。

类似于数组,将每个符号分隔,独立存储。


如何理解 .symtab 与源码的对应关系

.symtab 是源码中函数名、变量名和代码对应关系的“桥梁”,具体来说:

  • 源码中的函数名和变量名
    • 在 C/C++ 源码中,程序员定义了函数(如 int main(void))和变量(如 char label[] = "helloworld";)。这些名称是人类可读的标识符。
    • 编译器在生成目标文件时,将这些标识符(符号)记录到 .symtab 中,并关联到目标文件中对应的代码(.text Section)或数据(.data.bss.rodata Section)。
  • 代码的对应关系
    • 编译器将源码翻译成机器代码后,函数和变量会被分配到特定的内存地址或 Section。
    • .symtab 记录每个符号的名称、类型、地址(或偏移量)、大小和所属 Section。例如:
      • 函数 main 可能记录在 .text Section,符号表条目显示其类型为 FUNC(函数),地址为某个虚拟地址。
      • 变量 label 可能记录在 .data.rodata Section,符号表条目显示其类型为 OBJECT(对象/变量),地址为数据段的偏移量。
  • 未定义符号(Undefined Symbols)
    • 如果源码引用了外部函数或变量(如标准库的 printf),但未在当前文件定义,.symtab 会标记这些符号为 UND(未定义),等待链接器从其他目标文件或库(如 libc)中解析和绑定。

.symtab 的结构与内容

符号表(.symtab)由多个符号表条目(Symbol Table Entries)组成,每个条目包含以下字段(可以通过 nmreadelf -s 查看):

  • Name:符号的名称(如 mainlabelprintf)。
  • Value:符号的地址(在目标文件或可执行文件中,可能为 0 或虚拟地址,链接后确定)。
  • Size:符号占用的字节数(例如,函数的大小或变量的长度)。
  • Type:符号的类型,常见类型包括:
    • NOTYPE:未指定类型(通常为未定义符号)。
    • OBJECT:变量或数据对象(如 label)。
    • FUNC:函数(如 main)。
    • SECTION:Section 本身。
    • FILE:源文件名称。
  • Binding:符号的绑定属性,常见绑定包括:
    • LOCAL:本地符号,仅在当前文件可见。
    • GLOBAL:全局符号,可被其他文件引用。
    • WEAK:弱符号,如果未定义则可被忽略。
  • Section Index:符号所属的 Section(如 .text.data.bssUND 表示未定义)。

示例:符号表条目

假设有一个简单的 C 源码:

代码语言:javascript
代码运行次数:0
复制
#include <stdio.h>

char label[] = "helloworld";
int main(void) {
    printf("Hello, world!\n");
    return 0;
}

编译生成目标文件 example.o

代码语言:javascript
代码运行次数:0
复制
$ gcc -c example.c -o example.o
$ nm example.o

输出可能如下(简化):

代码语言:javascript
代码运行次数:0
复制
0000000000000000 T main           # 地址 0x0,类型 FUNC,绑定 GLOBAL,位于 .text
0000000000000000 D label          # 地址 0x0,类型 OBJECT,绑定 GLOBAL,位于 .data
                 U printf          # 未定义,类型 NOTYPE,绑定 GLOBAL,位于 UND(需链接 libc)
  • main:函数,存储在 .text Section,地址为 0(可重定位文件中的相对地址,链接后确定)。
  • label:变量,存储在 .data Section,地址为 0(链接后确定)。
  • printf:未定义符号,标记为 U,需从标准库 libc 中解析。

链接生成可执行文件 example

代码语言:javascript
代码运行次数:0
复制
$ gcc example.o -o example
$ nm example

输出中 mainlabel 的地址变为具体值(如 0x401000),printf 的地址也从 libc 绑定。


.symtab 的生成与使用
  • 生成过程
    • 编译器(如 gcc)在编译源代码时,解析源码中的函数和变量,生成目标文件(.o)。
    • 编译器创建 .symtab Section,记录符号的名称、类型和临时地址(相对于 Section 的偏移)。
    • 如果符号是外部引用(未定义),标记为 UND,等待链接器处理。
  • 使用场景
    • 链接阶段:链接器(如 ld)读取 .symtab,解析未定义符号(如 printf),从库文件(如 libc.alibc.so)或其他目标文件中查找定义,分配最终地址。
    • 调试阶段:调试工具(如 gdb)使用 .symtab 将源码中的函数名和变量名映射到内存地址,方便设置断点、查看变量值。
    • 分析阶段:工具如 nmreadelf -s 可查看符号表,分析程序结构和依赖。

如何查看 .symtab

您可以使用以下命令查看符号表:

  • 使用 nm 命令
代码语言:javascript
代码运行次数:0
复制
$ nm hello.o    # 查看目标文件的符号表
$ nm a.out      # 查看可执行文件的符号表
代码语言:javascript
代码运行次数:0
复制
- 输出显示符号名称、地址、类型和 Section(如 `T` 为 `.text`,`D` 为 `.data`,`U` 为未定义)。
  • 使用 readelf -s 命令
代码语言:javascript
代码运行次数:0
复制
$ readelf -s hello.o    # 详细查看目标文件的符号表
代码语言:javascript
代码运行次数:0
复制
- 输出包括符号的名称、值、大小、类型、绑定和 Section 索引,提供更详细的信息。

示例输出(如 readelf -s hello.o):

代码语言:javascript
代码运行次数:0
复制
Symbol table '.symtab' contains 13 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS hello.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 
     4: 0000000000000000    17 OBJECT  GLOBAL DEFAULT    3 label
     5: 0000000000000000     0 FUNC    GLOBAL DEFAULT    1 main
     6: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND printf
  • labelOBJECT 类型,GLOBAL 绑定,位于 Section 3(可能为 .data.rodata),大小为 17 字节(字符串 "helloworld\0" 的长度加终止符)。
  • mainFUNC 类型,GLOBAL 绑定,位于 Section 1(.text),大小为 0(实际大小由链接后确定)。
  • printfNOTYPE 类型,GLOBAL 绑定,位于 UND(未定义),需链接器从 libc 解析。

注意事项
  • 去除符号表:在生产环境中,为了减小文件大小,可以使用 strip 命令去除 .symtab 和调试信息:
代码语言:javascript
代码运行次数:0
复制
$ strip hello.o    # 去除符号表和调试信息

但这会使调试变得困难。

  • 动态链接与符号表
    • 可执行文件可能还有 .dynsym(动态符号表),用于动态链接,记录与共享库相关的符号(如 libc 中的函数)。
    • .symtab.dynsym 的区别在于:.symtab 包含所有符号(包括本地和全局),而 .dynsym 只包含与动态链接相关的全局符号。
  • 符号表的大小.symtab 可能占较大空间,尤其在包含大量函数和变量的程序中。通过优化代码或使用 gcc -fvisibility=hidden 减少导出符号,可以减小符号表大小。

总结:如何理解 .symtab
  • 本质.symtab 是源码中函数名、变量名和代码对应关系的“映射表”,记录程序的符号及其在目标文件或可执行文件中的位置和属性。
  • 作用
    • 帮助链接器解析和绑定符号,确保程序正确连接。
    • 辅助调试工具定位源码和内存地址之间的关系。
  • 对应关系
    • 源码中的 int main(void) 对应 .symtab 中的 main 条目,指向 .text Section 的代码。
    • 源码中的 char label[] = "helloworld"; 对应 .symtab 中的 label 条目,指向 .data.rodata Section 的数据。
    • 外部引用(如 printf)标记为未定义(UND),链接时从标准库(如 libc)解析。
  • 查看与验证:使用 nmreadelf -s 查看符号表,结合源码和目标文件理解符号的定义和引用。

ELF 头信息与文件结构

ELF 头(ELF Header)位于文件开头,描述文件的基本信息,并定位程序头表和节头表

查看目标文件(**.o**** 文件)**

代码语言:javascript
代码运行次数:0
复制
$ readelf -h hello.o
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64    # 64 位 ELF 文件
  Data:                              2's complement, little endian    # 小端字节序
  Version:                           1 (current)    # ELF 格式版本
  OS/ABI:                            UNIX - System V    # 目标操作系统
  ABI Version:                       0    # ABI 版本
  Type:                              REL (Relocatable file)    # 文件类型为可重定位文件
  Machine:                           Advanced Micro Devices X86-64    # 目标架构
  Version:                           0x1
  Entry point address:               0x0    # 入口地址(可重定位文件无入口)
  Start of program headers:          0 (bytes into file)    # 程序头表偏移(目标文件无程序头表)
  Start of section headers:          728 (bytes into file)    # 节头表偏移
  Flags:                             0x0    # 特定标志(无特殊标志)
  Size of this header:               64 (bytes)    # ELF 头大小
  Size of program headers:           0 (bytes)    # 程序头表项大小(目标文件无程序头表)
  Number of program headers:         0    # 程序头表项数量
  Size of section headers:           64 (bytes)    # 节头表项大小
  Number of section headers:         13    # 节头表项数量
  Section header string table index: 12    # 节名字符串表的索引

查看可执行文件

代码语言:javascript
代码运行次数:0
复制
$ gcc *.o -o a.out
$ readelf -h a.out
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 # ELF文件的标识符
  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:          14768 (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

ELF 头的作用:

  • ELF 头定义了文件的基本特性(如架构、字节序、文件类型)和结构布局(如程序头表和节头表的偏移量)。
  • Magic(魔数 7f 45 4c 46),每个二进制文件都有,随机,系统可以通过magic标识文件为 ELF 格式,防止误解析。
    • 例如图片会解析为图片,按照图片格式打开,视频会解析为视频格式,按照视频格式打开,exe会直接运行。
  • Type 字段区分文件类型:REL(可重定位)、EXEC(可执行)、DYN(共享对象)。
  • 程序头表和节头表的偏移量(Start of program headersStart of section headers)用于定位文件的其他部分,确保解析器正确读取数据。
  • 可执行文件的入口地址(Entry point address)指定程序启动时的起始指令地址(通常指向用于存储代码的 .text Section 的起始位置)。

ELF区域和文件偏移量之间的关系

ELF 文件的整体结构:像一本书

想象 ELF 文件是一本书,每个部分都有自己的“页码”(偏移量)和作用:

  • ELF 头 是封面和目录,告诉你这本书的基本信息和结构。
  • 程序头表 是搬运清单,告诉操作系统如何把书的内容搬到内存。
  • 节(Sections) 是书的章节,包含具体的代码、数据等内容。
  • 节头表 是详细目录,记录每个章节的位置和属性。 偏移量就像页码,告诉你每个部分从文件的哪一“页”开始。下面,我们逐一拆解这些部分和它们在文件中的偏移量关系。

ELF Header(ELF 头)
  • 位置:文件的最开头,偏移量固定为 0。
  • 作用:ELF 头是整个文件的“门面”,提供文件的基本信息和导航指南。
  • 内容
    • 文件类型(例如,可执行文件、共享库、目标文件)。
    • 目标架构(例如 x86、ARM)。
    • 程序头表和节头表的起始偏移量(分别由字段 e_phoffe_shoff 指定)。
  • 偏移量关系
    • 因为它是文件的起点,偏移量始终是 0。
    • 它的大小通常是固定的(比如 52 字节或 64 字节,取决于 32 位或 64 位架构),所以下一个区域(通常是程序头表)的偏移量从 ELF 头结束处开始。
  • 通俗理解: ELF 头就像书的封面,告诉你这本书是小说还是教材(文件类型),适合谁看(架构),以及“正文”(程序头表)和“目录”(节头表)从哪页开始。

Program Header Table(程序头表)
  • 位置:通常紧随 ELF 头之后,但具体偏移量由 ELF 头中的 e_phoff 字段指定。
  • 作用:程序头表是一张“搬运清单”,告诉操作系统如何将文件加载到内存中运行。
  • 内容
    • 描述了文件中的段(Segment),比如代码段(.text)、数据段(.data)等。
    • 每个段的信息包括:文件偏移量(从文件开头计算)、虚拟内存地址、大小、权限(可读、可写、可执行)。
  • 偏移量关系
    • 它的起始位置由 e_phoff 决定。例如,如果 e_phoff = 64,意味着程序头表从文件第 64 字节开始。
    • 程序头表是一个连续的表格,每个条目大小固定(32 位系统是 32 字节,64 位是 56 字节),条目数量由 ELF 头中的 e_phnum 指定。
  • 通俗理解: 程序头表就像物流清单,告诉搬运工(操作系统):“把这部分货物(段)搬到内存的这个地址,注意有些货物只能看(只读),有些可以改(可写)。” 它的“页码”(偏移量)由 ELF 头告诉你。

Sections(节,Section 1、Section 2、…)
  • 位置:通常在程序头表之后,但具体位置由节头表中的条目指定,可能是分散的。
  • 作用:节是文件的“逻辑章节”,用来组织代码、数据和元信息,方便链接器和调试工具使用。
  • 内容
    • 常见的节包括:
      • .text:存储程序的机器代码。
      • .data:存储初始化过的全局变量。
      • .bss:存储未初始化的全局变量(不占文件空间,只记录大小)。
      • .symtab:存储符号表(函数名、变量名等)。
    • 每个节有自己的类型、大小和文件偏移量。
  • 偏移量关系
    • 每个节的起始偏移量记录在节头表中(后面会讲到)。
    • 这些偏移量是从文件开头计算的字节数。例如,.text 节可能从偏移量 1024 开始,.data 节从 2048 开始。
    • 节的位置不一定是连续的,可能根据文件类型(目标文件、可执行文件)有所不同。
  • 通俗理解: 节就像书中的章节,每章有不同的内容(代码、数据、符号表),但具体从哪页开始要看“目录”(节头表)。操作系统运行程序时不直接用节,而是通过段来加载它们。

Section Header Table(节头表)
  • 位置:通常在文件末尾,具体偏移量由 ELF 头中的 e_shoff 字段指定。
  • 作用:节头表是一张“详细目录”,记录所有节的属性和位置,方便链接器或调试工具查找。
  • 内容
    • 每个节的条目包括:名称、类型、文件偏移量、大小、权限等。
    • 例如,一个条目可能说:.text 节从偏移量 1024 开始,大小 512 字节,可执行。
  • 偏移量关系
    • 它的起始位置由 e_shoff 决定。例如,e_shoff = 5000 意味着节头表从文件第 5000 字节开始。
    • 节头表也是一个连续的表格,每个条目大小固定(32 位系统是 40 字节,64 位是 64 字节),条目数量由 ELF 头中的 e_shnum 指定。
  • 通俗理解: 节头表就像书的详细目录,告诉你:“第 1 章(.text)从 10 页开始,讲故事;第 2 章(.data)从 20 页开始,放插图。” 它的“页码”(偏移量)由 ELF 头指明。

整体偏移量关系总结

ELF 文件中的每个区域通过偏移量紧密关联,以下是它们的位置和依赖关系:

  • ELF 头:偏移量 0,固定在开头,告诉我们程序头表(e_phoff)和节头表(e_shoff)的偏移量。
  • 程序头表:偏移量由 e_phoff 指定,描述段的偏移量和内存映射。
  • 节(Sections):偏移量由节头表指定,可能分散在文件中,存储具体内容。
  • 节头表:偏移量由 e_shoff 指定,通常在末尾,记录所有节的偏移量和属性。
图解(假设的偏移量示例)
代码语言:javascript
代码运行次数:0
复制
文件偏移量 (字节):
0          64          128         1024       2048       5000
|----------|-----------|-----------|----------|----------|----------|
ELF 头     程序头表    (其他内容)  .text 节   .data 节   节头表
  • ELF 头(0-64 字节):告诉我们程序头表从 64 开始,节头表从 5000 开始。
  • 程序头表(64-128 字节):描述段的偏移量(如 .text 从 1024 开始)。
  • 节(1024、2048 等):具体内容的位置由节头表指定。
  • 节头表(5000 开始):记录 .text 在 1024,.data 在 2048。
节(Section)与段(Segment)的偏移量区别
  • :由程序头表管理,用于内存加载。一个段可能包含多个节(比如 .text.rodata 合成一个只读段)。段的偏移量记录在程序头表中。
  • :由节头表管理,用于链接和调试。节的偏移量记录在节头表中,可能与段的偏移量重叠。
  • 通俗理解:段是大箱子,装着几个小盒子(节)。搬家时(加载内存)看箱子清单(程序头表),整理东西时(链接调试)看盒子标签(节头表)。
为什么要理解偏移量关系?
  • 文件结构分析:通过偏移量,可以用工具(如 readelfobjdump)定位 ELF 文件的每一部分。
  • 内存映射:程序头表的偏移量决定了程序运行时如何加载到内存。
  • 调试与链接:节头表的偏移量帮助查找代码、数据或符号的具体位置。
总结:ELF 文件就像一本书的搬家过程
  • ELF 头(封面):告诉你书的类型和目录页码。
  • 程序头表(搬运清单):告诉操作系统怎么把书的内容搬到内存。
  • (章节):书的实际内容,位置由目录指定。
  • 节头表(目录):详细记录每个章节的页码和信息。 偏移量是这些部分的“导航坐标”,ELF 头是起点,程序头表和节头表是指路牌,带你找到每个区域的具体位置。
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-03-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 目标文件
  • ELF文件:编译与链接基础
  • ELF从形成到加载轮廓
    • ELF 文件形成可执行文件
      • Step-1:将多份 C/C++ 源代码翻译成目标 .o 文件
      • Step-2:将多份 .o 文件的 Section 进行合并
    • ELF 可执行文件加载
      • Section 合并为 Segment
      • 查看可执行程序的 Section 和 Segment
      • 链接视图与执行视图:节头表与程序头表的区别与应用
      • 符号表
      • ELF 头信息与文件结构
    • ELF区域和文件偏移量之间的关系
      • ELF Header(ELF 头)
      • Program Header Table(程序头表)
      • Sections(节,Section 1、Section 2、…)
      • Section Header Table(节头表)
      • 整体偏移量关系总结
      • 节(Section)与段(Segment)的偏移量区别
      • 为什么要理解偏移量关系?
      • 总结:ELF 文件就像一本书的搬家过程
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档