Window PE -- 区块

Window PE – 区块

区块

在PE件头与原始数据之间存在一个区块表(SectionTable)。区块表中包含每个块在映像中的信息,分别指向不同的区块实体。

区块表

跟IMAGE_NT_HEADERS的是区块表,它是一个IMAGE_SECTION_HEADER结构数组。每个IMAGE_SECTION_HEADER结构包含了它所关联的区块的信息,例如位置、长度、属性,该数组的数目由MAGE_NT_HEADERS.FileHeader.NumberOfSections指出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress;
DWORD SizeOfRawData;
DWORD PointerToRawData;
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

为了便于理解,我们假设PE.exe的区块表含有3个块的描述,分别是.text、.rdata和.data。每个块对应于一个MAGE_SECTION_HEADER结构,用十六进制工具查看块表,如下表。表中的标号对应于第1个IMAGE_SECTION_HEADER结构的以下字段。

1554724992303

  1. Name:块名。这是一个8位ASCII码名(不是Unicode内码),用来定义块名。多数块名以-个“.”开始(例如.text),这个“.”实际上不是必需的。值得注意的是,如果块名超过8字节,则没有最后的终止标志“NULL”字节。带有一个“$”的区块名会被链接器特殊对待,前面有“$”的同名区块会被合并。这些区块是按“$”后面的字符的字母顺序合并的。

  2. VirtualSize:指出实际被使用的区块的大小,是在进行对齐处理前区块的实际大小。如果VirtualSize的值大于SizeOfRawData值,那么SizeOfRawData表示来自可执行文件初始化数据的大小,与VirtualSize相差的字节用填充。这个字段在OBJ文件中是被设置为0的。

  3. VirtualAddress:该块装载到内存中的RVA。这个地址是按照内存页对齐的,它的数值总是SectionAlignr阳It的整数倍。在Microsoft工具中,第1个块的默认RVA值为lOOOh。在OBJ中,该字段没有意义,并被设置为0

  4. SizeOfRawData:该块在磁盘中所占的空间。在可执行文件中,该字段包含经FileAlignment调整的块的长度。例如,指定FileAlignment的值为200h,如果VirtualSize中的块长度为19Ah字节,该块应保存的长度为200h字节。

  5. PointerToRawData:该块在磁盘文件中的偏移。程序经编译或汇编后生成原始数据,这个字段用于给出原始数据在文件中的偏移。如果程序自装载PE或COFF文件(而不是由操作系统载入的),这一字段将比Vi山alAddress还重要在这种状态下,必须完全使用线性映像的方法载入文件,所以需要在该偏移处找到块的数据,而不是VirtualAddress段中的RVA地址。

  6. PointerToRelocations:在EXE件中无意义。在OBJ文件中,表示本块重定位信息的偏移量在OBJ文件中,如果该值不是0,会指向一个IMAGE_RELOCATION结构数组。

  7. PointerToLineNumbers:行号表在文件中的偏移量,这是文件调试信息。

  8. NumberOfRelocations:在EXE文件中元意义。在OBJ文件中,表示本块在重定位表中的重定位数目。

  9. NumberOfLinenur由巳rs:该块在行号表中的行号数目。

  10. Chacteristics:块属性。该字段是一指出块属性(例如代码/数据、可读/可写等)的标志比较重要的标志如表11.5所示,多个标志值求或即为Characteristics的值。这些标志中的很多都可以通过链接器的/SECTION开关设置。例如,“E0000020h=20000000hI40000000hI80000000hI00000020h”表示该块包含执行代码,可读、可写、可执行,“C00000040h=40000000hI80000000hI00000040h”表示该块可读、可写,包含已初始化的数据,“60000020h=20000000hI40000000hI00000020h”表示该块包含执行代码,可读、可执行。

    15547252243021554725236875

常见区块与区块合并

区块中的数据逻辑通常是关联的。PE文件一般至少有两个区块,一个是代码块,另一个是数据块。每个区块都有特定的名字,这个名字用于表示区块的用途。例如,一个块叫作.rdata,表明它是一个只读区块。区块在映像中是按起始地址(RVA)排列的,而不是按字母表顺序排列的。使用区块名只是为了方便,它对操作系统来说是无关紧要的。微软给这些区块分别取了有特色的名字,但这不是必需的。Borland链接器使用的是像“CODE”和“ATA”这样的名字。

EXE和OBJ文件的一些常见区块如表11.6所示。非另外声明,表中的区块名称来自微软的定义。

15547253172521554725336273

虽然编译器自动产生一系列标准的区块,但这没有什么不可思议的读者可以创建和命名自己的区块。在VisualC++中用#pragma来声明,告诉编译器将数据插入一个区块,代码如下。

1
#pragma  data_seg  ( "MY_DATA" )

这样,所有被VisualC++处理的数据都将放到一个叫作MY_DATA的区块内,而不是默认.data区块内。大部分程序只使用编译器产生的默认区块,但偶尔可能有-些特殊的需求,需要将代码或数据放到一个单独的区块里,例如建立一个全局共享块。

区块并非全部在链接时形成,更准确地说,它们一般是从OBJ文件开始被编译器放置的。链接器的工作就是合并所有OBJ和库中需要的块,使其最终成为一个合适的区块。例如,工程中的每一个OBJ至少有一个包含代码的.text区块,链接器把这些区块合并成一个.text区块。链接器遵循一套完整的规则,以判断哪些块需要合并及如何合并。OBJ件中的一个区块可能是为链接器准备,不会放入最后的可执行文件中(这样的区块主要用于编译器向链接器传递信息)。

接器的一个有趣的特征就是能够合并区块。如果两个区块有相似或一致的属性,那么它们在链接时能合并成一个区块,这取决于是否使用/MERGE开关。如下链接器选项将.rdata与.text区块合并为一个.text区块。

1
/MERG E:  .rdata =.text

合并区块优点是节省磁盘和内存空间。个区块至少占用1个内存,如果能将可执行文件内的区块数从4个减少到3个,就可能少用1个内存页。当然,这依颇于两个合并区块结尾的未用空间加起来是否能达到l个内存页。

当合并区块时,事情将变得有趣,因为这没有什么硬性规定。例如,把.rdata合并到.text里不会有问题,但不应该将.rsre、.reloc或.rdata合并到其他区块里。在VisualStudio.NET出现之前,可以将.idata合并到其他区块里,但在VisualStudio.NET中就不允许进行这样的操作了。不过,在制作发行版本时,链接器经常将.idata的一部分合并到其他区块里,例如.rdata。

为部分输入数据是在载入内存时由Windows加载器写人的,所以读者可能会对它们如何被放入只读区块感到疑惑。这是由于在加载时,系统会临时修改那些包含输入数据的页属性为可读、可写,初始化完成后恢复为原来的属性。

区块的对齐值

区块的大小是要对齐的。有两种对齐值,一种用于盘文件内,另一种用于内存中。PE文件头指出了这两个值,它们可以不同。

PE文件头里,FileAlignment定义了磁盘区块的对齐值。每一个区块从对齐值的倍数的偏移位置开始,而区块的实际代码或数据的大小不一定刚好是这么多,所以在不足的地方一般以OOh来填充,这就是区块的间隙。例如,在PE文件中,一个典型的对齐值是200h,这样每个区块从200h的倍数的文件偏移位置开始。假设区块的第1个节在400h处,长度为90h,那么00h~490h为这一区块的内容,而文件对齐值是200h,为了使这一节的长度为FileAlignment的整数倍,490h~600h会被0填充,这段空间称为区块间隙,下一个区块的开始地址为600h。

PE文件头里,SectionAlignment义了内存中区块的对齐值。当PE文件被映射到内存中时,区块总是至少从一个页边界处开始。也就是说,当一个PE文件被映射到内存中时,每个区块的第l个字节对应于某个内存页。在x86系列CPU中,内存页是按4KB(1000h)排列的;在x64中,内存页是按8KB(2000h排列的。所以,在x86系统中,PE文件区块的内存对齐值一般为1000h,每个区块从1000h的倍数的内存偏移位置开始。

非使用/OPT:OWIN98或/ALIGN开关,否则VisualStudio6.0中的默认值都是4KB,VisualStudio.NET链器依然使用默认的10町:WIN98开关,但如果文件大小小于特定值,就会以200h为对齐值。另一种对齐方式来自.NET文件的规定。.NET文件的内存对齐值为8KB,而不是普通x86平台上的4KB,这样就保证了在x86平台上编译的程序可以在x64平台上运行。如果内存对齐值为4KB,那么x64加载器就不能载入这个程序了,因为64位Windows中的内存页大小是8KB。

可以建立一个区块在文件中的偏移与在内存中的偏移相同的PE文件。虽然这样做会使可执行文件变大,但是可以提高载入速度VisualStudio6.0的默认选项/OPT:WIN98将使PE文件按照这种方式来创建。在VisualStudio.NET中,链接器可以不使用/OPT:NOWIN98开关,这取决于文件是否足够小。

文件偏移与虚拟地址的转换

一些PE文件为减小体积磁盘对齐值不是一个内存页1000h而是200h。当这类文件被映射到内存中后,同一数据相对于文件头的偏移量在内存中和磁盘文件中是不同的,这偏移地址与虚拟地址的转换问题。而那些磁盘对齐值(1000h)与内存页相同的区块,同一数据在磁盘文件中的偏移与在内存中的偏移相同,因此不需要转换。

区块显示了实例文件在磁盘与内存中各区块的地址、大小等信息。虚拟地址和虚拟大小是指该区块在内存中的地址和大小。物理地址和物理大小是指该区块在磁盘文件中的地址和大小。由于其磁盘对齐值为200h,与内存对齐值不同,故其磁盘|泱像和内存映像不同。

1554725645315

可以看出,文件被映射到内存中时,MS-DOS头部、PE文件头和块表的偏移位置与大小均没有变化,而当各区块被映射到内存中后,其偏移位置就发生了变化。例如,磁盘文件中.text块起始端与文件头的偏移量为add1,映射到内存后,.text块起始端与文件头(基地址)的偏移量为add2。同时,.text块与块表之间形成了一大段空隙,这部分数据全是以0填充的。在这里,addl的值就是文件偏移地址(FileOffset),add2的值就是相对虚拟地址(RVA)。假设它们的差值为此,则文件偏移地址与虚拟地址的关系如下。

1554725718474

在同一区块中,各地址的偏移量是相等的,可用上面的公式对此区块中的任意FileOffset与VA进行转换。但请不要错误地认为在整个文件里FileOffset与VA的差值是Δk

,因为各区块在内存中是以一个页边界开始的,从第l个区块的结束到第2个区块的开始(1000h对齐处)全以数据0填充,所以不同区块在磁盘与内存中的差值不同。如下表所示是该实例文件各区块在磁盘与内存中的起始地址差值。

1554725752993

例如,假设某一虚拟地址(VA)为401112h,要计算它的文件偏移地址。401112h在.text块中,此时D,.k=0C00h,故

Fi le Offset= VA - ImageBase - Δk = 401l12h - 400000h - 0C00h = 512h

再来看一看虚拟地址4020D2h的转换。4020D站在.rdata块中,此时Δk=IA00h,故

File Offset = VA - ImageBase - Δk = 4020D2h - 400000h - IA00h = 6D2h

实际操作中,建议使用RVA-Offset类的转换工具。

1571159911481

Author: BarretGuy
Link: https://basicbit.cn/2018/11/01/2018-11-01-Windows PE 区块/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.