您的位置:首页 > 其它

PE文件格式(三)

2007-06-25 16:57 375 查看
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 块的一小部分字节。

3 块表
在PE首部和映像块之间的是块表。块表本质上是包含映像中每个块信息的电话本。映像中的块以他们的起始地址(RVA)排列,而不是按字母排列。
现在,我进一步澄清什么是一个块。在NE文件中,你的程序代码和数据存储在相互区别开来的段中。NE首部的一部分是一个结构数组,每个对应你的程序用到的一个段。数组中的每个结构包含一个段的信息。这些信息存储了段的类型(代码或数据)、大小、和它在文件中的位置。在PE文件中,块表和NE文件中的段表类似。和NE文件的段表不同,PE块表项不存储一个代码和数据块的选择子。代替的,每个块表项存储文件的生鲜数据映射到内存中以后的地址。于是块就和32位段类似,但他们实际上不是单独的段。它们实际上是进程虚拟空间的一个内存范围。
另一个PE文件和NE文件的不同之处是它怎样管理你的程序不用,但操作系统要用的支持数据;例如可执行文件使用的DLL列表或修正表的位置。在NE文件中,资源不被当作段。甚至分配给他们的选择子,资源的相关信息并未存储在NE文件首部的段表中。代替的,提交给一个分隔表的资源朝向PE首部的结尾。关于导入和导出函数的信息也没有授权给它自己的段;它交织在NE首部中。 PE文件的故事就不一样了。任何可能被认为是关键的代码或数据都存在一个完备的块中。于是,导入函数表的信息就存在它自己的块中,导出表也一样。对重定位数据也是一样的。程序或操作系统可能需要的任何代码或数据都可以得到它们自己的块。
在我讨论特定块之前,我需要先描述操作系统管理这些块的数据。在内存中紧跟在PE首部的是一个IMAGE_SECTION_HEADER数组。数组的元素个数在PE首部中给定(IMAGE_NT_HEADER.FileHeader.NumberOfSections域)。我用PEDUMP来输出块表和块的所有的域及其属性。表5 描述了用PEDUMP输出的一个典型EXE文件的块表,表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

每个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 。我将会简短的解释一下。

Figure 2. Calling a function in another module
对我来说,除了我用编译器创建的或从运行时库中得到的代码外,在 .text 块中找到附加的代码是比较有趣的。在一个PE文件中,当你在另一模块中调用一个函数时(比如在USER32.DLL中的GetMessage ),编译器产生的CALL 指令并不把控制直接转移到在DLL中的这个函数(见图8)。代替的,CALL 指令把把控制转移到一个也在 .text 中的
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: