5.3 用if构成的多分支流程
5.1 节和5.2节介绍了由if与if……else……组成的分支结构。本节将介绍它们的组合形式—多分支结构。多分支结构类似于if……else……的组合方式,在if……else……的else之后再添加一个else if进行二次比较,这样就可以进行多次比较,再次选择程序流程,形成了多分支流程。它的C++语法格式为:if……else if……else if……,可重复后缀为else if。当最后为else时,便到了多分支结构的末尾处,不可再分支。通过代码清单5-6可以查看多分支结构的组成。
代码清单5-6 多分支结构—Debug版
//C++源码说明:多分支结构
void IfElseIf(int argc){
if(argc>0){//判断函数参数argc是否大于0
printf("argc>0");//比较成功后执行printf("argc>0");
}else if(argc==0){//判断函数参数argc是否等于0
printf("argc==0");//比较成功后执行printf("argc==0");
}else{//前两次比较都失败,则此条语句被执行
printf("argc<=0");
}
}
//C++源码与对应汇编代码讲解
//C++源码对比
if(argc>0)
;if比较转换
00401108 cmp dword ptr[ebp+8],0
;使用JLE条件跳转指令,如果判断后的结果小于等于0,则跳转到地址0x0040111D
0040110C jle IfElseIf+2Dh(0040111d)
{
printf("argc>0");
;printf函数讲解略
0040110E push offset string"argc>0"(00420f9c)
00401113 call printf(00401150)
00401118 add esp,4
}else if(argc==0)
;对应else,当上一条if语句被执行,执行JMP指令,跳转到地址0x0040113F处
;该地址为多分支结构结束地址,即最后一个else或else if的结束地址
0040111B jmp IfElseIf+4Fh(0040113f)
;if比较转换,使用条件跳转指令JNE,不等于0则跳转到地址0x00401132
0040111D cmp dword ptr[ebp+8],0
00401121 jne IfElseIf+42h(00401132)
{
printf("argc==0");
;printf函数讲解略
00401123 push offset string"argc==0"(0042003c)
00401128 call printf(00401150)
0040112D add esp,4
}else
;跳转到多分支结构的结束地址
00401130 jmp IfElseIf+4Fh(0040113f)
{
printf("argc<=0");
;注意,此处无判定。当以上各个条件均不成立时,以下代码则无条件执行
;可将此处定义为最后的else块
00401132 push offset string"argc!=0"(00420030)
00401137 call printf(00401150)
0040113C add esp,4
}
0040113F pop edi
代码清单5-6给出了if……else if……else的组合。从代码中可以分析出,每条if语句由cmp和jxx组成,而else由一个jmp跳转到分支结构的最后一个语句块结束地址所组成。由此可见,虽然它们组合在了一起,但是每个if和else又都是独立的,if仍然是由CMP/TEST加jxx所组成,我们仍然可以根据上一节介绍的知识,利用jxx和jmp识别出if和else if语句块的边界,jxx指出了下一个else if的起始点,而jmp指出了整个多分支结构的末尾地址以及当前if或者else if语句块的末尾。最后的else块的边界也很容易识别,如果发现多分支块内的某一段代码在执行前没有判定,即可定义为else块,如上述代码中的00401132地址处。
总结:
;会影响标志位的指令
jxx ELSE_IF_BEGIN;跳转到下一条else if语句块的首地址
IF_BEGIN:
……;if语句块内的执行代码
IF_END:
jmp END;跳转到多分支结构的结尾地址
ELSE_IF_BEGIN:;else if语句块的起始地址
;可影响标志位的指令
jxx ELSE_BEGIN;跳转到else分支语句块的首地址
……;else if语句块内的执行代码
IF_ELSE_END:;else if结尾处
jmp END;跳转到多分支结构的结尾地址
ELSE_BEGIN:;else语句块的起始地址
……;else语句块内的执行代码
END:;多分支结构的结尾处
……
如果遇到这样的代码块,需要考察各跳转指令之间的关系。当每个条件跳转指令的跳转地址之前都紧跟JMP指令,并且它们跳转的地址值都一样时,可视为一个多分支结构。JMP指令指明了多分支结构的末尾,配合比较判断指令与条件跳转指令,可还原出各分支语句块的组成。如果某个分支语句块中没有判定类指令,但是存在语句块,且语句块的位置在多分支语句块范围内,可以判定其为else块。
由于编译器可以在编译期间对代码进行优化,当代码中的分支结构形成永远不可抵达的分支语句块时,它永远不会被执行,可以被优化掉而不参与编译处理。向代码清单5-6中插入一句“argc=0;”,这样argc将被“常量传播”,因此可以在编译期得知,“if(argc<0)”与“else”这两个分支语句块将永远不可抵达,它们就不会再参与编译。
void IfElseIf(int argc)
{
//仿造可分支归并代码
argc=0;
//其他代码与代码清单5-6相同
}
选择O2编译选项,将修改后的代码再次编译。使用IDA查看优化后的不可达分支是否被删除,如图5-2所示。
图 5-2 优化后的不可达分支结构
优化后,图5-2中的不可达分支被删除了。由于只剩下一个必达的分支语句块,编译器直接提取出必达分支语句块中的代码,将整个分支结构替换,就形成了如图5-2所示的代码。更多分支结构的优化,会遵循第4章中讲述的各种优化方案。以代码清单5-6为例,此多分支结构执行结束后,并没有做任何工作,直接函数返回;且当某一分支判断成立时,其他分支将不会被执行。可以选择在每个语句块内插入return语句,以减少跳转次数。
代码清单5-6中的多分支结构,共有两条比较语句块。如果其中一个分支成立,则其他分支结构语句块便会被跳过。因此可将前两个分支语句块转换为单分支if结构,在各分支语句块中插入return语句,这样既没有破坏程序流程,又可以省略掉else语句。由于没有了else,减少了一次JMP跳转,使程序执行效率得到提高。其C++代码表现为:
void IfElseIf(int argc){
if(argc>0){//判断函数参数argc是否大于0
printf("argc>0");//比较成功则执行printf("argc>0");
return;
}
if(argc==0){//判断函数参数argc是否等于0
printf("argc==0");//比较成功则执行printf("argc==0");
return;
}
printf("argc<=0");//否则执行printf("argc<0");
return;
}
以上是我们在源码中进行的手工优化,编译器是否会按照我们的意图提升运行效率呢?开启O2编译选项,还原修改过的代码清单5-6,去掉“argc=0;”再次编译。使用IDA分析反汇编代码,如代码清单5-7所示。
代码清单5-7 优化后的多分支结构—Release版
;函数入口处,对应代码清单5-6中if……else if函数
.text:00401000 sub_401000 proc near;CODE XREF:_main+5p
;arg_0为函数参数
.text:00401000 arg_0=dword ptr 4
;取出参数数据,放入eax,进行第一次if比较
.text:00401000 mov eax,[esp+arg_0]
.text:00401004 test eax, eax
;根据比较结果,使用条件跳转指令JLE,若小于等于则跳转到地址标号short loc_401016处
.text:00401006 jle short loc_401016
;跳转失败,执行printf函数参数传递及调用,显示字符串"argc>0"
.text:00401008 push offset Format;"argc>0"
.text:0040100D call_printf
.text:00401012 add esp,4
;使用retn指令返回,结束函数调用
.text:00401015 retn
;下面指令的注释是由IDA做的标记
;表示此处代码被标号sub_401000地址加6的地方引用
.text:00401016 loc_401016:;CODE XREF:sub_401000+6j
;第二条if比较,由于之前已经使用过test指令进行比较,这里省去重复操作
;直接使用条件跳转指令JNZ,若不等于0则跳转到地址标号short loc_401026处
.text:00401016 jnz short loc_401026
;跳转失败,执行printf函数参数传递及调用,显示字符串"argc==0"
.text:00401018 push offset aArgc0_0;"argc==0"
.text:0040101D call_printf
.text:00401022 add esp,4
;使用retn指令返回,结束函数调用
.text:00401025 retn
;前两次比较判断都失败,执行此处代码
.text:00401026 loc_401026:;CODE XREF:sub_401000:loc_401016j
.text:00401026 push offset aArgc0_1;"argc<=0"
.text:0040102B call_printf
.text:00401030 pop ecx
.text:00401031 retn
.text:00401031 sub_401000 endp
由于选择的是O2优化选项,因此在优化方向上更注重效率,而不是节省空间。既然是对效率的优化,就会尽量减少分支中指令的使用。代码清单5-7中就省去了else对应的JMP指令,当第一次比较成功后,则直接在执行分支语句块后返回,省去了一次跳转操作,从而提升效率。