我們先看一段代碼:
int _tmain(int argc, _TCHAR* argv[])
{
int nNum = 2;
switch (nNum)
{
case 0:
printf("nNum=0");
break;
case 1:
printf("nNum=1");
break;
case 2:
printf("nNum=2");
break;
default:
printf("nNum=%d,error!",nNum);
}
return 0;
}
看完這段代碼,在看完本小節的標題,有些讀者可能會產生一些疑問,例如switch-case的不可達分支會被剪掉嗎、switch-case分支以常量爲判斷條件的優化效果與if-else有多大區別、以變量爲判斷條件的switch-case分支優化效果與if-else分支有多大區別等等問題。
現在就讓我們帶着這些問題繼續,讓我們一一爲其找到答案。
先看Debug版的反彙編代碼:
004133AE MOV DWORD PTR SS:[EBP-8], 2 ; 給局部變量賦值
004133B5 MOV EAX, DWORD PTR SS:[EBP-8]
004133B8 MOV DWORD PTR SS:[EBP-D0], EAX
004133BE CMP DWORD PTR SS:[EBP-D0], 0 ; 比較是否等於0
004133C5 JE SHORT Test_0.004133DB ; 如果等於0則跳到相應分支,否則繼續
004133C7 CMP DWORD PTR SS:[EBP-D0], 1
004133CE JE SHORT Test_0.004133F4 ; 同上
004133D0 CMP DWORD PTR SS:[EBP-D0], 2
004133D7 JE SHORT Test_0.0041340D ; 同上
004133D9 JMP SHORT Test_0.00413426 ; 都不符合則直接跳轉到最後一個分支處
004133DD PUSH Test_0.00415808 ; /format = "nNum=0"
004133E2 CALL DWORD PTR DS:[<&MSVCR90D.printf>] ; /printf
004133E8 ADD ESP, 4
004133F2 JMP SHORT Test_0.00413441
004133F6 PUSH Test_0.004157B0 ; /format = "nNum=1"
004133FB CALL DWORD PTR DS:[<&MSVCR90D.printf>] ; /printf
00413401 ADD ESP, 4
0041340B JMP SHORT Test_0.00413441
0041340F PUSH Test_0.00415C18 ; /format = "nNum=2"
00413414 CALL DWORD PTR DS:[<&MSVCR90D.printf>] ; /printf
0041341A ADD ESP, 4
00413424 JMP SHORT Test_0.00413441
00413428 MOV EAX, DWORD PTR SS:[EBP-8]
0041342B PUSH EAX ; /<%d>
0041342C PUSH Test_0.004157A0 ; |format = "nNum=%d,error!"
00413431 CALL DWORD PTR DS:[<&MSVCR90D.printf>] ; /printf
00413437 ADD ESP, 8
通過以上反彙編代碼我們可以總結出以下特點:
cmp XXX,XXX
jXX CASE1_TAG
cmp XXX,XXX
jXX CASE2_TAG
cmp XXX,XXX
jXX CASE3_TAG
......
CMP XXX,XXX
JXX CASEN_TAG
......
JMP DEFAULT
CASE1_TAG:
......
CASE2_TAG:
......
CASE3_TAG:
......
......
CASEN_TAG:
......
......
DEFAULT:
......
SWITCH_END_TAG:
我們可以看到Debug版的反彙編指令與我們的源代碼的相似度還是非常高的,都是通過開始的一連串判斷,然後確定接下來走哪個Case分支。下面我們再看看Release版的反彙編代碼:
00401000 PUSH Test_0.004020F4 ; /format = "nNum=2"
00401005 CALL DWORD PTR DS:[<&MSVCR90.printf>] ; /printf
0040100B ADD ESP, 4
0040100E XOR EAX, EAX
00401010 RETN
瞧瞧,簡單至極呀!由此可見switch-case語句與我們之前接觸的if-else語句一樣,不可達分支都會被編譯器優化掉,那麼如果如果其部分分支相同,是否仍會像if-else分之一樣呢?先看源碼:
int _tmain(int argc, _TCHAR* argv[])
{
switch (argc)
{
case 0:
printf("argc=0",argc);
break;
case 1:
printf("argc=%d",argc);
break;
case 2:
printf("argc=%d",argc);
break;
default:
printf("argc=%d,error!",argc);
}
return 0;
}
按照if-esle的優化邏輯,case 1 與 csae 2 會指向同一處,真的是這樣嗎?我們直接看Release版反彙編代碼:
00401000 /$>MOV ECX, DWORD PTR SS:[ESP+4]
00401004 |.>MOV EAX, ECX
00401006 |.>SUB EAX, 0 ; Switch (cases 0..2)
00401009 |.>JE SHORT Test_0.0040104D
0040100B |.>SUB EAX, 1 ; 注意這裏用的是減法
0040100E |.>JE SHORT Test_0.0040103A
00401010 |.>SUB EAX, 1
00401013 |.>JE SHORT Test_0.00401027
00401015 |.>PUSH ECX ; /<%d>; Default case of switch 00401006
00401016 |.>PUSH Test_0.00402104 ; |format = "argc=%d,error!"
0040101B |.>CALL DWORD PTR DS:[<&MSVCR90.printf>] ; /printf
00401021 |.>ADD ESP, 8
00401024 |.>XOR EAX, EAX
00401026 |.>RETN ; 執行完某一個分支後會直接返回
00401027 |>>PUSH 2 ; /<%d> = 2; Case 2 of switch 00401006
00401029 |.>PUSH Test_0.004020FC ; |format = "argc=%d"
0040102E |.>CALL DWORD PTR DS:[<&MSVCR90.printf>] ; /printf
00401034 |.>ADD ESP, 8
00401037 |.>XOR EAX, EAX
00401039 |.>RETN
0040103A |>>PUSH 1 ; /<%d> = 1; Case 1 of switch 00401006
0040103C |.>PUSH Test_0.004020FC ; |format = "argc=%d"
00401041 |.>CALL DWORD PTR DS:[<&MSVCR90.printf>] ; /printf
00401047 |.>ADD ESP, 8
0040104A |.>XOR EAX, EAX
0040104C |.>RETN
0040104D |>>PUSH 0 ; Case 0 of switch 00401006
0040104F |.>PUSH Test_0.004020F4 ; /format = "argc=0"
00401054 |.>CALL DWORD PTR DS:[<&MSVCR90.printf>] ; /printf
0040105A |.>ADD ESP, 8
0040105D |.>XOR EAX, EAX
0040105F /.>RETN
看來switch-case並沒有將相同的分支合併,我們可以很清楚的看到它的4個分支仍都存在。
既然它的分支並沒有合併,那麼我們就討論點其他的,請各位讀者回頭仔細觀察反彙編代碼的第1-9行,我們可以發現Release在條件跳轉前用的不再是cmp,而是sub,很顯然編譯器這樣優化是有其理由的,但是這個理由究竟是什麼?
我們通過閱讀這塊代碼可知程序先將main函數的參數1傳遞給EAX,然後減0,這有點讓人迷糊,我們接着看下面的那個跳轉:
00401009 |.>JE SHORT Test_0.0040104D
讓我們回顧一下彙編語言,我們應該都記得JE的跳轉條件是ZF=1,因此當我們的EAX爲0時,那麼將其減0肯定會使ZF位置1,因此其實這就是一個變形的CMP指令,只不過這麼做程程的代碼體積更小、效率更高。
知道這些後,後面的優化自然就肯好理解了,現在假設我們的EAX等於2,因此按照上面代碼的流程走會先將其減0,此時ZF位不變,接着下面又對其減1,此時ZF位仍然沒變化,而當走到第三步時,此時EAX的值爲1,又將其減1後肯定就等於0了,ZF位置爲1,後面的JZ跳轉生效……
我們可以看到其實就是做了一連串的減法,到哪等於0後,就證明這個值原先爲多少,由此可見微軟的編譯器還是很聰明的。不過這些代碼從本質上來說還是屬於if-esle範疇內的。
1.5.2、多分支的switch-case識別
我們平時在寫程序時都會遇到一些問題,而這些問題肯定必須要用多分支的switch-case才能解決,但是你知道這種情況在反彙編狀態下應該怎麼去識別嗎?下面我們就一起看看switch-case的另外一種體現方式,我們先看代碼:
int _tmain(int argc, _TCHAR* argv[])
{
switch (argc)
{
case 0:
printf("argc=0",argc);
break;
case 1:
printf("argc=%d",argc);
break;
case 6:
printf("argc=%d",argc);
break;
case 7:
printf("argc=%d",argc);
break;
case 9:
printf("argc=%d",argc);
break;
default:
printf("argc=%d,error!",argc);
}
return 0;
}
注意上面的case條件並不是有規律的,我們直接看它的Release版反彙編代碼:
00401000 MOV EAX, DWORD PTR SS:[ESP+4]
00401004 CMP EAX, 9 ; Switch (cases 0..9)
00401007 JA SHORT Test_0.0040106F ; 如果大於最大值9則直接跳到Default處
00401009 JMP DWORD PTR DS:[EAX*4+401084] ; 注意這裏!!
00401010 PUSH 0 ; Case 0 of switch 00401004
00401012 PUSH Test_0.004020F4 ; /format = "argc=0"
00401017 CALL DWORD PTR DS:[<&MSVCR90.printf>] ; /printf
0040101D ADD ESP, 8
00401020 XOR EAX, EAX
00401022 RETN
00401023 PUSH 1 ; /<%d> = 1; Case 1 of switch 00401004
00401025 PUSH Test_0.004020FC ; |format = "argc=%d"
0040102A CALL DWORD PTR DS:[<&MSVCR90.printf>] ; /printf
00401030 ADD ESP, 8
00401033 XOR EAX, EAX
00401035 RETN
00401036 PUSH 6 ; /<%d> = 6; Case 6 of switch 00401004
00401038 PUSH Test_0.004020FC ; |format = "argc=%d"
0040103D CALL DWORD PTR DS:[<&MSVCR90.printf>] ; /printf
00401043 ADD ESP, 8
00401046 XOR EAX, EAX
00401048 RETN
00401049 PUSH 7 ; /<%d> = 7; Case 7 of switch 00401004
0040104B PUSH Test_0.004020FC ; |format = "argc=%d"
00401050 CALL DWORD PTR DS:[<&MSVCR90.printf>] ; /printf
00401056 ADD ESP, 8
00401059 XOR EAX, EAX
0040105B RETN
0040105C PUSH 9 ; /<%d> = 9; Case 9 of switch 00401004
0040105E PUSH Test_0.004020FC ; |format = "argc=%d"
00401063 CALL DWORD PTR DS:[<&MSVCR90.printf>] ; /printf
00401069 ADD ESP, 8
0040106C XOR EAX, EAX
0040106E RETN
0040106F PUSH EAX ; /<%d>; Default case of switch 00401004
00401070 PUSH Test_0.00402104 ; |format = "argc=%d,error!"
00401075 CALL DWORD PTR DS:[<&MSVCR90.printf>] ; /printf
0040107B ADD ESP, 8
0040107E XOR EAX, EAX
00401080 RETN
看完上段反彙編代碼後有些讀者可能感覺很奇怪,難道那個位於第4行的JMP指令就能解決這些問題嗎?當然不會這麼簡單……
我們現在就仔細分析一下頭幾條反彙編指令:
00401000 MOV EAX, DWORD PTR SS:[ESP+4] ; 取得局部變量後傳遞給EAX
00401004 CMP EAX, 9 ; 與9作比較,我們通過源代碼可知這個switch-case分支的最大值“case 9”,因
; 此如果傳入的值大於9就肯定會執行Default處代碼了。
00401007 JA SHORT Test_0.0040106F
00401009 JMP DWORD PTR DS:[EAX*4+401084] ; EAX此時保存的是輸入的值,將其乘以4後再加上一個地址,這其實就是一個典型
; 的數組尋址,由於我們還沒學數組尋址,所以這塊先放一放。我們現在只需要要
; 知道這是一個讀取int型的數組,裏面保存的是地址指針。
那麼目標地址裏究竟保存了什麼數據?這個機制的原理又是怎麼回事呢?我們看看如下內容便可以猜出一二了……
Address Data Tag
00401084 00401010 Test_0.00401010 ; Case0
00401088 00401023 Test_0.00401023 ; Case1
0040108C 0040106F Test_0.0040106F ; Case2
00401090 0040106F Test_0.0040106F ; Case3
00401094 0040106F Test_0.0040106F ; Case4
00401098 0040106F Test_0.0040106F ; Case5
0040109C 00401036 Test_0.00401036 ; Case6
004010A0 00401049 Test_0.00401049 ; Case7
004010A4 0040106F Test_0.0040106F ; Case8
004010A8 0040105C Test_0.0040105C ; Case9
通過上面表格的地址可以知道這就是我們上面提到的“數組指針”了,裏面保存的內容是各個Case塊的地址,我們將之稱爲“跳轉表”。跳轉表的作用就是通過數組的尋址運算代替複雜的if-else分支,這樣可以大大提供程序的執行效率。
它的基本原理是建立一張表格,裏面保存着從case1到caseN的所有分支應該到達的地址。以上面程序的情況爲例子,我們可以看出從case2至case5裏保存的地址都是跳向Default分支的地址,這就證明這幾個case在程序的源代碼中是屬於未處理(或稱爲非正常)的狀態。
但是什麼時候編譯器纔會決定用跳轉表的方式來優化程序呢?這顯然不是我等簡單的用個什麼公式就能計算出來的,這需要綜合各種條件、因素來決定是否使用跳轉表。在這裏我可以給出一個反例,既如果我們的最大case塊爲999999的話,難道程序還會用這個方法解決嗎?肯定不會!
代碼逆向(四)——switch-case識別技巧初探
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.