http://hi.baidu.com/softopen/blog/item/7ef2c2cc60b6fa570fb3452a.html
八、节的原始数据(Sections' raw data)
--------------------------------------
1.概述(general)
-------
所有的节在载入内存后都按“SectionAlignment”(节对齐)对齐,在文件中则以“FileAlignment”(文件对齐)对齐。节由节头中的相关项来描述:在文件中你可通过“PointerToRawData”(原始数据指针)来找到,在内存中你可通过“VirtualAddress”(虚拟地址)来找到;长度由“SizeOfRawData”(原始数据长度)决定。
根据节中包含的内容,可分为好几种节。大多数(并非所有)情况下,节中至少由一个数据目录,并在可选头的数据目录数组中有一个指针指向它。
2.代码节(code section)
------------------------
首先,我将提到代码节。此节,至少,要将“IMAGE_SCN_CNT_CODE”(含有代码节)、“IMAGE_SCN_MEM_EXECUTE”(内存可执行节)和“IMAGE_SCN_MEM_READ”(内存可读节)等标志位设为1,并且“AddressOfEntryPoint”(入口点地址)将指向节中的某个地方,指向开发者希望首先执行的那个函数的开始处。
“BaseOfCode”(代码基址)通常指向这一节的开始处,但是,如果一些非代码字节被放在代码之前的话,它也可能指向节中靠后的某个地方。
通常,除了可执行代码外,本节没有别的东东,并且通常只有一个代码节,但是不要太迷信这一点。
典型的节名有“.text”、“.code”、“AUTO”之类。
3.数据节(data section)
------------------------
我们要讨论的下一件事情就是已初始化变量;本节包含的是已初始化的静态变量(象“static int i = 5;”)。它将,至少,使“IMAGE_SCN_CNT_INITIALIZED_DATA”(含有已初始化数据节)、“IMAGE_SCN_MEM_READ”(内存可读节)和“IMAGE_SCN_MEM_WRITE”(内存可写节)等标志位被置为1。
一些链接器可能会将常量放在没有可写标志位的它们自己的节中。如果有一部分数据可共享,或者有其它的特定情况,那么可能会有更多的节,且它们的合适的标志位会被设置。
不管是一节,还是多节,它们都将处于从“BaseOfData”(数据基址)到“BaseOfData”+“SizeOfInitializedData”(数据基址+已初始化数据的大小)的范围之内。
典型的名称有“.data”、“.idata”、“DATA”、等等。
4.BSS节(bss section)
----------------------
其后就是未初始化的数据(一些象“static int k;”之类的静态变量);本节十分象已初始化的数据,但它的“PointerToRawData”(文件偏移量)却为0,表明它的内容不存储在文件中;并且“IMAGE_SCN_CNT_UNINITIALIZED_DATA”(含有未初始化数据节)而不是“IMAGE_SCN_CNT_INITIALIZED_DATA”(含有已初始化数据节)标志位被置为1,表明在载入时它的内容应该被置为0。这就意味着,在文件中只有节头,没有节身;节身将由加载器创建,并全部为0字节。
它的长度由“SizeOfUninitializedData”(未初始化数据大小)确定。
典型的名称有“.bss”、“BSS”之类。
有些节数据“没有”被数据目录指向。它们的内容和结构是由编译器而不是链接器提供。
(栈段和堆段不是二进制文件中的节,它们是由加载器根据可选头中的栈大小和堆大小项来创建的。)
5.版权(copyright)
-------------------
为了从一个简单的目录节开始讲解,让我们来看一看数据目录“IMAGE_DIRECTORY_ENTRY_COPYRIGHT”(版权目录项)项。它的内容是一个版权信息或ASCII形式的描述字符串(不是以0结尾的),象“Gonkulator control application, copyright (c) 1848 Hugendubel & Cie”这样。这个字符串,通常,是用命令行或者描述文件提供给链接器的。
这个字符串在运行时并不需要,并可能被丢弃。它是不可写的;事实上,应用程序根本不需要存取它。因此,如果已有一个可丢弃的、非可写的节存在,链接器就会找到它;如果没有,就创建一个(命名为“.descr”之类)。然后它就将那个字符串填入该节中并让版权目录项指针指向这个字符串。“IMAGE_SCN_CNT_INITIALIZED_DATA”(含有已初始化数据节)标志位应置为1。
6.输出符号(exported symbols)
------------------------------
(注意:本文的1993年03月12日之前的各个版本中,输出目录的描述有误。文中没有描述中转、只以序数输出、或者使用好几个名称输出等内容。)
下一件最简单的事情是输出目录,是由“IMAGE_DIRECTORY_ENTRY_EXPORT”(输出目录项)指向的。它是一个典型的在DLL中常见到的目录;包含一些输出函数的入口点(以及输出对象等的地址)。当然可执行文件也可能拥有输出符号但一般没有。
包含它们的节应该有“已初始化数据的”和“可读的”特性。这样的节应该是不可丢弃的,因为在运行时,进程有可能调用“GetProcAddress()”来寻找一个函数的入口点。如果单独成节的话,本节通常被称作“.edata”;更常见的是,它被并入象“已初始化数据”之类的节中。
输出表(“IMAGE_EXPORT_DIRECTORY”)的结构由一个头和输出数据,也就是:符号名称、它们的序号和它们的入口点偏移量等构成。
1)首先,我们有一个没被使用并通常为0的、32位的“Characteristics”(特性)。
2)然后是一个32位的“TimeDateStamp”(时间日期戳),大概是提供此表被创建的time_t格式的时间;天呀,它的值并不总是有效(有些链接器将它设置为0)。
3-4)往后我们看到2个16位的、有关版本信息的word单元(“MajorVersion”和“MinorVersion”,含义分别为‘主版本号’和‘小版本号’),同样,它们很多地被设为0。
5)下一个东东是32位的“Name”(名称);它是一个指向以0结尾的ASCII字符串为DLL名称的RVA。(为防DLL被改名时的错误,名称是必须的----参见输入目录中的“绑定”部分。)
6)然后是32位的“Base”(基址)。稍后我们再讨论。
7)下一个32位值是输出条目的总数(“NumberOfFunctions”,意为‘函数数’)。除了它们的序数外,各条目还可能使用一个或多个名称来输出。接下来的一个32位数字是输出名称的总数(“NumberOfNames”,意为‘名字数’)。
在大多数情况下,每一个输出条目都准确的有一个相应的名称,并且将用这个名称来使用它;但是一个条目可能拥有好几个相关联的名称(那样它们的每一个名称都可访问);或者它也可能没有名称,此时它只能以它的序数来访问。无名输出项(只有序数)的使用是不鼓励的,因为此时输出DLL的所有版本都必须使用相同的序数法,而这会造成维护的问题。
8)下一个32位值“AddressOfFunctions”(函数地址)是指向输出条目列表的RVA。它指向一个32位值的“NumberOfFunctions”(函数数)数组,数组的每一项都是一个指向输出函数或变量的RVA。
关于此列表有两个怪事:第一,这样一个输出的RVA竟可能会为0,在此情况下,此值没被使用。第二,如果一RVA指向含有输出目录的节,那么它就是一个中转输出。一个中转输出就是指指向另一个二进制文件中的输出项的指针;如果使用了它,就可用另一个二进制文件中的被指向的输出项来代替使用。此时的RVA指向,正如已提到的,输出目录的节中,指向一个以以零结尾的字符串组成的、被指向的DLL的名称和一个用点分开的输出项的名称,象“otherdll.exportname”这样,或者是DLL的名称和输出序数,象“otherdll.#19”这样。
现在到了解释输出序数的时候了。一个输出项的序数就是函数地址数组中的索引值加上上面提到的“Base”(基址)的值的和。在大多数情况下,“Base”(基址)的值为1,这就意味着第一个输出项的序数为1,第二个输出项的序数为2,以此类推。
9-10)“AddressOfFunctions”(函数地址)RVA之后,我们发现二个RVA,一个指向符号名称的32位RVA的数组“AddressOfNames”(名字的地址),另一个指向16位序数“AddressOfNameOrdinals”(名字序数的地址)的数组。两个数组都有“NumberOfNames”(名字数)个元素。
符号名称可能会全部丢失,此时“AddressOfNames”(名字的地址)为0;否则,被指向的数组并行运行,这意味着它们的每个索引中的元素共同拥有。“AddressOfNames”(名字的地址)数组由以0结尾的输出名称的RVA组成;这些名称以一个分类的列表排列(即:数组的第一个成员是按照字母顺序排列的最小的名称的RVA;这使当按名称查找一个输出符号时,搜索的效率更高。)
根据PE规范,“AddressOfNameOrdinals”(名字序数的地址)数组每个名称拥有一个相应的序数,然而,我发现这个数组却将实际的索引包含到“AddressOfFunctions”(函数地址)数组中去。
我将画一个有关这三个表的图:
函数地址
|
|
|
v
带序数‘基址’的输出RVA
带序数‘基址+1’的输出RVA
...
带序数‘基址+函数数-1’的输出RVA
名字地址 名字序数地址
| |
| |
| |
v v
第一个名字的RVA <-> 第一个名字的输出索引
第二个名字的RVA <-> 第二个名字的输出索引
... ...
第‘名字数’个名字的RVA <-> 第‘名字数’个名字的输出索引
举一些例子是适宜的。
为按序数找到一个输出符号,先减去“Base”(基址)值以得到索引值,再根据“AddressOfFunctions”(函数地址)的RVA得到输出项数组,并用索引值去找到数组中的输出RVA。如果结果没有指向输出节中,你就完了。否则,它就指向那里的一个描述输出DLL和(输出项)名称或序数的字符串,之后你就得在那里查找中转输出。
为按名称找到一个输出符号,先跟随“AddressOfNames”(名字的地址)的RVA(如果是0就没有名称)找到输出名称的RVA数组。在列表中搜寻你要找的名称。用该名称在“AddressOfNameOrdinals”(名字序数的地址)数组中的索引,得到和找到的名称相应的16位数字。根据PE规范,这是一个序数,你需先减去“Base”(基址)值以得到输出索引值;但依据我的经验,这就是输出索引值,你不需要再减了。使用输出索引值,你就能在“AddressOfFunctions”(函数地址)数组中找到输出RVA了,要么是输出RVA本身,要么是一个描述中转输出的字符串的RVA。
7.输入符号(imported symbols)
------------------------------
当编译器发现一个对别的可执行文件(大多数是DLL文件)中的函数调用时,在最简单化的情况下,它会对此情况一无所知,只是简单地输出一个对那个符号的正常调用指令。链接器不得不修正那个符号的地址,就象它为任何其它的外部符号所做的那样。
链接器使用一个输入库来查找从哪个DLL文件输入了哪个符号,并为所有的输入符号都建立存根,每个存根包含一个跳转指令;存根就是实际的调用目标。这些跳转指令实际上将跳往从所谓的输入地址表中提取的一个地址。在更复杂的应用程序(使用“__declspec(dllimport)”时)中,编译器会知道函数是输入的,并直接输出一个位于输入地址表中的地址的调用,绕过跳转。
不管怎样,DLL文件中的函数地址总是必要的,并将于应用程序载入时,由加载器从输出DLL文件的输出目录中提供。加载器知道哪个库中的哪些符号需要被查找以及哪些地址需要通过搜索输入目录来修正。
我最好给你一个例子。有或无__declspec(dllimport)的调用如下所示:
源文件:
int symbol(char *);
__declspec(dllimport) int symbol2(char*);
void foo(void)
{
int i=symbol("bar");
int j=symbol2("baz");
}
汇编:
...
call _symbol ; 没有declspec(dllimport)的
...
call [__imp__symbol2] ; 含有declspec(dllimport)的
...
在第一种(没有__declspec(dllimport))情况下,编译器不知道“_symbol”位于一个DLL文件中,因此链接器必须要提供“_symbol”函数。因为此函数不存在,它就为输入符号提供一个存根函数,即一个间接跳转。所有输入存根的集合被称为“转移区”(有时也叫做“跳板”,因为你跳到那里的目的是为了跳到别的地方)。
典型地,此转移区位于代码节中(它不是输入目录的一部分)。每一个函数存根都是一个跳往DLL文件中的实际函数的跳转。转移区的形式象这样:
_symbol: jmp [__imp__symbol]
_other_symbol: jmp [__imp__other__symbol]
...
这意味着:如果你不指定“__declspec(dllimport)”来使用输入符号,那么链接器将会为它们产生一个由间接跳转所组成的转移区。如果你真指定了“__declspec(dllimport)”,那么编译器就会自己做间接(跳转),转移区也就不需要了。(这也意味着:如果你输入的是变量或其它东西,你就必须指定“__declspec(dllimport)”,因为一个具有jmp指令的存根只合适于函数。)
不管怎样,符号“x”的地址都被存在“__imp_x”的存储单元。所有这样的存储单元一起形成所谓的“输入地址表”,此表是由被用到的各DLL文件中的输入库提供给链接器的。输入地址表就是由下面这种形式的一组地址组成的:
__imp__symbol: 0xdeadbeef
__imp__symbol2: 0x40100
__imp__symbol3: 0x300100
...
这个输入地址表是输入目录的一部分,并且被IMAGE_DIRECTORY_ENTRY_IAT(输入地址表目录项)目录指针所指向(尽管有些链接器不设置此目录项,程序也能运行;很明显地,这是因为加载器不使用IMAGE_DIRECTORY_ENTRY_IAT(输入地址表目录项)目录也能解决输入问题)。
这些地址并不被链接器所知;链接器只插入一些伪地址(函数名称的RVA;参见后面的更多信息),这些伪地址会在载入时被加载器用输出DLL文件中的输出目录来修正。输入地址表,以及它是怎样被加载器找到的,将会在本章的后面被详细讲述。
注意:这个介绍是针对C语言规范的;有些别的应用程序构建环境是不使用输入库的,尽管它们都需要建立一个输入地址表,用来让它们的程序访问输入对象和函数。C语言编译器往往使用输入库,因为无论如何讲,这都有利于它们----它们的链接器使用好库。别的环境使用的是例如:一个列出需要的DLL文件名称和函数名称的描述文件(比如“模块定义文件”),或者一个源文件中的声明形式的列表等。
这就是程序的代码如何使用输入函数的;现在我们再来看看输入目录是如何建立以便加载器使用的。
输入目录应该存在于是“已初始化数据”并且“可读”的节中。
输入目录是一个多IMAGE_IMPORT_DESCRIPTOR(输入描述结构)的数组,每个被使用的DLL文件都有一个。(它们的)列表由一个全部用0填充的IMAGE_IMPORT_DESCRIPTOR(输入地址表目录项)结构作为结束。
一个IMAGE_IMPORT_DESCRIPTOR(输入地址表目录项)是一个拥有下列成员的结构体:
OriginalFirstThunk(原始第一个换长)(汉译的说明见注释 )
它是一个RVA(32位),指向一个以0结尾的、由IMAGE_THUNK_DATA(换长数据)的RVA构成的数组,其每个IMAGE_THUNK_DATA(换长数据)元素都描述一个函数。此数组永不改变。
TimeDateStamp(时间日期戳)
它是一个具有好几个目的的32位的时间戳。让我们先假设时间戳为0,一些高级的情况将在以后处理。
ForwarderChain(中转链)
它是输入函数列表中第一个中转的、32位的索引。中转也是高级的东东。对初学者先将所有位设为-1。
Name(名称)
它是一个DLL文件的名称(0结尾的ASCII码字符串)的、32位的RVA。
FirstThunk(第一换长)
它也是一个RVA(32位),指向一个0结尾的、由IMAGE_THUNK_DATA(换长数据)的RVA构成的数组,其每个IMAGE_THUNK_DATA(换长数据)元素都描述一个函数。此数组是输入地址表的一部分,并且可以改变。
因此,数组中的每个IMAGE_IMPORT_DESCRIPTOR(输入描述结构)结构体都给出输出DLL文件的名称,并且,除了中转和时间日期戳,它还给出2个指向IMAGE_THUNK_DATA(换长数据)的数组的RVA,都是32位。(每个数组的最后一个成员都全部填充为0字节,以标志结尾。)
目前看来,每个IMAGE_THUNK_DATA(换长数据)都是一个RVA,指向一个描述输入函数的IMAGE_IMPORT_BY_NAME(输入名字)项。
现在,有趣的是两个数组并行运行,也就是说:它们指向同一组IMAGE_IMPORT_BY_NAME(输入名字)。
没有必要失望,我将再画一图。这里是IMAGE_IMPORT_DESCRIPTOR(输入描述结构)的关键内容:
原始第一个换长 第一个换长
| |
| |
| |
V V
0--> 函数1 <--0
1--> 函数2 <--1
2--> 函数3 <--2
3--> foo <--3
4--> mumpitz <--4
5--> knuff <--5
6-->0 0<--6 /* 最后的RVA是0! */
图当中的名字就是尚未讨论的IMAGE_IMPORT_BY_NAME(输入名字)。每一个都是一个16位的数字(一个提示)跟着一些数量未定的字节,它们都是以0结尾的、输入符号的ASCII码名字。
提示就是指向输出DLL文件名字表的索引(参见上面的输出目录)。那个索引中的名字将被一一尝试,如果没有相符的,再使用二进制搜索来寻找名字。
(有些链接器不愿意查找正确的提示,总是只简单的将其指定为1,或者其它的随意数字。这并无大害,只是使解决名字的第一次尝试总是失败,并迫使每个名字都使用二进制搜索来进行。)
总结一下:如果你想从“knurr”DLL中查找输入函数“foo”的信息,第一步你先找到数据目录中的IMAGE_DIRECTORY_ENTRY_IMPORT(输入目录项)项,得到一个RVA,再在原始节数据中找到那个地址,现在你就得到一个IMAGE_IMPORT_DESCRIPTOR(输入描述结构)数组了。通过查看根据它们的“名称”被指向的字符串,得到和“knurr”DLL有关的这个数组的成员(即一个输入描述结构)。在你找到正确的IMAGE_IMPORT_DESCRIPTOR(输入描述结构)后,顺着它的“OriginalFirstThunk”(原始第一个换长)得到被指向的IMAGE_THUNK_DATA(换长数据)数组;再通过查询RVA找到“foo”函数。
好了,为什么我们有“两”列指向IMAGE_IMPORT_BY_NAME(输入名字)的指针呢?这是因为在运行时,应用程序不需要输入函数的名字,只需要地址。在这里输入地址表又出现了。加载器将从相关的DLL文件的输出目录中查找每一个输入符号,并用DLL文件入口点的线性地址替换“FirstThunk”( 第一个换长)列表中的IMAGE_THUNK_DATA(换长数据)元素(到现在之前它还是指向IMAGE_IMPORT_BY_NAME(输入名字)的)。
请记住带有象“__imp__symbol”标签的地址列表;被数据目录IMAGE_DIRECTORY_ENTRY_IAT(输入地址表目录项)所指向的输入地址表,就是被“FirstThunk”( 第一个换长)所指向的列表。[在从好几个DLL文件输入的情况下,输入地址表是包含所有DLL文件的“FirstThunk”( 第一个换长)数组。目录项IMAGE_DIRECTORY_ENTRY_IAT(输入地址表目录项)可能会丢失,但输入(函数)仍能工作良好。]
“OriginalFirstThunk”( 原始第一个换长)数组保持不变,因此你总能通过“OriginalFirstThunk”( 原始第一个换长)列表查找原始的输入名字列表。
现在输入已经被用正确的线性地址修正,如下所示:
原始第一个换长 第一个换长
| |
| |
| |
V V
0--> 函数1 0--> 输出函数1
1--> 函数2 1--> 输出函数2
2--> 函数3 2--> 输出函数3
3--> foo 3--> 输出函数foo
4--> mumpitz 4--> 输出函数mumpitz
5--> knuff 5--> 输出函数knuff
6-->0 0<--6
这是简单情况下的基本结构。现在我们将要学习输入目录中的需细讲的东西。
第一,当数组中IMAGE_THUNK_DATA元(换长数据)素的IMAGE_ORDINAL_FLAG(序数标志)位(也是:MSB,参见注释 )被置1时,表示列表中没有符号的名字信息,符号只以序数输入。你可通过查看IMAGE_THUNK_DATA(换长数据)中的低地址word来得到序数。
通过序数输入是不鼓励的,通过名字输入会更安全,因为如果输出DLL文件不是预期的版本时输出序数可能会改变。
第二,有所谓的“绑定输入”。
请思考一下加载器的工作:当它想执行的一个二进制文件需要一个DLL中的函数时,加载器会载入该DLL,找到它的输出目录,查找函数的RVA并计算函数的入口点。然后用这样找到的地址修正“FirstThunk”( 第一个换长)列表。
假设程序员很聪明,给DLL文件提供的唯一优先载入地址不会发生冲突,那么我们就能认为函数的入口点将总是相同的。它们在链接时能被算出并被补进“FirstThunk”( 第一个换长)列表中,这就是“绑定输入”所发生的一切。(“绑定”工具就是干这个的,它是Win32 SDK的一部分。)
当然,你得慎重:用户的DLL可能是不同的版本,或者DLL必须重定位,这些都会使先前修正的“FirstThunk”( 第一个换长)列表不再有效;此时,加载器仍能查寻“OriginalFirstThunk”( 原始第一个换长)列表,找出输入符号并重新补正“FirstThunk”( 第一个换长)列表。加载器知道这是必须的,当:1)输出DLL文件的版本不符,或2)输出DLL文件需要重定位时。
确定有没有重定位表对加载器来说不是问题,但该怎样找出版本的不同呢?这时IMAGE_IMPORT_DESCRIPTOR(输入描述结构)的“时间戳”就派上用场了。如果它是0,表明输入列表没有被绑定,加载器总是要修复入口点。否则的话,输入被绑定,“时间戳”必须要和“文件头”中的输出DLL文件的“时间戳”相符;如果不符的话,加载器就认为该二进制文件被绑到一个“错误”的DLL文件上并重新补正输入列表。
这里有另外一个有关输入列表中的“中转”的怪事。一个DLL文件能输出一个不定义在本DLL文件中却需从另一个DLL文件中输入的符号;这样的符号据说就是被中转的(参见上面的输出目录描述)。
现在,很明显的,你不能通过查看那个实际上并不包含入口点信息的DLL文件的时间戳来确定一个符号的入口点是否有效。因此,出于安全的原因,中转符号的入口点必须总是被修正。在二进制文件的输入列表中,中转符号的输入必须被找出,以便加载器能补正它们。
这一点可通过“ForwarderChain”(中转链)来做到。它是一个指向换长列表中的索引值;被索引位置的输入就是一个中转输出,并且此位置的“FirstThunk”( 第一个换长)列表中的内容就是“下一个”中转输入的索引值,以此类推,直到索引值为-1,就表明已没有其他的中转了。如果根本就没有中转,那么“ForwarderChain”(中转链)的值本身就为-1。
这就是所谓的“老式”绑定。
至此,我们应该总结一下我们目前已掌握的情况 :-)
OK,我将认为你已找到了IMAGE_DIRECTORY_ENTRY_IMPORT(输入目录项)并且已根据它找到了它的输入目录,位于某个节中。现在你已处于IMAGE_IMPORT_DESCRIPTOR(输入描述结构)数组的开头了,此类数组的最后一个将以全0字节填充。
要读懂一个IMAGE_IMPORT_DESCRIPTOR(输入描述结构),你得先查看它的“名字”项,根据它的RVA,你就能找到输出DLL文件的名字。下一步你得确定输入是否是绑定的;如果输入是绑定的,“时间戳”就会是非“0”的。如果它们是绑定的,现在就是你通过比较“时间戳”来检查DLL文件的版本是否相符的好机会了。
现在你根据“OriginalFirstThunk”( 原始第一个换长)的RVA来到了IMAGE_THUNK_DATA(换长数据)数组;过完这些数组(它是0结尾的),它的每个成员都将是一个IMAGE_IMPORT_BY_NAME(输入名字)的RVA(除非它的高位被置1,此时你找不到名字只有序数)。根据那个RVA,并跳过2字节(即‘提示’),现在你就得到一个以0结尾的字符串,这就是输入函数的名字。
在绑定输入时要找到提供的入口点,先根据“FirstThunk”( 第一个换长)平行的来到“OriginalFirstThunk”( 原始第一个换长)数组;数组成员就是入口点的线性地址(暂时不考虑中转的话题)。
还有一件我到现在都没有提及的事情:明显地有些链接器在构建输入目录时会产生bug(我就发现一个还在被一个Borland C链接器使用的bug)。这些链接器把IMAGE_IMPORT_DESCRIPTOR(输入描述结构)中的“OriginalFirstThunk”( 原始第一个换长)设为0,并只建立“FirstThunk”( 第一个换长)。很明显的,这样的输入目录不能被绑定(否则重修输入的必须信息就会丢失----你根本找不到函数名字)。在这种情况下,你得根据“FirstThunk”( 第一个换长)数组来取得输入符号名字,你将永远得不到预先补正的入口地址。我已发现一个TIS文件(参考书目[6]),讲述一个在某种程度上和此bug兼容的输入目录,因此那个文件可能就是该bug的起源。
TIS文件规定:
IMPORT FLAGS(输入标志)
TIME/DATE STAMP(时间/日期戳)
MAJOR VERSION - MINOR VERSION(主版本号 - 小版本号)
NAME RVA(名字的RVA)
IMPORT LOOKUP TABLE RVA(输入查询表的RVA)
IMPORT ADDRESS TABLE RVA(输入地址表的RVA)
而别处使用的对应结构是:
OriginalFirstThunk( 原始第一个换长)
TimeDateStamp(时间日期戳)
ForwarderChain(中转链)
Name(名字)
FirstThunk(第一个换长)
最后一个关于输入目录的需要细讲的就是所谓的“新式”绑定(在参考书目[3]中讲述),它也可以由“绑定”工具来处理。当使用这种方式时,“时间日期戳”的所有位被置为1,并且没有中转链;此时所有输入符号的地址都将被补正,而不管它们是不是中转的。尽管如此,你还是需要知道DLL的版本,并且你还是需要将序数符号从中转符号中区分开来。为了达到这个目的,IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT(绑定输入目录项)目录被创建了。就我所见,它将不被放在节中,而是被放在头中,处于节头之后第一节之前。(咳,这不是我的发明,我只是讲述它而已!)
这个目录告诉你,每一个已使用的DLL文件的中转输出是从哪些别的DLL文件中来的。
结构是IMAGE_BOUND_IMPORT_DESCRIPTOR(绑定输入描述结构)形式的,包括(按这个顺序):
一个32位数字,“时间戳”。
一个16位数字,“OffsetModuleName(模块名字偏移量)”,是从目录开头到以0结尾的DLL文件名的偏移量;
一个16位数字,“NumberOfModuleForwarderRefs(模块中转参考的数字)”,给出这个DLL文件为它的中转使用的DLL文件数。
紧随这个结构之后你会发现“NumberOfModuleForwarderRefs(模块中转参考的数字)”结构,告诉你这个DLL文件的中转所来自的DLL文件的名称和版本。这些结构就是“IMAGE_BOUND_FORWARDER_REF(绑定中转参考)”结构的:
一个32位的数字“时间日期戳”(TimeDateStamp);
一个16位的数字“模块名称偏移量”(OffsetModuleName),它就是从目录开头到中转来自的那个DLL文件的0结尾的名字处的偏移量;
一个16位的未使用单元。
跟在“IMAGE_BOUND_FORWARDER_REF(绑定中转参考)”后的是下一个“IMAGE_BOUND_IMPORT_DESCRIPTOR(绑定输入描述结构)”,以此类推;列表最终以一个全部为0位的IMAGE_BOUND_IMPORT_DESCRIPTOR(绑定输入描述结构)结束。
我对由此(描述)造成的不便表示歉意,但这就是它看起来的样子:-)
现在,如果你有一个新的绑定输入目录,你得载入所有的DLL文件,并使用目录指针IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT(绑定输入目录项)找到IMAGE_BOUND_IMPORT_DESCRIPTOR(绑定输入描述结构),扫描整个IMAGE_BOUND_IMPORT_DESCRIPTOR(绑定输入描述结构),并检查被载入的DLL文件的“时间日期戳”和这个目录中提供的是否相符。如果不符,就将输入目录中“第一换长”(FirstThunk)中的错误全部修改过来。