通过封闭源 C++ DLL 访问 API






4.88/5 (19投票s)
在没有源代码或头文件的情况下调用 C++ DLL,即使 DLL 依赖于 C 运行时库
引言
本文介绍了一种有用的技术,用于使用闭源 C++ DLL(在运行时加载)来访问流行的消费类外围设备的 API。假定开发人员无法访问导入库或源代码。
背景
这是我关于 Continuum 博客文章的后续技术文章,该文章介绍了我如何自动化一个简单的软件任务,以实现一个没有提供 API 的消费类网络摄像机的自动平移缩放功能。我们需要在不干扰用户体验的情况下完成此任务,因此 AutoIt 等标准程序自动化工具被排除在外。
在此过程中,我意识到在调用 C++ DLL 时存在一些陷阱,并确定了解决方法。本文的灵感来自 Recx Ltd 的一篇 博客文章,《在没有源代码或头文件的情况下处理 C++ DLL 导出》。虽然提供了一个示例项目,但我们的具体案例依赖于一个专有的 DLL,由于许可限制无法包含。
API Monitor 是监视 API 调用活动的宝贵工具。它对 Windows API 调用具有更全面的功能,但也可以用于查看对外部 DLL 的调用。您可以在工具内部
启动进程,或附加到正在运行的进程。API 调用之前和之后的调用堆栈状态都可以看到,返回值也是可见的,甚至还提供了断点功能。
使用 API Monitor,我启动了网络摄像机控制器应用程序的一个实例,这使我能够看到哪些 DLL 正在被加载。从进程执行开始监视(而不是附加到正在运行的进程)对于查看初始化行为可能很重要。
模块依赖视图显示了大量的 DLL。其中大多数是 Windows 系统 DLL 或与 Qt 框架相关。有一个尤其引起了我们的注意:CameraControls_Core.dll。我设置了 API Monitor 来记录对该 DLL 的所有调用,这是相关的输出
LWS::CameraControls::CameraControls_Core::Init ( ... ) LWS::CameraControls::CameraControls_Core::GetCurrentVideoDevice ( ... ) LWS::CameraControls::CameraControls_Core::SetCurrentVideoDevice ( ... ) LWS::CameraControls::CameraControls_Core::GetFaceTracking ( ... )
SetFaceTracking()
。快速查看调用堆栈,发现布尔值作为参数传递给了该方法。我使用了 Microsoft 的 **dumpbin** 工具来检查 CameraControls_Core.dll 的导出方法列表。很快就发现我正在处理一个 C++ DLL(由于使用了名称修饰)。1 0 00001366 ??0CameraControls_Core@CameraControls@LWS@@QAE@XZ 2 1 000015C3 ??1CameraControls_Core@CameraControls@LWS@@UAE@XZ 3 2 00038740 ??_7CameraControls_Core@CameraControls@LWS@@6B@
我将 **dumpbin** 工具的输出输入到 Microsoft 的 **undname** 工具中,以取消对名称的修饰。
这提供了更清晰的输出:
1 0 00001366 public: __thiscall LWS::CameraControls::CameraControls_Core::CameraControls_Core(void)
2 1 000015C3 public: virtual __thiscall LWS::CameraControls::CameraControls_Core::~CameraControls_Core(void)
3 2 00038740 const LWS::CameraControls::CameraControls_Core::`vftable'
...
40 27 000012E9 public: bool __thiscall LWS::CameraControls::CameraControls_Core::Init(class QString)
...
86 55 000016D1 public: long __thiscall LWS::CameraControls::CameraControls_Core::SetFaceTracking(unsigned long,long)
大约在这个时候,我偶然发现了上面提到的 Recx Ltd 文章。我意识到我们的特定 DLL 会带来一些挑战,使得这种技术的简单脚本应用变得不可能。我将 DLL 的一个副本移到我的应用程序的工作目录中,并尝试编写一些代码来动态加载它。
static HINSTANCE CameraControlDllHandle = NULL;
CameraControlDllHandle = LoadLibrary(L"CameraControls_Core.dll");
逐步执行代码,发现了第一个依赖库:Qt4Gui4。随着错误的出现,我将依赖项复制到工作目录中。结果发现需要十个额外的 DLL。我最后一次运行代码,遇到了关于 MSVCR90.DLL 丢失的错误。
将此 DLL 放在目录中会导致 C 运行时加载不正确的错误。
MSVCR90.DLL 是 Visual C++ 2008 的 C 运行时库。我尝试重新构建我的项目以使用此运行时运行,以便它使用与 DLL 相同的运行时,但这并没有缓解问题。事实证明,Microsoft 在 Windows 98 中引入了一种新的系统范围 DLL 管理形式,用于允许冲突的 DLL 同时存在于内存中(Wikipedia DLL Hell)。符合此标准的 DLL 包含一个清单文件,该文件被导入到 DLL 中,以告知调用进程需要加载哪些依赖项。
为了克服 C 运行时依赖关系,我需要创建一个指向运行时库的清单文件,并将其嵌入到我的 CameraControls_Core.dll 中。这篇 MSDN 文章概述了如何使用 Microsoft 的 MT.EXE 工具将清单文件嵌入到已构建的可执行文件或 DLL 中。
我需要用于 MSVCR90.DLL 的清单文件如下:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.VC90.DebugCRT"
version="9.0.21022.8" processorArchitecture="x86"
publicKeyToken="1fc8b3b9a1e18e3b"></assemblyIdentity>
</dependentAssembly>
</dependency>
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.VC90.CRT"
version="9.0.21022.8" processorArchitecture="x86"
publicKeyToken="1fc8b3b9a1e18e3b"></assemblyIdentity>
</dependentAssembly>
</dependency>
</assembly>
完成此步骤后,库就可以无异常地加载到内存中了!现在,我只需要验证方法调用是否有效。早期尝试调用几乎任何方法时都会抛出异常。我意识到这是因为某个关键的初始化函数可能没有被调用。读者应该先阅读《在没有源代码或头文件的情况下处理 C++ DLL 导出》。
我将直接跳到有效的解决方案并进行解释: #include <QString>
//------------------------------------------------------------------------------
// Function Pointer Typedefs
//------------------------------------------------------------------------------
typedef long (__thiscall *_SetFaceTracking)(DWORD *pThis, unsigned long, long boolValue);
typedef long (__thiscall *_GetFaceTracking)(DWORD *pThis, unsigned long, long *boolValue);
typedef void (__thiscall *_CameraControls_Core)(DWORD *pThis);
typedef bool (__thiscall *_Init)(DWORD *pThis, class QString param1);
typedef long (__thiscall *_GetCurrentVideoDevice)(DWORD *pThis, unsigned long *param);
typedef long (__thiscall *_SetCurrentVideoDevice)(DWORD *pThis, unsigned long param);
//--------------------------------------------------------------------------------------------
// Static Variables
//--------------------------------------------------------------------------------------------
static BOOL freeResult;
static _SetFaceTracking CameraSetFaceTracking;
static _GetFaceTracking CameraGetFaceTracking;
static DWORD dwFakeObject[512];
static HINSTANCE CameraControlDllHandle = NULL;
static unsigned long currentVideoDevice = 0;
PRINCEDLL_API HRESULT APIENTRY InitLibrary()
{
long retValue = 0;
QString inputString = "";
CameraControlDllHandle = LoadLibrary(L"CameraControls_Core.dll");
if (CameraControlDllHandle == NULL)
return E_FAIL;
_CameraControls_Core CameraImportedConstructor = (_CameraControls_Core) GetProcAddress(
CameraControlDllHandle,"??0CameraControls_Core@CameraControls@LWS@@QAE@XZ");
if (CameraImportedConstructor == NULL)
return E_FAIL;
CameraSetFaceTracking = (_SetFaceTracking) GetProcAddress (CameraControlDllHandle,
"?SetFaceTracking@CameraControls_Core@CameraControls@LWS@@QAEJKJ@Z");
if (CameraSetFaceTracking == NULL)
return E_FAIL;
CameraGetFaceTracking = (_GetFaceTracking) GetProcAddress (CameraControlDllHandle,
"?GetFaceTracking@CameraControls_Core@CameraControls@LWS@@QAEJKPAJ@Z");
if (CameraGetFaceTracking == NULL)
return E_FAIL;
_Init CameraInitializer = (_Init) GetProcAddress (CameraControlDllHandle,
"?Init@CameraControls_Core@CameraControls@LWS@@QAE_NVQString@@@Z");
if (CameraInitializer == NULL)
return E_FAIL;
_SetCurrentVideoDevice CameraSetCurrentVideoDevice = (_SetCurrentVideoDevice) GetProcAddress (
CameraControlDllHandle, "?SetCurrentVideoDevice@CameraControls_Core@CameraControls@LWS@@QAEJK@Z");
if (CameraSetCurrentVideoDevice == NULL)
return E_FAIL;
_GetCurrentVideoDevice CameraGetCurrentVidDevice = (_GetCurrentVideoDevice) GetProcAddress (
CameraControlDllHandle, "?GetCurrentVideoDevice@CameraControls_Core@CameraControls@LWS@@QAEJPAK@Z");
if (CameraGetCurrentVidDevice == NULL)
return E_FAIL;
memset (dwFakeObject, 0x00, 512);
CameraImportedConstructor(dwFakeObject);
retValue = CameraInitializer(dwFakeObject, inputString);
retValue = CameraGetCurrentVidDevice (dwFakeObject, ¤tVideoDevice);
retValue = CameraSetCurrentVideoDevice (dwFakeObject, currentVideoDevice);
return S_OK;
}
文章中介绍的有所不同。他们的示例使用了
__cdecl
,我刚了解到这现在是 Win x64 系统上所有函数的标准调用约定(这是为了消除许多复杂调用约定带来的问题)。在 __cdecl
中,所有参数都通过调用堆栈传递。对象实例 this
指针最后传递。我们的 DLL 使用 __thiscall
约定。 事实证明,__thiscall
通知被调用者 this
指针是通过 ECX 寄存器传递的(不在调用堆栈上)。__thiscall
关键字告诉编译器第一个参数应放在 ECX
寄存器中。技术上,第一个参数应该是指向对象实例的指针。
由于我们的 DLL 的 Init()
函数需要一个 QString
作为参数,因此我需要确定目标 DLL 使用的 Qt 版本,构建 Qt,并将其静态链接到我的 DLL。幸运的是,将一个空的 QString
传递给 Init 函数完成了初始化而没有错误,并允许调用其他方法。dwFakeObject
数组是必需的,因为 C++ 实例方法期望传递一个指向它们要操作的对象的引用。我们在内存中预留一个区域,并将其视为指向传递给 DLL 中方法的 것입니다
一个虚拟对象的引用。
关注点
希望这对遇到类似情况尝试使用第三方 DLL 的人会有所启发。在无法获取底层接口和数据定义
类型源代码的情况下,问题可能比本示例更复杂或更简单。但是,它说明了访问 DLL 函数并不总是有通用解决方案,在某些情况下需要临时方法。
涉及的主题
- 嵌入式 DLL 清单文件
- 查看导出函数以及从 C++ DLL 进行调用
- APIMonitor 作为监视 API 调用的工具
- 调用约定
历史
2013 年 1 月 17 日 - 首次发布。