C 将参数作为空指针列表传递给从 LoadLibrary() 导入的函数
C Pass arguments as void-pointer-list to imported function from LoadLibrary()
我遇到的问题是我想创建一个通用的命令行应用程序,可用于加载库 DLL,然后调用库 DLL 中的函数。函数名称在命令行上指定,参数也在实用程序命令行上提供。
我可以从使用 LoadLibrary()
函数动态加载的 DLL 访问外部函数。加载库后,我可以获得指向该函数的指针 GetProcAddress()
我想使用命令行上指定的参数调用该函数。
我可以将 void-pointer-list 传递给由 LoadLibrary()
函数 return 编辑的函数指针,类似于下面的示例吗?
为了简化示例代码,我删除了错误检查。有没有办法让这样的东西工作:
//在另一个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(列表);
}
如果我忘了提及必要的信息,请告诉我。
提前致谢
在调用之前,您需要将 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]);
问题是您需要知道您获取指针的函数类型是什么,并调用适当的适配器函数。这通常意味着制作函数 name/adaptors 的 table 并在其中进行查找。
相关的问题是没有类似于 GetProcAddress
的函数可以告诉您库中函数的参数类型——该信息根本没有存储在 dll 中可访问的任何位置。
库 DLL 包含作为库一部分的函数的目标代码以及一些附加信息以允许 DLL 可用。
但是,库 DLL 不包含确定库 DLL 中包含的函数的特定参数列表和类型所需的实际类型信息。库 DLL 中的主要信息是:(1) DLL 导出的函数列表以及将函数调用连接到实际函数二进制代码的地址信息,以及 (2) 任何所需 DLL 的列表库 DLL 中的函数使用。
您实际上可以在文本编辑器中打开一个库 DLL,我建议使用一个小的,然后扫描二进制代码的神秘符号,直到您到达包含库 DLL 中的函数列表的部分与其他必需的 DLL 一样。
因此,库 DLL 包含 (1) 在库 DLL 中找到特定函数以便可以调用它以及 (2) 库 DLL 中的函数所需要的其他 DLL 的列表所需的最低限度信息取决于.
这与 COM 对象不同,COM 对象通常具有类型信息以支持执行基本上是反射和探索 COM 对象的服务以及如何访问这些服务的能力。您可以使用 Visual Studio 和其他生成已安装 COM 对象列表并允许您加载 COM 对象并探索它的 IDE 来执行此操作。 Visual Studio 还有一个工具可以生成源代码文件,这些文件提供了用于访问 COM 对象的服务和方法的存根和包含文件。
但是,库 DLL 不同于 COM 对象,库 DLL 无法提供 COM 对象提供的所有附加信息。相反,库 DLL 包通常由 (1) 库 DLL 本身,(2) 包含库 DLL 的链接信息以及存根和功能的 .lib 文件组成,以满足链接器在构建应用程序时使用库 DLL,以及 (3) 包含库 DLL 中函数的函数原型的包含文件。
因此,您通过调用驻留在库 DLL 中的函数创建应用程序,但使用包含文件中的类型信息并链接到相关联的 .lib 文件的存根。此过程允许 Visual Studio 自动执行使用库 DLL 所需的大部分工作。
或者您可以使用 GetProcAddress()
手动编写 LoadLibrary()
并在库 DLL 中构建 table 函数。通过手工编码,您真正需要的是库 DLL 中函数的函数原型,然后您可以自己键入这些函数原型和库 DLL 本身。如果您使用 .lib 库存根和包含文件,您实际上是在手动完成 Visual Studio 编译器为您完成的工作。
如果您知道库 DLL 中某个函数的实际函数名称和函数原型,那么您可以做的是让您的命令行实用程序需要以下信息:
- 要在命令中作为文本字符串调用的函数的名称
行
- 在命令行中用作一系列文本字符串的参数列表
- 描述函数原型的附加参数
这类似于 C 和 C++ 运行时中接受具有未知参数类型的变量参数列表的函数的工作方式。例如,打印参数值列表的 printf()
函数有一个格式字符串,后跟要打印的参数。 printf()
函数使用格式字符串来确定各种参数的类型、预期的参数数量以及要执行的值类型 t运行sformations。
因此,如果您的实用程序有如下命令行:
dofunc "%s,%d,%s" func1 "name of " 3 " things"
并且库 DLL 有一个函数,其原型如下所示:
void func1 (char *s1, int i, int j);
然后该实用程序将通过运行将命令行的字符串转换为要调用的函数所需的实际类型来动态生成函数调用。
这适用于采用普通旧数据类型的简单函数,但是更复杂的类型(例如 struct
类型参数将需要更多工作,因为您需要对 struct
的某种描述以及某种可能类似于 JSON.
的参数描述
附录一:一个简单的例子
以下是我在调试器中 运行 Visual Studio Windows 控制台应用程序的源代码。 Properties 中的命令参数是 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 位 executable,所有这四种变量类型都具有相同的字节长度。由于参数列表中的类型列表更加多样化,您需要了解编译器在执行调用之前将参数压入堆栈时如何对齐参数。
附录二:扩展简单示例
另一种可能性是拥有一组辅助函数以及不同版本的 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);
我遇到的问题是我想创建一个通用的命令行应用程序,可用于加载库 DLL,然后调用库 DLL 中的函数。函数名称在命令行上指定,参数也在实用程序命令行上提供。
我可以从使用 LoadLibrary()
函数动态加载的 DLL 访问外部函数。加载库后,我可以获得指向该函数的指针 GetProcAddress()
我想使用命令行上指定的参数调用该函数。
我可以将 void-pointer-list 传递给由 LoadLibrary()
函数 return 编辑的函数指针,类似于下面的示例吗?
为了简化示例代码,我删除了错误检查。有没有办法让这样的东西工作:
//在另一个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(列表); }
如果我忘了提及必要的信息,请告诉我。 提前致谢
在调用之前,您需要将 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]);
问题是您需要知道您获取指针的函数类型是什么,并调用适当的适配器函数。这通常意味着制作函数 name/adaptors 的 table 并在其中进行查找。
相关的问题是没有类似于 GetProcAddress
的函数可以告诉您库中函数的参数类型——该信息根本没有存储在 dll 中可访问的任何位置。
库 DLL 包含作为库一部分的函数的目标代码以及一些附加信息以允许 DLL 可用。
但是,库 DLL 不包含确定库 DLL 中包含的函数的特定参数列表和类型所需的实际类型信息。库 DLL 中的主要信息是:(1) DLL 导出的函数列表以及将函数调用连接到实际函数二进制代码的地址信息,以及 (2) 任何所需 DLL 的列表库 DLL 中的函数使用。
您实际上可以在文本编辑器中打开一个库 DLL,我建议使用一个小的,然后扫描二进制代码的神秘符号,直到您到达包含库 DLL 中的函数列表的部分与其他必需的 DLL 一样。
因此,库 DLL 包含 (1) 在库 DLL 中找到特定函数以便可以调用它以及 (2) 库 DLL 中的函数所需要的其他 DLL 的列表所需的最低限度信息取决于.
这与 COM 对象不同,COM 对象通常具有类型信息以支持执行基本上是反射和探索 COM 对象的服务以及如何访问这些服务的能力。您可以使用 Visual Studio 和其他生成已安装 COM 对象列表并允许您加载 COM 对象并探索它的 IDE 来执行此操作。 Visual Studio 还有一个工具可以生成源代码文件,这些文件提供了用于访问 COM 对象的服务和方法的存根和包含文件。
但是,库 DLL 不同于 COM 对象,库 DLL 无法提供 COM 对象提供的所有附加信息。相反,库 DLL 包通常由 (1) 库 DLL 本身,(2) 包含库 DLL 的链接信息以及存根和功能的 .lib 文件组成,以满足链接器在构建应用程序时使用库 DLL,以及 (3) 包含库 DLL 中函数的函数原型的包含文件。
因此,您通过调用驻留在库 DLL 中的函数创建应用程序,但使用包含文件中的类型信息并链接到相关联的 .lib 文件的存根。此过程允许 Visual Studio 自动执行使用库 DLL 所需的大部分工作。
或者您可以使用 GetProcAddress()
手动编写 LoadLibrary()
并在库 DLL 中构建 table 函数。通过手工编码,您真正需要的是库 DLL 中函数的函数原型,然后您可以自己键入这些函数原型和库 DLL 本身。如果您使用 .lib 库存根和包含文件,您实际上是在手动完成 Visual Studio 编译器为您完成的工作。
如果您知道库 DLL 中某个函数的实际函数名称和函数原型,那么您可以做的是让您的命令行实用程序需要以下信息:
- 要在命令中作为文本字符串调用的函数的名称 行
- 在命令行中用作一系列文本字符串的参数列表
- 描述函数原型的附加参数
这类似于 C 和 C++ 运行时中接受具有未知参数类型的变量参数列表的函数的工作方式。例如,打印参数值列表的 printf()
函数有一个格式字符串,后跟要打印的参数。 printf()
函数使用格式字符串来确定各种参数的类型、预期的参数数量以及要执行的值类型 t运行sformations。
因此,如果您的实用程序有如下命令行:
dofunc "%s,%d,%s" func1 "name of " 3 " things"
并且库 DLL 有一个函数,其原型如下所示:
void func1 (char *s1, int i, int j);
然后该实用程序将通过运行将命令行的字符串转换为要调用的函数所需的实际类型来动态生成函数调用。
这适用于采用普通旧数据类型的简单函数,但是更复杂的类型(例如 struct
类型参数将需要更多工作,因为您需要对 struct
的某种描述以及某种可能类似于 JSON.
附录一:一个简单的例子
以下是我在调试器中 运行 Visual Studio Windows 控制台应用程序的源代码。 Properties 中的命令参数是 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 位 executable,所有这四种变量类型都具有相同的字节长度。由于参数列表中的类型列表更加多样化,您需要了解编译器在执行调用之前将参数压入堆栈时如何对齐参数。
附录二:扩展简单示例
另一种可能性是拥有一组辅助函数以及不同版本的 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);