转自http://blog.163.com/airton@188/blog/static/1018619820079251459659/
VC++动态链接库编程
先来阐述一下DLL(Dynamic Linkable Library)的概念,你可以简单的把DLL看成一种仓库,它提供给你一些可以直接拿来用的变量、函数或类。在仓库的发展史上经历了“无库-静态链接库-动态链接库”的时代。静态链接库与动态链接库都是共享代码的方式,如果采用静态链接库,则无论你愿不愿意,lib中的指令都被直接包含在最终生成的EXE文件中了。但是若使用DLL,该DLL不必被包含在最终EXE文件中,EXE文件执行时可以“动态”地引用和卸载这个与EXE独立的DLL文件。静态链接库和动态链接库的另外一个区别在于静态链接库中不能再包含其他的动态链接库或者静态库,而在动态链接库中还可以再包含其他的动态或静态链接库。
对动态链接库,我们还需建立如下概念:
(1)DLL 的编制与具体的编程语言及编译器无关
只要遵循约定的DLL接口规范和调用方式,用各种语言编写的DLL都可以相互调用。譬如Windows提供的系统DLL(其中包括了Windows的API),在任何开发环境中都能被调用,不在乎其是Visual Basic、Visual C++还是Delphi。
(2)动态链接库随处可见
我们在Windows目录下的system32文件夹中会看到kernel32.dll、user32.dll和gdi32.dll,windows的大多数API都包含在这些DLL中。kernel32.dll中的函数主要处理内存管理和进程调度;user32.dll中的函数主要控制用户界面;gdi32.dll中的函数则负责图形方面的操作。
一般的程序员都用过类似MessageBox的函数,其实它就包含在user32.dll这个动态链接库中。由此可见DLL对我们来说其实并不陌生。
(3)VC动态链接库的分类
Visual C++支持三种DLL,它们分别是Non-MFC DLL(非MFC动态库)、MFC Regular DLL(MFC规则DLL)、MFC Extension DLL(MFC扩展DLL)。
非MFC动态库不采用MFC类库结构,其导出函数为标准的C接口,能被非MFC或MFC编写的应用程序所调用;MFC规则DLL 包含一个继承自CWinApp的类,但其无消息循环;MFC扩展DLL采用MFC的动态链接版本创建,它只能被用MFC类库所编写的应用程序所调用。
由于本文篇幅较长,内容较多,势必需要先对阅读本文的有关事项进行说明,下面以问答形式给出。
问:本文主要讲解什么内容?
答:本文详细介绍了DLL编程的方方面面,努力学完本文应可以对DLL有较全面的掌握,并能编写大多数DLL程序。
问:如何看本文?
答:本文每一个主题的讲解都附带了源代码例程,可以随文下载(每个工程都经WINRAR压缩)。所有这些例程都由笔者编写并在VC++6.0中调试通过。
当然看懂本文不是读者的最终目的,读者应亲自动手实践才能真正掌握DLL的奥妙。
问:学习本文需要什么样的基础知识?
答:如果你掌握了C,并大致掌握了C++,了解一点MFC的知识,就可以轻松地看懂本文。
2.静态链接库
对静态链接库的讲解不是本文的重点,但是在具体讲解DLL之前,通过一个静态链接库的例子可以快速地帮助我们建立“库”的概念。
图1 建立一个静态链接库
|
如图1,在VC++6.0中new一个名称为libTest的static library工程(单击此处下载本工程),并新建lib.h和lib.cpp两个文件,lib.h和lib.cpp的源代码如下:
//文件:lib.h
#ifndef LIB_H #define LIB_H extern "C" int add(int x,int y); //声明为C编译、连接方式的外部函数 #endif //文件:lib.cpp #include "lib.h" int add(int x,int y) { return x + y; } |
编译这个工程就得到了一个.lib文件,这个文件就是一个函数库,它提供了add的功能。将头文件和.lib文件提交给用户后,用户就可以直接使用其中的add函数了。
标准Turbo C2.0中的C库函数(我们用来的scanf、printf、memcpy、strcpy等)就来自这种静态库。
下面来看看怎么使用这个库,在libTest工程所在的工作区内new一个libCall工程。libCall工程仅包含一个main.cpp文件,它演示了静态链接库的调用方法,其源代码如下:
#include
#include "..lib.h" #pragma comment( lib, "../debug/libTest.lib" ) //指定与静态库一起连接 int main(int argc, char* argv[]) { printf( "2 + 3 = %d", add( 2, 3 ) ); } |
静态链接库的调用就是这么简单,或许我们每天都在用,可是我们没有明白这个概念。代码中#pragma comment( lib , "../debug/libTest.lib" )的意思是指本文件生成的.obj文件应与libTest.lib一起连接。如果不用#pragma comment指定,则可以直接在VC++中设置,如图2,依次选择tools、options、directories、library files菜单或选项,填入库文件路径。图2中加红圈的部分为我们添加的libTest.lib文件的路径。
图2 在VC中设置库文件路径
|
这个静态链接库的例子至少让我们明白了库函数是怎么回事,它们是哪来的。我们现在有下列模糊认识了:
(1)库不是个怪物,编写库的程序和编写一般的程序区别不大,只是库不能单独执行;
(2)库提供一些可以给别的程序调用的东东,别的程序要调用它必须以某种方式指明它要调用之。
以上从静态链接库分析而得到的对库的懵懂概念可以直接引申到动态链接库中,动态链接库与静态链接库在编写和调用上的不同体现在库的外部接口定义及调用方式略有差异。
3.库的调试与查看
在具体进入各类DLL的详细阐述之前,有必要对库文件的调试与查看方法进行一下介绍,因为从下一节开始我们将面对大量的例子工程。
由于库文件不能单独执行,因而在按下F5(开始debug模式执行)或CTRL+F5(运行)执行时,其弹出如图3所示的对话框,要求用户输入可执行文件的路径来启动库函数的执行。这个时候我们输入要调用该库的EXE文件的路径就可以对库进行调试了,其调试技巧与一般应用工程的调试一样。
图3 库的调试与“运行”
|
通常有比上述做法更好的调试途径,那就是将库工程和应用工程(调用库的工程)放置在同一VC工作区,只对应用工程进行调试,在应用工程调用库中函数的语句处设置断点,执行后按下F11,这样就单步进入了库中的函数。第2节中的libTest和libCall工程就放在了同一工作区,其工程结构如图4所示。
图4 把库工程和调用库的工程放入同一工作区进行调试
|
上述调试方法对静态链接库和动态链接库而言是一致的。所以本文提供下载的所有源代码中都包含了库工程和调用库的工程,这二者都被包含在一个工作区内,这是笔者提供这种打包下载的用意所在。
动态链接库中的导出接口可以使用Visual C++的Depends工具进行查看,让我们用Depends打开系统目录中的user32.dll,看到了吧?红圈内的就是几个版本的MessageBox了!原来它真的在这里啊,原来它就在这里啊!
图5 用Depends查看DLL
|
当然Depends工具也可以显示DLL的层次结构,若用它打开一个可执行文件则可以看出这个可执行文件调用了哪些DLL。
好,让我们正式进入动态链接库的世界,先来看看最一般的DLL,即非MFC DLL。
非MFC DLL编程
4.1一个简单的DLL
第2节给出了以静态链接库方式提供add函数接口的方法,接下来我们来看看怎样用动态链接库实现一个同样功能的add函数。
如图6,在VC++中new一个Win32 Dynamic-Link Library工程dllTest(单击此处下载本工程)。注意不要选择MFC AppWizard(dll),因为用MFC AppWizard(dll)建立的将是第5、6节要讲述的MFC 动态链接库。
图6 建立一个非MFC DLL
|
在建立的工程中添加lib.h及lib.cpp文件,源代码如下:
/* 文件名:lib.h */
#ifndef LIB_H #define LIB_H extern "C" int __declspec(dllexport)add(int x, int y); #endif /* 文件名:lib.cpp */ #include "lib.h" int add(int x, int y) { return x + y; } |
与第2节对静态链接库的调用相似,我们也建立一个与DLL工程处于同一工作区的应用工程dllCall,它调用DLL中的函数add,其源代码如下:
#include
#include typedef int(*lpAddFun)(int, int); //宏定义函数指针类型 int main(int argc, char *argv[]) { HINSTANCE hDll; //DLL句柄 lpAddFun addFun; //函数指针 hDll = LoadLibrary("../Debug/dllTest.dll"); if (hDll != NULL) { addFun = (lpAddFun)GetProcAddress(hDll, "add"); if (addFun != NULL) { int result = addFun(2, 3); printf("%d", result); } FreeLibrary(hDll); } return 0; }
|
分析上述代码,dllTest工程中的lib.cpp文件与第2节静态链接库版本完全相同,不同在于lib.h对函数add的声明前面添加了__declspec(dllexport)语句。这个语句的含义是声明函数add为DLL的导出函数。DLL内的函数分为两种:
(1)DLL导出函数,可供应用程序调用;
(2) DLL内部函数,只能在DLL程序使用,应用程序无法调用它们。
而应用程序对本DLL的调用和对第2节静态链接库的调用却有较大差异,下面我们来逐一分析。
首先,语句typedef int ( * lpAddFun)(int,int)定义了一个与add函数接受参数类型和返回值均相同的函数指针类型。随后,在main函数中定义了lpAddFun的实例addFun;
其次,在函数main中定义了一个DLL HINSTANCE句柄实例hDll,通过Win32 Api函数LoadLibrary动态加载了DLL模块并将DLL模块句柄赋给了hDll;
再次,在函数main中通过Win32 Api函数GetProcAddress得到了所加载DLL模块中函数add的地址并赋给了addFun。经由函数指针addFun进行了对DLL中add函数的调用;
最后,应用工程使用完DLL后,在函数main中通过Win32 Api函数FreeLibrary释放了已经加载的DLL模块。
通过这个简单的例子,我们获知DLL定义和调用的一般概念:
(1)DLL中需以某种特定的方式声明导出函数(或变量、类);
(2)应用工程需以某种特定的方式调用DLL的导出函数(或变量、类)。
下面我们来对“特定的方式进行”阐述。
4.2 声明导出函数
DLL中导出函数的声明有两种方式:一种为4.1节例子中给出的在函数声明中加上__declspec(dllexport),这里不再举例说明;另外一种方式是采用模块定义(.def) 文件声明,.def文件为链接器提供了有关被链接程序的导出、属性及其他方面的信息。
下面的代码演示了怎样同.def文件将函数add声明为DLL导出函数(需在dllTest工程中添加lib.def文件):
; lib.def : 导出DLL函数
LIBRARY dllTest EXPORTS add @ 1 |
.def文件的规则为:
(1)LIBRARY语句说明.def文件相应的DLL;
(2)EXPORTS语句后列出要导出函数的名称。可以在.def文件中的导出函数名后加@n,表示要导出函数的序号为n(在进行函数调用时,这个序号将发挥其作用);
(3).def 文件中的注释由每个注释行开始处的分号 (;) 指定,且注释不能与语句共享一行。
由此可以看出,例子中lib.def文件的含义为生成名为“dllTest”的动态链接库,导出其中的add函数,并指定add函数的序号为1。
4.3 DLL的调用方式
在4.1节的例子中我们看到了由“LoadLibrary-GetProcAddress-FreeLibrary”系统Api提供的三位一体“DLL加载-DLL函数地址获取-DLL释放”方式,这种调用方式称为DLL的动态调用。
动态调用方式的特点是完全由编程者用 API 函数加载和卸载 DLL,程序员可以决定 DLL 文件何时加载或不加载,显式链接在运行时决定加载哪个 DLL 文件。
与动态调用方式相对应的就是静态调用方式,“有动必有静”,这来源于物质世界的对立统一。“动与静”,其对立与统一竟无数次在技术领域里得到验证,譬如静态IP与DHCP、静态路由与动态路由等。从前文我们已经知道,库也分为静态库与动态库DLL,而想不到,深入到DLL内部,其调用方式也分为静态与动态。“动与静”,无处不在。《周易》已认识到有动必有静的动静平衡观,《易.系辞》曰:“动静有常,刚柔断矣”。哲学意味着一种普遍的真理,因此,我们经常可以在枯燥的技术领域看到哲学的影子。
静态调用方式的特点是由编译系统完成对DLL的加载和应用程序结束时 DLL 的卸载。当调用某DLL的应用程序结束时,若系统中还有其它程序使用该 DLL,则Windows对DLL的应用记录减1,直到所有使用该DLL的程序都结束时才释放它。静态调用方式简单实用,但不如动态调用方式灵活。
下面我们来看看静态调用的例子(单击此处下载本工程),将编译dllTest工程所生成的.lib和.dll文件拷入dllCall工程所在的路径,dllCall执行下列代码:
#pragma comment(lib,"dllTest.lib")
//.lib文件中仅仅是关于其对应DLL文件中函数的重定位信息 extern "C" __declspec(dllimport) add(int x,int y); int main(int argc, char* argv[]) { int result = add(2,3); printf("%d",result); return 0; } |
由上述代码可以看出,静态调用方式的顺利进行需要完成两个动作:
(1)告诉编译器与DLL相对应的.lib文件所在的路径及文件名,#pragma comment(lib,"dllTest.lib")就是起这个作用。
程序员在建立一个DLL文件时,连接器会自动为其生成一个对应的.lib文件,该文件包含了DLL 导出函数的符号名及序号(并不含有实际的代码)。在应用程序里,.lib文件将作为DLL的替代文件参与编译。
(2)声明导入函数,extern "C" __declspec(dllimport) add(int x,int y)语句中的__declspec(dllimport)发挥这个作用。
静态调用方式不再需要使用系统API来加载、卸载DLL以及获取DLL中导出函数的地址。这是因为,当程序员通过静态链接方式编译生成应用程序时,应用程序中调用的与.lib文件中导出符号相匹配的函数符号将进入到生成的EXE 文件中,.lib文件中所包含的与之对应的DLL文件的文件名也被编译器存储在 EXE文件内部。当应用程序运行过程中需要加载DLL文件时,Windows将根据这些信息发现并加载DLL,然后通过符号名实现对DLL 函数的动态链接。这样,EXE将能直接通过函数名调用DLL的输出函数,就象调用程序内部的其他函数一样。
4.4 DllMain函数
Windows在加载DLL的时候,需要一个入口函数,就如同控制台或DOS程序需要main函数、WIN32程序需要WinMain函数一样。在前面的例子中,DLL并没有提供DllMain函数,应用工程也能成功引用DLL,这是因为Windows在找不到DllMain的时候,系统会从其它运行库中引入一个不做任何操作的缺省DllMain函数版本,并不意味着DLL可以放弃DllMain函数。
根据编写规范,Windows必须查找并执行DLL里的DllMain函数作为加载DLL的依据,它使得DLL得以保留在内存里。这个函数并不属于导出函数,而是DLL的内部函数。这意味着不能直接在应用工程中引用DllMain函数,DllMain是自动被调用的。
我们来看一个DllMain函数的例子(单击此处下载本工程)。
BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{ switch (ul_reason_for_call) { 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; } |
DllMain函数在DLL被加载和卸载时被调用,在单个线程启动和终止时,DLLMain函数也被调用,ul_reason_for_call指明了被调用的原因。原因共有4种,即PROCESS_ATTACH、PROCESS_DETACH、THREAD_ATTACH和THREAD_DETACH,以switch语句列出。
来仔细解读一下DllMain的函数头BOOL APIENTRY DllMain( HANDLE hModule, WORD ul_reason_for_call, LPVOID lpReserved )。
APIENTRY被定义为__stdcall,它意味着这个函数以标准Pascal的方式进行调用,也就是WINAPI方式;
进程中的每个DLL模块被全局唯一的32字节的HINSTANCE句柄标识,只有在特定的进程内部有效,句柄代表了DLL模块在进程虚拟空间中的起始地址。在Win32中,HINSTANCE和HMODULE的值是相同的,这两种类型可以替换使用,这就是函数参数hModule的来历。
执行下列代码:
hDll = LoadLibrary("../Debug/dllTest.dll");
if (hDll != NULL) { addFun = (lpAddFun)GetProcAddress(hDll, MAKEINTRESOURCE(1)); //MAKEINTRESOURCE直接使用导出文件中的序号 if (addFun != NULL) { int result = addFun(2, 3); printf(" call add in dll:%d", result); } FreeLibrary(hDll); } |
我们看到输出顺序为:
process attach of dll
call add in dll:5 process detach of dll |
这一输出顺序验证了DllMain被调用的时机。
代码中的GetProcAddress ( hDll, MAKEINTRESOURCE ( 1 ) )值得留意,它直接通过.def文件中为add函数指定的顺序号访问add函数,具体体现在MAKEINTRESOURCE ( 1 ),MAKEINTRESOURCE是一个通过序号获取函数名的宏,定义为(节选自winuser.h):
#define MAKEINTRESOURCEA(i) (LPSTR)((DWORD)((WORD)(i)))
#define MAKEINTRESOURCEW(i) (LPWSTR)((DWORD)((WORD)(i))) #ifdef UNICODE #define MAKEINTRESOURCE MAKEINTRESOURCEW #else #define MAKEINTRESOURCE MAKEINTRESOURCEA |
4.5 __stdcall约定
如果通过VC++编写的DLL欲被其他语言编写的程序调用,应将函数的调用方式声明为__stdcall方式,WINAPI都采用这种方式,而C/C++缺省的调用方式却为__cdecl。__stdcall方式与__cdecl对函数名最终生成符号的方式不同。若采用C编译方式(在C++中需将函数声明为extern "C"),__stdcall调用约定在输出函数名前面加下划线,后面加“@”符号和参数的字节数,形如_functionname@number;而__cdecl调用约定仅在输出函数名前面加下划线,形如_functionname。
Windows编程中常见的几种函数类型声明宏都是与__stdcall和__cdecl有关的(节选自windef.h):
#define CALLBACK __stdcall //这就是传说中的回调函数
#define WINAPI __stdcall //这就是传说中的WINAPI #define WINAPIV __cdecl #define APIENTRY WINAPI //DllMain的入口就在这里 #define APIPRIVATE __stdcall #define PASCAL __stdcall |
在lib.h中,应这样声明add函数:
int __stdcall add(int x, int y); |
在应用工程中函数指针类型应定义为:
typedef int(__stdcall *lpAddFun)(int, int); |
若在lib.h中将函数声明为__stdcall调用,而应用工程中仍使用typedef int (* lpAddFun)(int,int),运行时将发生错误(因为类型不匹配,在应用工程中仍然是缺省的__cdecl调用),弹出如图7所示的对话框。
图7 调用约定不匹配时的运行错误
|
图8中的那段话实际上已经给出了错误的原因,即“This is usually a result of …”。
4.6 DLL导出变量
DLL定义的全局变量可以被调用进程访问;DLL也可以访问调用进程的全局数据,我们来看看在应用工程中引用DLL中变量的例子(单击此处下载本工程)。
/* 文件名:lib.h */
#ifndef LIB_H #define LIB_H extern int dllGlobalVar; #endif /* 文件名:lib.cpp */ #include "lib.h" #include int dllGlobalVar; BOOL APIENTRY DllMain(HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: dllGlobalVar = 100; //在dll被加载时,赋全局变量为100 break; case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; }
|
;文件名:lib.def
;在DLL中导出变量
LIBRARY "dllTest"
EXPORTS dllGlobalVar CONSTANT ;或dllGlobalVar DATA GetGlobalVar |
从lib.h和lib.cpp中可以看出,全局变量在DLL中的定义和使用方法与一般的程序设计是一样的。若要导出某全局变量,我们需要在.def文件的EXPORTS后添加:
变量名 CONSTANT //过时的方法
或
变量名 DATA //VC++提示的新方法
在主函数中引用DLL中定义的全局变量:
#include
#pragma comment(lib,"dllTest.lib") extern int dllGlobalVar; int main(int argc, char *argv[]) { printf("%d ", *(int*)dllGlobalVar); *(int*)dllGlobalVar = 1; printf("%d ", *(int*)dllGlobalVar); return 0; } |
特别要注意的是用extern int dllGlobalVar声明所导入的并不是DLL中全局变量本身,而是其地址,应用程序必须通过强制指针转换来使用DLL中的全局变量。这一点,从*(int*)dllGlobalVar可以看出。因此在采用这种方式引用DLL全局变量时,千万不要进行这样的赋值操作:
dllGlobalVar = 1; |
其结果是dllGlobalVar指针的内容发生变化,程序中以后再也引用不到DLL中的全局变量了。
在应用工程中引用DLL中全局变量的一个更好方法是:
#include
#pragma comment(lib,"dllTest.lib") extern int _declspec(dllimport) dllGlobalVar; //用_declspec(dllimport)导入 int main(int argc, char *argv[]) { printf("%d ", dllGlobalVar); dllGlobalVar = 1; //这里就可以直接使用, 无须进行强制指针转换 printf("%d ", dllGlobalVar); return 0; } |
通过_declspec(dllimport)方式导入的就是DLL中全局变量本身而不再是其地址了,笔者建议在一切可能的情况下都使用这种方式。
4.7 DLL导出类
DLL中定义的类可以在应用工程中使用。
下面的例子里,我们在DLL中定义了point和circle两个类,并在应用工程中引用了它们(单击此处下载本工程)。
//文件名:point.h,point类的声明
#ifndef POINT_H #define POINT_H #ifdef DLL_FILE class _declspec(dllexport) point //导出类point #else class _declspec(dllimport) point //导入类point #endif { public: float y; float x; point(); point(float x_coordinate, float y_coordinate); }; #endif //文件名:point.cpp,point类的实现 #ifndef DLL_FILE #define DLL_FILE #endif #include "point.h" //类point的缺省构造函数 point::point() { x = 0.0; y = 0.0; } //类point的构造函数 point::point(float x_coordinate, float y_coordinate) { x = x_coordinate; y = y_coordinate; } //文件名:circle.h,circle类的声明 #ifndef CIRCLE_H #define CIRCLE_H #include "point.h" #ifdef DLL_FILE class _declspec(dllexport)circle //导出类circle #else class _declspec(dllimport)circle //导入类circle #endif { public: void SetCentre(const point ¢rePoint); void SetRadius(float r); float GetGirth(); float GetArea(); circle(); private: float radius; point centre; }; #endif //文件名:circle.cpp,circle类的实现 #ifndef DLL_FILE #define DLL_FILE #endif #include "circle.h" #define PI 3.1415926 //circle类的构造函数 circle::circle() { centre = point(0, 0); radius = 0; } //得到圆的面积 float circle::GetArea() { return PI *radius * radius; } //得到圆的周长 float circle::GetGirth() { return 2 *PI * radius; } //设置圆心坐标 void circle::SetCentre(const point ¢rePoint) { centre = centrePoint; } //设置圆的半径 void circle::SetRadius(float r) { radius = r; } |
类的引用:
#include "..circle.h" //包含类声明头文件
#pragma comment(lib,"dllTest.lib"); int main(int argc, char *argv[]) { circle c; point p(2.0, 2.0); c.SetCentre(p); c.SetRadius(1.0); printf("area:%f girth:%f", c.GetArea(), c.GetGirth()); return 0; } |
从上述源代码可以看出,由于在DLL的类实现代码中定义了宏DLL_FILE,故在DLL的实现中所包含的类声明实际上为:
class _declspec(dllexport) point //导出类point
{ … } |
和
class _declspec(dllexport) circle //导出类circle
{ … } |
而在应用工程中没有定义DLL_FILE,故其包含point.h和circle.h后引入的类声明为:
class _declspec(dllimport) point //导入类point
{ … } |
和
class _declspec(dllimport) circle //导入类circle
{ … } |
不错,正是通过DLL中的
class _declspec(dllexport) class_name //导出类circle
{ … } |
与应用程序中的
class _declspec(dllimport) class_name //导入类
{ … } |
匹对来完成类的导出和导入的!
我们往往通过在类的声明头文件中用一个宏来决定使其编译为class _declspec(dllexport) class_name还是class _declspec(dllimport) class_name版本,这样就不再需要两个头文件。本程序中使用的是:
#ifdef DLL_FILE
class _declspec(dllexport) class_name //导出类 #else class _declspec(dllimport) class_name //导入类 #endif |
实际上,在MFC DLL的讲解中,您将看到比这更简便的方法,而此处仅仅是为了说明_declspec(dllexport)与_declspec(dllimport)匹对的问题。
由此可见,应用工程中几乎可以看到DLL中的一切,包括函数、变量以及类,这就是DLL所要提供的强大能力。只要DLL释放这些接口,应用程序使用它就将如同使用本工程中的程序一样!
本章虽以VC++为平台讲解非MFC DLL,但是这些普遍的概念在其它语言及开发环境中也是相同的,其思维方式可以直接过渡。 接下来,我们将要研究MFC规则DLL。
MFC规则DLL
第4节我们对非MFC DLL进行了介绍,这一节将详细地讲述MFC规则DLL的创建与使用技巧。
另外,自从本文开始连载后,收到了一些读者的e-mail。有的读者提出了一些问题,笔者将在本文的最后一次连载中选取其中的典型问题进行解答。由于时间的关系,对于读者朋友的来信,笔者暂时不能一一回复,还望海涵!由于笔者的水平有限,文中难免有错误和纰漏,也热诚欢迎读者朋友不吝指正!
5. MFC规则DLL
5.1 概述
MFC规则DLL的概念体现在两方面:
(1) 它是MFC的
“是MFC的”意味着可以在这种DLL的内部使用MFC;
(2) 它是规则的
“是规则的”意味着它不同于MFC扩展DLL,在MFC规则DLL的内部虽然可以使用MFC,但是其与应用程序的接口不能是MFC。而MFC扩展DLL与应用程序的接口可以是MFC,可以从MFC扩展DLL中导出一个MFC类的派生类。
Regular DLL能够被所有支持DLL技术的语言所编写的应用程序调用,当然也包括使用MFC的应用程序。在这种动态连接库中,包含一个从CWinApp继承下来的类,DllMain函数则由MFC自动提供。
Regular DLL分为两类:
(1)静态链接到MFC 的规则DLL
静态链接到MFC的规则DLL与MFC库(包括MFC扩展 DLL)静态链接,将MFC库的代码直接生成在.dll文件中。在调用这种DLL的接口时,MFC使用DLL的资源。因此,在静态链接到MFC 的规则DLL中不需要进行模块状态的切换。
使用这种方法生成的规则DLL其程序较大,也可能包含重复的代码。
(2)动态链接到MFC 的规则DLL
动态链接到MFC 的规则DLL 可以和使用它的可执行文件同时动态链接到 MFC DLL 和任何MFC扩展 DLL。在使用了MFC共享库的时候,默认情况下,MFC使用主应用程序的资源句柄来加载资源模板。这样,当DLL和应用程序中存在相同ID的资源时(即所谓的资源重复问题),系统可能不能获得正确的资源。因此,对于共享MFC DLL的规则DLL,我们必须进行模块切换以使得MFC能够找到正确的资源模板。
我们可以在Visual C++中设置MFC规则DLL是静态链接到MFC DLL还是动态链接到MFC DLL。如图8,依次选择Visual C++的project -> Settings -> General菜单或选项,在Microsoft Foundation Classes中进行设置。
图8 设置动态/静态链接MFC DLL
|
5.2 MFC规则DLL的创建
我们来一步步讲述使用MFC向导创建MFC规则DLL的过程,首先新建一个project,如图9,选择project的类型为MFC AppWizard(dll)。点击OK进入如图10所示的对话框。
图9 MFC DLL工程的创建
|
图10所示对话框中的1区选择MFC DLL的类别。
|
2区选择是否支持automation(自动化)技术, automation 允许用户在一个应用程序中操纵另外一个应用程序或组件。例如,我们可以在应用程序中利用 Microsoft Word 或Microsoft Excel的工具,而这种使用对用户而言是透明的。自动化技术可以大大简化和加快应用程序的开发。
3区选择是否支持Windows Sockets,当选择此项目时,应用程序能在 TCP/IP 网络上进行通信。 CWinApp派生类的InitInstance成员函数会初始化通讯端的支持,同时工程中的StdAfx.h文件会自动include 头文件。
添加socket通讯支持后的InitInstance成员函数如下:
BOOL CRegularDllSocketApp::InitInstance()
{ if (!AfxSocketInit()) { AfxMessageBox(IDP_SOCKETS_INIT_FAILED); return FALSE; } return TRUE; } |
4区选择是否由MFC向导自动在源代码中添加注释,一般我们选择“Yes,please”。
图10 MFC DLL的创建选项
|
5.3 一个简单的MFC规则DLL
这个DLL的例子(属于静态链接到MFC 的规则DLL)中提供了一个如图11所示的对话框。
图11 MFC规则DLL例子
|
在DLL中添加对话框的方式与在MFC应用程序中是一样的。 在图11所示DLL中的对话框的Hello按钮上点击时将MessageBox一个“Hello,pconline的网友”对话框,下面是相关的文件及源代码,其中删除了MFC向导自动生成的绝大多数注释(下载本工程):
第一组文件:CWinApp继承类的声明与实现
// RegularDll.h : main header file for the REGULARDLL DLL
#if !defined(AFX_REGULARDLL_H__3E9CB22B_588B_4388_B778_B3416ADB79B3__INCLUDED_) #define AFX_REGULARDLL_H__3E9CB22B_588B_4388_B778_B3416ADB79B3__INCLUDED_ #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 #ifndef __AFXWIN_H__ #error include ’stdafx.h’ before including this file for PCH #endif #include "resource.h" // main symbols class CRegularDllApp : public CWinApp { public: CRegularDllApp(); DECLARE_MESSAGE_MAP() }; #endif // RegularDll.cpp : Defines the initialization routines for the DLL. #include "stdafx.h" #include "RegularDll.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif BEGIN_MESSAGE_MAP(CRegularDllApp, CWinApp) END_MESSAGE_MAP() / // CRegularDllApp construction CRegularDllApp::CRegularDllApp() { } / // The one and only CRegularDllApp object CRegularDllApp theApp; |
分析:
在这一组文件中定义了一个继承自CWinApp的类CRegularDllApp,并同时定义了其的一个实例theApp。乍一看,您会以为它是一个MFC应用程序,因为MFC应用程序也包含这样的在工程名后添加“App”组成类名的类(并继承自CWinApp类),也定义了这个类的一个全局实例theApp。
我们知道,在MFC应用程序中CWinApp取代了SDK程序中WinMain的地位,SDK程序WinMain所完成的工作由CWinApp的三个函数完成:
virtual BOOL InitApplication( );
virtual BOOL InitInstance( ); virtual BOOL Run( ); //传说中MFC程序的“活水源头” |
但是MFC规则DLL并不是MFC应用程序,它所继承自CWinApp的类不包含消息循环。这是因为,MFC规则DLL不包含CWinApp::Run机制,主消息泵仍然由应用程序拥有。如果DLL 生成无模式对话框或有自己的主框架窗口,则应用程序的主消息泵必须调用从DLL 导出的函数来调用PreTranslateMessage成员函数。
另外,MFC规则DLL与MFC 应用程序中一样,需要将所有 DLL中元素的初始化放到InitInstance 成员函数中。
第二组文件 自定义对话框类声明及实现
#if !defined(AFX_DLLDIALOG_H__CEA4C6AF_245D_48A6_B11A_A5521EAD7C4E__INCLUDED_)
#define AFX_DLLDIALOG_H__CEA4C6AF_245D_48A6_B11A_A5521EAD7C4E__INCLUDED_ #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 // DllDialog.h : header file / // CDllDialog dialog class CDllDialog : public CDialog { // Construction public: CDllDialog(CWnd* pParent = NULL); // standard constructor enum { IDD = IDD_DLL_DIALOG }; protected: virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support // Implementation protected: afx_msg void OnHelloButton(); DECLARE_MESSAGE_MAP() }; #endif // DllDialog.cpp : implementation file #include "stdafx.h" #include "RegularDll.h" #include "DllDialog.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif / // CDllDialog dialog CDllDialog::CDllDialog(CWnd* pParent /*=NULL*/) : CDialog(CDllDialog::IDD, pParent) {} void CDllDialog::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); } BEGIN_MESSAGE_MAP(CDllDialog, CDialog) ON_BN_CLICKED(IDC_HELLO_BUTTON, OnHelloButton) END_MESSAGE_MAP() / // CDllDialog message handlers void CDllDialog::OnHelloButton() { MessageBox("Hello,pconline的网友","pconline"); } |
分析:
这一部分的编程与一般的应用程序根本没有什么不同,我们照样可以利用MFC类向导来自动为对话框上的控件添加事件。MFC类向导照样会生成类似ON_BN_CLICKED(IDC_HELLO_BUTTON, OnHelloButton)的消息映射宏。
第三组文件 DLL中的资源文件
//{{NO_DEPENDENCIES}}
// Microsoft Developer Studio generated include file. // Used by RegularDll.rc // #define IDD_DLL_DIALOG 1000 #define IDC_HELLO_BUTTON 1000 |
分析:
在MFC规则DLL中使用资源也与在MFC应用程序中使用资源没有什么不同,我们照样可以用Visual C++的资源编辑工具进行资源的添加、删除和属性的更改。
第四组文件 MFC规则DLL接口函数
#include "StdAfx.h"
#include "DllDialog.h" extern "C" __declspec(dllexport) void ShowDlg(void) { CDllDialog dllDialog; dllDialog.DoModal(); } |
分析:
这个接口并不使用MFC,但是在其中却可以调用MFC扩展类CdllDialog的函数,这体现了“规则”的概类。
与非MFC DLL完全相同,我们可以使用__declspec(dllexport)声明或在.def中引出的方式导出MFC规则DLL中的接口。
5.4 MFC规则DLL的调用
笔者编写了如图12的对话框MFC程序(下载本工程)来调用5.3节的MFC规则DLL,在这个程序的对话框上点击“调用DLL”按钮时弹出5.3节MFC规则DLL中的对话框。
图12 MFC规则DLL的调用例子
|
下面是“调用DLL”按钮单击事件的消息处理函数:
void CRegularDllCallDlg::OnCalldllButton()
{ typedef void (*lpFun)(void); HINSTANCE hDll; //DLL句柄 hDll = LoadLibrary("RegularDll.dll"); if (NULL==hDll) { MessageBox("DLL加载失败"); } lpFun addFun; //函数指针 lpFun pShowDlg = (lpFun)GetProcAddress(hDll,"ShowDlg"); if (NULL==pShowDlg) { MessageBox("DLL中函数寻找失败"); } pShowDlg(); } |
上述例子中给出的是显示调用的方式,可以看出,其调用方式与第4节中非MFC DLL的调用方式没有什么不同。
我们照样可以在EXE程序中隐式调用MFC规则DLL,只需要将DLL工程生成的.lib文件和.dll文件拷入当前工程所在的目录,并在RegularDllCallDlg.cpp文件(图12所示对话框类的实现文件)的顶部添加:
#pragma comment(lib,"RegularDll.lib")
void ShowDlg(void); |
并将void CRegularDllCallDlg::OnCalldllButton() 改为:
void CRegularDllCallDlg::OnCalldllButton()
{ ShowDlg(); } |
5.5 共享MFC DLL的规则DLL的模块切换
应用程序进程本身及其调用的每个DLL模块都具有一个全局唯一的HINSTANCE句柄,它们代表了DLL或EXE模块在进程虚拟空间中的起始地址。进程本身的模块句柄一般为0x400000,而DLL模块的缺省句柄为0x10000000。如果程序同时加载了多个DLL,则每个DLL模块都会有不同的HINSTANCE。应用程序在加载DLL时对其进行了重定位。
共享MFC DLL(或MFC扩展DLL)的规则DLL涉及到HINSTANCE句柄问题,HINSTANCE句柄对于加载资源特别重要。EXE和DLL都有其自己的资源,而且这些资源的ID可能重复,应用程序需要通过资源模块的切换来找到正确的资源。如果应用程序需要来自于DLL的资源,就应将资源模块句柄指定为DLL的模块句柄;如果需要EXE文件中包含的资源,就应将资源模块句柄指定为EXE的模块句柄。
这次我们创建一个动态链接到MFC DLL的规则DLL(下载本工程),在其中包含如图13的对话框。
图13 DLL中的对话框
|
另外,在与这个DLL相同的工作区中生成一个基于对话框的MFC程序,其对话框与图12完全一样。但是在此工程中我们另外添加了一个如图14的对话框。
图14 EXE中的对话框
|
图13和图14中的对话框除了caption不同(以示区别)以外,其它的都相同。
尤其值得特别注意,在DLL和EXE中我们对图13和图14的对话框使用了相同的资源ID=2000,在DLL和EXE工程的resource.h中分别有如下的宏:
//DLL中对话框的ID
#define IDD_DLL_DIALOG 2000 //EXE中对话框的ID #define IDD_EXE_DIALOG 2000 |
与5.3节静态链接MFC DLL的规则DLL相同,我们还是在规则DLL中定义接口函数ShowDlg,原型如下:
#include "StdAfx.h"
#include "SharedDll.h" void ShowDlg(void) { CDialog dlg(IDD_DLL_DIALOG); //打开ID为2000的对话框 dlg.DoModal(); } |
而为应用工程主对话框的“调用DLL”的单击事件添加如下消息处理函数:
void CSharedDllCallDlg::OnCalldllButton()
{ ShowDlg(); } |
我们以为单击“调用DLL”会弹出如图13所示DLL中的对话框,可是可怕的事情发生了,我们看到是图14所示EXE中的对话框!
惊讶?
产生这个问题的根源在于应用程序与MFC规则DLL共享MFC DLL(或MFC扩展DLL)的程序总是默认使用EXE的资源,我们必须进行资源模块句柄的切换,其实现方法有三:
方法一 在DLL接口函数中使用:
AFX_MANAGE_STATE(AfxGetStaticModuleState()); |
我们将DLL中的接口函数ShowDlg改为:
void ShowDlg(void) { //方法1:在函数开始处变更,在函数结束时恢复 //将AFX_MANAGE_STATE(AfxGetStaticModuleState());作为接口函数的第一//条语句进行模块状态切换 AFX_MANAGE_STATE(AfxGetStaticModuleState()); CDialog dlg(IDD_DLL_DIALOG);//打开ID为2000的对话框 dlg.DoModal(); } |
这次我们再点击EXE程序中的“调用DLL”按钮,弹出的是DLL中的如图13的对话框!嘿嘿,弹出了正确的对话框资源。
AfxGetStaticModuleState是一个函数,其原型为:
AFX_MODULE_STATE* AFXAPI AfxGetStaticModuleState( ); |
该函数的功能是在栈上(这意味着其作用域是局部的)创建一个AFX_MODULE_STATE类(模块全局数据也就是模块状态)的实例,对其进行设置,并将其指针pModuleState返回。
AFX_MODULE_STATE类的原型如下:
// AFX_MODULE_STATE (global data for a module) class AFX_MODULE_STATE : public CNoTrackObject { public: #ifdef _AFXDLL AFX_MODULE_STATE(BOOL bDLL, WNDPROC pfnAfxWndProc, DWORD dwVersion); AFX_MODULE_STATE(BOOL bDLL, WNDPROC pfnAfxWndProc, DWORD dwVersion,BOOL bSystem); #else AFX_MODULE_STATE(BOOL bDLL); #endif ~AFX_MODULE_STATE(); CWinApp* m_pCurrentWinApp; HINSTANCE m_hCurrentInstanceHandle; HINSTANCE m_hCurrentResourceHandle; LPCTSTR m_lpszCurrentAppName; … //省略后面的部分 } |
AFX_MODULE_STATE类利用其构造函数和析构函数进行存储模块状态现场及恢复现场的工作,类似汇编中call指令对pc指针和sp寄存器的保存与恢复、中断服务程序的中断现场压栈与恢复以及操作系统线程调度的任务控制块保存与恢复。
许多看似不着边际的知识点居然有惊人的相似!
AFX_MANAGE_STATE是一个宏,其原型为:
AFX_MANAGE_STATE( AFX_MODULE_STATE* pModuleState ) |
该宏用于将pModuleState设置为当前的有效模块状态。当离开该宏的作用域时(也就离开了pModuleState所指向栈上对象的作用域),先前的模块状态将由AFX_MODULE_STATE的析构函数恢复。
方法二 在DLL接口函数中使用:
AfxGetResourceHandle(); AfxSetResourceHandle(HINSTANCE xxx); |
AfxGetResourceHandle用于获取当前资源模块句柄,而AfxSetResourceHandle则用于设置程序目前要使用的资源模块句柄。
我们将DLL中的接口函数ShowDlg改为:
void ShowDlg(void) { //方法2的状态变更 HINSTANCE save_hInstance = AfxGetResourceHandle(); AfxSetResourceHandle(theApp.m_hInstance); CDialog dlg(IDD_DLL_DIALOG);//打开ID为2000的对话框 dlg.DoModal(); //方法2的状态还原 AfxSetResourceHandle(save_hInstance); } |
通过AfxGetResourceHandle和AfxSetResourceHandle的合理变更,我们能够灵活地设置程序的资源模块句柄,而方法一则只能在DLL接口函数退出的时候才会恢复模块句柄。方法二则不同,如果将ShowDlg改为:
extern CSharedDllApp theApp; //需要声明theApp外部全局变量 void ShowDlg(void) { //方法2的状态变更 HINSTANCE save_hInstance = AfxGetResourceHandle(); AfxSetResourceHandle(theApp.m_hInstance); CDialog dlg(IDD_DLL_DIALOG);//打开ID为2000的对话框 dlg.DoModal(); //方法2的状态还原 AfxSetResourceHandle(save_hInstance); //使用方法2后在此处再进行操作针对的将是应用程序的资源 CDialog dlg1(IDD_DLL_DIALOG); //打开ID为2000的对话框 dlg1.DoModal(); } |
在应用程序主对话框的“调用DLL”按钮上点击,将看到两个对话框,相继为DLL中的对话框(图13)和EXE中的对话框(图14)。
方法三 由应用程序自身切换
资源模块的切换除了可以由DLL接口函数完成以外,由应用程序自身也能完成(下载本工程)。
现在我们把DLL中的接口函数改为最简单的:
void ShowDlg(void) { CDialog dlg(IDD_DLL_DIALOG); //打开ID为2000的对话框 dlg.DoModal(); } |
而将应用程序的OnCalldllButton函数改为:
void CSharedDllCallDlg::OnCalldllButton() { //方法3:由应用程序本身进行状态切换 //获取EXE模块句柄 HINSTANCE exe_hInstance = GetModuleHandle(NULL); //或者HINSTANCE exe_hInstance = AfxGetResourceHandle(); //获取DLL模块句柄 HINSTANCE dll_hInstance = GetModuleHandle("SharedDll.dll"); AfxSetResourceHandle(dll_hInstance); //切换状态 ShowDlg(); //此时显示的是DLL的对话框 AfxSetResourceHandle(exe_hInstance); //恢复状态 //资源模块恢复后再调用ShowDlg ShowDlg(); //此时显示的是EXE的对话框 } |
方法三中的Win32函数GetModuleHandle可以根据DLL的文件名获取DLL的模块句柄。如果需要得到EXE模块的句柄,则应调用带有Null参数的GetModuleHandle。
方法三与方法二的不同在于方法三是在应用程序中利用AfxGetResourceHandle和AfxSetResourceHandle进行资源模块句柄切换的。同样地,在应用程序主对话框的“调用DLL”按钮上点击,也将看到两个对话框,相继为DLL中的对话框(图13)和EXE中的对话框(图14)。
在下一节我们将对MFC扩展DLL进行详细分析和实例讲解,欢迎您继续关注本系列连载。
前文我们对非MFC DLL和MFC规则DLL进行了介绍,现在开始详细分析DLL的最后一种类型――MFC扩展DLL。
6.1概论
MFC扩展DLL与MFC规则DLL的相同点在于在两种DLL的内部都可以使用MFC类库,其不同点在于MFC扩展DLL与应用程序的接口可以是MFC的。MFC扩展DLL的含义在于它是MFC的扩展,其主要功能是实现从现有MFC库类中派生出可重用的类。MFC扩展DLL使用MFC 动态链接库版本,因此只有用共享MFC 版本生成的MFC 可执行文件(应用程序或规则DLL)才能使用MFC扩展DLL。
从前文可知,MFC规则DLL被MFC向导自动添加了一个CWinApp的对象,而MFC扩展DLL则不包含该对象,它只是被自动添加了DllMain 函数。对于MFC扩展DLL,开发人员必须在DLL的DllMain函数中添加初始化和结束代码。
从下表我们可以看出三种DLL对DllMain入口函数的不同处理方式:
DLL类型 | 入口函数 |
非 MFC DLL | 编程者提供DllMain函数 |
MFC规则 DLL | CWinApp对象的InitInstance 和 ExitInstance |
MFC扩展 DLL | MFC DLL向导生成DllMain 函数 |
对于MFC扩展DLL,系统会自动在工程中添加如下表所示的宏,这些宏为DLL和应用程序的编写提供了方便。像AFX_EXT_CLASS、AFX_EXT_API、AFX_EXT_DATA这样的宏,在DLL和应用程序中将具有不同的定义,这取决于_AFXEXT宏是否被定义。这使得在DLL和应用程序中,使用统一的一个宏就可以表示出输出和输入的不同意思。在DLL中,表示输出(因为_AFXEXT被定义,通常是在编译器的标识参数中指定/D_AFXEXT);在应用程序中,则表示输入(_AFXEXT没有定义)。
宏 | 定义 |
AFX_CLASS_IMPORT | __declspec(dllexport) |
AFX_API_IMPORT | __declspec(dllexport) |
AFX_DATA_IMPORT | __declspec(dllexport) |
AFX_CLASS_EXPORT | __declspec(dllexport) |
AFX_API_EXPORT | __declspec(dllexport) |
AFX_DATA_EXPORT | __declspec(dllexport) |
AFX_EXT_CLASS | #ifdef _AFXEXT
AFX_CLASS_EXPORT #else AFX_CLASS_IMPORT |
AFX_EXT_API | #ifdef _AFXEXT
AFX_API_EXPORT #else AFX_API_IMPORT |
AFX_EXT_DATA | #ifdef _AFXEXT
AFX_DATA_EXPORT #else AFX_DATA_IMPORT |
6.2 MFC扩展DLL导出MFC派生类
在这个例子中,我们将产生一个名为“ExtDll”的MFC扩展DLL工程,在这个DLL中导出一个对话框类,这个对话框类派生自MFC类CDialog。
使用MFC向导生成MFC扩展DLL时,系统会自动添加如下代码:
static AFX_EXTENSION_MODULE ExtDllDLL = { NULL, NULL };
extern "C" int APIENTRY DllMain( HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved ) { // Remove this if you use lpReserved UNREFERENCED_PARAMETER( lpReserved ); //说明:lpReserved是一个被系统所保留的参数,对于隐式链接是一个非零值,对于显式链接值是零 if (dwReason == DLL_PROCESS_ATTACH) { TRACE0( "EXTDLL.DLL Initializing!/n" ); // Extension DLL one-time initialization if ( !AfxInitExtensionModule( ExtDllDLL, hInstance )) return 0; // Insert this DLL into the resource chain new CDynLinkLibrary( ExtDllDLL ); } else if (dwReason == DLL_PROCESS_DETACH) { TRACE0( "EXTDLL.DLL Terminating!/n" ); // Terminate the library before destructors are called AfxTermExtensionModule( ExtDllDLL ); } return 1; // ok } |
这一段代码含义晦涩,我们需要对其进行解读:
(1)上述代码完成MFC扩展DLL的初始化和终止处理;
(2)初始化期间所创建的 CDynLinkLibrary 对象使MFC扩展 DLL 可以将 DLL中的CRuntimeClass 对象或资源导出到应用程序;
(3)AfxInitExtensionModule函数捕获模块的CRuntimeClass 结构和在创建 CDynLinkLibrary 对象时使用的对象工厂(COleObjectFactory 对象);
(4)AfxTermExtensionModule函数使 MFC 得以在每个进程与扩展 DLL 分离时(进程退出或使用AfxFreeLibrary卸载DLL时)清除扩展 DLL;
(5)第一条语句static AFX_EXTENSION_MODULE ExtDllDLL = { NULL, NULL };定义了一个AFX_EXTENSION_MODULE类的静态全局对象,AFX_EXTENSION_MODULE的定义如下:
struct AFX_EXTENSION_MODULE
{ BOOL bInitialized; HMODULE hModule; HMODULE hResource; CRuntimeClass* pFirstSharedClass; COleObjectFactory* pFirstSharedFactory; }; |
由AFX_EXTENSION_MODULE的定义我们可以更好的理解(2)、(3)、(4)点。
在资源编辑器中添加一个如图15所示的对话框,并使用MFC类向导为其添加一个对应的类CExtDialog,系统自动添加了ExtDialog.h和ExtDialog.cpp两个头文件。
图15 MFC扩展DLL中的对话框
|
修改ExtDialog.h中CExtDialog类的声明为:
class AFX_EXT_CLASS CExtDialog : public CDialog
{ public: CExtDialog( CWnd* pParent = NULL ); enum { IDD = IDD_DLL_DIALOG }; protected: virtual void DoDataExchange( CDataExchange* pDX ); DECLARE_MESSAGE_MAP() }; |
这其中最主要的改变是我们在class AFX_EXT_CLASS CExtDialog语句中添加了“AFX_EXT_CLASS”宏,则使得DLL中的CExtDialog类被导出。
6.3 MFC扩展DLL的加载
6.3.1 隐式加载
我们在6.2工程所在的工作区中添加一个LoadExtDllDlg工程,用于演示MFC扩展DLL的加载。在LoadExtDllDlg工程中添加一个如图16所示的对话框,这个对话框上包括一个“调用DLL”按钮。
图16 MFC扩展DLL调用工程中的对话框
|
在与图16对应对话框类实现文件的头部添加:
// LoadExtDllDlg.cpp : implementation file
// #include "../ExtDialog.h" #pragma comment( lib, "ExtDll.lib" ) 而“调用DLL”按钮的单击事件的消息处理函数为: void CLoadExtDllDlg::OnDllcallButton() { CExtDialog extDialog; extDialog.DoModal(); } |
当我们单击“调用DLL”的时候,弹出了如图15的对话框。
为提供给用户隐式加载(MFC扩展DLL一般使用隐式加载,具体原因见下节),MFC扩展DLL需要提供三个文件:
(1)描述DLL中扩展类的头文件;
(2)与动态链接库对应的.LIB文件;
(3)动态链接库.DLL文件本身。
有了这三个文件,应用程序的开发者才可充分利用MFC扩展DLL。
6.3.2 显示加载
显示加载MFC扩展DLL应使用MFC全局函数AfxLoadLibrary而不是WIN32 API中的LoadLibrary。AfxLoadLibrary 最终也调用了 LoadLibrary这个API,但是在调用之前进行了线程同步的处理。
AfxLoadLibrary 的函数原型与 LoadLibrary完全相同,为:
HINSTANCE AFXAPI AfxLoadLibrary( LPCTSTR lpszModuleName ); |
与之相对应的是,MFC 应用程序应使用AfxFreeLibrary 而非FreeLibrary 卸载MFC扩展DLL。AfxFreeLibrary的函数原型也与 FreeLibrary完全相同,为:
BOOL AFXAPI AfxFreeLibrary( HINSTANCE hInstLib ); |
如果我们把上例中的“调用DLL”按钮单击事件的消息处理函数改为:
void CLoadExtDllDlg::OnDllcallButton()
{ HINSTANCE hDll = AfxLoadLibrary( "ExtDll.dll" ); if(NULL == hDll) { AfxMessageBox( "MFC扩展DLL动态加载失败" ); return; } CExtDialog extDialog; extDialog.DoModal(); AfxFreeLibrary(hDll); } |
则工程会出现link错误:
LoadExtDllDlg.obj : error LNK2001: unresolved external symbol "__declspec(dllimport) public: virtual __thiscall CExtDialog::~CExtDialog(void)" (__imp_??1CExtDialog@@UAE@XZ)
LoadExtDllDlg.obj : error LNK2001: unresolved external symbol "__declspec(dllimport) public: __thiscall CExtDialog::CExtDialog(class CWnd *)" (__imp_??0CExtDialog@@QAE@PAVCWnd@@@Z) |
提示CExtDialog的构造函数和析构函数均无法找到!是的,对于派生MFC类的MFC扩展DLL,当我们要在应用程序中使用DLL中定义的派生类时,我们不宜使用动态加载DLL的方法。
6.4 MFC扩展DLL加载MFC扩展DLL
我们可以在MFC扩展DLL中再次使用MFC扩展DLL,但是,由于在两个DLL中对于AFX_EXT_CLASS、AFX_EXT_API、AFX_EXT_DATA宏的定义都是输出,这会导致调用的时候出现问题。
我们将会在调用MFC扩展DLL的DLL中看到link错误:
error LNK2001: unresolved external symbol …....... |
因此,在调用MFC扩展DLL的MFC扩展DLL中,在包含被调用DLL的头文件之前,需要临时重新定义AFX_EXT_CLASS的值。下面的例子显示了如何实现:
//临时改变宏的含义“输出”为“输入”
#undef AFX_EXT_CLASS #undef AFX_EXT_API #undef AFX_EXT_DATA #define AFX_EXT_CLASS AFX_CLASS_IMPORT #define AFX_EXT_API AFX_API_IMPORT #define AFX_EXT_DATA AFX_DATA_IMPORT //包含被调用MFC扩展DLL的头文件 #include "CalledDLL.h" //恢复宏的含义为输出 #undef AFX_EXT_CLASS #undef AFX_EXT_API #undef AFX_EXT_DATA #define AFX_EXT_CLASS AFX_CLASS_EXPORT #define AFX_EXT_API AFX_API_EXPORT #define AFX_EXT_DATA AFX_DATA_EXPORT |
6.5 MFC扩展DLL导出函数和变量
MFC扩展DLL导出函数和变量的方法也十分简单,下面我们给出一个简单的例子。
我们在MFC向导生成的MFC扩展DLL工程中添加gobal.h和global.cpp两个文件:
//global.h:MFC扩展DLL导出变量和函数的声明
extern "C" { int AFX_EXT_DATA total; //导出变量 int AFX_EXT_API add( int x, int y ); //导出函数 } //global.cpp:MFC扩展DLL导出变量和函数定义 #include "StdAfx.h" #include "global.h" extern "C" int total; int add(int x,int y) { total = x + y; return total; } |
编写一个简单的控制台程序来调用这个MFC扩展DLL:
#include <iostream.h>
#include <afxver_.h> //AFX_EXT_DATA、AFX_EXT_API宏的定义在afxver_.h头文件中 #pragma comment ( lib, "ExtDll.lib" ) #include "../global.h" int main(int argc, char* argv[]) { cout << add(2,3) << endl; cout << total; return 0; } |
运行程序,在控制台上看到:
5
5
另外,在Visual C++下建立MFC扩展DLL时,MFC DLL向导会自动生成.def文件。因此,对于函数和变量,我们除了可以利用AFX_EXT_DATA、AFX_EXT_API宏导出以外,在.def文件中定义导出也是一个很好的办法。与之相比,在.def文件中导出类却较麻烦。通常需要从工程生成的.map文件中获得类的所有成员函数被C++编译器更改过的标识符,并且在.def文件中导出这些“奇怪”的标识符。因此,MFC扩展DLL通常以AFX_EXT_CLASS宏直接声明导出类。
6.6 MFC扩展DLL的应用
上述各小节所举MFC扩展DLL的例子均只是为了说明某方面的问题,没有真实地体现“MFC扩展” 的内涵,譬如6.2派生自CDialog的类也不具备比CDialog更强的功能。MFC扩展DLL的真实内涵体现在它提供的类虽然派生自MFC类,但是提供了比MFC类更强大的功能、更丰富的接口。下面我们来看一个具体的例子(单击此处下载本工程)。
我们知道static控件所对应的CStatic类不具备设置背景和文本颜色的接口,这使得我们不能在对话框或其它用户界面上自由灵活地修改static控件的颜色风格,因此我们需要一个提供了SetBackColor和SetTextColor接口的CStatic派生类CMultiColorStatic。
这个类的声明如下:
class AFX_EXT_CLASS CMultiColorStatic : public CStatic
{ // Construction public: CMultiColorStatic(); virtual ~CMultiColorStatic(); // Attributes protected: CString m_strCaption; COLORREF m_BackColor; COLORREF m_TextColor; // Operations public: void SetTextColor( COLORREF TextColor ); void SetBackColor( COLORREF BackColor ); void SetCaption( CString strCaption ); // Generated message map functions protected: afx_msg void OnPaint(); DECLARE_MESSAGE_MAP() }; |
在这个类的实现文件中,我们需要为它提供WM_PAINT消息的处理函数(这是因为颜色的设置依赖于WM_PAINT消息):
BEGIN_MESSAGE_MAP(CMultiColorStatic, CStatic)
//{{AFX_MSG_MAP(CMultiColorStatic) ON_WM_PAINT() //为这个类定义WM_PAINT消息处理函数 //}}AFX_MSG_MAP END_MESSAGE_MAP() |
下面是这个类中的重要成员函数:
//为CMultiColorStatic类添加“设置文本颜色”接口
void CMultiColorStatic::SetTextColor( COLORREF TextColor ) { m_TextColor = TextColor; //设置文字颜色 } //为CMultiColorStatic类添加“设置背景颜色”接口 void CMultiColorStatic::SetBackColor( COLORREF BackColor ) { m_BackColor = BackColor; //设置背景颜色 } //为CMultiColorStatic类添加“设置标题”接口 void CMultiColorStatic::SetCaption( CString strCaption ) { m_strCaption = strCaption; } //重画Static,颜色和标题的设置都依赖于这个函数 void CMultiColorStatic::OnPaint() { CPaintDC dc(this); // device context for painting CRect rect; GetClientRect( &rect ); dc.SetBkColor( m_BackColor ); dc.SetBkMode( TRANSPARENT ); CFont *pFont = GetParent()->GetFont();//得到父窗体的字体 CFont *pOldFont; pOldFont = dc.SelectObject( pFont );//选用父窗体的字体 dc.SetTextColor( m_TextColor );//设置文本颜色 dc.DrawText( m_strCaption, &rect, DT_CENTER );//文本在Static中央 dc.SelectObject( pOldFont ); } |
为了验证CMultiColorStatic类,我们制作一个基于对话框的应用程序,它包含一个如图17所示的对话框。该对话框上包括一个static控件和三个按钮,这三个按钮可分别把static控件设置为“红色”、“蓝色”和“绿色”。
图17 扩展的CStatic类调用演示
|
下面看看应如何编写与这个对话框对应的类。
包含这种Static的对话框类的声明如下:
#include "../MultiColorStatic.h"
#pragma comment ( lib, "ColorStatic.lib" ) // CCallDllDlg dialog class CCallDllDlg : public CDialog { public: CCallDllDlg(CWnd* pParent = NULL); // standard constructor enum { IDD = IDD_CALLDLL_DIALOG }; CMultiColorStatic m_colorstatic; //包含一个CMultiColorStatic的实例 protected: virtual void DoDataExchange(CDataExchange* pDX);//DDX/DDV support HICON m_hIcon; // Generated message map functions //{{AFX_MSG(CCallDllDlg) virtual BOOL OnInitDialog(); afx_msg void OnSysCommand(UINT nID, LPARAM lParam); afx_msg void OnPaint(); afx_msg HCURSOR OnQueryDragIcon(); afx_msg void OnRedButton(); afx_msg void OnBlueButton(); afx_msg void OnGreenButton(); //}}AFX_MSG DECLARE_MESSAGE_MAP() }; |
下面是这个类中与使用CMultiColorStatic相关的主要成员函数:
void CCallDllDlg::DoDataExchange(CDataExchange* pDX)
{ CDialog::DoDataExchange(pDX); //{{AFX_DATA_MAP(CCallDllDlg) DDX_Control(pDX, IDC_COLOR_STATIC, m_colorstatic); //使m_colorstatic与IDC_COLOR_STATIC控件关联 //}}AFX_DATA_MAP } BOOL CCallDllDlg::OnInitDialog() { … // TODO: Add extra initialization here // 初始static控件的显示 m_colorstatic.SetCaption("最开始为黑色"); m_colorstatic.SetTextColor(RGB(0,0,0)); return TRUE; // return TRUE unless you set the focus to a control } //设置static控件文本颜色为红色 void CCallDllDlg::OnRedButton() { m_colorstatic.SetCaption( "改变为红色" ); m_colorstatic.SetTextColor( RGB( 255, 0, 0 ) ); Invalidate( TRUE ); //导致发出WM_PAINT消息 } //设置static控件文本颜色为蓝色 void CCallDllDlg::OnBlueButton() { m_colorstatic.SetCaption( "改变为蓝色" ); m_colorstatic.SetTextColor( RGB( 0, 0, 255 ) ); Invalidate( TRUE ); //导致发出WM_PAINT消息 } //设置static控件文本颜色为绿色 void CCallDllDlg::OnGreenButton() { m_colorstatic.SetCaption( "改变为绿色" ); m_colorstatic.SetTextColor( RGB(0,255,0) ); Invalidate( TRUE ); //导致发出WM_PAINT消息 } |
至此,我们已经讲解完成了所有类型的动态链接库,即非MFC DLL、MFC规则DLL和MFC扩展DLL。下一节将给出DLL的三个工程实例,与读者朋友们共同体会DLL的应用范围和使用方法。
DLL典型实例
动态链接库DLL实现了库的共享,体现了代码重用的思想。我们可以把广泛的、具有共性的、能够多次被利用的函数和类定义在库中。这样,在再次使用这些函数和类的时候,就不再需要重新添加与这些函数和类相关的代码。具有共性的问题大致有哪些呢?笔者归纳如下:
(1)通用的算法
图像处理、视频音频解码、压缩与解压缩、加密与解密通常采用某些特定的算法,这些算法较固定且在这类程序中往往经常被使用。
(2)纯资源DLL
我们可以从DLL中获取资源,对于一个支持多种语言的应用程序而言,我们可以判断操作系统的语言,并自动为应用程序加载与OS对应的语言。这是多语言支持应用程序的一般做法。
(3)通信控制DLL
串口、网口的通信控制函数如果由DLL提供则可以使应用程序轻松不少。在工业控制、modem程序甚至socket通信中,经常使用通信控制DLL。
本节将给出DLL的三个典型应用实例。
7.1 算法DLL
我们直接用读者的一个提问作为例子。
宋宝华先生,您好!
我在dev.yesky.com上看到你连载的《VC++动态链接库编程》,觉得非常好。我以前主要是用Delphi的,C/C++学过,对Win32和VCL比较熟悉,但是没有接触过VC++,对MFC很陌生。这段时间和一个同学合作做光学成像的计算机模拟,用到傅立叶变换,手里面有例程是VC++写的。我们的界面是用Delphi开发,需要将其傅立叶变换功能提出做一个DLL供Delphi调用。苦于不懂MFC,试了很多方法,都不成功,最后只得采用折衷方案,简单修改一下程序,传一个参数进去,当作exe来调用,才没有耽搁后续进程。
……
谢谢!
致
礼!
某某
学习过较高级别数学(概率统计与随机过程)、信号与线性系统及数字信号处理的读者应该知道,傅立叶变换是一种在信号分析中常用的算法,用于时域和频域的相互转换。FFT变换算法通用而有共性,我们适宜把它集成在一个DLL中。
随后,这位读者提供了这样的一个函数:
/* 函数名称:FFT() * 参数: * complex<double> * TD - 指向时域数组的指针 * complex<double> * FD - 指向频域数组的指针 * r -2的幂数,即迭代次数 * 返回值: 无。 * 说明:该函数用来实现快速傅立叶变换 */ void FFT(complex<double> * TD, complex<double> * FD, int r) { LONG count; // 傅立叶变换点数 int i,j,k; // 循环变量 int bfsize,p; // 中间变量 double angle; // 角度 complex<double> *W,*X1,*X2,*X; count = 1 << r; //傅立叶变换点数 // 分配运算所需存储器 W = new complex<double>[count / 2]; X1 = new complex<double>[count]; X2 = new complex<double>[count]; // 计算加权系数 for(i = 0; i < count / 2; i++) { angle = -i * PI * 2 / count; W[i] = complex<double> (cos(angle), sin(angle)); } // 将时域点写入X1 memcpy(X1, TD, sizeof(complex<double>) * count); // 采用蝶形算法进行快速傅立叶变换 for(k = 0; k < r; k++) { for(j = 0; j < 1 << k; j++) { bfsize = 1 << (r-k); for(i = 0; i < bfsize / 2; i++) { p = j * bfsize; X2[i + p] = X1[i + p] + X1[i + p + bfsize / 2]; X2[i + p + bfsize / 2] = (X1[i + p] - X1[i + p + bfsize / 2]) * W[i * (1<<k)]; } } X = X1; X1 = X2; X2 = X; } // 重新排序 for(j = 0; j < count; j++) { p = 0; for(i = 0; i < r; i++) { if (j&(1<<i)) { p+=1<<(r-i-1); } } FD[j]=X1[p]; } // 释放内存 delete W; delete X1; delete X2; } |
既然有了FFT这个函数,我们要把它做在DLL中,作为DLL的一个接口将是十分简单的,其步骤如下:
(1)利用MFC向导建立一个非MFC DLL;
(2)在工程中添加fft.h和fft.cpp两个文件;
fft.h的源代码为:
#ifndef FFT_H #define FFT_H #include <complex> using namespace std; extern "C" void __declspec(dllexport) __stdcall FFT(complex<double> * TD, complex<double> * FD, int r); #define PI 3.1415926 #endif fft.cpp的源代码为: /* 文件名:fft.cpp */ #include "fft.h" void __stdcall FFT(complex<double> * TD, complex<double> * FD, int r) { …//读者提供的函数代码 } |
在任何编程语言中使用Win32 API LoadLibrary都可以加载这个DLL,而使用GetProcAddress(hDll, "FFT")则可以获得函数FFT的地址,读者所提到的Delphi当然也不例外。
这个DLL中有两点需要注意:
(1)使用extern "C"修饰函数声明,否则,生成的DLL只能供C++调用;
(2)使用__stdcall修饰函数声明及定义,__stdcall是Windows API的函数调用方式。
7.2纯资源DLL
我们在应用程序中产生如图18所示的资源(对话框),单击此处下载本工程。
图18 中文对话框
|
在与这个应用程序相同的工作区里利用MFC向导建立两个简单的DLL,把应用工程中的资源全选后分别拷贝到ChineseDll和EngLishDll,在EnglishDll工程的资源文件中搜索下面的语句:
/
// Chinese (P.R.C.) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_CHS) #ifdef _WIN32 LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED #pragma code_page(936) #endif //_WIN32 |
将其改为:
/
// English (U.S.) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) #ifdef _WIN32 LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US #pragma code_page(1252) #endif //_WIN32 |
并将其中所有的中文翻译为英文。这个DLL为我们提供了如图19所示的对话框资源。
图19英文对话框
|
修改应用工程的InitInstance()函数,在
CResourceDllCallDlg dlg;
m_pMainWnd = &dlg; int nResponse = dlg.DoModal(); |
之前(即对话框显示之前)添加如下代码:
//获取操作系统的语言
WORD wLangPID = PRIMARYLANGID( GetSystemDefaultLangID() ); if( LANG_CHINESE == wLangPID ) { hLanguageDll = LoadLibrary( "ChineseDll.dll" ); //加载中文资源 } else { hLanguageDll = LoadLibrary( "EnglishDll.dll" ); //加载英文资源 } if( NULL == hLanguageDll ) { AfxMessageBox( "Load DLL failure" ); return FALSE; } AfxSetResourceHandle( hLanguageDll ); //设置当前的资源句柄 |
这样的应用程序将具有自适应性质,在中文OS中显示中文资源,在英文OS中则显示英文资源。
7.3通信控制DLL
我们在这里举一个串口通信类的例子。
也许您需要了解一点串口通信的背景知识,其实串口到处都看得到,譬如PC机的COM口即为串行通讯口(简称串口)。如图20,打开Windows的设备管理器,我们看到了COM口。
在Windows系统,需通过DCB(Device Control Block)对串口进行配置。利用Windows API GetCommState函数可以获取串口当前配置;利用SetCommState函数则可以设置串口通讯的参数。
串行通信通常按以下四步进行:
(1)打开串口;
(2)配置串口;
(3)数据传送;
(4)关闭串口。
图20 PC的串口
|
由此可见,我们需要给串口控制DLL提供如下四个接口函数:
//打开指定的串口,其参数port为端口号
BOOL ComOpen(int port); //在这个函数里使用默认的参数设置串口 //将打开的串口关闭 void ComClose(int port); //将串口接收缓冲区中的数据放到buffer中 int GetComData(char *buf, int buf_len); //将指定长度的数据发送到串口 int SendDataToCom(LPBYTE buf,int buf_Len); |
下面给出了DLL接口的主要源代码框架:
//com.h:com类通信接口
class AFX_EXT_CLASS com { public: ComOpen(int port) { … } int SendDataToCom(LPBYTE buf,int buf_Len) { … } int GetComData(char *buf, int buf_len) { … } void ComClose() { … } } |
我们编写一控制台程序来演示DLL的调用:
#include <iostream>
#include <exception> using namespace std; #include <windows.h> #include "com.h" //包含DLL中导出类的头文件 int main(int argc, char *argv[]) { try { char str[] = "com_class test"; com com1; com1.ComOpen (1); for(int i=0; i<100; i++) //以同步方式写com的buffer { Sleep(500); com1.SendDataToCom (str,strlen(str)); } com1.ComClose (); } catch(exception &e) { cout << e.what() << endl; } return 0; } |
DLL的编写与调用方法及主要应用皆已讲完,在下一节里,我们将看到比较“高深”的主题――DLL木马。曾几何时,DLL木马成为了病毒的一种十分重要的形式,是DLL的什么特性使得它能够成为一种病毒?下一节我们将揭晓谜底。
DLL木马
从前文可知,DLL在程序编制中可作出巨大贡献,它提供了具共性代码的复用能力。但是,正如一门高深的武学,若被掌握在正义之侠的手上,便可助其仗义江湖;但若被掌握在邪恶之徒的手上,则必然在江湖上掀起腥风血雨。DLL正是一种这样的武学。DLL一旦染上了魔性,就不再是正常的DLL程序,而是DLL木马,一种恶贯满盈的病毒,令特洛伊一夜之间国破家亡。
DLL木马的原理
DLL木马的实现原理是编程者在DLL中包含木马程序代码,随后在目标主机中选择特定目标进程,以某种方式强行指定该进程调用包含木马程序的DLL,最终达到侵袭目标系统的目的。
正是DLL程序自身的特点决定了以这种形式加载木马不仅可行,而且具有良好的隐藏性:
(1)DLL程序被映射到宿主进程的地址空间中,它能够共享宿主进程的资源,并根据宿主进程在目标主机的级别非法访问相应的系统资源;
(2)DLL程序没有独立的进程地址空间,从而可以避免在目标主机中留下"蛛丝马迹",达到隐蔽自身的目的。
DLL木马实现了"真隐藏",我们在任务管理器中看不到木马"进程",它完全溶进了系统的内核。与"真隐藏"对应的是"假隐藏","假隐藏"木马把自己注册成为一个服务。虽然在任务管理器中也看不到这个进程,但是"假隐藏"木马本质上还具备独立的进程空间。"假隐藏"只适用于Windows9x的系统,对于基于WINNT的操作系统,通过服务管理器,我们可以发现系统中注册过的服务。
DLL木马注入其它进程的方法为远程线程插入。
远程线程插入技术指的是通过在另一个进程中创建远程线程的方法进入那个进程的内存地址空间。将木马程序以DLL的形式实现后,需要使用插入到目标进程中的远程线程将该木马DLL插入到目标进程的地址空间,即利用该线程通过调用Windows API LoadLibrary函数来加载木马DLL,从而实现木马对系统的侵害。
DLL木马注入程序
这里涉及到一个非常重要的Windows API――CreateRemoteThread。与之相比,我们所习惯使用的CreateThread API函数只能在进程自身内部产生一个新的线程,而且被创建的新线程与主线程共享地址空间和其他资源。而CreateRemoteThread则不同,它可以在另外的进程中产生线程!CreateRemoteThread有如下特点:
(1)CreateRemoteThread较CreateThread多一个参数hProcess,该参数用于指定要创建线程的远程进程,其函数原型为:
HANDLE CreateRemoteThread( HANDLE hProcess, //远程进程句柄 LPSECURITY_ATTRIBUTES lpThreadAttributes, SIZE_T dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId ); |
(2)线程函数的代码不能位于我们用来注入DLL木马的进程所在的地址空间中。也就是说,我们不能想当然地自己写一个函数,并把这个函数作为远程线程的入口函数;
(3)不能把本进程的指针作为CreateRemoteThread的参数,因为本进程的内存空间与远程进程的不一样。
以下程序由作者Shotgun的DLL木马注入程序简化而得(单击此处下载,在经典书籍《Windows核心编程》中我们也可以看到类似的例子),它将d盘根目录下的troydll.dll插入到ID为4000的进程中:
#include <windows.h>
#include <stdlib.h> #include <stdio.h> void CheckError ( int, int, char *); //出错处理函数 PDWORD pdwThreadId; HANDLE hRemoteThread, hRemoteProcess; DWORD fdwCreate, dwStackSize, dwRemoteProcessId; PWSTR pszLibFileRemote=NULL; void main(int argc,char **argv) { int iReturnCode; char lpDllFullPathName[MAX_PATH]; WCHAR pszLibFileName[MAX_PATH]={0}; dwRemoteProcessId = 4000; strcpy(lpDllFullPathName, "d://troydll.dll"); //将DLL文件全路径的ANSI码转换成UNICODE码 iReturnCode = MultiByteToWideChar(CP_ACP, MB_ERR_INVALID_CHARS, lpDllFullPathName, strlen(lpDllFullPathName), pszLibFileName, MAX_PATH); CheckError(iReturnCode, 0, "MultByteToWideChar"); //打开远程进程 hRemoteProcess = OpenProcess(PROCESS_CREATE_THREAD | //允许创建线程 PROCESS_VM_OPERATION | //允许VM操作 PROCESS_VM_WRITE, //允许VM写 FALSE, dwRemoteProcessId ); CheckError( (int) hRemoteProcess, NULL, "Remote Process not Exist or Access Denied!"); //计算DLL路径名需要的内存空间 int cb = (1 + lstrlenW(pszLibFileName)) * sizeof(WCHAR); pszLibFileRemote = (PWSTR) VirtualAllocEx( hRemoteProcess, NULL, cb, MEM_COMMIT, PAGE_READWRITE); CheckError((int)pszLibFileRemote, NULL, "VirtualAllocEx"); //将DLL的路径名复制到远程进程的内存空间 iReturnCode = WriteProcessMemory(hRemoteProcess, pszLibFileRemote, (PVOID) pszLibFileName, cb, NULL); CheckError(iReturnCode, false, "WriteProcessMemory"); //计算LoadLibraryW的入口地址 PTHREAD_START_ROUTINE pfnStartAddr = (PTHREAD_START_ROUTINE) GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryW"); CheckError((int)pfnStartAddr, NULL, "GetProcAddress"); //启动远程线程,通过远程线程调用用户的DLL文件 hRemoteThread = CreateRemoteThread( hRemoteProcess, NULL, 0, pfnStartAddr, pszLibFileRemote, 0, NULL); CheckError((int)hRemoteThread, NULL, "Create Remote Thread"); //等待远程线程退出 WaitForSingleObject(hRemoteThread, INFINITE); //清场处理 if (pszLibFileRemote != NULL) { VirtualFreeEx(hRemoteProcess, pszLibFileRemote, 0, MEM_RELEASE); } if (hRemoteThread != NULL) { CloseHandle(hRemoteThread ); } if (hRemoteProcess!= NULL) { CloseHandle(hRemoteProcess); } } //错误处理函数CheckError() void CheckError(int iReturnCode, int iErrorCode, char *pErrorMsg) { if(iReturnCode==iErrorCode) { printf("%s Error:%d/n/n", pErrorMsg, GetLastError()); //清场处理 if (pszLibFileRemote != NULL) { VirtualFreeEx(hRemoteProcess, pszLibFileRemote, 0, MEM_RELEASE); } if (hRemoteThread != NULL) { CloseHandle(hRemoteThread ); } if (hRemoteProcess!= NULL) { CloseHandle(hRemoteProcess); } exit(0); } } |
从DLL木马注入程序的源代码中我们可以分析出DLL木马注入的一般步骤为:
(1)取得宿主进程(即要注入木马的进程)的进程ID dwRemoteProcessId;
(2)取得DLL的完全路径,并将其转换为宽字符模式pszLibFileName;
(3)利用Windows API OpenProcess打开宿主进程,应该开启下列选项:
a.PROCESS_CREATE_THREAD:允许在宿主进程中创建线程;
b.PROCESS_VM_OPERATION:允许对宿主进程中进行VM操作;
c.PROCESS_VM_WRITE:允许对宿主进程进行VM写。
(4)利用Windows API VirtualAllocEx函数在远程线程的VM中分配DLL完整路径宽字符所需的存储空间,并利用Windows API WriteProcessMemory函数将完整路径写入该存储空间;
(5)利用Windows API GetProcAddress取得Kernel32模块中LoadLibraryW函数的地址,这个函数将作为随后将启动的远程线程的入口函数;
(6)利用Windows API CreateRemoteThread启动远程线程,将LoadLibraryW的地址作为远程线程的入口函数地址,将宿主进程里被分配空间中存储的完整DLL路径作为线程入口函数的参数以另其启动指定的DLL;
(7)清理现场。
DLL木马的防治
从DLL木马的原理和一个简单的DLL木马程序中我们学到了DLL木马的工作方式,这可以帮助我们更好地理解DLL木马病毒的防治手段。
一般的木马被植入后要打开一网络端口与攻击程序通信,所以防火墙是抵御木马攻击的最好方法。防火墙可以进行数据包过滤检查,我们可以让防火墙对通讯端口进行限制,只允许系统接受几个特定端口的数据请求。这样,即使木马植入成功,攻击者也无法进入到受侵系统,防火墙把攻击者和木马分隔开来了。
对于DLL木马,一种简单的观察方法也许可以帮助用户发现之。我们查看运行进程所依赖的DLL,如果其中有一些莫名其妙的DLL,则可以断言这个进程是宿主进程,系统被植入了DLL木马。"道高一尺,魔高一丈",现如今,DLL木马也发展到了更高的境界,它们看起来也不再"莫名其妙"。在最新的一些木马里面,开始采用了先进的DLL陷阱技术,编程者用特洛伊DLL替换已知的系统DLL。特洛伊DLL对所有的函数调用进行过滤,对于正常的调用,使用函数转发器直接转发给被替换的系统DLL;对于一些事先约定好的特殊情况,DLL会执行一些相应的操作。
本文给出的只是DLL木马最简单情况的介绍,读者若有兴趣深入研究,可以参考其它资料。
关于DLL的疑问
你好,看了你写的"VC++ DLL编程深入浅出",特别有收获。 只是有个地方我老搞不明白,就是用DLL导出全局变量时,指定了.lib的路径(#pragma comment(lib,"dllTest.lib")),那么.dll的文件的路径呢,我尝试着把.dll文件移到别的地方程序就无法正常运行了,请问.dll在这里怎么指定。
希望您能在百忙中抽空给我解答一下,不胜感激!
一位编程爱好者
回答:
Windows按下列顺序搜索DLL:
(1)当前进程的可执行模块所在的目录;
(2)当前目录;
(3)Windows 系统目录,通过GetSystemDirectory 函数可获得此目录的路径;
(4)Windows 目录,通过GetWindowsDirectory 函数可获得此目录的路径;
(5)PATH 环境变量中列出的目录。
因此,隐式链接时,DLL文件的路径不需要指定也不能指定,系统指定按照1~5的步骤寻找DLL,但是对应的.lib文件却需要指定路径;如果使用Windows API函数LoadLibrary动态加载DLL,则可以指定DLL的路径。
你好,我是一位C++初学者,我在PCONLINE看了教学之后,受益不浅。我想问一下能否在DLL里使用多线程?MSDN上用#using <mscorlib.dll>这个指令之后实现了多线程,不过好象不支持DLL..
请问有什么办法支持制作多线程DLL??能否给一个源码来?
回答:
在DLL中可以处理多线程,WIN32对于多线程的支持是操作系统本身提供的一种能力,并不在于用户编写的是哪一类程序。即便是一个控制台程序,我们都可以使用多线程:
#include <stdio.h>
#include <windows.h> void ThreadFun(void) { while(1) { printf( "this is new thread/n" ); Sleep( 1000 ); } } int main() { DWORD threadID; CreateThread( NULL, 0, (LPTHREAD_START_ROUTINE)ThreadFun, NULL, 0, &threadID ); while(1) { printf( "this is main thread/n" ); Sleep( 1000 ); } } |
观察程序运行的结果为在控制台窗口上交替输出this is main thread、this is new thread。
我们来看下面的一个多线程DLL的例子。
DLL程序提供一个接口函数SendInit,在此接口中启动发送线程SendThreadFunc,在这个线程的对应工作函数中我们使用原始套接字socket发送报文。参考微软出版的经典书籍《Windows核心编程》,我们发现,不宜在DLL被加载的时候(即进程绑定时)启动一个新的线程。
这个线程等待一个CEvent事件(用于线程间通信),应用程序调用DLL中的接口函数SendMsg( InterDataPkt sendData )可以释放此事件。下面是相关的源代码:
(1)发送报文线程入口函数
///
//函数名:SendThreadFunc //函数功能:发送报文工作线程入口函数,使用UDP协议
DWORD WINAPI SendThreadFunc( LPVOID lpvThreadParm ) //提示:对于线程函数应使用WINAPI声明,WINAPI被宏定义为__stdcall { /* 创建socket */ sendSock = socket ( AF_INET, SOCK_DGRAM, 0 ); if ( sendSock == INVALID_SOCKET ) { AfxMessageBox ( "Socket创建失败" ); closesocket ( recvSock ); } /* 获得目标节点端口与地址 */ struct sockaddr_in desAddr; desAddr.sin_family=AF_INET; desAddr.sin_port=htons( DES_RECV_PORT ); //目标节点接收端口 desAddr.sin_addr.s_addr = inet_addr( DES_IP ); /* 发送数据 */ while(1) { WaitForSingleObject( hSendEvent, 0xffffffffL );//无限等待事件发生 ResetEvent( hSendEvent ); sendto( sendSock, (char *)sendSockData.data, sendSockData.len, 0, (struct sockaddr*)&desAddr, sizeof(desAddr) ); } return -1; } |
(2)MFC规则DLL的InitInstance函数
/
// CMultiThreadDllApp initialization BOOL CMultiThreadDllApp::InitInstance() { if ( !AfxSocketInit() ) //初始化socket { AfxMessageBox( IDP_SOCKETS_INIT_FAILED ); return FALSE; } return TRUE; } |
(3)启动发送线程
//函数名:SendInit //函数功能:DLL提供给应用程序调用接口,用于启动发送线程 / void SendInit(void) { hSendThread = CreateThread( NULL, 1000, SendThreadFunc, this, 1, &uSendThreadID ); }
|
(4)SendMsg函数
//函数名:SendMsg //函数功能:DLL提供给应用程序调用接口,用于发送报文 / extern "C" void WINAPI SendMsg( InterDataPkt sendData ) { sendSockData = sendData; SetEvent( hSendEvent ); //释放发送事件 } |
以上程序仅仅是一个简单的例子,其实在许多工程应用中,我们经常看到这样的处理方式。这个DLL对用户而言仅仅使一个简单的接口函数SendMsg,对调用它的应用程序屏蔽了多线程的技术细节。与之类似,MFC提供的CSocket类在底层自己采用了多线程机制,所以使我们免去了对多线程的使用。
您好,看了您的DLL文章,发现导出函数可以直接用_declspec(dllexport)声明或在.def文件中定义,变量的导出也一样。我想知道类是否也可以在.def文件中导出?您的文章中只讲了在类前添加_declspec(dllexport)导出类的方法。请您指教!
回答:
一般我们不采用.def文件导出类,但是这并不意味着类不能用.def文件导出类。
使用Depends查看连载2的"导出类"例程生成的DLL,我们发现其导出了如图21的众多"怪"symbol,这些symbol都是经过编译器处理的。因此,为了以.def文件导出类,我们必须把这些"怪"symbol全部导出,实在是不划算啊!所以对于类,我们最好直接以_declspec(dllexport)导出。
图1 导出类时导出的symbol
|
您好,看了您的DLL文章,知道怎么创建DLL了,但是面对一个具体的工程,我还是不知道究竟应该把什么做成DLL?您能给一些这方面的经验吗?
回答:
DLL一般用于软件模块中较固定、较通用的可以被复用的模块,这里有一个非常好的例子,就是豪杰超级解霸。梁肇新大师把处理视频和音频的算法模块专门做成了两个DLL,供超级解霸的用户界面GUI程序调用,实在是DLL设计的模范教程。所谓"万变不离其宗",超级解霸的界面再cool,用到的还是那几个DLL!具体请参考《编程高手箴言》一书。
您好,您的DLL文章讲的都是Windows的,请问Linux操作系统上可以制作DLL吗?如果能,和Windows有什么不一样?谢谢!
回答:
在Linux操作系统中,也可以采用动态链接技术进行软件设计,但与Windows下DLL的创建和调用方式有些不同。
Linux操作系统中的共享对象技术(Shared Object)与Windows里的DLL相对应,但名称不一样,其共享对象文件以.so作为后缀。与Linux共享对象技术相关的一些函数如下:
(1)打开共享对象,函数原型:
//打开名为filename共享对象,并返回操作句柄;
void *dlopen (const char *filename, int flag); |
(2)取函数地址,函数原型:
//获得接口函数地址
void *dlsym(void *handle, char *symbol); |
(3)关闭共享对象,函数原型:
//关闭指定句柄的共享对象
int dlclose (void *handle); |
(4)动态库错误函数,函数原型:
//共享对象操作函数执行失败时,返回出错信息
const char *dlerror(void); |
从这里我们分明看到Windows API――LoadLibrary、FreeLibrary和GetProcAddress的影子!又一个"万变不离其宗"!