《C++反汇编与逆向分析技术揭秘》学习之第4章 观察各种表达式的求值过程(2)

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

笔记--------------------------------------------start

//x86_vs对应汇编代码讲解 Debug版本
//递归函数,用于计算整数累加,num为累加值
int accumulation(int num)
{
00771740  push        ebp  
00771741  mov         ebp,esp  
00771743  sub         esp,0C0h  
00771749  push        ebx  
0077174A  push        esi  
0077174B  push        edi  
0077174C  mov         edi,ebp  
0077174E  xor         ecx,ecx  
00771750  mov         eax,0CCCCCCCCh  
00771755  rep stos    dword ptr es:[edi]  
00771757  mov         ecx,77C003h  
0077175C  call        0077130C  
    //当num等于0时,逻辑与运算符左边的值为假,将不会执行右边语句,
    //形成表达式短路,从而找到递归出口
    num && (num += accumulation(num - 1));
00771761  cmp         dword ptr [ebp+8],0  			;比较变量num是否等于0
00771765  je          0077177C  								;相等则跳转 0077177C
00771767  mov         eax,dword ptr [ebp+8] 		;num != 0,进入递归调用  
0077176A  sub         eax,1  
0077176D  push        eax  											;参数1:num-1
0077176E  call        007711D1  								;继续调用自己,形成递归
00771773  add         esp,4  										;平衡栈
00771776  add         eax,dword ptr [ebp+8]  
00771779  mov         dword ptr [ebp+8],eax  		;num += accumulation(num-1)	
    return num;
0077177C  mov         eax,dword ptr [ebp+8]  
}
0077177F  pop         edi  
00771780  pop         esi  
00771781  pop         ebx  
00771782  add         esp,0C0h  
00771788  cmp         ebp,esp  
0077178A  call        00771235  
0077178F  mov         esp,ebp  
00771791  pop         ebp  
00771792  ret  
......

int main(int argc, char *argv[])
{
007717C0  push        ebp  
007717C1  mov         ebp,esp  
007717C3  sub         esp,0C0h  
007717C9  push        ebx  
007717CA  push        esi  
007717CB  push        edi  
007717CC  mov         edi,ebp  
007717CE  xor         ecx,ecx  
007717D0  mov         eax,0CCCCCCCCh  
007717D5  rep stos    dword ptr es:[edi]  
007717D7  mov         ecx,77C003h  
007717DC  call        0077130C  
    accumulation(10);
007717E1  push        0Ah  
007717E3  call        007711D1  
007717E8  add         esp,4  
    return 0;
007717EB  xor         eax,eax  
}
007717ED  pop         edi  
007717EE  pop         esi  
007717EF  pop         ebx  
007717F0  add         esp,0C0h  
007717F6  cmp         ebp,esp  
007717F8  call        00771235  
007717FD  mov         esp,ebp  
007717FF  pop         ebp  
00771800  ret  
.......
007711D1  jmp         00771740   
.......

//x64_vs对应汇编代码讲解 Debug版本
//递归函数,用于计算整数累加,num为累加值
int accumulation(int num)
{
00007FF78B211740  mov         dword ptr [rsp+8],ecx  
00007FF78B211744  push        rbp  
00007FF78B211745  push        rdi  
00007FF78B211746  sub         rsp,0E8h  
00007FF78B21174D  lea         rbp,[rsp+20h]  
00007FF78B211752  lea         rcx,[00007FF78B221003h]  
00007FF78B211759  call        00007FF78B211343  
    //当num等于0时,逻辑与运算符左边的值为假,将不会执行右边语句,
    //形成表达式短路,从而找到递归出口式短路,从而找到递归出口
    num && (num += accumulation(num - 1));
00007FF78B21175E  cmp         dword ptr [rbp+00000000000000E0h],0		;比较变量num是否等于0 
00007FF78B211765  je          00007FF78B211786  										;相等则跳转00007FF78B211786
00007FF78B211767  mov         eax,dword ptr [rbp+00000000000000E0h] ;num != 0,进入递归调用 
00007FF78B21176D  dec         eax  
00007FF78B21176F  mov         ecx,eax																;参数1:num-1  
00007FF78B211771  call        00007FF78B2111D6  										;继续调用自己,形成递归
00007FF78B211776  mov         ecx,dword ptr [rbp+00000000000000E0h]  
00007FF78B21177C  add         ecx,eax  
00007FF78B21177E  mov         eax,ecx  
00007FF78B211780  mov         dword ptr [rbp+00000000000000E0h],eax	;num += accumulation(num-1)	 
    return num;
00007FF78B211786  mov         eax,dword ptr [rbp+00000000000000E0h]	;返回变量num 
}
00007FF78B21178C  lea         rsp,[rbp+00000000000000C8h]  
00007FF78B211793  pop         rdi  
00007FF78B211794  pop         rbp  
00007FF78B211795  ret     
......
    
int main(int argc, char *argv[])
{
00007FF78B2117C0  mov         qword ptr [rsp+10h],rdx  
00007FF78B2117C5  mov         dword ptr [rsp+8],ecx  
00007FF78B2117C9  push        rbp  
00007FF78B2117CA  push        rdi  
00007FF78B2117CB  sub         rsp,0E8h  
00007FF78B2117D2  lea         rbp,[rsp+20h]  
00007FF78B2117D7  lea         rcx,[00007FF78B221003h]  
00007FF78B2117DE  call        00007FF78B211343  
    accumulation(10);
00007FF78B2117E3  mov         ecx,0Ah  
00007FF78B2117E8  call        00007FF78B2111D6  
    return 0;
00007FF78B2117ED  xor         eax,eax  
}
00007FF78B2117EF  lea         rsp,[rbp+00000000000000C8h]  
00007FF78B2117F6  pop         rdi  
00007FF78B2117F7  pop         rbp  
00007FF78B2117F8  ret 
......
00007FF78B2111D6  jmp         00007FF78B211740
......

笔记--------------------------------------------end

在代码清单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对应汇编代码相似,省略

笔记--------------------------------------------start

//x86_vs对应汇编代码讲解
;使用逻辑或运算造成的表达式短路,生成的反汇编代码与使用逻辑与是一样的
//递归函数,用于计算整数累加,num为累加值
int accumulation(int num)
{
00B41740  push        ebp  
00B41741  mov         ebp,esp  
00B41743  sub         esp,0C0h  
00B41749  push        ebx  
00B4174A  push        esi  
00B4174B  push        edi  
00B4174C  mov         edi,ebp  
00B4174E  xor         ecx,ecx  
00B41750  mov         eax,0CCCCCCCCh  
00B41755  rep stos    dword ptr es:[edi]  
00B41757  mov         ecx,0B4C003h  
00B4175C  call        00B4130C  
    //当num等于0时,逻辑或运算符左边的值为真,不会执行右边语句,
    //形成表达式短路,从而找到递归出口
    (num == 0) || (num += accumulation(num - 1));
00B41761  cmp         dword ptr [ebp+8],0  			;比较变量num是否等于0
00B41765  je          00B4177C  								;相等跳转到00B4177C
00B41767  mov         eax,dword ptr [ebp+8]  		;num != 0,进入递归调用
00B4176A  sub         eax,1  
00B4176D  push        eax  											;参数1:num-1
00B4176E  call        00B411D1  								;继续调用自己,形成递归
00B41773  add         esp,4  										;平衡栈
00B41776  add         eax,dword ptr [ebp+8]  
00B41779  mov         dword ptr [ebp+8],eax  		;num += accumulation(num-1)
    return num;
00B4177C  mov         eax,dword ptr [ebp+8]  		;返回变量num
}
00B4177F  pop         edi  
00B41780  pop         esi  
00B41781  pop         ebx  
00B41782  add         esp,0C0h  
00B41788  cmp         ebp,esp  
00B4178A  call        00B41235  
00B4178F  mov         esp,ebp  
00B41791  pop         ebp  
00B41792  ret  
......

int main(int argc, char *argv[])
{
00B417C0  push        ebp  
00B417C1  mov         ebp,esp  
00B417C3  sub         esp,0C0h  
00B417C9  push        ebx  
00B417CA  push        esi  
00B417CB  push        edi  
00B417CC  mov         edi,ebp  
00B417CE  xor         ecx,ecx  
00B417D0  mov         eax,0CCCCCCCCh  
00B417D5  rep stos    dword ptr es:[edi]  
00B417D7  mov         ecx,0B4C003h  
00B417DC  call        00B4130C  
    accumulation(10);
00B417E1  push        0Ah  					;参数
00B417E3  call        00B411D1  		;调用accumulation函数
00B417E8  add         esp,4  
    return 0;
00B417EB  xor         eax,eax  
}
00B417ED  pop         edi  
00B417EE  pop         esi  
00B417EF  pop         ebx  
00B417F0  add         esp,0C0h  
00B417F6  cmp         ebp,esp  
00B417F8  call        00B41235  
00B417FD  mov         esp,ebp  
00B417FF  pop         ebp  
00B41800  ret  
......
00B411D1  jmp         00B41740 
......

笔记--------------------------------------------end

对比代码清单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

笔记--------------------------------------------start

//x86_vs对应汇编代码讲解 (Debug版)
int main(int argc, char *argv[])
{
00851840  push        ebp  
00851841  mov         ebp,esp  
00851843  sub         esp,0C4h  
00851849  push        ebx  
0085184A  push        esi  
0085184B  push        edi  
0085184C  lea         edi,[ebp-4]  
0085184F  mov         ecx,1  
00851854  mov         eax,0CCCCCCCCh  
00851859  rep stos    dword ptr es:[edi]  
0085185B  mov         ecx,85C003h  
00851860  call        00851316  
    printf("%d\n", argc == 5 ? 5 : 6);
00851865  cmp         dword ptr [ebp+8],5  						;判断argc==5,影响标志寄存器
00851869  jne         00851877												;不相等跳转到 00851877
0085186B  mov         dword ptr [ebp+FFFFFF3Ch],5 		;5赋值于内存[ebp+FFFFFF3Ch]
00851875  jmp         00851881  											;跳转到00851881
00851877  mov         dword ptr [ebp+FFFFFF3Ch],6  		;6赋值于内存[ebp+FFFFFF3Ch]
00851881  mov         eax,dword ptr [ebp+FFFFFF3Ch]  	;把结果赋值给eax 
00851887  push        eax  														;参数1
00851888  push        857B30h  												;参数2
0085188D  call        008510CD  											;调用printf函数
00851892  add         esp,8														;平衡栈  
    return 0;
00851895  xor         eax,eax  
}
00851897  pop         edi  
00851898  pop         esi  
00851899  pop         ebx  
0085189A  add         esp,0C4h  
008518A0  cmp         ebp,esp  
008518A2  call        0085123F  
008518A7  mov         esp,ebp  
008518A9  pop         ebp  
008518AA  ret  

//x64_vs对应汇编代码讲解 (Debug版)
int main(int argc, char *argv[])
{
00007FF7A4871850  mov         qword ptr [rsp+10h],rdx  
00007FF7A4871855  mov         dword ptr [rsp+8],ecx  
00007FF7A4871859  push        rbp  
00007FF7A487185A  push        rdi  
00007FF7A487185B  sub         rsp,0F8h  
00007FF7A4871862  lea         rbp,[rsp+20h]  
00007FF7A4871867  lea         rcx,[00007FF7A4881003h]  
00007FF7A487186E  call        00007FF7A4871357  
    printf("%d\n", argc == 5 ? 5 : 6);
00007FF7A4871873  cmp         dword ptr [rbp+00000000000000F0h],5		;判断argc==5,影响标志寄存器
00007FF7A487187A  jne         00007FF7A4871888  										;不相等跳转到00007FF7A4871888
00007FF7A487187C  mov         dword ptr [rbp+00000000000000C0h],5		;结果为5  
00007FF7A4871886  jmp         00007FF7A4871892											;跳转到00007FF7A4871892
00007FF7A4871888  mov         dword ptr [rbp+00000000000000C0h],6		;结果为6  
00007FF7A4871892  mov         edx,dword ptr [rbp+00000000000000C0h]	;参数2  edx 
00007FF7A4871898  lea         rcx,[00007FF7A4879C24h]  							;参数1 	rcx
00007FF7A487189F  call        00007FF7A487118B											;调用printf函数
    return 0;
00007FF7A48718A4  xor         eax,eax  
}
00007FF7A48718A6  lea         rsp,[rbp+00000000000000D8h]  
00007FF7A48718AD  pop         rdi  
00007FF7A48718AE  pop         rbp  
00007FF7A48718AF  ret  

笔记--------------------------------------------end

代码清单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

笔记--------------------------------------------start

//x86_vs对应汇编代码讲解 (debug版)
int main(int argc, char *argv[])
{
00D51840  push        ebp  
00D51841  mov         ebp,esp  
00D51843  sub         esp,0C4h  
00D51849  push        ebx  
00D5184A  push        esi  
00D5184B  push        edi  
00D5184C  lea         edi,[ebp-4]  
00D5184F  mov         ecx,1  
00D51854  mov         eax,0CCCCCCCCh  
00D51859  rep stos    dword ptr es:[edi]  
00D5185B  mov         ecx,0D5C003h  
00D51860  call        00D51316  
    printf("%d\n", argc == 5 ? 4 : 10);
00D51865  cmp         dword ptr [ebp+8],5  					;判断argc==5,影响标志寄存器
00D51869  jne         00D51877  										;不相等跳转到 00D51877
00D5186B  mov         dword ptr [ebp+FFFFFF3Ch],4  	;4赋值于内存[ebp+FFFFFF3Ch]
00D51875  jmp         00D51881  										;跳转到 00D51881
00D51877  mov         dword ptr [ebp+FFFFFF3Ch],0Ah ;10赋值于内存[ebp+FFFFFF3Ch]
00D51881  mov         eax,dword ptr [ebp+FFFFFF3Ch] ;把结果赋值给eax 
00D51887  push        eax  													;参数1
00D51888  push        0D57B30h  										;参数2
00D5188D  call        00D510CD  										;调用printf函数
00D51892  add         esp,8  												;平衡栈
    return 0;
00D51895  xor         eax,eax  
}
00D51897  pop         edi  
00D51898  pop         esi  
00D51899  pop         ebx  
00D5189A  add         esp,0C4h  
00D518A0  cmp         ebp,esp  
00D518A2  call        00D5123F  
00D518A7  mov         esp,ebp  
00D518A9  pop         ebp  
00D518AA  ret

//x64_vs对应汇编代码讲解 (debug版)
int main(int argc, char *argv[])
{
00007FF730091850  mov         qword ptr [rsp+10h],rdx  
00007FF730091855  mov         dword ptr [rsp+8],ecx  
00007FF730091859  push        rbp  
00007FF73009185A  push        rdi  
00007FF73009185B  sub         rsp,0F8h  
00007FF730091862  lea         rbp,[rsp+20h]  
00007FF730091867  lea         rcx,[00007FF7300A1003h]  
00007FF73009186E  call        00007FF730091357  
    printf("%d\n", argc == 5 ? 4 : 10);
00007FF730091873  cmp         dword ptr [rbp+00000000000000F0h],5		;判断argc==5,影响标志寄存器 
00007FF73009187A  jne         00007FF730091888  										;不相等跳转到 00007FF730091888
00007FF73009187C  mov         dword ptr [rbp+00000000000000C0h],4 	;4赋值于内存 
00007FF730091886  jmp         00007FF730091892  										;跳转到00007FF730091892
00007FF730091888  mov         dword ptr [rbp+00000000000000C0h],0Ah ;10赋值于内存
00007FF730091892  mov         edx,dword ptr [rbp+00000000000000C0h]	;把结果赋值给参数2 edx
00007FF730091898  lea         rcx,[00007FF730099C24h]  							;参数1  rcx
00007FF73009189F  call        00007FF73009118B  										;调用printf函数
    return 0;
00007FF7300918A4  xor         eax,eax  
}
00007FF7300918A6  lea         rsp,[rbp+00000000000000D8h]  
00007FF7300918AD  pop         rdi  
00007FF7300918AE  pop         rbp  
00007FF7300918AF  ret  

总结:

在Microsoft Visual Studio Community 2019 版本 16.11.42的编译环境下,方案1和方案2反汇编代码是一样的。

笔记--------------------------------------------end

在代码清单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

笔记--------------------------------------------start

//x86_vs对应汇编代码讲解 (Debug版本)
int main(int argc, char *argv[])
{
00501930  push        ebp  
00501931  mov         ebp,esp  
00501933  sub         esp,0E0h  
00501939  push        ebx  
0050193A  push        esi  
0050193B  push        edi  
0050193C  lea         edi,[ebp-20h]  
0050193F  mov         ecx,8  
00501944  mov         eax,0CCCCCCCCh  
00501949  rep stos    dword ptr es:[edi]  
0050194B  mov         eax,dword ptr ds:[0050A004h]  
00501950  xor         eax,ebp  
00501952  mov         dword ptr [ebp-4],eax  
00501955  mov         ecx,50C003h  
0050195A  call        00501325  
    int n1, n2;
    scanf_s("%d %d", &n1, &n2);
0050195F  lea         eax,[ebp-18h]  										;eax=&n2
00501962  push        eax  															;参数3
00501963  lea         ecx,[ebp-0Ch] 										;eax=&n1 
00501966  push        ecx  															;参数2
00501967  push        507B30h  													;参数1 "%d %d"
0050196C  call        00501280  												;调用scanf函数
00501971  add         esp,0Ch  													;栈平衡
    printf("%d\n", argc ?  n1 : n2);
00501974  cmp         dword ptr [ebp+8],0  							;判断argc==0,影响标志寄存器
00501978  je          00501985  												;相等跳转到 00501985
0050197A  mov         eax,dword ptr [ebp-0Ch]  					;eax = n1
0050197D  mov         dword ptr [ebp+FFFFFF20h],eax  
00501983  jmp         0050198E  												;跳转到 0050198E
00501985  mov         ecx,dword ptr [ebp-18h]  					;ecx = n2
00501988  mov         dword ptr [ebp+FFFFFF20h],ecx  
0050198E  mov         edx,dword ptr [ebp+FFFFFF20h]  		;把结果赋给edx
00501994  push        edx  															;参数2  edx 入栈
00501995  push        507B38h  													;参数1  "%d\n" 入栈
0050199A  call        005010D7  												;调用printf函数
0050199F  add         esp,8 														;栈平衡 
    return 0;
005019A2  xor         eax,eax  
}

//x64_vs对应汇编代码讲解 (Debug版本)
int main(int argc, char *argv[])
{
00007FF69DD71940  mov         qword ptr [rsp+10h],rdx  
00007FF69DD71945  mov         dword ptr [rsp+8],ecx  
00007FF69DD71949  push        rbp  
00007FF69DD7194A  push        rdi  
00007FF69DD7194B  sub         rsp,128h  
00007FF69DD71952  lea         rbp,[rsp+20h]  
00007FF69DD71957  lea         rdi,[rsp+20h]  
00007FF69DD7195C  mov         ecx,12h  
00007FF69DD71961  mov         eax,0CCCCCCCCh  
00007FF69DD71966  rep stos    dword ptr [rdi]  
00007FF69DD71968  mov         ecx,dword ptr [rsp+0000000000000148h]  
00007FF69DD7196F  mov         rax,qword ptr [00007FF69DD7D008h]  
00007FF69DD71976  xor         rax,rbp  
00007FF69DD71979  mov         qword ptr [rbp+00000000000000F8h],rax  
00007FF69DD71980  lea         rcx,[00007FF69DD82003h]  
00007FF69DD71987  call        00007FF69DD71366  
    int n1, n2;
    scanf_s("%d %d", &n1, &n2);
00007FF69DD7198C  lea         r8,[rbp+24h]  										;参数3 r8=&n2
00007FF69DD71990  lea         rdx,[rbp+4]  											;参数2 rdx=&n1
00007FF69DD71994  lea         rcx,[00007FF69DD7AD70h]  					;参数1 "%d %d"
00007FF69DD7199B  call        00007FF69DD711F4  								;调用scanf_s函数
    printf("%d\n", argc ?  n1 : n2);
00007FF69DD719A0  cmp         dword ptr [rbp+0000000000000120h],0 ;判断argc==0,影响标志寄存器
00007FF69DD719A7  je          00007FF69DD719B4  									;相等跳转到00007FF69DD719B4
00007FF69DD719A9  mov         eax,dword ptr [rbp+4]								;eax = n1  
00007FF69DD719AC  mov         dword ptr [rbp+00000000000000F4h],eax  
00007FF69DD719B2  jmp         00007FF69DD719BD 										;跳转到 00007FF69DD719BD
00007FF69DD719B4  mov         eax,dword ptr [rbp+24h]  						;eax = n2
00007FF69DD719B7  mov         dword ptr [rbp+00000000000000F4h],eax  ;结果保存在内存中
00007FF69DD719BD  mov         edx,dword ptr [rbp+00000000000000F4h]  ;把结果赋给参数2  edx
00007FF69DD719C3  lea         rcx,[00007FF69DD7AD78h]  								;参数1
00007FF69DD719CA  call        00007FF69DD71190  											;调用printf函数
    return 0;
00007FF69DD719CF  xor         eax,eax  
}
00007FF69DD719D1  mov         edi,eax  
00007FF69DD719D3  lea         rcx,[rbp-20h]  
00007FF69DD719D7  lea         rdx,[00007FF69DD7AD40h]  
00007FF69DD719DE  call        00007FF69DD71302  
00007FF69DD719E3  mov         eax,edi  
00007FF69DD719E5  mov         rcx,qword ptr [rbp+00000000000000F8h]  
00007FF69DD719EC  xor         rcx,rbp  
00007FF69DD719EF  call        00007FF69DD711A9  
00007FF69DD719F4  lea         rsp,[rbp+0000000000000108h]  
00007FF69DD719FB  pop         rdi  
00007FF69DD719FC  pop         rbp  
00007FF69DD719FD  ret  

笔记--------------------------------------------end

在代码清单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

笔记--------------------------------------------start

//x86_vs对应汇编代码讲解 (Debug版)
int main(int argc, char *argv[])
{
00641930  push        ebp  
00641931  mov         ebp,esp  
00641933  sub         esp,0E0h  
00641939  push        ebx  
0064193A  push        esi  
0064193B  push        edi  
0064193C  lea         edi,[ebp-20h]  
0064193F  mov         ecx,8  
00641944  mov         eax,0CCCCCCCCh  
00641949  rep stos    dword ptr es:[edi]  
0064194B  mov         eax,dword ptr ds:[0064A004h]  
00641950  xor         eax,ebp  
00641952  mov         dword ptr [ebp-4],eax  
00641955  mov         ecx,64C003h  
0064195A  call        00641325  
    int n1, n2;
    scanf_s("%d %d", &n1, &n2);
0064195F  lea         eax,[ebp-18h]  					;eax=&n2
00641962  push        eax  										;参数3   (根据push的顺序判断)
00641963  lea         ecx,[ebp-0Ch]  					;ecx=&n1
00641966  push        ecx  										;参数2
00641967  push        647B30h  								;参数1  "%d %d"
0064196C  call        00641280  							;调用printf函数
00641971  add         esp,0Ch  								
    printf("%d\n", argc ?  n1 : n2 + 3);
00641974  cmp         dword ptr [ebp+8],0  		;比较argc是否等于0
00641978  je          00641985 								;使用语句流程进行比较和判断,相等跳转到 00641985
0064197A  mov         eax,dword ptr [ebp-0Ch] ;eax=n1 
0064197D  mov         dword ptr [ebp+FFFFFF20h],eax  
00641983  jmp         00641991  							;跳转到 00641991
00641985  mov         ecx,dword ptr [ebp-18h] ;ecx=n2 
00641988  add         ecx,3  									;ecx += 3
0064198B  mov         dword ptr [ebp+FFFFFF20h],ecx  
00641991  mov         edx,dword ptr [ebp+FFFFFF20h]  
00641997  push        edx  										;参数2
00641998  push        647B38h  								;参数1:  "%d\n"
0064199D  call        006410D7  							;调用printf函数
006419A2  add         esp,8  
    return 0;
006419A5  xor         eax,eax  
}
006419A7  push        edx  
006419A8  mov         ecx,ebp  
006419AA  push        eax  
006419AB  lea         edx,ds:[006419D8h]  
006419B1  call        006411EA  
006419B6  pop         eax  
006419B7  pop         edx  
006419B8  pop         edi  
006419B9  pop         esi  
006419BA  pop         ebx  
006419BB  mov         ecx,dword ptr [ebp-4]  
006419BE  xor         ecx,ebp  
006419C0  call        0064114F  
006419C5  add         esp,0E0h  
006419CB  cmp         ebp,esp  
006419CD  call        00641249  
006419D2  mov         esp,ebp  
006419D4  pop         ebp  
006419D5  ret  

//x64_vs对应汇编代码讲解 (Debug版)
int main(int argc, char *argv[])
{
00007FF7655D1940  mov         qword ptr [rsp+10h],rdx  
00007FF7655D1945  mov         dword ptr [rsp+8],ecx  
00007FF7655D1949  push        rbp  
00007FF7655D194A  push        rdi  
00007FF7655D194B  sub         rsp,128h  
00007FF7655D1952  lea         rbp,[rsp+20h]  
00007FF7655D1957  lea         rdi,[rsp+20h]  
00007FF7655D195C  mov         ecx,12h  
00007FF7655D1961  mov         eax,0CCCCCCCCh  
00007FF7655D1966  rep stos    dword ptr [rdi]  
00007FF7655D1968  mov         ecx,dword ptr [rsp+0000000000000148h]  
00007FF7655D196F  mov         rax,qword ptr [00007FF7655DD008h]  
00007FF7655D1976  xor         rax,rbp  
00007FF7655D1979  mov         qword ptr [rbp+00000000000000F8h],rax  
00007FF7655D1980  lea         rcx,[00007FF7655E2003h]  
00007FF7655D1987  call        00007FF7655D1366  
    int n1, n2;
    scanf_s("%d %d", &n1, &n2);
00007FF7655D198C  lea         r8,[rbp+24h]  		;参数3 &n2
00007FF7655D1990  lea         rdx,[rbp+4]  			;参数2 &n1
00007FF7655D1994  lea         rcx,[00007FF7655DAD70h] ;参数1:  "%d %d" 
00007FF7655D199B  call        00007FF7655D11F4  ;调用scanf_s函数
    printf("%d\n", argc ?  n1 : n2 + 3);
00007FF7655D19A0  cmp         dword ptr [rbp+0000000000000120h],0  ;比较argc == 0
00007FF7655D19A7  je          00007FF7655D19B4  		;相等跳转到00007FF7655D19B4
00007FF7655D19A9  mov         eax,dword ptr [rbp+4] ;eax = n1 
00007FF7655D19AC  mov         dword ptr [rbp+00000000000000F4h],eax  
00007FF7655D19B2  jmp         00007FF7655D19C0  
00007FF7655D19B4  mov         eax,dword ptr [rbp+24h]  ;eax = n2
00007FF7655D19B7  add         eax,3  										;eax = n2 + 3
00007FF7655D19BA  mov         dword ptr [rbp+00000000000000F4h],eax  
00007FF7655D19C0  mov         edx,dword ptr [rbp+00000000000000F4h]  ;参数2
00007FF7655D19C6  lea         rcx,[00007FF7655DAD78h]  							;参数1
00007FF7655D19CD  call        00007FF7655D1190  					;调用prinf函数
    return 0;
00007FF7655D19D2  xor         eax,eax  
}
00007FF7655D19D4  mov         edi,eax  
00007FF7655D19D6  lea         rcx,[rbp-20h]  
00007FF7655D19DA  lea         rdx,[00007FF7655DAD40h]  
00007FF7655D19E1  call        00007FF7655D1302  
00007FF7655D19E6  mov         eax,edi  
00007FF7655D19E8  mov         rcx,qword ptr [rbp+00000000000000F8h]  
00007FF7655D19EF  xor         rcx,rbp  
00007FF7655D19F2  call        00007FF7655D11A9  
00007FF7655D19F7  lea         rsp,[rbp+0000000000000108h]  
00007FF7655D19FE  pop         rdi  
00007FF7655D19FF  pop         rbp  
00007FF7655D1A00  ret  

笔记--------------------------------------------end

分析代码清单4-21中的汇编代码可以发现,条件表达式最后转换的汇编代码与分支结构的表现形式非常相似,它的判断比较与表达式短路很类似。详细讲解见5.3节。从P5时代开始,CPU拥有了一种先进的,解决处理分支指令(if-then-else)导致流水线失败的数据处理方法,叫作分支预测(Branch Prediction)功能,由CPU判断程序分支的进行方向,能够加快运算速度。因此在复杂的条件表达式中,使用分支通常比使用条件传送的性能更高。

4.3 位运算

二进制数据的运算称为位运算,位运算操作符如下。

“<<”:左移运算,最高位左移到CF中,最低位零。

“>>”:右移运算,最高位不变,最低位右移到CF中。

“|”:位或运算,在两个数的相同位上,只要有一个为1,则结果为1。

“&”:位与运算,在两个数的相同位上,只有同时为1时,结果才为1。

“^”:异或运算,在两个数的相同位上,当两个值相同时为0,不同时为1。

“~”:取反运算,将操作数每一位上的1变0,0变1。

在程序算法中大量使用位运算,如不可逆算法MD5,就是通过大量位运算完成的。如何使一个数不可逆转呢?利用位运算就可以达到这个目的,如x & 0结果为0,而根据结果,是不可以逆推x的值的。由于大多数位运算会导致数据信息的丢失(取反~和异或^可以反推),因此,在知道原算法的前提下,使用逆转算法是无法计算出原数据的。在算术运算中,编译器会将各种运算转换成位运算,因此掌握位运算对于学会算法识别是一件非常重要的事。在编译器中,位运算符号又是如何转换成汇编代码的呢?请看代码清单4-22。

代码清单4-22 位运算Debug版

// C++源码
#include <stdio.h>

int main(int argc, char* argv[]) {
  argc = argc << 3;        //将变量argc左移3位
  argc = argc >> 5;        //将变量argc右移5位
  argc = argc | 0xFFFF0000;  //将变量argc与0xFFFF0000做位或运算
  argc = argc & 0x0000FFFF; //将变量argc与0x0000FFFF做位与运算
  argc = argc ^ 0xFFFF0000;  //将变量argc与0x FFFF0000做异或运算
  argc = ~argc;              //对变量argc按位取反
  return argc;               //返回argc
}

//x86_vs对应汇编代码讲解
00401000    push    ebp
00401001    mov     ebp, esp
00401003    mov     eax, [ebp+8]               ;eax=argc
00401006    shl     eax, 3                     ;eax=argc<<3
00401009    mov     [ebp+8], eax               ;argc=argc<<3
0040100C    mov     ecx, [ebp+8]               ;ecx=argc
0040100F    sar     ecx, 5                     ;有符号右移,ecx=argc>>5
00401012    mov     [ebp+8], ecx               ;argc=argc>>5
00401015    mov     edx, [ebp+8]               ;edx=argc
00401018    or      edx, 0FFFF0000h            ;edx=argc|0xFFFF0000
0040101E    mov     [ebp+8], edx               ;argc=argc|0xFFFF0000
00401021    mov     eax, [ebp+8]               ;eax=argc
00401024    and     eax, 0FFFFh                ;eax=argc & 0x0000FFFF
00401029    mov     [ebp+8], eax               ;argc=argc & 0x0000FFFF
0040102C    mov     ecx, [ebp+8]               ;ecx=argc
0040102F    xor     ecx, 0FFFF0000h            ;ecx=argc ^ 0xFFFF0000
00401035    mov     [ebp+8], ecx               ;argc=argc ^ 0xFFFF0000
00401038    mov     edx, [ebp+8]               ;edx=argc
0040103B    not     edx                        ;edx=~argc
0040103D    mov     [ebp+8], edx               ;argc=~argc
00401040    mov     eax, [ebp+8]
00401043    pop     ebp
00401044    retn                               ;return argc

//x86_gcc  对应汇编代码相似,省略
//x86_clang对应汇编代码相似,省略
//x64_vs   对应汇编代码相似,省略
//x64_gcc  对应汇编代码相似,省略
//x64_clang对应汇编代码相似,省略

笔记--------------------------------------------start

//x86_vs对应汇编代码讲解 (Debug)
int main(int argc, char *argv[])
{
00921750  push        ebp  
00921751  mov         ebp,esp  
00921753  sub         esp,0C0h  
00921759  push        ebx  
0092175A  push        esi  
0092175B  push        edi  
0092175C  mov         edi,ebp  
0092175E  xor         ecx,ecx  
00921760  mov         eax,0CCCCCCCCh  
00921765  rep stos    dword ptr es:[edi]  
00921767  mov         ecx,92C003h  
0092176C  call        00921307  
    argc = argc << 3;               //将变量argc左移3位
00921771  mov         eax,dword ptr [ebp+8]  	;eax=argc
00921774  shl         eax,3  									;eax=argc<<3
00921777  mov         dword ptr [ebp+8],eax  	;argc=argc<<3
    argc = argc >> 5;               //将变量argc右移5位
0092177A  mov         eax,dword ptr [ebp+8]  	;eax=argc
0092177D  sar         eax,5  									;有符号右移,eax=argc>>5
00921780  mov         dword ptr [ebp+8],eax  	;argc=argc>>5
    argc = argc | 0xFFFF0000;   //将变量argc与0xFFFF0000做位或运算
00921783  mov         eax,dword ptr [ebp+8]  	;eax=argc
00921786  or          eax,0FFFF0000h 					;eax=argc|0xFFFF0000 
0092178B  mov         dword ptr [ebp+8],eax  	;argc=argc|0xFFFF0000
    argc = argc & 0x0000FFFF; //将变量argc与0x0000FFFF做位与运算
0092178E  mov         eax,dword ptr [ebp+8]  	;eax=argc
00921791  and         eax,0FFFFh  						;eax=argc & 0x0000FFFF
00921796  mov         dword ptr [ebp+8],eax  	;argc=argc & 0x0000FFFF
    argc = argc ^ 0xFFFF0000; //将变量argc与0x FFFF0000做异或运算
00921799  mov         eax,dword ptr [ebp+8]  	;eax=argc
0092179C  xor         eax,0FFFF0000h  				;eax=argc^0xFFFF0000
009217A1  mov         dword ptr [ebp+8],eax  	;argc=argc^0xFFFF0000
    argc = ~argc;                       //对变量argc按位取反
009217A4  mov         eax,dword ptr [ebp+8]  	;eax=argc
009217A7  not         eax  										;eax = ~argc 
009217A9  mov         dword ptr [ebp+8],eax  	;argc = ~argc
    return argc;
009217AC  mov         eax,dword ptr [ebp+8]  	;eax=argc
}
009217AF  pop         edi  
009217B0  pop         esi  
009217B1  pop         ebx  
009217B2  add         esp,0C0h  
009217B8  cmp         ebp,esp  
009217BA  call        00921230  
009217BF  mov         esp,ebp  
009217C1  pop         ebp  
009217C2  ret  

//x64_vs对应汇编代码讲解 (Debug)
int main(int argc, char *argv[])
{
00007FF683A21750  mov         qword ptr [rsp+10h],rdx  
00007FF683A21755  mov         dword ptr [rsp+8],ecx  
00007FF683A21759  push        rbp  
00007FF683A2175A  push        rdi  
00007FF683A2175B  sub         rsp,0E8h  
00007FF683A21762  lea         rbp,[rsp+20h]  
00007FF683A21767  lea         rcx,[00007FF683A31003h]  
00007FF683A2176E  call        00007FF683A2133E  
    argc = argc << 3;               //将变量argc左移3位
00007FF683A21773  mov         eax,dword ptr [rbp+00000000000000E0h]  
00007FF683A21779  shl         eax,3  
00007FF683A2177C  mov         dword ptr [rbp+00000000000000E0h],eax  
    argc = argc >> 5;               //将变量argc右移5位
00007FF683A21782  mov         eax,dword ptr [rbp+00000000000000E0h]  
00007FF683A21788  sar         eax,5  
00007FF683A2178B  mov         dword ptr [rbp+00000000000000E0h],eax  
    argc = argc | 0xFFFF0000;   //将变量argc与0xFFFF0000做位或运算
00007FF683A21791  mov         eax,dword ptr [rbp+00000000000000E0h]  
00007FF683A21797  or          eax,0FFFF0000h  
00007FF683A2179C  mov         dword ptr [rbp+00000000000000E0h],eax  
    argc = argc & 0x0000FFFF; //将变量argc与0x0000FFFF做位与运算
00007FF683A217A2  mov         eax,dword ptr [rbp+00000000000000E0h]  
00007FF683A217A8  and         eax,0FFFFh  
00007FF683A217AD  mov         dword ptr [rbp+00000000000000E0h],eax  
    argc = argc ^ 0xFFFF0000; //将变量argc与0x FFFF0000做异或运算
00007FF683A217B3  mov         eax,dword ptr [rbp+00000000000000E0h]  
00007FF683A217B9  xor         eax,0FFFF0000h  
00007FF683A217BE  mov         dword ptr [rbp+00000000000000E0h],eax  
    argc = ~argc;                       //对变量argc按位取反
00007FF683A217C4  mov         eax,dword ptr [rbp+00000000000000E0h]  
00007FF683A217CA  not         eax  
00007FF683A217CC  mov         dword ptr [rbp+00000000000000E0h],eax  
    return argc;
00007FF683A217D2  mov         eax,dword ptr [rbp+00000000000000E0h]  
}
00007FF683A217D8  lea         rsp,[rbp+00000000000000C8h]  
00007FF683A217DF  pop         rdi  
00007FF683A217E0  pop         rbp  
00007FF683A217E1  ret  

笔记--------------------------------------------end

代码清单4-22演示了有符号数的移位运算,对于无符号数而言,转换的位移指令将会发生转变,如代码清单4-23所示。

代码清单4-23 无符号数位移(Debug版)

// C++源码
#include <stdio.h>

int main(int argc, char* argv[]) {
  unsigned int n = argc;
  n <<= 3;
  n >>= 5;
  return n;
}
//x86_vs对应汇编代码讲解
00401000    push    ebp
00401001    mov     ebp, esp
00401003    push    ecx
00401004    mov     eax, [ebp+8]         ;eax=argc
00401007    mov     [ebp-4], eax         ;n=argc
0040100A    mov     ecx, [ebp-4]         ;ecx=n
0040100D    shl     ecx, 3               ;ecx=argc<<3
00401010    mov     [ebp-4], ecx         ;n=argc << 3
00401013    mov     edx, [ebp-4]         ;edx=n
00401016    shr     edx, 5               ;无符号右移,edx=argc >> 5
00401019    mov     [ebp-4], edx         ;n=argc >> 5
0040101C    mov     eax, [ebp-4]
0040101F    mov     esp, ebp
00401021    pop     ebp
00401022    retn

//x86_gcc  对应汇编代码相似,省略
//x86_clang对应汇编代码相似,省略
//x64_vs   对应汇编代码相似,省略
//x64_gcc  对应汇编代码相似,省略
//x64_clang对应汇编代码相似,省略

笔记--------------------------------------------start

//x86_vs对应汇编代码讲解 (Debug版)
int main(int argc, char *argv[])
{
005F1750  push        ebp  
005F1751  mov         ebp,esp  
005F1753  sub         esp,0CCh  
005F1759  push        ebx  
005F175A  push        esi  
005F175B  push        edi  
005F175C  lea         edi,[ebp-0Ch]  
005F175F  mov         ecx,3  
005F1764  mov         eax,0CCCCCCCCh  
005F1769  rep stos    dword ptr es:[edi]  
005F176B  mov         ecx,5FC003h  
005F1770  call        005F1307  
    unsigned int n = argc;
005F1775  mov         eax,dword ptr [ebp+8]  ;eax=argc
005F1778  mov         dword ptr [ebp-8],eax  ;n=argc
    n <<= 3;
005F177B  mov         eax,dword ptr [ebp-8]  ;eax=n
005F177E  shl         eax,3  								 ;eax=argc<<3
005F1781  mov         dword ptr [ebp-8],eax  ;n=argc << 3
    n >>= 5;
005F1784  mov         eax,dword ptr [ebp-8]  ;eax=n
005F1787  shr         eax,5  								 ;无符号右移,eax=argc >> 5
005F178A  mov         dword ptr [ebp-8],eax  ;n=argc >> 5 
    return n;
005F178D  mov         eax,dword ptr [ebp-8]  ;eax=n
}
005F1790  pop         edi  
005F1791  pop         esi  
005F1792  pop         ebx  
005F1793  add         esp,0CCh  
005F1799  cmp         ebp,esp  
005F179B  call        005F1230  
005F17A0  mov         esp,ebp  
005F17A2  pop         ebp  
005F17A3  ret  

//x64_vs对应汇编代码讲解 (Debug版)
int main(int argc, char *argv[])
{
00007FF78C451750  mov         qword ptr [rsp+10h],rdx  
00007FF78C451755  mov         dword ptr [rsp+8],ecx  
00007FF78C451759  push        rbp  
00007FF78C45175A  push        rdi  
00007FF78C45175B  sub         rsp,108h  
00007FF78C451762  lea         rbp,[rsp+20h]  
00007FF78C451767  lea         rcx,[00007FF78C461003h]  
00007FF78C45176E  call        00007FF78C45133E  
    unsigned int n = argc;
00007FF78C451773  mov         eax,dword ptr [rbp+0000000000000100h]  
00007FF78C451779  mov         dword ptr [rbp+4],eax  
    n <<= 3;
00007FF78C45177C  mov         eax,dword ptr [rbp+4]  
00007FF78C45177F  shl         eax,3  
00007FF78C451782  mov         dword ptr [rbp+4],eax  
    n >>= 5;
00007FF78C451785  mov         eax,dword ptr [rbp+4]  
00007FF78C451788  shr         eax,5  
00007FF78C45178B  mov         dword ptr [rbp+4],eax  
    return n;
00007FF78C45178E  mov         eax,dword ptr [rbp+4]  
}
00007FF78C451791  lea         rsp,[rbp+00000000000000E8h]  
00007FF78C451798  pop         rdi  
00007FF78C451799  pop         rbp  
00007FF78C45179A  ret  

笔记--------------------------------------------end

在代码清单4-23中,对于左移运算而言,无符号数和有符号数的移位操作是一样的,都不需要考虑符号位。但右移运算则有变化,有符号数对应的指令为sar,可以保留符号位;无符号数不需要符号位,所以直接使用shr将最高位补0。

4.4 编译器使用的优化技巧

本节将讨论基于Pentium微处理器的优化技术。由于代码优化技术博大精深,已成为另外一门学科,其知识体系和本书讨论的软件逆向分析也不一样,所以本书只对此技术做一些有针对性的讲解。如果大家对这方面的技术有兴趣,可阅读笔者推荐的著作。

Modern Compiler Implementation in C,作者:Andrew W.Appel和Maia Ginsburg。此书从编译器实现的角度讲解了代码优化的理论知识和具体方法。

Code Optimization:Effective Memory Usage,作者:Kaspersky。此书从实际工作的角度介绍了如何检查定位目标的低效代码位置以及调整优化的方法。

所谓代码优化,是指为了达到某一种优化目的对程序代码进行变换。这样的变换有一个原则:变换前和变换后等价(不改变程序的运行结果)。

就优化目的而言,代码优化一般有如下4个方向。

执行速度优化。

内存存储空间优化。

磁盘存储空间优化。

编译时间优化(别诧异,大型软件编译一次需要好几个小时是常事)。

如今,计算机的存储空间都不小,因此常见的都是以执行速度为主的优化,这里仅以速度优化为主展开讨论。编译器的工作过程可以分为几个阶段:预处理→词法分析→语法分析→语义分析→中间代码生成→目标代码生成。其中,优化的机会一般存在于中间代码生成和目标代码生成这两个阶段。尤其是在中间代码生成阶段所做的优化不具备设备相关性,在不同的硬件环境中都能通用,因此编译器设计者广泛采用这类办法。

常见的与设备无关的优化方案有以下几种。

1. 常量折叠

示例如下:

x = 1 + 2;

1和2都是常量,结果可以预见,必然是3,因此没必要产生加法指令,直接生成x=3即可。

2. 常量传播

接上例,下一行代码为y=x+3,由于上例最后生成了x=3,其结果还是可以预见的,所以直接生成y=6即可。

3. 减少变量

示例如下:

x = i*2; y = j*2;
if(x > y) // 其后再也没有引用 x、y
{
    …
}

这时对x、y的比较等价于对i、j的比较,因此可以去掉x、y,直接生成if(i>j)。

4. 公共表达式

示例如下:

x = i * 2;  y = i * 2;

这时i * 2被称为公共表达式,可以归并为一个,即

x = i * 2;  
y = x;

5. 复写传播

类似于常量传播,但是目标变成了变量,示例如下:

x = a;
……
y = x+c;

如果省略号表示的代码中没有修改变量x,则可以直接用变量a代替x,即

y = a + c;

6. 剪去不可达分支(剪支优化)

示例如下:

if( 1>2) //条件永远为假
{
…
}

由于if作用域内的代码内容永远不会执行,因此整个if代码块没有存在的理由。

7. 顺序语句代替分支

请参考4.2.3节中条件表达式的优化。

8. 强度削弱

用加法或者移位代替乘法,用乘法或者移位代替除法,请参考4.1.1节中关于乘除法的优化。

9. 数学变换

以下表达式都是代数恒等式:

x = a + 0; x = a - 0; x = a * 1; x = a / 1;

因此,不会产生运算指令,直接输入x=a即可。下面这个表达式稍复杂一点:

x = a * y + b * y;//等价于x = (a+b) * y;

这样只须进行一次加法一次乘法。

10. 代码外提

这类优化一般存在于循环中,如下列代码所示:

while(x > y/2)
{
… //循环体内没有修改y值
}

以上代码不必在每次判定循环条件时都做除法,可以进行如下优化:


t = y / 2;
while(x > t)
…

在实际分析过程中,很可能会组合应用以上优化方案,读者应先建立优化方案的概念,以后遇到各类方案的组合时用心琢磨体会即可。

以上是中间代码生成阶段的各类优化方案,下面我们讨论目标代码生成阶段的优化方案。生成目标代码,也就是二进制代码,是和设备有关的。这里讨论的是基于32位Pentium微处理器的优化。

目标代码生成阶段有以下3种优化案。

流水线优化。

分支优化。

高速缓存(cache)优化。

流水线技术的由来[1]

从前,在英格兰北部的一个小镇上,一个名叫艾薇的人经营着一家炸鱼和油煎土豆片商店。在店里面,每位顾客需要排队才能点餐(比如油炸鳕鱼、油煎土豆片、豌豆糊和一杯茶),然后等着盘子装满后坐下来用餐。

艾薇店里的油煎土豆片是小镇中最好吃的,在每个集市日的中午,长长的队伍都会排出商店。所以,当隔壁的木器店关门时,艾薇就把它租下了。

没办法再另外增加服务台了,艾薇想出了一个聪明的办法。把柜台加长,艾薇、伯特、狄俄尼索斯和玛丽站成一排。顾客进来时,艾薇先给他们一个盛着鳕鱼的盘子,然后伯特加上油煎土豆片,狄俄尼索斯再盛上豌豆糊,最后玛丽倒茶并收钱。顾客们不停地走动,一位顾客拿到豌豆糊的同时,他后面的人已经拿到了油煎土豆片,再后面的一个人已经拿到了鳕鱼。一些村民不吃豌豆糊,但这没关系,他们也能从狄俄尼索斯那里得到笑脸。

这样一来队伍变短了,不久以后,艾薇买下了对面的商店,又增加了更多的餐位。这就是流水线的由来。将那些具有重复性的工作分割成几个串行部分,使工作在工人们中间移动,每个熟练工人只需要依次将自己负责的工作做好就可以了。虽然每位顾客等待服务的总时间没变,但是同时有4位顾客接受服务,这样在集市日的午餐时段里能够照顾的顾客数增加了3倍。

4.4.1 流水线优化规则

1. 指令的工作流程

(1)取指令

CPU从高速缓存或内存中取机器码。

(2)指令译码

分析指令的长度、功能和寻址方式。

(3)按寻址方式确定操作数

指令的操作数可以是寄存器、内存单元或者立即数(包含在完整指令中),如果操作数在内存单元里,这一步就要计算出有效地址(Effective Address,EA)。

(4)取操作数

按操作数存放的位置获得数值,并存放在临时寄存器中。

(5)执行指令

由控制单元(Control Unit,CU)或者计算单元(Arithmetic Logic Unit,ALU)执行指令规定的操作。

(6)存放计算结果

第1、2、5步是必须的,其他步骤视指令功能而定,比如控制类指令NOP没有操作数,第3、4、6步也就没有了。下面举例说明一个完整的指令流程。

比如执行指令:add eax,dword ptrds:[ebx+40DA44]

对应的机器代码是:038344DA4000

注意,Intel处理器是以小尾方式排列的,数据的高位对应内存的高地址,低位对应内存的低地址。

第1步,取指令,得到第1个十六进制字节0x03,并且eip加1。

第2步,译码得知这个指令是个加法,但是信息不够,先把上次取到的机器码放入处理器的指令队列缓存中。

第3步,取指令,得到第2个十六进制字节0x83。机器码放入处理器的指令队列缓存中,eip加1。

第4步,译码得知这个指令是寄存器相对寻址方式的加法,而且参与寻址的寄存器是ebx,存放目标是eax,其后还跟着4字节的偏移量,这样指令长度也确定了。机器码放入处理器的指令队列缓存中。

第5步[2],取地址,得到第3个十六进制字节:0x44,这是指令中包含的4字节地址偏移量信息的第1个字节,先放入内部暂存器;同时ebx的值保存到ALU,准备计算有效地址,eip加1。

第6步,取指令,得到第4个十六进制字节:0xDA,先放入内部暂存器,eip加1。

第7步,取指令,得到第5个十六进制字节:0x40,先放入内部暂存器,eip加1。

第8步,取指令,得到第6个十六进制字节:0x00,先放入内部暂存器,eip加1。

第9步,此时4字节偏移量全部到位,ALU中也保留了ebx的值,于是开始计算有效地址。

第10步,将eax的值传送到ALU;调度内存管理单元(MMU),得到内存单元中的值,将其传送到ALU并计算结果。

第11步,按指令要求,将计算结果存回eax中。

2. 什么是流水线

由于每条指令的工作流程都是由取指令、译码、执行、回写等步骤构成的,所以处理器厂商设计了多流水线结构,也就是说,在A流水线处理的过程中,B流水线可以提前对下一条指令做处理。我们先来看下面的例子。

004010AA mov eax,92492493h
004010AF add esp,8

执行上述代码,不具备流水线的处理器先读取004010AA处的二进制指令,然后开始译码等操作,这一系列工作的每一步都是需要时间的,比如取指令,内存管理单元开始工作,其他部件闲置等待,等拿到了指令才进行下一步工作。于是,为了提高效率,Intel公司从i486处理器开始引入了流水线的机制。

引入流水线机制以后,在第一条流水线执行mov eax, 92492493h的过程中,第二条流水线可以开始对004010AF进行读取和译码。这样并行处理提高了处理器的工作效率。

对于流水线的设计,不同的厂商有不同的设计理念。比如Intel的长流水线设计,把每条指令划分为很多阶段(执行步骤),使得每个步骤的工作内容都很简单,更容易设计电路,加快工作效率,因此Intel的处理器的主频较高。但是这样也有缺点,举例说明,如果执行的指令变成如下所示。

00401063 jmp [00401000h]
00401069 add  esp,8

那么,按长流水线设计的处理器使A流水线先取得00401063的指令,然后开始译码等步骤,这时候B流水线开始工作,按部就班去00401069处取指令,也开始译码等步骤。当A流水线译码完成,知道这是个jmp指令,意识到B流水线取指令错误了,就需要立刻停止B流水线的工作,定位新地址,从取指令开始重新工作。有些时候甚至需要回滚操作,清除B流水线执行错误带来的影响(流水线冲洗)。由于长流水线设计步骤较多,会导致发生错误后损失较大。

相对Intel的长流水线设计,ARM的设计理念是多流水线设计,即为每条指令划分较少的工作阶段,但是流水线数量较多。这样一来,并行程度更高了,而且由于流水线的工作步骤少,弥补错误会更及时,错误的影响也较少。当然这样的设计也有缺点,同样的指令,由于划分的工作阶段少,每个阶段做的事情多了,电路设计就会较为复杂,主频会受到限制,同时由于流水线数量较多,处理器对流水线的管理成本也增大了。

3. 注意事项

明白流水线的设计初衷后,我们接下来探讨影响流水线工作的一些禁忌,了解编译器在这方面的工作。

(1)指令相关性

对于顺序安排的两条指令,后一条指令的执行依赖前一条指令的硬件资源,这样的情况就是指令相关,如下面的代码所示。

add edx,esi
sar edx, 2

由于以上两条代码都需要访问并设置edx,因此只能在执行完add edx, esi后才能执行sar edx, 2。这样会导致寄存器的争用,影响并行效率,应尽量避免。

(2)地址相关性

对于顺序安排的两条指令,前一条指令需要访问并回写到某一地址上,而后一条指令也需要访问这一地址,这样的情况就是地址相关,如下面的代码所示。

add [00401234],esi
mov eax,[00401234]

由于第一条指令计算并回写到[00401234],而第二条指令也需要访问[00401234],形成了对内存地址的争用,因此只能在第一条指令操作完成后再去执行第二条,这同样影响了并行效率,应尽量避免。

由C++的02选项生成的代码会考虑流水线执行的工作方式,如以下代码所示。

0040101F push eax
00401020 push offset aN; "n/ 2 =%d"
00401025 call _printf
0040102A mov eax,92492493h
0040102F add esp,8
00401032 imul esi
00401034 add edx,esi
00401036 sar edx,2
00401039 mov eax,edx
0040103B shr eax,1Fh
0040103E add edx,eax

00401025处的call_printf是C语言的调用方式,由调用方恢复栈顶,指令是0040102F处的add esp,8,中间有mov eax,92492493h指令,这就是典型的流水线优化。因为mov eax,92492493h和00401032处的imul esi存在指令相关性,mov eax,92492493h需要设置eax,而imul指令也是把计算结果的低位设置到eax。由于存在寄存器争用的问题,这两条指令是无法并行的,因此,编译器在不影响程序结果的前提下,将mov eax,92492493h安排到add esp,8之上了,二者没有任何相关性,能同时执行。但是,流水线优化的前提条件是不影响计算结果,请看00401034处的add edx, esi和00401036处的sar edx,2这两条指令,都需要设置edx,所以无法并行,为了保证计算结果正确,不能改变执行顺序,编译器只能放弃流水线优化了。

4.4.2 分支优化规则

引入流水线工作机制后,为了配合流水线工作,处理器增加了一个分支目标缓冲器(Branch TargetBuffer)。在流水线工作模式下,如果遇到分支结构,就可以利用分支目标缓冲器预测并读取指令的目标地址。分支目标缓冲器在程序运行时将动态记录和调整转移指令的目标地址,可以记录多个地址,对其进行表格化管理。当发生转移时,如果分支目标缓冲器中有记录,下一条指令在取指令阶段就会将其作为目标地址。如果记录地址等于实际目标地址,则并行成功;如果记录地址不等于实际目标地址,则流水线被冲洗。同一个分支,多次预测失败,则更新记录的目标地址。因此,分支预测属于“经验主义”或“机会主义”,存在一定的误测。

基于上述原因,大家以后在编写多重循环时应该把大循环放到内层,这样可以增加分支预测的准确度,如下面的示例所示。

for (int i = 0; i < 10; i++){
  // 下面每次循环会预测成功9999次
  // 第1次没有预测,最后退出循环时预测失败1次
  // 这样的过程重复10次
  for (int j = 0; j < 10000; j++){ 
      a[i][j]++;
  }
}

for (int j = 0; j < 10000; j++){
  // 下面每次循环会预测成功9次
  // 第1次没有预测,最后退出循环时预测失败1次
  // 这样的过程重复10000次
  for (int i = 0; i < 10; i++){ 
      a[i][j]++;
  }
}

想想看,上述两种方式哪个划算?

编译器对分支的优化主要体现在减少分支结构上,使用顺序结构代替分支结构,相关知识请参考4.2.3节。关于使用查表法代替多分支结构的相关知识,请参考5.4节。

4.4.3 高速缓存优化规则

我们都知道,计算机内存的访问效率大大低于处理器,而且在程序的运行中,被访问的数据和指令相对集中,为此处理器准备了片上高速缓存(cache)存放需要经常访问的数据和代码。这些数据的内容和所在的虚拟地址(Virtual Address,VA)以表格的形式一一对应,在处理器访问内存数据时,先去高速缓存中看看这个虚拟地址有没有记录,如果有,则命中(cachehit),无须访问内存单元;如果没有找到(cachemiss),则转换虚拟地址的访问数据,并保存到高速缓存中。通常,高速缓存不仅会读取指令需要的数据,还会读取这个地址附近的数据。为了节省高速缓存的宝贵空间,VA值的二进制低位不会被保存,也就是说保存的数据是以2n字节为单位的,VA值具体会被保存多少位是由高速缓存设计的数据组织方式确定的。

由于现代操作系统的内存管理是分段加分页的管理模式,而页级管理是虚拟内存的基础,为了避免频繁访问三级页表转换地址,处理器准备了页表缓冲(Translation Lookaside Buffer,TLB)存放长期命中的页表数据。需要访问虚拟内存时,处理器会先去TLB查询是否命中,如果命中则直接查询TLB表中对应的物理地址。对于虚拟内存的管理,长期没有命中的分页会被交换到磁盘上,下次访问时会触发缺页中断,中断处理程序会把磁盘数据读回RAM。

基于以上设计,高速缓存优化有以下几个特点。

1. 数据对齐

高速缓存不会保存虚拟地址的二进制低位,对于Intel的32位处理器来说,如果访问的地址是4的倍数,则可以直接查询并提取;如果不是4的倍数,则需要访问多次。因此,VC++编译器在设置变量地址时会按照4字节边界对齐。2. 数据集中

将访问次数多的数据或代码尽量安排在一起,一方面是高速缓存在抓取命中数据时会抓取周围的其他数据;另一方面是虚拟内存分页的问题,如果数据分散,保留到多个分页中,就会导致过多的虚拟地址转换,甚至会导致缺页中断频繁发生,这些都会影响效率。

3. 减少体积

命中率高的代码段应减少体积,尽量放入高速缓存中,以提高效率。

如果读者对与32位处理器相关的知识意犹未尽,可以阅读《80x86汇编语言程序设计教程》(作者:杨季文)一书,该书对保护模式下的开发讲解得很细致。


[1]节选自百度百科。

[2]对于CISC指令集,指令长度是可变的,所以CPU工作时需要按字节取得指令,然后译码。如果译码后得知后面是地址,可以直接以4字节的方式取地址。但是,这样又会导致流水线工作处于等待状态,因为只有在译码后才能取得后面的内容。因此,不同的CPU设计结构解决这个问题的方式都可能不一样。

4.5 一次算法逆向之旅

前面我们了了解各种运算的基础知识,接下来进行一次简单的逆向之旅:通过分析程序的反汇编代码来了解程序的算法,巩固所学知识。

这里要分析的程序为简单的CrackMe程序,是用VC++编写的控制台程序。程序功能是验证输入密码,显示密码验证结果(密码为命令行输入方式),如图4-5所示。

图4-5 CrackMe程序的运行结果

为了尽量减少分析的工作量,程序中没有设置任何错误检查,只有密码加密与密码检查。输入正确的密码后,程序将会显示“密码正确”的字样。本次将使用IDA进行静态分析。使用IDA加载分析程序后,IDA会直接定位到main()函数的入口处,省去了查找main()函数的过程。如果使用OllyDbg进行动态分析,需要先查找并定位到main函数的入口处。分析程序为Release版,如代码清单4-24所示。

代码清单4-24 CrackMe程序分析片段1(Release版)

var_10= byteptr-10h    ;从var_10到var_3都是连续的1字节大小的局部变量
var_F= byte ptr-0Fh
var_E= byte ptr -0Eh
var_D= byte ptr -0Dh
var_C= byte ptr -0Ch
var_B= byte ptr -0Bh
var_A= byte ptr -0Ah
var_9= byte ptr -9
var_8= byte ptr -8
var_7= byte ptr -7
var_6= byte ptr -6
var_5= byte ptr -5
var_4= byte ptr -4
var_3= byte ptr -3
argc= dword ptr 4
argv= dword ptr 8
envp= dword ptr0Ch

笔记--------------------------------------------start

笔记--------------------------------------------end

代码清单4-24为CrackMe程序在main()函数中的参数以及局部变量定义。在IDA中,正偏移为参数,负偏移为局部变量,详细讲解见第7章。从标号var_3到var_10的变量都占byte(1字节)的连续局部变量,将其暂时看作数组,找到起始标号var_10的地址,按*键转换14个字节数据为数组。转换数组后将标号名称var_10重命名,按N键修改名称为“charNumber14”,如图4-6所示。

图4-6 数据转换数组

笔记--------------------------------------------start

跳转到Stack of _main窗口的数组显示

按*号弹出数组转换对话框

数组大小,一行中的元素个数,元素打印宽度都写14

点【OK】按钮

转换结果:

按N重命令名charNumber14

按【OK】确定重命名

笔记--------------------------------------------end

按Esc键返回反汇编视图窗口,在反汇编视图窗口的反汇编代码中,所有引用var_3~var_10的地方都被替换成了数组访问方式,如代码清单4-25所示。

代码清单4-25 CrackMe程序分析片段2(Release版)

charNumber14= byte ptr -10h      ;转换后的数组
; main()函数的三个参数:argc命令个数、argv命令行信息、envp环境变量信息
argc= dword ptr 4
argv= dword ptr 8
envp= dword ptr0Ch
sub    esp,10h                        ; 取参数argv数据放入ecx中
mov    ecx,[esp+10h+argv]
mov    al,1
mov    [esp+10h+charNumber14+6],al    ;将数组charNumber14第6项赋值为al,即1
mov    [esp+10h+charNumber14+7],al    ;将数组charNumber14第7项赋值为al,即1
                                      ;即edx中保存为argv[1]
mov    edx,[ecx+4]                    ;在ecx中保存的参数为argv,根据argv类型,这里为
                                      ;argv[1]操作
                                      ;即edx中保存为argv[1]
mov    [esp+10h+charNumber14+0Ah],al  ;将数组charNumber14第10项赋值为al,即1
mov    al, byte ptr[esp+10h+argc]     ;将al赋值为命令行参数个数argc
push    ebx                           ;保存环境
mov    bl,al                          ;将bl赋值为al,即命令行参数个数argc
push    esi                           ;保存环境
dec    bl                             ;对bl执行减等于1操作,等同argc减1
push    edi                           ;保存环境
or    [edx],bl                        ;在edx中保存为argv[1],这步操作为argv[1]
                                      ;[0]|=argc-1    ①
mov    edx,[ecx+4]                    ;在edx中保存为argv[1]
mov    [esp+1Ch+charNumber14], 77h    ;将数组charNumber14第0项赋值为0x77
mov    [esp+1Ch+charNumber14+1], 76h  ;将数组charNumber14第1项赋值为0x76
xor    [edx+1],bl                     ;在edx中保存为argv[1],这步操作为argv[1]
                                      ;[1]^=argc-1    ②
mov    dl,6                           ;修改dl为数值6
imul    dl                            ;对dl做有符号乘法,乘以al,al中保存的数据为命令
                                      ;行参数个数
                                      ;结果存入al中
mov    esi,[ecx+4]                    ;在esi中保存为argv[1]
sub    al,dl                          ;使用al减等于dl
mov    [esp+1Ch+charNumber14+2],0Cah  ;将数组charNumber14第2项赋值为0xCA
mov    [esp+1Ch+charNumber14+3],0F9h  ;将数组charNumber14第3项赋值为0xF9
imul    byte ptr[esi+2]               ;在esi中保存argv[1],此句指令为argv[1]
                                      ;[2]*al,al中保存的值
                                      ;为命令行参数个数乘以6后,再减去6。转换为al =
                                      ;argv[1][2] * (argc - 1) * 6
mov    [esi+2],al                     ;esi+2中的数据等于al,即argv[1][2]=argv[1]
                                      ;[2]*(argc-1)*6  ③
mov    esi,[ecx+4]                    ;在esi中保存为argv[1]
mov    [esp+1Ch+charNumber14+4],0A8h  ;将数组charNumber14第4项赋值为0xA8
mov    [esp+1Ch+charNumber14+5], 0Ch  ;将数组charNumber14第5项赋值为0x0C
movsx    eax, byte ptr[esi+2]         ;取argv[1][2]数据存到eax中
cdq                                   ;扩展高位到 edx
and    edx,3                          ;使用eax扩展后的高位edx与3进行位与运算
mov    [esp+1Ch+charNumber14+8],0Feh  ;将数组charNumber14第8项赋值为0xFE
add    eax,edx                        ;使用eax加扩展高位edx
mov    [esp+1Ch+charNumber14+9],0DBh  ;将数组charNumber14第9项赋值为0xDB
sar    eax,2                          ;将eax右移动2位,此数可套用除法公式,移动次数为
                                      ;2的幂,因此除数为4转换后变为:eax=argv[1][2]/4
mov    [esi+3],al                     ;将al赋值到esi+3,即argv[1][3]=argv[1][2]/4 ④
mov    eax,[ecx+4]                    ;使用eax保存argv[1]
mov    [esp+1Ch+charNumber14+0Bh],0E0h;将数组charNumber14第11项赋值为0xE0
mov    [esp+1Ch+charNumber14+0Ch],0FBh;将数组charNumber14第12项赋值为0xFB
mov    dl,[eax+4]                     ;在eax中保存argv[1],即:argv[1][4]数据存入dl
mov    [esp+1Ch+charNumber14+0Dh],0   ;将数组charNumber14第13项赋值为0x00
                                      ;到此所有的数组成员都被赋值

笔记--------------------------------------------start

; int __cdecl main(int argc, const char **argv, const char **envp)
_main proc near
charNumber14= byte ptr -10h				;转换后的数组
; main()函数的三个参数:argc命令个数、argv命令行信息、envp环境变量信息
argc= dword ptr  4
argv= dword ptr  8
envp= dword ptr  0Ch
sub     esp, 10h					
mov     ecx, [esp+10h+argv]									;取参数argv数据放入ecx中
mov     al, 1																;al=1
mov     [esp+10h+charNumber14+6], al			 	;将数组charNumber14第6项赋值为al,即1
mov     [esp+10h+charNumber14+7], al				;将数组charNumber14第7项赋值为al,即1
mov     edx, [ecx+4]	    ;在ecx中保存的参数为argv,根据argv类型,这里为argv[1]操作,
                          ;即edx中保存为argv[1]
mov     [esp+10h+charNumber14+0Ah], al			;将数组charNumber14第10项赋值为al,即1
mov     al, byte ptr [esp+10h+argc]					;将al赋值为命令行参数个数argc
push    ebx																	;保存环境
mov     bl, al															;将bl赋值为al,即命令行参数个数argc
push    esi																	;保存环境
dec     bl																	;对bl执行减等于1操作,等同argc减1
push    edi																	;保存环境
or      [edx], bl								;在edx中保存为argv[1],这步操作为argv[1][0] |= argc-1 ①												
mov     edx, [ecx+4]						;在edx中保存为argv[1]
mov     [esp+1Ch+charNumber14], 77h		;将数组charNumber14第0项赋值为0x77
mov     [esp+1Ch+charNumber14+1], 76h	;将数组charNumber14第1项赋值为0x76
xor     [edx+1], bl										;在edx中保存为argv[1],这步操作为argv[1][1]^=argc-1 ②
mov     dl, 6													;修改dl为数值6
imul    dl						;对dl做有符号乘法,乘以al,al中保存的数据为命令行参数个数,结果存入al中
mov     esi, [ecx+4]	;在esi中保存为argv[1]
sub     al, dl				;使用al减等于dl
mov     [esp+1Ch+charNumber14+2], 0CAh		;将数组charNumber14第2项赋值为0xCA
mov     [esp+1Ch+charNumber14+3], 0F3h		;将数组charNumber14第3项赋值为0xF3
imul    byte ptr [esi+2]				;在esi中保存argv[1],此句指令为argv[1][2]*al,al中保存的值
;为命令行参数个数乘以6后,再减去6。转换为al = argv[1][2] * (argc - 1) * 6
mov     [esi+2], al							;esi+2中的数据等于al,即argv[1][2]=argv[1][2]*(argc-1)*6 ③
mov     esi, [ecx+4]						;在esi中保存为argv[1]
mov     [esp+1Ch+charNumber14+4], 0A8h	;将数组charNumber14第4项赋值为0xA8
mov     [esp+1Ch+charNumber14+5], 0Ch		;将数组charNumber14第5项赋值为0x0C
movsx   eax, byte ptr [esi+2]						;取argv[1][2]数据存到eax中
cdq																			;扩展高位到 edx
and     edx, 3													;使用eax扩展后的高位edx与3进行位与运算
mov     [esp+1Ch+charNumber14+8], 0FEh	;将数组charNumber14第8项赋值为0xFE                          
add     eax, edx												;使用eax加扩展高位edx
mov     [esp+1Ch+charNumber14+9], 0DBh	;将数组charNumber14第9项赋值为0xDB
sar     eax, 2					 ;将eax右移动2位,此数可套用除法公式,移动次数为2的幂,
                         ;因此除数为4转换后变为:eax=argv[1][2]/4
mov     [esi+3], al			 ;将al赋值到esi+3,即argv[1][3]=argv[1][2]/4 ④
mov     eax, [ecx+4]			;使用eax保存argv[1]
mov     [esp+1Ch+charNumber14+0Bh], 0E0h	;将数组charNumber14第11项赋值为0xE0
mov     [esp+1Ch+charNumber14+0Ch], 0FBh	;将数组charNumber14第12项赋值为0xFB
mov     dl, [eax+4]												;在eax中保存argv[1],即:argv[1][4]数据存入dl
mov     [esp+1Ch+charNumber14+0Dh], 0			;将数组charNumber14第13项赋值为0x00
shl     dl, 3
mov     [eax+4], dl
mov     eax, [ecx+4]
mov     dl, [eax+5]
sar     dl, 2
mov     [eax+5], dl
mov     esi, [ecx+4]
mov     al, bl
mov     dl, [esi+6]
and     al, 7
and     dl, al
mov     [esi+6], dl
mov     esi, [ecx+4]
movsx   edx, byte ptr [esi+7]
and     edx, 80000001h
jns     short loc_4010BF

笔记--------------------------------------------end

在代码清单4-25中,对数组charNumber14中的每一项进行赋值,并对命令行参数进行一些计算,这就是在对我们输入的密码信息进行加密。charNumber14数组中保存的就是加密后的密码,分析后得到charNumber14中:“0x77、0x76、0xCA、0xF3、0xA8、0x0C、0x01、0x01、0xFE、0xDB、0x01、0xE0、0xFB、0x00”,共14个字节的数据。这个数组为密码比较数组,这里保存的数据可以为密码加密信息,因此可知密码长度,以及加密后的密文字符串信息。代码清单4-25为CrackMe程序部分的反汇编信息,继续向下分析程序,获取完整的加密过程,如代码清单4-26所示。

代码清单4-26 CrackMe程序分析片段3(Release版)

shl dl,3 ;在代码清单4-25中,dl被赋值为argv[1][4]
;将argv[1][4]左移动3次
mov [eax+4],dl ;将结果存入eax+4,即argv[1][4]=argv[1][4]<<3⑤
mov eax,[ecx+4] ;将eax赋值为argv[1]
mov dl,[eax+5] ;将argv[1][5]赋值到dl中
sar dl,2 ;将dl向右移2位
mov [eax+5],dl ; 结 果 赋 值 到 argv[1][5] 中 , 即 argv[1][5]=argv[1][5]>>2 ⑥
mov esi,[ecx+4] ;将esi赋值为argv[1]
mov al,bl ;在代码清单4-25中,bl为argc减1,且未被修改
;将al赋值为bl,即argc减1
mov dl,[esi+6] ;取argv[1][6]数据存入dl中
and al,7 ;将al与数字7做位与运算,即(argc-1)&7
and dl,al ;将al位与运算后的结果与dl做位与运算
;即argv[1][6]&((argc-1)&7)
mov [esi+6],dl ;将dl中的数据赋值到esi+6地址处
;即argv[1][6]=argv[1][6]&((argc-1)&7)⑦
mov esi,[ecx+4] ;将esi赋值为argv[1]
movsx edx, byte ptr[esi+7] ;取esi+7地址处1字节数据带符号扩展到
edx,即edx=argv[1][7]
and edx,80000001h ;将edx与0x80000001做位与操作,此操作只会保留下
edx的最高位
;与最低位此操作会影响标记位SF,当edx为负数时,会修改SF标记位
jns shortloc_4010BF ;SF标记位为0则跳转到标记loc_4010BF处,edx为
负数跳转
dec edx ;以下为edx为负数的处理代码
;edx减等于1,由于之前操作会使edx只保留最低位和最高位
;这里对edx减1操作,结果必然为0x80000000or edx,0FFFFFFFEh ;对edx与0xFFFFFFFE做位或运算,除最低位外全部置1
;此时的edx值只有1种可能:0xFFFFFFFE
inc edx ;对edx加1后,变为0xFFFFFFFF为-1
loc_4010BF: ;地址标号
mov [esi+7],dl ; 将 dl 赋 值 到 esi+7 处 , 这 里 的 操 作 为 对 2 取 模 操 作 , 即
argv[1]
;[7]%=2 ⑧
mov eax,[ecx+4] ;eax赋值为argv[1]
not bl ;在bl中保存argc减1,对其取反
mov [eax+8],bl ;将bl的值赋值到eax+8地址处,即argv[1][8]=~(argc-
1) ⑨
mov eax,[ecx+4] ;将eax赋值为argv[1]
mov dl,[eax] ;取argv[1][0]数据赋值到dl
mov bl,[eax+2] ;取argv[1][2]数据赋值到bl
lea esi,[eax+9] ;取argv[1][9]地址到esi,这里使用esi保存地址,为一
个指针操作
;esi的使用将指针记作:char* pArgv9
sub dl,bl ;使用dl减等于bl,即argv[1][0]-argv[1][2]
mov bl,[esi] ;对esi取内容到bl,即bl=*pArgv9
add bl,dl ;bl加等于dl,即*pArgv9+argv[1][0]-argv[1][2]
mov [esi],bl ;将bl复制到esi保存地址中:*pArgv9=*pArgv9+argv[1]
;[0]-argv[1][2] ⑩
mov eax,[ecx+4] ;将eax赋值为argv[1]
inc esi ;将esi中保存数据为argv[1][9]地址,对其加1表示地址向前偏
;移1字节
;这时esi保存数据为argv[1][10]的地址,即pArgv9+=1
movsx edi, byte ptr[eax+7] ;取eax+7地址处1字节数据带符号扩展到
edi,即edi=argv[1][7]
movsx eax, byte ptr[eax+6] ;取eax+6地址处1字节数据带符号扩展到
eax,即:eax=argv[1][6]
cdq ;将eax扩展高位到edx
idiv edi ;使用有符号除法,即argv[1][6]/argv[1][7]
inc esi ;对esi指向加1操作,之前esi中保存的数据为argv[1][10]的地址
;加1后,保存的数据为argv[1][11]的地址,即pArgv9 += 1;
mov [esi-1],al ;对esi-1取内容,寻址到argv[1][10],赋值为al,al中保存数
;据为除法
;商值此条语句即*(pArgv9-1)=argv[1][6]/argv[1][7]
mov eax,[ecx+4] ;将eax赋值为argv[1]
mov dl,[eax+3] ;dl被赋值为argv[1][3]
mov bl,[eax+1] ;bl被赋值为argv[1][1]
mov al,[esi] ;al 被 赋 值 为 *pArgv9, pArgv9 中 保 存 的 数 据 为 argv[1][11]的地址
sub dl,bl ;dl减去bl,即argv[1][3]-argv[1][1]
add al,dl ;al加dl,即*pArgv9+argv[1][3]-argv[1][1]
mov [esi],al ;将al值复制到esi保存地址中,即*pArgv9+=argv[1][3]-
;argv[1][1]
mov eax,[ecx+4] ;将eax赋值为argv[1]
movsx dx, byte ptr[eax+5] ;赋值dx为argv[1][5]
movsx ax, byte ptr[eax+4] ;赋值ax为argv[1][4]
imul edx,eax ;执行有符号乘法,eax乘以edx,即argv[1][4]*argv[1]
[5]
mov [esi],dx ;将乘法结果dx复制到esi中,由于dx占2字节,而pArgv9为
char,
;需要转换即*(short)pArgv9 = argv[1][4]*argv[1][5]
;指针pArgv9未被修改,仍然指向argv[1][11]的地址
mov ecx,[ecx+4] ;ecx赋值为argv[1]
;这里为strcmp函数调用,比较密码是否正确
;strcmp函数是编译选项,可能会被转换成内联方式,详解略
;函数的讲解见第6章
;到此,整个程序的加密过程就结束了
retn

笔记--------------------------------------------start

shl     dl, 3					;在代码清单4-25中,dl被赋值为argv[1][4], 将argv[1][4]左移动3次
mov     [eax+4], dl		;将结果存入eax+4,即argv[1][4]=argv[1][4]<<3 ⑤
mov     eax, [ecx+4]	;将eax赋值为argv[1]
mov     dl, [eax+5]		;将argv[1][5]赋值到dl中
sar     dl, 2					;将dl向右移2位
mov     [eax+5], dl		; 结 果 赋 值 到 argv[1][5] 中 , 即 argv[1][5]=argv[1][5]>>2 ⑥
mov     esi, [ecx+4]	;将esi赋值为argv[1]
mov     al, bl				;在代码清单4-25中,bl为argc减1,且未被修改,将al赋值为bl,即argc减1
mov     dl, [esi+6]		;取argv[1][6]数据存入dl中
and     al, 7					;将al与数字7做位与运算,即(argc-1) & 7
and     dl, al				;将al位与运算后的结果与dl做位与运算,即argv[1][6]&((argc-1)&7)
mov     [esi+6], dl		;将dl中的数据赋值到esi+6地址处,即argv[1][6]=argv[1][6]&((argc-1)&7) ⑦
mov     esi, [ecx+4]	;将esi赋值为argv[1]
movsx   edx, byte ptr [esi+7]	;取esi+7地址处1字节数据带符号扩展到edx,即edx=argv[1][7]
and     edx, 80000001h		;将edx与0x80000001做位与操作,此操作只会保留下edx的最高位
;与最低位此操作会影响标记位SF,当edx为负数时,会修改SF标记位
jns     short loc_4010BF	;SF标记位为0则跳转到标记loc_4010BF处,edx为负数跳转

dec     edx								;以下为edx为负数的处理代码
;edx减等于1,由于之前操作会使edx只保留最低位和最高位
;这里对edx减1操作,结果必然为0x80000000
or      edx, 0FFFFFFFEh		;对edx与0xFFFFFFFE做位或运算,除最低位外全部置1
;此时的edx值只有1种可能:0xFFFFFFFE
inc     edx								;对edx加1后,变为0xFFFFFFFF为-1

loc_4010BF:								;地址标号
mov     [esi+7], dl				;将dl赋值到esi+7处,这里的操作为对2取模操作,即argv[1][7]%=2 ⑧
mov     eax, [ecx+4]			;eax赋值为argv[1]
not     bl								;在bl中保存argc减1,对其取反
mov     [eax+8], bl				;将bl的值赋值到eax+8地址处,即argv[1][8]=~(argc-1) ⑨
mov     eax, [ecx+4]			;将eax赋值为argv[1]
mov     dl, [eax]					;取argv[1][0]数据赋值到dl
mov     bl, [eax+2]				;取argv[1][2]数据赋值到bl
lea     esi, [eax+9]			;取argv[1][9]地址到esi,这里使用esi保存地址,为一个指针操作
;esi的使用将指针记作:char* pArgv9
sub     dl, bl						;使用dl减等于bl,即argv[1][0]-argv[1][2]
mov     bl, [esi]					;对esi取内容到bl,即bl=*pArgv9
add     bl, dl						;bl加等于dl,即*pArgv9+argv[1][0]-argv[1][2]
mov     [esi], bl					;将bl复制到esi保存地址中:*pArgv9=*pArgv9+argv[1][0]-argv[1][2] ⑩
mov     eax, [ecx+4]			;将eax赋值为argv[1]
inc     esi								;将esi中保存数据为argv[1][9]地址,对其加1表示地址向前偏移1字节
;这时esi保存数据为argv[1][10]的地址,即pArgv9+=1
movsx   edi, byte ptr [eax+7]	;取eax+7地址处1字节数据带符号扩展到edi,即edi=argv[1][7]
movsx   eax, byte ptr [eax+6]	;取eax+6地址处1字节数据带符号扩展到eax,即:eax=argv[1][6]
cdq														;将eax扩展高位到edx
idiv    edi										;使用有符号除法,即argv[1][6]/argv[1][7]
inc     esi										;对esi指向加1操作,之前esi中保存的数据为argv[1][10]的地址
;加1后,保存的数据为argv[1][11]的地址,即pArgv9 += 1;
mov     [esi-1], al						;对esi-1取内容,寻址到argv[1][10],赋值为al,al中保存数据为除法
;商值此条语句即*(pArgv9-1)=argv[1][6]/argv[1][7] ⑪
mov     eax, [ecx+4]					;将eax赋值为argv[1]
mov     dl, [eax+3]						;dl被赋值为argv[1][3]
mov     bl, [eax+1]						;bl被赋值为argv[1][1]
mov     al, [esi]							;al被赋值为*pArgv9,pArgv9中保存的数据为argv[1][11]的地址
sub     dl, bl								;dl减去bl,即argv[1][3]-argv[1][1]
add     al, dl								;al加dl,即*pArgv9+argv[1][3]-argv[1][1]
mov     [esi], al							;将al值复制到esi保存地址中,即*pArgv9+=argv[1][3]-argv[1][1]⑫
mov     eax, [ecx+4]					;将eax赋值为argv[1]
movsx   dx, byte ptr [eax+5]	;赋值dx为argv[1][5]
movsx   ax, byte ptr [eax+4]	;赋值ax为argv[1][4]
imul    edx, eax							;执行有符号乘法,eax乘以edx,即argv[1][4]*argv[1][5]
mov     [esi], dx							;将乘法结果dx复制到esi中,由于dx占2字节,而pArgv9为char,
;需要转换即*(short)pArgv9 = argv[1][4]*argv[1][5]
;指针pArgv9未被修改,仍然指向argv[1][11]的地址⑬
mov     ecx, [ecx+4]					;ecx赋值为argv[1]
;这里为strcmp函数调用,比较密码是否正确
;strcmp函数是编译选项,可能会被转换成内联方式,详解略
;函数的讲解见第6章
;到此,整个程序的加密过程就结束了
retn

lea     esi, [esp+1Ch+charNumber14]

笔记--------------------------------------------end

通过代码清单4-26对命令行参数argv[1]的层层运算,最终得到一个加密后的字符串,与程序中的密文进行比较,如果转换结果一样,表示密码正确,反之密码错误。

在转换过程中,遇到了没有接触过的对2取模运算。2的取模运算相对特殊,由于取模就是求余,所有有符号数对2求余只有3种结果:“-1”“0”“1”。因此编译器进行了优化,对十六进制数0x80000001做位与运算,无论这个数字是多少,只会保留最高位与最低位。如果数字为奇数,则最低位必然为1,会被保留下来,同理,符号位也会被保留。

使用跳转指令JNS判断正负标记位SF。edx和十六进制数0x80000001做位与运算后,如果为负数,则结果为0x80000001,由于存放编码方式为补码,而这个数字并不是补码的-1,需要进行补码转换。如果是正数,则不存在转换问题,直接跳过负数处理部分即可。分析代码清单4-25和代码清单4-26的加密过程可以得出,加密运算步骤共有13步。对这13步加密步骤进行分析并还原后可以得出整个加密过程,如代码清单4-27所示。

代码清单4-27 还原成源码的加密过程

// main()函数定义
void main(int argc, char* argv[],char *envp){
//还原加密算法的1~13步
argv[1][0] |= argc-1;
argv[1][1] ^= argc-1;
argv[1][2] = argv[1][2] * (argc - 1) * 6;
argv[1][3] = argv[1][2] / 4;
argv[1][4] = argv[1][4] <<3;
argv[1][5] = argv[1][5] >>2;
argv[1][6] = argv[1][6] &((argc-1)&7);
argv[1][7] %= 2;
argv[1][8] = ~(argc-1);
// 这里有一步指针定义操作
char *pArgv9 = &argv[1][9];
*pArgv9 = *pArgv9 + argv[1][0] - argv[1][2];
// 执行步骤⑩后,对指针pArgv9进行了加1操作
pArgv9 += 1;
//在步骤
中,首先对指针pArgv9进行加1操作,再参与运算。这里为一个前
加操作
pArgv9 += 1;
*( pArgv9 - 1) = argv[1][6] / argv[1][7];
*pArgv9 += argv[1][3] - argv[1][1];
*(short)pArgv9 = argv[1][4] * argv[1][5];
// 密码比对部分使用strcmp进行字符串比较,实现过程略
}

笔记--------------------------------------------start

笔记--------------------------------------------end

代码清单4-27为CrackMe程序加密算法还原后的代码,使用了大量的位运算。由于这些运算不可逆,所以无法推算回正确的密码。这里给出程序的正确密码:www.51asm.com。读者可自己将CrackMe程序的反汇编代码翻译成对应的C++代码,使用此密码进行程序验证。

4.6 本章小结

本章首先讲解了表达式求值,这是分析过程中还原目标算法的基础。然后由此引申出很多对优化思路的讨论,这里涉及很多数学知识。在软件开发和逆向分析领域,入门时数学知识可能并不重要,只用做个熟练的技术人员就行。但是技术水平到了一定程度后,如果还想达到更高的境界,就需要复习数学知识了。所以,对于逆向分析技术人员来说,数学水平的高低,直接决定了“你的饭碗里面有没有肉”。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值