|
-
May 1st, 2020, 12:06 PM
#1
[vb6] Getting AddressOf for VB Class/Object Modules
Terms...
Object modules are code pages that are not bas-modules, i.e., class, form, etc.
Methods are subs, functions, properties
Here is my latest version of Paul Caton's zAddressOf method that does the trick. It uses his logic, but no longer resembles his original code. See post #13 for performance, safety & speed comparison to Paul Caton's code.
But to be clear, you cannot just use the returned function pointer with typical APIs that want an AddressOf (callback) because they will not know to include the required pThis parameter to call COM methods. Any calls requiring a COM function pointer would likely be via thunks or DispCallFunc API which know how to call COM methods. If whatever wants the method address is not also asking for the Object or its ObjPtr, then it isn't for COM. See post #2 for more info on how COM calls work.
Why would you need this? The main reason would be that you need or want to call a code page method by its method pointer. Actually calling the method is another topic discussed in post #2 below.
Why would you want to do that? A couple scenarios, and creativity could produce other reasons.
1. Some thunk requires a COM function pointer.
2. For whatever reasons, you can only call the method by its pointer.
3. Another possible reason is to privately call a method within your own object, i.e., a class method from outside the class, without declaring that method Public or Friend. I have only used that scenario once and it applied to a set of classes where I absolutely didn't want the coder to curiously execute a Friend method in one of the classes. So I protected it by declaring it Private and called it by its pointer instead of a public or friend declaration.
Caveat: Can only be used for VB code pages in the current process.
The code is heavily commented. Read those comments to better understand how it works. This works both in the IDE and when compiled in native code or P-code. You can replace CopyMemory with VB's GetMem4 and GetMem1 APIs if you so choose.
Sample call: Debug.Print GetAddressOfEx(Me, 1) ' pointer to last method in the code page
Code:
Private Declare Sub CopyMemory Lib "kernel32.dll" Alias "RtlMoveMemory" (src As Any, dst As Any, ByVal cbLen As Long)
Private Declare Function VirtualQuery Lib "kernel32.dll" (ByVal addr As Long, pMBI As Any, ByVal lenMBI As Long) As Long
Private Enum CodePageTypeEnum
cptUnknown = 0
cptVbClass = 0
cptVbDataReport = 1
cptVbFormOrMDI = 2
cptVbPropertyPage = 3
cptVbUserControl = 4
End Enum
Private Function GetAddressOfEx(ByRef VbCodePage As Object, ByVal nOrdinal As Long, _
Optional ByVal CodePageType As CodePageTypeEnum = cptUnknown, _
Optional ByRef nMethodCount As Long, Optional ByRef nLastMethodOffset As Long) As Long
' Routine is basically an AddressOf function for VB code pages (forms, classes, etc)
If nOrdinal < 1 Then Exit Function
If VbCodePage Is Nothing Then Exit Function
' redesigned but based on Paul Caton's zAddressOf method that can find function
' pointers within VB forms, classes, etc. Redesign includes merging 2 routines,
' using known VB offsets, and use of VirtualQuery over IsBadCodePtr API. This
' revised logic is slower than Caton's latest versions, but will prevent
' unintended page-guard activation and will not fail to return results in
' cases where Caton's zAddressOf would due to his built-in loop restrictions.
' Modify the routine for large-address-aware application, as needed, i.e., pointer-safe math.
' Parameters
' :: VbCodePage is the VB class module (form, class, etc) containing the method ordinal
' :: nOrdinal is the ordinal whose function pointer/address is to be returned
' ordinals are always one-bound and counted from the bottom of the code page
' the last method is ordinal #1, second to last is #2, etc
' keep public methods near top of code page & private/friend near bottom because
' VB will move public ones closer to top during runtime, offsetting your ordinals.
' :: CodePageType when passed can help the function scan the code page more efficiently
' :: nMethodCount is returned with the number of user-defined methods in the code page
' :: nLastMethodOffset is returned with the address after the last user-defined method
' Return value
' If success, the function pointer will be returned, else zero is returned
' If zero is returned, nMethodCount and nLastMethodOffset may not be updated
' How this method works...
' With known offsets and function signatures, finding what we want is pretty easy.
' The function signature is simply the 1st byte of the function's code
' 1) If a function pointer is zero, then this is expected
' Seen typically when a code page uses Implements keyword
' Otherwise, there are four byte values we are interested in (the signature)
' 2) Byte &H33 start of XOR instruction in native code (always when in IDE)
' 3) Byte &HE9 start of XOR instruction in P-Code (only when compiled in P-Code)
' 4) Byte &H81 start of ADD instruction, regardless of P-Code usage
' 5) Byte &H58 start of POP instruction, regardless of P-Code usage
Dim bSig As Byte, bVal As Byte
Dim nAddr&, vOffset&, nFirst&
Dim nMethod&, nAttempts&, n&
Dim minAddrV&, maxAddrV&, minAddrM&, maxAddrM&
Dim MBI&(0 To 6) ' faux MEMORY_BASIC_INFORMATION structure
' (0) BaseAddress member minimum range of committed memory (same protection)
' (3) Range member maximum range BaseAddress+Range
' (5) Protect member
' This structure is key to not crashing while probing memory addresses.
' The Protect member of the structure is examined after each call. If it
' contains &H101 (mask), then the address is a page-guard or has no-access.
' Otherwise, if it contains &HFE (mask) then the address is readable.
' Step 1. Probe the passed code page to find the first user-defined method.
' The probe is quite fast. The outer For:Next loop helps to quickly filter the
' passed code page via the known offsets. The inner DO loop will execute up to
' four times to find the right code page offset as needed. After found, it will
' execute as little as one time or several times, depending on Implements usage
' and number of Public variables declared within the code page. That inner loop
' has a fudge-factor built in should some signature exist that is not known yet.
' However, no others have been found, to date, after the known offsets of the
' correct code page.
If CodePageType <= cptUnknown Or CodePageType > cptVbUserControl Then
n = 0: nAttempts = 4
Else
n = CodePageType: nAttempts = n
End If
CopyMemory nAddr, ByVal ObjPtr(VbCodePage), 4 ' host VTable
For n = n To nAttempts ' search in ascending order of offsets
Select Case n
Case 0: vOffset = nAddr + &H1C ' known offset for VB Class,DataEnvironment,Add-in,DHTMLPage
Case 1: vOffset = nAddr + &H9C ' known offset for VB DataReport
Case 2: vOffset = nAddr + &H6F8 ' known offset for VB Form, MDI
Case 3: vOffset = nAddr + &H710 ' known offset for VB Property Page
Case 4: vOffset = nAddr + &H7A4 ' known offset for VB UserControl
End Select
nAttempts = 4 ' fudge-factor
Do
' First validate the VTable slot address. If invalid, unsupported code page type
If vOffset < minAddrV Or vOffset > maxAddrV Then
MBI(5) = 0: VirtualQuery vOffset, MBI(0), 28
If (MBI(5) And &HFE) = 0 Or (MBI(5) And &H101) <> 0 Then Exit For
minAddrV = MBI(0): maxAddrV = minAddrV + MBI(3) - 4 ' set min/max range
End If
CopyMemory nMethod, ByVal vOffset, 4 ' get function address at VTable slot
If nMethod <> 0 Then ' zero = implemented, skip
' Next validate the function pointer. If invalid, unsupported code page type
If nMethod < minAddrM Or nMethod > maxAddrM Then
MBI(5) = 0: VirtualQuery nMethod, MBI(0), 28
If (MBI(5) And &HFE) = 0 Or (MBI(5) And &H101) <> 0 Then Exit For
minAddrM = MBI(0): maxAddrM = minAddrM + MBI(3) - 4 ' set min/max range
End If
CopyMemory bVal, ByVal nMethod, 1 ' get the 1st byte of that method
If bVal = &H33 Or bVal = &HE9 Then
nFirst = vOffset ' cache the location of first user-defined method
bSig = bVal: Exit For ' cache the function signature & done
ElseIf bVal <> &H81 Then ' if not one of these 4 signatures, decrement attempts
If bVal <> &H58 Then nAttempts = nAttempts - 1
End If
End If
vOffset = vOffset + 4 ' look at next VTable slot
Loop Until nAttempts = 0
Next
If nFirst = 0 Then Exit Function ' failure
' If failure, then likely one of two reasons:
' 1) Unsupported code page
' 2) Code page has no user-defined methods
' Step 2. Find the last user-defined method.
' VB stacks user-defined methods contiguously, back to back. So, to find the last method,
' we simply need to keep looking until a signature no longer matches or we hit end of page.
Do
' Validate the VTable slot address. If invalid, end of code page & done
vOffset = vOffset + 4
If vOffset < minAddrV Or vOffset > maxAddrV Then
MBI(5) = 0: VirtualQuery vOffset, MBI(0), 28
If (MBI(5) And &HFE) = 0 Or (MBI(5) And &H101) <> 0 Then Exit Do
minAddrV = MBI(0): maxAddrV = minAddrV + MBI(3) - 4 ' set min/max range
End If
CopyMemory nMethod, ByVal vOffset, 4 ' get function pointer at VTable slot
If nMethod = 0 Then Exit Do ' if zero, done because doesn't match our signature
' Validate the function pointer. If invalid, end of code page & done
If nMethod < minAddrM Or nMethod > maxAddrM Then
MBI(5) = 0: VirtualQuery nMethod, MBI(0), 28
If (MBI(5) And &HFE) = 0 Or (MBI(5) And &H101) <> 0 Then Exit Do
minAddrM = MBI(0): maxAddrM = minAddrM + MBI(3) - 4 ' set min/max range
End If
CopyMemory bVal, ByVal nMethod, 1 ' get function's signature
Loop Until bVal <> bSig ' done when doesn't match our signature
' Now set the optional parameter values
nMethodCount = (vOffset - nFirst) \ 4
nLastMethodOffset = vOffset
' Return the function pointer for requested ordinal, if a valid ordinal
If nOrdinal <= nMethodCount Then
CopyMemory GetAddressOfEx, ByVal vOffset - (nOrdinal * 4), 4
End If
End Function
FYI: The object's VTable is scanned and the methods in your code page are more or less in VTable order. So if the order of your methods doesn't magically change when you run the project, no issues should arise. But they can magically change and when they do, their ordinals can change. At runtime, VB will always move Public methods ahead of all private/friend methods within the VTable. This means if your last two methods were a private one followed by a public one, the ordinals for both methods changed at runtime. The public one was placed towards the top and that private method is now the last method in the VTable.
If using this code, always keep your public methods towards the top of the code page to prevent this issue.
Last edited by LaVolpe; May 6th, 2020 at 08:20 PM.
Reason: tweaked to increase speed & added ENUM - expand comments
-
May 1st, 2020, 12:07 PM
#2
Calling VB Methods by Pointer
First a bit about COM so you can better understand the process. We'll use a VB class as a sample. When the class is created, its code is only created once, regardless how many new instances of that class are created. This means the same method, in all instances of that class, has the same function pointer. To distinguish which class instance is being called, COM requires the caller to send the ObjPtr of that instance as the first parameter of any called method.
Ok, got that? You don't see that first parameter in any VB methods, you are actually looking at parameters #2 and on. If a function has no parameters, it really has two (explained next) and a Sub with no parameters really has one. The required first parameter is typically referred to as: pThis.
VB hides pThis from you, but it does exist in every COM call. So, VB is hiding a parameter in each of its methods, is that all? No. VB Functions and Property Get methods also have a hidden internal final parameter. It is the caller's ByRef value that receives the method's return value. Subs and Property Let/Set methods do not have that 2nd hidden parameter. A VB method (even a sub) always returns something, known as an HRESULT. If an HRESULT is being returned, then how does a function return some other value? Answer: that hidden last parameter.
In a class, Private Sub TestMe() looks a bit like this under the covers:
Code:
Private Function TestMe(ByVal pThis As Long) As HRESULT
So internally, a Sub in VB is really a function without an extra hidden parameter as seen here.
Private Function TestMe() As Long looks a bit like this under the covers:
Code:
Private Function TestMe(ByVal pThis As Long, ByVal ptrReturn As Long) As HRESULT
' ptrReturn is a pointer to memory that receives the function's value
Knowing this, how do we call a VB method by pointer? If it's a Function or Property Get, we need to include 2 "hidden" parameters else one "hidden" parameter when calling VB methods.
There is the CallWindowProc API that can be used, but has severe limitations. That API always passes 4 parameters, no more, no less. We already know that one or two will be used for the hidden parameters, so that reduces the number of usable ones. But that isn't the worse part. If building a function for that API's use, it forces you to design your method for that API. This means that if that API were used, a VB Function or Property Get must be coded to have exactly 2 parameters, no more, no less. Those 2 parameters + 2 "hidden" parameters equals API's 4 passed parameters. For the other VB method types, they must have exactly 3 parameters, no more, no less.
Code:
' Sample call to class Sub: TestMe() -- but...
' must include dummy params, so: TestMe(ByVal Dummy1 As Long, ByVal Dummy2 As Long, ByVal Dummy3 As Long)
CallWindowProc ptrFunction, ObjPtr(class), 0, 0, 0
' Sample call to class Function: TestMe(ByVal anything As Long) As Long -- but...
' must include dummy param, so: TestMe(ByVal anything As Long, ByVal Dummy As Long) As Long
Dim lRet As Long
CallWindowProc ptrFunction, ObjPtr(class), 1908, 0, VarPtr(lRet)
Another limitation with CallWindowProc besides the parameter count is the parameter sizes if needing to pass, ByVal vs. ByRef, vartypes like Double or Currency (8 bytes will use 2 API params). Passing 8 byte values/structures to COM is not uncommon. And passing a variant ByVal is not possible since it requires 4 API params (16 bytes) just for itself. When CallWindowProc API can't be used because of its limitations, you have two other choices: a thunk or another API: DispCallFunc. DispCallFunc has no limitations, but has a small learning curve if you have never used it. That API was specifically designed for calling COM methods and more
Last edited by LaVolpe; May 6th, 2020 at 08:24 PM.
-
May 1st, 2020, 01:59 PM
#3
Re: [vb6] Getting AddressOf for VB Class/Object Modules
On a hunch, I took this version and replaced zAddressOf in a UserControl using the self-sub code extensively (my ShellBrowse control), with no other changes, and this eliminated the incompatibility with IObjectSafety I posted about in this thread. No side effects found so far, checked both IDE and compiled. Thanks.
Last edited by fafalone; May 1st, 2020 at 02:04 PM.
-
May 1st, 2020, 02:05 PM
#4
Re: [vb6] Getting AddressOf for VB Class/Object Modules
 Originally Posted by fafalone
...and this eliminated the incompatibility with IObjectSafety I posted about in this thread. No side effects found so far, checked both IDE and compiled. Thanks.
Simply means there is a logic flaw in Paul's original work that my latest version doesn't suffer from. I'd really have to re-study Paul's original logic to figure out why. But honestly, not that interested. I have been using my own modifications for over a decade. This one is just the latest, but completely rewritten from scratch, and I have a thunk version too 
Edited: FYI, if you are using that function only for usercontrols, you can change the For:Next loop to start with 5 vs. 1. That will speed up the results even more in that specific case.
Last edited by LaVolpe; May 1st, 2020 at 03:20 PM.
-
May 1st, 2020, 03:25 PM
#5
Re: [vb6] Getting AddressOf for VB Class/Object Modules
 Originally Posted by LaVolpe
Edited: FYI, if you are only using that function for usercontrols, you can change the For:Next loop to start with 5 vs. 1. That will speed up the results even more in that specific case.
Speed is non-issue, reliability is king here. Hope you tested this with different compilation options if these affect relevant codegen. I mean Fast code vs Small code vs No optimizations and various advanced options too.
So if there is a form with several (private) SUBCLASSPROCs for various controls on it, how can GetAddressOfEx be used to find the addressed for these several SUBCLASSPROCs?
Have to pass correct nOrdinal? Have to figure it out beforehand by counting private methods or am I missing something?
cheers,
</wqw>
-
May 1st, 2020, 04:13 PM
#6
Re: [vb6] Getting AddressOf for VB Class/Object Modules
It is your modifications, "v2.0 Re-write by LaVolpe" 
But yeah, super low priority if you're not interested since this is a drop-in replacement that fixes it without breaking anything.
Edit: Can just elimate that entire For loop right? Don't see any reason for it if it's From 5 To 5. Not seeing any problems with so far (changed the Exit For's to Exit Do's), but given how unrelated the whole thing with IObjectSafety is, I'm not trusting the obvious to be true. Recall it's just simply the presence of the IObjectSafety methods anywhere in the code, not like they're public, or at the end with the subclass/callback procs, or in any way different from the several other Implemented interfaces. What's the connection? Who knows. So I ask about what should be obvious with just elimating the For loop all together.
Last edited by fafalone; May 1st, 2020 at 04:24 PM.
-
May 1st, 2020, 04:19 PM
#7
Re: [vb6] Getting AddressOf for VB Class/Object Modules
 Originally Posted by wqweto
Hope you tested this with different compilation options if these affect relevant codegen. I mean Fast code vs Small code vs No optimizations and various advanced options too.
I have tested quite a few including PCode. But I doubt any optimizations are going to change anything. VB seems to be using small stubs with an address to the actual function. The code returns the stub pointer. The optimizations will be applied to the function's actual code. The stub itself is just a few instructions.
 Originally Posted by wqweto
So if there is a form with several (private) SUBCLASSPROCs for various controls on it, how can GetAddressOfEx be used to find the addressed for these several SUBCLASSPROCs?
Ordinals.
 Originally Posted by wqweto
Have to pass correct nOrdinal? Have to figure it out beforehand by counting private methods or am I missing something?
You gotta count them from bottom to top, but ordinal #1 is the last private method. So add callbacks, that this function would be used for, towards the bottom of the code page.
If this helps, I usually use banners and mark ordinals so I don't screw things up a year later when I revisit for enhancements... In this example, the following would be the last 2 methods in the code page:
Code:
' //////////////// DO NOT ADD ANY NEW CODE FROM HERE TO THE END OF THE CODE PAGE \\\\\\\\\\\\\\\\\\\\
Private Function mySubclassProc( ... ) As Long
....
End Function ' ordinal #2
Private Function myHookProc( ... ) As Long
....
End Function ' ordinal #1
' //////////////////////////////// DO NOT ADD ANY NEW CODE BELOW THIS BANNER \\\\\\\\\\\\\\\\\\\\\\\\\\\
But to be clear for others that may be reading this. You cannot just use the returned function pointer with typical APIs that want AddressOf (callback) because they will not know to include the required pThis parameter to call COM methods. Any subclassing would likely be via thunks which would know how to callback to COM if they were designed that way.
Last edited by LaVolpe; May 1st, 2020 at 06:57 PM.
-
May 1st, 2020, 04:27 PM
#8
Re: [vb6] Getting AddressOf for VB Class/Object Modules
 Originally Posted by fafalone
Edit: Can just elimate that entire For loop right? Don't see any reason for it if it's From 5 To 5. Not seeing any problems with so far (changed the Exit For's to Exit Do's), but given how unrelated the whole thing with IObjectSafety is, I'm not trusting the obvious to be true. Recall it's just simply the presence of the IObjectSafety methods anywhere in the code, not like they're public, or at the end with the subclass/callback procs, or in any way different from the several other Implemented interfaces. What's the connection? Who knows. So I ask about what should be obvious with just elimating the For loop all together.
Yes, just initialize voffset to: vOffset = nAddr + &H7A4
However if you are also using this for property pages or any other code pages, recommend leaving it in. Otherwise, might want to just rem that other stuff out vs removing it? Should the need to use it for other code pages, at least it's there?
 Originally Posted by fafalone
It is your modifications, "v2.0 Re-write by LaVolpe"
In that version, the thunks were rewritten, not the support code, i.e., zAddressOf. Looking at notes in that routine, the only contribution I added was an offset for PropertyPages. Bottom line, appears to be his original logic though. And if I'm honest, I don't know if any of my previous revisions would have fixed the problem you mentioned, simply because I'm not sure what caused the problem to begin with
Edited: Problem with Paul Caton's routines discovered and fixed via PMs. Whether fafalone goes back to Paul's code or stays with mine...
Last edited by LaVolpe; May 1st, 2020 at 06:24 PM.
-
May 1st, 2020, 04:30 PM
#9
Re: [vb6] Getting AddressOf for VB Class/Object Modules
Yeah I just commented it out and left a note for anyone who might copy it from my code that they need to put it back.
-
May 1st, 2020, 06:59 PM
#10
Re: [vb6] Getting AddressOf for VB Class/Object Modules
Hello, very good work, I tried to do a simple subclass but it doesn't work for me, what am I doing wrong, am I not understanding something?
Class1
Code:
Option Explicit
Private Declare Function VirtualQuery Lib "kernel32.dll" (ByVal addr As Long, pMBI As Any, ByVal lenMBI As Long) As Long
Private Declare Sub CopyMemory Lib "kernel32.dll" Alias "RtlMoveMemory" (ByRef Destination As Any, ByRef Source As Any, ByVal Length As Long)
Private Declare Function SetWindowLongA Lib "USER32" (ByVal Hwnd As Long, ByVal nIndex As Long, ByVal dwNewLong As Long) As Long
Private Declare Function CallWindowProcA Lib "USER32" (ByVal lpPrevWndFunc As Long, ByVal Hwnd As Long, ByVal Msg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long
Private Const GWL_WNDPROC As Long = -4
Private Const WM_DESTROY As Long = &H2
Private Const WM_MOUSEMOVE As Long = &H200
Private PrevWndProc As Long
Public Function SubClass(Hwnd As Long)
PrevWndProc = SetWindowLongA(Hwnd, GWL_WNDPROC, GetAddressOfEx(Me, 1))
End Function
Public Function UnSubClass(Hwnd As Long)
PrevWndProc = SetWindowLongA(Hwnd, GWL_WNDPROC, PrevWndProc)
End Function
Private Function GetAddressOfEx(ByRef VbCodePage As Object, ByVal nOrdinal As Long, _
Optional ByRef nMethodCount As Long, Optional ByRef nLastMethodOffset As Long) As Long
' Routine is basically an AddressOf function for VB code pages (forms, classes, etc)
If nOrdinal < 1 Then Exit Function
If VbCodePage Is Nothing Then Exit Function
' redesigned but based on Paul Caton's zAddressOf method that can find function
' pointers within VB forms, classes, etc. Redesign includes merging 2 routines,
' using known VB offsets, and use of VirtualQuery over IsBadCodePtr API. This
' revised logic returns results faster in less code vs. using older brute-force style.
' Modify the routine for large-address-aware application, as needed, i.e., pointer-safe math.
' Parameters
' :: VbCodePage is the VB class module (form, class, etc) containing the method ordinal
' :: nOrdinal is the ordinal whose function pointer/address is to be returned
' ordinals are always one-bound and counted from the bottom of the code page
' the last method is ordinal #1, second to last is #2, etc
' keep public methods near top of code page & private/friend near bottom because
' VB will move public ones closer to top during runtime, offsetting your ordinals.
' :: nMethodCount is returned with the number of user-defined methods in the code page
' :: nLastMethodOffset is returned with the VTable address of the last user-defined method
' Return value
' If success, the function pointer will be returned, else zero is returned
' If zero is returned, nMethodCount and nLastMethodOffset may not be updated
' How this method works...
' With known offsets and function signatures, finding what we want is pretty easy.
' The function signature is simply the 1st byte of the function's code
' 1) If a function pointer is zero, then this is expected
' Seen typically when a code page uses Implements keyword
' Otherwise, their are four byte values we are interested in (the signature)
' 2) Byte &H33 start of XOR instruction in native code (always when in IDE)
' 3) Byte &HE9 start of XOR instruction in P-Code (only when compiled in P-Code)
' 4) Byte &H81 start of AND instruction, regardless of P-Code usage
' 5) Byte &H58 start of POP instruction, regardless of P-Code usage
Dim bSig As Byte, bVal As Byte
Dim nAddr&, vOffset&, nFirst&
Dim nMethod&, nAttempts&, n&
Dim MBI&(0 To 6) ' faux MEMORY_BASIC_INFORMATION structure
' This structure is key to not crashing while probing memory addresses.
' The 6th member of the structure is examined after each call. If it
' contains &H101 (mask), then the address is a page-guard or has no-access.
' Otherwise, if it contains &HFE (mask) then the address is readable.
' Step 1. Probe the passed code page to find the first user-defined method.
' The probe is quite fast. The outer For:Next loop helps to quickly filter the
' passed code page via the known offsets. The inner DO loop will execute at least
' four times until the right code page offset is found. After that, it will
' execute as little as one time or several times, depending on Implements usage
' and number of Public variables declared within the code page. That inner loop
' has a fudge-factor built in should some signature exist that is not known yet.
' However, no others have been found, to date, after the known offsets of the
' correct code page.
CopyMemory nAddr, ByVal ObjPtr(VbCodePage), 4 ' host VTable
For n = 1 To 5
Select Case n
Case 1: vOffset = nAddr + &H1C ' known offset for VB Class,DataEnvironment,Add-in,DHTMLPage
Case 2: vOffset = nAddr + &H9C ' known offset for VB DataReport
Case 3: vOffset = nAddr + &H6F8 ' known offset for VB Form, MDI
Case 4: vOffset = nAddr + &H710 ' known offset for VB Property Page
Case 5: vOffset = nAddr + &H7A4 ' known offset for VB UserControl
End Select
nAttempts = 4 ' fudge-factor
Do
' First validate the VTable slot address. If invalid, unsupported code page type
MBI(5) = 0: VirtualQuery vOffset, MBI(0), 28
If (MBI(5) And &HFE) = 0 Or (MBI(5) And &H101) <> 0 Then Exit For
CopyMemory nMethod, ByVal vOffset, 4 ' get function address at VTable slot
If nMethod <> 0 Then ' zero = implemented, skip
' Next validate the function pointer. If invalid, unsupported code page type
MBI(5) = 0: VirtualQuery nMethod, MBI(0), 28
If (MBI(5) And &HFE) = 0 Or (MBI(5) And &H101) <> 0 Then Exit For
CopyMemory bVal, ByVal nMethod, 1 ' get the 1st byte of that method
If bVal = &H33 Or bVal = &HE9 Then
nFirst = vOffset ' cache the location of first user-defined method
bSig = bVal: Exit For ' cache the function signature & done
ElseIf bVal <> &H81 Then ' if not one of these 4 signatures, decrement attempts
If bVal <> &H58 Then nAttempts = nAttempts - 1
End If
End If
vOffset = vOffset + 4 ' look at next VTable slot
Loop Until nAttempts = 0
Next
If nFirst = 0 Then Exit Function ' failure
' If failure, then likely one of two reasons:
' 1) Unsupported code page
' 2) Code page has no user-defined methods
' Step 2. Find the last user-defined method.
' VB stacks user-defined methods contiguously, back to back. So, to find the last method,
' we simply need to keep looking until a signature no longer matches or we hit end of page.
Do
' Validate the VTable slot address. If invalid, end of code page & done
vOffset = vOffset + 4
MBI(5) = 0: VirtualQuery vOffset, MBI(0), 28
If (MBI(5) And &HFE) = 0 Or (MBI(5) And &H101) <> 0 Then Exit Do
CopyMemory nMethod, ByVal vOffset, 4 ' get function pointer at VTable slot
If nMethod = 0 Then Exit Do ' if zero, done because doesn't match our signature
' Validate the function pointer. If invalid, end of code page & done
MBI(5) = 0: VirtualQuery nMethod, MBI(0), 28
If (MBI(5) And &HFE) = 0 Or (MBI(5) And &H101) <> 0 Then Exit Do
CopyMemory bVal, ByVal nMethod, 1 ' get function's signature
Loop Until bVal <> bSig ' done when doesn't match our signature
' Now set the optional parameter values
nMethodCount = (vOffset - nFirst) \ 4
nLastMethodOffset = vOffset - 4
' Return the function pointer for requested ordinal, if a valid ordinal
If nOrdinal <= nMethodCount Then
CopyMemory GetAddressOfEx, ByVal vOffset - (nOrdinal * 4), 4
End If
End Function
'////////////////////ORDINAL 1/////////////////////
Private Function WindowProc(ByVal Hwnd As Long, ByVal uMsg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long
WindowProc = CallWindowProcA(PrevWndProc, Hwnd, uMsg, wParam, lParam)
Debug.Print uMsg
End Function
-
May 1st, 2020, 07:10 PM
#11
Re: [vb6] Getting AddressOf for VB Class/Object Modules
 Originally Posted by LeandroA
Hello, very good work, I tried to do a simple subclass but it doesn't work for me, what am I doing wrong, am I not understanding something?
I tried to explain that in post #7. Maybe I need to emphasize in post #1 instead (which I just did)
I apologize. I should have foreseen a question similar to this and made it clear early on in the thread.
But to be clear for others that may be reading this. You cannot just use the returned function pointer with typical APIs that want AddressOf (callback) because they will not know to include the required pThis parameter to call COM methods. Any subclassing would likely be via thunks which would know how to callback to COM if they were designed that way.
Last edited by LaVolpe; May 1st, 2020 at 07:29 PM.
-
May 1st, 2020, 07:56 PM
#12
Re: [vb6] Getting AddressOf for VB Class/Object Modules
Hmm... how did you determine this was faster?
There's a part of my code that subclasses 4-10+ edit controls in a row, and with the old zAddressOf, there's no perceptible delay in them appearing after the click that triggers their creation, but with this method there's a 750-1000ms or so delay for 8 subclassings, a smaller delay for fewer. Switched the methods back and forth several times (and completely excluding the IObjectSafety thing I've been on about), and it's definitely a noticeable difference. (This is after the change you suggested to eliminate the For loop).
Last edited by fafalone; May 1st, 2020 at 08:00 PM.
-
May 2nd, 2020, 09:14 AM
#13
Re: [vb6] Getting AddressOf for VB Class/Object Modules
@fafalone. Thanx for pointing out the delay. There was room for improvement. I ran tests on a class with over 500 methods and 100 public variables
First. Tweaks were made and the routine is now 14x faster than before. However, when I compared against Paul Caton's most recent version, I can't beat it in speed and there is a reason. He uses IsBadCodePtr to verify memory is accessible.
That function can trigger a page-guard. And basically, it tests access like:
On Error Resume Next: ReadMemory: If Err (GPF) Then BadRead
Raymond Chen's article: IsBadXxxPtr should really be called CrashProgramRandomly
per Microsoft:
Important. IsBadCodePtr is obsolete and should not be used. Despite its name, it does not guarantee that the pointer is valid or that the memory pointed to is safe to use
I chose to use VirtualQuery which can prevent page-guard activation and is not obsolete. It is slower but I didn't want to use IsBadCodePtr.
Second. I also added an optional Enum parameter to select the type of code page to be scanned. That would prevent people commenting out, or changing, the For:Next loop and also adjusts that loop automatically. Ultimately, it can optimize the loop, better efficiency.
Third & Last. If you or anyone else is calling this routine multiple times on the same code page, you can actually improve performance if desired. Example if subclassing...
- instead of calling the function multiple times in the same code page...
Code:
DoSubclass Command1.hWnd, GetAddressOfEx(Me, 1, cptUserControl)
DoSubclass Command2.hWnd, GetAddressOfEx(Me, 2, cptUserControl)
DoSubclass Command3.hWnd, GetAddressOfEx(Me, 3, cptUserControl)
... etc
- call it once and use the optional offset. Just an option, more code but more efficient. However, the updated code is not slow. I think you would agree.
Code:
Dim nOffset As Long, pAddr As Long
pAddr = GetAddressOfEx(Me, 1, cptUserControl, , nOffset)
DoSubclass Command1.hWnd, pAddr
CopyMemory pAddr, ByVal nOffset - 8, 4 ' ordinal 2 * 4
DoSubclass Command2.hWnd, pAddr
CopyMemory pAddr, ByVal nOffset - 12, 4 ' ordinal 3 * 4
DoSubclass Command3.hWnd, pAddr
Last edited by LaVolpe; May 2nd, 2020 at 10:23 AM.
-
May 5th, 2020, 04:54 PM
#14
Lively Member
Re: [vb6] Getting AddressOf for VB Class/Object Modules
Amazing LaVolpe!
you cannot just use the returned function pointer with typical APIs that want an AddressOf
To be fair this is likely one of the reasons why AddressOf was never implemented to work with class functions.
I'm no expert with making thunks... How easy would it be to make a dynamic thunk which calls COM function pointers with the correct calling convention/arguments etc? In theory you can inject the objectPointer at runtime and allocate a new thunk in memory each time... I mean it's not very memory efficient of course, but it would likely be very useful if careful 
Code:
class SomeClass
private cb as long
sub test()
cb = GetThunkAddressOf(me,1,0,0)
Call someCFunction(cb)
end sub
function myCallback() as long
'whatever
Call FreeThunkAddress(cb)
end function
end class
Function GetThunkAddressOf(PagePtr as object, nOrdinal as integer, funcSubType as integer,nParams as integer)
Dim thunk() as byte
'populate thunk
'thunk(...) = ObjPtr(pagePtr)
'add params based on number of params in calling function (nParams+2)
'Call to GlobalAlloc()
'Call to VirtualProtect()
'return pointer
End Function
Function FreeThunkAddress(ptr as long)
'Call GlobalFree(ptr)
End Function
Thoughts? Not sure how possible this is but I remember a somewhat similar approach was taken for an AHK module (MCode) here: https://www.autohotkey.com/boards/vi...c.php?f=7&t=32
Last edited by sancarn; May 5th, 2020 at 05:10 PM.
-
May 5th, 2020, 05:37 PM
#15
Re: [vb6] Getting AddressOf for VB Class/Object Modules
In theory? Yes, but not so simple.
You need to know whether it's a sub or function (i.e., how many "hidden" parameters to send to VB) and also the total size of all parameters (if any).
You can't get that without TypeLib/TypeInfo unless one were to parse/disassemble the assembly code at the target function pointer to determine how many bytes are popped off the stack when the method returns. TypeLib/TypeInfo isn't going to exist for private methods. Via IDispatch, you may be able to get that info for Public methods.
Edited: And before I went through all that trouble, I'd use DispCallFunc API. No dynamic thunk creation needed. It does require knowledge of params, ObjPtr, and function pointer. If you're going to create a dynamic thunk, you'd have all that info anyway. See post #2
Edited #2: DispCallFunc can handle several calling conventions
Last edited by LaVolpe; May 5th, 2020 at 06:23 PM.
-
May 5th, 2020, 06:27 PM
#16
Lively Member
Re: [vb6] Getting AddressOf for VB Class/Object Modules
 Originally Posted by LaVolpe
In theory? Yes, but not so simple.
You need to know whether it's a sub or function (i.e., how many "hidden" parameters to send to VB) and also the total size of all parameters (if any).
You can't get that without TypeLib/TypeInfo unless one were to parse/disassemble the assembly code at the target function pointer to determine how many bytes are popped off the stack when the method returns. TypeLib/TypeInfo isn't going to exist for private methods. Via IDispatch, you may be able to get that info for Public methods.
Edited: And before I went through all that trouble, I'd use DispCallFunc API. No dynamic thunk creation needed. It does require knowledge of params, ObjPtr, and function pointer. If you're going to create a dynamic thunk, you'd have all that info anyway. See post #2
Edited #2: DispCallFunc can handle several calling conventions
Right! Size! I forgot about that one, yes I guess you would have to do what many dynamic languages do. E.G. GetPtr(me,1,"PPI","P") for params: String, String, Integer and return type String. That way you can at least calculate it on demand, although you still have limited functionality as far as STRUCTs are concerned.
I understand DispCallFunc is useful for calling directly indeed. Only unfortunate thing is it can't be passed directly to a C callback (correct me if I'm wrong).
Anyhow, nevermind. Maybe if I ever get some experience with thunking I'll give it a go
-
May 5th, 2020, 06:44 PM
#17
Re: [vb6] Getting AddressOf for VB Class/Object Modules
DispCallFunc can call CDecl, but if you need to pass a callback for that CDecl method, then you'll probably want a thunk for that callback to ensure no stack corruption & proper stack cleanup during calls to that callback. I have several thunks found here, including calling CDecl with/without callbacks.
http://www.vbforums.com/showthread.p...re-A-new-breed
-
May 5th, 2020, 11:20 PM
#18
Re: [vb6] Getting AddressOf for VB Class/Object Modules
The new update puts the delays back to imperceptible for the range of number of successive calls I need to cover, thanks.
A couple people using my control had pointed out completely random, but infrequent, crashes that I absolutely couldn't run down or replicate as no specific set of steps could trigger it, maybe it was the IsBadCodePtr call? So glad to be able to use this instead.
Last edited by fafalone; May 5th, 2020 at 11:28 PM.
-
May 6th, 2020, 08:07 AM
#19
Fanatic Member
Re: [vb6] Getting AddressOf for VB Class/Object Modules
very good work,
I could provide an example project, subclassing and call the function pointier
Greetings
-
Apr 27th, 2021, 07:01 PM
#20
Member
Re: [vb6] Getting AddressOf for VB Class/Object Modules
 Originally Posted by LaVolpe
Terms...
Object modules are code pages that are not bas-modules, i.e., class, form, etc.
Methods are subs, functions, properties
Here is my latest version of Paul Caton's zAddressOf method that does the trick. It uses his logic, but no longer resembles his original code. See post #13 for performance, safety & speed comparison to Paul Caton's code.
But to be clear, you cannot just use the returned function pointer with typical APIs that want an AddressOf (callback) because they will not know to include the required pThis parameter to call COM methods. Any calls requiring a COM function pointer would likely be via thunks or DispCallFunc API which know how to call COM methods. If whatever wants the method address is not also asking for the Object or its ObjPtr, then it isn't for COM. See post #2 for more info on how COM calls work.
Why would you need this? The main reason would be that you need or want to call a code page method by its method pointer. Actually calling the method is another topic discussed in post #2 below.
Why would you want to do that? A couple scenarios, and creativity could produce other reasons.
1. Some thunk requires a COM function pointer.
2. For whatever reasons, you can only call the method by its pointer.
3. Another possible reason is to privately call a method within your own object, i.e., a class method from outside the class, without declaring that method Public or Friend. I have only used that scenario once and it applied to a set of classes where I absolutely didn't want the coder to curiously execute a Friend method in one of the classes. So I protected it by declaring it Private and called it by its pointer instead of a public or friend declaration.
Caveat: Can only be used for VB code pages in the current process.
The code is heavily commented. Read those comments to better understand how it works. This works both in the IDE and when compiled in native code or P-code. You can replace CopyMemory with VB's GetMem4 and GetMem1 APIs if you so choose.
Sample call: Debug.Print GetAddressOfEx(Me, 1) ' pointer to last method in the code page
Code:
Private Declare Sub CopyMemory Lib "kernel32.dll" Alias "RtlMoveMemory" (src As Any, dst As Any, ByVal cbLen As Long)
Private Declare Function VirtualQuery Lib "kernel32.dll" (ByVal addr As Long, pMBI As Any, ByVal lenMBI As Long) As Long
Private Enum CodePageTypeEnum
cptUnknown = 0
cptVbClass = 0
cptVbDataReport = 1
cptVbFormOrMDI = 2
cptVbPropertyPage = 3
cptVbUserControl = 4
End Enum
Private Function GetAddressOfEx(ByRef VbCodePage As Object, ByVal nOrdinal As Long, _
Optional ByVal CodePageType As CodePageTypeEnum = cptUnknown, _
Optional ByRef nMethodCount As Long, Optional ByRef nLastMethodOffset As Long) As Long
' Routine is basically an AddressOf function for VB code pages (forms, classes, etc)
If nOrdinal < 1 Then Exit Function
If VbCodePage Is Nothing Then Exit Function
' redesigned but based on Paul Caton's zAddressOf method that can find function
' pointers within VB forms, classes, etc. Redesign includes merging 2 routines,
' using known VB offsets, and use of VirtualQuery over IsBadCodePtr API. This
' revised logic is slower than Caton's latest versions, but will prevent
' unintended page-guard activation and will not fail to return results in
' cases where Caton's zAddressOf would due to his built-in loop restrictions.
' Modify the routine for large-address-aware application, as needed, i.e., pointer-safe math.
' Parameters
' :: VbCodePage is the VB class module (form, class, etc) containing the method ordinal
' :: nOrdinal is the ordinal whose function pointer/address is to be returned
' ordinals are always one-bound and counted from the bottom of the code page
' the last method is ordinal #1, second to last is #2, etc
' keep public methods near top of code page & private/friend near bottom because
' VB will move public ones closer to top during runtime, offsetting your ordinals.
' :: CodePageType when passed can help the function scan the code page more efficiently
' :: nMethodCount is returned with the number of user-defined methods in the code page
' :: nLastMethodOffset is returned with the address after the last user-defined method
' Return value
' If success, the function pointer will be returned, else zero is returned
' If zero is returned, nMethodCount and nLastMethodOffset may not be updated
' How this method works...
' With known offsets and function signatures, finding what we want is pretty easy.
' The function signature is simply the 1st byte of the function's code
' 1) If a function pointer is zero, then this is expected
' Seen typically when a code page uses Implements keyword
' Otherwise, there are four byte values we are interested in (the signature)
' 2) Byte &H33 start of XOR instruction in native code (always when in IDE)
' 3) Byte &HE9 start of XOR instruction in P-Code (only when compiled in P-Code)
' 4) Byte &H81 start of ADD instruction, regardless of P-Code usage
' 5) Byte &H58 start of POP instruction, regardless of P-Code usage
Dim bSig As Byte, bVal As Byte
Dim nAddr&, vOffset&, nFirst&
Dim nMethod&, nAttempts&, n&
Dim minAddrV&, maxAddrV&, minAddrM&, maxAddrM&
Dim MBI&(0 To 6) ' faux MEMORY_BASIC_INFORMATION structure
' (0) BaseAddress member minimum range of committed memory (same protection)
' (3) Range member maximum range BaseAddress+Range
' (5) Protect member
' This structure is key to not crashing while probing memory addresses.
' The Protect member of the structure is examined after each call. If it
' contains &H101 (mask), then the address is a page-guard or has no-access.
' Otherwise, if it contains &HFE (mask) then the address is readable.
' Step 1. Probe the passed code page to find the first user-defined method.
' The probe is quite fast. The outer For:Next loop helps to quickly filter the
' passed code page via the known offsets. The inner DO loop will execute up to
' four times to find the right code page offset as needed. After found, it will
' execute as little as one time or several times, depending on Implements usage
' and number of Public variables declared within the code page. That inner loop
' has a fudge-factor built in should some signature exist that is not known yet.
' However, no others have been found, to date, after the known offsets of the
' correct code page.
If CodePageType <= cptUnknown Or CodePageType > cptVbUserControl Then
n = 0: nAttempts = 4
Else
n = CodePageType: nAttempts = n
End If
CopyMemory nAddr, ByVal ObjPtr(VbCodePage), 4 ' host VTable
For n = n To nAttempts ' search in ascending order of offsets
Select Case n
Case 0: vOffset = nAddr + &H1C ' known offset for VB Class,DataEnvironment,Add-in,DHTMLPage
Case 1: vOffset = nAddr + &H9C ' known offset for VB DataReport
Case 2: vOffset = nAddr + &H6F8 ' known offset for VB Form, MDI
Case 3: vOffset = nAddr + &H710 ' known offset for VB Property Page
Case 4: vOffset = nAddr + &H7A4 ' known offset for VB UserControl
End Select
nAttempts = 4 ' fudge-factor
Do
' First validate the VTable slot address. If invalid, unsupported code page type
If vOffset < minAddrV Or vOffset > maxAddrV Then
MBI(5) = 0: VirtualQuery vOffset, MBI(0), 28
If (MBI(5) And &HFE) = 0 Or (MBI(5) And &H101) <> 0 Then Exit For
minAddrV = MBI(0): maxAddrV = minAddrV + MBI(3) - 4 ' set min/max range
End If
CopyMemory nMethod, ByVal vOffset, 4 ' get function address at VTable slot
If nMethod <> 0 Then ' zero = implemented, skip
' Next validate the function pointer. If invalid, unsupported code page type
If nMethod < minAddrM Or nMethod > maxAddrM Then
MBI(5) = 0: VirtualQuery nMethod, MBI(0), 28
If (MBI(5) And &HFE) = 0 Or (MBI(5) And &H101) <> 0 Then Exit For
minAddrM = MBI(0): maxAddrM = minAddrM + MBI(3) - 4 ' set min/max range
End If
CopyMemory bVal, ByVal nMethod, 1 ' get the 1st byte of that method
If bVal = &H33 Or bVal = &HE9 Then
nFirst = vOffset ' cache the location of first user-defined method
bSig = bVal: Exit For ' cache the function signature & done
ElseIf bVal <> &H81 Then ' if not one of these 4 signatures, decrement attempts
If bVal <> &H58 Then nAttempts = nAttempts - 1
End If
End If
vOffset = vOffset + 4 ' look at next VTable slot
Loop Until nAttempts = 0
Next
If nFirst = 0 Then Exit Function ' failure
' If failure, then likely one of two reasons:
' 1) Unsupported code page
' 2) Code page has no user-defined methods
' Step 2. Find the last user-defined method.
' VB stacks user-defined methods contiguously, back to back. So, to find the last method,
' we simply need to keep looking until a signature no longer matches or we hit end of page.
Do
' Validate the VTable slot address. If invalid, end of code page & done
vOffset = vOffset + 4
If vOffset < minAddrV Or vOffset > maxAddrV Then
MBI(5) = 0: VirtualQuery vOffset, MBI(0), 28
If (MBI(5) And &HFE) = 0 Or (MBI(5) And &H101) <> 0 Then Exit Do
minAddrV = MBI(0): maxAddrV = minAddrV + MBI(3) - 4 ' set min/max range
End If
CopyMemory nMethod, ByVal vOffset, 4 ' get function pointer at VTable slot
If nMethod = 0 Then Exit Do ' if zero, done because doesn't match our signature
' Validate the function pointer. If invalid, end of code page & done
If nMethod < minAddrM Or nMethod > maxAddrM Then
MBI(5) = 0: VirtualQuery nMethod, MBI(0), 28
If (MBI(5) And &HFE) = 0 Or (MBI(5) And &H101) <> 0 Then Exit Do
minAddrM = MBI(0): maxAddrM = minAddrM + MBI(3) - 4 ' set min/max range
End If
CopyMemory bVal, ByVal nMethod, 1 ' get function's signature
Loop Until bVal <> bSig ' done when doesn't match our signature
' Now set the optional parameter values
nMethodCount = (vOffset - nFirst) \ 4
nLastMethodOffset = vOffset
' Return the function pointer for requested ordinal, if a valid ordinal
If nOrdinal <= nMethodCount Then
CopyMemory GetAddressOfEx, ByVal vOffset - (nOrdinal * 4), 4
End If
End Function
FYI: The object's VTable is scanned and the methods in your code page are more or less in VTable order. So if the order of your methods doesn't magically change when you run the project, no issues should arise. But they can magically change and when they do, their ordinals can change. At runtime, VB will always move Public methods ahead of all private/friend methods within the VTable. This means if your last two methods were a private one followed by a public one, the ordinals for both methods changed at runtime. The public one was placed towards the top and that private method is now the last method in the VTable.
If using this code, always keep your public methods towards the top of the code page to prevent this issue.
Is this routine better than:
Code:
Private Function zAddressOf(ByVal oCallback As Object, ByVal nOrdinal As Long) As Long
' Note: used both in subclassing and hooking routines
Dim bSub As Byte 'Value we expect to find pointed at by a vTable method entry
Dim bVal As Byte
Dim nAddr As Long 'Address of the vTable
Dim I As Long 'Loop index
Dim j As Long 'Loop limit
RtlMoveMemory VarPtr(nAddr), ObjPtr(oCallback), 4 'Get the address of the callback object's instance
If Not zProbe(nAddr + &H1C, I, bSub) Then 'Probe for a Class method
If Not zProbe(nAddr + &H6F8, I, bSub) Then 'Probe for a Form method
' \\LaVolpe - Added propertypage offset
If Not zProbe(nAddr + &H710, I, bSub) Then 'Probe for a PropertyPage method
If Not zProbe(nAddr + &H7A4, I, bSub) Then 'Probe for a UserControl method
Exit Function 'Bail...
End If
End If
End If
End If
I = I + 4 'Bump to the next entry
j = I + 2048 'Set a reasonable limit, scan 256 vTable entries
Do While I < j
RtlMoveMemory VarPtr(nAddr), I, 4 'Get the address stored in this vTable entry
If IsBadCodePtr(nAddr) Then 'Is the entry an invalid code address?
RtlMoveMemory VarPtr(zAddressOf), I - (nOrdinal * 4), 4 'Return the specified vTable entry address
Exit Do 'Bad method signature, quit loop
End If
RtlMoveMemory VarPtr(bVal), nAddr, 1 'Get the byte pointed to by the vTable entry
If bVal <> bSub Then 'If the byte doesn't match the expected value...
RtlMoveMemory VarPtr(zAddressOf), I - (nOrdinal * 4), 4 'Return the specified vTable entry address
Exit Do 'Bad method signature, quit loop
End If
I = I + 4 'Next vTable entry
Loop
End Function
-
Nov 11th, 2022, 08:12 PM
#21
Lively Member
Re: [vb6] Getting AddressOf for VB Class/Object Modules
Struggling to get this working in 64 bit... I'm probably missing something, but wondered whether anyone had any idea what?
Code:
'Routine is basically an AddressOf function for VB code pages (forms, classes, etc)
' Based on Paul Caton's zAddressOf method that can find function pointers within VB forms, classes, etc. Redesign
' by LaVolpe and includes merging 2 routines, using known VB offsets, and use of VirtualQuery over IsBadCodePtr API. This
' revised logic is slower than Caton's latest versions, but will prevent unintended page-guard activation and will not fail
' to return results in cases where Caton's zAddressOf would due to his built-in loop restrictions.
'@param {ByRef Object} VbCodePage - VBA Class / Form / Workbook / Sheet containing the method to obtain a pointer of
'@param {Long} nOrdinal - The ordinal whose function pointer/address is to be returned. Ordinals are always one-bound and
' counted from the bottom of the code page. I.E. The last method is ordinal #1, 2nd last is #2 etc. Keep public methods
' near the top of the class & private/friend near bottom because VB will move public ones closer to top during runtime
' offsetting the ordinals.
'@param {CodePageTypeEnum} CodePageType - CodePageType when passed can help the function scan the code page more efficiently
'@param {ByRef Long} nMethodCount - The number of methods is returned with the number of user-defined methods in the code page. [OUT]
'@param {ByRef Long} nLastMethodOffset - The address after the last user-defined method. [OUT]
'@returns {Long} On success, the function pointer else 0. If 0 nMethodCount and nLastMethodOffset may not be updated.
'@remark Modified by sancarn to make 64 bit compatible
'@dev How this method works...
' With known offsets and function signatures, finding what we want is pretty easy.
' The function signature is simply the 1st byte of the function's code
' 1) If a function pointer is zero, then this is expected
' Seen typically when a code page uses Implements keyword
' Otherwise, there are four byte values we are interested in (the signature)
' 2) Byte &H33 start of XOR instruction in native code (always when in IDE)
' 3) Byte &HE9 start of XOR instruction in P-Code (only when compiled in P-Code)
' 4) Byte &H81 start of ADD instruction, regardless of P-Code usage
' 5) Byte &H58 start of POP instruction, regardless of P-Code usage
Private Function GetAddressOfEx(ByRef VbCodePage As Object, ByVal nOrdinal As Long, _
Optional ByVal CodePageType As CodePageTypeEnum = cptUnknown, _
Optional ByRef nMethodCount As Long, Optional ByRef nLastMethodOffset As Long) As LongPtr
If nOrdinal < 1 Then Exit Function
If VbCodePage Is Nothing Then Exit Function
Dim bSig As Byte, bVal As Byte
Dim nAddr As LongPtr, vOffset As LongPtr, nFirst As LongPtr
Dim nMethod As LongPtr, nAttempts&, n&
Dim minAddrV As LongPtr, maxAddrV As LongPtr, minAddrM As LongPtr, maxAddrM As LongPtr
'faux MEMORY_BASIC_INFORMATION structure
'(0) BaseAddress member minimum range of committed memory (same protection)
'(3) Range member maximum range BaseAddress+Range
'(5) Protect member
'This structure is key to not crashing while probing memory addresses.
'The Protect member of the structure is examined after each call. If it
'contains &H101 (mask), then the address is a page-guard or has no-access.
'Otherwise, if it contains &HFE (mask) then the address is readable.
Dim MBI(0 To 6) As LongPtr
'Step 1. Probe the passed code page to find the first user-defined method.
'The probe is quite fast. The outer For:Next loop helps to quickly filter the
'passed code page via the known offsets. The inner DO loop will execute up to
'four times to find the right code page offset as needed. After found, it will
'execute as little as one time or several times, depending on Implements usage
'and number of Public variables declared within the code page. That inner loop
'has a fudge-factor built in should some signature exist that is not known yet.
'However, no others have been found, to date, after the known offsets of the
'correct code page.
If CodePageType <= cptUnknown Or CodePageType > cptVbUserControl Then
n = 0: nAttempts = 4
Else
n = CodePageType: nAttempts = n
End If
CopyMemory nAddr, ByVal ObjPtr(VbCodePage), SIZEOF_PTR ' host VTable
For n = n To nAttempts 'search in ascending order of offsets
Select Case n
Case 0: vOffset = nAddr + &H1C 'known offset for VB Class,DataEnvironment,Add-in,DHTMLPage
Case 1: vOffset = nAddr + &H9C 'known offset for VB DataReport
Case 2: vOffset = nAddr + &H6F8 'known offset for VB Form, MDI
Case 3: vOffset = nAddr + &H710 'known offset for VB Property Page
Case 4: vOffset = nAddr + &H7A4 'known offset for VB UserControl
End Select
nAttempts = 4 ' fudge-factor
Do
' First validate the VTable slot address. If invalid, unsupported code page type
If vOffset < minAddrV Or vOffset > maxAddrV Then
MBI(5) = 0: VirtualQuery vOffset, MBI(0), 7 * SIZEOF_PTR 'CHECK:28
If (MBI(5) And &HFE) = 0 Or (MBI(5) And &H101) <> 0 Then Exit For
minAddrV = MBI(0): maxAddrV = minAddrV + MBI(3) - SIZEOF_PTR ' set min/max range
End If
CopyMemory nMethod, ByVal vOffset, SIZEOF_PTR ' get function address at VTable slot
If nMethod <> 0 Then ' zero = implemented, skip
' Next validate the function pointer. If invalid, unsupported code page type
If nMethod < minAddrM Or nMethod > maxAddrM Then
MBI(5) = 0: VirtualQuery nMethod, MBI(0), 7 * SIZEOF_PTR 'CHECK:28
If (MBI(5) And &HFE) = 0 Or (MBI(5) And &H101) <> 0 Then Exit For
minAddrM = MBI(0): maxAddrM = minAddrM + MBI(3) - SIZEOF_PTR ' set min/max range
End If
CopyMemory bVal, ByVal nMethod, 1 ' get the 1st byte of that method
If bVal = &H33 Or bVal = &HE9 Then
nFirst = vOffset ' cache the location of first user-defined method
bSig = bVal: Exit For ' cache the function signature & done
ElseIf bVal <> &H81 Then ' if not one of these 4 signatures, decrement attempts
If bVal <> &H58 Then nAttempts = nAttempts - 1
End If
End If
vOffset = vOffset + SIZEOF_PTR ' look at next VTable slot
Loop Until nAttempts = 0
Next
If nFirst = 0 Then Exit Function ' failure
' If failure, then likely one of two reasons:
' 1) Unsupported code page
' 2) Code page has no user-defined methods
' Step 2. Find the last user-defined method.
' VB stacks user-defined methods contiguously, back to back. So, to find the last method,
' we simply need to keep looking until a signature no longer matches or we hit end of page.
Do
' Validate the VTable slot address. If invalid, end of code page & done
vOffset = vOffset + 4
If vOffset < minAddrV Or vOffset > maxAddrV Then
MBI(5) = 0: VirtualQuery vOffset, MBI(0), 28
If (MBI(5) And &HFE) = 0 Or (MBI(5) And &H101) <> 0 Then Exit Do
minAddrV = MBI(0): maxAddrV = minAddrV + MBI(3) - SIZEOF_PTR ' set min/max range
End If
CopyMemory nMethod, ByVal vOffset, SIZEOF_PTR ' get function pointer at VTable slot
If nMethod = 0 Then Exit Do ' if zero, done because doesn't match our signature
' Validate the function pointer. If invalid, end of code page & done
If nMethod < minAddrM Or nMethod > maxAddrM Then
MBI(5) = 0: VirtualQuery nMethod, MBI(0), SIZEOF_PTR * 7 'CHECK:28
If (MBI(5) And &HFE) = 0 Or (MBI(5) And &H101) <> 0 Then Exit Do
minAddrM = MBI(0): maxAddrM = minAddrM + MBI(3) - SIZEOF_PTR ' set min/max range
End If
CopyMemory bVal, ByVal nMethod, 1 ' get function's signature
Loop Until bVal <> bSig ' done when doesn't match our signature
' Now set the optional parameter values
nMethodCount = CLng(vOffset - nFirst) \ SIZEOF_PTR
nLastMethodOffset = CLng(vOffset)
' Return the function pointer for requested ordinal, if a valid ordinal
If nOrdinal <= nMethodCount Then
CopyMemory GetAddressOfEx, ByVal vOffset - (nOrdinal * SIZEOF_PTR), SIZEOF_PTR
End If
End Function
-
Nov 11th, 2022, 09:51 PM
#22
Re: [vb6] Getting AddressOf for VB Class/Object Modules
 Originally Posted by sancarn
Struggling to get this working in 64 bit... I'm probably missing something, but wondered whether anyone had any idea what?
64-bit? You mean in the VBA of some MS-Office file?
I doubt very seriously if you'll ever get it to work. Three reasons come to mind immediately: 1) there's a thunk (i.e., machine code), and that's written for a 32-bit environment (either 32-bit Windows, or running in the Windows WoW), 2) there are some "magic numbers" in that code that are very specific for VB6, and 3) it makes tons of assumptions about memory addresses (including vTable entries) being 32-bit (i.e., 4-bytes) each.
I wrote something similar in another CodeBank entry, but it's not going to work in 64-bit VBA either.
Any software I post in these forums written by me is provided "AS IS" without warranty of any kind, expressed or implied, and permission is hereby granted, free of charge and without restriction, to any person obtaining a copy. To all, peace and happiness.
-
Nov 12th, 2022, 05:36 AM
#23
Lively Member
Re: [vb6] Getting AddressOf for VB Class/Object Modules
 Originally Posted by Elroy
64-bit? You mean in the VBA of some MS-Office file?
Indeed
 Originally Posted by Elroy
1) there's a thunk
No there isn't? The thunk variant is wqweto's This on the other hand uses no thunk as far as I can see?
 Originally Posted by Elroy
3) it makes tons of assumptions about memory addresses (including vTable entries) being 32-bit (i.e., 4-bytes) each
Yes, this if you look at my sample is dealt with I believe, which is the main reason why I was asking the question
 Originally Posted by Elroy
2) there are some "magic numbers" in that code that are very specific for VB6
Yes, this is the major concern, these magic numbers however were clearly figured out, so one assumes they can be determined again for VBA if someone knows how / is willing to teach me how
-
Nov 12th, 2022, 09:03 AM
#24
Re: [vb6] Getting AddressOf for VB Class/Object Modules
@Elroy: I though that you already had this implemented much better here: https://www.vbforums.com/showthread....thin-an-object
It has to be "translated" to x64 i.e. GetMemX -> CopyMemory but the code is shorter and not that shaky (no probing).
I'm just worried about VTable entries in x64 VBA not being native pointers though, as discussed here: https://www.vbforums.com/showthread....g-Obj-pointers
cheers,
</wqw>
-
Nov 12th, 2022, 09:55 AM
#25
Re: [vb6] Getting AddressOf for VB Class/Object Modules
 Originally Posted by sancarn
No there isn't? The thunk variant is wqweto's This on the other hand uses no thunk as far as I can see?
You're actually correct. However, to actually make any use of a Class-AddressOf, you'd need to use a thunk because of all the (under the hood) differences in the way a class procedures works as opposed to a BAS procedure. So, you'll still need a thunk.
 Originally Posted by sancarn
Yes, this is the major concern, these magic numbers however were clearly figured out, so one assumes they can be determined again for VBA if someone knows how / is willing to teach me how 
Well yeah, maybe. As stated elsewhere, one of the nice things about VB6 is that we can depend on the infrastructure to be extremely unchanging. But there's no reason whatsoever that this needs to be true for the VBA. It could change for every release of MS-Office, or even for specific updates. And any of this could negate any particular set of magic numbers. There's no way I'd put any magic numbers like this in production code of the VBA. You'd be asking for trouble.
 Originally Posted by wqweto
Hi Wqweto. Yes, and I posted it all in the CodeBank here with extensive documentation. And I don't use any magic numbers. However, I do use certain VB6 internal structures that provide vTable offsets for any particular procedure. And there's no guarantee that those internal structures will be the same in the VBA. In fact, they can't be the same for VBA 64-bit.
---------------
And Sancarn, sorry but I've just got no interest at this time in figuring any of this out for the VBA (32-bit or 64-bit).
Any software I post in these forums written by me is provided "AS IS" without warranty of any kind, expressed or implied, and permission is hereby granted, free of charge and without restriction, to any person obtaining a copy. To all, peace and happiness.
-
Nov 13th, 2022, 06:20 AM
#26
Lively Member
Re: [vb6] Getting AddressOf for VB Class/Object Modules
 Originally Posted by Elroy
And Sancarn, sorry but I've just got no interest at this time in figuring any of this out for the VBA (32-bit or 64-bit).
That's totally fine Elroy, no one asked you (specifically) to contribute. Your thread will be a good basis though, even if VBA is changed. You're right, of course, that there are no guarantees, but equally theres no reason one shouldn't try either. The benefit of having such a procedure far outweighs the risk imo.
-
Nov 13th, 2022, 06:54 AM
#27
Re: [vb6] Getting AddressOf for VB Class/Object Modules
sancarn right off the bat I can tell you this is incorrect:
Code:
'faux MEMORY_BASIC_INFORMATION structure
'(0) BaseAddress member minimum range of committed memory (same protection)
'(3) Range member maximum range BaseAddress+Range
'(5) Protect member
'This structure is key to not crashing while probing memory addresses.
'The Protect member of the structure is examined after each call. If it
'contains &H101 (mask), then the address is a page-guard or has no-access.
'Otherwise, if it contains &HFE (mask) then the address is readable.
Dim MBI(0 To 6) As LongPtr
The correct x64 size of this structure is 48, and you've defined it as 56 bytes. So you're probably accessing off limits memory.
The offsets for the members the code uses will be different too, so you'll need to adjust that.
This is the full structure, so base adjustments off this:
Code:
Private Type MEMORY_BASIC_INFORMATION
BaseAddress As LongPtr
AllocationBase As LongPtr 'x64 offset: 8
AllocationProtect As Long 'x64 offset: 16
#If Win64 Then
PartitionId As Integer 'x64 offset: 20
#End If
'compiler will insert padding on x64, 2 bytes
RegionSize As LongPtr 'x64 offset: 24
State As Long 'x64 offset: 32
Protect As Long 'x64 offset: 36
Type As Long 'x64 offset: 40
'compiler will insert padding on x64, 4 bytes
End Type
It's probably best to explicitly define this structure and use the members themselves... MBI(3) is .RegionSize and MBI(5) is .Protect.
I think another problem might be the 'magic numbers'
Case 0: vOffset = nAddr + &H1C 'known offset for VB Class,DataEnvironment,Add-in,DHTMLPage etc
... they're address offsets. Even if they're in the same places, and we don't know that they are, they'd be at multiple of 8 instead of 4. So you'd expect the class offset above to be &H38 instead of &H1C.
But even with all the correct adjustments for x64 it doesn't work. My guess is the other magic numbers, for the function signature (values for bVal).
Last edited by fafalone; Nov 13th, 2022 at 06:00 PM.
-
Apr 19th, 2023, 07:08 AM
#28
Re: [vb6] Getting AddressOf for VB Class/Object Modules
how to get address by method name in class,form1,usercontrol
-
Apr 19th, 2023, 07:26 AM
#29
Re: [vb6] Getting AddressOf for VB Class/Object Modules
 Originally Posted by xiaoyao
how to get address by method name in class,form1,usercontrol
Read THIS THREAD. It's all outlined in there with working code.
Any software I post in these forums written by me is provided "AS IS" without warranty of any kind, expressed or implied, and permission is hereby granted, free of charge and without restriction, to any person obtaining a copy. To all, peace and happiness.
-
May 18th, 2023, 09:02 AM
#30
Re: [vb6] Getting AddressOf for VB Class/Object Modules
does it support on vba64 ,make new timer1.cls for timer events?
-
Jun 10th, 2023, 11:46 AM
#31
Re: [vb6] Getting AddressOf for VB Class/Object Modules
 Originally Posted by hennyere
Is this routine better than:
Code:
Private Function zAddressOf(ByVal oCallback As Object, ByVal nOrdinal As Long) As Long
' Note: used both in subclassing and hooking routines
Dim bSub As Byte 'Value we expect to find pointed at by a vTable method entry
End Function
can't get address of usercontrol method/functions
-
Mar 2nd, 2024, 02:51 PM
#32
Lively Member
Re: [vb6] Getting AddressOf for VB Class/Object Modules
Thank you very much for your excellent article, I have some questions to ask you.
' 2) Byte &H33 start of XOR instruction in native code (always when in IDE)
' 3) Byte &HE9 start of XOR instruction in P-Code (only when compiled in P-Code)
' 4) Byte &H81 start of ADD instruction, regardless of P-Code usage
' 5) Byte &H58 start of POP instruction, regardless of P-Code usage
I don't know what &H81 and &H58 stand for and how to get information about using them, can you explain?
If bVal = &H33 Or bVal = &HE9 Then
nFirst = vOffset ' cache the location of first user-defined method
bSig = bVal: Exit For ' cache the function signature & done
ElseIf bVal <> &H81 Then ' if not one of these 4 signatures, decrement attempts
If bVal <> &H58 Then nAttempts = nAttempts - 1
End If
I don't understand the logic of this paragraph. Can you explain it?
-
Mar 2nd, 2024, 04:31 PM
#33
Re: [vb6] Getting AddressOf for VB Class/Object Modules
LaVolpe has moved on and no longer participates in these forums.
Any software I post in these forums written by me is provided "AS IS" without warranty of any kind, expressed or implied, and permission is hereby granted, free of charge and without restriction, to any person obtaining a copy. To all, peace and happiness.
-
Mar 3rd, 2024, 01:56 PM
#34
Lively Member
Re: [vb6] Getting AddressOf for VB Class/Object Modules
Because the class function pointer is very important for vb6 to use, this is my private exploration, and the original author's note is a little different
' 2) Byte &H33 xor eax?eax Process code generated when using the P-code engine
' 3) Byte &HE9 jmp XXXXXXXX Process code generated when using the compiled machine code
' 4) Byte &H81 add dword ptr ss:[esp+0x4],XXXX Access procedures generated for public variables?
regardless of P-Code usage
' 5) Byte &H58 Didn't notice when it appeared
Tips from buddies would be greatly appreciated!
-
Mar 3rd, 2024, 02:27 PM
#35
Re: [vb6] Getting AddressOf for VB Class/Object Modules
I go into it all in excruciating detail here. Enjoy.
Any software I post in these forums written by me is provided "AS IS" without warranty of any kind, expressed or implied, and permission is hereby granted, free of charge and without restriction, to any person obtaining a copy. To all, peace and happiness.
-
Mar 3rd, 2024, 04:16 PM
#36
Lively Member
Re: [vb6] Getting AddressOf for VB Class/Object Modules
 Originally Posted by Elroy
Hi?Elroy?
Thank you very much for your generous and speedy recovery, I've been reading your posts on the subject and have gained a lot of knowledge about the inner workings of vb, thank you.
I tend to use private functions for callback operations, and want to be able to adapt to the IDE and compile to Pcode or machine code for various occasions, and want the final source code to be easy to maintain. So LaVolpe's GetAddressOfEx is the way to go, but since I don't understand how it works, I'm worried about stability for serious use.https://www.vbforums.com/images/smilies/biggrin.gif
-
Mar 3rd, 2024, 04:44 PM
#37
Re: [vb6] Getting AddressOf for VB Class/Object Modules
You can always call a private method of a class using the ubiquitous "DispCallFunc" function so you're not limited to using LaVolpe's GetAddressOfEx.
However the preferred way to use callback operations is by implementing a custom interface containing your callback function.
Last edited by VanGoghGaming; Mar 3rd, 2024 at 04:51 PM.
-
Mar 3rd, 2024, 05:24 PM
#38
Re: [vb6] Getting AddressOf for VB Class/Object Modules
 Originally Posted by TomCatChina
I tend to use private functions for callback operations...
In the link I gave you, I go through how to use Private functions. But it's similar to the way LaVolpe is doing it, but I'm not leaning on any magic-numbers to find the procedure entry points. I'm just doing it straight through the vTable. IDK, using magic-numbers in a search through executable code always made me very nervous. What's to prevent those magic-numbers from appearing for a reason other than identifying a procedure's entry point.
 Originally Posted by TomCatChina
... want to be able to adapt to the IDE and compile to Pcode or machine code...
In the end, none of that matters because, in all cases, you have to go through the object's vTable, which is the same regardless of whether it's compiled or IDE, or p-code or binary code. So you really don't have to worry about that.
 Originally Posted by TomCatChina
... and want the final source code to be easy to maintain.
That's a tricky one. If you're insisting on keeping your object methods declared as Private, then they're always going to be positionally dependent to find an AddressOf via the vTable. In other words, if you do something as simple as swap the position of your methods around, you'll break your code.
The only way to avoid this is to declare them as Public and then use that Vb6ComCodeObjectAddressOf function in the link I gave you above.
 Originally Posted by VanGoghGaming
You can always call a private method of a class using the ubiquitous "DispCallFunc" function so you're not limited to using LaVolpe's GetAddressOfEx.
If you're wanting to directly use a procedure within an object for API callback (subclassing or hooking), DispCallFunc isn't going to help. And, if we're not doing some kind of callback, why are we worried about AddressOf in the first place?
Any software I post in these forums written by me is provided "AS IS" without warranty of any kind, expressed or implied, and permission is hereby granted, free of charge and without restriction, to any person obtaining a copy. To all, peace and happiness.
-
Mar 3rd, 2024, 05:29 PM
#39
Re: [vb6] Getting AddressOf for VB Class/Object Modules
And TomCatChina, you really should read through that link I gave you.
Even after you get the address of a procedure in an object, you can't just call it like a procedure in a BAS module.
Procedures in objects have an extra argument implicitly inserted in the call, and they also return their results differently than a function in a BAS module. So, both LaVolpe and me patch in a piece of machine code (i.e., a "thunk") to rearrange things so that you can call class methods just like BAS procedures.
There's really no other way to do it without a thunk.
-------------
And just a further FYI, VB6 (compiled or p-code) takes care of all of this under-the-hood. But, when you're using a custom "AddressOf" for an object's procedure, those issues are no longer taken care of.
Any software I post in these forums written by me is provided "AS IS" without warranty of any kind, expressed or implied, and permission is hereby granted, free of charge and without restriction, to any person obtaining a copy. To all, peace and happiness.
-
Mar 3rd, 2024, 06:51 PM
#40
Lively Member
Re: [vb6] Getting AddressOf for VB Class/Object Modules
 Originally Posted by Elroy
And TomCatChina, you really should read through that link I gave you.
Yes,I have read your post carefully and clearly understand your point of view.
 Originally Posted by Elroy
Even after you get the address of a procedure in an object, you can't just call it like a procedure in a BAS module.
I realize that all of vb's class procedures follow the COM standard with THIS pointer, implied return parameters, and HRESULT return values, and that with clever THUNK code, these side effects can be eliminated for other code to call in STDCALL fashion.
 Originally Posted by Elroy
The only way to avoid this is to declare them as Public and then use that Vb6ComCodeObjectAddressOf function in the link I gave you above.
Yes, this is the most robust way to do it, and the source code is easy to maintain, at the cost of using a public process.
Thank you for your enthusiastic reply, and thank LaVolpe for his work.
Posting Permissions
- You may not post new threads
- You may not post replies
- You may not post attachments
- You may not edit your posts
-
Forum Rules
|
Click Here to Expand Forum to Full Width
|