标题: VB6拾遗:调用函数指针
作者: Demon
链接: https://demon.tw/programming/vb6-repick-call-function-pointer.html
版权: 本博客的所有文章,都遵守“署名-非商业性使用-相同方式共享 2.5 中国大陆”协议条款。
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寄存器的值,请问这样做是否安全?如何解决?
赞赏微信赞赏支付宝赞赏
随机文章:
我看call的定义中,参数数量是限制的。多于4个的参数是不是要重新定义接口?有没有万能call?
接口相当于函数原型,只有函数原型相同的函数指针才可以用同一个接口Call;否则需要定义新的接口。
没有万能Call,就算在C语言中函数指针也需要声明为符合某种函数原型才能调用。
另外,这种方式和callwindowproc的实现有什么区别么?
这个call 貌似可以调用所有的API函数
VB上的程序获取API函数指针比较多
使用API完全可以用常规方法 – –
pop ecx 可以用add esp, 4来代替,这样就不会损坏ECX的原有数据了。
很佩服您对编程知识的融会贯通!
应该是调用约定规定ecx不必保存吧