循环几乎是在我们的代码中应用频率最高的一种编程方式,例如拷贝字符串、笨拙的从1+2+3...100的程序等等,数不尽数,本小节就带领各位了解循环分支,并熟悉各种循环互相之间的差异。
我们都知道c语言的循环主要分为三种,既for、while与do-while,但是当我们以逆向的角度来看待这些循环结构的时候,我们会发现其本质上只有一种,而且即便我们再次细分的话,也仅仅能分离出两种情况。但是笔者仍然会将这一小节分为三部分讲解,以求尽量照顾读者们的接受能力。1.4.1、do-while循环的识别技巧
“嘿!为什么要先学这里,不应该是for循环吗?”
相信很多读者都会产生以上疑问,要的就是这种效果!就让我们带着这个疑问开始这一节的学习,先看源码:
int _tmain(int argc, _TCHAR* argv[])
{
int nNum = 26;
printf("Mom! I can sing my ABC!\r\n");
// 听!小Baby开始唱ABC了……
do
{
printf("%c ",0x41+(26-nNum) );
nNum--;
} while (nNum>0);
return 0;
}
现在让我们从反汇编的角度看看小Baby是怎么唱歌的:
004117DE MOV DWORD PTR SS:[EBP-8], 1A ; 16进制的0x1A等于10进制的26,不要在这里犯晕
004117E7 PUSH Test_0.00415C18 ; /format = "Mom! I can sing my ABC!
004117EC CALL DWORD PTR DS:[<&MSVCR90D.printf>] ; \printf
004117F2 ADD ESP, 4
004117FC /MOV EAX, 1A ; <--!!!!!
00411801 |SUB EAX, DWORD PTR SS:[EBP-8]
00411804 |ADD EAX, 41 ; EAX = 41+(1A-[EBP-8]) = 0x41+(26-nNum)
00411809 |PUSH EAX ; /<%c>
0041180A |PUSH Test_0.0041573C ; |format = "%c "
0041180F |CALL DWORD PTR DS:[<&MSVCR90D.printf>] ; \printf
00411815 |ADD ESP, 8
0041181F |MOV EAX, DWORD PTR SS:[EBP-8]
00411822 |SUB EAX, 1
00411825 |MOV DWORD PTR SS:[EBP-8], EAX ; [EBP-8]-- = nNum--
00411828 |CMP DWORD PTR SS:[EBP-8], 0 ; 看[EBP-8]是否仍大于0,是的话则跳转到标记处继续
0041182C \JG SHORT Test_0.004117FC
是不是感觉很容易理解?这似乎与我们前面所讲的内容差距不大,那么就让我们在看看Release版的:
00401000 PUSH ESI
00401001 PUSH EDI
00401002 MOV EDI, DWORD PTR DS:[<&MSVCR90.printf>>; MSVCR90.printf
00401008 PUSH Test_0.004020F4 ; /format = "Mom! I can sing my ABC!
0040100D CALL EDI ; \printf
0040100F ADD ESP, 4
00401012 MOV ESI, 41 ; 将ESI加0x41后准备
00401017 /PUSH ESI ; <--!!!!!
00401018 |PUSH Test_0.00402110 ; ASCII "%c "
0040101D |CALL EDI ; 又见此优化
0040101F |INC ESI ; 直接将ESI加1
00401020 |ADD ESP, 8 ; 为什么要将平衡堆栈操作放到这里?
| ; 这与CPU的流水线有关系,我们目前先不深究。
| ;
00401023 |CMP ESI, 5B ; 快看看,直接与0x5B相比较了(Z的ASCII码是0x5A)
00401026 \JL SHORT Test_0.00401017 ; 如果小于此值则继续
00401028 POP EDI
00401029 XOR EAX, EAX
0040102B POP ESI
0040102C RETN
通过以上代码我们不难看出,编译器直接将我们的代码优化为以下模式了:
int _tmain(int argc, _TCHAR* argv[])
{
int nNum = 0x41;
printf("Mom! I can sing my ABC!\r\n");
// 听!小Baby开始唱ABC了……
do
{
printf("%c ",nNum++ );
} while (nNum<0x5B);
return 0;
}
看看,多么聪明的编译器!直接看透了我们代码的本质!在感叹之余,不要忘记总结反汇编代码的特点,我们现在可以看到的最大的特点就是一个有条件判断的向上跳转,因此可以这样理解“如果我们看到了一个判断分支的跳转是向上的,那么这必然就是一个循环”。
特点总结:
DO_TAG:
......
......
CMP XXX,XXX
JXX DO_TAG1.4.2、while循环的识别技巧
先看源码:
int _tmain(int argc, _TCHAR* argv[])
{
int nNum = 26;
printf("Mom! I can sing my ABC!\r\n");
// 听!小Baby开始唱ABC了……
while(nNum>0)
{
printf("%c ",0x41+(26-nNum) );
nNum--;
}
return 0;
}
再看Debug版反汇编代码:
004117DE MOV DWORD PTR SS:[EBP-8], 1A
004117E7 PUSH Test_0.00415C18 ; /format = "Mom! I can sing my ABC!
004117EC CALL DWORD PTR DS:[<&MSVCR90D.printf>] ; \printf
004117F2 ADD ESP, 4
004117FC /CMP DWORD PTR SS:[EBP-8], 0
00411800 |JLE SHORT Test_0.00411830 ; 多了个判断,如果其小于等于0则跳出循环
00411802 |MOV EAX, 1A
00411807 |SUB EAX, DWORD PTR SS:[EBP-8]
0041180A |ADD EAX, 41
0041180F |PUSH EAX ; /<%c>
00411810 |PUSH Test_0.0041573C ; |format = "%c "
00411815 |CALL DWORD PTR DS:[<&MSVCR90D.printf>] ; \printf
0041181B |ADD ESP, 8
00411825 |MOV EAX, DWORD PTR SS:[EBP-8]
00411828 |SUB EAX, 1
0041182B |MOV DWORD PTR SS:[EBP-8], EAX
0041182E \JMP SHORT Test_0.004117FC
细心的读者可能发现了,这与do-while循环几乎如出一辙,仅仅在循环头部多了两条用于判断是否跳出循环的指令,那Release版的又会是怎样的呢?
00401000 PUSH ESI
00401001 PUSH EDI
00401002 MOV EDI, DWORD PTR DS:[<&MSVCR90.printf>>; MSVCR90.printf
00401008 PUSH Test_0.004020F4 ; /format = "Mom! I can sing my ABC!
0040100D CALL EDI ; \printf
0040100F ADD ESP, 4
00401012 MOV ESI, 41
00401017 /PUSH ESI
00401018 |PUSH Test_0.00402110 ; ASCII "%c "
0040101D |CALL EDI
0040101F |INC ESI
00401020 |ADD ESP, 8
00401023 |CMP ESI, 5B
00401026 \JL SHORT Test_0.00401017
00401028 POP EDI
00401029 XOR EAX, EAX
0040102B POP ESI
0040102C RETN
请不要怀疑我复制错了代码,事实就是这样!do-while与while生成的Release版在这里看就是100%完全相同的。
编译器很明显的已经探测出了我们的循环判断用的是一个常量,因此就不存在首次执行条件不匹配的情况。既然如此,它为什么还要在循环前面加上那个判断分支来浪费我们的空间与时间呢?
当然,如果我们将它的判断条件改为一个变量,那么就是另外一番景象了:
00401000 PUSH EBX
00401001 MOV EBX, DWORD PTR DS:[<&MSVCR90.printf>>; MSVCR90.printf
00401007 PUSH EDI
00401008 PUSH Test_0.004020F4 ; /format = "Mom! I can sing my ABC!
0040100D CALL EBX ; \printf
0040100F MOV EDI, DWORD PTR SS:[ESP+10] ; 取得参数(其实就是main函数的argc)
00401013 ADD ESP, 4
00401016 TEST EDI, EDI ; 测试参数是否为0
00401018 JLE SHORT Test_0.00401034 ; 如果为0则跳出循环
0040101A PUSH ESI
0040101B MOV ESI, 5B ; 0x5B = 0x41+26
00401020 SUB ESI, EDI ; 用0x5B减去参数
00401022 /PUSH ESI
00401023 |PUSH Test_0.00402110 ; ASCII "%c "
00401028 |CALL EBX
0040102A |DEC EDI ; 参数减1
0040102B |ADD ESP, 8
0040102E |INC ESI ; ESI加1
0040102F |TEST EDI, EDI
00401031 \JG SHORT Test_0.00401022 ; 如果参数大于ESI则结束循环
00401033 POP ESI
00401034 POP EDI
00401035 XOR EAX, EAX
00401037 POP EBX
00401038 RETN
我们可以看出用变量做判断条件很明显与常量不一样,而关于优化,很显然他只是单纯的将我们的“0x41+(26-argc)”优化成“0x5B-argc”。
特点总结:
WHILE_TAG:
CMP XXX,XXX
JXX WHILE_END_TAG
......
......
CMP XXX,XXX
JXX WHILE_TAG
WHILE_END_TAG:1.4.3、for循环的识别技巧
for循环与while循环本质上都是一样的,唯一的不同在于for循环在循环体内多了一个步长部分,接下来我们一起看看for循环的样子,先看源码:
int _tmain(int argc, _TCHAR* argv[])
{
printf("Mom! I can sing my ABC!\r\n");
// 听!小Baby开始唱ABC了……
for (int nNum = 26;nNum>0;nNum--)
{
printf("%c ",0x41+(26-nNum) );
}
return 0;
}
接下来我们再看看Debug版的反汇编代码:
004117E0 PUSH Test_0.00415C18 ; /format = "Mom! I can sing my ABC!
004117E5 CALL DWORD PTR DS:[<&MSVCR90D.printf>] ; \printf
004117EB ADD ESP, 4
004117F5 MOV DWORD PTR SS:[EBP-8], 1A
004117FC JMP SHORT Test_0.00411807
004117FE /MOV EAX, DWORD PTR SS:[EBP-8] ; / 步长控制部分开始
00411801 |SUB EAX, 1 ; | 步长为1
00411804 |MOV DWORD PTR SS:[EBP-8], EAX ; \ 将操作后的结果传回给局部变量,步长操作结束
00411807 |CMP DWORD PTR SS:[EBP-8], 0
0041180B |JLE SHORT Test_0.00411832 ; 如果此变量小于等于0则结束循环
0041180D |MOV EAX, 1A
00411812 |SUB EAX, DWORD PTR SS:[EBP-8]
00411815 |ADD EAX, 41 ; 0x41+(0x1A-[EBP-8])
0041181A |PUSH EAX ; /<%c>
0041181B |PUSH Test_0.0041573C ; |format = "%c "
00411820 |CALL DWORD PTR DS:[<&MSVCR90D.printf>] ; \printf
00411826 |ADD ESP, 8
00411830 \JMP SHORT Test_0.004117FE ; 跳到循环头部
看到这里不知道各位读者们是否发现了什么,记得我当时学到这里时,直觉上认识到了以下两点:
(1、)显然循环语句是do-while先诞生的,而后是while,最后才是for,这从侧面上讲for应该是最“高级”的了。
(2、)从执行效率上看,代码最短且判断最少的就是do-while循环了。
Release版反汇编:
00401000 PUSH ESI
00401001 PUSH EDI
00401002 MOV EDI, DWORD PTR DS:[<&MSVCR90.printf>>; MSVCR90.printf
00401008 PUSH Test_0.004020F4 ; /format = "Mom! I can sing my ABC!
0040100D CALL EDI ; \printf
0040100F ADD ESP, 4
00401012 MOV ESI, 41
00401017 /PUSH ESI
00401018 |PUSH Test_0.00402110 ; ASCII "%c "
0040101D |CALL EDI
0040101F |INC ESI
00401020 |ADD ESP, 8
00401023 |CMP ESI, 5B
00401026 \JL SHORT Test_0.00401017
00401028 POP EDI
00401029 XOR EAX, EAX
0040102B POP ESI
0040102C RETN
又是常量惹的祸,这段代码与do-while、while一模一样,有疑问的读者可以返回上面仔细观察一下,连地址都是一样的。
现在正好印证了我开篇时讲的一句话“其本质上只有一种”,很明显的,我们的while与for都是以do-while为基础框架的,只不过是在里面加了一些小判断。为了让各位读者更清晰的看到它们之间的异同,我再为各位献上一个变量版的:
00401000 PUSH EBX
00401001 MOV EBX, DWORD PTR DS:[<&MSVCR90.printf>>; MSVCR90.printf
00401007 PUSH EDI
00401008 PUSH Test_0.004020F4 ; /format = "Mom! I can sing my ABC!
0040100D CALL EBX ; \printf
0040100F MOV EDI, DWORD PTR SS:[ESP+10]
00401013 ADD ESP, 4
00401016 TEST EDI, EDI
00401018 JLE SHORT Test_0.00401034
0040101A PUSH ESI
0040101B MOV ESI, 5B
00401020 SUB ESI, EDI
00401022 /PUSH ESI
00401023 |PUSH Test_0.00402110 ; ASCII "%c "
00401028 |CALL EBX
0040102A |DEC EDI
0040102B |ADD ESP, 8
0040102E |INC ESI
0040102F |TEST EDI, EDI
00401031 \JG SHORT Test_0.00401022
00401033 POP ESI
00401034 POP EDI
00401035 XOR EAX, EAX
00401037 POP EBX
00401038 RETN
再重申一下,笔者并没有搞错,以变量为判断条件的for循环与while循环所生成的代码是完全相同的,连地址都一样……
特点总结:
FOR_START_TAG:
初始化块
JMP CMP_TAG
STEP_TAG:
步长块
CMP_TAG:
反条件判断
JXX FOR_END_TAG
……
……
JXX STEP_TAG
到此,我们应该可以做一个总结了,Debug版下三种循环各不相同,Release版下可总结如下:
(1、)当循环采用常量为判断条件时,相同逻辑的三种循环生成的代码完全相同。
(2、)当循环采用变量为判断条件时,相同逻辑的while与for生成的代码完全相同,而do-while则自成一格。小插曲:循环体的语句外提优化
我们看看下面这段代码:
int _tmain(int argc, _TCHAR* argv[])
{
printf("Mom! I can sing my ABC!\r\n");
// 听!小Baby开始唱ABC了……
for (int nNum = 24;nNum>0;nNum--)
{
argc = (int)argv;
printf("%c ",0x41+(26-nNum) );
}
printf("%p",argc);
return 0;
}
通过这段代码我们可以发现一处可以优化的地方,就是“argc = (int)argv”这条语句,很明显
00401000 PUSH ESI
00401001 PUSH EDI
00401002 MOV EDI, DWORD PTR DS:[<&MSVCR90.printf>>; MSVCR90.printf
00401008 PUSH Test_0.004020F4 ; /format = "Mom! I can sing my ABC!
0040100D CALL EDI ; \printf
0040100F ADD ESP, 4
00401012 MOV ESI, 43
00401017 /PUSH ESI
00401018 |PUSH Test_0.00402110 ; ASCII "%c "
0040101D |CALL EDI
0040101F |INC ESI
00401020 |ADD ESP, 8
00401023 |CMP ESI, 5B
00401026 \JL SHORT Test_0.00401017
00401028 MOV EAX, DWORD PTR SS:[ESP+10] ; EAX = (int)argv; <--注意这里
0040102C PUSH EAX
0040102D PUSH Test_0.00402114 ; ASCII "%p"
00401032 CALL EDI
00401034 ADD ESP, 8
00401037 POP EDI
00401038 XOR EAX, EAX
0040103A POP ESI
0040103B RETN
由上面代码可知,循环体内很明显没有我们“argc = (int)argv;”的代码,再向下看一行才知道,这段代码被提到了外面,这就是编译进行的代码外提优化。
本小节到此就结束了,笔者在本小节向大家详细的介绍了三种循环结构在逆向时的一些需要注意的特点,当你怎能快速的花柱这些特点之后,剩下的就是反复的练习了。【返回到目录】:http://bbs.pediy.com/showthread.php?t=113689
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!