When you call an asm procedure, the value in IP (the instruction
pointer) changes to the offset of the first byte of the procedure.
However, the system always pushes the callers instruction pointer
value onto the stack after pushing the procedure's parameters so
that it can find this value on return. The stack pointer always
points to the last thing pushed to the top of the stack.
This is why the first parameter is offset from the stack pointer
[esp+4]. Note also that when we push ebp before setting ebp to esp
the first parameter becomes [ebp+8]. For asm coders this should
When we patch VB's virtual function table of our class module to point
to our asm procedure, the loader loads the procedure by pushing both
the address of the callers next instruction in IP and also the
pointer to the callers object module onto the stack.
The net result is the first parameter is offset from the stack
pointer [esp+8]. Therefore, when we push ebp before setting ebp
to esp the first parameter becomes [ebp+12]. The new familiar.
The VB function call also effects the number of bytes we must drop
with RET when we return control back to VB. The RET instruction
will first pop the address of the callers next instruction into
IP, then drop any bytes we have specified in the
RET instruction, and then exit from our procedure.
With CallWindowProc we just need to account for the number of bytes
that are pushed onto the stack for the parameters, which are always
16 (4 * longs) no matter how many params we actually use.
When we patch the vft of our module we must account for the number of
bytes used by our parameters as well as whether it is a function or a
sub-routine. For a sub we need to add 4 for the caller object pointer
plus the number of bytes used by our parameters.
For each long or string parameter we must account for 4 bytes, and with
long alignment it's also 4 bytes for an integer or byte parameter unless
it's 2 integers in sequence, etc, so care must be taken.
Next we must account for a function, whose return value pointer is
pushed onto the stack first before any parameters.
For two parameters it would be param1 = [esp+8], param2 = [esp+12]
and the return value pointer would be placed at [esp+16].
Note for ebp it would be param1 = [ebp+12], param2 = [ebp+16]
and the return value pointer would be placed at [ebp+20].
mov esi, [ebp+20] ; get address of the return value
Now those who were paying attention may have noticed that our VB procedure
is receiving an extra piece of data on the stack than we newby asm coders
are used to - not only is it being pushed onto the stack after our parameters
but we also have to drop this data off the stack for the stack
to be returned to its correct state for the caller on return.
mov [esi], eax ; store the function result
pop esi ; restore pointer register
pop ebp ; restore base pointer register
xor eax, eax ; clear eax to tell VB all is okay
ret 16 ; function return (P1, P2, rc, lpObj)
sub p1, p2
func p1 rc
sub p1, p2, p3, p4
func p1, p2, p3 rc
And if I may digress for a moment, my one problem with using asm as a quick
and powerful extension of VB is that the asm routine runs pretty much in
isolation to my VB project, requiring everything it needs to be passed as
parameters. As it turns out we have access to all module level data in the
calling class without having to pack it all into custom udt's to pass a
pointer to the data as a parameter (Rob Rayment is known to do this trick
This extra data pushed onto the stack turns out to be a pointer to the object
module of the caller - in VB terms that means that the ObjPtr(cCaller) is being
passed as [esp+4]. If, for example, we have a module level variable named
lngMyData, and its VarPtr value is 52 bytes greater than the classes ObjPtr
value, then the value in [esp+4] + 52 will always point to lngMyData.
mov esi, [ebp+8] ; pointer to caller object module
mov dword ptr [esi+52], 33 ; assign the value 33 to lngMyData
I'm sure that this info will have a limited audience, but just for those
who may be interested I decided to post it here on PSC.