马特·彼得雷克
1994年3月
Matt Pietrek是 Windows Internals 的作者(Addison-Wesley,1993年)。他在Nu-Mega Technologies Inc.工作,可以通过CompuServe与他联系:71774,362
本文摘自1994年3月发行的 Microsoft Systems Journal *。版权所有©1994,Miller Freeman,Inc.保留所有权利。未经米勒·弗里曼(Miller Freeman)事先同意,不得以任何方式复制本文的任何部分(关键文章和评论中使用的简短引用除外)。*
要与Miller Freeman联系以获取订阅信息,请在美国致电(800)666-1084,或在所有其他国家/地区致电(303)447-9330。如有其他查询,请致电(415)358-9500。
操作系统的可执行文件的格式在许多方面都是操作系统的镜像。尽管研究可执行文件格式通常在大多数程序员的工作清单中并不重要,但是可以通过这种方式收集大量知识。在本文中,我将介绍Microsoft为所有基于Win32®的系统:WindowsNT®,Win32s™和Windows®95设计的可移植可执行(PE)文件格式。在可预见的将来,包括Windows 2000,它在Microsoft所有操作系统中扮演着重要角色。如果使用Win32s或Windows NT,则您已经在使用PE文件。即使只为使用Visual C ++®的Windows 3.1编程,您仍在使用PE文件(Visual C ++的32位MS-DOS®扩展组件使用此格式)。简而言之,PE已经很普遍,并且在不久的将来将不可避免。现在是时候找出这种新型的可执行文件给操作系统带来了什么。
我不会让您凝视无休止的十六进制转储,也不会为页面末尾的单个位的重要性而烦恼。相反,我将介绍嵌入在PE文件格式中的概念,并将它们与您每天遇到的事情相关联。例如,线程局部变量的概念,如
declspec(thread) int i;
让我发疯,直到我看到如何在可执行文件中以简洁的方式实现它。由于你们中的许多人来自16位Windows的背景,因此我将把Win32 PE文件格式的结构与其等效的16位NE文件格式相关联。
除了不同的可执行格式外,Microsoft还引入了由其编译器和汇编器产生的新对象模块格式。这种新的OBJ文件格式与PE可执行文件格式有很多共同点。我徒劳地寻找关于新OBJ文件格式的任何文档。因此,我自己解密了该文件,并在此介绍了PE格式以外的部分内容。
众所周知,Windows NT具有VAX®VMS®和UNIX®的传统。在加入Microsoft之前,许多Windows NT创建者都为这些平台进行了设计和编码。当设计Windows NT时,很自然地,他们试图使用以前编写和测试的工具来最小化引导时间。这些工具产生并使用的可执行文件和对象模块格式称为COFF(通用对象文件格式的缩写)。可以通过诸如以八进制格式指定的字段之类的东西来查看COFF的相对年龄。COFF格式本身是一个很好的起点,但需要扩展以满足诸如Windows NT或Windows 95之类的现代操作系统的所有需求。此更新的结果是可移植可执行格式。叫做“便携式” 因为Windows NT在各种平台(x86,MIPS®,Alpha等)上的所有实现都使用相同的可执行格式。当然,在CPU指令的二进制编码等方面也存在差异。重要的是,不必为到达现场的每个新CPU完全重写操作系统加载程序和编程工具。
Microsoft放弃了现有的32位工具和文件格式的事实证明了Microsoft对Windows NT的快速启动的坚定承诺。为16位Windows编写的虚拟设备驱动程序早在Windows NT出现之前就使用了不同的32位文件布局(LE格式)。比这更重要的是OBJ格式的转变。在Windows NT C编译器之前,所有Microsoft编译器都使用Intel OMF(对象模块格式)规范。如前所述,用于Win32的Microsoft编译器生成COFF格式的OBJ文件。一些Microsoft竞争对手(例如Borland和Symantec)选择放弃COFF格式的OBJ,而坚持使用Intel OMF格式。
PE格式记录在WINNT.H头文件中(从广义上来说)。大约在WINNT.H中途是标题为“图像格式”的部分。本节从进入熟悉的MS-DOS MZ格式和NE格式标头的小花絮开始,然后转移到较新的PE信息中。WINNT.H提供了PE文件使用的原始数据结构的定义,但仅包含一些有用的注释,以使它们理解结构和标志的含义。不管是谁写的PE格式的头文件(不断出现的名字Michael J. O‘Leary),肯定会相信带有描述性的长名称以及深层嵌套的结构和宏。当使用WINNT.H进行编码时,具有这样的表达式并不罕见:
pNTHeader->
OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DEBUG].VirtualAddress;
为使WINNT.H中的信息具有逻辑意义,请阅读可移植的可执行文件和公用对象文件格式规范,该规范可从MSDN Library季度CD-ROM发行,直到2001年10月(含)。
暂时转到COFF格式OBJ的主题,WINNT.H头文件包括COFF OBJ和LIB文件的结构定义和typedef。不幸的是,我无法找到与上述可执行文件类似的任何文档。由于PE文件和COFF OBJ文件是如此相似,我决定是时候将这些文件曝光并进行记录了。
除了阅读组成PE文件的内容之外,您还需要转储一些PE文件以亲自了解这些概念。如果您使用Microsoft®工具进行基于Win32的开发,则DUMPBIN程序将以可读形式剖析并输出PE文件和COFF OBJ / LIB文件。在所有PE文件转储器中,DUMPBIN无疑是最全面的。它甚至还有一个不错的选择,可以分解正在分解的文件中的代码部分。Borland用户可以使用TDUMP查看PE可执行文件,但是TDUMP无法理解COFF OBJ文件。这并不是什么大问题,因为Borland编译器最初不会产生COFF格式的OBJ。
我编写了一个PE和COFF OBJ文件转储程序PEDUMP(请参见表1),我认为它提供的输出比DUMPBIN更容易理解。尽管它没有反汇编程序或使用LIB文件,但在功能上等效于DUMPBIN,并添加了一些新功能使其值得考虑。PEDUMP的源代码可在任何MSJ公告板上找到,因此在此不会完整列出。相反,我将显示PEDUMP的示例输出,以在描述它们时说明这些概念。
表1. PEDUMP.C
//--------------------
// PROGRAM: PEDUMP
// FILE: PEDUMP.C
// AUTHOR: Matt Pietrek - 1993
//--------------------
#include <windows.h>
#include <stdio.h>
#include "objdump.h"
#include "exedump.h"
#include "extrnvar.h"
// Global variables set here, and used in EXEDUMP.C and OBJDUMP.C
BOOL fShowRelocations = FALSE;
BOOL fShowRawSectionData = FALSE;
BOOL fShowSymbolTable = FALSE;
BOOL fShowLineNumbers = FALSE;
char HelpText[] =
"PEDUMP - Win32/COFF .EXE/.OBJ file dumper - 1993 Matt Pietrek\n\n"
"Syntax: PEDUMP [switches] filename\n\n"
" /A include everything in dump\n"
" /H include hex dump of sections\n"
" /L include line number information\n"
" /R show base relocations\n"
" /S show symbol table\n";
// Open up a file, memory map it, and call the appropriate dumping routine
void DumpFile(LPSTR filename)
{
HANDLE hFile;
HANDLE hFileMapping;
LPVOID lpFileBase;
PIMAGE_DOS_HEADER dosHeader;
hFile = CreateFile(filename, GENERIC_READ, FILE_SHARE_READ, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
if ( hFile = = INVALID_HANDLE_VALUE )
{ printf("Couldn‘t open file with CreateFile()\n");
return; }
hFileMapping = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
if ( hFileMapping = = 0 )
{ CloseHandle(hFile);
printf("Couldn‘t open file mapping with CreateFileMapping()\n");
return; }
lpFileBase = MapViewOfFile(hFileMapping, FILE_MAP_READ, 0, 0, 0);
if ( lpFileBase = = 0 )
{
CloseHandle(hFileMapping);
CloseHandle(hFile);
printf("Couldn‘t map view of file with MapViewOfFile()\n");
return;
}
printf("Dump of file %s\n\n", filename);
dosHeader = (PIMAGE_DOS_HEADER)lpFileBase;
if ( dosHeader->e_magic = = IMAGE_DOS_SIGNATURE )
{ DumpExeFile( dosHeader ); }
else if ( (dosHeader->e_magic = = 0x014C) // Does it look like a i386
&& (dosHeader->e_sp = = 0) ) // COFF OBJ file???
{
// The two tests above aren‘t what they look like. They‘re
// really checking for IMAGE_FILE_HEADER.Machine = = i386 (0x14C)
// and IMAGE_FILE_HEADER.SizeOfOptionalHeader = = 0;
DumpObjFile( (PIMAGE_FILE_HEADER)lpFileBase );
}
else
printf("unrecognized file format\n");
UnmapViewOfFile(lpFileBase);
CloseHandle(hFileMapping);
CloseHandle(hFile);
}
// process all the command line arguments and return a pointer to
// the filename argument.
PSTR ProcessCommandLine(int argc, char *argv[])
{
int i;
for ( i=1; i < argc; i++ )
{
strupr(argv[i]);
// Is it a switch character?
if ( (argv[i][0] = = ‘-‘) || (argv[i][0] = = ‘/‘) )
{
if ( argv[i][1] = = ‘A‘ )
{ fShowRelocations = TRUE;
fShowRawSectionData = TRUE;
fShowSymbolTable = TRUE;
fShowLineNumbers = TRUE; }
else if ( argv[i][1] = = ‘H‘ )
fShowRawSectionData = TRUE;
else if ( argv[i][1] = = ‘L‘ )
fShowLineNumbers = TRUE;
else if ( argv[i][1] = = ‘R‘ )
fShowRelocations = TRUE;
else if ( argv[i][1] = = ‘S‘ )
fShowSymbolTable = TRUE;
}
else // Not a switch character. Must be the filename
{ return argv[i]; }
}
}
int main(int argc, char *argv[])
{
PSTR filename;
if ( argc = = 1 )
{ printf( HelpText );
return 1; }
filename = ProcessCommandLine(argc, argv);
if ( filename )
DumpFile( filename );
return 0;
}
Win32和PE基本概念
让我们研究一下渗透到PE文件设计中的一些基本思想(参见图1)。我将使用“模块”一词来表示已加载到内存中的可执行文件或DLL的代码,数据和资源。除了程序直接使用的代码和数据之外,模块还由Windows用于确定代码和数据在内存中位置的支持数据结构组成。在16位Windows中,支持的数据结构位于模块数据库(HMODULE所指的段)中。在Win32中,这些数据结构位于PE标头中,我将在稍后对此进行说明。
图1. PE文件格式
关于PE文件,首先要了解的是磁盘上的可执行文件与Windows加载该模块后的样子非常相似。Windows加载器不需要非常努力地从磁盘文件创建进程。加载程序使用内存映射文件机制将文件的相应部分映射到虚拟地址空间。使用建筑类比,PE文件就像一个预制房屋。它本质上是集成在一起的,然后进行少量工作以将其连接到世界其他地方(即,将其连接到其DLL等)。这种相同的加载简便性也适用于PE格式的DLL。加载模块后,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文件的最终概念是章节。PE文件中的部分大致相当于NE文件中的段或资源。部分包含代码或数据。与段不同,段是没有大小限制的连续内存块。有些部分包含您的程序直接声明和使用的代码或数据,而其他数据部分则由链接器和图书馆员为您创建,并且包含对操作系统至关重要的信息。在PE格式的某些描述中,部分也称为对象。术语“对象”具有许多重载的含义,以至于我将坚持调用代码和数据区域部分。
PE标题
像所有其他可执行文件格式一样,PE文件在已知(或易于查找)位置具有一组字段,这些字段定义文件其余部分的外观。该头文件包含诸如代码和数据区域的位置和大小,文件所针对的操作系统,初始堆栈大小以及我稍后将讨论的其他重要信息之类的信息。与Microsoft的其他可执行格式一样,此主标头也不在文件的开头。典型的PE文件的前几百个字节由MS-DOS存根占用。此存根是一个微型程序,可以打印出一些内容,效果是“此程序无法在MS-DOS模式下运行”。因此,如果您在不支持Win32的环境中运行基于Win32的程序,则会收到此提示性错误消息。当Win32加载程序内存映射PE文件时,映射文件的第一个字节对应于MS-DOS存根的第一个字节。那就对了。使用您启动的每个基于Win32的程序,您都可以免费下载一个基于MS-DOS的程序!
与其他Microsoft可执行文件格式一样,您可以通过查找存储在MS-DOS存根标题中的起始偏移量来找到真实的标题。WINNT.H文件包含用于MS-DOS存根头的结构定义,该结构定义使查找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;
视为ASCII文本的签名字段为“ PE \ 0 \ 0”。如果在MS-DOS标头中使用e_lfanew字段后,您在这里找到了NE签名而不是PE,那么您正在使用16位Windows NE文件。同样,签名字段中的LE将指示Windows 3.x虚拟设备驱动程序(VxD)。这里的LX是OS / 2 2.0文件的标记。
PE标头中的PE签名DWORD后面是IMAGE_FILE_HEADER类型的结构。此结构的字段仅包含有关文件的最基本信息。该结构似乎未从其原始COFF实现中进行任何修改。除了作为PE标头的一部分外,它还出现在Microsoft Win32编译器生成的COFF OBJ的开头。表2显示了IMAGE_FILE_HEADER的字段。
表2. IMAGE_FILE_HEADER字段
-
WORD Machine
该文件用于的CPU。定义了以下CPU ID:表格1 0x14d 英特尔i860 0x14c Intel I386(用于486和586的相同ID) 0x162 MIPS R3000 0x166 MIPS R4000 0x183 DEC Alpha AXP -
WORD NumberOfSections
文件中的节数。 -
DWORD TimeDateStamp
链接器(或OBJ文件的编译器)生成此文件的时间。此字段保存自1969年12月31日下午4:00以来的秒数。 -
DWORD PointerToSymbolTable
COFF符号表的文件偏移量。此字段仅在具有COFF调试信息的OBJ文件和PE文件中使用。PE文件支持多种调试格式,因此调试器应参考数据目录(稍后定义)中的IMAGE_DIRECTORY_ENTRY_DEBUG条目。 -
DWORD NumberOfSymbols
COFF符号表中的符号数。往上看。 -
WORD SizeOfOptionalHeader
可以遵循此结构的可选标头的大小。在OBJ中,该字段为0。在可执行文件中,它是紧随该结构的IMAGE_OPTIONAL_HEADER结构的大小。 -
WORD Characteristics
带有有关文件信息的标志。一些重要领域:表2 0x0001该文件中没有重定位0x0002文件是可执行映像(不是OBJ或LIB)0x2000文件是动态链接库,不是程序
其他字段在WINNT.H中定义
PE标头的第三个组件是IMAGE_OPTIONAL_HEADER类型的结构。对于PE文件,此部分当然不是可选的。COFF格式允许单个实现定义标准IMAGE_FILE_HEADER之外的其他信息的结构。PE设计人员认为IMAGE_OPTIONAL_HEADER中的字段是关键信息,而不是IMAGE_FILE_HEADER中的基本信息。
了解IMAGE_OPTIONAL_HEADER的所有字段并不一定很重要(请参见图4)。需要注意的更重要的是ImageBase和Subsystem字段。您可以浏览或跳过字段的描述。
表3. IMAGE_OPTIONAL_HEADER字段
-
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生成的可执行文件中,默认映像库为0x10000。对于DLL,默认值为0x400000。在Windows 95中,地址0x10000不能用于加载32位EXE,因为它位于所有进程共享的线性地址区域内。因此,Microsoft已将Win32可执行文件的默认基地址更改为0x400000。 -
DWORD SectionAlignment
当映射到内存中时,保证每个部分都从一个虚拟地址开始,该地址是该值的倍数。出于分页目的,默认节对齐方式为0x1000。 -
DWORD FileAlignment
在PE文件中,保证每个部分的原始数据都以该值的倍数开头。默认值是0x200字节,可能是为了确保节始终从磁盘扇区的开头开始(长度也为0x200字节)。该字段等效于NE文件中的段/资源对齐大小。与NE文件不同,PE文件通常没有数百个部分,因此对齐文件部分所浪费的空间几乎总是很小。 -
WORD MajorOperatingSystemVersion
WORD MinorOperatingSystemVersion
使用此可执行文件所需的最低操作系统版本。该字段有些模棱两可,因为子系统字段(以后几个字段)似乎起着类似的作用。迄今为止,此字段在所有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
这似乎是加载程序必须担心的图像部分的总大小。它是从图像底部开始直到最后一节结束的区域大小。最后一部分的末尾四舍五入到该部分对齐的最接近倍数。 -
DWORD SizeOfHeaders
PE标头和节(对象)表的大小。这些部分的原始数据在所有标题组件之后立即开始。 -
DWORD CheckSum
可能是文件的CRC校验和。与其他Microsoft可执行文件格式一样,此字段将被忽略并设置为0。此规则的一个例外是受信任的服务,并且这些EXE必须具有有效的校验和。 -
WORD Subsystem
该可执行文件用于其用户界面的子系统的类型。WINNT.H定义以下值:表3 原生1个不需要子系统(例如设备驱动程序)WINDOWS_GUI2在Windows GUI子系统中运行WINDOWS_CUI3在Windows字符子系统(控制台应用程序)中运行OS2_CUI5在OS / 2字符子系统中运行(仅OS / 2 1.x应用程序)POSIX_CUI7在Posix字符子系统中运行 -
WORD DllCharacteristics
一组标志,指示在什么情况下将调用DLL的初始化函数(例如DllMain)。该值似乎总是设置为0,但是操作系统仍然为所有四个事件调用DLL初始化函数。
定义了以下值:
1个 | DLL首次加载到进程的地址空间时调用 |
2 | 线程终止时调用 |
4 | 线程启动时调用 |
8 | DLL退出时调用 |
-
DWORD SizeOfStackReserve
为初始线程的堆栈保留的虚拟内存量。但是,并非所有的内存都已提交(请参见下一个字段)。该字段默认为0x100000(1MB)。如果将0指定为CreateThread的堆栈大小,则生成的线程也将具有相同大小的堆栈。 -
DWORD SizeOfStackCommit
最初为初始线程的堆栈提交的内存量。对于Microsoft链接器,此字段默认为0x1000字节(1页),而TLINK32将其设置为两页。 -
DWORD SizeOfHeapReserve
为初始进程堆保留的虚拟内存量。可以通过调用GetProcessHeap获得此堆的句柄。并非所有的内存都已提交(请参见下一个字段)。 -
DWORD SizeOfHeapCommit
最初在进程堆中提交的内存量。默认为一页。 -
DWORD LoaderFlags
从WINNT.H,这些似乎是与调试支持相关的字段。我从未见过启用了这些位的可执行文件,也不清楚如何使链接器对其进行设置。定义了以下值:表5 1。 在开始过程之前调用断点指令 2。 加载进程后在该进程上调用调试器 -
DWORD NumberOfRvaAndSizes
DataDirectory数组中的条目数(如下)。当前工具始终将此值设置为16。 -
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]
IMAGE_DATA_DIRECTORY结构的数组。初始数组元素包含开始的RVA和可执行文件重要部分的大小。数组末尾的某些元素当前未使用。数组的第一个元素始终是导出函数表的地址和大小(如果存在)。第二个数组条目是导入的函数表的地址和大小,依此类推。有关已定义数组条目的完整列表,请参见WINNT.H中的IMAGE_DIRECTORY_ENTRY_XXX #defines。该数组使加载程序可以快速找到映像的特定部分(例如,导入的功能表),而无需遍历每个映像部分,并在进行过程中比较名称。大多数数组条目描述了整个节的数据。然而,
截面表
在PE标头和图像节的原始数据之间是节表。分区表本质上是一个电话簿,其中包含有关图像中每个分区的信息。图像中的部分按其起始地址(RVA)而不是按字母顺序排序。
现在,我可以更好地阐明什么是节。在NE文件中,程序的代码和数据存储在文件中不同的“段”中。NE标头的一部分是结构数组,程序使用的每个段都包含一个结构。数组中的每个结构都包含有关一个段的信息。存储的信息包括段的类型(代码或数据),其大小以及其在文件中其他位置的位置。在PE文件中,节表类似于NE文件中的段表。但是,与NE文件段表不同,PE节表不存储每个代码或数据块的选择器值。而是,每个节表条目都存储一个地址,文件的原始数据已映射到该地址中。尽管段类似于32位段,但它们实际上并不是单独的段。他们‘
PE文件与NE文件不同的另一个方面是它们如何管理程序不使用而操作系统使用的支持数据。例如,可执行文件使用的DLL列表或修正表的位置。在NE文件中,资源不视为段。即使为它们分配了选择器,有关资源的信息也不会存储在NE标头的段表中。取而代之的是,在NE头的末尾将资源降级到一个单独的表中。有关导入和导出功能的信息也不保证其属于自己。它塞在NE头中。
PE文件的故事不同。任何可能被视为重要的代码或数据的内容都存储在完整的部分中。因此,有关导入功能的信息以及模块导出的功能表都存储在其自己的部分中。重定位数据也是如此。程序或操作系统可能需要的任何代码或数据都有自己的部分。
在讨论特定部分之前,需要描述操作系统用来管理这些部分的数据。紧随内存中PE头之后是IMAGE_SECTION_HEADER的数组。PE头(IMAGE_NT_HEADER.FileHeader.NumberOfSections字段)中给出了此数组中元素的数量。我使用PEDUMP输出节表以及该节的所有字段和属性。图5显示了典型EXE文件的节表的PEDUMP输出,图6显示了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中描述的格式。有趣的是,要注意每个部分存储的信息中缺少的内容。首先,请注意,没有任何PRELOAD属性的指示。NE文件格式允许您使用PRELOAD属性指定在模块加载时应加载哪些段。OS /2®2.0 LX格式具有类似的功能,允许您最多指定八个页面进行预加载。PE格式没有这样的东西。Microsoft必须对Win32按需分页加载的性能充满信心。
表6. IMAGE_SECTION_HEADER格式
-
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]
这是一个8字节的ANSI名称(不是UNICODE),用于命名该部分。大多数节名称以开头。(例如“ .text”),但这不是必需的,因为某些PE文档可能会让您相信。您可以使用汇编语言中的segment指令或Microsoft C / C ++编译器中的“ #pragma data_seg”和“ #pragma code_seg”来命名自己的节。重要的是要注意,如果段名占用了完整的8个字节,则没有NULL终止符字节。如果您是printf的奉献者,则可以使用%.8s避免将名称字符串复制到另一个可以用NULL终止的缓冲区。 -
union {
DWORD PhysicalAddress
DWORD VirtualSize
} Misc;
在EXE或OBJ中,此字段具有不同的含义。在EXE中,它保存代码或数据的实际大小。这是四舍五入到最接近的文件对齐倍数之前的大小。结构中的稍后的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)一起设置。
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格式还缺少页表的概念。LX格式的IMAGE_SECTION_HEADER的OS / 2等效项不会直接指向可以在文件中找到节的代码或数据的位置。相反,它引用页面查找表,该表指定节中页面的特定范围的属性和位置。PE格式无需执行所有操作,并确保将节的数据连续存储在文件中。在这两种格式中,LX方法可以提供更大的灵活性,但是PE样式明显更简单并且更易于使用。为两种格式编写了文件转储程序后,我可以为此提供保证!
PE格式的另一个受欢迎的变化是项目的位置存储为简单的DWORD偏移量。在NE格式中,几乎所有位置都存储为扇区值。要找到实际的偏移量,您需要首先在NE标头中查找对齐单元的大小,然后将其转换为扇区大小(通常为16或512字节)。然后,您需要将扇区大小乘以指定的扇区偏移量以获得实际的文件偏移量。如果偶然没有将某些内容作为扇区偏移量存储在NE文件中,则可能将其存储为相对于NE标头的偏移量。由于NE标头不在文件的开头,因此需要在代码中拖动NE标头的文件偏移量。总而言之,PE格式比NE,LX或LE格式更容易使用(假设您可以使用内存映射文件)。
共同部分
了解了一般的部分以及它们的位置之后,让我们看一下在EXE和OBJ文件中可以找到的常见部分。该列表绝不是完整的,而是包括您每天遇到的部分(即使您不知道)。
.text部分是编译器或汇编器发出的所有通用代码的结尾。由于PE文件以32位模式运行,并且不限于16位段,因此没有理由将代码从单独的源文件分解为单独的部分。而是,链接器将来自各个OBJ的所有.text节串联为EXE中的一个大.text节。如果使用Borland C ++,则编译器会将其代码发射到名为CODE的段中。用Borland C ++生成的PE文件有一个名为CODE的部分,而不是名为.text的部分。我会在稍后解释。
对于我来说,发现.text节中还有其他代码比我用编译器创建或在运行时库中使用的代码要有趣得多。在PE文件中,当您调用另一个模块中的函数(例如USER32.DLL中的GetMessage)时,编译器发出的CALL指令不会将控制权直接转移到DLL中的函数(请参见图8)。相反,调用指令将控制权转移到
JMP DWORD PTR [XXXXXXXX]
.text部分中的指令。JMP指令通过.idata节中的DWORD变量进行间接调用。该.idata节DWORD包含操作系统功能入口点的实际地址。考虑了一段时间后,我开始理解为什么以这种方式实现DLL调用。通过在一个位置集中对给定DLL函数的所有调用,加载程序无需修补所有调用DLL的指令。PE加载程序要做的只是将目标函数的正确地址放入.idata节中的DWORD中。无需修补呼叫说明。这与NE文件形成鲜明对比,NE文件中的每个段都包含需要应用于该段的修正列表。如果该段调用给定的DLL函数20次,加载程序必须将该函数的地址写入该段20次。PE方法的缺点是您不能使用DLL函数的真实地址来初始化变量。例如,您会认为
图2.在另一个模块中调用一个函数
FARPROC pfnGetMessage = GetMessage;
将把GetMessage的地址放入变量pfnGetMessage中。在16位Windows中,此方法有效,而在Win32中则无效。在Win32中,变量pfnGetMessage最终将保存我前面提到的JMP DWORD PTR [XXXXXXXX]重排的地址。如果您想通过函数指针进行调用,那么事情将按预期进行。但是,如果您想读取GetMessage开头的字节,则很不走运(除非您自己做一些额外的工作来遵循.idata“指针”)。稍后,在讨论导入表时,我将回到该主题。
尽管Borland本可以让编译器发出名称为.text的段,但它选择了默认的段名称CODE。为了确定PE文件中的节名,Borland链接器(TLINK32.EXE)从OBJ文件中获取节名,并将其截断为8个字符(如有必要)。
尽管部分名称的区别很小,但是Borland PE文件如何链接到其他模块还有一个更重要的区别。正如我在.text描述中提到的,对OBJ的所有调用都要通过JMP DWORD PTR [XXXXXXXX]转换。在Microsoft系统下,此文件来自导入库的.text部分。因为库管理器(LIB32)在链接外部DLL时会创建导入库(和thunk),所以链接器不必“知道”如何自行生成这些thunk。导入库实际上只是一些更多的代码和数据,可以链接到PE文件中。
Borland处理导入功能的系统只是对16位NE文件的处理方式的扩展。Borland链接器使用的导入库实际上只是函数名称列表以及它们所在的DLL的名称。因此TLINK32负责确定哪些修复程序针对外部DLL,并生成适当的JMP DWORD PTR [XXXXXXXX ]为此大声疾呼。TLINK32将它创建的thunk存储在名为.icode的部分中。
正如.text是代码的默认部分一样,.data部分是初始化数据所在的位置。此数据包含在编译时初始化的全局变量和静态变量。它还包括字符串文字。链接器将OBJ和LIB文件中的所有.data节组合为EXE中的一个.data节。局部变量位于线程的堆栈上,并且在.data或.bss节中不占空间。
.bss节是存储所有未初始化的静态和全局变量的位置。链接器将OBJ和LIB文件中的所有.bss部分组合为EXE中的一个.bss部分。在节表中,.bss节的RawDataOffset字段设置为0,指示此节不占用文件中的任何空间。TLINK不会发出此部分。相反,它扩展了DATA节的虚拟大小。
.CRT是Microsoft C / C ++运行时库使用的另一个初始化的数据部分(因此而得名)。为什么这些数据不能进入标准.data部分,这超出了我的范围。
.rsrc节包含该模块的所有资源。在Windows NT的早期,16位RC.EXE的RES文件输出不是Microsoft PE链接器可以理解的格式。CVTRES程序将这些RES文件转换为COFF格式的OBJ,将资源数据放入OBJ中的.rsrc节。然后,链接器可以将资源OBJ视为要链接的另一个OBJ,从而使链接器不会“知道”有关资源的任何特殊信息。Microsoft的最新链接器似乎能够直接处理RES文件。
.idata节包含有关模块从其他DLL导入的功能(和数据)的信息。本部分等效于NE文件的模块参考表。主要区别在于,本节专门列出了PE文件导入的每个功能。要在NE文件中找到等效信息,您必须仔细研究每个段原始数据末尾的重定位。
.edata部分是PE文件为其他模块导出的功能和数据的列表。其NE文件等效项是条目表,居民名称表和非居民名称表的组合。与16位Windows不同,很少有理由从EXE文件中导出任何内容,因此通常只在DLL中看到.edata部分。使用Microsoft工具时,.edata部分中的数据通过EXP文件进入PE文件。换句话说,链接器不会自行生成此信息。而是依靠库管理器(LIB32)扫描OBJ文件并创建EXP文件,链接器将其添加到要链接的模块列表中。是的,这是对的!这些讨厌的EXP文件实际上只是具有不同扩展名的OBJ文件。
.reloc节包含一个基本重定位表。基本重定位是对指令或初始化变量值的调整,如果加载程序无法在链接器认为的位置加载文件,则需要进行重定位。如果加载程序能够在链接器的首选基址上加载映像,则加载程序将完全忽略此部分中的重定位信息。如果您想抓住机会并希望加载程序始终可以在假定的基址上加载映像,则可以告诉链接程序使用/ FIXED选项剥离此信息。尽管这可能会节省可执行文件中的空间,但可能会导致可执行文件无法在其他基于Win32的实现中使用。例如,假设您为Windows NT构建了一个EXE,并将EXE基于0x10000。如果您告诉链接器剥离重定位,则EXE不会
重要的是要注意,编译器生成的JMP和CALL指令使用相对于指令的偏移量,而不是32位平面段中的实际偏移量。如果需要将图像加载到链接器所假定的基址之外的其他位置,则这些指令不需要更改,因为它们使用相对寻址。因此,重定位没有您想象的那么多。通常仅对于对某些数据使用32位偏移量的指令才需要重定位。例如,假设您具有以下全局变量声明:
int i;
int *ptr = &i;
如果链接器假定图像基数为0x10000,则变量i的地址最终将包含0x12004之类的内容。在用于保存指针“ ptr”的内存中,链接器将写出0x12004,因为那是变量i的地址。如果加载程序出于任何原因决定将文件加载到基址0x70000,则i的地址将为0x72004。.reloc节是映像中需要考虑链接器假定的加载地址和实际加载地址之间的差异的位置的列表。
使用编译器指令_ _declspec(thread)时,您定义的数据不会进入.data或.bss部分。它以.tls节结尾,该节引用“线程本地存储”,并与Win32函数的TlsAlloc系列有关。在处理.tls节时,内存管理器会设置页表,以便每当进程切换线程时,就会将一组新的物理内存页映射到.tls节的地址空间。这允许每个线程的全局变量。在大多数情况下,使用这种机制要比按线程分配内存并将其指针存储在TlsAlloc插槽中要容易得多。
关于.tls部分和_ _declspec(thread)变量,必须添加一个不幸的注意事项。在Windows NT和Windows 95中,如果DLL由LoadLibrary动态加载,则该线程本地存储机制在DLL中将不起作用。在EXE或隐式加载的DLL中,一切正常。如果您不能隐式链接到DLL,但需要每个线程的数据,则必须回退使用动态分配内存的TlsAlloc和TlsGetValue。
尽管.rdata节通常介于.data和.bss节之间,但是您的程序通常在此节中看不到或使用数据。.rdata节至少用于两件事。首先,在Microsoft链接程序生成的EXE中,.rdata节保存调试目录,该目录仅存在于EXE文件中。(在TLINK32 EXE中,调试目录位于名为.debug的节中。)调试目录是IMAGE_DEBUG_DIRECTORY结构的数组。这些结构包含有关文件中存储的各种调试信息的类型,大小和位置的信息。出现三种主要的调试信息类型:CodeView®,COFF和FPO。图9显示了典型调试目录的PEDUMP输出。
表7.典型的调试目录
类型 | 尺寸 | 地址 | FilePtr | 性格 | 时间数据 | 版 | |
关闭 | 000065C5 | 00000000 | 00009200 | 00000000 | 2CF8CF3D | 0.00 | |
??? | 00000114 | 00000000 | 0000F7C8 | 00000000 | 2CF8CF3D | 0.00 | |
FPO | 000004B0 | 00000000 | 0000F8DC | 00000000 | 2CF8CF3D | 0.00 | |
代码视图 | 0000B0B4 | 00000000 | 0000FD8C | 00000000 | 2CF8CF3D | 0.00 |
调试目录不一定位于.rdata节的开头。要查找调试目录表的开始,请在数据目录的第七项(IMAGE_DIRECTORY_ENTRY_DEBUG)中使用RVA。数据目录位于文件的PE标头部分的末尾。要确定Microsoft链接器生成的调试目录中的条目数,请用调试目录的大小(在数据目录项的size字段中找到)除以IMAGE_DEBUG_DIRECTORY结构的大小。TLINK32发出一个简单的计数,通常为1。PEDUMP示例程序演示了这一点。
.rdata节的另一个有用部分是描述字符串。如果在程序的DEF文件中指定了Description条目,则指定的描述字符串将显示在.rdata节中。在NE格式中,描述字符串始终是非居民名称表的第一项。描述字符串用于保存描述文件的有用文本字符串。不幸的是,我还没有找到一种简单的方法来找到它。我已经看到了在调试目录之前具有描述字符串的PE文件,以及在调试目录之后具有描述字符串的其他文件。我不知道找到描述字符串的任何一致方法(甚至根本不存在)。
这些.debug $ S和.debug $ T部分仅出现在OBJ中。它们存储CodeView符号和类型信息。节名称是从以前的16位编译器($$ SYMBOLS和$$ TYPES)用于此目的的节名称中得出的。.debug $ T节的唯一目的是保留PDB文件的路径名,该文件名包含项目中所有OBJ的CodeView信息。链接器读取PDB并使用它来创建CodeView信息的一部分,并将其放置在完成的PE文件的末尾。
.drective部分仅出现在OBJ文件中。它包含链接程序命令的文本表示形式。例如,在我使用Microsoft编译器编译的任何OBJ中,.drectve部分中都会出现以下字符串:
-defaultlib:LIBC -defaultlib:OLDNAMES
当您在代码中使用_ _declspec(export)时,编译器仅向.drectve部分发出等效的命令行(例如,“-export:MyFunction”)。
在玩PEDUMP时,我会不时遇到其他部分。例如,在Windows 95 KERNEL32.DLL中,有LOCKCODE和LOCKDATA节。大概这些部分将得到特殊的分页处理,因此它们永远不会被分页出内存。
从中可以吸取两个教训。首先,不要只使用编译器或汇编器提供的标准部分。如果出于某种原因需要单独的部分,请不要犹豫创建自己的部分。在C / C ++编译器中,使用#pragma code_seg和#pragma data_seg。用汇编语言,只需创建一个名称与标准节不同的32位节(将成为节)即可。如果使用TLINK32,则必须使用其他类或关闭代码段打包。要记住的另一件事是,与众不同的部分名称通常可以更深入地了解特定PE文件的目的和实现。
PE文件导入
之前,我描述了如何对外部DLL的函数调用不会直接调用DLL。而是将CALL指令转到可执行文件的.text节(如果使用Borland C ++,则为.icode节)中的JMP DWORD PTR [XXXXXXXX]指令。JMP指令查找并将控制权转移到的地址是实际的目标地址。PE文件的.idata节包含加载程序确定目标函数的地址并将其修补到可执行映像中所需的信息。
.idata节(或导入表,我更喜欢称呼它)以IMAGE_IMPORT_DESCRIPTORs数组开头。PE文件隐式链接到的每个DLL都有一个IMAGE_IMPORT_DESCRIPTOR。没有字段指示此数组中的结构数。相反,该数组的最后一个元素由IMAGE_IMPORT_DESCRIPTOR指示,该字段的字段填充为NULL。IMAGE_IMPORT_DESCRIPTOR的格式如图10所示。
表8. IMAGE_IMPORT_DESCRIPTOR格式
-
DWORD Characteristics
一次可能是一组标志。但是,Microsoft更改了其含义,从不费心更新WINNT.H。该字段实际上是指针数组的偏移量(RVA)。这些指针均指向IMAGE_IMPORT_BY_NAME结构。 -
DWORD TimeDateStamp
指示文件建立时间的时间/日期戳。 -
DWORD ForwarderChain
此字段与转发有关。转发涉及到一个DLL,它将对它的功能之一的引用发送到另一个DLL。例如,在Windows NT中,NTDLL.DLL似乎会将其某些导出的功能转发到KERNEL32.DLL。应用程序可能认为它正在调用NTDLL.DLL中的一个函数,但实际上实际上最终是调用KERNEL32.DLL。该字段包含FirstThunk数组的索引(暂时描述)。该字段索引的函数将被转发到另一个DLL。不幸的是,没有记录如何转发函数的格式,并且很难找到转发函数的示例。 -
DWORD Name
这是包含导入的DLL名称的NULL终止ASCII字符串的RVA。常见的示例是“ KERNEL32.DLL”和“ USER32.DLL”。 -
PIMAGE_THUNK_DATA FirstThunk
该字段是IMAGE_THUNK_DATA联合的偏移量(RVA)。在几乎每种情况下,联合都被解释为指向IMAGE_IMPORT_BY_NAME结构的指针。如果该字段不是这些指针之一,则应将其视为要导入的DLL的导出序数值。从文档中尚不清楚您是否真的可以按序而不是按名称导入函数。
IMAGE_IMPORT_DESCRIPTOR的重要部分是导入的DLL名称和IMAGE_IMPORT_BY_NAME指针的两个数组。在EXE文件中,两个数组(由Characteristics和FirstThunk字段指向)彼此平行运行,并在每个数组末尾由NULL指针条目终止。两个数组中的指针都指向IMAGE_IMPORT_BY_NAME结构。图11以图形方式显示了这种情况。图12显示了导入表的PEDUMP输出。
图3.指针的两个并行数组
表9.从EXE文件导入表
GDI32.dll
Hint/Name Table: 00013064
TimeDateStamp: 2C51B75B
ForwarderChain: FFFFFFFF
First thunk RVA: 00013214
Ordn Name
48 CreatePen
57 CreateSolidBrush
62 DeleteObject
160 GetDeviceCaps
// Rest of table omitted...
KERNEL32.dll
Hint/Name Table: 0001309C
TimeDateStamp: 2C4865A0
ForwarderChain: 00000014
First thunk RVA: 0001324C
Ordn Name
83 ExitProcess
137 GetCommandLineA
179 GetEnvironmentStrings
202 GetModuleHandleA
// Rest of table omitted...
SHELL32.dll
Hint/Name Table: 00013138
TimeDateStamp: 2C41A383
ForwarderChain: FFFFFFFF
First thunk RVA: 000132E8
Ordn Name
46 ShellAboutA
USER32.dll
Hint/Name Table: 00013140
TimeDateStamp: 2C474EDF
ForwarderChain: FFFFFFFF
First thunk RVA: 000132F0
Ordn Name
10 BeginPaint
35 CharUpperA
39 CheckDlgButton
40 CheckMenuItem
// Rest of table omitted...
PE文件导入的每个功能都有一个IMAGE_IMPORT_BY_NAME结构。IMAGE_IMPORT_BY_NAME结构非常简单,看起来像这样:
WORD Hint;
BYTE Name[?];
第一个字段是关于导入功能的导出序号是什么的最佳猜测。与NE文件不同,此值不必正确。取而代之的是,加载程序将其用作对导出的函数进行二进制搜索的建议起始值。接下来是带有导入函数名称的ASCIIZ字符串。
为什么有两个并行的指向IMAGE_IMPORT_BY_NAME结构的指针数组?第一个数组(“特征”字段所指向的数组)将保持不变,并且不会修改。有时称为提示名称表。PE加载程序将覆盖第二个数组(由FirstThunk字段指向)。加载程序循环访问数组中的每个指针,并找到每个IMAGE_IMPORT_BY_NAME结构所引用的函数的地址。然后,加载程序会使用找到的函数的地址覆盖指向IMAGE_IMPORT_BY_NAME的指针。JMP DWORD PTR [XXXXXXXX] thunk的[XXXXXXXX]部分引用FirstThunk数组中的一项。由于装入程序覆盖的指针数组最终会保存所有导入函数的地址,因此称为导入地址表。
对于您的Borland用户而言,以上描述略有不同。TLINK32生成的PE文件缺少阵列之一。在这样的可执行文件中,IMAGE_IMPORT_DESCRIPTOR(也称为提示名称数组)中的Characteristics字段为0。因此,只能保证在所有PE文件中都存在FirstThunk字段(导入地址表)所指向的数组。故事到此结束,除了在编写PEDUMP时遇到一个有趣的问题。在永无止境的优化搜索中,Microsoft“优化”了Windows NT系统DLL(KERNEL32.DLL等)中的thunk数组。在此优化中,数组中的指针不指向IMAGE_IMPORT_BY_NAME结构,而是已经包含导入函数的地址。换句话说,装载机不会 无需查找函数地址,并使用导入的函数地址覆盖thunk数组。这会导致PE转储程序出现问题,这些程序期望该数组包含指向IMAGE_IMPORT_BY_NAME结构的指针。您可能会想,“但是,马特,为什么不只使用提示名称表数组?” 这是一个理想的解决方案,除了在Borland文件中不存在提示名称表数组之外。PEDUMP程序可以处理所有这些情况,但是代码混乱是可以理解的。这是一个理想的解决方案,除了在Borland文件中不存在提示名称表数组之外。PEDUMP程序可以处理所有这些情况,但是代码混乱是可以理解的。这是一个理想的解决方案,除了在Borland文件中不存在提示名称表数组之外。PEDUMP程序可以处理所有这些情况,但是代码混乱是可以理解的。
由于导入地址表位于可写部分,因此拦截EXE或DLL对另一个DLL的调用相对容易。只需修补适当的导入地址表条目以指向所需的拦截功能。无需修改调用方或被调用方图像中的任何代码。有什么会更容易?
有趣的是,在Microsoft生产的PE文件中,导入表不是链接器完全合成的。在另一个DLL中调用函数的所有必要步骤都驻留在导入库中。链接DLL时,库管理器(LIB32.EXE或LIB.EXE)将扫描正在链接的OBJ文件并创建导入库。该导入库与16位NE文件链接器使用的导入库完全不同。32位LIB生成的导入库具有一个.text节和几个.idata $节。导入库中的.text部分包含JMP DWORD PTR [XXXXXXXX] thunk,其名称已存储在OBJ的符号表中。该符号的名称与DLL导出的函数的名称相同(例如,_Dispatch_Message @ 4)。其中一个 。导入库中的idata $部分包含thunk取消引用的DWORD。.idata $节中的另一个节在提示序号后面有一个空格,后跟导入函数的名称。这两个字段构成了IMAGE_IMPORT_BY_NAME结构。以后链接使用导入库的PE文件时,导入库的部分将添加到链接器需要处理的OBJ的部分列表中。由于导入库中的thunk与要导入的函数具有相同的名称,因此链接器认为thunk实际上是导入的函数,并修复了对导入函数的调用以指向thunk。导入库中的thunk本质上被“视为”作为导入函数。idata $节中的提示序号空间后跟导入函数的名称。这两个字段构成了IMAGE_IMPORT_BY_NAME结构。以后链接使用导入库的PE文件时,导入库的部分将添加到链接器需要处理的OBJ的部分列表中。由于导入库中的thunk与要导入的函数具有相同的名称,因此链接器认为thunk实际上是导入的函数,并修复了对导入函数的调用以指向thunk。导入库中的thunk本质上被“视为”作为导入函数。idata $节中的提示序号空间后跟导入函数的名称。这两个字段构成了IMAGE_IMPORT_BY_NAME结构。以后链接使用导入库的PE文件时,导入库的部分将添加到链接器需要处理的OBJ的部分列表中。由于导入库中的thunk与要导入的函数具有相同的名称,因此链接器认为thunk实际上是导入的函数,并修复了对导入函数的调用以指向thunk。导入库中的thunk本质上被“视为”作为导入函数。s的部分已添加到链接器需要处理的OBJ的部分列表中。由于导入库中的thunk与要导入的函数具有相同的名称,因此链接器认为thunk实际上是导入的函数,并修复了对导入函数的调用以指向thunk。导入库中的thunk本质上被“视为”作为导入函数。s的部分已添加到链接器需要处理的OBJ的部分列表中。由于导入库中的thunk与要导入的函数具有相同的名称,因此链接器认为thunk实际上是导入的函数,并修复了对导入函数的调用以指向thunk。导入库中的thunk本质上被“视为”作为导入函数。
除了提供导入函数thunk的代码部分之外,导入库还提供PE文件的.idata节(或导入表)的片段。这些片段来自库管理器放入导入库的各个.idata $部分。简而言之,链接器并不真正了解导入的函数与出现在不同OBJ文件中的函数之间的区别。链接器仅遵循其用于构建和组合节的预设规则,所有内容自然而然地就位。
PE文件导出
与导入功能相反,导出功能供EXE或其他DLL使用。PE文件在.edata部分中存储有关其导出功能的信息。通常,Microsoft链接程序生成的PE EXE文件不会导出任何内容,因此它们没有.edata节。Borland的TLINK32始终从EXE导出至少一个符号。大多数DLL都具有导出功能,并且具有.edata节。.edata节(也称为导出表)的主要组成部分是函数名称,入口点地址和导出序号值的表。在NE文件中,导出表的等效项是条目表,居民名称表和非居民名称表。这些表存储为NE标头的一部分,而不是存储在不同的段或资源中。
.edata节的开头是IMAGE_EXPORT_DIRECTORY结构(请参见表10)。该结构后紧跟着结构中的字段所指向的数据。
表10. IMAGE_EXPORT_DIRECTORY格式
-
DWORD Characteristics
该字段似乎未使用,并且始终设置为0。 -
DWORD TimeDateStamp
指示此文件创建时间的时间/日期戳。 -
WORD MajorVersion
WORD MinorVersion
这些字段似乎未使用,并设置为0。 -
DWORD Name
具有此DLL名称的ASCIIZ字符串的RVA。 -
DWORD Base
导出函数的起始序号。例如,如果文件导出函数的序号分别为10、11和12,则此字段包含10。要获取函数的导出序号,您需要将此值添加到AddressOfNameOrdinals数组的适当元素中。 -
DWORD NumberOfFunctions
AddressOfFunctions数组中的元素数。此值也是此模块导出的功能数。从理论上讲,此值可以与NumberOfNames字段(下一个)不同,但实际上它们始终相同。 -
DWORD NumberOfNames
AddressOfNames数组中的元素数。该值似乎总是与NumberOfFunctions字段相同,并且导出的函数数也是如此。 -
PDWORD *AddressOfFunctions
该字段是RVA,指向功能地址数组。功能地址是此模块中每个导出功能的入口点(RVA)。 -
PDWORD *AddressOfNames
该字段是RVA,指向字符串指针数组。字符串是此模块中导出函数的名称。 -
PWORD *AddressOfNameOrdinals
此字段是RVA,指向WORD数组。WORD是此模块中所有导出功能的导出序号。但是,不要忘记在“基本”字段中添加起始序号。
导出表的布局有些奇怪(请参见图4和表10)。如前所述,导出函数的要求是名称,地址和导出序数。您可能会认为,PE格式的设计人员会将所有这三个项目都放入一个结构中,然后具有这些结构的数组。而是,导出条目的每个组件都是数组中的一个元素。这些数组有三个(AddressOfFunctions,AddressOfNames,AddressOfNameOrdinals),并且它们彼此平行。要查找有关第四个函数的所有信息,您需要在每个数组中查找第四个元素。
图4.导出表布局
表11. EXE文件的典型导出表
Name: KERNEL32.dll
Characteristics: 00000000
TimeDateStamp: 2C4857D3
Version: 0.00
Ordinal base: 00000001
# of functions: 0000021F
# of Names: 0000021F
Entry Pt Ordn Name
00005090 1 AddAtomA
00005100 2 AddAtomW
00025540 3 AddConsoleAliasA
00025500 4 AddConsoleAliasW
00026AC0 5 AllocConsole
00001000 6 BackupRead
00001E90 7 BackupSeek
00002100 8 BackupWrite
0002520C 9 BaseAttachCompleteThunk
00024C50 10 BasepDebugDump
// Rest of table omitted...
顺便说一句,如果您转储了Windows NT系统DLL(例如KERNEL32.DLL和USER32.DLL)的导出,您会注意到,在许多情况下,有两个函数的结尾仅相差一个字符名称,例如CreateWindowExA和CreateWindowExW。这就是透明地实现UNICODE支持的方式。以A结尾的功能是ASCII(或ANSI)兼容功能,而以W结尾的功能是该功能的UNICODE版本。在您的代码中,您没有明确指定要调用的函数。而是通过预处理程序#ifdefs在WINDOWS.H中选择适当的功能。Windows NT WINDOWS.H的摘录显示了此工作原理的示例:
#ifdef UNICODE
#define DefWindowProc DefWindowProcW
#else
#define DefWindowProc DefWindowProcA
#endif // !UNICODE
PE文件资源
在PE文件中查找资源比在NE文件中复杂得多。各个资源的格式(例如,菜单)没有显着变化,但是您需要遍历一个奇怪的层次结构才能找到它们。
浏览资源目录层次结构就像浏览硬盘一样。有一个主目录(根目录),其中包含子目录。子目录具有自己的子目录,这些子目录可以指向诸如对话框模板之类的原始资源数据。在PE格式中,资源目录层次结构的根目录及其所有子目录都是IMAGE_RESOURCE_DIRECTORY类型的结构(请参见表12)。
表12. IMAGE_RESOURCE_DIRECTORY格式
-
DWORD Characteristics
从理论上讲,该字段可以保留资源的标志,但始终显示为0。 -
DWORD TimeDateStamp
描述资源创建时间的时间/日期戳。 -
WORD MajorVersion
WORD MinorVersion
理论上,这些字段将保存资源的版本号。这些字段似乎总是设置为0。
WORD NumberOfNamedEntries
使用名称并遵循此结构的数组元素的数量。
-
WORD NumberOfIdEntries
使用整数ID且遵循此结构的数组元素的数量。 -
IMAGE_RESOURCE_DIRECTORY_ENTRY DirectoryEntries[]
该字段实际上不是IMAGE_RESOURCE_DIRECTORY结构的一部分。相反,它是紧随IMAGE_RESOURCE_DIRECTORY结构的IMAGE_RESOURCE_DIRECTORY_ENTRY结构的数组。数组中元素的数量是NumberOfNamedEntries和NumberOfIdEntries字段的总和。具有名称标识符(而不是整数ID)的目录条目元素在数组中排在第一位。
目录条目可以指向子目录(即另一个IMAGE_RESOURCE_DIRECTORY),也可以指向资源的原始数据。通常,在获取实际的原始资源数据之前,至少有三个目录级别。顶层目录(只有一个目录)始终位于资源部分(.rsrc)的开头。*目录的子目录对应于文件中找到的各种资源。例如,如果PE文件包含对话框,字符串表和菜单,将有三个子目录:对话框目录,字符串表目录和菜单目录。这些类型子目录中的每个子目录又将具有ID子目录。给定资源类型的每个实例将有一个ID子目录。在上面的示例中,如果有三个对话框,对话框目录将包含三个ID子目录。每个ID子目录都将具有一个字符串名称(例如“ MyDialog”)或用于标识RC文件中资源的整数ID。图5以可视形式显示了资源目录层次结构示例。表13显示了Windows NT CLOCK.EXE中资源的PEDUMP输出。
图5.资源目录层次结构
表13. CLOCK.EXE的资源层次结构
ResDir (0) Named:00 ID:06 TimeDate:2C3601DB Vers:0.00 Char:0
ResDir (ICON) Named:00 ID:02 TimeDate:2C3601DB Vers:0.00 Char:0
ResDir (1) Named:00 ID:01 TimeDate:2C3601DB Vers:0.00 Char:0
ID: 00000409 Offset: 00000200
ResDir (2) Named:00 ID:01 TimeDate:2C3601DB Vers:0.00 Char:0
ID: 00000409 Offset: 00000210
ResDir (MENU) Named:02 ID:00 TimeDate:2C3601DB Vers:0.00 Char:0
ResDir (CLOCK) Named:00 ID:01 TimeDate:2C3601DB Vers:0.00 Char:0
ID: 00000409 Offset: 00000220
ResDir (GENERICMENU) Named:00 ID:01 TimeDate:2C3601DB Vers:0.00 Char:0
ID: 00000409 Offset: 00000230
ResDir (DIALOG) Named:01 ID:01 TimeDate:2C3601DB Vers:0.00 Char:0
ResDir (ABOUTBOX) Named:00 ID:01 TimeDate:2C3601DB Vers:0.00 Char:0
ID: 00000409 Offset: 00000240
ResDir (64) Named:00 ID:01 TimeDate:2C3601DB Vers:0.00 Char:0
ID: 00000409 Offset: 00000250
ResDir (STRING) Named:00 ID:03 TimeDate:2C3601DB Vers:0.00 Char:0
ResDir (1) Named:00 ID:01 TimeDate:2C3601DB Vers:0.00 Char:0
ID: 00000409 Offset: 00000260
ResDir (2) Named:00 ID:01 TimeDate:2C3601DB Vers:0.00 Char:0
ID: 00000409 Offset: 00000270
ResDir (3) Named:00 ID:01 TimeDate:2C3601DB Vers:0.00 Char:0
ID: 00000409 Offset: 00000280
ResDir (GROUP_ICON) Named:01 ID:00 TimeDate:2C3601DB Vers:0.00 Char:0
ResDir (CCKK) Named:00 ID:01 TimeDate:2C3601DB Vers:0.00 Char:0
ID: 00000409 Offset: 00000290
ResDir (VERSION) Named:00 ID:01 TimeDate:2C3601DB Vers:0.00 Char:0
ResDir (1) Named:00 ID:01 TimeDate:2C3601DB Vers:0.00 Char:0
ID: 00000409 Offset: 000002A0
如前所述,每个目录条目都是IMAGE_RESOURCE_DIRECTORY_ENTRY类型的结构(男孩,这些名称越来越长!)。每个IMAGE_RESOURCE_DIRECTORY_ENTRY的格式如表13所示。
表14. IMAGE_RESOURCE_DIRECTORY_ENTRY格式
-
DWORD Name
该字段包含整数ID或指向包含字符串名称的结构的指针。如果高位(0x80000000)为零,则此字段将解释为整数ID。如果高位不为零,则低31位是IMAGE_RESOURCE_DIR_STRING_U结构的偏移量(相对于资源的开始)。此结构包含WORD字符计数,后跟带有资源名称的UNICODE字符串。是的,即使用于非UNICODE Win32实现的PE文件在此处也使用UNICODE。要将UNICODE字符串转换为ANSI字符串,请使用WideCharToMultiByte函数。 -
DWORD OffsetToData
该字段可以是到另一个资源目录的偏移量,也可以是指向有关特定资源实例的信息的指针。如果设置了高位(0x80000000),则此目录条目引用一个子目录。低31位是相对于另一个IMAGE_RESOURCE_DIRECTORY的偏移量(相对于资源的开始)。如果未设置高位,则低31位指向IMAGE_RESOURCE_DATA_ENTRY结构。IMAGE_RESOURCE_DATA_ENTRY结构包含资源原始数据的位置,其大小和其代码页。
为了进一步介绍资源格式,我需要讨论每种资源类型(对话框,菜单等)的格式。涵盖这些主题可以轻松地单独填写整篇文章。
PE文件库重定位
链接器创建EXE文件时,会假设文件将映射到内存中的位置。基于此,链接器将代码和数据项的实际地址放入可执行文件中。如果由于某种原因可执行文件最终被加载到虚拟地址空间中的其他位置,则链接器插入映像的地址是错误的。存储在.reloc节中的信息使PE加载程序可以将这些地址固定在加载的映像中,以便再次正确。另一方面,如果加载程序能够在链接器假定的基址上加载文件,则不需要.reloc节数据,该数据将被忽略。.reloc节中的条目称为基址重定位,因为它们的使用取决于所加载映像的基址。
与NE文件格式的重定位不同,基本重定位非常简单。它们归结为图像中需要添加值的位置列表。基本重定位数据的格式有些古怪。基本重定位条目打包在一系列可变长度的块中。每个块描述了图像中一个4KB页面的重定位。让我们看一个例子,看看基本重定位是如何工作的。假定基地址为0x10000,链接可执行文件。图像内的偏移量0x2134是包含字符串地址的指针。该字符串从物理地址0x14002开始,因此指针包含值0x14002。然后,您加载文件,但是加载器决定它需要映射从物理地址0x60000开始的映像。链接器假定的基本加载地址和实际加载地址之间的差称为增量。在这种情况下,增量为0x50000。由于整个图像在内存中高0x50000字节,因此字符串(现在位于地址0x64002)也是如此。现在,指向该字符串的指针不正确。可执行文件包含指向字符串指针所在的内存位置的基本重定位。为了解决基本重定位,加载程序将增量值添加到基本重定位地址处的原始值。在这种情况下,加载程序将0x50000添加到原始指针值(0x14002),并将结果(0x64002)存储回指针的内存中。由于字符串实际上位于0x64002,因此一切正常。由于整个图像在内存中高0x50000字节,因此字符串(现在位于地址0x64002)也是如此。现在,指向该字符串的指针不正确。可执行文件包含指向字符串指针所在的内存位置的基本重定位。为了解决基本重定位,加载程序将增量值添加到基本重定位地址处的原始值。在这种情况下,加载程序将0x50000添加到原始指针值(0x14002),并将结果(0x64002)存储回指针的内存中。由于字符串实际上位于0x64002,因此一切正常。由于整个图像在内存中高0x50000字节,因此字符串(现在位于地址0x64002)也是如此。现在,指向该字符串的指针不正确。可执行文件包含指向字符串指针所在的内存位置的基本重定位。为了解决基本重定位,加载程序将增量值添加到基本重定位地址处的原始值。在这种情况下,加载程序将0x50000添加到原始指针值(0x14002),并将结果(0x64002)存储回指针的内存中。由于字符串实际上位于0x64002,因此一切正常。加载程序将增量值添加到基本重定位地址处的原始值。在这种情况下,加载程序将0x50000添加到原始指针值(0x14002),并将结果(0x64002)存储回指针的内存中。由于字符串实际上位于0x64002,因此一切正常。加载程序将增量值添加到基本重定位地址处的原始值。在这种情况下,加载程序将0x50000添加到原始指针值(0x14002),并将结果(0x64002)存储回指针的内存中。由于字符串实际上位于0x64002,因此一切正常。
每个基本重定位数据块均以类似于表14的IMAGE_BASE_RELOCATION结构开头。表15显示了一些基本重定位,如PEDUMP所示。请注意,显示的RVA值已由IMAGE_BASE_RELOCATION字段中的VirtualAddress取代。
图15. IMAGE_BASE_RELOCATION格式
-
DWORD VirtualAddress
该字段包含此重定位块的起始RVA。紧随其后的每个重定位的偏移量都添加到该值,以形成需要应用重定位的实际RVA。 -
DWORD SizeOfBlock
该结构的大小以及随后的所有WORD重定位。要确定此块中的重定位次数,请从该字段的值中减去IMAGE_BASE_RELOCATION的大小(8个字节),然后除以2(WORD的大小)。例如,如果此字段包含44,则紧随其后的是18个重定位:(44 - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD) = 18 WORD TypeOffset
这不仅是单个WORD,而且是一个WORD数组,其数量由上述公式计算得出。每个WORD的低12位是重定位偏移,需要从此重定位块的标头添加到“虚拟地址”字段的值中。每个WORD的高4位是重定位类型。对于在Intel CPU上运行的PE文件,您只会看到两种类型的重定位:
0 | IMAGE_REL_BASED_ABSOLUTE | 此重定位是没有意义的,仅用作占位符以将重定位块舍入为DWORD倍数的大小。 |
3 | IMAGE_REL_BASED_HIGHLOW | 这种重定位意味着将增量的高16位和低16位都添加到由计算出的RVA指定的DWORD中。 |
表16. EXE文件中的基本重定位
Virtual Address: 00001000 size: 0000012C
00001032 HIGHLOW
0000106D HIGHLOW
000010AF HIGHLOW
000010C5 HIGHLOW
// Rest of chunk omitted...
Virtual Address: 00002000 size: 0000009C
000020A6 HIGHLOW
00002110 HIGHLOW
00002136 HIGHLOW
00002156 HIGHLOW
// Rest of chunk omitted...
Virtual Address: 00003000 size: 00000114
0000300A HIGHLOW
0000301E HIGHLOW
0000303B HIGHLOW
0000306A HIGHLOW
// Rest of relocations omitted...
PE和COFF OBJ文件之间的差异
PE文件有两个部分未被操作系统使用。这些是COFF符号表和COFF调试信息。当可以获得更完整的CodeView信息时,为什么有人需要COFF调试信息?如果您打算使用Windows NT系统调试器(NTSD)或Windows NT内核调试器(KD),则COFF是唯一的选择。对于感兴趣的人,我在本文随附的在线帖子中提供了PE文件这些部分的详细说明(可在所有MSJ公告板上找到)。
在前面的讨论中的许多地方,我都注意到COFF OBJ文件和由此创建的PE文件中的许多结构和表都是相同的。COFF OBJ和PE文件在其开始处或附近都有IMAGE_FILE_HEADER。该头之后是节表,该节表包含有关文件中所有节的信息。两种格式也共享相同的行号和符号表格式,尽管PE文件也可以具有其他非COFF符号表。PEDUMP中的大量通用代码证明了OBJ和PE EXE格式之间的通用性(请参阅任何MSJ公告板上的COMMON.C)。
两种文件格式之间的相似之处并非偶然。此设计的目的是使链接程序的工作尽可能容易。从理论上讲,从单个OBJ创建EXE文件只需要插入几个表并在映像中修改几个文件偏移即可。考虑到这一点,您可以将COFF文件视为原始PE文件。只有几件事丢失或有所不同,因此我将在这里列出。
- COFF OBJ文件在IMAGE_FILE_HEADER之前没有MS-DOS存根,在IMAGE_FILE_HEADER之前也没有“ PE”签名。
- OBJ文件没有IMAGE_OPTIONAL_HEADER。在PE文件中,此结构紧随IMAGE_FILE_HEADER。有趣的是,COFF LIB文件确实具有IMAGE_OPTIONAL_HEADER。空间限制使我无法在这里谈论LIB文件。
- OBJ文件没有基本重定位。相反,它们具有基于符号的常规修复程序。我还没有进入COFF OBJ文件重定位的格式,因为它们相当模糊。如果要深入研究此特定区域,则节表条目中的PointerToRelocations和NumberOfRelocations字段指向每个节的重定位。重定位是IMAGE_RELOCATION结构的数组,该结构在WINNT.H中定义。如果启用正确的开关,PEDUMP程序可以显示OBJ文件重定位。
- OBJ文件中的CodeView信息存储在两个部分中(.debug $ S和.debug $ T)。链接器处理OBJ文件时,不会将这些部分放在PE文件中。相反,它将收集所有这些部分并构建一个存储在文件末尾的符号表。此符号表不是正式的节(即,PE的节表中没有它的条目)。
使用PEDUMP
PEDUMP是用于转储PE文件和COFF OBJ格式文件的命令行实用程序。它使用Win32控制台功能来消除大量用户界面工作的需要。PEDUMP的语法如下:
PEDUMP [switches] filename
通过不带参数运行PEDUMP可以看到这些开关。PEDUMP使用表17中所示的开关。默认情况下,未启用任何开关。在没有任何开关的情况下运行PEDUMP可以提供大多数有用的信息,而不会产生大量的输出。PEDUMP将其输出发送到标准输出文件,因此可以在命令行上使用>将其输出重定向到文件。
表17. PEDUMP开关
/一个 | 将所有内容都包括在转储中(基本上,启用所有开关) |
/H | 在转储末尾包含每个部分的十六进制转储 |
/升 | 包括行号信息(PE和COFF OBJ文件) |
/ R | 显示基本重定位(仅PE文件) |
/秒 | 显示符号表(PE和COFF OBJ文件) |
摘要
随着Win32的到来,Microsoft对OBJ和可执行文件格式进行了全面更改,以节省时间并建立在以前为其他操作系统完成的工作上。这些文件格式的主要目标是增强跨不同平台的可移植性。
Peering Inside the PE: A Tour of the Win32 Portable Executable File Format
Matt Pietrek
March 1994
Matt Pietrek is the author of Windows Internals (Addison-Wesley, 1993). He works at Nu-Mega Technologies Inc., and can be reached via CompuServe: 71774,362
This article is reproduced from the March 1994 issue of Microsoft Systems Journal*. Copyright © 1994 by Miller Freeman, Inc. All rights are reserved. No part of this article may be reproduced in any fashion (except in brief quotations used in critical articles and reviews) without the prior consent of Miller Freeman.*
To contact Miller Freeman regarding subscription information, call (800) 666-1084 in the U.S., or (303) 447-9330 in all other countries. For other inquiries, call (415) 358-9500.
The format of an operating system‘s executable file is in many ways a mirror of the operating system. Although studying an executable file format isn‘t usually high on most programmers‘ list of things to do, a great deal of knowledge can be gleaned this way. In this article, I‘ll give a tour of the Portable Executable (PE) file format that Microsoft has designed for use by all their Win32®-based systems: Windows NT®, Win32s™, and Windows® 95. The PE format plays a key role in all of Microsoft‘s operating systems for the foreseeable future, including Windows 2000. If you use Win32s or Windows NT, you‘re already using PE files. Even if you program only for Windows 3.1 using Visual C++®, you‘re still using PE files (the 32-bit MS-DOS® extended components of Visual C++ use this format). In short, PEs are already pervasive and will become unavoidable in the near future. Now is the time to find out what this new type of executable file brings to the operating system party.
I‘m not going to make you stare at endless hex dumps and chew over the significance of individual bits for pages on end. Instead, I‘ll present the concepts embedded in the PE file format and relate them to things you encounter everyday. For example, the notion of thread local variables, as in
declspec(thread) int i;
drove me crazy until I saw how it was implemented with elegant simplicity in the executable file. Since many of you are coming from a background in 16-bit Windows, I‘ll correlate the constructs of the Win32 PE file format back to their 16-bit NE file format equivalents.
In addition to a different executable format, Microsoft also introduced a new object module format produced by their compilers and assemblers. This new OBJ file format has many things in common with the PE executable format. I‘ve searched in vain to find any documentation on the new OBJ file format. So I deciphered it on my own, and will describe parts of it here in addition to the PE format.
It‘s common knowledge that Windows NT has a VAX® VMS® and UNIX® heritage. Many of the Windows NT creators designed and coded for those platforms before coming to Microsoft. When it came time to design Windows NT, it was only natural that they tried to minimize their bootstrap time by using previously written and tested tools. The executable and object module format that these tools produced and worked with is called COFF (an acronym for Common Object File Format). The relative age of COFF can be seen by things such as fields specified in octal format. The COFF format by itself was a good starting point, but needed to be extended to meet all the needs of a modern operating system like Windows NT or Windows 95. The result of this updating is the Portable Executable format. It‘s called "portable" because all the implementations of Windows NT on various platforms (x86, MIPS®, Alpha, and so on) use the same executable format. Sure, there are differences in things like the binary encodings of CPU instructions. The important thing is that the operating system loader and programming tools don‘t have to be completely rewritten for each new CPU that arrives on the scene.
The strength of Microsoft‘s commitment to get Windows NT up and running quickly is evidenced by the fact that they abandoned existing 32-bit tools and file formats. Virtual device drivers written for 16-bit Windows were using a different 32-bit file layout—the LE format—long before Windows NT appeared on the scene. More important than that is the shift of OBJ formats. Prior to the Windows NT C compiler, all Microsoft compilers used the Intel OMF (Object Module Format) specification. As mentioned earlier, the Microsoft compilers for Win32 produce COFF-format OBJ files. Some Microsoft competitors such as Borland and Symantec have chosen to forgo the COFF format OBJs and stick with the Intel OMF format. The upshot of this is that companies producing OBJs or LIBs for use with multiple compilers will need to go back to distributing separate versions of their products for different compilers (if they weren‘t already).
The PE format is documented (in the loosest sense of the word) in the WINNT.H header file. About midway through WINNT.H is a section titled "Image Format." This section starts out with small tidbits from the old familiar MS-DOS MZ format and NE format headers before moving into the newer PE information. WINNT.H provides definitions of the raw data structures used by PE files, but contains only a few useful comments to make sense of what the structures and flags mean. Whoever wrote the header file for the PE format (the name Michael J. O‘Leary keeps popping up) is certainly a believer in long, descriptive names, along with deeply nested structures and macros. When coding with WINNT.H, it‘s not uncommon to have expressions like this:
pNTHeader->
OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DEBUG].VirtualAddress;
To help make logical sense of the information in WINNT.H, read the Portable Executable and Common Object File Format Specification, available on MSDN Library quarterly CD-ROM releases up to and including October 2001.
Turning momentarily to the subject of COFF-format OBJs, the WINNT.H header file includes structure definitions and typedefs for COFF OBJ and LIB files. Unfortunately, I‘ve been unable to find any documentation on this similar to that for the executable file mentioned above. Since PE files and COFF OBJ files are so similar, I decided that it was time to bring these files out into the light and document them as well.
Beyond just reading about what PE files are composed of, you‘ll also want to dump some PE files to see these concepts for yourself. If you use Microsoft® tools for Win32-based development, the DUMPBIN program will dissect and output PE files and COFF OBJ/LIB files in readable form. Of all the PE file dumpers, DUMPBIN is easily the most comprehensive. It even has a nifty option to disassemble the code sections in the file it‘s taking apart. Borland users can use TDUMP to view PE executable files, but TDUMP doesn‘t understand the COFF OBJ files. This isn‘t a big deal since the Borland compiler doesn‘t produce COFF-format OBJs in the first place.
I‘ve written a PE and COFF OBJ file dumping program, PEDUMP (see Table 1), that I think provides more understandable output than DUMPBIN. Although it doesn‘t have a disassembler or work with LIB files, it is otherwise functionally equivalent to DUMPBIN, and adds a few new features to make it worth considering. The source code for PEDUMP is available on any MSJ bulletin board, so I won‘t list it here in its entirety. Instead, I‘ll show sample output from PEDUMP to illustrate the concepts as I describe them.
Table 1. PEDUMP.C
//--------------------
// PROGRAM: PEDUMP
// FILE: PEDUMP.C
// AUTHOR: Matt Pietrek - 1993
//--------------------
#include <windows.h>
#include <stdio.h>
#include "objdump.h"
#include "exedump.h"
#include "extrnvar.h"
// Global variables set here, and used in EXEDUMP.C and OBJDUMP.C
BOOL fShowRelocations = FALSE;
BOOL fShowRawSectionData = FALSE;
BOOL fShowSymbolTable = FALSE;
BOOL fShowLineNumbers = FALSE;
char HelpText[] =
"PEDUMP - Win32/COFF .EXE/.OBJ file dumper - 1993 Matt Pietrek\n\n"
"Syntax: PEDUMP [switches] filename\n\n"
" /A include everything in dump\n"
" /H include hex dump of sections\n"
" /L include line number information\n"
" /R show base relocations\n"
" /S show symbol table\n";
// Open up a file, memory map it, and call the appropriate dumping routine
void DumpFile(LPSTR filename)
{
HANDLE hFile;
HANDLE hFileMapping;
LPVOID lpFileBase;
PIMAGE_DOS_HEADER dosHeader;
hFile = CreateFile(filename, GENERIC_READ, FILE_SHARE_READ, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
if ( hFile = = INVALID_HANDLE_VALUE )
{ printf("Couldn‘t open file with CreateFile()\n");
return; }
hFileMapping = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
if ( hFileMapping = = 0 )
{ CloseHandle(hFile);
printf("Couldn‘t open file mapping with CreateFileMapping()\n");
return; }
lpFileBase = MapViewOfFile(hFileMapping, FILE_MAP_READ, 0, 0, 0);
if ( lpFileBase = = 0 )
{
CloseHandle(hFileMapping);
CloseHandle(hFile);
printf("Couldn‘t map view of file with MapViewOfFile()\n");
return;
}
printf("Dump of file %s\n\n", filename);
dosHeader = (PIMAGE_DOS_HEADER)lpFileBase;
if ( dosHeader->e_magic = = IMAGE_DOS_SIGNATURE )
{ DumpExeFile( dosHeader ); }
else if ( (dosHeader->e_magic = = 0x014C) // Does it look like a i386
&& (dosHeader->e_sp = = 0) ) // COFF OBJ file???
{
// The two tests above aren‘t what they look like. They‘re
// really checking for IMAGE_FILE_HEADER.Machine = = i386 (0x14C)
// and IMAGE_FILE_HEADER.SizeOfOptionalHeader = = 0;
DumpObjFile( (PIMAGE_FILE_HEADER)lpFileBase );
}
else
printf("unrecognized file format\n");
UnmapViewOfFile(lpFileBase);
CloseHandle(hFileMapping);
CloseHandle(hFile);
}
// process all the command line arguments and return a pointer to
// the filename argument.
PSTR ProcessCommandLine(int argc, char *argv[])
{
int i;
for ( i=1; i < argc; i++ )
{
strupr(argv[i]);
// Is it a switch character?
if ( (argv[i][0] = = ‘-‘) || (argv[i][0] = = ‘/‘) )
{
if ( argv[i][1] = = ‘A‘ )
{ fShowRelocations = TRUE;
fShowRawSectionData = TRUE;
fShowSymbolTable = TRUE;
fShowLineNumbers = TRUE; }
else if ( argv[i][1] = = ‘H‘ )
fShowRawSectionData = TRUE;
else if ( argv[i][1] = = ‘L‘ )
fShowLineNumbers = TRUE;
else if ( argv[i][1] = = ‘R‘ )
fShowRelocations = TRUE;
else if ( argv[i][1] = = ‘S‘ )
fShowSymbolTable = TRUE;
}
else // Not a switch character. Must be the filename
{ return argv[i]; }
}
}
int main(int argc, char *argv[])
{
PSTR filename;
if ( argc = = 1 )
{ printf( HelpText );
return 1; }
filename = ProcessCommandLine(argc, argv);
if ( filename )
DumpFile( filename );
return 0;
}
Win32 and PE Basic Concepts
Let‘s go over a few fundamental ideas that permeate the design of a PE file (see Figure 1). I‘ll use the term "module" to mean the code, data, and resources of an executable file or DLL that have been loaded into memory. Besides code and data that your program uses directly, a module is also composed of the supporting data structures used by Windows to determine where the code and data is located in memory. In 16-bit Windows, the supporting data structures are in the module database (the segment referred to by an HMODULE). In Win32, these data structures are in the PE header, which I‘ll explain shortly.
Figure 1. The PE file format
The first important thing to know about PE files is that the executable file on disk is very similar to what the module will look like after Windows has loaded it. The Windows loader doesn‘t need to work extremely hard to create a process from the disk file. The loader uses the memory-mapped file mechanism to map the appropriate pieces of the file into the virtual address space. To use a construction analogy, a PE file is like a prefabricated home. It‘s essentially brought into place in one piece, followed by a small amount of work to wire it up to the rest of the world (that is, to connect it to its DLLs and so on). This same ease of loading applies to PE-format DLLs as well. Once the module has been loaded, Windows can effectively treat it like any other memory-mapped file.
This is in marked contrast to the situation in 16-bit Windows. The 16-bit NE file loader reads in portions of the file and creates completely different data structures to represent the module in memory. When a code or data segment needs to be loaded, the loader has to allocate a new segment from the global heap, find where the raw data is stored in the executable file, seek to that location, read in the raw data, and apply any applicable fixups. In addition, each 16-bit module is responsible for remembering all the selectors it‘s currently using, whether the segment has been discarded, and so on.
For Win32, all the memory used by the module for code, data, resources, import tables, export tables, and other required module data structures is in one contiguous block of memory. All you need to know in this situation is where the loader mapped the file into memory. You can easily find all the various pieces of the module by following pointers that are stored as part of the image.
Another idea you should be acquainted with is the Relative Virtual Address (RVA). Many fields in PE files are specified in terms of RVAs. An RVA is simply the offset of some item, relative to where the file is memory-mapped. For example, let‘s say the loader maps a PE file into memory starting at address 0x10000 in the virtual address space. If a certain table in the image starts at address 0x10464, then the table‘s RVA is 0x464.
(Virtual address 0x10464)-(base address 0x10000) = RVA 0x00464
To convert an RVA into a usable pointer, simply add the RVA to the base address of the module. The base address is the starting address of a memory-mapped EXE or DLL and is an important concept in Win32. For the sake of convenience, Windows NT and Windows 95 uses the base address of a module as the module‘s instance handle (HINSTANCE). In Win32, calling the base address of a module an HINSTANCE is somewhat confusing, because the term "instance handle" comes from 16-bit Windows. Each copy of an application in 16-bit Windows gets its own separate data segment (and an associated global handle) that distinguishes it from other copies of the application, hence the term instance handle. In Win32, applications don‘t need to be distinguished from one another because they don‘t share the same address space. Still, the term HINSTANCE persists to keep continuity between 16-bit Windows and Win32. What‘s important for Win32 is that you can call GetModuleHandle for any DLL that your process uses to get a pointer for accessing the module‘s components.
The final concept that you need to know about PE files is sections. A section in a PE file is roughly equivalent to a segment or the resources in an NE file. Sections contain either code or data. Unlike segments, sections are blocks of contiguous memory with no size constraints. Some sections contain code or data that your program declared and uses directly, while other data sections are created for you by the linker and librarian, and contain information vital to the operating system. In some descriptions of the PE format, sections are also referred to as objects. The term object has so many overloaded meanings that I‘ll stick to calling the code and data areas sections.
The PE Header
Like all other executable file formats, the PE file has a collection of fields at a known (or easy to find) location that define what the rest of the file looks like. This header contains information such as the locations and sizes of the code and data areas, what operating system the file is intended for, the initial stack size, and other vital pieces of information that I‘ll discuss shortly. As with other executable formats from Microsoft, this main header isn‘t at the very beginning of the file. The first few hundred bytes of the typical PE file are taken up by the MS-DOS stub. This stub is a tiny program that prints out something to the effect of "This program cannot be run in MS-DOS mode." So if you run a Win32-based program in an environment that doesn‘t support Win32, you‘ll get this informative error message. When the Win32 loader memory maps a PE file, the first byte of the mapped file corresponds to the first byte of the MS-DOS stub. That‘s right. With every Win32-based program you start up, you get an MS-DOS-based program loaded for free!
As in other Microsoft executable formats, you find the real header by looking up its starting offset, which is stored in the MS-DOS stub header. The WINNT.H file includes a structure definition for the MS-DOS stub header that makes it very easy to look up where the PE header starts. The e_lfanew field is a relative offset (or RVA, if you prefer) to the actual PE header. To get a pointer to the PE header in memory, just add that field‘s value to the image base:
// Ignoring typecasts and pointer conversion issues for clarity...
pNTHeader = dosHeader + dosHeader->e_lfanew;
Once you have a pointer to the main PE header, the fun can begin. The main PE header is a structure of type IMAGE_NT_HEADERS, which is defined in WINNT.H. This structure is composed of a DWORD and two substructures and is laid out as follows:
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER OptionalHeader;
The Signature field viewed as ASCII text is "PE\0\0". If after using the e_lfanew field in the MS-DOS header, you find an NE signature here rather than a PE, you‘re working with a 16-bit Windows NE file. Likewise, an LE in the signature field would indicate a Windows 3.x virtual device driver (VxD). An LX here would be the mark of a file for OS/2 2.0.
Following the PE signature DWORD in the PE header is a structure of type IMAGE_FILE_HEADER. The fields of this structure contain only the most basic information about the file. The structure appears to be unmodified from its original COFF implementations. Besides being part of the PE header, it also appears at the very beginning of the COFF OBJs produced by the Microsoft Win32 compilers. The fields of the IMAGE_FILE_HEADER are shown in Table 2.
Table 2. IMAGE_FILE_HEADER Fields
-
WORD Machine
The CPU that this file is intended for. The following CPU IDs are defined:TABLE 1 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
The number of sections in the file. -
DWORD TimeDateStamp
The time that the linker (or compiler for an OBJ file) produced this file. This field holds the number of seconds since December 31st, 1969, at 4:00 P.M. -
DWORD PointerToSymbolTable
The file offset of the COFF symbol table. This field is only used in OBJ files and PE files with COFF debug information. PE files support multiple debug formats, so debuggers should refer to the IMAGE_DIRECTORY_ENTRY_DEBUG entry in the data directory (defined later). -
DWORD NumberOfSymbols
The number of symbols in the COFF symbol table. See above. -
WORD SizeOfOptionalHeader
The size of an optional header that can follow this structure. In OBJs, the field is 0. In executables, it is the size of the IMAGE_OPTIONAL_HEADER structure that follows this structure. -
WORD Characteristics
Flags with information about the file. Some important fields:TABLE 2 0x0001There are no relocations in this file0x0002File is an executable image (not a OBJ or LIB)0x2000File is a dynamic-link library, not a program
Other fields are defined in WINNT.H
The third component of the PE header is a structure of type IMAGE_OPTIONAL_HEADER. For PE files, this portion certainly isn‘t optional. The COFF format allows individual implementations to define a structure of additional information beyond the standard IMAGE_FILE_HEADER. The fields in the IMAGE_OPTIONAL_HEADER are what the PE designers felt was critical information beyond the basic information in the IMAGE_FILE_HEADER.
All of the fields of the IMAGE_OPTIONAL_HEADER aren‘t necessarily important to know about (see Figure 4). The more important ones to be aware of are the ImageBase and the Subsystem fields. You can skim or skip the description of the fields.
Table 3. IMAGE_OPTIONAL_HEADER Fields
-
WORD Magic
Appears to be a signature WORD of some sort. Always appears to be set to 0x010B. -
BYTE MajorLinkerVersion
BYTE MinorLinkerVersion
The version of the linker that produced this file. The numbers should be displayed as decimal values, rather than as hex. A typical linker version is 2.23. -
DWORD SizeOfCode
The combined and rounded-up size of all the code sections. Usually, most files only have one code section, so this field matches the size of the .text section. -
DWORD SizeOfInitializedData
This is supposedly the total size of all the sections that are composed of initialized data (not including code segments.) However, it doesn‘t seem to be consistent with what appears in the file. -
DWORD SizeOfUninitializedData
The size of the sections that the loader commits space for in the virtual address space, but that don‘t take up any space in the disk file. These sections don‘t need to have specific values at program startup, hence the term uninitialized data. Uninitialized data usually goes into a section called .bss. -
DWORD AddressOfEntryPoint
The address where the loader will begin execution. This is an RVA, and usually can usually be found in the .text section. -
DWORD BaseOfCode
The RVA where the file‘s code sections begin. The code sections typically come before the data sections and after the PE header in memory. This RVA is usually 0x1000 in Microsoft Linker-produced EXEs. Borland‘s TLINK32 looks like it adds the image base to the RVA of the first code section and stores the result in this field. -
DWORD BaseOfData
The RVA where the file‘s data sections begin. The data sections typically come last in memory, after the PE header and the code sections. -
DWORD ImageBase
When the linker creates an executable, it assumes that the file will be memory-mapped to a specific location in memory. That address is stored in this field, assuming a load address allows linker optimizations to take place. If the file really is memory-mapped to that address by the loader, the code doesn‘t need any patching before it can be run. In executables produced for Windows NT, the default image base is 0x10000. For DLLs, the default is 0x400000. In Windows 95, the address 0x10000 can‘t be used to load 32-bit EXEs because it lies within a linear address region shared by all processes. Because of this, Microsoft has changed the default base address for Win32 executables to 0x400000. Older programs that were linked assuming a base address of 0x10000 will take longer to load under Windows 95 because the loader needs to apply the base relocations. -
DWORD SectionAlignment
When mapped into memory, each section is guaranteed to start at a virtual address that‘s a multiple of this value. For paging purposes, the default section alignment is 0x1000. -
DWORD FileAlignment
In the PE file, the raw data that comprises each section is guaranteed to start at a multiple of this value. The default value is 0x200 bytes, probably to ensure that sections always start at the beginning of a disk sector (which are also 0x200 bytes in length). This field is equivalent to the segment/resource alignment size in NE files. Unlike NE files, PE files typically don‘t have hundreds of sections, so the space wasted by aligning the file sections is almost always very small. -
WORD MajorOperatingSystemVersion
WORD MinorOperatingSystemVersion
The minimum version of the operating system required to use this executable. This field is somewhat ambiguous since the subsystem fields (a few fields later) appear to serve a similar purpose. This field defaults to 1.0 in all Win32 EXEs to date. -
WORD MajorImageVersion
WORD MinorImageVersion
A user-definable field. This allows you to have different versions of an EXE or DLL. You set these fields via the linker /VERSION switch. For example, "LINK /VERSION:2.0 myobj.obj". -
WORD MajorSubsystemVersion
WORD MinorSubsystemVersion
Contains the minimum subsystem version required to run the executable. A typical value for this field is 3.10 (meaning Windows NT 3.1). -
DWORD Reserved1
Seems to always be 0. -
DWORD SizeOfImage
This appears to be the total size of the portions of the image that the loader has to worry about. It is the size of the region starting at the image base up to the end of the last section. The end of the last section is rounded up to the nearest multiple of the section alignment. -
DWORD SizeOfHeaders
The size of the PE header and the section (object) table. The raw data for the sections starts immediately after all the header components. -
DWORD CheckSum
Supposedly a CRC checksum of the file. As in other Microsoft executable formats, this field is ignored and set to 0. The one exception to this rule is for trusted services and these EXEs must have a valid checksum. -
WORD Subsystem
The type of subsystem that this executable uses for its user interface. WINNT.H defines the following values:TABLE 3 NATIVE1Doesn‘t require a subsystem (such as a device driver)WINDOWS_GUI2Runs in the Windows GUI subsystemWINDOWS_CUI3Runs in the Windows character subsystem (a console app)OS2_CUI5Runs in the OS/2 character subsystem (OS/2 1.x apps only)POSIX_CUI7Runs in the Posix character subsystem -
WORD DllCharacteristics
A set of flags indicating under which circumstances a DLL‘s initialization function (such as DllMain) will be called. This value appears to always be set to 0, yet the operating system still calls the DLL initialization function for all four events.
The following values are defined:
1 | Call when DLL is first loaded into a process‘s address space |
2 | Call when a thread terminates |
4 | Call when a thread starts up |
8 | Call when DLL exits |
-
DWORD SizeOfStackReserve
The amount of virtual memory to reserve for the initial thread‘s stack. Not all of this memory is committed, however (see the next field). This field defaults to 0x100000 (1MB). If you specify 0 as the stack size to CreateThread, the resulting thread will also have a stack of this same size. -
DWORD SizeOfStackCommit
The amount of memory initially committed for the initial thread‘s stack. This field defaults to 0x1000 bytes (1 page) for the Microsoft Linker while TLINK32 makes it two pages. -
DWORD SizeOfHeapReserve
The amount of virtual memory to reserve for the initial process heap. This heap‘s handle can be obtained by calling GetProcessHeap. Not all of this memory is committed (see the next field). -
DWORD SizeOfHeapCommit
The amount of memory initially committed in the process heap. The default is one page. -
DWORD LoaderFlags
From WINNT.H, these appear to be fields related to debugging support. I‘ve never seen an executable with either of these bits enabled, nor is it clear how to get the linker to set them. The following values are defined:TABLE 5 1. Invoke a breakpoint instruction before starting the process 2. Invoke a debugger on the process after it‘s been loaded -
DWORD NumberOfRvaAndSizes
The number of entries in the DataDirectory array (below). This value is always set to 16 by the current tools. -
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]
An array of IMAGE_DATA_DIRECTORY structures. The initial array elements contain the starting RVA and sizes of important portions of the executable file. Some elements at the end of the array are currently unused. The first element of the array is always the address and size of the exported function table (if present). The second array entry is the address and size of the imported function table, and so on. For a complete list of defined array entries, see the IMAGE_DIRECTORY_ENTRY_XXX #defines in WINNT.H. This array allows the loader to quickly find a particular section of the image (for example, the imported function table), without needing to iterate through each of the images sections, comparing names as it goes along. Most array entries describe an entire section‘s data. However, the IMAGE_DIRECTORY_ENTRY_DEBUG element only encompasses a small portion of the bytes in the .rdata section.
The Section Table
Between the PE header and the raw data for the image‘s sections lies the section table. The section table is essentially a phone book containing information about each section in the image. The sections in the image are sorted by their starting address (RVAs), rather than alphabetically.
Now I can better clarify what a section is. In an NE file, your program‘s code and data are stored in distinct "segments" in the file. Part of the NE header is an array of structures, one for each segment your program uses. Each structure in the array contains information about one segment. The information stored includes the segment‘s type (code or data), its size, and its location elsewhere in the file. In a PE file, the section table is analogous to the segment table in the NE file. Unlike an NE file segment table, though, a PE section table doesn‘t store a selector value for each code or data chunk. Instead, each section table entry stores an address where the file‘s raw data has been mapped into memory. While sections are analogous to 32-bit segments, they really aren‘t individual segments. They‘re just really memory ranges in a process‘s virtual address space.
Another area where PE files differ from NE files is how they manage the supporting data that your program doesn‘t use, but the operating system does; for example, the list of DLLs that the executable uses or the location of the fixup table. In an NE file, resources aren‘t considered segments. Even though they have selectors assigned to them, information about resources is not stored in the NE header‘s segment table. Instead, resources are relegated to a separate table towards the end of the NE header. Information about imported and exported functions also doesn‘t warrant its own segment; it‘s crammed into the NE header.
The story with PE files is different. Anything that might be considered vital code or data is stored in a full-fledged section. Thus, information about imported functions is stored in its own section, as is the table of functions that the module exports. The same goes for the relocation data. Any code or data that might be needed by either the program or the operating system gets its own section.
Before I discuss specific sections, I need to describe the data that the operating system manages the sections with. Immediately following the PE header in memory is an array of IMAGE_SECTION_HEADERs. The number of elements in this array is given in the PE header (the IMAGE_NT_HEADER.FileHeader.NumberOfSections field). I used PEDUMP to output the section table and all of the section‘s fields and attributes. Figure 5 shows the PEDUMP output of a section table for a typical EXE file, and Figure 6 shows the section table in an OBJ file.
Table 4. A Typical Section Table from an EXE File
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
Table 5. A Typical Section Table from an OBJ File
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
Each IMAGE_SECTION_HEADER has the format described in Figure 7. It‘s interesting to note what‘s missing from the information stored for each section. First off, notice that there‘s no indication of any PRELOAD attributes. The NE file format allows you to specify with the PRELOAD attribute which segments should be loaded at module load time. The OS/2® 2.0 LX format has something similar, allowing you to specify up to eight pages to preload. The PE format has nothing like this. Microsoft must be confident in the performance of Win32 demand-paged loading.
Table 6. IMAGE_SECTION_HEADER Formats
-
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]
This is an 8-byte ANSI name (not UNICODE) that names the section. Most section names start with a . (such as ".text"), but this is not a requirement, as some PE documentation would have you believe. You can name your own sections with either the segment directive in assembly language, or with "#pragma data_seg" and "#pragma code_seg" in the Microsoft C/C++ compiler. It‘s important to note that if the section name takes up the full 8 bytes, there‘s no NULL terminator byte. If you‘re a printf devotee, you can use %.8s to avoid copying the name string to another buffer where you can NULL-terminate it. -
union {
DWORD PhysicalAddress
DWORD VirtualSize
} Misc;
This field has different meanings, in EXEs or OBJs. In an EXE, it holds the actual size of the code or data. This is the size before rounding up to the nearest file alignment multiple. The SizeOfRawData field (seems a bit of a misnomer) later on in the structure holds the rounded up value. The Borland linker reverses the meaning of these two fields and appears to be correct. For OBJ files, this field indicates the physical address of the section. The first section starts at address 0. To find the physical address in an OBJ file of the next section, add the SizeOfRawData value to the physical address of the current section. -
DWORD VirtualAddress
In EXEs, this field holds the RVA to where the loader should map the section. To calculate the real starting address of a given section in memory, add the base address of the image to the section‘s VirtualAddress stored in this field. With Microsoft tools, the first section defaults to an RVA of 0x1000. In OBJs, this field is meaningless and is set to 0. -
DWORD SizeOfRawData
In EXEs, this field contains the size of the section after it‘s been rounded up to the file alignment size. For example, assume a file alignment size of 0x200. If the VirtualSize field from above says that the section is 0x35A bytes in length, this field will say that the section is 0x400 bytes long. In OBJs, this field contains the exact size of the section emitted by the compiler or assembler. In other words, for OBJs, it‘s equivalent to the VirtualSize field in EXEs. -
DWORD PointerToRawData
This is the file-based offset of where the raw data emitted by the compiler or assembler can be found. If your program memory maps a PE or COFF file itself (rather than letting the operating system load it), this field is more important than the VirtualAddress field. You‘ll have a completely linear file mapping in this situation, so you‘ll find the data for the sections at this offset, rather than at the RVA specified in the VirtualAddress field. -
DWORD PointerToRelocations
In OBJs, this is the file-based offset to the relocation information for this section. The relocation information for each OBJ section immediately follows the raw data for that section. In EXEs, this field (and the subsequent field) are meaningless, and set to 0. When the linker creates the EXE, it resolves most of the fixups, leaving only base address relocations and imported functions to be resolved at load time. The information about base relocations and imported functions is kept in their own sections, so there‘s no need for an EXE to have per-section relocation data following the raw section data. -
DWORD PointerToLinenumbers
This is the file-based offset of the line number table. A line number table correlates source file line numbers to the addresses of the code generated for a given line. In modern debug formats like the CodeView format, line number information is stored as part of the debug information. In the COFF debug format, however, the line number information is stored separately from the symbolic name/type information. Usually, only code sections (such as .text) have line numbers. In EXE files, the line numbers are collected towards the end of the file, after the raw data for the sections. In OBJ files, the line number table for a section comes after the raw section data and the relocation table for that section. -
WORD NumberOfRelocations
The number of relocations in the relocation table for this section (the PointerToRelocations field from above). This field seems relevant only for OBJ files. -
WORD NumberOfLinenumbers
The number of line numbers in the line number table for this section (the PointerToLinenumbers field from above). -
DWORD Characteristics
What most programmers call flags, the COFF/PE format calls characteristics. This field is a set of flags that indicate the section‘s attributes (such as code/data, readable, or writeable,). For a complete list of all possible section attributes, see the IMAGE_SCN_XXX_XXX #defines in WINNT.H. Some of the more important flags are shown below:
0x00000020 This section contains code. Usually set in conjunction with the executable flag (0x80000000).
0x00000040 This section contains initialized data. Almost all sections except executable and the .bss section have this flag set.
0x00000080 This section contains uninitialized data (for example, the .bss section).
0x00000200 This section contains comments or some other type of information. A typical use of this section is the .drectve section emitted by the compiler, which contains commands for the linker.
0x00000800 This section‘s contents shouldn‘t be put in the final EXE file. These sections are used by the compiler/assembler to pass information to the linker.
0x02000000 This section can be discarded, since it‘s not needed by the process once it‘s been loaded. The most common discardable section is the base relocations (.reloc).
0x10000000 This section is shareable. When used with a DLL, the data in this section will be shared among all processes using the DLL. The default is for data sections to be nonshared, meaning that each process using a DLL gets its own copy of this section‘s data. In more technical terms, a shared section tells the memory manager to set the page mappings for this section such that all processes using the DLL refer to the same physical page in memory. To make a section shareable, use the SHARED attribute at link time. For example
LINK /SECTION:MYDATA,RWS ...
tells the linker that the section called MYDATA should be readable, writeable, and shared.
0x20000000 This section is executable. This flag is usually set whenever the "contains code" flag (0x00000020) is set.
0x40000000 This section is readable. This flag is almost always set for sections in EXE files.
0x80000000 The section is writeable. If this flag isn‘t set in an EXE‘s section, the loader should mark the memory mapped pages as read-only or execute-only. Typical sections with this attribute are .data and .bss. Interestingly, the .idata section also has this attribute set.
Also missing from the PE format is the notion of page tables. The OS/2 equivalent of an IMAGE_SECTION_HEADER in the LX format doesn‘t point directly to where the code or data for a section can be found in the file. Instead, it refers to a page lookup table that specifies attributes and the locations of specific ranges of pages within a section. The PE format dispenses with all that, and guarantees that a section‘s data will be stored contiguously within the file. Of the two formats, the LX method may allow more flexibility, but the PE style is significantly simpler and easier to work with. Having written file dumpers for both formats, I can vouch for this!
Another welcome change in the PE format is that the locations of items are stored as simple DWORD offsets. In the NE format, the location of almost everything is stored as a sector value. To find the real offset, you need to first look up the alignment unit size in the NE header and convert it to a sector size (typically 16 or 512 bytes). You then need to multiply the sector size by the specified sector offset to get an actual file offset. If by chance something isn‘t stored as a sector offset in an NE file, it is probably stored as an offset relative to the NE header. Since the NE header isn‘t at the beginning of the file, you need to drag around the file offset of the NE header in your code. All in all, the PE format is much easier to work with than the NE, LX, or LE formats (assuming you can use memory-mapped files).
Common Sections
Having seen what sections are in general and where they‘re located, let‘s look at the common sections that you‘ll find in EXE and OBJ files. The list is by no means complete, but includes the sections you encounter every day (even if you‘re not aware of it).
The .text section is where all general-purpose code emitted by the compiler or assembler ends up. Since PE files run in 32-bit mode and aren‘t restricted to 16-bit segments, there‘s no reason to break the code from separate source files into separate sections. Instead, the linker concatenates all the .text sections from the various OBJs into one big .text section in the EXE. If you use Borland C++ the compiler emits its code to a segment named CODE. PE files produced with Borland C++ have a section named CODE rather than one called .text. I‘ll explain this in a minute.
It was somewhat interesting to me to find out that there was additional code in the .text section beyond what I created with the compiler or used from the run-time libraries. In a PE file, when you call a function in another module (for example, GetMessage in USER32.DLL), the CALL instruction emitted by the compiler doesn‘t transfer control directly to the function in the DLL (see Figure 8). Instead, the call instruction transfers control to a
JMP DWORD PTR [XXXXXXXX]
instruction that‘s also in the .text section. The JMP instruction indirects through a DWORD variable in the .idata section. This .idata section DWORD contains the real address of the operating system function entry point. After thinking about this for a while, I came to understand why DLL calls are implemented this way. By funneling all calls to a given DLL function through one location, the loader doesn‘t need to patch every instruction that calls a DLL. All the PE loader has to do is put the correct address of the target function into the DWORD in the .idata section. No call instructions need to be patched. This is in marked contrast to NE files, where each segment contains a list of fixups that need to be applied to the segment. If the segment calls a given DLL function 20 times, the loader must write the address of that function 20 times into the segment. The downside to the PE method is that you can‘t initialize a variable with the true address of a DLL function. For example, you would think that something like
Figure 2. Calling a function in another module
FARPROC pfnGetMessage = GetMessage;
would put the address of GetMessage into the variable pfnGetMessage. In 16-bit Windows, this works, while in Win32 it doesn‘t. In Win32, the variable pfnGetMessage will end up holding the address of the JMP DWORD PTR [XXXXXXXX] thunk that I mentioned earlier. If you wanted to call through the function pointer, things would work as you‘d expect. However, if you want to read the bytes at the beginning of GetMessage, you‘re out of luck (unless you do additional work to follow the .idata "pointer" yourself). I‘ll come back to this topic later, in the discussion of the import table.
Although Borland could have had the compiler emit segments with a name of .text, it chose a default segment name of CODE. To determine a section name in the PE file, the Borland linker (TLINK32.EXE) takes the segment name from the OBJ file and truncates it to 8 characters (if necessary).
While the difference in the section names is a small matter, there is a more important difference in how Borland PE files link to other modules. As I mentioned in the .text description, all calls to OBJs go through a JMP DWORD PTR [XXXXXXXX] thunk. Under the Microsoft system, this thunk comes to the EXE from the .text section of an import library. Because the library manager (LIB32) creates the import library (and the thunk) when you link the external DLL, the linker doesn‘t have to "know" how to generate these thunks itself. The import library is really just some more code and data to link into the PE file.
The Borland system of dealing with imported functions is simply an extension of the way things were done for 16-bit NE files. The import libraries that the Borland linker uses are really just a list of function names along with the name of the DLL they‘re in. TLINK32 is therefore responsible for determining which fixups are to external DLLs, and generating an appropriate JMP DWORD PTR [XXXXXXXX] thunk for it. TLINK32 stores the thunks that it creates in a section named .icode.
Just as .text is the default section for code, the .data section is where your initialized data goes. This data consists of global and static variables that are initialized at compile time. It also includes string literals. The linker combines all the .data sections from the OBJ and LIB files into one .data section in the EXE. Local variables are located on a thread‘s stack, and take no room in the .data or .bss sections.
The .bss section is where any uninitialized static and global variables are stored. The linker combines all the .bss sections in the OBJ and LIB files into one .bss section in the EXE. In the section table, the RawDataOffset field for the .bss section is set to 0, indicating that this section doesn‘t take up any space in the file. TLINK doesn‘t emit this section. Instead it extends the virtual size of the DATA section.
.CRT is another initialized data section utilized by the Microsoft C/C++ run-time libraries (hence the name). Why this data couldn‘t go into the standard .data section is beyond me.
The .rsrc section contains all the resources for the module. In the early days of Windows NT, the RES file output of the 16-bit RC.EXE wasn‘t in a format that the Microsoft PE linker could understand. The CVTRES program converted these RES files into a COFF-format OBJ, placing the resource data into a .rsrc section within the OBJ. The linker could then treat the resource OBJ as just another OBJ to link in, allowing the linker to not "know" anything special about resources. More recent linkers from Microsoft appear to be able to process RES files directly.
The .idata section contains information about functions (and data) that the module imports from other DLLs. This section is equivalent to an NE file‘s module reference table. A key difference is that each function that a PE file imports is specifically listed in this section. To find the equivalent information in an NE file, you‘d have to go digging through the relocations at the end of the raw data for each of the segments.
The .edata section is a list of the functions and data that the PE file exports for other modules. Its NE file equivalent is the combination of the entry table, the resident names table, and the nonresident names table. Unlike in 16-bit Windows, there‘s seldom a reason to export anything from an EXE file, so you usually only see .edata sections in DLLs. When using Microsoft tools, the data in the .edata section comes to the PE file via the EXP file. Put another way, the linker doesn‘t generate this information on its own. Instead, it relies on the library manager (LIB32) to scan the OBJ files and create the EXP file that the linker adds to its list of modules to link. Yes, that‘s right! Those pesky EXP files are really just OBJ files with a different extension.
The .reloc section holds a table of base relocations. A base relocation is an adjustment to an instruction or initialized variable value that‘s needed if the loader couldn‘t load the file where the linker assumed it would. If the loader is able to load the image at the linker‘s preferred base address, the loader completely ignores the relocation information in this section. If you want to take a chance and hope that the loader can always load the image at the assumed base address, you can tell the linker to strip this information with the /FIXED option. While this may save space in the executable file, it may cause the executable not to work on other Win32-based implementations. For example, say you built an EXE for Windows NT and based the EXE at 0x10000. If you told the linker to strip the relocations, the EXE wouldn‘t run under Windows 95, where the address 0x10000 is already in use.
It‘s important to note that the JMP and CALL instructions that the compiler generates use offsets relative to the instruction, rather than actual offsets in the 32-bit flat segment. If the image needs to be loaded somewhere other than where the linker assumed for a base address, these instructions don‘t need to change, since they use relative addressing. As a result, there are not as many relocations as you might think. Relocations are usually only needed for instructions that use a 32-bit offset to some data. For example, let‘s say you had the following global variable declarations:
int i;
int *ptr = &i;
If the linker assumed an image base of 0x10000, the address of the variable i will end up containing something like 0x12004. At the memory used to hold the pointer "ptr", the linker will have written out 0x12004, since that‘s the address of the variable i. If the loader for whatever reason decided to load the file at a base address of 0x70000, the address of i would be 0x72004. The .reloc section is a list of places in the image where the difference between the linker assumed load address and the actual load address needs to be factored in.
When you use the compiler directive _ _declspec(thread), the data that you define doesn‘t go into either the .data or .bss sections. It ends up in the .tls section, which refers to "thread local storage," and is related to the TlsAlloc family of Win32 functions. When dealing with a .tls section, the memory manager sets up the page tables so that whenever a process switches threads, a new set of physical memory pages is mapped to the .tls section‘s address space. This permits per-thread global variables. In most cases, it is much easier to use this mechanism than to allocate memory on a per-thread basis and store its pointer in a TlsAlloc‘ed slot.
There‘s one unfortunate note that must be added about the .tls section and _ _declspec(thread) variables. In Windows NT and Windows 95, this thread local storage mechanism won‘t work in a DLL if the DLL is loaded dynamically by LoadLibrary. In an EXE or an implicitly loaded DLL, everything works fine. If you can‘t implicitly link to the DLL, but need per-thread data, you‘ll have to fall back to using TlsAlloc and TlsGetValue with dynamically allocated memory.
Although the .rdata section usually falls between the .data and .bss sections, your program generally doesn‘t see or use the data in this section. The .rdata section is used for at least two things. First, in Microsoft linker-produced EXEs, the .rdata section holds the debug directory, which is only present in EXE files. (In TLINK32 EXEs, the debug directory is in a section named .debug.) The debug directory is an array of IMAGE_DEBUG_DIRECTORY structures. These structures hold information about the type, size, and location of the various types of debug information stored in the file. Three main types of debug information appear: CodeView®, COFF, and FPO. Figure 9 shows the PEDUMP output for a typical debug directory.
Table 7. A Typical Debug Directory
Type | Size | Address | FilePtr | Charactr | TimeData | Version | |
COFF | 000065C5 | 00000000 | 00009200 | 00000000 | 2CF8CF3D | 0.00 | |
??? | 00000114 | 00000000 | 0000F7C8 | 00000000 | 2CF8CF3D | 0.00 | |
FPO | 000004B0 | 00000000 | 0000F8DC | 00000000 | 2CF8CF3D | 0.00 | |
CODEVIEW | 0000B0B4 | 00000000 | 0000FD8C | 00000000 | 2CF8CF3D | 0.00 |
The debug directory isn‘t necessarily found at the beginning of the .rdata section. To find the start of the debug directory table, use the RVA in the seventh entry (IMAGE_DIRECTORY_ENTRY_DEBUG) of the data directory. The data directory is at the end of the PE header portion of the file. To determine the number of entries in the Microsoft linker-generated debug directory, divide the size of the debug directory (found in the size field of the data directory entry) by the size of an IMAGE_DEBUG_DIRECTORY structure. TLINK32 emits a simple count, usually 1. The PEDUMP sample program demonstrates this.
The other useful portion of an .rdata section is the description string. If you specified a DESCRIPTION entry in your program‘s DEF file, the specified description string appears in the .rdata section. In the NE format, the description string is always the first entry of the nonresident names table. The description string is intended to hold a useful text string describing the file. Unfortunately, I haven‘t found an easy way to find it. I‘ve seen PE files that had the description string before the debug directory, and other files that had it after the debug directory. I‘m not aware of any consistent method of finding the description string (or even if it‘s present at all).
These .debug$S and .debug$T sections only appear in OBJs. They store the CodeView symbol and type information. The section names are derived from the segment names used for this purpose by previous 16-bit compilers ($$SYMBOLS and $$TYPES). The sole purpose of the .debug$T section is to hold the pathname to the PDB file that contains the CodeView information for all the OBJs in the project. The linker reads in the PDB and uses it to create portions of the CodeView information that it places at the end of the finished PE file.
The .drective section only appears in OBJ files. It contains text representations of commands for the linker. For example, in any OBJ I compile with the Microsoft compiler, the following strings appear in the .drectve section:
-defaultlib:LIBC -defaultlib:OLDNAMES
When you use _ _declspec(export) in your code, the compiler simply emits the command-line equivalent into the .drectve section (for instance, "-export:MyFunction").
In playing around with PEDUMP, I‘ve encountered other sections from time to time. For instance, in the Windows 95 KERNEL32.DLL, there are LOCKCODE and LOCKDATA sections. Presumably these are sections that will get special paging treatment so that they‘re never paged out of memory.
There are two lessons to be learned from this. First, don‘t feel constrained to use only the standard sections provided by the compiler or assembler. If you need a separate section for some reason, don‘t hesitate to create your own. In the C/C++ compiler, use the #pragma code_seg and #pragma data_seg. In assembly language, just create a 32-bit segment (which becomes a section) with a name different from the standard sections. If using TLINK32, you must use a different class or turn off code segment packing. The other thing to remember is that section names that are out of the ordinary can often give a deeper insight into the purpose and implementation of a particular PE file.
PE File Imports
Earlier, I described how function calls to outside DLLs don‘t call the DLL directly. Instead, the CALL instruction goes to a JMP DWORD PTR [XXXXXXXX] instruction somewhere in the executable‘s .text section (or .icode section if you‘re using Borland C++). The address that the JMP instruction looks up and transfers control to is the real target address. The PE file‘s .idata section contains the information necessary for the loader to determine the addresses of the target functions and patch them into the executable image.
The .idata section (or import table, as I prefer to call it) begins with an array of IMAGE_IMPORT_DESCRIPTORs. There is one IMAGE_IMPORT_DESCRIPTOR for each DLL that the PE file implicitly links to. There‘s no field indicating the number of structures in this array. Instead, the last element of the array is indicated by an IMAGE_IMPORT_DESCRIPTOR that has fields filled with NULLs. The format of an IMAGE_IMPORT_DESCRIPTOR is shown in Figure 10.
Table 8. IMAGE_IMPORT_DESCRIPTOR Format
-
DWORD Characteristics
At one time, this may have been a set of flags. However, Microsoft changed its meaning and never bothered to update WINNT.H. This field is really an offset (an RVA) to an array of pointers. Each of these pointers points to an IMAGE_IMPORT_BY_NAME structure. -
DWORD TimeDateStamp
The time/date stamp indicating when the file was built. -
DWORD ForwarderChain
This field relates to forwarding. Forwarding involves one DLL sending on references to one of its functions to another DLL. For example, in Windows NT, NTDLL.DLL appears to forward some of its exported functions to KERNEL32.DLL. An application may think it‘s calling a function in NTDLL.DLL, but it actually ends up calling into KERNEL32.DLL. This field contains an index into FirstThunk array (described momentarily). The function indexed by this field will be forwarded to another DLL. Unfortunately, the format of how a function is forwarded isn‘t documented, and examples of forwarded functions are hard to find. -
DWORD Name
This is an RVA to a NULL-terminated ASCII string containing the imported DLL‘s name. Common examples are "KERNEL32.DLL" and "USER32.DLL". -
PIMAGE_THUNK_DATA FirstThunk
This field is an offset (an RVA) to an IMAGE_THUNK_DATA union. In almost every case, the union is interpreted as a pointer to an IMAGE_IMPORT_BY_NAME structure. If the field isn‘t one of these pointers, then it‘s supposedly treated as an export ordinal value for the DLL that‘s being imported. It‘s not clear from the documentation if you really can import a function by ordinal rather than by name.
The important parts of an IMAGE_IMPORT_DESCRIPTOR are the imported DLL name and the two arrays of IMAGE_IMPORT_BY_NAME pointers. In the EXE file, the two arrays (pointed to by the Characteristics and FirstThunk fields) run parallel to each other, and are terminated by a NULL pointer entry at the end of each array. The pointers in both arrays point to an IMAGE_IMPORT_BY_NAME structure. Figure 11 shows the situation graphically. Figure 12 shows the PEDUMP output for an imports table.
Figure 3. Two parallel arrays of pointers
Table 9. Imports Table from an EXE File
GDI32.dll
Hint/Name Table: 00013064
TimeDateStamp: 2C51B75B
ForwarderChain: FFFFFFFF
First thunk RVA: 00013214
Ordn Name
48 CreatePen
57 CreateSolidBrush
62 DeleteObject
160 GetDeviceCaps
// Rest of table omitted...
KERNEL32.dll
Hint/Name Table: 0001309C
TimeDateStamp: 2C4865A0
ForwarderChain: 00000014
First thunk RVA: 0001324C
Ordn Name
83 ExitProcess
137 GetCommandLineA
179 GetEnvironmentStrings
202 GetModuleHandleA
// Rest of table omitted...
SHELL32.dll
Hint/Name Table: 00013138
TimeDateStamp: 2C41A383
ForwarderChain: FFFFFFFF
First thunk RVA: 000132E8
Ordn Name
46 ShellAboutA
USER32.dll
Hint/Name Table: 00013140
TimeDateStamp: 2C474EDF
ForwarderChain: FFFFFFFF
First thunk RVA: 000132F0
Ordn Name
10 BeginPaint
35 CharUpperA
39 CheckDlgButton
40 CheckMenuItem
// Rest of table omitted...
There is one IMAGE_IMPORT_BY_NAME structure for each function that the PE file imports. An IMAGE_IMPORT_BY_NAME structure is very simple, and looks like this:
WORD Hint;
BYTE Name[?];
The first field is the best guess as to what the export ordinal for the imported function is. Unlike with NE files, this value doesn‘t have to be correct. Instead, the loader uses it as a suggested starting value for its binary search for the exported function. Next is an ASCIIZ string with the name of the imported function.
Why are there two parallel arrays of pointers to the IMAGE_IMPORT_BY_NAME structures? The first array (the one pointed at by the Characteristics field) is left alone, and never modified. It‘s sometimes called the hint-name table. The second array (pointed at by the FirstThunk field) is overwritten by the PE loader. The loader iterates through each pointer in the array and finds the address of the function that each IMAGE_IMPORT_BY_NAME structure refers to. The loader then overwrites the pointer to IMAGE_IMPORT_BY_NAME with the found function‘s address. The [XXXXXXXX] portion of the JMP DWORD PTR [XXXXXXXX] thunk refers to one of the entries in the FirstThunk array. Since the array of pointers that‘s overwritten by the loader eventually holds the addresses of all the imported functions, it‘s called the Import Address Table.
For you Borland users, there‘s a slight twist to the above description. A PE file produced by TLINK32 is missing one of the arrays. In such an executable, the Characteristics field in the IMAGE_IMPORT_DESCRIPTOR (aka the hint-name array) is 0. Therefore, only the array that‘s pointed at by the FirstThunk field (the Import Address Table) is guaranteed to exist in all PE files. The story would end here, except that I ran into an interesting problem when writing PEDUMP. In the never ending search for optimizations, Microsoft "optimized" the thunk array in the system DLLs for Windows NT (KERNEL32.DLL and so on). In this optimization, the pointers in the array don‘t point to an IMAGE_IMPORT_BY_NAME structure—rather, they already contain the address of the imported function. In other words, the loader doesn‘t need to look up function addresses and overwrite the thunk array with the imported function‘s addresses. This causes a problem for PE dumping programs that are expecting the array to contain pointers to IMAGE_IMPORT_BY_NAME structures. You might be thinking, "But Matt, why don‘t you just use the hint-name table array?" That would be an ideal solution, except that the hint-name table array doesn‘t exist in Borland files. The PEDUMP program handles all these situations, but the code is understandably messy.
Since the import address table is in a writeable section, it‘s relatively easy to intercept calls that an EXE or DLL makes to another DLL. Simply patch the appropriate import address table entry to point at the desired interception function. There‘s no need to modify any code in either the caller or callee images. What could be easier?
It‘s interesting to note that in Microsoft-produced PE files, the import table is not something wholly synthesized by the linker. All the pieces necessary to call a function in another DLL reside in an import library. When you link a DLL, the library manager (LIB32.EXE or LIB.EXE) scans the OBJ files being linked and creates an import library. This import library is completely different from the import libraries used by 16-bit NE file linkers. The import library that the 32-bit LIB produces has a .text section and several .idata$ sections. The .text section in the import library contains the JMP DWORD PTR [XXXXXXXX] thunk, which has a name stored for it in the OBJ‘s symbol table. The name of the symbol is identical to the name of the function being exported by the DLL (for example, _Dispatch_Message@4). One of the .idata$ sections in the import library contains the DWORD that the thunk dereferences through. Another of the .idata$ sections has a space for the hint ordinal followed by the imported function‘s name. These two fields make up an IMAGE_IMPORT_BY_NAME structure. When you later link a PE file that uses the import library, the import library‘s sections are added to the list of sections from your OBJs that the linker needs to process. Since the thunk in the import library has the same name as the function being imported, the linker assumes the thunk is really the imported function, and fixes up calls to the imported function to point at the thunk. The thunk in the import library is essentially "seen" as the imported function.
Besides providing the code portion of an imported function thunk, the import library provides the pieces of the PE file‘s .idata section (or import table). These pieces come from the various .idata$ sections that the library manager put into the import library. In short, the linker doesn‘t really know the differences between imported functions and functions that appear in a different OBJ file. The linker just follows its preset rules for building and combining sections, and everything falls into place naturally.
PE File Exports
The opposite of importing a function is exporting a function for use by EXEs or other DLLs. A PE file stores information about its exported functions in the .edata section. Generally, Microsoft linker-generated PE EXE files don‘t export anything, so they don‘t have an .edata section. Borland‘s TLINK32 always exports at least one symbol from an EXE. Most DLLs do export functions and have an .edata section. The primary components of an .edata section (aka the export table) are tables of function names, entry point addresses, and export ordinal values. In an NE file, the equivalents of an export table are the entry table, the resident names table, and the nonresident names table. These tables are stored as part of the NE header, rather than in distinct segments or resources.
At the start of an .edata section is an IMAGE_EXPORT_DIRECTORY structure (see Table 10). This structure is immediately followed by data pointed to by fields in the structure.
Table 10. IMAGE_EXPORT_DIRECTORY Format
-
DWORD Characteristics
This field appears to be unused and is always set to 0. -
DWORD TimeDateStamp
The time/date stamp indicating when this file was created. -
WORD MajorVersion
WORD MinorVersion
These fields appear to be unused and are set to 0. -
DWORD Name
The RVA of an ASCIIZ string with the name of this DLL. -
DWORD Base
The starting ordinal number for exported functions. For example, if the file exports functions with ordinal values of 10, 11, and 12, this field contains 10. To obtain the exported ordinal for a function, you need to add this value to the appropriate element of the AddressOfNameOrdinals array. -
DWORD NumberOfFunctions
The number of elements in the AddressOfFunctions array. This value is also the number of functions exported by this module. Theoretically, this value could be different than the NumberOfNames field (next), but actually they‘re always the same. -
DWORD NumberOfNames
The number of elements in the AddressOfNames array. This value seems always to be identical to the NumberOfFunctions field, and so is the number of exported functions. -
PDWORD *AddressOfFunctions
This field is an RVA and points to an array of function addresses. The function addresses are the entry points (RVAs) for each exported function in this module. -
PDWORD *AddressOfNames
This field is an RVA and points to an array of string pointers. The strings are the names of the exported functions in this module. -
PWORD *AddressOfNameOrdinals
This field is an RVA and points to an array of WORDs. The WORDs are the export ordinals of all the exported functions in this module. However, don‘t forget to add in the starting ordinal number specified in the Base field.
The layout of the export table is somewhat odd (see Figure 4 and Table 10). As I mentioned earlier, the requirements for exporting a function are a name, an address, and an export ordinal. You‘d think that the designers of the PE format would have put all three of these items into a structure, and then have an array of these structures. Instead, each component of an exported entry is an element in an array. There are three of these arrays (AddressOfFunctions, AddressOfNames, AddressOfNameOrdinals), and they are all parallel to one another. To find all the information about the fourth function, you need to look up the fourth element in each array.
Figure 4. Export table layout
Table 11. Typical Exports Table from an EXE File
Name: KERNEL32.dll
Characteristics: 00000000
TimeDateStamp: 2C4857D3
Version: 0.00
Ordinal base: 00000001
# of functions: 0000021F
# of Names: 0000021F
Entry Pt Ordn Name
00005090 1 AddAtomA
00005100 2 AddAtomW
00025540 3 AddConsoleAliasA
00025500 4 AddConsoleAliasW
00026AC0 5 AllocConsole
00001000 6 BackupRead
00001E90 7 BackupSeek
00002100 8 BackupWrite
0002520C 9 BaseAttachCompleteThunk
00024C50 10 BasepDebugDump
// Rest of table omitted...
Incidentally, if you dump out the exports from the Windows NT system DLLs (for example, KERNEL32.DLL and USER32.DLL), you‘ll note that in many cases there are two functions that only differ by one character at the end of the name, for instance CreateWindowExA and CreateWindowExW. This is how UNICODE support is implemented transparently. The functions that end with A are the ASCII (or ANSI) compatible functions, while those ending in W are the UNICODE version of the function. In your code, you don‘t explicitly specify which function to call. Instead, the appropriate function is selected in WINDOWS.H, via preprocessor #ifdefs. This excerpt from the Windows NT WINDOWS.H shows an example of how this works:
#ifdef UNICODE
#define DefWindowProc DefWindowProcW
#else
#define DefWindowProc DefWindowProcA
#endif // !UNICODE
PE File Resources
Finding resources in a PE file is quite a bit more complicated than in an NE file. The formats of the individual resources (for example, a menu) haven‘t changed significantly but you need to traverse a strange hierarchy to find them.
Navigating the resource directory hierarchy is like navigating a hard disk. There‘s a master directory (the root directory), which has subdirectories. The subdirectories have subdirectories of their own that may point to the raw resource data for things like dialog templates. In the PE format, both the root directory of the resource directory hierarchy and all of its subdirectories are structures of type IMAGE_RESOURCE_DIRECTORY (see Table 12).
Table 12. IMAGE_RESOURCE_DIRECTORY Format
-
DWORD Characteristics
Theoretically this field could hold flags for the resource, but appears to always be 0. -
DWORD TimeDateStamp
The time/date stamp describing the creation time of the resource. -
WORD MajorVersion
WORD MinorVersion
Theoretically these fields would hold a version number for the resource. These field appear to always be set to 0.
WORD NumberOfNamedEntries
The number of array elements that use names and that follow this structure.
-
WORD NumberOfIdEntries
The number of array elements that use integer IDs, and which follow this structure. -
IMAGE_RESOURCE_DIRECTORY_ENTRY DirectoryEntries[]
This field isn‘t really part of the IMAGE_RESOURCE_DIRECTORY structure. Rather, it‘s an array of IMAGE_RESOURCE_DIRECTORY_ENTRY structures that immediately follow the IMAGE_RESOURCE_DIRECTORY structure. The number of elements in the array is the sum of the NumberOfNamedEntries and NumberOfIdEntries fields. The directory entry elements that have name identifiers (rather than integer IDs) come first in the array.
A directory entry can either point at a subdirectory (that is, to another IMAGE_RESOURCE_DIRECTORY), or it can point to the raw data for a resource. Generally, there are at least three directory levels before you get to the actual raw resource data. The top-level directory (of which there‘s only one) is always found at the beginning of the resource section (.rsrc). The subdirectories of the top-level directory correspond to the various types of resources found in the file. For example, if a PE file includes dialogs, string tables, and menus, there will be three subdirectories: a dialog directory, a string table directory, and a menu directory. Each of these type subdirectories will in turn have ID subdirectories. There will be one ID subdirectory for each instance of a given resource type. In the above example, if there are three dialog boxes, the dialog directory will have three ID subdirectories. Each ID subdirectory will have either a string name (such as "MyDialog") or the integer ID used to identify the resource in the RC file. Figure 5 shows a resource directory hierarchy example in visual form. Table 13 shows the PEDUMP output for the resources in the Windows NT CLOCK.EXE.
Figure 5. Resource directory hierarchy
Table 13. Resources Hierarchy for CLOCK.EXE
ResDir (0) Named:00 ID:06 TimeDate:2C3601DB Vers:0.00 Char:0
ResDir (ICON) Named:00 ID:02 TimeDate:2C3601DB Vers:0.00 Char:0
ResDir (1) Named:00 ID:01 TimeDate:2C3601DB Vers:0.00 Char:0
ID: 00000409 Offset: 00000200
ResDir (2) Named:00 ID:01 TimeDate:2C3601DB Vers:0.00 Char:0
ID: 00000409 Offset: 00000210
ResDir (MENU) Named:02 ID:00 TimeDate:2C3601DB Vers:0.00 Char:0
ResDir (CLOCK) Named:00 ID:01 TimeDate:2C3601DB Vers:0.00 Char:0
ID: 00000409 Offset: 00000220
ResDir (GENERICMENU) Named:00 ID:01 TimeDate:2C3601DB Vers:0.00 Char:0
ID: 00000409 Offset: 00000230
ResDir (DIALOG) Named:01 ID:01 TimeDate:2C3601DB Vers:0.00 Char:0
ResDir (ABOUTBOX) Named:00 ID:01 TimeDate:2C3601DB Vers:0.00 Char:0
ID: 00000409 Offset: 00000240
ResDir (64) Named:00 ID:01 TimeDate:2C3601DB Vers:0.00 Char:0
ID: 00000409 Offset: 00000250
ResDir (STRING) Named:00 ID:03 TimeDate:2C3601DB Vers:0.00 Char:0
ResDir (1) Named:00 ID:01 TimeDate:2C3601DB Vers:0.00 Char:0
ID: 00000409 Offset: 00000260
ResDir (2) Named:00 ID:01 TimeDate:2C3601DB Vers:0.00 Char:0
ID: 00000409 Offset: 00000270
ResDir (3) Named:00 ID:01 TimeDate:2C3601DB Vers:0.00 Char:0
ID: 00000409 Offset: 00000280
ResDir (GROUP_ICON) Named:01 ID:00 TimeDate:2C3601DB Vers:0.00 Char:0
ResDir (CCKK) Named:00 ID:01 TimeDate:2C3601DB Vers:0.00 Char:0
ID: 00000409 Offset: 00000290
ResDir (VERSION) Named:00 ID:01 TimeDate:2C3601DB Vers:0.00 Char:0
ResDir (1) Named:00 ID:01 TimeDate:2C3601DB Vers:0.00 Char:0
ID: 00000409 Offset: 000002A0
As mentioned earlier, each directory entry is a structure of type IMAGE_RESOURCE_DIRECTORY_ENTRY (boy, these names are getting long!). Each IMAGE_RESOURCE_DIRECTORY_ENTRY has the format shown in Table 13.
Table 14. IMAGE_RESOURCE_DIRECTORY_ENTRY Format
-
DWORD Name
This field contains either an integer ID or a pointer to a structure that contains a string name. If the high bit (0x80000000) is zero, this field is interpreted as an integer ID. If the high bit is nonzero, the lower 31 bits are an offset (relative to the start of the resources) to an IMAGE_RESOURCE_DIR_STRING_U structure. This structure contains a WORD character count, followed by a UNICODE string with the resource name. Yes, even PE files intended for non-UNICODE Win32 implementations use UNICODE here. To convert the UNICODE string to an ANSI string, use the WideCharToMultiByte function. -
DWORD OffsetToData
This field is either an offset to another resource directory or a pointer to information about a specific resource instance. If the high bit (0x80000000) is set, this directory entry refers to a subdirectory. The lower 31 bits are an offset (relative to the start of the resources) to another IMAGE_RESOURCE_DIRECTORY. If the high bit isn‘t set, the lower 31 bits point to an IMAGE_RESOURCE_DATA_ENTRY structure. The IMAGE_RESOURCE_DATA_ENTRY structure contains the location of the resource‘s raw data, its size, and its code page.
To go further into the resource formats, I‘d need to discuss the format of each resource type (dialogs, menus, and so on). Covering these topics could easily fill up an entire article on its own.
PE File Base Relocations
When the linker creates an EXE file, it makes an assumption about where the file will be mapped into memory. Based on this, the linker puts the real addresses of code and data items into the executable file. If for whatever reason the executable ends up being loaded somewhere else in the virtual address space, the addresses the linker plugged into the image are wrong. The information stored in the .reloc section allows the PE loader to fix these addresses in the loaded image so that they‘re correct again. On the other hand, if the loader was able to load the file at the base address assumed by the linker, the .reloc section data isn‘t needed and is ignored. The entries in the .reloc section are called base relocations since their use depends on the base address of the loaded image.
Unlike relocations in the NE file format, base relocations are extremely simple. They boil down to a list of locations in the image that need a value added to them. The format of the base relocation data is somewhat quirky. The base relocation entries are packaged in a series of variable length chunks. Each chunk describes the relocations for one 4KB page in the image. Let‘s look at an example to see how base relocations work. An executable file is linked assuming a base address of 0x10000. At offset 0x2134 within the image is a pointer containing the address of a string. The string starts at physical address 0x14002, so the pointer contains the value 0x14002. You then load the file, but the loader decides that it needs to map the image starting at physical address 0x60000. The difference between the linker-assumed base load address and the actual load address is called the delta. In this case, the delta is 0x50000. Since the entire image is 0x50000 bytes higher in memory, so is the string (now at address 0x64002). The pointer to the string is now incorrect. The executable file contains a base relocation for the memory location where the pointer to the string resides. To resolve a base relocation, the loader adds the delta value to the original value at the base relocation address. In this case, the loader would add 0x50000 to the original pointer value (0x14002), and store the result (0x64002) back into the pointer‘s memory. Since the string really is at 0x64002, everything is fine with the world.
Each chunk of base relocation data begins with an IMAGE_BASE_RELOCATION structure that looks like Table 14. Table 15 shows some base relocations as shown by PEDUMP. Note that the RVA values shown have already been displaced by the VirtualAddress in the IMAGE_BASE_RELOCATION field.
Figure 15. IMAGE_BASE_RELOCATION Format
-
DWORD VirtualAddress
This field contains the starting RVA for this chunk of relocations. The offset of each relocation that follows is added to this value to form the actual RVA where the relocation needs to be applied. -
DWORD SizeOfBlock
The size of this structure plus all the WORD relocations that follow. To determine the number of relocations in this block, subtract the size of an IMAGE_BASE_RELOCATION (8 bytes) from the value of this field, and then divide by 2 (the size of a WORD). For example, if this field contains 44, there are 18 relocations that immediately follow:(44 - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD) = 18 WORD TypeOffset
This isn‘t just a single WORD, but rather an array of WORDs, the number of which is calculated by the above formula. The bottom 12 bits of each WORD are a relocation offset, and need to be added to the value of the Virtual Address field from this relocation block‘s header. The high 4 bits of each WORD are a relocation type. For PE files that run on Intel CPUs, you‘ll only see two types of relocations:
0 | IMAGE_REL_BASED_ABSOLUTE | This relocation is meaningless and is only used as a place holder to round relocation blocks up to a DWORD multiple size. |
3 | IMAGE_REL_BASED_HIGHLOW | This relocation means add both the high and low 16 bits of the delta to the DWORD specified by the calculated RVA. |
Table 16. The Base Relocations from an EXE File
Virtual Address: 00001000 size: 0000012C
00001032 HIGHLOW
0000106D HIGHLOW
000010AF HIGHLOW
000010C5 HIGHLOW
// Rest of chunk omitted...
Virtual Address: 00002000 size: 0000009C
000020A6 HIGHLOW
00002110 HIGHLOW
00002136 HIGHLOW
00002156 HIGHLOW
// Rest of chunk omitted...
Virtual Address: 00003000 size: 00000114
0000300A HIGHLOW
0000301E HIGHLOW
0000303B HIGHLOW
0000306A HIGHLOW
// Rest of relocations omitted...
Differences Between PE and COFF OBJ Files
There are two portions of the PE file that are not used by the operating system. These are the COFF symbol table and the COFF debug information. Why would anyone need COFF debug information when the much more complete CodeView information is available? If you intend to use the Windows NT system debugger (NTSD) or the Windows NT kernel debugger (KD), COFF is the only game in town. For those of you who are interested, I‘ve included a detailed description of these parts of the PE file in the online posting that accompanies this article (available on all MSJ bulletin boards).
At many points throughout the preceding discussion, I‘ve noted that many structures and tables are the same in both a COFF OBJ file and the PE file created from it. Both COFF OBJ and PE files have an IMAGE_FILE_HEADER at or near their beginning. This header is followed by a section table that contains information about all the sections in the file. The two formats also share the same line number and symbol table formats, although the PE file can have additional non-COFF symbol tables as well. The amount of commonality between the OBJ and PE EXE formats is evidenced by the large amount of common code in PEDUMP (see COMMON.C on any MSJ bulletin board).
This similarity between the two file formats isn‘t happenstance. The goal of this design is to make the linker‘s job as easy as possible. Theoretically, creating an EXE file from a single OBJ should be just a matter of inserting a few tables and modifying a couple of file offsets within the image. With this in mind, you can think of a COFF file as an embryonic PE file. Only a few things are missing or different, so I‘ll list them here.
- COFF OBJ files don‘t have an MS-DOS stub preceding the IMAGE_FILE_HEADER, nor is there a "PE" signature preceding the IMAGE_FILE_HEADER.
- OBJ files don‘t have the IMAGE_OPTIONAL_HEADER. In a PE file, this structure immediately follows the IMAGE_FILE_HEADER. Interestingly, COFF LIB files do have an IMAGE_OPTIONAL_HEADER. Space constraints prevent me from talking about LIB files here.
- OBJ files don‘t have base relocations. Instead, they have regular symbol-based fixups. I haven‘t gone into the format of the COFF OBJ file relocations because they‘re fairly obscure. If you want to dig into this particular area, the PointerToRelocations and NumberOfRelocations fields in the section table entries point to the relocations for each section. The relocations are an array of IMAGE_RELOCATION structures, which is defined in WINNT.H. The PEDUMP program can show OBJ file relocations if you enable the proper switch.
- The CodeView information in an OBJ file is stored in two sections (.debug$S and .debug$T). When the linker processes the OBJ files, it doesn‘t put these sections in the PE file. Instead, it collects all these sections and builds a single symbol table stored at the end of the file. This symbol table isn‘t a formal section (that is, there‘s no entry for it in the PE‘s section table).
Using PEDUMP
PEDUMP is a command-line utility for dumping PE files and COFF OBJ format files. It uses the Win32 console capabilities to eliminate the need for extensive user interface work. The syntax for PEDUMP is as follows:
PEDUMP [switches] filename
The switches can be seen by running PEDUMP with no arguments. PEDUMP uses the switches shown in Table 17. By default, none of the switches are enabled. Running PEDUMP without any of the switches provides most of the useful information without creating a huge amount of output. PEDUMP sends its output to the standard output file, so its output can be redirected to a file with an > on the command line.
Table 17. PEDUMP Switches
/A | Include everything in dump (essentially, enable all the switches) |
/H | Include a hex dump of each section at the end of the dump |
/L | Include line number information (both PE and COFF OBJ files) |
/R | Show base relocations (PE files only) |
/S | Show symbol table (both PE and COFF OBJ files) |
Summary
With the advent of Win32, Microsoft made sweeping changes in the OBJ and executable file formats to save time and build on work previously done for other operating systems. A primary goal of these file formats is to enhance portability across different platforms.