【问题标题】:Overload COM class methods with methods from a standard module using VTable hacking使用 VTable hacking 使用标准模块中的方法重载 COM 类方法
【发布时间】:2021-03-29 21:10:24
【问题描述】:

快速提问 - 我刚刚通过使用低级复制内存 API 更改其 VTable 中的条目来测试类的覆盖方法。

背景

我已经取得了一些成功,如果它们具有相同的签名,则可以交换一个类的 VTable 中的 2 个条目。所以像这样的类定义:

Option Explicit

Public Sub Meow()
    Debug.Print "Meow"
End Sub

Public Sub Woof()
    Debug.Print "Woof"
End Sub 

... 生成一个像这样的 VTable:

...我可以交换位置 7 和 8 的条目以使 cls.Meow 打印 Woof ,反之亦然。我还可以将一个类的 VTable 中的条目与完全不同的 VTable 交换(前提是我不尝试通过调用 Me.anything 来取消引用隐式 this 指针)

这样我就可以再上课了

Option Explicit

Public Sub Tweet()
    Debug.Print "Tweet"
End Sub

并将Woof 的行为从一个与另一个Tweet 交换。不太复杂,有需要的可以分享代码。

我不能做什么...

...但是,想知道如何用标准模块中的方法交换类方法吗?

根据this 文章,看来构建 VBA 的 COM 机制需要 VBA 隐藏的两个类方法:

  • 他们有一个隐含的this 指针
  • 他们返回 HRESULT (typedef long)

原来如此

Public Sub Meow()

在一个类模块中Class1等价于

Public Function Meow(ByVal this As LongPtr) As Long

我也试过

Public Function Meow(ByRef meObj As Class1) As Long
Public Function Meow(ByRef meObj As Class1) As LongPtr 'but HResult is 32 bit int
Public Sub Meow(ByVal this As LongPtr)

等等。但是当我尝试从 VTable 调用该方法时,VBA 总是崩溃。所以我有点茫然。我想知道 64 位计算机上的情况是否有所不同,或者标准模块函数是否对调用堆栈做了一些奇怪的事情。问题是我见过examples of code,其中整个 VTable 是由标准模块函数组装而成的,所以我知道这是可能的,但只是不确定如何正确转换签名

如何使用标准模块中定义的方法覆盖 VTable 条目?

【问题讨论】:

  • 在 x64 上的行为完全相同。但是,您可以将 IUnknown 和 IDispatch 方法“重定向”到 .bas 模块中的函数(以及链接示例中的 IEnumVariant 方法)。我认为这些工作是因为它们不是 VB。我认为由于 VB 类的工作方式,您无法重定向 VB 类方法。考虑Me 关键字(其行为类似于函数/属性获取)。您不可能在 .bas 模块中复制 Me 行为。幕后一定有更多的事情发生。也许调用约定也不同。

标签: vba com 64-bit vtable iunknown


【解决方案1】:

我对您的问题的评论只是部分正确。我仍然相信Me 关键字在防止类方法“重定向”到标准.bas 模块中的方法方面发挥作用。但这仅适用于早期绑定。

IDispatch::Invoke 实际上可以毫无问题地调用 .bas 模块中的方法。您的初始方法签名是正确的:

Public Function Meow(ByRef meObj As Class1) As Long

Class1代码:

Option Explicit

Public Sub Meow()
    Debug.Print "Meow"
End Sub

Public Sub Woof()
    Debug.Print "Woof"
End Sub

标准 .bas 模块中的代码:

Option Explicit

Sub Test()
    Dim c As Object 'Must be late-binded!
    Dim vTblPtr As LongPtr
    Dim vTblMeowPtr As LongPtr
    Dim originalMeow As LongPtr
    '
    Set c = New Class1
    c.Meow 'Prints "Meow" to the Immediate Window
    '
    'The address of the virtual table
    vTblPtr = MemLongPtr(ObjPtr(c))
    '
    'The address of the Class1.Meow method within the virtual table
    vTblMeowPtr = vTblPtr + 7 * PTR_SIZE
    '
    'The current address of the Class1.Meow method
    originalMeow = MemLongPtr(vTblMeowPtr)
    '
    'Replace the address of Meow with the one in a .bas module
    MemLongPtr(vTblMeowPtr) = VBA.Int(AddressOf Moew)
    '
    c.Meow 'Prints "Meow in .bas" to the Immediate Window
    '
    'Revert the original address
    MemLongPtr(vTblMeowPtr) = originalMeow
    '
    c.Meow 'Prints "Meow" to the Immediate Window
End Sub

Public Function Moew(ByVal this As Class1) As Long
    Debug.Print "Meow in .bas"
End Function

我使用LibMemory 进行内存操作。

如果您将Meow 类方法更改为Function 而不是Sub,那么您需要在 .基本模块。

编辑#1

我想到了下面 cmets 中讨论的问题,我能想到的唯一原因是 IDispatch 仅适用于指向 IUnknown 接口的指针。

这意味着:

Public Function Meow(ByRef this As Class1) As Long

会使应用程序崩溃

但是,这行得通:

Public Function Moew(ByVal this As Class1) As Long
    Debug.Print "Meow in .bas"
End Function

因为传递 ByVal 会强制 IUnknown 上的 QueryInterface 和 AddRef(退出范围时使用 Release)

这也有效:

Public Function Moew(ByRef this As IUnknown) As Long
    Debug.Print "Meow in .bas"
End Function

编辑#2

抱歉再次编辑。

Invoke 方法没有使用指向 IUnknown 的指针。它正在使用指向 IDispatch 的指针。这可以通过以下方式检查:

Public Function Moew(ByVal this As LongPtr) As Long
    Debug.Print this
    Debug.Print "Meow in .bas"
End Function

这会将 ptr 打印到 IDispatch 接口。那么,为什么ByRef this As Class1 会失败?为什么ByVal this As Class1ByRef this As IUnknown 有效?

ByRef this As Class1
我相信 VB 无法访问 VarPtr(this) 地址,因此我们正在读取我们不应该读取的内存。 IUnknown 接口上没有额外的 AddRef 或 Release,因为该方法永远不会使用此声明调用。当 Invoke 尝试调用该方法时,应用程序只会崩溃。

ByVal this As Class1
该方法只是创建一个 VB 变量(在 VB 内存空间上)并调用 AddRef

ByRef this As IUnknown
由于这不是双接口,因此完成了对 QueryInterface 和 AddRef 的调用。 'this' 的内存地址在本地内存空间,和第二个例子一样。

【讨论】:

  • 感谢您的跟进!一个问题;您使用 ByVal this 而不是 ByRef;那是错字吗?你有没有幸运地调用this 的任何方法或属性?
  • 抱歉。我什至没有意识到。我这样做是出于习惯。很久以前,在挂钩 IUnknown::Release 时,我发现当非 VB 函数调用 VB 函数时,最好将实例参数传递给 ByVal 以避免崩溃。我会想一个解释,如果我想出一个像样的解释,我会相应地编辑答案。是的,使用答案中的代码允许我访问 .bas 模块的 Meow 方法中的类的所有方法。引发错误也可以正常工作。
  • @Greedo 编辑了答案,希望我的假设是正确的。顺便说一句,你似乎总是问正确的问题。干得好!
  • @Greedo 我又做了一次编辑。希望这次我做对了。
  • @Greedo 您的问题帮助我更新了instance redirection 逻辑中的逻辑(参见编辑#1)。谢谢你!与后期绑定相比,早期绑定中类的入口点似乎确实有一些额外的安全机制。
猜你喜欢
  • 1970-01-01
  • 2016-06-26
  • 1970-01-01
  • 2017-01-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-12-12
  • 1970-01-01
相关资源
最近更新 更多