5.7 do/while/for的比较

VC++使用三种语法来完成循环结构,分别为为do、while、for。虽然它们完成的功能都是循环,但是每种语法有着不同的执行流程。

do循环:先执行循环体,后比较判断。

while循环:先比较判断,后执行循环体。

for循环:先初始化,再比较判断,最后执行循环体。

对每种结构进行分析,了解它们生成的汇编代码,它们之间的区别,以及如何根据每种循环结构的特性进行还原。

(1)do循环

do循环的工作流程清晰,识别起来也相对简单。根据其特性,先执行语句块,再进行比较判断。当条件成立时,会继续执行语句块。C++中的goto语句也可以用来模拟do循环结构,如代码清单5-21所示。

代码清单5-21 使用goto语句模拟do循环


//goto模拟do循环完成正数累加和

int GoToDo(int nCount){

int nSum=0;

int nIndex=0;

//用于goto语句跳转使用标记

GOTO_DO:

//此处为循环语句块

nSum+=nIndex;//保存每次累加和

nIndex++;//指定循环步长为每次递增1

//若nIndex大于nCount,则结束goto调用

if(nIndex<=nCount){

goto GOTO_DO;

}

return nSum;//返回结果

}


代码清单5-21演示了使用goto语句与if分支结构来实现do循环过程。程序执行流程是自上向下地顺序执行代码,通过goto语句向上跳转修改程序流程,实现循环。do循环结构也是如此,如代码清单5-22所示。

代码清单5-22 do循环—Debug版


//C++源码说明:do循环完成整数累加和

int LoopDO(int nCount){

int nSum=0;

int nIndex=0;

do{

nSum+=nIndex;

nIndex++;

//循环判断,是否结束循环体

}while(nIndex<=nCount);

return nSum;

}

//C++源码与对应汇编代码讲解

//C++源码对比,变量初始化

int nSum=0;

0040B4D8 mov dword ptr[ebp-4],0

int nIndex=0;

0040B4DF mov dword ptr[ebp-8],0

//C++源码对比,进入循环语句块

do{

nSum+=nIndex;

;循环语句块的首地址,即循环跳转地址

0040B4E6 mov eax, dword ptr[ebp-4]

0040B4E9 add eax, dword ptr[ebp-8]

0040B4EC mov dword ptr[ebp-4],eax

nIndex++;

0040B4EF mov ecx, dword ptr[ebp-8]

0040B4F2 add ecx,1

0040B4F5 mov dword ptr[ebp-8],ecx

//C++源码对比,比较是否结束循环

}while(nIndex<=nCount);

0040B4F8 mov edx, dword ptr[ebp-8]

;比较两个内存中的数据

0040B4FB cmp edx, dword ptr[ebp+8]

;根据比较结果,使用条件跳转指令JLE,小于等于则跳转到地址0x0040B4E6处

0040B4FE jle LoopDO+26h(0040b4e6)

return nSum;

0040B500 mov eax, dword ptr[ebp-4]


代码清单5-22中的循环比较语句“while(nIndex<=nCount)”转换成的汇编代码和if分支结构非常相似,分析后发现它们并不相同。if语句的比较是相反的,并且跳转地址大于当前代码的地址,是一个向下跳转的过程;而do中的跳转地址小于当前代码的地址,是一个向上跳转的过程,所以条件跳转的逻辑与源码中的逻辑相同。有了这个特性,if语句与do循环判断就很好区分了。

总结:


DO_BEGIN:

……;循环语句块


;影响标记位的指令

jxx DO_BEGIN;向上跳转


如果遇到以上代码块,即可判定它为一个do循环结构,只有do循环结构无需先检查,直接执行循环语句块。根据条件跳转指令所跳转到的地址,可以得到循环语句块的首地址,jxx指令的地址为循环语句块的结尾地址。在还原while比较时,应该注意,它与if不同,while的比较数并不是相反,而是相同的。依此分析即可还原do循环结构的原型。

(2)while循环

while循环和do循环正好相反,在执行循环语句块之前,必须要进行条件判断,根据比较结果再选择是否执行循环语句块,如代码清单5-23所示。

代码清单5-23 while循环—Debug版


//C++源码说明:while循环完成整数累加和

int LoopWhile(int nCount){

int nSum=0;

int nIndex=0;

//先执行条件比较,再进入循环体

while(nIndex<=nCount){

nSum+=nIndex;

nIndex++;

}

return nSum;

}

//C++源码于对应汇编代码讲解

int nSum=0;

0040B7C8 mov dword ptr[ebp-4],0

int nIndex=0;

0040B7CF mov dword ptr[ebp-8],0

//C++源码对比,判断循环条件

while(nIndex<=nCount)

0040B7D6 mov eax, dword ptr[ebp-8]

0040B7D9 cmp eax, dword ptr[ebp+8]

;条件判断比较,使用JG指令,大于则跳转到地址0x0040B7F2处,和if语句一样

;地址0x0040B7F2为while循环结束地址

0040B7DC jg LoopWhile+42h(0040b7f2)

{

//循环语句块

nSum+=nIndex;

0040B7DE mov ecx, dword ptr[ebp-4]

0040B7E1 add ecx, dword ptr[ebp-8]

0040B7E4 mov dword ptr[ebp-4],ecx

nIndex++;

0040B7E7 mov edx, dword ptr[ebp-8]

0040B7EA add edx,1

0040B7ED mov dword ptr[ebp-8],edx

}

;执行跳转指令JMP,跳转到地址0x0040B7D6处

0040B7F0 jmp LoopWhile+26h(0040b7d6)

return nSum;

0040B7F2 mov eax, dword ptr[ebp-4]


在代码清单5-23中,转换后的while比较和if语句一样,也是比较相反,向下跳转。如何区分代码中是分支结果还是循环结构呢?查看条件指令跳转地址0x0040B7F2,如果这个地址上有一句JMP指令,并且此指令跳转到的地址小于当前代码地址,那么很明显是一个向上跳转。要完成语句循环,就需要修改程序流程,回到循环语句处,因此向上跳转就成了循环结构的明显特征。根据这些特性可知while循环结构的特征,在条件跳转到的地址附近会有JMP指令修改程序流程,向上跳转,回到条件比较指令处。

while循环结构中使用了两次跳转指令完成循环,由于多使用了一次跳转指令,因此while循环要比do循环效率低一些。

总结:


WHILE_BEGIN:

;影响标记位的指令

jxx WHILE_END;条件成立跳转到循环语句块结尾处

……;循环语句块

jmp WHILE_BEGIN;跳转到取出条件比较数据处

WHILE_END:


遇到以上代码块,即可判定它为一个while循环结构。根据条件跳转指令,可以还原相反的while循环判断。循环语句块的结尾地址即为条件跳转指令的目标地址,在这个地址之前会有一条jmp跳转指令,指令的目标地址为while循环的起始地址。需要注意的是,while循环结构很可能会被优化成do循环结构,被转换后的while结构由于需要检查是否可以被成功执行一次,通常会被嵌套在if单分支结构中,其还原的高级代码如下所示:


if(xxx)

{

do

{

//……

}while(xxx)

}


(3)for循环

for循环是三种循环结构中最复杂的一种。for循环由赋初值、设置循环条件、设置循环步长这三条语句组成。由于for循环更符合人类的思维方式,在循环结构中被使用的频率也最高。根据for语句组成特性分析代码清单5-24。

代码清单5-24 for循环结构—Debug版


//C++源码说明:for循环完成整数累加和

int LoopFor(int nCount){

int nSum=0;

//初始计数器变量、设置循环条件、设置循环步长

for(int nIndex=0;nIndex<=nCount;nIndex++){

nSum+=nIndex;

}

return nSum;

}

//C++源码于对应汇编代码讲解

int nSum=0;

0040B818 mov dword ptr[ebp-4],0

//C++源码对比,for语句

for(int nIndex=0;nIndex<=nCount;nIndex++)

;=====================================================

;初始化计数器变量—nIndex  1.赋初值部分

0040B81F mov dword ptr[ebp-8],0

;跳转到地址0x0040B831处,跳过步长操作

0040B826 jmp LoopFor+31h(0040b831)

;=====================================================

;取出计数器变量,用于循环步长 2.步长计算部分

0040B828 mov eax, dword ptr[ebp-8]

;对计数器变量执行加1操作,步长值为1

0040B82B add eax,1

;将加1后的步长值放回计数器变量—nIndex

0040B82E mov dword ptr[ebp-8],eax

;=====================================================

;取出计数器变量nIndex放入ecx  3.条件比较部分

0040B831 mov ecx, dword ptr[ebp-8]

;ebp+8地址处存放数据为参数nCount,见C++源码说明

0040B834 cmp ecx, dword ptr[ebp+8]

;比较nIndex与nCount,大于则跳转到地址0x0040B844处,结束循环

0040B837 jg LoopFor+44h(0040b844)

;=====================================================

{

//for循环内执行语句块

nSum+=nIndex;

mov edx, dword ptr[ebp-4]; 4.循环体代码

0040B83C add edx, dword ptr[ebp-8]

0040B83F mov dword ptr[ebp-4],edx

}

;跳转到地址0x0040B828处,这是一个向上跳

0040B842 jmp LoopFor+28h(0040b828)

return nSum;

//设置返回值eax为ebp-4,即nSum

0040B844 mov eax, dword ptr[ebp-4]


代码清单5-24演示了for循环结构在Debug调试版下的汇编代码组成。需要由3次跳转来完成循环过程,其中一次为条件比较跳转,另外两次为jmp跳转。for循环结构为什么要设计得如此复杂呢?由于for循环分为赋初值、设置循环条件、设置循环步长这三个部分,为了可以单步调试程序,将汇编代码与源码进行一一对应,因此在Debug版下有了这样的设计,其循环流程如图5-12所示。

图 5-12 for循环结构流程图

根据对代码清单5-24及图5-12中for循环流程的分析,总结出for循环结构在Debug版下的特性。

总结:


mov mem/reg, xxx;赋初值

jmp FOR_CMP;跳到循环条件判定部分

FOR_STEP:;步长计算部分

;修改循环变量Step

mov reg, Step

add reg, xxxx;修改循环变量的计算过程,在实际分析中,视算法不同而不同

mov Step, eax

FOR_CMP:;循环条件判定部分

mov ecx, dword ptr Step

;判定循环变量和循环终止条件StepEnd的关系,满足条件则退出for循环

cmp ecx, StepEnd

jxx FOR_END;条件成立则结束循环

……

jmp FOR_STEP;向上跳转,修改流程回到步长计算部分

FOR_END:


遇到以上代码块,即可判定它为一个for循环结构。这种结构是for循环独有的,在计数器变量被赋初值后,利用jmp跳过第一次步长计算。然后,可以通过三个跳转指令还原for循环的各个组成部分:第一个jmp指令之前的代码为初始化部分;从第一个jmp指令到循环条件比较处(也就是上面代码中FOR_CMP标号的位置)之间的代码为步长计算部分;在条件跳转指令jxx之后寻找一个jmp指令,这jmp指令必须是向上跳转的,且其目标是到步长计算的位置,在jxx和这个jmp(也就是上面代码中省略号所在的位置)之间的代码即为循环语句块。

在这三种循环结构中,while循环和for循环一样,都是先判断再循环。由于需要先判断,因此需要将判断语句放置在循环语句之前,这就使while循环和for循环在结构上没有do循环那么简洁。那么在效率上这三个循环之间又有哪些区别呢?下一节将分析这三者间的效率对比。