ReadProcessMemory函数的ByRef与ByVal参数区别

6
我正在使用VBA/VB6中的Windows函数ReadProcessMemory,但我不明白为什么当我将lpBuffer的传递机制更改为ByVal时,该函数仍然修改通过此参数传递的原始对象的值。在文档中,指定了这个参数作为一个输出应该通过引用传递。将传递机制更改为按值传递难道不能防止修改原始实例吗?为什么它没有?
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


1
我希望这个问题能够得到应有的关注。 - Mathieu Guindon
1
只是澄清一下,在你的代码片段中,“lpBuffer”确实被传递了“ByRef”。VBA/VB6在未指定参数时按引用传递参数,所以使用这个代码来声明它为什么有效是因为它只是隐式地传递了“ByRef”。你的意思是即使你明确指定它是“ByVal”,它也能正常工作,对吗? - Mathieu Guindon
1
@Mat'sMug "...即使我们将lpBuffer从默认的(ByRef)更改为ByVal,当被调用时仍然可以正常工作。" - CBRF23
@CBRF23 我也看到了。但是问题陈述并不是很清楚,特别是因为代码片段确实通过引用传递了 _Out_ 参数。 - Mathieu Guindon
4个回答

4
首先,对于一个 _Out_ 参数,ByVal .. As Any 不是个好主意(我甚至不确定这是否可能)。如果你为这样的参数使用了 ByVal,则应将其作为 As Long(有关 "why" 的更多信息请参见下文)。
因此,对于具有一个或多个旨在表示缓冲区/变量/内存位置的 _Out_ 参数的 API,有两种方法(对于每个相关的参数),可以根据要传递的内容来编写声明:
1. ByRef lpBuffer As Any,或者简单地 lpBuffer As Any:如果在调用 API 时您打算传递数据应被复制到的实际变量,则在 _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 提供缓冲区的大小,因为否则被调用者无法知道所提供的缓冲区大小。还要注意我们传递了(字节)数组的第一项,因为这是被调用者应该开始写入数据的位置。
使用相同的声明,即使您想要传递一个 long(例如,如果您要检索的是某种地址或 DWord 值),也可以这样做,但是 nSize 的大小必须最多为 4 字节。
还要注意,最后一个参数 lpNumberOfBytesWritten 也是一个 _Out_ 参数,并通过按引用传递,但您不需要为其提供大小;这是因为调用方和被调用方之间存在协议,无论传递哪个变量,始终会向其中写入确切的 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)))

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

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

警告 #1:对于 _Out_ 参数,如果你将它们声明为 ByVal,则它们应该始终为 As Long。这是因为调用约定期望值由恰好 4 个字节(32 位值 / DWORD)组成。例如,如果你通过 Integer 类型传递值,你将得到意外的行为,因为内存位置的值将是该 Integer 的 2 个字节以及紧随其后的下一个 2 个字节,这可能是任何内容。如果这发生在被调用者将要写入的内存位置上,那么你可能会崩溃。

警告 #2:你不想使用 VarPtrArray()(需要明确声明),因为返回的值将是数组的 SAFEARRAY 结构的地址(项目数量、项目大小等),而不是数组数据的指针(与数组中第一个项目的地址相同)。

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

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

最后,即使你将 _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)

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

注意:本答案假定讨论的架构始终为IA32或其仿真


我仅想添加一个“带着所有保留”注释,关于“警告#1”,涉及到VB6分配变量的方式:所使用的示例可能并不反映现实情况,因为我意识到我不知道VB6是否在4字节边界上分配其变量,这将与所描述的行为相矛盾。尽管如此,对于这种情况的最终结果仍应视为未定义的。 - johnwait
你说传递'as any'给缓冲区不是一个好主意,但是如果要使用ReadProcessMemory读取字符串,该怎么处理呢?如果字符串比4个字节大,那么一个长缓冲区将无法包含所有数据。 - Stavm
不,我说如果你将 _Out_ 参数声明为 ByVal,那么不要使用 As Any,而应该使用 As Long,因为你应该传递的只是一个指针(指针是32位/4字节长的值,而不是缓冲区)。如果你将 _Out_ 参数声明为 ByRef(或者未指定传递机制),那么你可以使用 As Any(然后你可以选择传递任何你想要的)。关于字符串,只要你使用 API 的“A”版本,你就没问题了,因为 VB6 会将其转换为 ANSI 并返回。 - johnwait

1

@polisha989 我认为lpBuffer中的“lp”表示它是一个长指针类型。我怀疑,由于您传递的对象是指针,无论是按值还是按引用传递参数都不会有任何区别。即使您按值传递参数,系统也只是复制指针 - 因此两个对象将指向内存中的同一值。因此,无论通过引用还是按值传递指针,您都会看到更新后的值,因为指针就是这样,它指向内存中的值。无论您有多少指针,如果它们都指向内存中的同一位置,它们都会显示相同的内容。

如果您要进行API调用,一个建议是您真的不能花太多时间浏览MSDN。您越了解函数的工作原理,实现它就会变得更容易。确保您向函数传递正确的对象类型将有助于确保您获得预期的结果。


但这正是我的问题,当我传递ByVal时,我正在复制原始变量,它不是具有共享地址的两个变量,而是两个不同的变量,这就是ByVal的全部意义,这也是我的问题的本质。 - Stavm
@polisha989 我相信我已经回答了那个问题。即使您按值传递参数,系统也只是复制指针 - 因此两个对象将指向内存中的同一值。在这里阅读Wikipedia-Pointer - CBRF23
我记得在某个地方读到过这个。MSDN-编码规范 - CBRF23
1
在这个低级实现(API)中,关于ByVal的重点是函数期望一个长整型值,它理解为内存地址。如果你传递ByRef,它会以同样的方式理解。因此,你将告诉函数它应该使用包含长整型值本身的内存,这通常会导致程序崩溃。 - BobRodes
@BobRodes - 很好的建议!我假设OP正在传递一个长指针(在VBA中声明为类型“LngPtr”),因为这是API函数所要求的 - 但这可能不是情况,因为他将此参数声明为类型“Any”,因此实际上可以传递任何东西。 - CBRF23

1

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

通过引用传递或按值传递会更改值,因为被更改的是lpbuffer指向的缓冲区中的内存。指针只是指示在哪里进行工作,它不是完成工作的实体。

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


0

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

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

请注意我为“从不”添加的例外部分。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接