“嘿!爲什麼要先學這裏,不應該是for循環嗎?”
相信很多讀者都會產生以上疑問,要的就是這種效果!就讓我們帶着這個疑問開始這一節的學習,先看源碼:
int _tmain(int argc, _TCHAR* argv[])
{
int nNum = 26;
printf("Mom! I can sing my ABC!/r/n");
// 聽!小Baby開始唱ABC了……
do
{
printf("%c ",0x41+(26-nNum) );
nNum--;
} while (nNum>0);
return 0;
}
現在讓我們從反彙編的角度看看小Baby是怎麼唱歌的:
004117DE MOV DWORD PTR SS:[EBP-8], 1A ; 16進制的0x1A等於10進制的26,不要在這裏犯暈
004117E7 PUSH Test_0.00415C18 ; /format = "Mom! I can sing my ABC!
004117EC CALL DWORD PTR DS:[<&MSVCR90D.printf>] ; /printf
004117F2 ADD ESP, 4
004117FC /MOV EAX, 1A ; <--!!!!!
00411801 |SUB EAX, DWORD PTR SS:[EBP-8]
00411804 |ADD EAX, 41 ; EAX = 41+(1A-[EBP-8]) = 0x41+(26-nNum)
00411809 |PUSH EAX ; /<%c>
0041180A |PUSH Test_0.0041573C ; |format = "%c "
0041180F |CALL DWORD PTR DS:[<&MSVCR90D.printf>] ; /printf
00411815 |ADD ESP, 8
0041181F |MOV EAX, DWORD PTR SS:[EBP-8]
00411822 |SUB EAX, 1
00411825 |MOV DWORD PTR SS:[EBP-8], EAX ; [EBP-8]-- = nNum--
00411828 |CMP DWORD PTR SS:[EBP-8], 0 ; 看[EBP-8]是否仍大於0,是的話則跳轉到標記處繼續
0041182C /JG SHORT Test_0.004117FC
是不是感覺很容易理解?這似乎與我們前面所講的內容差距不大,那麼就讓我們在看看Release版的:
00401000 PUSH ESI
00401001 PUSH EDI
00401002 MOV EDI, DWORD PTR DS:[<&MSVCR90.printf>>; MSVCR90.printf
00401008 PUSH Test_0.004020F4 ; /format = "Mom! I can sing my ABC!
0040100D CALL EDI ; /printf
0040100F ADD ESP, 4
00401012 MOV ESI, 41 ; 將ESI加0x41後準備
00401017 /PUSH ESI ; <--!!!!!
00401018 |PUSH Test_0.00402110 ; ASCII "%c "
0040101D |CALL EDI ; 又見此優化
0040101F |INC ESI ; 直接將ESI加1
00401020 |ADD ESP, 8 ; 爲什麼要將平衡堆棧操作放到這裏?
| ; 這與CPU的流水線有關係,我們目前先不深究。
| ;
00401023 |CMP ESI, 5B ; 快看看,直接與0x5B相比較了(Z的ASCII碼是0x5A)
00401026 /JL SHORT Test_0.00401017 ; 如果小於此值則繼續
00401028 POP EDI
00401029 XOR EAX, EAX
0040102B POP ESI
0040102C RETN
通過以上代碼我們不難看出,編譯器直接將我們的代碼優化爲以下模式了:
int _tmain(int argc, _TCHAR* argv[])
{
int nNum = 0x41;
printf("Mom! I can sing my ABC!/r/n");
// 聽!小Baby開始唱ABC了……
do
{
printf("%c ",nNum++ );
} while (nNum<0x5B);
return 0;
}
看看,多麼聰明的編譯器!直接看透了我們代碼的本質!在感嘆之餘,不要忘記總結反彙編代碼的特點,我們現在可以看到的最大的特點就是一個有條件判斷的向上跳轉,因此可以這樣理解“如果我們看到了一個判斷分支的跳轉是向上的,那麼這必然就是一個循環”。
特點總結:
DO_TAG:
......
......
CMP XXX,XXX
JXX DO_TAG
1.4.2、while循環的識別技巧
先看源碼:
int _tmain(int argc, _TCHAR* argv[])
{
int nNum = 26;
printf("Mom! I can sing my ABC!/r/n");
// 聽!小Baby開始唱ABC了……
while(nNum>0)
{
printf("%c ",0x41+(26-nNum) );
nNum--;
}
return 0;
}
再看Debug版反彙編代碼:
004117DE MOV DWORD PTR SS:[EBP-8], 1A
004117E7 PUSH Test_0.00415C18 ; /format = "Mom! I can sing my ABC!
004117EC CALL DWORD PTR DS:[<&MSVCR90D.printf>] ; /printf
004117F2 ADD ESP, 4
004117FC /CMP DWORD PTR SS:[EBP-8], 0
00411800 |JLE SHORT Test_0.00411830 ; 多了個判斷,如果其小於等於0則跳出循環
00411802 |MOV EAX, 1A
00411807 |SUB EAX, DWORD PTR SS:[EBP-8]
0041180A |ADD EAX, 41
0041180F |PUSH EAX ; /<%c>
00411810 |PUSH Test_0.0041573C ; |format = "%c "
00411815 |CALL DWORD PTR DS:[<&MSVCR90D.printf>] ; /printf
0041181B |ADD ESP, 8
00411825 |MOV EAX, DWORD PTR SS:[EBP-8]
00411828 |SUB EAX, 1
0041182B |MOV DWORD PTR SS:[EBP-8], EAX
0041182E /JMP SHORT Test_0.004117FC
細心的讀者可能發現了,這與do-while循環幾乎如出一轍,僅僅在循環頭部多了兩條用於判斷是否跳出循環的指令,那Release版的又會是怎樣的呢?
00401000 PUSH ESI
00401001 PUSH EDI
00401002 MOV EDI, DWORD PTR DS:[<&MSVCR90.printf>>; MSVCR90.printf
00401008 PUSH Test_0.004020F4 ; /format = "Mom! I can sing my ABC!
0040100D CALL EDI ; /printf
0040100F ADD ESP, 4
00401012 MOV ESI, 41
00401017 /PUSH ESI
00401018 |PUSH Test_0.00402110 ; ASCII "%c "
0040101D |CALL EDI
0040101F |INC ESI
00401020 |ADD ESP, 8
00401023 |CMP ESI, 5B
00401026 /JL SHORT Test_0.00401017
00401028 POP EDI
00401029 XOR EAX, EAX
0040102B POP ESI
0040102C RETN
請不要懷疑我複製錯了代碼,事實就是這樣!do-while與while生成的Release版在這裏看就是100%完全相同的。
編譯器很明顯的已經探測出了我們的循環判斷用的是一個常量,因此就不存在首次執行條件不匹配的情況。既然如此,它爲什麼還要在循環前面加上那個判斷分支來浪費我們的空間與時間呢?
當然,如果我們將它的判斷條件改爲一個變量,那麼就是另外一番景象了:
00401000 PUSH EBX
00401001 MOV EBX, DWORD PTR DS:[<&MSVCR90.printf>>; MSVCR90.printf
00401007 PUSH EDI
00401008 PUSH Test_0.004020F4 ; /format = "Mom! I can sing my ABC!
0040100D CALL EBX ; /printf
0040100F MOV EDI, DWORD PTR SS:[ESP+10] ; 取得參數(其實就是main函數的argc)
00401013 ADD ESP, 4
00401016 TEST EDI, EDI ; 測試參數是否爲0
00401018 JLE SHORT Test_0.00401034 ; 如果爲0則跳出循環
0040101A PUSH ESI
0040101B MOV ESI, 5B ; 0x5B = 0x41+26
00401020 SUB ESI, EDI ; 用0x5B減去參數
00401022 /PUSH ESI
00401023 |PUSH Test_0.00402110 ; ASCII "%c "
00401028 |CALL EBX
0040102A |DEC EDI ; 參數減1
0040102B |ADD ESP, 8
0040102E |INC ESI ; ESI加1
0040102F |TEST EDI, EDI
00401031 /JG SHORT Test_0.00401022 ; 如果參數大於ESI則結束循環
00401033 POP ESI
00401034 POP EDI
00401035 XOR EAX, EAX
00401037 POP EBX
00401038 RETN
我們可以看出用變量做判斷條件很明顯與常量不一樣,而關於優化,很顯然他只是單純的將我們的“0x41+(26-argc)”優化成“0x5B-argc”。
特點總結:
WHILE_TAG:
CMP XXX,XXX
JXX WHILE_END_TAG
......
......
CMP XXX,XXX
JXX WHILE_TAG
WHILE_END_TAG:
1.4.3、for循環的識別技巧
for循環與while循環本質上都是一樣的,唯一的不同在於for循環在循環體內多了一個步長部分,接下來我們一起看看for循環的樣子,先看源碼:
int _tmain(int argc, _TCHAR* argv[])
{
printf("Mom! I can sing my ABC!/r/n");
// 聽!小Baby開始唱ABC了……
for (int nNum = 26;nNum>0;nNum--)
{
printf("%c ",0x41+(26-nNum) );
}
return 0;
}
接下來我們再看看Debug版的反彙編代碼:
004117E0 PUSH Test_0.00415C18 ; /format = "Mom! I can sing my ABC!
004117E5 CALL DWORD PTR DS:[<&MSVCR90D.printf>] ; /printf
004117EB ADD ESP, 4
004117F5 MOV DWORD PTR SS:[EBP-8], 1A
004117FC JMP SHORT Test_0.00411807
004117FE /MOV EAX, DWORD PTR SS:[EBP-8] ; / 步長控制部分開始
00411801 |SUB EAX, 1 ; | 步長爲1
00411804 |MOV DWORD PTR SS:[EBP-8], EAX ; / 將操作後的結果傳回給局部變量,步長操作結束
00411807 |CMP DWORD PTR SS:[EBP-8], 0
0041180B |JLE SHORT Test_0.00411832 ; 如果此變量小於等於0則結束循環
0041180D |MOV EAX, 1A
00411812 |SUB EAX, DWORD PTR SS:[EBP-8]
00411815 |ADD EAX, 41 ; 0x41+(0x1A-[EBP-8])
0041181A |PUSH EAX ; /<%c>
0041181B |PUSH Test_0.0041573C ; |format = "%c "
00411820 |CALL DWORD PTR DS:[<&MSVCR90D.printf>] ; /printf
00411826 |ADD ESP, 8
00411830 /JMP SHORT Test_0.004117FE ; 跳到循環頭部
看到這裏不知道各位讀者們是否發現了什麼,記得我當時學到這裏時,直覺上認識到了以下兩點:
(1、)顯然循環語句是do-while先誕生的,而後是while,最後纔是for,這從側面上講for應該是最“高級”的了。
(2、)從執行效率上看,代碼最短且判斷最少的就是do-while循環了。
Release版反彙編:
00401000 PUSH ESI
00401001 PUSH EDI
00401002 MOV EDI, DWORD PTR DS:[<&MSVCR90.printf>>; MSVCR90.printf
00401008 PUSH Test_0.004020F4 ; /format = "Mom! I can sing my ABC!
0040100D CALL EDI ; /printf
0040100F ADD ESP, 4
00401012 MOV ESI, 41
00401017 /PUSH ESI
00401018 |PUSH Test_0.00402110 ; ASCII "%c "
0040101D |CALL EDI
0040101F |INC ESI
00401020 |ADD ESP, 8
00401023 |CMP ESI, 5B
00401026 /JL SHORT Test_0.00401017
00401028 POP EDI
00401029 XOR EAX, EAX
0040102B POP ESI
0040102C RETN
又是常量惹的禍,這段代碼與do-while、while一模一樣,有疑問的讀者可以返回上面仔細觀察一下,連地址都是一樣的。
現在正好印證了我開篇時講的一句話“其本質上只有一種”,很明顯的,我們的while與for都是以do-while爲基礎框架的,只不過是在裏面加了一些小判斷。爲了讓各位讀者更清晰的看到它們之間的異同,我再爲各位獻上一個變量版的:
00401000 PUSH EBX
00401001 MOV EBX, DWORD PTR DS:[<&MSVCR90.printf>>; MSVCR90.printf
00401007 PUSH EDI
00401008 PUSH Test_0.004020F4 ; /format = "Mom! I can sing my ABC!
0040100D CALL EBX ; /printf
0040100F MOV EDI, DWORD PTR SS:[ESP+10]
00401013 ADD ESP, 4
00401016 TEST EDI, EDI
00401018 JLE SHORT Test_0.00401034
0040101A PUSH ESI
0040101B MOV ESI, 5B
00401020 SUB ESI, EDI
00401022 /PUSH ESI
00401023 |PUSH Test_0.00402110 ; ASCII "%c "
00401028 |CALL EBX
0040102A |DEC EDI
0040102B |ADD ESP, 8
0040102E |INC ESI
0040102F |TEST EDI, EDI
00401031 /JG SHORT Test_0.00401022
00401033 POP ESI
00401034 POP EDI
00401035 XOR EAX, EAX
00401037 POP EBX
00401038 RETN
再重申一下,筆者並沒有搞錯,以變量爲判斷條件的for循環與while循環所生成的代碼是完全相同的,連地址都一樣……
特點總結:
FOR_START_TAG:
初始化塊
JMP CMP_TAG
STEP_TAG:
步長塊
CMP_TAG:
反條件判斷
JXX FOR_END_TAG
……
……
JXX STEP_TAG
到此,我們應該可以做一個總結了,Debug版下三種循環各不相同,Release版下可總結如下:
(1、)當循環採用常量爲判斷條件時,相同邏輯的三種循環生成的代碼完全相同。
(2、)當循環採用變量爲判斷條件時,相同邏輯的while與for生成的代碼完全相同,而do-while則自成一格。
小插曲:循環體的語句外提優化
我們看看下面這段代碼:
int _tmain(int argc, _TCHAR* argv[])
{
printf("Mom! I can sing my ABC!/r/n");
// 聽!小Baby開始唱ABC了……
for (int nNum = 24;nNum>0;nNum--)
{
argc = (int)argv;
printf("%c ",0x41+(26-nNum) );
}
printf("%p",argc);
return 0;
}
通過這段代碼我們可以發現一處可以優化的地方,就是“argc = (int)argv”這條語句,很明顯
00401000 PUSH ESI
00401001 PUSH EDI
00401002 MOV EDI, DWORD PTR DS:[<&MSVCR90.printf>>; MSVCR90.printf
00401008 PUSH Test_0.004020F4 ; /format = "Mom! I can sing my ABC!
0040100D CALL EDI ; /printf
0040100F ADD ESP, 4
00401012 MOV ESI, 43
00401017 /PUSH ESI
00401018 |PUSH Test_0.00402110 ; ASCII "%c "
0040101D |CALL EDI
0040101F |INC ESI
00401020 |ADD ESP, 8
00401023 |CMP ESI, 5B
00401026 /JL SHORT Test_0.00401017
00401028 MOV EAX, DWORD PTR SS:[ESP+10] ; EAX = (int)argv; <--注意這裏
0040102C PUSH EAX
0040102D PUSH Test_0.00402114 ; ASCII "%p"
00401032 CALL EDI
00401034 ADD ESP, 8
00401037 POP EDI
00401038 XOR EAX, EAX
0040103A POP ESI
0040103B RETN
由上面代碼可知,循環體內很明顯沒有我們“argc = (int)argv;”的代碼,再向下看一行才知道,這段代碼被提到了外面,這就是編譯進行的代碼外提優化。
本小節到此就結束了,筆者在本小節向大家詳細的介紹了三種循環結構在逆向時的一些需要注意的特點,當你怎能快速的花柱這些特點之後,剩下的就是反覆的練習了。
代碼逆向(三)——循環分支的識別技巧
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.