switch的汇编
switch语句的汇编总共有4种
形式一
当分支少于4种时,其汇编本质上与if else相同,如下:
c代码:
void fun(int x) {//传入2
switch(x) {
case 1:
printf("1");
break;
case 2:
printf("2");
break;
case 3:
printf("3");
break;
default:
printf("error");
break;
}
}
汇编(主要部分):
0040D758 mov eax,dword ptr [ebp+8]
0040D75B mov dword ptr [ebp-4],eax
0040D75E cmp dword ptr [ebp-4],1
0040D762 je fun+32h (0040d772)
0040D764 cmp dword ptr [ebp-4],2
0040D768 je fun+41h (0040d781)
0040D76A cmp dword ptr [ebp-4],3
0040D76E je fun+50h (0040d790)
0040D770 jmp fun+5Fh (0040d79f)
0040D772 push offset string "1" (00422fa8)
0040D777 call printf (0040d6c0)
0040D77C add esp,4
0040D77F jmp fun+6Ch (0040d7ac)
0040D781 push offset string "2" (00422fa4)
0040D786 call printf (0040d6c0)
0040D78B add esp,4
0040D78E jmp fun+6Ch (0040d7ac)
0040D790 push offset string "%d " (0042210c)
0040D795 call printf (0040d6c0)
0040D79A add esp,4
0040D79D jmp fun+6Ch (0040d7ac)
0040D79F push offset string "error" (00422f9c)
显然,对标与if else语句,当分支少于4种时,使用switch语句将没有意义。
形式二
而当分支情况大于等于4种时且case后的常量值连续,情况会完全不同:
c代码:
void fun(int x) {//传入2
switch(x) {
case 1:
printf("1");
break;
case 2:
printf("2");
break;
case 3:
printf("3");
break;
case 4:
printf("4");
break;
default:
printf("error");
break;
}
}
汇编(主要部分):
0040D818 mov eax,dword ptr [ebp+8]
0040D81B mov dword ptr [ebp-4],eax
0040D81E mov ecx,dword ptr [ebp-4]
0040D821 sub ecx,1
0040D824 mov dword ptr [ebp-4],ecx
0040D827 cmp dword ptr [ebp-4],3
0040D82B ja $L539+0Fh (0040d873)
0040D82D mov edx,dword ptr [ebp-4]
0040D830 jmp dword ptr [edx*4+40D891h]
$L533:
0040D837 push offset string "1" (00422fac)
0040D83C call printf (0040d6c0)
0040D841 add esp,4
0040D844 jmp $L539+1Ch (0040d880)
$L535:
0040D846 push offset string "2" (00422fa8)
0040D84B call printf (0040d6c0)
0040D850 add esp,4
0040D853 jmp $L539+1Ch (0040d880)
$L537:
0040D855 push offset string "3" (00422fa4)
0040D85A call printf (0040d6c0)
0040D85F add esp,4
0040D862 jmp $L539+1Ch (0040d880)
$L539:
0040D864 push offset string "%d " (0042210c)
0040D869 call printf (0040d6c0)
0040D86E add esp,4
0040D871 jmp $L539+1Ch (0040d880)
0040D873 push offset string "error" (00422f9c)
0040D878 call printf (0040d6c0)
可以看到,编译器在将参数2传入ecx后减1,再与3比较,若大于3,则直接输出error,否则通过表达式[edx*4+40D891h]进行跳转。
我们通过看具体地址存储的内容,可以知道:
[edx*4+40D891h]分别代表了每个分支的首地址(edx=0,1,2)。
switch语句的汇编过程:
编译器会在编译时生成一个大表,用来存储每个分支的首地址,通过特定的表达式来直接进行跳转(表达式是否固定?)。
当case后的常量值可以无序,不影响大表的生成,如下:
void fun(int x) {//传入2
switch(x) {
case 4:
printf("4");
break;
case 2:
printf("2");
break;
case 3:
printf("3");
break;
case 1:
printf("1");
break;
default:
printf("error");
break;
}
}
所以,在分配每个分支的地址时,编译器会采取某种排序算法。
形式三
当分支情况大于等于4种时且case后的常量值总体不连续但有规律,情况又会不同:
c代码:
void fun(int x){
switch(x){
case 1:
printf("1");
break;
case 4:
printf("4");
break;
case 5:
printf("5");
break;
case 6:
printf("6");
break;
case 7:
printf("7");
break;
case 8:
printf("8");
break;
case 9:
printf("9");
break;
case 10:
printf("10");
break;
default:
printf("error");
break;
}
}
把等于2和3的情况删除后,case 2和case 3对应的地址上的地址值将被替换为default的地址值(default没写的时候或许是跳出switch)。
0040D830 jmp dword ptr [edx*4+40D8BEh]
显然当删除的情况过多时,将删除的情况对应的地址替换成default的地址这种做法过于浪费空间。此时,会有另外的做法:
当我删除6种情况(不同编译器上限不同)
void fun(int x){
switch(x){
case 1:
printf("1");
break;
case 8:
printf("8");
break;
case 9:
printf("9");
break;
case 10:
printf("10");
break;
default:
printf("error");
break;
}
}
对应汇编(主要部分):
0040D818 mov eax,dword ptr [ebp+8]
0040D81B mov dword ptr [ebp-4],eax
0040D81E mov ecx,dword ptr [ebp-4]
0040D821 sub ecx,1
0040D824 mov dword ptr [ebp-4],ecx
0040D827 cmp dword ptr [ebp-4],9
0040D82B ja $L539+0Fh (0040d87b)
0040D82D mov eax,dword ptr [ebp-4]
0040D830 xor edx,edx
0040D832 mov dl,byte ptr (0040d8ad)[eax]
0040D838 jmp dword ptr [edx*4+40D899h]
(0040d8ad)即为小表,且小表紧挨着大表,此时大表中的被替换的部分会消除,目的是为了节省内存。操作过程,假设我们传入的参数为2:
减1后传给eax,edx清0,查小表,将小表中第eax个字节(dl是8位)位置的值传给edx,再去查询大表,因为case 2是不存在的,所以会直接跳到default处。
形式四
当分支情况大于等于4种时且case后的常量值毫无规律时。此时编译器会按if else语句处理。
do while的汇编
c代码(传入参数2):
void fun(int x){
do {
printf("hello!");
x--;
}while(x>0);
}
汇编:
0040D818 push offset string "hello!" (00422f9c)
0040D81D call printf (0040d6c0)
0040D822 add esp,4
0040D825 mov eax,dword ptr [ebp+8]
0040D828 sub eax,1
0040D82B mov dword ptr [ebp+8],eax
0040D82E cmp dword ptr [ebp+8],0
0040D832 jg fun+18h (0040d818)
do while语句有下面的特点:
- 根据条件跳转指令所跳转到的地址,可以得到循环语句块的起始地址
- 条件跳转的逻辑与源码相同
while的汇编
c代码(传入参数2)
while(x>0) {
printf("hello!");
x--;
}
汇编:
0040D818 cmp dword ptr [ebp+8],0
0040D81C jle fun+36h (0040d836)
0040D81E push offset string "hello!" (00422f9c)
0040D823 call printf (0040d6c0)
0040D828 add esp,4
0040D82B mov eax,dword ptr [ebp+8]
0040D82E sub eax,1
0040D831 mov dword ptr [ebp+8],eax
0040D834 jmp fun+18h (0040d818)
while语句有下面的特点:
- 根据条件跳转指令所跳转到的地址,可以得到循环语句块的结束地址;
- 根据jmp 指令所跳转到的地址,可以得到循环语句块的起始地址
- 在还原while比较时,条件跳转的逻辑与源码相反
for循环的汇编
c代码(传入参数2)
void fun(int x){
for(int i=0; i<x; i++)
printf("hello!");
}
汇编:
0040D818 mov dword ptr [ebp-4],0
0040D81F jmp fun+2Ah (0040d82a)
0040D821 mov eax,dword ptr [ebp-4]
0040D824 add eax,1
0040D827 mov dword ptr [ebp-4],eax
0040D82A mov ecx,dword ptr [ebp-4]
0040D82D cmp ecx,dword ptr [ebp+8]
0040D830 jge fun+41h (0040d841)
0040D832 push offset string "hello!" (00422f9c)
0040D837 call printf (0040d6c0)
0040D83C add esp,4
0040D83F jmp fun+21h (0040d821)
0040D841 pop edi
0040D842 pop esi
0040D843 pop ebx
for循环语句有下面的特点:
第一个jmp指令之前为赋初值部分
第一个jmp指令所跳转的地址为循环条件判定部分起始
判断条件后面的跳转指令条件成立时跳转的循环体外面
条件判断跳转指令所指向的地址上面有一个jmp,jmp地址为表达式3(第二个分号后的表达式)的起始位置
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1666739907@qq.com