Dos头结构:
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header WORD e_magic; // Magic number WORD e_cblp; // Bytes on last page of file WORD e_cp; // Pages in file WORD e_crlc; // Relocations WORD e_cparhdr; // Size of header in paragraphs WORD e_minalloc; // Minimum extra paragraphs needed WORD e_maxalloc; // Maximum extra paragraphs needed WORD e_ss; // Initial (relative) SS value WORD e_sp; // Initial SP value WORD e_csum; // Checksum WORD e_ip; // Initial IP value WORD e_cs; // Initial (relative) CS value WORD e_lfarlc; // File address of relocation table WORD e_ovno; // Overlay number WORD e_res[4]; // Reserved words WORD e_oemid; // OEM identifier (for e_oeminfo) WORD e_oeminfo; // OEM information; e_oemid specific WORD e_res2[10]; // Reserved words LONG e_lfanew; // File address of new exe header } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
NT头结构:
typedef struct _IMAGE_NT_HEADERS64 { DWORD Signature; //Signature PE文件标识,被定义为00004550 IMAGE_FILE_HEADER FileHeader; //FileHeader 结构。该结构指向IMAGE_FILE_HEADER。 IMAGE_OPTIONAL_HEADER64 OptionalHeader; //OptionalHeader 结构。该结构指向_IMAGE_OPTIONAL_HEADER32。Windows操作系统可执行文件的大部分特性均在这个结构里面呈现 } IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64; typedef struct _IMAGE_NT_HEADERS { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER32 OptionalHeader; } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32; typedef struct _IMAGE_ROM_HEADERS { IMAGE_FILE_HEADER FileHeader; IMAGE_ROM_OPTIONAL_HEADER OptionalHeader; } IMAGE_ROM_HEADERS, *PIMAGE_ROM_HEADERS; #ifdef _WIN64 typedef IMAGE_NT_HEADERS64 IMAGE_NT_HEADERS; typedef PIMAGE_NT_HEADERS64 PIMAGE_NT_HEADERS; #else typedef IMAGE_NT_HEADERS32 IMAGE_NT_HEADERS; typedef PIMAGE_NT_HEADERS32 PIMAGE_NT_HEADERS; #endif
文件头结构:
typedef struct _IMAGE_FILE_HEADER { WORD Machine; //用来指定PE文件的运行平台 /*Machine字段:用来指定PE文件运行的平台。由于Windows最初被设计为可以运行在Intel、Sun、Dec、 IBM等多种硬件平台上,或者能模拟这些平台的软件环境中,而不同 的硬件平台其指令的机器码不相同,因此为不同平台编译的EXE文件是无法通用的。假设将运行在Intel 386机器上的PE文件的该字段设置为01f0h,即指定平台为 IBM POWERPC (小尾方式),则系统会有如图3-12所示的提示。该字段预定义的值如表3-2所示。*/ WORD NumberOfSections; // PE中节的数量 DWORD TimeDateStamp; // 文件创建日期 时间 /*TimeDateStamp字段:编译器创建此文件时的时间戳。低32位存放的值是自1970年1月1日00:00时开始到创建时间为止的总秒数。 该数值可以随意修改而不会影响程序运行。所以,有的链接器在这里填人固定的值,有的则随意写入任何值,这对用户创建的文件并没有实际的意义。另外, 这个时间值与操作系统文件属性里看到的三个时间(创建时间、修改时间、访问时间)也没有任何联系。*/ DWORD PointerToSymbolTable; // 指向符号表 DWORD NumberOfSymbols; // 符号表 符号数量 WORD SizeOfOptionalHeader; // 扩展头结构 /*SizeOfOptionalHeader字段:指定_IMAGE_OPTIONAL_HEADER的长度,默认情况下这个值为00e0h,如果是64位PE文件,该结构默认大小为00F0h. 用户可以自己定义这个值的大小,不过需要注意: 更改完以后,需要自行将文件中的IMAGE_OPTIONAL_HEADER的大小扩充为你指定的值。扩充完以后,要维持文件的对齐特性。*/ WORD Characteristics; // 文件属性 位13 为0 字段值 010fh 普通可执行PE文件 位13为1 字段值 为210eh Dll /*Characteristics字段:文件属性字段。他的不同数据位定义了不同的文件属性,具体内容见表3-3.这是一个很重要的字段,不同的定义将影响系统对文件的装入方式,比如位 13为1时,表示这是一个DLL文件,那么系统将使用调用DLL入口函数的方式执行文件入口函数;当位13为0时,表示这是一个普通的可执行文件,系统直接跳到入口处执行。对于 普通的可执行PE文件来说,这个字段的值一般是010fh,而对于DLL文件来说,这个字段的值一般是210eh。 如表3-3所示,当第0位为1时,表明此文件不包含基址重定位信息,因此必须将其加载到文件头中指定的基地址字段位置。 如果进程空间此处的基地址被占用,加载器会 报错。在程序运行前如果发现文件中存在可重定位信息, 链接器会执行移除可执行文件中的重定位信息的操作。 当第1位为1时,表明此映像文件是合法的, 可以运行。如果未设置此标志, 表明出现了链接器错误。 当第7位为1时,表示文件为小尾方式,即内存中,最低有效位LSB位于最高有效位MSB的前面,与第15位的大尾方式(MSB在前,LSB在后)一样, 都不赞成使用该标志,最好将其设置为0。 当第10位为1时,如果此映像文件在可移动存储介质上,那么加载器将完全加载它并把它复制到内存交换文件中。 当第11位为1时,如果此映像文件在网络上,那么加载器也将完全加载它并把它复制到内存交换文件中。 当第13位为1时,表明此映像文件是动态链接库(DLL)。这样的文件总被认为是可执行文件,尽管它们并不能直接运行。 可执行文件的标志位设置为010fh,即第0、1、2、3、8位分别被设置为1 (如下所示),表示该文件为可执行文件,不含重定位信息,不含符号和行号信息,文件只在32位平台运行。*/ } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
选项头结构:
typedef struct _IMAGE_OPTIONAL_HEADER { // // Standard fields. // WORD Magic; // 标志字, ROM 映像(0107h),普通可执行文件(010Bh) /*Magic字段 :说明文件的类型,如果为010Bh,表面文件为PE32;如果为0107h,表明文件为ROM映像;如果为20Bh,表面文件为PE64.*/ BYTE MajorLinkerVersion; // 链接程序的主版本号 BYTE MinorLinkerVersion; // 链接程序的次版本号 DWORD SizeOfCode; // 所有含代码的节的总大小 DWORD SizeOfInitializedData; // 所有含已初始化数据的节的总大小 DWORD SizeOfUninitializedData; // 所有含未初始化数据的节的大小 DWORD AddressOfEntryPoint; // 程序执行入口RVA /*AddressOfEntryPoint字段 指出文件被执行时的入口地址,这是一个RVA地址。如果在一个可执行文件上附加了一段代码并想让这段代码首先被执行,那么只需要将这个 入口地址指向附加的代码就可以了。 */ DWORD BaseOfCode; // 代码的区块的起始RVA /*BaseOfCode字段 指出代码段的起始RVA。在内存中,代码段通常在PE文件头之后、数据块之前。在Microsoft链接器生成的执行文件中,RVA通常是1000h。 Borland的Tlink32是将ImageBase加上第一个Code Section的RVA,并将该结果存入该字段。*/ DWORD BaseOfData; // 数据的区块的起始RVA // // NT additional fields. 以下是属于NT结构增加的领域。 // DWORD ImageBase; // 程序的首选装载地址 /*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。*/ DWORD SectionAlignment; // 内存中的区块的对齐大小 /*SectionAlignment 字段指定了节被装入内存后的对齐单位。也就是说,每个节被装入的地址必定是本字段指定数值的整数倍。*/ DWORD FileAlignment; // 文件中的区块的对齐大小 /*FileAlignment字段指定了节存储在磁盘文件中时的对齐单位。 */ WORD MajorOperatingSystemVersion; // 要求操作系统最低版本号的主版本号 WORD MinorOperatingSystemVersion; // 要求操作系统最低版本号的副版本号 WORD MajorImageVersion; // 本PE文件映像的主版本号 WORD MinorImageVersion; // 本PE文件映像的次版本号 WORD MajorSubsystemVersion; // 运行所需要的子系统的主版本号 WORD MinorSubsystemVersion; // 运行所需要的子系统的次版本号 DWORD Win32VersionValue; // 子系统版本号,暂时保留未用。必须设置为0 DWORD SizeOfImage; // 映像装入内存后的总尺寸 +54h /*SizeOfImage字段。内存中整个PE文件的映射尺寸。以加载在内存中的xxx.exe为例,xxx,exe中文件头占用了1000h字节,三个节各占用了1000h个字节,所以文件在内存中占用 的空间总大小是04000h。该值可能比实际的大,但不能比它小,而且必须保证该值是SectionAlignment的整数倍。 */ DWORD SizeOfHeaders; // 所有头 + 区块表的尺寸大小 /*SizeOfHeaders字段 指定所有头+节表按照文件对齐粒度对齐后的大小(即含补足的0),在PE文件中,该部分数据是严格按照200h对齐的,如果不对齐,系统在加载时会提示出错。*/ DWORD CheckSum; // 映像的校检和 +5Ch WORD Subsystem; // 可执行文件期望的子系统 表3-4 /*Subsystem字段 指定使用界面的子系统,它的取值如表所示。这个字段决定了系统如何为程序建立初始的界面,链接时的/subsystem:**选项指定的就是这个字段的值, 在前面章节的编程中我们早已知道:如果将子系统指定为Windows CUI,那么系统会自动为程序建立一个控制台窗口,而指定为Windows GUI的话,窗口必须由程序自己建立。 */ WORD DllCharacteristics; // Dll文件属性 +60h 表3-6 /*DllCharacteristics字段 DLL文件属性。它是一个标志集,不是针对DLL文件的,而是针对所以PE文件的。这个字段定义了PE文件装载时的一些特性。*/ DWORD SizeOfStackReserve; // 初始化时保留的栈大小 /*SizeOfStackReserve字段:初始化时保留栈的大小。该字段表示初始线程的栈而保留的虚拟内存数量,然而并不是留出的所有虚拟内存都可以做栈 (真正的栈大小由下一个字段SizeOfStackCommit决定)。该字段的默认值是0x100000(1MB),如果调用API函数CreateThread时,把NULL当作传入的参数 那么创建出来的栈大小也会是1MB*/ DWORD SizeOfStackCommit; // 初始化时实际提交的栈大小 +68h /*SizeOfStackCommit字段:初始化时实际提交的栈大小。保证初始线程的栈实际占用内存空间的大小,它是被系统提交的。这些提交的栈不存在于 交换文件里,而是存在于内存中。对于Microsoft的链接器来说,这个值的初始值为0x1000字节(1页),对于TLINK32,则为2页。*/ DWORD SizeOfHeapReserve; // 初始化时保留的堆大小 /*SizeOfHeapReserve字段:初始化时保留的堆大小。用来保留给初始进程堆使用的虚拟内存,这个堆的句柄可以通过调用GetProcessHeap函数获得。 每一个进程至少会有一个默认的进程堆,改堆在进程启动的时候被创建,而且在进程的生命期中永远不会被删除。默认值1MB,我们可以通过链接器 的“-heap”参数指定起始的保留堆内存大小和实际提交的堆大小。*/ DWORD SizeOfHeapCommit; // 初始化时实际提交的堆大小 +70h /*SizeOfHeapCommit字段:初始化时提交的堆大小,在进程初始化时设定的堆所占用的内存空间。默认值为1页。*/ DWORD LoaderFlags; // 加载标志 与调试有关,默认为 0 DWORD NumberOfRvaAndSizes; // 下边数据目录的项数,这个字段自Windows NT 发布以来一直是16 IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; // 数据目录表 /*DataDirectory字段 这个字段可以说是最重要的字段之一,它由16个相同的IMAGE_DATA_DIRECTORY结构组成。虽然PE文件中的数据是按照装入内存后的页属性归类而被放在不同 的节中的,但是这些处于各个节中的数据按照用途可以被分为导出表、导入表、资源、重定位表等数据块,这16个IMAGE_DATA_DIRECTORY结构就是用来定义多种不同用途的 数据块的。*/ } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
数据目录项结构:
typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress;//数据起始RVA /*VirtualAddress 这个字段记录了特定类型数据的其实RVA。当然,针对不同的数据结构,该字段包含的是数据含义并不一样,有的数据甚至还不是RVA(如 属性证书数据中该字段表示的是FOA)*/ DWORD Size; //数据块长度 /*Size 这个字段记录了特定类型的数据块的长度。*/ } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
导入表结构:
typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { ULONG Characteristics; // 最高位1 0 for terminating null import descriptor ULONG OriginalFirstThunk; //最高位0 RVA to original unbound IAT (桥一) } DUMMYUNIONNAME; ULONG TimeDateStamp; // 0 if not bound, // -1 if bound, and real date\time stamp // in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND) // O.W. date/time stamp of DLL bound to (Old BIND) ULONG ForwarderChain; //链表的前一个结构 ULONG Name; //该结构对应DLL的文件名 ULONG FirstThunk; // RVA to IAT (桥二) } IMAGE_IMPORT_DESCRIPTOR; typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
导出表结构:
typedef struct _IMAGE_EXPORT_DIRECTORY { DWORD Characteristics; DWORD TimeDateStamp; WORD MajorVersion; WORD MinorVersion; DWORD Name; DWORD Base; //Base:这个字段包含用于这个可执行文件导出表的起始序数值(基数)。正常地,这个值是1.但是并不需要非得这样。当通过序数来查询一个导出函数时,这个值 序数重被威去,结果被用作进入导出地址表(EAT)的索引。 DWORD NumberOfFunctions; //NumberOfFunctions EAT中的条目数量。注意,一些条目可能是0,表明用这个序数值没有代码或数据被导出。 DWORD NumberOfNames; //NumberOfNames 导出函数名称表(ENT)里的条目数量。这个值总是小于或等于NumberOfFunctions域值。小于的情况发生在符号只通过序数来导出,另外当被赋值的序数里有数字间距时也会是小于的。 DWORD AddressOfFunctions; // RVA from base of image // AddressOfFunctions; EATE的RVA。EAT是一个RVA数组,数组中的每一个非零的RVA都对应于一个被导出的符号。 DWORD AddressOfNames; // RVA from base of image //AddressOfNames: ENT的RVA。 ENT是一个指向ASCII字符串的RVA数组。每一个ASCII字符串对应于一个通过名字导出的符号。这个表是排序的,所以ASCII字符串也是按顺序排列的。这允许加载器在查询一个被导出的符号时 //用二进制查找方式,名称的排序是二进制的(像C++ RTL中stremp函数提供的一样),而不是一个环境特定的字母顺序。 DWORD AddressOfNameOrdinals; // RVA from base of image //AddressofNameOrdinals:导出序数表的RVA。这个表是字的数组。这个表将ENT中的数组索引映射到相应的导出地址表条目。 } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
TLS表结构;
typedef struct _IMAGE_TLS_DIRECTORY32 { DWORD StartAddressOfRawData;//-定范围内存的起始地址 DWORD EndAddressOfRawData;//一定范围内存的各止地址 PDWORD AddressOfIndex;//索引地址 PIMAGE_TLS_CALLBACK *lddressOfCallBacks;//回调函数指针数组的地址PIMAGE_TLS_CALLBACK * DWORD SizeOfZeroF1ll;//初始化数据的大小 union { DWORD Characteristics; struct { DWORD Reserved0 : 20; DWORD Alignment : 4; DWORD Reserved1 : 8; }DUMMYSTRUCTNAME; }DUMMYUNIONNAME; }IMAGE_TLS_DIRECTORY32; typedef IMAGE_TLS_DIRECTORY32 * PIMAGE_TLS_DIRECTORY32;
区块表结构:
#define IMAGE_SIZEOF_SHORT_NAME 8 typedef struct _IMAGE_SECTION_HEADER { BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // \0结尾的节的名称 union { DWORD PhysicalAddress; DWORD VirtualSize; /*VirtualSize: 指出实际的,被使用的区块大小,是区块在没有对齐处理前的实际大小,如果VirtualSize大于SizeOfRawData,那么SizeOfRawData是来自 可执行文件初始化数据的大小,与VirtualSize相差的字节用零填充。这个字段在OBJ文件中是被设为0的。*/ } Misc; //节的数据在没有对齐前的尺寸 DWORD VirtualAddress; //该块装载到内存中的RVA 第一个块的默认RVA为1000h /*VirtualAddress:该块装载到内存中的RVA.这个地址是按照内存页对齐的。它的数值总是SectionAlignment的整数倍。在Microsofe工具中,第一个块的 默认RVA为1000h。在OBJ中,该字段没有意义,并被设为0。*/ DWORD SizeOfRawData; //节在文件中对齐后的尺寸。 /*SizeOfRawData:该块在磁盘文件中所占的大小。在可执行文件中,该字段包含经过FileAlignment调整后的块的长度。例如,指定FileAlignment的大小 为200h,如果VirtualSize中的块长度为19Ah个字节,这一块应保存的长度为200h个字节。*/ DWORD PointerToRawData; //节起始数据在文件中的偏移 /*PointerToRawData.该块在磁盘文件中的偏移。程序经编译或汇编生成原始数据,这个字段用于给出原始数据在文件中的偏移。如果程序自装载PE或COFF文件 (不是由操作系统装入),这一字段比VirtualAddress还重要。在这种状态下,必须完全使用线性映象方法装入文件,所有需要在该偏移处找到块的数据,而不是 VirtualAddress字段中的RVA地址。*/ DWORD PointerToRelocations; //在“.obj”文件中使用,指向重定位表的指针 DWORD PointerToLinenumbers; //行号表的位置(调试用) WORD NumberOfRelocations; //重定位表的个数(“.obj”文件) WORD NumberOfLinenumbers; //行号表中行号的数量 DWORD Characteristics; //节的属性 /*Characteristics 节的属性。这个字段很重要,这是节的属性标志字段,其中不同的数据为代表了不同的属性,具体的定义如表3-7所示。这些数据位的组合 描述了内存中的一个节所拥有的访问属性。*/ } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
重定向表结构:
与导入表类似,重定位表指针指向的位置是一个数组,而不像导出表一样只有一个结构。这个数组的每一项都是如下结构:
一页 = 1000h = 4096B =2^12 即一页的地址只需要12位即可表示 也就是1个字 两个字节
n个重定位项 需要2*n个地址+4字节页面起始RVA+4字节的本页重定位项个数
typedef struct _IMAGE_BASE_RELOCATION { ULONG VirtualAddress; //表示上述公式中第一个4 即重定位内存页的起始RVA ULONG SizeOfBlock; //重定位块长度 // USHORT TypeOffset[1]; } IMAGE_BASE_RELOCATION;
PE文件总体结构如下:
(图源网络,侵删)