探索PE文件内幕——Win32可移植可执行文件格式之旅作者:MattPietrek一种操作系统之上的可执行文件格式从许多方面反映了这种操作系统本身的情况。尽管研究可执行文件格式并不是大多数程序员的首要任务,但从中你却能够获得许多知识。在本文中,我要带你游历可移植可执行(PortableExecutable,PE)文件格式,它是Microsoft设计用于所有基于Win32®的系统,包括WindowsNT®、Win32s™以及Windows95之上的可执行文件格式。在可预见的将来,PE格式会在Microsoft的所有操作系统中扮演重要角色,其中包括Windows2000。如果你使用过Win32s或WindowsNT,那么你已经使用过PE文件了。即使你只是使用VisualC++®为Windows3.1编写程序,你也使用了PE文件(VisualC++的32位MS-DOS®扩展组件使用这种格式)。简而言之,PE格式已经深入到各种系统以及系统的各个角落,在将来不可避免要碰到它。现在是找出这种新型的可执行文件格式到底给操作系统带来了什么好处的时候了。我并不是让你从无穷无尽的十六进制数据的角度去研究PE文件格式,也不是让你记住整页整页的PE文件中各个位的含义。相反,我要向你呈现嵌入在PE文件格式中的内容以及它们与你日常工作之间的关系。例如下面的语句中涉及到的线程局部变量的概念:__declspec(thread)inti;曾经几乎让我发疯,直到我看到它在可执行文件中是如何简洁优美地被实现的。由于你们中许多人都来自16位的Windows,所以我会把Win32PE文件格式的结构与16位NE文件格式中等价的内容作一比较。除了不同的可执行文件格式之外,Microsoft也在他的编译器和汇编程序生成的目标文件中使用了新的格式。这种新的OBJ文件格式与PE可执行文件格式有许多相同的地方。我虽然试图去寻找这种新的OBJ文件格式的文档,但昀终一无所获。因此我要以自己的方式来解密这种格式,在这里我除了讲解PE格式之外也会讲解它的部分内容。众所周知,WindowsNT继承自VAX®VMS®和UNIX。WindowsNT的许多创建者在到Microsoft之前都曾为这些平台设计和编写程序。当他们设计WindowsNT时,很自然会使用以前写过的和测试过的工具以尽快开始他们的新项目。这些工具产生的和使用的可执行文件和目标模块的格式被称为COFF(CommonObjectFileFormat的首字母,通用目标文件格式)。你从COFF的一些域所用的竟然是八进制形式的数据就可以看出它是多么老。COFF格式本身是很好的起点,但需要扩充才能满足现代操作系统,例如WindowsNT或者Windows95的需要。结果就产生了可移植可执行格式。它之所以被称为“可移植”是因为WindowsNT在各种平台(x86、MIPS®、Alpha等等)上的所有实现都使用同样的可执行文件格式。当然,像CPU指令的二进制编码之类的内容会有所不同。重要的是操作系统加载器和编程工具不需要针对遇到的每种新的CPU再完全重写。从Microsoft抛弃现有的32位工具和文件格式上可以看出它承诺让WindowsNT运行得更快的决心。16位Windows上的虚拟设备驱动程序使用的是不同的32位文件格式——LE格式,它在WindowsNT出现之前很早就出现了。比这更重要的是更换了OBJ文件的格式。在WindowsNT的C编译器之前,所有的Microsoft编译器使用的都是Intel的OMF(ObjectModuleFormat,目标模块格式)规范。正如前面提到的那样,Microsoft的Win32编译器产生的都是COFF格式的OBJ文件。一些Microsoft的竞争者,例如Borland和Symantec,放弃COFF格式的OBJ而继续使用IntelOMF格式。结果导致这些公司为多个编译器产生的OBJ或LIB文件需要针对不同的编译器发布不同的版本。PE格式被公开在WINNT.H头文件中(非常零散)。大概在WINNT.H文件的中间有一个“ImageFormat”节。这个节以MS-DOSMZ格式和NE格式开头,后面才是新的PE格式。WINNT.H提供了PE文件使用的原始数据结构的定义,但是它包含的关于这些结构和标志的意义的有用注释却很少。为PE格式写头文件的人(MichaelJ.O'Leary)一定特别喜欢冗长的、描述性的名称,以及嵌套很深的结构和宏。当使用WINNT.H编写代码时,你经常会使用类似下面这样的表达式:pNTHeader-OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DEBUG].VirtualAddress;为了对WINNT.H中的信息有一个较好的认识,昀好阅读Microsoft可移植可执行文件和通用目标文件格式文件规范,可以在直到2001年十月(含)的MSDNLibrary每季度的光盘中找到。现在再来看COFF格式的OBJ文件,WINNT.H头文件中包含了COFF格式的OBJ和LIB文件使用的结构定义和类型定义。不幸的是,与上面提到的可执行文件一样,我找不到关于它的任何文档。由于PE文件与COFF格式的OBJ文件非常相似,我觉得是时候把这些文件呈现给大众,并为它们写些文档了。在阅读PE文件的结构之余,你可能想转储(DUMP)一些PE文件来自己看看这些概念。如果你使用Microsoft®的基于32位的开发工具,它提供的DUMPBIN程序能够剖析PE文件以及OBJ和LIB文件并且能够以易读的形式输出其结果。在所有的PE文件转储工具中,DUMPBIN是昀全面的,它甚至有一个很好的选项用来对它处理的文件的代码节进行反汇编。Borland的用户可以使用TDUMP来查看PE可执行文件,但是TDUMP并不能理解COFF格式的OBJ文件。这并不是个大问题,因为首先Borland的编译器根本就不生成COFF格式的OBJ文件。我已经写了一个PE和COFF格式的OBJ文件的转储程序,称为PEDUMP(见表1),它的输出比DUMPBIN的输出更容易理解。尽管它不包含反汇编程序,也不能处理LIB文件,但其它功能与DUMPBIN一样,并且增加了新的功能。PEDUMP的源代码在MSJ的BBS上可以找到,因此我在这里不列出它的全部代码。我会用它的某些输出实例来解释我要讲解的概念。表1PEDUMP.C//--------------------//PROGRAM:PEDUMP//FILE:PEDUMP.C//AUTHOR:MattPietrek-1993//--------------------#includewindows.h#includestdio.h#includeobjdump.h#includeexedump.h#includeextrnvar.h//这里是EXEDUMP.C和OBJDUMP.C中使用的全局变量BOOLfShowRelocations=FALSE;BOOLfShowRawSectionData=FALSE;BOOLfShowSymbolTable=FALSE;BOOLfShowLineNumbers=FALSE;charHelpText[]=PEDUMP-Win32/COFF.EXE/.OBJfiledumper-1993MattPietrek\n\nSyntax:PEDUMP[switches]filename\n\n/Aincludeeverythingindump\n/Hincludehexdumpofsections\n/Lincludelinenumberinformation\n/Rshowbaserelocations\n/Sshowsymboltable\n;//打开一个文件,然后对它创建内存映射文件,并调用合适的转储例程voidDumpFile(LPSTRfilename){HANDLEhFile;HANDLEhFileMapping;LPVOIDlpFileBase;PIMAGE_DOS_HEADERdosHeader;hFile=CreateFile(filename,GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0);if(hFile==INVALID_HANDLE_VALUE){printf(Couldn'topenfilewithCreateFile()\n);return;}hFileMapping=CreateFileMapping(hFile,NULL,PAGE_READONLY,0,0,NULL);if(hFileMapping==0){CloseHandle(hFile);printf(Couldn'topenfilemappingwithCreateFileMapping()\n);return;}lpFileBase=MapViewOfFile(hFileMapping,FILE_MAP_READ,0,0,0);if(lpFileBase==0){CloseHandle(hFileMapping);CloseHandle(hFile);printf(Couldn'tmapviewoffilewithMapViewOfFile()\n);return;}printf(Dumpoffile%s\n\n,filename);dosHeader=(PIMAGE_DOS_HEADER)lpFileBase;if(dosHeader-e_magic==IMAGE_DOS_SIGNATURE){DumpExeFile(dosHeader);}elseif((dosHeader-e_magic==0x014C)//它看起来像i386上的COFF&&(dosHeader-e_sp==0))//格式的OBJ文件吗?{//以上两个测试实际是在检测IMAGE_FILE_HEADER.Machine==i386(0x14C)//以及IMAGE_FILE_HEADER.SizeOfOptionalHeader==0;DumpObjFile((PIMAGE_FILE_HEADER)lpFileBase);}elseprintf(unrecognizedfileformat\n);UnmapViewOfFile(lpFileBase);CloseHandle(hFileMapping);CloseHandle(hFile);}//处理所有的命令行参数并返回指向文件名参数的指针PSTRProcessCommandLine(intargc,char*argv[]){inti;for(i=1;iargc;i++){strupr(argv[i]);//它是一个选项吗?if((argv[i][0]=='-')||(argv[i][0]=='/')){if(argv[i][1]=='A'){fShowRelocations=TRUE;fShowRawSectionData=TRUE;fShowSymbolTable=TRUE;fShowLineNumbers=TRUE;}elseif(argv[i][1]=='H')fShowRawSectionData=TRUE;elseif(argv[i][1]=='L')fShowLineNumbers=TRUE;elseif(argv[i][1]=='R')fShowRelocations=TRUE;elseif(argv[i][1]=='S')fShowSymbolTable=TRUE;}else//不是选项,一定是文件名{returnargv[i];}}}intmain(intargc,char*argv[]){PSTRfilename;if(argc==1){printf(HelpText);return1;}filename=ProcessCommandLine(argc,arg