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

被折叠的 条评论
为什么被折叠?



