汇编:C语言switch转汇编
switch概述
根据不同的表达式值,执行不同的分支块代码
常见应用方式如下
switch(表达式){
case xx1:{
//功能代码
break;
}
case xx2:{
//功能代码
break;
}
....
}
根据case后边紧跟的常量值的特点(下文简称case常量),编译器会根据一定取舍,将switch的性能尽可能的优化,常见switch的汇编代码有下几种。
第一种:case常量连续型,但case语句较少
C语言代码
#include
void Fun(int n){
switch (n){
case 1:{
printf("1");
break;
}
case 2:{
printf("2");
break;
}
default:{
printf("default");
break;
}
}
}
int main(){
Fun(1);
return 0;
}
汇编代码
被调函数开头汇编代码
switch汇编代码
;提升堆栈
009F3C00 push ebp
009F3C01 mov ebp,esp
009F3C03 sub esp,0C4h
;保存现场
009F3C09 push ebx
009F3C0A push esi
009F3C0B push edi
;初始化堆栈空间
009F3C0C lea edi,[ebp+FFFFFF3Ch]
009F3C12 mov ecx,31h
009F3C17 mov eax,0CCCCCCCCh
009F3C1C rep stos dword ptr es:[edi]
;switch功能代码
009F3C1E mov eax,dword ptr [ebp+8]
009F3C21 mov dword ptr [ebp+FFFFFF3Ch],eax
009F3C27 cmp dword ptr [ebp+FFFFFF3Ch],1 ;判断
009F3C2E je 009F3C3B ;跳转
009F3C30 cmp dword ptr [ebp+FFFFFF3Ch],2 ;判断
009F3C37 je 009F3C54 ;跳转
009F3C39 jmp 009F3C6D
009F3C3B mov esi,esp
009F3C3D push 9F58A8h
009F3C42 call dword ptr ds:[009F9114h]
009F3C48 add esp,4
009F3C4B cmp esi,esp
009F3C4D call 009F113B
009F3C52 jmp 009F3C84
009F3C54 mov esi,esp
009F3C56 push 9F58ACh
009F3C5B call dword ptr ds:[009F9114h]
009F3C61 add esp,4
009F3C64 cmp esi,esp
009F3C66 call 009F113B
009F3C6B jmp 009F3C84
009F3C6D mov esi,esp
009F3C6F push 9F59C4h
009F3C74 call dword ptr ds:[009F9114h]
009F3C7A add esp,4
009F3C7D cmp esi,esp
009F3C7F call 009F113B
;switch功能结束,开始恢复现场
009F3C84 pop edi
009F3C85 pop esi
009F3C86 pop ebx
009F3C87 add esp,0C4h
009F3C8D cmp ebp,esp
009F3C8F call 009F113B
009F3C94 mov esp,ebp
009F3C96 pop ebp
009F3C97 ret
通过上述图片可以看出,如果case个数较少,那么形成的汇编代码与if分支语句效果等同,效率上并不会提高很多。
第二种:case常量连续型,但case语句较多
C语言代码
#include
void Fun(int n){
switch (n){
case 1:{
printf("1");
break;
}
case 2:{
printf("2");
break;
}
case 3:{
printf("3");
break;
}
case 4:{
printf("4");
break;
}
case 5:{
printf("5");
break;
}
default:{
printf("default");
break;
}
}
}
int main(){
Fun(1);
return 0;
}
汇编代码
在看switch汇编之前,需要了解下大表的概念,如果case的分支过多,那么编译器将会生成一个记录各个case地址的大表。
接下来让我们观察大表的具体值,将大表基址拖拽到内存模块中
根据大表进行跳转
综上,switch产生的汇编代码如下
;提升堆栈
006A3C00 push ebp
006A3C01 mov ebp,esp
006A3C03 sub esp,0C4h
;保存现场
006A3C09 push ebx
006A3C0A push esi
006A3C0B push edi
;初始化堆栈
006A3C0C lea edi,[ebp+FFFFFF3Ch]
006A3C12 mov ecx,31h
006A3C17 mov eax,0CCCCCCCCh
006A3C1C rep stos dword ptr es:[edi]
;函数功能代码
006A3C1E mov eax,dword ptr [ebp+8] ;将函数参数放入eax寄存器中
006A3C21 mov dword ptr [ebp+FFFFFF3Ch],eax
006A3C27 mov ecx,dword ptr [ebp+FFFFFF3Ch]
006A3C2D sub ecx,1 ;参数值减一
006A3C30 mov dword ptr [ebp+FFFFFF3Ch],ecx
006A3C36 cmp dword ptr [ebp+FFFFFF3Ch],4 ;将处理后的参数值与常数4进行对比(4是case常量中的最大值减一)
006A3C3D ja 006A3CCD ;如果比较结果是大于,证明要执行的代码块不存在,因此直接跳转到default
006A3C43 mov edx,dword ptr [ebp+FFFFFF3Ch]
006A3C49 jmp dword ptr [edx*4+006A3CF8h] ;否则根据大表计算应该跳到哪个地址
;对应的各个case代码块
$LN6:
006A3C50 mov esi,esp
006A3C52 push 6A58A8h
006A3C57 call dword ptr ds:[006A9114h]
006A3C5D add esp,4
006A3C60 cmp esi,esp
006A3C62 call 006A113B
006A3C67 jmp 006A3CE4
$LN5:
006A3C69 mov esi,esp
006A3C6B push 6A58ACh
006A3C70 call dword ptr ds:[006A9114h]
006A3C76 add esp,4
006A3C79 cmp esi,esp
006A3C7B call 006A113B
006A3C80 jmp 006A3CE4
$LN4:
006A3C82 mov esi,esp
006A3C84 push 6A58B0h
006A3C89 call dword ptr ds:[006A9114h]
006A3C8F add esp,4
006A3C92 cmp esi,esp
006A3C94 call 006A113B
006A3C99 jmp 006A3CE4
$LN3:
006A3C9B mov esi,esp
006A3C9D push 6A58B4h
006A3CA2 call dword ptr ds:[006A9114h]
006A3CA8 add esp,4
006A3CAB cmp esi,esp
006A3CAD call 006A113B
006A3CB2 jmp 006A3CE4
$LN2:
006A3CB4 mov esi,esp
006A3CB6 push 6A5918h
006A3CBB call dword ptr ds:[006A9114h]
006A3CC1 add esp,4
006A3CC4 cmp esi,esp
006A3CC6 call 006A113B
006A3CCB jmp 006A3CE4
006A3CCD mov esi,esp
006A3CCF push 6A591Ch
006A3CD4 call dword ptr ds:[006A9114h]
006A3CDA add esp,4
006A3CDD cmp esi,esp
006A3CDF call 006A113B
;恢复现场,结束函数调用
006A3CE4 pop edi
006A3CE5 pop esi
006A3CE6 pop ebx
006A3CE7 add esp,0C4h
006A3CED cmp ebp,esp
006A3CEF call 006A113B
006A3CF4 mov esp,ebp
006A3CF6 pop ebp
006A3CF7 ret
006A3CF8 push eax
006A3CF9 cmp al,6Ah
006A3CFB add byte ptr [ecx+3Ch],ch
006A3CFE push 0
006A3D00 cmp byte ptr [edx+ebp*2],0
006A3D04 wait
006A3D05 cmp al,6Ah
006A3D07 add byte ptr [esp+edi+CCCC006Ah],dh
第三种:case常量不连续型
1)大表中缺少的位置可能由default的地址来填充
#include
void Fun(int n){
switch (n){
case 1:{
printf("1");
break;
}
case 2:{
printf("2");
break;
}
case 3:{
printf("3");
break;
}
case 4:{
printf("4");
break;
}
case 5:{
printf("5");
break;
}
case 6:{
printf("6");
break;
}
case 10:{
printf("10");
break;
}
default:{
printf("default");
break;
}
}
}
int main(){
Fun(1);
return 0;
}
将大表基址拖拽到内存模块中,查看内存中大表的存储内容
其中一部分直接指向对应的case地址
但是我们能够看见,其中一部分缺失的case常量,如case 7,case 8,case 9三个位置的值相同,都指向了default代码块的地址
综上:如果case常量不连续个数比较少,那么缺少的位置可由default代码块地址填充
2)大表中缺少的位置过多时,产生小表,利用小表来对大表进行优化
大表:记录了每一个case块的地址,一条记录占用四个字节
小表:记录了使用第几个大表数据,一条记录占用一个字节
#include
void Fun(int n){
switch (n){
case 1:{
printf("1");
break;
}
case 5:{
printf("5");
break;
}
case 6:{
printf("6");
break;
}
case 10:{
printf("10");
break;
}
default:{
printf("default");
break;
}
}
}
int main(){
Fun(1);
return 0;
}
大表(低地址)一般在小表(高地址)上方位置
由于switch中的case常量范围是case1 - case 10,再加上default默认代码块,一共是十个块,位置从0开始计数
查询顺序
1)根据 switch中的参数,计算{参数 - 1},查询小表中对应的值
2)根据小表查询结果,在大表中查询case对应地址
3)根据case地址,跳转到对应代码块,执行对应代码块命令
除了上述三种情况外,switch还有一个需要注意的地方
上述实验过程,都是从1开始,那么如果出现case的范围中最小的值就比较大(如:100、101、102、103),会出现什么情况?
//case常量范围100 - 103
#include
void Fun(int n){
switch (n){
case 100:{
printf("100");
break;
}
case 101:{
printf("101");
break;
}
case 102:{
printf("102");
break;
}
case 103:{
printf("103");
break;
}
default:{
printf("default");
break;
}
}
}
int main(){
Fun(1);
return 0;
}
其中汇编代码有sub ecx,64H的一条语句,将其转换成十进制:ecx = ecx - 100
case范围中最小值是1的时候,sub eax,1H
综上可以看出在case的取值范围中,最小值在大表中永远是处在零的位置
总结
1)如果case连续且分支不多,那么汇编代码将生成类似if跳转的汇编代码
2)如果case连续但是分支很多,那么编译器将为其生成一个大表,用来记录每一个case的地址,从而通过计算,直接跳转到所需执行的命令处
3)如果case不连续(缺少的case不多),但是分支较多,那么编译器会在大表的空缺处,填写default代码块的地址
4)如果case不连续(缺少的case较多),但是分支还比较多,那么编译器会将大表进行压缩,并且在大表的下边生成一个小表,用来对大表进行存储空间的优化
5)如果case不连续(缺少的case较多),但是分支较少,那么编译器将舍弃生成大表,直接按照if跳转的形式生成汇编指令(最后一条情况未实验,感兴趣可自行实验)
应用注意
在使用switch的时候,应该尽量使case连续。如果switch的分支较少,那么可以直接考虑使用if,而不使用switch