总结一下:如果你想从“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(第一个换长)