I assume you all know what a WndProc is, and what you need it for... Let me give you an 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.
So, wouldn't it be nice if we didn't build a stack frame at all? It will save some time and space. Sounds good, so let's try it.
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 (ebp), 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.
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.
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?
Ofcourse these stunts can be applied to other procedures as well. Be careful,
and use them in good health.
Well, we all had a haunch it was possible, since there are SBYTE, SWORD and
SDWORD types nowadays, and when using these, the assembler actually generates
signed comparisons already.
For example:
.data
foo SDWORD -25
.code
.IF eax < foo
mov eax, [foo]
.ENDIF
That's nice, but we want to be able to do signed compares with immediates
aswell.
In ASM, the thing that comes closest to a typecast is the PTR directive. This is what we need to use. We can cast the immediate operand to a signed type, and the macro will generate a signed comparison.
Looking something like this:
.IF eax < SDWORD PTR 0
xor eax, eax
.ENDIF
And the last option we have is using registers to compare. Well I can be very
short about that one, because it works in exactly the same way:
.IF eax < SDWORD PTR edx
xor eax, eax
.ENDIF
While we're on the subject... Did you know you could also do a .IF statement
on flags?
There are special symbols for the flags:
zero? carry? overflow? sign? parity?With these you can make .IF statements like this:
sub eax, 50
.IF carry?
xor eax, eax
.ENDIF
And that's about all there is to tell you...