逆向随笔 - switch 语句深入分析
switch case 语句在c语言里还是比較简单的。可是被编译出来之后,优化结果往往让人非常疑惑。全然看不懂,以下我们一次次的尝试,看看编译器究竟把switch语句变成什么样了。
① 先上个最简单的:
switch ( argc )
{
case 10:
printf("case 10 ! \r\n");
break; case 11:
printf("case 11 ! \r\n");
break; default:
printf("default ! \r\n");
break;
} getchar();
丢进OD里。看下反汇编代码:
第三行開始,取值到eax中
eax -= 10 ( 0xA )
if ( eax == 0 ) // 假设 eax - 10 == 0,直接能够得出结论 eax == 10
je 0x002C103E
else
{
eax--;
if ( eax == 0 ) // 刚上面 eax - 10 了。这里又减 1,一起就是 假设 eax - 11 == 0, 那么 eax == 11
je 0x002C1026
else
default
}
仅仅有少数分支且case的值连续的时候,会用被推断的值 - 最小值,然后 dec 减1。je 推断
② 少数分支,但值不连续的时候
switch ( argc )
{
case 10:
printf("case 10 ! \r\n");
break; case 100:
printf("case 100 ! \r\n");
break; default:
printf("default ! \r\n");
break;
} getchar();
反汇编:
我们能够看到。直接就是cmp , je。类似于 if else 结构
仅仅有少数分支。case的值不连续的时候,直接cmp , je
③ 当分支数量大于3个且连续的时候
switch ( argc )
{
case 10:
printf("case 10 ! \r\n");
break; case 11:
printf("case 11 ! \r\n");
break; case 12:
printf("case 12 ! \r\n");
break; case 13:
printf("case 13 ! \r\n");
break; default:
printf("default ! \r\n");
break;
} getchar();
反汇编:
依然是第三行開始,这次貌似代码不太一样了,没错,这又是一个新姿势了
eax = eax - 10 ( 0xA )
cmp eax, 3 这里是什么意思呢??? 为什么突然跟3比較?为嘛不是 4,5,6 ? 原来,这个时候为了达到更好的性能。编译器替我们生成了一张表。跳转表。这也是switch语句的精髓所在
大跳转表(为嘛叫大表。后面解释),事实上就是一个地址数组
下标范围:case最大值 - case最小值
大小:case最大值 - case最小值 + 1
我们看后面的寻址方式,jmp dword ptr ds:[ eax*4 + 0xFC1090 ]。典型的数组寻址,这个0xFC1090就是跳转表首地址,我们看看这个表,上图红色选中部分。我们发现里面存储的值刚好是case的地址。我们理清下思路:
值 - case中的最小值 得到大表的索引。假设这个索引不在大表下标范围内,ja (无符号大于跳转)到 default,否则。jmp dword ptr ds:[ eax*4 + 0xFC1090 ],用这个索引在大表中取得 case 相应地址。直接过去。
这个大表是编译器生成。我们不用去管。至于怎么生成?大家能够自己来尝试实现一下。
④ 当分支数量大于3个且部分不连续的时候(差值较小)
switch ( argc )
{
case 10:
printf("case 10 ! \r\n");
break; case 11:
printf("case 11 ! \r\n");
break; case 13:
printf("case 13 ! \r\n");
break; case 15:
printf("case 12 ! \r\n");
break; default:
printf("default ! \r\n");
break;
} getchar();
反汇编:
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvQW50aUhpcHM=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast">
大表大小: 15 - 10 + 1 = 6 ,这个时候我们也仅仅case了4个值,可是大表仍然被补齐成6个了。观察发现,中间缺少的值被补成default
当我们的值为14时,14 - 0xA = 4, 4 < 5, [ 4*4 + 0xFC1090 ] = 0x00041075 -> default,是不是非常机智。
⑤ 当分支数量大于3个且部分不连续的时候(差值较大)
switch ( argc )
{
case 10:
printf("case 10 ! \r\n");
break; case 11:
printf("case 11 ! \r\n");
break; case 12:
printf("case 12 ! \r\n");
break; case 19:
printf("case 19 ! \r\n");
break; default:
printf("default ! \r\n");
break;
} getchar();
反汇编:
这个时候我们的两个值的差值是7。这个时候发现又不一样了,寻址方式变成了:movzx eax, byte ptr ds:[ eax + 0xE610A8 ]。dword 变 byte 了,我们数据窗体中看一下。如图选中内容,这就是小表。每一个元素仅仅占1个字节。小表大小也是最大值 19 - 最小值 10。接下来就是 jmp dword ptr ds:[ eax*4
+ 0xE61094 ],这个当然,又是我们亲爱的大表了,联系上下文我们发现。小表里面存的就是大表的下标。为什么要这样设计呢? 由于大表占四个字节,当差距比較大时,生成的大表自然也会变得非常大,这个时候使用小表,能够更加节约内存。
⑥ 当分支数量大于3个且部分不连续的时候(差值很大)
switch ( argc )
{
case 10:
printf("case 10 ! \r\n");
break; case 11:
printf("case 11 ! \r\n");
break; case 12:
printf("case 12 ! \r\n");
break; case 600:
printf("case 600 ! \r\n");
break; default:
printf("default ! \r\n");
break;
} getchar();
反汇编:
最大值 600 - 最小值 10 = 590,。小表 590 字节 ? 这种话,小表就非常大了。所以,又进行了改变,连续的部分依旧使用第一种方法,不连续的使用 if else 结构,不再使用跳转表了。
通过对 switch case 的一步步分析。我们发现情况还是非常多的,可能不同的编译器不一样的优化。搞清楚原理。才干真正游刃有余。
逆向随笔 - switch 语句深入分析的更多相关文章
- 黑马程序员_毕向东_Java基础视频教程——switch语句练习(随笔)
switch(练习) /* if和 switch 语句很像. 具体什么场景下使用什么语句呢? 如果判断的具体数值不多且符合byte.short.int.char.String类型,虽然两个语句都可以使 ...
- switch语句的妙用
switch语句的普通用法很简单,如下: var a = 3; switch (a) { case 1: console.log(a); break; case 2: case 3: console. ...
- 106运用SWITCH语句打印星期几的单词
package com.chongrui.test;/*运用SWITCH语句打印星期几的单词 * */ public class TypeConvertion { public static void ...
- 通过goto语句学习if...else、switch语句并简单优化
goto语句在C语言中实现的就是无条件跳转,第二章一上来就介绍goto语句就是要通过goto语句来更加清楚直观的了解控制结构. 我理解的goto语句其实跟switch语句有相似之处,都是进行跳转.不同 ...
- Java中简单的操作(if语句、常用操作符、switch语句、变量赋值等)
---------------------if语句介绍--------------------------------------------------- class IfDemo { public ...
- Switch语句的case穿透
Switch语句的case穿透 一 switch语句几点说明: 1. case后面只能是常量,不能是变量,而且,多个case后面的值不能出现相同的. 2.case后面表达式可以接受: 基本数据类型,b ...
- ECMA中的switch语句
switch借鉴自其他语言,但也有自己的特色. 1.可以在switch语句中使用任何数据类型(数值.字符串.对象等),很多其他语言中只能使用数值. 2.每个case的值不一定是常量,可以是变量或者表达 ...
- switch语句下的变量声明和定义
switch语句下的变量声明和定义的问题: switch...case...语句中存在声明和定义会出现一些问题.这个由switch语法特性决定的, switch中每个case都是平等的层次,区别于一般 ...
- 透过IL看C#:switch语句(转)
透过IL看C# switch语句(上) 摘要: switch语句是 C#中常用的跳转语句,可以根据一个参数的不同取值执行不同的代码.本文介绍了当向 switch语句中传入不同类型的参数时,编译器为其生 ...
随机推荐
- 从零开始学JavaScript一(简介)
概要:JavaScript的组成. 各个组成部分的作用 . 一.JavaScript是一种专为与网页交互而设计的脚本语言,它的的组成 Javascript ECMAScript(核心) DOM ...
- 包装类、object、单例模式、final、抽象类
/*包装类*/ /* byte Byte short Short int Integer long Long char Character float Float ...
- .NET 垃圾回收机制要点整理
1. .NET资源分托管资源和非托管资源,对于托管资源,.NET GC可以很好的回收无用的垃圾,而对于非托管(例如文件访问,网络访问等)需要手动清理垃圾(显式释放). 2. 非托管资源的释放,.NET ...
- 1个比较简单的使用java反射机制获取前台数据进行数据封装的例子
//利用反射机制 和 从request获取所有参数的形式来构建查询对象 CustomerChooseSearchParma searchObject = new CustomerChooseSearc ...
- Java多线程中run(), start(), join(), wait(), yield(), sleep()的使用
Run 每个Thread中需要实现的方法, 如果直接调用的话, 会是和单线程一样的效果, 要另起线程需要使用start(). start 新起线程调用run(). 主线程不等待直接往下执行 Yield ...
- 使用SpringMVC搭建第一个项目
概述 使用SpringMVC搭建第一个项目,入门教程,分享给大家. 详细 代码下载:http://www.demodashi.com/demo/10596.html 一.概述 1.什么是Spring ...
- AJAX 跨域 CORS 解决方案
本篇文章由:http://xinpure.com/solutions-for-cross-domain-ajax-cors/ 两种跨域方法 在 Javascript 中跨域访问是比较常见的事情 就像现 ...
- 如何用python轻松破解wifi密码( 源码 )
摘要: 我得说明下这个东西一点都不高端,甚至看起来有点糟糕.而且用的是单线程~,因为过几天要搬家了,于是.. 环境准备 python2.7 凑合的linux 差不多的无线网卡 pywifi模块 弱口令 ...
- 理解Lucene中的Query
Query是一个接口,它有很多实现类. QueryParser是Query解析器,用于将一个字符串解析为一个Query对象,这个Query对象可能属于TermQuery,也可能属于PhraseQuer ...
- Linux安装JDK详细步骤(rpm方式)
1. 先从网上下载jdk(jdk-6u32-linux-i586-rpm) ,下载后放在/home目录中,当然其它地方也行. 进入安装目录 #cd /home#cp jdk-6u32-linux-i5 ...