<span id="7ztzv"></span>
<sub id="7ztzv"></sub>

<span id="7ztzv"></span><form id="7ztzv"></form>

<span id="7ztzv"></span>

        <address id="7ztzv"></address>

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

            <span id="7ztzv"></span>
            <sub id="7ztzv"></sub>

            <span id="7ztzv"></span><form id="7ztzv"></form>

            <span id="7ztzv"></span>

                  <address id="7ztzv"></address>

                      亚洲欧美在线