动态链接库(DLL)
DLL(Dynamic Link Library)也就是动态链接库,是一个可以被其他应用程序调用的程序模块,其中封装了可以被调用的资源或函数。DLL文件属于可执行文件,它符合Windows系统的PE文件格式,它依附于EXE文件创建的进程来执行,不能单独运行。一个DLL文件可以被多个进程所装载调用。
一、DllMain( )函数
DLL程序的入口函数为DllMain( )函数。使用VC++创建DLL程序,系统默认会生成如下代码
1 BOOL APIENTRY DllMain( HMODULE hModule,//指向DLL本身的实例句柄; 2 DWORD ul_reason_for_call,//指明了DLL被调用的原因; 3 LPVOID lpReserved //保留值,暂时没有意义; 4 ) 5 { 6 switch (ul_reason_for_call) 7 { 8 case DLL_PROCESS_ATTACH: 9 case DLL_THREAD_ATTACH: 10 case DLL_THREAD_DETACH: 11 case DLL_PROCESS_DETACH: 12 break; 13 } 14 return TRUE; 15 }
但在使用过程中,DllMain( )函数不是必须由我们提供、定义的, 这是因为Windows在找不到DllMain的时候,系统会从其它运行库中引入一个不做任何操作的缺省DllMain函数版本。
根据编写规范,Windows必须查找并执行DLL里的DllMain函数作为加载DLL的依据,它使得DLL得以保留在内存里。DllMain( )函数并不属于导出函数,而是DLL的内部函数。这意味着不能直接在应用工程中引用DllMain函数,它是自动被调用的。不要将DllMain写成DLLMain。
二、DLL函数的导出
方式1:关键词_declspec(dllexport) 导出方式
首先添加一个用于导出函数的头文件,在头文件中放置需要导出的函数声明。
在使用C语言编写DLL函数时,在函数的声明前加上_declspec(dllexport) 链接器便会在生成的DLL文件中嵌入一个导出符号表。使用Depends工具可以查看到。使用C++编写DLL时,则要在函数声明前加上extern "C" _declspec(dllexport),可避免编译器对函数名进行改编。
注意!当C/C++编写的函数使用了__stdcall也就是WINAPI调用约定时,即使使用的是extern "C" _declspec(dllexport),Microsoft的编译器还是会对函数名进行改编。改编的方法是给函数名添加下划线前缀和一个特殊的后缀,该后缀由一个@符号后跟作为参数传给函数的字节数组成。
extern "C" _declspec(dllexport) LONG __stdcall MyFunc(int a, int b);//该函数在DLL的导出段中被导出为_MyFunc@8。
如果想要导出未经改编的函数名,可在DLL的源文件中加入如下代码:
#pragma comment(linker, "/export:MyFunc=_MyFunc@8")
使用这种方法时,DLL实际上导出了两个函数MyFunc和_MyFunc@8 ,而使用下面介绍的Def文件导出方法则不会存在这个问题。
方式2:Def文件导出
■文件中的第一个语句必须是 LIBRARY 语句。此语句将 .def 文件标识为属于 DLL。LIBRARY 语句的后面是 DLL 的名称。链接器将此名称放到 DLL 的导入库中。
■EXPORTS 语句列出名称,可能的话还会列出 DLL 导出函数的序号值。通过在函数名的后面加上 @ 符和一个数字,给函数分配序号值。当指定序号值时,序号值的范围必须是从 1 到 N,其中 N 是 DLL 导出函数的个数。
■注释语句,在语句前面加分号 “;” 。
例如:
;DLLTest.def : Declares the module parameters for the DLL. LIBRARY "DLLTest" EXPORTS add @1 fun @2
三、对DLL程序的调用
对DLL的调用分为两种,静态调用和动态调用。
我们可以首先创建一个名为MyDLL的DLL工程,其中添加DLLShow函数。
1 #include <Windows.h> 2 extern "C" _declspec(dllexport) void MyDLL(char* szMessage); 3 4 void MyDLL(char* szMessage) 5 { 6 MessageBox(NULL,szMessage,"MyDLL",MB_OK); 7 }
编译生成的MyDLL.dll和MyDLL.lib两个文件将在调用中使用到。
调用方法一:静态调用。
创建一个调用DLL文件的EXE程序,命名为CallDLL。如下:
#include<Windows.h> #pragma comment(lib,"MyDLL") extern "C" void DLLShow(char* szMessage); int _stdcall WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance, __in LPSTR lpCmdLine, __in int nShowCmd ) { DLLShow("HelloWorld!"); return 0; }
直接编译的话,会产生一个连接错误。无法打开文件“MyDLL.lib”,需要将之前编译好的MyDLL.lib和MyDLL.dll文件添加到工程目录下。
注意,是工程目录下(有.cpp文件的目录),添加到其它目录下仍旧会提示这个错误。
静态调用是通过连接器将DLL的导出函数写进可执行文件中。下面提到的动态调用则是在程序运行时完成调用的。
调用方法二:动态调用。
首先需要知道DLL中函数的声明。然后通过以下几个步骤进行动态的加载。
1、根据函数声明构建函数指针。
2、使用LoadLibrary函数加载dll文件。
3、使用GetProcAddress将DLL中的函数指针赋值给我们构建的函数指针。
到此,函数指针就具有了DLL函数一样的功能。
1 #include<Windows.h> 2 typedef void (*PFUN)(char*); 3 int _stdcall WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nShowCmd ) 4 { 5 HMODULE hModule =LoadLibrary("Test.dll"); 6 if (hModule==NULL) 7 { 8 MessageBox(NULL,"MyDLL文件不存在","加载失败",MB_OK); 9 return -1; 10 } 11 PFUN pFun=(PFUN)GetProcAddress(hModule,"DLLShow"); 12 pFun("Hello World!"); 13 return 0; 14 }
相比两种调用方式,动态调用有很多的优点。比如有些动态链接库没有提供lib文件,也有时候我们可以能要编写DLL文件供别人使用、或自己在其他语言中调用,等等很多情况下,我们只有动态调用一种方式。
四、卸载DLL模块
当进程不再需要引用DLL中的符号时,我们应该将其从进程的地址空间中卸载。
如果DLL没有创建线程,我们使用FreeLibrary函数
BOOL FreeLibrary(HMODULE hInstDll);
我们需要传入标识DLL的句柄,也就是我们使用调用的LoadLibrary函数的返回值。
如果DLL创建了其他线程,我们需要使用FreeLibraryAndExitThread函数。
VOID FreeLibraryAndExitThread(HMODULE hInstDll, DWORD dwExitCode);