1. Windows动态链接下的导入函数的调用过程
在ELF结构下,函数调用因为 有全局符号介入的可能,所以除非用static关键词修饰,否则只要是函数调用,无论是否是模块内还是模块外,都需要经过.got.plt间接跳转,来实现ELF结构下代码段的地址无关性。

在PE文件结构下,是不存在全局符号介入的,所以对于模块内部的函数调用,编译器将产生直接调用指令CALL XXXXXXXX(不是相对地址偏移,是直接地址调用。这是因为Windows PE下,任何一个PE文件在编译时都会给出自己的一个优先装载位置,然后根据此位置产生一系列的定位,当然这个绝对地址是需要在实际装载运行时再重新修正的,采用了一种重定基地址的方法)。而对于模块外部函数调用,则会采用在IAT表中的地址间接调用call Dword PTR [0x0040D11C],这里面的IAT表元素地址0x0040D11C也是绝对地址,这是也需要后续修正的。故而可以看到PE结构下,DLL的代码段并非地址无关的,所以Windows系统就是大气,根本不像Linux那么在意代码段指令的重复利用。

所以在PE结构下,如果通过_declspec(import)修饰符显式声明了某个函数外部函数,则编译器将直接跳转到IAT该函数对应项,
CALL DWORD PTR [0x0040D11C]

如果不显式声明,也是可以的,但会产生两次跳转

CALL  0x0040100C
…
0x0040100C:
JMP  DWORD  PTR [0x0040D11C]

其中JMP指令处被称为桩stub代码,是链接器在链接过程中通过同名.lib文件中的胶水代码插入的。
(注:以Math.dll为例,一般DLL文件加载是同同名的.lib文件加载的,比如link TestMath.obj Math.lib,其中Math.lib是用来描述Math.dll的导出符号信息,除此之外,.lib文件还包含了一些胶水代码(桩代码)用来将主程序和DLL符号黏在一起。)

现在MSVC编译器对于以上两种导入方式都是允许的,但是还是推荐采用_declspec(import)修饰符显式声明符号导入,减少了一步跳转指令加速运行过程。

2. DLL优化手段
DLL的代码段和数据段本身并非地址无关的,即它默认需要被转载到由ImageBase指定的目标地址中,如果目标地址被占用,那么就需要装载到其他地址,便会引起整个DLL的Rebase。这对于拥有大量DLL的程序来说,频繁的Rebase是不可避免的( 意味着系统需要频繁遍历当前进程空间找到空间合适的空档将DLL插进去), 故而这也是影响DLL性能的另外一个原因。

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

一般来说,PE文件结构中.exe文件的基地址默认是0x00400000,而DLL文件基地址默认是0x10000000。

讨论到共享对象的地址冲突问题,主要有三种方法:1.前面介绍的“静态库”方法,人为地限制进程空间中某区域为特定库的装载地址;2.ELF的代码段地址无关;3.便是PE的基地址重定

基地址重定位
DLL模块装载默认的base如何已经被占用,那么操作系统会重新再进程空间中分配一块区域,但是DLL的代码段并非地址无关的,故而DLL代码段中所有的绝对地址引用都要重定位。但是这个重定位比较特殊,是所有绝对地址整体加上一个相同的基地址偏移量即可。所以这种重定基地址的方法比一般重定位要快。

PE文件的重定位信息放在.reloc段,可以从PE文件头中的DataDirectory的16组数据中提取到重定位段的信息。(.exe一般MSVC编译器是不产生.reloc段的,DLL也可以通过/FIXED参数取消重定位信息,但是这种固定加载起点的方式可能会导致DLL装载失败)

PE的DLL代码段非地址无关性相比于ELF最大的缺点是多个进程无法共享代码段,浪费内存;但是正是DLL代码段放弃了共享所需的动态性,在整个过程中以直接调用为主的偏向静态链接的方式,可以让DLL机制比ELF的PIC机制运行的更快,不需要每次调用外部数据和函数都需要计算下该符号在GOT的位置,而DLL在重定基地址后直接给出的是各符号在IAT(导入地址数组 Import Address Table)中的地址。

Fig.1 COFF PE文件结构下的符号导入导出表对应关系 2. 1 DLL优化手段之一:改变默认装载基地址
如果程序的DLL装载顺序固定,并且这些DLL变动并不频繁,为了让这些日常使用到的程序启动加速,则可以根据DLL装载顺序和空间使用情况,直接设置各DLL的装载Base,这样就可以加速装载时重定位过程。


如果bar.dll默认基地址为0x1000 0000,当时因为在该程序的装载过程中,DLL文件的装载顺序是固定的(广度优先),那么显然bar.dll的默认基地址是被foo.dll抢占去的,但既然如此干脆直接将bar.dll的默认装载基地址改成0x1001 0000即可,这样省去了每次装载时重定位基地址的过程。可以采用如下

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

MSVC还提供了一个editbin工具用来直接改变已有DLL的基地址,在DLL文件自身的装载点更改数据

editbin /REBASE:BASE=0x10010000  bar.dll

2. 2 系统DLL专用加载区间
将改变默认基地址这一思想给具体化、通用化,每个程序在运行时都要使用一些系统DLL,这些系统DLL基本上都固定了,如kernel32.dll, ntdll.dll, shell32.dll, user32.dll, msvcrt.dll等,故而Windows操作系统直接将进程的虚拟空间中0x70000000~0x80000000固定下来专门用来存放这些系统DLL,这样就可以部分加速任意程序的重定位过程。然后系统在安装这些.dll时便调整了这些dll的装载基地址,程序装载时无需再次更改基地址。如下便是一个案例:

通过 dumpbin /IMPORTS main.exe可以查看各系统DLL的加载信息,我的系统中kernel32.dll的默认基地址是78D20000 to 78E3EFFF;user32.dll的默认基地址是78C20000 to 78D19FFF。 2. 3 DLL优化之导入函数绑定
前面说的优化过程是从DLL这些辅助对象出发的,现在考虑主模块的优化。

每一次一个程序运行时,所有被依赖的DLL都会被装载,并且一系列的导入导出符号依赖关系都会被重新解析。在大多数情况下,这些DLL都会以同样的顺序被装载到相同的内存地址,所以它们的导出符号的地址应该都是不变的,既然这些符号的地址不变,那程序主模块的导入表应该还是和上次程序运行时相同,故而可以保留下来,这样就可以省去每次启动时符号解析的过程。这种方法称为DLL绑定。

DLL绑定实现
思路很简单,editbin对被绑定的程序的导入表内符号进行遍历,将该符号在运行时的目标地址写入到被绑定程序的导入表内,还记得前面IAT旁边还有个冗余的INT吗?这个INT就是用来记录DLL绑定信息的,故而下次运行时,一旦检测到INT里面有信息,则不需要再次进行符号重定位了,如果遇到问题(如依赖的DLL更新,DLL装载顺序打乱了和此前装载位置不一致),导致INT中绑定符号信息失效,则也可以依靠IAT的信息再重来一次重定位。Windows系统中很多系统自带程序便采用DLL绑定用以加速程序启动

dumpbin /IMPORTS C:\Winodws\notepad.exe

失效情况和应对措施一:依赖的DLL更新,导致该DLL内部RVA信息发生变化
链接器把DLL的时间戳(timestamp)和校验和(eg:MD5)都封存在PE文件的导入表中,每次重新装载时,先验证一下,如果没有变,则启动INT高速路,否则就按照IAT这条正常的主干道运行。
(注:Windows系统所附带的程序都是和它依赖的DLL绑定的,至少在DLL升级之前,这些DLL绑定是有效。当然绑定过程会让可执行文件大小和内容发生变化,对于一些经过加密的或是有数字签名的程序而言需要考虑这些问题。)

失效情况和应对措施二:DLL由于运行环境变化导致装载时发生了重定基地址
可以在验证信息中再添加一条绑定默认base(如何下次装载该DLL的base不同,则知道发生了重定基地址),故而需要从正常的IAT途径走。

Windows下动态链接之二:DLL优化加速相关推荐

  1. Windows下动态链接之三:DLL Hell !

    不止Linux下关于共享库存在版本兼容性困扰问题,Windows下DLL共享库的使用问题更甚.很多Windows的应用程序在发布release版本时会一次性将所有用到的DLL一起打包形成一个大的安装包 ...

  2. Windows下查看exe可执行程序或dll动态库所依赖dll动态库的方法

    Windows下查看exe可执行程序或dll动态库所依赖dll动态库的方法 如需转载请标明出处:http://blog.csdn.net/itas109 技术交流:129518033 文章目录 Win ...

  3. mysql实现程序的动态链接_程序的链接和装入及Linux下动态链接的实现

    链接器和装入器的基本工作原理 一个程序要想在内存中运行,除了编译之外还要经过链接和装入这两个步骤.从程序员的角度来看,引入这两个步骤带来的好处就是可以直接在程序中使用printf和errno这种有意义 ...

  4. windows下如何使用QT编写dll程序 .

    Windows 下如何使用 QT 编写 dll 程序 因为 QT 必须有调用 QApplication 的 exec 方法,这样才能产生消息循环, QT 的程序才可以运行.所以说如果我们使用了 QT ...

  5. Windows 下创建目录链接 映射文件的方式不想改变路径直接使用映射路径的方式

    Windows 下创建目录链接 发表于 2019-10-27 | 分类于 后端 | 没有评论 在 Windows 下如果通过右键菜单->创建快捷方式生成的文件或文件夹,其实是生成了一个后辍为.l ...

  6. Windows下动态内存分配方式http://whx.tzgt.gov.cn/newOperate/html/7/71/711/3938.html

    这里的"动态内存"包含以下两个方面的内容:   1.内存.这里的"内存"指的是进程的虚拟内存空间.在Win32环境下,每一个进程拥有独立的,大小为4G(0x00 ...

  7. linux 生成dll文件,Linux和Windows平台 动态库.so和.dll文件的生成

    Linux动态库的生成 1. 纯cpp文件打包动态库 将所有cpp文件和所需要的头文件放在同一文件夹,然后执行下面命令 gcc -shared - fpic *.c -o xxx.so: g++ -s ...

  8. Windows下动态加载可执行代码原理简述

    xiaotie同学比较蛋疼,问C#里面能不能动态加载SIMD的汇编代码.C#我不知道,反正c/c++下面这事情很好做.顺手花了几个小时写了个例子和这篇博客. 总的来说,windows下要动态加载bin ...

  9. linux编写日志接口so,linux下动态链接问题(.so文件的编写与调用) .

    .o 就相当于windows里的obj文件 .a 是好多个.o合在一起,用于静态连接 .so 是shared object,用于动态连接的,和dll差不多 sotest.c #include int ...

最新文章

  1. 某程序员大牛放弃130万年薪,离开北京回老家事业单位!网友:太可惜!何不再忍两年?...
  2. 集合的get方法中参数从多少开始_JAVA从头开始一基础梳理(4-5-3)
  3. 慎重使用volatile关键字
  4. Google Guava官方教程
  5. python数据分析第三方库是_python数据分析复盘——数据分析相关库之Pandas
  6. 电脑亮度多少对眼睛好_鲁大师性能测试多少分算好电脑?
  7. 与时俱进:在JAX-RS API中采用OpenAPI v3.0.0
  8. 【Coursera】Getting Started with Python:Week One - Reading: Welcome to The Class
  9. css scale 缩放基准点
  10. linux切换sid,linux – 将sid字段添加到ps -f输出
  11. python获取网页源码被拒绝_Python3 请求网页源码 目标计算机积极拒绝,无法连接...
  12. JavaMail(2)——给多人发送、抄送
  13. Bresenham画线算法详解及其OpenGL编程实现
  14. MATLAB 创建不定长数组
  15. 78M05-ASEMI三端稳压管78M05
  16. SQL IF语句的使用
  17. 如何在Jsp页面加载时候就能执行某个方法
  18. 数据挖掘与数据分析项目链家租房数据(一)数据爬虫
  19. 转--Android 文件外/内部存储的获取各种存储目录路径
  20. 5G时代的应用场景及商业模式和财富商机

热门文章

  1. tkinter布局详解
  2. Linux中fork系统调用
  3. 数据结构期末复习(4.二叉树的遍历)
  4. 脑与认知神经科学Matlab Psytoolbox认知科学实验设计——实验设计二
  5. RK3368项目首次编译
  6. 宝塔面板windows建站教程_WindowsVPS-服务器安装宝塔面板建站教程宝塔面板,
  7. Windows版iCloud更改默认路径
  8. Mathematica 与数值计算
  9. 财务报表合并怎么快速完成
  10. 西安Android软件工程师,西安安卓软件工程师培训封闭班