目标文件
编译和链接这两个步骤,在Windows下被IDE封装的很完美,我们一般是使用一键编译并运行,但是当链接出错的话我们就束手无措了。在Linux下有gcc/g++编译器,可以直接展示出编译链接的过程。
在软件开发中,编译是将程序的源代码(通常是人类可读的高级语言,如 C/C++)翻译成 CPU 能够直接执行的机器代码(二进制代码)。通过这一步骤,源文件被转换为目标文件,为后续的链接奠定基础。
编译过程与目标文件的生成
以一个简单的例子为例:假设我们有一个源文件 hello.c
,其内容如下:
// hello.c
#include <stdio.h>
int main() {
printf("hello world!\n");
return 0;
}
使用 gcc
编译器,我们可以通过以下命令编译该源文件:
$ gcc -c hello.c
编译完成后,生成一个扩展名为 .o
的文件(例如 hello.o
),被称为目标文件(Object File)。我们可以通过以下命令查看生成的文件:
$ ls
hello.c hello.o
目标文件的特性:
- 目标文件是二进制文件,通常采用 ELF(Executable and Linkable Format)格式(在 Linux/x86_64 系统中)。
- 使用
file
命令可以检查目标文件的类型,例如:
$ 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 文件的四种类型
- 可重定位文件(Relocatable File)
- 即
.o
文件(目标文件)。 - 包含适合与其他目标文件链接,以创建可执行文件或共享目标文件的代码和数据。
- 这些文件是在编译阶段生成的,通常通过
gcc -c
命令生成,尚未进行最终的地址解析和链接。
- 即
- 可执行文件(Executable File)
- 即可直接运行的程序文件(如
a.out
或其他二进制可执行文件)。 - 通过链接器将多个目标文件和库文件组合后生成,包含完整的机器代码和数据,可由操作系统加载并执行。
- 即可直接运行的程序文件(如
- 共享目标文件(Shared Object File)
- 即
.so
文件(动态库)。 - 可以在运行时由多个程序共享加载,节省内存空间,但需要确保运行环境中有正确的库文件支持。
- 即
- 内核转储(Core Dumps)
- 用于存储当前进程的执行上下文,通常在进程因信号(如段错误)触发时生成。
- 这些文件可用于调试,分析程序崩溃的原因。
ELF 文件的结构组成
一个 ELF 文件由以下四个主要部分组成:
- ELF 头(ELF Header)
- 位于文件开头,描述文件的主要特性,如目标架构(例如 x86-64)、文件类型(可重定位、可执行等)以及版本信息。
- 其主要作用是定位文件的其他部分,为解析文件提供基础。
- 程序头表(Program Header Table)
- 列出文件中的所有有效段(Segments)及其属性。
- 记录每个段的起始位置、偏移量和长度,因为这些段在二进制文件中紧密排列,程序头表提供必要的描述信息以区分和加载这些段。
- 主要用于可执行文件和共享库,在加载时由操作系统或动态链接器使用。
- 节头表(Section Header Table)
- 包含对文件中的节(Sections)的描述,记录每个节的类型、位置、大小等信息。
- 节是 ELF 文件的基本组成单位,用于组织和存储不同的数据和代码。
- 节(Sections)
- ELF 文件中的基本数据单元,包含特定类型的信息。各种数据和代码存储在不同的节中,常见节包括:
- 代码节(.text):保存机器指令,是程序的主要执行部分。
- 数据节(.data):保存已初始化的全局变量和局部静态变量。
- 其他节如
.bss
(未初始化的全局变量和静态变量)、.rodata
(只读数据,如字符串字面量)等,具体取决于文件类型和编译选项。
- ELF 文件中的基本数据单元,包含特定类型的信息。各种数据和代码存储在不同的节中,常见节包括:
ELF从形成到加载轮廓
ELF 文件形成可执行文件
ELF(Executable and Linkable Format)文件是 Linux 系统中编译和链接的核心格式。为了生成可执行文件,涉及以下两个主要步骤,并补充相关的知识点:
Step-1:将多份 C/C++ 源代码翻译成目标 .o
文件
- 编译过程:通过编译器(如
gcc
或g++
)将 C/C++ 源代码(.c
或.cpp
文件)翻译成目标文件(.o
文件)。编译器会执行以下几个阶段:- 预处理(Preprocessing):处理
#include
、宏定义和条件编译指令,生成预处理文件(.i
文件)。 - 编译(Compilation):将预处理后的代码转换为汇编代码(
.s
文件),生成特定架构的机器指令。 - 汇编(Assembly):将汇编代码转换为二进制目标文件(
.o
文件),格式为 ELF 可重定位文件。
- 预处理(Preprocessing):处理
- 命令示例:使用
gcc -c
编译源文件,例如:
$ 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 合并细节:
- 链接器读取每个目标文件的节头表(Section Header Table),识别
.text
(代码)、.data
(初始化数据)、.bss
(未初始化数据)、.rodata
(只读数据)等 Section。 - 根据 Section 的属性(如可读、可写、可执行)和逻辑关系,合并这些 Section,形成连续的内存布局。
- 解析符号表(
.symtab
)和重定位表(.rela
),解决未定义符号(如函数或变量的引用),确保所有地址引用正确。 - 如果使用动态链接,还会处理动态符号表(
.dynsym
)和全局偏移表/过程链接表(.got.plt
),为运行时加载动态库做准备。
- 链接器读取每个目标文件的节头表(Section Header Table),识别
- 命令示例:生成可执行文件:
$ gcc source1.o source2.o -o program
或直接从源文件生成:
$ 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(段),作为执行视图的物理加载单元。
- ELF 文件中的 Section(如
- 合并原则:
- 相同属性:如可读(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,用于运行时解析共享库符号。
- 合并方式已在 ELF 文件生成时通过
查看可执行程序的 Section 和 Segment
- 查看 Section(节头表):使用
readelf -S
命令。例如:
$ readelf -S a.out
输出显示可执行文件中的各个 Section,如 .text
(代码)、.data
(初始化数据)、.rodata
(只读数据)、.bss
(未初始化数据)等,及其属性(地址、偏移、大小、权限等)。
- 查看 Segment(程序头表):使用
readelf -l
命令。例如:
$ readelf -l a.out
输出显示程序头表中的 Segment 信息,包括类型(如 LOAD
、DYNAMIC
)、虚拟地址、文件偏移、文件大小、内存大小、权限(R/W/E)和对齐方式。
- `LOAD` 段:需要加载到内存的代码和数据段,可能包含 `.text`、`.data` 等 Section。
- `DYNAMIC` 段:用于动态链接,包含动态库加载信息。
- `GNU_STACK` 段:指定栈的权限(通常可读写)。
查看可执行程序的 Section:
$ readelf -S a.out
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:
$ readelf -l a.out
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,加载共享库并绑定符号。
链接视图与执行视图:节头表与程序头表的区别与应用
-
链接视图(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
)的节头表。
-
执行视图(Execution View)
- 对应**程序头表(Program Header Table)**。
- 指导操作系统加载可执行文件,完成进程内存的初始化,服务于运行时的内存加载和初始化
- 告诉操作系统哪些模块可以被加载进内存。
- 加载进内存之后哪些分段是可读可写,哪些分段是只读,哪些分段是可执行的。
- 包含 Segment 信息(如
LOAD
、DYNAMIC
、GNU_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)。
- 在 C/C++ 源码中,程序员定义了函数(如
- 代码的对应关系:
- 编译器将源码翻译成机器代码后,函数和变量会被分配到特定的内存地址或 Section。
.symtab
记录每个符号的名称、类型、地址(或偏移量)、大小和所属 Section。例如:- 函数
main
可能记录在.text
Section,符号表条目显示其类型为FUNC
(函数),地址为某个虚拟地址。 - 变量
label
可能记录在.data
或.rodata
Section,符号表条目显示其类型为OBJECT
(对象/变量),地址为数据段的偏移量。
- 函数
- 未定义符号(Undefined Symbols):
- 如果源码引用了外部函数或变量(如标准库的
printf
),但未在当前文件定义,.symtab
会标记这些符号为UND
(未定义),等待链接器从其他目标文件或库(如libc
)中解析和绑定。
- 如果源码引用了外部函数或变量(如标准库的
.symtab
的结构与内容
符号表(.symtab
)由多个符号表条目(Symbol Table Entries)组成,每个条目包含以下字段(可以通过 nm
或 readelf -s
查看):
- Name:符号的名称(如
main
、label
、printf
)。 - Value:符号的地址(在目标文件或可执行文件中,可能为 0 或虚拟地址,链接后确定)。
- Size:符号占用的字节数(例如,函数的大小或变量的长度)。
- Type:符号的类型,常见类型包括:
NOTYPE
:未指定类型(通常为未定义符号)。OBJECT
:变量或数据对象(如label
)。FUNC
:函数(如main
)。SECTION
:Section 本身。FILE
:源文件名称。
- Binding:符号的绑定属性,常见绑定包括:
LOCAL
:本地符号,仅在当前文件可见。GLOBAL
:全局符号,可被其他文件引用。WEAK
:弱符号,如果未定义则可被忽略。
- Section Index:符号所属的 Section(如
.text
、.data
、.bss
或UND
表示未定义)。
示例:符号表条目
假设有一个简单的 C 源码:
#include <stdio.h>
char label[] = "helloworld";
int main(void) {
printf("Hello, world!\n");
return 0;
}
编译生成目标文件 example.o
:
$ gcc -c example.c -o example.o
$ nm example.o
输出可能如下(简化):
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
:
$ gcc example.o -o example
$ nm example
输出中 main
和 label
的地址变为具体值(如 0x401000
),printf
的地址也从 libc
绑定。
.symtab
的生成与使用
- 生成过程:
- 编译器(如
gcc
)在编译源代码时,解析源码中的函数和变量,生成目标文件(.o
)。 - 编译器创建
.symtab
Section,记录符号的名称、类型和临时地址(相对于 Section 的偏移)。 - 如果符号是外部引用(未定义),标记为
UND
,等待链接器处理。
- 编译器(如
- 使用场景:
- 链接阶段:链接器(如
ld
)读取.symtab
,解析未定义符号(如printf
),从库文件(如libc.a
或libc.so
)或其他目标文件中查找定义,分配最终地址。 - 调试阶段:调试工具(如
gdb
)使用.symtab
将源码中的函数名和变量名映射到内存地址,方便设置断点、查看变量值。 - 分析阶段:工具如
nm
和readelf -s
可查看符号表,分析程序结构和依赖。
- 链接阶段:链接器(如
如何查看 .symtab
您可以使用以下命令查看符号表:
- 使用
nm
命令:
$ nm hello.o # 查看目标文件的符号表
$ nm a.out # 查看可执行文件的符号表
- 输出显示符号名称、地址、类型和 Section(如 `T` 为 `.text`,`D` 为 `.data`,`U` 为未定义)。
- 使用
readelf -s
命令:
$ readelf -s hello.o # 详细查看目标文件的符号表
- 输出包括符号的名称、值、大小、类型、绑定和 Section 索引,提供更详细的信息。
示例输出(如 readelf -s hello.o
):
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
label
:OBJECT
类型,GLOBAL
绑定,位于 Section 3(可能为.data
或.rodata
),大小为 17 字节(字符串"helloworld\0"
的长度加终止符)。main
:FUNC
类型,GLOBAL
绑定,位于 Section 1(.text
),大小为 0(实际大小由链接后确定)。printf
:NOTYPE
类型,GLOBAL
绑定,位于UND
(未定义),需链接器从libc
解析。
注意事项
- 去除符号表:在生产环境中,为了减小文件大小,可以使用
strip
命令去除.symtab
和调试信息:
$ 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
)解析。
- 源码中的
- 查看与验证:使用
nm
、readelf -s
查看符号表,结合源码和目标文件理解符号的定义和引用。
ELF 头信息与文件结构
ELF 头(ELF Header)位于文件开头,描述文件的基本信息,并定位程序头表和节头表。
查看目标文件(**.o**
** 文件)**
$ 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 # 节名字符串表的索引
查看可执行文件
$ 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 headers
和Start of section headers
)用于定位文件的其他部分,确保解析器正确读取数据。 - 可执行文件的入口地址(
Entry point address
)指定程序启动时的起始指令地址(通常指向用于存储代码的.text
Section 的起始位置)。
ELF区域和文件偏移量之间的关系
ELF 文件的整体结构:像一本书
想象 ELF 文件是一本书,每个部分都有自己的“页码”(偏移量)和作用:
- ELF 头 是封面和目录,告诉你这本书的基本信息和结构。
- 程序头表 是搬运清单,告诉操作系统如何把书的内容搬到内存。
- 节(Sections) 是书的章节,包含具体的代码、数据等内容。
- 节头表 是详细目录,记录每个章节的位置和属性。
偏移量就像页码,告诉你每个部分从文件的哪一“页”开始。下面,我们逐一拆解这些部分和它们在文件中的偏移量关系。
ELF Header(ELF 头)
- 位置:文件的最开头,偏移量固定为 0。
- 作用:ELF 头是整个文件的“门面”,提供文件的基本信息和导航指南。
- 内容:
- 文件类型(例如,可执行文件、共享库、目标文件)。
- 目标架构(例如 x86、ARM)。
- 程序头表和节头表的起始偏移量(分别由字段
e_phoff
和e_shoff
指定)。
- 偏移量关系:
- 因为它是文件的起点,偏移量始终是 0。
- 它的大小通常是固定的(比如 52 字节或 64 字节,取决于 32 位或 64 位架构),所以下一个区域(通常是程序头表)的偏移量从 ELF 头结束处开始。
- 通俗理解:
ELF 头就像书的封面,告诉你这本书是小说还是教材(文件类型),适合谁看(架构),以及“正文”(程序头表)和“目录”(节头表)从哪页开始。
Program Header Table(程序头表)
- 位置:通常紧随 ELF 头之后,但具体偏移量由 ELF 头中的
e_phoff
字段指定。 - 作用:程序头表是一张“搬运清单”,告诉操作系统如何将文件加载到内存中运行。
- 内容:
- 描述了文件中的段(Segment),比如代码段(
.text
)、数据段(.data
)等。 - 每个段的信息包括:文件偏移量(从文件开头计算)、虚拟内存地址、大小、权限(可读、可写、可执行)。
- 描述了文件中的段(Segment),比如代码段(
- 偏移量关系:
- 它的起始位置由
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
指定,通常在末尾,记录所有节的偏移量和属性。
图解(假设的偏移量示例)
文件偏移量 (字节):
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
合成一个只读段)。段的偏移量记录在程序头表中。 - 节:由节头表管理,用于链接和调试。节的偏移量记录在节头表中,可能与段的偏移量重叠。
- 通俗理解:段是大箱子,装着几个小盒子(节)。搬家时(加载内存)看箱子清单(程序头表),整理东西时(链接调试)看盒子标签(节头表)。
为什么要理解偏移量关系?
- 文件结构分析:通过偏移量,可以用工具(如
readelf
或objdump
)定位 ELF 文件的每一部分。 - 内存映射:程序头表的偏移量决定了程序运行时如何加载到内存。
- 调试与链接:节头表的偏移量帮助查找代码、数据或符号的具体位置。
总结:ELF 文件就像一本书的搬家过程
- ELF 头(封面):告诉你书的类型和目录页码。
- 程序头表(搬运清单):告诉操作系统怎么把书的内容搬到内存。
- 节(章节):书的实际内容,位置由目录指定。
- 节头表(目录):详细记录每个章节的页码和信息。
偏移量是这些部分的“导航坐标”,ELF 头是起点,程序头表和节头表是指路牌,带你找到每个区域的具体位置。