VB6拾遗:调用函数指针

标签: , , , , , ,

VB提供了AddressOf操作符获取函数指针,却没有原生的调用函数指针的方法。

不过不要紧,有了轻量级对象和内联汇编,我们可以轻松实现对函数指针的调用。Matthew Curland在他的《Advanced Visual Basic 6》中提供了一个非常好用的FunctionDelegator对象,只可惜作者在代码中如此写道:

‘ You are entitled to license free distribution of any application

‘   that uses this file if you own a copy of the book, or if you

‘   have obtained the file from a source approved by the author. You

‘   may redistribute this file only with express written permission

‘   of the author.

由于版权的问题,这里就不贴代码了,读者可以自己到网上找。根据版权法的一般理论,版权保护的是思想的原创表达,而不是所表达的思想,虽然代码不能抄袭,但是讲讲其中所表达的思想却是可以的。

FunctionDelegator的核心思想是函数代理——VB不能直接调用函数指针,但是能调用COM方法,而在COM方法中可以使用内联汇编,于是我们可以在COM方法中用内联汇编调用函数指针。所以FunctionDelegator对象除了VTable指针外还需要多一个字段来保存函数指针:


Public Type FunctionDelegator
    pVTable As Long  'This has to stay at offset 0
    pfn As Long      'This has to stay at offset 4
End Type

那是不是在COM方法中直接用call指令调用函数指针就可以了呢?当然没有那么简单,但是也并不复杂,只要把this指针去掉就行了。

在COM中,函数的调用都遵循__stdcall调用约定,即所有的参数按从右至左的顺序入栈,并且由被调用的函数来维持堆栈平衡。除此之外,COM还要求第一个参数必须是this指针。也就是说,当COM对象的方法被调用时,堆栈中的结构是这样的:


ESP      > 返回地址
ESP+4    > this指针
ESP+8    > 参数1
ESP+C    > 参数2
ESP+10   > 参数3
......   > .....

而一般的__stdcall函数是没有this指针的,所以必须把this指针去掉,可以用下面的汇编代码:


pop     ecx                  ; 保存返回地址
pop     eax                  ; 保存this指针
push    ecx                  ; 还原返回地址
jmp     dword ptr [eax+4]    ; 调用函数指针

执行到jmp指令时堆栈中的结构如下:


ESP      > 返回地址
ESP+4    > 参数1
ESP+8    > 参数2
ESP+C    > 参数3
......   > .....

原来堆栈中的this指针被去掉,这样参数的顺序就跟需要调用的函数相符合了,然后直接jmp到目标函数中继续执行就达到了调用函数指针的效果。

一个简单的例子,从DLL中动态加载MessageBox函数并调用:

IDL文件定义接口:


[
  uuid(885D90B5-6550-4416-A25D-40C36AB12489),
  version(1.0)
]
library NewLib
{
    importlib("stdole2.tlb");

    [
      odl,
      uuid(0C203F37-E3B2-4D00-9036-22DBA672B75D),
      nonextensible
    ]
    interface IMessageBox : IUnknown {
        long _stdcall Call(
                        [in] long hWnd , 
                        [in] BSTR lpText , 
                        [in] BSTR lpCaption , 
                        [in] long uType );
    };
};

标准模块调用函数指针:


Option Explicit
Private Declare Function LoadLibrary Lib "kernel32" Alias "LoadLibraryA" ( _
    ByVal lpLibFileName As String) As Long

Private Declare Function GetProcAddress Lib "kernel32" ( _
    ByVal hModule As Long, ByVal lpProcName As String) As Long


Sub Main()
    Dim hModule As Long
    Dim pFunc As Long
    Dim IMessageBox As IMessageBox
    
    hModule = LoadLibrary("user32.dll")
    If hModule Then
        pFunc = GetProcAddress(hModule, "MessageBoxW")
        If pFunc Then
            Set IMessageBox = NewDelegator(pFunc)
            IMessageBox.Call 0, "https://demon.tw", "Demon's Blog", 0
        End If
    End If
End Sub

当然,这个例子没有太大的实用价值。函数指针最经典的应用莫过于C标准库函数中的qsort函数:


void qsort(
   void *base,
   size_t num,
   size_t width,
   int (__cdecl *compare )(const void *, const void *) 
);

最后一个参数compare为函数调用者自己编写的比较函数的指针,qsort函数内部通过调用compare函数指针,就可以实现对任意类型的数组进行快速排序。

VB没有原生的函数指针调用,这意味着对于每一种类型的数组都必须编写一个排序函数。而本文介绍的调用函数指针的方法,使得在VB中编写像qsort那样通用的函数成为可能。时间关系就不写了,读者感兴趣的话可以自己动手试试。

最后思考一个问题,在汇编代码中pop ecx改变了ecx寄存器的值,请问这样做是否安全?如何解决?

赞赏

微信赞赏支付宝赞赏

随机文章:

  1. ASCII Assembly技术简介
  2. 各浏览器禁止内容选中的方式
  3. PathFindFileName函数,由文件路径获得文件名
  4. OpenWrt安装CIFS客户端挂载网络驱动器
  5. AscW函数返回负数的问题

6 条评论 发表在“VB6拾遗:调用函数指针”上

  1. coo_boi说道:

    我看call的定义中,参数数量是限制的。多于4个的参数是不是要重新定义接口?有没有万能call?

    • Demon说道:

      接口相当于函数原型,只有函数原型相同的函数指针才可以用同一个接口Call;否则需要定义新的接口。

      没有万能Call,就算在C语言中函数指针也需要声明为符合某种函数原型才能调用。

  2. coo_boi说道:

    另外,这种方式和callwindowproc的实现有什么区别么?

  3. prophetk说道:

    这个call 貌似可以调用所有的API函数
    VB上的程序获取API函数指针比较多
    使用API完全可以用常规方法 – –

  4. LiuGH说道:

    pop ecx 可以用add esp, 4来代替,这样就不会损坏ECX的原有数据了。

    很佩服您对编程知识的融会贯通!

留下回复