VB6拾遗:函数指针与CallWindowProc函数

标签: , , , , , , ,

在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定义接口来进行调用。这样的接口调用更自然,也更高效。

赞赏

微信赞赏支付宝赞赏

随机文章:

  1. 隐藏Nginx和PHP版本号
  2. VbsEdit 5.6.1新增功能
  3. JavaScript 中小数和大整数的精度丢失
  4. wprintf输出中文
  5. VBS获取硬盘序列号

3 条评论 发表在“VB6拾遗:函数指针与CallWindowProc函数”上

  1. winkey88说道:

    好高深的文章,在VB学Api真的吃力,怀疑可以这样使用VB的人都是C/C++的高手。
    谢谢

  2. 路過客说道:

    你好,今年是2017年,而您的文章是2013年所著。

    當年我也是學VB6,但是我好奇的是,我的能力就只停在VB6那些簡單的物件上。
    我其實好奇的是你當年是如何取得這些訊息?

    這些都是高手的境界才會的事。

  3. laserw9说道:

    其实学会C\C++、x86汇编,再深入了解WinAPI后回头看这代码就简单多了

留下回复