PeeringInsidethePE中文版.doc_第1页
PeeringInsidethePE中文版.doc_第2页
PeeringInsidethePE中文版.doc_第3页
PeeringInsidethePE中文版.doc_第4页
PeeringInsidethePE中文版.doc_第5页
已阅读5页,还剩20页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

翻译:Jason Sun(木水鱼)2004年5月10日译注:仅供大家学习使用,您在复制或使用此文档时请保留这个文件头Peering Inside the PE: A Tour of the Win32 Portable Executable File FormatMatt Pietrek1994 年3月Matt Pietrek 是Windows Internals (Addison-Wesley, 1993)的作者。他就职于Nu-Mega 技术有限公司,可通过CompuServe: 71774,362联系到他。这篇文章出自1994年3月发行的Microsoft系统期刊。版权所有1994 Miller Freeman, Inc.保留所有权利。未经Miller Freeman同意,这篇文章的任何部分不得以任何形式被复制(除了在评论文章里以摘要引用)。一个操作系统的可执行文件的格式在很多方面是这个操作系统的一面镜子。虽然学习一个可执行文件格式不是大多数程序员的首要任务,但是从中你可学到大量的知识。这篇文章中,我将给出Microsoft为他们的基于Win32的系统所设计的PE文件格式的详细说明。可以预知在未来,PE文件格式在Microsoft的所有操作系统包括Windows 2000中都将扮演着很重要的角色。如果你在使用Win32s或WinNT,那么你已经在使用PE文件了。甚至你只是在Windows3.1下用Visual C+编程,你也已在使用PE文件了(Visual C+的32位DOS扩展组件使用此格式)。简而言之,PE格式已得到普遍应用并且在不短的将来也不会取消。现在是时间找出这种新的可执行文件格式为操作系统所带来的影响了。我不会让你盯住无穷无尽的16进制Dumps和详细讨论页面中每个单独位的重要性。代替的,我将介绍PE文件格式中内含的概念并且把它们和你每天都会遇到的东西联系起来。例如,线程局部变量的概念,比如declspec(thread) int i;它使我快要发疯了,直到我明白它是怎样在可执行文件里优雅而简单的实现的。既然你们大多数都有使用16位Windows的背景,我将把Win32 PE文件格式的结构和与其等价的16位的NE文件格式联系起来。除了一个不同的可执行文件格式之外, Microsoft还引入了一个由它的编译器和汇编器生成的新的目标模块格式。这个新的OBJ文件格式和PE格式有许多相同的东西。为了找到这个新的OBJ文件格式的文档我做了许多徒劳的搜索。所以我以自己的理解来解释它,并且除了PE格式之外我会在这里描述它其中的一部分。大家都知道Windows NT继承了VAX VMS 和 UNIX。许多Windows NT的创建者在进入Microsoft之前都在那些平台上进行设计和编码。当开始设计Windows NT时, 很自然的他们设法使用以前编写的并经过测试的工具以最小化项目启动时间。这些工具生成的并且与之一起工作的可执行文件和目标模块格式被叫做COFF(Common Object File Format的首字母缩写)。COFF格式自身是一个好的起点,但需要被扩展以满足一个现代操作系统如Windows NT或者Windows 95的所有需要。这个扩展的结果就是PE格式。它被称为“可移植”是因为Windows NT在不同的平台(x86, MIPS, Alpha, 等等)上的所有实现都使用这个相同的可执行格式。当然,也有不同的地方比如CPU指令的二进制编码。重要的是操作系统加载器和程序设计工具不必为每种CPU完全重写。Microsoft抛弃了现存的32位工具和文件格式的事实证明了他们想让Windows NT升级并且运行的更快的决心。为16位Windows编写的虚拟设备驱动使用一个不同的32位文件布局LE 格式它在Windows NT出现很早以前就存在了。比那更重要的是OBJ格式的改变。在Windows NT 的C编译器以前,所有的Microsoft编译器使用Intel OMF(Object Module Format)规范。以前提到,Microsoft的Win32编译器生成COFF格式的OBJ文件。一些Microsoft的竞争者例如Borland 和 Symantec 选择放弃 COFF 格式的 OBJs 而坚持使用 Intel OMF格式。结果导致生成OBJs或LIBs的公司为了使用不同的编译器就必须回去为不同的编译器发布他们产品的不同版本 (如果他们还没有那么做)。PE格式在WINNT.H头文件中被文档化了。大约在WINNT.H文件的中间一个标题为“Image Format”的区域。这块区域的开头是我们熟悉的老的MS-DOS MZ格式和NE格式文件头接下来才是更新的PE格式的信息。WINNT.H 提供PE文件用到的原始数据结构的定义,但只包含了很少有用的以助于理解这些结构和标志的意思的注释。当使用WINNT.H编码时, 类似这样的表达式很常见:pNTHeader-OptionalHeader.DataDirectoryIMAGE_DIRECTORY_ENTRY_DEBUG.VirtualAddress;要帮助逻辑地理解WINNT.H中的信息,可以阅读PE和COFF规范。即刻转到COFF格式的OBJs文件的主题上来,WINNT.H头文件包括COFF格式的OBJ和LIB文件的structure定义和typedefs定义。不幸的是,我还没有找到上面提到的可执行文件格式的类似文档。既然PE文件和COFF OBJ文件是如此的相似,我决定是时候把这些文件拿出来,并且文档化。阅读了PE文件由什么组成后,你自己也想Dump一些PE文件来看看这些概念。如果你使用Microsoft的工具进行基于Win32的开发,DUMPBIN程序可以分析并把PE文件和COFF OBJ/LIB文件输出为可读的形式。在所有的Dump工具中,DUMPBIN是容易的和最全面的。它甚至有一个极好的选项来反汇编它正在解析的文件的代码节(code sections)。Borland 用户可使用TDUMP查看PE可执行文件,但TDUMP不能解析COFF OBJ文件。这不是一个大的问题因为Borland编译器首先就不生成COFF格式的OBJs文件。我写了一个PE和COFF OBJ文件的Dump程序,PEDUMP(见表1),我是想提供比DUMPBIN更可理解的输出。虽然它没有反汇编器也不能和LIB文件一起工作,但它在其它方面和DUMPBIN的功能是一样的,并且添加了一些新的特性以使它值得被认同。PEDUMP的源代码在任何MSJ电子公告板都可找到,因此我不把它在这儿全部列出。代替的,我将会列出一些PEDUMP输出的例子以举例说明我描述到的概念。表 1. PEDUMP.C略Win32和PE基本概念让我们复习一下几个基本概念,这些基本概念渗透于整个PE文件的设计(见图1)。我将用术语“模块(module)”来表示一个可执行文件或DLL加载到内存中的代码,数据和资源。除了你的程序直接使用的代码和数据之外,一个模块还包括Windows用来确定代码和数据在内存中被载入的位置的支撑数据结构。在16位Windows中,这些支撑数据结构位于模块数据库中(HMODULE指向的一个段)。在Win32中,这些数据结构位于PE头中,我将简要地介绍一下这些。图 1. PE文件格式对于PE文件重要的是要知道磁盘上的可执行文件和在被Windows调入内存后是很相似的。Windows加载器从磁盘文件创建一个进程时不必很费力。加载器使用内存映射文件机制把文件中适当的部分映射到虚拟地址空间中。这种方式应用到PE格式的DLLs也同样容易。一旦模块被载入,Windows就能有效的把它和其它内存映射文件同等对待。这和16位Windows明显不同。16位NE文件加载器读取文件的一部分并且创建完全不同的数据结构来描述内存中的模块。当一个代码或数据段需要被调入时,加载器必须从全局堆中分配一个新的段,从可执行文件中找到原始数据的存储位置,转到这个位置,读入原始数据,并且进行适当的修正。另外,每个16位模块都有责任记住它用到的所有段选择器,不管这个段是否已被丢弃,等等。对Win32来说,模块中的代码,数据,资源,导入表,导出表,和其它必需的模块数据结构用到的所有内存都在一个连续的内存块中。这种情况下你所要知道的就是加载器把文件映射到内存中的位置。通过存储在映像中的指针你可以很容易地找到模块中的所有部分。你需要熟悉的另一个概念是相对虚拟地址(RVA)。PE文件中的许多域都用术语RVA指定。一个RVA只是一些项目相对于映射到内存后的文件的偏移。例如,让我们假定加载器把一个PE文件映射到了虚拟地址空间中起始地址为0x10000的位置。如果映像中某个表的起始地址是0x10464,那么这个表的RVA是0x464。 (Virtual address 0x10464)-(base address 0x10000) = RVA 0x00464要把一个RVA转换成一个有用的指针,只要把RVA和模块的基址相加就行了。基址是一个EXE或DLL内存映射的起始地址,在Win32中是一个重要的概念。为了方便,Windows NT和Windows 95使用模块的基址作为这个模块的实例句柄(HINSTANCE)。在Win32中,把模块的基址称为HINSTANCE可能有点混淆,因为术语“实例句柄”来自于16位Windows。16位Windows中一个应用程序的每个拷贝都有它自己的单独的数据段(和一个关联的全局句柄) 把它和这个应用程序的其它拷贝区别开来,因此就形成了术语实例句柄。在Win32中,应用程序不必和其它程序区分开,因为它们不会共享相同的地址空间。尽管如此,术语HINSTANCE 仍被用来保持16位Windows和Win32之间的连续性。Win32中重要的是你可以为任何DLL调用GetModuleHandle方法得到一个指针用来访问这个模块的组件。你要知道的关于PE文件的最后的概念是“节(Section)”。PE文件中的一个节和NE文件中的一个段或者资源大致等价。节中包含的不是代码就是数据。和段不同,节是内存中的连续的空间并且没有大小限制。一些节中包含你的程序中直接声明和使用的代码和数据,另一些被链接器和库为你创建的数据节中包含操作系统要用到的重要的信息。在PE格式的一些描述中,节也被称为“对象(objects)”。术语“对象(object)”有太多的含义,因此我将把代码和数据区称为“节(Section)”。PE头就像所有其它的可执行文件格式一样,PE文件中在一个大家都知道的 (或者容易找到)位置有一个包括很多字段的集合,它定义了文件其余部分的样式。这个头中包含了一些信息例如代码和数据区的位置和大小,是什么操作系统下的文件,初始堆栈大小,和另外一些我将要讨论到的重要的信息块。和Microsoft的其它一些可执行格式不一样,这个主要的头部不在文件的最开始。PE文件开始的几百个字节被MS-DOS stub占用了。这个stub是一个很小的程序,它输出一些东西比如“This program cannot be run in MS-DOS mode.”。因此如果你在一个不支持Win32的环境中运行一个Win32程序,你将会得到这个错误信息。当Win32加载器映射一个PE文件时,映像文件的第一个字节就是MS-DOS stub的第一个字节。非常正确。在你启动任何一个Win32程序的同时,都有一个基于MS-DOS的程序连带被载入!和Microsoft的其它可执行格式一样,你可以通过查找它的起始偏移来找到真正的头部,这个偏移被存储在MS-DOS stub头中。WINNT.H头文件中包括一个MS-DOS stub头的结构定义,使得找到PE头的起始位置很容易。e_lfanew域是到真实PE头部的相对偏移(或者叫做 RVA,如果你更喜欢)。要得到内存中PE头的指针,把那个成员的值和映像基址相加即可:/ Ignoring typecasts and pointer conversion issues for clarity.pNTHeader = dosHeader + dosHeader-e_lfanew;一旦你有了一个PE主头部的指针,有趣的事情就开始了。 主PE头是一个IMAGE_NT_HEADERS 类型的结构,它被定义在WINNT.H中。这个结构由一个DWORD和两个子结构组成,布局如下:DWORD Signature;IMAGE_FILE_HEADER FileHeader;IMAGE_OPTIONAL_HEADER OptionalHeader;Signature域被看作ASCII文本就是“PE00”。如果用了MS-DOS头的域e_lfanew后,你得到了一个NE签名而不是一个PE签名,那么你正在处理的是16位Windows的NE文件。同样地,如果在Signature域中是LE则表示是一个Windows 3.x的虚拟设备驱动程序(VxD)。如果是LX则表示是OS/2 2.0的文件。PE头中在PE签名DWORD之后是一个IMAGE_FILE_HEADER类型的结构。这个结构的域中只包含了关于这个文件的最基本的信息。除了是PE头的一部分之外,它也出现在Microsoft Win32编译器生成的COFF OBJ文件的最开始。IMAGE_FILE_HEADER结构的各域在表 2中列出。表 2. IMAGE_FILE_HEADER Fields WORD Machine 文件运行于哪种类型的CPU之上。下面是已定义的一些CPU ID。0x14dIntel i8600x14cIntel I386 (same ID used for 486 and 586)0x162MIPS R30000x166MIPS R40000x183DEC Alpha AXPWORD NumberOfSections 文件中节(Section)的数量。DWORD TimeDateStamp 链接器(或者编译器如果是OBJ文件)生成这个文件的时间。这个域保存的是从1969年12月31日下午4点到生成这个文件时经过的秒数。DWORD PointerToSymbolTable COFF符号表的文件偏移量。这个域只在有COFF调试信息的OBJ文件和PE文件中使用。PE文件支持多种调试格式,因此调试器应该指到数据目录(在后面被定义)的IMAGE_DIRECTORY_ENTRY_DEBUG 入口。DWORD NumberOfSymbols COFF符号表中的符号的数量。见上面。WORD SizeOfOptionalHeader 此结构后面的一个可选头的字节大小。在OBJ文件中,这个域是0。在可执行文件中它是这个结构后面紧跟的IMAGE_OPTIONAL_HEADER结构的大小。WORD Characteristics 关于这个文件的信息的标记。一些重要的域如下: 0x0001文件中没有重定位信息0x0002文件是一个可执行的映像(不是一个OBJ或LIB)0x2000文件是一个DLL不是一个程序其它域定义在WINNT.H中。PE头的第三个组成部分是一个IMAGE_OPTIONAL_HEADER类型的结构。对于PE文件,这一部分当然不是可选的。COFF格式允许具体的实现超出标准的IMAGE_FILE_HEADER结构定义一个附加信息结构。IMAGE_OPTIONAL_HEADER中的域是PE的设计者认为超出IMAGE_FILE_HEADER中的基本信息外很重要的信息。并不是所有IMAGE_OPTIONAL_HEADER中的域都是必须要了解的(参见图4)(译注:应该是表 3)。重要的域是ImageBase和Subsystem。你可以跳过其它域的描述。表 3. IMAGE_OPTIONAL_HEADER Fields WORD Magic 表现为一些类别的魔数。始终是0x010B。BYTE MajorLinkerVersion BYTE MinorLinkerVersion 生成这个文件的链接器的版本号。这个数字显示成十进制比十六进制要好。比如一个典型的链接器版本号2.23。DWORD SizeOfCode 所有代码节的大小。通常,大多数文件只有一个代码节,因此这个域就是.text节的大小。DWORD SizeOfInitializedData 想象中它是已初始化的数据(不包括代码段)组成的所有的节的大小。然而,它似乎和文件中显示的并不一致。DWORD SizeOfUninitializedData 加载器在虚拟地址空间中提交空间但在磁盘文件中并不占用任何空间的节的大小。这些节在程序启动时不需要指定值,因此有了“未初始化数据”这个术语。未初始化数据通常在一个名称为“.bss”的节中。DWORD AddressOfEntryPoint加载器将要开始执行程序的地址。这是一个RVA,并且通常位于“.text”节中。DWORD BaseOfCode 代码节起始位置的RVA。在内存中,代码节通常在数据节之前PE头之后。在Microsoft的链接器生成的EXE文件中这个RVA通常是0x1000。Borland的TLINK32好像是把映像的基址和第一个代码节的RVA相加存在这个域中。DWORD BaseOfData 数据节起始位置的RVA。在内存中,数据通常在最后,在PE头和代码节之后。DWORD ImageBase 当链接器创建一个可执行文件时,它假定这个文件将被映射到内存中的一个指定的位置。那个地址被存在这个域中,假如一个载入地址可以使链接器最优化。如果这个文件真的被加载器映射到那个地址,代码不需要任何修补在运行它之前。在为Windows NT生成的可执行文件中,缺省的ImageBase是0x10000。对于DLL文件,缺省是0x400000。在Windows 95中, 地址0x10000不能被用来加载32位的EXE因为它位于一个被所有进程共享的线性地址空间中。因为此,Microsoft把Win32可执行文件的缺省基址改为0x400000。假定基址为0x10000的老程序在Windows 95下加载将需要更长的时间,因为加载器需要重定位基址。DWORD SectionAlignment 被映射到内存时,每个节都被保证开始于这个值的整数倍的虚拟地址。为了便于分页缺省的SectionAlignment是0x1000。DWORD FileAlignment 在PE文件中,组成每个节的原始数据都被保证开始于这个值的整数倍。缺省值是0x200 字节,也是是为了确保每个节总是位于一个磁盘扇区的开头(磁盘扇区的长度也是0x200字节)。这个域的值等价于NE文件中段/资源的对齐大小。不像NE文件,PE文件通常没有好几百那么多的节,因此为了对齐节而浪费的空间几乎总是很少。WORD MajorOperatingSystemVersion WORD MinorOperatingSystemVersion 运行这个可执行文件所必需的最小的操作系统的版本号。这个域有点不明确因为Subsystem 域 (后面会提到) 可以提供一个类似的功能。目前为止在所有的Win32 EXE中这个域缺省是1.0。WORD MajorImageVersion WORD MinorImageVersion 一个可由用户定义的域。这允许你的EXE和DLL可以有不同的版本。你可以通过链接器的/VERSION 选项来设置这个域的值。例如,“LINK /VERSION:2.0 myobj.obj”。WORD MajorSubsystemVersion WORD MinorSubsystemVersion 包含运行这个可执行文件所需要的最小子系统版本号。这个域的一个典型的值是3.10 (表示Windows NT 3.1)。DWORD Reserved1 似乎总是0。DWORD SizeOfImage 加载器必须关心的这个映像的总的大小。它是从映像的最开始一直到最后一个节的末尾的大小。最后一个节的末尾按SectionAlignment对齐。DWORD SizeOfHeaders PE头和节表的大小。节的实际数据紧跟在所有头部组件之后。DWORD CheckSum 这个文件的CRC校验和。和Microsoft其它的可执行格式一样,这个域被忽略并被设为0。这个规则的一个例外是信任服务,这些EXE必须有一个有效的校验和。WORD Subsystem 这个可执行文件为它的用户界面使用的子系统类型。WINNT.H中定义了以下值:NATIVE 1不需要子系统(例如一个设备驱动程序)WINDOWS_GUI 2在Windows GUI子系统下运行WINDOWS_CUI 3在Windows字符子系统下运行(控制台程序)OS2_CUI 5在OS/2 字符子系统下运行(仅对OS/2 1.x)POSIX_CUI 7在Posix字符子系统下运行WORD DllCharacteristics 指示一个DLL的初始化方法(例如DllMain)在哪种情况下应该被调用的一组标记。这个值总被设为0,然而操作系统仍然调用DLL的初始化方法在下面所有的四种情况下。下面的值被定义:1 当DLL第一次被加载到一个进程的地址空间中时调用2 当一个线程结束时调用4 当启动一个线程时调用8 DLL被卸载时调用DWORD SizeOfStackReserve 为初始线程的堆栈保留的虚拟内存数量。然而,并不是所有这些内存都被提交(参见下一个域)。这个域缺省是0x100000 (1MB)。如果你用CreateThread创建线程时指定堆栈大小为0,创建出来的线程也会使用这个域的值作为堆栈大小。DWORD SizeOfStackCommit 为初始线程的堆栈最开始提交的内存的数量。对于Microsoft的链接器这个域缺省是0x1000字节(1页),然而对于TLINK32是两页。DWORD SizeOfHeapReserve 为初始进程的堆保留的虚拟内存的数量。可以通过调用GetProcessHeap方法来获得这个堆的句柄。并不是所有的这些内存都被提交。(参见下一个域)。DWORD SizeOfHeapCommit 在进程堆中最开始被提交的内存数量。缺省是一页。DWORD LoaderFlags 从WINNT.H文件知道,这些是与调试支持相关的字段。我从来没有见过一个可执行文件中的这些位被置位过,也不清楚链接器怎么使用它们。以下的值被定义:1.在启动进程前调用一个断点指令2.进程被加载后调用一个调试器DWORD NumberOfRvaAndSizes 数据目录数组中条目的数量(见下面)。这个值总是被当前的工具设为16。IMAGE_DATA_DIRECTORY DataDirectoryIMAGE_NUMBEROF_DIRECTORY_ENTRIES 一个IMAGE_DATA_DIRECTORY结构的数组。开始的数组元素包含可执行文件的重要部分的起始RVA和大小。数组末尾的几个元素当前并没有使用。数组的第一个元素总是导出函数表的址和大小。第二个数组条目是导入函数表的地址和大小,等等。至于完整的已定义数组条目的列表,参见WINNT.H中的IMAGE_DIRECTORY_ENTRY_XXX #defines。这个数组允许加载器快速的定位映像中一个特定的块 (例如导入函数表),而不必通过比较名字来编历映像中的每一个节。大多数数组条目描述一个整个节的数据。然而,IMAGE_DIRECTORY_ENTRY_DEBUG 元素只包含.rdata节中一小部分字节的数据。节表在PE头和映像的各节的实际数据之间是节表。节表实际上就相当于包含映像中每个节的信息的电话本。映像中的节用它们的起始地址(RVA)来排序,而不是按字母排序的。现在我可以更好地来阐明什么是一个节。在一个NE文件里,你程序的代码和数据被存储在文件中截然不同的“段”里。NE头的一部分是一个结构数组,数组中的每个元素都对应你程序用到的一个段。数组中的每个结构包含一个段的信息。这些信息包括段的类型(代码还是数据),段的大小,和段在文件中的位置。在一个PE文件里,节表类似于NE文件里的段表。和NE文件段表不一样的是,PE节表不为每个代码或数据块存储一个选择子。 代替的,节表的每个条目存储一个文件的实际数据被映射到内存中的地址。虽然节类似于32位的段,但它们确实不是单独的段。它们实际上是一个进程的虚拟地址空间的内存范围。PE文件和NE文件的另一个不同的地方是它们怎样管理你的程序不会用到但操作系统要用到的一些支持数据;例如,程序要用到的DLL的列表或修正表的位置。在一个NE文件中,资源不被当作一个段。甚至尽管为它们分配了选择子,关于资源的信息没有被存储在NE头的段表里。代替的,资源归属于接近NE头尾部的一个单独的表。关于导入和导出函数的信息也没有它们自己的段;它被安排在NE头部中。PE文件是不同的。任何可能被认为重要的代码或数据都被存在一个完整的节中。因此,导入函数信息被存在它的自己的节中,模块的导出函数表也一样。重定位数据也一样。任何程序用到的或操作系统用到的代码和数据都位于它们自己的节中。在我讨论特定的节之前,我需要描述操作系统用来管理这些节的数据。内存中紧跟PE头的是一个IMAGE_SECTION_HEADERs 数组。PE头中给出了这个数组的元素个数(IMAGE_NT_HEADER.FileHeader.NumberOfSections 域)。我用PEDUMP来输出节表和节的所有域和属性。图5(译者注:应该是表 4)显示PEDUMP输出的一个典型的EXE文件的节表,图 6(译者注:应该是表 5)显示一个OBJ文件的节表。表 4. 一个典型的EXE文件的节表01 .text VirtSize: 00005AFA VirtAddr: 00001000 raw data offs: 00000400 raw data size: 00005C00 relocation offs: 00000000 relocations: 00000000 line # offs: 00009220 line #s: 0000020C characteristics: 60000020 CODE MEM_EXECUTE MEM_READ 02 .bss VirtSize: 00001438 VirtAddr: 00007000 raw data offs: 00000000 raw data size: 00001600 relocation offs: 00000000 relocations: 00000000 line # offs: 00000000 line #s: 00000000 characteristics: C0000080 UNINITIALIZED_DATA MEM_READ MEM_WRITE 03 .rdata VirtSize: 0000015C VirtAddr: 00009000 raw data offs: 00006000 raw data size: 00000200 relocation offs: 00000000 relocations: 00000000 line # offs: 00000000 line #s: 00000000 characteristics: 40000040 INITIALIZED_DATA MEM_READ 04 .data VirtSize: 0000239C VirtAddr: 0000A000 raw data offs: 00006200 raw data size: 00002400 relocation offs: 00000000 relocations: 00000000 line # offs: 00000000 line #s: 00000000 characteristics: C0000040 INITIALIZED_DATA MEM_READ MEM_WRITE 05 .idata VirtSize: 0000033E VirtAddr: 0000D000 raw data offs: 00008600 raw data size: 00000400 relocation offs: 00000000 relocations: 00000000 line # offs: 00000000 line #s: 00000000 characteristics: C0000040 INITIALIZED_DATA MEM_READ MEM_WRITE 06 .reloc VirtSize: 000006CE VirtAddr: 0000E000 raw data offs: 00008A00 raw data size: 00000800 relocation offs: 00000000 relocations: 00000000 line # offs: 00000000 line #s: 00000000 characteristics: 42000040 INITIALIZED_DATA MEM_DISCARDABLE MEM_READ 表 5. 一个典型的OBJ文件的节表01 .drectve PhysAddr: 00000000 VirtAddr: 00000000 raw data offs: 000000DC raw data size: 00000026 relocation offs: 00000000 relocations: 00000000 line # offs: 00000000 line #s: 00000000 characteristics: 00100A00 LNK_INFO LNK_REMOVE 02 .debug$S PhysAddr: 00000026 VirtAddr: 00000000 raw data offs: 00000102 raw data size: 000016D0 relocation offs: 000017D2 relocations: 00000032 line # offs: 00000000 line #s: 00000000 characteristics: 42100048 INITIALIZED_DATA MEM_DISCARDABLE MEM_READ 03 .data PhysAddr: 000016F6 VirtAddr: 00000000 raw data offs: 000019C6 raw data size: 00000D87 relocation offs: 0000274D relocations: 00000045 line # offs: 00000000 line #s: 00000000 characteristics: C0400040 INITIALIZED_DATA MEM_READ MEM_WRITE 04 .text PhysAddr: 0000247D VirtAddr: 00000000 raw data offs: 000029FF raw data size: 000010DA relocation offs: 00003AD9 relocations: 000000E9 line # offs: 000043F3 line #s: 000000D9 characteristics: 60500020 CODE MEM_EXECUTE MEM_READ 05 .debug$T PhysAddr: 00003557 VirtAddr: 00000000 raw data offs: 00004909 raw data size: 00000030 relocation offs: 00000000 relocations: 00000000 line # offs: 00000000 line #s: 00000000 characteristics: 42100048 INITIALIZED_DATA MEM_DISCARDABLE MEM_READ 每个IMAGE_SECTION_HEADER都有如图 7(译者注:应该是表 6)所描述的格式。要注意到为每个节存储的信息中缺少了什么。首先,注意到没有指定任何预载入属性。NE文件格式允许你指定和模块一起载入的段的预载入属性。OS/2 2.0 LX格式有类似的东西,允许你指定预载入八页。PE格式没有类似的东西。Microsoft必须确保Win32需求页的载入性能。表 6. IMAGE_SECTION_HEADER 格式BYTE NameIMAGE_SIZEOF_SHORT_NAME 节的八字节的ASCII字符串名称(不是UNICODE)。大多数节的名称以一个“.”开始(例如“.text”),但这并不是必须的。你在汇编语言中可以用段指示符命名你自己的节,或者在Microsoft C/C+编译器中用“#pragma data_seg”和“#pragma code_seg”。要注意到如果节名占用了所有的这8个字节,就没有NULL结束字节了。如果你喜欢使用printf,你可以用%.8s来避免把这个名字拷贝到另一个可以用NULL结尾的缓冲区中。union DWORD PhysicalAddress DWORD VirtualSize Misc; 这个域在EXE文件和OBJ文件中有不同的含义。在EXE文件中,它保存代码或数据在使用FileAlignment进行对齐之前的实际大小。SizeOfRawData 域(这个名字似乎有 点不确切) 保存对齐后的大小。Borland链接器调换了这两个域的意思,然后似乎是正确的。而OBJ文件中,这个域指出节的物理地址。第一个节开始于地址0。要找到OBJ文件中下一个节的物理地址,把SizeOfRawData和当前节的物理地址相加就行了。DWORD VirtualAddress EXE文件中,这个域保存加载器应该把这个节映射到的位置的RVA。要计算一个给定节在内存中的真正的起始地址,把映像的基址和存储在这个域中的这个节的VirtualAddress相加就行了。如果用Microsoft的工具,第一个节的RVA缺省是0x1000。在OBJ文件中,这个域没有使用,并且被设为0。DWORD SizeOfRawData EXE文件中,这个域包含节在被按照文件对齐尺寸对齐后的大小。例如,假定一个文件对齐尺寸是0x200。如果上面的VirtualSize域显示这个节的长度是0x35A字节,那么这个域的值是0x400。OBJ文件中,这个域包含由编译器或汇编器生成的节的精确的大小。换句话说,对于OBJ文件,这个域等价于EXE文件中的VirtualSize域。DWORD PointerToRawData 编译器或汇编器生成的原始数据基于文件的偏移位置。如果你的程序自己读取一个PE或COFF文件到内存中(而不是让操作系统加载它),这个域比VirtualAddress域更重要。在这种情况下你会得到一个完全线性的文件映射,因此你将会在这个偏移处找到节中的数据,而不是在VirtualAddress域指定的RVA中。DWORD PointerToRelocations 在OBJ文件中,是这个节的重定位信息基于文件的偏移量。每个OBJ的节的重定位信息紧跟在那个节的原始数据之后。在EXE文件中,这个域(和后面的域)没有使用并被设为0。当链接器创建EXE文件时,它已经解决了大部分的修正,只剩下基址重定位和导入函数在加载时再修正。关于基址重定位和导入函数的信息位于它们自己的节中,因此对一个EXE文件来说没有必要在每个节的原始数据后紧跟着节的重定位数据。DWORD PointerToLinenumbers 行号表基于文件的偏移量。行号表把源文件中给定一行的行号和为这一行生成的机器码的地址联系起来。在现代调试格式中比如CodeView,行号信息被存储为调试信息的一部分。然而在COFF调试格式中,行号信息和符号名/类型信息是分开存储的。通常,只有代码节(如.text)有行号。在EXE文件中,行号被收集在文件的结尾,在节的原始数据后。在OBJ文件中,一个节的行号表跟在节的原始数据和重定位表之后。WORD NumberOfRelocations 节的重位表中重定位项的数目(参见上面的PointerToRelocations域)。这个域似乎只和OBJ文件有关。WORD NumberOfLinenumbers 节的行号表中行号的数量(参见上面的PointerToLinenumbers域)。DWORD Characteristics 大多数程序员称之为标记,COFF/PE格式称之为特征。这个域是一组指示节的特征的标记(例如是代码还是数据,是否可读,或者是否可写)。要得到一个节的所有可能的特征的列表,请参见WINNT.H文件中的IMAGE_SCN_XXX_XXX #defines部分。下面是一些比较重要的标记: 0x00000020 节中包含代码。通常和可执行标记(0x80000000译者注:应该是0x20000000)一起使用。0x00000040 节中包含已初始化数据。除了可执行节和.bss节之外几乎所有的节的这个标记都被置位。0x00000080 节中包含未初始化数据(例如.bss节)。0x00000200 节中包含注释或一些其它类型的信息。一个典型的例子是编译器生成的.drectve节,它里面包含了链接器命令。0x00000800 节中的内容不应该被放在最终的EXE文件中。编译器或者汇编器使用这些节为链接器传递一些信息。0x02000000 节可以被丢弃,因为一旦它被载入进程就不需要它了。通常可被丢弃的节是重定位节(.reloc)。0x10000000 节是可共享的。在DLL中使用此标记时,这个节的数据可以在使用此DLL的进程之间共享。数据节默认是不共享的,意味着使用到一个DLL的每个进程都有一份这个节的数据的自己的副本。用更专业的术语,共享节告诉内存管理器为所有使用到此DLL的进程映射这个节时都映射到内存中相同的物理页面。要使一个节可被共享,在链接时用SHARED属性 。例如LINK /SECTION:MYDATA,RWS .告诉链接器名称为MYDATA的节应该是可读的,可写的和共享的。0x20000000 节是可执行的。通常当“包含代码”标记(0x00000020)被置位时这个节也被置位。0x40000000 节是可读的。在EXE文件的节中这个标记几乎总是被置位。0x80000000 节是可写的。在一个EXE的节中如果这个标记没有被置位,加载器将标记内存映射页为只读的或者只执行的。具有这个特征的典型的节是.data和.bss。有趣地是,.idata节的这个特征也被置位。PE格式中还缺少页表的概念。在OS/2的LX格式中一个IMAGE_SECTION_HEADER并不直接指到文件中一个节的代码或数据的位置。代替的,它指向一个页查找表,这个表指定了一个节中指定范围的页的特征和位置。PE格式无需这些,并且保证一个节的数据将会被连续的存储在文件中。比较这两种格式,LX格式的灵活性更大,但PE格式使用起来更简单更容易。在为这两种格式分别写了dumper后,我能保证这个!PE格式中另一个受欢迎的改变是条目的位置用一个简单的DWORD偏移来存储。NE文件格式中,几乎所有东西的位置都被存储为一个扇区值。要找到真实的偏移,首先你必须查找NE头中的对齐单元大小并且把它转换为一个扇区大小(典型的是16或512字节)。然后你必须用指定的扇区偏移乘以这个扇区大小才能得到真正的文件偏移量。在一个NE文件中如果偶然的一些东西没有被存储为一个扇区偏移,那它或许是被存储为一个相对于NE头的偏移。因为NE头并不在文件的开始,你必须在你的代码中调整NE头的文件偏移。总而言之,PE格式使用起来比NE,LX或LE格式更容易(假设你能使用内存映射文件)。通用节(Common Sections)在大概地了解了什么是节和它们的位置后,让我们看一下你将会在EXE和OBJ文件中发现的通用的节。这个列表决不是完整的,但包括了你每天都会遇到的节(即使你没有意识到)。所有由编译器或汇编器最终生成的代码都在.text节中。因为PE文件运行于32位模式并且没有16位段的限制,所以没有理由把不同源文件的代码分开到不同的段中。代替的,链接器把各个OBJ文件中的.text节连接在一起放到EXE文件一个大的.text节中。如果你使用Borland C+,编译器把它生成的代码放在一个叫做CODE的段中。Borland C+生成的PE文件有一个叫做CODE的节而不是.text。一会儿我将对此进行解释。我发现除了我用编译器生

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论