关于c ++:C将参数作为void-pointer-list传递给LoadLibrary()的导入函数

C Pass arguments as void-pointer-list to imported function from LoadLibrary()

我有一个问题,我想创建一个通用的命令行应用程序,可以用来加载库dll,然后调用库dll中的函数。函数名在命令行上指定,参数也在实用程序命令行上提供。

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

我可以将一个空指针列表传递给我由LoadLibrary()函数返回的函数指针吗?类似于下面的例子。

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

1
2
3
4
5
    //Somewhere in another dll
    int DoStuff(int a, int b)
    {
        return a + b;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    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);
    }

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


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

1
2
3
4
5
6
7
8
9
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函数:

1
2
3
4
5
6
7
8
9
10
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中任何可访问的地方。


库dll包含作为库一部分的函数的对象代码以及一些附加信息,以使该dll可用。好的。

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

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

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

这与通常具有类型信息的COM对象不同,后者支持执行基本反射的操作,并探索COM对象的服务以及访问这些服务的方式。您可以使用Visual Studio和其他IDE来实现这一点,这些IDE生成已安装的COM对象列表,并允许您加载COM对象并对其进行浏览。Visual Studio还具有一个工具,该工具将生成提供存根的源代码文件,并包含用于访问COM对象的服务和方法的文件。好的。

但是,库dll与COM对象不同,并且随COM对象提供的所有附加信息都不能从库dll中获得。相反,库dll包通常由以下部分组成:(1)库dll本身,(2)一个.lib文件,其中包含库dll的链接信息以及存根和功能,以便在构建使用库dll的应用程序时满足链接器的要求,(3)一个包含文件,其中包含t中函数的函数原型图书馆DLL。好的。

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

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

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

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

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

因此,如果您的实用程序有如下命令行:好的。

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

库dll有一个函数,其原型如下:好的。

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

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

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

附录一:一个简单的例子好的。

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

注意:下面的示例依赖于基于堆栈的参数传递约定,这与大多数x86 32位编译器使用的约定相同。除了基于堆栈的参数传递(如Visual Studio的__fastcall修饰符)之外,大多数编译器还允许指定调用约定。同样,正如注释中指出的,x64和64位Visual Studio的默认设置是默认使用__fastcall约定,以便函数参数在寄存器中传递,而不是在堆栈上传递。请参阅Microsoft msdn中的x64调用约定概述。参见GCC中如何实现变量参数的注释和讨论?.好的。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 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位可执行文件,所有四个变量类型的长度(字节)都相同。如果参数列表中的类型列表更为多样,则需要了解编译器在执行调用之前将参数推送到堆栈时如何对齐参数。好的。

附录二:扩展简单示例好的。

另一种可能是有一组助手函数以及不同版本的structstruct提供了一个内存区来创建必要堆栈的副本,帮助功能用于构建副本。好的。

因此,struct及其助手函数可能如下所示。好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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和helper函数来构建参数列表,如下所示,然后使用提供的堆栈副本调用库dll中的函数:好的。

1
2
3
4
5
6
7
8
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);

好啊。