switch语句的几种反汇编结构及效率分析

我们或许知道if…else if…与switch的功能基本相似,而switch也完全可以转换成if…else if…的结构,并且switch总体来说效率要高于if…else if…。但是很少有人知道总体来说if…else if…的效率为什么低于switch,以及为什么说总体来说switch效率高,而不是一定高,以及什么时候效率一定比if…else if…高。今天我们就从反汇编的层面来看看switch语句的多种结构与其效率分析(测试采用VC++6.0集成环境)。

1、普通结构:

所谓普通结构,指的是和普通的if…else if…反汇编结构是完全相同的,在遇到正确匹配条件之前是需要一直去判断的,直到条件匹配后面的指令才不再继续执行。如下所示:

#include "stdafx.h"

int main(int argc, char* argv[])
{
    int s = 5;
    switch(s){
        case 1:
            printf("1\n");
            break;
        case 2:
            printf("2\n");
            break;
        case 3:
            printf("3\n");
            break;
        default:
            printf("error\n");
            break;
    }
    return 0;
}

对应的反汇编代码如下所示:

;main:
00401010   push        ebp
00401011   mov         ebp,esp
00401013   sub         esp,48h
00401016   push        ebx
00401017   push        esi
00401018   push        edi
00401019   lea         edi,[ebp-48h]
0040101C   mov         ecx,12h
00401021   mov         eax,0CCCCCCCCh
00401026   rep stos    dword ptr [edi]

00401028   mov         dword ptr [ebp-4],5

0040102F   mov         eax,dword ptr [ebp-4]
00401032   mov         dword ptr [ebp-8],eax
;if(s == 1),即case 1
00401035   cmp         dword ptr [ebp-8],1
00401039   je          main+39h (00401049)
;else if(s == 2),即case 2
0040103B   cmp         dword ptr [ebp-8],2
0040103F   je          main+48h (00401058)
;else if(s == 3),即case 3
00401041   cmp         dword ptr [ebp-8],3
00401045   je          main+57h (00401067)
;else,即default
00401047   jmp         main+66h (00401076)
;case 1:
00401049   push        offset string "1\n" (0042202c)
0040104E   call        printf (004010f0)
00401053   add         esp,4
;break;
00401056   jmp         main+73h (00401083)
;case 2:
00401058   push        offset string "2\n" (00422028)
0040105D   call        printf (004010f0)
00401062   add         esp,4
;break;
00401065   jmp         main+73h (00401083)
;case 3:
00401067   push        offset string "5\n" (00422024)
0040106C   call        printf (004010f0)
00401071   add         esp,4
;break;
00401074   jmp         main+73h (00401083)
;default:
00401076   push        offset string "error\n" (0042201c)
0040107B   call        printf (004010f0)
00401080   add         esp,4
;break;

00401083   xor         eax,eax

00401085   pop         edi
00401086   pop         esi
00401087   pop         ebx
00401088   add         esp,48h
0040108B   cmp         ebp,esp
0040108D   call        __chkesp (00401170)
00401092   mov         esp,ebp
00401094   pop         ebp
00401095   ret

该结构产生的条件(情况):当case分支语句比较少(VC++6.0中是少于等于4条时),我们测试用的是三条,则switch…case被反汇编成和if…else if…相同的结构。那么在该情况在switch…case效率和if…else if…的效率是一样的。

2、大表结构(索引表):

然而我们说switch也有效率高于if…else if…的情况,即下面所示的情况,举例说明:

#include "stdafx.h"

int main(int argc, char* argv[])
{
    int s = 5;
    switch(s){
        case 101:
            printf("101\n");
            break;
        case 102:
            printf("102\n");
            break;
        case 103:
            printf("103\n");
            break;
        case 104:
            printf("104\n");
            break;
        default:
            printf("error\n");
            break;
    }
    return 0;
}

对应汇编代码如下所示:


;main:
;提升栈、保存现场、初始化函数栈等操作(省略)

00401028   mov         dword ptr [ebp-4],5

;eax存储s,ecx存储101
0040102F   mov         eax,dword ptr [ebp-4]
00401032   mov         dword ptr [ebp-8],eax
00401035   mov         ecx,dword ptr [ebp-8]
;由于是从101开始的连续常量,所以需要减去65H(即101)则索引(下标)从0开始
00401038   sub         ecx,65h
0040103B   mov         dword ptr [ebp-8],ecx

;将减去101case值与3比较(0123default)
;大于3则为默认,直接跳转到default指令处
0040103E   cmp         dword ptr [ebp-8],3
00401042   ja          $L541+0Fh (0040108a)

;这里jmp指令是核心,"edx*4+4010AAh"case分支指令地址在表中的下标,即4010AAh是索引表的首地址
00401044   mov         edx,dword ptr [ebp-8]
00401047   jmp         dword ptr [edx*4+4010AAh]

;case 101
$L535:
0040104E   push        offset string "101\n" (00422fbc)
00401053   call        printf (004010f0)
00401058   add         esp,4
0040105B   jmp         $L541+1Ch (00401097)

;case 102
$L537:
0040105D   push        offset string "1\n" (0042202c)
00401062   call        printf (004010f0)
00401067   add         esp,4
0040106A   jmp         $L541+1Ch (00401097)    ;break

;case 103
$L539:
0040106C   push        offset string "3\n" (00422024)
00401071   call        printf (004010f0)
00401076   add         esp,4
00401079   jmp         $L541+1Ch (00401097)    ;break

;case 104
$L541:
0040107B   push        offset string "104\n" (00422fb4)
00401080   call        printf (004010f0)
00401085   add         esp,4
00401088   jmp         $L541+1Ch (00401097)    ;break
;default
0040108A   push        offset string "error\n" (0042201c)
0040108F   call        printf (004010f0)
00401094   add         esp,4    ;不需要jmp,直接可达到break的效果

00401097   xor         eax,eax
;销毁函数栈、恢复现场等操作(省略)

下面我们来具体分析:为什么叫大表结构?首先我们根据上面代码中标出的重要的那条jmp指令:jmp dword ptr [edx*4+4010AAh]来查看基址4010AAh的内存情况:
这里写图片描述

从0X004010AAh开始的内存中形成了一张所谓的“表”(该表由编译器产生),由于表中每个元素以四字节为单位,存储了对应case语句的地址,则称为大表(小表是1个字节)。而该地址是根据case n:中的n来计算的,比如case 104,所以edx = 104 - 101=3(sub edx,65H),3*4+4010AAh=4010B6jmp dword ptr [edx*4+4010AAh]jmp dword ptr [4010B6],而0X004010B6处存放的就是满足“case 104:”条件的指令的地址0X00401078,eip被修改为0X004010B6处存放的值即eip = 0X00401078,因此CPU下一步执行时就从0X00401078处开始执行,执行完毕后,一个jmp 00401097使得swtich语句结束。无论switch(s)的s是多少只需要提前做好初始化,然后一步jmp dword ptr [edx*4+4010AAh]指令就可以找到对应case的指令。而不需要一个一个去判断,节省了CPU指令周期,而其时间节省的代价是内存的付出。这也是为什么sub edx,65H。因为在现有规律下没有必要浪费不必要浪费的内存。

大表结构的另一种情况:
但是如果case后面的常量如果有不连续的情况,又该如何?我们先来看看比较轻微的不连续情况,即不连续的case值比较少时的情况,case分支依旧为4个,分别为1001,1002,1003,1005(1004为不连续点):
减去的值不再是65H,而是1001即3E9H,此时依旧为大表结构,但是1004不存在,其处理方式观察下图:
这里写图片描述
由于case 1004不存在,所以1004属于默认default之列,故用1004索引出来的表中的地址即为default处的地址,这里只有一个不连续点,但是1004依旧占用了大表的4个字节(4个case,5个地址元素)。同理在不连续情况比较少的时候,编译器都会将这些连续值中间“断开”的少部分(不止一个)填充为default的地址,虽然内存有些浪费但是换得的效率依旧是比较划算的。如下为case 1001、1002、1003、1007,而缺少1004、1005、1006三个值的大表情况(4个case,7个地址元素):

这里写图片描述

总结一下:在case分语句比较多的时候if…else if…结构明显效率不高的情况下,如果case的常量是连续的且有小部分间断,则采用大表结构(即四个字节存储case跳转的指令地址)来编译switch…case语句。间断处填充的地址则为default处执行的指令地址。这一些都是编译器根据相应的算法自行识别后做的。

3、大表+小表结构:

但是如果间断点比较多,大表中重复填充default地址的4字节空间比较多时,就显得有点浪费空间。这时候就需要引入小表,以下例来做分析:

#include "stdafx.h"

int main(int argc, char* argv[])
{
    int s = 5;
    switch(s){
        case 1001:
            printf("1001\n");
            break;
        case 1002:
            printf("1002\n");
            break;
        case 1003:
            printf("1003\n");
            break;
        case 1007:
            printf("1007\n");
            break;
        case 1009:
            printf("1009\n");
            break;
        case 1012:
            printf("1012\n");
            break;
        case 1014:
            printf("1014\n");
            break;
        case 1017:
            printf("1017\n");
            break;
        default:
            printf("error\n");
            break;
    }
    return 0;
}

部分汇编代码如下:

;main:
;提升栈、保存现场、初始化函数栈等操作(省略)

0040D798   mov         dword ptr [ebp-4],5
0040D79F   mov         eax,dword ptr [ebp-4]
0040D7A2   mov         dword ptr [ebp-8],eax
0040D7A5   mov         ecx,dword ptr [ebp-8]
;该处依旧是为了减小大表的内存占用,减去1001
0040D7A8   sub         ecx,3E9h
0040D7AE   mov         dword ptr [ebp-8],ecx
;由于对于连续的大表来说,总共有1007-1001=16条case语句(其实只有8条有效的case)
;所以如果减去1001后的索引值依旧大于10H即16则超过大表索引范围,直接跳去default
0040D7B1   cmp         dword ptr [ebp-8],10h
0040D7B5   ja          $L549+0Fh (0040d845)

;依旧是根据基址来索引,但是此时的索引是有一点不同的,不同处在下面具体分析
0040D7BB   mov         eax,dword ptr [ebp-8]
0040D7BE   xor         edx,edx
0040D7C0   mov         dl,byte ptr  (0040d889)[eax]
0040D7C6   jmp         dword ptr [edx*4+40D865h]

;case 1001
$L535:
0040D7CD   push        offset string "1001\n" (00422fdc)
0040D7D2   call        printf (004010f0)
0040D7D7   add         esp,4
0040D7DA   jmp         $L549+1Ch (0040d852)
;case 1002
$L537:
0040D7DC   push        offset string "1002\n" (00422fd4)
0040D7E1   call        printf (004010f0)
0040D7E6   add         esp,4
0040D7E9   jmp         $L549+1Ch (0040d852)

;case 1003
$L539:
0040D7EB   push        offset string "1003\n" (00422fcc)
0040D7F0   call        printf (004010f0)
0040D7F5   add         esp,4
0040D7F8   jmp         $L549+1Ch (0040d852)

;case 1007
$L541:
0040D7FA   push        offset string "1007\n" (00422fc4)
0040D7FF   call        printf (004010f0)
0040D804   add         esp,4
0040D807   jmp         $L549+1Ch (0040d852)

;case 1009
$L543:
0040D809   push        offset string "1009\n" (00422fbc)
0040D80E   call        printf (004010f0)
0040D813   add         esp,4
0040D816   jmp         $L549+1Ch (0040d852)

;case 1012
$L545:
0040D818   push        offset string "1012\n" (0042202c)
0040D81D   call        printf (004010f0)
0040D822   add         esp,4
0040D825   jmp         $L549+1Ch (0040d852)

;case 1014
$L547:
0040D827   push        offset string "1014\n" (00422024)
0040D82C   call        printf (004010f0)
0040D831   add         esp,4
0040D834   jmp         $L549+1Ch (0040d852)

;case 1017
$L549:
0040D836   push        offset string "104\n" (00422fb4)
0040D83B   call        printf (004010f0)
0040D840   add         esp,4
0040D843   jmp         $L549+1Ch (0040d852)

;default
0040D845   push        offset string "error\n" (0042201c)
0040D84A   call        printf (004010f0)
0040D84F   add         esp,4

0040D852   xor         eax,eax
;销毁函数栈、恢复现场等操作(省略)

我们具体来分析在索引处的四行代码:

0040D7BB   mov         eax,dword ptr [ebp-8]
0040D7BE   xor         edx,edx
0040D7C0   mov         dl,byte ptr  (0040d889)[eax]
0040D7C6   jmp         dword ptr [edx*4+40D865h]

[ebp-8]中现在存放的是switch(s)中的s减去1001之后的值,mov dl,byte ptr (0040d889)[eax]指令则是将[eax+0040d889H]处的值复制到dl处。我们看看对应的内存情况:

这里写图片描述

黑色选中部分即所谓小表(小表首地址即0040d889),而小表上方为大表(大表首地址为0040D865),我们的case分支总共有8条,所以大表中有九个地址(包括了default)。而我们mov dl,byte ptr (0040d889)[eax]是将小表中的值作为大表的索引,即case常量处理后的值为小表的索引,小表所索引到的值为大表的索引,大表所索引到的值为case的指令地址。比如:case 1009,常量处理后,eax=1009 - 1001 = 8,则mov dl,byte ptr (0040d889)[eax],获取0040d889+8=0040d891处的值,dl=04H,edx=0X00000004H,所以jmp dword ptr [edx*4+40D865h]修改eip的值为edx*4+40D865h处的值,即4*4H+0040D865h=0040D875H,即00D04809处的值,eip = 00D04809。而我们可以从上面反汇编代码中获取:

;case 1009,eip = 00D04809从此处开始执行
$L543:
0040D809   push        offset string "1009\n" (00422fbc)
0040D80E   call        printf (004010f0)
0040D813   add         esp,4
0040D816   jmp         $L549+1Ch (0040d852)

则下一条要执行的指令即为1009的地址,经过case常量、小表、大表的索引最终成功修改eip为准确的要执行的指令地址。而对于空出来的间断点,其在小表中的值均为08H,4*8H+0040D865h=0040D885H,该内存存储的均是default的地址,所以说小表中有效的case索引从00开始01、02、03、依次增加,而间断处均填充有效的case分支数(本例为8条case,即填充8),这样就使得原本只使用大表的内存(需要16*4=64B)节省了16字节(小表只需要:8*4+16*1=48B),当分支越多并且间断点越多节省相对也更多。但是小表也有局限性,当分支间隔大于256时,小表一个字节就存不下default的偏移量了(据说能设计出间隔大于256的cae常量的程序员应该直接拉出去…(枪毙))。

4、树形结构:

树形结构就是小表不能表示时,即间断大于256时的一种反汇编结构。我们以下例来分析:
该代码中switch有0、500、100、1500、2000、2500、3000、3500、4000几条case分支(间断大于256),反汇编如下(我们要分析的是判断阶段的树形结构):

main:
;提升栈、保存现场、初始化函数栈等操作(省略)

;判断阶段
0040102F   mov         eax,dword ptr [ebp-4]
00401032   mov         dword ptr [ebp-8],eax
00401035   cmp         dword ptr [ebp-8],7D0h
0040103C   jg          main+73h (00401083)
0040103E   cmp         dword ptr [ebp-8],7D0h
00401045   je          main+0F1h (00401101)
0040104B   cmp         dword ptr [ebp-8],3E8h
00401052   jg          main+65h (00401075)
00401054   cmp         dword ptr [ebp-8],3E8h
0040105B   je          main+0D3h (004010e3)
00401061   cmp         dword ptr [ebp-8],0
00401065   je          main+0B2h (004010c2)
00401067   cmp         dword ptr [ebp-8],1F4h
0040106E   je          main+0C4h (004010d4)
00401070   jmp         main+13Ch (0040114c)
00401075   cmp         dword ptr [ebp-8],5DCh
0040107C   je          main+0E2h (004010f2)
0040107E   jmp         main+13Ch (0040114c)
00401083   cmp         dword ptr [ebp-8],0DACh
0040108A   jg          main+0A0h (004010b0)
0040108C   cmp         dword ptr [ebp-8],0DACh
00401093   je          main+11Eh (0040112e)
00401099   cmp         dword ptr [ebp-8],9C4h
004010A0   je          main+100h (00401110)
004010A2   cmp         dword ptr [ebp-8],0BB8h
004010A9   je          main+10Fh (0040111f)
004010AB   jmp         main+13Ch (0040114c)
004010B0   cmp         dword ptr [ebp-8],0FA0h
004010B7   je          main+12Dh (0040113d)
004010BD   jmp         main+13Ch (0040114c)
;具体处理阶段
004010C2   push        offset string "0\n" (00422064)
004010C7   call        printf (004011d0)
004010CC   add         esp,4
004010CF   jmp         main+149h (00401159)
004010D4   push        offset string "500\n" (0042205c)
004010D9   call        printf (004011d0)
004010DE   add         esp,4
004010E1   jmp         main+149h (00401159)
004010E3   push        offset string "1000\n" (00422054)
004010E8   call        printf (004011d0)
004010ED   add         esp,4
004010F0   jmp         main+149h (00401159)
004010F2   push        offset string "1500\n" (0042204c)
004010F7   call        printf (004011d0)
004010FC   add         esp,4
004010FF   jmp         main+149h (00401159)
00401101   push        offset string "2000\n" (00422044)
00401106   call        printf (004011d0)
0040110B   add         esp,4
0040110E   jmp         main+149h (00401159)
00401110   push        offset string "2500\n" (0042203c)
00401115   call        printf (004011d0)
0040111A   add         esp,4
0040111D   jmp         main+149h (00401159)
0040111F   push        offset string "3000\n" (00422034)
00401124   call        printf (004011d0)
00401129   add         esp,4
0040112C   jmp         main+149h (00401159)
0040112E   push        offset string "3500\n" (0042202c)
00401133   call        printf (004011d0)
00401138   add         esp,4
0040113B   jmp         main+149h (00401159)
0040113D   push        offset string "4000\n" (00422024)
00401142   call        printf (004011d0)
00401147   add         esp,4
0040114A   jmp         main+149h (00401159)
0040114C   push        offset string "error\n" (0042201c)
00401151   call        printf (004011d0)
00401156   add         esp,4

00401159   xor         eax,eax
;销毁函数栈、恢复现场等操作(省略)

根据判断阶段的分析,由jg指令分,可以将0、500、100、1500、2000、2500、3000、3500、4000分为以下的树形结构:
这里写图片描述
根据树形结构的每个节点再分为等于和不等于两种情况,进行处理。但是由于该情况基本不常见(写出这样的代码似乎应该已经被拉了出去……),所以我们不在仔细分析。而大表的小表是比较常见的两种结构,需要熟练掌握。

  • 11
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值