从汇编代码看switch针对if的优化

本文通过从一个简单的程序进而一步步推导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 寄存器的意义

  1. ebp:栈底指针
  2. esp:栈顶指针
  3. eax:用于保存累加数据的寄存器
  4. ecx:特用于循环中计数的寄存器
  5. jg/ja:大于的时候进行跳转指令
  6. je:等于的时候用于跳转的指令

1.2.2 数据区的分类以及各自存储的内容

首先我们需要知道三个区以及相关数据寄存器,三个区分别是(对应的就是在下图中的结构里): a .   a.\space a. 代码区; b .   b.\space b. 静态数据区(这是不常关注的,因为里面保存的是常量等); c .   c.\space c. 动态数据区;
一个程序的整体结构

  1. 基本过程就是在代码去不断地执行,正常情况下是顺序执行,当遇到跳转指令的时候才会进行代码的跳转
  2. 而数据寄存器我们实际就可以看做是另外开辟了一些内存,但是并不需要像静态数据区那样的堆栈访问,而是直接就可以访问每一个寄存器
  3. 动态数据区的数据产生就是在代码中拥有赋值的时候就会有代码的录入,并且栈顶栈底都会有相应的变量保存在动态数据区中——实际上就是临时产生的值都会存在这个里面

1.3 栈调用过程

1.3.1 栈存储数据类型

内存数据结构示意图如下:

内存数据结构

  1. 栈区(stack):由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其 操作方式类似于数据结构中的栈。
  2. 堆区(heap) :一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。
  3. 全局区(静态区)(static):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放
  4. 文字常量区:常量字符串就是放在这里的程序结束后由系统释放
  5. 程序代码区:存放函数体的二进制代码。
栈存储数据类型
静态数据区
1.常量
2.常变量:const 变量
3.全局变量
4.静态变量
动态数据区
1.堆栈
a.堆:new和malloc得到的空间-动态变量
b.栈:函数的返回地址/参数/局部变量
2.局部变量
3.函数调用时的现场保护和返回地址等
代码区
用于存放实际代码

1.3.2 栈调用时候的具体情形

1. 全局变量m保存进行静态数据区

代码区运行至int m=10;因为前面的int fun(int a,int b)只是一个函数声明并不需要在数据里面操作什么

全局变量m的保存

2. main开始运行的准备——压栈以及清栈

在main函授开始之前,动态数据区时空白的,那么当代码进行到int main()的时候就需要进行建栈和清栈的操作。这里需要注意的是:

  1. 上面的就是高地址,下面的就是低地址——没错,就像你看到的那样,建栈的过程是从高地址到低地址;非常反人类的是:栈底是在最上面,栈顶则是一步步向下;
  2. 这里对应了三个寄存器,就是:
    1. eip永远指向代码区将要执行的下一条指令;
    2. ebp和esp用来管控栈空间:
      1. ebp指向栈底,在每次使用ebp做栈底的时候都需要将原来的值进行保存,因为在栈使用完成之后就需要恢复之前ebp的值
      2. esp指向栈顶;

main函数开始运行,进行压栈以及清栈

3. 构建main函数栈内的数据

需要注意到的是不仅仅是i和j是在main函数的栈内,其中用来传参的a和b也在栈中,还有fun的返回值以及返回地址。可以理解的是fun的返回值以及返回地址,这些都是都是在fun函数栈之外的,其实仔细细想,a、b作为参数也是函数用来调用并不是内部成员,所以也应该是main栈中。

构建main函数栈内的数据

4. fun函数栈

执行m = fun(i, j);进行的操作,相关建栈,数据计算以及清栈的操作。

  1. 建栈需要保存ebp原本的地址,因为建模需要占用ebp寄存器;

  2. 数据计算——计算变量c的值:

    计算变量c

  3. 清栈,恢复main函数调用fun函数的现场

    (动态数据区)ebp地址值出栈后,esp的指向自动指向fun函数执行后的返回地址;(代码区)之后执行ret指令,即返回指令,把地址值传给eip,使之指向fun函数执行后的返回地址,这一步就使得代码的指向就返回到了main函数中

    1. main函数的栈要恢复,包括栈顶和栈底
    2. 要找到fun函数执行后的返回地址,然后再跳转到那里继续执行。

    恢复main函数栈底
    return——返回到main函数中执行

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. 各个阶段的意义
  1. 首先是对各个寄存器的保存以及赋值;

    00401010 push ebp;进入函数后的第一件事,保存栈底指针ebp
    00401011 mov ebp, esp;调整当前栈底指针位置到栈顶
    00401016 push ebx;保存寄存器ebx
    00401017 push esi;保存寄存器esi
    00401018 push edi;保存寄存器edi
    
  2. 对栈空间的申请

    00401013 sub esp,0D8h;抬高栈顶esp,此时开辟栈空间0x40,作为局部变量的存储空间
    
  3. 对申请的栈空间对值的填充

    1. 首地址:00401019 lea edi,[ebp-0D8h]
    2. 循环次数:0040101C mov ecx,36h
    3. 每次循环初始化的值:00401021 mov eax,0CCCCCCCCh
    4. 循环上一行的代码,直到达到栈顶: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的赋值

  1. 主要使用了mov指令,其意义就是:将一个值写入寄存器中
  2. dword表示双节,也就是四个字节;
    1. 对应到这里的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. 代码1中就是对j的传递,从这里我们可以看出一个对等关系dword ptr [j]和int j,和将这个值放在eax寄存器中;
  2. 代码2中与1基本类似,只是对应的j改为了i,对应的eax寄存器改为了ecx寄存器。这两步其实就是i和j先入栈
  3. 代码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需要在在运行过程中更改它的值吗?

  • 答:
    1. edp保存的是指向的值,也就是存在其中的地址,而不是指向指向ebp的指针,这里我们很容易理解,因为正常的变量保存也是指向的值,而不是其本身的指针;
    2. 当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部分的汇编过程如下:

switch跳转结构
以间隔2018作为序列的界线
小于
1.与区间最大做比较2019
2.计算与最小相隔间隔,进行跳转
大于
直接跳转,与if一样的逻辑,需要每一个做比较

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;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值