<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/1963

    CHAPER7


    訪問傳遞參數

    現在我們來看函數調用者通過棧把參數傳遞到被調用函數。被調用函數是如何訪問這些參數呢?

    #!cpp
    #include <stdio.h>
    int f (int a, int b, int c)
    {
            return a*b+c;
    };
    int main() 
    {
            printf ("%d
    ", f(1, 2, 3));
            return 0; 
    };
    

    7.1 X86

    7.1.1 MSVC

    如下為相應的反匯編代碼(MSVC 2010 Express)

    Listing 7.2 MSVC 2010 Express

    #!bash
    _TEXT   SEGMENT
    _a$ = 8                                  ; size = 4
    _b$ = 12                                 ; size = 4
    _c$ = 16                                 ; size = 4
    _f      PROC
            push ebp
            mov ebp, esp
            mov eax, DWORD PTR _a$[ebp]
            imul eax, DWORD PTR _b$[ebp]
            add eax, DWORD PTR _c$[ebp]
            pop ebp
            ret 0
    _f      ENDP
    
    _main   PROC
            push ebp
            mov ebp, esp
            push 3 ; 3rd argument
            push 2 ; 2nd argument
            push 1 ; 1st argument
            call _f
            add esp, 12
            push eax
            push OFFSET $SG2463 ; ’%d’, 0aH, 00H
            call _printf
            add esp, 8
            ; return 0
            xor eax, eax
            pop ebp
            ret 0
    _main ENDP
    

    我們可以看到函數main()中3個數字被圧棧,然后函數f(int, int, int)被調用。函數f()內部訪問參數時使用了像_ a$=8 的宏,同樣,在函數內部訪問局部變量也使用了類似的形式,不同的是訪問參數時偏移值(為正值)。因此EBP寄存器的值加上宏_a$的值指向壓棧參數。

    _a$[ebp]的值被存儲在寄存器eax中,IMUL指令執行后,eax的值為eax與_b$[ebp]的乘積,然后eax與_c$[ebp]的值相加并將和放入eax寄存器中,之后返回eax的值。返回值作為printf()的參數。

    7.1.2 MSVC+OllyDbg

    我們在OllyDbg中觀察,跟蹤到函數f()使用第一個參數的位置,可以看到寄存器EBP指向棧底,圖中使用紅色箭頭標識。棧幀中第一個被保存的是EBP的值,第二個是返回地址(RA),第三個是參數1,接下來是參數2,以此類推。因此,當我們訪問第一個參數時EBP應該加8(2個32-bit字節寬度)。

    enter image description here

    Figure 7.1: OllyDbg: 函數f()內部

    7.1.3 GCC

    使用GCC4.4.1編譯后在IDA中查看

    Listing 7.3: GCC 4.4.1

    #!bash
                public f
    f           proc near
    
    arg_0       = dword ptr 8
    arg_4       = dword ptr 0Ch
    arg_8       = dword ptr 10h
    
                push    ebp
                mov     ebp, esp
                mov     eax, [ebp+arg_0]  ; 1st argument
                imul    eax, [ebp+arg_4]  ; 2nd argument
                add     eax, [ebp+arg_8]  ; 3rd argument
                pop     ebp
                retn
    f           endp
    
                public main
    main        proc near
    
    var_10      = dword ptr -10h
    var_C       = dword ptr -0Ch
    var_8       = dword ptr -8
    
                push    ebp
                mov     ebp, esp
                and     esp, 0FFFFFFF0h
                sub     esp, 10h
                mov     [esp+10h+var_8], 3  ; 3rd argument
                mov     [esp+10h+var_C], 2  ; 2nd argument
                mov     [esp+10h+var_10], 1  ; 1st argument
                call    f
                mov     edx, offset aD ; "%d
    "
                mov     [esp+10h+var_C], eax
                mov     [esp+10h+var_10], edx
                call    _printf
                mov     eax, 0
                leave
                retn
    main    endp
    

    幾乎相同的結果。

    執行兩個函數后棧指針ESP并沒有顯示恢復,因為倒數第二個指令LEAVE(B.6.2)會自動恢復棧指針。

    7.2 X64

    x86-64架構下有點不同,函數參數(4或6)使用寄存器傳遞,被調用函數通過訪問寄存器來訪問傳遞進來的參數。

    7.2.1 MSVC

    MSVC優化后:

    Listing 7.4: MSVC 2012 /Ox x64

    #!bash
    $SG2997     DB      ’%d’, 0aH, 00H
    
    main        PROC
                sub     rsp, 40
                mov     edx, 2
                lea     r8d, QWORD PTR [rdx+1]  ; R8D=3
                lea     ecx, QWORD PTR [rdx-1]  ; ECX=1
                call    f
                lea     rcx, OFFSET FLAT:$SG2997  ; ’%d’
                mov     edx, eax
                call    printf
                xor     eax, eax
                add     rsp, 40
                ret     0
    main        ENDP
    
    f           PROC
                ; ECX - 1st argument
                ; EDX - 2nd argument
                ; R8D - 3rd argument
                imul    ecx, edx
                lea     eax, DWORD PTR [r8+rcx]
                ret     0
    f           ENDP
    

    我們可以看到函數f()直接使用寄存器來操作參數,LEA指令用來做加法,編譯器認為使用LEA比使用ADD指令要更快。在mian()中也使用了LEA指令,編譯器認為使用LEA比使用MOV指令效率更高。

    我們來看看MSVC沒有優化的情況:

    Listing 7.5: MSVC 2012 x64

    #!bash
    f           proc near
    
    ; shadow space:
    arg_0       = dword ptr 8
    arg_8       = dword ptr 10h
    arg_10      = dword ptr 18h
    
                ; ECX - 1st argument
                ; EDX - 2nd argument
                ; R8D - 3rd argument
                mov     [rsp+arg_10], r8d
                mov     [rsp+arg_8], edx
                mov     [rsp+arg_0], ecx
                mov     eax, [rsp+arg_0]
                imul    eax, [rsp+arg_8]
                add     eax, [rsp+arg_10]
                retn
    f endp
    
    main        proc    near
                sub     rsp, 28h
                mov     r8d, 3 ; 3rd argument
                mov     edx, 2 ; 2nd argument
                mov     ecx, 1 ; 1st argument
                call    f
                mov     edx, eax
                lea     rcx, $SG2931 ; "%d
    "
                call    printf
    
                ; return 0
                xor     eax, eax
                add     rsp, 28h
                retn
    main        endp
    

    這里從寄存器傳遞進來的3個參數因為某種情況又被保存到棧里。這就是所謂的“shadow space”2:每個Win64通常(不是必需)會保存所有4個寄存器的值。這樣做由兩個原因:1)為輸入參數分配所有寄存器(即使是4個)太浪費,所以要通過堆棧來訪問;2)每次中斷下來調試器總是能夠定位函數參數3。

    調用者負責在棧中分配“shadow space”。

    7.2.2 GCC

    GCC優化后的代碼:

    Listing 7.6: GCC 4.4.6 -O3 x64

    #!bash
    f:
            ; EDI - 1st argument
            ; ESI - 2nd argument
            ; EDX - 3rd argument
            imul    esi, edi
            lea     eax, [rdx+rsi]
            ret
    
    main:
            sub     rsp, 8
            mov     edx, 3
            mov     esi, 2
            mov     edi, 1
            call    f
            mov     edi, OFFSET FLAT:.LC0 ; "%d
    "
            mov     esi, eax
            xor     eax, eax ; number of vector registers passed
            call    printf
            xor     eax, eax
            add     rsp, 8
            ret
    

    GCC無優化代碼:

    Listing 7.7: GCC 4.4.6 x64

    #!bash
    f:
            ; EDI - 1st argument
            ; ESI - 2nd argument
            ; EDX - 3rd argument
            push    rbp
            mov     rbp, rsp
            mov     DWORD PTR [rbp-4], edi
            mov     DWORD PTR [rbp-8], esi
            mov     DWORD PTR [rbp-12], edx
            mov     eax, DWORD PTR [rbp-4]
            imul    eax, DWORD PTR [rbp-8]
            add     eax, DWORD PTR [rbp-12]
            leave
            ret
    
    main:
            push    rbp
            mov     rbp, rsp
            mov     edx, 3
            mov     esi, 2
            mov     edi, 1
            call    f
            mov     edx, eax
            mov     eax, OFFSET FLAT:.LC0 ; "%d
    "
            mov     esi, edx
            mov     rdi, rax
            mov     eax, 0 ; number of vector registers passed
            call    printf
            mov     eax, 0
            leave
            ret
    

    System V *NIX [21]沒有“shadow space”,但被調用者可能會保存參數,這也是造成寄存器短缺的原因。

    7.2.3 GCC: uint64_t instead int

    我們例子使用的是32位int,寄存器也為32位寄存器(前綴為E-)。

    為處理64位數值內部會自動調整為64位寄存器:

    #!cpp
    #include <stdio.h>
    #include <stdint.h>
    
    uint64_t f (uint64_t a, uint64_t b, uint64_t c)
    {
        return a*b+c;
    };
    int main()
    {
        printf ("%lld
    ", f(0x1122334455667788,0x1111111122222222,0x3333333344444444));
        return 0;
    };
    

    Listing 7.8: GCC 4.4.6 -O3 x64

    #!cpp
    f       proc near
            imul    rsi, rdi
            lea     rax, [rdx+rsi]
            retn
    f       endp
    
    main    proc near
            sub     rsp, 8
            mov     rdx, 3333333344444444h ; 3rd argument
            mov     rsi, 1111111122222222h ; 2nd argument
            mov     rdi, 1122334455667788h ; 1st argument
            call    f
            mov     edi, offset format ; "%lld
    "
            mov     rsi, rax
            xor     eax, eax ; number of vector registers passed
            call    _printf
            xor     eax, eax
            add     rsp, 8
            retn
    main    endp
    

    代碼非常相似,只是使用了64位寄存器(前綴為R)。

    7.3 ARM

    7.3.1 未優化的Keil + ARM mode

    #!bash
    .text:000000A4 00 30 A0 E1              MOV     R3, R0
    .text:000000A8 93 21 20 E0              MLA     R0, R3, R1, R2
    .text:000000AC 1E FF 2F E1              BX      LR
    ...
    .text:000000B0             main
    .text:000000B0 10 40 2D E9              STMFD   SP!, {R4,LR}
    .text:000000B4 03 20 A0 E3              MOV     R2, #3
    .text:000000B8 02 10 A0 E3              MOV     R1, #2
    .text:000000BC 01 00 A0 E3              MOV     R0, #1
    .text:000000C0 F7 FF FF EB              BL      f
    .text:000000C4 00 40 A0 E1              MOV     R4, R0
    .text:000000C8 04 10 A0 E1              MOV     R1, R4
    .text:000000CC 5A 0F 8F E2              ADR     R0, aD_0  ; "%d
    "
    .text:000000D0 E3 18 00 EB              BL      __2printf
    .text:000000D4 00 00 A0 E3              MOV     R0, #0
    .text:000000D8 10 80 BD E8              LDMFD   SP!, {R4,PC}
    

    main()函數里調用了另外兩個函數,3個值被傳遞到f();

    正如前面提到的,ARM通常使用前四個寄存器(R0-R4)傳遞前四個值。

    f()函數使用了前三個寄存器(R0-R2)作為參數。

    MLA (Multiply Accumulate)指令將R3寄存器和R1寄存器的值相乘,然后再將乘積與R2寄存器的值相加將結果存入R0,函數返回R0。

    一條指令完成乘法和加法4,如果不包括SIMD新的FMA指令5,通常x86下沒有這樣的指令。

    第一條指令MOV R3,R0,看起來冗余是因為該代碼是非優化的。

    BX指令返回到LR寄存器存儲的地址,處理器根據狀態模式從Thumb狀態轉換到ARM狀態,或者反之。函數f()可以被ARM代碼或者Thumb代碼調用,如果是Thumb代碼調用BX將返回到調用函數并切換到Thumb模式,或者反之。

    7.3.2 Optimizing Keil + ARM mode

    #!bash
    .text:00000098            f
    .text:00000098 91 20 20 E0                MLA R0, R1, R0, R2
    .text:0000009C 1E FF 2F E1                BX  LR
    

    這里f()編譯時使用完全優化模式(-O3),MOV指令被優化,現在MLA使用所有輸入寄存器并將結果置入R0寄存器。

    7.3.3 Optimizing Keil + thumb mode

    #!bash
    .text:0000005E 48 43                  MULS R0, R1
    .text:00000060 80 18                  ADDS R0, R0, R2
    .text:00000062 70 47                  BX   LR
    

    Thumb模式下沒有MLA指令,編譯器做了兩次間接處理,MULS指令使R0寄存器的值與R1寄存器的值相乘并將結果存入R0。ADDS指令將R0與R2的值相加并將結果存入R0。

    Chapter 8


    一個或者多個字的返回值

    X86架構下通常返回EAX寄存器的值,如果是單字節char,則只使用EAX的低8位AL。如果返回float類型則使用FPU寄存器ST(0)。ARM架構下通常返回寄存器R0。

    假如main()函數的返回值是void而不是int會怎么樣?

    通常啟動函數調用main()為:

    #!bash
    push envp
    push argv
    push argc
    call main
    push eax
    call exit
    

    換句話說為

    #!cpp
    exit(main(argc,argv,envp));
    

    如果main()聲明為void類型并且函數沒有明確返回狀態值,通常在main()結束時EAX寄存器的值被返回,然后作為exit()的參數。大多數情況下函數返回的是隨機值。這種情況下程序的退出代碼為偽隨機的。

    我們看一個實例,注意main()是void類型:

    #!cpp
    #include <stdio.h>
    void main()
    {
        printf ("Hello, world!
    ");
    };
    

    我們在linux下編譯。

    GCC 4.8.1會使用puts()替代printf()(看前面章節2.3.3),沒有關系,因為puts()會返回打印的字符數,就行printf()一樣。請注意,main()結束時EAX寄存器的值是非0的,這意味著main()結束時保留puts()返回時EAX的值。

    Listing 8.1: GCC 4.8.1

    #!bash
    .LC0:
            .string "Hello, world!"
    main:
            push    ebp
            mov     ebp, esp
            and     esp, -16
            sub     esp, 16
            mov     DWORD PTR [esp], OFFSET FLAT:.LC0
            call    puts
            leave
            ret
    

    我們寫bash腳本來看退出狀態:

    Listing 8.2: tst.sh

    #!bash
    #!/bin/sh
    ./hello_world
    echo $?
    

    運行:

    #!bash
    $ tst.sh
    Hello, world!
    14
    

    14為打印的字符數。

    回到返回值是EAX寄存器值的事實,這也就是為什么老的C編譯器不能夠創建返回信息無法擬合到一個寄存器(通常是int型)的函數。如果必須這樣,應該通過指針來傳遞。現在可以這樣,比如返回整個結構體,這種情況應該避免。如果必須要返回大的結構體,調用者必須開辟存儲空間,并通過第一個參數傳遞指針,整個過程對程序是透明的。像手動通過第一個參數傳遞指針一樣,只是編譯器隱藏了這個過程。

    小例子:

    #!cpp
    struct s
    {
        int a;
        int b;
        int c;
    };
    
    struct s get_some_values (int a)
    {
        struct s rt;
        rt.a=a+1;
        rt.b=a+2;
        rt.c=a+3;
    
        return rt;
    };
    

    …我們可以得到(MSVC 2010 /Ox):

    #!bash
    $T3853 = 8                  ; size = 4
    _a$ = 12                    ; size = 4
    ?get_some_values@@YA?AUs@@[email protected] PROC      ; get_some_values
        mov     ecx, DWORD PTR _a$[esp-4]
        mov     eax, DWORD PTR $T3853[esp-4]
        lea     edx, DWORD PTR [ecx+1]
        mov     DWORD PTR [eax], edx
        lea     edx, DWORD PTR [ecx+2]
        add     ecx, 3
        mov     DWORD PTR [eax+4], edx
        mov     DWORD PTR [eax+8], ecx
        ret     0
    ?get_some_values@@YA?AUs@@[email protected] ENDP      ; get_some_values
    

    內部變量傳遞指針到結構體的宏為$T3853。

    這個例子可以用C99語言擴展來重寫:

    #!bash
    struct s
    {
        int a;
        int b;
        int c;
    };
    
    struct s get_some_values (int a)
    {
        return (struct s){.a=a+1, .b=a+2, .c=a+3};
    };
    

    Listing 8.3: GCC 4.8.1

    #!bash
    _get_some_values proc near
    
    ptr_to_struct   = dword ptr 4
    a               = dword ptr 8
                    mov     edx, [esp+a]
                    mov     eax, [esp+ptr_to_struct]
                    lea     ecx, [edx+1]
                    mov     [eax], ecx
                    lea     ecx, [edx+2]
                    add     edx, 3
                    mov     [eax+4], ecx
                    mov     [eax+8], edx
                    retn
    _get_some_values endp
    

    我們可以看到,函數僅僅填充調用者申請的結構體空間的相應字段。因此沒有性能缺陷。

    Chapter 9


    指針

    指針通常被用作函數返回值(recall scanf() case (6)).例如,當函數返回兩個值時。

    9.1 Global variables example

    #!bash
    #include <stdio.h>
    
    void f1 (int x, int y, int *sum, int *product)
    {
        *sum=x+y;
        *product=x*y;
    };
    
    int sum, product;
    
    void main()
    {
        f1(123, 456, &sum, &product);
        printf ("sum=%d, product=%d
    ", sum, product);
    };
    

    編譯后

    Listing 9.1: Optimizing MSVC 2010 (/Ox /Ob0)

    #!bash
    COMM        _product:DWORD
    COMM        _sum:DWORD
    $SG2803 DB              ’sum=%d, product=%d’, 0aH, 00H
    
    _x$ = 8                                     ; size = 4
    _y$ = 12                                    ; size = 4
    _sum$ = 16                                  ; size = 4
    _product$ = 20                              ; size = 4
    _f1         PROC
                mov     ecx, DWORD PTR _y$[esp-4]
                mov     eax, DWORD PTR _x$[esp-4]
                lea     edx, DWORD PTR [eax+ecx]
                imul    eax, ecx
                mov     ecx, DWORD PTR _product$[esp-4]
                push    esi
                mov     esi, DWORD PTR _sum$[esp]
                mov     DWORD PTR [esi], edx
                mov     DWORD PTR [ecx], eax
                pop     esi
                ret     0
    _f1         ENDP
    
    _main       PROC
                push    OFFSET _product
                push    OFFSET _sum
                push    456                     ; 000001c8H
                push    123                     ; 0000007bH
                call    _f1
                mov     eax, DWORD PTR _product
                mov     ecx, DWORD PTR _sum
                push    eax
                push    ecx
                push    OFFSET $SG2803
                call    DWORD PTR __imp__printf
                add     esp, 28                 ; 0000001cH
                xor     eax, eax
                ret     0
    _main   ENDP
    

    讓我們在OD中查看:圖9.1。首先全局變量地址被傳遞進f1()。我們在堆棧元素點擊“數據窗口跟隨”,可以看到數據段上分配兩個變量的空間。這些變量被置0,因為未初始化數據(BSS1)在程序運行之前被清理為0。這些變量屬于數據段,我們按Alt+M可以查看內存映射fig. 9.5.

    讓我們跟蹤(F7)到f1()fig. 9.2.在堆棧中為456 (0x1C8) 和 123 (0x7B),接著是兩個全局變量的地址。

    讓我們跟蹤到f1()結尾,可以看到兩個全局變量存放了計算結果。

    現在兩個全局變量的值被加載到寄存器傳遞給printf(): fig. 9.4.

    enter image description here

    Figure 9.1: OllyDbg: 全局變量地址被傳遞進f1()

    enter image description here

    Figure 9.2: OllyDbg: f1()開始

    enter image description here

    Figure 9.3: OllyDbg: f1()完成

    enter image description here

    Figure 9.4: OllyDbg: 全局變量被傳遞進printf()

    enter image description here

    Figure 9.5: OllyDbg: memory map

    9.2 Local variables example

    讓我們修改一下例子:

    Listing 9.2: 局部變量

    #!bash
    void main()
    {
        int sum, product; // now variables are here
    
        f1(123, 456, &sum, &product);
        printf ("sum=%d, product=%d
    ", sum, product);
    };
    

    f1()函數代碼沒有改變。僅僅main()代碼作了修改。

    Listing 9.3: Optimizing MSVC 2010 (/Ox /Ob0)

    #!bash
    _product$ = -8              ; size = 4
    _sum$ = -4                  ; size = 4
    _main   PROC
    ; Line 10
            sub     esp, 8
    ; Line 13
            lea     eax, DWORD PTR _product$[esp+8]
            push    eax
            lea     ecx, DWORD PTR _sum$[esp+12]
            push    ecx
            push    456         ; 000001c8H
            push    123         ; 0000007bH
            call    _f1
    ; Line 14
            mov     edx, DWORD PTR _product$[esp+24]
            mov     eax, DWORD PTR _sum$[esp+24]
            push    edx
            push    eax
            push    OFFSET $SG2803
            call    DWORD PTR __imp__printf
    ; Line 15
            xor     eax, eax
            add     esp, 36     ; 00000024H
            ret     0
    

    我們在OD中查看,局部變量地址在堆棧中是0x35FCF4和0x35FCF8。我們可以看到是如何圧棧的fig. 9.6.

    f1()開始的時候,隨機棧地址為0x35FCF4和0x35FCF8 fig. 9.7.

    f1()完成時結果0xDB18和0x243存放在地址0x35FCF4和0x35FCF8。

    enter image description here

    Figure 9.6: OllyDbg: 局部變量地址被圧棧

    enter image description here

    Figure 9.7: OllyDbg: f1()starting

    enter image description here

    Figure 9.8: OllyDbg: f1()finished

    9.3 小結

    f1()可以返回結果到內存的任何地方,這是指針的本質和特性。順便提一下,C++引用的工作方式和這個類似。詳情閱讀相關內容(33)。

    Chapter 10


    條件跳轉

    現在我們來了解條件跳轉。

    #!cpp
    #include <stdio.h>
    
    void f_signed (int a, int b)
    {
        if (a>b)
            printf ("a>b
    ");
        if (a==b)
            printf ("a==b
    ");
        if (a<b)
            printf ("a<b
    ");
    };
    
    void f_unsigned (unsigned int a, unsigned int b)
    {
        if (a>b)
            printf ("a>b
    ");
        if (a==b)
            printf ("a==b
    ");
        if (a<b)
            printf ("a<b
    ");
    };
    
    int main()
    {
        f_signed(1, 2);
        f_unsigned(1, 2);
        return 0;
    };
    

    10.1 x86

    10.1.1 x86 + MSVC

    f_signed() 函數:

    Listing 10.1: 非優化MSVC 2010

    #!bash
    _a$ = 8
    _b$ = 12
    _f_signed   PROC
            push    ebp
            mov     ebp, esp
            mov     eax, DWORD PTR _a$[ebp]
            cmp     eax, DWORD PTR _b$[ebp]
            jle     SHORT [email protected]_signed
            push    OFFSET $SG737       ; ’a>b’
            call    _printf
            add     esp, 4
    [email protected]_signed:
            mov     ecx, DWORD PTR _a$[ebp]
            cmp     ecx, DWORD PTR _b$[ebp]
            jne     SHORT [email protected]_signed
            push    OFFSET $SG739       ; ’a==b’
            call    _printf
            add     esp, 4
    [email protected]_signed:
            mov     edx, DWORD PTR _a$[ebp]
            cmp     edx, DWORD PTR _b$[ebp]
            jge     SHORT [email protected]_signed
            push    OFFSET $SG741       ; ’a<b’
            call    _printf
            add     esp, 4
            [email protected]_signed:
            pop     ebp
            ret     0
    _f_signed   ENDP
    

    第一個指令JLE意味如果小于等于則跳轉。換句話說,第二個操作數大于或者等于第一個操作數,控制流將傳遞到指定地址或者標簽。否則(第二個操作數小于第一個操作數)第一個printf()將被調用。第二個檢測JNE:如果不相等則跳轉。如果兩個操作數相等控制流則不變。第三個檢測JGE:大于等于跳轉,當第一個操作數大于或者等于第二個操作數時跳轉。如果三種情況都沒有發生則無printf()被調用,事實上,如果沒有特殊干預,這種情況幾乎不會發生。

    f_unsigned()函數類似,只是JBE和JAE替代了JLE和JGE,我們來看f_unsigned()函數

    Listing 10.2: GCC

    #!bash
    _a$ = 8                                 ; size = 4
    _b$ = 12                                ; size = 4
    _f_unsigned     PROC
            push    ebp
            mov     ebp, esp
            mov     eax, DWORD PTR _a$[ebp]
            cmp     eax, DWORD PTR _b$[ebp]
            jbe     SHORT [email protected]_unsigned
            push    OFFSET $SG2761 ; ’a>b’
            call    _printf
            add     esp, 4
    [email protected]_unsigned:
            mov     ecx, DWORD PTR _a$[ebp]
            cmp     ecx, DWORD PTR _b$[ebp]
            jne     SHORT [email protected]_unsigned
            push    OFFSET $SG2763 ; ’a==b’
            call    _printf
            add     esp, 4
    [email protected]_unsigned:
            mov     edx, DWORD PTR _a$[ebp]
            cmp     edx, DWORD PTR _b$[ebp]
            jae     SHORT [email protected]_unsigned
            push    OFFSET $SG2765 ; ’a<b’
            call    _printf
            add     esp, 4
    [email protected]_unsigned:
            pop     ebp
            ret     0
    _f_unsigned     ENDP
    

    幾乎是相同的,不同的是:JBE-小于等于跳轉和JAE-大于等于跳轉。這些指令(JA/JAE/JBE/JBE)不同于JG/JGE/JL/JLE,它們使用無符號值。

    我們也可以看到有符號值的表示(35)。因此我們看JG/JL代替JA/JBE的用法或者相反,我們幾乎可以確定變量的有符號或者無符號類型。

    main()函數沒有什么新的內容:

    Listing 10.3: main()

    #!bash
    _main   PROC
            push    ebp
            mov     ebp, esp
            push    2
            push    1
            call    _f_signed
            add     esp, 8
            push    2
            push    1
            call    _f_unsigned
            add     esp, 8
            xor     eax, eax
            pop     ebp
            ret     0
    _main   ENDP
    

    10.1.2 x86 + MSVC + OllyDbg

    我們在OD里允許例子來查看標志寄存器。我們從f_unsigned()函數開始。CMP執行了三次,每次的參數都相同,所以標志位也相同。

    第一次比較的結果:fig. 10.1.標志位:C=1, P=1, A=1, Z=0, S=1, T=0, D=0, O=0.標志位名稱為OD對其的簡稱。

    當CF=1 or ZF=1時JBE將被觸發,此時將跳轉。

    接下來的條件跳轉:fig. 10.2.當ZF=0(zero flag)時JNZ則被觸發

    第三個條件跳轉:fig. 10.3.我們可以發現14當CF=0 (carry flag)時,JNB將被觸發。在該例中條件不為真,所以第三個printf()將被執行。

    enter image description here

    Figure 10.1: OllyDbg: f_unsigned(): 第一個條件跳轉

    enter image description here

    Figure 10.2: OllyDbg: f_unsigned(): 第二個條件跳轉

    enter image description here

    Figure 10.3: OllyDbg: f_unsigned(): 第三個條件跳轉

    現在我們在OD中看f_signed()函數使用有符號值。

    可以看到標志寄存器:C=1, P=1, A=1, Z=0, S=1, T=0, D=0, O=0.

    第一種條件跳轉JLE將被觸發fig. 10.4.我們可以發現14,當ZF=1 or SF≠OF。該例中SF≠OF,所以跳轉將被觸發。

    下一個條件跳轉將被觸發:如果ZF=0 (zero flag): fig. 10.5.

    第三個條件跳轉將不會被觸發,因為僅有SF=OF,該例中不為真: fig. 10.6.

    enter image description here

    Figure 10.4: OllyDbg: f_signed(): 第一個條件跳轉

    enter image description here

    Figure 10.5: OllyDbg: f_signed(): 第二個條件跳轉

    enter image description here

    Figure 10.6: OllyDbg: f_signed(): 第三個條件跳轉

    10.1.3 x86 + MSVC + Hiew

    我們可以修改這個可執行文件,使其無論輸入的什么值f_unsigned()函數都會打印“a==b”。

    在Hiew中查看:fig. 10.7.

    我們要完成以下3個任務:

    1. 使第一個跳轉一直被觸發;
    2. 使第二個跳轉從不被觸發;
    3. 使第三個跳轉一直被觸發。
    

    我們需要使代碼流進入第二個printf(),這樣才一直打印“a==b”。

    三個指令(或字節)應該被修改:

    1. 第一個跳轉修改為JMP,但跳轉偏移值不變。
    2. 第二個跳轉有時可能被觸發,我們修改跳轉偏移值為0后,無論何種情況,程序總是跳向下一條指令。跳轉地址等于跳轉偏移值加上下一條指令地址,當跳轉偏移值為0時,跳轉地址就為下一條指令地址,所以無論如何下一條指令總被執行。
    3. 第三個跳轉我們也修改為JMP,這樣跳轉總被觸發。
    

    修改后:fig. 10.8.

    如果忘了這些跳轉,printf()可能會被多次調用,這種行為可能是我們不需要的。

    enter image description here

    Figure 10.7: Hiew: f_unsigned() 函數

    enter image description here

    Figure 10.8: Hiew:我們修改 f_unsigned() 函數

    10.1.4 Non-optimizing GCC

    GCC 4.4.1非優化狀態產生的代碼幾乎一樣,只是用puts() (2.3.3) 替代 printf()。

    10.1.5 Optimizing GCC

    細心的讀者可能會問,為什么要多次執行CMP,如果標志寄存器每次都相同呢?可能MSVC不會做這樣的優化,但是GCC 4.8.1可以做這樣的深度優化:

    Listing 10.4: GCC 4.8.1 f_signed()

    #!bash
    f_signed:
            mov     eax, DWORD PTR [esp+8]
            cmp     DWORD PTR [esp+4], eax
            jg      .L6
            je      .L7
            jge     .L1
            mov     DWORD PTR [esp+4], OFFSET FLAT:.LC2 ; "a<b"
            jmp     puts
    .L6:
            mov     DWORD PTR [esp+4], OFFSET FLAT:.LC0 ; "a>b"
            jmp     puts
    .L1:
            rep     ret
    .L7:
            mov     DWORD PTR [esp+4], OFFSET FLAT:.LC1 ; "a==b"
            jmp     puts
    

    我們可以看到JMP puts替代了CALL puts/RETN。稍后我們介紹這種情況11.1.1.。

    不用說,這種類型的x86代碼是很少見的。MSVC2012似乎不會這樣做。其他情況下,匯編程序能意識到此類使用。如果你在其它地方看到此類代碼,更可能是手工構造的。

    f_unsigned()函數代碼:

    Listing 10.5: GCC 4.8.1 f_unsigned()

    #!bash
    f_unsigned:
            push    esi
            push    ebx
            sub     esp, 20
            mov     esi, DWORD PTR [esp+32]
            mov     ebx, DWORD PTR [esp+36]
            cmp     esi, ebx
            ja      .L13
            cmp     esi, ebx ; instruction may be removed
            je      .L14
    .L10:
            jb      .L15
            add     esp, 20
            pop     ebx
            pop     esi
            ret
    .L15:
            mov     DWORD PTR [esp+32], OFFSET FLAT:.LC2 ; "a<b"
            add     esp, 20
            pop     ebx
            pop     esi
            jmp     puts
    .L13:
            mov     DWORD PTR [esp], OFFSET FLAT:.LC0 ; "a>b"
            call    puts
            cmp     esi, ebx
            jne     .L10
    .L14:
            mov     DWORD PTR [esp+32], OFFSET FLAT:.LC1 ; "a==b"
            add     esp, 20
            pop     ebx
            pop     esi
            jmp     puts
    

    因此,GCC 4.8.1的優化算法并不總是完美的。

    10.2 ARM

    10.2.1 Keil + ARM mode優化后

    Listing 10.6: Optimizing Keil + ARM mode

    #!bash
    .text:000000B8                          EXPORT f_signed
    .text:000000B8              f_signed                ; CODE XREF: main+C
    .text:000000B8 70 40 2D E9              STMFD   SP!, {R4-R6,LR}
    .text:000000BC 01 40 A0 E1              MOV     R4, R1
    .text:000000C0 04 00 50 E1              CMP     R0, R4
    .text:000000C4 00 50 A0 E1              MOV     R5, R0
    .text:000000C8 1A 0E 8F C2              ADRGT   R0, aAB ; "a>b
    "
    .text:000000CC A1 18 00 CB              BLGT    __2printf
    .text:000000D0 04 00 55 E1              CMP     R5, R4
    .text:000000D4 67 0F 8F 02              ADREQ   R0, aAB_0 ; "a==b
    "
    .text:000000D8 9E 18 00 0B              BLEQ    __2printf
    .text:000000DC 04 00 55 E1              CMP     R5, R4
    .text:000000E0 70 80 BD A8              LDMGEFD SP!, {R4-R6,PC}
    .text:000000E4 70 40 BD E8              LDMFD   SP!, {R4-R6,LR}
    .text:000000E8 19 0E 8F E2              ADR     R0, aAB_1 ; "a<b
    "
    .text:000000EC 99 18 00 EA              B       __2printf
    .text:000000EC              ; End of function f_signed
    

    ARM下很多指令只有某些標志位被設置時才會被執行。比如做數值比較時。

    舉個例子,ADD實施上是ADDAL,這里的AL是Always,即總被執行。判定謂詞是32位ARM指令的高4位(條件域)。無條件跳轉的B指令其實是有條件的,就行其它任何條件跳轉一樣,只是條件域為AL,這意味著總是被執行,忽略標志位。

    ADRGT指令就像和ADR一樣,只是該指令前面為CMP指令,并且只有前面數值大于另一個數值時(Greater Than)時才被執行。

    接下來的BLGT行為和BL一樣,只有比較結果符合條件才能出發(Greater Than)。ADRGT把字符串“a>b ”的地址寫入R0,然后BLGT調用printf()。因此,這些指令都帶有GT后綴,只有當R0(a值)大于R4(b值)時指令才會被執行。

    然后我們看ADREQ和BLEQ,這些指令動作和ADR及BL一樣,只有當兩個操作數對比后相等時才會被執行。這些指令前面是CMP(因為printf()調用可能會修改狀態標識)。 然后我們看LDMGEFD,該指令行為和LDMFD指令一樣1,僅僅當第一個值大于等于另一個值時(Greater Than),指令才會被執行。

    “LDMGEFD SP!, {R4-R6,PC}”恢復寄存器并返回,只是當a>=b時才被觸發,這樣之后函數才執行完成。但是如果a<b,觸發條件不成立是將執行下一條指令LDMFD SP!, {R4-R6,LR},該指令保存R4-R6寄存器,使用LR而不是PC,函數并不返回。最后兩條指令是執行printf()(5.3.2)。

    f_unsigned與此一樣只是使用對應的指令為ADRHI, BLHI及LDMCSFD,判斷謂詞(HI = Unsigned higher, CS = Carry Set (greater than or equal))請類比之前的說明,另外就是函數內部使用無符號數值。

    我們來看一下main()函數:

    Listing 10.7: main()

    #!bash
    .text:00000128                              EXPORT main
    .text:00000128          main
    .text:00000128 10 40 2D E9                  STMFD SP!, {R4,LR}
    .text:0000012C 02 10 A0 E3                  MOV R1, #2
    .text:00000130 01 00 A0 E3                  MOV R0, #1
    .text:00000134 DF FF FF EB                  BL f_signed
    .text:00000138 02 10 A0 E3                  MOV R1, #2
    .text:0000013C 01 00 A0 E3                  MOV R0, #1
    .text:00000140 EA FF FF EB                  BL f_unsigned
    .text:00000144 00 00 A0 E3                  MOV R0, #0
    .text:00000148 10 80 BD E8                  LDMFD SP!, {R4,PC}
    .text:00000148          ; End of function main
    

    這就是ARM模式如何避免使用條件跳轉。

    這樣做有什么好處呢?因為ARM使用精簡指令集(RISC)。簡言之,處理器流水線技術受到跳轉的影響,這也是分支預測重要的原因。程序使用的條件或者無條件跳轉越少越好,使用斷言指令可以減少條件跳轉的使用次數。

    x86沒有這也的功能,通過使用CMP設置相應的標志位來觸發指令。

    10.2.2 Optimizing Keil + thumb mode

    Listing 10.8: Optimizing Keil + thumb mode

    #!bash
    .text:00000072      f_signed                        ; CODE XREF: main+6
    .text:00000072 70 B5                PUSH    {R4-R6,LR}
    .text:00000074 0C 00                MOVS    R4, R1
    .text:00000076 05 00                MOVS    R5, R0
    .text:00000078 A0 42                CMP     R0, R4
    .text:0000007A 02 DD                BLE     loc_82
    .text:0000007C A4 A0                ADR     R0, aAB         ; "a>b
    "
    .text:0000007E 06 F0 B7 F8          BL      __2printf
    .text:00000082
    .text:00000082      loc_82                      ; CODE XREF: f_signed+8
    .text:00000082 A5 42                CMP     R5, R4
    .text:00000084 02 D1                BNE     loc_8C
    .text:00000086 A4 A0                ADR     R0, aAB_0   ; "a==b
    "
    .text:00000088 06 F0 B2 F8          BL      __2printf
    .text:0000008C
    .text:0000008C      loc_8C                      ; CODE XREF: f_signed+12
    .text:0000008C A5 42                CMP     R5, R4
    .text:0000008E 02 DA                BGE     locret_96
    .text:00000090 A3 A0                ADR     R0, aAB_1   ; "a<b
    "
    .text:00000092 06 F0 AD F8          BL      __2printf
    .text:00000096
    .text:00000096      locret_96                   ; CODE XREF: f_signed+1C
    .text:00000096 70 BD                POP     {R4-R6,PC}
    .text:00000096      ; End of function f_signed
    

    僅僅Thumb模式下的B指令可能需要條件代碼輔助,所以thumb代碼看起來更普通一些。

    BLE通常是條件跳轉小于或等于(Less than or Equal),BNE—不等于(Not Equal),BGE—大于或等于(Greater than or Equal)。

    f_unsigned函數是同樣的,只是使用的指令用來處理無符號數值:BLS (Unsigned lower or same) 和BCS (Carry Set (Greater than or equal)).

    97av