首先,我们使用C/C++创建一个WIN32 DLL,这样VB才可以使用C/C++的代码。
此前我对DLL导出函数已有专文讲述,这里就不再赘述,当然为了各位能够去验证,我还是把步骤截图上来:
打开VC6,新建一个Win32 DLL工程(使用VS.NET的朋友因为有中文版MSDN我就不截图了,我也不喜欢用.NET)
选择空白的DLL就可以了,代码我们自己写。
创建头文件和代码文件,其实最重要的是#include <windows.h>,标准化吧。
#ifndef __HOOK_H_ #define __HOOK_H_ #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 #define WIN32_LEAN_AND_MEAN //#define EXPORT_API extern "C" __declspec(dllexport) #define EXPORT_API __declspec(dllexport) #include <windows.h> // ... typedef union _unTagPack{ BYTE Byte; SHORT Integer; LONG Long; DWORD Pointer; float Single; double Double; char Text[256]; }TAGPACK,*PTAGPACK; /* //{{AFX_INSERT_LOCATION}} // Microsoft Visual C++ will insert additional declarations immediately before the prevIoUs line. */ #endif
程序代码如下:
#include "Hook.h" BOOL APIENTRY DllMain(HANDLE hModule,DWORD ul_reason_for_call,LPVOID lpReserved) { switch(ul_reason_for_call ) { case DLL_PROCESS_ATTACH: break; case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; } EXPORT_API DWORD __stdcall fnHook(DWORD dwIndex) { return dwIndex; } EXPORT_API DWORD __stdcall fnVarptr(void *dwArg,DWORD dwFlag) { TAGPACK Arg; switch(dwFlag) { case 0: Arg.Pointer = (DWORD)dwArg; Arg.Byte = *(BYTE*)dwArg; break; case 1: Arg.Pointer = (DWORD)dwArg; Arg.Integer = *(SHORT*)dwArg; break; case 2: Arg.Pointer = (DWORD)dwArg; Arg.Long = *(LONG*)dwArg; *(LONG*)dwArg = 0x10000000; break; case 3: Arg.Pointer = (DWORD)dwArg; Arg.Single = *(float*)dwArg; break; case 4: Arg.Pointer = (DWORD)dwArg; Arg.Double = *(double*)dwArg; break; case 5: Arg.Pointer = (DWORD)dwArg; Arg.Pointer = *(DWORD*)dwArg; strcpy(Arg.Text,(const char*)dwArg); break; } //*dwArg = 0x80000001; return 0; }
EXPORTS fnHook fnVarptr
那么,我们给两个函数做个断点,方便等下调试。
OK!现在我们编写VB的代码,并生成Project1.exe,就一个窗口:
Option Explicit Private Declare Function fnHook Lib "Hook.dll" (ByVal dwIndex As Long) As Long Private Declare Function fnVarptr Lib "Hook.dll" (ByVal dwPtr As Long,ByVal dwFlag As Long) As Long Private Type T_ARG b As Byte i As Integer l As Long s As Single d As Double t(32) As Byte End Type Dim a As T_ARG Private Sub Form_Load() ' Dim ret As Long,s As String a.b = 255 a.i = 32767 a.l = &H7FFFFFFF a.s = 12345.6789 a.d = 0.123456789 a.t(0) = Asc("D") a.t(1) = Asc("L") a.t(2) = Asc("L") a.t(3) = 0 s = "Fuck You!" Call fnHook(0) ret = fnVarptr(VarPtr(a.b),0) Call fnHook(1) ret = fnVarptr(VarPtr(a.i),1) Call fnHook(2) ret = fnVarptr(VarPtr(a.l),2) Call fnHook(3) ret = fnVarptr(VarPtr(a.s),3) Call fnHook(4) ret = fnVarptr(VarPtr(a.d),4) Call fnHook(5) ret = fnVarptr(VarPtr(a.t(0)),5) Call fnHook(6) ret = fnVarptr(StrPtr(s),5) Call fnHook(7) End Sub
现在,我们在VC++的环境中配置好Project1.exe的路径,F5直接运行就会在这个断点停下:
你可能已经明白,这个函数只是方便我们定位VB的代码。不错,看汇编窗口的代码:
17: EXPORT_API DWORD __stdcall fnHook(DWORD dwIndex) 18: { return dwIndex; 10001070 push ebp 10001071 mov ebp,esp 10001073 sub esp,40h 10001076 push ebx 10001077 push esi 10001078 push edi 10001079 lea edi,[ebp-40h] 1000107C mov ecx,10h 10001081 mov eax,0CCCCCCCCh 10001086 rep stos dword ptr [edi] 10001088 mov eax,dword ptr [ebp+8] 19: } 1000108B pop edi 1000108C pop esi 1000108D pop ebx 1000108E mov esp,ebp 10001090 pop ebp 10001091 ret 4
呵呵,所以说不是必要最好别定义函数,你至少多有10条汇编指令要运行。而且,这只是在C/C++。
当然,我们“装”聪明一点,跳开这个函数回到主调函数,那么主调函数的代码就是VB的代码(DLL函数是VB调用的)。
比如第一次传入一个Byte的时候:
00401B43 push 0
00401B45 call 004018B0
00401B4A mov esi,dword ptr ds:[401014h]
00401B50 call esi
00401B52 mov edx,dword ptr [ebp-34h]
00401B55 mov edi,dword ptr ds:[40105Ch]
00401B5B push edx
00401B5C call edi
00401B5E push 0
00401B60 push eax
00401B61 call 004018F4
00401B66 call esi
00401B68 push 1
00401B6A call 004018B0
也许你会认为,这个004018B0就是fnHook的地址,其实不是。从上面的汇编代码我们已经知道它的地址是10001070。
那么为什么不是PUSH 0,然后CALL 10001070呢?这是因为VB调用DLL是DllFunctionCall来完成的。
VB跟.NET一样,应该说.NET跟VB一样在虚拟机环境中运行,它编译后的代码很接近机器码。
这跟VB的历史有关,VB是伴随着ActiveX技术发展起来的,它的成功远超出了微软的预料,这也是.NET借鉴它的原因之一。
点到为止,回到正题。
我们不管此CALL 004018B0后做什么样的操作,最终都是调用fnHook然后返回,那么我们就得到了VB代码所对应的汇编代码:
00401B68 push 1 00401B6A call 004018B0 00401B6F call esi 00401B71 mov eax,dword ptr [ebp-38h] 00401B74 push eax 00401B75 call edi 00401B77 push 1 00401B79 push eax 00401B7A call 004018F4 00401B7F call esi 00401B81 push 2 00401B83 call 004018B0 00401B88 call esi 00401B8A mov ecx,dword ptr [ebp-3Ch] 00401B8D push ecx 00401B8E call edi 00401B90 push 2 00401B92 push eax 00401B93 call 004018F4 00401B98 call esi 00401B9A push 3 00401B9C call 004018B0 00401BA1 call esi 00401BA3 mov edx,dword ptr [ebp-40h] 00401BA6 push edx 00401BA7 call edi 00401BA9 push 3 00401BAB push eax 00401BAC call 004018F4 00401BB1 call esi 00401BB3 push 4 00401BB5 call 004018B0 00401BBA call esi 00401BBC push ebx 00401BBD call edi 00401BBF push 4 00401BC1 push eax 00401BC2 call 004018F4 00401BC7 call esi 00401BC9 push 5 00401BCB call 004018B0 00401BD0 call esi 00401BD2 mov eax,dword ptr [ebp-44h] 00401BD5 push eax 00401BD6 call edi 00401BD8 push 5 00401BDA push eax 00401BDB call 004018F4 00401BE0 call esi 00401BE2 push 6 00401BE4 call 004018B0 00401BE9 call esi 00401BEB mov ecx,dword ptr [ebp-1Ch] 00401BEE push ecx 00401BEF call dword ptr ds:[40105Ch] 00401BF5 push 5 00401BF7 push eax 00401BF8 call 004018F4 00401BFD call esi 00401BFF push 7 00401C01 call 004018B0 00401C06 call esi 00401C08 mov dword ptr [ebp-4],0 00401C0F wait
代码中CALL esi及以后的东西是VB自己生成的指令,我们不用理会。
好,注意下第一次传递Byte时候汇编代码的红色部分,所调用的函数会变代码是这样的:
7345C195 push esi
7345C196 call dword ptr ds:[7339122Ch]
7345C19C push dword ptr ds:[7349EF94h]
7345C1A2 mov esi,eax
7345C1A4 call dword ptr ds:[73391278h]
7345C1AA mov dword ptr [eax+9Ch],esi
7345C1B0 pop esi
7345C1B1 ret
注意红色那行,起跳转到这里执行:
7C92FE01 mov eax,fs:[00000018]
7C92FE07 mov eax,dword ptr [eax+34h]
7C92FE0A ret
第二个CALL,我们不去理会它,你也可以继续跟踪,不过现在我们已经有了答案:
就是它就是VarPtr(),我们看VB IDE中函数的提示:
Ptr As Any中的Any就好比C/C++中的void*指针,既然是这样,就不能说VB完全不支持指针,而是把它和谐了。
当然,就一此说VB支持指针,也是错的。我们无法用*ptr = value 来操作指针,也无法给指针赋值。
然而,通过VarPtr我们得到了指针的值,把它传递给C/C++,我们看下以此传递进来后C/C++处理的结果:
将Byte的值传递给共用体的Byte成员。
将Integer赋值给共用体成员。
类似的Long也是一样:
单精度浮点型。
这里也许你会发现,VB中的12345.6789变成了12345.7,为什么?
这跟浮点型的存储方式有关,这里就不详细讲了。不过一点是,不要把long直接用内存复制(包括指针转换),赋值给Single
反之也是一样。虽然两者使用的内存都是4个字节。
双精度浮点型。
赋值后:
字符串,直接把字节数组传递进来:
字符串,用StrPtr传递的结果有所不同
少尉有点功底的都知道,这是Unicode编码的字符串。我们的DLL中使用strcpy,对此我们应该使用另外一个函数:wcscpy
最关键的是,到这里我们已经认识到,VB所谓VarPtr只是把指针搞成一个Long,字符串就多一点玄机。
只要认识到这一点,那么在处理VB和C/C++的DLL中数据的交换,或者换个说法叫函数调用,就了然了。
你还可以再去验证,把Byval As String传递给char*或者const char*是完全OK的。
Doc End!