5.2 if……else……语句

5.1 节讲述了if语句的构成,但是,只有if的语句是不完整的分支结构,图5-1对比了两种语句结构的执行流程。

图 5-1 if与if……else……结构对比

如图5-1所示,if语句是一个单分支结构,if……else……组合后是一个双分支结构。两者间完成的功能有所不同。从语法上看,if……else……只比if语句多出了一个else。else有两个功能,如果if判断成功,则跳过else分支语句块;如果if判断失败,则进入else分支语句块中。有了else语句的存在,程序在进行流程选择时,必会经过两个分支中的一个。通过代码清单5-3,我们来分析else如何实现这两个功能。

代码清单5-3 if……else……组合—Debug版


//C++源码说明:if……else……组合

if(argc==0){

//执行if语句块

printf("argc==0");

}else{

//执行else语句块

printf("argc!=0");

}

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

//C++源码对比,比较参数变量argc==0

if(argc==0)

;使用变量argc减去0

004010B8 cmp dword ptr[ebp+8],0

;使用条件跳转JNE,检查cmp影响标记位

;跳转成立,跳转到地址0x004010CD处,即else语句块的首地址

004010BC jne IfElse+2Dh(004010cd)

{

//C++源码对比,进入if语句块,调用printf函数

printf("argc==0");

;printf函数汇编讲解略

}else

;直接跳转到地址0x004010DA地址处,当if语句执行后跳转过else语句块

004010CB jmp IfElse+3Ah(004010da)

{

//C++源码对比,进入else语句块,调用printf函数

printf("argc!=0");

;printf函数汇编讲解略

004010CD push offset string"argc!=0"(00420030)

004010D2 call printf(00401150)

004010D7 add esp,4

}

;else语句结束处

004010DA pop edi


在代码清单5-3中,if语句转换的条件跳转和代码清单5-1中的if语句相同,都是取相反的条件跳转指令。而在else处(地址004010CB)多了一句jmp指令,这是为了在if语句比较后,如果结果为真,则程序流程执行if语句块并且跳过else语句块,反之执行else语句块。else处的jmp指令跳转的目标地址为else语句块结尾处的地址,这样的设计可以跳过else语句块,实现两个分支语句二选一的功能。

4.2.3节介绍了条件表达式,当条件表达式中的表达式2或表达式3为变量时,没有进行优化处理。条件表达式转换后的汇编代码和if……else……结构非常相似,将代码清单4-17与代码清单5-3进行分析对比可以发现,两者间有很多相似之处,如没有源码比照,想要分辨出是条件表达式还是if……else……结构实在太难。它们都是先比较,然后再执行条件跳转指令,最后进行流程选择的。

通常,VC++6.0对条件表达式和if……else……使用同一套处理方式。代码清单5-3对应条件表达式转换方式4。将代码清单5-3稍作改动,改为符合条件表达式转换方式1的形式,如代码清单5-4所示。

代码清单5-4 模拟条件表达式转换方式1


//C++源码说明:if……else……模拟条件表达式转换方式1

if(argc==0){//等价条件表达式中的表达式1

//修改上例,将上例中的printf函数替换成变量赋值语句

argc=5;//等价条件表达式中的表达式2

}else{

//代码上例,将上例中的printf函数替换成变量赋值语句

argc=6;//等价条件表达式中的表达式3

}

//防止变量被优化处理

printf("%d\r\n",argc);

//Debug调试版,由于注重调试功能,没有进行优化,反汇编讲解如下

22:if(argc==0){

;直接与0进行比较,注意后面的jne,如果不相等就跳走,C源码中的逻辑与汇编代码相反

00401098 cmp dword ptr[ebp+8],0

0040109C jne main+27h(004010a7)

23:argc=5;

;这里是if语句块的内容,将参数赋值为5

0040109E mov dword ptr[ebp+8],5

24:}else{

;注意这里的跳转,正好跳出了else块

004010A5 jmp main+2Eh(004010ae)

25:argc=6;

;这里是else语句块的内容,将参数赋值为6

004010A7 mov dword ptr[ebp+8],6

26:}

27:printf("%d\r\n",argc);

;printf函数汇编讲解略

004010AE……


按if……else……的逻辑,如果满足if条件,则执行if语句块;否则执行else语句块,两者有且仅有一个会执行。所以,如果编译器生成的代码在0040109C处的跳转条件成立,则必须到达else块的代码开始处。而004010A5处有个无条件跳转jmp,它的作用是绕过else块,因为如果能执行到这个jmp, if条件必然成立,对应的反汇编代码处的跳转条件必然不能成立,且if语句块已经执行完毕。由此,我们可以将这里的两处跳转指令作为“指路明灯”,准确划分if块和else块的边界。

总结:


;先执行影响标志位的相关指令

jxx ELSE_BEGIN;该地址为else语句块的首地址

IF_BEGIN:

……;if语句块内的执行代码

IF_END:

jmp ELSE_END;跳转到else语句块的结束地址

ELSE_BEGIN:

……;else语句块内的执行代码

ELSE_END:


如果遇到以上指令序列,先考察其中的两个跳转指令,当第一个条件跳转指令跳转到地址ELSE_BEGIN处之前有个JMP指令,则可将其视为由if……else……组合而成的双分支结构。根据这两个跳转指令可以得到if和else语句块的代码边界。通过cmp与jxx可还原出if的比较信息,jmp指令之后即为else块的开始。依此分析,即可逆向分析出if……else……组合的原型。

在Debug编译模式下,所使用的编译选项是Od+ZI,于是在这里不能做流水线优化,分支必须存在,以便于开发者设置断点观察程序流程。

使用O2优化选项,重新编译代码清单5-4。通过IDA查看优化后的反汇编代码,如代码清单5-5所示。

代码清单5-5 模拟条件表达式转换方案1—Release版


;参数标记定义,arg_表示函数参数1

arg_0=dword ptr 4

;将取得的参数数据放到edx中

mov edx,[esp+arg_0]

;将eax清0

xor eax, eax

;对edx和edx执行相与操作,结果不影响edx

test edx, edx

;检查ZF标记位,edx不等于0则al=1,反之al=0,这里的操作与代码清单4-14类似

setnz al

add eax,5;到此,eax的取值只可能是5或6,如果edx为0,则eax为5,反之则为6

push eax

push offset Format;"%d\r\n"

call_printf

add esp,8

retn


代码清单5-5中的这些指令似曾相识,与条件表达式使用了同样的优化手法。其他3种优化方案同样适用于if……else……。通过以上分析,得出VC++6.0编译的代码,在很多情况下,会发现条件表达式的反汇编代码和if……else……组合是一样的,这时,可以根据个人习惯还原出等价的高级代码。

有时候会遇到复杂的条件表达式作为分支或者循环结构的判定条件的情况,这时即使直接阅读高级源码也会让人抓狂。在没有高级源码的情况下,分析者需要先定位语句块的边界,然后根据跳转目标和逻辑依赖慢慢反推出高级代码。