天天看點

在無函數聲明的情況下運作時動态調用DLL函數

原文出處: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 ParamN
push ParamN-1
...
push Param2
push Param1
call FuncPtr
pop EAX
pop EAX
...
pop EAX      

而調用一個__stdcall函數則應當将後面所有的pop指令略去。

現在要解決的問題是,如何傳遞各種類型的參數?如何獲的函數不同類型的傳回值?首先要了解push指令,push指令一次隻能将4個位元組的資料壓入堆棧,如果要傳遞double, int64等8位元組的資料,需要分成兩個部分壓入堆棧。低位位元組在前,高位位元組在後。也即,如果有

union
{
    double d
     struct
    {
        int HighPart, LowPart;
    }Parts;
} data      

的結構,現在要把d壓入堆棧,那麼彙編寫起來應該是:

push data.LowPart
push 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.Tail
				push 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.Tail
				push 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 fptr
		mov int ptr[ihval], EDX
		mov 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: "

<運作結果:

在無函數聲明的情況下運作時動态調用DLL函數

示例程式和全部代碼單擊這裡下載下傳(Visual C++ 2008)。