switch語句逆向分析
有序
小於3時
- 程式碼:
#include "stdafx.h"
void MySwitch(int x){
switch(x) {
case 1:
printf("num is 1\n");
break;
case 2:
printf("num is 2\n");
break;
case 3:
printf("num is 3\n");
break;
default:
printf("no cases match\n");
break;
}
}
int main(int argc, char* argv[])
{
MySwitch(2);
return 0;
}
- 反彙編:
10: switch(x) {
0040D7A8 mov eax,dword ptr [ebp+8]
0040D7AB mov dword ptr [ebp-4],eax
0040D7AE cmp dword ptr [ebp-4],1
0040D7B2 je MySwitch+32h (0040d7c2)
0040D7B4 cmp dword ptr [ebp-4],2
0040D7B8 je MySwitch+41h (0040d7d1)
0040D7BA cmp dword ptr [ebp-4],3
0040D7BE je MySwitch+50h (0040d7e0)
0040D7C0 jmp MySwitch+5Fh (0040d7ef)
11: case 1:
12: printf("num is 1\n");
0040D7C2 push offset string "num is 1\n" (00422fc4)
0040D7C7 call printf (00401060)
0040D7CC add esp,4
13: break;
0040D7CF jmp MySwitch+6Ch (0040d7fc)
14: case 2:
15: printf("num is 2\n");
0040D7D1 push offset string "num is 2\n" (00422fb8)
0040D7D6 call printf (00401060)
0040D7DB add esp,4
16: break;
0040D7DE jmp MySwitch+6Ch (0040d7fc)
17: case 3:
18: printf("num is 3\n");
0040D7E0 push offset string "num is 3\n" (00422fac)
0040D7E5 call printf (00401060)
0040D7EA add esp,4
19: break;
0040D7ED jmp MySwitch+6Ch (0040d7fc)
20: default:
21: printf("no cases match\n");
0040D7EF push offset string "Hello World!\n" (0042201c)
0040D7F4 call printf (00401060)
0040D7F9 add esp,4
22: break;
23: }
24: }
- 反彙編分析:
1.反彙編程式碼為將引數x的值賦給eax
2.將eax的值放入堆疊中
3.將前面放入堆疊中的eax拿出來和第1個case中的條件進行比較(也就是比較引數x和case)
4.判斷是否要跳轉,je:jump equal,前面比較的兩個數相同則跳轉,跳轉的地址為case 1對應的地址
5.如果沒有跳轉則繼續將引數和第2個case中的條件進行比較
6.依舊是根據比較的結果判斷是否要跳轉,跳轉的地址為case 2對應的地址
7.如果沒有跳轉則繼續將引數和第3個case中的條件進行比較
8.依舊是根據比較的結果判斷是否要跳轉,跳轉的地址為case 3對應的地址
9.如果沒有跳轉則絕對跳轉到default:
下面的內容就是 case 1,case 2,case 3了
可以注意到,case裡面的break都對應為跳出switch,而default裡的break因為下面就已經是退出switch所以沒有生成對應的彙編程式碼
case1裡的break
case2裡的break
case3裡的break
透過上面的分析,發現此時(switch 中的case數量≤3時)的反彙編程式碼和if else並無本質上的區別,都是要依次比較判斷條件
大於三時:(同理簡化)
ja指令:jump above,大於時跳轉(無符號),也就是比較引數x-1和3(case中的最大差值),最大差值就是最大值減最小值,此案例中就是4-1=3
如果x-1>3則跳轉,如果前面引數沒有減1的話,就變成了直接判斷x>3,如果此時x=4也會產生跳轉,不符合程式的邏輯(原本x=4應該對應跳轉到case 4)
注意到這裡採用的是無符號比較,而不採用有符號比較指令jg:jump greater,大於時跳轉(有符號),為什麼?
這裡的比較程式碼其實就是判斷引數是否在(case中的最小值,case中的最大值)這個區間內
當引數小於case中的最小值時,前面的sub ecx,case中的最小值就後就會產生下溢,此時將其看作無符號數就會相當大,一定會大於case中的最大差值,舉個簡單的例子,假如此時的引數為0,0-1 = -1對應的是十六進位制為FFFF FFFF,將其看作無符號數就是4294967295
0040D7BB ja $L539+0Fh (0040d803)
跳轉的地址為:0040d803,對應為default的地址
23: default:
24: printf("no cases match\n");
0040D803 push offset string "Hello World!\n" (0042201c)
0040D808 call printf (00401060)
0040D80D add esp,4
25: break;
如果前面沒有跳轉,這裡則又將前面儲存的引數-1的值取了出來,並賦值給edx
0040D7BD mov edx,dword ptr [ebp-4]
過上面的分析,發現此時(switch 中的case數量>3時)的反彙編程式碼和if else的差別就體現出來了,但到達一定條件後一般都會採用到case地址表首地址+偏移的方法,此時是將引數的值減case中的最小值,然後判斷這個減完的數值是否大於case中的最大差值,如果大於則直接跳轉到default,如果小於或等於則透過jmp [儲存case地址表的首地址+偏移×4]的方式直接跳轉到對應case的地址,而不再像if else中那樣依次比較來判斷是否要跳轉
無序
- 程式碼
switch(x) {
case 2:
printf("num is 2\n");
break;
case 3:
printf("num is 3\n");
break;
case 4:
printf("num is 4\n");
break;
case 1:
printf("num is 1\n");
break;
default:
printf("no cases match\n");
break;
}
簡單地調換了一下case語句的順序,觀察其反彙編
- 反彙編程式碼
10: switch(x) {
0040D7A8 mov eax,dword ptr [ebp+8]
0040D7AB mov dword ptr [ebp-4],eax
0040D7AE mov ecx,dword ptr [ebp-4]
0040D7B1 sub ecx,1
0040D7B4 mov dword ptr [ebp-4],ecx
0040D7B7 cmp dword ptr [ebp-4],3
0040D7BB ja $L539+0Fh (0040d803)
0040D7BD mov edx,dword ptr [ebp-4]
0040D7C0 jmp dword ptr [edx*4+40D821h]
11: case 2:
12: printf("num is 2\n");
0040D7C7 push offset string "num is 1\n" (00422fd0)
0040D7CC call printf (00401060)
0040D7D1 add esp,4
13: break;
0040D7D4 jmp $L539+1Ch (0040d810)
14: case 3:
15: printf("num is 3\n");
0040D7D6 push offset string "num is 2\n" (00422fc4)
0040D7DB call printf (00401060)
0040D7E0 add esp,4
16: break;
0040D7E3 jmp $L539+1Ch (0040d810)
17: case 4:
18: printf("num is 4\n");
0040D7E5 push offset string "num is 3\n" (00422fb8)
0040D7EA call printf (00401060)
0040D7EF add esp,4
19: break;
0040D7F2 jmp $L539+1Ch (0040d810)
20: case 1:
21: printf("num is 1\n");
0040D7F4 push offset string "num is 271\n" (00422fac)
0040D7F9 call printf (00401060)
0040D7FE add esp,4
22: break;
0040D801 jmp $L539+1Ch (0040d810)
23: default:
24: printf("no cases match\n");
0040D803 push offset string "no cases match\n" (0042201c)
0040D808 call printf (00401060)
0040D80D add esp,4
25: break;
26: }
27: }
EAX = 00000002 EBX = 7FFDB000
ECX = 00000001 EDX = 00000001
ESI = 00000000 EDI = 0012FF28
EIP = 0040D4C0 ESP = 0012FED8
EBP = 0012FF28 EFL = 00000293
記憶體
0040D521 D6 D4 40 00 衷@.
0040D525 C7 D4 40 00 竊@.
0040D529 F4 D4 40 00 粼@.
0040D52D E5 D4 40 00 逶@.
在值連續的情況下,順序並不會影響生成大表
空缺地址透過填充default語句塊地址解決,但會造成記憶體浪費
;大表
0040D8A6 1F D8 40 00 .谸.
0040D8AA 2E D8 40 00 .谸.
0040D8AE 3D D8 40 00 =谸.
0040D8B2 4C D8 40 00 L谸.
0040D8B6 5B D8 40 00 [谸.
0040D8BA 6A D8 40 00 j谸.
0040D8BE 79 D8 40 00 y谸.
0040D8C2 88 D8 40 00 堌@.
;小表
0040D8C6 00 01 02 07 ....
0040D8CA 07 07 03 07 ....
0040D8CE 04 07 07 07 ....
0040D8D2 05 07 07 06 ....
小表的解釋:
當空缺值太多時記憶體的浪費也會變多,編譯器當然知道這樣不是辦法,所以利用小表來解決這個問題。小表可以看作是一個智慧蹦床,對於不同的玩家會給出不同的力,遇到沒有付費的玩家(空缺值)直接將他丟擲場外(給出引數,使其跳轉到default的語句塊),遇到付費玩家(存在的值)則按照他的等級給出不同的力(給出引數,使其跳轉到其對應的語句塊)
可以看出,在小表中所有的空缺值都是07(因為在這個樣例中,當edx=7時[edx*4+40D8A6h]的地址為default語句塊的地址),而存在的值的對應值從0遞增。