从LoadLibrary()导入函数时,将参数作为void指针列表传递

6
我遇到的问题是要创建一个通用的命令行应用程序,可以用来加载库DLL并调用库DLL中的函数。函数名在命令行上指定,并且也提供了实用程序命令行上的参数。
我可以使用“LoadLibrary()”函数动态加载DLL中的外部函数。一旦库被加载,我就可以使用“GetProcAddress()”函数获得函数的指针,然后使用命令行上指定的参数来调用该函数。
我是否可以将void指针列表传递给通过“LoadLibrary()”函数返回的函数指针,类似于下面的示例?
为了保持示例代码简单,我删除了错误检查。是否有办法使类似这样的代码工作:
//在另一个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); }
如果我忘记了必要的信息,请告诉我。先谢谢你。

调用约定最终可能会给你带来麻烦。实际函数期望你如何调用它是一个明显的假设。而且,“...通过传递参数。”,注意到该语句的复数形式是有趣的。你只传递了一个。如果你需要更多(即你期望这个函数自动发送argv[3]...argv[argc-1]作为函数参数),那么这样做行不通,正确地做起来很快就变得复杂了。 - WhozCraig
不清楚你所说的“如果我能够将void指针列表传递给函数指针”的意思。如果你调用的函数被定义为__ MyFunction(void*)__,那么是可以这样调用的,否则就不能。另外,请确保它被标记为“declspec(stdcall)”。 - seva titov
我刚刚在寻找一种方法来获取命令行工具,以调用例如kernel32.dll及其任何函数并向其传递参数。我希望能够像WhozCraig所说的那样,通过一种神奇的方式从导入的函数中找到参数列表。 - h4x0r
@WhozCraig,感谢您的评论。"你期望这个神奇地发送argv[3]...argv[argc-1]作为函数参数,但这不会实现"正是我正在寻找的(或者至少是对此的一种解决方法)。 - h4x0r
2个回答

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中任何可访问的位置。


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执行此操作,这些IDE生成已安装的COM对象列表,并允许您加载COM对象并进行探索。Visual Studio还具有一个工具,该工具将生成提供存根和包括文件以访问COM对象的服务和方法的源代码文件。

然而,库DLL与COM对象不同,并且所有附加信息都不可从库DLL中获得。相反,库DLL包通常由(1)库DLL本身,(2)包含用于库DLL的链接信息以及满足使用库DLL的应用程序构建器的存根和功能的.lib文件,以及(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);

然后,该实用程序将通过将命令行的字符字符串转换为函数调用所需的实际类型来动态生成函数调用。这对于采用Plain Old Data类型的简单函数有效,但是更复杂的类型,例如结构类型参数,需要更多的工作,因为您需要某种描述结构的说明以及类似于JSON的参数说明。附录I:一个简单的例子以下是我在调试器中运行的Visual Studio Windows控制台应用程序的源代码。属性中的命令参数是“pif.dll PifLogAbort”,这会导致从另一个项目加载库DLL pif.dll,然后调用该库中的函数PifLogAbort()。
注:以下示例依赖于基于堆栈的参数传递约定,这是大多数x86 32位编译器使用的方式。大多数编译器还允许指定除基于堆栈的参数传递之外的调用约定,例如Visual Studio 的__fastcall修饰符。此外,如评论中所指出,x64和64位Visual Studio的默认设置是默认使用__fastcall约定,因此函数参数以寄存器而不是堆栈传递。请参阅Microsoft MSDN中的{{link1:x64调用约定概述}}。还可以参考gcc中如何实现可变参数?的评论和讨论。 注意函数 PifLogAbort() 的参数列表是如何构建成一个包含数组的结构体的。参数值被放入结构体变量的数组中,然后通过传递整个结构体的方式调用函数。这样做的作用是将参数数组的副本推入堆栈,然后调用函数。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;
}

这个简单的例子说明了必须克服的一些技术难题。您需要知道如何将各种参数类型翻译为正确的内存区域对齐方式,然后将其推送到堆栈上。
这个例子函数的接口非常类似,因为大多数参数都是无符号字符指针,最后一个参数是整数。在32位可执行文件中,这四种变量类型的字节长度相同。如果参数列表中包含更多不同类型的变量,则需要了解编译器在将参数推送到堆栈之前如何对齐参数。
附录II:扩展简单示例
另一个可能性是使用一组帮助函数以及不同版本的结构体。该结构体提供了一个内存区域来创建必要的堆栈副本,而辅助函数用于构建该副本。
因此,该结构体及其辅助函数可能如下所示。
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);

非常有趣的方法!我没有想到可以通过接口来调用/传递参数列表。我一定会进一步研究这个问题。感谢您精细详尽的解释和答案。干得好! - h4x0r
这假设调用约定是通过将像 struct { ... } sarglistlist; 这样的大对象按值传递,通过将整个对象复制到堆栈中来实现的,对吗?(因此,在不使用任何寄存器参数的堆栈参数调用约定中,它位于函数期望常规整数/指针/FP参数的相同位置)。除了使用寄存器参数外,Windows x86-64调用约定还通过引用传递大对象,因此即使在该约定的第5个参数之后,这也无法工作。(有关64位问题,请参见https://stackoverflow.com/questions/49283616/how-to-set-function-arguments) - Peter Cordes
@PeterCordes 我不理解你的评论,太多内容放在了太小的空间里。通过阅读链接文章和Overview of x64 Calling Conventions,似乎64位ABI默认使用__fastcall说明符Argument Passing and Naming Conventions和https://software.intel.com/en-us/node/682402,因此x64参数传递使用寄存器而不是堆栈。关于`va_arg`实现,我发现了这个https://groups.google.com/forum/#!topic/gnu.gcc/PC6xIUSZlr4 - Richard Chambers
我的观点是,一些堆栈参数调用约定可能会决定通过隐藏引用传递 xx2.arglist,因此这个答案不仅取决于纯堆栈参数调用约定,还取决于通过将整个大聚合体复制到堆栈上来进行传值的方式。这就是为什么/如何它在32位Windows上可以工作的原因,如果有人想知道的话。 - Peter Cordes
@PeterCordes 我已经在帖子中添加了关于x64 ABI的注释。感谢您提醒我。 - Richard Chambers

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接