【问题标题】:ByRef vs. ByVal for the ReadProcessMemory functionReadProcessMemory 函数的 ByRef 与 ByVal
【发布时间】:2015-05-22 18:10:57
【问题描述】:

我在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

【问题讨论】:

  • 我希望这个问题得到应有的重视。
  • 澄清一下,您的 sn-p is 中的lpBuffer 正在通过ByRef。 VBA/VB6 在未指定时通过引用传递参数,因此如何使用此代码来声明它 的工作原理是因为它是通过ByRef 传递的,只是隐式传递的。你确实的意思是问为什么即使你明确指定它是ByVal,它也能工作,对吧?
  • @Mat'sMug "...即使我们将 lpBuffer 更改为 ByVal 而不是默认的 (ByRef) ,它在被调用时仍然有效。”
  • @CBRF23 我也读过。但是问题陈述不是很清楚,特别是因为 sn -p 确实 通过引用传递了_Out_ 参数。

标签: vba vb6 windows-api-code-pack


【解决方案1】:

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

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

  1. ByRef lpBuffer As Any 或简单的 lpBuffer As Any:如果在调用 API 时,您打算将数据复制到的实际变量传递给 _Out_ 参数,则可以在声明中使用它。例如,您可以像这样使用 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)

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

使用相同的声明,您甚至可以根据需要传递一个 long(例如,如果您要检索的是地址或某种 DWord 值),但是 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)))

看,这实际上又是一回事,唯一的区别是我们提供了指向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 个字节紧跟在内存中那个 Integer 变量的内容之后,它可以是任何东西。如果这恰好是被调用者将写入的内存位置,那么您可能会崩溃。

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

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

  • 无论何时声明参数ByRef,将使用正在传递的任何变量的内存位置;
  • 无论何时声明参数ByVal .. As Long,将使用正在传递的任何变量的(32 位)值(该值不一定是内存位置,例如@987654357 的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() 的地址,而是VarPtr(bytBuffer(0)) 返回的值。但是如果参数声明为ByVal .. As Long,那么你别无选择,你只能传递一个指针(即内存位置的地址)。

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

【讨论】:

  • 我只是想添加一个关于“警告#1”的“全部保留”注释,与 VB6 分配变量的方式有关:使用的示例可能无法反映现实,因为我意识到我没有不知道 VB6 是否分配其变量,例如,在 4 字节边界上,这将与所描述的行为相矛盾。尽管如此,这种情况的最终结果仍应视为未定义。
  • 您说为缓冲区传递“任意”不是一个好主意,但是您将如何处理使用 ReadProcessMemory 读取字符串?大于 4 字节的长缓冲区不会包含其中的所有数据
  • 不,我说如果你将_Out_ 参数声明为ByVal,那么不要使用As Any,而是使用As Long,因为无论如何你应该传递的唯一东西就是一个指针(指针是 32 位/4 字节长值,不是缓冲区)。如果您将_Out_ 参数声明为ByRef(或者如果未指定传递机制),他们是的,您可以使用As Any(然后您可以选择传递任何您想要的)。关于字符串,只要您使用 API 的“A”版本就可以了,因为 VB6 将执行与 ANSI 之间的转换。
【解决方案2】:

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

如果您要进行 API 调用,有一点建议是,您真的不能太多花太多时间在 MSDN 中涉水。你越能理解如何一个函数的工作原理,它就越容易实现。确保将正确的对象类型传递给函数将帮助您确保获得预期的结果。

【讨论】:

  • 但这正是我的问题,当我通过 ByVal 时,我正在复制原始变量,它不是具有共享地址的两个变量,而是两个不同的变量,这就是 ByVal 的全部意义,即我的问题的本质。
  • @polisha989 我相信我已经回答了这个问题。 即使您按值传递参数,系统也只是复制一个指针 - 因此两个对象将指向内存中的相同值。阅读此处Wikipedia-Pointer
  • 我知道我记得在某处读过这篇文章。 MSDN-coding conventions
  • 在这个较低级别的实现(API)中,关于 ByVal 的要点是该函数需要一个长整数值,它理解为它​​应该使用的内存地址。如果你通过 ByRef,它会以同样的方式理解这一点。因此,您将告诉函数它应该使用包含长整数值本身的内存,并且它通常会挂起。
  • @BobRodes - 好建议!我假设 OP 正在传递一个长指针(在 VBA 中声明为 LngPtr 类型),因为这是 API 函数所要求的 - 但情况可能并非如此,因为他确实将此参数声明为“Any”类型,所以可能是真的通过任何东西。
【解决方案3】:

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

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

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

【讨论】:

    【解决方案4】:

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

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-03-11
      • 2015-02-15
      • 2013-03-16
      • 2012-01-21
      • 1970-01-01
      • 2011-06-21
      • 1970-01-01
      • 2020-06-30
      相关资源
      最近更新 更多