6

我在 VBA/VB6 中使用 Windows 函数ReadProcessMemory,我不明白为什么当我将传递机制更改lpBufferByVal时,该函数仍会修改通过此参数传递的原始对象的值。在文档中,此参数被指定为应通过引用传递的输出。不应该将传递机制更改为按值阻止原始实例被修改吗?为什么不呢?

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

MSDN ReadProcessMemory

4

4 回答 4

4

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

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

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

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)

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

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

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

  1. ByVal lpBuffer As Long_Out_:如果在调用 API 时打算以32 位值(即指针)的形式传递内存位置,则在参数声明中使用它;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)))

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

lReturn = ReadProcessMemory(hTargetProcess, &H400000&, VarPtr(bytBuffer(0)), 256, _
          lWrittenBytes)

警告 #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 位)值(该值不一定是内存位置,例如 的hProcess参数ReadProcessMemory())。

最后,即使您声明了一个_Out_参数ByRef(或者,例如,如果这是声明 API 的方式并且您无法更改它,因为 if 来自类型库),您始终可以通过在它之前添加一个指针而不是实际变量来传递ByVal打电话。回到ReadProcessMemory()(when lpBufferis declared 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)

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

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

于 2015-05-23T02:43:57.020 回答
1

@polisha989 我相信其中的“lp”lpBuffer将类型表示为长指针。我怀疑由于您传递的对象是一个指针,所以如果它是按值或引用传递的,它不会有任何区别。即使您按值传递参数,系统也只是制作指针的副本 - 因此两个对象将指向内存中的相同值。因此,无论您通过 ref 还是通过 val 传递指针,您看到更新值的原因是因为这就是指针的作用;它指向内存中的一个值。不管你有多少个指针,如果它们都指向内存中的同一个地方,它们都会显示相同的东西。

如果您正在参与 API 调用,那么您真的不能花太多时间涉足 MSDN。你越能理解一个函数是如何工作的,它就越容易实现。确保将正确的对象类型传递给函数将帮助您确保获得预期的结果。

于 2015-05-22T20:14:59.713 回答
1

CBRF23 是正确的。当 API 函数具有字符串参数时,您传递的值是指向缓冲区的长指针。该指针值是一个长整数,并且在指针的生命周期内它的值是不可变的。因此,是否有指针值的两个副本无关紧要,因为值永远不会改变。

无论您传递 byref 还是 byval,该值都会发生变化,因为改变的是 lpbuffer 指向的缓冲区中的内存。指针只是说明在哪里完成工作,而不是完成工作的实体。

指针(大致)类似于您的电子邮件地址,它指向的内存类似于您的收件箱,如果这有助于形象化概念的话。

于 2015-05-25T19:37:43.550 回答
0

As Any声明永远不会按值传递。

当您删除类型限制时,Visual Basic 假定参数是通过引用传递的。在对过程的实际调用中包含 ByVal 以按值传递参数。

请注意我为“从不”的例外添加的斜体。

于 2015-05-22T20:38:32.260 回答