4.2 关系运算和逻辑运算
关系运算用于判断两者之间的关系,如等于、不等于、大于或等于、小于或等于、大于和小于,对应的符号分别为“==”“!=”“>=”“<=”“>”“<”。关系运算的作用是比较关系运算符左右两边的操作数的值,得出一个判断结果:真或假。
逻辑运算用于判定两个逻辑值之间的依赖关系,如或、与、非,对应的符号有“||”“&&”“!”。逻辑运算也是可以组合的,执行顺序和关系运算相同。
1. 或运算
比较运算符||左右的语句的结果,如果有一个值为真,则返回真值;如果都为假,则返回假值。
2. 与运算
比较运算符&&左右的语句的结果,如果有一个值为假,则返回假值;如果都为真值,则返回真值。
3. 非运算
改变运算符!后面语句的真假结果,如果该语句的结果为真值,则返回假值;如果为假值,则返回真值。
4.2.1 关系运算和条件跳转的对应
在C++中,可以利用各种类型的跳转比较两者之间的关系,根据比较结果影响到的标记位来选择对应的条件跳转指令。根据两个需要进行比较的数值所使用到的关系运算选择条件跳转指令,不同的关系运算对应的条件跳转指令也不相同。各种关系对应的条件跳转指令如表4-1所示。
表4-1 条件跳转指令表
通常情况下,这些条件跳转指令都与CMP和TEST匹配出现,但条件跳转指令检查的是标记位。因此,在有修改标记位的代码处,也可以根据需要使用条件跳转指令修改程序流程。
4.2.2 表达式短路
表达式短路通过逻辑与运算和逻辑或运算使语句根据条件在执行时发生中断,从而不予执行后面的语句。如何利用表达式短路来实现语句中断呢?根据逻辑与和逻辑或运算的特性,如果是与运算,当运算符左边的语句块为假值时,直接返回假值,不执行右边的语句;如果是或运算,当运算符左边的语句块为真值时,直接返回真值,不执行右边的语句块。
利用表达式短路可以实现用递归方式计算累加和。下面我们进一步学习和理解表达式短路的构成,如代码清单4-16所示。
代码清单4-16 使用逻辑与完成表达式短路
// C++源码 #include <stdio.h> //递归函数,用于计算整数累加,num为累加值 int accumulation(int num) { //当num等于0时,逻辑与运算符左边的值为假,将不会执行右边语句,形成表达式短路,从而找到递归出口 num && (num += accumulation(num - 1)); return num; } int main(int argc, char* argv[]) { accumulation(10); return 0; } //x86_vs对应汇编代码讲解 00401000 push ebp 00401001 mov ebp, esp 00401003 cmp dword ptr [ebp+8], 0 ;比较变量num是否等于0 00401007 jz short loc_40101E ;if (num == 0) goto loc_40101E 00401009 mov eax, [ebp+8] ;num != 0,进入递归调用 0040100C sub eax, 1 0040100F push eax ;参数1:num-1 00401010 call sub_401000 ;继续调用自己,形成递归 00401015 add esp, 4 ;平衡栈 00401018 add eax, [ebp+8] 0040101B mov [ebp+8], eax ;num += accumulation(num-1) 0040101E loc_40101E: 0040101E mov eax, [ebp+8] ;返回变量num 00401021 pop ebp 00401022 retn //x86_gcc对应汇编代码讲解 00401510 push ebp 00401511 mov ebp, esp 00401513 sub esp, 18h 00401516 cmp dword ptr [ebp+8], 0 ;比较变量num是否等于0 0040151A jz short loc_401531 ;if (num == 0) goto loc_401531 0040151C mov eax, [ebp+8] ;num != 0,进入递归调用 0040151F sub eax, 1 00401522 mov [esp], eax ;参数1:num-1 00401525 call __Z12accumulationi ;继续调用自己,形成递归 0040152A add [ebp+8], eax ;num += accumulation(num-1) 0040152D cmp dword ptr [ebp+8], 0 00401531 loc_401531: 00401531 mov eax, [ebp+8] ;返回变量num 00401534 leave 00401535 retn //x86_clang对应汇编代码讲解 00401000 push ebp 00401001 mov ebp, esp 00401003 sub esp, 0Ch 00401006 mov eax, [ebp+8] 00401009 cmp dword ptr [ebp+8], 0 ;比较变量num是否等于0 0040100D mov [ebp-4], eax 00401010 jz loc_401033 ;if (num == 0) goto loc_401033 00401016 mov eax, [ebp+8] ;num != 0,进入递归调用 00401019 sub eax, 1 0040101C mov [esp], eax ;参数1:num-1 0040101F call sub_401000 ;继续调用自己,形成递归 00401024 add eax, [ebp+8] 00401027 mov [ebp+8], eax ;num += accumulation(num-1) 0040102A cmp eax, 0 0040102D setnz cl 00401030 mov [ebp-5], cl 00401033 loc_401033: 00401033 mov eax, [ebp+8] ;返回变量num 00401036 add esp, 0Ch 00401039 pop ebp 0040103A retn //x64_vs对应汇编代码讲解 0000000140001000 mov [rsp+8], ecx 0000000140001004 sub rsp, 28h 0000000140001008 cmp dword ptr [rsp+30h], 0 ;比较变量num是否等于0 000000014000100D jz short loc_140001028 ;if (num == 0) goto loc_140001028 000000014000100F mov eax, [rsp+30h] ;num != 0,进入递归调用 0000000140001013 dec eax 0000000140001015 mov ecx, eax ;参数1:num-1 0000000140001017 call sub_140001000 ;继续调用自己,形成递归 000000014000101C mov ecx, [rsp+30h] 0000000140001020 add ecx, eax 0000000140001022 mov eax, ecx 0000000140001024 mov [rsp+30h], eax ;num += accumulation(num-1) 0000000140001028 loc_140001028: 0000000140001028 mov eax, [rsp+30h] ;返回变量num 000000014000102C add rsp, 28h 0000000140001030 retn //x64_gcc对应汇编代码讲解 0000000000401550 push rbp 0000000000401551 mov rbp, rsp 0000000000401554 sub rsp, 20h 0000000000401558 mov [rbp+10h], ecx 000000000040155B cmp dword ptr [rbp+10h], 0 ;比较变量num是否等于0 000000000040155F jz short loc_401575 ;if (num == 0) goto loc_401575 0000000000401561 mov eax, [rbp+10h] ;num != 0,进入递归调用 0000000000401564 sub eax, 1 0000000000401567 mov ecx, eax ;参数1:num-1 0000000000401569 call _Z12accumulationi ;继续调用自己,形成递归 000000000040156E add [rbp+10h], eax ;num += accumulation(num-1) 0000000000401571 cmp dword ptr [rbp+10h], 0 0000000000401575 loc_401575: 0000000000401575 mov eax, [rbp+10h] ;返回变量num 0000000000401578 add rsp, 20h 000000000040157C pop rbp 000000000040157D retn //x64_clang对应汇编代码讲解 0000000140001000 sub rsp, 28h 0000000140001004 mov [rsp+24h], ecx 0000000140001008 cmp dword ptr [rsp+24h], 0 ;比较变量num是否等于0 000000014000100D jz loc_140001033 ;if (num == 0) goto loc_140001033 0000000140001013 mov eax, [rsp+24h] ;num != 0,进入递归调用 0000000140001017 sub eax, 1 000000014000101A mov ecx, eax ;参数1:num-1 000000014000101C call sub_140001000 ;继续调用自己,形成递归 0000000140001021 add eax, [rsp+24h] 0000000140001025 mov [rsp+24h], eax ;num += accumulation(num-1) 0000000140001029 cmp eax, 0 000000014000102C setnz dl 000000014000102F mov [rsp+23h], dl 0000000140001033 loc_140001033: 0000000140001033 mov eax, [rsp+24h] ;返回变量num 0000000140001037 add rsp, 28h 000000014000103B ret
在代码清单4-16中,通过递归函数accumulation完成了整数累加和计算。在递归函数中,必须有一个出口,本示例选择了逻辑运算“&&”制造递归函数的出口。使用CMP指令检查运算符左边的语句是否为假值,根据跳转指令JZ决定是否跳过程序流程。当变量num为假时,JZ成功跳转,跳过递归函数调用,程序流程将会执行到出口return处。
逻辑运算“||”虽然与逻辑运算“&&”有些不同,但它们的构成原理相同,只须稍做修改就可以解决这一类型的问题。修改代码清单4-16,将逻辑与运算修改为逻辑或运算来实现表达式短路,如代码清单4-17所示。
代码清单4-17 使用逻辑与运算完成表达式短路
// C++源码 #include <stdio.h> //递归函数,用于计算整数累加,num为累加值 int accumulation(int num) { //当num等于0时,逻辑或运算符左边的值为真,不会执行右边语句,形成表达式短路,从而找到递归出口 (num == 0) || (num += accumulation(num - 1)); return num; } int main(int argc, char* argv[]) { accumulation(10); return 0; } //x86_vs对应汇编代码讲解 ;使用逻辑或运算造成的表达式短路,生成的反汇编代码与使用逻辑与是一样的 00401000 push ebp 00401001 mov ebp, esp 00401003 cmp dword ptr [ebp+8], 0 00401007 jz short loc_40101E 00401009 mov eax, [ebp+8] 0040100C sub eax, 1 0040100F push eax 00401010 call sub_401000 00401015 add esp, 4 00401018 add eax, [ebp+8] 0040101B mov [ebp+8], eax 0040101E loc_40101E: 0040101E mov eax, [ebp+8] 00401021 pop ebp 00401022r etn //x86_gcc对应汇编代码相似,省略 //x86_clang对应汇编代码相似,省略 //x64_vs对应汇编代码相似,省略 //x64_gcc对应汇编代码相似,省略 //x64_clang对应汇编代码相似,省略
对比代码清单4-16与代码清单4-17发现,编译器会将两种短路表达式编译为相同的汇编代码。虽然使用的逻辑运算符不同,但在两种情况下,运算符左边的语句块都是在与0值作比较,而且判定的结果都是等于0时不执行运算符右边的语句块,因此就变成了相同的汇编代码。
转换成汇编代码后,通过比较后跳转来实现短路,这种结构实质上就是分支结构。在反汇编代码中是没有表达式短路的,我们能够看到的都是分支结构。分支结构的知识见5.3节。
4.2.3 条件表达式
条件表达式也称为三目运算,根据比较表达式1得到的结果进行选择。如果是真值,执行表达式2;如果是假值,执行表达式3,语句的构成如下。
表达式1?表达式2:表达式3
条件表达式也属于表达式的一种,所以表达式1、表达式2、表达式3都可以套用到条件表达式中。套用条件表达式后,执行顺序依然是由左向右,自内向外。
条件表达式的构成是先判断再选择。但是,编译器并不一定会按照这种方式进行编译,当表达式2与表达式3都为常量时,条件表达式可以被优化;当表达式2或表达式3有变量时,条件表达式可以被优化;当表达式2或表达式3有变量表达式时,会转换成分支结构。当表达式1为常量值时,编译器会在编译期间得到答案,不会存在条件表达式。下面讨论编译器是如何优化及避免使用分支结构的。
条件表达式有如下4种转换方案。
方案1:表达式1为简单比较,而表达式2和表达式3两者为常量且差值等于1。
方案2:表达式2和表达式3两者为常量且差值大于1。
方案3:表达式2或表达式3有变量。
方案4:表达式2或表达式3有变量表达式。
下面通过反汇编的形式对比这4种转换方案,找出它们的特性,分析它们之间的区别。方案1如代码清单4-18所示。
代码清单4-18 条件表达式转换方案1
// C++源码 #include <stdio.h> int main(int argc, char* argv[]) { printf("%d\n", argc == 5 ? 5 : 6); return 0; } //x86_vs对应汇编代码讲解 00401010 xor eax, eax 00401012 cmp dword ptr [esp+4], 5 ;判断argc==5,影响标志寄存器 00401017 setnz al ;eax=标志寄存器ZF的值取反 0040101A add eax, 5 ;if (argc==5) eax=0+5 ;if (argc!=5) eax=1+5 0040101D push eax ;参数1 0040101E push offset unk_412160 ;参数2 00401023 call sub_401030 ;调用printf函数 00401028 add esp, 8 ;平衡栈 0040102B xor eax, eax 0040102D retn //x86_gcc对应汇编代码讲解 00402580 push ebp 00402581 mov ebp, esp 00402583 push ebx 00402584 and esp, 0FFFFFFF0h ;对齐栈 00402587 sub esp, 10h 0040258A mov ebx, [ebp+8] ;ebx=argc 0040258D call ___main ;调用初始化函数 00402592 mov eax, 6 00402597 mov dword ptr [esp], offset aD ;参数1 "%d\n" 0040259E cmp ebx, 5 ;判断argc==5,影响标志寄存器 004025A1 cmovnz ebx, eax ;条件传送指令,argc!=5传送数据,argc==5不传送数据 ;if (argc==5) ebx=5 ;if (argc!=5) ebx=6 004025A4 mov [esp+4], ebx ;参数2 004025A8 call _printf ;调用printf函数 004025AD xor eax, eax 004025AF mov ebx, [ebp-4] 004025B2 leave 004025B3 retn //x86_clang对应汇编代码讲解 00401000 xor eax, eax 00401002 cmp dword ptr [esp+4], 5 ;判断argc==5,影响标志寄存器 00401007 mov ecx, 6 0040100C setz al ;eax=标志寄存器ZF的值 0040100F sub ecx, eax ;if (argc==5) ecx=6-1 ;if (argc!=5) eax=6-0 00401011 push ecx ;参数1 00401012 push offset aD ;参数2 "%d\n" 00401017 call sub_401030 ;调用printf函数 0040101C add esp, 8 ;平衡栈 0040101F xor eax, eax 00401021 retn //x64_vs对应汇编代码讲解 0000000140001010 sub rsp, 28h 0000000140001014 xor edx, edx 0000000140001016 cmp ecx, 5 ;判断argc==5,影响标志寄存器 0000000140001019 lea rcx, aD ;参数1 "%d\n" 0000000140001020 setnz dl ;edx=标志寄存器ZF的值取反 0000000140001023 add edx, 5 ;参数2 if (argc==5) edx=0+5 ;if (argc!=5) edx=1+5 0000000140001026 call sub_140001040 ;调用printf函数 000000014000102B xor eax, eax 000000014000102D add rsp, 28h 0000000140001031 retn //x64_gcc对应汇编代码讲解 0000000000402BF0 push rbx 0000000000402BF1 sub rsp, 20h 0000000000402BF5 mov ebx, ecx 0000000000402BF7 call __main 0000000000402BFC cmp ebx, 5 ;判断argc==5,影响标志寄存器 0000000000402BFF mov eax, 6 0000000000402C04 cmovnz ebx, eax ;条件传送指令,argc!=5传送数据,argc==5不传送数据 ;if (argc==5) ebx=5 ;if (argc!=5) ebx=6 0000000000402C07 lea rcx, aD ;参数1 "%d\n" 0000000000402C0E mov edx, ebx ;参数2 0000000000402C10 call printf ;调用printf函数 0000000000402C15 xor eax, eax 0000000000402C17 add rsp, 20h 0000000000402C1B pop rbx 0000000000402C1C retn //x64_clang对应汇编代码讲解 0000000140001000 sub rsp, 28h 0000000140001004 xor eax, eax 0000000140001006 cmp ecx, 5 ;判断argc==5,影响标志寄存器 0000000140001009 setz al ;eax=标志寄存器ZF的值 000000014000100C mov edx, 6 0000000140001011 sub edx, eax ;参数2 if (argc==5) ecx=6-1 ;if (argc!=5) eax=6-0 0000000140001013 lea rcx, aD ;参数1 "%d\n" 000000014000101A call sub_140001030 ;调用printf函数 000000014000101F xor eax, eax 0000000140001021 add rsp, 28h 0000000140001025 retn
代码清单4-18利用表达式2和表达式3之间的差值1,使用setz、setnz、cmov指令进行无分支优化。这种情况是三目运算中最为简单的转换方式。当表达式2和表达式3之间的差值大于1后,setz、setnz指令就无法满足要求了,如代码清单4-19所示。
代码清单4-19 条件表达式转换方案2
// C++源码 #include <stdio.h> int main(int argc, char* argv[]) { printf("%d\n", argc > 5 ? 4 : 10); return 0; } //x86_vs对应汇编代码讲解 00401010 cmp dword ptr [esp+4], 5 ;判断argc==5,影响标志寄存器 00401015 mov ecx, 4 0040101A mov eax, 0Ah ;eax=10 0040101F cmovg eax, ecx ;条件传送指令,argc>5传送数据,argc<=5不传送数据 ;if (argc>5) eax=4 ;if (argc<=5) eax=10 00401022 push eax ;参数2 00401023 push offset unk_412160 ;参数1 00401028 call sub_401040 ;调用printf函数 0040102D add esp, 8 ;平衡栈 00401030 xor eax, eax 00401032 retn //x86_gcc对应汇编代码讲解 00402580 push ebp 00402581 mov ebp, esp 00402583 and esp, 0FFFFFFF0h ;对齐栈 00402586 sub esp, 10h 00402589 call ___main ;调用初始化函数 0040258E cmp dword ptr [ebp+8], 5 ;判断argc==5,影响标志寄存器 00402592 mov edx, 0Ah 00402597 mov eax, 4 ;eax=4 0040259C cmovle eax, edx ;条件传送指令,argc<=5传送数据,argc>5不传送数据 ;if (argc>5) eax=4 ;if (argc<=5) eax=10 0040259F mov dword ptr [esp], offset aD ;参数1 "%d\n" 004025A6 mov [esp+4], eax ;参数2 004025AA call _printf ;调用printf函数 004025AF xor eax, eax 004025B1 leave 004025B2 retn //x86_clang对应汇编代码讲解 00401000 cmp dword ptr [esp+4], 5 ;判断argc==5,影响标志寄存器 00401005 mov eax, 4 0040100A mov ecx, 0Ah ;ecx=10 0040100F cmovg ecx, eax ;条件传送指令,argc>5传送数据,argc<=5不传送数据 ;if (argc>5) ecx=4 ;if (argc<=5) ecx=10 00401012 push ecx ;参数2 00401013 push offset unk_412160 ;参数1 00401018 call sub_401030 ;调用printf函数 0040101D add esp, 8 ;平衡栈 00401020 xor eax, eax 00401022 retn //x64_vs对应汇编代码讲解 0000000140001010 sub rsp, 28h 0000000140001014 cmp ecx, 5 ;判断argc==5,影响标志寄存器 0000000140001017 mov edx, 0Ah ;edx=10 000000014000101C mov eax, 4 0000000140001021 lea rcx, unk_1400122C0 ;参数1 0000000140001028 cmovg edx, eax ;条件传送指令,argc>5传送数据,argc<=5不传送数据 ;if (argc>5) edx=4 ;if (argc<=5) edx=10 参数2 000000014000102B call sub_140001040 ;调用printf函数 0000000140001030 xor eax, eax 0000000140001032 add rsp, 28h 0000000140001036 retn //x64_gcc对应汇编代码讲解 0000000000402BF0 push rbx 0000000000402BF1 sub rsp, 20h 0000000000402BF5 mov ebx, ecx ;ebx=argc 0000000000402BF7 call __main ;调用初始化函数 0000000000402BFC cmp ebx, 5 ;判断argc==5,影响标志寄存器 0000000000402BFF mov eax, 0Ah 0000000000402C04 mov edx, 4 ;edx=4 0000000000402C09 cmovle edx, eax ;条件传送指令,argc<=5传送数据,argc>5不传送数据 ;if (argc>5) edx=4 ;if (argc<=5) edx=10 参数2 0000000000402C0C lea rcx, aD ;参数1 "%d\n" 0000000000402C13 call printf ;调用printf函数 0000000000402C18 xor eax, eax 0000000000402C1A add rsp, 20h 0000000000402C1E pop rbx 0000000000402C1F retn //x64_clang对应汇编代码讲解 0000000140001000 sub rsp, 28h 0000000140001004 cmp ecx, 5 ;判断argc==5,影响标志寄存器 0000000140001007 mov eax, 4 000000014000100C mov edx, 0Ah ;edx=10 0000000140001011 cmovg edx, eax ;条件传送指令,argc>5传送数据,argc<=5不传送数据 ;if (argc>5) edx=4 ;if (argc<=5) edx=10 参数2 0000000140001014 lea rcx, unk_1400122C0 ;参数1 "%d\n" 000000014000101B call sub_140001030 ;调用printf函数 0000000140001020 xor eax, eax 0000000140001022 add rsp, 28h 0000000140001026 retn
在代码清单4-19中,对于表达式2和表达式3两者为常量且差值大于1,所有编译器都选择使用条件传送指令cmov进行无分支优化。当表达式有变量时,优化方案如代码清单4-20所示。
代码清单4-20 条件表达式转换方案3
// C++源码说明:条件表达式 #include <stdio.h> int main(int argc, char* argv[]) { int n1, n2; scanf("%d %d", &n1, &n2); printf("%d\n", argc ? n1 : n2); return 0; } //x86_vs对应汇编代码讲解 00401020 sub esp, 8 00401023 lea eax, [esp] ;eax=&n2 00401026 push eax ;参数3 00401027 lea eax, [esp+8] ;eax=&n1 0040102B push eax ;参数2 0040102C push offset aDD ;参数1 "%d %d" 00401031 call sub_401090 ;调用scanf函数 00401036 mov eax, [esp+0Ch] ;eax=n2 0040103A cmp dword ptr [esp+18h], 0 ;判断argc==0,影响标志寄存器 0040103F cmovnz eax, [esp+10h] ;条件传送指令,argc!=0传送数据,argc==0不传送数据 ;if (argc==0) eax=n2 ;if (argc!=0) eax=n1 00401044 push eax ;参数2 00401045 push offset aD ;参数1 "%d\n" 0040104A call sub_401060 ;调用printf函数 0040104F xor eax, eax 00401051 add esp, 1Ch 00401054 retn //x86_gcc对应汇编代码讲解 00402590 push ebp 00402591 mov ebp, esp 00402593 and esp, 0FFFFFFF0h ;对齐栈 00402596 sub esp, 20h 00402599 call ___main ;调用初始化函数 0040259E lea eax, [esp+1Ch] ;eax=&n2 004025A2 mov dword ptr [esp], offset aDD ;参数1 "%d %d" 004025A9 mov [esp+8], eax ;参数3 004025AD lea eax, [esp+18h] ;eax=&n1 004025B1 mov [esp+4], eax ;参数2 004025B5 call _scanf ;调用scanf函数 004025BA mov eax, [ebp+8] 004025BD test eax, eax 004025BF jz short loc_4025D9 ;判断argc==0,影响标志寄存器 004025C1 mov eax, [esp+18h] ;if (argc!=0) eax=n1 004025C5 loc_4025C5: 004025C5 mov [esp+4], eax ;参数2 004025C9 mov dword ptr [esp], offset aD ;参数1 "%d\n" 004025D0 call _printf ;调用printf函数 004025D5 xor eax, eax 004025D7 leave 004025D8 retn 004025D9 loc_4025D9: 004025D9 mov eax, [esp+1Ch] ;if (argc==0) eax=n2 004025DD jmp short loc_4025C5 //x86_clang对应汇编代码讲解 00401000 push edi 00401001 push esi 00401002 sub esp, 8 00401005 mov esi, esp ;esi=&n2 00401007 lea edi, [esp+4] ;edi=&n1 0040100B push esi ;参数3 0040100C push edi ;参数2 0040100D push offset aDD ;参数1 "%d %d" 00401012 call sub_401040 ;调用scanf函数 00401017 add esp, 0Ch ;平衡栈 0040101A cmp [esp+14h], 0 ;判断argc==0,影响标志寄存器 0040101F cmovz edi, esi ;条件传送指令,argc==0传送数据,argc!=0不传送数据 ;if (argc==0) eax=&n2 ;if (argc!=0) eax=&n1 00401022 push dword ptr [edi] ;参数2 00401024 push offset aD ;参数1 "%d\n" 00401029 call sub_401080 ;调用printf函数 0040102E add esp, 8 ;平衡栈 00401031 xor eax, eax 00401033 add esp, 8 00401036 pop esi 00401037 pop edi 00401038 retn //x64_vs对应汇编代码讲解 0000000140001020 push rbx 0000000140001022 sub rsp, 20h 0000000140001026 mov ebx, ecx ;ebx=argc 0000000140001028 lea r8, [rsp+30h] ;参数3 r8=&n2 000000014000102D lea rcx, aDD ;参数1 "%d %d" 0000000140001034 lea rdx, [rsp+40h] ;参数2 rdx=&n1 0000000140001039 call sub_1400010C0 ;调用scanf函数 000000014000103E mov edx, [rsp+30h] ;参数2 edx=n2 0000000140001042 lea rcx, aD ;参数1 "%d\n" 0000000140001049 test ebx, ebx ;判断argc==0,影响标志寄存器 000000014000104B cmovnz edx, [rsp+40h] ;条件传送指令,argc!=0传送数据,argc==0不传送数据 ;参数2 if (argc==0) edx=n2 ;参数2 if (argc!=0) edx=n1 0000000140001050 call sub_140001060 ;调用printf函数 0000000140001055 xor eax, eax 0000000140001057 add rsp, 20h 000000014000105B pop rbx 000000014000105C retn //x64_gcc对应汇编代码讲解 0000000000402C00 push rbx 0000000000402C01 sub rsp, 30h 0000000000402C05 mov ebx, ecx ;ebx=argc 0000000000402C07 call __main ;调用初始化函数 0000000000402C0C lea rdx, [rsp+28h] ;参数2 rdx=&n1 0000000000402C11 lea r8, [rsp+2Ch] ;参数3 r8=&n2 0000000000402C16 lea rcx, aDD ;参数1 "%d %d" 0000000000402C1D call scanf ;调用scanf函数 0000000000402C22 test ebx, ebx ;判断argc==0,影响标志寄存器 0000000000402C24 jz short loc_402C3E 0000000000402C26 mov edx, [rsp+28h] ;参数2 if (argc!=0) edx=n1 0000000000402C2A loc_402C2A: 0000000000402C2A lea rcx, aD ;参数1 "%d\n" 0000000000402C31 call printf ;调用printf函数 0000000000402C36 xor eax, eax 0000000000402C38 add rsp, 30h 0000000000402C3C pop rbx 0000000000402C3D retn 0000000000402C3E loc_402C3E: 0000000000402C3E mov edx, [rsp+2Ch] ;参数2 if (argc==0) edx=n2 0000000000402C42 jmp short loc_402C2A //x64_clang对应汇编代码讲解 0000000140001000 push rsi 0000000140001001 push rdi 0000000140001002 push rbx 0000000140001003 sub rsp, 30h 0000000140001007 mov esi, ecx ;esi=argc 0000000140001009 lea rcx, aDD ;参数1 "%d\n" 0000000140001010 lea rdi, [rsp+2Ch] ;rdi=&n1 0000000140001015 lea rbx, [rsp+28h] ;rbx=&n2 000000014000101A mov rdx, rdi ;参数2 rdx=&n1 000000014000101D mov r8, rbx ;参数3 r8=&n2 0000000140001020 call sub_140001050 ;调用scanf函数 0000000140001025 test esi, esi ;判断argc==0,影响标志寄存器 0000000140001027 cmovz rdi, rbx ;条件传送指令,argc==0传送数据,argc!=0不传送数据 ;if (argc==0) eax=&n2 ;if (argc!=0) eax=&n1 000000014000102B mov edx, [rdi] ;参数2 000000014000102D lea rcx, aD ;参数1 "%d\n" 0000000140001034 call sub_1400010B0 ;调用printf函数 0000000140001039 xor eax, eax 000000014000103B add rsp, 30h 000000014000103F pop rbx 0000000140001040 pop rdi 0000000140001041 pop rsi 0000000140001042 retn
在代码清单4-20中,GCC编译器采用语句流程进行比较和判断,而其他编译器依然采用条件传送指令进行优化。当表达式2或表达式3中的值为变量表达式时,就无法使用之前的方案进行优化了。编译器会按照常规语句流程进行比较和判断,选择对应的表达式,如代码清单4-21所示。
代码清单4-21 条件表达式无优化使用分支结果
// C++源码 #include <stdio.h> int main(int argc, char* argv[]) { int n1, n2; scanf("%d %d", &n1, &n2); printf("%d\n", argc ? n1 : n2+3); return 0; } //x86_vs对应汇编代码讲解 00401020 sub esp, 8 00401023 lea eax, [esp+4] 00401027 push eax 00401028 lea eax, [esp+4] 0040102C push eax 0040102D push offset aDD ;"%d %d" 00401032 call sub_4010A0 00401037 add esp, 0Ch 0040103A cmp dword ptr [esp+0Ch], 0 0040103F jz short loc_401055 ;使用语句流程进行比较和判断 00401041 mov eax, [esp] 00401044 push eax 00401045 push offset aD ;"%d\n" 0040104A call sub_401070 0040104F xor eax, eax 00401051 add esp, 10h 00401054 retn 00401055 loc_401055: 00401055 mov eax, [esp+4] 00401059 add eax, 3 0040105C push eax 0040105D push offset aD ;"%d\n" 00401062 call sub_401070 00401067 xor eax, eax 00401069 add esp, 10h 0040106C retn
分析代码清单4-21中的汇编代码可以发现,条件表达式最后转换的汇编代码与分支结构的表现形式非常相似,它的判断比较与表达式短路很类似。详细讲解见5.3节。从P5时代开始,CPU拥有了一种先进的,解决处理分支指令(if-then-else)导致流水线失败的数据处理方法,叫作分支预测(Branch Prediction)功能,由CPU判断程序分支的进行方向,能够加快运算速度。因此在复杂的条件表达式中,使用分支通常比使用条件传送的性能更高。