动态链接库(VC_Win32)
目录
动态链接库概述
相关函数
动态链接库编程
dumpbin工具
(本章节中例子都是用 VS2005 编译调试的)
动态链接概述
说明
所谓动态链接,就是把一些经常会共用的代码(静态链接的OBJ程序库)制作成DLL档,当可执行文件调用到DLL档内的函数时,windows操作系统才会把DLL档加载存储器内,DLL档本身的结构就是可执行文件,当程序需求函数才进行链接.通过动态链接方式,存储器浪费的情形将可大幅降低.
DLL的文档格式与视窗EXE文档一样——也就是说,等同于32位视窗的可移植执行文档(PE)和16位视窗的New Executable(NE).作为EXE格式,DLL可以包括源代码、数据和资源的多种组合.
在使用动态库的时候,往往提供两个文件:一个引入库(LIB)和一个动态链接库(DLL).引入库(LIB)包含被动态连接库(DLL)所导出的函数和变量的符号名,动态连接库(DLL)包含实际的函数和数据.在编译链接可执行文件时,只需要链接引入库(LIB),动态连接库(DLL)中的函数代码和数据并不复制到可执行文件中,在运行的时候,再去加载DLL,访问动态链接库(DLL)中导出的函数.
动态链接库(DLL)通常都不能直接运行,也不能接收消息.它们是一些独立的文件,其中包含能被可执行程序或其它动态连接库(DLL)调用来完成某项工作的函数.只有在其它模块调用动态链接库(DLL)中的函数时,它才发挥作用.但是动态连接库(DLL)被多进程调用时候,动态链接库(DLL)中进程访问到动态链接库(DLL)的成员时,系统会为它开辟一个新的数据成员页面给访问进程提供单独的动态连接库(DLL)数据区.
特征(来自维基百科此处为链接)
- 内存管理
在Win32中,DLL文档按照片段(sections)进行组织.每个片段有它自己的属性,如可写或是只读、可执行(代码)或者不可执行(数据)等等.
DLL代码段通常被使用这个DLL的进程所共享;也就是说它们在物理内存中占据一个地方,并且不会出现在页面文档中.如果代码段所占据的物理内存被收回,它的内容就会被放弃,后面如果需要的话就直接从DLL文档重新加载.
与代码段不同,DLL的数据段通常是私有的;也就是说,每个使用DLL的进程都有自己的DLL数据副本.作为选择,数据段可以设置为共享,允许通过这个共享内存区域进行进程间通信.但是,因为用户权限不能应用到这个共享DLL内存,这将产生一个安全漏洞;也就是一个进程能够破坏共享数据,这将导致其它的共享进程异常.例如,一个使用访客账号的进程将可能通过这种方式破坏其它运行在特权账号的进程.这是在DLL中避免使用共享片段的一个重要原因.
当DLL被如UPX这样一个可执行的packer压缩时,它的所有代码段都标记为可以读写并且是非共享的.可以读写的代码段,类似于私有数据段,是每个进程私有的并且被页面文档备份.这样,压缩DLL将同时增加内存和磁盘空间消耗,所以共享DLL应当避免使用压缩DLL. - 符号解析和绑定
DLL输出的每个函数都由一个数字序号唯一标识,也可以由可选的名字标识.同样,DLL引入的函数也可以由序号或者名字标识.对于内部函数来说,只输出序号的情形很常见.对于大多数视窗API函数来说名字是不同视窗版本之间保留不变的;序号有可能会发生变化.这样,我们不能根据序号引用视窗API函数.
按照序号引用函数并不一定比按照名字引用函数性能更好:DLL输出表是按照名字排列的,所以对半查找可以用来在在这个表中根据名字查找这个函数.另外一方面,只有线性查找才可以用于根据序号查找函数.
将一个可执行文件绑定到一个特定版本的DLL也是可能的,这也就是说,可以在编译时解析输入函数(imported functions)的地址.对于绑定的输入函数,连结工具保存了输入函数绑定的DLL的时间戳和校验和.在运行时Windows检查是否正在使用同样版本的库,如果是的话,Windows将绕过处理输入函数;否则如果库与绑定的库不同,Windows将按照正常的方式处理输入函数.
绑定的可执行文件如果运行在与它们编译所用的环境一样,函数调用将会较快,如果是在一个不同的环境它们就等同于正常的调用,所以绑定输入函数没有任何的缺点.例如,所有的标准Windows应用程序都绑定到它们各自的Windows发布版本的系统DLL.将一个应用程序输入函数绑定到它的目的环境的好机会是在应用程序安装的过程. - 运行时显式链接
对每个DLL来说,Windows存储了一个全局计数器,每多一个进程使用便多额外一个.LoadLibrary与FreeLibrary指令影响每一个进程内含的计数器;动态链接则不影响.因此借由调用FreeLibrary多次,从存储器反加载一DLL是很重要的.一个进程可以从它自己的VAS注销此计数器.
DLL文档能够在运行时使用LoadLibrary(或者LoadLibraryEx)API函数进行显式调用,这个的过程微软简单地称为运行时动态调用.API函数GetProcAddress根据查找输出名称符号、FreeLibrary卸载DLL.这些函数类似于POSIX标准API中的dlopen、dlsym、和dlclose.
注意微软简单称为运行时动态链接的运行时隐式链接,如果不能找到链接的DLL文档,Windows将提示一个错误消息并且调用应用程序失败.应用程序开发人员不能通过编译链接来处理这种缺少DLL文档的隐式链接问题.另外一方面,对于显式链接,开发人员有机会提供一个完善的出错处理机制.
运行时显式链接的过程在所有语言中都是相同的,因为它依赖于Windows API而不是语言结构.
与静态链接库的区别
静态库本身就包含了实际执行代码、符号表等等,而对于导入库而言,其实际的执行代码位于动态库中,动态链接库只包含了地址符号表等,确保程序找到对应函数的一些基本地址信息.
Windows 下 3 个重要的 DLL
Windows API中的所有函数都包含在DLL中。其中有3个最重要的DLL
- Kernel32.dll,它包含用于管理内存、进程和线程的各个函数
- User32.dll,它包含用于执行用户界面任务(如窗口的创建和消息的传送)的各个函数
- GDI32.dll,它包含用于画图和显示文本的各个函数
动态链接库的优点
- 可以采用多种编程语言来编写
我们可以采用自己熟悉的开发语言编写DLL,然后由其他语言编写的可执行程序来调用这些DLL.例如,可以利用VB来编写程序界面,然后利用VC++或Delphi编写的完成程序作业逻辑的DLL
- 增强产品的功能
在发布产品时,可以发布产品功能实现的动态链接库规范,让其他公司或个人遵照这种规范开发自己的DLL,以取代产品原有的DLL.让产品调用新的DLL,从而实现功能的增强,在实际工作中,我们看到许多产品都提供了界面插件功能,允许用户动态地更换程序的界面,这就可以通过更换界面DLL来实现
- 提供二次开发的平台
在销售产品时,可以采用DLL的形式提供一个二次开发的平台,让用户可以利用该DLL调用其中实现的功能,编写符合自己业务需要的产品,从而实现二次开发
- 简化项目管理
在一个大型项目开发中,通常都是由多个项目小组同时开发,如果采用串行开发,则效率非常低的,我们可以将项目细分,将不同功能交由各个项目小组以多个DLL方式实现,这样各个项目小组就可以同时进行开发了
- 可以节省磁盘空间和内存
如果多个应用程序需要访问同样的功能,那么可以将该功能以DLL的形式提供,这样在机器上只需要存在一份该DLL文件就可以了,从而节省了磁盘空间.另外如果多个应用程序使用同一个DLL,该DLL的页面只需要放入内存一次,所有的应用程序就都可以共享它的页面了.这样,内存的使用将更加有效
如下图所示就是一个动态链接库被两个进程调用时的内存示意图,当进程被加载时,系统为它分配4GB的地址空间,接着分析该可执行模块,找到该程序要调用那些DLL模块,然后系统搜索这些DLL,找到后就加载它们,并为它们分配虚拟的内存空间,最后将DLL的页面映射到进程的地址空间.,从此可以导致多个程序中共享DLL的同一份代码,这样就可以节省空间 - 有助于资源的共享
DLL可以包含对话框模板,字符串,图标和位图等多种资源,多个应用程序可以使用DLL来共享这些资源.在实际工作中,可以写一个纯资源的动态链接库文件,供其他应用程序访问
- 有助于实现应用程序的本地化
如果产品需要提供多语言版本,那么就可以使用DLL来支持多语言,可以为每种语言创建一个只支持这种语言的动态链接库
相关函数
[动态链接库入口函数原型][加载动态链接库][获得动态链接库的函数地址][释放动态链接库链接][获得动态连接库句柄]
(在加载动态连接库时候会自动被调用,作用如控制台的main函数,窗体程序的WinMain函数)
函数原型
BOOL WINAPI DllMain(HINSTANCE hinstDLL,DWORD fdwReason,LPVOID lpvReserved);
参数说明
- hinstDLL
动态链接库模块句柄.当DLL初次被加载时,它的句柄会通过此参数传递进来,就好像WinMain函数有一个当前实例句柄参数一样,因此,在编写DLL程序时,如果这些函数需要用到当前的DLL模块句柄,那么就可以为该DLL提供DllMain函数,然后将通过参数hinstDLL传递进来的模块句柄保存到一个全局变量中,供其他函数使用
- fdwReason
一个标记值,用来调用该DLL入口函数的原因.该参数的取值是下列值之一
值 说明
DLL_PROCESS_ATTACH 当进程第一次加载DLL并调用DllMain函数
DLL_THREAD_ATTACH 当前进程正创建一个新线程
DLL_THREAD_DETACH 线程结束
DLL_PROCESS_DETACH 线程结束 - lpvReserved
保留参数.不需关心此参数,但可以检测这个指针,如果DLL被动态加载,则此参数为NULL,如果是静态加载,则此参数非NULL值
说明
如果提供了DllMain函数,那么在此函数中不要进行太复杂的调用.因为在动态链接库时,可能还有一些核心动态链接库没有加载.例如,我们自己编写的某个DLL被加载时,user32.dll或GDI32.dll这两个核心动态链接库还没有被加载.前面的内容已经介绍了,程序在运行时,加载程序会依次加载该进程需要的dll,而我们自己编写的DLL可能会比较靠前地被加载,如果自己编写的DllMain函数需要调用这些核心的DLL中的某个函数的话,这时就会失败,从而导致程序终止
函数原型
HMODULE LoadLibrary(LPCTSTR lpFileName);
参数说明
- lpFileName: 一个字符串类型参数,该参数指定了可执行模块的名称,既可以是一个.dll文件,也可以是一个.exe文件.
返回值
如果调用成函数返回所加载的那个模块的句柄.该函数的返回类型是HMODULE(和HINSTANCE类型可以通用)
说明
该函数不仅可以加载DLL(.dll),还可以加载可执行模块(.exe),一般来说,当可加载可执行模块时,主要是为了访问该模块内的一些资源,例如对话框资源,位图资源或图标资源等.
函数原型
FARPROC GetProcAddress(HMODULE hModule,LPCSTR lpProcName);
参数说明
- hModule: 指定动态链接库模块的句柄,即LoadLibrary函数的返回值
- lpProcName: 一个指向常量的字符指针(必须是改编后C++函数的名字),指定DLL导出函数的名字或者函数的序号(函数名和函数序号可以由dumpbin工具去查看),这里应该注意,如果参数指定的是导出函数的序号,那么序号必须在低字节中,高字节必须是0(可以用MAKEEINTRESOURCE宏,该宏会吧指定的函数序列号转换为相应的函数名字字符串)
返回值
成功返回函数的函数地址,失败的话返回NULL
函数原型
BOOL FreeLibrary( HMODULE hLibModule);
参数说明
- hLibModule: 指向连接库的句柄
函数原型
HMODULE GetModuleHandle ( LPCTSTR lpModuleName);
参数说明
- plModuleName: 一个指字符串用于表示模块名字,一个动态连接库(name.dll)或者执行文件的名字(name.exe),若没有加后缀默认是动态连接库即系统会在帮你加上后缀 .dll .
返回值
若函数成功返回对应句柄,若失败返回空
编写动态连接库
本程序使用的编译环境是VS2005,如果是使用VC6.0环境的可以去网上找孙鑫关于VC的视频,视频第19讲讲的就是动态连接库编写.
[编写动态链接库][加载动态链接库][C++命名改编][调用约定]
编写动态链接库
建立DLL项目
- Win32 -> Win32项目 -> DLL(D)
- MFC -> MFC DLL
生成导出函数,类,成员函数
- 生成导出函数: 在函数前添加_declspec(dllexport)[导出标示符]生成导出函数,
- 生成导出类: 在class后类名前添加_declspec(dllexport)[[导出标示符]这样就可以导出整个类,但是访问类的函数时候,仍然受限于函数自身的范围权限.也就是说,如果该函数访问权限是private或protect的话,那么外部程序仍然无法访问这个函数
- 生成导出成员函数: 在成员函数的返回类型后在函数的函数名前面加_declspec(dllexport)
代码示例(例子链接)
加载动态连接库
隐式链接方式加载动态库(例子链接)
步骤:
- 加载dll.lib文件
- VC6.0: 点击 Project/Settings后对话框下Link的Object/library modules下添加dll的lib文件
- VS/VC6.0: 在文件中利用#pragma comment(lib,"链接库地址")
- 加载dll.dll文件
把dll文件放在下面路径的一种中- 程序目录
- 当前目录
- 系统目录
- path环境变量中列出的路径中
- 利用extern / _declspec(dllimport)[导出标示符]声明动态链接库的函数
- _declspec(dllimport) 可以调用dll中非导出函数[没有_declspec(dllexport)的函数]
- extern 只能调用dll中导出函数[有_declspec(dllexport)的函数]
流程图:
显示加载方式加载DLL(例子链接)
步骤:
- 将最新的dll文件复制到以下路径中
- 程序目录
- 当前目录
- 系统目录
- path环境变量中列出的路径中
- 调用LoadLibrary,加载动态库
- 声明需要的动态链接的函数指针类型(此处可以要可以不要,这样只是方便以后定义相关函数指针)
//例如下面要调用动态链接库中的 int max_dll(int a,int b) 函数 //所以需要定义相关类型的指针声明 typedef int (/*_stdcall*/ *INTMAX)(int a,int b); //以后就可以用INTMAX来定义相关指针如 INTMAX pMaxInt;
- 获得函数地址GetProcAddress
- 调用FreeLibrary释放动态链接库
流程图:
两种加载方式的比较
动态加载和隐式链接这两种加载DLL的方式各有优点.如果采用动态加载的方式,那么可以在需要加载时才加载DLL.而隐式链接方式实现起来比较简单,在编写客户端代码时就可以把链接工作做好,在程序中可以随时调用DLL导出的函数,但是访问十多个DLL,如果都采用隐式链接的方式链接加载他们的话,那么在启动程序时候,这些DLL都需要加载到内存中,并映射到调用进程的地址空间,这样将加大启动程序的时间,而且,一般来说,在程序运行过程中只是在某个条件满足时候,这时才会访问某个DLL中的某个函数,其他情况下都不需要访问这些DLL,但是,这时所有的DLL都被已经加载到内存中,资源浪费会比较严重,这种情况下,就可以采用动态加载DLL技术,也就是说,在需要时,DLL才会被加载到内存中,并被映射到进程的地址空间中,有一点需要说明的是,实际上,采用隐式链接的方式访问DLL时,在启动时也是通过调用LoadLibrary函数加载到该进程需要的动态链接库的
代码示例
动态连接库源程序代码
程序源码
// .h 头文件 ----------------------------------------------------- #ifndef DLL_API #define DLL_API _declspec(dllimport) #endif DLL_API int max_dll(int a,int b); DLL_API double max_dll(double a,double b); class DLL_API testClass{ public: testClass(); int getValue(); private: int value; }; // .cpp 源程序 --------------------------------------------------- #define DLL_API _declspec(dllexport) #include "test.h" int max_dll(int a,int b){ return a>b?a:b; } double max_dll(double a,double b){ return a>b?a:b; } testClass::testClass():value(100) {} int testClass::getValue() { return value; }
隐式调用动态链接
程序源码
#include<iostream> #include<cstdlib> //加载动态连接库头文件 #include"../dll/test.h" //加载动态连接库的引入库(LIB) #pragma comment(lib, "../release/dll.lib") using namespace std; void main(){ int a=6,b=10; double a1=11.1,b1=32.22; //调用动态连接库中的类 testClass c; //调用动态链接库函数 cout<<"the max:"<<max_dll(a,b)<<endl; cout<<"the max:"<<max_dll(a1,b1)<<endl; //调用动态链接库成员函数 cout<<"the c.value is "<<c.getValue()<<endl; system("pause"); }
运行结果
显示调用动态链接库
程序源码
#include"windows.h" #include<iostream> #include<cstdlib> //加载动态连接库头文件 #include"../dll/test.h" using namespace std; //声明函数指针类型 typedef int (/*_stdcall*/ *MAXINT)(int a,int b); typedef double (/*_stdcall*/ *MAXDOUBLE)(double a,double b); void main(){ int a=6,b=10; double a1=11.1,b1=32.22; HINSTANCE hInst; hInst=LoadLibrary("../release/dll.dll"); //动态加载动态链接库中的函数 int max_dll(int a,int b) //MAXINT max_int=(MAXINT)GetProcAddress(hInst,"?max_dll@@YAHHHH@Z");//用函数名调用 //获取函数指针 MAXINT max_int=(MAXINT)GetProcAddress(hInst,MAKEINTRESOURCE(4));//用函数序号调用 if(!max_int) { cout<<"获取max_int函数地址失败!"<<endl; system("pause"); return; } //动态加载动态链接库中的函数 double max_dll(double a,double b) //获取函数指针 MAXDOUBLE max_double=(MAXDOUBLE)GetProcAddress(hInst,"?max_dll@@YANNN@Z");//用函数名调用 //MAXDOUBLE max_double=(MAXDOUBLE)GetProcAddress(hInst,MAKEINTRESOURCE(5));//用函数序号调用 if(!max_double) { cout<<"获取max_double函数地址失败!"<<endl; system("pause"); return; } //调用动态链接库函数 cout<<"the max:"<<max_int(a,b)<<endl; cout<<"the max:"<<max_double(a1,b1)<<endl; //释放动态连接库链接 FreeLibrary(hInst); system("pause"); }
运行结果
dumpbin 工具
概述
用途
查看dll与exe相关导入导出函数信息
dumpbin程序的文件位置
- VC6.0: VC98 \ bin
- VS2005: Microsoft Visual Studio 8\VC\bin
相关参数
- -exports 文件名.dll (查看导出函数和导出类)
- -imports 文件名.exe (查看导入函数)
设置VC++使用环境信息
VCVAR32.bat 建立VC++使用环境信息,但是注意当在命令行界面下执行VCVARS32.bat文件后,该文件所设置的环境信息只是在当前命令行窗口生效.如果关闭该窗口,再次启动一个新的命令行窗口后,仍需要运行VCVAR32.bat文件
使用步骤
- 以上面的DLL源程序为例子,做个使用说明,先打开控制台(如下图)
- 然后,把建立VC++使用环境信息,把VCVAR32.bat(批处理的位置和dumpbin一样)拖到控制台界面中(如下图).
- 然后回车,便建立好了VC++使用环境信息(如下图).这样就可以是用 dumpbin 这个命名了.
- 然后切换到工程目录中的 debug 或 release 中去.(如下图)
- 接着用 dumpbin 查看工程生成的 dll.dll 动态链接库的导出函数(如下图)(注意:这个函数必须有_declspec(dllexport)修饰,否则在这里是看不到).
流程图如下:
C++名字改编(C++名字粉碎)
在上面使用 dumpbin 程序查看 dll.dll 的导出函数发现函数名有点奇怪,我们定义的函数名max_dll两个重载函数名在这里变成了 ?max_dll@@YAHHH@Z 与 ?max_dll@@YANNN@Z,因为C++支持函数重载,对于重载的多个函数来说,其函数名都是一样的,为了加以区分,在编译连接时,C++会按照自己的规则篡改函数名字,这一过程为"名字改编".有的书中也称为"名字粉碎".不同的C++编译器会采用不同的规则进行名字改编,这个的话,利用不同的C++编译器生成的程序在调用对方提供的函数时,可能会出现问题
解决名字改变问题
第一种
- 声明:
- 在定义导出函数时,需要加上限定符: extern"C" (双引号中的C一定要大写)
- 注意:
- 使动态链接库文件在链接时,导出函数的函数名称不发生改变.
- 利用 extern "C" 可以解决 C++ 和 C 语言之间相互调用时函数命名的问题.但是这种方法有一个缺陷,就是不能用于导出一个类成员函数,只能导出全局函数这种情况
- 如果导出函数的调用约定发生了改变,那么即使使用了限定符: extern "C" ,该函数的名字仍然会发生改编.
- 记得导出和导入都要加入 extern "C" 否则在调用动态链接库时候会发生找不到函数这个现象.
代码样例:
利用 extern"C" 解决名字改编
动态连接库程序源码
// .h 头文件 --------------------------------------------------------- #ifndef DLL_API #define DLL_API extern "C" _declspec(dllimport) #endif DLL_API int max_dll(int a,int b); // .cpp 源文件 ------------------------------------------------------- #define DLL_API extern "C"_declspec(dllexport) #include "test.h" int max_dll(int a,int b){ return a>b?a:b; }
主程序源码
#include<iostream> #include<cstdlib> //加载动态连接库头文件 #include"../dll/test.h" //加载动态连接库的引入库(LIB) #pragma comment(lib, "../release/dll.lib") using namespace std; void main(){ int a=6,b=10; //调用动态链接库函数 cout<<"the max:"<<max_dll(a,b)<<endl; system("pause"); }
利用 dumpbin 查看命名
调用约定
四种调用方式:
__cdecl
__cdecl调用约定又称为 C 调用约定,是 C/C++ 语言缺省的调用约定。参数按照从右至左的方式入栈,函数本身不清理栈,此工作由调用者负责,返回值在EAX中。由于由调用者清理栈,所以允许可变参数函数存在,如int sprintf(char* buffer,const char* format,...);。
__stdcall
__stdcall 很多时候被称为 pascal 调用约定。pascal 语言是早期很常见的一种教学用计算机程序设计语言,其语法严谨。参数按照从右至左的方式入栈,函数自身清理堆栈,返回值在EAX中。
__fastcall
顾名思义,__fastcall 的特点就是快,因为它通过 CPU 寄存器来传递参数。他用 ECX 和 EDX 传送前两个双字(DWORD)或更小的参数,剩下的参数按照从右至左的方式入栈,函数自身清理堆栈,返回值在 EAX 中。
__thiscall
这是 C++ 语言特有的一种调用方式,用于类成员函数的调用约定。如果参数确定,this 指针存放于 ECX 寄存器,函数自身清理堆栈;如果参数不确定,this指针在所有参数入栈后再入栈,调用者清理栈。__thiscall 不是关键字,程序员不能使用。参数按照从右至左的方式入栈。
相关链接:
代码示例(编译环境VS2005):
使用 extern "C" 时
动态连接库源码:
// .h 头文件---------------------------------------------------------- #ifndef DLL_API #define DLL_API extern "C" _declspec(dllimport) #endif DLL_API int __stdcall stdcall_max_dll(int a,int b); DLL_API int __cdecl cdecl_max_dll(int a,int b); DLL_API int __fastcall fastcall_std_max_dll(int a,int b); // c.pp 源文件-------------------------------------------------------- #define DLL_API extern "C" _declspec(dllexport) #include "test.h" int __stdcall stdcall_max_dll(int a,int b){ return a>b?a:b; } int __cdecl cdecl_max_dll(int a,int b){ return a>b?a:b; } int __fastcall fastcall_std_max_dll(int a,int b){ return a>b?a:b; }
用 dumpbin 查看导出函数:
未使用 extern "C" 时
动态连接库源码:
// .h 头文件 -------------------------------------------------------------- #ifndef DLL_API #define DLL_API /*extern "C"*/ _declspec(dllimport) #endif DLL_API int __stdcall stdcall_max_dll(int a,int b); DLL_API int __cdecl cdecl_max_dll(int a,int b); DLL_API int __fastcall fastcall_std_max_dll(int a,int b); // .cpp 源文件 ----------------------------------------------------------- #define DLL_API /*extern "C"*/ _declspec(dllexport) #include "test.h" int __stdcall stdcall_max_dll(int a,int b){ return a>b?a:b; } int __cdecl cdecl_max_dll(int a,int b){ return a>b?a:b; } int __fastcall fastcall_std_max_dll(int a,int b){ return a>b?a:b; }
用 dumpbin 查看导出函数: