<dd id="sga4y"><optgroup id="sga4y"></optgroup></dd>
<nav id="sga4y"><code id="sga4y"></code></nav>
  • <optgroup id="sga4y"></optgroup>

    原文地址:http://drops.wooyun.org/tips/2046

    第11章


    選擇結構switch()/case/default

    11.1 一些例子

    #!bash
    void f (int a)
    {
        switch (a)
        {
            case 0: printf ("zero
    "); break;
            case 1: printf ("one
    "); break;
            case 2: printf ("two
    "); break;
            default: printf ("something unknown
    "); break;
        };
    };
    

    11.1.1 X86

    反匯編結果如下(MSVC 2010):

    清單11.1: MSVC 2010

    #!bash
    tv64 = -4       ; size = 4
    _a$ = 8         ; size = 4
    _f  PROC
        push    ebp
        mov     ebp, esp
        push    ecx
        mov     eax, DWORD PTR _a$[ebp]
        mov     DWORD PTR tv64[ebp], eax
        cmp     DWORD PTR tv64[ebp], 0
        je      SHORT [email protected]
        cmp     DWORD PTR tv64[ebp], 1
        je      SHORT [email protected]
        cmp     DWORD PTR tv64[ebp], 2
        je      SHORT [email protected]
        jmp     SHORT [email protected]
    [email protected]:
        push    OFFSET $SG739 ; ’zero’, 0aH, 00H
        call    _printf
        add     esp, 4
        jmp     SHORT [email protected]
    [email protected]:
        push    OFFSET $SG741 ; ’one’, 0aH, 00H
        call    _printf
        add     esp, 4
        jmp     SHORT [email protected]
    [email protected]:
        push    OFFSET $SG743 ; ’two’, 0aH, 00H
        call    _printf
        add     esp, 4
        jmp     SHORT [email protected]
    [email protected]:
        push    OFFSET $SG745 ; ’something unknown’, 0aH, 00H
        call    _printf
        add     esp, 4
    [email protected]:
        mov     esp, ebp
        pop     ebp
        ret     0
    _f    ENDP
    

    輸出函數的switch中有一些case選擇分支,事實上,它是和下面這個形式等價的:

    #!cpp
    void f (int a)
    {
        if (a==0)
            printf ("zero
    ");
        else if (a==1)
            printf ("one
    ");
        else if (a==2)
            printf ("two
    ");
        else
            printf ("something unknown
    ");
    };
    

    當switch()中有一些case分支時,我們可以看到此類代碼,雖然不能確定,但是,事實上switch()在機器碼級別上就是對if()的封裝。這也就是說,switch()其實只是對有一大堆類似條件判斷的if()的一個語法糖。

    在生成代碼時,除了編譯器把輸入變量移動到一個臨時本地變量tv64中之外,這塊代碼對我們來說并無新意。

    如果是在GCC 4.4.1下編譯同樣的代碼,我們得到的結果也幾乎一樣,即使你打開了最高優化(-O3)也是如此。

    讓我們在微軟VC編譯器中打開/Ox優化選項: cl 1.c /Fa1.asm /Ox

    清單11.2: MSVC

    #!bash
    _a$ = 8                 ; size = 4
    _f  PROC
        mov     eax, DWORD PTR _a$[esp-4]
        sub     eax, 0
        je      SHORT [email protected]
        sub     eax, 1
        je      SHORT [email protected]
        sub     eax, 1
        je      SHORT [email protected]
        mov     DWORD PTR _a$[esp-4], OFFSET $SG791 ; ’something unknown’, 0aH, 00H
        jmp     _printf
    [email protected]:
        mov     DWORD PTR _a$[esp-4], OFFSET $SG789 ; ’two’, 0aH, 00H
        jmp     _printf
    [email protected]:
        mov     DWORD PTR _a$[esp-4], OFFSET $SG787 ; ’one’, 0aH, 00H
        jmp     _printf
    [email protected]:
        mov     DWORD PTR _a$[esp-4], OFFSET $SG785 ; ’zero’, 0aH, 00H
        jmp     _printf
    _f ENDP
    

    我們可以看到瀏覽器做了更多的難以閱讀的優化(Dirty hacks)。

    首先,變量的值會被放入EAX,接著EAX減0。聽起來這很奇怪,但它之后是需要檢查先前EAX寄存器的值是否為0的,如果是,那么程序會設置上零標志位ZF(這也表示了減去0之后,結果依然是0),第一個條件跳轉語句JE(Jump if Equal 或者同義詞 JZ - Jump if Zero)會因此觸發跳轉。如果這個條件不滿足,JE沒有跳轉的話,輸入值將減去1,之后就和之前的一樣了,如果哪一次值是0,那么JE就會觸發,從而跳轉到對應的處理語句上。

    (譯注:SUB操作會重置零標志位ZF,但是MOV不會設置標志位,而JE將只有在ZF標志位設置之后才會跳轉。如果需要基于EAX的值來做JE跳轉的話,是需要用這個方法設置標志位的)。

    并且,如果沒有JE語句被觸發,最終,printf()函數將收到“something unknown”的參數。

    其次:我們看到了一些不尋常的東西——字符串指針被放在了變量里,然后printf()并沒有通過CALL,而是通過JMP來調用的。 這個可以很簡單的解釋清楚,調用者把參數壓棧,然后通過CALL調用函數。CALL通過把返回地址壓棧,然后做無條件跳轉來跳到我們的函數地址。我們的函數在執行時,不管在任何時候都有以下的棧結構(因為它沒有任何移動棧指針的語句):

    · ESP —— 指向返回地址
    · ESP+4 —— 指向變量a (也即參數)
    

    另一方面,當我們這兒調用printf()函數的時候,它也需要有與我們這個函數相同的棧結構,不同之處只在于printf()的第一個參數是指向一個字符串的。 這也就是你之前看到的我們的代碼所做的事情。

    我們的代碼把第一個參數的地址替換了,然后跳轉到printf(),就像第一個沒有調用我們的函數f()而是先調用了printf()一樣。 printf()把一串字符輸出到stdout 中,然后執行RET語句, 這一句會從棧上彈出返回地址,因此,此時控制流會返回到調用f()的函數上,而不是f()上。

    這一切之所以能發生,是因為printf()在f()的末尾。在一些情況下,這有些類似于longjmp()函數。當然,這一切只是為了提高執行速度。

    ARM編譯器也有類似的優化,請見5.3.2節“帶有多個參數的printf()函數調用”。

    11.1.2 ARM: 優化后的 Keil + ARM 模式

    #!bash
    .text:0000014C             f1
    .text:0000014C 00 00 50 E3          CMP R0, #0
    .text:00000150 13 0E 8F 02          ADREQ R0, aZero     ; "zero
    "
    .text:00000154 05 00 00 0A          BEQ loc_170
    .text:00000158 01 00 50 E3          CMP R0, #1
    .text:0000015C 4B 0F 8F 02          ADREQ R0, aOne      ; "one
    "
    .text:00000160 02 00 00 0A          BEQ loc_170
    .text:00000164 02 00 50 E3          CMP R0, #2
    .text:00000168 4A 0F 8F 12          ADRNE R0, aSomethingUnkno ; "something unknown
    "
    .text:0000016C 4E 0F 8F 02          ADREQ R0, aTwo      ; "two
    "
    .text:00000170
    .text:00000170                      loc_170             ; CODE XREF: f1+8
    .text:00000170                                          ; f1+14
    .text:00000170 78 18 00 EA          B __2printf
    

    我們再一次看看這個代碼,我們不能確定的說這就是源代碼里面的switch()或者說它是if()的封裝。

    但是,我們可以看到這里它也在試圖預測指令(像是ADREQ(相等)),這里它會在R0=0的情況下觸發,并且字符串“zero97av