PE文件头(IMAGE_NT_HEADER)才真正存放了PE文件的有用信息。PE文件头包含三个数据成员:一个数字签名和两个文件头数据结构—IMAGE_FILE_HEADER和IMAGE_OPTIONAL_HEADER,这些数据结构都是在winnt.h头文件中定义的。我们看一下IMAGE_NT_HEADER结构的定义:
其中数字签名(Signature)指出了这个PE文件适应的目标操作系统,需要注意的是,PE格式的文件不仅适用于Windows NT系列操作系统,还被应用在OS/2等操作系统内。对于NT系列操作系统,这个签名的值为0x00004550,其中0x4550即是“PE”的ASCII码。
IMAGE_FILE_HEADER的定义如下:
IMAGE_FILE_HEADER结构中与后续内容关系比较密切的变量有三个:Machine、Number OfSections和SizeOfOptionalHeader。Machine可以用来判断目标平台,即运行的目标CPU。操作系统在加载一个PE文件的时候,首先检查这个变量是否与当前的CPU类型吻合。如果吻合则继续运行,否则就放弃进一步运行。SizeOfOptionalHeader指出IMAGE_OPTIONAL_HEADER结构,即IMAGE_NT_HEADERS中的第二个结构体的大小。显然,IMAGE_OPTIONAL_HEADER是一个大小可变的结构体,其大小由该变量确定。
对我们来说最重要的是NumberOfSections变量,这个变量记录了PE文件中节(section)的个数。可执行文件的相关数据,都以节的形式存放在PE文件中,如代码节(.text)、全局数据节(.data)、未初始化的数据节(.bss)等。节的具体内容,在14.2.3中讲解。
另外一个文件头—IMAGE_OPTIONAL_HEADER,是更重要的一个头,虽然其名字中包含optional,但该头绝不是可选的,而是必须的。我们的开发辅助工具在对PE文件进行处理的时候,重点就是针对该结构所包含的信息,对文件进行处理。
下面是其定义:
这是一个庞大的数据结构,但大多数变量与我们没有关系,我们重点关注上述定义中用黑体标注出来的四个变量,下面分别进行说明。(www.xing528.com)
AddressOfEntryPoint,这个域表示应用程序入口点的位置,这个位置是程序被加载到内存,完成全部预处理后的位置,而不是PE文件在磁盘上的位置。需要注意的是,PE文件在磁盘上的存储格式,与被操作系统加载到内存并处理后的格式,大多数情况下是不相同的。因此在PE格式的相关描述数据结构(比如我们介绍的这些HEADER结构)中,对于一个具体位置,一般会用两种方式来描述:虚拟相对位置和文件相对位置。所谓虚拟相对位置,是PE文件被加载到内存并预处理后的位置。由于PE文件加载到内存的起始地址是不固定的,因此这里的虚拟相对位置是一个“相对”值。其绝对值则是PE文件的加载地址加上虚拟相对位置。AddressOfEntryPoint就是一个虚拟相对位置。比如一个PE文件,PE头部中定义的AddressOfEntryPoint为0x400,这是个虚拟相对位置。但是PE文件被加载到内存后,其加载地址是0x400000,则该可执行文件入口点的真正地址是0x400400。
另外一个描述位置的方法,是文件相对位置,即相对文件开始处的具体存储位置。理解虚拟相对位置和文件相对位置的前提,是理解PE文件在磁盘上的存储方式,与最终加载到内存后的布局是不一样的。我们前面讲过,PE文件的主体部分是由许多节组成的。不同的节,在磁盘上可能是连续存放,也可能是以16B为单位进行对齐的。但是在加载到内存后,缺省情况下却是以CPU的页面尺寸为单位对齐的,比如以4KB为单位对齐。显然,文件被加载到内存后的布局“膨大”了。虚拟相对位置用的是文件被加载到内存后“膨大”的布局为基础进行描述的,而文件相对位置则是以文件在磁盘上的存储布局为基础进行描述的。还是以上述AddressOfEntryPoint为例,其虚拟相对位置是0x400,但是其文件相对位置可能是0x200。
理解虚拟相对位置和文件相对位置后,剩下的几个变量就容易理解了。我们先看SectionAlignment和FileAlignment。SectionAlignment是不同的节被加载到内存后的对齐大小。比如一个节的实际大小是3KB,但是SectionAlignment是4KB,则这个节必须占用4KB的空间,下一个节需要从另一个4KB的开始处进行加载。FileAlignment与此类似,就是一个节在文件中的对齐大小。比如一个节的大小是3KB,但是FileAlignment是4KB,则该节存放在磁盘上的时候,也必须占用4KB空间。
ImageBase变量的含义很简单,是PE模块被加载到内存后的基地址。需要注意的是,这个基地址只是对操作系统的一个建议,即建议以这个地址为基地址加载PE模块。操作系统可以以这个地址进行加载,但是如果这个地址被占用(加载DLL时,经常遇到),则操作系统需要找另外的地址进行加载。如果以PE头中的ImageBase进行加载,则无需对代码进行重定位操作,因为编译器缺省按照ImageBase为基地址对代码进行编译和链接。如果以不同于ImageBase的地址加载PE文件,则操作系统需要对加载后的PE文件重定位。这个加载地址可以使用编译器的/BASE选项进行修改,因为Hello China内核的加载地址是预先设计好的,因此我们在编译Master等操作系统核心模块的时候,就使用/BASE选项对编译器进行了配置,使得编译器以我们指定的基地址为依据对源代码进行编译和链接。
至此,我们把PE头,即IMAGE_NT_HEADER结构简略地讲完了,下面看一下如何访问这些结构体中的变量。假设PE文件被读入内存的pBinFile位置处,下面是一段显示PE文件头关键信息的代码:
代码比较简单,无非是通过层层定位,找到对应的结构体的开始处,然后用结构体指针访问感兴趣的变量即可。
免责声明:以上内容源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。