C将参数作为void-pointer-list传递给LoadLibrary()中的导入函数

时间:2015-03-11 20:28:59

标签: c++ c pointers winapi function-pointers

我遇到的问题是我想创建一个通用命令行应用程序,可用于加载库DLL,然后调用库DLL中的函数。函数名称在命令行中指定,参数也在实用程序命令行中提供。

我可以使用LoadLibrary()函数从动态加载的DLL访问外部函数。加载库后,我可以使用GetProcAddress()获取指向函数的指针。我想用命令行中指定的参数调用该函数。

我是否可以将void-pointer-list传递给LoadLibrary()函数返回的函数指针,类似于下面的例子?

为了保持示例代码简单,我删除了错误检查。有没有办法让这样的工作:

    //Somewhere in another dll
    int DoStuff(int a, int b)
    {
        return a + b;
    }
    int main(int argc, char **argv)
    {
        void *retval;
        void *list = argv[3];
        HMODULE dll;
        void* (*generic_function)(void*);

        dll = LoadLibraryA(argv[1]);

        //argv[2] = "DoStuff"
        generic_function = GetProcAddress(dll, argv[2]);

        //argv[3] = 4, argv[4] = 7, argv[5] = NULL
        retval = generic_function(list);
    }

如果我忘记提及必要的信息,请告诉我。 提前致谢

2 个答案:

答案 0 :(得分:4)

在调用之前,需要将LoadLibrary返回的函数指针强制转换为具有正确参数类型的函数指针。管理它的一种方法是使用一个数字调用适配器函数,为您可能想要调用的每种可能的函数类型做正确的事情:

void Call_II(void (*fn_)(), char **args) {
    void (*fn)(int, int) = (void (*)(int, int))fn_;
    fn(atoi(args[0]), atoi(args[1]));
}
void Call_IS(void (*fn_)(), char **args) {
    void (*fn)(int, char *) = (void (*)(int, char *))fn_;
    fn(atoi(args[0]), args[1]);
}
...various more functions

然后你从GetProcAddress获取指针和其他参数并将它们传递给正确的Call_X函数:

void* (*generic_function)();

dll = LoadLibraryA(argv[1]);

//argv[2] = "DoStuff"
generic_function = GetProcAddress(dll, argv[2]);

//argv[3] = 4, argv[4] = 7, argv[5] = NULL

Call_II(generic_function, &argv[3]);

问题是你需要知道你获取指针的函数的类型是什么,并调用适当的适配器函数。这通常意味着制作一个函数名称/适配器表并在其中进行查找。

相关的问题是没有类似于GetProcAddress的功能会告诉你库中函数的参数类型 - 这些信息根本不存储在dll中可访问的任何地方。

答案 1 :(得分:2)

库DLL包含作为库一部分的函数的目标代码以及允许DLL可用的一些附加信息。

但是,库DLL不包含确定特定参数列表所需的实际类型信息以及库DLL中包含的函数的类型。库DLL中的主要信息是:(1)DLL导出的函数列表以及将函数调用连接到实际函数二进制代码的地址信息,以及(2)任何所需DLL的列表库DLL中的函数使用。

你可以在文本编辑器中实际打开一个库DLL,我建议一个小的,并扫描二进制代码的神秘符号,直到你到达包含库DLL中的函数列表的部分以及其他必需的DLL。

因此,库DLL包含以下所需的最小信息:(1)在库DLL中查找特定函数以便可以调用它;以及(2)库DLL中的函数所依赖的其他所需DLL的列表

这与通常具有类型信息的COM对象不同,以支持能够执行基本上反射并探索COM对象服务以及如何访问这些服务的能力。您可以使用Visual Studio和其他IDE生成安装的COM对象列表,并允许您加载COM对象并进行探索。 Visual Studio还有一个工具,它将生成提供存根的源代码文件,并包含用于访问COM对象的服务和方法的文件。

但是,库DLL与COM对象不同,并且库DLL不提供COM对象提供的所有附加信息。相反,库DLL包通常由(1)库DLL本身,(2).lib文件组成,该文件包含库DLL的链接信息以及在构建使用的应用程序时满足链接器的存根和功能。库DLL,以及(3)包含库DLL中函数原型的包含文件。

因此,您可以通过调用驻留在库DLL中的函数来创建应用程序,但使用包含文件中的类型信息并链接相关.lib文件的存根。此过程允许Visual Studio自动执行使用库DLL所需的大部分工作。

或者您可以使用LoadLibrary()手动编写GetProcAddress()代码并在库DLL中构建函数表。通过手工编码,您真正需要的是库DLL中函数的函数原型,然后您可以自己键入库DLL本身。如果您正在使用.lib库存根和包含文件,那么您实际上是手工完成Visual Studio编译器为您所做的工作。

如果您知道库DLL中函数的实际函数名称和函数原型,那么您可以做的是让命令行实用程序需要以下信息:

  • 要在命令上作为文本字符串调用的函数的名称 线
  • 要在命令行上用作一系列文本字符串的参数列表
  • 描述函数原型的附加参数

这类似于C和C ++运行时中接受具有未知参数类型的变量参数列表的函数的工作方式。例如,打印参数值列表的printf()函数具有格式字符串,后跟要打印的参数。 printf()函数使用格式字符串来确定各种参数的类型,期望的参数数量以及要进行的值转换类型。

因此,如果您的实用程序有一个类似于以下内容的命令行:

dofunc "%s,%d,%s" func1 "name of " 3 " things"

库DLL有一个原型如下的函数:

void func1 (char *s1, int i, int j);

然后该实用程序将通过将命令行的字符串转换为要调用的函数所需的实际类型来动态生成函数调用。

这适用于采用普通旧数据类型的简单函数,但更复杂的类型(如struct类型参数)需要更多工作,因为您需要某种struct的描述以及某种参数描述可能类似于JSON。

附录I:一个简单的例子

以下是我在调试器中运行的Visual Studio Windows控制台应用程序的源代码。属性中的命令参数是pif.dll PifLogAbort,它导致来自另一个项目pif.dll的库DLL被加载,然后调用该库中的函数PifLogAbort()

注意: 以下示例依赖于基于堆栈的参数传递约定,与大多数x86 32位编译器一样。大多数编译器还允许指定调用约定而不是基于堆栈的参数传递,例如Visual Studio的__fastcall修饰符。同样如注释中所指出的,x64和64位Visual Studio的默认设置是默认使用__fastcall约定,以便函数参数在寄存器中传递,而不是在堆栈中传递。请参阅Microsoft MSDN中的Overview of x64 Calling Conventions。另请参阅How are variable arguments implemented in gcc? 中的评论和讨论。

注意函数PifLogAbort()的参数列表是如何构建为包含数组的结构。参数值被放入struct变量的数组中,然后调用函数按值传递整个struct。这样做是将参数数组的副本推送到堆栈然后调用该函数。 PifLogAbort()函数根据参数列表查看堆栈,并将数组元素作为单独的参数或参数进行处理。

// dllfunctest.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"

typedef struct {
    UCHAR *myList[4];
} sarglist;

typedef void ((*libfunc) (sarglist q));
/*
 *  do a load library to a DLL and then execute a function in it.
 *
 * dll name.dll "funcname"
*/
int _tmain(int argc, _TCHAR* argv[])
{
    HMODULE  dll = LoadLibrary(argv[1]);
    if (dll == NULL) return 1;

    // convert the command line argument for the function name, argv[2] from
    // a TCHAR to a standard CHAR string which is what GetProcAddress() requires.
    char  funcname[256] = {0};
    for (int i = 0; i < 255 && argv[2][i]; i++) {
        funcname[i] = argv[2][i];
    }

    libfunc  generic_function = (libfunc) GetProcAddress(dll, funcname);
    if (generic_function == NULL) return 2;

    // build the argument list for the function and then call the function.
    // function prototype for PifLogAbort() function exported from the library DLL
    // is as follows:
    // VOID PIFENTRY PifLogAbort(UCHAR *lpCondition, UCHAR *lpFilename, UCHAR *lpFunctionname, ULONG ulLineNo);
    sarglist xx = {{(UCHAR *)"xx1", (UCHAR *)"xx2", (UCHAR *)"xx3", (UCHAR *)1245}};

    generic_function(xx);

    return 0;
}

这个简单的例子说明了必须克服的一些技术障碍。您将需要知道如何将各种参数类型转换为内存区域中的正确对齐方式,然后将其推入堆栈。

这个示例函数的接口是非常同质的,因为大多数参数都是unsigned char指针,除了最后一个是int。对于32位可执行文件,所有这四种变量类型都具有相同的字节长度。通过参数列表中更多变化的类型列表,您需要了解编译器在执行调用之前将参数推送到堆栈时如何对齐参数。

附录II:扩展简单示例

另一种可能性是拥有一组辅助函数以及struct的不同版本。 struct提供了一个内存区域来创建必要堆栈的副本,​​帮助函数用于构建副本。

因此struct及其辅助函数可能如下所示。

typedef struct {
    UCHAR myList[128];
} sarglist2;

typedef struct {
    int   i;
    sarglist2 arglist;
} sarglistlist;

typedef void ((*libfunc2) (sarglist2 q));

void pushInt (sarglistlist *p, int iVal)
{
    *(int *)(p->arglist.myList + p->i) = iVal;
    p->i += sizeof(int);
}

void pushChar (sarglistlist *p, unsigned char cVal)
{
    *(unsigned char *)(p->arglist.myList + p->i) = cVal;
    p->i += sizeof(unsigned char);
}

void pushVoidPtr (sarglistlist *p, void * pVal)
{
    *(void * *)(p->arglist.myList + p->i) = pVal;
    p->i += sizeof(void *);
}

然后struct和辅助函数将用于构建参数列表,如下所示,之后使用提供的堆栈副本调用库DLL中的函数:

sarglistlist xx2 = {0};
pushVoidPtr (&xx2, "xx1");
pushVoidPtr (&xx2, "xx2");
pushVoidPtr (&xx2, "xx3");
pushInt (&xx2, 12345);

libfunc2  generic_function2 = (libfunc2) GetProcAddress(dll, funcname);
generic_function2(xx2.arglist);