<pre id="vvttv"><mark id="vvttv"><progress id="vvttv"></progress></mark></pre>
    <pre id="vvttv"></pre>

      <p id="vvttv"></p>

          <p id="vvttv"></p>

                <p id="vvttv"></p>

                <pre id="vvttv"><cite id="vvttv"><progress id="vvttv"></progress></cite></pre>

                  <output id="vvttv"><dfn id="vvttv"><th id="vvttv"></th></dfn></output>

                    <p id="vvttv"></p>

                    原文地址: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)).

                      <pre id="vvttv"><mark id="vvttv"><progress id="vvttv"></progress></mark></pre>
                      <pre id="vvttv"></pre>

                        <p id="vvttv"></p>

                            <p id="vvttv"></p>

                                  <p id="vvttv"></p>

                                  <pre id="vvttv"><cite id="vvttv"><progress id="vvttv"></progress></cite></pre>

                                    <output id="vvttv"><dfn id="vvttv"><th id="vvttv"></th></dfn></output>

                                      <p id="vvttv"></p>

                                      这里只有精品视频