windows下的动态链接

1. DLL简介

DLL即动态链接库(Dynamic-Link Library)的缩写,它相当于Linux下的共享对象。Window系统中大量采用了这种 DLL机制,甚至包括Windows 的内核的结构都很大程度依赖于DLL机制。Windows 下的 DLL文件和EXE文件实际上是一个概念,它们都是有PE格式的二进制文件,稍微有些不同的是PE文件头部中有个符号位表示该文件是EXE或是DLL,而 DLL文件的扩展名不一定是.dll,也有可能是别的比如.ocx (OCX控件)或是.CPL(控制面板程序)。

DLL的设计目的与共享对象有些出入,DLL更加强调模块化,即微软希望通过DLL机制加强软件的模块化设计,使得各种模块之间能够松散地组合、重用和升级。所以我们在Windows平台上看到大量的大型软件都通过升级DLL的形式进行自我完善,微软经常将这些升级补丁积累到一定程度以后形成一个软件更新包(Service Packs)。比如我们常见的微软Office系列、Visual Studio系列、Internet Explorer 甚至Windows本身也通过这种方式升级。

另外,我们知道ELF的动态链接可以实现运行时加载,使得各种功能模块能以插件的形式存在。在 Windows下,也有类似ELF的运行时加载,这种技术在Windows下被应用得更加广泛,比如著名的ActiveX技术就是基于这种运行时加载机制实现的。

1.1 基地址和RVA

PE里面有两个很常用的概念就是基地址(Base Address)和相对地址(RVA,RelativeVirtual Address)。当一个PE文件被装载时,其进程地址空间中的起始地址就是基地址。对于任何一个PE文件来说,它都有一个优先装载的基地址,这个值就是PE文件头中的ImageBase。

对于一个可执行EXE文件来说,Image Base -般值是0x400000,对于DLL文件来说,这个值一般是0x10000000。Windows在装载DLL 时,会先尝试把它装载到由Image Base指定的虚拟地址;若该地址区域已被其他模块占用,那PE装载器会选用其他空闲地址。而相对地址就是一个地址相对于基地址的偏移,比如一个PE文件被装载到0x10000000,即基地址为0x10000000,那么RVA为0x1000的地址为0x10001000。

1.2 DLL共享数据段

在Win32下,如果要实现进程间通信,当然有很多方法,Windows系统提供了一系列API可以实现进程间的通信。其中有一种方法是使用DLL来实现进程间通信,这个原理与16位Windows 中的 DLL 实现进程间通信十分类似。正常情况下,每个 DLL的数据段在各个进程中都是独立的,每个进程都拥有自己的副本。但是Windows 允许将DLL的数据段设置成共享的,即任何进程都可以共享该DLL的同一份数据段。当然很多时候比较常见的做法是将一些需要进程间共享的变量分离出来,放到另外一个数据段中,然后将这个数据段设置成进程间可共享的。也就是说-一个DLL中有两个数据段,一个进程间共享,另外一个私有。

当然这种进程间共享方式也产生了一定的安全漏洞,因为任意一个进程都可以访问这个共享的数据段,那么只要破坏了该数据段的数据就会导致所有使用该数据段的进程出现问题。甚至恶意攻击者可以在GUEST的权限下运行某个进程破坏该共享的数据,从而影响那些系统管理员权限的用户使用同一个DLL的进程。所以从这个角度讲,这种 DLL共享数据段来实现进程间通信应该尽量避免。

1.3 DLL的简单例子

DLL的创建和使用,最基本的概念是导出(Export)的概念。在 ELF 中,共亨库中所有的全局函数和变量在默认情况下都可以被其他模块使用,也就是说ELF默认导出所有的全局符号。但是在DLL中情况有所不同,我们需要显式地“告诉”编译器我们需要导出某个符号,否则编译器默认所有符号都不导出。当我们在程序中使用DLL导出的符号时,这个过程被称为导入(Import)。

Microsoft Visual C++(MSVC)编译器提供了一系列CIC++的扩展来指定符号的导入导出,对于一些支持Windows平台的编译器比如 Intel C++、GcC Window版( mingw Gcc,cygwin GCC)等都支持这种扩展。我们可以通过“_declspec”属性关键字来修饰某个函数或者变量,当我们使用“_declspec(dllexport)”时表示该符号是从本 DLL导出的符号,“_declspec(dllimport)”表示该符号是从别的 DLL导入的符号。在C++中,如果你希望导入或者导出的符号符合C语言的符号修饰规范,那么必须在这个符号的定义之前加上external “c”,以防止 C++编译器进行符号修饰。

除了使用“_declspec”扩展关键字指定导入导出符号之外,我们也可以使用“.def”文件来声明导入导出符号。".def”扩展名的文件是类似于ld链接器的链接脚本文件,可以被当作 link 链接器的输入文件,用于控制链接过程。".def”文件中的IMPORT或者EXPORTS段可以用来声明导入导出符号,这个方法不仅对C/C++有效,对其他语言也有效。

1.4 创建DLL

假设我们的一个DLL提供3个数学运算的函数,分别是加(Add)、减(Sub)、乘(Mul),它的源代码如下(Math.c):

_declspec(dllexport) double Add( double a,double b )
{
    return a + b;
}
_declspec (dllexport ) double sub( double a, double b )
{
    return a - b;
}
_declspec (dllexport) double Mul ( double a, double b )
{
    return a * b;
}

代码很简单,就是传入两个双精度的值然后返回相应的计算结果〈有人能告诉我为什么没有除法吗?不要着急,我们留着除法到后面用)。然后我们使用MSVC的编译器cl进行编译:

cl /LDd Math.c

参数/LDd表示生产Debug 版的 DLL,不加任何参数则表示生产EXE可执行文件;我们可以使用/LD来编译生成Release版的DLL

上面的编译结果生成了“Math.dll”、“Math.obj”、“Math.exp”和“Math.lib”这4个文件。很明显“Math.dll”就是我们需要的DLL文件,“"Math.obj”是编译的目标文件,"Math.exp”和“Math.lib”将在后面作介绍。我们可以通过dumpbin工具看到DLL的导出符号:

//dumpbin /EXPORTS Math.dll

...

ordinal hint RVA name

1 0 00001000 Add

2 1 00001020 Mul

3 2 00001010 Sub

...

很明显,我们可以看到DLL有3个导出函数以及它们的相对地址。

1.5 使用DLL

程序使用DLL 的过程其实是引用DLL中的导出函数和符号的过程,即导入过程。对于从其他 DLL导入的符号,我们需要使用“_declspec(dllimport)”显式地声明某个符号为导入符号。这与ELF中的情况不一样,在ELF中,当我们使用一个外部模块的符号的时候,我们不需要额外声明该变量是从其他共享对象导入的。

我们来看一个使用Math.dll的例子:

/* TestMath.c */
#include <stdio.h>

__declspec(dllimport ) double sub(double a, double b) ;

int main (int argc, char **argv)
{
    double result = sub(3.0,2.0 ) ;
    printf ( "Result = %f\n" , result ) ;
    return 0 ;
}

在编译时,我们通过下面的命令行:

cl /c TestMath.c

link TestMath.obj Math. lib

第一行使用编译器将TestMath.c 编译成TestMath.obj,然后使用链接器将‘TestMath.obj和 Math.lib链接在一起产生一个可执行文件TestMath.exe。

在最终链接时,我们必须把与DLL一起产生的“Math.lib”与“TestMath.o”链接起来,形成最终的可执行文件。在静态链接的时候,我们介绍过“.lib”文件是一组目标文件的集合,在动态链接里面这一点仍然没有错,但是“Math.lib”里面的目标文件是什么呢?

“Math.lib”中并不真正包含“Math.c”的代码和数据,它用来描述“Math.dll”的导出符号,它包含了TestMath.o链接Math.dll时所需要的导入符号以及一部分“桩”代码,又被称作“胶水”代码,以便于将程序与DLL粘在一起。像“Math.lib”这样的文件又被称为导入库(ImportLibrary)。

DLL的编译将生成DLLLIB两个文件。LIB文件被称为输入库(import library),在编译时为调用DLL的程序提供“桩”(stub),实际上是间接跳转到运行时加载的DLL的对应的函数上再继续执行。这种通过输入库来使用DLL的方式,在程序运行时启动进程时就会自动(隐式)加载所有用到的DLL。另一种使用DLL的方式是通过LoadLibrary(或者LoadLibraryEx) API函数进行显式加载DLL,用GetProcAddress API函数通过函数名称获取其加载后的内存地址、通过FreeLibrary卸载DLL。

1.6 使用模块定义文件

声明DLL中的某个函数为导出函数的办法有两种,一种就是前面我们演示过的使用“_declspec(dllexport)”扩展;另外一种就是采用模块定义(.def)文件声明。实际上.def文件在 MSVC 链接过程中的作用与链接脚本文件(Link Script)文件在ld链接过程中的作用类似,它是用于控制链接过程,为链接器提供有关链接程序的导出符号、属性以及其他信息。不过相比于ld的链接脚本文件,.def文件的语法要简单的多,而且功能也更少。

假设我们在前面例子的Math.c中将所有的“_declspec(dlexport)”去掉,然后创建一个Math.def 文件,以下面作为内容:

LIBRARY Math
EXPORTS
Add
sub
Mul
Div

然后使用下面的命令行来编译Math.c:

cl Math.c /LD /DEF Math.def

这样编译器(更准确地讲是link链接器)就会使用Math.def 文件中的描述产生最终输出寸文件。

1.7 DLL显示运行时链接

与ELF类似,DLL也支持运行时链接,即运行时加载。Windows提供了3个API为:

  1. LoadLibrary(或者LoadLibraryEx),这个函数用来装载一个DLL到进程的地址空间,它的功能跟dlopen类似。
  2. GetProcAddress,用来查找某个符号的地址,与dlsym类似。
  3. FreeLibrary,用来卸载某个已加载的模块,与dlclose类似。
#include <windows .h>
#include <stdio.h>
int main(int argc, char **argv)
{
    Func function ;
    double result;
    //Load DLL
    HINSTANCE hinstLib = LoadLibrary ( "Math.dll");
    //Get function address 
    function = (Func)GetProcAddress (hinstLib,"Add" ) ;
    if( function == NULL )
    {
        printf ( "ERROR: unable to find DLL function\n" ) ;
        FreeLibrary (hinstLib) ;
        return 1;
    }
    // Get function address
    function = (Func)GetProcAddress (hinstLib,"Add " ) ;
    if ( function == NULL)
    {
        printf ( "ERROR: unable to find DLL function\n" ) ;
        FreeLibrary (hinstLib) ;
        return 1;
    }
    // cal1 function 
    result = function ( 1.0,2.0 ) ;
    // Unload DLL fi1e
	FreeLibrary (hinstLib) ;
    // Display result
    printf ( "Result = %f\n", result ) ;
    return 0;
}

2. 符号导出导入表

2.1 导出表

当一个PE需要将一些函数或变量提供给其他PE文件使用时,我们把这种行为叫做符号导出(Symbol Exporting)。最典型的情况就是一个DLL将符号导出给EXE文件使用。在前面介绍ELF动态连接时,我们已经接触过了符号导出的概念,ELF将导出的符号保存在“.dynsym”段中,供动态链接器查找和使用。在 Windows PE中,符号导出的概念也是类似,所有导出的符号被集中存放在了被称作导出表(Export Table)的结构中。事实上导出表从最简单的结构上来看,它提供了一个符号名与符号地址的映射关系,即可以通过某个符号查找相应的地址。基本上这些每个符号都是个ASCII字符串,我们知道符号名可能跟相应的函数名或者变量名相同,也可能不同,因为有符号修饰这个机制存在。

PE文件头中有一个叫做 DataDirectory 的结构数组,这个数组共有16个元素,每个元素中保存的是一个地址和一个长度。其中第一个元素就是导出表的结构的地址和长度。导出表是一个IMAGE_EXPORT_DIRECTORY的结构体,它被定义在“Winnt.h”中:

typedef struct _IMAGE_EXPORT_DIRECTORY{
    DWORD	characteristics;
    DWORD	TimeDateStamp;
    WORD	MajorVersion;
    WORD	MinorVersion;
    DWORD	Name;
    DWORD	Base;
    DWORD	NumberOfFunctions;
    DWORD	NumberOfName;
    DWORD	AddresssOfFunctions;		//RVA from base of image
    DWORD	AddressOfNames;				//RVA from base of image
    DWORD	AddressOfNameOrdinals;		//RVA from base of image
}IMAGE__EXPORT_DIRECTORY

导出表结构中,最后的3个成员指向的是3个数组,这3个数组是导出表中最重要的结构,它们是导出地址表(EAT,Export Address Table)、符号名表(Name Table〉和名字序号对应表(Name-Ordinal Table)。对于“Math.dll”来说,这个导出表的结构将会如下图所示。

Characteristics
Characteristics
....
....
Name
Name
Base = 1
Base = 1
NumberOfFunction=3
NumberOfFunction=3
NumberOfNames = 3
NumberOfNames = 3
AddressOfNames
AddressOfNames
AddressOfNameOrdinals
AddressOfNameOrdinals
Math.dll
Math...
1000
1000
1020
1020
1010
1010
Add
Add
Mul
Mul
Sul
Sul
1
1
2
2
3
3
Viewer does not support full SVG 1.1

这3个数组中,前两个比较好理解。第一个叫做导出地址表EAT,它存放的是各个导出函数的RVA,比如第一项是Ox1000,它是Add函数的RVA;第二个表是函数名表,它保存的是导出函数的名字,这个表中,所有的函数名是按照 ASCI顺序排序的,以便于动态链接器在查找函数名字时可以速度更快(可以使用二分法查找).

2.2 EXP文件

在创建DLL的同时也会得到一个 EXP 文件,这个文件实际上是链接器在创建DLL时的临时文件。链接器在创建DLL时与静态链接时一样采用两遍扫描过程,DLL一般都有导出符号,链接器在第一遍时会遍历所有的目标文件并且收集所有导出符号信息并且创建DLL的导出表。为了方便起见,链接器把这个导出表放到一个临时的目标文件叫做“.edata”的段中,这个目标文件就是EXP文件,EXP文件实际上是一个标准的 PE/COFF目标文件,只不过它的扩展名不是.obj而是.exp。在第二遍时,链接器就把这个EXP文件当作普通目标文件一样,与其他输入的目标文件链接在一起并且输出 DLL。这时候EXP文件中的“.edata”段也就会被输出到DLL文件中并且成为导出表。不过一般现在链接器很少会在DLL中单独保留“.edata”段,而是把它合并到只读数据段“.rdata”中。

2.3 导出表

当某个PE文件被加载时,Windows 加载器的其中一个任务就是将所有需要导入的函数地址确定并且将导入表中的元素调整到正确的地址,以实现动态链接的过程。

在 Windows 中,系统的装载器会确保任何一个模块的依赖条件都得到满足,即每个PE文件所依赖的文件都将被装载。比如一般 Windows程序都会依赖于KERNEL32.DLL,而KERNEL32.DLL又会导入 NTDLL.DLL,即依赖于NTDLL.DLL,那么Windows在加载该程序时确保这两个DLL都被加载。如果程序用到了Windows GDI,那么就会需要从GDI32.DLL中导入函数,而 GDI32.DLL又依赖于USER32.DLL、ADVAPI32.DLL、NTDLL.DLL和KERNEL32.DLL,Windows 将会保证这些依赖关系的正确,并且保证所有的导入符号都被正确地解析。在这个动态链接过程中,如果某个被依赖的模块无法正确加载,那么系统将会提示错误(我们经常会看到那种“缺少某个DLL”之类的错误),并且终止运行该进程。

在PE文件中,导入表是一个IMAGE_IMPORT_DESCRIPTOR的结构体数组,每一个IMAGE_IMPORT_DESCRIPTOR结构对应一个被导入的 DLL。这个结构体被定义在"Winnt.h”中:

typedef struct{
    DWORD	originalFirstThunk ;
    DWORD	TimeDateStamp ;
	DWORD	ForwarderChain ;
    DWORD	Name;
    DWORD	FirstThunk ;
}IMAGE_IMPORT__DESCRIPTOR;

结构体中的FirstThunk指向一个导入地址数组(lmport Address Table),IAT是导入表中最重要的结构,IAT中每个元素对应一个被导入的符号,元素的值在不同的情况下有不同的含义。在动态链接器刚完成映射还没有开始重定位和符号解析时,IAT中的元素值表示相对应的导入符号的序号或者是符号名;当 Windows 的动态链接器在完成该模块的链接时,元素值会被动态链接器改写成该符号的真正地址,从这一点看,导入地址数组与ELF中的GOT非常类似。

Windows的动态链接器会在装载一个模块的时候,改写导入表中的IAT,这一点很像ELF中的.got。其区别是,PE的导入表一般是只读的,它往往位于“.rdata”这样的段中。这样就产生了一个问题,对于一个只读的段,动态链接器是怎么改写它的呢?解决方法是这样的,对于Windows来说,由于它的动态链接器其实是Windows内核的一部分,所以它可以随心所欲地修改PE装载以后的任意一部分内容,包括内容和它的页面属性。Windows 的做法是,在装载时,将导入表所在的位置的页面改成可读写的,一旦导入表的IAT被改写完毕,再将这些页面设回至只读属性。从某些角度来看,PE的做法比ELF要更加安全一些,因为ELF运行程序随意修改.got,而 PE则不允许。

2.5 导入函数调用

如果在PE的模块中需要调用·个导入函数,仿照ELF GOT机制的一个办法就是使用一个间接调用指令,比如:

CALL DwORD PTR[0x0040D11C]

在 Windows下使用微软汇编器语法。如果你不熟悉微软汇编器语法也没多大关系,上面这条指令的意思是间接调用0x0040D11C这个地址中保存的地址,即从地址Ox0040D11C开始取4个字节作为目标地址(DWORD PTR表示4个字节的操作前缀),然后调用该目标地址。而0x0040D11C这个地址刚好是IAT中的某一项,即我们需要调用的外部函数在IAT中所对应的元素,比如TestMath.exe中,我们需要调用Math.dl中的Sub函数,那么Ox0040D11C正好对应Sub导入函数在TestMath.exe的1AT中的位置。这个过程跟ELF通过GOT间接跳转十分类似,IAT相当于GOT(不考虑PLT的情况下)。

我们可以看到,这个Ox0040D11C是作为常量被写入在指令中,由此我们可以得出结论,PE DLL 的代码段并不是地址无关的。

PE为了解决装载时模块在进程空间中地址冲突的问题,使用了一种叫做重定基地址的方法,我们在后面将会详细介绍。

PE采用上面的这个方法实现导入函数的调用,但是与ELF一样存在一个问题:对于编译器来说,它无法判断一个函数是本模块内部的,还是从外部导入的。因为对于普通的模块内部函数调用来说,编译器产生的指令是这样的:

CALL XXXXXXXX

其中 XXXXXXXX是模块内部的函数地址。这是一个直接调用指令,与上面的间接调用指令形式不同。所以为了使得编译器能够区分函数是从外部导入的还是模块内部定义的,MSVC引入了我们前面用过的扩展属性“_declspec(dllimport)”,一旦一个函数被声明为“_declspec(dllimport)”,那么编译器就知道它是外部导入的,以便于产生相应的指令形式。

3. DLL优化

我们在前面经过对DLL的分析得知,DLL的代码段和数据段本身并不是地址无关的,也就是说它默认需要被装载到由ImageBase指定的目标地址中。如果目标地址被占用,那么就需要装载到其他地址,便会引起整个DLL 的 Rebase。这对于拥有大量DLL的程序来说,频繁的 Rebase也会造成程序启动速度减慢。这是影响DLL性能的另外一个原因。

动态链接过程中,导入函数的符号在运行时需要被逐个解析。在这个解析过程中,免不了会涉及到符号字符串的比较和查找过程,这个查找过程中,动态链接器会在目标DLL的导出表中进行符号字符串的二分查找。即使是使用了二分查找法,对于拥有DLL 数量很多,并且有大量导入导出符号的程序来说,这个过程仍然是非常耗时的。这是影响DLL性能的一个原因之—-。

这两个原因可能会导致应用程序的速度非常慢,因为系统需要在启动程序时进行大量的符号解析和 Rebase 工作。

3.1 重定基地址(Rebasing)

PE的 DLL中的代码段并不是地址无关的,也就是说它在被装载时有一个固定的目标地址,这个地址也就是PE里面所谓的基地址(Base Address)。默认情况下,PE文件将被装载到这个基地址。一般来说,EXE文件的基地址默认为0x00400000,而 DLL文件基地址默认为0x10000000。

一个进程中,多个DLL不可以被装载到同一个虚拟地址,每个DLL所占用的虚拟地址区域之间都不可以重叠。

为了解决共享对象的地址冲突问题,Windows PE采用了一种与ELF不同的办法,它采用的是装载时重定位的方法。在DLL模块装载时,如果目标地址被占用,那么操作系统就会为它分配一块新的空间,并且将DLL装载到该地址。这时候问题来了,因为DLL的代码段不是地址无关的,DLL中所有涉及到绝对地址的引用该怎么办呢?答案是对于每个绝对地址引用都进行重定位。

当然,这个重定位过程有些特殊,因为所有这些需要重定位的地方只需要加上一个固定的差值,也就是说加上一个目标装载地址与实际装载地址的差值。我们来看一个例子,比如有一个DLL的基地址是Ox10000000,那么如果它的代码中有这样一条指令;

MOV DWORD PTR [0x10001000], 0x100

我们假设0x10001000是该模块中一个变量foo的地址,即该变量的RVA是0x1000。如果DLL在装载时,0x10000000这个地址被其他DLL占用了,Windows就会将它加载到一个新的地址,假设是0x20000000。因为0x10001000是个绝对地址,所以我们需要对这条指令进行重定位。这时候新的基地址是Ox20000000,而 RVA是不变的,所以foo的地址实际上已经变成了Ox20001000,也就是指令的地址部分要加上Ox20000000 - Ox10000000的这个差值。经过调整后的指令应该是:

MOV DWORD PTR [0x20001000], 0x100

事实上,由于DLL内部的地址都是基于基地址的,或者是相对于基地址的RVA。那么所有需要重定位的地方都只需要加上一个固定差值,在这个例子里面是0x10000000。所以这个重定位的过程相对简单一点,速度也要比一般的重定位要快。PE里面把这种特殊的重定位过程又被叫做重定基地址( Rebasing)。

PE文件的重定位信息都放在了“.reloc”段,我们可以从PE文件头中的DataDirectory 里面得到重定位段的信息。重定位段的结构跟ELF中的重定位段结构十分类似,在这里就不再详细介绍了。对于EXE文件来说,MSVC编译器默认不会产生重定位段,也就是默认情况下,EXE是不可以重定位的,不过这也没有问题,因为EXE文件是进程运行时第一个装入到虚拟空间的,所以它的地址不会被人抢占。而DLL则没那么幸运了,它们被装载的时间是不确定的,所以一般情况下,编译器都会给DLL文件产生重定位信息。当然你也可以使用“/FIXED”参数来禁止DLL产生重定位信息,不过那样可能会造成DLL的装载失败。

这种重定基地址的方法导致的一个问题是,如果一个DLL被多个进程共享,且该DLL被这些进程装载到不同的位置,那么每个进程都需要有一份单独的DLL代码段的副本。很明显,这种方案相对于ELF的共享对象代码段地址无关的方案来说,它更加浪费内存,而且当被重定基址的代码段需要被换出时,它需要被写到交换空间中,而不像没有重定基址的DLL代码段,只需要释放物理页面,再次用到时可以直接从DLL文件里面重新读取代码段即可。但是有一个好处是,它比ELF的PIC机制有着更快的运行速度。因为PE的 DLL对数据段的访问不需要通过类似于GOT的机制,对于外部数据和函数的引用不需要每次都计算GOT的位置,所以理论上会比ELF的PIC的方案快一些。这又是一个空间换时间的案例。

改变默认基地址

重定基地址过程实际上是在DLL文件装载时进行的, 所以又叫做装载时重定位。对于一个程序来说,它所用到的DLL基本是固定的(除了通过LoadLibrary()装载的以外)。程序每次运行时,这些DLL的装载顺序和地址也是一样的。比如一个程序由程序主模块main.exe、foo.dll和 bar.dll 3个模块组成,它们的大小都是64 KB。于是当程序运行起来以后进程虚拟地址空间的布局应该如下图所示。

模块 起始地址 结束地址
main.exe 0x00400000 0x00410000
foo.dll 0x10000000 0x10010000
bar.dll 0x10010000 0x10020000

可以看到bar.dll 原先默认的基地址是Ox10000000,但是它被重定基址到了0x10010000,因为0x10000000到0x10010000这块地址被先前加载的 foo.dll占用了(假设foo.dll t比 bar.dl先装载)。那么既然bar.dll每次运行的时候基地址都是0x10010000,为什么不把它的基地址就设成0x10010000呢?这样就省掉了bar.dll每次装载时重定基址的过程,不是可以让程序运行得更快吗?

MSVC的链接器提供了指定输出文件的基地址的功能。那么可以在链接时使用link 命令中的“/BASE”参数为bar.dll指定基地址:

link /BASE : 0x10010000,0x10000 / DLL bar.obj

这个基地址必须是64K的倍数,如果不是64 K的倍数,链接器将发出错误。这里还有一个参数0x10000是指DLL占用空间允许的最大的长度,如果超出这个长度,那么编译器会给出警告。这个看似没用的选项实际上非常有用,比如我们的程序中用到了10个DLL,那么我们就可以为每个DLL手工指定一块区域,以防止它们在地址空间中相互冲突。假设我们为bar.dll 指定的空间是Ox10010000到0x10020000这块空间,那么在使用“/BASE”参数时,我们不光指定bar.dll的起始地址,还指定它的最长的长度。如果超出这个长度,它就会占用其他DLL的地址块,如果链接器能够给出警告的话,我们就很快能发现问题并且进行调整。

系统DLL

由于Windows系统本身自带了很多系统的DLL,比如 kernel32.dll、ntdll.dll、shell32.dll、user32.dll、msvcrt.dll等,这些 DLL基本上是Windows 的应用程序运行时都要用到的。Windows 系统就在进程空间中专门划出一块0x70000000~0x80000000区域,用于映射这些常用的系统DLL。Windows在安装时就把这块地址分配给这些DLL,调整这些DLL的基地址使得它们相互之间不冲突,从而在装载时就不需要进行重定基址了。

参考自:程序员的自我修养:链接、装载与库

​ 维基百科-動態鏈接庫

posted @ 2020-12-24 18:03  Redwarx008  阅读(293)  评论(0编辑  收藏  举报