If…else…语句比较简单,这里就不做详细说明。这里对switch语句和循环语句进行判断。
此时,switch语句块反汇编的代码与if…else…的相差无几,只是switch语句将所有条件跳转都放到了一起并与case语句块分开,而if…else…则是每一条跳转语句后都跟着语句块,反汇编都比较简单,这里不再赘述。下面举出一个例子:
8: switch(i){
0042B3F6 mov eax,dword ptr [i]
0042B3F9 mov dword ptr[ebp-0D0h],eax
//取出i的值
0042B3FF cmp dword ptr [ebp-0D0h],1 //和1比较
0042B406 je test_switch_if+5Ch (42B41Ch) //条件成立跳转到case1
0042B408 cmp dword ptr [ebp-0D0h],3 //和3比较
0042B40F je test_switch_if+6Bh(42B42Bh) //条件成立跳转到case3
0042B411 cmp dword ptr [ebp-0D0h],64h //和100比较
0042B418 je test_switch_if+7Ah (42B43Ah) //条件成立跳转到case100
0042B41A jmp test_switch_if+87h(42B447h) //否则跳转至switch结束处
9: case 1:
10: printf("i == 1");
0042B41C push offset string "i== 1" (47DC80h)
0042B421 call @ILT+4100(_printf) (42A009h)
0042B426 add esp,4
//调用printf函数
11: break;
//跳转至switch结束处,下面两个结构相同
0042B429 jmp test_switch_if+87h (42B447h)
12: case 3:
13: printf("i == 3");
0042B42B push offset string "i == 3" (47DC78h)
0042B430 call @ILT+4100(_printf) (42A009h)
0042B435 add esp,4
14: break;
0042B438 jmp test_switch_if+87h (42B447h)
15: case 100:
16: printf("i == 100");
0042B43A push offset string "i== 100" (47DC6Ch)
0042B43F call @ILT+4100(_printf) (42A009h)
0042B444 add esp,4
17: break;
18: }
当case的最大值和最小值之间差距较小时使用这种方法优化,一半差距不会大于10(VC++6.0似乎是不大于7,下面会看到和VS2010编译器的区别)。
直接看例子:
26: switch(nIndex)
0042B4C6 mov eax,dword ptr[nIndex]
0042B4C9 mov dword ptr[ebp-0D0h],eax
0042B4CF mov ecx,dword ptr [ebp-0D0h]
//到此为止的三行是取出i的值
0042B4D5 sub ecx,1
//这里的减1则是为了与线性表的对齐,线性表第一个为0
0042B4D8 mov dword ptr [ebp-0D0h],ecx
0042B4DE cmp dword ptr [ebp-0D0h],6
//这里和6比较是因为case最大值为7,减1后为6,若超过最大值则直接跳出语句或跳向default
0042B4E5 ja $LN1+0Dh (42B54Ch)
0042B4E7 mov edx,dword ptr [ebp-0D0h]
0042B4ED jmp dword ptr (42B59Ch)[edx*4]
//决定跳向哪个case
27: {
28: case 1:
29: printf("nIndex == 1");
0042B4F4 push offset string"nIndex == 1" (47DCECh)
0042B4F9 call @ILT+4100(_printf) (42A009h)
0042B4FE add esp,4
//Printf函数的调用
30: break;
0042B501 jmp $LN1+0Dh (42B54Ch)
//跳向结束或是default语句块,以下的case都类似
31: case 2:
32: printf("nIndex == 2");
0042B503 push offset string "nIndex == 2" (47DCDCh)
0042B508 call @ILT+4100(_printf) (42A009h)
0042B50D add esp,4
33: break;
0042B510 jmp $LN1+0Dh (42B54Ch)
34: case 3:
35: printf("nIndex == 3");
0042B512 push offset string "nIndex == 3" (47DCCCh)
0042B517 call @ILT+4100(_printf) (42A009h)
0042B51C add esp,4
36: break;
0042B51F jmp $LN1+0Dh(42B54Ch)
37: case 5:
38: printf("nIndex == 5");
0042B521 push offset string "nIndex == 5" (47DCBCh)
0042B526 call @ILT+4100(_printf) (42A009h)
0042B52B add esp,4
39: break;
0042B52E jmp $LN1+0Dh (42B54Ch)
40: case 6:
41: printf("nIndex == 6");
0042B530 push offset string "nIndex == 6" (47DCACh)
0042B535 call @ILT+4100(_printf) (42A009h)
0042B53A add esp,4
42: break;
0042B53D jmp $LN1+0Dh (42B54Ch)
43: case 7:
44: printf("nIndex == 7");
0042B53F push offset string"nIndex == 7" (47DC9Ch)
0042B544 call @ILT+4100(_printf) (42A009h)
0042B549 add esp,4
45: break;
46: }
在这种情况下编译器会生成一张地址表来帮助寻找case,如下所示:
0x0042B59C f4 b4 42 00
0x0042B5A0 03 b5 42 00
0x0042B5A4 12 b5 42 00
0x0042B5A8 4c b5 42 00
0x0042B5AC 21 b5 42 00
0x0042B5B0 30b5 42 00
0x0042B5B4 3f b5 42 00
这里的每个元素代表着一个case的地址,例如f4 b4 4200地址是0x0042b4f4,再看看书上面case1的地址发现是一样的,依次类推。你还会看到那些没有的元素,就是第四个元素4c b5 42 00,这里就把没有的case都定为default或是switch的结束地址。
以上是debug版本的程序反汇编的结果,release版本的差不多,就是在每个case的最后一句有所不同,因为这个程序在switch语句后就直接返回了且每个case都以break结尾所以编译器决定减少一个跳转,直接在case结束后返回以节约时间提高效率。
当两个case值相差较大采取上面的方法会浪费许多空间,而采取以下索引表的结构则不会浪费太多。下面是一个简单的例子:
switch(i){
0042B5F6 mov eax,dword ptr [i]
0042B5F9 mov dword ptr[ebp-0D0h],eax
0042B5FF mov ecx,dword ptr [ebp-0D0h]
//到此为止的三行是取出i的值
0042B605 sub ecx,1
//这里的减1则是为了与线性表的对齐,线性表第一个为0
0042B608 mov dword ptr [ebp-0D0h],ecx
0042B60E cmp dword ptr [ebp-0D0h],18h
0042B615 ja $LN1+0Dh (42B692h)
//若超过最大值则直接跳转到switch结束处
0042B617 mov edx,dword ptr [ebp-0D0h]
0042B61D movzx eax,byte ptr (42B6F0h)[edx]
0042B624 jmp dword ptr (42B6D0h)[eax*4] //根据输入的元素决定跳转到哪一个case
56: case 1:
57: printf("i == 1");
0042B62B push offset string "i == 1" (47DC80h)
0042B630 call @ILT+4100(_printf) (42A009h)
0042B635 add esp,4
//以上是调用printf函数
58: break;
0042B638 jmp $LN1+0Dh (42B692h)
//直接跳到switch结束处
//下面的每个case都是一样的结构
59: case 2:
60: printf("i == 2");
0042B63A push offset string "i== 2" (47DD1Ch)
0042B63F call @ILT+4100(_printf) (42A009h)
0042B644 add esp,4
61: break;
0042B647 jmp $LN1+0Dh (42B692h)
62: case 3:
63: printf("i == 3");
0042B649 push offset string "i == 3" (47DC78h)
0042B64E call @ILT+4100(_printf) (42A009h)
0042B653 add esp,4
64: break;
0042B656 jmp $LN1+0Dh (42B692h)
65: case 5:
66: printf("i == 5");
0042B658 push offset string "i == 5" (47DD10h)
0042B65D call @ILT+4100(_printf) (42A009h)
0042B662 add esp,4
67: break;
0042B665 jmp $LN1+0Dh (42B692h)
68: case 6:
69: printf("i == 6");
0042B667 push offset string "i == 6" (47DD08h)
0042B66C call @ILT+4100(_printf) (42A009h)
0042B671 add esp,4
70: break;
0042B674 jmp $LN1+0Dh (42B692h)
71: case 7:
72: printf("i == 7 ");
0042B676 push offset string "i == 7 " (47DD00h)
0042B67B call @ILT+4100(_printf) (42A009h)
0042B680 add esp,4
73: break;
0042B683 jmp $LN1+0Dh (42B692h)
74: case 25:
75: printf("i == 25");
0042B685 push offset string "i == 255" (47DDF0h)
0042B68A call @ILT+4100(_printf) (42A009h)
0042B68F add esp,4
76: break;
这里最关键的是这一句:
0042B624 jmp dword ptr (42B6D0h)[eax*4]
也就是选择跳转到哪一个case的语句。在这种情况下,编译器会为switch建立两张表,一张是前面类似的地址表,一张则是索引表。下面来看一下具体内容是什么:
0x0042B6D0 2bb6 42 00
0x0042B6D4 3a b6 42 00
0x0042B6D8 49b6 42 00
0x0042B6DC 58b6 42 00
0x0042B6E0 67b6 42 00
0x0042B6E4 76b6 42 00
0x0042B6E8 85b6 42 00
0x0042B6EC 92b6 42 00
上面的这个就是case地址表,里面存着每个case对应的地址。如第一个2b b6 42 00,它的地址就是0x0042b62b,这正是case 1所在的地址,这就是为什么前面的代码要将i-1了,这就是地址表的第0号元素。后面的就很简单了,如果是3那么找到线性表的第三个元素内容49 b6 42 00,对应地址为0x0042b649,这就是case 3的地址。但是这里的没有case 4,所以第四个元素的地址就是default或是switch的结束地址。但是要如何找到地址表呢?这里通过一个索引表来找到:
0x0042B6F0 00 01 02 07 03 04 05 07 07 07 07 07 07 07 07 07 07 07 0707 07 07 07 07 06
说是索引不如说是地址表的下标。如果i=3,减去1之后是2,找到索引表中下标为2的内容是02,再由02去地址表中找到地址49 b6 42 00即0x0042b649,这就是要跳转的地址,接下去就很简单了,不必多说。你还会看到那些没有的索引,比如10,这里就把没有的case索引都定为地址表中的最后一个元素,这就是default或是switch的结束地址。
这里总共有25个元素,共占资源7*4+25=53个字节,若是全部按照地址表来存储则需25*4=100个字节。这样存储明显就减少了很多的存储空间。当然这也并非全是好处,由于索引表中只用一个字节存储索引,故最大只能有256个索引,比这还大的需要下面一种方法优化。
还是和上面一样先看例子:
85: switch(i){
0042B746 mov eax,dword ptr [i]
0042B749 mov dword ptr [ebp-0D0h],eax
//取出i的值
0042B74F cmp dword ptr[ebp-0D0h],23h
0042B756 jg test_switch_tree+7Eh (42B78Eh)
//和35比较,大于则跳转至42B78Eh
0042B758 cmp dword ptr [ebp-0D0h],23h
0042B75F je $LN6+0Fh(42B7EDh)
//等于35的跳转至case35处
0042B765 mov ecx,dword ptr [ebp-0D0h]
0042B76B sub ecx,2
//case值与地址表中的元素对齐,所以要减2
0042B76E mov dword ptr [ebp-0D0h],ecx
0042B774 cmp dword ptr [ebp-0D0h],8
0042B77B ja $LN6+4Bh (42B829h)
//大于10(-2后大于8就相当于大于10)则跳转至default处
0042B781 mov edx,dword ptr [ebp-0D0h]
0042B787 jmp dword ptr (42B874h)[edx*4]
在小于10的case中利用前面提到过的线性表进行优化,此处即为线性表的寻址
0042B78E cmp dword ptr [ebp-0D0h],25h
0042B795 je $LN6+1Eh (42B7FCh)
//和37比较,相等则跳至case37处
0042B797 cmp dword ptr [ebp-0D0h],29Ah
0042B7A1 je $LN6+2Dh (42B80Bh)
//和666比较,相等则跳至case666处
0042B7A3 cmp dword ptr[ebp-0D0h],2710h
0042B7AD je $LN6+3Ch (42B81Ah)
//和10000比较,相等则跳至case10000处
0042B7AF jmp $LN6+4Bh (42B829h)
//跳至default处
86: case 2:
87: printf("i == 2\n");
0042B7B1 push offset string "i == 2\n" (47DCF8h)
0042B7B6 call @ILT+4100(_printf) (42A009h)
0042B7BB add esp,4
88: break;
0042B7BE jmp $LN6+58h (42B836h)
89: case 3:
90: printf("i == 3\n");
0042B7C0 push offset string "i== 3\n" (47DC90h)
0042B7C5 call @ILT+4100(_printf) (42A009h)
0042B7CA add esp,4
91: break;
0042B7CD jmp $LN6+58h (42B836h)
92: case 8:
93: printf("i == 8\n");
0042B7CF push offset string "i == 8\n" (47DD74h)
0042B7D4 call @ILT+4100(_printf) (42A009h)
0042B7D9 add esp,4
94: break;
0042B7DC jmp $LN6+58h (42B836h)
95: case 10:
96: printf("i == 10\n");
0042B7DE push offset string "i == 10\n" (47DD68h)
0042B7E3 call @ILT+4100(_printf) (42A009h)
0042B7E8 add esp,4
97: break;
0042B7EB jmp $LN6+58h (42B836h)
98: case 35:
99: printf("i == 35\n");
0042B7ED push offset string "i == 35\n" (47DD5Ch)
0042B7F2 call @ILT+4100(_printf) (42A009h)
0042B7F7 add esp,4
100: break;
0042B7FA jmp $LN6+58h (42B836h)
101: case 37:
102: printf("i == 37\n");
0042B7FC push offset string "i == 37\n" (47DD50h)
0042B801 call @ILT+4100(_printf) (42A009h)
0042B806 add esp,4
103: break;
0042B809 jmp $LN6+58h (42B836h)
104: case 666:
105: printf("i == 666\n");
0042B80B push offset string "i == 666\n" (47DD44h)
0042B810 call @ILT+4100(_printf) (42A009h)
0042B815 add esp,4
106: break;
0042B818 jmp $LN6+58h (42B836h)
107: case 10000:
108: printf("i == 10000\n");
0042B81A push offset string "i== 10000\n" (47DD38h)
0042B81F call @ILT+4100(_printf) (42A009h)
0042B824 add esp,4
109: break;
0042B827 jmp $LN6+58h (42B836h)
110: default:
111: printf("default\n");
0042B829 push offset string "default\n" (47DD2Ch)
0042B82E call @ILT+4100(_printf) (42A009h)
0042B833 add esp,4
112: break;
在这里,由于case值相差过大,因此采取类似于二叉树的结构来进行查找。首先判断是否大于35,之后分成两部分进行判断,小于35的由于比较接近因此采用线性查找法,而大于35的部分则采用if...else...模拟法即使用第一种优化方式进行优化。但是在我所购买的书中是使用VC++6.0编译器,所以它的代码反汇编出的结果看出无论是大于35还是小于35的部分都使用了if...else...模拟法进行优化。
以上是用Visual Studio 2010反汇编器对Deubg版本进行的反汇编,下面可以利用IDA Pro反汇编Release版本并查看其结构,具体如下:
这是简单的结构图。
放大之后可以看到里面都有代码,利用这个分析将会事半功倍。
以上是其主要的结构流程,可以看到和二叉树的形状非常类似, Release版本的具体代码和Debug版本的差不多,具体还是像之前那样分析就可以了。