dll的两种加载方式(pend)+ delayload

看过关于动态库的调用例子,于是决定动手做一做:
dll的对外接口声明头文件,Mydll.h:

//Mydll.h
#include <stdio.h>
#include <stdlib.h>

#include "Public.h"

#define  DLL_EXPORT	/*extern "c"*/ __declspec(dllexport)	//导出
#define  CUST_API	_stdcall				//标准调用

DLL_EXPORT void CUST_API DisplayVersion(TCHAR *Info);	//显示版本

DLL_EXPORT int CUST_API Calc(int ia,int ib);

//DLL_EXPORT int CUST_API MetiCalc(int ia,int ib);	//新增加接口

//mydll.cpp
#include "MyDll.h"

void CUST_API DisplayVersion(TCHAR *Info)
{
	wcscpy_s(Info,sizeof(VERSION),VERSION);	//#define VERSION  ver 1.0
	return;
}

int CUST_API Calc(int ia,int ib)
{
	return ia+ib;
}

int CUST_API MetiCalc(int ia,int ib)
{
	return ia*ib;
}

编译后,生成DllTest.lib 和 DllTest.dll
第一种方法:静态调用
理解:lib描述dll信息和函数入口地址,在编译时期加载到可执行程序中的。
若dll增加新API接口,新接口在使用时,必须要同时更新lib 才能使用,否则会找不到新接口函数的地址,由此可见,lib包含了描述dll 的接口描述信息。

//dlltest.h
#include <iostream>
#include <Windows.h>
using namespace std;

#pragma comment(lib,"..\\ApDll\\DllTest.lib")		//加载lib库

#define  DLL_EXPORT	/*extern "c"*/ __declspec(dllexport)	//导出
#define  CUST_API	_stdcall								//标准调用

DLL_EXPORT void CUST_API DisplayVersion(TCHAR *Info);	//dll中显示版本函数
DLL_EXPORT int CUST_API Calc(int ia,int ib);
DLL_EXPORT int CUST_API MetiCalc(int ia,int ib);
int _tmain(int argc, _TCHAR* argv[])
{
	TCHAR Version[50] = {0};
	int	  a = 10,b=12;
	DisplayVersion(Version);
	wcout<<Version<<endl;
	wcout<<Calc(a,b)<<endl;
}
Result: ver 1.0
         120

第二种方法:动态加载
首先,要定义指向动态库中所对外提供的函数类型,的函数指针。
函数指针定义的理解:
typedef void(_stdcall *FunName)(paramtypes 1,paramtypes 2);//定义指向调用类型为_stdcall,参数个数,类型如paramtypes1,paramtypes 2,返回值为void类型的函数指针
这里注意,定义函数指针时,返回值 (*pName)(参数),3个部分;
然后,LoadLibrary(Path); Path为dll所在路径,可以是system目录,也可以其他指定目录。加载成功之后会返回一个Hmodel模块句柄。
再利用这个模块句柄去,获取相应函数的地址。
函数指针调用时,不同于普通的指针,它不需要间接寻址,“*”;
用完dll之后要记得ReleaseLibrary() ;

#include <iostream>
#include <Windows.h>
using namespace std;
typedef void (CUST_API *DisVer)(TCHAR *Info);
typedef int  (CUST_API *CalcOprt)(int ia,int ib);
int _tmain(int argc, _TCHAR* argv[])
{
	TCHAR Version[50] = {0};
	int	  a = 10,b=12;
	HMODULE hmodle = LoadLibrary(_T("..\\ApDll\\DllTest.dll"));	//动态加载dll
	if(NULL == hmodle)
		{
			wcout<<"load dll failed!"<<endl;
			return -1;
		}
	DisVer displayVer = (DisVer)::GetProcAddress(hmodle,"DisplayVersion");	//根据模块地址,按找函数名,获取函数地址
	DisplayVersion(Version);
try
{
	if (NULL == displayVer)
	{
		wcout<<_T("Load  function error!")<<endl;
	}

	(displayVer)(Version);	//用函数指针调用函数
	wcout<<Version<<endl;
}

catch (...)
{
	;
}

	system("pause");
	return 0;
}

看十,百遍,不如自己敲一遍,小小的动态库调用,也是有讲究的。



以下是delay load最基本的一些知识:

我们知道,dll的的载入有两种最基本的方法:隐式加载和显式加载。所谓隐式加载就是上一篇文章中介绍的方法,通过PE的输入表在进入入口函数之前将dll加载到内存空间。显式加载就是用LoadLibrary和GetProAddress的方法在需要的时候将dll加载到进程空间。这两种方法都是我们最常用的。那什么叫delay load呢?

被指定为delay load的dll只有当需要的时候才会真正载入进程空间,也就是说如果没有位于该dll中的函数被调用该dll将不被加载。而这个加载过程正是由LoadLibrary和GetProcAddress完成的,当然这一切对程序员是透明的。

这样做的好处是什么?毫无疑问,程序的启动速度快了,因为很多dll在启动的时候可能还没有使用到。甚至有些dll可能在整个生命周期中都未曾使用到,这样用delay load的话这个dll就不需占用任何内存。

那么如何使用delay load的呢?你需要做两件事情:

1. 在cpp的开头加上#pragma comment(lib, "DelayImp.lib"),如果你使用vs也可以在input里面加上这么一项。稍后会解释。

2. 在编译的时候加上:/link /DELAYLOAD:xxx.dll,如果使用vs只要在项目属性中找到delay load这一项,加上dll的名字。

(网上很多把#pragma comment(linker, "/DELAYLOAD:xxx.dll")加到cpp中,经证实,这种做法不可行。只能在命令行中作为参数使用)

现在让我利用现有的知识分析一下用delay load之后跟之前产生了什么变化:

1. 该dll相关的输入表肯定没了。毫无疑问,否则程序启动的时候还是会被无辜的加载。

2. 需要有什么字段记录dll的加载地址和函数的地址吧?否则每次调用都要LoadLibrary+GetProcAddress岂不是太不智能了?

3. 谁来调用LoadLibrary+GetProcAddress以及填充这些字段么?看来在链接的时候肯定要嵌入一下代码。那嵌入的代码哪里来?还记得之前的#pragma comment(lib, "DelayImp.lib")么?对了,就是这里来。

接下来我们就用一个最简单的例子来分析整个过程,让我再一次体会到了一个道理:作为程序员不学汇编真是寸步难行啊。在此之前我们再来回想一些PE结构中有什么跟delay load相关的东西么?对了!data directory中有一项IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT就是维护了所有跟delay load相关的信息。

我们先看一下对应的ImgDelayDescr结构体:

大小 成员 描述
DWORD    grAttrs 这个结构的属性。目前唯一被定义的旗标是dlattrRva,表明这个结构中的字段应该被认为是RVA,而不是虚地址
RVA rvaDLLName 指向一个被输入的DLL的名称的RVA。这个字符串被传递给LoadLibrary
RVA rvaHmod 指向一个HMODULE大小的内存位置的RVA。当延迟装入的DLL被装入内存后,它的模块句柄(hModule)被保存在这个地方
RVA rvaIAT 指向这个Dll的输入地址表的RVA,它与常规的IAT的格式相同
RVA rvaINT 指向这个DLL的输入名称表的RVA,它与常规的INT表格式相同
RVA rvaBoundIAT 可选的绑定IAT的RVA,指向这个DLL的输入地址表的绑定拷贝,它与常规的IAT表的格式相同,目前,这个IAT的拷贝并不是实际的绑定,但是这个特征可能会加到绑定程序的未来版本中
RVA rvaUnloadIAT 原始IAT的可选拷贝的RVA。它指向这个DLL的输入地址表的未绑定拷贝。它与常规的IAT表的格式相同,通常设为0
DWORD dwTimeStamp   延迟装入的输入DLL的时间/日期戳,通常设为0

看着是不是有点眼熟?对了,跟IMAGE_IMPORT_DESCRIPTOR有点像:都有dll name,INT,IAT,在继续往下看之前请确保对INT/IAT有基本的了解,没有还不是很清楚的话建议先看一下之前的一片文章。

到此为止,基本的知识介绍完毕,在给出我们的例子之前先介绍一下我使用的工具:ollydbg+stud_pe,ollydbg是一款强大的汇编级调试器,暂时属于摸索阶段,网上有不少教程,但是看懂这些教程本身需要一定的功力。stud_pe是一款很小的PE分析器,通过它可以很快的定位到PE的任意一部分,是初学PE的利器。接下来我的很多截图都源自这两个工具。

我们的例程尽量精简,又最好能覆盖所有的delay load知识:

当然您还需要一个delayLoad.dll,这个dll只需要导出两个函数export1+export2,函数的参数我们也省去了,加上不必要的参数只会增加汇编代码的复杂性,对我们的分析没有任何帮助。至于如何创建这个delayLoad.dll就不用我再具体说了吧,如果您还不会,建议你补补基础知识了哈~

编译+链接:cl sample.cpp  /link /DELAYLOAD:delayload.dll

开始研究汇编之前我们先看一下sample.exe中ImgDelayDescr现在是什么情况:

我们先看一下最重要的几项(如何通过上面的virtual address获得文件中对应的内容不再介绍,参见上一篇文章):

rvaDLLName: 64 65 6C 61 79 4C 6F 61 64 2E 64 6C 6C (delayload.dll)

rvaIAT: 34 10 40 00 19 10 40 00(按照正常推理:这两项将用于保存函数地址)

rvaINT: 72 7A 00 00 68 7A 00 00

72 7A 00 00:00 00 65 78 70 6F 72 74 31(..export1)

68 7A 00 00:00 00 65 78 70 6F 72 74 32(..export2)
rvaINT与import table中的INT用法完全一样:每一项都指向了一个IMAGE_IMPORT_BY_NAME,前两个字节表示Hint(具体用途请查阅PE结构,对本无无用),后面的直接表示函数名,用ASCII码表示。

rvaIAT与import table中的IAT有点不同,import table的IAT在程序加载之前跟INT指向相同的内容,而这里确不是。另外import table的IAT在加载之前获得所有导入函数的地址并更新,而rvaIAT则会在函数被调用到的时候进行更新。那么在程序加载之前rvaIAT中的值是什么意义呢?别急,马上就知道了。

接下来用ollydbg打开sample.exe,找到入口函数,开始我们的体力活T_T:

00031000   55                          PUSH EBP
00031001   8BEC                      MOV EBP,ESP
00031003   FF15 209B0300       CALL DWORD PTR DS:[39B20]     //export1
00031009   FF15 209B0300       CALL DWORD PTR DS:[39B20]     //export1
0003100F   FF15 249B0300        CALL DWORD PTR DS:[39B24]     //export2
00031015   33C0                      XOR EAX,EAX
00031017   5D                          POP EBP
00031018   C3                          RETN
00031019   B8 249B0300          MOV EAX,test.00039B24
0003101E   E9 00000000          JMP test.00031023
00031023   51                          PUSH ECX
00031024   52                          PUSH EDX
00031025   50                          PUSH EAX

00031026   68 1C7A0300          PUSH test.00037A1C
0003102B   E8 0E000000          CALL test.0003103E
00031030   5A                          POP EDX
00031031   59                          POP ECX
00031032   FFE0                      JMP EAX
00031034   B8 209B0300          MOV EAX,test.00039B20

00031039   E9 E5FFFFFF           JMP test.00031023

0003103E  // 先不关心这里的代码

我们以第一个export1的调用为例:CALL DWORD PTR DS:[39B20]

39B20是什么?30000是加载地址(exe不是会加载到400000上么?为什么用ollydbg调试的时候会加载到30000的位置?不解...经测试ollydbg每次exe的加载地址都会随机变化),那么我们需要的其实是9B20,往上看看rvaIAT的值正是9B20!也就是rvaIAT中的第一项。我们之前的疑问马上要解开了,我们看到rvaIAT中第一项对应的数据是401034(1034),也就是说这个调用其实就是CALL 31034:

00051034   B8 209B0300         MOV EAX,test.00039B20

00051039   E9 E5FFFFFF          JMP test.00031023

结合00031023中的代码我们已经可以推出如下结论:

1. 每一个rvaIAT项中保存了一个地址,该地址位于代码段中,由CALL DWORD PTR DS:[XXX]跳转进入到该代码段。(XXX是rvaIAT中某一项的地址)

2. 由CALL DWORD PTR DS:[XXX]跳转进入的代码段具有统一的格式:

1. 将该rvaIAT项的地址保存在EAX中。

2. 跳转到某一个地址(本程序中31023)

3. 在该地址中调用一个函数(本程序中3103E,代码暂时未给出,将在之后的介绍中详细分析),该函数将完成delay load的所有工作,并修改rvaIAT中的对应项使之拥有正确的函数地址。

4. 调用完该函数后JMP EAX,这个时候rvaIAT中对应的项已经有正确的函数地址了。

接下来我们重点研究3103E中的代码:

以上基本是针对第一次调用DLL中的函数的情况。注释已经写的很清楚了,虽然还有不少地方没有彻底搞清楚,但是核心的部分已经一目了然。正当我打算继续研究未明白的代码时,突然发现原来微软提供了这部分的源代码T_T: (delayhlp.cpp)

对比汇编,我们看看我们得到了什么新的信息:

1. RaiseException的最后一个参数指向了一个DelayLoadInfo. 可以在异常过滤器中获取相应的信息.

2. 之前一直很困惑我们的39B4C和终于知道什么用途了!是系统提供给我们的一个Hook:__pfnDliNotifyHook2,我们可以在自己的代码中定义这个函数,由系统在特定的时候调用。与此同时系统还提供了一个Hook:__pfnDliFailureHook2,在汇编代码中对应的是39B48. 这两个函数的用途将在后面介绍。

3. 还记得000311DC附近一系列的条件跳转么?这个是用来判断是否存在绑定信息的,如果一切正常的话就直接用绑定的地址,不需要GetProcessAdress了。为了确保这个绑定的地址还能正确使用,需要进行一系列的条件判断:

1. rvaBoundIAT & dwTimeStamp都不为0

2. IMAGE_NT_SIGNATURE & 时间戳一样 & 加载地址跟首选加载地址一致

那么绑定的加载地址哪里来?记得rvaBoundIAT么?这里分析汇编比cpp更简单: MOV EBX,DWORD PTR DS:[ECX+EAX],其中EAX是rvaBouldIAT的地址,ECX是偏移(该函数存放地址到rvaIAT首地址的偏移)

4. 如果DLL加载失败或者函数地址寻找失败,程序不会崩溃,而是会引发异常供开发者处理。

5. 如果该DLL可能会被unload(使用__FUnloadDelayLoadedDLL2),那么我们需要准备一些数据结构:new ULI(pidd);

到现在为止,基本上我们已经非常清楚delay load的工作原理的,那么再让我们思考一下当第二次调用export1时发生了什么事情呢?还是调用CALL DWORD PTR DS:[39B20],但是此时39B20已经存放了正确的export1地址,以后再使用到这个函数的话就可以直接使用了!

再从头回顾一下,还有没有什么内容没有介绍到:

1. __pfnDliNotifyHook2 & __pfnDliFailureHook2

2. unload...

3. 如何让绑定工作起来?显然在这个例子中没有绑定。

接下来的工作我们针对上面的三个方面一一介绍:

__pfnDliNotifyHook2 & __pfnDliFailureHook2

上面的例子再清楚不过,接下来我们结合__delayLoadHelper2的实现看看我们能为delayHook自定义什么行为:

1. dliStartProcessing: 如果在这里就获得了函数地址,直接跳到__delayLoadHelper2的最后。

2. dliNotePreLoadLibrary: LoadLibrary之前. 这个时候我们可以自己找到DLL的地址并返回,如果返回0,由__delayLoadHelper2调用LoadLibrary.

3. dliNotePreGetProcAddress: 在收到这个flag的时候我们可以自己获得函数地址. 如果返回0,则由__delayLoadHelper2负责.

4. dliNoteEndProcessing: 所有操作都结束了准备从__delayLoadHelper2返回。

__pfnDliFailureHook2的用法相似:

1. dliFailLoadLib: 当__delayLoadHelper2调用LoadLibrary出错的时候. 再这里我们可以继续尝试Load这个DLL或者做一些错误处理。

2. dliFailGetProc: 当__delayLoadHelper2调用GetProcAddress失败. 同理.

UNLOAD

默认情况下延迟加载的DLL不具备unload功能. 什么意思呢?

1. FreeLibrary无论如何不能用. 因为FreeLibrary不会清理函数地址. 当下一次调用该DLL中的函数的可以就会导致异常访问。

2. 既然不能FreeLibrary那也没办法unload的了。默认情况下就是这个样子的。

当然微软不会这样傻,你可以在delayhlp.cpp中找到一个名为__FUnloadDelayLoadedDLL2的函数,就是专门用来unload延迟加载的DLL的,但是要把它加到自己的程序中需要一个链接开关:/delay:unload. 如果没有设定这个开关,那么调用__FUnloadDelayLoadedDLL2什么也不会做。除此之外当然要加上#include<delayimp.h>&#include<windows.h>保证编译能够通过。

我们再来看一下__FUnloadDelayLoadedDLL2做了什么?

1. 遍历所有的ImgDelayDescr, 找到相同名字的DLL对应的ImgDelayDescr

2. 如果该ImgDelayDescr对应的rvaUnloadIAT不为0,那么将rvaUnloadIAT中的数据覆盖rvaIAT中的。

提出一个小问题:rvaUnloadIAT存放了什么?有兴趣的读者可以自己尝试一下,其实不需要尝试我们也应该可以想明白。因为unload之后我们还是可以调用该DLL中的函数进行延迟加载,那么覆盖之后的rvaIAT必须和初始时(从未调用过该DLL中的函数)rvaIAT中的数据一致。也就是说,在程序未加载之前,rvaUnloadIAT中存放了一份rvaIAT的拷贝。

绑定

关于绑定,还有一大堆的内容可以介绍。因为不是本文的重点,我们就简单的介绍一下:

我们知道,一般情况下,从一个dll导入一个函数的话这个函数的地址是在加载时获得并填入IAT中的。这样势必导致加载时间变长。绑定所要做的事情就是将这个工作提前。那么就有一个问题了,dll加载的地址是不定的,如何得到正确的函数地址呢?其实绑定有个前提条件,就是dll的加载地址一定要跟PE中定义的加载地址一致,绑定才会有效。否则,还是会在加载的时候通过INT重新获得函数名及函数地址。除此之外,还需要做一系列的判断,比如dll的时间戳,因为重新编译过后的dll地址可能都变了,之前的绑定也是无效的。

那么如何绑定呢?微软提供了一个名为bind的工具。用法如下BIND -u sample.exe delayLoad.dll

运行bind的命令后我们可以看到data directory中有一项变化了:IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT. 如果不是延迟加载的dll相信运行这个命令以后绑定就启作用了。可惜的是在这个例子中因为使用了delay load, 我尝试着使用BIND进行绑定,却没有成功(ImgDelayDescr中的时间戳没有改变,所以在比较时间戳的时候失败了,结果还是通过GetProcAddress取得了函数地址)。具体原因不知道,暂时先告一个段落。

说到加载地址,还有一个工具不得不提,就是rebase, rebase的作用就是调整dll的首选加载地址,使得每个dll都能加载到首选地址上,这样就达到了一定程度的优化。通常微软建议的做法是先运行rebase再运行bind,这样能保证bind后都是有效地。这里相关的内容还有不少,有兴趣的读者可以自己再找找资料研究一下,如果能再写个程序测试一下bind, rebase之后程序的加载时间加快了多少那就再好不过了:)

时间: 2024-10-10 10:00:36

dll的两种加载方式(pend)+ delayload的相关文章

UIImage的两种加载方式

UIImage的两种加载方式 1.有缓存:读取后放入缓存中下次可直接读取,适用于图片较少且频繁使用. [UIImage imageNamed:@"文件名"]: 在缓存中由系统管理,当收到memoryWarning时会释放这些内存资源. 2.无缓存:用完就释放掉,参数传的是全路径,适用于图片较多较大的情况下. NSString *path = [[NSBundlemainBundle] pathForResource: @"1.png"ofType: nil]; [U

Linux驱动的两种加载方式过程分析

一.概念简述 在Linux下可以通过两种方式加载驱动程序:静态加载和动态加载. 静态加载就是把驱动程序直接编译进内核,系统启动后可以直接调用.静态加载的缺点是调试起来比较麻烦,每次修改一个地方都要重新编译和下载内核,效率较低.若采用静态加载的驱动较多,会导致内核容量很大,浪费存储空间. 动态加载利用了Linux的module特性,可以在系统启动后用insmod命令添加模块(.ko),在不需要的时候用rmmod命令卸载模块,采用这种动态加载的方式便于驱动程序的调试,同时可以针对产品的功能需求,进行

Linux共享库两种加载方式简述

动态库技术通常能减少程序的大小,节省空间,提高效率,具有很高的灵活性,对于升级软件版本也更加容易.与静态库不同,动态库里面的函数不是执行程序本身 的一部分,而是在程序执行时按需载入,其执行代码可以同时在多个程序中共享.由于在编译过程中无法知道动态库函数的地址,所以需要在运行期间查找,这对程 序的性能会有影响. 共享库 对于共享库来讲,它只包括2个段:只读的代码段 和可修改的数据段.堆和栈段,只有进程才有.如果你在共享库的函数里,分配了一块内存,这段内存将被算在调用该函数的进程的堆中.代码段由于其

【Android进阶篇】Fragment的两种加载方式

一.概述 Fragment(碎片,片段)是在Android 3.0后才引入的,主要的目的是为了实现在大屏幕设备上的更加动态更加灵活的UI设计.这是因为平板电脑的屏幕比手机大得多,所以屏幕上可以放更多的组件,而不是简单地只是把手机上的组件放大.所以Fragment在应用中的是一个可重用的模块化组件,它有自己的布局.自己的生命周期,在一个Activity中可以包含多个Fragment. 二.在Activity中加载Fragment Fragment的加载方式包含两种:静态加载和动态加载.静态加载很简

ios 图片的两种加载方式

控件加载图片,plist,懒加载,序列帧动画,添加动画效果. IOS中有2种加载图片的方式. 方式一:有缓存(图片所占用的内存会一直停留在程序中) [objc] view plaincopy + (UIImage *)imageNamed:(NSString *)name; 注:必须是png文件.需要把图片添加到 images.xcassets中 例如: [objc] view plaincopy @property (weak, nonatomic) IBOutlet UIImageView 

Android动画Animation的两种加载执行方式

本文以简单的AlphaAnimation("淡入淡出(透明度改变)"动画)为例,简单的说明Android动画Animation的两种加载执行方法: (1) 直接写Java代码,制作Android动画. (2) 写XML配置文件,加载XML资源文件执行. 其实这两者是一致的.要知道,在Android中,凡是可以在XML文件完成的View,代码亦可完全写出来. 现在先给出一个Java代码完成的动画AlphaAnimation,AlphaAnimation功能简单,简言之,可以让一个View

链接库DLL的概念,加载方式的区别

使用LR进行基于windows socket协议做接口测试,只提供了lr_load_dll方法来动态加载动态链接库.之前学习阶段,对TinyXML的学习,使用的静态链接库,当时在程序调用的时候方法也跟LR里的不一样,那问题来了:lib和dll的区别是什么,每种链接库有多少种加载方式,怎么加载呢. 链接库可以向应用程序提供一些函数,变量和类.动态链接库的动态调用(也叫显式调用,手工加载)我是可以运用了,但是静态调用(也叫隐式调用,自动加载).静态链接库:lib中的函数不仅被连接,全部实现都被直接包

三种加载方式

重点总结:    即:三种加载方式    1>传统加载方式------默认路径:tomcat/bin/目录    2>使用ServletContext对象-----默认路径:web应用(工程)目录    3>使用类加载器------默认路径:WEB-INF/classes/目录 一.利用ServletContext对象读取资源文件--默认目录为:工程(应用)路径                重点方法:                        InputStream getReso

hive--udf函数(开发-4种加载方式)

h2 { color: #fff; background-color: #7CCD7C; padding: 3px; margin: 10px 0px } h3 { color: #fff; background-color: #008eb7; padding: 3px; margin: 10px 0px } UDF函数开发 标准函数(UDF):以一行数据中的一列或者多列数据作为参数然后返回解雇欧式一个值的函数,同样也可以返回一个复杂的对象,例如array,map,struct. 聚合函数(UD