在无函数声明的情况下运行时动态调用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: "
<运行结果:

 

 

 

示例程序和全部代码单击这里下载(Visual C++ 2008)。

 

 

 

posted @ 2016-01-31 21:38  princessd8251  阅读(419)  评论(0)    收藏  举报