Windows API编程之动态链接库(DLL)
Posted xmr183729
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Windows API编程之动态链接库(DLL)相关的知识,希望对你有一定的参考价值。
转自:http://blog.chinaunix.net/uid-20255102-id-1713076.html
!建议耐心看完!
链接库分为静态链接库和动态链接库,而动态链接库在使用时,又进一步分为装载时链接和运行时链接。装载时链接是指该动态链接库是在程序装入时进行加载链接的,而运行时链接是指该动态链接库是在程序运行时执行LoadLibrary(或LoadLibraryEx,下同)函数动态加载的。因此,由于动态链接库有这两种链接方式,所以在编写使用DLL的程序时,就有了两种可选方案。
可能有人会问“为什么需要装载时链接?直接静态链接不就行了吗?”,这是模块化程序设计的需要。试想,如果你开发一个很大的程序,并且经常需要更新。如果你选择静态链接,那么每次更新就必须更新整个exe文件,而如果你把需要经常更新的模块做成dll,那么只需要更新这个文件即可,每次程序运行时加载这个更新的文件即可。
/* * dll_lib.h */ #ifndef DLL_LIB_H #define DLL_LIB_H #ifdef __cplusplus #define EXPORT extern "C" __declspec (dllexport) #else #define EXPORT __declspec (dllexport) #endif EXPORT int WINAPI GetMax(int a, int b); #endif
/* * dll_lib.c */ #include <windows.h> #include <stdio.h> #include "dll_lib.h" int WINAPI DllMain (HINSTANCE hInstance, DWORD fdwReason, PVOID pvReserved) { switch (fdwReason) { case DLL_PROCESS_ATTACH: printf("> process attach of dll "); break; case DLL_THREAD_ATTACH: printf("> thread attach of dll "); break; case DLL_THREAD_DETACH: printf("> thread detach of dll "); break; case DLL_PROCESS_DETACH: printf("> process detach of dll "); break; } return TRUE; } int GetMax(int a, int b) { return a > b ? a : b; }
接着,再创建一个Win32 Console Application工程DLL_Test,同样将该工程加入先前的DLLTest工作区中,并直接保存在该工作区目录下。然后向工程DLL_Test加入下面的文件:
/* * testMain.c */ #include <windows.h> #include <stdio.h> #include "dll_lib.h" int main() { int a = 2; int b = 3; printf(" max(2, 3) = %d ", GetMax(2, 3)); return 0; }
此时,工作差不多做完了,但还需进行一下设置。在Project|Settings里,把两个工程里的General标签里的Intermediate files和Output files都设置为Debug。这样确保两个工程的输出文件在一个目录中,以便后面动态库链接时的查找。另外,设置DLL_Test为活动工程(Project|Set Active Project),设置DLL_Test依赖于DLL_Lib(Project|Dependencies)。此时,就可以编译运行了。运行结果为:
> process attach of dll
max(2, 3) = 3
> process detach of dll
Press any key to continue
下面对上面的代码和结果进行分析。
在dll_lib.h中,EXPORT宏实质上就是一个导出函数所需要的关键字。__declspec (dllexport)是Windows扩展关键字的组合,表示DLL里的对象的存储类型关键字。extern "C"用于C++程序使用该函数时的函数声明的链接属性。WINAPI是宏定义,等价于__stdcall。下面列出Windows编程中常见的几种有关调用约定的宏,它们都是与__stdcall和__cdecl有关的(from windef.h):
#define CALLBACK __stdcall // 用于回调函数
#define WINAPI __stdcall // 用于API函数
#define WINAPIV __cdecl
#define APIENTRY WINAPI
#define APIPRIVATE __stdcall
#define PASCAL __stdcall
另外,关于__stdcall:如果通过VC++编写的DLL欲被其他语言编写的程序调用,应将函数的调用约定声明为__stdcall方式,WINAPI、CALLBACK都采用这种方式,而C/C++缺省的调用方式却为__cdecl。__stdcall方式与__cdecl对函数名最终生成符号的方式不同。若采用C编译方式(在C++中需将函数声明为extern "C"),__stdcall调用约定在输出函数名前面加下划线,后面加“@”符号和参数的字节数,形如_functionName@number ,而__cdecl调用约定仅在输出函数名前面加下划线,形如_functionName。(小技巧:如何查看这些符号?写一个程序,只提供函数的声明而不给定义,就可以看到链接器给出的符号了)
因此,在前面例子中,该DLL声明了一个导出函数GetMax,其连接属性采用CALLBACK(即__stdcall)。另外,请注意,例子中的宏EXPORT会根据是在C程序还是在C++程序中被调用选择相应的连接方式。在定义导出函数时,不需要EXPORT宏,只需要在函数声明时使用即可。
DllMain函数在DLL载入和卸载时被调用。它的第一个参数是DLL句柄,第三个参数保留。第二个参数用来区分该DLLMain函数是在什么情况下被调用的,如程序所示。如果初始化成功,则DllMain应该返回一个非零值。如果返回零值将导致程序停止运行(你可以修改上面例子中的DllMain的返回值为0,将看到相应的出错结果)。如果在你的DLL程序中没有编写DllMain函数,那么在执行该DLL时,系统将引入一个不做任何操作的缺省DllMain函数版本。
/* * testMain.c */ #include <windows.h> #include <stdio.h> typedef int (* PGetMax)(int, int); int main() { int a = 2; int b = 3; HINSTANCE hDll; // DLL句柄 PGetMax pGetMax; // 函数指针 hDll = LoadLibrary(".\Debug\DLL_lib.dll"); if (hDll == NULL) { printf("Can‘t find library file "dll_lib.dll" "); exit(1); } pGetMax = (PGetMax)GetProcAddress(hDll, "GetMax"); if (pGetMax == NULL) { printf("Can‘t find function "GetMax" "); exit(1); } printf(" max(2, 3) = %d ", pGetMax(2, 3)); FreeLibrary(hDll); return 0; }
此时,不再需要动态的.h文件和.lib文件,只需要提供.dll文件即可。在具体使用时,先用LoadLibrary加载Dll文件,然后用GetProcAddress寻找函数的地址,此时必须提供该函数的在Dll中的名字(不一定与函数名相同)。
然后编译链接、运行,结果与前面的运行结果相同。
下面将解释,为什么前面要去掉WINAPI调用约定(即采用默认的__cdecl方式)。我们可以先看看DLL_Lib.dll里面的链接符号。在cmd中运行命令:
dumpbin /exports DLL_Lib.dll
得到如下结果:
Dump of file f:codeDLLTestDebugDll_lib.dll File Type: DLL Section contains the following exports for DLL_Lib.dll 0 characteristics 4652C3B1 time date stamp Tue May 22 18:19:29 2007 0.00 version 1 ordinal base 1 number of functions 1 number of names ordinal hint RVA name 1 0 0000100A GetMax Summary 4000 .data 1000 .idata 3000 .rdata 2000 .reloc 28000 .text
可以看到GetMax函数在编译后在Dll中的名字仍为GetMax,所以在前面的程序中使用的是:
pGetMax = (PGetMax)GetProcAddress(hDll, "GetMax");
然后,我们把WINAPI添加回去,重新编译DLL_Lib工程。运行刚才的DLL_Test程序,运行出错,结果如下:
> process attach of dll
Can‘t find function "GetMax"
> process detach of dll
Press any key to continue
显然,运行失败原因是因为没有找到GetMax函数。再次运行命令:dumpbin /exports DLL_Lib.dll,结果如下(部分结果):
1 ordinal base 1 number of functions 1 number of names ordinal hint RVA name 1 0 0000100A _GetMax@8
从上面dumpbin的输出看,GetMax函数在WINAPI调用约定方式下在DLL里的名字与源码中的函数定义时的名字不再相同,其导出名是"_GetMax@8"。此时,你把testMain.c中的函数指针类型声明和函数查找语句作如下修改:
typedef int (WINAPI* PGetMax)(int, int);
pGetMax = (PGetMax)GetProcAddress(hDll, "_GetMax@8");
再次编译链接,然后运行,发现结果又正确了。
现在找到了问题所在。很显然,这种修改方式并不适用,而默认生成的名字又不是我们所想要的。那么该怎么解决这个问题呢?这就需要用到.def文件来解决。
模块定义文件(.def)
模块定义文件(.def文件)是一个描述DLL的各种属性的文件,可以包含一个或多个模块定义语句。如果你不使用关键字__declspec(dllexport)关键字导出DLL中的函数,那么DLL就需要一个.def文件。
一个最小的.def文件必须包含下面的模块定义语句:
(1)文件中第一个语句必须是LIBRARY语句。该语句标记该.def文件属于哪个DLL。语法形式为:LIBRARY <dll名>。
(2)EXPORTS语句列表。第一个导出语句的形式为:entryname[=internalname] [@ordinal],列出DLL中要导出的函数的名字和可选的序号(ordinal value)。要导出的函数名可以是程序源码中的函数名,也可以定义新的函数别名(但后面必须紧跟[=<原函数名>]);序号必须在范围1到N之间且不能重复,其中N是DLL中导出的函数个数。因此,EXPORTS语句语法形式为:
EXPORTS
[=<internalname1] [@<num1="">]
[=<internalname2] [@<num2="">]
;...
(3)虽然不是必须的,一个.def文件也常常包含DESCRIPTION语句,用来描述该DLL的用途之类,语法形式为:
DESCRIPTION ""
(4)在任意位置,可以包含注释语句,以分号(;)开始。
例如,在本文中后面将用到的.def文件为:
; DLL_Lib.def LIBRARY DLL_Lib ; the dll name DESCRIPTION "Learn how to use the dll." EXPORTS GetMax @1 Max=GetMax @2 ; alias name of GetMax ; Ok, over
现在,让我们回到DLL_Lib工程,修改GetMax函数的声明,把EXPORT去掉,重新编译该工程。然后,运行dumpbin命令,我们发现此时没有导出函数。再将上面的DLL_Lib.def文件添加进DLL_Lib工程,再次编译,并运行dumpbin命令,得到如下结果(引用部分结果):
1 ordinal base 2 number of functions 2 number of names ordinal hint RVA name 1 0 0000100A GetMax 2 1 0000100A Max
正如我们所预期的,有两个导出函数GetMax和Max。注意,此时源码中的GetMax函数的导出名不再是默认的“_GetMax@8”。另外,需要注意的是,两个导出函数有相同的相对虚拟地址(RVA),也说明了两个导出名实质是同一个函数的不同名字而已,都是源码中GetMax函数的导出名。
现在,回到DLL_Test工程,修改testMain.c文件内容如下:
/* * testMain.c */ #include <windows.h> #include <stdio.h> typedef int (WINAPI* PGetMax)(int, int); int main() { int a = 2; int b = 3; HINSTANCE hDll; // DLL句柄 PGetMax pGetMax; // 函数指针 hDll = LoadLibrary(".\Debug\DLL_lib.dll"); if (hDll == NULL) { printf("Can‘t find library file "dll_lib.dll" "); exit(1); } pGetMax = (PGetMax)GetProcAddress(hDll, "GetMax"); if (pGetMax == NULL) { printf("Can‘t find function "GetMax" "); exit(1); } printf(" GetMax(2, 3) = %d ", pGetMax(2, 3)); pGetMax = (PGetMax)GetProcAddress(hDll, "Max"); if (pGetMax == NULL) { printf("Can‘t find function "GetMax" "); exit(1); } printf(" Max(2, 3) = %d ", pGetMax(2, 3)); FreeLibrary(hDll); return 0; }
编译链接、运行,结果如下:
> process attach of dll
GetMax(2, 3) = 3
Max(2, 3) = 3
> process detach of dll
Press any key to continue
运行结果正如前面分析的那样,GetMax和Max都得到了相同的结果。
到这里,我们解决了DLL导出函数名在各种调用约定下的默认名可能不同于源码中函数名的问题。此时,你就可以制作跟Windows的自带API函数库相同的库了:使用__stdcall调用约定以满足Windows下的任何语言都可以调用DLL库,同时使用函数名作为导出名,以方便用户使用DLL里的函数。
#pragma data_seg ("shared")
int g_oneNumber = 0;
#pragma data_seg ()
以上是关于Windows API编程之动态链接库(DLL)的主要内容,如果未能解决你的问题,请参考以下文章