【x86汇编】C语言的switch&case结构的反汇编分析

目录

1.前置知识

2.C语言代码

运行环境:VS2010+Win32+Debug

sourcecode1

sourcecode2

3.反汇编分析

sourcecode1的反汇编代码

不带机器码

 带机器码

逐条分析

1.向提前开辟好的栈帧空间压入几个寄存器的值(push保存关键的寄存器的值,pop在main函数的最后恢复关键寄存器的值)

2.变量的定义,赋初值

3.将参数num的值写入内存

4.为比较指令做准备

5.★★★重点指令分析(本文的核心)★★★

cmp指令(全称compare)

ja指令(全称jump if above)

6.程序main函数的返回,空间被销毁,交还给操作系统(非本文重点)

7.题外话:其他细节说明

附:完整流程的栈区图

sourcecode2的反汇编代码

和sourcecode1的不同点

4.总结switch&case的执行原理

5.题外话


1.前置知识

switch的用法参见22.【C语言】选择结构之switch

附:《C语言程序与设计 第四版》 对switch的解释

8995991a9e1443859c92ed5b60f2e0e7.jpeg

注意画红线的等值比较,下面会重点讲

2.C语言代码

运行环境:VS2010+Win32+Debug

修改项目属性的两个地方,减少反汇编代码

68806fb6a2f3478a90e1f28b3b18c4d8.png

sourcecode1

int main()
{
  int num = 2;
  int tmp=0;
  switch (num)
  {
    case 1:tmp=1;
    case 2:tmp=2;
    case 3:tmp=3;
    case 4:tmp=4;
    default:tmp=5;
  }
  return tmp;
}

按switch的语法分析, 依次执行int num = 2;,int tmp = 0;,tmp=2;tmp=3;tmp=4;tmp=5;最后返回tmp的值

sourcecode2

int main()
{
  int num = 2;
  int tmp=0;
  switch (num)
  {
    case 1:tmp=1;break;
    case 2:tmp=2;break;
    case 3:tmp=3;break;
    case 4:tmp=4;break;
    default:tmp=5;
  }
  return tmp;
}

按switch的语法分析, 依次执行int num = 2;,int tmp = 0;,tmp=2;,break;最后返回tmp的值

3.反汇编分析

注:视频调试演示

C语言的switch结构的反汇编分析文章的配套视频

 点我跳转去观看

按F11进入调试模式,右击转到反汇编

d08ed8d49cc24d2484e45728111d37eb.png

sourcecode1的反汇编代码

不带机器码

int main()
{
 push        ebp  
 mov         ebp,esp  
 sub         esp,4Ch  
 push        ebx  
 push        esi  
 push        edi  
  int num = 2;
 mov         dword ptr [num],2  
  int tmp=0;
 mov         dword ptr [tmp],0  
  switch (num)
 mov         eax,dword ptr [num]  
 mov         dword ptr [ebp-4Ch],eax  
 mov         ecx,dword ptr [ebp-4Ch]  
 sub         ecx,1  
 mov         dword ptr [ebp-4Ch],ecx  
 cmp         dword ptr [ebp-4Ch],3  
 ja          $LN2+7 (0B712A2h)  
 mov         edx,dword ptr [ebp-4Ch]  
 jmp         dword ptr  (0B712B4h)[edx*4]  
  {
    case 1:tmp=1;
 mov         dword ptr [tmp],1  
    case 2:tmp=2;
 mov         dword ptr [tmp],2  
    case 3:tmp=3;
 mov         dword ptr [tmp],3  
    case 4:tmp=4;
 mov         dword ptr [tmp],4  
    default:tmp=5;
 mov         dword ptr [tmp],5  
  }
  return tmp;
 mov         eax,dword ptr [tmp]  
}
 pop         edi  
 pop         esi  
 pop         ebx  
 mov         esp,ebp  
 pop         ebp  
 ret  
 nop  

 带机器码

int main()
{
55                   push        ebp  
8B EC                mov         ebp,esp  
83 EC 4C             sub         esp,4Ch  
53                   push        ebx  
56                   push        esi  
57                   push        edi  
  int num = 2;
C7 45 FC 02 00 00 00 mov         dword ptr [num],2  
  int tmp=0;
C7 45 F8 00 00 00 00 mov         dword ptr [tmp],0  
  switch (num)
8B 45 FC             mov         eax,dword ptr [num]  
89 45 B4             mov         dword ptr [ebp-4Ch],eax  
8B 4D B4             mov         ecx,dword ptr [ebp-4Ch]  
83 E9 01             sub         ecx,1  
89 4D B4             mov         dword ptr [ebp-4Ch],ecx  
83 7D B4 03          cmp         dword ptr [ebp-4Ch],3  
77 26                ja          $LN2+7 (0B712A2h)  
8B 55 B4             mov         edx,dword ptr [ebp-4Ch]  
FF 24 95 B4 12 B7 00 jmp         dword ptr  (0B712B4h)[edx*4]  
  {
    case 1:tmp=1;
C7 45 F8 01 00 00 00 mov         dword ptr [tmp],1  
    case 2:tmp=2;
C7 45 F8 02 00 00 00 mov         dword ptr [tmp],2  
    case 3:tmp=3;
C7 45 F8 03 00 00 00 mov         dword ptr [tmp],3  
    case 4:tmp=4;
C7 45 F8 04 00 00 00 mov         dword ptr [tmp],4  
    default:tmp=5;
C7 45 F8 05 00 00 00 mov         dword ptr [tmp],5  
  }
  return tmp;
8B 45 F8             mov         eax,dword ptr [tmp]  
}
5F                   pop         edi  
5E                   pop         esi  
5B                   pop         ebx  
8B E5                mov         esp,ebp  
5D                   pop         ebp  
C3                   ret  
90                   nop  

逐条分析

画栈区图会对反汇编分析有帮助

1.向提前开辟好的栈帧空间压入几个寄存器的值(push保存关键的寄存器的值,pop在main函数的最后恢复关键寄存器的值)

 push        ebp  
 mov         ebp,esp  
 sub         esp,4Ch  
 push        ebx  
 push        esi  
 push        edi  

 push ebp指令未执行时,打开寄存器窗口

dd37b6699a3c4046b37c373c83e69d57.png

发现ESP=0x0012FFC18,为画栈区图做准备

EBP的值入栈后,再次查看寄存器窗口

e566b6acedf9452b895aafd049a2f3ae.png
发现有两个寄存器的值发生变动:EIP和ESP

EIP寄存器(Extend Instruction Pointer 扩展指令指针寄存器,其存储指令的地址)的值+1,表明push ebp机器码(0x55)占1个字节,此时EIP指向下一条指令mov ebp,esp,eax的机器码的第一个字节(0x8B),不过EIP的值不是我们所关心的

注意看ESP寄存器的值减了4(0x0012FFC18-4=0x0012FFC14)ESP指针从高地址向低地址移动

下面依次将EBX,ESI,EDI寄存器的值入栈,执行完后ESP=0x0012FFBBC

(ESP值的变动过程0x0012FFC18-->0x0012FFC14-(-4Ch)->0x0012FFBC8-->0x0012FFCBC4-->0x0012FFBC0-->0x0012FFBBC)

可以画栈区图:

82f5dc8d94b04fc0a786238c7f29374f.png

备注:为什么有sub esp,4Ch这一指令见第2点的分析

2.变量的定义,赋初值

  int num = 2;
 mov         dword ptr [num],2  
  int tmp=0;
 mov         dword ptr [tmp],0  

备注:有关为什么用mov指令为变量赋初值参见动态内存管理练习题的反汇编代码分析(底层),非本文的重点

视频中打开了内存窗口

f675c9f52d794a4cbddab149d4a627aa.png

输入了&num,发现num存储在0x0012FFC10~0x0012FFC13处,分析0x0012FFC13的相邻地址就是0x0012FFC14( sub esp,4Ch指令执行前ESP寄存器的值)

看栈区图更直观:

59380a5418ef48c088c4ce3a78488f8e.png

注意:

1.push指令为压栈从高地址向低地址存储数据

2.mov指令从低地址向高地址存储数据

可知sub esp,4Ch的作用:为num和tmp变量留出空间

3.将参数num的值写入内存

 mov         eax,dword ptr [num]  
 mov         dword ptr [ebp-4Ch],eax  

先把num的值暂存到eax中转寄存器中,再将eax寄存器的值复制到dword ptr [ebp-4Ch]指向的地址空间中

(注:x86汇编不支持从[地址]到[地址]的格式,即像mov dword ptr [ebp-4Ch],dword ptr [num]这样的指令是非法的)

在mov dword ptr [ebp-4Ch],eax指令执行前看一下EBP寄存器的值

e406e1796da64ca5a3df66d9305623da.png

计算EBP-4Ch=0x012FFC60

则mov dword ptr [ebp-4Ch],eax是将eax的值复制到0x012FFC60~0x012FFC63处

4.为比较指令做准备

sub         ecx,1  
mov         dword ptr [ebp-4Ch],ecx  

更新ecx的值,重新写入dword ptr [ebp-4Ch]指向的地址空间中,为cmp指令的执行做准备,ja和jmp会依据cmp指令的比较结果做跳转(跳转到case1?case2?case3?case4?default?)

5.★★★重点指令分析(本文的核心)★★★

 cmp         dword ptr [ebp-4Ch],3  
 ja          $LN2+7 (0B712A2h)  
 mov         edx,dword ptr [ebp-4Ch]  
 jmp         dword ptr  (0B712B4h)[edx*4]  

先讲几个汇编指令

cmp指令(全称compare)(用于等值比较)

格式:cmp dest,src

作用:顾名思义,compare是比较,因此比较dest和src的值,将比较后的结果写入标志寄存器(x86下的EFL:Extend FLags),但不改变dest和src原来的值

ja指令(全称jump if above)

格式:ja 地址

作用:顾名思义,jump if above指的是:如果上面比较的结果大于(即不小于)时跳转.ja指令会影响程序的控制流,但它不会直接影响如果高于(dest>src)则跳转,否则(dest=src或dest<src)按顺序执行ja的下一条指令(这里的反汇编指令mov edx,dword ptr [ebp-4Ch])

底层原理:ja指令执行时,CPU会先读取标志寄存器的CF位(进位标志位Carry Flag)和ZF(零标志位Zero Flag)位,只有当CF=0(未发生进位)且ZF=0(cmp dest,src中dest不等于src)时,ja指令才会触发跳转

 cmp         dword ptr [ebp-4Ch],3  
 ja          $LN2+7 (0B712A2h)  

一看到ja就可以知道当dword ptr [ebp-4Ch]指向的地址空间大于3时,会跳到0x0B712A2h处执行

对应的default:tmp=5;的部分,可以验证下

内存窗口输入0x00B712A2,查看对应的机器码,内存窗口中的数据确实对应default:tmp=5;的机器码

cab2682f2df443d69562f8e366aa820d.png

dword ptr [ebp-4Ch]的值为1,因此不会跳转到地址0x0B712A2h处执行,而是按顺序执行下一条指令mov edx,dword ptr [ebp-4Ch],执行完后edx的值为0x00000001

下面将会讲解本文的核心部分:跳转表

switch&case语句的核心汇编指令就是这个jmp dword ptr  (0B712B4h)[edx*4]

(0B712B4h)[edx*4]意思是[0x0B&12B4+edx*4]寻址方式为基址(0x005412B4)+变址(edx*4)

因此会取出0x0B712B4+edx*4地址处的值,跳转到这个地方执行

内存窗口中输入0x0B712B4+1*4

提问:从下面图片的数据中你发现什么规律?

645019ef63eb40f7b8874613d0f24244.png

答:

93b27d76e7664f87a3e5d02b31771e25.png

注意到画框的地方,从高地址向地址方向读:0054129b 00541294 0054128d 00541286

这些数据是连续的!!!

右击选择显示代码字节

470886989ee24318aa017e198d9afaaf.png

如果在内存窗口中把他们视为地址依次输入就会发现

2866658a59d94371864e14bba8bfbcc0.png

ecb075b61c0e4af6a149fd4303580b25.png

回过头来看,实际上下方这张图片的内存区域属于跳转表,拥有连续的地址数据,而jmp dword ptr  (0B712B4h)[edx*4]履行查表的功能,跳转到对应的地方执行(case1?case2?case3?case4?)93b27d76e7664f87a3e5d02b31771e25.png

6.程序main函数的返回,空间被销毁,交还给操作系统(非本文重点)

 pop         edi  
 pop         esi  
 pop         ebx  
 mov         esp,ebp  
 pop         ebp  
 ret  

7.题外话:其他细节说明

9d8dd1362ed141f8947b3e1c48ddab33.png

9f1d50a133074d45b29b6d1834d5bcbf.png

附:完整流程的栈区图

da17994c130a4ec89c4aad4af5789d9f.png

思考:如果为sourcecode1的case后都添加break;反汇编指令是什么样的?

sourcecode2的反汇编代码

int main()
{
 push        ebp  
 mov         ebp,esp  
 sub         esp,4Ch  
 push        ebx  
 push        esi  
 push        edi  
  int num = 2;
 mov         dword ptr [num],2  
  int tmp=0;
 mov         dword ptr [tmp],0  
  switch (num)
 mov         eax,dword ptr [num]  
 mov         dword ptr [ebp-4Ch],eax  
 mov         ecx,dword ptr [ebp-4Ch]  
 sub         ecx,1  
 mov         dword ptr [ebp-4Ch],ecx  
 cmp         dword ptr [ebp-4Ch],3  
 ja          $LN2+9 (6112AAh)  
 mov         edx,dword ptr [ebp-4Ch]  
 jmp         dword ptr  (6112BCh)[edx*4]  
  {
    case 1:tmp=1;break;
 mov         dword ptr [tmp],1  
 jmp         $LN2+10h (6112B1h)  
    case 2:tmp=2;break;
 mov         dword ptr [tmp],2  
 jmp         $LN2+10h (6112B1h)  
    case 3:tmp=3;break;
 mov         dword ptr [tmp],3  
 jmp         $LN2+10h (6112B1h)  
    case 4:tmp=4;break;
 mov         dword ptr [tmp],4  
 jmp         $LN2+10h (6112B1h)  
    default:tmp=5;
 mov         dword ptr [tmp],5  
  }
  return tmp;
 mov         eax,dword ptr [tmp]  
}
 pop         edi  
 pop         esi  
 pop         ebx  
 mov         esp,ebp  
 pop         ebp  
 ret  

和sourcecode1的不同点

break;相当于反汇编代码的

 jmp         $LN2+10h (6112B1h)  

显示代码字节并代打开内存窗口,输入0x006112B1

674c4de7ea274c88b20d11ddc7c242ab.png

 因此 jmp $LN2+10h (6112B1h)是跳转到return tmp;处,脱离了switch&case结构

4.总结switch&case的执行原理

对于VS2022,当case分支较多时(分支较少会退化为if/else模式),某一个地址空间得到switch (num)的num-1的值,先判断是否为default情况,若不是,则通过基址+变址来查询跳转表(跳转表中存储了所有case情况的所有地址),再依据跳转表的数据跳到指定的地址处执行;如果case的某个分支含有break;则break;的反汇编指令jmp会跳离switch&case结构

5.题外话

其实在Dev C++中查询反汇编指令中会发现不同于VS2022的地方,选择TDM-GCC 4.9.2 32-bit Debug环境

下断点后,将sourcecode1在Dev C++中调试,查看CPU窗口

7aeeb8f3788844aab90285880c9d473d.png

好像退化成if else了,也有可能Dev C++对跳转表这个部分做隐藏了

猜你喜欢

转载自blog.csdn.net/2401_85828611/article/details/143340354