打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
程序编译过程

相对虚拟地址

在可执行文件中,许多地方都需要被指定一个在内存中的地址。例如在使用全局变量时需要它的地址。PE文件可以被加载到进程地址空间中的任何地方。虽然它有一个首选地址,但你却不能依赖可执行文件一定会被加载到那个地址。因此就需要按一定方式指定地址,使它们并不依赖于可执行文件的加载地址。
为了避免在PE文件中硬编码内存地址,因此就使用了RVA。RVA只是一个相对于PE文件在内存中的加载位置的偏移。例如假定一个EXE文件被加载在0x400000处,而它的代码节在0x401000处。那么这个代码节的RVA就是:
         (目标地址)0x401000 - (加载地址)0x400000 = (RVA)0x1000
    要把一个RVA转换成实际地址,只需要简单地逆着上述过程进行:将RVA与实际加载地址相加就能得到实际的内存地址。顺便说一下,按照PE格式中的说法,实际的内存地址被称为虚拟地址(Virtual Address,VA)。另外一种考虑VA的方式就是把它当成RVA加上首选加载地址。不要忘了我前面说过加载地址与HMODULE是一回事。
想在内存中探索一些DLL内部的数据结构吗?这里就是方法——用DLL的名称作为参数调用GetModuleHandle函数,它返回的HMODULE就是这个DLL的加载地址,你可以利用你学的关于PE文件结构的知识在这个模块中找到你想找到的一切。

数据目录

在可执行文件中有许多数据结构需要被快速地定位。导入表、导出表、资源以及基址重定位信息等就是一些明显的例子。所有这些广为人知的结构都是以同样的方式被定位的,这些位置被称为数据目录。

struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;
    DWORD   Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES    16

数据目录是一个有16个(WINNT.H中定义为IMAGE_NUMBEROF_DIRECTORY_ENTRIES)元素的结构数组。每个数组元素所指代的内容已经被预先定义好了。WINNT.H文件中的这些IMAGE_DIRECTORY_ENTRY_xxx定义就是数据目录的索引(从0到15)。下表描述了每个IMAGE_DIRECTORY_ENTRY_xxx值所指代的内容。由它们指向的许多数据结构将在本文的第二部分中详细描述。
描述
IMAGE_DIRECTORY_ENTRY_EXPORT
指向导出表(IMAGE_EXPORT_DIRECTORY结构)。
IMAGE_DIRECTORY_ENTRY_IMPORT
指向导入表(IMAGE_IMPORT_DESCRIPTOR结构数组)。
IMAGE_DIRECTORY_ENTRY_RESOURCE
指向资源(IMAGE_RESOURCE_DIRECTORY结构)。
IMAGE_DIRECTORY_ENTRY_EXCEPTION
指向异常处理程序表(IMAGE_RUNTIME_FUNCTION_ENTRY结构数组)。它特定于CPU,用于基于表的异常处理。适用于除x86之外所有类型的CPU。
IMAGE_DIRECTORY_ENTRY_SECURITY
指向WIN_CERTIFICATE结构列表。此结构定义在WinTrust.H文件中。它并不作为映像的一部分被映射进内存。因此VirtualAddress域是文件偏移,而不是RVA。
IMAGE_DIRECTORY_ENTRY_BASERELOC
指向基址重定位信息。
IMAGE_DIRECTORY_ENTRY_DEBUG
指向IMAGE_DEBUG_DIRECTORY结构数组。其中的每个元素描述了映像中的一些调试信息。要获得IMAGE_DEBUG_DIRECTORY结构的数目,用Size域除以IMAGE_DEBUG_DIRECTORY结构的大小。早期的Borland链接器将这个IMAGE_DATA_DIRECTORY项的Size域设置成IMAGE_DEBUG_DIRECTORY结构的数目,而不是数组的大小。
IMAGE_DIRECTORY_ENTRY_ARCHITECTURE
指向与平台相关的数据,这个数据是一个IMAGE_ARCHITECTURE_HEADER结构数组。x86平台和IA-64平台并不使用,但好像已经用于DEC/Compaq Alpha平台。
IMAGE_DIRECTORY_ENTRY_GLOBALPTR
在某些平台上,其VirtualAddress域保存的是全局指针(Global Pointer ,GP)的RVA。x86平台上不使用,但IA-64平台上使用。Size域并未使用。要获取更多关于IA-64 GP方面的信息,可以参考2000年11月的Under The Hood专栏
IMAGE_DIRECTORY_ENTRY_TLS
指向线程局部存储(Thread Local Storage)初始化节。
IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG
指向IMAGE_LOAD_CONFIG_DIRECTORY结构。此结构中的信息特定于Windows NT、Windows 2000和Windows XP(例如GlobalFlag值)。如果你的可执行文件要使用这个结构,需要定义一个名称为__load_config_used,类型为IMAGE_LOAD_CONFIG_DIRECTORY的全局结构体。对于非x86平台,这个名称需要被定义成_load_config_used(单下划线)。如果你想使用IMAGE_LOAD_CONFIG_DIRECTORY结构,必须使用这个技巧才能在你的C++代码中得到正确的名字。链接器看到的符号名一定要是__load_config_used(带两个下划线)。C++编译器要在全局符号前加一个下划线。另外,它还使用类型信息来修饰(decorate)全局符号。因此要使一切正常,你应该像下面这个样子使用:
extern "C"
IMAGE_LOAD_CONFIG_DIRECTORY _load_config_used = {...}
IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT
指向IMAGE_BOUND_IMPORT_DESCRIPTOR结构数组。每个结构对应于这个映像已经绑定的一个DLL。这个结构中的日期/时间戳(TimeDateStamp)可以让加载器快速确定这个绑定是否是最新的。如果不是,加载器将忽略绑定信息,并正常地解析导入的函数。
IMAGE_DIRECTORY_ENTRY_IAT
指向第一个导入地址表(IAT)的开头。对应于每一个导入的DLL都有一个相应的IAT,并且它们在内存中依次排列。Size域指出了所有IAT的总大小。加载器在解析导入符号期间使用这个地址和大小临时将包含IAT的页面标记为可读/可写。
IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT
指向延迟加载信息,它是CImgDelayDescr结构数组,这个结构被定义在Visual C++的DELAYIMP.H文件中。直到首次调用延迟加载的DLL中的函数时这个DLL才会被加载。特别需要注意的是:Windows并不知道关于延迟加载DLL方面的任何信息。延迟加载特性完全是由链接器与运行时库来实现的。
IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR
这个值在最新的系统头文件(CorHdr.h)中被更名为IMAGE_DIRECTORY_ENTRY_COMHEADER。它指向可执行文件中的.NET信息中的顶层信息,包括元数据。这个信息保存在IMAGE_COR20_HEADER结构中。

 

导入函数

当你使用其它DLL中的代码或数据时,就需要导入它们。当加载PE文件时,Windows加载器的工作之一就是定位所有导入的函数和数据,以便加载的PE文件可以使用它们。我把对完成这个任务所需的数据结构的详细讨论留给本文的第二部分,在这里仅给出一个整体概念。
当你直接链接其它DLL中的代码和数据时,实际上隐含链接到了相应的DLL上。你并不需要做任何工作来让你的程序使用这些导入的函数。加载器把这些全包了。另一种使用DLL的方式是显式链接(explicit linking)。这意味着你需要明确地加载这些DLL,然后查找这些函数的地址。这种方法几乎总是通过LoadLibrary和GetProcAddress这两个API来完成的。
当你隐含链接函数时,类似于使用LoadLibrary和GetProcAddress这两个API的代码仍然存在,但加载器自动为你做这些事。同时加载器确保这个被加载的PE文件所需的其它DLL也会被加载。例如用Visual C++?创建的程序一般都会链接到KERNEL32.DLL,而KERNEL32.DLL又从NTDLL.DLL中导入了函数。同样,如果你从GDI32.DLL导入函数,而实际上这个DLL依赖于USER32、ADVAPI32、NTDLL以及KERNEL32这些DLL。加载器会确保这些DLL都被加载,以便解析这些导入的函数。(Visual Basic 6.0和Microsoft .NET可执行文件并不直接链接到KERNEL32,而是链接到了其它的DLL上,但原理是一样的。)
当隐含链接(也称为隐式链接)时,对主要的EXE文件及其依赖的所有DLL的解析过程发生在程序启动时。如果这时出现任何问题(例如它引用的一个DLL找不到),相应的进程就会被终止。
Visual C++ 6.0引入了一个延迟加载(delayload)特性,它是隐含链接与显式链接的混合。当你延迟加载DLL时,链接器生成了一些非常类似于它为正常导入的DLL生成的数据那样的数据,但是操作系统却忽略这些数据。当你的程序在执行过程中首次调用这些延迟加载的函数其中之一时,由链接器为此生成的一部分代码就会执行,由它加载相应的DLL(如果尚未加载),然后调用GetProcAddress来确定要调用的函数的地址。这些额外的工作使得接下来对这个函数的调用就好像这个函数是正常导入的一样。
在PE文件中,对应于每一个导入的DLL有一个相应的结构数组。其中的每个结构都给出了导入的DLL的名称和一个指向函数指针数组的指针。这个函数指针数组被称为导入地址表(Import Address Talbe,IAT)。每个导入的函数都在IAT中有一个对应的位置,Windows加载器就在这个位置上写入这个导入函数的地址。这一点非常重要:一旦一个模块的加载过程结束,那么其IAT中就包含了导入函数的地址。
IAT的美妙之处就在于,在PE文件中,只有一个地方保存了导入函数的地址。无论在你的程序中对某个导入的函数调用多少次,所有调用使用的同样都是IAT中对应于这个函数的指针。
现在让我们来看一下如何调用导入函数。它分为两种情况:高效率方式与低效率方式。按最好的情况(高效率方式)来说,对一个导入函数的调用应该像下面这个样子:
    CALL DWORD PTR [0x00405030]
如果你不熟悉x86汇编语言,我可以告诉你这条指令表示通过函数指针来调用相应的函数。在地址0x00405030处的一个DWORD类型的值就是CALL指令要将控制权转到的地方。在这个例子中,地址0x00405030在IAT中。
调用导入函数的低效率方式类似下面这个样子:
CALL 0x0040100C
...
0x0040100C:
JMP       DWORD PTR [0x00405030]
在这种情况下,CALL指令把控制权转到了一个小占位程序(stub)中。这个占位程序只是一条JMP指令,用以跳转到保存在地址0x405030处的地址中。同样,记住0x405030是IAT中的一个元素。一句话,调用API的这种低效率方式使用了5个字节的附加代码(JMP指令是1字节,地址是4个字节),并且由于使用了额外的JMP指令,因此执行时要花费更长的时间。
你可能奇怪既然有高效率的调用方式,为什么还要使用低效率的调用方式呢?理由是很充足的。由于自身的限制,编译器并不能区分调用导入的函数与调用同一模块中的函数之间的区别。因此编译器为函数调用生成的指令是这样的:
     CALL XXXXXXXX
XXXXXXXX处是实际代码的地址,这个地址由链接器在后面填充。注意这最后的CALL指令并不是通过函数指针(调用函数的)。相反,它使用的是实际代码的地址。为了保持一致的方式,链接器需要用一个代码块来替换XXXXXXXX。最简单的做法就是调用一个JMP之类的占位程序,就像上面你所看到的那样。
那JMP指令是从哪里来的呢?令人惊讶的是,它来自于相应函数的导入库。如果你仔细查看导入库,并且查看与导入函数名称相连的代码时,就会看到类似上面的JMP指令。这意味着,默认情况下,如果没有任何干涉,调用导入函数使用的总是低效率的调用方式。
按照逻辑推理,下一个要问的问题一定是如何才能使用高效率的调用方式。答案是你必须给编译器一个提示。__declspec(dllimport)这个函数修饰符告诉编译器这个函数在其它的DLL中,编译器应该生成下面这样的指令:
    CALL DWORD PTR [XXXXXXXX]
而不是下面这样的指令:
   
CALL XXXXXXXX
另外,编译器生成相应的信息来告诉链接器去解析上面那条指令中的函数指针部分时应该去找的符号是__imp_函数名(就是在相应的函数名称前加上__imp_)。例如,如果你调用MyFunction这个函数,那么相应的符号名应为__imp_MyFunction。看一下导入库,除了看到正常的符号名外,你还会看到一个以__imp_为前缀的同样的符号名。这个__imp_类型的符号被直接解析成了IAT项,而不是JMP占位程序。
这对你日常工作有什么影响呢?如果你编写导出函数并且提供了相应的.H文件,记得使用__declspec(dllimport)函数修饰符。例如:
    __declspec(dllimport) void Foo(void);
如果你看一下Windows系统头文件,你会发现所有的Windows API都使用了__declspec(dllimport)。要想看到它并不容易,但是如果你搜索定义在WINNT.H中,并且用于像WinBase.h之类的头文件中的DECLSPEC_IMPORT宏,你就会发现__declspec(dllimport)是如何用于系统API声明的。
本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
PE文件结构详解(六)重定位
动态链接库、静态库区别与VS2005项目相关设置
开始进行 64位 Windows 系统编程之前需要了解的所有信息
深度探索win32可执行文件格式
【原创】使用IDA PRO+OllyDbg+PEview 追踪windows API 动态链接库函数的调用过程。
DLL基础
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服