PE文件结构详解 超详细 C代码

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

翻译:Jason Sun(木水鱼).
2004年5月10日
[译注:仅供大家学习使用,您在复制或使用此文档时请保留这个文件头]
Peering Inside the PE: A Tour of the Win32 Portable Executable File Format
Matt Pietrek
1994 年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.DataDirectory[IMAGE_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文本就是“PE\0\0”。

如果用了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。

0x14d Intel i860
0x14c Intel I386 (same ID used for 486 and 586)
0x162 MIPS R3000
0x166 MIPS R4000
0x183 DEC Alpha AXP
WORD 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 DataDirectory[IMAGE_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 Name[IMAGE_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属。

相关文档
最新文档