逆向-循環結構

對於循環結構而言,主要就三種,do-while循環,while循環,for循環,這三種循環,在debug下其特點還是比較明顯的,在release下的話可以說基本上都被優化爲do-while循環(效率高),所以說release下的循環,根據其彙編代碼我們只能做等價的還原。

下面先來看do-while循環

int main(int argc, char* argv[])
{
    int i = 0;
    int sum = 0;
    do 
    {
        sum += i;
        ++i;
    } while (i < argc);
    printf("%d\r\n",sum);
    return 0;
}

對應的彙編代碼

9:        int i = 0;
0040D718 C7 45 FC 00 00 00 00 mov         dword ptr [ebp-4],0
10:       int sum = 0;
0040D71F C7 45 F8 00 00 00 00 mov         dword ptr [ebp-8],0
11:       do
12:       {
13:           sum += i;
0040D726 8B 45 F8             mov         eax,dword ptr [ebp-8]
0040D729 03 45 FC             add         eax,dword ptr [ebp-4]
0040D72C 89 45 F8             mov         dword ptr [ebp-8],eax
14:           ++i;
0040D72F 8B 4D FC             mov         ecx,dword ptr [ebp-4]
0040D732 83 C1 01             add         ecx,1
0040D735 89 4D FC             mov         dword ptr [ebp-4],ecx
15:       } while (i < argc);
0040D738 8B 55 FC             mov         edx,dword ptr [ebp-4]
0040D73B 3B 55 08             cmp         edx,dword ptr [ebp+8]
0040D73E 7C E6                jl          main+26h (0040d726)  小於則跳轉

可以發現,do-while循環的彙編代碼和高級代碼的邏輯真的是一模一樣,對於其跳轉的邏輯也是一樣的(if相反),因爲對於循環而言,條件滿足則進行循環,這裏和彙編的邏輯也是一樣的,條件滿足進行跳轉。

對於Do-While循環的大體架構如下:

DO_BEGIN:
	//.... 中間循環體
jxx DO_BEGIN  //這裏是一個減量地址
DO_END:

這裏do-while循環的release版本與debug結構是類似的,所以這裏就不分析了。

下面再來看while循環

int main(int argc, char* argv[])
{
	int i = 0;
    int sum = 0;
    while (i < argc)
    {
        sum += i;
        ++i;
    } 
    printf("%d\r\n",sum);
    return 0;
}

對應的反彙編代碼

9:        int i = 0;
0040D718 C7 45 FC 00 00 00 00 mov         dword ptr [ebp-4],0
10:       int sum = 0;
0040D71F C7 45 F8 00 00 00 00 mov         dword ptr [ebp-8],0
11:       while (i < argc)
0040D726 8B 45 FC             mov         eax,dword ptr [ebp-4]
0040D729 3B 45 08             cmp         eax,dword ptr [ebp+8]
0040D72C 7D 14                jge         main+42h (0040d742)  //先進行比較,反條件
12:       {
13:           sum += i;
0040D72E 8B 4D F8             mov         ecx,dword ptr [ebp-8] //循環體部分
0040D731 03 4D FC             add         ecx,dword ptr [ebp-4]
0040D734 89 4D F8             mov         dword ptr [ebp-8],ecx
14:           ++i;
0040D737 8B 55 FC             mov         edx,dword ptr [ebp-4]
0040D73A 83 C2 01             add         edx,1
0040D73D 89 55 FC             mov         dword ptr [ebp-4],edx
15:       }
0040D740 EB E4                jmp         main+26h (0040d726) //往上跳到比較部分

這裏的話主要就是while部分的比較是和邏輯是相反的,爲什麼只要想想if語句即可,和其同理。

對於while循環的主體架構

WHILE_BEGIN:
    jxx WHILE_END
    //...循環體
    jmp WHILE_BEGIN  //減量跳是while,增量跳說明是if else
WHILE_END:

下面看一下while循環的release版本

.text:00401008                 test    edx, edx
.text:0040100A                 jle     short loc_401013 //先進行if比較
.text:0040100C//下面就是do-while循環結構
.text:0040100C loc_40100C:                             ; CODE XREF: _main+11↓j
.text:0040100C                 add     ecx, eax
.text:0040100E                 inc     eax
.text:0040100F                 cmp     eax, edx
.text:00401011                 jl      short loc_40100C
.text:00401013
.text:00401013 loc_401013:                             ; CODE XREF: _main+A↑j

可以發現,對於while循環,其release先使用了if語句判斷,然後裏面套了一個do-while循環,高級對應代碼如下

    if(argc > 0) //i已經初始化爲0,只要argc大於0,第一次肯定就能滿足i < argc
    {
        do 
        {
            sum += i;
            ++i;
        } while (i < argc);
    }

 

最後再來看一下for循環

int main(int argc, char* argv[])
{
    int sum = 0;
    for(int i=0;i < argc;++i)
        sum += i;
    printf("%d\r\n",sum);
    return 0;
}

對應的彙編代碼

10:       for(int i=0;i < argc;++i)
0040D71F C7 45 F8 00 00 00 00 mov         dword ptr [ebp-8],0
0040D726 EB 09                jmp         main+31h (0040d731) //初始化後先跳轉到比較部分
//步長部分,也就是 i++
0040D728 8B 45 F8             mov         eax,dword ptr [ebp-8]
0040D72B 83 C0 01             add         eax,1
0040D72E 89 45 F8             mov         dword ptr [ebp-8],eax
//這裏是比較部分
0040D731 8B 4D F8             mov         ecx,dword ptr [ebp-8]
0040D734 3B 4D 08             cmp         ecx,dword ptr [ebp+8]
0040D737 7D 0B                jge         main+44h (0040d744)
11:           sum += i;
//循環的主體
0040D739 8B 55 FC             mov         edx,dword ptr [ebp-4]
0040D73C 03 55 F8             add         edx,dword ptr [ebp-8]
0040D73F 89 55 FC             mov         dword ptr [ebp-4],edx
0040D742 EB E4                jmp         main+28h (0040d728)  //往上跳轉到步長部分
12:       printf("%d\r\n",sum);
0040D744 8B 45 FC             mov         eax,dword ptr [ebp-4]

可以發現,debug版下的代碼和for的邏輯基本一致,這裏需要注意的是在第一次初始化完畢後,需要先使用一個jmp跳過步長部分,進行循環比較,因爲在我們的for邏輯裏面第一次是不進行i++的,而後面每次循環主體完畢後在進行i++,在進行比較,所以步長部分設計在比較部分的上面是合理的,只是第一次比較特殊而已。

for循環的主體架構

FOR_INTI:
    //... 初始化
    jmp FOR_CMP
FOR_STEP:
    //...步長部分
FOR_CMP:
    //..比較部分
    jxx FOR_END
    //...循環體
    //...
    jmp FOR_STEP
FOR_END

對於for循環的release版本,這裏就不分析了,因爲其實for循環和while循環兩者的執行邏輯上而言幾乎是一樣的,所以其release版本的優化會和上面while版本一致。

對於循環語句而言,這裏需要解釋的並沒有太多,因爲基本上分析的都是一些很基礎的情況,對於正向對循環邏輯結構很熟悉的同學,因爲其彙編代碼的循環邏輯是一致的,所以很容易就看懂。但是在真實情況下,很多時候可能並不是那麼的好還原,這還是需要經驗的積累。

下面再來看一下在release下會遇到的優化手段

首先來看第一種,代碼外提的優化,這種優化屬於自身代碼寫的並不是那麼高效造成的,看下面的例子。

int main(int argc, char* argv[])
{
    int i = 0;
    int sum = 0;
    while (i < argc * 22)  // 理論上argc * 22會在每次循環的時候需要乘
    {
        sum += i;
        ++i;
    } 
    printf("%d\r\n",sum);
    return 0;
}

下面我們來看一下release版的彙編代碼

.text:00401009                 lea     esi, [eax+eax*4] //esi = argc * 5
.text:0040100C                 lea     eax, [eax+esi*2] //eax = argc * 11
.text:0040100F                 pop     esi
.text:00401010                 shl     eax, 1 //相當於*2, eax = argc * 22
//下面爲優化後的循環
.text:00401012                 test    eax, eax
.text:00401014                 jle     short loc_40101D
.text:00401016
.text:00401016 loc_401016:                             ; CODE XREF: _main+1B↓j
.text:00401016                 add     edx, ecx
.text:00401018                 inc     ecx
.text:00401019                 cmp     ecx, eax  //eax一直都是上面計算出來的定值
.text:0040101B                 jl      short loc_401016
.text:0040101D
.text:0040101D loc_40101D:                             ; CODE XREF: _main+14↑j

可以發現,如果這裏的結果是一個定值,那麼就根本不需要每次循環的時候都去計算,可以外提到外面,上面的彙編代碼還原

    int eax = argc * 22; //代碼外提,先計算結果
    if(argc > 0)
    {
        do 
        {
            sum += i;
            ++i;
        } while (i < eax);
    }

好了,再來擴展一下,如果此時不是一個基本運算,而是一個函數會是如何呢?

    while (i < strlen("hello world"))

這裏的話,對於函數而言,編譯器是不會外提優化的,因爲編譯器並不能保證一個函數的參數是常量但其返回值肯定都是一樣,比如說使用了下面這個函數

rand()

如果使用了隨機種子相關的函數,那麼返回值就不確定了,所以編譯器對函數情況不會做外提優化。

OK,下面再來看一種,強度削弱優化

int main(int argc, char* argv[])
{
    int sum = 0;
    int n = 0;
    scanf("%d",&n);
    while(argc <= 100)
    {
        sum = argc * n;
        argc++;
    }
    printf("%d\r\n",sum);
    return 0;
}

來看一下對應的反彙編代碼

.text:0040101F                 cmp     edi, 100
.text:00401022                 jg      short loc_40103B
.text:00401024                 mov     edx, [esp+0Ch+var_4]
.text:00401028                 mov     ecx, 101
.text:0040102D                 mov     eax, edx
.text:0040102F                 imul    eax, edi //注意這裏的乘法是在循環外
.text:00401032                 sub     ecx, edi
.text:00401034
.text:00401034 loc_401034:                             ; CODE XREF: _main+39↓j
.text:00401034                 mov     esi, eax
.text:00401036                 add     eax, edx
.text:00401038                 dec     ecx
.text:00401039                 jnz     short loc_401034
.text:0040103B
.text:0040103B loc_40103B:
.text:0040103B                 push    esi //esi爲結果

對於上面的彙編代碼中,我們先不管其內容如何優化,後面在分析,我們至少可以發現在源代碼循環中我們使用了乘法,而此時彙編代碼中卻沒有看見乘法(循環中),所以強度削弱,也就是使用一些低週期指令替換高週期指令,所以上面的是使用了加法替代了乘法。爲什麼可以替換呢,其主要原因看下面

sum = argc * n; //這裏是直接=,說明這個循環最後結果就是最後一個循環的argc*n

注意上面的代碼中sum的值是=,而不是+=,如果是=,那麼其結果可以說是100*n,雖然看似直接輸出即可,當時編譯器並沒有直接省去循環。對於循環而言,其需要循環(100-argc)次,那麼假設每次都加n,那麼最終結果爲(100-argc)*n,對於其結果是不是還差argc*n,所以這部分就是上面彙編代碼中出現的imul指令,先計算出argc*n,然後每次加上n,那麼sum的最終結果肯定是100*n。

對應的上面的等價高級代碼還原

int main(int argc, char* argv[])
{
	int n = 0;
    int sum = 0;
    scanf("%d",&n);
    //也就是隻要滿足argc加足100*n即可
    int esi;
    if(argc >= 100)
    {
        sum = argc * n;
        //使用101去減主要是當argc等於100時,如果使用100去減那麼下面就不能使用do-while結構,而是需要使用while
        int ecx = 101 - argc; 
        do
        {
            esi = sum; //這裏使用了esi記錄前一次的結果,因爲上面使用101去減,所以理論上會多一次
            sum = sum + n; //最後多+的一次n相當於會廢棄,因爲結果輸出的是esi
            ecx--;
        }while(ecx != 0);
    }
    printf("%d\r\n",esi); //最後只要輸出前一次的結果即可
    return 0;
}

 

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