5.1 if语句
if语句是分支结构的重要组成部分。if语句的功能是先对运算条件进行比较,然后根据比较结果执行对应的语句块。if语句只能判断两种情况:“0”为假值,“非0”为真值。如果判断结果为真值,则进入语句块内执行语句;如果判断为假值,则跳过if语句块,继续运行程序的其他语句。需要注意的是,if语句转换的条件跳转指令与if语句的判断结果是相反的。我们以代码清单5-1为例,逐步展开对if语句的分析。
代码清单5-1 if语句构成(Debug版)
// C++ 源码 #include <stdio.h> int main(int argc, char* argv[]) { if (argc == 0) { printf("argc == 0"); } return 0; } //x86_vs对应汇编代码讲解 00401000 push ebp 00401001 mov ebp, esp 00401003 cmp dword ptr [ebp+8], 0 00401007 jnz short loc_401016 ;如果argc!=0,则跳转到if结束块代码 { 00401009 push offset aArgc0 0040100E call printf 00401013 add esp, 4 ;if语句块代码 } 00401016 loc_401016: 00401016 xor eax, eax ;if结束块代码 00401018 pop ebp 00401019 retn //x86_gcc对应汇编代码相似,略 //x86_clang对应汇编代码相似,略 //x64_vs对应汇编代码讲解 0000000140001000 mov [rsp+10h], rdx 0000000140001005 mov [rsp+8], ecx 0000000140001009 sub rsp, 28h 000000014000100D cmp dword ptr [rsp+30h], 0 0000000140001012 jnz short loc_140001020 ;如果argc!=0,则跳转到if结束块代码 { 0000000140001014 lea rcx, aArgc0 000000014000101B call sub_140001090 ;if语句块代码 } 0000000140001020 xor eax, eax ;if结束块代码 0000000140001022 add rsp, 28h 0000000140001026 retn //x64_gcc对应汇编代码相似,略 //x64_clang对应汇编代码相似,略
代码清单5-1中if的比较条件为“argc == 0”,如果成立,即为真值,则进入if语句块内执行语句。但是,转换后的汇编代码使用的条件跳转指令JNE判断结果为“不等于0跳转”,这是为什么呢?按照if语句的规定,满足if判定的表达式才能执行if的语句块,而汇编语言的条件跳转却是满足某条件则跳转,绕过某些代码块,这一点与C语言是相反的。
既然这样,那为什么C语言编译器不将else语句块提到前面并把if语句块放到后面呢?这样汇编语言和C语言中的判定条件不就一致了吗?
因为C语言是根据代码行的位置决定编译后二进制代码地址高低的,也就是说,低行数对应低地址,高行数对应高地址,所以有时会使用标号相减得到代码段的长度。鉴于此,C语言的编译器不能随意改变代码行在内存中的顺序。
根据这一特性,如果将if语句中的比较条件“argc == 0”修改为“if(argc > 0)”,则其对应的汇编语言使用的条件跳转指令会是“小于等于0”。
代码清单5-2 if语句大于0比较(Debug版)
// C++ 源码 #include <stdio.h> int main(int argc, char* argv[]) { if (argc > 0){ printf("%d\n", argc); } return 0; } //x86_vs对应汇编代码讲解 00401000 push ebp 00401001 mov ebp, esp 00401003 cmp dword ptr [ebp+8], 0 00401007 jle short loc_40101A ;如果argc<=0,则跳转到if结束代码块 { 00401009 mov eax, [ebp+8] 0040100C push eax 0040100D push offset unk_412160 00401012 call sub_401060 00401017 add esp, 8 ;if语句块代码 } 0040101A loc_40101A: ;if结束代码块 0040101A xor eax, eax 0040101C pop ebp 0040101D retn //x86_gcc对应汇编代码相似,略 //x86_clang对应汇编代码相似,略 //x64_vs对应汇编代码讲解 0000000140001000 mov [rsp+10h], rdx 0000000140001005 mov [rsp+8], ecx 0000000140001009 sub rsp, 28h 000000014000100D cmp dword ptr [rsp+30h], 0 0000000140001012 jle short loc_140001024 ;如果argc<=0,则跳转到if结束代码块 { 0000000140001014 mov edx, [rsp+30h] 0000000140001018 lea rcx, unk_1400122C0 000000014000101F call sub_140001090 ;if语句块代码 } 0000000140001024 xor eax, eax ;if结束代码块 0000000140001026 add rsp, 28h 000000014000102A retn //x64_gcc对应汇编代码相似,略 //x64_clang对应汇编代码相似,略
分析代码清单5-1和代码清单5-2,可以总结出if语句的转换规则:在转换成汇编代码后,由于当if比较结果为假时,需要跳过if语句块内的代码,因此使用了相反的条件跳转指令。
将4.2.2节的代码清单4-21与代码清单5-2进行对比分析可知,代码结构特征十分相似,但使用的条件跳转指令不同。由此可见,在反汇编时,表达式短路和if语句这两种分支结构的实现过程都是一样的,很难在源码中对它们进行区分。
总结
如果遇到以上指令序列,可高度怀疑它是一个由if语句组成的单分支结构,根据比较信息与条件跳转指令,找到与跳转条件相反的逻辑,即可恢复分支结构原型。由于循环结构也会出现类似代码,因此在分析过程中还需要结合上下文。