开发报告2

Posted 2940500426yingxin

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了开发报告2相关的知识,希望对你有一定的参考价值。

内容 介绍你函数指针怎么说的?使用指针不是类型安全的使调用传递参数嵌入本地代码返回值超出基本摊薄与基于转发结论修订历史 图1:想象这样在VB代码是合法的。 介绍 这个调查的目的是使在Visual Basic 6编写的应用程序使用函数指针。其他优势可能被发现,比如在Visual Basic应用程序中嵌入本地代码,从而扩展可能性的世界不需要外部的dll。为了保持尽可能简单和简洁,使用的其他变体技术被忽视了,一直保持关注细节的方法处理更为常见的情况。在阅读综合考试之前,我假设你想看到一个工作示例项目:NativeCode.zip。 你是说函数指针吗? 因为指针一般没有Visual Basic 6具体,这也许听起来疯狂谈论在Visual Basic 6函数指针。所以,让我们先看我们如何通常在Visual Basic 6得到的函数指针。AddressOf运营商吗?是的,但不是!嗯,还有从kernel32.dll GetProcAddress Win32 API可用于在运行时检索地址由其他dll函数导出。不仅仅是这个……还有其他的场景!但是为什么一个使用运行时加载时可以使用声明语句?因为同样的原因一个C / c++程序员,例如,使用不同的DLL(或相同版本的DLL)取决于应用程序运行的环境。 事实上,基于加载插件的整体概念,可能在需求,外部组件出口预期的功能。例如,一些编解码器甚至下载需求,然后使用它们的应用程序加载。同时,函数指针可能由外部模块给出一个回调(即委托),其价值可能取决于一些对象的状态。在OOP中,一个众所周知的行为模式被称为“模板法”的特点是在运行时通过改变控制流。使用函数指针可以减少相当努力的实现通过减少需要定义的类的数量。 使用指针不是类型安全的 如果你曾经使用Visual Basic 6中的EnumWindows API,您可能已经注意到,没有什么强制你通过回调,一个函数的地址与正确的原型。代码在运行时将会失败,但是编译没有抱怨。代表们在Visual Basic . net克服这个问题,虽然他们的主要目的可能是确保可用性的代码,因为CLR可能丢弃不引用的编译后的代码。而其他语言有一种申报类型安全的指针(如C / c++类型定义的),在Visual Basic 6中我们只能把他们当作签署4字节数。开发人员将负责类型检查,从编译器没有任何帮助。 使电话 选择的方法调用地址只要正在取代条目存储在“虚拟函数表”(音频电报)的一个类,因为它提供了足够的灵活性,同时仍然容易使用。它有同样的行为在IDE,本机代码和p,这有助于调试。vftable是一种机制中使用的编程语言实现为了支持动态多态性,即运行时绑定的方法。一些在线资源描述如何Visual c++ 6.0编译器创建vftables类,我找不到任何关于Visual Basic 6.0编译器。 通常,编译器为每一个类创建一个单独的vftable和存储每个基类的指针作为隐藏的成员,经常是第一个成员。Visual c++编译器构建的每个类vftable只读页面代码的部分类似于字符串字面值。为了修改其内容,需要使用VirtualProtect API为暂时改变访问权限。第一个地址vftable由Visual c++指向标量类的析构函数,这是紧随其后的是地址的虚函数声明的顺序。 在Visual c++支持多重继承和处理纯虚拟函数调用,我们的目标可以实现通过识别Visual Basic如何检索一个公共方法的地址的地址从一个类的一个实例。目视检查的类实例被要求建立vftable的位置和内容。显示内存内容从同一个类的两个实例的地址,我们应该能够识别vftable的指针。两个实例指向同一个vftable以来,指针值必须相同,必须属于我们的视觉基本模块(默认情况下,addr开始ess 0 x00400000)。可以看到,指针vftable是作为第一个4字节存储在类实例。 图2:两个对象共享相同的音频电报。 图3:地址在抵消,H1C属于我们的模块。 前七地址在msvbvm60.dll vftable指出代码中。修改类定义通过添加更多的公共方法将改变vftable内容开始偏移,H1C。为了巩固理论,我写了一个外部的DLL闯入Visual Basic调用对象的方法和阅读的拆卸Visual c++调试器。 这是比听起来更容易。全球过程创建Visual Basic类的一个实例,并调用它的第一个公共方法。在运行时,显示全局函数的地址,地址vftable类实例的指针。加载过程和Visual c++调试器按F11。写下当前指令指针(eip的应用程序的入口点)和改变它的地址全球过程(输入寄存器窗口,按回车)。在这个地址设置一个断点。改变eip旧值和恢复执行。 当达到断点,单步调试代码,观察注册表值,直到类的方法被调用。截图展示了eax寄存器的地址对象(0 x14be70),从这4个字节复制到ecx寄存器,代表音频电报(0 x4033b8)的地址。指令在0 x402391调用第一个公共方法的类,如您所见,它抵消在vftable, H1C。 图4:拆卸展示如何从vftable获得地址。 我继续看私有成员数据或调查方法影响vftable的位置或内容。答案是否定的,但是公共成员变量变化的内容vftable插入访问器和修饰语时将调用使用数据以外的类成员。概念证明,我写了以下测试: 隐藏,收缩,复制Code

VERSION 1.0 CLASS
Attribute VB_Name = "DynamicVFT"
Byte offset of first index in Virtual Function Table (VFT).
Private Const OffsetToVFT = &H1C
Swaps the addresses of 2 public methods with the same prototype.
Private Sub SwapPlayers()
  Dim pVFT As Long
  CopyMemory pVFT, ByVal ObjPtr(Me), 4 get the VFT address.
  Dim fnAddress As Long
  CopyMemory fnAddress, ByVal pVFT + OffsetToVFT, 4 get AddressOf Play.
  CopyMemory ByVal pVFT + OffsetToVFT, ByVal pVFT + OffsetToVFT + 4, 4
                replace Play address with Replay address.
  CopyMemory ByVal pVFT + OffsetToVFT + 4, fnAddress, 4 _
            replace Replay address with Play address.
End Sub
Address of this method can be found at first index in the VFT.
Public Sub Play()
  Debug.Print "Dynamic VFT plays: White move."
  Call SwapPlayers
End Sub
Address of this method can be found at second index in the VFT.
Public Sub Replay()
  Debug.Print "Dynamic VFT plays: Black move."
  Call SwapPlayers
End Sub
Sub Main()
  Phase 1: Making the call.
  Dim dynObj As New DynamicVFT
  Dim idx As Long
  For idx = 0 To 9
    dynObj.Play
  Next idx
End Sub

请注意,这些方法有相同的原型。交换的地址vftable像预期的那样工作,以上代码的输出如下所示: 隐藏,复制Code

Dynamic VFT: White move.
Dynamic VFT: Black move.
Dynamic VFT: White move.
Dynamic VFT: Black move.
...

所以,改变值的音频电报Visual Basic 6类将取代这个类的方法。更重要的是要记住当修改vftables之一,是他们被类的所有实例共享。 传递参数 假设我们将改变音频电报地址的一个类,需要进一步检查调用这些方法。本节将描述使用的调用协定,以及参数在堆栈上的位置。从音频电报显示一个地址的一员过程一个长参数(4字节的堆栈)和加载过程在Visual c++调试器,组装可以观察到那个地址。它属于一个跳表: 隐藏,复制Code

00401451 jmp         004025A0

跳表的每个条目指向的位置定义的成员方法编译后的代码: 隐藏,收缩,复制Code004025A0 ebp推 004025 a1 mov ebp, esp 004025 a3子esp, 0 ch 004025 a6推004025 h 004025 ab mov eax, fs (00000000): 004025 b1 eax推 004025 b2 mov dword ptr fs: [0], esp 004025 b9子esp, 8 公元前004025 ebx推 004025 bd推动应急服务国际公司 004025是edi推 004025字bf mov ptr ebp-0Ch, esp 004025 c2 mov dword ptr [ebp-8], 004025 h 004025 c9 mov dword ptr ebp-4, 0 004025 d0 mov eax, dword ptr (ebp + 8) 004025 d3 eax推 004025 d4 mov连成一片,dword ptr [eax] 004025 d6叫dword ptr (ecx + 4) 004025 d9 mov eax, dword ptr (ebp + 8) 004025直流eax推 004025 dd mov edx, dword ptr [eax] 004025 df叫dword ptr [edx + 8] 004025 e2 mov eax, dword ptr [ebp-4] 004025 e5 mov连成一片,dword ptr [ebp-14h] 004025 e8流行edi 004025 e9流行应急服务国际公司 004025 ea mov dword ptr fs:[0],连成一片 004025 f1流行ebx 004025 f2 mov esp, ebp 004025 f4流行ebp 004025 f5 ret 8 图5:栈的差异。 对于经验丰富的装配读者,上面的清单很简单。在这一点上,重点是指令修改堆栈。事实上,最后指令告诉我们几乎所有我们需要知道。首先,被清理堆栈,而不是调用者。因此,调用协定不能__cdecl或__fastcall。因为这方法只需要一个长时间的参数(4字节大小),为什么方法从堆栈中删除8个字节?因为之前有一个额外的参数压入堆栈调用:我们的指向对象的指针调用过程(又称这个指针)。 确认__thiscall调用协定并不使用,还有一个厕所k在装配清单。你会发现第一次使用ecx寄存器,在4025 d4地址,它被写,而不是阅读。因此,指针指向的对象不是通过ecx寄存器,也没有其他寄存器。连看都没看一眼,对方做什么,我们已经可以考虑的对象指针调用协定作为__stdcall作为最后一个参数传递到栈上。这毫不奇怪,因为Visual Basic 6广泛使用它。另一项测试是为了,应该确认最后的价值,附加参数压入堆栈。记住在__stdcall调用约定,参数从右到左,宣布。 隐藏,收缩,复制Code

VERSION 1.0 CLASS
Attribute VB_Name = "MemberVsGlobal"
Replaces address of MemberProcedure with given address at which
should reside a procedure using
__stdcall calling convention and accepts 2 parameters of type Long;
restores the original address
of MemberProcedure when given address is 0
Private Sub ReplaceMemberWithGlobal(ByVal fnAddress As Long)
  Dim pVFT As Long
  CopyMemory pVFT, ByVal ObjPtr(Me), 4 get the VFT address
  Static oldAddress As Long
    static variable which stores the original MemberProcedure address
  If (oldAddress = 0) Then
    CopyMemory oldAddress, ByVal pVFT + OffsetToVFT, 4
                get MemberProcedure address
  End If
  If (fnAddress = 0) Then
    CopyMemory ByVal pVFT + OffsetToVFT, oldAddress, 4
                restores original MemberProcedure address
  Else
    CopyMemory ByVal pVFT + OffsetToVFT, _
      fnAddress, 4 replace MemberProcedure address with given global address
  End If
End Sub
Restores the original MemberProcedure address
Private Sub Class_Terminate()
  ReplaceMemberWithGlobal 0
End Sub
Its address in the VFT will be replaced with given fnAddress
after the first call, thus becoming inaccessible after its first return
Public Sub MemberProcedure(ByVal fnAddress As Long)
  Debug.Print Hex$(ObjPtr(Me)) & ".MemberProcedure(0x" & Hex$(fnAddress) & ")"
  ReplaceMemberWithGlobal fnAddress
End Sub
Procedure for replacing a member procedure
Private Sub GlobalProcedure(ByVal objInstance As Long, ByVal parameter As Long)
  Debug.Print Hex$(objInstance) & ".GlobalProcedure(0x" & Hex$(parameter) & ")"
End Sub
Sub Main()
  Phase 2: Passing parameters
  Dim MvsG As New MemberVsGlobal
  MvsG.MemberProcedure AddressOf GlobalProcedure
  MvsG.MemberProcedure &HB0A
End Sub

注意,在输出中有相同的值通过ObjPtr(我)在类的方法作为objInstance参数对全球过程取代它,从而证实了我们的理论,一个指向对象的指针被推到堆栈调用之前: 隐藏,复制Code

2499D8.MemberProcedure(0xAB16B4)
2499D8.GlobalProcedure(0xB0A)

一个有趣的观察行为在运行下的最后一个例子IDE。如果您删除Class_Terminate实现,运行代码不会调用原始MemberProcedure的两倍,如果不是翻拍(Alt + F K)可执行或重新加载项目。考试所需的IDE下这种行为并不是我们的目的,但是好知道如果你观察一个腐败对象调试时,你应该重塑之前重新启动执行。 嵌入本地代码 在这一点上,我们知道,我们可以使用一个全局过程取代过程类的一个成员如果全球过程具有相同的原型,但用一个额外的参数:指针指向的对象被称为方法。嗯,但是我们需要的是几乎相反的!我们不需要调用全球过程,作为第一个参数,一个指向对象的指针。我们如何删除额外的参数调用之前到达全球过程?我们将嵌入一些本地代码写在组装。 这样的实现被称为存根或代理。在其他情况下,它可以用于日志记录或生成子类。我们的目的是适应调用。当调用成员程序,将包含返回地址栈,指向对象的指针,其次是调用的方法的参数。我们的存根应该删除指向对象的指针,如返回地址立即紧随其后的参数,然后跳转到所需的转发地址。记住,我们假设在这个阶段的转发地址指向一个过程,不是一个函数(不返回任何值)和它的调用协定__stdcall。大会将做这项工作: 隐藏,复制Code

pop         eax // remove return address from stack
pop         ecx // remove pointer to object from stack
push        eax // push return address onto stack
mov         eax,XXXXXXXXh // XXXXXXXX can be replaced here with
             // any forwarding address
jmp         eax // jump to forwarding address

您可以使用任何从上面的汇编程序生成本机代码。我写了一个Visual c++ __asm块然后复制从拆卸视图产生的本地代码。本机代码也可以发现在相关的*。鳕鱼文件(装配清单文件)如果您设置Visual c++编译器选项/ FAc或流式细胞仪(清单文件类型:组装机器码或汇编,机器代码,和源)。我们如何将本机代码嵌入到Visual Basic 6 ?复制清单;删除地址每一行的开头和组装来源,只保留这台机器代码字节十六进制字符;删除任何间距;格式为Visual Basic常量字符串,你应该获得这样的:“58”,“59”,“50”,“B8XXXXXXXX”,“FFE0”。 XXXXXXXX可以拥有任何你想要的价值,因为它将取代与转发地址,我们需要在运行时调用,函数指针。声明一个十六进制字符串作为常数;将它转换为一个字节数组;分配相同数量的字节GlobalAlloc API和复制的字节数组;使用的内存处理作为我们的本机代码的地址;丢弃的分配GlobalFree API时,本机代码不需要了。展示嵌入本机代码是如何工作的,一个存根能够成功取代成员过程与非成员过程相同的原型,我提供一个更一般的方法在以下类的实现和使用类的示例测试: 隐藏,收缩,复制Code

VERSION 1.0 CLASS
Attribute VB_Name = "StubCallToSub"
Private Const hexStub4ProcedureCall = "58" & "59" & "50" & "B822114000" & _
                                    "FFE0"
An array that saves the original addresses to the member procedures
Private VFTable() As Long
An array that saves addresses of allocations made for stubs,
in order to be freed
Private VFArray() As Long
Sets initial state of the private arrays,
thus UBound will not fail on first call
Private Sub Class_Initialize()
  ReDim VFTable(0)
  VFTable(0) = 0
  ReDim VFArray(0)
  VFArray(0) = 0
End Sub
Removes only existing stub for member specified by index
Private Sub RemoveStub(ByVal index As Long)
  Dim pVFT As Long
  CopyMemory pVFT, ByVal ObjPtr(Me), 4 get the VFT address
  If (index < 1) Then Exit Sub
  If (index > UBound(VFTable)) Then Exit Sub
  If (VFTable(index) <> 0) Then stub exists for this member
    Dim oldAddress As Long
    oldAddress = VFTable(index)
    CopyMemory ByVal pVFT + OffsetToVFT + index * 4, oldAddress, 4
        restore original member address
    VFTable(index) = 0
    GlobalFree VFArray(index)
        discard the allocated memory for stub implementation
    VFArray(index) = 0
  End If
End Sub
Replaces / restores the address of a member procedure of this class
Public Sub ReplaceMemberSubWithStub(ByVal index As Long, _
                    ByVal fnAddress As Long)
  Dim pVFT As Long
  CopyMemory pVFT, ByVal ObjPtr(Me), 4 get the VFT address
  If (index < 1) Then restore all the original addresses
    For index = 1 To UBound(VFTable)
      RemoveStub index
    Next index
  Else
    If (fnAddress = 0) Then restore only the address for the index specified
      RemoveStub index
    Else replace the address of a member specified by index
      If (index > UBound(VFTable)) Then
        ReDim Preserve VFTable(index)
            resize the array to save original addresses
        VFTable(index) = 0
        ReDim Preserve VFArray(index)
            resize the array to save changes in the VFT
        VFArray(index) = 0
      End If
      RemoveStub index check if a stub exists for this member
            and needs to be removed first
      Dim oldAddress As Long
      CopyMemory oldAddress, ByVal pVFT + OffsetToVFT + index * 4, 4
            get original member address
      VFTable(index) = oldAddress
      Dim hexCode As String
      hexCode = hexStub4ProcedureCall
      Dim nBytes As Long number of code bytes to allocate
      nBytes = Len(hexCode)  2
      Dim Bytes() As Byte array of code bytes converted from hex
      ReDim Preserve Bytes(1 To nBytes)
      Dim idx As Long loop counter
      convert each pair of hex chars to a byte code
      For idx = 1 To nBytes
        Bytes(idx) = Val("&H" & Mid$(hexCode, idx * 2 - 1, 2))
      Next idx
      CopyMemory Bytes(5), fnAddress, 4
            replace the forwarding address in the native code
      Dim addrStub As Long address where the code bytes will be copied
      addrStub = GlobalAlloc(GMEM_FIXED, nBytes)
            allocate memory to store the code bytes
      CopyMemory ByVal addrStub, Bytes(1), nBytes copy given code bytes
      CopyMemory ByVal pVFT + OffsetToVFT + index * 4, _
        addrStub, 4 replace member address with stub address
      VFArray(index) = addrStub save the handle to the stub for cleanup
    End If
  End If
End Sub
Restores the original addresses in the VFT and discards
the allocated memory for stub implementations
Private Sub Class_Terminate()
  ReplaceMemberSubWithStub 0, 0
End Sub
Member procedure can be replaced by a stub by
calling ReplaceMemberSubWithStub(1,address)
Public Sub PrintMessage(ByVal msg As String)
  Debug.Print "PrintMessage says: " & msg
End Sub
Procedure called through embedded stub
Private Sub PrintFirstParameter(ByVal msg As String)
  Debug.Print "PrintFirstParameter says: " & msg
End Sub
Sub Main()
  Phase 3: Embedding native code
  Dim fwdSub As New StubCallToSub
  fwdSub.PrintMessage "Hello!"
  fwdSub.ReplaceMemberSubWithStub 1, AddressOf PrintFirstParameter
  fwdSub.PrintMessage "A stub called me instead!"
  fwdSub.ReplaceMemberSubWithStub 1, 0
  fwdSub.PrintMessage "My address has been restored in VFT!"
End Sub

请观察代码的输出打印消息后每次调用ReplaceMemberSubWithStub方法: 隐藏,复制Code

PrintMessage says: Hello!
PrintFirstParameter says: A stub called me instead!
PrintMessage says: My address has been restored in VFT!

额外的成员过程(函数)与不同的原型可能是这个类中声明,调用ReplaceMemberSubWithStub方法,我们可以改变他们的目的地。类是Class_Terminate自洁,所以你只需要锻炼要谨慎选择的索引方法你设置指针。对于较大的项目,我建议使用Enum与公关相关函数指针的名字ototypes: 隐藏,复制Code

Public Enum FwdSubIdx
  idxPrintMessage = 1
  index of second public sub (ReplaceMemberSubWithStub is not to be replaced)
  idxOtherSub
  idxAnotherSub
End Enum

返回值 这正是事情变得复杂。如果你想休息,它可能是一个很好的时间来反省一直所说的,吸收此处提出的想法。它可能看起来像我们已经找到了一个一般方法处理函数指针,但事实是,需要更多的调查。只是改变成员从前面的示例程序到成员函数将不会工作。返回值将丢失,堆栈将不能正确调整和控制流函数调用后将未定义。其背后的原因是,一个成员函数被调用以不同的方式比一个全局函数。因为全球函数可以作为API调用的回调方法,他们的行为非常著名。例如,如果它应该返回一个长,我们将使用eax寄存器如下所示: 隐藏,复制Code

Private Function GlobalFunction(ByVal param As Long) As Long
  GlobalFunction = param
End Function

这就变成了: 隐藏,复制Code

00402AA0   mov         eax,dword ptr [esp+4] // GlobalFunction = param
00402AA4   ret         4 // removes param from stack on return

另一方面,成员函数有不同的机制。他们被告诉调用者复制返回值,我将向您展示如何。再一次的愉快/痛苦的过程发现,阅读和理解的拆卸Visual Basic 6编译器生成的本地代码是必需的: 隐藏,复制Code

Public Function MemberFunction(ByVal param As Long) As Long
  MemberFunction = param
End Function

这就变成了: 隐藏,收缩,复制Code

00404230   push        ebp
    // ‘ebp‘ register is saved on the stack bellow the return address
00404231   mov         ebp,esp
    // stack frame established (new ebp points to old ebp)
00404233   sub         esp,0Ch
00404236   push        4011E6h // exception handler address
0040423B   mov         eax,fs:[00000000]
00404241   push        eax
00404242   mov         dword ptr fs:[0],esp
    // register exception handler frame
00404249   sub         esp,0Ch
0040424C   push        ebx
0040424D   push        esi // ‘esi‘ register saved
0040424E   push        edi
0040424F   mov         dword ptr [ebp-0Ch],esp
00404252   mov         dword ptr [ebp-8],4011D0h
00404259   xor         esi,esi // esi set to zero
0040425B   mov         dword ptr [ebp-4],esi // local temp0 gets zero value
0040425E   mov         eax,dword ptr [ebp+8] // gets pointer to object
00404261   push        eax
00404262   mov         ecx,dword ptr [eax]
00404264   call        dword ptr [ecx+4]
    // the address called here belongs to ‘msvbvm60.dll‘
00404267   mov         edx,dword ptr [ebp+0Ch]
    // ‘edx‘ register gets the value of param
0040426A   mov         dword ptr [ebp-18h],esi
0040426D   mov         dword ptr [ebp-18h],edx
    // local temp2 stores value of param
00404270   mov         eax,dword ptr [ebp+8] // gets pointer to object
00404273   push        eax
00404274   mov         ecx,dword ptr [eax]
00404276   call        dword ptr [ecx+8]
    // the address called here belongs to ‘msvbvm60.dll‘
00404279   mov         edx,dword ptr [ebp+10h]
    // given ‘return value‘ address is copied in ‘edx‘!!!
0040427C   mov         eax,dword ptr [ebp-18h]
    // ‘eax‘ register gets value of param from local temp2
0040427F   mov         dword ptr [edx],eax
    // param value is copied at given ‘return value‘ address!!!
00404281   mov         eax,dword ptr [ebp-4]
    // ‘eax‘ register gets zero value from local temp0
00404284   mov         ecx,dword ptr [ebp-14h]
00404287   pop         edi
00404288   pop         esi // restores ‘esi‘ register
00404289   mov         dword ptr fs:[0],ecx
    // restores previous exception handler frame
00404290   pop         ebx
00404291   mov         esp,ebp // removes stack frame
00404293   pop         ebp // restores ‘ebp‘ register
00404294   ret         0Ch // removes 12 bytes from the stack on return!!!

我们已经知道过程是一个额外的参数传递给成员代表指针调用对象的方法。现在我们发现一个成员函数给出了一个参数代表的地址返回值需要存储。不幸的是,它是作为最后一个参数(第一次推压入堆栈,在我们定义的参数),使情况更加复杂。这样的一个成员函数的实现是如何取代在处理准确返回值?你自己看: 隐藏,收缩,复制Code

VERSION 1.0 CLASS
Attribute VB_Name = "StubFctCall"
Replaces address of MemberFunction with given address at which
should reside a function using
__stdcall calling convention and accepts 3 parameters of type Long;
restores the original address
of MemberFunction when given address is 0
Private Sub ReplaceMemberWithGlobal(ByVal fnAddress As Long)
  Dim pVFT As Long
  CopyMemory pVFT, ByVal ObjPtr(Me), 4 get the VFT address
  Static oldAddress As Long
    static variable which stores the original MemberFunction address
  If (oldAddress = 0) Then
    CopyMemory oldAddress, ByVal pVFT + OffsetToVFT, 4
    get MemberFunction address
  End If
  If (fnAddress = 0) Then
    CopyMemory ByVal pVFT + OffsetToVFT, oldAddress, 4
    restores original MemberFunction address
  Else
    CopyMemory ByVal pVFT + OffsetToVFT, _
      fnAddress, 4 replace MemberFunction address with given global address
  End If
End Sub
Its address in the VFT will be replaced with given fnAddress
after the first call, thus
becoming inaccessible after its first return
Public Function MemberFunction(ByVal fnAddress As Long) As Long
  Debug.Print Hex$(ObjPtr(Me)) & ".MemberFunction(0x" & Hex$(fnAddress) & ")"
  ReplaceMemberWithGlobal fnAddress
  MemberFunction = fnAddress
End Function
Function for replacing a member function
Private Function GlobalFunction(ByVal objInstance As Long, _
  ByVal parameter As Long, ByRef retVal As Long) As Long
  Debug.Print Hex$(objInstance) & ".GlobalFunction(0x" & Hex$(parameter) & ")"
  retVal = parameter copy return value at given location
  GlobalFunction = 0 return success
End Function
Sub Main()
  Phase 4: Returning values
  Dim FwdFct As New StubFctCall
  Dim retVal As Long
  retVal = FwdFct.MemberFunction(AddressOf GlobalFunction)
  Debug.Print "StubFctCall.MemberFunction() returned value: " & Hex$(retVal)
  retVal = FwdFct.MemberFunction(&HB0AB0A)
  Debug.Print "GlobalFunction() returned value: " & Hex$(retVal)
End Sub

上面的测试的输出显示了正确的行为在返回相同的长参数: 隐藏,复制Code

1729D0.MemberFunction(0xAB1BD4)
StubFctCall.MemberFunction() returned value: AB1BD4
1729D0.GlobalFunction(0xB0AB0A)
GlobalFunction() returned value: B0AB0A

但是为什么我们eax寄存器设置为0 (GlobalFunction = 0)返回之前?因为有一些成员函数被调用后验证机制: 隐藏,复制Code

Dim retVal As Long
retVal = FwdFct.MemberFunction(&HB0AB0A)

这就变成了: 隐藏,复制Code

00402E52   lea         edx,[ebp-0C8h]
    // ‘edx‘ register gets address of return value
00402E58   push        edx // push address of return value
00402E59   push        0B0AB0Ah // push parameter
00402E5E   push        eax // push pointer to object ‘FwdFct‘
00402E5F   mov         ebx,eax // save pointer to object
00402E61   call        dword ptr [ecx+1Ch]
    // call first public member of the class
00402E64   cmp         eax,esi
    // here, ‘esi‘ register is zero!!! success is returned as 0!!!
00402E66   fnclex
00402E68   jge         00402E79 // if success returned, jump after next call
00402E6A   push        1Ch
00402E6C   push        4022A8h
00402E71   push        ebx
00402E72   push        eax
00402E73   call        dword ptr ds:[401024h]
00402E79   mov         eax,dword ptr [ebp-0C8h]
    // ‘eax‘ register gets return value
00402E7F   lea         edx,[ebp-94h]
00402E85   lea         ecx,[ebp-24h]
00402E88   push        edx
00402E89   mov         dword ptr [ebp-24h],eax
    // retVal variable gets return value

除了生活必需品 同样的行为可以扩展为返回类型,如双日期、货币等。一个API返回一个货币类型是使用eax和edx寄存器,而我们不得不在给定的地址复制指向的位置所期望的返回值是调用者。双和日期类型返回浮点寄存器。重要的是要理解,我们需要编写不同的存根取决于函数的返回类型指针。我的计划是有一个通用的解决方案过程,函数,返回32位和64位类型和函数返回quad-words浮点寄存器。 返回的字符串引用,这意味着他们可以处理32位指针,返回的eax寄存器。程序不返回任何值,已经给出的转发存根可以安全地使用。然而,对于函数,我们需要编写不同的存根复制一个或两个寄存器值,或浮点寄存器,在给定的位置调用者预期返回值。此外,由于额外的,最后一个参数是由调用者,我们的存根也应该删除指针返回值,正确调整堆栈指针返回给调用者。 很明显,这样的存根实现不能简单地跳到转发地址作为过程调用转发我们做。相反,必须调用转发地址,为了返回我们的存根和原调用者。转发时调用,返回地址存根被推到栈上,这必须立即发生后所期望的参数我们调用函数。然而,这意味着我们必须删除Visual Basic调用者从堆栈的地址并将其保存在安全的位置后将转发电话。 我找到了一个解决方案是将其保存在的位置由调用者返回值。可行的,但它涉及改变每个原型的存根实现我们转发调用的函数。这意味着我们将不得不提供大小字节的所有参数的函数的代码块替换方法在音频电报的地址。嗯,不是一件容易的事做…… 幸运的是,还有另一种方式!如果你不熟悉它,让我把你介绍给“线程信息块”(TIB)。TIB的格式结构中可以找到许多在线资源(例如,“引擎盖下面——MSJ, 1996年5月由马特·饶舌的人用,不会这里描述。为了我们的利益,TIB结构包含一个条目pvArbitrary可供应用程序使用。很少有应用程序利用这个位置,因此不会影响其他组件通过重写他们的数据。自从TIB的结构可以在线程的基础上,我们的实现将是线程安全的。 它足以将返回地址存储在pvArbitrary TIB的?恐怕答案是否定的。它将失败的凹角电话或者任何存根取代另一个存根的返回地址。一个常见的场景是一个API调用通过我们转发技术,通过一个回调也通过存根调用的API。我们如何使两个叠瓦状呼吁同一个线程而不是覆盖他们的返回地址吗?我们创建一个链表,作为一个后进先出(后进先出)堆栈。pvArbitrary总是指向列表的头,商店返回地址的最后一个电话。 这样一个链表需要分配和删除,我选择了从kernel32.dll GlobalAlloc和GlobalFree api,因为他们总是可用的。存根生成的程序集,可用于形式处理双/日期(64位浮点寄存器),货币(eax和edx寄存器)和长/整数/布尔值传递字符串(eax寄存器)返回如下解释: 隐藏,收缩,复制Code

   push        8 // we need 8 bytes allocated
   push        0 // we need fixed memory (GMEM_FIXED)
   mov         eax,XXXXXXXXh
        // replace here XXXXXXXX with the address of GlobalAlloc
   call        eax // allocate new list node
   pop         ecx // remove return address from stack
   mov         dword ptr [eax],ecx // store return address in list node
   pop         ecx // remove pointer to object from stack
   mov         ecx,dword ptr fs:[18h] // get pointer to TIB structure
   mov         edx,dword ptr [ecx+14h]
        // get pointer to previous list node from pvArbitrary
   mov         dword ptr [eax+4],edx // link list nodes
   mov         dword ptr [ecx+14h],eax
        // store new head of list at pvArbitrary
   mov         eax,XXXXXXXXh
        // XXXXXXXX can be replaced here with any forwarding address
   call        eax // call the forwarding address
   pop         ecx
        // get the location where return value is expected by VB caller
#ifdef _RETURN64_DOUBLE_ // return 64-bit floating point value
   fstp        qword ptr [ecx] // return value gets double result
#else
   mov         dword ptr [ecx],eax // copy first 32-bit of the return value
#ifdef _RETURN64_ // return 64-bit value
   mov         dword ptr [ecx+4],edx
        // copy second 32-bit of the return value
#endif
#endif
   mov         ecx,dword ptr fs:[18h] // get pointer to TIB structure
   mov         eax,dword ptr [ecx+14h]
        // get pointer to head of list from pvArbitrary
   mov         edx,dword ptr [eax] // get return address from list node
   push        edx // restore return address onto stack
   mov         edx,dword ptr [eax+4] // get pointer to previous list node
   mov         dword ptr [ecx+14h],edx
        // store pointer to previous node at pvArbitrary
   push        eax // we need to free the list node
   mov         eax,XXXXXXXXh
        // replace here XXXXXXXX with the address of GlobalFree
   call        eax // free list node
   ret // return to VB caller

所发生的与成员函数被调用后验证机制吗?Visual Basic调用者返回之前,eax寄存器需要设置为0。GlobalFree API的文档表示,当函数成功返回0。我指望成功丢弃分配节点,我指望成功分配。如果你愿意,插入一个xor eax, eax(本机代码是“33 c0”)以上ret指令。其他改进该存根是留给读者作为练习。可以编写存根实现处理甚至__stdcall __cdecl转发。世界的可能性是无止境的。从上面的装配产生的原生代码可以分成三部分,来缓解一个存根处理所需的返回类型的形成仅通过连接字符串: 隐藏,收缩,复制Code

VERSION 1.0 CLASS
Attribute VB_Name = "StubFwdWithStackOnTIB"
Private Const hexStub4FunctionProlog = "6A08" & "6A00" & _
        "B8XXXXXXXX" & "FFD0" & "59" & "8908" & _
        "59" & "648B0D18000000" & "8B5114" & "895004" & "894114" & _
        "B8XXXXXXXX" & "FFD0" & "59"
The hex string bellow represents the code bytes compiled
from a short assembly described as follows:
   fstp        qword ptr [ecx] // return value gets double result
Private Const hexStub4ReturnDbl = "DD19"
The hex string bellow represents the code bytes compiled
from a short assembly described as follows:
   mov         dword ptr [ecx],eax // copy first 32-bit of the return value
Private Const hexStub4Return32bit = "8901"
The hex string bellow represents the code bytes compiled
from a short assembly described as follows:
   mov         dword ptr [ecx],eax // copy first 32-bit of the return value
   mov         dword ptr [ecx+4],edx
    // copy second 32-bit of the return value
Private Const hexStub4Return64bit = "8901" & "895104"
Private Const hexStub4FunctionEpilog = "648B0D18000000" & _
        "8B4114" & "8B10" & "52" & "8B5004" & _
        "895114" & "50" & "B8XXXXXXXX" & "FFD0" & "C3"
Private Enum StubTypes supported stub types
  ret0bit method is a procedure and does not return a value
  ret32bit method is a function that returns 32-bit type
    (String included, since returned ByRef)
  ret64bit method is a function that returns 64-bit type (ex. Currency)
  retDbl method is a function that returns 64-bit float type
    (ex. Double, Date)
End Enum
An array that saves the original addresses to the member procedures
Private VFTable() As Long
An array that saves addresses of allocations made for stubs,
in order to be freed
Private VFArray() As Long
address of GlobalAlloc from kernel32.dll
Private pGlobalAlloc As Long
address of GlobalFree from kernel32.dll
Private pGlobalFree As Long
Sets initial state of the private arrays, thus UBound will not fail
on first call;
also, obtains the addresses of GlobalAlloc and GlobalFree used
for the linked list stored at pvArbitrary entry in the TIB
Private Sub Class_Initialize()
  ReDim VFTable(0)
  VFTable(0) = 0
  ReDim VFArray(0)
  VFArray(0) = 0
  Dim hKernel32 As Long handle to kernel32.dll
  hKernel32 = LoadLibrary("kernel32.dll")
  pGlobalAlloc = GetProcAddress(hKernel32, "GlobalAlloc")
  pGlobalFree = GetProcAddress(hKernel32, "GlobalFree")
End Sub
Restores the original addresses in the VFT and discards
the allocated memory for stub implementations
Private Sub Class_Terminate()
  ReplaceMethodWithStub 0, 0, 0
End Sub
Removes only existing stub for method specified by index
Private Sub RemoveStub(ByVal index As VFTidxs)
  Dim pVFT As Long
  CopyMemory pVFT, ByVal ObjPtr(Me), 4 get the VFT address
  If (index < 1) Then Exit Sub
  If (index > UBound(VFTable)) Then Exit Sub
  If (VFTable(index) <> 0) Then stub exists for this member
    Dim oldAddress As Long
    oldAddress = VFTable(index)
    CopyMemory ByVal pVFT + OffsetToVFT + index * 4, oldAddress, 4
    restore original member address
    VFTable(index) = 0
    GlobalFree VFArray(index)
    discard the allocated memory for stub implementation
    VFArray(index) = 0
  End If
End Sub
Replaces / restores the address of a method of this class
If given index is 0 then all original addresses are restored in the VFT
Private Sub ReplaceMethodWithStub(ByVal index As VFTidxs, _
    ByVal fnType As StubTypes, ByVal fnAddress As Long)
  Dim pVFT As Long
  CopyMemory pVFT, ByVal ObjPtr(Me), 4 get the VFT address
  If (index < 1) Then restore all the original addresses
    For index = 1 To UBound(VFTable)
      RemoveStub index
    Next index
  Else
    If (fnAddress = 0) Then restore only the address for the index specified
      RemoveStub index
    Else replace the address of a member specified by index
      If (index > UBound(VFTable)) Then
        ReDim Preserve VFTable(index)
        resize the array to save original addresses
        VFTable(index) = 0
        ReDim Preserve VFArray(index)
        resize the array to save changes in the VFT
        VFArray(index) = 0
      End If
      RemoveStub index
    check if a stub exists for this member and needs to be removed first
      Dim oldAddress As Long
      CopyMemory oldAddress, ByVal pVFT + OffsetToVFT + index * 4, 4
    get original member address
      VFTable(index) = oldAddress
      Dim hexCode As String
      Select Case fnType
      Case StubTypes.retDbl
    method is a function that returns 64-bit float type
        hexCode = hexStub4FunctionProlog & hexStub4ReturnDbl & _
                        hexStub4FunctionEpilog
      Case StubTypes.ret64bit method is a function that returns 64-bit type
        hexCode = hexStub4FunctionProlog & hexStub4Return64bit & _
                        hexStub4FunctionEpilog
      Case StubTypes.ret32bit method is a function that returns 32-bit type
        hexCode = hexStub4FunctionProlog & hexStub4Return32bit & _
                        hexStub4FunctionEpilog
      Case Else method is a procedure and does not return a value
        (default:StubTypes.ret0bit)
        hexCode = hexStub4ProcedureCall
      End Select
      Dim nBytes As Long number of code bytes to allocate
      nBytes = Len(hexCode)  2
      Dim Bytes() As Byte array of code bytes converted from hex
      ReDim Preserve Bytes(1 To nBytes)
      Dim idx As Long loop counter
      convert each pair of hex chars to a byte code
      For idx = 1 To nBytes
        Bytes(idx) = Val("&H" & Mid$(hexCode, idx * 2 - 1, 2))
      Next idx
      If (fnType = ret0bit) Then
        method is a procedure and does not return a value
        CopyMemory Bytes(5), fnAddress, 4
        replace the forwarding address in the native code
      Else method is a function returning a 32-bit, 64-bit or
        64-bit float type
        CopyMemory Bytes(6), pGlobalAlloc, 4
        replace the address of GlobalAlloc
        CopyMemory Bytes(33), fnAddress, 4
        replace the forwarding address in the native code
        CopyMemory Bytes(nBytes - 6), pGlobalFree, 4
        replace the address of GlobalFree
      End If
      Dim addrStub As Long address where the code bytes will be copied
      addrStub = GlobalAlloc(GMEM_FIXED, nBytes)
        allocate memory to store the code bytes
      CopyMemory ByVal addrStub, Bytes(1), nBytes copy given code bytes
      CopyMemory ByVal pVFT + OffsetToVFT + index * 4, _
        addrStub, 4 replace member address with stub address
      VFArray(index) = addrStub save the handle to the stub for cleanup
    End If
  End If
End Sub

提供的示例项目显示了一个类的实现,测试所有的讨论了返回类型,以及一个回调使用呼叫转移。这样做是为了确保我们的返回地址时不被保存为一个链表存储在pvArbitrary TIB的条目。 薄与基于转发扩散 虽然c++类型定义为原型的定义函数指针,__asm块,内在__asm __emit __declspec(裸)函数声明规范,Visual Basic 6是最好的我可以写这最后一节中描述的类,我相信这可以用来实现一切你可以用c++。对于那些过于自信在编写代码时,类型检查的参数传递给函数指针可以被完全移除。在大多数情况下,开发人员将在编译时知道一个函数的原型,它有一个指针。很少有建筑在运行时,应用程序需要调用任何已知的任意数量的参数,类型。 几年前,我在寻找一个应用程序,该应用程序可以读一些脚本,我可以定义如何测试一些dll开发。它会帮助回归测试,已经由一个非开发人员的测试人员不能完成可以忍受易于维护。好吧,这样的应用程序只可以写成一个Visual Basic 6模块,借助TypelessFwd类中提供的示例项目。 我不建议使用基于转发当你知道在编译时函数的原型。这种技术仅仅有助于理解函数调用和参数传递。它应该很少被使用,建议谨慎,当编写一个大会。因为它破裂参数的推动和调用本身,没有相关的编译器可以将测试或理解。如果你可以忍受和承担的全部责任,检查所期望的类型参数通过指针调用函数,那么它的最大的灵活性将奖励: 隐藏,收缩,复制Code

Phase 6: Spreading thin with typeless forwarding
Dim pFn As New TypelessFwd
Dim sqValue As Double
sqValue = 5
Function SquareRoot(ByVal value As Double) As Double
Dim hVBVM As Long, pSquareRoot As Long
pSquareRoot = pFn.AddressOfHelper(hVBVM, "msvbvm60.dll", "rtcSqr")
                  already loaded msvbvm60
pFn.ByRefPush VarPtr(sqValue) + 4
pFn.ByRefPush VarPtr(sqValue)
Debug.Print "Sqr(" & sqValue & ") = " & pFn.CallDblReturn(pSquareRoot)
pFn.AddressOfHelper hVBVM, vbNullString, vbNullString
                  unload runtime module, if required
Dim interval As String, number As Double, later As Date
interval = "h"
number = 10
Function DateFromNow(ByVal interval As String, ByRef number As Double)
As Date
pFn.ByValPush VarPtr(number)
pFn.ByValPush StrPtr(interval)
later = pFn.CallDblReturn(AddressOf DateFromNow)
Debug.Print "In " & number & " " & interval & " will be: " & later
Dim sSentence As String
sSentence = "The third character in this sentence is:"
Function SubString(ByRef sFrom As String, ByVal start As Long,
ByVal length As Long) As String
pFn.ByValPush 1
pFn.ByValPush 3
pFn.ByValPush VarPtr(sSentence)
Dim retVal As String
Debug.Print sSentence & " ‘" & pFn.CallStrReturn(AddressOf SubString) & "‘."
Dim sCpuVendorID As String
sCpuVendorID = Space$(12) pre-allocate 12 bytes (don‘t use String * 12)
Sub Get_CPU_VendorID(ByVal sCpuVendorID As String)
Dim pGet_CPU_VendorID As Long
pFn.NativeCodeHelper pGet_CPU_VendorID, hexGet_CPU_VendorID
              allocate memory storing native code
pFn.ByValPush StrPtr(sCpuVendorID)
pFn.Call0bitReturn pGet_CPU_VendorID
pFn.NativeCodeHelper pGet_CPU_VendorID, vbNullString
              free memory storing native code
Debug.Print "CPU Vendor ID: " & sCpuVendorID & "."

类提供了两个助手方法获取函数指针。AddressOfHelper可用于在运行时加载和卸载dll导出功能和检索指针按名称或序数,与GetProcAddress API。NativeCodeHelper将分配和释放内存存储本地代码作为十六进制字符串。当你发现一个瓶颈在Visual Basic代码,写一些装配,它更快和嵌入本机代码通过一个十六进制字符串。的话应该是关于嵌入式本地代码:edi, esi和ebx寄存器的值应该被保留,因为似乎Visual Basic利用其中的一个为验证返回值。示例项目展示了如何检索一些CPU信息(使用cpuid指令)通过调用嵌入式本地代码。 这个类的其他两个方法会帮助你把参数压入堆栈之前调用一个函数指针。ByValPush需要很长的值(32位)和让它压入堆栈。当一个参数函数时传递ByRef,指针的值应该被推送到堆栈上。时为例,ByRef参数作为双意味着双变量的地址应该被推到堆栈调用之前,可以通过使用ByValPush VarPtr(参数)。这就像说,“把32位的地址值双参数。”如何通过一种双时按值传递,而不是ByRef吗?使用ByRefPush方法间接引用任何地址,通过推到堆栈它指向的32位值。由于双是一个64位的类型,我们必须叫ByRefPush两次。 首先,我们将会把最后一个32位的值的两倍,然后推动第一个32位,因为参数及其内容从右到左。原因,对于那些不理解,是esp寄存器(堆栈指针)减少4个字节后按指令(堆栈增长向下)。不要被这些方法的命名,因为他们不应该与按值传递参数时/ ByRef声明函数原型。称“ByRefPush VarPtr(参数)+ 4”就像说,“添加4个字节的地址双参数和使用产生地址读一个32位的值必须被推送到堆栈上。” 通常情况下,自定义类型以引用的方式传递。然而,即使通过价值,可以应用相同的政策而考虑它们的大小和字节对齐。推动每一个32位值的结构,使其地址ByRefPush方法,将实现一个自定义类型的值传递参数传递。字符串类型直接映射型(一个自动化通过COM类型代表一个基本的字符串常用),你可以得到两个指针封装的缓冲区,以及指向对象的指针。 当定义一个字符串类型的参数值传递,这意味着函数需要实际缓冲区的指针,可以与StrPtr检索。时定义的字符串参数是ByRef时,它意味着函数需要指向对象的指针(双指向缓冲区的指针),可与VarPtr检索。在这两种情况下,使用ByValPush合适的地址。你可以找到在我的测试中调用接收字符串和双打时按值传递,ByRef传递。 类的其他五个方法,取而代之的是存根作为前两个,也使调用给定地址的目的在处理不同类型的返回值:(过程没有返回值),没有一个32位值,指向字符串(32位地址),64位值(货币)和双(64位浮点值)。请注意这里,字符串返回一个指针是特别声明不能使用(长),因为Visual Basic必须查看返回值传递字符串。赋值运算符复制长到一个字符串会导致文本数量代表缓冲区的地址而不是缓冲区的内容。看着存根的组装用于这些方法,你会注意到,我们的列表保存在pvArbitrary TIB结构商店也返回值的位置。这是因为,在电话的那一刻,它被推到堆栈后转发的参数调用。因此必须删除并保存返回地址的Visual Basic调用者。 结论 之前经历的示例代码并试图适应你的需要,让我总结讨论的主题: 类的公共方法被称为通过跳表和相应的地址存储在一个“虚拟函数表”(音频电报)。指针指向vftable存储作为第一个4字节(隐藏成员数据)在每个类的实例。编译器将生成访问器和公共变量修饰符类的(数据)和他们的地址将插入音频电报。类没有公共成员数据,音频电报将包含地址的定义的方法开始偏移,H1C(28字节),按照他们的声明。取代音频电报地址将重定向代码调用任何我们所需要的,只要原型和调用协定并不改变,调用将返回成功。每个vftable由同一个类的所有实例共享。因此,改变为一个对象意味着它将影响甚至后来创建的对象的生命周期中,代码部分(直到分别对应的Visual Basic DLL卸载或当它驻留在整个过程主要可执行文件)。成员程序给出了一个额外的参数(将最后一个压入堆栈)代表我们的指向对象的指针调用。成员函数有两个额外的参数:对象指针(作为第一个参数,最后推入堆栈)和调用者期望返回值的位置(作为最后一个参数,第一个推入堆栈)。不存在参数的字符串转换或其他类型转换,就像对使用Declare语句定义的函数调用所做的那样。对于返回类型可以假设同样的情况。通过值传递或返回的字符串可以作为指向封装缓冲区的32位指针处理,如BSTR类型描述中所述。通过将等效的十六进制字符串转换为用GlobalAlloc动态分配的固定内存缓冲区,可以很容易地嵌入本地代码。通过嵌入本地代码,存根函数可以替换类的方法。它们可以转发我们的调用,同时按预期调整堆栈并根据其类型处理返回值。这里没有显示,但是甚至可以转换使用剩余stdcall调用约定发出的调用,以及由编写为剩余cdecl调用约定的函数接收的调用。当这些存根在转发调用返回后需要控制时,它们必须临时存储Visual Basic调用者的返回地址。不同的设计也可能需要存储返回值的指针,或者甚至一些上下文寄存器(这里没有显示)。为了线程安全和可重入能力,可以将它们保存在作为后进先出栈动态管理的链表中。指向头节点的指针可以复制到“线程信息块”(TIB)结构的pv任意成员中。Visual Basic生成一些验证代码来测试eax寄存器对其中一个寄存器:edi、esi、ebx。如果嵌入的本机代码正在使用这些寄存器,则必须在返回之前保存它们并将eax设置为零。 我希望你不会觉得太晦涩难懂。非常感谢您的阅读,我希望它能在您未来的实现中被证明是有用的。任何形式的反馈都将非常感谢。 修订历史 06-06-2007:原始版本。已知问题:由于缺少可用的时间,在样例代码中注意到的一个未解决的问题留作进一步研究。我可以描述行为和原因,这样你就会意识到这个问题。在Visual Basic的IDE中运行代码不会报告任何问题,因为不会向用户显示第一次出现的异常。堆管理器报告给RtlSizeHeap和RtlFreeHeap的是同一个无效地址。当返回字符串类型的方法被不返回可丢弃BSTR的函数替换时,似乎会发生这种情况。如果将该方法替换为另一个Visual Basic实现,那么一切都很完美。但是,在测试GetCommandLine API(它返回一个常量缓冲区而不是可丢弃的BSTR)时,Visual Basic尝试删除返回的内存。因此,堆管理器抱怨是无效的地址。对于返回常量字符串缓冲区的api,可以使用一种变通方法。将它们声明为返回一个长指针,并从返回的指针复制内存,直到空终止字符。 本文转载于:http://www.diyabc.com/frontweb/news2192.html

以上是关于开发报告2的主要内容,如果未能解决你的问题,请参考以下文章

前端开发工具vscode如何快速生成代码片段

前端开发工具vscode如何快速生成代码片段

VS2015 代码片段整理

201671010432词频统计软件项目报告

Xcode 快速开发 代码块

导出eclipse可部署插件和片段时出错