-
-
[原创]从反汇编的角度学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年春季班招生中!