《PE总结 》– DOS文件头、PE文件头、节表和表详解(一)

PE(Portable Executeable File Format,可移植的执行体文件格式),使用该格式的目标是使链接生成的EXE文件能在不同的CPU工作指令下工作。

可执行文件的格式是操作系统工作方法的真实写照。Windows操作系统中可执行程序有好多种,比如COM、PIF、SCR、EXE等,这些文件的格式大部分都继承自PE。其中,EXE是最常见的PE文件,动态链接库(大部分以dll为扩展名的文件)也是PE文件。

PE格式是Windows下最常用的可执行文件格式,在DOS时代COM文件是最早的也是结构最简单的可执行文件,COM文件中仅包含可执行代码,没有附带任何“支持性”数据,所以,第一句执行指令必须安排在文件头部:再就是没有重定位的信息,这样代码中不能有跨段操作数据的指令,造成代码和数据,甚至包括堆栈只能限制在同一个64KB的段中,由于这个原因,DOS系统中又定义了一种可执行文件—EXE文件,EXE文件在代码的前面加了一个文件头,文件头中包括各种说明数据,如文件入口,堆栈位置,重定位表等,操作系统根据文件头的信息将代码部分装入内存,根据重定位表修正代码,最后在设置好堆栈后从文件头中指定的入口开始执行。
    当Windows3.X出现的时候,可执行文件中出现了32位代码,程序运行时转到保护模式之前需要在实模式下做一些初始化,这样实模式的16位代码必须和32位代码一起放在可执行文件中,旧的DOS可执行文件格式无法满足需要,所以Windows3.X执行文件使用新的LE格式的可执行文件(Linear executable/线性可执行文件),Window9x中的VxD程序也是使用LE格式,因为这些驱动程序中也同时包括16位和32位代码。
    而在Windows 9x,Windows NT,Windows 2000下,纯32位可执行文件都使用微软设计的一种新格式——PE格式(Portable Executable File Format/可移值的执行体)。

PE文件的基本结构如图示:

image_8

一、DOS ME头IMAGE_DOS_HEADER

IMGAE_DOS_HEADER的具体定义如下:

IMAGE_DOS_HEADER STRUCT 

+00h WORD e_magic  // Magic DOS signature MZ(4Dh 5Ah)   DOS可执行文件标记 

+02h  WORD e_cblp   // Bytes on last page of file  

+04h WORD e_cp   // Pages in file

+06h WORD e_crlc   // Relocations

+08h WORD e_cparhdr   // Size of header in paragraphs

+0ah WORD e_minalloc   // Minimun extra paragraphs needs

+0ch WORD e_maxalloc  // Maximun extra paragraphs needs

+0eh WORD e_ss   // intial(relative)SS value   DOS代码的初始化堆栈SS 

+10h WORD e_sp   // intial SP value   DOS代码的初始化堆栈指针SP 

+12h WORD e_csum   // Checksum 

+14h WORD e_ip   //  intial IP value   DOS代码的初始化指令入口[指针IP] 

+16h WORD e_cs   // intial(relative)CS value   DOS代码的初始堆栈入口 CS

+18h WORD e_lfarlc   // File Address of relocation table 

+1ah WORD e_ovno  //  Overlay number 

+1ch WORD e_res[4]  // Reserved words 

+24h WORD e_oemid   //  OEM identifier(for e_oeminfo) 

+26h WORD e_oeminfo  //  OEM information;e_oemid specific  

+29h WORD e_res2[10]  //  Reserved words 

+3ch LONG  e_lfanew  // Offset to start of PE header   指向PE文件头 

IMAGE_DOS_HEADER ENDS

第一个字段e_magic被定义成字符“MZ”作为识别标志,后面的一些字段指明了入口地址、堆栈位置和重定位表位置等。

对于PE文件来说,有用的是最后的e_lfanew字段,这个字段指出了真正的PE文件头在文件中的位置,这个位置总是以8字节为单位对齐的。

image

从图中我们可以看到e_lfanew的值为000000E8,也就是说000000E8处是我们的PE文件头的位置。

二、PE文件头

PE文件头是由IMAGE_NT_HEADERS结构定义的:

IMAGE_NT_HEADERS STRUCT 

+0h DWORD Signature                    PE文件标识

+4h   IMAGE_FILE_HEADER  FileHeader

+18h IMAGE_OPTIONAL_HEADER32 OptionalHeader  

IMAGE_NT_HEADERS ENDS

PE文件头的第一个双字是一个标志,它被定义为00004550,也就是字符P E加上两个0,这也是PE这个称呼的由来。从名称来看似乎后面的这个PE文件表头结构是可选的,但实际上这个名称是名不符实的,因为它总是存在于每个PE文件中。

1.IMAGE_FILE_HEADER结构

IMAGE_FILE_HEADER STRUCT
+04h    WORD          Machine;   // 运行平台
+06h      WORD          NumberOfSections; // 文件的区块数目
+08h    DWORD         TimeDateStamp;  // 文件创建日期和时间
+0Ch      DWORD         PointerToSymbolTable; // 指向符号表(主要用于调试)
+10h     DWORD         NumberOfSymbols;  // 符号表中符号个数(同上)
+14h      WORD          SizeOfOptionalHeader;  // IMAGE_OPTIONAL_HEADER32 结构大小
+16h      WORD          Characteristics;  // 文件属性
IMAGE_FILE_HEADER ENDS

image为大家详细解释各个成员的含义和用法:

①Machine:可执行文件的目标CPU类型。

image

更多定义参见Windows.inc文件。

②NumberOfSection: 区块的数目。(注:区块表是紧跟在 IMAGE_NT_HEADERS 后边的)

③TimeDataStamp: 表明文件是何时被创建的。

这个值是自1970年1月1日以来用格林威治时间(GMT)计算的秒数,这个值是比文件系统(FILESYSTEM)的日期时间更加精确的指示器。

④PointerToSymbolTable: COFF 符号表的文件偏移位置,现在基本没用了。

⑤NumberOfSymbols: 如果有COFF 符号表,它代表其中的符号数目,COFF符号是一个大小固定的结构,如果想找到COFF 符号表的结束位置,则需要这个变量。

⑥SizeOfOptionalHeader: 紧跟着IMAGE_FILE_HEADER 后边的数据结构(IMAGE_OPTIONAL_HEADER)的大小。(对于32位PE文件,这个值通常是00E0h;对于64位PE32+文件,这个值是00F0h )。

⑦Characteristics: 文件属性,有选择的通过几个值可以运算得到。( 这些标志的有效值是定义于 winnt.h 内的 IMAGE_FILE_** 的值,具体含义见下表。普通的EXE文件这个字段的值一般是 0100h,DLL文件这个字段的值一般是 210Eh。)多种属性可以通过 “或运算” 使得同时拥有!

image

2.IMAGE_OPTIONAL_HEADER32结构

IMAGE_OPTIONAL_HEADER32 STRUCT
+18h    WORD    Magic;         // 标志字, ROM 映像(0107h),普通可执行文件(010Bh)
+1Ah    BYTE      MajorLinkerVersion;     // 链接程序的主版本号
+1Bh    BYTE      MinorLinkerVersion;     // 链接程序的次版本号
+1Ch    DWORD   SizeOfCode;     // 所有含代码的节的总大小
+20h    DWORD   SizeOfInitializedData;    // 所有含已初始化数据的节的总大小
+24h    DWORD   SizeOfUninitializedData; // 所有含未初始化数据的节的大小
+28h    DWORD   AddressOfEntryPoint;    // 程序执行入口RVA
+2Ch    DWORD   BaseOfCode;      // 代码的区块的起始RVA
+30h    DWORD   BaseOfData;      // 数据的区块的起始RVA
+34h    DWORD   ImageBase;      // 程序的首选装载地址
+38h    DWORD   SectionAlignment;      // 内存中的区块的对齐大小
+3Ch    DWORD   FileAlignment;      // 文件中的区块的对齐大小
+40h    WORD    MajorOperatingSystemVersion;  // 要求操作系统最低版本号的主版本号
+42h    WORD    MinorOperatingSystemVersion;  // 要求操作系统最低版本号的副版本号
+44h    WORD    MajorImageVersion;       // 可运行于操作系统的主版本号
+46h    WORD    MinorImageVersion;       // 可运行于操作系统的次版本号
+48h    WORD    MajorSubsystemVersion;  // 要求最低子系统版本的主版本号
+4Ah    WORD    MinorSubsystemVersion;  // 要求最低子系统版本的次版本号
+4Ch    DWORD   Win32VersionValue;       // 莫须有字段,不被病毒利用的话一般为0
+50h    DWORD   SizeOfImage;       // 映像装入内存后的总尺寸
+54h    DWORD   SizeOfHeaders;       // 所有头 + 区块表的尺寸大小
+58h    DWORD   CheckSum;       // 映像的校检和
+5Ch    WORD    Subsystem;       // 可执行文件期望的子系统
+5Eh    WORD    DllCharacteristics;       // DllMain()函数何时被调用,默认为 0
+60h    DWORD   SizeOfStackReserve;       // 初始化时的栈大小
+64h    DWORD   SizeOfStackCommit;       // 初始化时实际提交的栈大小
+68h    DWORD   SizeOfHeapReserve;        // 初始化时保留的堆大小
+6Ch    DWORD   SizeOfHeapCommit;        // 初始化时实际提交的堆大小
+70h    DWORD   LoaderFlags;        // 与调试有关,默认为 0
+74h    DWORD   NumberOfRvaAndSizes;  // 下边数据目录的项数,这个字段自Windows NT 发布以来一直是16
+78h    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];  // 数据目录表
IMAGE_OPTIONAL_HEADER32 ENDS

事实上,这个结构中的大部分字段都不重要,大家可以从注释中理解它们的含义,我将比较重要的字段在下边跟大家详细讲解:

①AddressOfEntryPoint字段

指出文件被执行时的入口地址,这是一个RVA地址。如果在一个可执行文件上附加了一段代码并想让这段代码首先被执行,那么只需要将这个入口地址指向附加的代码就可以了。

②ImageBase字段

指出文件的优先装入地址。也就是说当文件被执行时,如果可能的话,Windows优先将文件装入到由ImageBase字段指定的地址中,只有指定的地址已经被**模块使用时,文件才被装入到**地址中。链接器产生可执行文件的时候对应这个地址来生成机器码,所以当文件被装入这个地址时不需要进行重定位操作,装入的速度最快,如果文件被装载到**地址的话,将不得不进行重定位操作,这样就要慢一点。

对于EXE文件来说,由于每个文件总是使用独立的虚拟地址空间,优先装入地址不可能被**模块占据,所以EXE总是能够按照这个地址装入,这也意味着EXE文件不再需要重定位信息。对于DLL文件来说,由于多个DLL文件全部使用宿主EXE文件的地址空间,不能保证优先装入地址没有被**的DLL使用,所以DLL文件中必须包含重定位信息以防万一。因此,在前面介绍的 IMAGE_FILE_HEADER 结构的 Characteristics 字段中,DLL 文件对应的 IMAGE_FILE_RELOCS_STRIPPED 位总是为0,而EXE文件的这个标志位总是为1。

在链接的时候,可以通过对link.exe指定/base:address选项来自定义优先装入地址,如果不指定这个选项的话,一般EXE文件的默认优先装入地址被定为00400000h,而DLL文件的默认优先装入地址被定为10000000h。

③SectionAlignment 字段和 FileAlignment字段

SectionAlignment字段指定了节被装入内存后的对齐单位。也就是说,每个节被装入的地址必定是本字段指定数值的整数倍。而FileAlignment字段指定了节存储在磁盘文件中时的对齐单位。

④Subsystem字段

指定使用界面的子系统,它的取值如表所示。这个字段决定了系统如何为程序建立初始的界面,链接时的/subsystem:**选项指定的就是这个字段的值,在前面章节的编程中我们早已知道:如果将子系统指定为Windows CUI,那么系统会自动为程序建立一个控制台窗口,而指定为Windows GUI的话,窗口必须由程序自己建立。界面子系统的取值和含义如下:

image

⑤DataDirectory字段

这个字段可以说是最重要的字段之一,它由16个相同的IMAGE_DATA_DIRECTORY结构组成,虽然PE文件中的数据是按照装入内存后的页属性归类而被放在不同的节中的,但是这些处于各个节中的数据按照用途可以被分为导出表、导入表、资源、重定位表等数据块,这16个IMAGE_DATA_DIRECTORY结构就是用来定义多种不同用途的数据块的IMAGE_DATA_DIRECTORY结构的定义很简单,它仅仅指出了某种数据块的位置和长度。

IMAGE_DATA_DIRECTORY STRUCT

VirtualAddress DWORD ?    ;数据的起始RVA

isize DWORD ?    ;数据块的长度

IMAGE_DATA_DIRECTORY ENDS

数据目录列表的含义如下:

image

在PE文件中寻找特定的数据时就是从这些IMAGE_DATA_DIRECTORY结构开始的,比如要存取资源,那么必须从第3个IMAGE_DATA_DIRECTORY结构(索引为2)中得到资源数据块的大小和位置;同理,如果要查看PE文件导入了哪些DLL文件的哪些API函数,那就必须首先从第2个IMAGE_DATA_DIRECTORY结构得到导入表的位置和大小。

 

转载请标注来源:http://www.alonemonkey.com

By:AloneMonkey

本文链接:http://www.alonemonkey.com/pe-header-one.html