PE格式大量地使用所谓的RVA(相对虚拟地址)。一个RVA,亦即一个“Relative Virtual Addresses”(相对虚拟地址),是在你不知道基地址时,被用来描述一个内存地址的。它是需要加上基地址才能获得线性地址的数值。基地址就是PE映象文件被装入内存的地址,并且可能会随着一次又一次的调用而变化。
例如:假若一个可执行文件被装入的地址是0x400000,并且从RVA 0x1560处开始执行,那么有效的执行开始处将位于0x401560地址处。假若它被装入的地址为0x100000,那么执行开始处就位于0x101560地址处。
因为PE-文件中的各部分(各节)不需要像已载入的映象文件那样对齐,事情变得复杂起来。例如,文件中的各节常按照512(十六进制的0x200----译者注)字节边界对齐,而已载入的映象文件则可能按照4096(十六进制的0x1000----译者注)字节边界对齐。参见下面的“SectionAlignment”(节对齐)和“FileAlignment”(文件对齐)。
因此,为了在PE文件中找到一个特定RVA地址的信息,你得按照文件已被载入时的那样来计算偏移量,但要按照文件的偏移量来跳过。
试举一例,假若你已知道执行开始处位于RVA 0x1560地址处,并且想从那里开始的代码处反汇编。为了从文件中找到这个地址,你得先查明在RAM(内存)中各节是按照4096字节对齐的,并且“.code”节是从RVA 0x1000地址处开始,有16384字节长;然后你才知道RVA 0x1560地址位于此节的偏移量0x560处。你还要查明在文件中那节是按512字节边界对齐,且“.code”节在文件中从偏移量0x800处开始,然后你就知道在文件中代码的执行开始处就在0x800+0x560=0xd60字节处。
然后你反汇编它并发现访问一个变量的线性地址位于0x1051d0处。二进制文件的线性地址在装入时将被重定位,并常被假定使用的是优先载入地址。因为你已查明优先载入地址为0x100000,因此我们可开始处理RVA 0x51d0了。因数据节开始于RVA 0x5000处,且有2048字节长,所以它处于数据节中。又因数据节在文件中开始于偏移量0x4800处,所以该变量就可以在文件中的0x4800+0x51d0-0x5000=0x49d0处找到。
六、可选头(Optional Header)
----------------------------
紧跟在文件头后面的就是IMAGE_OPTIONAL_HEADER(尽管它名叫“可选头”,它却一直都在那里)。它包含有怎样去准确处理PE文件的信息。我们也将从头至尾的介绍其成员。
1)第一个16位的word单元叫“Magic”(魔数),就我目前所观察过的PE文件而言,它的值总是0x010b。
2-3)下面2个字节是创建此文件的链接器的版本(‘MajorLinkerVersion’,“主链接器版本”和‘MinorLinkerVersion’,“最小链接器版本”)。这两个值又是不可靠的,并不能总是正确地反映链接器的版本。(有好几个链接器根本就不设置这个值。)况且,你可想象一下,你连使用的是“什么”链接器都不知道,知道它的版本又有什么作用呢?
4-6)下面3个longword(每个32位)分别用来设定可执行代码的大小(“SizeOfCode”)、已初始化数据的大小(“SizeOfInitializedData”,所谓的“数据段”)、以及未初始化数据的大小(“SizeOfUninitializedData”,所谓的“bss段”)。这些值也是不可靠的(例如:数据段实际上可能会被编译器或者链接器分成好几段),并且你可以通过查看可选头后面的各个“节”来获得更准确的大小。
7)下一个32位值是RVA。这个RVA是代码入口点的偏移量(‘AddressOfEntryPoint’,“入口点地址”)。执行将从这里开始,它可以是:例如DLL文件的LibMain()的地址,或者一个程序的开始代码(这里相应的叫main())的地址,或者驱动程序的DriverEntry()的地址。如果你敢于“手工”装载映象文件,那么在你完成所有的修正和重定位后,你可以从这个地址开始执行你的进程。
8-9)下两个32位值分别是可执行代码的偏移值(‘BaseOfCode’,“代码基址”)和已初始化数据的偏移值(‘BaseOfData’,“数据基址”),两个都是RVA,并且两个对我们来说都没有多少意义,因为你可以通过查看可选头后面的各个“节”来获得更可靠的信息。
未初始化的数据没有偏移量,正因为它没有初始化,所以在映象文件中提供这些数据是没有用处的。
10)下一项是个32位值,提供整个二进制文件包括所有头的优先(线性)载入地址(‘ImageBase’,“映象文件基址”)。这是一个文件已被链接器重定位后的地址(总是64 KB的倍数)。如果二进制文件事实上能被载入这个地址,那么装载器就不用再重定位文件了,也就节省了一些载入时间。
优先载入地址在另一个映象文件已被先载入那个地址(“地址冲突”,在当你载入好几个全部按照链接器的缺省值重定位的DLL文件时经常发生)时,或者该内存已被用于其它目的(堆栈、malloc()、未初始化数据、或不管是什么)时,就不能用了。在这些情况下,映象文件必须被载人其它的地址,并且需要重定位(参见下面的“重定位目录”)。如果是一个DLL文件,这么做还会产生其它问题,因为此时的“绑定输入”已不再有效,所以使用DLL的二进制文件必须被修正----参见下面的“输入目录”一节。
11-12)下两个32位值分别是RAM中的“SectionAlignment”(当映象文件已被载入后,意为“节对齐”)和文件中的“FileAlignment”(文件对齐),它们都是PE文件的各节的对齐值。这两个值通常都是32,或者是:FileAlignment为512,SectionAlignment为4096。节会在以后讨论。
13-14)下2个16位word单元都是预期的操作系统版本信息(MajorOperatingSystemVersion,“主操作系统版本”)和(MinorOperatingSystemVersion,“最小操作系统版本”)[它们都使用微软自己书面确定的名字]。这个版本信息应该为操作系统的版本信息(如NT 或 Win95),而不是子系统的版本信息(如Win32)。版本信息常常被不提供或者错误提供。很明显的,装载器并不使用它们。
15-16)下2个16位word单元都是本二进制文件的版本信息('MajorImageVersion'“主映象文件版本”和
'MinorImageVersion'“最小映象文件版本”)。很多链接器不正确地设定这个信息,许多程序员也懒得提供这些,因此即便存在这样的信息,你最好也不要信赖它。
17-18)下2个16位word单元都是预期的子系统版本信息('MajorSubsystemVersion'“主子系统版本”和'MinorSubsystemVersion'“最小子系统版本”)。此信息应该为Win32或POSIX的版本信息,因为很明显的,16位程序或OS/2程序都不是PE格式的。子系统版本应该被正确的提供,因为它“会”被检验和使用:
如果一个应用程序是一个Win32-GUI应用程序并运行于NT4系统之上,而且子系统版本“不是”4.0的话,那么对话框就不会是以3D形式显示,并且一些其它的特征也只会按“老式”的方式工作,因为此应用程序预期是在NT 3.51系统上运行的,而NT 3.51系统上只有程序管理器而没有浏览器、等等,于是NT 4.0系统就尽可能地仿照那个系统的行为来运行程序。
19)然后,我们便碰到32位的“Win32VersionValue”(Win32版本值)。我不清楚它有什么作用。在我所观察过的PE文件中,它全部都为0。
20)下一个是32位值,给出映象文件将要使用的内存数量,单位为字节(‘SizeOfImage’,“映象文件大小”)。如果是按照“SectionAlignment”对齐的,它就是所有头和节的长度的总和。它提示装载器,为了载入映象文件需要多少页。
21)下一个是32位值,给出所有头的总长度,包括数据目录和节头(‘SizeOfHeaders’,“头的大小”)。同时,它也是从文件的开头到第一节的原始数据的偏移量。
22)然后,我们发现一个32位的校验和(“CheckSum”)。这个校验和,对于当前的NT版本,只在映象文件是NT驱动程序时才校验(如果校验和不正确,驱动就将装载失败)。对于其他的二进制文件形式,校验和不需提供并且可能为0。计算校验和的算法是微软的私产,他们不会告诉你的。但是,Win32 SDK的好几个工具都会计算和/或补正一个有效的校验和,而且imagehelp.dll中的CheckSumMappedFile()函数也会做同样的工作。
使用校验和的目的是为了防止载入无论如何都会冲突的、已损坏的二进制文件----况且一个冲突的驱动程序会导致一个BSOD错误,因此最好根本就不载入这样的坏文件。
23)然后,就到了一个16位的word单元“Subsystem”(子系统),用来说明映象文件应运行于什么样的NT子系统之上:
IMAGE_SUBSYSTEM_NATIVE (1)
二进制文件不需要子系统。用于驱动程序。
IMAGE_SUBSYSTEM_WINDOWS_GUI (2)
映象文件是一个Win32二进制图象文件。(它还是能用AllocConsole()打开一个控制台界面,但在开始时却不能自动地打开。)
IMAGE_SUBSYSTEM_WINDOWS_CUI (3)
二进制文件是一个Win32控制台界面二进制文件。(它将在开始时按照缺省值打开一个控制台,或者继承其父程序的控制台。)
IMAGE_SUBSYSTEM_OS2_CUI (5)
二进制文件是一个OS/2控制台界面二进制文件。(OS/2控制台界面二进制文件是OS/2格式,因此此值在PE文件中很少使用。)
IMAGE_SUBSYSTEM_POSIX_CUI (7)
二进制文件使用POSIX控制台子系统。
Windows 95的二进制文件总是使用Win32子系统,因此它的二进制文件的合法值只有2和3;我不知道windows 95的“原”二进制文件是否可能(会有其它值----译者添加,仅供参考)。
24)下一个是16位的值,指明,如果是DLL文件,何时调用DLL文件的入口点(‘DllCharacteristics’,“DLL特性”)。此值似乎不用;很明显地,DLL文件总是被通报所有的情况。
如果位0被置1,DLL文件被通知进程附加(亦即DLL载入)。
如果位1被置1,DLL文件被通知线程附加(亦即线程终止)。
如果位2被置1,DLL文件被通知线程附加(亦即线程创建)。
如果位3被置1,DLL文件被通知进程附加(亦即DLL卸载)。
25-28)下4个32位值分别是:保留栈的大小(SizeOfStackReserve)、初始时指定栈大小(SizeOfStackCommit)、保留堆的大小(SizeOfHeapReserve)和指定堆大小(SizeOfHeapCommit)。
“保留的”数量是保留给特定目的的地址空间(不是真正的RAM);在程序开始时,“指定的”数量是指在RAM中实际分配的大小。如果需要的话,“指定的”值也是指定的堆或栈用来增加的数量。(有资料说,不管“SizeOfStackCommit”的值是多少,栈都是按页增加的。我没有验证过。)
因此,举例来说,如一个程序的保留堆有1 MB,指定堆为64 KB,那么启动时堆的大小为64 KB,并且保证可以扩大到1 MB。堆将按64 KB一块来增加。
“堆”在本文中是指主要(缺省)堆。如果它愿意的话,一个进程可创建很多堆。
栈是指第一个线程的栈(启动main()的那个)。进程可以创建很多线程,每个线程都有自己的栈。
DLL文件没有自己的堆或栈,所以它们的映象文件忽略这些值。我不知道驱动程序是否有它们自己的堆或栈,但我认为它们没有。
29)堆和栈的这些描述之后,我们就发现一个32位的“LoaderFlags”(加载器标志),我没有找到它的任何有用的描述。我只发现一篇时新的关于设置此标志位的短文,说设置此标志位会在映象文件载入后自动地调用一个断点或者调试器;可似乎不正确。
30)接着我们会发现32位的“NumberOfRvaAndSizes”(Rva数和大小),它是紧随其后的目录的有效项的数目。我已发现此值不可靠;你也许希望用常量IMAGE_NUMBEROF_DIRECTORY_ENTRIES来代替它,或者用它们中的较小者。
NumberOfRvaAndSizes之后是一个IMAGE_NUMBEROF_DIRECTORY_ENTRIES (16位)(映象文件目录项数目)个IMAGE_DATA_DIRECTORY(映象文件数据目录)数组。这些目录中的每一个目录都描述了一个特定的、位于目录项后面的某一节中的信息的位置(32位的RVA,叫“VirtualAddress”(虚拟地址))和大小(也是32位,叫“Size”(大小))。
例如,安全目录能在索引4中给定的RVA处发现并具有索引4中给定的大小。
稍后我将讨论我知道其结构的目录。
已定义的目录及索引有:
IMAGE_DIRECTORY_ENTRY_EXPORT (0)
输出符号目录;大多用于DLL文件。
后面介绍。
IMAGE_DIRECTORY_ENTRY_IMPORT (1)
输入符号目录;参见后面。
IMAGE_DIRECTORY_ENTRY_RESOURCE (2)
资源目录。后面介绍。
IMAGE_DIRECTORY_ENTRY_EXCEPTION (3)
异常目录 - 结构和用途不详。
IMAGE_DIRECTORY_ENTRY_SECURITY (4)
安全目录 - 结构和用途不详。
IMAGE_DIRECTORY_ENTRY_BASERELOC (5)
基址重定位表 - 参见后面。
IMAGE_DIRECTORY_ENTRY_DEBUG (6)
调试目录 - 内容编译器相关。此外, 许多编译器将编译信息填入代码节,并不为此创建一个单独的节。
IMAGE_DIRECTORY_ENTRY_COPYRIGHT (7)
描述字符串 - 一些随意的版权信息之类。
IMAGE_DIRECTORY_ENTRY_GLOBALPTR (8)
机器值 (MIPS GP) - 结构和用途不详。
IMAGE_DIRECTORY_ENTRY_TLS (9)
线程级局部存储目录 - 结构不详;包含声明为“__declspec(thread)”的变量, 也就是每线程的全局变量。
IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG (10)
载入配置目录 - 结构和用途不详。