标题: VB6拾遗:函数指针与CallWindowProc函数
作者: Demon
链接: https://demon.tw/programming/vb6-repick-function-pointer-callwindowproc.html
版权: 本博客的所有文章,都遵守“署名-非商业性使用-相同方式共享 2.5 中国大陆”协议条款。
在VB中,除了内联汇编,CallWindowProc还能调用函数指针。
AdamBear在他的《VB真是想不到系列之三:VB指针葵花宝典之函数指针》中就使用CallWindowProc函数来调用函数指针,以实现对任意类型数组进行排序的qsort函数。
原理很简单,CallWindowProc本来是调用窗口回调函数的,但它不会关心传递给它的到底是不是窗口回调函数,只是负责调用而已。一个简单的例子:
Option Explicit Private Declare Function CallWindowProc Lib "user32.dll" Alias "CallWindowProcW" ( _ ByVal lpPrevWndFunc As Long, _ ByVal hWnd As Long, _ ByVal Msg As Long, _ ByVal wParam As Long, _ ByVal lParam As Long) As Long Private Declare Function GetProcAddress Lib "kernel32.dll" ( _ ByVal hModule As Long, _ ByVal lpProcName As String) As Long Private Declare Function LoadLibrary Lib "kernel32.dll" Alias "LoadLibraryW" ( _ ByVal lpLibFileName As Long) As Long 'By Demon 'https://demon.tw Sub Main() Dim hModule As Long Dim pFn As Long hModule = LoadLibrary(StrPtr("user32.dll")) If hModule Then pFn = GetProcAddress(hModule, "MessageBoxW") CallWindowProc pFn, 0, StrPtr("https://demon.tw"), StrPtr("Demon's Blog"), 0 pFn = GetProcAddress(hModule, "MessageBeep") '这里会出错 CallWindowProc pFn, 0, 0, 0, 0 End If '这句MsgBox不会执行 MsgBox "complete" End Sub
调用MessageBoxW不会有什么问题,因为它的参数正好是4个;而MessageBeep的参数却只有一个,会导致堆栈不平衡而出错。这就是用CallWindowProc调用函数指针最大的局限性——函数的参数必须正好为4个!少了会导致堆栈不平衡,多了参数没法传递。
不过网上有高手用内联汇编突破了这个限制:
调用函数指针,在Delphi,VC及汇编中,是非常简单的事情,所以,如果你不是VB程序员,请不要浪费你的时间,不要再看这篇文章了。。
在VB中就不能直接调用了,VB唯一与之有点关系的就是AddressOf 操作符,所有VB程序都知道,它能取得模块内的函数地址,看到这里,相信很多VB程序员马上就说,调用函数指针,用 CallWindowProc 不就可以啦,还有什么可以讲的,呵呵,不错,你掌握了这个技巧!是的,这确实是利用这个API可以调用函数指针,不过呢,它只支持4个参数,给你的应用带来很多不便。又有程序员要讲了,四个参数我可以扩展到任意功能呀,比如将其中一个参数传入的是一个结构块的指针,在结构中我可以随意的定义数据呀。鼓掌!鼓掌!鼓掌!很不错!是的,你完全可以这样做。不过呢,如果有一个函数不是你写的呢,你了解这个函数的所有参数及意义,也得到了它的指针,你怎么调用呀?惨了!呵呵。不过不被困难吓倒的你可能又说,呵呵,有了,在《高级VB编程》这本被称为VB程序员的圣经的书中有解决方案。鼓掌!鼓掌!鼓掌!不过,太累了!太累了,我对这本书的评价就是,它确实是一本高深,很值得读的书。不过,毕竟作者的宗旨是基于COM为出发点,再来解决问题的。所以难免很多事情复杂化了,(题外话,很多朋友同我讲,说这本书很难看懂,呵呵,如果你研究一下COM,你就很容易读懂这本书了!)
下面,我所提供的方法就是,利用嵌入一段汇编代码,借助 CallWindowProc 函数实现调用任意个数的参数(当然参数类型也任意啦)的函数指针的能力。
有关CallWindowProc 调用函数指针,在这我就不重复介绍了,毕竟掌握的人很多,网上资料也一大把的。我就省了它吧
Private Declare Function CallAsmCode Lib "user32" Alias "CallWindowProcA" ( _ lpPrevWndFunc As Long, _ ByVal hWnd As Long, _ ByVal Msg As Long, _ ByVal wParam As Long, _ lParam As Long) As Long '------------------------------------------------------ ' 功能:借用API调用任意个数参数的函数的纯VB实现版. ' 此函数内部自带SEH错误处理机制,但并不保证任意 ' 错误调用均不会让您的VB崩溃(比如破坏栈平衡后返回) ' 作者:阿国哥 hackor@yeah.net '------------------------------------------------------ Private Function CallAnyFunc(ByVal pFn As Long, _ ByVal pParam As Long, _ ByVal Count As Long) As Long Dim CallAnyFuncCode(34) As Long, lRet As Long CallAnyFuncCode(0) = &H53EC8B55 CallAnyFuncCode(1) = &HE8& CallAnyFuncCode(2) = &HEB815B00 CallAnyFuncCode(3) = &H1000112C CallAnyFuncCode(4) = &H114A938D CallAnyFuncCode(5) = &H64521000 CallAnyFuncCode(6) = &H35FF& CallAnyFuncCode(7) = &H89640000 CallAnyFuncCode(8) = &H25& CallAnyFuncCode(9) = &H8B1FEB00 CallAnyFuncCode(10) = &HE80C2444 CallAnyFuncCode(11) = &H0& CallAnyFuncCode(12) = &H53E98159 CallAnyFuncCode(13) = &H8D100011 CallAnyFuncCode(14) = &H119791 CallAnyFuncCode(15) = &HB8908910 CallAnyFuncCode(16) = &H33000000 CallAnyFuncCode(17) = &H558BC3C0 CallAnyFuncCode(18) = &H104D8B0C CallAnyFuncCode(19) = &HEB8A148D CallAnyFuncCode(20) = &HFC528D06 CallAnyFuncCode(21) = &HB4932FF CallAnyFuncCode(22) = &H8BF675C9 CallAnyFuncCode(23) = &HD0FF0845 CallAnyFuncCode(24) = &H58F64 CallAnyFuncCode(25) = &H83000000 CallAnyFuncCode(26) = &H4D8B04C4 CallAnyFuncCode(27) = &H5B018914 CallAnyFuncCode(28) = &H10C2C9 CallAnyFuncCode(29) = &H58F64 CallAnyFuncCode(30) = &H83000000 CallAnyFuncCode(31) = &HC03304C4 CallAnyFuncCode(32) = &H89144D8B CallAnyFuncCode(33) = &HC2C95B21 CallAnyFuncCode(34) = &H90900010 CallAnyFunc = CallAsmCode(CallAnyFuncCode(0), pFn, pParam, Count, lRet) If CallAnyFunc <> lRet Then CallAnyFunc = 0 '这里表示出现严重错误,你应当再了解目的函数的使用方法 Debug.Assert False '因为你的参数传递问题,导致程序已出现了非法操作。 End If End Function
下面介绍一下如何使用这个函数
参数一(pFn ):函数指针
参数二(pParam ):参数指针,指向一个连续的内存块,比如目的函数有三个参数,分别为A,B,C。你可以定义一个结构,结构体为A,B,C(每个参数均为4字节长),然后传这个结构的地址。
参数三(Count ):参数个数。
返回:目的函数的返回值。(你可以修改成其它类型的返回值)
此文和代码原始出处不可考,不过幸运的是至少可以知道它的作者是阿国哥。
这个阿国哥是何方神圣?根据能够搜索到的信息,他是AsmInVB插件的作者。
AsmInVB是一款目前国内外优秀的VB6内嵌汇编代码插件,比知名的TweakVB(http://www.tweakvb.com),VBinLineAsm等插件功能更强大,使用范围更广泛(处理代码的内核短小精悍,稳定,完美解决了VB输出lst文件的栈定位问题)。她支持在VB6源代码中直接使用微软宏汇编指令。让您的VB6自然编译后的程序在不需要借助任何其它文件(如DLL)的支持下实现底层功能。
随着VB6退出历史舞台,这款插件已经停止更新,官网变成了赌博网站,作者也消失不见了。
他能很容易读懂《高级VB编程》,是个高手,下面简单分析一下高手写的汇编:
push ebp ; 注释By Demon mov ebp, esp push ebx ; 保存ebx call L004 L004: pop ebx ; ebx的值为当前eip,即L004 sub ebx, 0x1000112C lea edx, dword ptr [ebx+0x1000114A] ; edx的值为L004+0x1E ; 即mov eax, dword ptr [esp+0xC] push edx push dword ptr fs:[0] mov dword ptr fs:[0], esp ; 构造SEH jmp L019 mov eax, dword ptr [esp+0xC] ; 这里是异常处理 ; eax指向CONTEXT结构 call L013 L013: pop ecx ; ecx的值为当前eip,即L013 sub ecx, 0x10001153 lea edx, dword ptr [ecx+0x10001197] ; edx的值为L013+0x44 ; 即第二个pop dword ptr fs:[0] mov dword ptr [eax+0xB8], edx ; CONTEXT->Eip = edx xor eax, eax ; 返回ExceptionContinueExecution ; 代码跳转到刚才设置的eip处继续执行 retn L019: mov edx, dword ptr [ebp+0xC] ; 参数pParam mov ecx, dword ptr [ebp+0x10] ; 参数Count lea edx, dword ptr [edx+ecx*4] ; pParam指向参数结构末尾 jmp L026 L023: lea edx, dword ptr [edx-0x4] ; pParam指向前一个参数 push dword ptr [edx] ; 当前参数入栈 dec ecx ; 参数数量减一 L026: or ecx, ecx ; 是否还有参数 jnz L023 ; 是 mov eax, dword ptr [ebp+0x8] ; 参数pFn call eax ; 调用pFn pop dword ptr fs:[0] ; 恢复SEH add esp, 0x4 mov ecx, dword ptr [ebp+0x14] ; 参数lRet mov dword ptr [ecx], eax ; pFn函数返回值保存到lRet pop ebx ; 恢复ebx leave retn 0x10 ; 返回 pop dword ptr fs:[0] ; 从异常处理跳转到这里 ; 恢复SEH add esp, 0x4 xor eax, eax ; eax清零 即函数返回值为0 mov ecx, dword ptr [ebp+0x14] ; 参数lRet mov dword ptr [ecx], esp ; 当前esp的值保存到参数lRet pop ebx ; 恢复ebx leave retn 0x10 ; 返回 nop ; 对齐 nop ; 对齐
要构造出这样的代码,不仅需要熟悉汇编,还需要一定的耐心和毅力。
阿国哥写的CallAnyFunc函数固然强大,但这并不意味着轻量级对象就没有用武之地了。用轻量级对象也可以像CallAnyFunc那样实现调用任意个数参数的函数,但是没有必要这样做,因为轻量级对象的方法可以通过IDL定义接口来进行调用。这样的接口调用更自然,也更高效。
赞赏微信赞赏支付宝赞赏
随机文章:
好高深的文章,在VB学Api真的吃力,怀疑可以这样使用VB的人都是C/C++的高手。
谢谢
你好,今年是2017年,而您的文章是2013年所著。
當年我也是學VB6,但是我好奇的是,我的能力就只停在VB6那些簡單的物件上。
我其實好奇的是你當年是如何取得這些訊息?
這些都是高手的境界才會的事。
其实学会C\C++、x86汇编,再深入了解WinAPI后回头看这代码就简单多了