目录
1.向提前开辟好的栈帧空间压入几个寄存器的值(push保存关键的寄存器的值,pop在main函数的最后恢复关键寄存器的值)
6.程序main函数的返回,空间被销毁,交还给操作系统(非本文重点)
1.前置知识
switch的用法参见22.【C语言】选择结构之switch
附:《C语言程序与设计 第四版》 对switch的解释
注意画红线的等值比较,下面会重点讲
2.C语言代码
运行环境:VS2010+Win32+Debug
修改项目属性的两个地方,减少反汇编代码
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进入调试模式,右击转到反汇编
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指令未执行时,打开寄存器窗口
发现ESP=0x0012FFC18,为画栈区图做准备
EBP的值入栈后,再次查看寄存器窗口
发现有两个寄存器的值发生变动: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)
可以画栈区图:
备注:为什么有sub esp,4Ch这一指令见第2点的分析
2.变量的定义,赋初值
int num = 2;
mov dword ptr [num],2
int tmp=0;
mov dword ptr [tmp],0
备注:有关为什么用mov指令为变量赋初值参见动态内存管理练习题的反汇编代码分析(底层),非本文的重点
视频中打开了内存窗口
输入了&num,发现num存储在0x0012FFC10~0x0012FFC13处,分析0x0012FFC13的相邻地址就是0x0012FFC14( sub esp,4Ch指令执行前ESP寄存器的值)
看栈区图更直观:
注意:
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寄存器的值
计算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;的机器码
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
提问:从下面图片的数据中你发现什么规律?
答:
注意到画框的地方,从高地址向地址方向读:0054129b 00541294 0054128d 00541286
这些数据是连续的!!!
右击选择显示代码字节
如果在内存窗口中把他们视为地址依次输入就会发现
回过头来看,实际上下方这张图片的内存区域属于跳转表,拥有连续的地址数据,而jmp dword ptr (0B712B4h)[edx*4]履行查表的功能,跳转到对应的地方执行(case1?case2?case3?case4?)
6.程序main函数的返回,空间被销毁,交还给操作系统(非本文重点)
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret
7.题外话:其他细节说明
附:完整流程的栈区图
思考:如果为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
因此 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窗口
好像退化成if else了,也有可能Dev C++对跳转表这个部分做隐藏了