首页
论坛
课程
招聘
[原创]从反汇编的角度学C/C++之条件判断
2021-9-29 10:06 5348

[原创]从反汇编的角度学C/C++之条件判断

2021-9-29 10:06
5348

    由于条件判断中需要用到条件指令,这里为了方便把常见的几个跳转指令放这里参考

一.if-else if-else条件判断

    在C/C++中我们使用使用if-else if -else语句来实现程序根据不同情况来跳转运行。下面通过一个实例来学习其中的内部实现

	int x = 1900;
006110E8  mov         dword ptr [x],76Ch      //为变量x赋值

	if (x < 0)
006110EF  cmp         dword ptr [x],0          //将x的值与0做比较
006110F3  jge         main+44h (0611104h)      //如果x大于等于0则跳转
	{
		printf("x < 0\n");
006110F5  push        offset string "x < 0\n" (06831B0h)  
006110FA  call        printf (0611140h)              
006110FF  add         esp,4                    //执行满足x < 0时的代码
00611102  jmp         main+66h (0611126h)      //跳转到结束位置,也就是判断结构的下一句指令
	}
	else if (x == 0)
00611104  cmp         dword ptr [x],0          //将x与0作比较
00611108  jne         main+59h (0611119h)      //如果x不等于0则跳转
	{
		printf("x = 0\n");
0061110A  push        offset string "x = 0\n" (06831B8h)  
0061110F  call        printf (0611140h)  
00611114  add         esp,4                    //执行满足x < 0时的代码  
00611117  jmp         main+66h (0611126h)      //跳转到结束位置,也就是判断结构的下一句指令
	}
	else  
	{
		printf("x > 0\n");
00611119  push        offset string "x > 0\n" (06831C0h)  
0061111E  call        printf (0611140h)  
00611123  add         esp,4                    // 执行else的代码
   } 
		return 0;
00611126  xor         eax,eax                  //判断语句块的下一句指令

 由上可以看出,在经过if或者else if的时候程序是判断是否不满足条件。在不满足条件的情况下,程序根据情况跳转到相应的地方,如果满足条件程序就会往下执行完相应的指令之后在跳转到整个if-else if-else结构的下一条指令。转换为汇编以后的程序执行流程就如下图所示

二.&&与||

    在条件判断中,我们经常使用&&与||。接下来看看他们转成汇编以后的样子。

	int x = 0, y = 1;
002710E8  mov         dword ptr [x],0  
002710EF  mov         dword ptr [y],1          //分别为x,y赋值

	if (x == 0 && y == 1)
002710F6  cmp         dword ptr [x],0          //判断x是否等于0
002710FA  jne         main+4Fh (027110Fh)          //不等于0则跳转到条件不成立的指令
002710FC  cmp         dword ptr [y],1          //判断y是否等于1
00271100  jne         main+4Fh (027110Fh)          //不等于则跳转到条件不成立的指令
	{
		printf("x == 0 && y == 1\n");
00271102  push        offset string "x == 0 && y == 1\n" (02E31B0h)  //如果x等于0,y等于1则会执行这里的代码
00271107  call        printf (0271140h)  
0027110C  add         esp,4  
	}

	if (x == 0 || y == 1)
0027110F  cmp         dword ptr [x],0         //判断x是否等于0 
00271113  je          main+5Bh (027111Bh)         //等于0则跳转到条件成立的指令
00271115  cmp         dword ptr [y],1         //判断y是否等于1
00271119  jne         main+68h (0271128h)         //不等于1则跳转到条件不成立的指令
	{
		printf("x == 0 || y == 1\n");
0027111B  push        offset string "x == 0 || y == 1\n" (02E31C4h) //如果x等于0或者y等于1则会执行这里的代码 
00271120  call        printf (0271140h)  
00271125  add         esp,4  
	}

	return 0;
00271128  xor         eax,eax

    由上可以看出,对于&&程序会经过两次判断条件是否都成立,任何一次条件不成立都会导致语句块不被执行且第一次判断如果不成立就不会进行第二次判断直接跳过程序块

    而对于||,任何一次条件成立都会导致语句块被执行,且如果第一次条件成立就不会进行第二次判断,直接跳转到条件成立的语句进行执行。

三.对if语句的错误使用

    下面列举了两种新手比较容易犯的错误。第一次情况是把=号当做==来进行使用,第二种情况是由于else匹配if是根据最近匹配的原则,由于第一个if成立的语句块没有加{}导致else匹配了第二个if。

int main()
{
	int x = 0, y = 0;

	if (x = 1)
	{
		printf("x等于1\n");
	}

	if (x == 0)
		if (y == 1)
			printf("x等于0 y等于1");
	else
		printf("x不等于0");

	return 0;
}

    最终生成的反汇编代码如下:

	int x = 0, y = 1;
00DF10E8  mov         dword ptr [x],0  
00DF10EF  mov         dword ptr [y],1          //为x赋值0,y赋值1

	if (x = 1)
00DF10F6  mov         dword ptr [x],1          //这里本意是判断x是否等于1,却因为用错符号导致程序先对x赋值为1
00DF10FD  cmp         dword ptr [x],0          //然后在判断x的值是否等于0,成立则跳转
00DF1101  je          main+50h (0DF1110h)     
	{
		printf("x等于1\n");
00DF1103  push        offset string "x\xb5\xc8\xd3\xda1\n" (0E631B0h)  
00DF1108  call        printf (0DF1150h)  
00DF110D  add         esp,4  
	}

	if (x == 0)
00DF1110  cmp         dword ptr [x],0          //判断x是否等于0
00DF1114  jne         main+78h (0DF1138h)      //不等0不是跳到else执行也是直接跳到函数末尾
		if (y == 1)
00DF1116  cmp         dword ptr [y],1          //判断y是否等于1
00DF111A  jne         main+6Bh (0DF112Bh)      //不等于1跳到else执行
			printf("x等于0 y等于1");
00DF111C  push        offset string "x\xb5\xc8\xd3\xda0 y\xb5\xc8\xd3\xda1" (0E631B8h)  
00DF1121  call        printf (0DF1150h)  
00DF1126  add         esp,4  
	else
00DF1129  jmp         main+78h (0DF1138h)  
		printf("x不等于0");
00DF112B  push        offset string "x\xb2\xbb\xb5\xc8\xd3\xda0" (0E631C8h)  
00DF1130  call        printf (0DF1150h)  
00DF1135  add         esp,4  

	return 0;
00DF1138  xor         eax,eax

    可以看到,由于把=当成==使用,导致第一个if判断时候先是对x赋值为1,然后在判断x是否等于0。而在第二个由于else没正确配对导致跳转语句不符合预期。最终运行结果如下:

四.switch-case

    swich-case在c/c++中也是一种常见的条件判断语法,那么在内存中的表现形式与if-else结构有什么不同呢,请看下面的实例:

	int x = 0;
00C910E8  mov         dword ptr [x],0              //为x赋值为0

	switch (x)
00C910EF  mov         eax,dword ptr [x]            //将x的值赋值给eax
00C910F2  mov         dword ptr [ebp-0D0h],eax     //将eax赋值到ebp-0x0D0
00C910F8  cmp         dword ptr [ebp-0D0h],0       //将ebp-0x0D0中的值与0做比较,也就是将x的值与0做比较
00C910FF  je          main+55h (0C91115h)          //等于0则跳转到case 0的语句块执行
00C91101  cmp         dword ptr [ebp-0D0h],1       //是否等于1
00C91108  je          main+64h (0C91124h)          //等于1则跳转到case 1的语句块执行
00C9110A  cmp         dword ptr [ebp-0D0h],2       //是否等于2
00C91111  je          main+73h (0C91133h)          //等于2则跳转到case 2的语句块执行
00C91113  jmp         main+82h (0C91142h)          //如果上述条件都不满足则跳转到default执行
	{
		case 0:
		{
			printf("0\n");
00C91115  push        offset string "0\n" (0D031B0h)  
00C9111A  call        printf (0C91170h)  
00C9111F  add         esp,4  
			break;
00C91122  jmp         main+8Fh (0C9114Fh) //由于有break所以这里会生成一条跳出结构外的语句如果没有,他就会继续向下执行case 1的语句
		}
		case 1:
		{
			printf("0\n");
00C91124  push        offset string "0\n" (0D031B0h)  
00C91129  call        printf (0C91170h)  
00C9112E  add         esp,4  
			break;
00C91131  jmp         main+8Fh (0C9114Fh)  
		}
		case 2:
		{
			printf("0\n");
00C91133  push        offset string "0\n" (0D031B0h)  
00C91138  call        printf (0C91170h)  
		}
		case 2:
		{
			printf("0\n");
00C9113D  add         esp,4  
			break;
00C91140  jmp         main+8Fh (0C9114Fh)  
		}
		default:
		{
			printf("default\n");
00C91142  push        offset string "default\n" (0D031B4h)  
00C91147  call        printf (0C91170h)  
00C9114C  add         esp,4  
			break;
		}
	}

	return 0;
00C9114F  xor         eax,eax

    可以看出与if-else if-else结构相比,switch case生成的结构会在最开始就一一判断符合的情况然后跳转到相应的地方。

    如果case的情况比较多,像下面这种情况。

int main()
{
	int x = 0;

	switch (x)
	{
		case 0:
		{
			printf("0\n");
			break;
		}
		case 1:
		{
			printf("1\n");
			break;
		}
		case 2:
		{
			printf("2\n");
			break;
		}
		case 4:
		{
			printf("4\n");
			break;
		}
		case 5:
		{
			printf("5\n");
			break;
		}
		case 7:
		{
			printf("7\n");
			break;
		}
		case 9:
		{
			printf("9\n");
			break;
		}
		default:
		{
			printf("default\n");
			break;
		}
	}

	return 0;
}

    此时如果还是一一判断对计算机的性能损耗是比较大的,编译器是否有优化呢?查看对应反汇编如下

	int x = 0;
002610E8  mov         dword ptr [x],0                  //为x赋值为0

	switch (x)
002610EF  mov         eax,dword ptr [x]  
002610F2  mov         dword ptr [ebp-0D0h],eax         //将x的值赋值到ebp-0xD0
002610F8  cmp         dword ptr [ebp-0D0h],9           //将x的值与9做比较,此时9是case块中最大的数字
002610FF  ja          $LN10+0Fh (0261177h)             //如果大于9则跳转到default语句块执行
00261101  mov         ecx,dword ptr [ebp-0D0h]         //取出x的值放入ecx
00261107  jmp         dword ptr [ecx*4+26119Ch]         //跳转到ecx*4+0x26119C地址中存的内存 
	{
		case 0:
		{
			printf("0\n");
0026110E  push        offset string "0\n" (02D31B0h)  
00261113  call        printf (02611D0h)  
00261118  add         esp,4  
			break;
0026111B  jmp         $LN10+1Ch (0261184h)  
		}
		case 1:
		{
			printf("1\n");
0026111D  push        offset string "1\n" (02D31B4h)  
00261122  call        printf (02611D0h)  
00261127  add         esp,4  
			break;
0026112A  jmp         $LN10+1Ch (0261184h)  
		}
		case 2:
		{
			printf("2\n");
0026112C  push        offset string "2\n" (02D31B8h)  
00261131  call        printf (02611D0h)  
00261136  add         esp,4  
			break;
00261139  jmp         $LN10+1Ch (0261184h)  
		}
		case 4:
		{
			printf("4\n");
0026113B    push        offset string "4\n" (02D31BCh)  
00261140  call        printf (02611D0h)  
00261145  add         esp,4  
			break;
00261148  jmp         $LN10+1Ch (0261184h)  
		}
		case 5:
		{
			printf("5\n");
0026114A  push        offset string "5\n" (02D31C0h)  
0026114F  call        printf (02611D0h)  
00261154  add         esp,4  
			break;
00261157  jmp         $LN10+1Ch (0261184h)  
		}
		case 7:
		{
			printf("7\n");
00261159  push        offset string "7\n" (02D31C4h)  
0026115E  call        printf (02611D0h)  
00261163  add         esp,4  
			break;
00261166  jmp         $LN10+1Ch (0261184h)  
		}
		case 9:
		{
			printf("9\n");
00261168  push        offset string "9\n" (02D31C8h)  
0026116D  call        printf (02611D0h)  
00261172  add         esp,4  
			break;
00261175  jmp         $LN10+1Ch (0261184h)  
		}
		default:
		{
			printf("default\n");
00261177  push        offset string "default\n" (02D31CCh)  
0026117C  call        printf (02611D0h)  
00261181  add         esp,4  
			break;
		}
	}

	return 0;
00261184  xor         eax,eax

    可以看到这个时候会首先判断是否大于最大的数,如果大于则跳到default执行,如果不大于他会根据与0x00261190偏移找到地址跳转过去,而这个偏移是根据x的值乘以4来计算,所以我们完全可以认为这个地址其实是一个整型数组,里面存储的就是不同情况下应该跳转的地址。查看这个地址的内容如下所示:

    可以看到这个数组对应下标0,1,2的值,分别对应了case 0, case 1, case 2的语句块的地址。而由于没case 3,所以下标为3的数组的值保存的就是default的值。其他的case 4 5 6 7 8 9的情况与刚刚的一样。

    由此我们可以得出结论,当case语句比较多的时候,编译器会为我们生成一个整型数组,里面保存了各自情况下需要跳转的地址,而程序也会根据偏移得到需要的地址进行跳转。


【看雪培训】《Adroid高级研修班》2022年春季班招生中!

最后于 2021-12-13 17:00 被1900编辑 ,原因:
收藏
点赞3
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回