ByRef vs. ByVal for the ReadProcessMemory function
Asked Answered
P

4

6

I'm using the windows function ReadProcessMemory in VBA/VB6 and I don't understand why when I change the passing mechanism of lpBuffer to ByVal the function still modifies the value of the original object passed through this argument. In the documentation, this argument is specified as an output that should be passed by reference. Shouldn't changing the passing mechanism to by value prevent the original instance from being modified? Why does it not?

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

Petasus answered 22/5, 2015 at 18:10 Comment(4)
I hope this question gets the attention it deserves.Seato
Just to clarify, lpBuffer in your snippet is being passed ByRef. VBA/VB6 passes parameters by reference when unspecified, so how come using this code to declare it works is because it's passed ByRef, only implicitly. You did mean to ask why it works even if you explicitly specify it's ByVal, right?Seato
@Mat'sMug "...even if we change lpBuffer to ByVal instead of the default (ByRef) , it still works when being called."Udelle
@Udelle I read that too. But the question statement isn't very clear, especially since the snippet does pass the _Out_ parameters by reference.Seato
A
4

First, ByVal .. As Any for an _Out_ argument is not a good idea (I'm not even sure if that's possible); if you use ByVal for such you want it to be As Long (see further below for the "why").

So, for APIs having one or more _Out_ arguments meant to represent a buffer/variable/memory location, there are two ways (for each concerned argument anyway) to write the declaration, depending on what you want to pass:

  1. ByRef lpBuffer As Any, or simply lpBuffer As Any: You use this in the declaration for an _Out_ argument if, when calling the API, you intend to pass the actual variable where data should be copied to. For example, you could use a Byte array like so:

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)

Note that the callee (here, ReadProcessMemory()) will fill whatever you provide as lpBuffer with data, regardless of the actual size of the variable passed. That's why the size of the buffer must be provided through nSize, because otherwise the callee has no way to know the size of the buffer being provided. Also note that we're passing the first item of the (byte) array, as this is where the callee should start writing data to.

With the same declaration, you could even pass a long if you wanted to (if, for example, what you want to retrieve is an address or a DWord value of some sort), but then nSize must be 4 bytes (at most).

Also note that the last argument, lpNumberOfBytesWritten, is also an _Out_ argument and passed ByRef but you don't need to provide the callee with its size; that's because there's an agreement between the caller & callee that whatever variable is passed, exactly 4 bytes will always be written to it.

  1. ByVal lpBuffer As Long: You use this in a declaration for an _Out_ argument if, when calling the API, you intend to pass a memory location in the form of a 32-bit value (i.e. a pointer); the value of the Long being passed will not change, what will be overwritten is the memory location being referenced by the value of that Long. Reusing the same example, but with a slightly different declaration, we get:

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)))

See, this is practically the same thing again, the only difference being we're providing a pointer (memory address) to bytBuffer instead of passing bytBuffer directly. We could even provide the value returned by VarPtr() directly instead of using a Long (here, lPointer):

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

Warning #1: For _Out_ arguments, if you declare them ByVal they should always be As Long. This is because the calling convention expects the value to be composed of exactly 4 bytes (32-bit value/DWORD). If you were to pass the value through an Integer type, for example, you'd get unexpected behaviour because what will be used as the value for the memory location are the 2 bytes of that Integer plus the next 2 bytes that come right after the content of that Integer variable in memory, which could be anything. And if this happens to be a memory location the callee will write to, you'll probably crash.

Warning #2: You DO NOT want to use VarPtrArray() (which would need to be explicitly declared anyway), as the value returned will be the address of the SAFEARRAY structure of the array (number of items, size of items, etc.), not the pointer to the array's data (which is the same address as the first item in the array).

In essence, for Win32 APIs (i.e. stdcall) arguments are always passed as 32-bit values, always. The meaning of those 32-bit values will depend on what the specific API expects, so its declaration must reflect this. So:

  • whenever an argument is declared ByRef, what will be used is the memory location of whatever variable is being passed;
  • whenever an argument is declared ByVal .. As Long, what will be used is the (32-bit) value of whatever variable is being passed (the value must not necessarily be a memory location, e.g. the hProcess argument of ReadProcessMemory()).

Finally, even if you declare an _Out_ argument ByRef (or if, for example, that's the way an API is declared and you cannot change it because if comes from a typelib) you can always pass a pointer instead of the actual variable by adding ByVal before it when making the call. Going back to the first declaration of ReadProcessMemory() (when lpBuffer is declared ByRef), we would do the following:


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)

Adding ByVal tells the compiler that what should be passed on stack is not the address of VarPtr() but instead the value returned by VarPtr(bytBuffer(0)). But if the argument was declared ByVal .. As Long then you don't have a choice, you can only pass a pointer (i.e. address of a memory location).

NOTA: this answer assumed throughout the architecture being discussed was IA32 or an emulation of it

Acanthopterygian answered 23/5, 2015 at 2:43 Comment(3)
I just wanted to add a "with all reserve" note regarding "Warning #1", related to the way VB6 allocates variables: the example used might not reflect the reality, as I realized I don't know whether VB6 allocates its variables, for example, on a 4-byte boundary, which would then contradict the behaviour described. Nevertheless, the end result for such case should still be considered as undefined.Acanthopterygian
You're saying passing 'as any' for the buffer is not a good idea, but then how would you handle reading a string using ReadProcessMemory ? a bit bigger than 4 bytes a long buffer will not contain all data in itPetasus
No, I said if you declare an _Out_ argument as ByVal then don't use As Any, use As Long instead because the only thing you should be passing is a pointer anyway (the pointer is the 32-bit/4-byte long value, not the buffer). If you declare an _Out_ argument as ByRef (or if not passing mechanism is specified), they yes you can use As Any (then you have the choice to pass whatever you want). Regarding strings, as long as you use the "A" version of an API you're fine since VB6 will do the conversion to & back from ANSI.Acanthopterygian
U
1

@polisha989 I believe the "lp" in lpBuffer indicates the type as a long pointer. I suspect that since the object you are passing is a pointer, it won't make any difference if it's passed by value or reference. Even if you pass the argument by value, the system is just making a copy of a pointer - so both objects will be pointing to the same value in memory. So the reason that you see the updated value whether you pass the pointer by ref or by val, is because that is what a pointer does; it points to a value in memory. No matter how many pointers you have, if they are all pointing to the same place in memory, they will all show the same thing.

One word of advice if you are getting into API calls is you really can't spend too much time wading through the MSDN. The better you can understand how a function works, the easier it will become to implement it. Making sure you are passing the right object types to the function will help you to ensure the results you get are expected.

Udelle answered 22/5, 2015 at 20:14 Comment(5)
but that's exactly my question, when i pass ByVal im making a copy of the original variable, its not two variables with a shared address, its two different variables, that's the whole point of ByVal, that is the essence of my question.Petasus
@polisha989 I believe I've answered that question. Even if you pass the argument by value, the system is just making a copy of a pointer - so both objects will be pointing to the same value in memory. Have a read here Wikipedia-PointerUdelle
I knew I remembered reading this somewhere. MSDN-coding conventionsUdelle
The point about ByVal in this lower-level implementation (the API) is that the function is expecting a long integer value which it understands to be the address of the memory it's supposed to use. If you pass ByRef, it will understand that in the same way. So, you'll be telling the function that it's supposed to use the memory containing the long integer value itself, and it will generally hang spectacularly.Magnetism
@Magnetism - Good advice! I assumed the OP was passing a long pointer (in VBA declared as type LngPtr) as that is what the API function calls for - but that may not be the case since he did declare this argument as type "Any" so could be passing anything really.Udelle
M
1

CBRF23 is correct. When an API function has a string argument, the value that you pass is a long pointer to a buffer. That pointer value is a long integer, and for the life of the pointer its value is immutable. Therefore, whether you have two copies of the pointer value or not is irrelevant, since the value never changes.

The value changes whether you pass byref or byval because what gets changed is the memory in the buffer that the lpbuffer is pointing to. The pointer is just saying where to do the work, it isn't the entity that the work gets done on.

The pointer is (roughly) analogous to your email address, and the memory it points to is analogous to your inbox, if that helps to visualize the concept.

Magnetism answered 25/5, 2015 at 19:37 Comment(0)
K
0

As Any declarations never get passed by value.

When you remove type restrictions, Visual Basic assumes the argument is passed by reference. Include ByVal in the actual call to the procedure to pass arguments by value.

Note the italics I added for the exception to "never."

Kra answered 22/5, 2015 at 20:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.