内存分配与映射
在计算机科学与逆向工程领域,理解二进制文件的静态结构(在磁盘上)与动态布局(在内存中)之间的映射关系至关重要。IDA Pro 等反汇编工具中显示的 .text, .plt, .got, .tls 等“节”(Sections)与用户提供的内存布局图中的“段”(Segments)存在着严密的对应与转换关系。
以下是对这一体系的全面、专业讲解,涵盖 ELF 文件格式、链接视图与执行视图的差异,以及各类关键节的具体含义。
1. 核心概念:链接视图与执行视图
要理解 IDA 中的区段与内存图的关系,首先需要区分两种看待二进制文件的视角:
-
链接视图 (Linking View):
-
基本单位:节 (Section)。
-
描述对象:编译器和链接器。
-
作用:将代码和数据按照逻辑功能分类(如代码归一类,只读数据归一类,可读写数据归一类)。IDA 默认主要展示的就是这种视图。
-
-
执行视图 (Execution View):
-
基本单位:段 (Segment)。
-
描述对象:操作系统加载器 (Loader)。
-
作用:告诉操作系统如何将文件映射到内存中。操作系统并不关心具体的函数在哪里,只关心哪些内存区域是“可读可执行”的(RX),哪些是“可读可写”的(RW)。
-
联系:一个“段”通常包含多个属性相似的“节”。例如,
.text(代码)和.rodata(只读数据)通常被合并进同一个可执行段(Text Segment)。
-
2. 标准代码与数据节 (Standard Code & Data Sections)
这是程序运行最基础的部分,对应常规的逻辑区域:
代码与指令类
-
.text:-
存放程序的核心机器代码(Machine Code)。
-
归属:Text Segment。
-
权限:Read-Execute (RX)。禁止写入,以防止代码被篡改。
-
-
.init/.fini:-
存放程序初始化和终止时的代码。
.init在main函数之前执行(用于环境准备),.fini在main结束后执行。 -
归属:通常合并入 Text Segment。
-
数据类
-
.rodata(Read-Only Data):-
存放常量数据,如字符串字面量("Hello World")、
const修饰的全局变量、switch语句的跳转表。 -
归属:Text Segment 或独立的只读段 (RO)。
-
-
.data:-
存放已初始化的全局变量和静态变量(例如
int count = 10;)。 -
归属:Data Segment。
-
权限:Read-Write (RW)。
-
-
.bss(Block Started by Symbol):-
存放未初始化或初始化为 0 的全局变量和静态变量。
-
特点:在磁盘文件中不占用实际空间(只有大小记录),但在内存加载时会分配空间并自动清零。
-
归属:紧随 Data Segment 之后。
-
3. 线程局部存储节 (Thread Local Storage Sections)
在多线程程序中,某些变量需要对每个线程“私有”,即每个线程都有一份独立的副本。
-
.tls/.tdata(Thread Local Data):-
存放已初始化的线程局部变量(例如使用
__thread int i = 5;定义的变量)。 -
机制:ELF 文件中存储的是这些变量的“初始镜像”。每当系统创建一个新线程时,都会分配一块新的内存区域,并将
.tdata的内容复制进去。 -
寻址:在 x86_64 Linux 中,通常通过
FS段寄存器进行访问(例如fs:0x28)。
-
-
.tbss(Thread Local BSS):-
存放未初始化的线程局部变量。
-
机制:同样不占用磁盘空间,线程创建时分配并清零。
-
4. 初始化与终止数组节 (Initialization & Termination Arrays)
除了 .init 和 .fini 代码段外,现代二进制文件(尤其是 C++ 程序)依赖函数指针数组来管理构造和析构。
-
.init_array:-
存放函数指针数组。这些函数会在
main函数之前被依次调用。 -
应用:C++ 全局对象的构造函数、
__attribute__((constructor))标记的 C 函数。 -
逆向重点:这是恶意代码或加壳程序隐藏初始化逻辑的常见位置。
-
-
.fini_array:-
存放函数指针数组。这些函数会在
main返回后或exit()被调用时执行。 -
应用:C++ 全局对象的析构函数、
__attribute__((destructor))标记的函数。
-
-
.preinit_array:- 在
.init_array之前执行,通常用于极其底层的运行时库初始化,用户代码很少涉及。
- 在
-
.ctors/.dtors:- 旧版 GCC 编译器生成的构造/析构列表,功能分别等同于
.init_array和.fini_array,在现代 ELF 中已较少见。
- 旧版 GCC 编译器生成的构造/析构列表,功能分别等同于
5. 动态链接类节 (Dynamic Linking Sections)
用于处理对外部共享库(Shared Libraries)的依赖和调用。
-
.interp:- 存放动态链接器的路径字符串(例如
/lib64/ld-linux-x86-64.so.2)。加载器首先读取此节,加载动态链接器,然后由动态链接器接管后续加载工作。
- 存放动态链接器的路径字符串(例如
-
.dynamic:- 动态链接信息的“头部”。包含了一系列标签(Tags),指向字符串表、符号表、重定位表等其他动态节的位置。
-
.plt(Procedure Linkage Table,过程链接表):-
本质:一小段跳板代码(Trampoline)。
-
作用:代理外部函数调用。属于 Text Segment (RX)。
-
-
.got(Global Offset Table,全局偏移表):-
本质:指针数组。
-
作用:存放外部数据的地址。属于 Data Segment (RW)。
-
-
.got.plt:- 专门用于存放外部函数的解析地址。延迟绑定(Lazy Binding)机制的核心数据区。
6. 符号、字符串与重定位节 (Symbol, String & Relocation)
这些节主要用于链接过程或调试,部分在运行时必须存在,部分可被剥离(Strip)。
-
.symtab/.strtab:-
全符号表与字符串表。包含程序中所有符号(包括静态函数、局部变量名)。
-
特点:非运行时必需,通常被
strip命令删除以减小体积并阻碍逆向。
-
-
.dynsym/.dynstr:-
动态符号表与字符串表。仅包含动态链接所需的符号(如导出的全局函数、引用的库函数)。
-
特点:不可剥离,否则程序无法运行。IDA 分析时若无
.symtab,则主要依赖此表恢复函数名。
-
-
.rel.dyn/.rela.dyn:- 数据引用的重定位表。用于修正全局变量的地址。
-
.rel.plt/.rela.plt:- 函数引用的重定位表。用于修正
.got.plt中的函数地址。
- 函数引用的重定位表。用于修正
-
.shstrtab:- 节区名称字符串表。存放
.text,.data等节名称本身的字符串。
- 节区名称字符串表。存放
7. IDA 中的 LOAD 段
在 IDA 的段列表中,经常能看到名为 LOAD 的条目。
-
含义:
LOAD对应 ELF 程序头表(Program Header)中的PT_LOAD类型的段。 -
作用:这代表了实际会被加载器映射到内存中的物理块。
-
结构:一个
LOAD段内部通常“包裹”了多个节。-
第一个
LOAD段通常包含:ELF Header,.init,.text,.plt,.rodata(权限 R-X)。 -
第二个
LOAD段通常包含:.data,.bss,.got,.init_array,.tls(权限 RW-)。
-
8. 节与内存布局图的完整映射
结合用户提供的内存布局图(image_e7fadc.png),这种联系如下:
-
Text Segment (低地址):
-
对应图示:
.text Section/.rodata Section。 -
包含的 IDA 节:
.text,.init,.plt,.rodata。 -
物理特性:由第一个
LOAD段映射而来,通常受硬件保护,不可写。
-
-
Data Segment (接续):
-
对应图示:
Data Section。 -
包含的 IDA 节:
.data,.got,.got.plt,.init_array,.fini_array,.tdata(初始镜像)。 -
物理特性:由第二个
LOAD段映射而来,可读可写,包含已初始化的全局数据。
-
-
BSS Section (接续):
-
对应图示:
BSS Section。 -
包含的 IDA 节:
.bss,.tbss(模板)。 -
物理特性:紧跟在 Data Segment 之后,由操作系统在加载时动态分配并清零。
-
-
Heap (堆):
-
对应图示:
Heap。 -
来源:不在 ELF 文件中以“节”的形式存在。通常由
brk或sbrk系统调用管理。
-
-
Memory Mapping Region (mmap):
-
对应图示:中间的
mmap区域。 -
来源:用于加载动态链接库(Shared Libraries)。每个被加载的
.so文件在内存中也会拥有自己独立的.text,.data,.bss和.tls副本。 -
TLS 运行时位置:虽然 ELF 中有
.tls节,但实际运行时的线程局部存储(TLS)区域通常由线程库(如 pthreads)在mmap区域或栈附近动态分配,并初始化为 ELF 中.tdata的内容。
-
-
Stack (栈):
-
对应图示:
Stack。 -
来源:由操作系统内核在创建进程时分配,位于高地址,向低地址增长。
-
总结:IDA 显示的区段列表是编译器视角的逻辑划分(功能分类);而用户提供的内存布局图是操作系统视角的物理划分(权限分类)。操作系统通过 LOAD 指令将多个节合并映射到内存中的同一个段。