ByRef与ByVal的ReadProcessMemory函数

Sta*_*avm 6 vb6 vba windows-api-code-pack

我在VBA/VB6中使用了windows函数ReadProcessMemory,我不明白为什么当我更改ByVal的传递机制时lpBuffer,该函数仍然会修改通过此参数传递的原始对象的值.在文档中,此参数被指定为应通过引用传递的输出.不应该通过值更改传递机制来防止原始实例被修改?为什么不呢?

Declare Function ReadProcessMemory Lib "kernel32" (ByVal hProcess As Long, ByVal lpBaseAddress As Any  _
,byVal lpBuffer As Any, ByVal nSize As Long, lpNumberOfBytesWritten As Long) As Long
Run Code Online (Sandbox Code Playgroud)

MSDN ReadProcessMemory

joh*_*ait 4

首先,ByVal .. As Any争论_Out_不是一个好主意(我什至不确定这是否可能);如果您使用ByVal这样的东西,您希望它是这样的As Long(请参阅下面的“为什么”)。

因此,对于具有一个或多个_Out_参数来表示缓冲区/变量/内存位置的 API,有两种方法(无论如何对于每个相关参数)来编写声明,具体取决于您想要传递的内容:

  1. ByRef lpBuffer As Any,或者简单地lpBuffer As Any说:_Out_如果在调用 API 时您打算传递数据应复制到的实际变量,则可以在参数声明中使用它。例如,您可以像这样使用 Byte 数组:

Private Declare Function ReadProcessMemory Lib "kernel32" (ByVal hProcess As Long, _
   ByVal lpBaseAddress As Long, lpBuffer As Any, ByVal nSize As Long, _
   lpNumberOfBytesWritten As Long) As Long
'[..]
Dim bytBuffer(255) As Byte, lWrittenBytes As Long, lReturn As Long
lReturn = ReadProcessMemory(hTargetProcess, &H400000&, bytBuffer(0), 256, lWrittenBytes)
Run Code Online (Sandbox Code Playgroud)

请注意,被调用者(此处为ReadProcessMemory())将用数据填充您提供的任何内容lpBuffer,无论传递的变量的实际大小如何。这就是为什么必须通过 提供缓冲区的大小nSize,因为否则被调用者无法知道所提供的缓冲区的大小。另请注意,我们正在传递(字节)数组的第一项,因为这是被调用者应开始写入数据的位置。

使用相同的声明,您甚至可以根据需要传递 long(例如,如果您要检索的是地址或某种类型的双字值),但必须nSize 4 个字节(最多)。

另请注意,最后一个参数lpNumberOfBytesWritten也是一个_Out_参数并通过 ByRef 传递,但您不需要向被调用者提供其大小;这是因为调用者和被调用者之间有一个协议,即无论传递什么变量,都将始终写入 4 个字节。

  1. ByVal lpBuffer As Long:如果在调用 API 时,您打算以 32 位值(即指针)的形式_Out_传递内存位置,则可以在参数声明中使用它;Long传递的值不会改变,将被覆盖的是该值引用的内存位置Long。重用相同的示例,但声明略有不同,我们得到:

Private Declare Function ReadProcessMemory Lib "kernel32" (ByVal hProcess As Long, _
   ByVal lpBaseAddress As Long, ByVal lpBuffer As Long, ByVal nSize As Long, _
   lpNumberOfBytesWritten As Long) As Long
'[..]
Dim bytBuffer(255) As Byte, lPointer As Long, lWrittenBytes As Long, lReturn As Long
lPointer = VarPtr(bytBuffer(0))
lReturn = ReadProcessMemory(hTargetProcess, &H400000&, lPointer, 256, lWrittenBytes)
' If we want to make sure the value of lPointer didn't change:
Debug.Assert (lPointer = VarPtr(bytBuffer(0)))
Run Code Online (Sandbox Code Playgroud)

看,这实际上又是同样的事情,唯一的区别是我们提供一个指针(内存地址)而bytBuffer不是bytBuffer直接传递。我们甚至可以提供返回的值VarPtr()而不是使用 a Long(此处为lPointer):

lReturn = ReadProcessMemory(hTargetProcess, &H400000&, VarPtr(bytBuffer(0)), 256, _
          lWrittenBytes)
Run Code Online (Sandbox Code Playgroud)

警告#1:对于_Out_参数,如果你声明它们,ByVal它们应该总是 As Long. 这是因为调用约定期望该值恰好由 4 个字节组成(32 位值/DWORD)。例如,如果您要通过类型传递值Integer,您会得到意外的行为,因为将用作内存位置值的是该内存位置的 2 个字节Integer加上紧随其内容之后的接下来的 2 个字节。内存中的变量Integer,可以是任何东西。如果这恰好是被调用者将写入的内存位置,您可能会崩溃。

警告#2:您不想使用VarPtrArray()(无论如何都需要显式声明),因为返回的值将是数组的 SAFEARRAY 结构的地址(项目数、项目大小等),而不是指向数组数据的指针(与数组中第一项的地址相同)。

本质上,对于 Win32 API(即 stdcall),参数是始终作为 32 位值传递。这些 32 位值的含义取决于特定 API 的期望,因此其声明必须反映这一点。所以:

  • 每当声明一个参数时ByRef,将使用正在传递的任何变量的内存位置;
  • 每当声明参数时ByVal .. As Long,将使用正在传递的任何变量的(32 位)值(该值不一定是内存位置,例如hProcessReadProcessMemory()

最后,即使您声明了一个_Out_参数ByRef(或者,例如,如果这是声明 API 的方式,并且您无法更改它,因为如果来自类型库),您始终可以通过在其ByVal前面添加来传递指针而不是实际变量:打电话。回到第一个声明ReadProcessMemory()(当lpBuffer声明时ByRef),我们将执行以下操作:


Private Declare Function ReadProcessMemory Lib "kernel32" (ByVal hProcess As Long, _
   ByVal lpBaseAddress As Long, lpBuffer As Any, ByVal nSize As Long, _
   lpNumberOfBytesWritten As Long) As Long
'[..]
Dim bytBuffer(255) As Byte, lWrittenBytes As Long, lReturn As Long
lReturn = ReadProcessMemory(hTargetProcess, &H400000&, ByVal VarPtr(bytBuffer(0)), 256, _
          lWrittenBytes)
Run Code Online (Sandbox Code Playgroud)

添加ByVal告诉编译器应该在堆栈上传递的不是地址VarPtr(),而是 的返回值VarPtr(bytBuffer(0))。但是,如果声明了参数ByVal .. As Long,那么您别无选择,只能传递指针(即内存位置的地址)。

注意:这个答案在整个讨论的架构中假设是 IA32 或其仿真