设为首页收藏本站Access中国

Office中国论坛/Access中国论坛

 找回密码
 注册

QQ登录

只需一步,快速开始

返回列表 发新帖
查看: 2520|回复: 6
打印 上一主题 下一主题

[模块/函数] 【转载 / 资料】Matthew Curland的VB函数指针调用

[复制链接]
跳转到指定楼层
1#
发表于 2005-8-28 08:51:00 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
Matthew Curland简介:

    Visual Studio开发小组成员,参与开发了VB的IntelliSense和Object Browser。他是VB资深专家,对VB有非常深入的研究,堪称VB大师。所著《Advanced Visual Basice》是阐述VB高级编程技巧的一本好书。

    本文英文原著可见2000年2月份《Visual Basic Programmer's Journal》(VB程序员月刊)里的《Call Function Pointers》,这是他发表的妙文之一,他的书里的第11章和本文同名,本文应该是这一章节的精华。



    之所以推荐此文,是因为它综合运用了VB里的不少技术。我们可从中看到Matt大师对VB的深刻理解,而各位技术的综合运用正体现了他深厚的功力。



本文原文:http://www.devx.com/premier/mgznarch/vbpj/2000/02feb00/mc0200/mc0200.asp

(要先注册成premier用户)

本文配套代码:

http://www.devx.com/free/mgznarch/vbpj/code/2000/02feb00/vb0002mc_p.zip





关键字:函数指针,COM、对象、接口,vTalbe,VB汇编,动态DLL调用。

级别:高级

要求:了解VB对象编程,了解汇编。



                          调用函数指针

    通过使用函数指针,我们能够动态地在代码中插入不同行为的函数,从而使代码拥有动态改变自身行为的能力。



作者:Matther Curland

要求:使用本文的示例代码,你需要VB5或VB6的专业版或企业版。

    从Visual Basic 5.0开始Basic语言引入了一个重要的特性:AddressOf运算符。这个运算符能够让VB程序员直接体会到将自己的函数指针送出去的快感。比如我们在VB里就能够得到系统字体的列表,我们能够通过标准的API调用来进行子类化。一句话,我们终于可以象文档里所说的那样来使用Win32 API了。

    不过,这个新玩具只能给我们带来短暂的快感,因为这个礼物并不完整。我们可以送出函数指针,但却没人能将函数指针送给我们。事实上,我们甚至不能给我们自己送函数指针,这使我们不能够体验送礼的真正乐趣(译者:呵呵,光送礼却不能收礼的确没趣)。AddressOf让我们看到了广袤天地的一角,但是VB却不让我们全面地探索它,因为VB根本就不让我们调用函数指针,我们只能提供函数指针(译者:可以先将函数指针送给API,然后让API回调自已的函数指针来完成函数指针调用的功能,但这还是要先把礼物送给别人)。其实,我们能够自己来实现调用函数指针的功能,我们可以手工将一个对COM接口的vTable绑定调用变成一个函数指针调用。最妙的是:我们能够在纯VB里写出调用函数指针的代码,不需要任何辅助的DLL。



    告诉编译器函数指针是什么样子,是使VB能够调用任何函数的关键。将参数类型和返回值类型交给VB编译器,让编译器将我们的函数调用编译到我们的程序里,这样程序才能在运行时知道怎样去定位函数。在程序被编译后,一个函数就是内存里一串汇编字节流,通过CPU解释执行而形成我们的程序。调用一个函数指针,首先需要程序获得指向这个函数字节流的指针,再通过x86汇编指令call将当前指令指针(译注:即x86汇编里的IP寄存器)转到函数所在的字节流上。在函数完成后,再用ret指令返回给调用此函数的程序来继续操作。



    我下面将要提到的方法,利用了VB自己的函数调用方式,所以我先来解释一下VB是怎样来实现函数调用的。VB内部使用三种函数指针,但是,在本质上,不论VB是如何来定位这几类函数指针,调用它们的方法却是一样的。VB编译器必须知道准确的函数原型才能生成调用函数的代码。



    第一类,最常见的函数指针类型,就是VB用来调用函数的普通指针,这样的函数定义在标准模块内(或类模块里的友元函数和私有函数)。调用友元函数和私有函数时,调用指令定位在当前指令指针的一个偏移地址处,或者先跳到一个记录着函数位置的查找表里,再跳到函数内(译者:即先"Call 绝对地址"跳到一个跳转表内,表里的每个入口都是一个"Jmp"到函数)。这些函数都在同一个工程内,联结器总是将所有的模块联结在一起,所以总是知道在内存何处能够找到VB内部函数,因此转移控制到内部函数时,其运行时开销是很少的。



VB对某些函数指针的调用却困难得多

    对于另两类函数指针,VB必须在运行时进行额外的工作才能够找出它们。

    第二类,VB调用一个COM对象接口里的方法。我们可能认为建立COM对象的工作是相当复杂的,如果完全用VB来为我们建造COM的所有组成部分的话,但事实上并不是这样。按照COM的二进制标准,一个COM对象是一个指针,这个指针指向一个结构,这个特定结构的第一个元素是一个指向函数指针数组的指针。这个函数指针数组(又叫虚拟函数表,简称vTable)里的前三个指针,一定是标准QueryInterface,AddRef,Release函数。vTable里接下来的函数符合给定的COM对象接口定义里的函数定义(见图一)









图一:

函数指针代理是怎么工作的?click here



    当VB通过一个对象类型的变量来调用一个COM对象的方法或属性时,这个变量里存放着对这个COM对象接口的引用。VB要定位函数时,首先要通过COM引用的第一个元素来获得指向vTalbe的指针,然后才能在vTable里定位函数指针。对一个vTable调用来说,编译器提供了COM
分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
收藏收藏 分享分享 分享淘帖 订阅订阅
2#
 楼主| 发表于 2005-8-28 08:51:00 | 只看该作者
动态指定函数指针

    无论是Declare还是库型库,当函数载入后,VB调用函数指针的方式是一样的。指针已经因为先前的调用而被载入了,所以第二次调用会更快,并且速度接近调用静态联结的函数。Declare语句是VB调用动态载入的函数指针的最自然的方法。但是,函数指针由VB决定而不是由我们来指定(译者:此为原文直译,意思应该是:函数指针只能在编译前指定,由VB来载入,而不能在运行时指定由我们自己动态载入的函数指针),所以我们不能用Declare语句来调用任意的函数指针。Declare语句的限制使我们只能载入在设计时通过Lib和Alias字句指定的函数。



    到这里,我已经解释了VB是怎么样来调用自己的函数指针的。对VB本身没有的功能进行扩展都应该通过VB本身提供的工具来实现(译者:看来作者Matt是一位VB纯粹论支持者)。静态联结不用考虑——如果你喜欢自己修改PE文件头的话,请自便(译者:关于修改PE头来Hook输入函数的方法,在1998年2月MSJ专栏Bugslayer里,John Robbins大师就用纯VB实现了HookImportedFunctionsByName,不过用来调用函数指针那是杀鸡用牛刀)。我们不可能静态地指定函数指针,所以Declare语句也不用考虑。但是,我们能够在VB里自己用LoadLibaray和GetProcAddress这两个API来从外部DLL里获取函数指针,就象Declare为我们做的那样。vTable调用是唯一一种让VB自已绑定函数的调用方式。我们的任务是建一个符合COM二进制标准的结构,再将这个手工建立的COM对象的引用放到一个对象类型的变量里,然后调用手工建立的vTable入口。通过调用这个vTable里的函数,就能够直接代理到要调用的函数指针。我称这个对象为FunctionDelegotor(函数代理者)。



    这个方法需要我们解决三个特有的问题。第一,vTalbe调用有额外的参数(this指针),我们不想将它也传给我们的函数指针。所以我们需要一个通用的代理函数来将这个额外的this指针处理掉,然后才能进行调用。第二,我们需要建立一个vTable里有这个代理函数的COM对象。第三,我们需要一个接口定义才能让VB编译器知道我们的函数指针的样子。接口定义应该将函数原型也包括在vTable里,并且和代理函数在对象vTable里的位置一样(译者:当通过接口调用函数指针时,只有这样才能够让代理函数处理掉做为函数参数压在栈里的this指针)。



    我们可以用汇编代码很容易地的写出代理函数(译者:对作者Matt来说的确很容易,因为他对在VB里插入线内汇编代码有相当深入的研究。其实作者这里的容易也是相对于Alpha平台来说的)。在Intel平台,所有传递给COM对象或标准API调用的参数都是通过堆栈来传的。不幸的是,对Alpha平台的VB来说不是这样,它不能提供一种简单的方法来写出同样功能的汇编代码(译注:Alpha平台是一个RISC精简指令集系统,其参数传递多直接使用寄存器,要在这个平台上手工写汇编代码要难得,从他的书的目录里知道他在书里专门拿出一节介绍Alpha平台下的汇编代码)。



压栈

    只要我们知道栈是什么样子,我们就可以很清楚的知道汇编代码需要做什么。VB仅仅支持符合stdcall调用规范的函数。这种调用规范,参数总是从右向左压入栈中,并且是由调用者来负责栈的清理。清理的义务跟本文没什么关系,但是压栈的顺序却很重要。尤其要注意的是COM类里的this指针(在VB类里称为Me),它总是作为最左边的参数压栈的。当函数被调用时,函数返回地址(函数返回后程序继续执行的地方)也被call指令本身压入栈中。在任何COM接口输出函数被执行前,栈的样子如下:



parameter n (第n个参数,最右边的参数)

...

parameter 2

parameter 1 (第1个参数)

this pointer(暗藏的this指针才是最左前的参数)

return address (返回地址)



    但是,我们只想调用函数指针,并不需要暗藏的相关联的this指针。调用一个符合vTable调用却没有额外参数的函数,需要我们将this指针从栈里挤出来,然后才能将控制转移到目标函数指针。让this指针在栈里放着的好处是因为它指向结构。考虑我们定义了一个结构,它的第二个成员是一个函数指针。这个成员距结构开始位置的偏移是4个字节。那么将这个函数指出挤出来并通过代理函数调用它的汇编代码如下:



;弹出返回地址到临时的ecx寄存器,

;后面还要将它恢复。

pop ecx



;从栈里弹掉this指针(译注:做为后面跳转的基址)

pop eax



;重新将ecx寄存器里保存的返回地址压栈

;以使得函数指针调用后知道返回到哪儿

push ecx



;将控制转移到函数指针,

;它在this指针后偏移4个字节处。

jmp DWORD PTR [eax + 4]



这四条指令的连在一起需要6个字节:59 58 51 FF 60 04。我们在后面补两个Int3指令(CC CC)以凑足8个字节,这正好可以一个VB的Currency变量内。这样一个Currency变量的地址里会放着如下的magic number(幻数)——368956918007638.6215@ ——这个Currency变量是指向代理函数的函数指针。这个代理函数挤掉this指针,并可跳到任何函数,而不用考虑函数的参数。这就是说,我们可以用同样的汇编代码来代理任何函数指针。我们现在需要一个vTable来包含这个指向字节流的指针
3#
 楼主| 发表于 2005-8-28 08:52:00 | 只看该作者
Listing 1 这段代码将一个FunctionDelegator转换成一个支持特定函数指针的COM对象。这是一个特殊的COM对象,因为它不要求任何内存分配并且对我们的接口请求总是盲目合作。请求仅有的正确接口是我们的责任。



'The magic number

Private Const cDelegateASM _

   As Currency = -368956918007638.6215@



'到处到用的辅助函数

Private Declare Sub CopyMemory _

   Lib "kernel32" Alias "RtlMoveMemory" _

   (pDest As Any, pSrc As Any, ByVal ByteLen As Long)



Private m_DelegateASM As Currency



'vTable的类型声明

Private Type DelegatorVTables

   'OKQI vtable in 0 to 3, FailQI vtable in 4 to 7

   VTable(7) As Long

End Type



Private m_VTables As DelegatorVTables



'指向vtable的指针, 成功QI

Private m_pVTableOKQI As Long



'指向vtable的指针, 失败QI

Private m_pVTableFailQI As Long



'函数指针代理的结构声明

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



'初始化FunctionDelegator结构,并将指向它的指针

'    作为一个COM对象返回.

Public Function InitDelegator( _

   Delegator As FunctionDelegator, _

   Optional ByVal pfn As Long) As IUnknown

   '第一次访问时初始化vTable

   If m_pVTableOKQI = 0 Then InitVTables

   With Delegator

      .pVTable = m_pVTableOKQI

      .pfn = pfn

   End With

   CopyMemory InitDelegator, VarPtr(Delegator), 4

End Function



'初始化vTable

Private Sub InitVTables()

Dim pAddRefRelease As Long

   With m_VTables

      .VTable(0) = _

         FuncAddr(AddressOf QueryInterfaceOK)

      .VTable(4) = _

         FuncAddr(AddressOf QueryInterfaceFail)

      pAddRefRelease = FuncAddr(AddressOf AddRefRelease)

      .VTable(1) = pAddRefRelease

      .VTable(5) = pAddRefRelease

      .VTable(2) = pAddRefRelease

      .VTable(6) = pAddRefRelease

      m_DelegateASM = cDelegateASM

      .VTable(3) = VarPtr(m_DelegateASM)

      .VTable(7) = .VTable(3)

      m_pVTableOKQI = VarPtr(.VTable(0))

      m_pVTableFailQI = VarPtr(.VTable(4))

   End With

End Sub



'成功QI

Private Function QueryInterfaceOK( _

   This As FunctionDelegator, _

   riid As Long, pvObj As Long) As Long

   '对第一次请求总是盲目合作

   pvObj = VarPtr(This)

   '交换成失败时vTable,仅在调用函数指针会返回HRESULT错误代码

   '    时才需要这么做,当然这么做总是更安全。

   This.pVTable = m_pVTableFailQI

End Function



Private Function AddRefRelease( _

   ByVal This As Long) As Long

   '什么都不做,无需要引用计数。

End Function



'失败QI

Private Function QueryInterfaceFail( _

   ByVal This As Long, _

   riid As Long, pvObj As Long) As Long

   '对任何请求都说:"不"

   pvObj = 0

   QueryInterfaceFail = &H80004002 'E_NOINTERFACE

End Function



'返回函数指针的辅助函数

Private Function FuncAddr (ByVal pfn As Long) As Long

   FuncAddr = pfn

End Function



译者:上面的代码在原文已经发表后经过了修改,因此原文没有提到为什么上面的代码需要两个不同的vTable。Matt在更新的示例代码的Readme文件里解释这个原因。我下面将这个原因简单的叙述如下:

    这是因为当调用的函数指针需要返回HRESULT错误代码时,VB会用再次调用QI来向对象请求一个ISupportErrorInfo接口的引用。但是,由于原来代码里的QI完全采用盲目合作的信任方式,它总是返回对象自身的接口指针,哪怕它并不支持所要求的接口。由于返回的接口引用并不支持ISupportErrorInfo,所以当VB试图用ISupportErrorInfo的方法来搜集错误信息时程序就会崩溃。解决的办法,就是提供两个vTable。当第一次调用初始化后的vTable里的QI时,它采取信任方式返回接口指针,并在返回之前将包含失败QI的vTable交换进来。这样下一次访问的QI将是失败QI,而失败QI拒绝所有接口请求,这样就有效的阻塞了后继的QI请求,包括VB对ISupportErrorInfo的请求。在后面的Listing3的代码中我们可以看到,一旦我们增加引用就会有类型不匹配错误。

    还有VB在对Err对象的处理上有BUG,那就是当VB用QI向某个对象请求ISupportErrorInfo接口失败后,Err对象内总是保留着对这个对象的引用。由于我们的vTalbe会先于Err对象释放,所以Err对象里有一个挂起的引用,当释放Err对象时程序会崩溃。解决的方法是:在程序结束前自己用Err.Raise来引发一个新错误。具体做法,见源代码。



   

Listing 2 用来告诉VB编译怎样调用我们的函数指针的外部ODL文件。没有对这个接口的描述,我们虽仍能生成代理到正确函数指针的COM对象
4#
 楼主| 发表于 2005-8-28 08:54:00 | 只看该作者
How VB Allocates Fixed-Size Arrays <TR align=left bgColor=#ffffcc>You might think the entire array is being created as a local variable on the stack when you allocate a fixed-sized array in VB (I did). This is only a half-truth. The array descriptor, a structure that describes an array抯 size and type, is placed on the stack like any other local variable. However, the array抯 data is allocated on the heap. If you want a fully stack-allocated array, you need to embed your fixed-size array in a user-defined type (UDT) and use a local variable typed as the UDT, not as a fixed-size array. The reason for these allocation semantics is that the 64K size restriction on embedded fixed-size arrays does not apply to nonembedded fixed-size arrays, but the 64K limit does apply to the total size of local variables. Similar semantics apply to a module-level fixed-size array, except that embedded fixed-size arrays are allocated with the module instead of on the stack. You should take the precaution of defining all vtables as fixed-size arrays in a UDT to protect against the case where an object is still pointing at a function delegator structure when VB is tearing the process down. During teardown, VB clears all heap-allocated variables (objects, arrays, strings) in all modules before releasing the memory allocated for each specific module. Your tear-down code is dependent on the ordering of your modules in the VBP file if your vtable is heap-allocated, and you don抰 have to worry about custom vtables vaporizing beneath your objects or disappearing during tear-down if you define any custom vtable in an embedded fixed-size array.
5#
 楼主| 发表于 2005-8-28 08:55:00 | 只看该作者
本文的附图:









我花了整整5个小时,从网上找到的本文的VB源码:







[此贴子已经被作者于2005-8-28 1:33:01编辑过]

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x
6#
发表于 2005-8-29 16:06:00 | 只看该作者
提示: 作者被禁止或删除 内容自动屏蔽

点击这里给我发消息

7#
发表于 2005-8-30 02:17:00 | 只看该作者
好文!!
您需要登录后才可以回帖 登录 | 注册

本版积分规则

QQ|站长邮箱|小黑屋|手机版|Office中国/Access中国 ( 粤ICP备10043721号-1 )  

GMT+8, 2024-4-30 10:24 , Processed in 0.084787 second(s), 32 queries .

Powered by Discuz! X3.3

© 2001-2017 Comsenz Inc.

快速回复 返回顶部 返回列表