热度 2
Windows平台下静态库与动态库的初探
程序编译一般需经预处理、编译、汇编和链接几个步骤。在程序设计中,有一些公共代码需要反复使用,通常会把这些代码编译为“库”文件。你可以简单的把“库”文件看成一种仓库,它提供一些可以直接拿来用的变量、函数或类。在仓库的发展史上经历了“无库-静态链接库-动态链接库”的时代。下面我们以VC6为例,初步学习一下Windows平台下静态库与动态库的创建和使用。
一、初识Windows下的库文件
在Windows下提供用户使用的静态库通常包括“.lib”和“.h”文件;动态库通常包括“.lib”文件(导入库)、“.h”文件和“.dll”文件。
静态库和动态库是两种共享程序代码的方式,它们的区别是:
1、在编译过程中,静态库中的代码会被连接器复制到程序中,程序运行时不再需要静态库的存在;动态库中的代码不会被复制到程序中,程序运行时由系统动态加载到内存中供程序调用,因此所所涉及的动态库是必须存在的。
2、静态链接库中不能再包含其他的动态链接库或者静态库,而在动态链接库中还可以再包含其他的动态或静态链接库。
静态库和动态库的优缺点:
1、静态库的优点是程序独立运行性好,程序运行时不再需要静态库的存在。缺点是可执行文件被多次执行时或使用相同静态库代码的不同程序同时运行时会有多份冗余拷贝,占用内存资源,且程序体积大。
2、动态库的优点是系统只需载入一次动态库,程序被多次执行时或使用相同动态库代码的不同程序同时运行时可以得到内存中相同的动态库的副本,节省内存资源,且程序体积小。缺点是程序独立运行性差,即程序的正常运行依赖所需要的动态库,这些动态库一个都不能少。
二、静态库的创建与使用
1、静态库的创建
在VC中静态库可以采用C语言或C++来编写,使用时略有区别。自己使用或提供给用户的静态库一般包括.lib和.h文件。
使用C语言或C++编写的静态库的过程如下:
①建立一个Win32 Static Library空项目,名字随意
②在Source Files和Head Files下分别建立myclib.c和myclib.h两个空文件(如果使用C++,则为mycpplib.cpp和mycpplib.h)。
③在myclib.h/mycpplib.h中输入以下内容(声明自定义函数原形):
int CAdd (int x,int y);//C++为CppAdd
int CSub (int x,int y);// C++为CppSub
设置此头文件,是为了方便将来使用。如果不设置此头文件,就必须专门做一个说明文档,记下函数原形,以便将来使用时在源文件中声明函数原形。
④在myclib.c/mycpplib.cpp中输入以下内容实现自定义函数:
#include "myclib.h" //C++为”mycpplib.h”
int CAdd (int x,int y) //C++为CppAdd
{
return x+y;
}
int CSub (int x,int y) //C++为CppSub
{
return x-y;
}
⑤在项目上点右键,然后选Setting,然后选Library项,在Output file name中,将已有内容改为myclib.lib(c++为mycpplib.lib)。
⑥编译生成。会在工程文件夹下找到myclib.lib和myclib.h(C++为mycpplib.lib和mycpplib.h)两个文件。
使用C语言创建的静态库
使用C++语言创建的静态库
2、静态库的使用
为简化操作采用,此部分采用Win32控制台程序程序示例。在工程中使用静态库分为3步:
<1>在工程中加入静态库(两种方法)
方法一、项目设置中引用“.lib”文件:
在project->setting-> link->object/library modules中添加所需“.lib”文件名,记住和前面已有的lib文件以空格隔开。
注意:这样设置必须在tools/options设置正确的引用路径。此种方法最好用于ide提供的静态库文件。
方法二、在项目中直接加入lib:
在project->add to project->files,选择正确的.lib。
建议使用。
方法三、在源文件中用代码的形式引入:
#pragma comment(lib,"***/***/***.lib"),系统会优先查找环境path。
建议使用。
<2>在工程中包括.h文件
注意:可能需要在tools/options设置正确的引用路径。此种方法最好用于ide提供的静态库文件。
对于自己的头文件,建议给出相对路径。如:#include "mylib/mylib.h"。
<3>在工程中使用静态库中的函数
①源文件和静态库均为C语言:
在工程文件夹中建立clib文件夹,复制myclib.h和myclib.lib到此文件夹下,新建一个Win32控制台项目,在项目头文件内导入myclib.h,在项目源文件内建立c_clib.c,并输入以下代码:
//c_clib.c
#include "stdio.h"
#include "clib/myclib.h"
#pragma comment(lib,"clib/myclib.lib")
int main()
{
int Add = CAdd(100,100);
int Sub = CSub(100,100);
printf("Add=%d Sub=%d \n",Add,Sub);
getchar();
return 1;
}
②源文件和静态库均为C++语言:
在工程文件夹中建立cpplib文件夹,复制mycpplib.h和mycpplib.lib到此文件夹下,新建一个Win32控制台项目,在项目头文件内导入mycpplib.h,在项目源文件内建立cpp_cpplib.cpp,并输入以下代码:
//cpp_cpplib.cpp
#include "stdio.h"
#include "cpplib/mycpplib.h"
#pragma comment(lib,"cpplib/mycpplib.lib")
int main()
{
int Add = CppAdd(100,100);
int Sub = CppSub(100,100);
printf("Add=%d Sub=%d \n",CppAdd,CppSub);
getchar();
return 1;
}
③源文件为C++,静态库为C语言编写
在工程文件夹中建立clib文件夹,复制myclib.h和myclib.lib到此文件夹下,新建一个Win32控制台项目,在项目头文件内导入myclib.h,在项目源文件内建立cpp_clib.c,并输入以下代码:
//cpp_clib.cpp
#include "stdio.h"
#include "clib/myclib.h"
#pragma comment(lib,"clib/myclib.lib")
int main()
{
int Add = CAdd(100,100);
int Sub = CSub(100,100);
printf("Add=%d Sub=%d \n",Add,Sub);
getchar();
return 1;
}
将myclib.h的内容修改为:
extern "C" int CAdd (int x,int y);
extern "C" int CSub (int x,int y);
含义是我现在导入的是C语言编写的静态库,编译器请按C语言格式编译。
④源文件为C语言,静态库为C++编写
通过上面的测试,可以发现:C语言和C++调用自己创建的静态库,直接在源文件中包含所提供的头文件(其中为函数原型定义)即可,不会有什么问题。但C++如果调用C语言编写的静态库,则需要修改头文件,增加提示标记extern "C"。这说明编译环境不同时,需要通知编译器按C的格式来调用。
由此可以想到,静态库为C++编写时,能不能也按C的格式来生成,这样不是C和C++都可以调用了吗?实现过程如下:
建立工程,名字为commonlib。建立项目,名字定为mylib。头文件中建立mylib.h文件,指定位置为"../lib/mylib.h",输入代码:
//mylib.h
#ifndef _MYLIB_H
#define _MYLIB_H
#ifdef __cplusplus
extern "C"
{
#endif
int myAdd (int x, int y);
int mySub (int x, int y);
#ifdef __cplusplus
}
#endif
#endif //_MYLIB_H
__cplusplus是C++编译器的保留宏定义.就是说C++编译器认为这个宏已经定义了。extern "C"是告诉C++编译器,括号里的东东按照C的obj文件格式编译,要连接的话按照C的命名规则去找。
源文件中建立mylib.cpp文件,输入代码:
//mylib.cpp
extern "C"
{
#include "../lib/mylib.h"
int myAdd (int x,int y)
{
return x+y;
}
int mySub (int x,int y)
{
return x-y;
}
}
(经测试,源文件中的extern “C” { }不加好像也可以)
在项目上点右键,Setting,Library,Output file name,“..\lib\mylib.lib”,设置lib文件的生成位置。编译,找到lib文件夹,可以看到下面会有mylib.h和mylib.lib两个文件,把这个文件夹复制到需要用的地方或直接来这里调用都可以。
接下来在工程中新建2个Win32控制台项目,名字随意,在项目头文件内都从lib文件夹下导入mylib.h,然后在项目源文件内分别建立Uselib.c和Uselib.cpp,并输入以下公用代码:
//Uselib.c和Uselib.cpp公用代码
#include "stdio.h"
#include "../lib/mylib.h"
#pragma comment(lib,"../lib/mylib.lib")
int main()
{
int Add = myAdd(100,100);
int Sub = mySub(100,100);
printf("Add=%d Sub=%d \n",Add,Sub);
getchar();
return 1;
}
总结:C++是C语言的升级版,C++可以调用C语言生成的东西(extern “C”方式),C++如果想生成的东西让C语言也能使用,就必须按C格式来声明和生成。
三、动态库的创建与使用
1、Dll基础知识
动态链接库技术是Windows最得要的实现技术之一,Windows的许多功能和特性都是通过DLL来实现的。其实,Windows本身就是由许多DLL组成的,它最基本的三大组成模块Kernel、GDI和User都是DLL。
一般来说,DLL是一种磁盘文件,以“.dll”、“.DRV”、“.FON”、“.SYS”和许多以“.EXE”为扩展名的系统文件都可以是DLL。它由全局数据、服务函数和资源组成,在运行时被系统加载到进程的虚拟空间中,成为调用进程的一部分。如果与其它DLL之间没有冲突,该文件通常映射到进程虚拟空间的同一地址上。
动态链接库一般包括“.dll”、“.lib(导入库)”、“.h”、文件。
动态库,目标库的主体,内部为函数代码、类等,扩展名“.dll”,发生在运行时。
导入库,扩展名“.lib”,目标库的一种特殊形式。导入库不含代码,而是为链接程序提供信息,提供在“.exe”文件中建立动态链接时要用到的重定位表。导入库是动态库的辅助库,在vc中隐式导入动态库的时候要用到该库,一般是在头文件或源文件中包含该库。一般来说,vb导入动态库时用显式导入(LoadLibrary),vc用隐式导入简单一些。导入库和静态库的区别很大,他们实质是不一样的东西。静态库本身就包含了实际执行代码、符号表等等,而对于导入库而言,其实际的执行代码位于动态库中,导入库只包含了地址符号表等,确保程序找到对应函数的一些基本地址信息。以“.lib”为扩展名的库有两种,一种为静态链接库(Static Libary),另一种为动态连接库(DLL)的导入库(Import Libary)。
2、Dll分类
微软的Visual C++支持三种DLL,它们分别是Non-MFC Dll(非MFC动态库)、Regular Dll(常规DLL)、Extension Dll(扩展DLL)。
<1>Non-MFC DLL(非MFC动态库):
这种动态链接库指的是不用MFC的类库结构,直接用C语言写的DLL,其导出的函数是标准的C接口,能被非MFC或MFC编写的应用程序所调用。如果建立的DLL不需要使用MFC,那么应该建立Non-MFC DLL,因为使用MFC会增大用户库的大小,从而浪费用户的磁盘和内存空间。
<2>Regular DLL(常规DLL):
这种动态链接库和下述的扩展Dll一样,是用MFC类库编写的。它的一个明显的特点是在源文件里有一个继承CWinApp的类(注意:此类DLL虽然从CWinApp派生,但没有消息循环),被导出的函数是C函数、C++类或者C++成员函数(注意不要把术语C++类与MFC的微软基础C++类相混淆),调用常规DLL的应用程序不必是MFC应用程序,只要是能调用类C函数的应用程序就可以。如Visual C++、Delphi、Visual Basic、Borland C等。常规DLL又可细分成静态链接到MFC和动态链接到MFC两种:
①静态连接到MFC的动态连接库
只被VC的专业般和企业版所支持。该类DLL里的输出函数可以被任意Win32程序使用,包括使用MFC的应用程序。输出函数有如下形式:
extern "C" EXPORT YourExportedFunction( );
如果没有extern "C"修饰,输出函数仅仅能从C++代码中调用。
②动态链接到MFC的动态连接库
该类DLL里的输出函数可以被任意 Win32 程序使用,包括使用MFC的应用程序。所有从DLL输出的函数应该以如下语句开始:
AFX_MANAGE_STATE(AfxGetStaticModuleState( ))
此语句用来正确地切换 MFC 模块状态。
<3>Extension Dll(扩展 DLL)
这种动态链接库是使用MFC的动态链接版本所创建的,并且它只被用MFC类库所编写的应用程序所调用。
例如你已经创建了一个从MFC的CtoolBar类的派生的类,用于创建一个新的工具栏。为了导出这个类,你必须把它放到一个MFC扩展的DLL中。扩展DLL和常规DLL不一样,它没有一个从CWinApp继承而来的类的对象,所以,开发人员必须在DLL中的DllMain函数添加初始化代码和结束代码。与常规DLL相比,扩展DLL有如下不同点:
①它没有一个从CWinApp派生的对象;
②它必须有一个DLLMain函数;
③DLLMain 调用AfxInitExtensionModule函数,必须检查该函数的返回值,如果返回0,DLLMmain也返回0;
④如果它希望输出CRuntimeClass类型的对象或者资源(Resources),则需要提供一个初始化函数来创建一个CDynLinkLibrary 对象。并且有必要把初始化函数输出;
⑤使用扩展DLL的MFC应用程序必须有一个从CWinApp 派生的类,而且一般在InitInstance里调用扩展DLL的初始化函数。
注意:VC6下没这种划分,只有以下选项,实际只有Non-MFC DLL(非MFC动态库)一种。
3、动态连接库的建立与使用
<1>隐式调用
方法一、_declspec(dllexport/dellimport)方式
①建立工程,名字testdllfunc。
②在此工程添加项目,类型为Win32 Dynamic-Link Library,名字mydll。项目设置为General(../dll,设置生成lib文件的位置),Link(../dll/mydll.dll,设置生成dll文件的位置)。
头文件,建立在上级目录的dll文件夹下,以便调用时方便,内容为:
//mydll.h
#ifndef _MYDLL_H //_MYDLL_H防止重复载入
#define _MYDLL_H
//dllexport与dllimport自动切换
//项目上点右键,Setting,C/C++
//Preprocesser中如无MYDLL_EXPORTS则加入
#ifdef MYDLL_EXPORTS
#define MYDLL _declspec(dllexport)
#else
#define MYDLL _declspec(dllimport)
#endif //dllexport或dllimport选择结束
#ifdef __cplusplus //c++格式检测
extern "C" {
#endif //c++格式检测结束
//导入导出函数
MYDLL int myAdd (int x,int y);
MYDLL int mySub (int x,int y);
//导入导出变量
#ifdef MYDLL_EXPORTS
_declspec(dllexport) int g_Value1=100;
_declspec(dllexport) int g_Value2=200;
#else//导入时如带值会报错
_declspec(dllimport) int g_Value1;
_declspec(dllimport) int g_Value2;
#endif
#ifdef __cplusplus //c++格式检测
}
#endif //c++格式检测结束
#endif //_MYDLL_H防止重复载入结束
源文件建立在项目文件夹下,内容为:
//mydll.cpp或mydll.c
#ifdef __cplusplus //c++格式检测
extern "C" {
#endif //c++格式检测结束
#include "../dll/mydll.h"
int myAdd (int x,int y)
{
return x+y;
}
int mySub (int x,int y)
{
return x-y;
}
#ifdef __cplusplus //c++格式检测
}
#endif //c++格式检测结束
③在工程testdlfunc下添加Win32控制台项目,名字Use_mydll,项目设置为Link(../dll/Use_mydll.exe,因为dll文件在这里,不然会报错)。
从上级目录下的dll文件夹内导入头文件mydll.h,源文件建立在项目文件夹下,内容为:
//Usemydll_c.c 或 Usemydll_cpp.cpp
#include "stdio.h"
#include "../dll/mydll.h"
#pragma comment(lib,"../dll/mydll.lib")
int main()
{
int nAdd = myAdd(100,100);
int nSub = mySub(100,100);
printf("g_Value1=%d\n",g_Value1);
printf("g_Value2=%d\n",g_Value2);
printf("myAdd=%d mySub=%d \n",nAdd,nSub);
getchar();
return 1;
}
方法二、DEF导出方式
和方法一方式相差不大,主要区别为导出采用DEF文件,头文件中不在需要dllexport和dllimport的判断
①建立工程,名字testdllfunc2。
②在此工程添加项目,类型为Win32 Dynamic-Link Library,名字mydll。项目设置为General(../dll,设置生成lib文件的位置),Link(../dll/mydll.dll,设置生成dll文件的位置)。
在源文件中分别建立mydll.cpp(或mydll.c)和mydll.def;在头文件中建立mydll.h,位置设置在上级目录的dll文件夹下,以便调用时方便(此头文件用于查看函数原型等)。
源文件mydll.cpp(或mydll.c)的内容:
//mydll.cpp//或mydll.c//
#ifdef __cplusplus //c++格式检测
extern "C" {
#endif //c++格式检测结束
int myAdd (int x,int y)
{
return x+y;
}
int mySub (int x,int y)
{
return x-y;
}
int g_Value1=100;
int g_Value2=200;
#ifdef __cplusplus //c++格式检测
}
#endif //c++格式检测结束
DEF文件mydll.def的内容为:
LIBRARY mydll.dll
EXPORTS
myAdd @1
mySub @2
g_Value1 @3 DATA
g_Value2 @4 DATA
头文件mydll.h的内容为:
//mydll.h
#ifndef _MYCDLL_H //_MYCDLL_H防止重复载入
#define _MYCDLL_H
#define MYDLL _declspec(dllimport)
#ifdef __cplusplus //c++格式检测
extern "C" {
#endif //c++格式检测结束
//导入函数
MYDLL int myAdd (int x,int y);
MYDLL int mySub (int x,int y);
//导入导出变量
extern _declspec(dllimport) int g_Value1;
extern _declspec(dllimport) int g_Value2;
#ifdef __cplusplus //c++格式检测
}
#endif //c++格式检测结束
#endif //_MYCDLL_H防止重复载入结束
③在工程testdlfunc2下添加Win32控制台项目,名字Use_mydll,项目设置为Link(../dll/Use_mydll.exe,因为dll文件在这里,不然会报错)。
从上级目录下的dll文件夹内导入头文件mydll.h,源文件建立在项目文件夹下,内容为:
//Usemydll_c.c 或 Usemydll_cpp.cpp
#include "stdio.h"
#include "../dll/mydll.h"
#pragma comment(lib,"../dll/mydll.lib")
int main()
{
int nAdd = myAdd(100,100);
int nSub = mySub(100,100);
printf("g_Value1=%d\n",g_Value1);
printf("g_Value2=%d\n",g_Value2);
printf("myAdd=%d mySub=%d \n",nAdd,nSub);
getchar();
return 1;
}
通过对比可以发现使用DEF文件做导出后,头文件简化了许多。其实def的功能相当于extern “C”__declspec(dllexport),所以它也仅能处理C函数,而不能处理重载函数。而__declspec(dllexport)和__declspec(dllimport)配合使用能够适应任何情况,因此__declspec(dllexport)是更为先进的方法。所以,目前普遍的看法是不使用def文件。
<2>显式调用
仅在需要时,载入所需DLL,程序占用内存小,启动快。
载入动态库,获取函数地址,使用函数,释放动态库。
假如没有头文件,没有lib文件,只有一个Dll文件,那么就只能显式调用了。可以先用命令行查看一下函数有哪些输出内容:dumpbin –exports mydll.dll。
①新建一个空白工程,名字随意,在此工程中加入一个空白Win32控制台项目,名字ApiCallDll。将前面工程中的dll文件夹复制到此工程下,dll文件夹下需要有mydll.h、mydll.lib和mydll.dll,其余文件删除。
②在项目点右键,Setting->Link->Output file name,输入“../dll/ApiCallDll.exe”,防止程序运行时找不到dll。
③设置源文件和头文件的内容
头文件从dll文件夹导入,源文件建立在项目文件夹下,名字为test.cpp或test.c,文件内容为:
//test.c或test.cpp
#include "stdio.h"
#include <Windows.h>
//定义函数指针
typedef int (*DLL_ADD)(int a,int b);
typedef int (*DLL_SUB)(int a,int b);
int * pg_Value1;
void usedll()
{
HINSTANCE hDll = LoadLibrary("../dll/mydll.dll");
if (hDll==NULL)
{
printf("动态链接库加载失败\n");
return;
}
//定义函数指针变量和变量指针
DLL_ADD pDlladd = NULL;
DLL_SUB pDllsub = NULL;
int * pg_Value1 = NULL;
int * pg_Value2 = NULL;
//获取函数地址
pDlladd=(DLL_ADD)GetProcAddress(hDll,"myAdd");
pDllsub=(DLL_SUB)GetProcAddress(hDll,"mySub");
pg_Value1=(int*)GetProcAddress(hDll,"g_Value1");
pg_Value2=(int*)GetProcAddress(hDll,"g_Value2");
//使用函数
int nAdd = pDlladd(100,100);
int nSub = pDllsub(100,100);
printf("nAdd=%d nSub=%d \n",nAdd,nSub);
//获取DLL中变量的值
printf("pg_Value1=%d\n",*pg_Value1);
printf("pg_Value2=%d\n",*pg_Value2);
//释放DLL
FreeLibrary(hDll);
getchar();
}
int main()
{
usedll();
return 1;
}
可以看出,显式调用DLL十分复杂,一般是大型工程才使用或VB等编程语言才使用。还是使用隐式调用方便一些,如果想制作可以供别的语言使用的DLL,COM编程是真正的解决方案。
<3>DLL中类的使用
类是C++特有的东西,因此DLL创建时,源文件必须为C++格式。
①建立空白工程,名字testdll_class。
②在此工程添加项目,类型为Win32 Dynamic-Link Library,名字dllclass。项目设置为General(../dll,设置生成lib文件的位置),Link(../dll/mydll.dll,设置生成dll文件的位置)。
③在源文件中建立mydll.cpp。在头文件中建立mydll.h,位置设置在上级目录的dll文件夹下,以便调用时方便。
mydll.h内容:
//mydll.h
#ifndef _MYDLL_H //_MYDLL_H防止重复载入
#define _MYDLL_H
//选择是用dllexport还是dllimport
//项目上点右键,Setting,C/C++
//Preprocesser中如无DLLCLASS_EXPORTS则加入
#ifdef DLLCLASS_EXPORTS
#define MYDLL _declspec(dllexport)
#else
#define MYDLL _declspec(dllimport)
#endif //dllexport或dllimport选择结束
//导入导出函数
MYDLL int myAdd (int x,int y);
MYDLL int mySub (int x,int y);
//导入导出变量
#ifdef DLLCLASS_EXPORTS
_declspec(dllexport) int g_Value1=100;
_declspec(dllexport) int g_Value2=200;
#else//导入时如带值会报错
extern _declspec(dllimport) int g_Value1;
extern _declspec(dllimport) int g_Value2;
#endif
//类的导入导出(C语言不能使用)
class MYDLL myClass
{
public:
int myMul(int x,int y);
int myDiv(int x,int y);
};
#endif //_MYDLL_H防止重复载入结束
mydll.cpp内容:
//mydll.cpp
#include "../dll/mydll.h"
int myAdd (int x,int y)
{
return x+y;
}
int mySub (int x,int y)
{
return x-y;
}
//类的导入导出(C语言不能使用)
int myClass::myMul(int x,int y)
{
return x*y;
}
int myClass::myDiv(int x,int y)
{
return x/y;
}
④在工程中添加项目,名字UseDllClass,头文件从上级目录的dll中导入,源文件内容为:
//UseDllClass.cpp
#include "stdio.h"
#include "../dll/mydll.h"
#pragma comment(lib,"../dll/mydll.lib")
int main()
{
int nAdd = myAdd(100,100);
int nSub = mySub(100,100);
printf("g_Value1=%d\n",g_Value1);
printf("g_Value2=%d\n",g_Value2);
printf("myAdd=%d mySub=%d \n",nAdd,nSub);
//类的使用
myClass myMulDiv;
int nDiv = myMulDiv.myDiv(10,5);
int nMul = myMulDiv.myMul(10,5);
printf("myDiv=%d myMul=%d \n",nDiv,nMul);
getchar();
return 1;
}
⑤设置文件生成路径为“../dll/UseDllClass.exe”,不然提示找不到DLL文件。设置方法,项目上点右键,Setting,Link,Output file name中输入“../dll/UseDllClass.exe”。
思考:经测试使用extern "C"模式后,函数和变量输出名字正常(用dumpbin –exports mdll.dll查看),类以及类内函数不正常,那么用Def导出效果会如何呢?百度了一下,相当麻烦,最简单的,还是上面的方法。那么DLL中的类能不能显式调用呢?应该是可以的,待测试。
<4>关于DLL的入口函数
DLL的入口函数,并非必需,如果没有,Windows系统会从其它进程中复制一个来用。
跟exe有个main或者WinMain入口函数一样,DLL也有一个入口函数,就是DllMain。以“DllMain”为关键字,来看看MSDN帮助文档怎么介绍这个函数的:The DllMain function is an optional method of entry into a dynamic-link library (DLL)。(翻译:DllMain函数是DLL文件的入口函数,它是可选的。)这句话很重要,很多初学者可能都认为一个动态链接库肯定要有DllMain函数。其实不然,像很多仅仅包含资源信息的DLL是没有DllMain函数的。
系统是在什么时候调用DllMain函数的呢?静态链接或动态链接时调用LoadLibrary和FreeLibrary都会调用DllMain函数。 DllMain的第二个参数fdwReason指明了系统调用Dll的原因,它可能是DLL_PROCESS_ATTACH、DLL_PROCESS_DETACH、DLL_THREAD_ATTACH和DLL_THREAD_DETACH。
以下从这四种情况来分析系统何时调用了DllMain。
①DLL_PROCESS_ATTACH
大家都知道,一个程序要调用Dll里的函数,首先要先把DLL文件映射到进程的地址空间。要把一个DLL文件映射到进程的地址空间,有两种方法:静态链接和动态链接(LoadLibrary或者LoadLibraryEx)。链接理解为调用也可以。
当一个DLL文件被映射到进程的地址空间时,系统调用该DLL的DllMain函数,传递的fdwReason参数为 DLL_PROCESS_ATTACH。这种调用只会发生在第一次映射时。如果同一个进程后来为已经映射进来的DLL再次调用LoadLibrary或者LoadLibraryEx,操作系统只会增加DLL的使用次数,它不会再用DLL_PROCESS_ATTACH调用DLL的DllMain函数。不同进程用LoadLibrary同一个DLL时,每个进程的第一次映射都会用DLL_PROCESS_ATTACH调用 DLL的DllMain函数。
②DLL_PROCESS_DETACH
当DLL被从进程的地址空间解除映射时,系统调用了它的DllMain,传递的fdwReason值是DLL_PROCESS_DETACH。当DLL处理该值时,它应该执行进程相关的清理工作。
那么什么时候DLL被从进程的地址空间解除映射呢?两种情况:
◆FreeLibrary解除DLL映射(有几个LoadLibrary,就要有几个FreeLibrary)
◆进程结束而解除DLL映射,当然是在进程结束前还没有这个解除DLL的映射的情况。(如果进程的终结是因为调用了TerminateProcess,系统就不会用 DLL_PROCESS_DETACH来调用DLL的DllMain函数。这就意味着DLL在进程结束前没有机会执行任何清理工作。)
注意:当用DLL_PROCESS_ATTACH调用DLL的DllMain函数时,如果返回FALSE,说明没有初始化成功,系统仍会用DLL_PROCESS_DETACH调用DLL的DllMain函数。因此,必须确保没有清理那些没有成功初始化的东西。
③DLL_THREAD_ATTACH
当进程创建线程时,系统查看当前映射到进程地址空间中的所有DLL文件映像,并用值DLL_THREAD_ATTACH调用DLL的DllMain函数。
新创建的线程负责执行这次的DLL的DllMain函数,只有当所有的DLL都处理完这一通知后,系统才允许线程开始执行它的线程函数。
注意跟DLL_PROCESS_ATTACH的区别:第n(n>=2)次以后地把DLL映像文件映射到进程的地址空间时,不再用 DLL_PROCESS_ATTACH调用DllMain,而是使用DLL_THREAD_ATTACH调用DllMain。进程中每次建立线程,都会用值 DLL_THREAD_ATTACH调用DllMain函数,哪怕是线程中建立线程也一样。
④DLL_THREAD_DETACH
如果线程调用了ExitThread来结束线程(线程函数返回时,系统也会自动调用ExitThread),系统查看当前映射到进程空间中的所有DLL文件映像,并用DLL_THREAD_DETACH来调用DllMain函数,通知所有的DLL去执行线程级的清理工作。
注意:如果线程的结束是因为系统中的一个线程调用了TerminateThread,系统就不会用值DLL_THREAD_DETACH来调用所有DLL的DllMain函数。
代码测试环节:
①新建一个空白工程,名字随意,如DllMainTest。然后建立2个空白项目,一个是Win32动态链接库,使用空项目,名字为mydll;一个是Win32控制台项目,使用空项目,名字DllMainTest。
②在mydll上点右键,Setting,General->Output files:中输入“../dll”,Link->选General->Output file name:中输入“../dll/mycdll.dll”。在项目工程文件夹下(mydll文件夹之外)建立一个dll文件夹,并在其中建立一个mycdll.h头文件,内容为:
//mycdll.h
#ifndef _MYCDLL_H //_MYCDLL_H防止重复载入
#define _MYCDLL_H
//选择是用dllexport还是dllimport
//项目上点右键,Setting,C/C++
//Preprocesser中如无MYDLL_EXPORTS则加入
#ifdef MYDLL_EXPORTS
#define MYDLL _declspec(dllexport)
#else
#define MYDLL _declspec(dllimport)
#endif //dllexport或dllimport选择结束
#ifdef __cplusplus //c++格式检测
extern "C" {
#endif //c++格式检测结束
MYDLL int myAdd (int x,int y);
MYDLL int mySub (int x,int y);
#ifdef __cplusplus //c++格式检测
}
#endif //c++格式检测结束
#endif //_MYCDLL_H防止重复载入结束
③在mydll文件夹下建立mydll.c源文件,内容:
//mycdll.c
#include <windows.h>
#include <stdio.h>
#include "../dll/mycdll.h"
BOOL WINAPI DllMain(
HINSTANCE hinstDLL,
DWORD fdwReason,
LPVOID lpvReserved )
{
switch (fdwReason)
{
case DLL_PROCESS_ATTACH://相当于LoadLibrary
printf("进程初始化完成\n");
break;
case DLL_THREAD_ATTACH:
printf("线程初始化完成\n");
break;
case DLL_THREAD_DETACH:
printf("线程卸载完成\n");
break;
case DLL_PROCESS_DETACH://相当于FreeLibrary
printf("进程卸载完成\n");
break;
}
printf("hinstDLL=%p \n", hinstDLL);
printf("fdwReason=%d\n", fdwReason);
return TRUE;
}
int myAdd (int x,int y)
{
return x+y;
}
int mySub (int x,int y)
{
return x-y;
}
/*
//此部分内容WinNT.h为头文件中所定义
#define DLL_PROCESS_ATTACH 1
#define DLL_THREAD_ATTACH 2
#define DLL_THREAD_DETACH 3
#define DLL_PROCESS_DETACH 0
*/
编译此项目。
④在DllMainTest项目上点右键,Setting,Link->选General->Output file name:中输入“../dll/DllMainTest.exe”,在DllMainTest项目文件夹下建立main.cpp,内容为:
//main.cpp
#include "stdio.h"
#include "../dll/mycdll.h"
#pragma comment(lib,"../dll/mycdll.lib")
int main()
{
int nAdd = myAdd(100,100);
int nSub = mySub(100,100);
printf("myAdd=%d mySub=%d \n",nAdd,nSub);
return 1;
}
头文件从上级目录的dll文件夹引入,名字与内容和mycdll.h头文件相同。
编译运行,可以看到如下显示内容:
进程初始化完成
hinstDLL=10000000 fdwReason=1
myAdd=200 mySub=0
进程卸载完成
hinstDLL=10000000 fdwReason=0
Press any key to continue
观察显示内容,可以发现,DllMain这个函数被调用了2次,一次为dll载入时,一次为dll卸载时。
小黑屋|手机版|Archiver|鱼C工作室 ( 粤ICP备18085999号-1 | 粤公网安备 44051102000585号)
GMT+8, 2024-3-29 02:55
Powered by Discuz! X3.4
© 2001-2023 Discuz! Team.