The Microsoft compiler employs a few patterns in its code generation that you may want to become familiar with.
As we saw earlier, a call to a __thiscall
function passes the this
pointer in the ecx register, with the remaining parameters passed on the stack. A typical calling sequence would go something like this:
; p->Foo(x, 42); push 42 ; parameter 2 push dword ptr [ebp-24h] ; parameter 1 mov ecx, dword ptr [ebp-20h] ; "this" for call call CThing::Foo ; call the function directly
If the method is virtual, then there is a vtable lookup:
; p->Foo(x, 42); push 42 ; parameter 2 push dword ptr [ebp-24h] ; parameter 1 mov ecx, dword ptr [ebp-20h] ; "this" for call mov eax, dword ptr [ecx] ; fetch vtable call dword ptr [eax+10h] ; call through the vtable
If the method uses __stdcall
instead of __thiscall
(typically because it is a COM method), then the this parameter is passed on the stack rather than in the ecx register.
; p->Foo(x, 42); ; non-virtual call push 42 ; parameter 2 push dword ptr [ebp-24h] ; parameter 1 push dword ptr [ebp-20h] ; "this" for call call CThing::Foo ; call the function directly ; virtual call push 42 ; parameter 2 push dword ptr [ebp-24h] ; parameter 1 mov ecx, dword ptr [ebp-20h] ; "this" for call push ecx ; pass as stack parameter mov eax, dword ptr [ecx] ; fetch vtable call dword ptr [eax+10h] ; call through the vtable
The Microsoft compiler uses a jump table for dense switch
statements, but it adds a level of indirection so that multiple cases that leads to the same target share the same jump entry.
Consider the following fragment:
switch (value) { case 2: case 3: case 5: case 7: printf("prime"); break; case 4: case 9: printf("perfect square"); break; default: printf("I'm sure you're special"); break; }
The resulting code may look like this:
mov eax, dword ptr [ebp-30h] ; load value sub eax, 2 ; table starts with "case 2" cmp eax, 8 ; table has 8 entries jae case_default ; not in table, go to default case movzx eax, byte ptr [eax+level1] ; get the index into the second table jmp dword ptr [eax+level2] ; jump to handler ... level2 dd offset case_prime ; slot 0 is for cases 2, 3, 5, and 7 dd offset case_square ; slot 1 is for cases 4 and 9 dd offset case_default ; slot 2 is for everybody else level1 db 0, 0, 1, 0, 2, 0, 2, 1 ; generate the slots ; 2 3 4 5 6 7 8 9 ; corresponding cases
Adding a level of indirection allows the level-2 jump table to be smaller. The trade-off is that you have to pay for a level-1 jump table, but if there are a lot of duplicates (such as those created by all the missing cases that go to default:
), the trade-off may be worth it.
If you stare at the output and do some reverse-compiling, you can imagine that the compiler internally rewrote it as
enum SwitchResult { Prime, PerfectSquare, Default }; static const unsigned char level1[] = { Prime, Prime, PerfectSquare, Prime, Default, Prime, Default, PerfectSquare }; unsigned int index1 = (unsigned)value - 2; switch (index1 < 8 ? level1[index1] : Default) { case Prime: printf("prime"); break; case PerfectSquare: printf("perfect square"); break; case Default: printf("I'm sure you're special"); break; default: __assume(0); // not reached }
Next time, we’ll wrap up our quick tour of the 80386 by walking through a simple function.
0 comments