| 查看: 268 | 回复: 0 | |||
| 当前主题已经存档。 | |||
sdlj8051金虫 (著名写手)
|
[交流]
[转贴]DLL输出类使用研究手记
|
||
|
在写一个程序时,我想使用一个共享软件中的C++类。该类名为Crypt,封装在一个DLL中,文件名为Crypt.dll。通过SoftIce和IDA Pro,我已基本弄清了其成员函数的用法。现在的问题是,没有相应的.H文件及.LIB文件(当然更没有源码)。另外,其成员函数显然不能以GetProcAddress取得地址后直接调用。 该软件是用Borland C++写的。 先用Borland C++提供的工具获取必要的文件 C:\bc5\bin\impdef Crypt.def Crypt.dll//得到Crypt.def文件 C:\bc5\bin\implib Crypt.lib Crypt.dll//得到Crypt.lib文件 Crypt.def中的相关内容如下: @Crypt@$bctr$qpxc@1; Crypt::Crypt(const char*) @Crypt@$bctr$qpxuci@2; Crypt::Crypt(const unsigned char*,int) @Crypt@DecodeFrom$qpuct1l @3; Crypt: ecodeFrom(unsigned char*,unsigned char*,long) @Crypt@EncodeTo$qpuct1l @4; Crypt::EncodeTo(unsigned char*,unsigned char*,long) 从分号后的注释,可以得到Demangled后的成员函数原型,但是没有类的定义,我们不知道这个类包含什么数据成员(以及别的未exported的成员函数,这一点不重要,因为原来的编程者在使用这个封装在DLL中的类时,也只能使用exported的函数)。如何构造一个正确的头文件?先来看看原来的代码是如何使用这个类的。以下为IDA Pro的输出: 00453E3E 0F0push 8 00453E40 0F4lea eax, [ebp+var_8] 00453E43 0F4push eax 00453E44 0F8lea ecx, [ebp+var_E0] 00453E4A 0F8push ecx 00453E4B 0FCcall Crypt::Crypt(uchar *,int) 00453E50 0FCadd esp, 0Ch 这段代码调用Crypt::Crypt(uchar *,int)成员函数,使用__cdecl调用规则,由调用者维护堆栈。函数有2个参数,向堆栈中压入了3个值,最后一个push入栈的是指向当前Crypt对象的this指针,即变量var_E0就是在栈上分配的Crypt类对象。从IDA Pro中可看到,var_E0覆盖了从FFFFFF20到FFFFFF80共96字节的空间。我们知道,C++类的成员函数、静态数据成员是不放在对象内的,对象只含有数据成员(若类中或其基类中定义有虚函数,还包含vptr)。也就是说,Crypt类的所有数据成员共占据96字节。具体细节请参照Stanley Lippman的《深度探索C++对象模型》。 由此,我们可以自己定义Crypt类的数据成员(使用字节数组),使其占据同样的内存空间,与原来的类在内存布局上一致即可。实际上,只要我们给出的类定义保证能分配足够的内存空间,原来的构造函数就可以在分配的内存中创建出正确的对象。这种方法与COM的思想有相似之处,都是在二进制的级别上保证内存布局的兼容。我写的头文件如下: class _import Crypt { public: Crypt(const char* lpszPassword); Crypt(const unsigned char* lpszPassword,int cbBuffer); EncodeTo(unsigned char* lpSource,unsigned char* lpDestination,int nSize); DecodeFrom(unsigned char* lpSource,unsigned char* lpDestination,int nSize); public: char dummy[96]; //Bingo!:-) }; 将此头文件及前面的Crypt.lib文件加入项目,证明此方法是可行的。测试代码如下: Crypt obj("123456" ;obj.EncodeTo(lpData,lpData,nSize); 以上的尝试都是在Borland C++下做的,与原来的程序具有同样的环境,如果想在Visual C++下使用该类又如何实现?从DLL中输出类的技术细节是因编译器厂商而异的,显然不能再如法炮制(VC++甚至不能识别用implib生成的Crypt.lib文件)。我们可以变通一下,自己用VC++写一个Crypt.dll,包裹在原来的DLL外面,输出与原有DLL相同名字和序号的函数,用VC++写的客户程序使用这个Wrapper DLL,由其再去调用原来的DLL。这种编写一个包在原有DLL外面的动态链接库的方法,相关资料很多,这里不再详细解释。 将原来的DLL改名为OldDll.dll。源代码如下: 头文件Crypt.h: #ifdef CRYPT_EXPORTS #define CRYPT_API __declspec(dllexport) #else #define CRYPT_API __declspec(dllimport) #endif class CRYPT_API Crypt { public: Crypt& operator=(const Encrypt& rhs);//赋值运算符,禁止 Crypt(const Encrypt& rhs);//拷贝构造函数,禁止 public: Crypt(const char* lpszPassword); Crypt(const unsigned char* lpszPassword,int cbBuffer); void __cdecl EncodeTo(unsigned char* lpSource,unsigned char* lpDestination,int nSize); void __cdecl DecodeFrom(unsigned char* lpSource,unsigned char* lpDestination,int nSize); public: char dummy[96]; //Bingo!:-) }; 实现文件Crypt.cpp: #include "stdafx.h" #include "Crypt.h" static HINSTANCE hOldDll=NULL; static DWORD dwRet; static DWORD dwRetAddr; static FARPROC lpCrypt1;//带1个参数的构造函数 static FARPROC lpCrypt2;//带2个参数的构造函数 static FARPROC lpEncodeTo; static FARPROC lpDecodeFrom; BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { BOOL bRet=false; switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: //加载原来的DLL,获取原函数地址 hOldDll=LoadLibrary("c:\\test\\OldDll.dll" ;if(hOldDll) { lpCrypt1=::GetProcAddress(hOldDll,MAKEINTRESOURCE(0x1)); lpCrypt2=::GetProcAddress(hOldDll,MAKEINTRESOURCE(0x2)); lpEncodetTo=::GetProcAddress(hOldDll,MAKEINTRESOURCE(0x3)); lpDecodeFrom=::GetProcAddress(hOldDll,MAKEINTRESOURCE(0x4)); bRet=true; } break; case DLL_THREAD_ATTACH: break; case DLL_THREAD_DETACH: break; case DLL_PROCESS_DETACH: if(hOldDll) { ::FreeLibrary(hOldDll); } break; } return bRet; } __declspec(naked) Crypt::Crypt(const char *lpszPassword) { //手工模仿__cdel调用规则 _asm { pop eax//弹出并保存返回地址 mov dwRetAddr,eax push ecx//压this指针入栈 call lpCrypt1 //调用原函数 mov dwRet,eax //保存调用返回值 mov eax,dwRetAddr push eax//重新压返回地址入栈 mov eax,dwRet //恢复调用返回值 ret 8 //返回,丢弃2个dword(参数和this指针) } } __declspec(naked) Crypt::Crypt(const unsigned char* lpszPassword,int cbBuffer) { //手工模仿__cdel调用规则 _asm { pop eax mov dwRetAddr,eax push ecx call lpCrypt2 mov dwRet,eax mov eax,dwRetAddr push eax mov eax,dwRet ret 0xC //丢弃3个dword } } void __declspec(naked) __cdecl Crypt::EncodeTo(unsigned char* lpSource, unsigned char* lpDestination,int nSize) { //直接跳转到原函数 _asm jmp far dword ptr lpEncodeTo } void __declspec(naked) __cdecl Crypt: ecodeFrom(unsigned char* lpSource,unsigned char* lpDestination,int nSize) { //直接跳转到原函数 _asm jmp far dword ptr lpDecodeFrom } 有几处需要注意:首先,这里使用了naked调用规则(Borland C++不支持),以便于直接操作堆栈及用内嵌的汇编语言编程。另外,虽然我们的类中并没有包含虚函数或对象成员,VC++编译器却仍生成了member-wise的拷贝构造函数和bit-wise赋值运算符,并导致原来的DLL中的构造函数不能正确创建对象。为了禁止编译器自动生成不必要的代码,在头文件中定义了赋值运算符和拷贝构造函数,但并未提供实现。两个构造函数有点特殊,我发现无论指定何种调用规则,生成的代码总是使用thiscall调用规则,即在ecx寄存器中传递this指针,为此构造函数需要特殊处理,用汇编代码手工模仿__cdecl调用规则去调用原来DLL中的函数,包括维护栈指针。 其余的代码已作了注释,易于理解,不再赘述。 [ Last edited by sdlj8051 on 2006-10-6 at 12:35 ] |
» 猜你喜欢
拟解决的关键科学问题还要不要写
已经有3人回复
基金申报
已经有5人回复
基金委咋了?2026年的指南还没有出来?
已经有7人回复
国自然申请面上模板最新2026版出了吗?
已经有17人回复
纳米粒子粒径的测量
已经有8人回复
疑惑?
已经有5人回复
计算机、0854电子信息(085401-058412)调剂
已经有5人回复
Materials Today Chemistry审稿周期
已经有5人回复
溴的反应液脱色
已经有7人回复
推荐一本书
已经有12人回复











ecodeFrom(unsigned char*,unsigned
;
回复此楼