逆向-分支結構下

上一篇說完IF結構,這一篇就接着總結分支結構中的switch,對於switch結構,編譯器共有四種方案提供選擇,我們不必去關心編譯器何時會選擇何種結構,因爲這個完全取決於編譯器作者,所以我們只需能識別出這四種方案是switch結構即可。

對於switch結構而言,不管哪種方案,其主要分兩部分

引導代碼
case塊集合

對於不同的方案,其case塊集合是不變的,主要在於其引導方式的變化。

首先,來看方案一,這是最基礎的情況,在vs系列中,其觸發的條件是case在三個以內(由編譯器決定)。

int main(int argc, char* argv[])
{
    switch(argc)
    {
        case 1:
            printf("case 1");
            break;
        case 2:
            printf("case 2");
            break;
        default:
            printf("default"); //注意此時沒有break,表示default執行完繼續執行
        case 3:
            printf("case 3");
            break;
    }
    return 0;
}

首先,先來看一下上面的代碼,其實對於switch而言,其與if結構最大的一個不同是當case塊中沒有break時,需要順序執行,那麼這意味着switch的結構並不會同if結構一樣。

對應的彙編代碼

9:        switch(argc)
10:       {
//引導部分
0040D718 8B 45 08             mov         eax,dword ptr [ebp+8]
0040D71B 89 45 FC             mov         dword ptr [ebp-4],eax
0040D71E 83 7D FC 01          cmp         dword ptr [ebp-4],1
0040D722 74 0E                je          main+32h (0040d732)
0040D724 83 7D FC 02          cmp         dword ptr [ebp-4],2
0040D728 74 17                je          main+41h (0040d741)
0040D72A 83 7D FC 03          cmp         dword ptr [ebp-4],3
0040D72E 74 2D                je          main+5Dh (0040d75d)
0040D730 EB 1E                jmp         main+50h (0040d750)  //上面都沒比較成功則跳轉到default
//case塊集合部分
11:           case 1:
12:               printf("case 1");
0040D732 68 24 20 42 00       push        offset string "case 1" (00422024)
0040D737 E8 34 39 FF FF       call        printf (00401070)
0040D73C 83 C4 04             add         esp,4
13:               break;
0040D73F EB 29                jmp         main+6Ah (0040d76a)  //break 執行完跳轉到switch結束
14:           case 2:
15:               printf("case 2");
0040D741 68 BC 2F 42 00       push        offset string "argc == 0" (00422fbc)
0040D746 E8 25 39 FF FF       call        printf (00401070)
0040D74B 83 C4 04             add         esp,4
16:               break;
0040D74E EB 1A                jmp         main+6Ah (0040d76a)
17:           default:
18:               printf("default"); //注意此時沒有break,表示default執行完繼續執行
0040D750 68 2C 20 42 00       push        offset string "argc == 1" (0042202c)
0040D755 E8 16 39 FF FF       call        printf (00401070)
0040D75A 83 C4 04             add         esp,4  //此處沒有jmp,說明default執行完會順序執行下面case 3
19:           case 3:
20:               printf("case 3");
0040D75D 68 1C 20 42 00       push        offset string "else else" (0042201c)
0040D762 E8 09 39 FF FF       call        printf (00401070)
0040D767 83 C4 04             add         esp,4
21:               break;
22:       }
23:       return 0;
0040D76A 33 C0                xor         eax,eax

在上面的彙編代碼中,引導部分主要就是用於比較case值,當匹配到對應的值時,跳轉到對應的case塊中,因爲case塊中有break存在,此時就會產生jmp的語句(執行完跳轉到switch結束),如果沒有jmp,說明該case塊沒有break,此時會接着順序執行,也就是因爲這個特點,需要把所有的case塊都集中在一起,否則無法做到沒有無break時順序執行。

對於switch的邊界問題其實還是比較好識別的,因爲這個特點比較明顯,主要的是如何識別switch結束位置,這裏switch結束的位置可以在case塊中找,因爲如果有break,那麼其jmp的位置一般就是switch結束位置了。

在有些代碼中,可能沒有default,那麼上面在引導部分都不符合條件時,就會直接jmp到switch結束的位置。

明白了switch的機制後,也就可以明白這種方案,其實在時間上來說,與if語句其實並沒有顯著的優點,畢竟這裏再引導部分也是用if一個個進行比較的。

 

下面再來看方案二,方案二使用的是地址表引導的方案。對於之前的引導部分,其可以細分爲三個部分,並不是簡簡單單的使用jxx進行比較。

引導部分
	表達式求值
	範圍檢查
	查表並轉移

下面來觀察下面這段的反彙編進行觀察

int main(int argc, char* argv[])
{
    switch(argc)
    {
    case 19:
        printf("case 10");
        break;
    case 12:
        printf("case 1");
        break;
    default:
        printf("default");
        break;
    case 13:
        printf("case 3");
        break;
    case 17:
        printf("case 3");
        break;
    case 20:
        printf("case 3");
        break;
    case 16:
        printf("case 3");
        break;
    }
    return 0;
}

對應的反彙編代碼

9:        switch(argc)
10:       {
//表達式求值部分
0040D718 8B 45 08             mov         eax,dword ptr [ebp+8]
0040D71B 89 45 FC             mov         dword ptr [ebp-4],eax
0040D71E 8B 4D FC             mov         ecx,dword ptr [ebp-4]
0040D721 83 E9 0C             sub         ecx,0Ch
0040D724 89 4D FC             mov         dword ptr [ebp-4],ecx
//範圍檢查
0040D727 83 7D FC 08          cmp         dword ptr [ebp-4],8
0040D72B 77 28                ja          $L858+0Fh (0040d755)
//查表並轉移
0040D72D 8B 55 FC             mov         edx,dword ptr [ebp-4]
0040D730 FF 24 95 B1 D7 40 00 jmp         dword ptr [edx*4+40D7B1h]
11:       case 19:
12:           printf("case 10");
0040D737 68 24 20 42 00       push        offset string "case 10" (00422024)
0040D73C E8 2F 39 FF FF       call        printf (00401070)
0040D741 83 C4 04             add         esp,4
13:           break;
0040D744 EB 58                jmp         $L866+0Dh (0040d79e)
14:       case 12:
15:           printf("case 1");
0040D746 68 BC 2F 42 00       push        offset string "case 2" (00422fbc)
0040D74B E8 20 39 FF FF       call        printf (00401070)
0040D750 83 C4 04             add         esp,4
16:           break;
0040D753 EB 49                jmp         $L866+0Dh (0040d79e)
17:       default:
18:           printf("default");
0040D755 68 2C 20 42 00       push        offset string "argc == 1" (0042202c)
0040D75A E8 11 39 FF FF       call        printf (00401070)
0040D75F 83 C4 04             add         esp,4
19:           break;
0040D762 EB 3A                jmp         $L866+0Dh (0040d79e)
//.....此處省略部分代碼
29:       case 16:
30:           printf("case 3");
0040D791 68 1C 20 42 00       push        offset string "else else" (0042201c)
0040D796 E8 D5 38 FF FF       call        printf (00401070)
0040D79B 83 C4 04             add         esp,4
31:           break;
32:       }
33:       return 0;
0040D79E 33 C0                xor         eax,eax

歸根結底這個機制其實就是查表機制,表中其實存着對應的case的地址,那麼通過其索引就能跳轉到對應的位置,下面我們對引導的三個部分細細來討論一下

表達式求值,對於這部分而言,我們可以知道該case的最小值。拿上面那段程序來說,我們的case的最小值爲12,那麼對於數組而言,其下標的最小值肯定是0,那麼對於數組的0~11項是不是就浪費了呢,所以這裏使用了座標平移的辦法,也就是說最小值case 12對應其下標0。

好了,明白了上面的對應關係後,如何得出case的最小值

0040D721 83 E9 0C             sub         ecx,0Ch

在這行彙編代碼中,我們發現其case值減去了一個值,其實這個值就是最小值,也就是0xC(十進制12)。

下面再來看範圍檢查這一部分,對於範圍檢查,因爲可知其case的最大值,因爲對於範圍檢查而言肯定是對地址越界檢查,那麼將地址表的最大項加上我們上面得出的case最小值就是case最大值。

0040D727 83 7D FC 08          cmp         dword ptr [ebp-4],8
0040D72B 77 28                ja          $L858+0Fh (0040d755)
//這裏使用的是無符號比較,所以當其值爲負數也會不符合條件(很大的正數)而跳轉

通過這裏的彙編代碼,我們可以得知其case的最大值爲12 + 8 = 20。

好了,最後就是到了查表並跳轉的部分了。這裏的話我們主要解決一個問題,那就是當case值不存在時,數組表中填什麼呢?如case 14就不存在,那麼對於上圖中的下標爲三填寫什麼呢,這裏我們需要看一下內存,補充一下上表即可得出結論

    

通過實踐可以得出其地址表中空缺的地址會填寫default的地址,如果沒有default呢?那麼其實就會填寫switch end處的地址,這樣子的就通過查表就可以滿足當case值不存在時執行default了。

 

OK,下面該來說方案三了,對於方案三而言,其實算是對於上面的優化,對於引導部分,其多了一個步驟

引導部分
    表達式求值
    界限檢查
    查索引部分-此步驟相比方案二是多出來的
    查表並轉移

下面先來說說方案二的問題,通過上面的分析,我們可以知道在地址表中,對於不存在的case項會填寫default的地址,那麼假設在一組序列中,其不存在的case很多這麼辦?如下面的代碼

int main(int argc, char* argv[])
{
	switch(argc)
    {
        case 23:
            printf("hello1");
            break;
        case 89:
            printf("hello2");
            break;
        case 47:
            printf("hello3");
            break;
        case 72:
            printf("hello2");
            break;
        case 22:
            printf("hello3");
            break;
        case 56:
            printf("hello4");
            break;
    }
    return 0;
}

我們來簡單計算一下空間成本

數組項數* 4 = (89-23 + 1)*4 = 67 * 4 =  268

有什麼辦法可以節省一下空間麼,那麼此時需要多一張索引表即可解決問題,其索引表每一項佔一字節,用於記錄地址表的索引位置,相當於之前的直接查地址表,現在需要先查找索引表得到索引,然後在根據索引表查地址,算是一個以時間換空間的方案吧。

 大概的思想就和左邊的表一樣,這裏左邊的地址表與上面代碼中的不符,因爲正常情況,一般是case值有多少項,其地址表就會有多少項,注意default地址也會在裏面(只有一份)。這樣子很明顯因爲索引值只佔一字節,所以其總大小肯定比上面小的。

下面再來看一下反彙編代碼

9:        switch(argc)
10:       {
//表達式求值
0040D718 8B 45 08             mov         eax,dword ptr [ebp+8]
0040D71B 89 45 FC             mov         dword ptr [ebp-4],eax
0040D71E 8B 4D FC             mov         ecx,dword ptr [ebp-4]
0040D721 83 E9 16             sub         ecx,16h  //這裏可以得出case最小值爲22
//界限檢查
0040D724 89 4D FC             mov         dword ptr [ebp-4],ecx
0040D727 83 7D FC 43          cmp         dword ptr [ebp-4],43h //case最大值 22 + 0x43=89
0040D72B 77 6A                ja          $L864+0Dh (0040d797)
//查索引部分
0040D72D 8B 45 FC             mov         eax,dword ptr [ebp-4]
0040D730 33 D2                xor         edx,edx
0040D732 8A 90 C6 D7 40 00    mov         dl,byte ptr  (0040d7c6)[eax]  //在索引表取值,注意這裏是一字節
//查表並轉移
0040D738 FF 24 95 AA D7 40 00 jmp         dword ptr [edx*4+40D7AAh] //根據索引表值在到地址表中獲取地址跳轉
11:           case 23:
12:               printf("hello1");
0040D73F 68 24 20 42 00       push        offset string "case 10" (00422024)
0040D744 E8 27 39 FF FF       call        printf (00401070)
0040D749 83 C4 04             add         esp,4
13:               break;
0040D74C EB 49                jmp         $L864+0Dh (0040d797)
14:           case 89:
15:               printf("hello2");
0040D74E 68 BC 2F 42 00       push        offset string "argc == 0" (00422fbc)
0040D753 E8 18 39 FF FF       call        printf (00401070)
0040D758 83 C4 04             add         esp,4
16:               break;
0040D75B EB 3A                jmp         $L864+0Dh (0040d797)
//.....這裏省略部分代碼
26:           case 56:
27:               printf("hello4");
0040D78A 68 1C 20 42 00       push        offset string "case 3" (0042201c)
0040D78F E8 DC 38 FF FF       call        printf (00401070)
0040D794 83 C4 04             add         esp,4
28:               break;
29:       }
30:       return 0;
0040D797 33 C0                xor         eax,eax

其原理就是中間使用了一字節的索引表進行間接查詢地址,好了上面說了索引表的每一項都只佔一字節,然而對於索引表還有一個就是最多隻能存儲256項,因此索引表只能存儲256項索引編號,那麼問題來了,當case最小值和case最大值的間隔大於255時,那麼索引表就無法進行索引了。

 

所以,針對上面的問題,方案四就來了,方案四使用的是二叉平衡樹加混合的方案。至於如何混,下面我們先來看一下例子

int main(int argc, char* argv[])
{
	switch (argc)
	{
        case 3:
            printf("hello 3");
            break;
        case 58:
            printf("hello 7");
            break;
        case 153:
            printf("hello 3");
            break;
        case 250:
            printf("hello 5");
            break;
        case 256:
            printf("hello 3");
            break;
        case 300:
            printf("hello 5");
            break;
        case 317:
            printf("hello 3");
            break;
        default:
            printf("default");
            break;
	}
	return 0;
}

在這個例子中,很明顯其case最小和最大的差值已經255,所以此時使用平衡樹的方案,下面我們對上面的數值來構建一顆平衡樹

對於二叉平衡樹而言,其查找速度也會快很多了,其查找規則就是如果比根節點的值小,那麼往左邊找,大的話就往右邊找。我們下面來看一下反彙編代碼是否是這樣呢?

0040D718 8B 45 08             mov         eax,dword ptr [ebp+8]
0040D71B 89 45 FC             mov         dword ptr [ebp-4],eax
0040D71E 81 7D FC FA 00 00 00 cmp         dword ptr [ebp-4],0FAh //250
0040D725 7F 23                jg          main+4Ah (0040d74a) //大於走右邊
0040D727 81 7D FC FA 00 00 00 cmp         dword ptr [ebp-4],0FAh
0040D72E 74 64                je          main+94h (0040d794) //根節點值
//下面是左子樹部分
0040D730 83 7D FC 03          cmp         dword ptr [ebp-4],3     //3
0040D734 74 31                je          main+67h (0040d767)
0040D736 83 7D FC 3A          cmp         dword ptr [ebp-4],3Ah  //58
0040D73A 74 3A                je          main+76h (0040d776)
0040D73C 81 7D FC 99 00 00 00 cmp         dword ptr [ebp-4],99h  //158
0040D743 74 40                je          main+85h (0040d785)
0040D745 E9 86 00 00 00       jmp         main+0D0h (0040d7d0)  //都不符合到default
//下面是右子樹部分
0040D74A 81 7D FC 00 01 00 00 cmp         dword ptr [ebp-4],100h   //256
0040D751 74 50                je          main+0A3h (0040d7a3)
0040D753 81 7D FC 2C 01 00 00 cmp         dword ptr [ebp-4],12Ch   //300
0040D75A 74 56                je          main+0B2h (0040d7b2)
0040D75C 81 7D FC 3D 01 00 00 cmp         dword ptr [ebp-4],13Dh  //317
0040D763 74 5C                je          main+0C1h (0040d7c1)
0040D765 EB 69                jmp         main+0D0h (0040d7d0)  //都不符合到default

看完上面的彙編代碼,其實可以發現第一次是對了,查找的是根節點,大於則跳轉到右子樹,等於說明找到了,剩下的就左子樹情況,對於左右子樹而言,可以發現其是順序查找的,按照我們上面的平衡二叉樹,應該也是要先查找中值呀?

其實對於方案四來說,先是使用平衡二叉樹的方案,使用完二叉樹方案,可以發現其左右子樹的節點並未超過三個,符合方案一(由編譯器決定),所以左右子樹使用的是方案一。

  此時如果再增加一個多個值,那麼其可能又會由方案一轉變爲方案或者方案三。

至於如何選擇方案就是編譯器做的事,我們只需知道不管case情況如何複雜,其最終肯定在這四種方案間混合。對於方案四而言,在還原時候不用去關心jg或者gl,因爲對於二叉樹查找而言,要麼比較大於,然後比較等於,剩下的情況肯定就是小於了,所以在比較時我們只需把重心放在je或者jne上,因爲此時cmp的值可能就是對應的case值。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章