I assume you all know what a WndProc is, and what you need it for. Let me
give you a quick example of a WndProc:
WndProc PROC hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg == WM_DESTROY
INVOKE PostQuitMessage, NULL
.ELSE
INVOKE DefWindowProc, hWnd, uMsg, wParam, lParam
ret
.ENDIF
xor eax, eax
ret
WndProc ENDP
This generates the following code:
push ebp ; Create stack frame
mov ebp, esp ; Why does MASM use 'leave',
; but not 'enter'?
cmp dword ptr [ebp+0C], WM_DESTROY ; ebp+0C is uMsg
jne @@notDestroy
push NULL
Call PostQuitMessage
jmp @@exitFromDestroy
@@notDestroy:
push [ebp+14] ; ebp+14 is lParam
push [ebp+10] ; epb+10 is wParam
push [ebp+0C] ; ebp+0C is uMsg
push [ebp+08] ; ebp+08 is hWnd
Call DefWindowProcA ; Let Windows handle the other
; messages
leave ; Remove stack frame
ret 0010 ; Remove function arguments
; from stack and return
@@exitFromDestroy:
xor eax, eax ; Return 'FALSE'
leave ; Remove stack frame
ret 0010 ; Remove function arguments
; from stack and return
Looks nice, and works fine... But, it builds a stack frame, even though we are
not using local variables. And if you code in a good fashion, there almost
never will be ...after all, this procedure is just a messagehandler, and to keep
your code tidy, you will not put all the code in here, but in separate procedures,
which you will call from here.
There's only one reason why MASM builds a stack frame for a function: The
function has a prototype for a hll call. A hll call uses the stack to transfer
its arguments.
So, all we have to do, is remove the prototype. That's easy: Just don't tell
MASM that this function uses any arguments.
This simple tweak will do the trick:
WndProc PROC
...
WndProc ENDP
The arguments will still be passed to the function, since that part of the
code is in the Windows kernel, and has not changed. Be careful though: Since
MASM does not know that there are arguments on the stack, it no longer cleans
up the stack. You have to specify that yourself.
Now we have a slight problem: How can we access the arguments now?
The answer is surprisingly easy: We create aliases for the addresses relative
to the stack pointer (esp). MASM does the same, except that it uses the base
pointer since it created a stack frame, and saved the original stack pointer
in ebp.
Knowing that Windows hll calls always push the arguments in reverse order, and
that the return address is stored on the stack aswell, we can devise these
indices for our parameters:
hWnd EQU dword ptr [esp][4]
uMsg EQU dword ptr [esp][8]
wParam EQU dword ptr [esp][12]
lParam EQU dword ptr [esp][16]
There, now we can refer to the arguments as usual.
There's 1 drawback however: Since the indices are relative to esp, they are
only valid when esp is not touched. In other words: Don't try to push or pop
anything and then use these arguments again. They can be used if you push some
variables, then pop them again before you access any of these arguments again,
because the stack pointer will be at the correct position again.
Let's say you need to use the stack again (eg. for an INVOKE), so the indices
will be invalidated. You might think that the only option then is to save the
stack pointer again, so we're back to the stack frame...
It's an option, but not the best one. Namely, ebp is a non-volatile register,
and needs to be saved and restored after use.
But, there are more registers in the CPU, and most of them are volatile. How
about using esi for example?
WndProc PROC
mov esi, esp
hWnd EQU dword ptr [esi][4]
uMsg EQU dword ptr [esi][8]
wParam EQU dword ptr [esi][12]
lParam EQU dword ptr [esi][16]
...
WndProc ENDP
And if you leave the stack as you found it (which should always be the case
with decent code), you don't even need to restore esp again.
If you got dirty and the stack still contains variables you don't want
anymore, then this is enough for a clean exit:
WndProc PROC
...
mov esp, esi
ret 4 * sizeof dword ; As I mentioned earlier, we have to clean
; the stack ourselves.
; We had 4 dword arguments, so this does
; the trick
WndProc ENDP
Still less code, and thus faster than the original. And just as rigid. You
have one register less to use during the WndProc, but as I said earlier, there
shouldn't be too much code here, so should be able to spare the register.
Well, there's just 1 more thing that can be done with this tweaked WndProc.
Namely, if you leave the stack as you found it, the arguments for the
DefWindowProc are already in place, and the return address of our caller is
there too.
So basically we can just jump to it without any further ado. The resulting
WndProc that is equivalent to the original one will look like this then:
WndProc PROC
hWnd EQU dword ptr [esp][4]
uMsg EQU dword ptr [esp][8]
wParam EQU dword ptr [esp][12]
lParam EQU dword ptr [esp][16]
.IF uMsg == WM_DESTROY
INVOKE PostQuitMessage, NULL
.ELSE
jmp DefWindowProc
.ENDIF
xor eax, eax
ret 4 * sizeof dword ; Be sure to clean that stack!
WndProc ENDP
Yes, much shorter, and faster. Let's take a look at the generated code to get
a better understanding of how much shorter it actually is:
cmp dword ptr [esp+08], WM_DESTROY
jne @@noDestroy
push NULL
Call PostQuitMessage
jmp @@exitFromDestroy
@@noDestroy:
Jmp DefWindowProcA
@@exitFromDestroy
xor eax, eax
ret 0010
If you code it 'by hand' instead of with the .IF statement, there's another
tweak we can pull, but the rest looks great, doesn't it?
Of course these stunts can be applied to other procedures as well. Be careful,
and use them in good health.
|