目录
switch 与 if else
switch语句用法
switch底层汇编实现分析
switch原理总结
switch 与 if else
if else是人工优化的,而switch则是编译器进行优化的
使用场合:命中样本一致,每个case命中概率一样,case的数据必须是线性的,可以缺,但不能缺太多 而 if else 的某个分支结构的命中则是必须要经过前面的。
swich优化有4中策略,case<1,走方案一,大于3才走别的。这里先学习第二种优化
switch语句用法
基本格式
switch(表达式)
{
case 常量1:语句1
case 常量2:语句2
default:语句n
break;
}
样例
// swith语句底层
#include <stdio.h>
#include <stdlib.h>
int main()
{
int n = 3;
switch (n)
{
case 0:
printf("n == 0\r\n");
break;
case 1:
printf("n == 1\r\n");
break;
case 2:
printf("n == 2\r\n");
break;
case 3:
printf("n == 3\r\n");
break;
case 4:
printf("n == 4\r\n");
break;
case 5:
printf("n == 5\r\n");
break;
default:
printf("default\r\n");
}
system("pause");
return 0;
}
switch底层汇编实现分析
C代码如下:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int n = 3;
switch (n)
{
case 0:
printf("n == 0\r\n");
break;
case 1:
printf("n == 1\r\n");
break;
case 2:
printf("n == 2\r\n");
break;
case 3:
printf("n == 3\r\n");
break;
case 4:
printf("n == 4\r\n");
break;
case 5:
printf("n == 5\r\n");
break;
default:
printf("default\r\n");
}
system("pause");
return 0;
}
switch结构汇编代码
switch (n)
009B187C 8B 45 F8 mov eax,dword ptr [n]
009B187F 89 85 30 FF FF FF mov dword ptr [ebp-0D0h],eax
009B1885 83 BD 30 FF FF FF 05 cmp dword ptr [ebp-0D0h],5
009B188C 77 67 ja $LN9+0Fh (09B18F5h)
009B188E 8B 8D 30 FF FF FF mov ecx,dword ptr [ebp-0D0h]
009B1894 FF 24 8D 30 19 9B 00 jmp dword ptr [ecx*4+9B1930h]
{
case 0:
printf("n == 0\r\n");
009B189B 68 E0 7B 9B 00 push offset string "n == 0\r\n" (09B7BE0h)
009B18A0 E8 28 F8 FF FF call _printf (09B10CDh)
009B18A5 83 C4 04 add esp,4
break;
009B18A8 EB 58 jmp $LN9+1Ch (09B1902h)
case 1:
printf("n == 1\r\n");
009B18AA 68 EC 7B 9B 00 push offset string "n == 1\r\n" (09B7BECh)
009B18AF E8 19 F8 FF FF call _printf (09B10CDh)
009B18B4 83 C4 04 add esp,4
break;
009B18B7 EB 49 jmp $LN9+1Ch (09B1902h)
case 2:
printf("n == 2\r\n");
009B18B9 68 30 7B 9B 00 push offset string "* " (09B7B30h)
009B18BE E8 0A F8 FF FF call _printf (09B10CDh)
009B18C3 83 C4 04 add esp,4
break;
009B18C6 EB 3A jmp $LN9+1Ch (09B1902h)
case 3:
printf("n == 3\r\n");
009B18C8 68 E0 7C 9B 00 push offset string "n == 3\r\n" (09B7CE0h)
009B18CD E8 FB F7 FF FF call _printf (09B10CDh)
009B18D2 83 C4 04 add esp,4
break;
009B18D5 EB 2B jmp $LN9+1Ch (09B1902h)
case 4:
printf("n == 4\r\n");
009B18D7 68 EC 7C 9B 00 push offset string "n == 4\r\n" (09B7CECh)
009B18DC E8 EC F7 FF FF call _printf (09B10CDh)
009B18E1 83 C4 04 add esp,4
break;
009B18E4 EB 1C jmp $LN9+1Ch (09B1902h)
case 5:
printf("n == 5\r\n");
009B18E6 68 F8 7C 9B 00 push offset string "n == 5\r\n" (09B7CF8h)
009B18EB E8 DD F7 FF FF call _printf (09B10CDh)
009B18F0 83 C4 04 add esp,4
break;
009B18F3 EB 0D jmp $LN9+1Ch (09B1902h)
default:
printf("default\r\n");
009B18F5 68 04 7D 9B 00 push offset string "default\r\n" (09B7D04h)
009B18FA E8 CE F7 FF FF call _printf (09B10CDh)
009B18FF 83 C4 04 add esp,4
分析switch部分汇编代码解读
mov eax,dword ptr [n] // 把n存储的数据交给eax
mov dword ptr [ebp-0D0h],eax // 把eax寄存器的数据到以ebp-0D0h为地址的内存位置中
cmp dword ptr [ebp-0D0h],5 // 比较两个值
ja $LN9+0Fh (09B18F5h) // 地址跳转
mov ecx,dword ptr [ebp-0D0h]
jmp dword ptr [ecx*4+9B1930h] // 该指令将无条件跳转到这个地址,继续执行后续的指令
更加具体的细节
- dword ptr是一个指令修饰符,用于指示内存操作数的大小为双字(32位)。
- eax是寄存器,用于存储数据
- [n] 指n的地址
- [ebp-0D0h] 表示以基址寄存器 ebp 减去偏移 0D0h 所得到的内存地址。
- ebp 是栈基指针寄存器,-0D0h 是一个偏移量,用于定位相对于 ebp 的内存位置。
- ecx 寄存器经常被用作循环计数器,特别是在汇编语言中经常使用 ecx 寄存器来实现循环操作。它还可以用于存储临时数据、中间结果以及其他需要在计算过程中使用的值。
- [ecx*4+9B1930h]:将寄存器
ecx
中的值乘以 4,并加上偏移量9B1930h
,得到一个内存地址。然后,该指令将无条件跳转到这个地址,继续执行后续的指令。 ja $LN9+0Fh (09B18F5h)
是一个条件跳转指令,用于根据处理器标志寄存器中的条件进行无条件跳转。ja
是无符号大于条件跳转指令,当处理器标志寄存器中的条件满足无符号大于(CF=0且ZF=0)时,会执行跳转操作。ebp
寄存器通常用于存储当前函数的基址指针,指向当前函数的栈帧的底部。栈帧是用于存储局部变量、函数参数和返回地址等信息的一块内存区域。- 这里数据后面的h表示是16进制
swith汇编处理的逻辑:
- 根据传入的n值,mov到指定的地址
- 进行有条件的比较,根据条件跳转地址
- 再把n的值到ecx里面
- 经过计算得到一个偏移的地址并跳转
- 通过内存最后内存地址计算,并且在反汇编页面进行跳转到case 3的代码位置
因为case的汇编代码都大差不差,所以直接阅读case 3的代码
push offset string "n == 3\r\n" (09B7CE0h) // 把字符串的地址送到栈中
call _printf (09B10CDh) // 将当前指令的下一条指令的地址(即返回地址)推送到栈中,并跳转到 _printf 函数的入口地址开始执行。
add esp,4 // 将栈指针 esp 的值增加 4 字节(一个字)
break;
jmp $LN9+1Ch (09B1902h) // 无条件地跳转到地址为 09B1902h 的目标位置继续执行后续的指令。
更加具体的细节:
esp
是x86架构中的栈指针寄存器,也称为堆栈指针寄存器(Stack Pointer Register)。栈是一种数据结构,用于存储临时数据、函数参数、返回地址和局部变量等信息。- 使用
offset
运算符可以获取一个标号或字符串的内存地址,并将该地址推送(存储)到栈中。这在处理字符串和传递参数时非常常见。 call
是一个用于调用函数或子程序的指令。当执行call
指令时,它会将当前指令的下一条指令的地址(即返回地址)推送到栈中,并跳转到指定的函数入口地址开始执行。也就说会把下一条add指令移动栈中作为参数传递
case部分处理逻辑:
- 把字符串的地址送到栈中、
- call指令调用输出的函数,并把下一条的add指令放到栈中
- 执行函数后,无条件跳转到别的地址
switch原理总结
通过阅读switch汇编实现源码,可以总结出它的实现原理
首先,它实现了一个表结构,用来记录每个case汇编代码的首地址
这个表结构如何生成?
是根据case的值确定下标,所以就解释了case的值为什么必须要求是整形或者类整形的原因了。
case的值确定下标,存储case代码的地址相对应
下图是在内存中表的存储
之后switch会把case的值*4(因为4个字节存储地址)作为0x9b1930的偏移量到对应的case地址。之后就是case内部具体实现逻辑了。
这个图的意思是要说:
表建立的下标必然是根据case从小到大的线性排布建立的,但是代码的位置并不一样,因为代码的书写上并不一定是按照的case从小到大线性排布的。
这是第一次阅读底层汇编实现代码,感觉还有很多疏漏之处,比如:函数调用时的栈处理流程和内存变化,printf函数实现原理,包括汇编指令的了解都存在细节上的问题。
正所谓万事开头难,相信随着后续的学习,认识不断增加,会再次更深入了解switch实现原理。