做者:MSDN
译者:李马算法
Windows NT 3.1引入了一种名为PE文件格式的新可执行文件格式。PE文件格式的规范包含在了MSDN的CD中(Specs and Strategy, Specifications, Windows NT File Format Specifications),可是它很是之晦涩。
然而这一的文档并未提供足够的信息,因此开发者们没法很好地弄懂PE格式。本文旨在解决这一问题,它会对整个的PE文件格式做一个十分完全的解释,另外,本文中还带有对全部必需结构的描述以及示范如何使用这些信息的源码示例。
为了得到PE文件中所包含的重要信息,我编写了一个名为PEFILE.DLL的动态连接库,本文中全部出现的源码示例亦均摘自于此。这个DLL和它的源代码都做为PEFile示例程序的一部分包含在了CD中(译注:示例程序请在MSDN中寻找,本站恕不提供),你能够在你本身的应用程序中使用这个DLL;一样,你亦能够依你所愿地使用并构建它的源码。在本文末尾,你会找到PEFILE.DLL的函数导出列表和一个如何使用它们的说明。我以为你会发现这些函数会让你从容应付PE文件格式的。
介绍
Windows操做系统家族最近增长的Windows NT为开发环境和应用程序自己带来了很大的改变,这之中一个最为重大的当属PE文件格式了。新的PE文件格式主要来自于UNIX操做系统所通用的COFF规范,同时为了保证与旧版本MS-DOS及Windows操做系统的兼容,PE文件格式也保留了MS-DOS中那熟悉的MZ头部。
在本文之中,PE文件格式是以自顶而下的顺序解释的。在你从头开始研究文件内容的过程之中,本文会详细讨论PE文件的每个组成部分。
许多单独的文件成分定义都来自于Microsoft Win32 SDK开发包中的WINNT.H文件,在这个文件中你会发现用来描述文件头部和数据目录等各类成分的结构类型定义。可是,在WINNT.H中缺乏对PE文件结构足够的定义,在这种状况下,我定义了本身的结构来存取文件数据。你会在PEFILE.DLL工程的PEFILE.H中找到这些结构的定义,整套的PEFILE.H开发文件包含在PEFile示例程序之中。
本文配套的示例程序除了PEFILE.DLL示例代码以外,还有一个单独的Win32示例应用程序,名为EXEVIEW.EXE。建立这一示例目的有二:首先,我须要测试PEFILE.DLL的函数,而且某些状况要求我同时查看多个文件;其次,不少解决PE文件格式的工做和直接观看数据有关。例如,要弄懂导入地址名称表是如何构成的,我就得同时查看.idata段头部、导入映像数据目录、可选头部以及当前的.idata段实体,而EXEVIEW.EXE就是查看这些信息的最佳示例。
闲话少叙,让咱们开始吧。
PE文件结构
PE文件格式被组织为一个线性的数据流,它由一个MS-DOS头部开始,接着是一个是模式的程序残余以及一个PE文件标志,这以后紧接着PE文件头和可选头部。这些以后是全部的段头部,段头部以后跟随着全部的段实体。文件的结束处是一些其它的区域,其中是一些混杂的信息,包括重分配信息、符号表信息、行号信息以及字串表数据。我将全部这些成分列于图1。
图1.PE文件映像结构
从MS-DOS文件头结构开始,我将按照PE文件格式各成分的出现顺序依次对其进行讨论,而且讨论的大部分是以示例代码为基础来示范如何得到文件的信息的。全部的源码均摘自PEFILE.DLL模块的PEFILE.C文件。这些示例都利用了Windows NT最酷的特点之一——内存映射文件,这一特点容许用户使用一个简单的指针来存取文件中所包含的数据,所以全部的示例都使用了内存映射文件来存取PE文件中的数据。
注意:请查阅本文末尾关于如何使用PEFILE.DLL的那一段。
MS-DOS头部/实模式头部
如上所述,PE文件格式的第一个组成部分是MS-DOS头部。在PE文件格式中,它并不是一个新概念,由于它与MS-DOS 2.0以来就已有的MS-DOS头部是彻底同样的。保留这个相同结构的最主要缘由是,当你尝试在Windows 3.1如下或MS-DOS 2.0以上的系统下装载一个文件的时候,操做系统可以读取这个文件并明白它是和当前系统不相兼容的。换句话说,当你在MS-DOS 6.0下运行一个Windows NT可执行文件时,你会获得这样一条消息:“This program cannot be run in DOS mode.”若是MS-DOS头部不是做为PE文件格式的第一部分的话,操做系统装载文件的时候就会失败,并提供一些彻底没用的信息,例如:“The name specified is not recognized as an internal or external command, operable program or batch file.”
MS-DOS头部占据了PE文件的头64个字节,描述它内容的结构以下:数组
//WINNT.H typedef struct _IMAGE_DOS_HEADER { // DOS的.EXE头部 USHORT e_magic; // 魔术数字 USHORT e_cblp; // 文件最后页的字节数 USHORT e_cp; // 文件页数 USHORT e_crlc; // 重定义元素个数 USHORT e_cparhdr; // 头部尺寸,以段落为单位 USHORT e_minalloc; // 所需的最小附加段 USHORT e_maxalloc; // 所需的最大附加段 USHORT e_ss; // 初始的SS值(相对偏移量) USHORT e_sp; // 初始的SP值 USHORT e_csum; // 校验和 USHORT e_ip; // 初始的IP值 USHORT e_cs; // 初始的CS值(相对偏移量) USHORT e_lfarlc; // 重分配表文件地址 USHORT e_ovno; // 覆盖号 USHORT e_res[4]; // 保留字 USHORT e_oemid; // OEM标识符(相对e_oeminfo) USHORT e_oeminfo; // OEM信息 USHORT e_res2[10]; // 保留字 LONG e_lfanew; // 新exe头部的文件地址 } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;第一个域e_magic,被称为魔术数字,它被用于表示一个MS-DOS兼容的文件类型。全部MS-DOS兼容的可执行文件都将这个值设为0x5A4D,表示ASCII字符MZ。MS-DOS头部之因此有的时候被称为MZ头部,就是这个缘故。还有许多其它的域对于MS-DOS操做系统来讲都有用,可是对于Windows NT来讲,这个结构中只有一个有用的域——最后一个域e_lfnew,一个4字节的文件偏移量,PE文件头部就是由它定位的。对于Windows NT的PE文件来讲,PE文件头部是紧跟在MS-DOS头部和实模式程序残余以后的。
//PEFILE.H #define NTSIGNATURE(a) ((LPVOID)((BYTE *)a + \ ((PIMAGE_DOS_HEADER)a)->e_lfanew))在处理PE文件信息的时候,我发现文件之中有些位置须要常常查阅。既然这些位置仅仅是对文件的偏移量,那么用宏来实现这些定位就比较容易,由于它们较之函数有更好的表现。
//PEFILE.C DWORD WINAPI ImageFileType (LPVOID lpFile) { /* 首先出现的是DOS文件标志 */ if (*(USHORT *)lpFile == IMAGE_DOS_SIGNATURE) { /* 由DOS头部决定PE文件头部的位置 */ if (LOWORD (*(DWORD *)NTSIGNATURE (lpFile)) == IMAGE_OS2_SIGNATURE || LOWORD (*(DWORD *)NTSIGNATURE (lpFile)) == IMAGE_OS2_SIGNATURE_LE) return (DWORD)LOWORD(*(DWORD *)NTSIGNATURE (lpFile)); else if (*(DWORD *)NTSIGNATURE (lpFile) == IMAGE_NT_SIGNATURE) return IMAGE_NT_SIGNATURE; else return IMAGE_DOS_SIGNATURE; } else /* 不明文件种类 */ return 0; }以上列出的代码当即告诉了你NTSIGNATURE宏有多么有用。对于比较不一样文件类型而且返回一个适当的文件种类来讲,这个宏就会使这两件事变得很是简单。WINNT.H之中定义的四种不一样文件类型有:
//WINNT.H #define IMAGE_DOS_SIGNATURE 0x5A4D // MZ #define IMAGE_OS2_SIGNATURE 0x454E // NE #define IMAGE_OS2_SIGNATURE_LE 0x454C // LE #define IMAGE_NT_SIGNATURE 0x00004550 // PE00首先,Windows的可执行文件类型没有出如今这一列表中,这一点看起来很奇怪。可是,在稍微研究一下以后,就能获得缘由了:除了操做系统版本规范的不一样以外,Windows的可执行文件和OS/2的可执行文件实在没有什么区别。这两个操做系统拥有相同的可执行文件结构。
//PEFILE.C #define PEFHDROFFSET(a) ((LPVOID)((BYTE *)a + \ ((PIMAGE_DOS_HEADER)a)->e_lfanew + \ SIZE_OF_NT_SIGNATURE))这个宏与上一个宏的惟一不一样是这个宏加入了一个常量SIZE_OF_NT_SIGNATURE。不幸的是,这个常量并未定义在WINNT.H之中,因而我将它定义在了PEFILE.H中,它是一个DWORD的大小。
PIMAGE_FILE_HEADER pfh; pfh = (PIMAGE_FILE_HEADER)PEFHDROFFSET(lpFile);在这个例子中,lpFile表示一个指向可执行文件内存映像基地址的指针,这就显出了内存映射文件的好处:不须要执行文件的I/O,只需使用指针pfh就能存取文件中的信息。PE文件头结构被定义为:
//WINNT.H typedef struct _IMAGE_FILE_HEADER { USHORT Machine; USHORT NumberOfSections; ULONG TimeDateStamp; ULONG PointerToSymbolTable; ULONG NumberOfSymbols; USHORT SizeOfOptionalHeader; USHORT Characteristics; } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER; #define IMAGE_SIZEOF_FILE_HEADER 20请注意这个文件头部的大小已经定义在这个包含文件之中了,这样一来,想要获得这个结构的大小就很方便了。可是我以为对结构自己使用sizeof运算符(译注:原文为“function”)更简单一些,由于这样的话我就没必要记住这个常量的名字IMAGE_SIZEOF_FILE_HEADER,而只须要记住结构IMAGE_FILE_HEADER的名字就能够了。另外一方面,记住全部结构的名字已经够有挑战性的了,尤为在是这些结构只有WINNT.H中才有的状况下。
PEFILE.C int WINAPI NumOfSections(LPVOID lpFile) { /* 文件头部中所表示出的段数目 */ return (int)((PIMAGE_FILE_HEADER) PEFHDROFFSET (lpFile))->NumberOfSections); }如你所见,PEFHDROFFSET以及其它宏用起来很是方便。
//PEFILE.H #define OPTHDROFFSET(a) ((LPVOID)((BYTE *)a + \ ((PIMAGE_DOS_HEADER)a)->e_lfanew + \ SIZE_OF_NT_SIGNATURE + \ sizeof(IMAGE_FILE_HEADER)))可选头部包含了不少关于可执行映像的重要信息,例如初始的堆栈大小、程序入口点的位置、首选基地址、操做系统版本、段对齐的信息等等。IMAGE_OPTIONAL_HEADER结构以下:
//WINNT.H typedef struct _IMAGE_OPTIONAL_HEADER { // // 标准域 // USHORT Magic; UCHAR MajorLinkerVersion; UCHAR MinorLinkerVersion; ULONG SizeOfCode; ULONG SizeOfInitializedData; ULONG SizeOfUninitializedData; ULONG AddressOfEntryPoint; ULONG BaseOfCode; ULONG BaseOfData; // // NT附加域 // ULONG ImageBase; ULONG SectionAlignment; ULONG FileAlignment; USHORT MajorOperatingSystemVersion; USHORT MinorOperatingSystemVersion; USHORT MajorImageVersion; USHORT MinorImageVersion; USHORT MajorSubsystemVersion; USHORT MinorSubsystemVersion; ULONG Reserved1; ULONG SizeOfImage; ULONG SizeOfHeaders; ULONG CheckSum; USHORT Subsystem; USHORT DllCharacteristics; ULONG SizeOfStackReserve; ULONG SizeOfStackCommit; ULONG SizeOfHeapReserve; ULONG SizeOfHeapCommit; ULONG LoaderFlags; ULONG NumberOfRvaAndSizes; IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; } IMAGE_OPTIONAL_HEADER, *PIMAGE_OPTIONAL_HEADER;如你所见,这个结构中所列出的域实在是冗长得过度。为了避免让你对全部这些域感到厌烦,我会仅仅讨论有用的——就是说,对于探究PE文件格式而言有用的。
//PEFILE.C LPVOID WINAPI GetModuleEntryPoint(LPVOID lpFile) { PIMAGE_OPTIONAL_HEADER poh; poh = (PIMAGE_OPTIONAL_HEADER)OPTHDROFFSET(lpFile); if (poh != NULL) return (LPVOID)poh->AddressOfEntryPoint; else return NULL; }·BaseOfCode。已载入映像的代码(“.text”段)的相对偏移量。
//WINNT.H // 目录入口 // 导出目录 #define IMAGE_DIRECTORY_ENTRY_EXPORT 0 // 导入目录 #define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // 资源目录 #define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // 异常目录 #define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // 安全目录 #define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // 重定位基本表 #define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // 调试目录 #define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // 描述字串 #define IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // 机器值(MIPS GP) #define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // TLS目录 #define IMAGE_DIRECTORY_ENTRY_TLS 9 // 载入配置目录 #define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10基本上,每一个数据目录都是一个被定义为IMAGE_DATA_DIRECTORY的结构。虽然数据目录入口自己是相同的,可是每一个特定的目录种类倒是彻底惟一的。每一个数据目录的定义在本文的之后部分被描述为“预约义段”。
//WINNT.H typedef struct _IMAGE_DATA_DIRECTORY { ULONG VirtualAddress; ULONG Size; } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;每一个数据目录入口指定了该目录的尺寸和相对虚拟地址。若是你要定义一个特定的目录的话,就须要从可选头部中的数据目录数组中决定相对的地址,而后使用虚拟地址来决定该目录位于哪一个段中。一旦你决定了哪一个段包含了该目录,该段的段头部就会被用于查找数据目录的精确文件偏移量位置。
//WINNT.H #define IMAGE_SIZEOF_SHORT_NAME 8 typedef struct _IMAGE_SECTION_HEADER { UCHAR Name[IMAGE_SIZEOF_SHORT_NAME]; union { ULONG PhysicalAddress; ULONG VirtualSize; } Misc; ULONG VirtualAddress; ULONG SizeOfRawData; ULONG PointerToRawData; ULONG PointerToRelocations; ULONG PointerToLinenumbers; USHORT NumberOfRelocations; USHORT NumberOfLinenumbers; ULONG Characteristics; } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;你如何才能得到一个特定段的段头部信息?既然段头部是被连续的组织起来的,并且没有一个特定的顺序,那么段头部必须由名称来定位。如下的函数示范了如何从一个给定了段名称的PE映像文件中得到一个段头部:
//PEFILE.C BOOL WINAPI GetSectionHdrByName(LPVOID lpFile, IMAGE_SECTION_HEADER *sh, char *szSection) { PIMAGE_SECTION_HEADER psh; int nSections = NumOfSections (lpFile); int i; if ((psh = (PIMAGE_SECTION_HEADER)SECHDROFFSET(lpFile)) != NULL) { /* 由名称查找段 */ for (i = 0; i < nSections; i++) { if (!strcmp(psh->Name, szSection)) { /* 向头部复制数据 */ CopyMemory((LPVOID)sh, (LPVOID)psh, sizeof(IMAGE_SECTION_HEADER)); return TRUE; } else psh++; } } return FALSE; }这个函数经过SECHDROFFSET宏将第一个段头部定位,而后它开始在全部段中循环,并将要寻找的段名称和每一个段的名称相比较,直到找到了正确的那一个为止。当找到了段的时候,函数将内存映像文件的数据复制到传入函数的结构中,而后IMAGE_SECTION_HEADER结构的各域就可以被直接存取了。
// PEFILE.C LPVOID WINAPI ImageDirectoryOffset(LPVOID lpFile, DWORD dwIMAGE_DIRECTORY) { PIMAGE_OPTIONAL_HEADER poh; PIMAGE_SECTION_HEADER psh; int nSections = NumOfSections(lpFile); int i = 0; LPVOID VAImageDir; /* 必须为0到(NumberOfRvaAndSizes-1)之间 */ if (dwIMAGE_DIRECTORY >= poh->NumberOfRvaAndSizes) return NULL; /* 得到可选头部和段头部的偏移量 */ poh = (PIMAGE_OPTIONAL_HEADER)OPTHDROFFSET(lpFile); psh = (PIMAGE_SECTION_HEADER)SECHDROFFSET(lpFile); /* 定位映像目录的相对虚拟地址 */ VAImageDir = (LPVOID)poh->DataDirectory [dwIMAGE_DIRECTORY].VirtualAddress; /* 定位包含映像目录的段 */ while (i++ < nSections) { if (psh->VirtualAddress <= (DWORD)VAImageDir && psh->VirtualAddress + psh->SizeOfRawData > (DWORD)VAImageDir) break; psh++; } if (i > nSections) return NULL; /* 返回映像导入目录的偏移量 */ return (LPVOID)(((int)lpFile + (int)VAImageDir. psh->VirtualAddress) + (int)psh->PointerToRawData); }
该函数首先确认被请求的数据目录入口数字,而后它分别获取指向可选头部和第一个段头部的两个指针。它从可选头部决定数据目录的虚拟地址,而后它使用这个值来决定数据目录定位在哪一个段实体之中。若是适当的段实体已经被标识了,那么数据目录特定的位置就能够经过将它的相对虚拟地址转换为文件中地址的方法来找到。
缓存
预约义段
一个Windows NT的应用程序典型地拥有9个预约义段,它们是.text、.bss、.rdata、.data、.rsrc、.edata、.idata、.pdata和.debug。一些应用程序不须要全部的这些段,一样还有一些应用程序为了本身特殊的须要而定义了更多的段。这种作法与MS-DOS和Windows 3.1中的代码段和数据段类似。事实上,应用程序定义一个独特的段的方法是使用标准编译器来指示对代码段和数据段的命名,或者使用名称段编译器选项-NT——就和Windows 3.1中应用程序定义独特的代码段和数据段同样。
如下是一个关于Windows NT PE文件之中一些有趣的公共段的讨论。
可执行代码段,.text
Windows 3.1和Windows NT之间的一个区别就是Windows NT默认的作法是将全部的代码段(正如它们在Windows 3.1中所提到的那样)组成了一个单独的段,名为“.text”。既然Windows NT使用了基于页面的虚拟内存管理系统,那么将分开的代码放入不一样的段之中的作法就不太明智了。所以,拥有一个大的代码段对于操做系统和应用程序开发者来讲,都是十分方便的。
.text段也包含了早先提到过的入口点。IAT亦存在于.text段之中的模块入口点以前。(IAT在.text段之中的存在很是有意义,由于这个表事实上是一系列的跳转指令,而且它们的跳转目标位置是已固定的地址。)当Windows NT的可执行映像装载入进程的地址空间时,IAT就和每个导入函数的物理地址一同肯定了。要在.text段之中查找IAT,装载器只用将模块的入口点定位,而IAT偏偏出现于入口点以前。既然每一个入口拥有相同的尺寸,那么向后退查找这个表的起始位置就很容易了。
数据段,.bss、.rdata、.data
.bss段表示应用程序的未初始化数据,包括全部函数或源模块中声明为static的变量。
.rdata段表示只读的数据,好比字符串文字量、常量和调试目录信息。
全部其它变量(除了出如今栈上的自动变量)存储在.data段之中。基本上,这些是应用程序或模块的全局变量。
资源段,.rsrc
.rsrc段包含了模块的资源信息。它起始于一个资源目录结构,这个结构就像其它大多数结构同样,可是它的数据被更进一步地组织在了一棵资源树之中。如下的IMAGE_RESOURCE_DIRECTORY结构造成了这棵树的根和各个结点。安全
//WINNT.H typedef struct _IMAGE_RESOURCE_DIRECTORY { ULONG Characteristics; ULONG TimeDateStamp; USHORT MajorVersion; USHORT MinorVersion; USHORT NumberOfNamedEntries; USHORT NumberOfIdEntries; } IMAGE_RESOURCE_DIRECTORY, *PIMAGE_RESOURCE_DIRECTORY;
请看这个目录结构,你将会发现其中居然没有指向下一个结点的指针。可是,在这个结构中有两个域NumberOfNamedEntries和NumberOfIdEntries代替了指针,它们被用来表示这个目录附有多少入口。附带说一句,个人意思是目录入口就在段数据之中的目录后边。有名称的入口按字母升序出现,再日后是按数值升序排列的ID入口。
一个目录入口由两个域组成,正以下面IMAGE_RESOURCE_DIRECTORY_ENTRY结构所描述的那样:app
// WINNT.H typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY { ULONG Name; ULONG OffsetToData; } IMAGE_RESOURCE_DIRECTORY_ENTRY, *PIMAGE_RESOURCE_DIRECTORY_ENTRY;
根据树的层级不一样,这两个域也就有着不一样的用途。Name域被用于标识一个资源种类,或者一种资源名称,或者一个资源的语言ID。OffsetToData与经常被用来在树之中指向兄弟结点——即一个目录结点或一个叶子结点。
叶子结点是资源树之中最底层的结点,它们定义了当前资源数据的尺寸和位置。IMAGE_RESOURCE_DATA_ENTRY结构被用于描述每一个叶子结点:less
// WINNT.H typedef struct _IMAGE_RESOURCE_DATA_ENTRY { ULONG OffsetToData; ULONG Size; ULONG CodePage; ULONG Reserved; } IMAGE_RESOURCE_DATA_ENTRY, *PIMAGE_RESOURCE_DATA_ENTRY;
OffsetToData和Size这两个域表示了当前资源数据的位置和尺寸。既然这一信息主要是在应用程序装载之后由函数使用的,那么将OffsetToData做为一个相对虚拟的地址会更有意义一些。——幸甚,刚好是这样没错。很是有趣的是,全部其它的偏移量,好比从目录入口到其它目录的指针,都是相对于根结点位置的偏移量。
要更清楚地了解这些内容,请参考图2。
图2.一个简单的资源树结构
图2描述了一个很是简单的资源树,它包含了仅仅两个资源对象:一个菜单和一个字串表。更深一层地来讲,它们各自都有一个子项。然而,你仍然能够看到资源树有多么复杂——即便它像这个同样只有一点点资源。
在树的根部,第一个目录有一个文件中包含的全部资源种类的入口,而无论资源种类有多少。在图2中,有两个由树根标识的入口,一个是菜单的,另外一个是字串表的。若是文件中拥有一个或多个对话框资源,那么根结点会再拥有一个入口,所以,就有了对话框资源的另外一个分支。
WINUSER.H中标识了基本的资源种类,我将它们列到了下面:函数
//WINUSER.H /* * 预约义的资源种类 */ #define RT_CURSOR MAKEINTRESOURCE(1) #define RT_BITMAP MAKEINTRESOURCE(2) #define RT_ICON MAKEINTRESOURCE(3) #define RT_MENU MAKEINTRESOURCE(4) #define RT_DIALOG MAKEINTRESOURCE(5) #define RT_STRING MAKEINTRESOURCE(6) #define RT_FONTDIR MAKEINTRESOURCE(7) #define RT_FONT MAKEINTRESOURCE(8) #define RT_ACCELERATOR MAKEINTRESOURCE(9) #define RT_RCDATA MAKEINTRESOURCE(10) #define RT_MESSAGETABLE MAKEINTRESOURCE(11)
在树的第一层级,以上列出的MAKEINTRESOURCE值被放置在每一个种类入口的Name处,它标识了不一样的资源种类。
每一个根目录的入口都指向了树中第二层级的一个兄弟结点,这些结点也是目录,而且每一个都拥有它们本身的入口。在这一层级,目录被用来以给定的种类标识每个资源种类。若是你的应用程序中有多个菜单,那么树中的第二层级会为每一个菜单都准备一个入口。
你可能意识到了,资源能够由名称或整数标识。在这一层级,它们是经过目录结构的Name域来分辨的。若是若是Name域最重要的位被设置了,那么其它的31个位就会被用做一个到IMAGE_RESOURCE_DIR_STRING_U结构的偏移量。测试
// WINNT.H typedef struct _IMAGE_RESOURCE_DIR_STRING_U { USHORT Length; WCHAR NameString[1]; } IMAGE_RESOURCE_DIR_STRING_U, *PIMAGE_RESOURCE_DIR_STRING_U;
这个结构仅仅是由一个2字节长的Length域和一个UNICODE字符Length组成的。
另外一方面,若是Name域最重要的位被清空,那么它的低31位就被用于表示资源的整数ID。图2示范的就是菜单资源做为一个命名的资源,以及字串表做为一个ID资源。
若是有两个菜单资源,一个由名称标识,另外一个由资源标识,那么它们两者就会在菜单资源目录以后拥有两个入口。有名称的资源入口在第一位,以后是由整数标识的资源。目录域NumberOfNamedEntries和NumberOfIdEntries将各自包含值1,表示当前的1个入口。
在第二层级的下面,资源树就再也不更深一步地扩展分支了。第一层级分支至表示每一个资源种类的目录中,第二层级分支至由标识符表示的每一个资源的目录中,第三层级是被个别标识的资源与它们各自的语言ID之间一对一的映射。要表示一个资源的语言ID,目录入口结构的Name域就被用来表示资源的主语言ID和子语言ID了。Windows NT的Win32 SDK开发包中列出了默认的值资源,例如对于0x0409这个值来讲,0x09表示主语言LANG_ENGLISH,0x04则被定义为子语言的SUBLANG_ENGLISH_CAN。全部的语言ID值都定义于Windows NT Win32 SDK开发包的文件WINNT.H中。
既然语言ID结点是树中最后的目录结点,那么入口结构的OffsetToData域就是到一个叶子结点(即前面提到过的IMAGE_RESOURCE_DATA_ENTRY结构)的偏移量。
再回过头来参考图2,你会发现每一个语言目录入口都对应着一个数据入口。这个结点仅仅表示了资源数据的尺寸以及资源数据的相对虚拟地址。
在资源数据段(.rsrc)之中拥有这么多结构有一个好处,就是你能够不存取资源自己而直接能够从这个段收集不少信息。例如,你能够得到有多少种资源、哪些资源(若是有的话)使用了特别的语言ID、特定的资源是否存在以及单独种类资源的尺寸。为了示范如何利用这一信息,如下的函数说明了如何决定一个文件中包含的不一样种类的资源:ui
// PEFILE.C int WINAPI GetListOfResourceTypes(LPVOID lpFile, HANDLE hHeap, char **pszResTypes) { PIMAGE_RESOURCE_DIRECTORY prdRoot; PIMAGE_RESOURCE_DIRECTORY_ENTRY prde; char *pMem; int nCnt, i; /* 得到资源树的根目录 */ if ((prdRoot = (PIMAGE_RESOURCE_DIRECTORY)ImageDirectoryOffset (lpFile, IMAGE_DIRECTORY_ENTRY_RESOURCE)) == NULL) return 0; /* 在堆上分配足够的空间来包括全部类型 */ nCnt = prdRoot->NumberOfIdEntries * (MAXRESOURCENAME + 1); *pszResTypes = (char *)HeapAlloc(hHeap, HEAP_ZERO_MEMORY, nCnt); if ((pMem = *pszResTypes) == NULL) return 0; /* 将指针指向第一个资源种类的入口 */ prde = (PIMAGE_RESOURCE_DIRECTORY_ENTRY)((DWORD)prdRoot + sizeof (IMAGE_RESOURCE_DIRECTORY)); /* 在全部的资源目录入口类型中循环 */ for (i = 0; i < prdRoot->NumberOfIdEntries; i++) { if (LoadString(hDll, prde->Name, pMem, MAXRESOURCENAME)) pMem += strlen(pMem) + 1; prde++; } return nCnt; }
这个函数将一个资源种类名称的列表写入了由pszResTypes标识的变量中。请注意,在这个函数的核心部分,LoadString是使用各自资源种类目录入口的Name域来做为字符串ID的。若是你查看PEFILE.RC,你会发现我定义了一系列的资源种类的字符串,而且它们的ID与它们在目录入口中的定义彻底相同。PEFILE.DLL还有有一个函数,它返回了.rsrc段中的资源对象总数。这样一来,从这个段中提取其它的信息,借助这些函数或另外编写函数就方便多了。
导出数据段,.edata
.edata段包含了应用程序或DLL的导出数据。在这个段出现的时候,它会包含一个到达导出信息的导出目录。spa
// WINNT.H typedef struct _IMAGE_EXPORT_DIRECTORY { ULONG Characteristics; ULONG TimeDateStamp; USHORT MajorVersion; USHORT MinorVersion; ULONG Name; ULONG Base; ULONG NumberOfFunctions; ULONG NumberOfNames; PULONG *AddressOfFunctions; PULONG *AddressOfNames; PUSHORT *AddressOfNameOrdinals; } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
导出目录中的Name域标识了可执行模块的名称。NumberOfFunctions域和NumberOfNames域表示模块中有多少导出的函数以及这些函数的名称。
AddressOfFunctions域是一个到导出函数入口列表的偏移量。AddressOfNames域是到一个导出函数名称列表起始处偏移量的地址,这个列表是由null分隔的。AddressOfNameOrdinals是一个到相同导出函数顺序值(每一个值2字节长)列表的偏移量。
三个AddressOf...域是当模块装载时进程地址空间中的相对虚拟地址。一旦模块被装载,那么要得到进程地质空间中的确切地址的话,就应该在相对虚拟地址上加上模块的基地址。但是,在文件被装载前,仍然能够决定这一地址:只要从给定的域地址中减去段头部的虚拟地址(VirtualAddress),再加上段实体的偏移量(PointerToRawData),这个结果就是映像文件中的偏移量了。如下的例子解说了这一技术:
// PEFILE.C int WINAPI GetExportFunctionNames(LPVOID lpFile, HANDLE hHeap, char **pszFunctions) { IMAGE_SECTION_HEADER sh; PIMAGE_EXPORT_DIRECTORY ped; char *pNames, *pCnt; int i, nCnt; /* 得到.edata域中的段头部和指向数据目录的指针 */ if ((ped = (PIMAGE_EXPORT_DIRECTORY)ImageDirectoryOffset (lpFile, IMAGE_DIRECTORY_ENTRY_EXPORT)) == NULL) return 0; GetSectionHdrByName (lpFile, &sh, ".edata"); /* 决定导出函数名称的偏移量 */ pNames = (char *)(*(int *)((int)ped->AddressOfNames - (int)sh.VirtualAddress + (int)sh.PointerToRawData + (int)lpFile) - (int)sh.VirtualAddress + (int)sh.PointerToRawData + (int)lpFile); /* 计算出要为全部的字符串分配多少内存 */ pCnt = pNames; for (i = 0; i < (int)ped->NumberOfNames; i++) while (*pCnt++); nCnt = (int)(pCnt.pNames); /* 在堆上为函数名称分配内存 */ *pszFunctions = HeapAlloc (hHeap, HEAP_ZERO_MEMORY, nCnt); /* 将全部字符串复制到缓冲区 */ CopyMemory((LPVOID)*pszFunctions, (LPVOID)pNames, nCnt); return nCnt; }
请注意,在这个函数之中,变量pNames是由决定偏移量地址和当前偏移量位置的方法来赋值的。偏移量的地址和偏移量自己都是相对虚拟地址,所以在使用以前必须进行转换——函数之中体现了这一点。虽然你能够编写一个相似的函数来决定顺序值或函数入口点,可是我为何不为你作好呢?——GetNumberOfExportedFunctions、GetExportFunctionEntryPoints和GetExportFunctionOrdinals已经存在于PEFILE.DLL之中了。
导入数据段,.idata
.idata段是导入数据,包括导入库和导入地址名称表。虽然定义了IMAGE_DIRECTORY_ENTRY_IMPORT,可是WINNT.H之中并没有相应的导入目录结构。做为代替,其中有若干其它的结构,名为IMAGE_IMPORT_BY_NAME、IMAGE_THUNK_DATA与IMAGE_IMPORT_DESCRIPTOR。在我我的看来,我实在不知道这些结构是如何和.idata段发生关联的,因此我花了若干个小时来破译.idata段实体而且获得了一个更简单的结构,我名之为IMAGE_IMPORT_MODULE_DIRECTORY。
// PEFILE.H typedef struct tagImportDirectory { DWORD dwRVAFunctionNameList; DWORD dwUseless1; DWORD dwUseless2; DWORD dwRVAModuleName; DWORD dwRVAFunctionAddressList; } IMAGE_IMPORT_MODULE_DIRECTORY, *PIMAGE_IMPORT_MODULE_DIRECTORY;
和其它段的数据目录不一样的是,这个是做为文件中的每一个导入模块重复出现的。你能够将它看做模块数据目录列表中的一个入口,而不是一个整个数据段的数据目录。每一个入口都是一个指向特定模块导入信息的目录。
IMAGE_IMPORT_MODULE_DIRECTORY结构中的一个域dwRVAModuleName是一个相对虚拟地址,它指向模块的名称。结构中还有两个dwUseless参数,它们是为了保持段的对齐。PE文件格式规范提到了一些东西,关于导入标记、时间/日期标志以及主/次版本,可是在个人实验中,这两个域自始而终都是空的,因此我仍然认为它们没有什么用处。
基于这个结构的定义,你即可以得到可执行文件中导入的全部模块和函数名称了。如下的函数示范了如何得到特定的PE文件中的全部导入函数名称:
//PEFILE.C int WINAPI GetImportModuleNames(LPVOID lpFile, HANDLE hHeap, char **pszModules) { PIMAGE_IMPORT_MODULE_DIRECTORY pid; IMAGE_SECTION_HEADER idsh; BYTE *pData; int nCnt = 0, nSize = 0, i; char *pModule[1024]; char *psz; pid = (PIMAGE_IMPORT_MODULE_DIRECTORY)ImageDirectoryOffset (lpFile, IMAGE_DIRECTORY_ENTRY_IMPORT); pData = (BYTE *)pid; /* 定位.idata段头部 */ if (!GetSectionHdrByName(lpFile, &idsh, ".idata")) return 0; /* 提取全部导入模块 */ while (pid->dwRVAModuleName) { /* 为绝对字符串偏移量分配缓冲区 */ pModule[nCnt] = (char *)(pData + (pid->dwRVAModuleName-idsh.VirtualAddress)); nSize += strlen(pModule[nCnt]) + 1; /* 增至下一个导入目录入口 */ pid++; nCnt++; } /* 将全部字符串赋值到一大块的堆内存中 */ *pszModules = HeapAlloc(hHeap, HEAP_ZERO_MEMORY, nSize); psz = *pszModules; for (i = 0; i < nCnt; i++) { strcpy(psz, pModule[i]); psz += strlen (psz) + 1; } return nCnt; }
这个函数很是好懂,然而有一点值得指出——注意while循环。这个循环当pid->dwRVAModuleName为0的时候终止,这就暗示了在IMAGE_IMPORT_MODULE_DIRECTORY结构列表的末尾有一个空的结构,这个结构拥有一个0值,至少dwRVAModuleName域为0。这即是我在对文件的实验中以及以后在PE文件格式中研究的行为。
这个结构中的第一个域dwRVAFunctionNameList是一个相对虚拟地址,这个地址指向一个相对虚拟地址的列表,这些地址是文件中的一些文件名。以下面的数据所示,全部导入模块的模块和函数名称都列于.idata段数据中了:
E6A7 0000 F6A7 0000 08A8 0000 1AA8 0000 ................ 28A8 0000 3CA8 0000 4CA8 0000 0000 0000 (...<...L....... 0000 4765 744F 7065 6E46 696C 654E 616D ..GetOpenFileNam 6541 0000 636F 6D64 6C67 3332 2E64 6C6C eA..comdlg32.dll 0000 2500 4372 6561 7465 466F 6E74 496E ..%.CreateFontIn 6469 7265 6374 4100 4744 4933 322E 646C directA.GDI32.dl 6C00 A000 4765 7444 6576 6963 6543 6170 l...GetDeviceCap 7300 C600 4765 7453 746F 636B 4F62 6A65 s...GetStockObje 6374 0000 D500 4765 7454 6578 744D 6574 ct....GetTextMet 7269 6373 4100 1001 5365 6C65 6374 4F62 ricsA...SelectOb 6A65 6374 0000 1601 5365 7442 6B43 6F6C ject....SetBkCol 6F72 0000 3501 5365 7454 6578 7443 6F6C or..5.SetTextCol 6F72 0000 4501 5465 7874 4F75 7441 0000 or..E.TextOutA..
以上的数据是EXEVIEW.EXE示例程序.idata段的一部分。这个特别的段表示了导入模块列表和函数名称列表的起始处。若是你开始检查数据中的这个段,你应该认出一些熟悉的Win32 API函数以及模块名称。从上往下读的话,你能够找到GetOpenFileNameA,紧接着是COMDLG32.DLL。而后你能发现CreateFontIndirectA,紧接着是模块GDI32.DLL,以及以后的GetDeviceCaps、GetStockObject、GetTextMetrics等等。
这样的式样会在.idata段中重复出现。第一个模块是COMDLG32.DLL,第二个是GDI32.DLL。请注意第一个模块只导出了一个函数,而第二个模块导出了不少函数。在这两种状况下,函数和模块的排列的方法是首先出现一个函数名,以后是模块名,而后是其它的函数名(若是有的话)。
如下的函数示范了如何得到指定模块的全部函数名。
// PEFILE.C int WINAPI GetImportFunctionNamesByModule(LPVOID lpFile, HANDLE hHeap, char *pszModule, char **pszFunctions) { PIMAGE_IMPORT_MODULE_DIRECTORY pid; IMAGE_SECTION_HEADER idsh; DWORD dwBase; int nCnt = 0, nSize = 0; DWORD dwFunction; char *psz; /* 定位.idata段的头部 */ if (!GetSectionHdrByName(lpFile, &idsh, ".idata")) return 0; pid = (PIMAGE_IMPORT_MODULE_DIRECTORY)ImageDirectoryOffset (lpFile, IMAGE_DIRECTORY_ENTRY_IMPORT); dwBase = ((DWORD)pid. idsh.VirtualAddress); /* 查找模块的pid */ while (pid->dwRVAModuleName && strcmp (pszModule, (char *)(pid->dwRVAModuleName+dwBase))) pid++; /* 若是模块未找到,就退出 */ if (!pid->dwRVAModuleName) return 0; /* 函数的总数和字符串长度 */ dwFunction = pid->dwRVAFunctionNameList; while (dwFunction && *(DWORD *)(dwFunction + dwBase) && *(char *)((*(DWORD *)(dwFunction + dwBase)) + dwBase+2)) { nSize += strlen ((char *)((*(DWORD *)(dwFunction + dwBase)) + dwBase+2)) + 1; dwFunction += 4; nCnt++; } /* 在堆上分配函数名称的空间 */ *pszFunctions = HeapAlloc (hHeap, HEAP_ZERO_MEMORY, nSize); psz = *pszFunctions; /* 向内存指针复制函数名称 */ dwFunction = pid->dwRVAFunctionNameList; while (dwFunction && *(DWORD *)(dwFunction + dwBase) && *((char *)((*(DWORD *)(dwFunction + dwBase)) + dwBase+2))) { strcpy (psz, (char *)((*(DWORD *)(dwFunction + dwBase)) + dwBase+2)); psz += strlen((char *)((*(DWORD *)(dwFunction + dwBase))+ dwBase+2)) + 1; dwFunction += 4; } return nCnt; }
就像GetImportModuleNames函数同样,这一函数依靠每一个信息列表的末端来得到一个置零的入口。这在种状况下,函数名称列表就是以零结尾的。
最后一个域dwRVAFunctionAddressList是一个相对虚拟地址,它指向一个虚拟地址表。在文件装载的时候,这个虚拟地址表会被装载器置于段数据之中。可是在文件装载前,这些虚拟地址会被一些严密符合函数名称列表的虚拟地址替换。因此在文件装载以前,有两个一样的虚拟地址列表,它们指向导入函数列表。
调试信息段,.debug
调试信息位于.debug段之中,同时PE文件格式也支持单独的调试文件(一般由.DBG扩展名标识)做为一种将调试信息集中的方法。调试段包含了调试信息,可是调试目录却位于早先提到的.rdata段之中。这其中每一个目录都涉及了.debug段之中的调试信息。调试目录的结构IMAGE_DEBUG_DIRECTORY被定义为:
// WINNT.H typedef struct _IMAGE_DEBUG_DIRECTORY { ULONG Characteristics; ULONG TimeDateStamp; USHORT MajorVersion; USHORT MinorVersion; ULONG Type; ULONG SizeOfData; ULONG AddressOfRawData; ULONG PointerToRawData; } IMAGE_DEBUG_DIRECTORY, *PIMAGE_DEBUG_DIRECTORY;
这个段被分为单独的部分,每一个部分为不一样种类的调试信息数据。对于每一个部分来讲都是一个像上边同样的调试目录。不一样的调试信息种类以下:
// WINNT.H #define IMAGE_DEBUG_TYPE_UNKNOWN 0 #define IMAGE_DEBUG_TYPE_COFF 1 #define IMAGE_DEBUG_TYPE_CODEVIEW 2 #define IMAGE_DEBUG_TYPE_FPO 3 #define IMAGE_DEBUG_TYPE_MISC 4
每一个目录之中的Type域表示该目录的调试信息种类。如你所见,在上边的表中,PE文件格式支持不少不一样的调试信息种类,以及一些其它的信息域。对于那些来讲,IMAGE_DEBUG_TYPE_MISC信息是惟一的。这一信息被添加到描述可执行映像的混杂信息之中,这些混杂信息不能被添加到PE文件格式任何结构化的数据段之中。这就是映像文件中最合适的位置,映像名称则确定会出如今这里。若是映像导出了信息,那么导出数据段也会包含这一映像名称。
每种调试信息都拥有本身的头部结构,该结构定义了它本身的数据。这些结构都列于WINNT.H之中。关于IMAGE_DEBUG_DIRECTORY一件有趣的事就是它包括了两个标识调试信息的域。第一个是AddressOfRawData,为相对文件装载的数据虚拟地址;另外一个是PointerToRawData,为数据所在PE文件之中的实际偏移量。这就使得定位指定的调试信息至关容易了。
做为最后的例子,请你考虑如下的函数代码,它从IMAGE_DEBUG_MISC结构中提取了映像名称。
//PEFILE.C int WINAPI RetrieveModuleName(LPVOID lpFile, HANDLE hHeap, char **pszModule) { PIMAGE_DEBUG_DIRECTORY pdd; PIMAGE_DEBUG_MISC pdm = NULL; int nCnt; if (!(pdd = (PIMAGE_DEBUG_DIRECTORY)ImageDirectoryOffset(lpFile, IMAGE_DIRECTORY_ENTRY_DEBUG))) return 0; while (pdd->SizeOfData) { if (pdd->Type == IMAGE_DEBUG_TYPE_MISC) { pdm = (PIMAGE_DEBUG_MISC)((DWORD)pdd->PointerToRawData + (DWORD)lpFile); nCnt = lstrlen(pdm->Data) * (pdm->Unicode ? 2 : 1); *pszModule = (char *)HeapAlloc(hHeap, HEAP_ZERO_MEMORY, nCnt+1); CopyMemory(*pszModule, pdm->Data, nCnt); break; } pdd ++; } if (pdm != NULL) return nCnt; else return 0; }
你看到了,调试目录结构使得定位一个特定种类的调试信息变得相对容易了些。只要定位了IMAGE_DEBUG_MISC结构,提取映像名称就如同调用CopyMemory函数同样简单。
如上所述,调试信息能够被剥离到单独的.DBG文件中。Windows NT SDK包含了一个名为REBASE.EXE的程序能够实现这一目的。例如,如下的语句能够将一个名为TEST.EXE的调试信息剥离:
rebase -b 40000 -x c:\samples\testdir test.exe
调试信息被置于一个新的文件中,这个文件名为TEST.DBG,位于c:\samples\testdir之中。这个文件起始于一个单独的IMAGE_SEPARATE_DEBUG_HEADER结构,接着是存在于原可执行映像之中的段头部的一份拷贝。在段头部以后,是.debug段的数据。也就是说,在段头部以后,就是一系列的IMAGE_DEBUG_DIRECTORY结构及其相关的数据了。调试信息自己保留了如上所描述的常规映像文件调试信息。
PE文件格式总结
Windows NT的PE文件格式向熟悉Windows和MS-DOS环境的开发者引入了一种全新的结构。然而熟悉UNIX环境的开发者会发现PE文件格式与COFF规范很相像(若是它不是以COFF为基础的话)。
整个格式的组成:一个MS-DOS的MZ头部,以后是一个实模式的残余程序、PE文件标志、PE文件头部、PE可选头部、全部的段头部,最后是全部的段实体。
可选头部的末尾是一个数据目录入口的数组,这些相对虚拟地址指向段实体之中的数据目录。每一个数据目录都表示了一个特定的段实体数据是如何组织的。
PE文件格式有11个预约义段,这是对Windows NT应用程序所通用的,可是每一个应用程序能够为它本身的代码以及数据定义它本身独特的段。
.debug预约义段也能够分离为一个单独的调试文件。若是这样的话,就会有一个特定的调试头部来用于解析这个调试文件,PE文件中也会有一个标志来表示调试数据被分离了出去。
PEFILE.DLL函数描述
PEFILE.DLL主要由一些函数组成,这些函数或者被用来得到一个给定的PE文件中的偏移量,或者被用来把文件中的一些数据复制到一个特定的结构中去。每一个函数都有一个需求——第一个参数是一个指针,这个指针指向PE文件的起始处。也就是说,这个文件必须首先被映射到你进程的地址空间中,而后映射文件的位置就能够做为每一个函数第一个参数的lpFile的值来传入了。
我意在使函数的名称使你可以一见而知其意,而且每一个函数都随一个详细描述其目的的注释而列出。若是在读完函数列表以后,你仍然不明白某个函数的功能,那么请参考EXEVIEW.EXE示例来查明这个函数是如何使用的。如下的函数原型列表能够在PEFILE.H中找到:
// PEFILE.H /* 得到指向MS-DOS MZ头部的指针 */ BOOL WINAPI GetDosHeader(LPVOID, PIMAGE_DOS_HEADER); /* 决定.EXE文件的类型 */ DWORD WINAPI ImageFileType(LPVOID); /* 得到指向PE文件头部的指针 */ BOOL WINAPI GetPEFileHeader(LPVOID, PIMAGE_FILE_HEADER); /* 得到指向PE可选头部的指针 */ BOOL WINAPI GetPEOptionalHeader(LPVOID, PIMAGE_OPTIONAL_HEADER); /* 返回模块入口点的地址 */ LPVOID WINAPI GetModuleEntryPoint(LPVOID); /* 返回文件中段的总数 */ int WINAPI NumOfSections(LPVOID); /* 返回当可执行文件被装载入进程地址空间时的首选基地址 */ LPVOID WINAPI GetImageBase(LPVOID); /* 决定文件中一个特定的映像数据目录的位置 */ LPVOID WINAPI ImageDirectoryOffset(LPVOID, DWORD); /* 得到文件中全部段的名称 */ int WINAPI GetSectionNames(LPVOID, HANDLE, char **); /* 复制一个特定段的头部信息 */ BOOL WINAPI GetSectionHdrByName(LPVOID, PIMAGE_SECTION_HEADER, char *); /* 得到由空字符分隔的导入模块名称列表 */ int WINAPI GetImportModuleNames(LPVOID, HANDLE, char **); /* 得到一个模块由空字符分隔的导入函数列表 */ int WINAPI GetImportFunctionNamesByModule(LPVOID, HANDLE, char *, char **); /* 得到由空字符分隔的导出函数列表 */ int WINAPI GetExportFunctionNames(LPVOID, HANDLE, char **); /* 得到导出函数总数 */ int WINAPI GetNumberOfExportedFunctions(LPVOID); /* 得到导出函数的虚拟地址入口点列表 */ LPVOID WINAPI GetExportFunctionEntryPoints(LPVOID); /* 得到导出函数顺序值列表 */ LPVOID WINAPI GetExportFunctionOrdinals(LPVOID); /* 决定资源对象的种类 */ int WINAPI GetNumberOfResources (LPVOID); /* 返回文件中所使用的全部资源对象的种类 */ int WINAPI GetListOfResourceTypes(LPVOID, HANDLE, char **); /* 决定调试信息是否已从文件中分离 */ BOOL WINAPI IsDebugInfoStripped(LPVOID); /* 得到映像文件名称 */ int WINAPI RetrieveModuleName(LPVOID, HANDLE, char **); /* 决定文件是不是一个有效的调试文件 */ BOOL WINAPI IsDebugFile(LPVOID); /* 从调试文件中返回调试头部 */ BOOL WINAPI GetSeparateDebugHeader(LPVOID, PIMAGE_SEPARATE_DEBUG_HEADER); 除了以上所列的函数以外,本文中早先提到的宏也定义在了PEFILE.H中,完整的列表以下: /* PE文件标志的偏移量 */ #define NTSIGNATURE(a) ((LPVOID)((BYTE *)a + \ ((PIMAGE_DOS_HEADER)a)->e_lfanew)) /* MS操做系统头部标识了双字的NT PE文件标志;PE文件头部就紧跟在这个双字以后 */ #define PEFHDROFFSET(a) ((LPVOID)((BYTE *)a + \ ((PIMAGE_DOS_HEADER)a)->e_lfanew + \ SIZE_OF_NT_SIGNATURE)) /* PE可选头部紧跟在PE文件头部以后 */ #define OPTHDROFFSET(a) ((LPVOID)((BYTE *)a + \ ((PIMAGE_DOS_HEADER)a)->e_lfanew + \ SIZE_OF_NT_SIGNATURE + \ sizeof(IMAGE_FILE_HEADER))) /* 段头部紧跟在PE可选头部以后 */ #define SECHDROFFSET(a) ((LPVOID)((BYTE *)a + \ ((PIMAGE_DOS_HEADER)a)->e_lfanew + \ SIZE_OF_NT_SIGNATURE + \ sizeof(IMAGE_FILE_HEADER) + \ sizeof(IMAGE_OPTIONAL_HEADER)))
要使用PEFILE.DLL,你只用包含PEFILE.H文件并在应用程序中连接到这个DLL便可。全部的这些函数都是互斥性的函数,可是有些函数的功能能够相互支持以得到文件信息。例如,GetSectionNames能够用于得到全部段的名称,这样一来,为了得到一个拥有独特段名称(在编译期由应用程序开发者定义的)的段头部,你就须要首先得到全部名称的列表,而后再对那个准确的段名称调用函数GetSectionHeaderByName了。如今,你能够享受我为你带来的这一切了!