首页 > 代码库 > PE文件格式---节和节表
PE文件格式---节和节表
17.1.4 节表和节
从排列位置来看,PE文件在DOS部分和PE文件头部分以后就是节表和多个不同的节(如图17.1中的③和④所示)。要理解什么是节表,什么是节以及它们之间的关系,那就首先要了解Windows是如何将PE文件映射到内存的。
1. PE文件到内存的映射
在执行一个PE文件的时候,Windows并不在一开始就将整个文件读入内存,而是采用与内存 映射文件类似的机制,也就是说,Windows装载器在装载的时候仅仅建立好虚拟地址和PE文件之间的映射关系,只有真正执行到某个内存页中的指令或者访 问某一页中的数据时,这个页面才会被从磁盘提交到物理内存,这种机制使文件装入的速度和文件大小没有太大的关系。
图17.2 PE文件到内存的映射
但是系统装载可执行文件的方法又不完全等同于内存映射文件。当使用内存映射文件时,系统对“原 著”非常忠实,如果将磁盘文件和内存映像对比一下,可以发现不管是数据本身还是数据之间的相对位置都是完全相同的。而装载可执行文件的时候,有些数据在装 入前会被预先处理(如需要重定位的代码),而装入以后,数据之间的相对位置也可能改变,如图17.2所示,一个节的偏移和大小在装入内存前后可能是完全不 同的。
Windows装载器在装载DOS部分、PE文件头部分和节表部分时不进行任何处理,而装载节的时候将根据节的属性做不同的处理,一般需要处理以下几个方面的内容。
● 内存页的属性
对于磁盘映射文件来说,所有的页都是按照磁盘映射文件函数指定的属性设置的,但是装载可执行文件时,与节对应的内存页的属性要按照节的属性来设置。所以在同属一个模块的内存页中,从不同节映射过来的内存页的属性是不同的。
● 节的偏移地址
节的起始地址在磁盘文件中是按照IMAGE_OPTIONAL_HEADER32结构的 FileAlignment字段的值对齐的,而被装载到内存中时是按照同一结构中的SectionAlignment字段的值对齐的,两者的值可能不同, 所以一个节被装入内存后相对于文件头的偏移和在磁盘文件中的偏移可能是不同的。
节是相同属性数据的组合,当节被装入到内存中的时候,同一个节对应的内存页将被赋予相同的页属 性,Windows系统对内存属性的设置是以页为单位来进行的,所以节在内存中的对齐单位必须至少是一个页的大小,对于Win32来说,这个值是4 Kb(1000h),而对于Win64来说,这个值是8 Kb(2000h)。
节在磁盘文件中的对齐单位就没有最小4 Kb的限制,为了减少磁盘文件的大小,文件对齐的单位一般要小于内存对齐的单位(FileAlignment的值一般为200h),这样,在磁盘中就不必为每个节最后的零头数据补足4 Kb的大小了。
● 节的尺寸
对节尺寸的处理有两个方面,首先是由于磁盘映像和内存映像中节对齐单位的不同而造成的长度扩展;其次是对包含未初始化数据的节的处理(如图17.2中的.data节)。
对于未初始化数据来说(比如在源代码中定义的.data?段),没必要为它们在磁盘文件中预留空间,只要在可执行文件被装载到内存中后为它们分配空间就可以了,所以包含未初始化数据的节在磁盘文件中的长度被定义为0,但是被装载到内存中的地址和大小是被明确指定的。
对于这种节来说,它所包含的内存页并没有磁盘文件内容与之对应,这些内存页是Windows装载器根据节的定义额外开辟出来的。
● 不进行映射的节
有些节中包含的数据仅仅在装载的时候用到,当文件装载完毕的时候,它们不会被递交到物理内存 页。最典型的例子就是包含重定位数据的节(如图17.2中的.reloc节),重定位数据对于文件的执行代码来说是透明的,它只供Windows装载器使 用,执行代码根本不会去访问它们,一旦装载完毕,继续为它们提交内存页是一种浪费。所以这些节存在于磁盘文件中,但并不会被映射到内存中。
2. 节表
PE文件中所有节的属性都被定义在节表中,节表由一系列的 IMAGE_SECTION_HEADER结构排列而成,每个结构用来描述一个节,结构的排列顺序和它们描述的节在文件中的排列顺序是一致的。全部有效结 构的最后以一个空的IMAGE_SECTION_HEADER结构作为结束,所以节表中总的IMAGE_SECTION_HEADER结构数量等于节的数 量加一。节表总是被存放在紧接在PE文件头的地方,也就是从PE文件头(注意:不是文件本身的头部)开始的偏移为00f8h的地方。
IMAGE_SECTION_HEADER结构的定义如下:
IMAGE_SECTION_HEADER STRUCT
Name1 db IMAGE_SIZEOF_SHORT_NAME dup(?) ;8个字节的节区名称
union Misc
PhysicalAddress dd ?
VirtualSize dd ? ;节区的尺寸
ends
VirtualAddress dd ? ;节区的RVA地址
SizeOfRawData dd ? ;在文件中对齐后的尺寸
PointerToRawData dd ? ;在文件中的偏移
PointerToRelocations dd ? ;在OBJ文件中使用
PointerToLinenumbers dd ? ;行号表的位置(供调试用)
NumberOfRelocations dw ? ;在OBJ文件中使用
NumberOfLinenumbers dw ? ;行号表中行号的数量
Characteristics dd ? ;节的属性
IMAGE_SECTION_HEADER ENDS
结构中的有些字段是供COFF格式的obj文件使用的,对可执行文件来说不代表任何意义,在分析的时候可以不予理会,真正有用的几个字段说明如下。
● Name1字段
这个字段的字段名原来应该是“Name”,但是这个名称和MASM中的关键字冲突,所以在定义的时候改为“Name1”,Name1字段定义了节的名称,字段的长度为8个字节。
PE文件中的节的名称是一个由ANSI字符组成的字符串,但并没有规定以0结束,如果节的名称字符串长度小于8个字节的话,后面以0补齐,但是字符串长度达到8个字节的话,后面就没有0字符了,所以在处理的时候要注意字符串的结束方式。
每个节的名称是惟一的,不能有同名的两个节,但是节的名称不代表任何含义,它仅仅是为了查看方便而设置的一个标记而已,可以选择任何名称甚至将它空着也可以,将包含代码的节命名为“DATA”或者将包含数据的节命名为“CODE”都是合法的。
各种编译器都以自己的方式对节进行命名,所以,在PE文件中可以看到各式各样的节名称,比如,在MASM32产生的可执行文件中,代码节被命名为“.text”;可读写的数据节被命名为“.data”;包含只读数据、导入表以及导出表的节被命名为“.rdata”;而资源节被命名为“.rsrc”等。但是在其他一些编译器中,导入表被单独放在“.idata”中;而代码节可能被命名为“.code”。
当从PE文件中读取需要的节时,不能以节的名称作为定位标准,正确的方法是按照IMAGE_OPTIONAL_HEADER32结构中的数据目录字段定位。
笔者曾看过一篇介绍如何存取PE文件资源的文章,其中用查找“.rsrc”节的方法得到资源,虽然大部分情况下用这种方法也可以正确地找到资源,但是严格地讲,只有数据目录的IMAGE_DIRECTORY_ENTRY_RESOURCE项才永远正确地指向资源数据。
● VirtualSize字段
代表节的大小,这是节的数据在没有进行对齐处理前的实际大小。
● VirtualAddress字段
指出节被装载到内存中后的偏移地址,这是一个RVA地址。这个地址是按照内存页对齐的,它的数值总是SectionAlignment的值的整数倍。
● PointerToRawData字段
指出节在磁盘文件中的所处的位置。这个数值是从文件头开始算起的偏移量。
● SizeOfRawData字段
指出节在磁盘文件中所占的空间大小,这个数值等于VirtualSize字段的值按照FileAlignment的值对齐以后的大小。
依靠这4个字段的值,装载器就可以从PE文件中找出某个节(从 PointerToRawData偏移开始的SizeOfRawData字节)的数据,并将它映射到内存中去(映射到从模块基地址开始偏移 VirtualAddress的地方,并占用以VirtualSize的值按照页的尺寸对齐后的空间大小)。
● Characteristics字段
这是节的属性标志字段,其中的不同数据位代表了不同的属性,具体的定义如表17.5所示,这些数据位组合起来描述了节的属性。
表17.5 节的属性标志位含义
位 | 数据位在Windows.inc中的预定义值以及为1时的含义 |
5 | (IMAGE_SCN_CNT_CODE或00000020h)节中包含代码 |
6 | (IMAGE_SCN_CNT_INITIALIZED_DATA或00000040h)节中包含已初始化数据 |
7 | (IMAGE_SCN_CNT_UNINITIALIZED_DATA或00000080h)节中包含未初始化数据 |
25 | (IMAGE_SCN_MEM_DISCARDABLE或02000000h)节中的数据在进程开始以后将被丢弃,前面举例的包含重定位表的.reloc节就是一个例子 |
26 | (IMAGE_SCN_MEM_NOT_CACHED或04000000h)节中的数据不会经过缓存 |
27 | (IMAGE_SCN_MEM_NOT_PAGED或08000000h)节中的数据不会被交换到磁盘 |
28 | (IMAGE_SCN_MEM_SHARED或10000000h)表示节中的数据将被不同的进程所共享,在第11章的钩子例子中的共享数据的节就设置了这个属性标志 |
29 | (IMAGE_SCN_MEM_EXECUTE或20000000h)映射到内存后的页面包含可执行属性 |
30 | (IMAGE_SCN_MEM_READ或40000000h)映射到内存后的页面包含可读属性 |
31 | (IMAGE_SCN_MEM_WRITE或80000000h)映射到内存后的页面包含可写属性 |
代码节的属性一般为60000020h,也就是可执行、可读和“节中包含代码”;数据节的属性 一般为c0000040h,也就是可读、可写和“包含已初始化数据”;而常量节(对应源代码中的.const段)的属性为40000040h,也就是可读 和“包含已初始化数据”;资源节的属性和常量节的属性一般是相同的。
当然节属性的定义不一定就是这些值,比如,当PE文件被压缩工具压缩以后,包含代码的节往往被 同时设置了可执行、可读和可写属性,因为解压部分需要将解压后的代码回写到代码段中。读者可以做个实验:在程序中往代码段写数据,编译链接完成后执行一下 肯定会引发异常,然而用Upx等压缩软件压缩后再执行,就会发现文件可以正常执行了,这就是因为压缩软件为了解压的需要而将节的属性设置为可写了。
PE文件格式---节和节表