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 routinevoid 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);
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;}
1 WIN32 与 PE 基本概念让我们复习一下几个透过PE文件的设计了解到的基本概念(见图1)。我用术语"MODULE"来表示一个可执行文件或一个DLL载入内存的代码(CODE)、数据(DATA)、资源(RESOURCES),除了代码和数据是你的程序直接使用的,一个模块还可以由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只是一些项目相对于文件映射到内存的偏移。比如说,载入器把一个文件映射到虚拟地址0x10000开始的内存块。如果一个映像中的实际的表的首址是0x10464,那么它的RVA就是0x464。(虚拟地址 0x10464)-(基地址 0x10000)=RVA 0x00464为了把一个RVA转化成一个有用的指针,只需要把RVA值加到模块的基地址上即可。基地址是内存映射EXE和DLL文件的首址,在Win32中这是一个很重要的概念。为了方便起见,WindowsNT 和Windows9x用模块的基地址作为这个模块的实例句柄(HINSTANCE)。在Win32中,把模块的基地址叫做HINSTANCE可能导致混淆,因为术语"实例句柄"来自16位Windows。一个程序在16位Windows中的每个拷贝得到它自己分开的数据段(和一个联系起来的全局句柄)来把它和这个程序其它的拷贝分别开来,就形成了术语"实例句柄"。在Win32中,每个程序不必和其它程序区别开来,因为他们不共享相同的地址空间。术语INSTANCE仍然保持16位windows和32位Windows之间的连续性。在Win32中重要的是你可以对任何DLL调用GetModuleHandle()得到一个指针去访问它的组件(译注)。译注:如果 dllname 为 NULL,则得到执行体自己的模块句柄。这是非常有用的,如通常编译器产生的启动代码将取得这个句柄并将它作为一个参数hInstance传给WinMain !你最终需要理解的PE文件的概念是"块(Section)"。PE文件中的一个块和NE文件中的一个段或者资源等价。块可以包含代码或者数据。和段不同的是,块是内存中连续的空间,而没有尺寸限制。当你的连接器和库为你建立,并且包含对操作系统非常重要的信息的其它的数据块时,这些块包含你的程序直接声明和使用的代码或数据。在一些PE格式的描述中,块也叫做对象。术语对象有如此多的涵义,以至于只能把代码和数据叫做"块"。2 PE首部和其它可执行文件格式一样,PE文件在众所周知的地方有一些定义文件其余部分面貌的域。首部就包含这样象代码和数据的位置和尺寸的地方,操作系统要对它进行干预,比如初始堆栈大小,和其它重要的块的信息,我将要简短的介绍一下。和微软其它可执行格式相比,主要的首部不是在文件的最开始。典型的PE文件最开始的数百个字节被DOS残留部分占用。这个残留部分是一个可以打印如"这个程序不能在DOS下运行!"这类信息的小程序。所以,你在一个不支持Win32的系统中运行这个程序,便可以得到这类错误信息。当载入器把一个Win32程序映射到内存,这个映射文件的第一个字节对应于DOS残留部分的第一个字节。那是无疑的。和你启动的任一个基于Win32 的程序一起,都有一个基于DOS的程序连带被载入。和微软的其它可执行格式一样,你可以通过查找它的起始偏移来得到真实首部,这个偏移放在DOS残留首部中。WINNT.H头文件包含了DOS残留程序的数据结构定义,使得很容易找到PE首部的起始位置。e_lfanew 域是PE真实首部的偏移。为了得到PE首部在内存中的指针,只需要把这个值加到映像的基址上即可。file://忽/略类型转化和指针转化 ...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"。如果在DOS首部中用了e_lfanew域,你得到一个NE标志而不是PE,那么这是16位NE文件。同样的,在标志域中的LE表示这是一个Windows3.x 的虚拟设备驱动程序(VxD)。LX表示这个文件是OS/2 2.0文件。PEDWORD标志后的是结构 IMAGE_FILE_HEADER。这个域只包含这个文件最基本的信息。这个结构表现为并未从它的原始COFF实现更改过。除了是PE首部的一部分,它还表现在微软Win32编译器生成的COFF OBJ 文件的最开始部分。IMAGE_FILE_HEADER的这个域显示在下面:表2 IMAGE_FILE_HEADER Fields WORD Machine 表示CPU的类型,下面定义了一些CPU的ID0x14d Intel i8600x14c Intel I386 (same ID used for 486 and 586)0x162 MIPS R30000x166 MIPS R40000x183 DEC Alpha AXP
WORD NumberOfSections 这个文件中的块数目。
DWORD TimeDateStamp 连接器产生这个文件的日期(对OBJ文件是编译器),这个域保存的数是从1969年12月下午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 关于这个文件信息的标志。一些重要的域如下:
0x0001 这个文件中没有重定位信息0x0002 可执行文件映像(不是OBJ或LIB文件)0x2000 文件是动态连接库,而非程序
其它域定义在WINNT.H中。PE首部的第三个组成部分是一个IMAGE_OPTIONAL_HEADER型的结构。对PE文件,这一部分当然不是"可选的"。COFF格式允许单独实现来定义一个超出标准IMAGE_FILE_HEADER附加信息的结构。IMAGE_OPTIONAL_HEADER里面的域是PE的实现者感到超出IMAGE_FILE_HEADER基本信息以外非常关键的信息。并非 IMAGE_OPTIONAL_HEADER 的所有域都是重要的(见图4)。比较重要,需要知道的是ImageBase 和 SubSystem 域。你可以忽略其它域的描述。表3 IMAGE_FILE_HEADER 的域:WORD Magic 表现为一些类别的标志字,通常是0X010B 。BYTE MajorLinkerVersion BYTE MinorLinkerVersion 生成这个文件的连接器的版本。这个数字以十进制显示比用十六进制好。一个典型的连接器版本是2.23。
DWORD SizeOfCode 所有代码块的进位尺寸。通常大多数文件只有一个代码块,所以这个域和 .TEXT 块匹配。
DWORD SizeOfInitializedData 已初始化的数据组成的块的大小(不包括代码段)。然而,和它在文件中的表现形式并不一致。
DWORD SizeOfUninitializedData 载入器在虚拟内存中申请空间,但在磁盘上的文件中并不占用空间的块的尺寸。这些块在程序启动时不需要指定初值,因此术语名就是"未初始化的数据"。未初始化的数据通常在一个名叫 .bss 的块中。
DWORD AddressOfEntryPoint 载入器开始执行这个程序的地址,即这个PE文件的入口地址。这是一个RVA,通常在 .text 块中。
DWORD BaseOfCode 代码块起始地址的RVA 。在内存中,代码块通常在PE首部之后,数据块之前。在微软的连接器产生的EXE文件中,这个值通常是0x1000 。Borland 的连接器 TLINK32 也一样,把映像第一个代码块的RVA和映像基址相加,填入这个域。译注:这个域好像一直没有什么用
DWORD BaseOfData 数据块起始地址的RVA 。在内存中,数据块经常在最后,在PE首部和代码块之后。译注:这个域好像也一直没有什么用
DWORD ImageBase 连接器创建一个可执行文件时,它假定这个文件被映射到内存中的一个指定的地方,这个地址就存在这个域中,假定一个载入地址可以使连接器优化以便节省空间。如果载入器真的把这个文件映射到了这个地方,在运行之前代码不需要任何改变。在为WindowsNT 创建的可执行文件中,默认的ImageBase是0x10000。对DLL,默认是0x40000。在Window95中,地址0x10000不能用来载入32位EXE文件,因为这个区域在一个被所有进程共享的线性地址空间中。因此,微软把Win32可执行文件的默认基址改为0x40000,假定基址为0x10000的老程序坐在Windows95 中需要更长的载入时间,这是因为载入器需要重定位基址。译注:这个域即"Prefered Load Address",如果没有什么意外,这就是该PE文件载入内存后的地址。
DWORD SectionAlignment 映射到内存中时,每个块都必须保证开始于这个值的整数倍。为了分页的目的,默认的SectionAlignment 是 0x1000。
DWORD FileAlignment 在PE文件中,组成每个块的生鲜数据必须保证开始于这个值的整数倍。默认值是0x200 字节,也许是为了保证块都开始于一个磁盘扇区(一个扇区通常是 512字节)。这个域和NE文件中的段/资源对齐(segment/resourcealignment)尺寸是等价的。和NE文件不同的是,PE文件通常没有数百个的块,所以,为了对齐而浪费的通常空间很少。
WORD MajorOperatingSystemVersion WORD MinorOperatingSystemVersion 这个程序运行需要的操作系统的最小版本号。这个域有点含糊,因为Subsystem 域(后面将会说到)可以提供类似的功能。这个域在到目前为止的Win32中默认是1.0。
WORD MajorSubsystemVersion WORD MinorSubsystemVersion 这个程序运行需要的最小子系统版本号。这个域的一个典型值是3.10 (表示WindowsNT 3.1)。
DWORD SizeOfImage 载入器必须关心的这个映像所有部分的大小总和。是从映像的开始到最后一个块结尾这段区域的大小。最后一个块结尾按SectionAlignment进位。译注:这个很重要,可以大,但不可以小!
DWORD SizeOfHeaders PE首部和块表的大小。块的实际数据紧跟在所有首部组件之后。
DWORD CheckSum 这个文件的CRC校验和。在微软可执行格式中,这个域被忽略并且置为0 。这个规则的一个例外情况是信任服务,这类EXE文件必须有一个合法的校验和。
WORD Subsystem 可执行文件的用户界面使用的子系统类型。WINNT.H 定义了下面这些值: NATIVE 1 不需要子系统(比如设备驱动)WINDOWS_GUI 2 在Windows图形用户界面子系统下运行WINDOWS_CUI 3 在Windows字符子系统下运行(控制台程序)OS2_CUI 5 在OS/2字符子系统下运行(仅对OS/2 1.x)POSIX_CUI 7 在 Posix 字符子系统下运行
WORD DllCharacteristics 指定在何种环境下一个DLL的初始化函数(比如DllMain)将被调用的标志变量。这个值经常被置为0 。但是操作系统在下面四种情况下仍然调用DLL的初始化函数。
下面的值定义为:1 DLL第一次载入到进程中的地址空间中时调用2 一个线程结束时调用4 一个线程开始时调用8 退出DLL时调用
DWORD SizeOfStackReserve 为初始线程保留的虚拟内存总数。然而并不是所有这些内存都被提交(见下一个域)。这个域的默认值是0x100000(1Mbytes)。如果你在CreateThread 中把堆栈尺寸指定为 0 ,结果将是用这个相同的值(0x10000)。
DWORD SizeOfStackCommit 开始提交的初始线程堆栈总数。对微软的连接器,这个域默认是0x1000字节(一页),TLINK32 是两页。
DWORD SizeOfHeapReserve 为初始进程的堆保留的虚拟内存总数。这个堆的句柄可以用GetPocessHeap 得到。并不是所有这些内存都被提交(见下一个域)。
DWORD SizeOfHeapCommit 开始为进程堆提交的内存总数。默认是一页。
DWORD LoaderFlags 从WINNT.H中可以看到,这些标志是和调试支持相联系的。我从没有见到过在哪个可执行文件中这些位都置位了,清除它让连接器来设置它。下面的值定义为: 1. 在开始进程前调用一个端点指令2. 进程被载入时调用一个调试器
DWORD NumberOfRvaAndSizes 数据目录数组中的的条目数目(见下面)。当前的工具通常把这个值设为16。 IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES] 一个IMAGE_DATA_DIRECTORY结构数组。初始数组元素包含可执行文件的重要部分的起始RVA和大小。这个数组最末的一些元素现在没有使用。这个数组的第一个元素经常时导出函数表的地址和尺寸。第二个数组条目是导入函数表的地址和尺寸,等等。对一个完整的、已定义的数组条目,见IMAGE_DIRECTORY_ENTRY_XXX在WINNT.H中的定义。这个数组允许载入器迅速查找这个映像的一个指定的块(例如,导入函数表),而不需要遍历映像的每个块,通过比较名字来确定。大部分数组条目描述一整块数据。然而,IMAGE_DIRECTORY_ENTRY_DEBUG项只包括 .rdata 块的一小部分字节。
02 .bss VirtSize: 00001438 VirtAddr: 00007000raw data offs: 00000000 raw data size: 00001600relocation offs: 00000000 relocations: 00000000line # offs: 00000000 line #'s: 00000000characteristics: C0000080UNINITIALIZED_DATA MEM_READ MEM_WRITE
03 .rdata VirtSize: 0000015C VirtAddr: 00009000raw data offs: 00006000 raw data size: 00000200relocation offs: 00000000 relocations: 00000000line # offs: 00000000 line #'s: 00000000characteristics: 40000040INITIALIZED_DATA MEM_READ
04 .data VirtSize: 0000239C VirtAddr: 0000A000raw data offs: 00006200 raw data size: 00002400relocation offs: 00000000 relocations: 00000000line # offs: 00000000 line #'s: 00000000characteristics: C0000040INITIALIZED_DATA MEM_READ MEM_WRITE
05 .idata VirtSize: 0000033E VirtAddr: 0000D000raw data offs: 00008600 raw data size: 00000400relocation offs: 00000000 relocations: 00000000line # offs: 00000000 line #'s: 00000000characteristics: C0000040INITIALIZED_DATA MEM_READ MEM_WRITE
06 .reloc VirtSize: 000006CE VirtAddr: 0000E000raw data offs: 00008A00 raw data size: 00000800relocation offs: 00000000 relocations: 00000000line # offs: 00000000 line #'s: 00000000characteristics: 42000040INITIALIZED_DATA MEM_DISCARDABLE MEM_READ 表 5 一个典型OBJ文件的块表01 .drectve PhysAddr: 00000000 VirtAddr: 00000000raw data offs: 000000DC raw data size: 00000026relocation offs: 00000000 relocations: 00000000line # offs: 00000000 line #'s: 00000000characteristics: 00100A00LNK_INFO LNK_REMOVE
02 .debug$S PhysAddr: 00000026 VirtAddr: 00000000raw data offs: 00000102 raw data size: 000016D0relocation offs: 000017D2 relocations: 00000032line # offs: 00000000 line #'s: 00000000characteristics: 42100048INITIALIZED_DATA MEM_DISCARDABLE MEM_READ
03 .data PhysAddr: 000016F6 VirtAddr: 00000000raw data offs: 000019C6 raw data size: 00000D87relocation offs: 0000274D relocations: 00000045line # offs: 00000000 line #'s: 00000000characteristics: C0400040INITIALIZED_DATA MEM_READ MEM_WRITE
04 .text PhysAddr: 0000247D VirtAddr: 00000000raw data offs: 000029FF raw data size: 000010DArelocation offs: 00003AD9 relocations: 000000E9line # offs: 000043F3 line #'s: 000000D9characteristics: 60500020CODE MEM_EXECUTE MEM_READ
05 .debug$T PhysAddr: 00003557 VirtAddr: 00000000raw data offs: 00004909 raw data size: 00000030relocation offs: 00000000 relocations: 00000000line # offs: 00000000 line #'s: 00000000characteristics: 42100048INITIALIZED_DATA MEM_DISCARDABLE MEM_READ
每个IAMGE_SECTION_HEADER都有一个如图7描述的格式。注意每个块中存储的信息缺失了什么是很有趣的。首先,注意没有指明任何预载入的属性。NE文件格式允许你指定应该和模块一起载入的预载入段的属性。OS/2? 2.0 LX 格式有点类似,允许你指定预载入八页(内存页:译注,下同)。PE格式就没有任何类似的东西。微软必须确保Win32 需求页面的载入性能。表 6 IMAGE_SECTION_HEADER 的格式BYTE Name[IMAGE_SIZEOF_SHORT_NAME] 这是一个为块命名的8字节ANSI名字(不UNICODE)。大部分块名开始于一个 "."(比如".text"),但这并非必须的,就像你可能相信的一些PE文档一样。你可以在汇编语言中用任何一个段指示你自己的块。或者在微软C/C++编译器中用"#pragma data_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即可。用微软的工具,第一个块的默认RVA是0x1000 。在OBJ文件中,这个域没有意义,被置为0 。
DWORD SizeOfRawData 在EXE中,这个域包含这个块按文件对齐尺寸进位后的尺寸。比如说,假定一个文件的对齐尺寸是0x200 。如果这个块的VirtualAddress域(前面那个域)的是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的定义。如下是比较重要的一些标志:
0x00000020 这个块包含代码。通常和可执行标志(0x80000000)一起置位。0x00000040 这个块包含已初始化的数据。除了可执行块和 .bss 块之外几乎所有的块的这个标志都置位。0x00000080 这个块包含未初始化的数据(如 .bss 块)0x00000200 这个块包含注释或其它的信息。这个块的一个典型用法是编译器产生的 .drectve 块,包含链接器命令。0x00000800 这个块的内容不应放进最终的EXE文件中。这些块是编译器或汇编器用来给连接器传递信息的。0x02000000 这个块可以被丢弃,因为一旦它被载入,其进程就不需要它了。最通常的可丢弃块是基本重定位块( .reloc )。0x10000000这个块是可共享的。和DLL一起使用时,这个块的数据可以在使用这个DLL的进程之间共享。默认时数据块是非共享的,这意味着使用这个DLL的各个进程都有自己对这个块的数据的副本。在更专业的术语中,共享块告诉内存管理器把使用这个DLL的所有进程把的这个块的页面映射到内存中相同的物理页面。为使一个块可共享,在连接时用SHARE属性。如:LINK /SECTION:MYDATA,RWS ...告诉连接器叫做"MYDATA"的块是可读的,可写的,共享的。0x20000000 这个块是可执行的。这个标志通常在"包含代码"标志(0x00000020)被置位时置位。0x40000000 这个块是可读的。在EXE文件中,这个域几乎总被置位。0x80000000 这个块是可写的。如果在一个EXE块中这个块未被置位,载入器会把这块的内存映射页面标为只读或"只执行"。有此属性的典型的块是 .data 和 .bss 。有趣的是,.idata 块也有这个属性。PE格式中还缺少"页表"的概念。在LX格式中,OS/2的IMAGE_SECTION_TABLE等价物不直接指向文件中的代码或数据块。代替的,它指向一个指示块中特定范围的属性和位置的页查找表。PE格式分配所有的,并且确保所有的块中的数据将连续的存储在文件中。比较这两种格式:LX可以允许更大的灵活性,但PE风格更简单,更容易协同工作。我已经写了这两种文件的Dumper 。PE格式另一个值得欢迎的改变是所有项目的位置都存储为简单的双字(DWORD)偏移。在NE格式中,几乎所有东西的位置都存储为它们的扇区值。为了得到实际的偏移,你第一步需要查找NE首部的对齐单元尺寸并把它转化为扇区尺寸(典型的是 16 和512字节)。然后你需要把扇区尺寸乘以指定的扇区偏移才得到实际的文件偏移。如果NE文件的某些东西偶然存储为一个扇区偏移,这可能是相对于NE首部的。因为NE首部并不在文件的开始,你需要在自己的代码中调整这个文件的NE首部。总之,PE格式比NE,LX,或LE格式更容易协同工作(假定你能使用内存映像文件)。
4 通用块已经看到了大体上块是什么和它们位于何处,让我们看一下你将会在EXE和OBJ文件中找到的通用块。这个列表决不是完整的,但包含了你每天都碰到的块(甚至你没有意识到的)。.text块是编译器或汇编器结束时产生的通用代码块。因为PE文件运行在32位模式下,并且没有16位段的限制,没有理由根据分开的源文件把代码分为分开的块。代替的,连接器把从不同的OBJ文件得来的 .text 块连接起来放到EXE文件中的一个大 .text 块中。如果你用 Borland C++,编译器把产生的代码放到名为 CODE 的块中。Borland C++ 生成的PE文件有一个名为 CODE 的块而不是名为 .text。我将会简短的解释一下。
5 PE文件的导入表前面,我描述了函数调用怎样到一个外部DLL中而不直接调用这个DLL 。代替的,在执行体中的 .text 块中(如果你用Borland C++ 就是 .icode 块),CALL指令到达一条 JMP DWORD PTR [XXXXXXXX] 指令处。JMP指令寻找的地址把控制转移到实际的目标地址。PE文件的 .idata 会包含一些必要的信息,这些信息是载入器用来确定目标函数的地址以及在执行体映像中去修正他们的。.idata块(或称导入表,我更喜欢这样叫)开始于一个IMAGE_IMPORT_DESCRIPTOR数组。每个DLL都有一个PE文件隐含链接上的IMAGE_IMPORT_DESCRIPTOR。没有指定这个数组中结构的数目的域。代替的,这个数组的最后一个元素是一个全NULL的IMAGE_IMPORT_DESCRIPTOR 。IMAGE_IMPORT_DESCRIPTOR的格式显示在表8 。表 8 IMAGE_IMPORT_DESCRIPTOR Format DWORD Characteristics 在一个时刻,这可能已是一个标志集。然而,微软改变了它的涵义并不再糊涂地升级WINNT.H 。这个月实际上是一个指向指针数组的偏移(RVA)。其中每个指针都指向一个IMAGE_IMPORT_BY_NAME结构。
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 结构。表3以图形显示了这种布局。表12显示了PEDUMP对一个导入表的输出。