C++反汇编与逆向分析技术揭秘(第2版)
上QQ阅读APP看书,第一时间看更新

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判断程序分支的进行方向,能够加快运算速度。因此在复杂的条件表达式中,使用分支通常比使用条件传送的性能更高。