目录 Table of Contents
6 各种区块的描述和对齐值, RVA 详解
各种区块的描述
通常, 区块中的数据在逻辑上是关联的. PE 文件一般至少都会有两个区块 : 一个是代码块, 一个是数据块.
每一个区块都需要有一个截然不同的名字, 这个名字主要是用来表达区块的用途.
例如有一个区块叫 .rdata
, 表名它是一个只读区块. 注意 : 区块在映像中是按起始地址 (RVA) 来排列的, 而不是按字母表顺序.
另外, 使用区块名字只是人们为了认识和编程的方便, 这些名字对于操作系统来说都是无关紧要的. 微软给这些区块取了个有特色的名字, 但这不是必须的.
当我们要编程从 PE 文件中读取需要的内容时, 比如输入表, 输出表, 我们不能以区块名字作为参考, 而是应该按照数据目录表中的字段进行定位.
下面是区块的名称和意义
名称 | 描述 |
---|---|
.text | 默认的区块代码, 它的内容全是指令代码. 链接器把所有目标文件的 .text 块链接成一个大的 .text 块. 如果用的是 Borland C++, 其编译器把产生的代码存在叫作 CODE 的区域中. |
.data | 默认的读/写数据区块. 全局变量和静态变量一般放在这里. |
.rdata | 默认的只读数据区块. 至少有两种情况下要用到 .rdata. 一个是在 Microsoft 的链接器产生的 exe 文件中, 用于存放调试目录; 另一个是用于存放说明字符串. |
.idata | 包含其他外来 dll 的函数以及数据信息, 也就是我们说的输入表. 将 .idata 区块合并到另外一个区块已经成了现在的惯例, 典型的就是 .rdata 区块. |
.edata | 输出表. 当创建一个输出 API 或者数据的可执行文件时, 链接器会创建一个 .exp 文件, 这个文件就包含了一个 .edata 区块, 这个区块会被加入到最后的可执行文件中. 与 .idata 区块一样, .edata 区块也经常被发现合并到了 .text 或者 .tdata 区块中. |
.rsrc | 资源, 包含模块的全部资源, 比如图标, 菜单, 位图等等. 这个区块是只读的, 无论如何它不应该命名为 .rsrc 以外的其它任何名字, 也不能被合并到其它区块里. |
.bss | 未初始化数据. 很少用了, 取而代之的是执行文件的 .data 区块的 Vitual Size 被扩展到足够大的空间来装下未初始化数据. |
.tls | TLS的意思就是线程局部存储器, 用于支持通过 __declspec(thread) 声明的线程局部存储变量的数据. 这包括数据的初始化值, 也包括运行时所需要的额外变量. |
.reloc | 可执行文件的基址重定位. 基址重定位一般仅仅是 dll 文件才需要的. 在 Release 模式, 链接器并不给 exe 加上基址重定位, 重定位可以在链接的时候通过 /FIXED 开关去掉. |
.sdata | 可通过全局指针相对寻址的 "短" 可读/写数据. 用于 IA-64 和其它使用全局指针寄存器的体系上. IA-64 上的正常大小的全局变量位于这个区块中. |
.srcdata | 可通过全局指针相对寻址的 "短" 只读数据. 用于 IA-64 和其它使用全局指针寄存器的体系上. |
.pdata | 异常表. 包含一个 IMAGE_RUNTIME_FUNCTION_ENTRY 类型的结构数组, 这个结构是特定于 CPU 的. 数据目录中的 IMAGE_DIRECTORY_ENTRY_EXCEPTION 指向它. 用于使用基于表的异常处理的体系, 比如 IA-64. 唯一一个不使用基于表的异常处理的体系是 x86. |
.debug$S | obj 文件中 Codeview 格式的符号. 这是一个可变长的 Codeview 格式符号记录流. |
.debug$T | obj 文件中 Codeview 格式的类型记录. 这是一个可变长的 Codeview 格式类型记录流. |
.debug$P | 当使用预编译头时会出现在 obj 文件中. |
.drectve | 只用于 obj 文件, 包含一些链接器指令. 这些指令是一些能被传递到链接器命令行的 ASCII 字符串. 例如 : -defaultlib:LIBC 多个指令之间用空格隔开. |
.didat | 延迟加载的导入数据. 可在用非 Release 模式创建的可执行文件中找到它. 而在 Release 模式, 延迟加载数据被合并到其它区块中. |
当然我们在 Vitual C++ 中也可以命名我们自己的区块, 用 #pragma
来声明, 告诉编译器插入数据到一个区块内, 格式如下 :
#pragma data_msg("FC_data")
大家应该知道,
#
是宏处理符号. 啥是宏 ? 就是编译的时候由编译器直接先进行编译, 或者说按照指定格式机械替换.
以上语句告诉编译器, 叫它把数据都放进一个叫 "FC_data" 的区块内, 而不是默认的 .data 区块.
区块一般是从 obj 文件开始, 被编译器放置的. 链接器的工作就是合并左右 obj 和库中需要的块, 使其成为一个最终合适的区块. 链接器会遵循一套相当完整的规则, 它会判断哪些区块将被合并以及如何被合并.
合并区块 :
链接器的一个有趣特征就是能够合并区块. 如果两个区块有相似, 一致性的属性, 那么它们在链接的时候能够被合并成一个单一的区块. 这取决于是否开启编译器的 /merge 开关. 事实上合并区块有一个好处就是可以节省磁盘的内存空间...... 注意, 我们不应该将 .rsrc, .reloc, .pdata 合并到其他的区块里.
各种区块的文件对齐值
磁盘上的对齐值
之前我们简单了解过区块是要对齐的, 无论是在内存中存放还是在磁盘中存放. 但它们的对齐值一般是不同的.
PE 文件头里边的 FileAlignment 定义了磁盘区块的对齐值. 每一个区块从对齐值的倍数的偏移位置开始存放. 而区块的实际代码或数据的大小不一定刚好是这么多, 所以在多余的地方一般以 00H 来填充, 这就是区块之间的间隙.
例如, 在 PE 文件中, 一个典型的对齐值就是 200H, 这样, 每个区块都将从 200H 的倍数的文件偏移位置开始. 假设第一个区块在 400H 处, 长度为 90H, 那么从文件 400H ~ 490H 为这一个区块的内容, 而由于文件的对齐值是 200H, 所以为了是这一个区块的长度为 FileAlignment 的整数倍, 490H ~ 600H 这一个区间都会被 00H 填充, 这段空间称为区块间隙, 下一个区块的起始地址为 600H.
这些填充进去的 00H, 也就是空白间隙这里, 你可以在这里瞎改, 只要你不影响程序正常运行.
有些病毒程序就是这样来做免杀的.
内存中的对齐值
PE 文件头里边的 SectionAlignment 定义了内存中的区块的对齐值. PE 文件被映射到内存中时, 区块总是至少从一个页边界开始.
一般在 X86 系列的 CPU 中, 页是按 4KB (1000H) 来排列的; 在 IA-64 上, 是按 8KB(2000H) 来排列的. 所以在 X86 系统中, PE 文件区块的内存对齐值一般等于 1000H, 每个区块按 1000H 的倍数在内存中存放. 也就是第一个区块一般就是从 1000H 开始的.
RVA 详解
在前面我们探讨过 RVA 这个词, 现在我们深入探讨这个概念.
RVA 是相对虚拟地址 (Relative Virtual Address) 的缩写, 顾名思义, 它是一个 :相对地址. PE 文件的各种数据结构中涉及地址的字段大部分都是以 RVA 来表示的.
更为准确的说, RVA 是当 PE 文件被装载到内存中后, 某个数据位置相对于文件头的偏移量. 举个例子, 如果 windows 装载器将一个 PE 文件装入到 00400000H 处的内存中, 而某个区块中的某个数据被装入 0040xxxxH 处, 那么这个数据的 RVA 就是 0040xxxxH - 00400000H = 00008xxxxH, 反过来说, 将 RVA 的值加上文件被装载的基地址 (Image Base), 就可以得到数据在内存中的真实地址.
很明显, 我们发现, DOS 文件头, PE 文件头和区块表的偏移位置与大小均没有发生变化.
RVA 使得文件装入内存后的数据定位变得方便, 然而却给我们要定位放在硬盘上的静态 PE 文件带来了麻烦.
如何换算 RVA 和文件偏移
当处理 PE 文件的时候, 任何的 RVA 必须经过到文件偏移的换算, 才能用来定位并访问文件中的数据, 但是换算无法用一个简单的公式来完成. 事实上, 唯一可用的方法就是最土最笨的方法 :
- 步骤一 :
循环扫描区块表得出每个区块在内存中的起始 RVA (根据 IMAGE_SECTION_HEADER 中的 VirtualAddress 字段), 并根据区块的大小 (根据 IMAGE_SECTION_HEADER 中的 SizeOfRawData 段) 算出区块的结束 RVA (二者相加即可), 最后判断我们要计算的目标 RVA 是否落在这个区块内.
- 步骤二 :
我们通过步骤一定位了目标 RVA 在哪一个具体的区块, 然后我们用目标 RVA 减去该区块的起始 RVA, 这样就能得到目标 RVA 相对于起始地址的偏移量 RVA2.
- 步骤三 :
在区块表中获取该区块在文件中所处的偏移地址 (根据 IMAGE_SECTION_HEADER 中的 PointerToRawData 字段), 将这个偏移值加上步骤二得到的 RVA2 值, 就得到了真正的文件偏移地址.