本文通过从一个简单的程序进而一步步推导switch针对if的优化
一个简单的程序汇编详解
1.1 源代码
int fun(int a, int b);
int m=10;
int main()
{
inti=4;
int j-5;
m = fun(i, j);
return 0
}
int fun(int a, int b)
{
int c=0; c-a+b; return c;
}
1.2 基础知识储备
1.2.1 寄存器的意义
- ebp:栈底指针
- esp:栈顶指针
- eax:用于保存累加数据的寄存器
- ecx:特用于循环中计数的寄存器
- jg/ja:大于的时候进行跳转指令
- je:等于的时候用于跳转的指令
1.2.2 数据区的分类以及各自存储的内容
首先我们需要知道三个区以及相关数据寄存器,三个区分别是(对应的就是在下图中的结构里):
a
.
a.\space
a. 代码区;
b
.
b.\space
b. 静态数据区(这是不常关注的,因为里面保存的是常量等);
c
.
c.\space
c. 动态数据区;
- 基本过程就是在代码去不断地执行,正常情况下是顺序执行,当遇到跳转指令的时候才会进行代码的跳转
- 而数据寄存器我们实际就可以看做是另外开辟了一些内存,但是并不需要像静态数据区那样的堆栈访问,而是直接就可以访问每一个寄存器
- 动态数据区的数据产生就是在代码中拥有赋值的时候就会有代码的录入,并且栈顶栈底都会有相应的变量保存在动态数据区中——实际上就是临时产生的值都会存在这个里面
1.3 栈调用过程
1.3.1 栈存储数据类型
内存数据结构示意图如下:
- 栈区(stack):由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其 操作方式类似于数据结构中的栈。
- 堆区(heap) :一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。
- 全局区(静态区)(static):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放。
- 文字常量区:常量字符串就是放在这里的程序结束后由系统释放
- 程序代码区:存放函数体的二进制代码。
1.3.2 栈调用时候的具体情形
1. 全局变量m保存进行静态数据区
代码区运行至int m=10
;因为前面的int fun(int a,int b)
只是一个函数声明并不需要在数据里面操作什么
2. main开始运行的准备——压栈以及清栈
在main函授开始之前,动态数据区时空白的,那么当代码进行到int main()的时候就需要进行建栈和清栈的操作。这里需要注意的是:
- 上面的就是高地址,下面的就是低地址——没错,就像你看到的那样,建栈的过程是从高地址到低地址;非常反人类的是:栈底是在最上面,栈顶则是一步步向下;
- 这里对应了三个寄存器,就是:
- eip永远指向代码区将要执行的下一条指令;
- ebp和esp用来管控栈空间:
- ebp指向栈底,在每次使用ebp做栈底的时候都需要将原来的值进行保存,因为在栈使用完成之后就需要恢复之前ebp的值。
- esp指向栈顶;
3. 构建main函数栈内的数据
需要注意到的是不仅仅是i和j是在main函数的栈内,其中用来传参的a和b也在栈中,还有fun的返回值以及返回地址。可以理解的是fun的返回值以及返回地址,这些都是都是在fun函数栈之外的,其实仔细细想,a、b作为参数也是函数用来调用并不是内部成员,所以也应该是main栈中。
4. fun函数栈
执行m = fun(i, j);
进行的操作,相关建栈,数据计算以及清栈的操作。
-
建栈需要保存ebp原本的地址,因为建模需要占用ebp寄存器;
-
数据计算——计算变量c的值:
-
清栈,恢复main函数调用fun函数的现场
(动态数据区)ebp地址值出栈后,esp的指向自动指向fun函数执行后的返回地址;(代码区)之后执行ret指令,即返回指令,把地址值传给eip,使之指向fun函数执行后的返回地址,这一步就使得代码的指向就返回到了main函数中
- main函数的栈要恢复,包括栈顶和栈底
- 要找到fun函数执行后的返回地址,然后再跳转到那里继续执行。
int fun(int a, int b)
{
011F1E30 push ebp
011F1E31 mov ebp,esp
011F1E33 sub esp,0CCh
011F1E39 push ebx
011F1E3A push esi
011F1E3B push edi
011F1E3C lea edi,[ebp-0CCh]
011F1E42 mov ecx,33h
011F1E47 mov eax,0CCCCCCCCh
011F1E4C rep stos dword ptr es:[edi]
011F1E4E mov ecx,offset _3D002CC6_test2@cpp (011FC027h)
011F1E53 call @__CheckForDebuggerJustMyCode@4 (011F120Dh)
int c = 0; c = a * b; return c;
011F1E58 mov dword ptr [c],0
011F1E5F mov eax,dword ptr [a]
011F1E62 imul eax,dword ptr [b]
011F1E66 mov dword ptr [c],eax
011F1E69 mov eax,dword ptr [c]
}
1.4 汇编的代码拆解
1.4.1 对栈的申请以及赋初始值
1. 各个阶段的意义
-
首先是对各个寄存器的保存以及赋值;
00401010 push ebp;进入函数后的第一件事,保存栈底指针ebp 00401011 mov ebp, esp;调整当前栈底指针位置到栈顶 00401016 push ebx;保存寄存器ebx 00401017 push esi;保存寄存器esi 00401018 push edi;保存寄存器edi
-
对栈空间的申请
00401013 sub esp,0D8h;抬高栈顶esp,此时开辟栈空间0x40,作为局部变量的存储空间
-
对申请的栈空间对值的填充
- 首地址:
00401019 lea edi,[ebp-0D8h]
- 循环次数:
0040101C mov ecx,36h
- 每次循环初始化的值:
00401021 mov eax,0CCCCCCCCh
- 循环上一行的代码,直到达到栈顶:
00401026 rep stos dword ptr[edi]
00401019 lea edi,[ebp-0D8h];//取出此函数可用栈空间首地址 0040101C mov ecx,36h;//设置ecx为0x10 00401021 mov eax,0CCCCCCCCh;//将局部变量初始化为0CCCCCCCCh;根据ecx的值,将eax中的内容,以4字节为单位写到edi指向的内存中 00401026 rep stos dword ptr[edi];//将上述代码循环到edi对应的变量次数
- 首地址:
2. 整体代码
00401010 push ebp;//进入函数后的第一件事,保存栈底指针ebp
00401011 mov ebp, esp;//调整当前栈底指针位置到栈顶
00401013 sub esp,0D8h;//抬高栈顶esp,此时开辟栈空间0x40,作为局部变量的存储空间
00401016 push ebx;//保存寄存器ebx
00401017 push esi;//保存寄存器esi
00401018 push edi;//保存寄存器edi
00401019 lea edi,[ebp-0D8h];//取出此函数可用栈空间首地址
0040101C mov ecx,36h;//设置ecx为0x10
00401021 mov eax,0CCCCCCCCh;//将局部变量初始化为0CCCCCCCCh;根据ecx的值,将eax中的内容,以4字节为单位写到edi指向的内存中
00401026 rep stos dword ptr[edi]//将上述代码循环到edi对应的变量次数
1.4.2 对申请的栈检查
由于有了这部分对栈的检查函数,所以栈的内存大小会在原有的基础上多多一些大小。
00D0256E mov ecx,offset _3D002CC6_test2@cpp (0D0C027h)
00D02573 call @__CheckForDebuggerJustMyCode@4 (0D0120Dh)
1.4.3 对变量i和j的赋值
- 主要使用了mov指令,其意义就是:将一个值写入寄存器中
- dword表示双节,也就是四个字节;
- 对应到这里的a由于是int类型,所以对应的就是四个字节。
int i = 4;
00D02578 mov dword ptr [i],4
int j = 5;
00D0257F mov dword ptr [j],5
1.4.4 函数fun的调用
eax和ecx分别是累加寄存器和计数寄存器,但是平常也可以用作数据寄存器,这里用做了函数的参数传递值
- 代码1中就是对j的传递,从这里我们可以看出一个对等关系
dword ptr [j]和int j
,和将这个值放在eax寄存器中; - 代码2中与1基本类似,只是对应的j改为了i,对应的eax寄存器改为了ecx寄存器。这两步其实就是i和j先入栈
- 代码3中调用函数栈,然后fun函数执行之后,就会进行对函数栈的内存释放,这里也就是对参数a和b(变量i和j对应的传参)的释放
m = fun(i, j);
//代码1
00D02586 mov eax,dword ptr [j]
00D02589 push eax
//代码2
00D0258A mov ecx,dword ptr [i]
00D0258D push ecx
//代码3
00D0258E call fun (0D0137Ah)
00D02593 add esp,8
//eax为加减乘除算法中的缺省寄存器,所以i+j的计算结果是保存在这个寄存器里面的
00D02596 mov dword ptr [m (0D0A014h)],eax
1.4.5 main函数返回
//如果是return 1的时候就是下面的结果了
return 1;
00D6259B mov eax,1
//如果是return 0的情况
return 0;
00D0259B xor eax,eax
1.5 对应的汇编代码
问题:ebp需要在在运行过程中更改它的值吗?
- 答:
- edp保存的是指向的值,也就是存在其中的地址,而不是指向指向ebp的指针,这里我们很容易理解,因为正常的变量保存也是指向的值,而不是其本身的指针;
- 当ebp指向不同的栈的时候就需要对值进行修改,但是在修改之前需要把之前对应的值进行保存下来,这样在当前栈调用完成之后就能够很轻松地进行恢复场景。
#include<iostream>
int fun(int a, int b);
int m = 10;
int main()
{
00D02550 push ebp
00D02551 mov ebp,esp
00D02553 sub esp,0D8h
00D02559 push ebx
00D0255A push esi
00D0255B push edi
00D0255C lea edi,[ebp-0D8h]
00D02562 mov ecx,36h
00D02567 mov eax,0CCCCCCCCh
00D0256C rep stos dword ptr es:[edi]
00D0256E mov ecx,offset _3D002CC6_test2@cpp (0D0C027h)
00D02573 call @__CheckForDebuggerJustMyCode@4 (0D0120Dh)
int i = 4;
00D02578 mov dword ptr [i],4
int j = 5;
00D0257F mov dword ptr [j],5
m = fun(i, j);
00D02586 mov eax,dword ptr [j]
00D02589 push eax
00D0258A mov ecx,dword ptr [i]
00D0258D push ecx
00D0258E call fun (0D0137Ah)
00D02593 add esp,8
00D02596 mov dword ptr [m (0D0A014h)],eax
return 0;
00D0259B xor eax,eax
}
00D0259D pop edi
00D0259E pop esi
00D0259F pop ebx
00D025A0 add esp,0D8h
00D025A6 cmp ebp,esp
00D025A8 call __RTC_CheckEsp (0D01217h)
00D025AD mov esp,ebp
00D025AF pop ebp
00D025B0 ret
2. switch汇编详解
switch的优化就是当出现很多case的时候,会建立一个长达208的序列,且是连续的,其意义就相当于建立208个case,所以它会与最大的做一个比较看是否超过了,然后将最小的case与变量的值做一个比较,直接跳转相隔的间距就行。
注意:这里比if运行更快的地方就是因为做了一个序列,但是如果case很少编译器就不会做这个序列,实际效果就和if一样了。
其中case部分的汇编过程如下:
2.1 汇编代码
switch (a)
0135262F mov eax,dword ptr [a]
01352632 mov dword ptr [ebp-0D0h],eax
01352638 cmp dword ptr [ebp-0D0h],7E9h //2019→2019-2012=7间距
01352642 jg main+80h (01352680h) //表示大于的话就进行跳转
01352644 cmp dword ptr [ebp-0D0h],7E9h
0135264E je $LN10+1Ch (01352783h) //等于就进行跳转
01352654 mov ecx,dword ptr [ebp-0D0h]
0135265A sub ecx,7DCh //2012
01352660 mov dword ptr [ebp-0D0h],ecx
01352666 cmp dword ptr [ebp-0D0h],7
0135266D ja $LN10+50h (013527B7h) //大于进行跳转
01352673 mov edx,dword ptr [ebp-0D0h]
01352679 jmp dword ptr [edx*4+13527D0h]
01352680 cmp dword ptr [ebp-0D0h],908h //2312→2312-2012=300>208
0135268A je $LN10+36h (0135279Dh) //
01352690 jmp $LN10+50h (013527B7h)
2.2 源码
int main() {
int a{2015};
switch (a)
{
case 2012:
cout << "2012" << endl;
break;
case 2013:
cout << "2018" << endl;
break;
case 2014:
cout << endl;
break;
case 2015:
cout << endl;
break;
case 2016:
cout << endl;
break;
case 2017:
cout << endl;
break;
case 2019:
cout << endl;
break;
case 2025:
cout << endl;
case 2312:
cout << endl;
default:
break;
}
return 0;
}