程序员自我修养阅读笔记——Widnows下的动态链接

这篇具有很好参考价值的文章主要介绍了程序员自我修养阅读笔记——Widnows下的动态链接。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

  Linux下的共享库为so文件为ELF格式,Windows下的共享库为dll文件PE格式。

1 dll简介

  windows下采用dll共享对象让程序更加模块化方便升级,大多数情况为.dll文件,也可以是``.ocx,.CPL```文件。

1.1 进程地址空间和内存管理

  早期的windows中的进程并没有独立的地址空间,32bit的windows开始进程才有独立的地址空间,一个dll在不同进程中拥有不同的私有数据副本。但是和elf不同dll中的代码并不是地址无关的,只是在某些情况下被过个进程共享。

1.2 基地址和RVA。

  基地址:就是一个PE文件被装载时,其进程空间的起始地址。
  RVA(相对虚拟地址):也就是相对于基地址的偏移地址。

1.3 DLL共享数据段

  dll允许进程将一部分数据设置为共享的,也就是一个进程包含两个数据段,一个进程成共享的数据段,另一个私有。可以利用dll共享数据段进行进程间通信。

1.4 dll的简单例子

  elf文件默认是导出所有符号,而dll不同,默认不导出符号,需要显式指定某个需要导出的符号。而相对的在程序中使用dll的导出符号的过程为导入。mscv编译期可以通过__declspec(dllexport)__declspec(dllimport)指定导出和导入的符号。
  另外,也可以使用.def文件声明导入和导出的符号,改文件类似一个链接器的脚本。

1.5 创建dll

  对于下面的math.c文件使用命令cl /LDd math.c便可以得到四个math.dll,math.obj,math.exp,math.lib文件。可以通过dumpbin查看dll的导出符号。

__declspec(dllexport) int add(int a, int b){
    return a + b;
}

__declspec(dllexport) int sub(int a, int b){
    return a - b;
}
E:\code\tmp>dumpbin /EXPORTS math.dll
Microsoft (R) COFF/PE Dumper Version 14.16.27045.0
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
    FFFFFFFF time date stamp
        0.00 version
           1 ordinal base
           2 number of functions
           2 number of names

    ordinal hint RVA      name

          1    0 00001000 add
          2    1 00001020 sub

  Summary

        3000 .data
        3000 .pdata
       12000 .rdata
        1000 .reloc
       37000 .text

1.6 使用dll

  使用dll的过程就是导入的过程,只需要将相关符号显示生命为导入符号,然后在编译时链接即可。

#include <stdio.h>

__declspec(dllimport) int add(int a, int b);

int main(){
    int result = add(1, 2);
    printf("value = %d\n", result);
    return 0;
}

  能够看到这里链接的是math.lib而不是math.dll。只是因为链接时仅仅需要符号相关信息,这里的math.lib仅仅包含链接时需要的信息(通过查看文件大小也能看出区别,lib文件仅仅2kb)。

E:\code\tmp>cl -c main.c
用于 x64 的 Microsoft (R) C/C++ 优化编译器 19.16.27045 版
版权所有(C) Microsoft Corporation。保留所有权利。

main.c

E:\code\tmp>link main.obj math.lib
Microsoft (R) Incremental Linker Version 14.16.27045.0
Copyright (C) Microsoft Corporation.  All rights reserved.

E:\code\tmp>main.exe
value = 3

1.7 使用模块定义文件

  dll可以通过__declspec(dllexport)定义导出符号,也可以通过.def文件定义。.def文件类似linux的链接脚本,用于控制链接过程,为链接器提供有关链接程序的导出符号、属性以及其他信息。
  下面分别是math.cmath.def的内容。

int add(int a, int b){
    return a + b;
}

int sub(int a, int b){
    return a - b;
}
LIBRARY math
EXPORTS
add
sub

  然后通过下面的命令生成动态库。

E:\code\tmp>cl math.c -LD /DEF math.def
用于 x64 的 Microsoft (R) C/C++ 优化编译器 19.16.27045 版
版权所有(C) Microsoft Corporation。保留所有权利。

math.c
Microsoft (R) Incremental Linker Version 14.16.27045.0
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:math.dll
/dll
/implib:math.lib
/def:math.def
math.obj
  正在创建库 math.lib 和对象 math.exp

  使用.def可以很方便的控制链接过程,自定义链接中的一些规则。比如指定导出符号修饰方式、堆大小、文件名、段属性等等。

1.8 dll显示运行时链接

  类似于elf,dll提供了类似的api可以在运行时链接动态库。

  • LoadLibrary:加载动态库;
  • GetProcAddress:获取某个符号的地址
  • FreeLibrary:卸载已经加载的模块。
#include <stdio.h>
#include <windows.h>

typedef int (*myfunction)(int, int);

int main(){
    HINSTANCE id = LoadLibrary("math.dll");
    if(id == NULL){
        printf("无法加载动态库\n");
        exit(1);
    }

    myfunction function = (myfunction)GetProcAddress(id, "add");
    if(function == NULL){
        printf("无法找到库函数\n");
        exit(1);
    }

    int result = function(11, 2);
    printf("handle is %p, address is %p, value = %d\n", id, function, result);
    FreeLibrary(id);
    return 0;
}

  下面是运行的结果。

E:\code\tmp>cl main.c
用于 x64 的 Microsoft (R) C/C++ 优化编译器 19.16.27045 版
版权所有(C) Microsoft Corporation。保留所有权利。

main.c
Microsoft (R) Incremental Linker Version 14.16.27045.0
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:main.exe
main.obj

E:\code\tmp>main.exe
handle is 00007FFAA3D80000, address is 00007FFAA3D81000, value = 13

2 符号导出导入表

2.1 导出表

  windows上存在和linux类似的导出表,导出表用来收集导出的符号,存储符号名和符号地址的映射关系。PE文件Header中又一个DataDirectory的结构数组,该数组的第一个元素就是导出表的结构地址和长度。导出表是一个定义在Winnt.h中的_IMAGE_EXPORT_DIRECTORY结构体。该结构的最后三个成员是三个指针,分别表示三个数组,分别是导出地址表EAT(即对应的符号地址表)、符号名表和名字序号对照表。其中名字序号对照表有一定历史原因,不做详述。

    typedef struct _IMAGE_EXPORT_DIRECTORY {
      DWORD Characteristics;
      DWORD TimeDateStamp;
      WORD MajorVersion;
      WORD MinorVersion;
      DWORD Name;
      DWORD Base;
      DWORD NumberOfFunctions;
      DWORD NumberOfNames;
      DWORD AddressOfFunctions;
      DWORD AddressOfNames;
      DWORD AddressOfNameOrdinals;
    } IMAGE_EXPORT_DIRECTORY,*PIMAGE_EXPORT_DIRECTORY;

可以给链接器使用/EXPORTS传递参数表明希望导出的符号。

程序员自我修养阅读笔记——Widnows下的动态链接

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 导出重定向

  将某个导出符号重定向到另外一个dll。导出表的地址数组中包含的是函数的RVA,但是如果这个RVA指向的位置位于导出表中,那么表示这个符号被重定向了。

2.4 导入表

  windows上有导入表保存模块所需要导入的符号等信息,Windows加载器会将所需要导入的函数地址确定并且将导入表中的元素调整到正确的地址,以实现动态链接。我们可以使用dumpbin /IMPORTS main.dll查看导入的符号,下面只是一部分输出,可以看出一些是程序运行必须的系统库,比如kernel32.dll,ntdll.dll等。

E:\code\tmp>dumpbin /IMPORTS math.dll
Microsoft (R) COFF/PE Dumper Version 14.16.27045.0
Copyright (C) Microsoft Corporation.  All rights reserved.


Dump of file math.dll

File Type: DLL

  Section contains the following imports:

    KERNEL32.dll
             18000C000 Import Address Table
             180014148 Import Name Table
                     0 time date stamp
                     0 Index of first forwarder reference

                         450 QueryPerformanceCounter
                         21E GetCurrentProcessId
                         222 GetCurrentThreadId
                         2F0 GetSystemTimeAsFileTime

  在PE文件中,导入表是一个_IMAGE_IMPORT_DESCRIPTOR的结构体数组(定义在winnt.h中),每个_IMAGE_IMPORT_DESCRIPTOR对应一个被导入的dll。其中FirstThunk是一个导入地址数组的首地址,即导入地址表(IAT),该数组的每个元素就是一个被导入的符号,该元素的含义在不同情况下不同。当链接器刚开始重定位和符号解析时,该表中元素值表示对应的符号名或者序号;当解析完成时,则表示符号的真正地址。对于32bitPE,当元素值的高1bit被置为1时,低31bit表示序号;当为0时指向一个_IMAGE_IMPORT_BY_NAME的结构体表示符号名。另外OriginalFirstThunk是一个导入名称表(INT)保存的内容和导入符号表相同。

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
//导入名称表,简称INT。和IAT一样
} DUMMYUNIONNAME;
DWORD TimeDateStamp; // 0 if not bound,
// -1 if bound, and real date\time stamp
// in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
// O.W. date/time stamp of DLL bound to (Old BIND)

DWORD ForwarderChain; // -1 if no forwarders
DWORD Name;
DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses)
//指向一个导入地址数组,IAT是导入表中最重要的结构,IAT中每个元素对应一个被导入的符号,元素的值在不同情况下有不同的含义。
} IMAGE_IMPORT_DESCRIPTOR;

typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD Hint;
    BYTE Name[1];
} IMAGE_IMPORT_BY_NAME,*PIMAGE_IMPORT_BY_NAME;

  windows通过提供延迟载入,仅仅在符号第一次被使用时载入。

程序员自我修养阅读笔记——Widnows下的动态链接

2.5 导入函数的调用

  PE采用类似ELF GOT的方式间接访问一个函数,但是链接器无法判断一个符号的可见性。msvc使用__desclspec(import)来判断函数是否为外部导入,以便于生成相应的代码。另一种方式是不区分导入和导出函数,统一产生调用的指令,链接器在链接时会将导入函数的目标地址导向一小段桩代码,由桩代码将控制权移交给IAT真正的地址。然而实际上链接器不会生成代码,只负责链接,这些代码来自于lib文件,即导入库。当编译器生成导入库时,同一个导出符号会长生两个符号的定义,即func__imp__func,前者指向桩代码,后者指向包含的IAT中的位置。但是明显的使用__desclspec(import)的方式少一条跳转指令,性能上更友好。

3 dll优化

  DLL的代码段和数据段本身并不是地址无关的,它默认需要被装载到由ImageBase指定的目标地址中。如果目标地址被占用,那么就需要装载到其他地址,便会引起整个DLL的Rebase。符号和字符串的比较和查找过程也会影响DLL性能。

3.1 重定基地址(Rebasing)

  PE的DLL中的代码段并不是地址无关的,它在被装载时有一个固定的目标地址,这个地址也就是PE里面所谓的基地址。对于DLL来说,一个进程中,多个DLL不可以被装载到同一个虚拟地址,每个DLL所占用的虚拟地址区域之间都不可以重叠。
  Windows PE采用装载时重定位:在DLL模块装载时,如果目标地址被占用,那么操作系统就会为它分配一块新的空间,并且将DLL装载到该地址。对于DLL每个绝对地址引用都进程重定位。
由于DLL内部地址都是基于基地址的,或者是相对于基地址的RVA。那么所有需要重定位的地址都只需要加上一个固定差值。PE里面把这种特殊的重定位过程叫做重定基地址。EXE是不可以重定位的,不过这也没问题,因为EXE文件是进程运行时第一个装入虚拟空间的,所以它的地址不会被人抢占。

  link可以通过参数指定装载的地址,editbin可以改变已有dll的基地址。
  Windows系统在进程空间中专门划出一块0x70000000~0x80000000区域,用于映射这些常用的系统DLL。

3.2 序号

  一个DLL中每一个导出的函数都有一个对应的序号。序号标示被导出函数地址在DLL导出表中位置。一个导出函数甚至没有函数名,但它必须有唯一的序号。当从一个dll导入函数时可以使用函数名也可以使用序号。不同windows版本之间api的函数名可能没有变化,但是序号是在不停的变化。另外我们可以在def文件中指定导出符号的序号。

3.3 导入符号绑定

  当一个程序运行时,所有被依赖的DLL都会被装载,并且一系列导入导出符号依赖关系都会被重新解析。这些DLL都会以同样的顺序被装载到同样的内存地址,所以它们的导出符号的地址都是不变的。将这些导出函数的地址保存到模块的导入表中的过程就叫做dll绑定。dll绑定能够省去启动时符号解析的过程提升性能。

  可以利用editbin完成dll绑定。
  dll的绑定实现:editbin对被绑定的程序的导入符号进行遍历查找,找到以后就把符号的运行时的目标地址写入到绑定程序的导入表内。
  dll绑定地址失效的情况:

  1. 被依赖的dll更新导致dll的导出函数地址发生变化。解决方式:windows将dll的时间戳和md5校验和保存到绑定的pe文件的导入表中,运行时如果校验失败则走正常流程导入;
  2. 被依赖的dll在装载时发生重定基址导致dll的装载地址与绑定时不一致。

4 C++与动态链接

  C++编写动态链接库在Windows平台下最好遵循以下指导:

  • 所有的接口都应该抽象;
  • 所有的全局函数都应该使用”extern C”来防止名字修饰的不兼容;
  • 不要使用C++标准库STL;
  • 不要使用异常;
  • 不要使用虚析构函数;
  • 不要在DLL里面申请内存;
  • 不要在接口中使用重载方法。

5 dll hell

  由于早期Windows缺乏一种很有效的DLL版本控制机制,DLL不兼容文件在Windows非常严重,被人们称为DLL噩梦(DLL hell)。DLL HELL发生的三种可能原因:

  • 由使用旧版本的DLL替代原来一个新版本的DLL引起
  • 由新版DLL中的函数无意发生改变而引起
  • 由新版DLL按照引入一个新BUG
      解决DLL Hell的方法:
  • 静态链接;
  • 防止DLL覆盖:使用windows保护技术;
  • 避免DLL冲突:每个应用程序拥有一份自己依赖的DLL。

  .NET 下DLL Hell的解决方案:在.NET框架中,一个程序集有两种类型:应用程序集以及库程序。一个程序集包块一个或多个文件,所以需要一个清单文件来描述程序集。这个清单文件叫做Manifest文件。Manifest文件描述了程序集的名字,版本号以及程序集的各种资源,同时也描述了该程序集的运行所依赖的资源,包括DLL以及其他资源文件等。操作系统会根据DLL的manifest文件去寻找对应的DLL并调用。文章来源地址https://www.toymoban.com/news/detail-444806.html

到了这里,关于程序员自我修养阅读笔记——Widnows下的动态链接的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 〖程序员的自我修养 - 认知剖析篇⑤〗- 选择前端还是后端?

    人之所以会觉得迷茫,本质上是欠缺对自己的一个控制力、识别庞杂信息、去伪存真的独立思考与认知能力。 说明:该文属于 程序员的自我修养 专栏, 购买任意白宝书体系化专栏可加入 易编程社区, 早鸟价订阅模式除外 。 福利:加入社区的小伙伴们,除了可以获取博主

    2024年02月14日
    浏览(29)
  • 〖程序员的自我修养 - 认知剖析篇②〗- 学习编程之前你需要知道这些

    人之所以会觉得迷茫,本质上是欠缺对自己的一个控制力、识别庞杂信息、去伪存真的独立思考与认知能力。 说明:该文属于 程序员的自我修养 专栏, 购买任意白宝书体系化专栏可加入 易编程社区, 早鸟价订阅模式除外 。 福利:加入社区的小伙伴们,除了可以获取博主

    2024年02月12日
    浏览(30)
  • 〖程序员的自我修养 - 精炼面试篇⑨〗- 技术面试不用怕,也是有套路可利用的

    人之所以会觉得迷茫,本质上是欠缺对自己的一个控制力、识别庞杂信息、去伪存真的独立思考与认知能力。 说明:该文属于 程序员的自我修养 专栏, 购买任意白宝书体系化专栏可加入 易编程社区, 早鸟价订阅模式除外 。 福利:加入社区的小伙伴们,除了可以获取博主

    2024年02月15日
    浏览(50)
  • 〖程序员的自我修养 - 认知剖析篇④〗- 关于前端方向与后端方向的一些个人见解

    人之所以会觉得迷茫,本质上是欠缺对自己的一个控制力、识别庞杂信息、去伪存真的独立思考与认知能力。 说明:该文属于 程序员的自我修养 专栏, 购买任意白宝书体系化专栏可加入 易编程社区, 早鸟价订阅模式除外 。 福利:加入社区的小伙伴们,除了可以获取博主

    2024年02月14日
    浏览(41)
  • 测试/开发程序员有8大好处,自我实现和自我超越......

    测试/开发程序员有8大好处 很早前网络就关于程序员种种不好的标签,比如“不修边幅,生活邋遢“,“无趣呆板,不浪漫“,“加班多“,“不疼爱女朋友“,“情商低”等,通通都是瞎扯,除了加班多,其他跟程序员行一点关系都没有。 当测试/开发程序员好处多多 1、相

    2024年02月03日
    浏览(69)
  • 【C++程序员的自我修炼】拷贝构造函数

    心存希冀 追光而遇目有繁星 沐光而行 目录 拷贝构造函数概念 拷贝构造的特征 无穷递归的解释 浅拷贝 总结:  深拷贝 拷贝构造函数典型调用场景 总结  契子 ✨ 在生活中总有很多琐事,不做不行做了又怕麻烦,有时候想要是有个和自己一模一样的人就好了 可以帮我上早读

    2024年04月14日
    浏览(36)
  • 书栈网-BookStack 程序员IT互联网开源编程书籍免费阅读

    书栈网是一个提供各种编程语言和技术的开源书籍免费阅读的网站,涵盖前端、后端、数据库、算法、数据分析等领域。您可以在书栈网找到最新最热的开源书籍,以及相关的文档资料。 书栈网提供网页版跟手机APP。 网页版书栈网官网地址:https://www.bookstack.cn/ 手机版 Book

    2024年02月04日
    浏览(44)
  • 嵌入式C语言自我修养笔记1-ARM体系结构与编译运行

    ARM 体系结构 ARM 体系结构 ARM 相对精简指令集 RISC 还有如下区别 ● ARM 有桶型移位寄存器,单周期内可以完成数据的各种移位操作。 ● 并不是所有的 ARM 指令都是单周期的。 ● ARM 有 16 位的 Thumb 指令集,是 32 位 ARM 指令集的压缩形式,提高了代码密度 ● 条件执行:通过指令

    2024年02月07日
    浏览(34)
  • 嵌入式C语言自我修养《GNU C编译器扩展语法》学习笔记

    目录 一、C语言标准和编译器 二、指定初始化 三、宏构造“利器”:语句表达式 四、typeof与container_of宏 五、零长度数组 六、属性声明:section  七、属性声明:aligned  C语言标准的发展过程: ● KR C. ● ANSI C. ● C99. ● C11. 指定初始化结构体成员:         和数组类似,

    2024年02月08日
    浏览(34)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包