原文出處:http://www.graphixer.com.cn/ShowWorks.asp?Type=1&ID=77
我們都知道DLL的調用方式有兩種,即所謂動態調用和靜態調用。靜態調用就是告訴編譯器我需要某個DLL,然後把要用的函式宣告都定義出來,然後在運行時調用這些函數,這種用法和靜態庫的用法相似。動態調用就是運行時使用LoadLibrary將一個DLL載入到運行時環境,然後通過GetProcAddress擷取具體的函數指標然後調用。
然而動態調用依然要求在編譯時間就確定函數的原型,因為在C++中只有通過函數指標才能實現函數的調用。但是在某些情況下,DLL中函數的參數表和傳回型別在編譯時間還不能確定,只有在運行時才能通過某種辦法得到,那麼這個時候如何調用DLL函數呢?這就是本文要解決的問題。
首先,在什麼情況下有可能遇到這個問題。比如我現在正在做一指令碼語言,需要允許指令碼在運行時調用DLL函數,而指令碼要調用什麼DLL函數主程式是不知道的,但是指令碼在調用之前會先給一個所需要調用的DLL函數的聲明,該聲明包括函數的傳回型別、參數表和函數所在的DLL檔案名稱和函數在DLL中的名稱以及函數的呼叫慣例。有了這些資訊,主程式如何響應指令碼的要求調用相關DLL呢?傳統的方法要求在編譯器知道所需調用的函數原型,這顯然是不行的。那麼,我們需要在運行時動態地把這些參數傳給函數,然後調用之。這超出了C++的範圍,因此需要用藉助內嵌彙編實現。
通過使用彙編,我們繞過了C++的參數檢查,從而可以直接調用一個函數地址,當然,我們需要保證所傳參數個數與所調用函數的參數個數相同。
在編寫代碼之前,需要瞭解C++的函數呼叫慣例。C++允許下面幾種呼叫慣例:__cdecl, __stdcall, __fastcal,__thiscall和__clrcall。__thiscall用於調用類成員函數,__clrcall為託管C++所用,而__fastcall則是將參數放在寄存器中傳遞。__thiscall用於訪問對象成員函數,這不屬於我們討論範疇。__fastcall由於通過寄存器傳遞參數,需要函數調用者和被調用函數的配合才能實現,由於我們只能控制函數調用者,因此__fastcall的行為不能確定,因此也不屬於我們討論範疇之列,實際上__fastcall在程式中較少使用,更不會出現在dll的匯出函數中。而__clrcall用於.Net,也不屬於我們討論之列。因此我們要關心的是__cdecl和__stdcall。__cdecl是C/C++的預設呼叫慣例,即函數調用者在調用函數時先將函數的所有參數按從右至左的順序依次壓入堆棧,然後調用函數,最後函數調用者要負責將所有參數彈出堆棧。而__stdcall與__cdecl的不同之處在於__stdcall是由被調用函數將參數彈出堆棧的。
因此調用一個__cdecl函數的彙編代碼應該是如下形式:
push ParamNpush ParamN-1...push Param2push Param1call FuncPtrpop EAXpop EAX...pop EAX
而調用一個__stdcall函數則應當將後面所有的pop指令略去。
現在要解決的問題是,如何傳遞各種類型的參數?如何獲的函數不同類型的傳回值?首先要瞭解push指令,push指令一次只能將4個位元組的資料壓入堆棧,如果要傳遞double, int64等8位元組的資料,需要分成兩個部分壓入堆棧。低位位元組在前,高位位元組在後。也即,如果有
union{ double d struct { int HighPart, LowPart; }Parts;} data
的結構,現在要把d壓入堆棧,那麼彙編寫起來應該是:
push data.LowPartpush data.HighPart
int64也一樣。注意在彙編中,是不管你操作的資料是什麼類型的,所關心的只是要傳的資料有多少個位元組。
對於char, wchar_t這種小於4位元組的資料結構,應當將其轉化為4位元組整數來傳遞。如:
wchar_t wparam;int param = wparam;__asm{ push param}
將wparam轉化為4位元組後壓入堆棧。
接著是傳回值。不同類型的函數傳回值是放在不同地方的。整形的傳回值,如char,wchar_t,int,unsign int等,存放在EAX寄存器中,int64則存放在EDX:EAX寄存器對中,其中EDX存放高位位元組,EAX存放低位位元組。而浮點類型的傳回值,則存放在FPU堆棧的棧頂。需要通過FSTP指令獲得FPU棧頂的資料。
因此當我們call完函數的時候,如果函數類型為整型,我們通過下面的代碼獲得傳回值:
int HighPart, LowPart;__asm{ mov int ptr[HighPart], EDX mov int ptr[LowPart], EAX}
彙編中的int是類型指示符,表示將寄存器的值放在以ptr[HighPart]為起始地址,長度為sizeof(int)的記憶體空間中。ptr的作用和C++中的取址操作符“&”相當。
這樣,在上述代碼中,如果函數傳回值為小於或等於4位元組的有符號/不帶正負號的整數,那麼該整數值就是LowPart變數的值了。如果是int64的話,通過將HighPart和LowPart合成可以得到相應的int64的值:
union{ __int64 Value; struct{ int High, Low} Parts;} Data;Data.Parts.High = HighPart;Data.Parts.Low = LowPart;
這樣,Data.Value就是我們要的值了。
現在看浮點型。要獲得浮點型傳回值,必須使用FSTP指令將FPU堆棧的棧頂資料彈到一個變數中。如果是double類型,下面的代碼可以獲得該資料:
double result;__asm{ FSTP [result]}
同理,如果是float,下面的代碼可以獲得其傳回值:
float result;__asm{ FSTP [result]}
和double版本一模一樣。
好,所有東西全部講完,現在看下面的範例程式碼。
HMODULE lib = LoadLibraryW(L"User32.dll");
FARPROC Func = GetProcAddress(lib, "MessageBoxW");
wchar_t * Msg = L"Test Msg";
wchar_t * title = L"Test title";
int result;
__asm
{
push 0
push title
push Msg
push 0
call Func
mov int ptr[result], EAX
}
上述代碼調用了MessageBoxW函數。MessageBoxW的原型是:int __stdcall MessageBoxW(int Hwnd, const wchar_t * Msg, const wchar_t * title, int MsgBoxType)。代碼就不用多解釋了。
C++中的變數是可以直接寫在彙編中的。但是彙編中只能引用函數中的局部變數,不能引用成員變數,否則要通過指標。而且彙編中引用的變數必須是原始類型的,即不能是struct, union和class等類型的資料。
現在來看一下如何把c++代碼和彙編代碼混合起來實現通用的動態DLL調用器。這裡要注意凡是使用了寄存器的彙編指令都應該和向寄存器存入想要資料的指令寫在同一個__asm塊裡面。如果寫在不同__asm塊中,即使連在一起,也是不能獲得正確的寄存器資料的。只要確保了這一點,__asm塊和C++代碼想怎麼混就怎麼混了。不多說,直接貼代碼。
代碼:DllCall.h
#ifndef GX_DLL_RUNTIME_CALL_H#define GX_DLL_RUNTIME_CALL_H#include "GxLibBasic.h"#include "windows.h"// gxDllVariable : 用於儲存調用DLL函數的參數和傳回值class gxDllVariable : public gxObject{public:enum gxVariableType{gxatVoid,gxatInt,gxatChar,gxatWChar,gxatDouble,gxatFloat,gxatInt64};gxVariableType Type;union{int IntVal;char CharVal;wchar_t WCharVal;double DoubleVal;float FloatVal;__int64 Int64Val;} Data;gxDllVariable();gxDllVariable(int val);gxDllVariable(float val);gxDllVariable(double val);gxDllVariable(__int64 val);gxDllVariable(char val);gxDllVariable(wchar_t val);};class gxDllFunction : public gxObject{public:enum gxCallConvention{gxccStdCall,gxccCdecl};private:FARPROC FFuncPtr;public:gxDllFunction(HMODULE lib, gxString FuncName);~gxDllFunction();public:gxDllVariable Invoke(gxArray& Args, gxDllVariable::gxVariableType ReturnType, gxCallConvention conv = gxccStdCall);};#endif
代碼:DllCall.cpp
#include "DllCall.h"gxDllFunction::gxDllFunction( HMODULE lib, gxString FuncName ){FFuncPtr = GetProcAddress(lib, FuncName.ToMBString());if (!FFuncPtr)throw gxDllLinkException(FuncName);}gxDllVariable gxDllFunction::Invoke( gxArray& Args, gxDllVariable::gxVariableType ReturnType, gxCallConvention conv ){// 用於存放8位元組資料的結構union LongType{double DoubleVal;__int64 IntVal;struct {int Head,Tail;} Parts;};// 使用stdcall/cdecl函數呼叫慣例,參數從右至左壓棧for (int i=Args.Count()-1; i>=0; i--){gxDllVariable var = Args[i];LongType l;// 將單位元組資料放在4位元組變數中,以便入棧int tmp = var.Data.CharVal;// 將不同類型的資料壓入堆棧switch(Args[i].Type){case gxDllVariable::gxatChar: // 單位元組整數__asm{push tmp};break;case gxDllVariable::gxatDouble: // 8位元組浮點// 8位元組資料分兩部分壓入堆棧,低位先入棧l.DoubleVal = var.Data.DoubleVal;__asm{push l.Parts.Tailpush l.Parts.Head}break;case gxDllVariable::gxatFloat: // 4位元組浮點__asm{push var.Data.FloatVal;}break;case gxDllVariable::gxatInt: // 32位整數__asm push var.Data.IntVal;break;case gxDllVariable::gxatWChar: // 16位整數__asm push var.Data.WCharVal;break;case gxDllVariable::gxatInt64: // 64位整數l.IntVal = var.Data.Int64Val;__asm{push l.Parts.Tailpush l.Parts.Head}break;case gxDllVariable::gxatVoid: // 對於函數參數,void類型是非法的throw L"Cannot pass void as an argument.";break;}}// 嵌入式彙編只能訪問函數內部變數,故將函數指標複製一份FARPROC fptr = FFuncPtr;// 調用函數,並獲得儲存在EDX,EAX中的整型函數傳回值LongType ltVal;int itval, ihval;__asm{call fptrmov int ptr[ihval], EDXmov int ptr[itval], EAX}ltVal.Parts.Head = ihval; // 高位字只為int64類型所使用ltVal.Parts.Tail = itval;// 將函數傳回值整理到gxDllVaraiable結構中gxDllVariable retval;retval.Type = ReturnType;switch (ReturnType){case gxDllVariable::gxatChar:retval.Data.CharVal = ltVal.Parts.Tail;break;case gxDllVariable::gxatDouble:// 對於浮點類型傳回值,需從FPU堆棧的棧頂中讀取__asm fstp [retval.Data.DoubleVal];break;case gxDllVariable::gxatFloat:// 對於浮點類型傳回值,需從FPU堆棧的棧頂中讀取__asm fstp [retval.Data.FloatVal];break;case gxDllVariable::gxatInt:retval.Data.IntVal = ltVal.Parts.Tail;break;case gxDllVariable::gxatWChar:retval.Data.WCharVal = ltVal.Parts.Tail;break;case gxDllVariable::gxatInt64:retval.Data.Int64Val = ltVal.IntVal;break;case gxDllVariable::gxatVoid:break;}// 使用C/C++預設呼叫慣例,需要由調用者彈出變數if (conv == gxccCdecl){for (int i=0; i |
下面給出gxDllFunction類的一個使用例子。
用於編譯DLL的TestDLL.cpp:
double DoSomething(int a, double b, __int64 c, double * d){*d = b/2;return (double)(c+a);}
注意編譯此DLL需要在DEF檔案中將此函數匯出。
調用此DLL的主程式Main.cpp:
#include "GxLibrary/DLLCall.h" #include using namespace std; void RunTest() { double d = 0.0; // 載入動態連結程式庫 HMODULE lib = LoadLibraryW(L"TestDll"); // 從動態連結程式庫獲得函數 gxDllFunction RuntimeFunction(lib,L"DoSomething"); // 將參數做成gxDllVariable類型放在數組中 gxArray Args; Args.Add(gxDllVariable(4)); // 參數a, int 類型 Args.Add(gxDllVariable(6.4));// 參數b, double類型 Args.Add(gxDllVariable((__int64)1<<32)); // 參數c, int64類型 Args.Add(gxDllVariable((int)(&d))); // 參數d, double* 類型 // 調用函數,並將函數返回結果轉換為int64後放在Result變數中 __int64 Result = (__int64) RuntimeFunction.Invoke ( Args, gxDllVariable::gxatDouble, gxDllFunction::gxccCdecl ).Data.DoubleVal; // 輸出結果 cout<<"DLL function invoked !/nReturn value: " <運行結果: |
樣本程式和全部代碼單擊這裡下載(Visual C++ 2008)。