作为PE格式文件,存在的目的就是为了在系统中执行。所以要在一个环境中执行,这个环境就会为这个文件的执行进行一些相关的操作,而操作需要条件和操作的量化!所以在PE文件中存储着一些数据,这些数据的存在就是为系统执行PE文件进行量化和设置约束条件。

在了解PE文件中为系统加载执行所存储信息之前,有必要先了解一下一个PE文件被分配进程空间后进程空间的布局(Layout)情况,这样会更容易理解我们所阐述的内容。一个PE文件被系统加载进属于自己的进程空间后,进程空间分成用户地址空间和系统地址空间,分别占据进程4G地址空间的高低2G(对于32位系统),由于PE文件中存储的所有信息都是为了控制系统对PE文件的加载和执行,和系统自身并无多大联系,因此,我们这里只列出用户地址空间的布局。因此,当前进程空间的用户地址空间布局大概如下:
1.进程参数
2.进程堆
3.进程载入的模块,就是为执行当前进程需要的外部引用和当前进程数据本身
这里需要注意的一点,假如存在一个PE文件,名称为A.EXE,其中需要使用外部引用B.DLL和C.DLL,那么系统加载器将先按照在A中引用B和C的顺序先加载B和C,并把对A的加载紧跟在B和C之后。例如,假设A中引用B和C的顺序为C、B,那么最后的地址空间中模块区的内容分别是:C的.text、C的.data、B的.text、B的.data、A的.text和A的.data
4.进程PEB,即进程环境块
5.线程堆栈及线程TEB,即线程环境块

当我们在某个IDE中写好代码,经过编译器、连接器的加工,多个目标文件组合成可执行文件,然后可执行文件被加载器加载到内存空间做好开始执行的准备。在PE文件中的PE头存储着一些与系统相关的信息,系统在加载可执行文件到内存时,首先会根据PE头中的.ImageBase字段获取PE映像文件在进程空间中的加载起始地址,从而确立PE文件应该加载到进程空间的什么位置;进程需要堆空间,而线程需要栈空间,对这些空间的分配是依据PE头中的SizeOfStackReserve、SizeOfStackCommit以及SizeOfHeapReserve和SizeOfHeapCommit来进行的,这些字段分别代表着为PE文件执行保留的进程堆和线程堆栈的大小以及对应增量大小。但需要注意的是加载器对进程堆和线程堆栈的分配工作并不是连贯进行的,中间穿插着载入模块的加载工作;接着从PE头字段SizeOfCode、SizeOfInitializedData和SizeOfUninitializedData获取PE文件.text、.data和.bss区域的大小,并在系统的进程空间中保留等同尺寸的内存区域,待之后将相关内容加载到此内存范围中。当前应用需要的外部引用中也包含着SizeOfCode的类似信息。因此,对引用的加载信息来源于引用本身;除了加载PE代码节和数据节的内容,系统加载器还会将PE文件的PE/COFF头加载到对应进程空间,具体加载到什么位置依据的是前面提到的.ImageBase字段值,而为PE/COFF头预留空间大小则根据字段.SizeOfHeaders来进行分配;当然,仅仅确立PE映像的起始加载地址是不够的,因为如果不确定PE文件需占VA空间的大小,那么就无法确定线程堆栈的分配地址,因为线程堆栈的分配是在用户地址空间的最后进行的。因此,为了确认PE文件载入跨度,系统加载器需要读取PE头中的SizeOfImage字段值。

至此,系统完成了对PE的加载,开始执行PE内容。

由上不难看出,在进程空间中,进程堆在代码区和数据区之前,而代码区和数据区又在线程堆栈之前。这点对我们开发调试软件很重要,我们可以在具体的软件设计过程中使用相关工具进行查看验证.....由于.NET PE的结构和传统的PE相差并不大,因此,这里的内容对传统PE同样适用.....