逆向基礎(三)

wyzsk發表於2020-08-19
作者: reverse-engineering · 2014/05/15 17:23

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)).

本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章