逆向随笔 - switch 语句深入分析

Posted ___stdcall

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了逆向随笔 - 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();


反汇编:

 

 

大表大小: 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 语句深入分析的主要内容,如果未能解决你的问题,请参考以下文章

switch语句随笔

逆向知识第九讲,switch case语句在汇编中表达的方式

Android 逆向整体加固脱壳 ( DEX 优化流程分析 | DexPrepare.cpp 中 dvmOptimizeDexFile() 方法分析 | /bin/dexopt 源码分析 )(代码片段

Android逆向-Android逆向基础10(so文件分析大合集)

java基础知识文章汇总

逆向分析-之深入理解函数