【程序员的自我修养】第9章 Windows下的动态链接

来源:互联网 发布:java file类能干嘛 编辑:程序博客网 时间:2024/05/22 01:39

9章 Windows下的动态链接

DLL简介

DLLEXE文件实际上是一个概念,都是PE格式的二进制文件,不同的是在PE文件头部将两者区分,DLL文件扩展名也可以是.oxc(空间)或者.CPL(控制面板程序)

一个DLL在不同的进程中拥有不同的私有数据版本,DLL的代码不是地址无关的

一个PE文件被装载时,其进程地址空间的起始地址就是基地址,任何一个PE文件都有一个优点装载的基地址,即Image Base

Windows可以将DLL的数据段设为共享,这就意味着一个DLL中有两个数据段,一个进程间共享,另一个私有

当我们使用“__declspec(dllexport)”时表示该符号是从本DLL导出的符号。“__declspec(dllimport)”表示该符号是从别的DLL导入的符号。

创建DLL

/*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;

}

使用cl编译器进行编译:

cl   /LDd  Math.c

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

通过dumpbin 查看DLL的导出符号:

D:\Program Files\home\*>dumpbin /EXPORTS Math.dll

Microsoft (R) COFF/PE Dumper Version 11.00.50214.1

Copyright (C) Microsoft Corporation.  All rights reserved.

Dump of file Math.dll

File Type: DLL

  Section contains the following exports for math.dll

    00000000 characteristics

    4F878A19 time date stamp Fri Apr 13 10:06:17 2012

        0.00 version

           1 ordinal base

           3 number of functions

           3 number of names

    ordinal hint RVA      name  //三个导出函数即相对地址

          1    0 00001000 Add

          2    1 00001040 Mul

          3    2 00001020 Sub

  Summary

        3000 .data

        9000 .rdata

        3000 .reloc

       1E000 .text

使用DLL:程序使用DLL的过程就是一弄DLL中的导出函数和符号的过程,即导入过程

/*test_math.c*/

#include <stdio.h>

__declspec(dllexport) 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;

}

编译:

D:\Program Files\home\*>cl    /c test_math.c

D:\Program Files\home\*> link  test_math.obj  math.lib

声明DLL中的某个函数为导出函数的办法有两种,使用“__declspec(dllexport)”扩展;另一种就是采用模块定义(.def)文件声明。

创建math.def文件:

/*math.def*/

LIBRARY Math

EXPORTS

Add

Sub

Mul

Div

编译:

D:\Program Files\home\*>cl  math.c /LD  /DEF  math.def

最终输出跟第一种方法相同的结果。

DLL支持显式运行时链接,Windows提供了3API为:

LoadLibrary,这个函数用来装载一个DLL到进程的地址空间,它的功能跟dlopen类似。

GetProcAddress, 用来查找某个符号的地址,与dlsym类似

FreeLibrary, 用来卸载某个已加载的模块,与dlclose类似

/*RunDllSimple.c*/

#include <windows.h>

#include <stdio.h>

typedef double (*Func)(double, double);

int main(int argc, char *argv[])

{

  Func function;

  double result;

  HINSTANCE hinstLib=LoadLibrary("math.dll");

  if (hinstLib==NULL)

  {

    printf("ERROR:unable to load DLL.\n");

    return 1;

  }

  function=(Func)GetProcAddress(hinstLib, "Add");

  if (function==NULL)

  {

    printf("ERROR:unable to find DLL function.\n");

    FreeLibrary(hinstLib);

    return 1;

  }

   result=function(1.0, 2.0);

  FreeLibrary(hinstLib);

  printf("result=%f\n", result);

  return 0;

}

D:\Program Files\home\*>cl RunDllSimple.c

D:\Program Files\home\*>RunDllSimple.exe

result = 3.000000

符号导出导入表

导出表

当一个PE需要将一些函数或变量提供给其他PE文件使用时,我们把这种行为叫做符号导出。导出表提供了一个符号名与符号地址的映射关系。

ELF中,“.rel.dyn”和“.rel.plt”两个段中分别保存了该模块所需要导入的变量和函数的符号以及所在的模块等信息。而“.got”和“.got.plt”则保存着这些变量和函数的真正地址。

PE文件头一个DataDirectory的结构数组,其第一个元素就是导出表的地址和长度,导出表是一个IMAGE_EXPORT_DIRECTORY的结构体,导出表最后的三个成员指向的是三个数组:导出地址表、符号名表、名字序号对应表。

使用序号的好处就是省去了函数名查找过程,最大的问题就是函数会发生变化

导入表

查看依赖了哪些DLL

D:\Program Files\home\*>dumpbin    /IMPORTS   math.dll

我们的math.dll导入了KERNEL32.dll

PE文件中,导入表是一个IMAGE_IMPORT_DESCRIPTOR的结构数组,每个IMAGE_IMPORT_DESCRIPTOR结构对应一个被导入的DLL

EXE文件的基地址默认为0x00400000,而DLL文件基地址默认为0x10000000

DLL优化

重定基地址(Rebasing),链接时可以指定DLL的基地址,还有一个editbin工具改变已有的DLL的基地址

序号

导入函数绑定

DLL绑定:把导出函数的地址保存到模块的导入表中,可以省去每次启动时符号解析的过程。

DLL绑定实现,editbin对被绑定的程序的导入符号进行遍历查找,找到以后就把符号的运行时的目标地址写入到被绑定程序的导入表内。

c++与动态链接

Windows平台型,尽量遵循以下指导意见使编写动态链接库:

(1)所有接口函数都应该是抽象的。所有的方法都应该是纯虚的。(或者inline的方法也可以)

(2)所有的全局函数都应该使用extern C”来防止名字修饰的不兼容。并且导出函数的都应该是__stdcall调用规范(COMDLL都使用这样的规范)。这样即使用户本身的程序默认以__cdecl方式编译的,对于DLL的调用也能正确。

(3)不要使用C++标准库STL

(4)不要使用异常

(5)不要使用虚析构函数。可以创建一个destroy()方法并且重载delete操作符并且调用destroy()

(6)不要在DLL里面申请内存。而且在DLL外释放(或者相反)。不同的DLL和可执行文件可能使用不同的堆,在一个堆里面申请内存而在另外一个堆里面释放会导致错误。比如,对于内存分配相关的函数不应该是inline的,以防止它在编译时被展开到不同的DLL和可执行文件

(7)不要在接口中使用重载方法。因为不同的编译器对于vtable的安排可能不同。

DLL HELLdll噩梦)

Windows缺乏一种有效的DLL版本控制机制,解决方法:

静态链接

防止DLL覆盖(windows文件保护实现)

避免DLL冲突(让每个应用程序拥有自己依赖的DLL

.NetDLL Hell的解决方案(Manifest - XML描述文件,(side-by-side manager)SxS Manager实现对相应版本的DLL的加载)