一个库 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()
函数根据其参数列表查看堆栈,并将数组元素处理为单独的参数。
#include "stdafx.h"
typedef struct {
UCHAR *myList[4];
} sarglist;
typedef void ((*libfunc) (sarglist q));
int _tmain(int argc, _TCHAR* argv[])
{
HMODULE dll = LoadLibrary(argv[1]);
if (dll == NULL) return 1;
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;
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);
argv[3]...argv[argc-1]
作为函数参数),那么这样做行不通,正确地做起来很快就变得复杂了。 - WhozCraig