February 7th, 2019

The Intel 80386, part 15: Common compiler-generated code sequences

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.

Topics
History

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

0 comments

Discussion are closed.