第4章
观察各种表达式的求值过程
4.1 算术运算和赋值
算术运算包括加法、减法、乘法和除法,也称为四则运算。计算机中的四则运算和数学上的四则运算有些不同,本章将揭秘计算机中的四则运算是如何在C++中完成的。
赋值运算类似于数学中的“等于”,是将一个内存空间中的数据传递到另一个内存空间。因为内存没有处理器那样的控制能力,所以各个内存单元之间是无法直接传递数据的,必须通过处理器访问并中转,以实现两个内存单元之间的数据传输。
在编译器中,通常算术运算与其他传递计算结果的代码组合后才能被视为一条有效的语句,如赋值运算或函数的参数传递。单独的算术运算虽然也可以编译通过,但是并不会生成代码。因为只进行计算而没有传递结果的运算不会对程序结果有任何影响,此时编译器会将其视为无效语句,等价于空语句,不会有任何编译处理,图4-1所示的代码便是一个很好的例子。
图4-1 无效语句块
4.1.1 各种算术运算的工作形式
1. 加法
加法运算对应的汇编指令为ADD。在执行加法运算时,不同的操作数对应的转换指令不同,编译器会根据优化方式选择最佳的匹配方案。在编译器中常用的优化方案有如下两种。
- 01选项:生成文件占用空间最少。
- 02选项:执行效率最快。
在VS中,Release编译选项组的默认选项为02选项——执行效率最快。在Debug编译选项组中,使用的是Od+ZI选项,此选项使编译器产生的一切代码都以便于调试为根本前提,甚至为了便于单步跟踪以及源码和目标代码块的对应阅读,不惜增加冗余代码。当然也不是完全放弃优化,在不影响调试的前提下,会尽可能地进行优化。
本章主要对比和分析Debug编译选项组与Release编译选项组对各种计算产生的目标代码方案。在使用Debug编译选项组时,产生的目标汇编代码和源码是一一对应的。以加法运算为例,分别使用不同类型的操作数查看在Debug编译选项组下编译后对应的汇编代码,如代码清单4-1所示。
代码清单4-1 使用不同类型的操作数查看加法运算在Debug编译选项组下编译后的汇编代码
// C++源码 #include <stdio.h> int main(int argc, char* argv[]) { 15+20; //无效语句,不参与编译 int n1 = 0; //变量定义 int n2 = 0; n1 = n1 + 1; //变量加常量的加法运算 n1 = 1 + 2; //两个常量相加的加法运算 n1 = n1 + n2; //两个变量相加的加法运算 printf("n1 = %d\n", n1); return 0; } //x86_vs对应汇编代码讲解 00401000 push ebp 00401001 mov ebp, esp 00401003 sub esp, 8 00401006 mov dword ptr [ebp-4], 0 ;n1 = 0 0040100D mov dword ptr [ebp-8], 0 ;n2 = 0 00401014 mov eax, [ebp-4] 00401017 add eax, 1 0040101A mov [ebp-4], eax ;n1 = n1 + 1 0040101D mov dword ptr [ebp-4], 3 ;n1 = 3 00401024 mov ecx, [ebp-4] 00401027 add ecx, [ebp-8] 0040102A mov [ebp-4], ecx ;n1 = n1 + n2 0040102D mov edx, [ebp-4] 00401030 push edx ;参数2 n1入栈 00401031 push offset aN1D ;参数1"n1 = %d\n" 00401036 call sub_401090 ;调用printf函数 0040103B add esp, 8 ;平衡栈 0040103E xor eax, eax 00401040 mov esp, ebp 00401042 pop ebp 00401043 retn //x86_gcc对应汇编代码讲解 00401510 push ebp 00401511 mov ebp, esp 00401513 and esp, 0FFFFFFF0h ;对齐栈 00401516 sub esp, 20h 00401519 call ___main ;调用初始化函数 0040151E mov dword ptr [esp+1Ch], 0 ;n1 = 0 00401526 mov dword ptr [esp+18h], 0 ;n2 = 0 0040152E add dword ptr [esp+1Ch], 1 ;n1 = n1 + 1 00401533 mov dword ptr [esp+1Ch], 3 ;n1 = 3 0040153B mov eax, [esp+18h] 0040153F add [esp+1Ch], eax ;n1 = n1 + n2 00401543 mov eax, [esp+1Ch] 00401547 mov [esp+4], eax ;参数2 n1入栈 0040154B mov dword ptr [esp], offset aN1D ;参数1"n1 = %d\n" 00401552 call _printf ;调用printf函数 00401557 mov eax, 0 0040155C leave 0040155D retn //x86_clang对应汇编代码讲解 00401000 push ebp 00401001 mov ebp, esp 00401003 push esi 00401004 sub esp, 20h 00401007 mov eax, [ebp+0Ch] 0040100A mov ecx, [ebp+8] 0040100D mov dword ptr [ebp-8], 0 00401014 mov dword ptr [ebp-0Ch], 0 ;n1 = 0 0040101B mov dword ptr [ebp-10h], 0 ;n2 = 0 00401022 mov edx, [ebp-0Ch] 00401025 add edx, 1 00401028 mov [ebp-0Ch], edx ;n1 = n1 + 1 0040102B mov dword ptr [ebp-0Ch], 3 ;n1 = 3 00401032 mov edx, [ebp-0Ch] 00401035 add edx, [ebp-10h] 00401038 mov [ebp-0Ch], edx ;n1 = n1 + n2 0040103B mov edx, [ebp-0Ch] 0040103E lea esi, aN1D 00401044 mov [esp], esi ;参数1"n1 = %d\n" 00401047 mov [esp+4], edx ;参数2 n1入栈 0040104B mov [ebp-14h], eax 0040104E mov [ebp-18h], ecx 00401051 call sub_401070 ;调用printf函数 00401056 xor ecx, ecx 00401058 mov [ebp-1Ch], eax 0040105B mov eax, ecx 0040105D add esp, 20h 00401060 pop esi 00401061 pop ebp 00401062 retn //x64_vs对应汇编代码讲解 0000000140001000 mov [rsp+10h], rdx 0000000140001005 mov [rsp+8], ecx 0000000140001009 sub rsp, 38h 000000014000100D mov dword ptr [rsp+20h], 0 ;n1 = 0 0000000140001015 mov dword ptr [rsp+24h], 0 ;n2 = 0 000000014000101D mov eax, [rsp+20h] 0000000140001021 inc eax 0000000140001023 mov [rsp+20h], eax ;n1 = n1 + 1 0000000140001027 mov dword ptr [rsp+20h], 3 ;n1 = 3 000000014000102F mov eax, [rsp+24h] 0000000140001033 mov ecx, [rsp+20h] 0000000140001037 add ecx, eax 0000000140001039 mov eax, ecx 000000014000103B mov [rsp+20h], eax ;n1 = n1 + n2 000000014000103F mov edx, [rsp+20h] ;参数2 n1 0000000140001043 lea rcx, aN1D ;参数1 "n1 = %d\n" 000000014000104A call sub_1400010C0 ;调用printf函数 000000014000104F xor eax, eax 0000000140001051 add rsp, 38h 0000000140001055 retn //x64_gcc对应汇编代码讲解 0000000000401550 push rbp 0000000000401551 mov rbp, rsp 0000000000401554 sub rsp, 30h 0000000000401558 mov [rbp+10h], ecx 000000000040155B mov [rbp+18h], rdx 000000000040155F call __main ;调用初始化函数 0000000000401564 mov dword ptr [rbp-4], 0 ;n1 = 0 000000000040156B mov dword ptr [rbp-8], 0 ;n2 = 0 0000000000401572 add dword ptr [rbp-4], 1 ;n1 = n1 + 1 0000000000401576 mov dword ptr [rbp-4], 3 ;n1 = 3 000000000040157D mov eax, [rbp-8] 0000000000401580 add [rbp-4], eax ;n1 = n1 + n2 0000000000401583 mov eax, [rbp-4] 0000000000401586 mov edx, eax ;参数2 n1 0000000000401588 lea rcx, Format ;参数1 "n1 = %d\n" 000000000040158F call printf ;调用printf函数 0000000000401594 mov eax, 0 0000000000401599 add rsp, 30h 000000000040159D pop rbp 000000000040159E retn //x64_clang对应汇编代码讲解 0000000140001000 sub rsp, 48h 0000000140001004 mov dword ptr [rsp+44h], 0 000000014000100C mov [rsp+38h], rdx 0000000140001011 mov [rsp+34h], ecx 0000000140001015 mov dword ptr [rsp+30h], 0 ;n1 = 0 000000014000101D mov dword ptr [rsp+2Ch], 0 ;n2 = 0 0000000140001025 mov ecx, [rsp+30h] 0000000140001029 add ecx, 1 000000014000102C mov [rsp+30h], ecx ;n1 = n1 + 1 0000000140001030 mov dword ptr [rsp+30h], 3 ;n1 = 3 0000000140001038 mov ecx, [rsp+30h] 000000014000103C add ecx, [rsp+2Ch] 0000000140001040 mov [rsp+30h], ecx ;n1 = n1 + n2 0000000140001044 mov edx, [rsp+30h] ;参数2 n1 0000000140001048 lea rcx, aN1D ;参数1 "n1 = %d\n" 000000014000104F call sub_140001070 ;调用printf函数 0000000140001054 xor edx, edx 0000000140001056 mov [rsp+28h], eax 000000014000105A mov eax, edx 000000014000105C add rsp, 48h 0000000140001060 retn
代码清单4-1展示了3种操作数的加法运算。在两个常量相加的情况下,编译器在编译期间会计算出结果,将这个结果作为立即数参与运算,减少了程序在运行期的计算。当有变量参与加法运算时,会先取出内存中的数据,放入通用寄存器中,再通过加法指令完成计算过程并得到结果,最后存回到变量占用的内存空间中。
开启02选项后,编译出来的汇编代码会有较大的变化。由于效率优先,编译器会将无用代码去除,并对可合并代码进行归并处理。例如在代码清单4-1中,“n1 = n1 + 1;”这样的代码将被删除,因为在其后又重新对变量n1进行了赋值操作,而在此之前没有对变量n1做任何访问,所以编译器判定此句代码是可被删除的。先来看图4-2中的代码,然后我们讨论此代码的其他优化方案。
图4-2 开启02选项的Release版加法运算
图4-2中唯一的加法运算是做参数平衡,而非源码中的加法运算,这是怎么回事呢?别着急,保持耐心,我先给大家介绍两个关于优化的概念。在编译过程中,编译器常常会采用“常量传播”和“常量折叠”的方案对代码中的变量与常量进行优化,下面详细讲解这两种方案。
(1)常量传播
将编译期间可计算出结果的变量转换成常量,这样就减少了变量的使用,请看下面的示例。
int main(int argc, char* argv[]){ int n = 1; printf("n= %d\n", n); return 0; }
变量n是一个在编译期间可以计算出结果的变量。因此,在程序中所有引用到n的地方都会直接使用常量1来代替,于是上述代码等价于下列代码。
int main(int argc, char* argv[]){ printf("n= %d\n", 1); return 0; }
(2)常量折叠
当出现多个常量进行计算,且编译器可以在编译期间计算出结果时,源码中所有的常量计算都将被计算结果代替,代码如下所示。
int main(int argc, char* argv[]){ int n= 1 + 5 - 3 * 6; printf("n= %d\n", n); return 0; }
此时不会生成计算指令,因为“1+5-3×6”的值是可以在编译过程中计算出来的,所以编译器首先会计算出“1+5-3×6”的结果:-12,然后用数值-12替换掉原表达式,其结果依然等价,代码如下所示。
int main(int argc, char* argv[]){ int n= -12; printf("n= %d\n", n); return 0; }
现在变量n为在编译期间可计算出结果的变量,那么接下来组合使用常量传播对其进行常量转换就是合理的,程序中将不会出现变量n,而是直接以常量-12代替,代码如下所示。
int main(int argc, char* argv[]){ printf("n= %d\n", -12); return 0; }
在代码清单4-1中,变量n1和n2的初始值是一个常量,VC++编译器在开启02优化方案后,会尝试使用常量替换变量。如果在程序的逻辑中,声明的变量没有被修改过,而且上下文中不存在针对此变量的取地址和间接访问操作,那么这个变量就等价于常量,编译器就认为可以删除这个变量,直接用常量代替。使用常量的好处是可以生成立即数寻址的目标代码,常量作为立即数成为指令的一部分,从而减少了内存的访问次数。关于内存访问次数的概念,读者可以参考一些计算机组成原理类书籍,了解主频和外频的概念,考察对比处理器的频率和总线的频率。前面的代码变换后如下所示(以下注释表示被优化剔除的代码)。
int n1 = 0; // 常量化以后,n1用0代替了 int n2 = 0; // 同上,这句也没有了 // 变量加常量的加法运算 n1 = n1 + 1; // n1 = 0 + 1; // 两常量相加的加法运算 n1 = 1 + 2; // n1 = 1 + 2; n1 = n1 + n2; // n1 = n1 + 0; printf("n1 = %d\n", n1);
因为变量转换成了常量,所以在编译期间可以直接计算出结果,而“n1=0+1;”这句赋值代码之后又对n1再次赋值,所以这是一句对程序没有任何影响的代码,被剔除掉。后面的“n1=1+2;”满足常量折叠的条件,所以直接计算出了加法结果3,“n1=1+2”由此转变为“n1=3”,此时满足常量传播条件,对n1的引用转变为对常量3的引用,printf的参数引用了n1,于是代码直接变为“printf("n1 = %d\n", 3);”,过程如下所示。
n1 = 0 + 1; // 优化过程: n1 = 0 + 1;被删除了 n1 = 1 + 2; // n1 = 3;常量折叠 n1 = n1 + n2; // n1= 3 + 0; printf("n1= %d\n", n1); //printf("n1 = %d\n", 3);
进一步研究优化方案,修改代码清单4-1中的源码,将变量的初始值0修改为命令行参数的个数arg c。由于arg c在编译期间无法确定,所以编译器无法在编译过程中提前计算出结果,程序中的变量就不会被常量替换掉。源码修改后如下所示。
#include <stdio.h> int main(int argc, char* argv[]){ int n1 = argc; // 修改处 int n2 = argc; // 修改处 n1 = n1 + 1; n1 = 1 + 2; n1 = n1 + n2; printf("n1 = %d\n", n1); return 0; }
将代码再次编译为Release版,如图4-3所示。
图4-3 另一种优化后的加法运算
与图4-2相比,图4-3中多了arg c的定义使用。arg c为IDA分析出的参数偏移,参数偏移的知识将在第6章讲解,这里我们只需要知道[esp+argc]是在获取参数即可。
图4-3中只定义了一个参数变量偏移,而在源码中定义的两个局部变量却不见了。为什么呢?我们还是要考察优化过程,代码如下。
int main(int argc, char* argv[]){ // int n1 = argc; 在后面的代码中被常量代替 // int n2 = argc; 虽然不能用常量代替,但是因为之后没有对n2进行修改,所以引用 n2等价于引用argc,n2则被删除,这种方法称为复写传播 // n1 = n1 + 1; 其后即刻重新对n1赋值,这句被删除了 // n1 = 1 + 2; 常量折叠,等价于n1 = 3; // n1 = n1 + n2; 常量传播和复写传播,等价于n1 = 3 + argc; // printf("n1 = %d\n", n1); // 其后n1没有被访问,可以用3 + argc代替 printf("n1 = %d\n", 3 + argc); return 0; }
编译器在编译期间通过对源码的分析,判定第二个变量n2可省略,因为它都被赋值为第一个参数arg c。在变量n1被赋值为3后,就做了两个变量的加法n1=n1+n2,这等同于变量n1=3+arg c。其后printf引用n1,也就等价于引用3+arg c,因此n1也可以被删除,于是有了图4-3中的代码。
2. 减法
减法运算对应汇编指令SUB,虽然计算机只会做加法,但是可以通过补码转换将减法转变为加法的形式实现。先来复习一下将减法转变为加法的过程。
设有二进制数Y,其反码记为Y(反),假定其二进制长度为8位,有:
Y + Y(反)= 11111111B
Y + Y(反)+ 1 = 0(进位丢失)
根据以上公式,推论之:
Y(反)+ 1 = 0 - Y<==>Y(反)+ 1 = -Y<==>Y(补)= -Y
以上就是负数的补码可以简化为取反加1的原因。例如,5-2就可对照公式进行如下转换。
5+(0-2)<==> 5+(2(反)+1)<==> 5+2(补)
有了这个特性,所有的减法运算都可以转换为加法运算。减法转换的示例如代码清单4-2所示。
代码清单4-2 减法运算示例(Debug版)
// C++源码 #include <stdio.h> int main(int argc, char* argv[]) { int n1 = argc; int n2 = 0; scanf("%d", &n2); n1 = n1 - 100; n1 = n1 + 5 - n2 ; printf("n1 = %d \r\n", n1); return 0; } //x86_vs对应汇编代码讲解 00401000 push ebp 00401001 mov ebp, esp 00401003 sub esp, 8 00401006 mov eax, [ebp+8] 00401009 mov [ebp-4], eax ;n1=argc 0040100C mov dword ptr [ebp-8], 0 ;n2=0 00401013 lea ecx, [ebp-8] 00401016 push ecx ;参数2 &n2 00401017 push offset unk_417160 ;参数1 "%d" 0040101C call sub_401110 ;调用scanf函数 00401021 add esp, 8 ;平衡栈 00401024 mov edx, [ebp-4] 00401027 sub edx, 64h 0040102A mov [ebp-4], edx ;n1=n1-100 0040102D mov eax, [ebp-4] 00401030 add eax, 5 ;eax=n1+5 00401033 sub eax, [ebp-8] 00401036 mov [ebp-4], eax ;n1=n1+5-n2 00401039 mov ecx, [ebp-4] 0040103C push ecx ;参数1 n1 0040103D push offset aN1D ;参数2 "n1 = %d \r\n" 00401042 call sub_4010D0 ;调用printf函数 00401047 add esp, 8 ;平衡栈 0040104A xor eax, eax 0040104C mov esp, ebp 0040104E pop ebp 0040104F retn //x86_gcc对应汇编代码讲解 00401510 push ebp 00401511 mov ebp, esp 00401513 and esp, 0FFFFFFF0h ;对齐栈 00401516 sub esp, 20h 00401519 call ___main ;调用初始化函数 0040151E mov eax, [ebp+8] 00401521 mov [esp+1Ch], eax ;n1=argc 00401525 mov dword ptr [esp+18h], 0 ;n2=0 0040152D lea eax, [esp+18h] 00401531 mov [esp+4], eax ;参数2 &n2 00401535 mov dword ptr [esp], offset aD; ;参数1 "%d" 0040153C call _scanf ;调用scanf函数 00401541 sub dword ptr [esp+1Ch], 64h ;n1=n1-100 00401546 mov eax, [esp+1Ch] 0040154A lea edx, [eax+5] ;edx=n1+5 0040154D mov eax, [esp+18h] 00401551 sub edx, eax ;edx=n1+5-n2 00401553 mov eax, edx 00401555 mov [esp+1Ch], eax ;n1=n1+5-n2 00401559 mov eax, [esp+1Ch] 0040155D mov [esp+4], eax ;参数2 n1 00401561 mov dword ptr [esp], offset aN1D ;参数1 "n1 = %d \r\n" 00401568 call _printf ;调用printf函数 0040156D mov eax, 0 00401572 leave 00401573 retn //x86_clang对应汇编代码讲解 00401000 push ebp 00401001 mov ebp, esp 00401003 sub esp, 24h 00401006 mov eax, [ebp+0Ch] ;eax=argv 00401009 mov ecx, [ebp+8] ;edx=argc 0040100C mov dword ptr [ebp-4], 0 00401013 mov edx, [ebp+8] ;edx=argc 00401016 mov [ebp-8], edx ;n1=argc 00401019 mov dword ptr [ebp-0Ch], 0 ;n2=0 00401020 lea edx, unk_417160 ;"%d" 00401026 mov [esp], edx ;参数1"%d" 00401029 lea edx, [ebp-0Ch] 0040102C mov [esp+4], edx ;参数2 &n2 00401030 mov [ebp-10h], eax 00401033 mov [ebp-14h], ecx 00401036 call sub_401080 ;调用scanf函数 0040103B mov ecx, [ebp-8] 0040103E sub ecx, 64h 00401041 mov [ebp-8], ecx ;n1=n1-100 00401044 mov ecx, [ebp-8] 00401047 add ecx, 5 ;ecx=n1+5 0040104A sub ecx, [ebp-0Ch] 0040104D mov [ebp-8], ecx ;n1=n1+5-n2 00401050 mov ecx, [ebp-8] ;ecx=n1 00401053 lea edx, aN1D ;"n1=%d \r\n" 00401059 mov [esp], edx ;参数1 "n1=%d \r\n" 0040105C mov [esp+4], ecx ;参数2 n1 00401060 mov [ebp-18h], eax 00401063 call sub_4010E0 ;调用printf函数 00401068 xor ecx, ecx 0040106A mov [ebp-1Ch], eax 0040106D mov eax, ecx 0040106F add esp, 24h 00401072 pop ebp 00401073 retn //x64_vs对应汇编代码讲解 0000000140001000 mov [rsp+10h], rdx 0000000140001005 mov [rsp+8], ecx 0000000140001009 sub rsp, 38h 000000014000100D mov eax, [rsp+40h] ;eax=argc 0000000140001011 mov [rsp+20h], eax ;n1=argc 0000000140001015 mov dword ptr [rsp+24h], 0 ;n2=0 000000014000101D lea rdx, [rsp+24h] ;参数2 &n2 0000000140001022 lea rcx, unk_1400182D0 ;参数1 "%d" 0000000140001029 call sub_140001180 ;调用printf函数 000000014000102E mov eax, [rsp+20h] 0000000140001032 sub eax, 64h 0000000140001035 mov [rsp+20h], eax ;n1=n1-100 0000000140001039 mov eax, [rsp+20h] 000000014000103D add eax, 5 ;eax=n1+5 0000000140001040 sub eax, [rsp+24h] 0000000140001044 mov [rsp+20h], eax ;n1=n1+5-n2 0000000140001048 mov edx, [rsp+20h] ;参数2 n1 000000014000104C lea rcx, aN1D ;参数1 "n1 = %d \r\n" 0000000140001053 call sub_140001120 ;调用printf函数 0000000140001058 xor eax, eax 000000014000105A add rsp, 38h 000000014000105E retn //x64_gcc对应汇编代码讲解 0000000000401550 push rbp 0000000000401551 mov rbp, rsp 0000000000401554 sub rsp, 30h 0000000000401558 mov [rbp+10h], ecx 000000000040155B mov [rbp+18h], rdx 000000000040155F call __main ;调用初始化函数 0000000000401564 mov eax, [rbp+10h] 0000000000401567 mov [rbp-4], eax ;n1=argc 000000000040156A mov dword ptr [rbp-8], 0 ;n2=0 0000000000401571 lea rax, [rbp-8] 0000000000401575 mov rdx, rax ;参数2 &n2 0000000000401578 lea rcx, Format ;参数1 "%d" 000000000040157F call scanf ;调用scanf函数 0000000000401584 sub dword ptr [rbp-4], 64h ;n1=n1-100 0000000000401588 mov eax, [rbp-4] 000000000040158B lea edx, [rax+5] ;edx=n1+5 000000000040158E mov eax, [rbp-8] 0000000000401591 sub edx, eax ;edx=n1+5-n2 0000000000401593 mov eax, edx 0000000000401595 mov [rbp-4], eax ;n1=n1+5-n2 0000000000401598 mov eax, [rbp-4] 000000000040159B mov edx, eax ;参数2 n1 000000000040159D lea rcx, aN1D ;参数1 "n1 = %d \r\n" 00000000004015A4 call printf ;调用printf函数 00000000004015A9 mov eax, 0 00000000004015AE add rsp, 30h 00000000004015B2 pop rbp 00000000004015B3 retn //x64_clang对应汇编代码讲解 0000000140001000 sub rsp, 48h 0000000140001004 mov dword ptr [rsp+44h], 0 000000014000100C mov [rsp+38h], rdx 0000000140001011 mov [rsp+34h], ecx 0000000140001015 mov ecx, [rsp+34h] 0000000140001019 mov [rsp+30h], ecx ;n1=argc 000000014000101D mov dword ptr [rsp+2Ch], 0 ;n2=0 0000000140001025 lea rcx, unk_1400182D0 ;参数1 "%d" 000000014000102C lea rdx, [rsp+2Ch] ;参数2 &n2 0000000140001031 call sub_140001080 ;调用scanf函数 0000000140001036 mov r8d, [rsp+30h] 000000014000103B sub r8d, 64h 000000014000103F mov [rsp+30h], r8d ;n1=n1-100 0000000140001044 mov r8d, [rsp+30h] 0000000140001049 add r8d, 5 ;r8d=n1+5 000000014000104D sub r8d, [rsp+2Ch] ;r8d=n1+5-n2 0000000140001052 mov [rsp+30h], r8d ;n1=n1+5-n2 0000000140001057 mov edx, [rsp+30h] ;参数2 n1 000000014000105B lea rcx, aN1D ;参数1 "n1 = %d \r\n" 0000000140001062 mov [rsp+28h], eax 0000000140001066 call sub_1400010F0 ;调用printf函数 000000014000106B xor edx, edx 000000014000106D mov [rsp+24h], eax 0000000140001071 mov eax, edx 0000000140001073 add rsp, 48h 0000000140001077 retn
代码清单4-2中的减法运算没有使用加负数的表现形式。那么,在实际分析中,根据加法操作数的情况,加数为负数时,执行的并非加法而是减法操作。
在02选项下,优化策略和加法一致,不再赘述。
3. 乘法
乘法运算对应的汇编指令分为有符号imul和无符号mul两种。由于乘法指令的执行周期较长,在编译过程中,编译器会先尝试将乘法转换成加法,或使用移位等周期较短的指令。当它们都不可转换时,才会使用乘法指令。具体示例如代码清单4-3所示。
代码清单4-3 各类型乘法转换Debug版
// C++源码 #include <stdio.h> int main(int argc, char* argv[]) { int n1 = argc; int n2 = argc; printf("n1 * 15 = %d\n", n1 * 15); //变量乘常量(常量值为非2的幂) printf("n1 * 16 = %d\n", n1 * 16); //变量乘常量(常量值为2的幂) printf("2 * 2 = %d\n", 2 * 2); //两常量相乘 printf("n2 * 4 + 5 = %d\n", n2 * 4 + 5); //混合运算 printf("n1 * n2 = %d\n", n1 * n2); //两变量相乘 return 0; } //x86_vs对应汇编代码讲解 00401000 push ebp 00401001 mov ebp, esp 00401003 sub esp, 8 00401006 mov eax, [ebp+8] 00401009 mov [ebp-4], eax ;n1 = argc 0040100C mov ecx, [ebp+8] 0040100F mov [ebp-8], ecx ;n2 = argc 00401012 imul edx, [ebp-4], 0Fh ;edx = n1 * 15 00401016 push edx ;参数2 n1 * 15 00401017 push offset aN115D ;参数1 "n1 * 15 = %d\n" 0040101C call sub_4010C0 ;调用printf函数 00401021 add esp, 8 ;平衡栈 00401024 mov eax, [ebp-4] 00401027 shl eax, 4 ;eax = n1 * 16 0040102A push eax ;参数2 n1 * 16 0040102B push offset aN116D ;参数1 "n1 * 16 = %d\n" 00401030 call sub_4010C0 ;调用printf函数 00401035 add esp, 8 ;平衡栈 00401038 push 4 ;参数2 4 0040103A push offset a22D ;参数1"2 * 2 = %d\n" 0040103F call sub_4010C0 ;调用printf函数 00401044 add esp, 8 ;平衡栈 00401047 mov ecx, [ebp-8] 0040104A lea edx, ds:5[ecx*4] ;edx = n2 * 4 + 5 00401051 push edx ;参数2 n2 * 4 + 5 00401052 push offset aN245D ;参数2 "n2 * 4 + 5 = %d\n" 00401057 call sub_4010C0 ;调用printf函数 0040105C add esp, 8 ;平衡栈 0040105F mov eax, [ebp-4] 00401062 imul eax, [ebp-8] ;eax = n1 * n2 00401066 push eax ;参数2 n1 * n2 00401067 push offset aN1N2D ;参数1 "n1 * n2 = %d\n" 0040106C call sub_4010C0 ;调用printf函数 00401071 add esp, 8 ;平衡栈 00401074 xor eax, eax 00401076 mov esp, ebp 00401078 pop ebp 00401079 retn //x86_gcc对应汇编代码讲解 00401510 push ebp 00401511 mov ebp, esp 00401513 and esp, 0FFFFFFF0h ;对齐栈 00401516 sub esp, 20h 00401519 call ___main ;调用初始化函数 0040151E mov eax, [ebp+8] 00401521 mov [esp+1Ch], eax ;n1 = argc 00401525 mov eax, [ebp+8] 00401528 mov [esp+18h], eax ;n2 = argc 0040152C mov edx, [esp+1Ch] 00401530 mov eax, edx 00401532 shl eax, 4 ;eax = n1 * 16 00401535 sub eax, edx ;eax = n1 * 16 - n1 00401537 mov [esp+4], eax ;参数2 n1 * 15 0040153B mov dword ptr [esp], offset aN115D ;参数1"n1 * 15 = %d\n" 00401542 call _printf ;调用printf函数 00401547 mov eax, [esp+1Ch] 0040154B shl eax, 4 0040154E mov [esp+4], eax ;参数2 n1 * 16 00401552 mov dword ptr [esp], offset aN116D ;参数1 "n1 * 16 = %d\n" 00401559 call _printf ;调用printf函数 0040155E mov dword ptr [esp+4], 4 ;参数2 4 00401566 mov dword ptr [esp], offset a22D ;参数1 "2 * 2 = %d\n" 0040156D call _printf ;调用printf函数 00401572 mov eax, [esp+18h] 00401576 shl eax, 2 ;eax = n2 * 4 00401579 add eax, 5 ;eax = n2 * 4 + 5 0040157C mov [esp+4], eax ;参数2 n2 * 4 + 5 00401580 mov dword ptr [esp], offset aN245D ;参数1 "n2 * 4 + 5 = %d\n" 00401587 call _printf ;调用printf函数 0040158C mov eax, [esp+1Ch] 00401590 imul eax, [esp+18h] ;eax = n1 * n2 00401595 mov [esp+4], eax ;参数2 n1 * n2 00401599 mov dword ptr [esp], offset aN1N2D ;参数1 "n1 * n2 = %d\n" 004015A0 call _printf ;调用printf函数 004015A5 mov eax, 0 004015AA leave 004015AB retn //x86_clang对应汇编代码讲解 00401000 push ebp 00401001 mov ebp, esp 00401003 push esi 00401004 sub esp, 30h 00401007 mov eax, [ebp+0Ch] ;eax = argv 0040100A mov ecx, [ebp+8] ;ecx = argc 0040100D mov dword ptr [ebp-8], 0 00401014 mov edx, [ebp+8] ;edx = argc 00401017 mov [ebp-0Ch], edx ;n1 = argc 0040101A mov edx, [ebp+8] 0040101D mov [ebp-10h], edx ;n2 = argc 00401020 imul edx, [ebp-0Ch], 0Fh ;edx = n1 * 15 00401024 lea esi, aN115D 0040102A mov [esp], esi ;参数1 "n1 * 15 = %d\n" 0040102D mov [esp+4], edx ;参数2 n1 * 15 00401031 mov [ebp-14h], eax 00401034 mov [ebp-18h], ecx 00401037 call sub_4010C0 ;调用printf函数 0040103C mov ecx, [ebp-0Ch] 0040103F shl ecx, 4 ;ecx = n1 * 16 00401042 lea edx, aN116D ;"n1 * 16 = %d\n" 00401048 mov [esp], edx ;参数1 "n1 * 16 = %d\n" 0040104B mov [esp+4], ecx ;参数2 n1 * 16 0040104F mov [ebp-1Ch], eax 00401052 call sub_4010C0 ;调用printf函数 00401057 lea ecx, a22D ;"2 * 2 = %d\n" 0040105D mov [esp], ecx ;参数1 "2 * 2 = %d\n" 00401060 mov dword ptr [esp+4], 4 ;参数2 4 00401068 mov [ebp-20h], eax 0040106B call sub_4010C0 ;调用printf函数 00401070 mov ecx, [ebp-10h] 00401073 shl ecx, 2 ;ecx = n2 * 4 00401076 add ecx, 5 ;ecx = n2 * 4 + 5 00401079 lea edx, aN245D ;"n2 * 4 + 5 = %d\n" 0040107F mov [esp], edx ;参数1 "n2 * 4 + 5 = %d\n" 00401082 mov [esp+4], ecx ;参数2 n2 * 4 + 5 00401086 mov [ebp-24h], eax 00401089 call sub_4010C0 ;调用printf函数 0040108E mov ecx, [ebp-0Ch] 00401091 imul ecx, [ebp-10h] ;ecx = n1 * n2 00401095 lea edx, aN1N2D ;"n1 * n2 = %d\n" 0040109B mov [esp], edx ;参数1 "n1 * n2 = %d\n" 0040109E mov [esp+4], ecx ;参数2 n1 * n2 004010A2 mov [ebp-28h], eax 004010A5 call sub_4010C0 ;调用printf函数 004010AA xor ecx, ecx 004010AC mov [ebp-2Ch], eax 004010AF mov eax, ecx 004010B1 add esp, 30h 004010B4 pop esi 004010B5 pop ebp 004010B6 retn //x64_vs对应汇编代码讲解 0000000140001000 mov [rsp+10h], rdx 0000000140001005 mov [rsp+8], ecx 0000000140001009 sub rsp, 38h 000000014000100D mov eax, [rsp+40h] 0000000140001011 mov [rsp+20h], eax ;n1 = argc 0000000140001015 mov eax, [rsp+40h] 0000000140001019 mov [rsp+24h], eax ;n2 = argc 000000014000101D imul eax, [rsp+20h], 0Fh ;eax = n1 * 15 0000000140001022 mov edx, eax ;参数2 n1 * 15 0000000140001024 lea rcx, aN115D ;参数1 "n1 * 15 = %d\n" 000000014000102B call sub_1400010F0 ;调用printf函数 0000000140001030 imul eax, [rsp+20h], 10h ;eax = n1 * 16 0000000140001035 mov edx, eax ;参数2 n1 * 16 0000000140001037 lea rcx, aN116D ;参数1 "n1 * 16 = %d\n" 000000014000103E call sub_1400010F0 ;调用printf函数 0000000140001043 mov edx, 4 ;参数1 4 0000000140001048 lea rcx, a22D ;参数2 "2 * 2 = %d\n" 000000014000104F call sub_1400010F0 ;调用printf函数 0000000140001054 mov eax, [rsp+24h] 0000000140001058 lea eax, ds:5[rax*4] ;eax = n2 * 4 + 5 000000014000105F mov edx, eax ;参数2 n2 * 4 + 5 0000000140001061 lea rcx, aN245D ;参数1 "n2 * 4 + 5 = %d\n" 0000000140001068 call sub_1400010F0 ;调用printf函数 000000014000106D mov eax, [rsp+20h] 0000000140001071 imul eax, [rsp+24h] ;eax = n1 * n2 0000000140001076 mov edx, eax ;参数2 n1 * n2 0000000140001078 lea rcx, aN1N2D ;参数1 "n1 * n2 = %d\n" 000000014000107F call sub_1400010F0 ;调用printf函数 0000000140001084 xor eax, eax 0000000140001086 add rsp, 38h 000000014000108A retn //x64_gcc对应汇编代码讲解 0000000000401550 push rbp 0000000000401551 mov rbp, rsp 0000000000401554 sub rsp, 30h 0000000000401558 mov [rbp+10h], ecx 000000000040155B mov [rbp+18h], rdx 000000000040155F call __main ;调用初始化函数 0000000000401564 mov eax, [rbp+10h] 0000000000401567 mov [rbp-4], eax ;n1 = argc 000000000040156A mov eax, [rbp+10h] 000000000040156D mov [rbp-8], eax ;n2 = argc 0000000000401570 mov edx, [rbp-4] 0000000000401573 mov eax, edx 0000000000401575 shl eax, 4 ;eax = n1 * 16 0000000000401578 sub eax, edx ;eax = n1 * 16 - n1 000000000040157A mov edx, eax ;参数2 n2 * 15 000000000040157C lea rcx, Format ;参数1 "n1 * 15 = %d\n" 0000000000401583 call printf ;调用printf函数 0000000000401588 mov eax, [rbp-4] 000000000040158B shl eax, 4 ;eax = n1 * 16 000000000040158E mov edx, eax ;参数2 n1 * 16 0000000000401590 lea rcx, aN116D ;参数1 "n1 * 16 = %d\n" 0000000000401597 call printf ;调用printf函数 000000000040159C mov edx, 4 ;参数2 4 00000000004015A1 lea rcx, a22D ;参数1 "2 * 2 = %d\n" 00000000004015A8 call printf ;调用printf函数 00000000004015AD mov eax, [rbp-8] 00000000004015B0 shl eax, 2 ;eax = n2 * 4 00000000004015B3 add eax, 5 ;eax = n2 * 4 + 5 00000000004015B6 mov edx, eax ;参数2 n2 * 4 + 5 00000000004015B8 lea rcx, aN245D ;参数1 "n2 * 4 + 5 = %d\n" 00000000004015BF call printf ;调用printf函数 00000000004015C4 mov eax, [rbp-4] 00000000004015C7 imul eax, [rbp-8] ;eax = n1 * n2 00000000004015CB mov edx, eax ;参数2 n1 * n2 00000000004015CD lea rcx, aN1N2D ;参数1 "n1 * n2 = %d\n" 00000000004015D4 call printf ;调用printf函数 00000000004015D9 mov eax, 0 00000000004015DE add rsp, 30h 00000000004015E2 pop rbp 00000000004015E3 retn //x64_clang对应汇编代码讲解 0000000140001000 sub rsp, 58h 0000000140001004 mov dword ptr [rsp+54h], 0 000000014000100C mov [rsp+48h], rdx 0000000140001011 mov [rsp+44h], ecx 0000000140001015 mov ecx, [rsp+44h] 0000000140001019 mov [rsp+40h], ecx ;n1 = argc 000000014000101D mov ecx, [rsp+44h] 0000000140001021 mov [rsp+3Ch], ecx ;n2 = argc 0000000140001025 imul edx, [rsp+40h], 0Fh ;参数2 n2 * 15 000000014000102A lea rcx, aN115D ;参数1 "n1 * 15 = %d\n" 0000000140001031 call sub_1400010B0 ;调用printf函数 0000000140001036 mov edx, [rsp+40h] 000000014000103A shl edx, 4 ;参数2 n1 * 16 000000014000103D lea rcx, aN116D ;参数1 "n1 * 16 = %d\n" 0000000140001044 mov [rsp+38h], eax 0000000140001048 call sub_1400010B0 ;调用printf函数 000000014000104D lea rcx, a22D ;参数1 "2 * 2 = %d\n" 0000000140001054 mov edx, 4 ;参数2 4 0000000140001059 mov [rsp+34h], eax 000000014000105D call sub_1400010B0 ;调用printf函数 0000000140001062 mov edx, [rsp+3Ch] 0000000140001066 shl edx, 2 ;edx = n2 * 4 0000000140001069 add edx, 5 ;参数2 n2 * 4 + 5 000000014000106C lea rcx, aN245D ;参数1 "n2 * 4 + 5 = %d\n" 0000000140001073 mov [rsp+30h], eax 0000000140001077 call sub_1400010B0 ;调用printf函数 000000014000107C mov edx, [rsp+40h] 0000000140001080 imul edx, [rsp+3Ch] ;参数2 n1 * n2 0000000140001085 lea rcx, aN1N2D ;参数1 "n1 * n2 = %d\n" 000000014000108C mov [rsp+2Ch], eax 0000000140001090 call sub_1400010B0 ;调用printf函数 0000000140001095 xor edx, edx 0000000140001097 mov [rsp+28h], eax 000000014000109B mov eax, edx 000000014000109D add rsp, 58h 00000001400010A1 retn
代码清单4-3中,有符号数乘以常量值,且这个常量非2的幂,会直接使用有符号乘法imul指令或者左移加减运算进行优化。当常量值为2的幂时,编译器会采用执行周期短的左移运算代替执行周期长的乘法指令。
由于任何十进制数都可以转换成二进制数表示,在二进制数中乘以2就等同于所有位依次向左移动1位。如十进制数3的二进制数为0011,3乘以2后等于6,6转换成二进制数为0110。
在上例中,乘法运算与加法运算的结合编译器采用LEA指令处理。在代码清单4-3中,LEA语句的目的并不是获取地址。
在代码清单4-3中,除了两个未知变量的相乘无法优化外,其他形式的乘法运算都可以进行优化处理。如果运算表达式中有一个常量值,则此时编译器会首先匹配各类优化方案,最后对不符合优化方案的运算进行调整。
通过示例演示,我们学习了有符号乘法的各种转换模式以及优化方法,无符号乘法的原理与之相同,读者可以举一反三,自己动手调试,总结经验。
4. 除法
(1)除法计算约定
除法运算对应的汇编指令分为有符号idiv和无符号div两种。除法指令的执行周期较长,效率也较低,所以编译器会想尽办法用其他运算指令代替除法指令。C++中的除法和数学中的除法不同,在C++中,除法运算不保留余数,有专门求取余数的运算(运算符为%),也称之为取模运算。对于整数除法,C++的规则是仅保留整数部分,小数部分完全舍弃。
我们先讨论一下除法计算的相关约定。以下讨论的除法是计算机整数除法,我们使用C语言中的a/b表示除法关系。在C语言中,两个无符号整数相除,结果依然是无符号的;两个有符号整数相除,结果依然是有符号的;如果有符号数和无符号数混除,其结果则是无符号的,有符号数的最高位(符号位)被作为数据位对待,然后作为无符号数参与计算。
对于除法而言,计算机面临着如何处理小数部分的问题。在数学意义上,7÷2=3.5,而对于计算机而言,整数除法的结果必须为整数。对于3.5这样的数值,计算机取整数部分的方式有如下几种。
a. 向下取整
根据整数值的取值范围,可以画出以下坐标轴。
所谓对x向下取整,就是取得在-∞方向最接近x的整数值,换言之,就是取得不大于x的最大整数。
例如,+3.5向下取整得到3,-3.5向下取整得到-4。
在数学描述中,表示对x向下取整。
在标准C语言的math.h中定义了floor函数,其作用就是向下取整,也有人称之为地板取整。
向下取整的除法,当除数为2的幂时,可以直接用带符号右移指令(sar)完成。但是,向下取整存在一个问题:
可能是因为这个问题,C语言的整数除法没有使用floor方式。
b. 向上取整
所谓对x向上取整,就是取得在+∞方向最接近x的整数值,换言之,就是取得不小于x的最小整数。
例如,+3.5向上取整得到4;-3.5向上取整得到-3。
在我们的数学描述中,表示对x向上取整。
在标准C语言的math.h中定义了ceil函数,其作用就是向上取整,也有人称之为天花板取整。
向上取整也存在一个问题:
c. 向零取整
所谓对x向零取整,就是取得往0方向最接近x的整数值,换言之,就是放弃小数部分。
举例说明,+3.5向零取整得到3,-3.5向零取整得到-3。
在我们的数学描述中,[x]表示对x向零取整。
向零取整的除法满足:
在C语言和其他多数高级语言中,对整数除法规定为向零取整。也有人称这种取整方法为截断除法(truncate)。
(2)除法相关的数学定义和性质
我们先来做道题,阅读下面的C语言代码并写出结果。
// 代码1 printf("8 % -3 = %d\r\n", 8 % -3); // 代码2 printf("-8 % -3 = %d\r\n", -8 % -3); // 代码3 printf("-8 % 3 = %d\r\n", -8 % 3);
如果你的答案是:
8 % -3 = 2 -8 % -3 = -2 -8 % 3 = -2
恭喜你,你答对了,可以跳过本节直接阅读后面的内容。
如果你得出的答案是错误的,而且不明白为什么错了,那么请和我一起回顾数学知识。
定义1:已知两个因数的积和其中一个因数,求另一个因数的运算,叫作除法。
定义2:在整数除法中,只有能整除和不能整除两种情况。当不能整除时,会产生余数。
设被除数为a,除数为b,商为q,余数为r,有如下重要性质。
性质1:|r|<|b|
性质2:a=bq+r
性质3:b=(a-r)÷q
性质4:q=(a-r)÷b
性质5:r=a-qb
C语言规定整数除法向零取整,那么将前面的“代码1”代入定义和运算性质得:
q=(a-r)/b=(9-r)/(-3)=-2
r=a-q×b=8-[(-2)×(-3)]=2
将前面的“代码2”代入定义和运算性质得:
q=(a-r)/b=(-8-r)/(-3)=2
r=a-q×b=-8-[(2×(-3)]=-2
将前面的“代码3”代入定义和运算性质得:
q=(a-r)/b=(-8-r)/3=-2
r=a-q×b=-8-(-2×3)=-2
现在是不是明白自己错在哪里了?
(3)相关定理和推导
对于下面的数学定义和推导,如果你已经掌握或者暂无兴趣,可跳过本节阅读后面的内容。后面内容中涉及的定义和推导将会以编号形式指出,感兴趣的读者可以回到本节考察相关推论和证明。如果对数学论证没有兴趣也没关系,重点掌握论证结束后粗体标注的分析要点即可。
定理1:若x为实数,有
进而可推导:
当x不为整数时:
定理2:若x为整数,则
定理3:由于上下取整相对于0点是对称的,所以
进而可推导:
定理4:若x为实数,n为整数,有
结合定理1,可推导:
因,若x<n,则;若,则,因,可得x<n。
推导6:有整数a,b和实数x,a≠b且b≠0,有
- 若,则;
- 若,则。
证明:设q为a÷b的商,r为余数(0≤|r|<|b|,且q、r均为整数),则
因,可得0≤x·|b|<1;
因0≤|r|<|b|,可得;
因|r|+1≤|b|,且|b|·x<1,结合上式可得0≤|r|+|b|·x<|r|+1≤|b|;
因0≤|r|+|b|·x<|b|,故。
当r、b同号时,;
当r>0,b<0,时,
-1<bx<0,r+1≤-b,r+bx<-b
得到0<r+bx<-b,。
由,得。
当r<0,b>0,时,
0≤bx<1,r+bx<0,-b<r+bx
得到-b<r+bx<0,。
,由此得。
由于,且已证,可得
同理可证:当时,则。
推导7:设有a、b两整数,当b>0时,有
当b<0时,有
证明1:设q为a÷b的商,r为余数,
根据定理4,有
因b>0,|r|<b,有
因此得。
同理可证,过程略。
证明2:设q为a÷b的商,r为余数,
根据定理4,有
因b<0,|r|<|b|,有
当r>0时,,
当r=0时,
当r<0时,
得到:,故:
代入得:
同理可证,过程略。
(4)编译器对除数为整型常量的除法的处理
如果除数是变量,则只能使用除法指令。如果除数为常量,就有了优化的余地。根据除数值的相关特性,编译器有对应的处理方式。
下面讨论编译器对除数为2的幂、非2的幂、负数等各类情况的处理方式。假定整型为4字节补码的形式。
a. 除数为无符号2的幂
除数为无符号2的幂优化如代码清单4-4所示。
代码清单4-4 除数为无符号2的幂优化(Release版)
// C++源码 #include <stdio.h> int main(int argc, char* argv[]) { printf("argc / 16 = %u", (unsigned)argc / 16); //变量除以常量,常量为无符号2的幂 return 0; } //x86_vs对应汇编代码讲解 00401010 mov eax, [esp+4] ;eax=argc 00401014 shr eax, 4 ;eax=argc>>4 00401017 push eax ;参数2 00401018 push offset aArgc16U ;参数1 "argc / 16 = %u" 0040101D call sub_401030 ;调用printf函数 00401022 add esp, 8 ;平衡栈 00401025 xor eax, eax 00401027 retn //x86_gcc对应汇编代码讲解 00402580 push ebp 00402581 mov ebp, esp 00402583 and esp, 0FFFFFFF0h ;对齐栈 00402586 sub esp, 10h 00402589 call ___main ;调用初始化函数 0040258E mov eax, [ebp+8] ;eax=argc 00402591 mov dword ptr [esp], offset aArgc16U ;参数1 "argc / 16 = %u" 00402598 shr eax, 4 ;eax=argc>>4 0040259B mov [esp+4], eax ;参数2 0040259F call _printf ;调用printf函数 004025A4 xor eax, eax 004025A6 leave ;释放环境变量空间,恢复环境 004025A7 retn ;return 0 //x86_clang对应汇编代码讲解 00401000 mov eax, [esp+4] ;eax=argc 00401004 shr eax, 4 ;eax=argc>>4 00401007 push eax ;参数2 00401008 push offset aArgc16U ;参数1 "argc / 16 = %u" 0040100D call sub_401020 ;调用printf函数 00401012 add esp, 8 ;平衡栈 00401015 xor eax, eax 00401017 retn //x64_vs对应汇编代码讲解 0000000140001010 sub rsp, 28h 0000000140001014 shr ecx, 4 ;edx=argc>>4 0000000140001017 mov edx, ecx ;参数2 0000000140001019 lea rcx, aArgc16U ;参数1 "argc / 16 = %u" 0000000140001020 call sub_140001030 ;调用printf函数 0000000140001025 xor eax, eax 0000000140001027 add rsp, 28h 000000014000102B retn //x64_gcc对应汇编代码讲解 0000000140001000 sub rsp, 28h 0000000140001004 mov edx, ecx ;edx=argc 0000000140001006 shr edx, 4 ;edx=argc>>4 参数2 0000000140001009 lea rcx, aArgc16U ;参数1 "argc / 16 = %u" 0000000140001010 call sub_140001020 ;调用printf函数 0000000140001015 xor eax, eax 0000000140001017 add rsp, 28h 000000014000101B retn //x64_clang对应汇编代码讲解 0000000140001000 sub rsp, 28h 0000000140001004 mov edx, ecx ;edx=argc 0000000140001006 shr edx, 4 ;edx=argc>>4 参数2 0000000140001009 lea rcx, aArgc16U ;参数2 "argc / 16 = %u" 0000000140001010 call sub_140001020 ;调用printf函数 0000000140001015 xor eax, eax 0000000140001017 add rsp, 28h 000000014000101B retn
如代码清单4-4所示,对于有符号除法,C语言的除法规则是向0取整,对无符号数做右移运算,编译后使用的指令为shr,相当于向下取整。
对于,当x≥0时,有:
举例说明一下,比如x为4,等价于4>>1,结果为2;但当x为5呢?处理为5>>1,结果还是2。因此本例中直接使用无符号右移完成除法运算,无需做任何取整调整。
b. 除数为无符号非2的幂(上)
除数为无符号非2的幂优化如代码清单4-5所示。
代码清单4-5 除数为无符号非2的幂优化(Release版)
// C++源码 #include <stdio.h> int main(int argc, char* argv[]) { printf("argc / 3 = %u", (unsigned)argc / 3); //变量除以常量,常量为无符号非2的幂 return 0; } //x86_vs对应汇编代码讲解 00401010 mov eax, 0AAAAAAABh ;eax=M 00401015 mul dword ptr [esp+4] ;无符号乘法,edx.eax=argc*M 00401019 shr edx, 1 ;无符号右移,edx=argc*M >>32>>1 0040101B push edx ;参数2 0040101C push offset aArgc3U ;参数1 "argc / 3 = %u" 00401021 call sub_401030 ;调用printf函数 00401026 add esp, 8 ;平衡栈 00401029 xor eax, eax 0040102B retn //x86_gcc对应汇编代码讲解 00402580 push ebp 00402581 mov ebp, esp 00402583 and esp, 0FFFFFFF0h ;对齐栈 00402586 sub esp, 10h 00402589 call ___main ;调用初始化函数 0040258E mov edx, 0AAAAAAABh ;edx=M 00402593 mov dword ptr [esp], offset aArgc3U ;参数1 "argc / 3 = %u" 0040259A mov eax, edx ;eax=M 0040259C mul dword ptr [ebp+8] ;无符号乘法,edx.eax=argc*M 0040259F shr edx, 1 ;无符号右移,edx=argc*M >>32>>1 004025A1 mov [esp+4], edx ;参数2 004025A5 call _printf ;调用printf函数 004025AA xor eax, eax 004025AC leave 004025AD retn //x86_clang对应汇编代码讲解 00401000 mov eax, 0AAAAAAABh ;eax=M 00401005 mul dword ptr [esp+4] ;无符号乘法,edx.eax=argc*M 00401009 shr edx, 1 ;无符号右移,edx=argc*M>>32>>1 0040100B push edx ;参数2 0040100C push offset aArgc3U ;参数1 "argc / 3 = %u" 00401011 call sub_401020 ;调用printf函数 00401016 add esp, 8 ;平衡栈 00401019 xor eax, eax 0040101B retn //x64_vs对应汇编代码讲解 0000000140001010 sub rsp, 28h 0000000140001014 mov eax, 0AAAAAAABh ;eax=M 0000000140001019 mul ecx ;无符号乘法,edx.eax=argc*M 000000014000101B lea rcx, aArgc3U ;参数1 "argc / 3 = %u" 0000000140001022 shr edx, 1 ;无符号右移,edx=argc*M>>32>>1,参数2 0000000140001024 call sub_140001030 ;调用printf函数 0000000140001029 xor eax, eax 000000014000102B add rsp, 28h 000000014000102F retn //x64_gcc对应汇编代码讲解 0000000000402BF0 push rbx 0000000000402BF1 sub rsp, 20h 0000000000402BF5 mov ebx, ecx ;ebx=argc 0000000000402BF7 call __main ;调用初始化函数 0000000000402BFC mov eax, ebx ;eax=argc 0000000000402BFE mov edx, 0AAAAAAABh ;edx=M 0000000000402C03 mul edx ;无符号乘法,edx.eax=argc*M 0000000000402C05 lea rcx, aArgc3U ;参数1 "argc / 3 = %u" 0000000000402C0C shr edx, 1 ;无符号右移,edx=argc*M>>32>>1,参数2 0000000000402C0E call printf ;调用printf函数 0000000000402C13 xor eax, eax 0000000000402C15 add rsp, 20h 0000000000402C19 pop rbx 0000000000402C1A retn //x64_clang对应汇编代码讲解 0000000140001000 sub rsp, 28h 0000000140001004 mov eax, ecx ;eax=argc 0000000140001006 mov edx, 0AAAAAAABh ;edx=M 000000014000100B imul rdx, rax ;有符号乘法,rdx=argc*M 000000014000100F shr rdx, 21h ;无符号右移,edx=argc*M>>33等价无符号乘法 0000000140001013 lea rcx, aArgc3U ;参数1 "argc / 3 = %u" 000000014000101A call sub_140001030 ;调用printf函数 000000014000101F xor eax, eax 0000000140001021 add rsp, 28h 0000000140001025 retn
如代码清单4-5所示,除法的情况处理起来很复杂。在代码起始处会出现一个超大数字:0x0AAAAAAABh。这个数值是从哪里来的呢?由于除法指令的周期比乘法指令周期长很多,所以编译器会使用周期较短的乘法和其他指令代替除法。我们先看看数学证明。
设x为被除数变量,c为某一常量,则有:
由于c为常量,且2n的取值由编译器选择,所以的值在编译期间可以计算出来。对于n的取值,在32位除法中通常都会大于等于32,在64位除法中通常都会大于等于64。这样就可以直接调整使用乘法结果的高位了。
如此一来,就是一个编译期间先行计算出来的常量值了,这个值常被称为Magic Number(魔数、幻数),我们先用M代替这个Magic常量,于是又有:
简单证明如下:
设,x、c皆为整数,c为正数,当x≥0时,有:
反推过程:
(eax×argc)>>33
等价于:
由此得:
解方程得:(注:此处的“约等于”在后面讨论除法优化原则处详细解释)。
于是,我们反推出优化前的高级代码如下:
argc/3
总结
当遇到数学优化公式,且使用mul无符号乘法时,基本可判定为除法优化后的代码,其除法原型为x除以常量c,其操作数是优化前的被除数x,接下来统计右移次数以确定公式中的n值,然后使用公式将魔数作为M值代入公式,求解常量除数c的近似值,四舍五入取整后,即可恢复除法原型。
c. 除数为无符号非2的幂(下)
另一种除数为无符号非2的幂优化方式如代码清单4-6所示。
代码清单4-6 另一种除数为无符号非2的幂优化(Release版)
// C++源码 #include <stdio.h> int main(int argc, char* argv[]) { printf("argc / 7 = %u", (unsigned)argc / 7); //变量除以常量,常量为无符号非2的幂 return 0; } //x86_vs对应汇编代码讲解 00401010 mov ecx, [esp+4] ;ecx=argc 00401014 mov eax, 24924925h ;eax=M 00401019 mul ecx ;无符号乘法,edx.eax=argc*M 0040101B sub ecx, edx ;ecx=argc-(argc*M>>32) 0040101D shr ecx, 1 ;无符号右移,ecx=(argc-(argc*M>>32))>>1 0040101F add ecx, edx ;ecx=((argc-(argc*M>>32))>>1)+(argc*M>>32) 00401021 shr ecx, 2 ;ecx=(((argc-(argc*M>>32))>>1)+(argc*M>>32))>>2 00401024 push ecx ;参数2 00401025 push offset aArgc7U ;参数1 "argc / 7 = %u" 0040102A call sub_401040 ;调用printf函数 0040102F add esp, 8 ;平衡栈 00401032 xor eax, eax 00401034 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 edx, 24924925h ;edx=M 00402597 mov dword ptr [esp], offset aArgc7U ;参数1 "argc / 7 = %u" 0040259E mov eax, ebx ;eax=argc 004025A0 mul edx ;无符号乘法,edx.eax=argc*M 004025A2 sub ebx, edx ;ebx=argc-(argc*M>>32) 004025A4 shr ebx, 1 ;无符号右移,ebx=(argc-(argc*M>>32))>>1 004025A6 add edx, ebx ;edx=((argc-(argc*M>>32))>>1)+(argc*M>>32) 004025A8 shr edx, 2 ;edx=(((argc-(argc*M>>32))>>1)+(argc*M>>32))>>2 004025AB mov [esp+4], edx ;参数2 004025AF call _printf ;调用printf函数 004025B4 xor eax, eax 004025B6 mov ebx, [ebp-4] 004025B9 leave 004025BA retn //x86_clang对应汇编代码讲解 00401000 mov ecx, [esp+4] ;ecx=argc 00401004 mov edx, 24924925h ;edx=M 00401009 mov eax, ecx ;eax=argc 0040100B mul edx ;无符号乘法,edx.eax=argc*M 0040100D sub ecx, edx; ;ecx=argc-(argc*M>>32) 0040100F shr ecx, 1 ;无符号右移,ecx=(argc-(argc*M>>32))>>1 00401011 add ecx, edx ;ecx=((argc-(argc*M>>32))>>1)+(argc*M>>32) 00401013 shr ecx, 2 ;ecx=(((argc-(argc*M>>32))>>1)+(argc*M>>32))>>2 00401016 push ecx ;参数2 00401017 push offset aArgc7U ;参数1 "argc / 7 = %u" 0040101C call sub_401030 ;调用printf函数 00401021 add esp, 8 ;平衡栈 00401024 xor eax, eax 00401026 retn //x64_vs对应汇编代码讲解 0000000140001010 sub rsp, 28h 0000000140001014 mov eax, 24924925h ;eax=M 0000000140001019 mul ecx ;无符号乘法,edx.eax=argc*M 000000014000101B sub ecx, edx ;ecx=argc-(argc*M>>32) 000000014000101D shr ecx, 1 ;无符号右移,ecx=(argc-(argc*M>>32))>>1 000000014000101F add edx, ecx ;edx=((argc-(argc*M>>32))>>1)+(argc*M>>32) 0000000140001021 lea rcx, aArgc7U ;"argc / 7 = %u" 0000000140001028 shr edx, 2 ;edx=(((argc-(argc*M>>32))>>1)+(argc*M>>32))>>2参数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 mov eax, ebx ;eax=argc 0000000000402BFE mov edx, 24924925h;edx=M 0000000000402C03 mul edx ;无符号乘法,edx.eax=argc*M 0000000000402C05 lea rcx, aArgc7U ;参数1 "argc / 7 = %u" 0000000000402C0C sub ebx, edx ;ebx=argc-(argc*M>>32) 0000000000402C0E shr ebx, 1 ;无符号右移,ebx=(argc-(argc*M>>32))>>1 0000000000402C10 add edx, ebx ;edx=((argc-(argc*M>>32))>>1)+(argc*M>>32) 0000000000402C12 shr edx, 2 ;edx=(((argc-(argc*M>>32))>>1)+(argc*M>>32))>>2参数2 0000000000402C15 call printf ;调用printf函数 0000000000402C1A xor eax, eax 0000000000402C1C add rsp, 20h 0000000000402C20 pop rbx 0000000000402C21 retn //x64_clang对应汇编代码讲解 0000000140001000 sub rsp, 28h 0000000140001004 mov eax, ecx ;eax=argc 0000000140001006 imul rdx, rax, 24924925h ;rdx=argc*M 000000014000100D shr rdx, 20h ;rdx= argc*M>>32 0000000140001011 sub ecx, edx ;ecx=argc-(argc*M>>32) 0000000140001013 shr ecx, 1 ;无符号右移,ecx=(argc-(argc*M>>32))>>1 0000000140001015 add edx, ecx ;edx=((argc-(argc*M>>32))>>1)+(argc*M>>32) 0000000140001017 shr edx, 2 ;edx=(((argc-(argc*M>>32))>>1)+(argc*M>>32))>>2参数2 000000014000101A lea rcx, aArgc7U ;参数1 "argc / 7 = %u" 0000000140001021 call sub_140001030 ;调用printf函数 0000000140001026 xor eax, eax 0000000140001028 add rsp, 28h 000000014000102C retn
遇到类似上述的代码不要手足无措,我们可以一步步地论证。先看看这段代码都做了什么:00401014处疑似,看后面的代码,不符合前面例子得出的结论,所以不能使用前面推导的公式。接着一边看后面的指令,一边先写出等价于上述步骤的数学表达式。
设M为常量24924925h,以上代码等价于:
0040101B处的sub ecx,edx直接减去乘法结果的高32位,数学表达式等价于;
其后shr ecx,1相当于除以2:;
其后add ecx,edx再次加上乘法结果的高32位:;
其后shr ecx,2等价于把加法的结果再次除以4:;
最后直接使用ecx,乘法结果低32位ecx弃而不用。
先简化表达式:
至此,我们可以看出除法优化的原型,232+M是带进位值的魔数。编译器作者在实现除法优化的过程中,通过计算得到的魔数超过了4字节整数范围,为了避免大数运算的开销,对此做了数学转换,于是得到最开始的表达式,规避了所有的大数计算问题。
若有,n=3,x为ecx,则有:
数数看一共移动了几次。ecx一共右移了3位,因为是直接与edx运算并作为结果,所以还要加上乘法的低32位,共计35位,最终n的取值为3。已知M值为常量24924925h,根据上述推导可得:
解方程求c:
于是,我们反推出优化前的高级代码为:
argc/7
在计算魔数后,如果值超出4字节整数的表达范围,编译器会对其进行调整。如上例中的argc/7,在计算魔数时,编译器选择,但是其结果超出了4字节整数的表达范围,所以编译器调整魔数的取值为,导致整个除法的推导也随之产生变化。
综上所证,可推导出除法等价优化公式如下:
设,
总结
当遇到数学优化公式时,基本可判定其是除法优化后的代码,除法原型为x除以常量c,mul可表明是无符号计算,操作数是优化前的被除数x,接下来统计右移的总次数以确定公式中的n值,然后使用公式将魔数作为M值代入公式求解常量除数c,即可恢复除法原型。
d. 除数为有符号2的幂
除数为有符号2的幂优化如代码清单4-7所示。
代码清单4-7 除数为有符号2的幂优化(Release版)
// C++源码 #include <stdio.h> int main(int argc, char* argv[]) { printf("argc / 8 = %d", argc / 8); //变量除以常量,常量为2的幂 return 0; } //x86_vs对应汇编代码讲解 00401010 mov eax, [esp+4] ;eax = argc 00401014 cdq ;eax符号位扩展,正数edx=0,负数edx=0xffffffff 00401015 and edx, 7 ;负数edx=7,正数edx=0 00401018 add eax, edx ;if(argc<0),eax=argc+7 ;if(argc>=0),eax=argc+0 0040101A sar eax, 3 ;if(argc<0),eax=(argc+7)>>3 ;if(argc >= 0),eax=argc>>3 0040101D push eax ;参数2 0040101E push offset aArgc8D ;参数1 "argc / 8 = %d" 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 dword ptr [esp], offset aArgc8D ;参数1 "argc / 8 = %d" 00402599 test ebx, ebx ;做与运算影响标志位 0040259B lea eax, [ebx+7] ;eax = argc + 7 0040259E cmovns eax, ebx ;条件执行,不为负数执行 ;if(argc >= 0),eax=argc 004025A1 sar eax, 3 ;if(argc < 0),eax=(argc+7)>>3 ;if(argc >= 0),eax=argc>>3 004025A4 mov [esp+4], eax ;参数2 004025A8 call _printf ;调用printf函数 004025AD xor eax, eax 004025AF mov ebx, [ebp-4] 004025B2 leave 004025B3 retn //x86_clang对应汇编代码讲解 00401000 mov eax, [esp+4] ;eax = argc 00401004 mov ecx, eax ;ecx = argc 00401006 sar ecx, 1Fh ;ecx算术右移31位,正数ecx=0,负数ecx=0xffffffff 00401009 shr ecx, 1Dh ;ecx逻辑右移29位,正数ecx=0,负数ecx=7 0040100C add ecx, eax ;if(argc < 0),ecx=argc+7 ;if(argc >= 0),ecx=argc+0 0040100E sar ecx, 3 ;if(argc < 0),ecx=(argc+7)>>3 ;if(argc >= 0),ecx=argc>>3 00401011 push ecx ;参数2 00401012 push offset aArgc8D ;参数1 "argc / 8 = %d" 00401017 call sub_401030 ;调用printf函数 0040101C add esp, 8 ;平衡栈 0040101F xor eax, eax 00401021 retn //x64_vs对应汇编代码讲解 0000000140001010 sub rsp, 28h 0000000140001014 mov eax, ecx ;eax = argc 0000000140001016 lea rcx, aArgc8D;参数1 "argc / 8 = %d" 000000014000101D cdq ;eax符号位扩展,正数edx=0,负数edx=0xffffffff 000000014000101E and edx, 7 ;负数edx=7,正数edx=0 0000000140001021 add edx, eax ;if(argc < 0),edx=argc+7 ;if(argc >= 0),edx=argc+0 0000000140001023 sar edx, 3 ;if(argc < 0),edx=(argc+7)>>3 ;if(argc >= 0),edx=argc>>3 参数2 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 ;ebx = argc 0000000000402BF7 call __main ;调用初始化函数 0000000000402BFC lea edx, [rbx+7] ;edx = argc + 7 0000000000402BFF test ebx, ebx ;做与运算影响标志位 0000000000402C01 cmovns edx, ebx ;条件执行,不为负数执行 ;if(argc >= 0),edx=argc 0000000000402C04 lea rcx, aArgc8D ;参数1 "argc / 8 = %d" 0000000000402C0B sar edx, 3 ;if(argc < 0),edx=(argc+7)>>3 ;if(argc >= 0),edx=argc>>3 参数2 0000000000402C0E call printf ;调用printf函数 0000000000402C13 xor eax, eax 0000000000402C15 add rsp, 20h 0000000000402C19 pop rbx 0000000000402C1A retn //x64_clang对应汇编代码讲解 0000000140001000 sub rsp, 28h 0000000140001004 mov eax, ecx ;eax = argc 0000000140001006 sar eax, 1Fh ;eax算术右移31位,正数eax=0,负数eax=0xffffffff 0000000140001009 shr eax, 1Dh ;eax逻辑右移29位,正数eax=0,负数eax=7 000000014000100C lea edx, [rax+rcx] ;if(argc < 0),edx=argc+7 ;if(argc >= 0),edx=argc+0 000000014000100F sar edx, 3 ;if(argc < 0),edx=(argc+7)>>3 ;if(argc >= 0),edx=argc>>3 参数2 0000000140001012 lea rcx, aArgc8D ;参数1 "argc / 8 = %d" 0000000140001019 call sub_140001030 ;调用printf函数 000000014000101E xor eax, eax 0000000140001020 add rsp, 28h 0000000140001024 retn
C语言的除法规则是向0取整,对有符号数做右移运算,编译后使用的指令为sar,相当于向下取整。
对于,当x≥0时,有:
当x<0时,有:
根据推导7可得:
例如:。
总结
当遇到数学优化公式:如果x≥0,则,如果x<0,则时,基本可判定是除法优化后的代码,根据n(右移次数)即可恢复除法原型。
e. 除数为有符号非2的幂(上)
除数为有符号非2的幂优化如代码清单4-8所示。
代码清单4-8 除数为有符号非2的幂优化(Release版)
// C++源码 #include <stdio.h> int main(int argc, char* argv[]) { printf("argc / 9 = %d", argc / 9); //变量除以常量,常量为非2的幂 return 0; } //x86_vs对应汇编代码讲解 00401010 mov eax, 38E38E39h ;eax = M 00401015 imul dword ptr [esp+4] ;edx.eax=argc*M 00401019 sar edx, 1 ;edx=(argc*M>>32)>>1 0040101B mov eax, edx 0040101D shr eax, 1Fh ;eax=eax>>31取符号位 00401020 add eax, edx ;if(edx < 0),eax=((argc*M>>32)>>1)+1 ;if(edx >=0),eax=(argc*M>>32)>>1 00401022 push eax ;参数2 00401023 push offset aArgc9D ;参数1 "argc / 9 = %d" 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 push ebx 00402584 and esp, 0FFFFFFF0h ;对齐栈 00402587 sub esp, 10h 0040258A mov ebx, [ebp+8] ;ebx = argc 0040258D call ___main ;调用初始化函数 00402592 mov edx, 38E38E39h ;edx = M 00402597 mov dword ptr [esp], offset aArgc9D ;参数1 "argc / 9 = %d" 0040259E mov eax, ebx ;eax = argc 004025A0 sar ebx, 1Fh ;argc是正数ebx=0 负数ebx=-1 004025A3 imul edx ;edx.eax=argc*M 004025A5 sar edx, 1 ;edx=(argc*M>>32)>>1 004025A7 sub edx, ebx ;if(edx < 0),edx=((argc*M>>32)>>1)+1 ;if(edx >=0),edx=(argc*M>>32)>>1 004025A9 mov [esp+4], edx ;参数2 004025AD call _printf ;调用printf函数 004025B2 xor eax, eax 004025B4 mov ebx, [ebp-4] 004025B7 leave 004025B8 retn //x86_clang对应汇编代码讲解 00401000 mov eax, 38E38E39h ;eax = M 00401005 imul dword ptr [esp+4] ;edx.eax=argc*M 00401009 mov eax, edx 0040100B sar edx, 1 ;edx=(argc*M>>32)>>1 0040100D shr eax, 1Fh ;eax=eax>>31取符号位 00401010 add edx, eax ;if(edx < 0),edx=((argc*M>>32)>>1)+1 ;if(edx >=0),edx=(argc*M>>32)>>1 00401012 push edx ;参数2 00401013 push offset aArgc9D ;参数1 "argc / 9 = %d" 00401018 call sub_401030 ;调用printf函数 0040101D add esp, 8 ;平衡栈 00401020 xor eax, eax 00401022 retn //x64_vs对应汇编代码讲解 0000000140001010 sub rsp, 28h 0000000140001014 mov eax, 38E38E39h;eax = M 0000000140001019 imul ecx ;edx.eax=argc*M 000000014000101B lea rcx, aArgc9D ;参数1 "argc / 9 = %d" 0000000140001022 sar edx, 1 ;edx=(argc*M>>32)>>1 0000000140001024 mov eax, edx 0000000140001026 shr eax, 1Fh ;eax=eax>>31取符号位 0000000140001029 add edx, eax ;if(edx < 0),edx=((argc*M>>32)>>1)+1 ;if(edx >=0),edx=(argc*M>>32)>>1参数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 mov eax, ebx ;eax = argc 0000000000402BFE sar ebx, 1Fh ;argc是正数ebx=0、负数ebx=-1 0000000000402C01 mov edx, 38E38E39h;edx = M 0000000000402C06 imul edx ;edx.eax=argc*M 0000000000402C08 lea rcx, aArgc9D ;参数1 "argc / 9 = %d" 0000000000402C0F sar edx, 1 ;edx=(argc*M>>32)>>1 0000000000402C11 sub edx, ebx ;if(edx < 0),edx=((argc*M>>32)>>1)+1 ;if(edx >=0),edx=(argc*M>>32)>>1参数2 0000000000402C13 call printf ;调用printf函数 0000000000402C18 xor eax, eax 0000000000402C1A add rsp, 20h 0000000000402C1E pop rbx 0000000000402C1F retn //x64_clang对应汇编代码讲解 0000000140001000 sub rsp, 28h 0000000140001004 movsxd rax, ecx ;rax = argc符号扩展成64位 0000000140001007 imul rdx, rax, 38E38E39h ;rdx = argc * M 000000014000100E mov rax, rdx 0000000140001011 shr rax, 3Fh ;rax=rax>>63取符号位 0000000140001015 sar rdx, 21h ;rdx = argc*M>>33 0000000140001019 add edx, eax ;if(rdx < 0),edx=(argc*M>>33)+1 ;if(rdx >=0),edx=argc*M>>33参数2 000000014000101B lea rcx, aArgc9D ;参数1 "argc / 9 = %d" 0000000140001022 call sub_140001030 ;调用printf函数 0000000140001027 xor eax, eax 0000000140001029 add rsp, 28h 000000014000102D retn
如代码清单4-8所示,和无符号2的幂有相似的地方,也是乘以一个魔数。在地址00401010处,我们看到了mov eax,38E38E39h,其后做了乘法和移位操作,最后直接使用edx显示。在乘法指令中,因为edx存放乘积数据的高32位字节,所以直接使用edx等价于乘积右移了32位,再算上00401019 sar edx,1,一共移动了33位。在地址0040101B处,eax得到了edx的值,然后对eax右移了1Fh位,也就是右移了31位。这里有个很奇怪的加法,移位的目的是得到有符号数的符号位,如果结果是正数,add eax,edx汇编代码的计算结果就是加0,等于什么都没做;如果是负数,后面的代码直接使用edx作为计算结果,就需要对除法的商调整加1。简单证明如下:
设,x、c皆为整数,c为正数,当x≥0时,有:
当x < 0时,根据推导3:
反推过程:
(eax×arg c)>> 33
等价于:
由此得:
解方程得:
于是,我们反推出优化前的高级代码为:
argc/9
总结
当遇到数学优化公式:如果x≥0,则;如果x<0,则时,基本可判定是除法优化后的代码,其除法原型为x除以常量c,imul可表明是有符号计算,其操作数是优化前的被除数x,接下来统计右移的总次数以确定公式中的n值,然后使用公式,将魔数作为M值代入公式求解常量除数c的近似值,四舍五入取整后,即可恢复除法原型。
f. 除数为有符号非2的幂(下)
另一种除数为有符号非2的幂优化方式如代码清单4-9所示。
代码清单4-9 第二种除数为有符号非2的幂优化(Release版)
// C++源码 #include <stdio.h> int main(int argc, char* argv[]) { printf("argc / 7 = %d", argc / 7); //变量除以常量,常量为非2的幂 return 0; } //x86_vs对应汇编代码讲解 00401010 mov eax, 92492493h ;eax = M 00401015 imul dword ptr [esp+4] ;edx.eax=argc*M 00401019 add edx, [esp+4] ;edx=(argc*M>>32)+argc 0040101D sar edx, 2 ;edx=((argc*M>>32)+argc)>>2 00401020 mov eax, edx ;eax=edx 00401022 shr eax, 1Fh ;eax=eax>>31取符号位 00401025 add eax, edx ;if(edx<0),eax=((argc*M>>32)+argc)>>2+1 ;if(edx >=0),eax=((argc*M>>32)+argc)>>2 00401027 push eax ;参数1 00401028 push offset aArgc7D ;参数2 "argc / 7 = %d" 0040102D call sub_401040 ;调用printf函数 00401032 add esp, 8 ;平衡栈 00401035 xor eax, eax 00401037 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 edx, 92492493h ;edx = M 00402597 mov dword ptr [esp], offset aArgc7D ;参数1 "argc / 7 = %d" 0040259E mov eax, ebx ;eax = argc 004025A0 imul edx ;edx.eax=argc*M 004025A2 add edx, ebx ;edx=(argc*M>>32)+argc 004025A4 sar ebx, 1Fh ;argc正数ebx=0 负数ebx=0xffffffff 004025A7 sar edx, 2 ;edx=((argc*M>>32)+argc)>>2 004025AA sub edx, ebx ;if(argc< 0),edx=((argc*M>>32)+argc)>>2+1 ;if(argc>=0),edx=((argc*M>>32)+argc)>>2 004025AC mov [esp+4], edx ;参数2 004025B0 call _printf ;调用printf函数 004025B5 xor eax, eax 004025B7 mov ebx, [ebp-4] 004025BA leave 004025BB retn //x86_clang对应汇编代码讲解 00401000 mov ecx, [esp+4] ;ecx = argc 00401004 mov edx, 92492493h ;edx = M 00401009 mov eax, ecx ;eax = argc 0040100B imul edx ;edx.eax=argc*M 0040100D add edx, ecx ;edx=(argc*M>>32)+argc 0040100F mov eax, edx 00401011 sar edx, 2 ;edx=((argc*M>>32)+argc)>>2 00401014 shr eax, 1Fh ;eax=edx>>31取符号位 00401017 add edx, eax ;if(edx < 0),edx=((argc*M>>32)+argc)>>2+1 ;if(edx >=0),edx=((argc*M>>32)+argc)>>2 00401019 push edx ;参数2 0040101A push offset aArgc7D ;参数1 "argc / 7 = %d" 0040101F call sub_401030 ;调用printf函数 00401024 add esp, 8 ;平衡栈 00401027 xor eax, eax 00401029 retn //x64_vs对应汇编代码讲解 0000000140001010 sub rsp, 28h 0000000140001014 mov eax, 92492493h ;eax = M 0000000140001019 imul ecx ;edx.eax=argc*M 000000014000101B add edx, ecx ;edx=(argc*M>>32)+argc 000000014000101D lea rcx, aArgc7D;参数1 "argc / 7 = %d" 0000000140001024 sar edx, 2 ;edx=((argc*M>>32)+argc)>>2 0000000140001027 mov eax, edx 0000000140001029 shr eax, 1Fh ;eax=edx>>31取符号位 000000014000102C add edx, eax ;if(edx < 0),edx=((argc*M>>32)+argc)>>2+1 ;if(edx >=0),edx=((argc*M>>32)+argc)>>2参数2 000000014000102E call sub_140001040 ;调用printf函数 0000000140001033 xor eax, eax 0000000140001035 add rsp, 28h 0000000140001039 retn //x64_gcc对应汇编代码讲解 0000000000402BF0 push rbx 0000000000402BF1 sub rsp, 20h 0000000000402BF5 mov ebx, ecx ;ebx = argc 0000000000402BF7 call __main ;调用初始化函数 0000000000402BFC mov eax, ebx ;eax = argc 0000000000402BFE mov edx, 92492493h ;edx = M 0000000000402C03 imul edx ;edx.eax=argc*M 0000000000402C05 lea rcx, aArgc7D ;参数1 "argc / 7 = %d" 0000000000402C0C add edx, ebx ;edx=(argc*M>>32)+argc 0000000000402C0E sar ebx, 1Fh ;argc正数ebx=0 负数ebx=0xffffffff 0000000000402C11 sar edx, 2 ;edx=((argc*M>>32)+argc)>>2 0000000000402C14 sub edx, ebx ;if(argc < 0),edx=((argc*M>>32)+argc)>>2+1 ;if(argc >=0),edx=((argc*M>>32)+argc)>>2参数2 0000000000402C16 call printf ;调用printf函数 0000000000402C1B xor eax, eax 0000000000402C1D add rsp, 20h 0000000000402C21 pop rbx 0000000000402C22 retn //x64_clang对应汇编代码讲解 0000000140001000 sub rsp, 28h 0000000140001004 movsxd rdx, ecx ;rdx=argc符号扩展成64位 0000000140001007 imul rax, rdx, 0FFFFFFFF92492493h ;rax=argc*M 000000014000100E shr rax, 20h ;rax=argc*M>>32 0000000140001012 add edx, eax ;edx=(argc*M>>32)+argc 0000000140001014 mov eax, edx 0000000140001016 shr eax, 1Fh ;eax=edx>>31取符号位 0000000140001019 sar edx, 2 ;edx=((argc*M>>32)+argc)>>2 000000014000101C add edx, eax ;if(edx < 0),edx=((argc*M>>32)+argc)>>2+1 ;if(edx >=0),edx=((argc*M>>32)+argc)>>2参数2 000000014000101E lea rcx, aArgc7D ;参数1 "argc / 7 = %d" 0000000140001025 call sub_140001040;调用printf函数 000000014000102A xor eax, eax 000000014000102C add rsp, 28h 0000000140001030 retn
虽然这个例子中的源码我们并不陌生,但是在00401019处的加法却显得很奇怪,其实这就是上面介绍的除法转乘法公式的变化。在00401015处的指令是imul dword ptr[esp+4],这是个有符号数的乘法。请注意,编译器在计算魔数时是作为无符号数处理的,而代入到除法转换乘法的代码里又是作为有符号乘数处理的。因为有符号数的最高位不是数值,而是符号位,所以,对应的有符号乘法指令是不会让最高位参与数值计算的,这样会导致乘数的数学意义和魔数不一致。
于是,在有符号乘法中,如果的取值大于0x80000000(最高位为1,补码形式为负数),实际参与乘法计算的是个负数,其值应等价于,证明如下:
设有符号整数x、无符号整数y,y≥0x80000000,我们定义y的有符号补码值表示为y(补),y的无符号值表示为y(无)。比如当y=0xffffffff时,y(补)的真值为-1,y(无)=232-1。
对x、y进行有符号乘法,根据求补计算规则可推出y(补)=232-y(无),因y≥0x80000000,所以y(补)表达为负数,其真值为-[232-y(无)],简化得到:
y(补)的真值=-[232-y(无)]=y(无)-232
例如:neg(5)=0xFFFFFFFB。
neg(5)的真值=-5=-(232-0xfffffffb)=0xfffffffb-232
代入到有符号乘法:
x·y(补)=x·[y(无)-232]
由此可得,对于有符号整数x、无符号整数y,y≥0x80000000,当x、y进行有符号乘法计算时,其结果等于x·[y(无)-232],若期望的乘法结果为x·y(无),则需要调整为:
x·y(无)=x·[y(无)-232]+232·x=x·y(补)+232·x
对于前面例题中的在计算机补码中表示为负数,根据以上推导,等价于,故其除法等价优化公式也相应调整:
完全理解以上证明后,我们回过头来分析代码清单4-7并还原高级代码。
先看00401010处的代码,如下所示。
00401010 mov eax, 92492493h 00401015 imul dword ptr [esp+4] 00401019 add edx, [esp+4] 0040101D sar edx, 2
这里先乘后加,但是参与加法的是edx,由于edx保留了乘法计算的高32位,于是edx等价于,然后加上被除数argc,对edx右移两位,负数调整后直接使用edx中的值,那么舍弃了低32位,相当于一共右移了34位,于是可推导原来的除数如下。
将以上代码转换为公式:
上式等价于,c为某常量,eax为魔数,arg c为被除数。
解方程得:
于是,我们反推出优化前的高级代码为:
argc / 7
注意,这里的arg c是有符号整型,因为指令中使用的是imul有符号乘法指令。
总结
当遇到数学优化公式:如果x≥0,则;如果x<0,则时,基本可判定是除法优化后的代码,其除法原型为x除以常量c,imul表明是有符号计算,其操作数是优化前的被除数x,接下来统计右移的总次数以确定公式中的n值,然后使用公式,将魔数作为M值代入公式求解常量除数c,即可恢复除法原型。
g. 除数为有符号负2的幂
除数为有符号负2的幂优化如代码清单4-10所示。
代码清单4-10 除数为有符号负2的幂优化(Release版)
// C++源码 #include <stdio.h> int main(int argc, char* argv[]) { printf("argc / -4 = %d", argc / -4); //变量除以常量,常量为-2的幂 return 0; } //x86_vs对应汇编代码讲解 00401010 mov eax, [esp+4] ;eax = argc 00401014 cdq ;eax符号位扩展,正数edx=0,负数edx=0xffffffff 00401015 and edx, 3 ;负数edx=3,正数edx=0 00401018 add eax, edx ;if(argc < 0),eax=argc+3 ;if(argc >= 0),eax=argc+0 0040101A sar eax, 2 ;if(argc < 0),eax=(argc+3)>>2 ;if(argc >= 0),eax=argc>>2 0040101D neg eax ;eax = -eax 0040101F push eax ;参数2 00401020 push offset aArgc4D ;参数1 "argc / -4 = %d" 00401025 call sub_401030 ;调用printf函数 0040102A add esp, 8 ;平衡栈 0040102D xor eax, eax 0040102F 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 dword ptr [esp], offset aArgc4D ;参数1 "argc / -4 = %d" 00402599 test ebx, ebx ;做与运算影响标志位 0040259B lea eax, [ebx+3] ;eax = argc + 3 0040259E cmovns eax, ebx ;条件执行,不为负数执行 ;if(argc >= 0),eax=argc 004025A1 sar eax, 2 ;if(argc < 0),eax=(argc+3)>>2 ;if(argc >= 0),eax=argc>>2 004025A4 neg eax ;eax = -eax 004025A6 mov [esp+4], eax ;参数2 004025AA call _printf ;调用printf函数 004025AF xor eax, eax 004025B1 mov ebx, [ebp-4] 004025B4 leave 004025B5 retn //x86_clang对应汇编代码讲解 00401000 mov eax, [esp+4] ;eax = argc 00401004 mov ecx, eax ;ecx = argc 00401006 sar ecx, 1Fh ;ecx算术右移31位,正数ecx=0,负数ecx=0xffffffff 00401009 shr ecx, 1Eh ;ecx逻辑右移30位,正数ecx=0,负数ecx=3 0040100C add ecx, eax ;if(argc < 0),ecx=argc+3 ;if(argc >= 0),ecx=argc+0 0040100E sar ecx, 2 ;if(argc < 0),ecx=(argc+3)>>2 ;if(argc >= 0),ecx=argc>>2 00401011 neg ecx ;ecx = -ecx 00401013 push ecx ;参数2 00401014 push offset aArgc4D ;参数1 "argc / -4 = %d" 00401019 call sub_401030 ;调用printf函数 0040101E add esp, 8 ;平衡栈 00401021 xor eax, eax 00401023 retn //x64_vs对应汇编代码讲解 0000000140001010 sub rsp, 28h 0000000140001014 mov eax, ecx ;eax = argc 0000000140001016 lea rcx, aArgc4D;参数1 "argc / -4 = %d" 000000014000101D cdq ;eax符号位扩展,正数edx=0,负数edx=0xffffffff 000000014000101E and edx, 3 ;负数edx=3,正数edx=0 0000000140001021 add edx, eax ;if(argc < 0),edx=argc+3 ;if(argc >= 0),edx=argc+0 0000000140001023 sar edx, 2 ;if(argc < 0),edx=(argc+3)>>2 ;if(argc >= 0),edx=argc>>2 0000000140001026 neg edx ;edx = -edx 参数2 0000000140001028 call sub_140001040;调用printf函数00401 000000014000102D xor eax, eax 000000014000102F add rsp, 28h 0000000140001033 retn //x64_gcc对应汇编代码讲解 0000000000402BF0 push rbx 0000000000402BF1 sub rsp, 20h 0000000000402BF5 mov ebx, ecx ;ebx = argc 0000000000402BF7 call __main ;调用初始化函数 0000000000402BFC lea edx, [rbx+3];edx = argc + 3 0000000000402BFF test ebx, ebx ;做与运算影响标志位 0000000000402C01 cmovns edx, ebx ;条件执行,不为负数执行 ;if(argc >= 0),edx=argc 0000000000402C04 lea rcx, aArgc4D;参数1 "argc / -4 = %d" 0000000000402C0B sar edx, 2 ;if(argc < 0),edx=(argc+3)>>2 ;if(argc >= 0),edx=argc>>2 0000000000402C0E neg edx ;edx = -edx 参数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 mov eax, ecx ;eax = argc 0000000140001006 sar eax, 1Fh ;eax算术右移32位,正数eax=0,负数eax=0xffffffff 0000000140001009 shr eax, 1Eh ;eax逻辑右移30位,正数eax=0,负数eax=3 000000014000100C lea edx, [rax+rcx];if(argc < 0),edx=argc+3 ;if(argc >= 0),edx=argc+0 000000014000100F sar edx, 2 ;if(argc < 0),edx=(argc+3)>>2 ;if(argc >= 0),edx=argc>>2 0000000140001012 neg edx ;edx = -edx 参数2 0000000140001014 lea rcx, aArgc4D;参数1 "argc / -4 = %d" 000000014000101B call sub_140001030 ;调用printf函数 0000000140001020 xor eax, eax 0000000140001022 add rsp, 28h 0000000140001026 retn
有了前面的基础,我们现在可以理解代码清单4-10的代码了,在0040100A地址处有符号右移指令存在的原因和前面讨论的道理一致,其后的neg相当于取负。对于除数为负的情况,neg的出现很合理。证明过程可参考除数为正的2的幂的示例,这里不再赘述。
总结
当遇到数学优化公式:如果x≥0,则;如果x < 0,则时,基本可判定是除法优化后的代码,根据n的值,可恢复除法原型。
h. 除数为有符号负非2的幂(上)
除数为有符号负非2的幂优化如代码清单4-11所示。
代码清单4-11 除数为有符号负非2的幂优化(Release版)
// C++源码 #include <stdio.h> int main(int argc, char* argv[]) { printf("argc / -5 = %d", argc / -5); //变量除以常量,常量为负非2的幂 return 0; } //x86_vs对应汇编代码讲解 00401010 mov eax, 99999999h ;eax = M 00401015 imul dword ptr [esp+4] ;edx.eax=argc*M 00401019 sar edx, 1 ;edx=argc*M>>32>>1 0040101B mov eax, edx 0040101D shr eax, 1Fh ;eax=edx>>31取符号位 00401020 add eax, edx ;if(edx < 0),eax=(argc*M>>32>>1)+1 ;if(edx >=0),eax=argc*M>>32>>1 00401022 push eax ;参数2 00401023 push offset aArgc5D ;参数1 "argc / -5 = %d" 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 push ebx 00402584 and esp, 0FFFFFFF0h ;对齐栈 00402587 sub esp, 10h 0040258A mov ebx, [ebp+8] ;ebx=argc 0040258D call ___main ;调用初始化函数 00402592 mov edx, 66666667h ;edx=c 00402597 mov dword ptr [esp], offset aArgc5D ;参数1 "argc / -5 = %d" 0040259E mov eax, ebx ;eax=argc 004025A0 sar ebx, 1Fh ;argc正数ebx=0 负数ebx=0xffffffff 004025A3 imul edx ;edx.eax=argc*M 004025A5 sar edx, 1 ;edx=argc*M>>32>>1 004025A7 sub ebx, edx ;if(argc < 0),edx=-1-(argc*M>>32>>1) ;edx=-((argc*M>>32>>1) + 1) ;if(argc >=0),edx=-(argc*M>>32>>1) 004025A9 mov [esp+4], ebx ;参数2 004025AD call _printf ;调用printf函数 004025B2 xor eax, eax 004025B4 mov ebx, [ebp-4] 004025B7 leave 004025B8 retn //x86_clang对应汇编代码讲解 00401000 mov eax, 99999999h ;eax=M 00401005 imul dword ptr [esp+4] ;edx.eax=argc*M 00401009 mov eax, edx 0040100B sar edx, 1 ;edx=argc*M>>32>>1 0040100D shr eax, 1Fh ;eax=edx>>31取符号位 00401010 add edx, eax ;if(edx < 0),edx=(argc*M>>32>>1) + 1 ;if(edx >=0),edx=argc*M>>32>>1 00401012 push edx ;参数2 00401013 push offset aArgc5D ;参数1 "argc / -5 = %d" 00401018 call sub_401030 ;调用printf函数 0040101D add esp, 8 ;平衡栈 00401020 xor eax, eax 00401022 retn //x64_vs对应汇编代码讲解 0000000140001010 sub rsp, 28h 0000000140001014 mov eax, 99999999h;eax=M 0000000140001019 imul ecx ;edx.eax=argc*M 000000014000101B lea rcx, aArgc5D ;参数1 "argc / -5 = %d" 0000000140001022 sar edx, 1 ;edx=argc*M>>32>>1 0000000140001024 mov eax, edx 0000000140001026 shr eax, 1Fh ;eax=edx>>31取符号位 0000000140001029 add edx, eax ;if(edx < 0),edx=(argc*M>>32>>1) + 1 ;if(edx >=0),edx=argc*M>>32>>1 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 mov eax, ebx ;eax=argc 0000000000402BFE sar ebx, 1Fh ;argc正数ebx=0 负数ebx=0xffffffff 0000000000402C01 mov edx, 66666667h;edx=M 0000000000402C06 imul edx ;edx.eax=argc*M 0000000000402C08 lea rcx, aArgc5D ;参数1 "argc / -5 = %d" 0000000000402C0F sar edx, 1 ;edx=argc*M>>32>>1 0000000000402C11 sub ebx, edx ;if(argc < 0),edx=-1-(argc*c>>32>>1) ;edx=-((argc*M>>32>>1) + 1) ;if(argc >=0),edx=-(argc*M>>32>>1) 0000000000402C13 mov edx, ebx ;参数2 0000000000402C15 call printf ;调用printf函数 0000000000402C1A xor eax, eax 0000000000402C1C add rsp, 20h 0000000000402C20 pop rbx 0000000000402C21 retn //x64_clang对应汇编代码讲解 0000000140001000 sub rsp, 28h 0000000140001004 movsxd rax, ecx ;rax=argc符号扩展成64位 0000000140001007 imul rdx, rax, 0FFFFFFFF99999999h ;argc*M 000000014000100E mov rax, rdx 0000000140001011 shr rax, 3Fh ;rax=rdx>>63取符号位 0000000140001015 sar rdx, 21h ;rdx=arg*M>>33 0000000140001019 add edx, eax ;if(rdx < 0),edx=(argc*M>>33) + 1 ;if(rdx >=0),edx=argc*M>>33 000000014000101B lea rcx, aArgc5D ;参数1 "argc / -5 = %d" 0000000140001022 call sub_140001030 ;调用printf函数 0000000140001027 xor eax, eax 0000000140001029 add rsp, 28h 000000014000102D retn
GCC编译器采用求正数的除法,再对结果求补就可以算出除数为负的结果,即。对于非求补除数为负的求值过程,有什么需要注意的呢?我们先看除法转乘法的过程。
当c为正数时,设,有:
当c为负数时,有:
我们再来看编译器产生的代码。eax是魔数,argc为被除数,根据代码体现以下表达式:
下面我们介绍一个分析编译器行为的好方法。步骤很简单,先写出高级语言,然后看对应的汇编代码,接着论证数学模型,最后归纳出还原的办法和依据。在这个例子中,我们出于研究目的,分析自己写的代码,所以对应的运算是已知的。
eax保存了魔的数值,据上式可得:
接下来,我们求解|c|:
分析以上代码时,很容易与除数为正的情况混淆,我们先看这两者之间的重要差异。关键在于上述代码中魔数的值。在前面讨论的除以7的例子中,当魔数最高位为1时,对于正除数,编译器会在imul和sar之间产生调整作用的add指令,而本例没有,故结合上下流程可分析魔数为补码形式,除数为负。这点应作为区分负除数的重要依据。
于是,我们反推出优化前的高级代码如下。
argc / -5
总结
当遇到数学优化公式:如果x≥0,则;如果x<0,则时,则基本可判定是除法优化后的代码,其除法原型为x除以常量c,imul可表明是有符号计算,其操作数是优化前的被除数x。由于魔数取值大于7fffffffh,而imul和sar之间未见任何调整代码,故可认定除数为负,且魔数为补码形式,接下来统计右移的总次数,以确定公式中的n值,然后使用公式,将魔数作为M值代入公式求解常量除数|c|,即可恢复除法原型。
i. 除数为有符号负非2的幂(下)
另一种除数为有符号负非2的幂优化方式如代码清单4-12所示。
代码清单4-12 另一种除数为有符号负非2的幂优化(Release版)
// C++源码 #include <stdio.h> int main(int argc, char* argv[]) { printf("argc / -7 = %d", argc / -7); //变量除以常量,常量为负非2的幂 return 0; } //x86_vs对应汇编代码讲解 00401010 mov eax, 6DB6DB6Dh ;eax=M 00401015 imul dword ptr [esp+4];edx.eax=argc*M 00401019 sub edx, [esp+4] ;edx=(argc*M>>32)-argc 0040101D sar edx, 2 ;edx=(argc*M>>32)-argc>>2 00401020 mov eax, edx 00401022 shr eax, 1Fh ;eax=edx>>31取符号位 00401025 add eax, edx ;if(edx < 0),eax=((argc*M>>32)-argc>>2) + 1 ;if(edx >=0),eax=(argc*M>>32)-argc>>2 00401027 push eax ;参数2 00401028 push offset aArgc7D ;参数1 "argc / -7 = %d" 0040102D call sub_401040 ;调用printf函数 00401032 add esp, 8 ;平衡栈 00401035 xor eax, eax 00401037 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 edx, 92492493h ;edx=M 00402597 mov dword ptr [esp], offset aArgc7D ;参数1 "argc / -7 = %d" 0040259E mov eax, ebx ;eax=argc 004025A0 imul edx ;edx.eax=argc*M 004025A2 add edx, ebx ;edx=(argc*M>>32)+argc 004025A4 sar ebx, 1Fh ;argc正数ebx=0 负数ebx=0xffffffff 004025A7 sar edx, 2 ;edx=((argc*M>>32)+argc)>>2 004025AA sub ebx, edx ;if(argc < 0),edx=-1-(((argc*M>>32)+argc)>>2) ;edx=-((((argc*M>>32)+argc)>>2) + 1) ;if(argc >=0),edx=-(((argc*M>>32)+argc)>>2) 004025AC mov [esp+4], ebx ;参数2 004025B0 call _printf ;调用printf函数 004025B5 xor eax, eax 004025B7 mov ebx, [ebp-4] 004025BA leave 004025BB retn //x86_clang对应汇编代码讲解 00401000 mov ecx, [esp+4] ;ecx=argc 00401004 mov edx, 6DB6DB6Dh ;edx=M 00401009 mov eax, ecx ;eax=argc 0040100B imul edx ;edx.eax=argc*M 0040100D sub edx, ecx ;edx=(argc*M>>32)-argc 0040100F mov eax, edx 00401011 sar edx, 2 ;edx=(argc*M>>32)-argc>>2 00401014 shr eax, 1Fh ;eax=edx>>31取符号位 00401017 add edx, eax ;if(edx < 0),edx=((argc*M>>32)-argc>>2) + 1 ;if(edx >=0),edx=(argc*M>>32)-argc>>2 00401019 push edx ;参数2 0040101A push offset aArgc7D ;参数1 "argc / -7 = %d" 0040101F call sub_401030 ;调用printf函数 00401024 add esp, 8 ;平衡栈 00401027 xor eax, eax 00401029 retn ;return 0 //x64_vs对应汇编代码讲解 0000000140001010 sub rsp, 28h 0000000140001014 mov eax, 6DB6DB6Dh ;eax=M 0000000140001019 imul ecx ;edx.eax=argc*M 000000014000101B sub edx, ecx ;edx=(argc*M>>32)-argc 000000014000101D lea rcx, aArgc7D ;参数1 "argc / -7 = %d" 0000000140001024 sar edx, 2 ;edx=(argc*M>>32)-argc>>2 0000000140001027 mov eax, edx 0000000140001029 shr eax, 1Fh ;eax=edx>>31取符号位 000000014000102C add edx, eax ;if(edx<0),edx=((argc*M>>32)-argc>>2)+1 ;if(edx >=0),edx=(argc*M>>32)-argc>>2参数2 000000014000102E call sub_140001040 ;调用printf函数 0000000140001033 xor eax, eax 0000000140001035 add rsp, 28h 0000000140001039 retn //x64_gcc对应汇编代码讲解 0000000000402BF0 push rbx 0000000000402BF1 sub rsp, 20h 0000000000402BF5 mov ebx, ecx ;ebx=argc 0000000000402BF7 call __main ;调用初始化函数 0000000000402BFC mov eax, ebx ;eax=argc 0000000000402BFE mov edx, 92492493h ;edx=M 0000000000402C03 imul edx ;edx.eax=argc*M 0000000000402C05 lea rcx, aArgc7D ;参数1 "argc / -7 = %d" 0000000000402C0C lea eax, [rdx+rbx] ;eax=(argc*M>>32)+argc 0000000000402C0F sar ebx, 1Fh ;argc正数ebx=0 负数ebx=0xffffffff 0000000000402C12 sar eax, 2 ;eax=((argc*M>>32)+argc)>>2 0000000000402C15 mov edx, ebx 0000000000402C17 sub edx, eax ;if(argc < 0),edx=-1-((argc*M>>32)+ argc>>2) ;edx=-(((argc*M>>32)+argc>>2) + 1) ;if(argc >=0),edx=-((argc*M>>32)+argc>>2) 0000000000402C19 call printf ;调用printf函数 0000000000402C1E xor eax, eax 0000000000402C20 add rsp, 20h 0000000000402C24 pop rbx 0000000000402C2 5retn //x64_clang对应汇编代码讲解 0000000140001000 sub rsp, 28h 0000000140001004 movsxd rax, ecx ;rax=argc符号扩展成64位 0000000140001007 imul rdx, rax, 6DB6DB6Dh ;rdx=argc*M 000000014000100E shr rdx, 20h ;rdx=argc*M>>32 0000000140001012 sub edx, eax ;edx=(argc*M>>32)-argc 0000000140001014 mov eax, edx 0000000140001016 shr eax, 1Fh ;eax=edx>>31取符号位 0000000140001019 sar edx, 2 ;edx=(argc*M>>32)-argc>>2 000000014000101C add edx, eax ;if(edx < 0),edx=((argc*M>>32)-argc>>2) + 1 ;if(edx >=0),edx=(argc*M>>32)-argc>>2 000000014000101E lea rcx, aArgc7D ;"argc / -7 = %d" 0000000140001025 call sub_140001040 000000014000102A xor eax, eax 000000014000102C add rsp, 28h 0000000140001030 retn
GCC编译器采用代码清单4-12的公式做正数的除法,再对结果求补,就可以算出除数为负的结果,即。对于非求补除数为负的求值过程,回忆前面除数等于+7的讨论,对于正除数,魔数大于0x7fffffff的处理如下。
魔数为。
当除数c为负数时,我们可以直接对上式的魔数取负,设魔数为M:
对应调整除法转换公式:
理解以上推导后,可先将以上代码转换为公式:
由上式可得:
接下来,我们求解|c|:
于是,我们反推出优化前的高级代码为:
argc / -7
总结
当遇到数学优化公式:如果x≥0,则,如果x<0,则,可判定是除法优化后的汇编代码,其除法原型为x除以常量c,imul可表明是有符号计算,其操作数是优化前的被除数x。由于魔数取值小于等于7fffffffh,而imul和sar之间有sub指令调整乘积,故可认定除数为负,且魔数为补码形式,接下来统计右移的总次数以确定公式中的n值,然后使用公式将魔数作为M值代入公式求解常量除数|c|,即可恢复除法原型。
j. 除法优化的原则
看到这里,大家应该注意到了,在以上讨论中,还原所得的除数是近似值,说明给出的公式还不够严格。我们接下来可以好好思考一下其值近似但不等的原因,先看看余数是多少。
回忆一下除法和余数的关系,根据性质3,有:
b=(a-r)÷q
代入,公式改为。
以除以9为例。
解方程求r:
r=38E38E39h×9−233=200000001h−200000000h=1
于是找到不等于的原因:,这里的是存在错误的,魔数是整数值,而是实数值。
于是修改推导公式为:
现在又出现了“新问题”,在反汇编代码流程中,还原的公式为:
(eax * arg c)>> 33
等价于:
38E38E39h是的值,代入得到:
看起来前面的步骤都前功尽弃了。
现在我们来解决这个问题,当x≥0时,根据C语言除法向零取整规则,有:
证明:
在编译器计算魔数时,如果给出一个合适的n值,使下式成立:
则根据推导6可得:
举例说明一下,以前面讨论的arg c/9为例,设arg c/9商为q:
当arg c≥0时,有
显然arg c<233,于是得到,根据推导6可以得到:
当x<0时,有:
证明:
根据推导3,
根据推导7,
在编译器计算魔数时,如果给出一个合适的n值,使下式成立:
则根据推导6可得:
举例说明一下,以前面讨论的arg c/9为例,设arg c/9商为q:
当arg c<0时,有
根据推导7:
显然,根据推导6可以得到:
由以上讨论可以看出,关键在于编译器计算魔数的值,使得运算结果满足推导6中的等式,其中计算确定魔数表达式中2n的值尤为重要。其他案例读者可以自行分析。
笔者曾经分析过VC++6.0中计算魔数的过程,现在将分析的要点和还原的代码提供出来,供有兴趣的读者研究。
找到VC++6.0安装目录下的\VC98\Bin文件夹里c2.dll(版本12.0.9782.0),先用OD载入这个目录下的CL.EXE,加入命令行后开始调试(如:cl/c/O2 test.cpp)。然后在LoadLibrary这个函数下断点,等待c2加载,有符号整数除法魔数的计算过程在c2的文件偏移5FACE处,加载后的虚拟地址请自行计算(参考PE格式相关资料)。断点设置在此处可以看到有符号整数除法魔数的推算过程,其汇编代码过长,读者可以使用IDA查看,本书不再展开,下面提供F5后修改的C代码(见随附资源中的SignedDivision.cpp)。
k. 取模
理解了整数除法后,我们顺便也谈谈取模。取模优化如代码清单4-13所示。
代码清单4-13 取模优化(Release版)
// C++源码 #include <stdio.h> int main(int argc, char* argv[]) { printf("%d", argc % 8); //变量模常量,常量为2的幂 printf("%d", argc % 9); //变量模常量,常量为非2的幂 return 0; } //x86_vs对应汇编代码讲解 00401010 push esi 00401011 mov esi, [esp+8] ;esi=argc 0040101 5 mov eax, esi ;eax=argc 00401017 and eax, 80000007h ;eax=argc&7(最高位1为了检查负数) 0040101C jns short loc_401023 ;if (argc >= 0) goto loc_401023 0040101E dec eax 0040101F or eax, 0FFFFFFF8h 00401022 inc eax ;if (argc < 0) eax= ;((argc&7)-1 | ~7) + 1 00401023 loc_401023: 00401023 push eax ;参数2 00401024 push offset unk_412160 ;参数1 00401029 call sub_401060 ;调用printf函数 0040102E mov eax, 38E38E39h 00401033 imul esi 00401035 sar edx, 1 00401037 mov eax, edx 00401039 shr eax, 1Fh 0040103C add eax, edx ;eax=argc/9 0040103E lea eax, [eax+eax*8] ;eax=argc/9*9 00401041 sub esi, eax ;esi=argc-argc/9*9 00401043 push esi ;参数2 00401044 push offset unk_412160 ;参数1 00401049 call sub_401060 ;调用printf函数 0040104E add esp, 10h 00401051 xor eax, eax 00401053 pop esi 00401054 retn //x86_gcc对应汇编代码讲解 00402580 push ebp 00402581 mov ebp, esp 00402583 push esi 00402584 push ebx 00402585 and esp, 0FFFFFFF0h ;对齐栈 00402588 sub esp, 10h 0040258B mov ebx, [ebp+8] ;ebx=argc 0040258E call ___main ;调用初始化函数 00402593 mov dword ptr [esp], offset aD ;参数1 "%d" 0040259A mov esi, ebx ;esi=argc 0040259C sar esi, 1Fh ;argc正数esi = 0 负数esi=0xffffffff 0040259F mov edx, esi 004025A1 shr edx, 1Dh ;argc正数edx = 0 负数edx=7 004025A4 lea eax, [ebx+edx] ;if (argc >=0) eax=argc ;if (argc < 0) eax=argc+7 004025A7 and eax, 7 ;if (argc >=0) eax=argc&7 ;if (argc < 0) eax=argc+7&7 004025AA sub eax, edx ;if (argc >=0) eax=argc&7 ;if (argc < 0) eax=(argc+7&7)-7 004025AC mov [esp+4], eax ;参数2 004025B0 call _printf ;调用printf函数 004025B5 mov eax, ebx ;eax=argc 004025B7 mov edx, 38E38E39h 004025BC mov dword ptr [esp], offset aD ;参数1 "%d" 004025C3 imul edx 004025C5 sar edx, 1 004025C7 sub edx, esi ;edx=argc/9 004025C9 lea eax, [edx+edx*8] ;eax=argc/9*9 004025CC sub ebx, eax ;ebx=argc-argc/9*9 004025CE mov [esp+4], ebx ;参数2 004025D2 call _printf ;调用printf函数 004025D7 lea esp, [ebp-8] ;释放环境变量空间 004025DA xor eax, eax 004025DC pop ebx 004025DD pop esi 004025DE pop ebp 004025DF retn //x86_clang对应汇编代码讲解 00401000 push esi 00401001 mov esi, [esp+8] ;esi=argc 00401005 mov eax, esi ;eax=argc 00401007 mov ecx, esi ;ecx=argc 00401009 sar eax, 1Fh ;argc正数 eax=0 负数 eax=0xffffffff 0040100C shr eax, 1Dh ;argc正数 eax=0 负数 eax=7 0040100F add eax, esi ;if (argc >=0) eax=argc ;if (argc < 0) eax=argc+7 00401011 and eax, 0FFFFFFF8h ;if (argc >=0) eax=argc&(˜7) ;if (argc < 0) eax=argc+7&(˜7) 00401014 sub ecx, eax ;if (argc >=0) ecx=argc-(argc&(˜7)) ;if (argc < 0) ecx=argc-(argc+7&(˜7)) 00401016 push ecx ;参数2 00401017 push offset unk_412160 ;参数1 0040101C call sub_401050 ;调用printf函数 00401021 add esp, 8 ;平衡栈 00401024 mov ecx, 38E38E39h 00401029 mov eax, esi ;eax=argc 0040102B imul ecx 0040102D mov eax, edx 0040102F sar edx, 1 00401031 shr eax, 1Fh 00401034 add edx, eax ;edx=argc/9 00401036 lea eax, [edx+edx*8] ;eax=argc/9*9 00401039 sub esi, eax ;esi=argc-argc/9*9 0040103B push esi ;参数2 0040103C push offset unk_412160 ;参数1 00401041 call sub_401050 ;调用printf函数 00401046 add esp, 8 ;平衡栈 00401049 xor eax, eax 0040104B pop esi 0040104C retn //x64_vs对应汇编代码讲解 0000000140001010 push rbx 0000000140001012 sub rsp, 20h 0000000140001016 mov edx, ecx ;edx=argc 0000000140001018 mov ebx, ecx ;ebx=argc 000000014000101A and edx, 80000007h ;edx=argc&7(最高位1为了检查负数) 0000000140001020 jge short loc_140001029;if (argc >= 0) goto loc_140001029 0000000140001022 dec edx 0000000140001024 or edx, 0FFFFFFF8h 0000000140001027 inc edx ;if(argc < 0) edx=((argc&7)-1 | ~7) +1参数2 0000000140001029 loc_140001029: 0000000140001029 lea rcx, unk_1400122C0 ;参数1 0000000140001030 call sub_140001060 ;调用printf函数 0000000140001035 mov eax, 38E38E39h 000000014000103A lea rcx, unk_1400122C0 ;参数1 0000000140001041 imul ebx 0000000140001043 sar edx, 1 0000000140001045 mov eax, edx 0000000140001047 shr eax, 1Fh 000000014000104A add edx, eax ;edx=argc/9 000000014000104C lea eax, [rdx+rdx*8] ;eax=argc/9*9 000000014000104F sub ebx, eax ;ebx=argc-argc/9*9 0000000140001051 mov edx, ebx ;参数2 0000000140001053 call sub_140001060 ;调用printf函数 0000000140001058 xor eax, eax 000000014000105A add rsp, 20h 000000014000105E pop rbx 000000014000105F retn //x64_gcc对应汇编代码讲解 0000000000402BF0 push rsi 0000000000402BF1 push rbx 0000000000402BF2 sub rsp, 28h 0000000000402BF6 mov ebx, ecx ;ebx=argc 0000000000402BF8 call __main ;调用初始化函数 0000000000402BFD lea rcx, aD ;参数1 "%d" 0000000000402C04 mov esi, ebx ;esi=argc 0000000000402C06 sar esi, 1Fh ;argc正数 esi=0 负数esi=0xffffffff 0000000000402C09 mov eax, esi 0000000000402C0B shr eax, 1Dh ;argc正数 eax=0 负数eax=7 0000000000402C0E lea edx, [rbx+rax];if (argc >=0) edx=argc ;if (argc < 0) edx=argc+7 0000000000402C11 and edx, 7 ;if (argc >=0) edx=argc&7 ;if (argc < 0) edx=argc+7&7 0000000000402C14 sub edx, eax ;if (argc >=0) edx=argc&7 ;if (argc < 0) edx=(argc+7&7)-7 参数2 0000000000402C16 call printf ;调用printf函数 0000000000402C1B mov eax, ebx ;eax=argc 0000000000402C1D mov ecx, 38E38E39h 0000000000402C22 imul ecx 0000000000402C24 lea rcx, aD ;参数1 "%d" 0000000000402C2B mov eax, edx 0000000000402C2D sar eax, 1 0000000000402C2F sub eax, esi ;eax=argc/9 0000000000402C31 lea eax, [rax+rax*8] ;eax=argc/9*9 0000000000402C34 sub ebx, eax ;ebx=argc-argc/9*9 0000000000402C36 mov edx, ebx ;参数2 0000000000402C38 call printf ;调用printf函数 0000000000402C3D xor eax, eax 0000000000402C3F add rsp, 28h 0000000000402C43 pop rbx 0000000000402C44 pop rsi 0000000000402C45 retn //x64_clang对应汇编代码讲解 0000000140001000 push rsi 0000000140001001 push rdi 0000000140001002 sub rsp, 28h 0000000140001006 mov esi, ecx ;esi=argc 0000000140001008 mov eax, ecx ;eax=argc 000000014000100A sar eax, 1Fh ;argc正数 eax=0 负数 eax=0xffffffff 000000014000100D shr eax, 1Dh ;argc正数 eax=0 负数 eax=7 0000000140001010 add eax, ecx ;if (argc >=0) eax=argc ;if (argc < 0) eax=argc+7 0000000140001012 and eax, 0FFFFFFF8h ;if (argc >=0) eax=argc&(˜7) ;if (argc < 0) eax=argc+7&(˜7) 0000000140001015 mov edx, ecx ;edx=argc 0000000140001017 sub edx, eax ;if (argc >=0) ecx=argc-(argc&(˜7)) ;if (argc < 0) ecx=argc-(argc+7&(˜7))参数2 0000000140001019 lea rdi, unk_1400122C0 0000000140001020 mov rcx, rdi ;参数1 0000000140001023 call sub_140001060 ;调用printf函数 0000000140001028 movsxd rdx, esi 000000014000102B imul rax, rdx, 38E38E39h 0000000140001032 mov rcx, rax 0000000140001035 shr rcx, 3Fh 0000000140001039 sar rax, 21h 000000014000103D add eax, ecx ;eax=argc/9 000000014000103F lea eax, [rax+rax*8] ;eax=argc/9*9 0000000140001042 sub edx, eax ;eax=argc-argc/9*9 参数2 0000000140001044 mov rcx, rdi ;参数1 0000000140001047 call sub_140001060 ;调用printf函数 000000014000104C xor eax, eax 000000014000104E add rsp, 28h 0000000140001052 pop rdi 0000000140001053 pop rsi 0000000140001054 retn
如代码清单4-13所示,对2的幂取模,有以下4种优化方案。
方案1:
对2的k次方取余,余数的值只须取被除数低k位的值即可,负数则还须在k位之前补1,设k为5,代码如下所示。
mov reg, 被除数 and reg,80000007h jns LAB1 or reg, 0FFFFFFF8h LAB1:
如果余数的值非0,以上代码是没有问题的;如果余数的值为0,则根据以上代码计算出的结果(FFFFFFE0h)是错误的。因此应该加以调整,调整的方法为在or运算之前减1,在or运算之后加1。对于余数不为0的情况,此调整不影响计算结果;对于余数为0的情况,末尾k位全部为0值,此时减1得到末尾k位为1,or运算得到-1,最后加1得到余数值为0。调整后的代码如下。
mov reg, 被除数 and reg,80000007 ; 这里的立即数是去掉高位保留低位的掩码,其值由2<sup><i>k</i></sup>决定 jns LAB1 dec reg or reg, FFFFFFF8 inc reg LAB1:
当遇到以上指令序列时,基本可判定是取模代码,其取模原型为被除数(变量)对2k(常量)执行取模运算,jns可表明为有符号计算,考察“and reg, 00000007”这类去掉高位保留低位的代码,统计出低位一共保留了多少1,即可得到k的值,代入求得2k的值后,可恢复取模代码原型。
方案2:
对2的k次方取余,对于正数采用方案1相同方案,对于负数采用以下调整公式:
x%2k=(x+(2k-1)&(2k-1))-(2k-1)
当遇到以上数学公式时,基本可判定是取模代码,其取模原型为被除数(变量)对2k(常量)执行取模运算,考察“and reg,0FFFFFFF8”统计出低位一共保留了多少0,即可得到k的值,代入求得2k的值后,可恢复取模代码原型。
方案3:
对2的k次方取余,对于正数采用以下调整公式。
x%2k=x-(x &~(2k-1))
对于负数采用以下调整公式:
x%2k=x-(x+(2k-1)&~(2k-1))
当遇到以上数学公式时,基本可判定是取模代码,其取模原型为被除数(变量)对2k(常量)执行取模运算,考察“and reg,0FFFFFFF8”统计出低位一共保留了多少0,即可得到k的值,代入求得2k的值后,可恢复取模代码原型。
方案4:
对非2的k次方取余,r=a%b,根据性质5,r=a-q·b=a-a÷b·b。
当遇到以上数学公式时,基本可判定是取模代码,考察b,可恢复取模代码原型。
4.1.2 算术结果溢出
我们在前面已经接触过算术结果溢出的相关知识,例如占据4字节32位内存空间的数据经过运算后,得到的结果超出存储空间的大小,就会产生溢出现象。又如,int类型的数据0xFFFFFFFF加2得到的结果将会超出int类型的存储范围,超出的部分也称为溢出数据。溢出数据无法被保存,将会丢失。对于有符号数而言,原数据为一个负数,溢出后由于表示符号的最高位而进位,原来的1变成了0,这时负数也相应地成为了正数,如图4-4所示。
图4-4 溢出结果对比
图4-4演示了数据是如何产生溢出以及溢出后为什么数据会改变符号(由一个负数变为正数)。一个无符号数产生溢出后会从最大数变为最小数。有符号数溢出会修改符号位,如代码清单4-14所示。
代码清单4-14 利用溢出跳出循环
// 看似死循环的for语句 for (int i = 1; i > 0; i++) { printf("%d\n", i); }
代码清单4-14中的for循环看上去是一个死循环,但由于i是一个有符号数,当i等于允许取得的最大正数值0x7FFFFFFF时,再次加1后,数值会进位,将符号位0修改为1,最终结果为0x80000000。这时的最高位为1,按照有符号数解释,这便是一个负数。对于for循环而言,当循环条件为假时,跳出循环体,结束循环。
溢出是由于数据进位后超出数据保存范围导致的。溢出和进位都表示数据超出了存储范围,它们之间有什么区别呢?
1. 进位
无符号数超出存储范围叫作进位。因为没有符号位,不会破坏数据,而进位的1位数据会被进位标志为CF保存。而在标志位CF中,可通过查看进位标志位CF,检查数据是否进位。
2. 溢出
有符号数超出存储范围叫作溢出,由于数据进位,从而破坏了有符号数的最高位——符号位。只有有符号数才有符号位,所以溢出只针对有符号数。可查看溢出标志位OF,检查数据是否溢出。OF的判定规则很简单,如果参与加法计算的数值符号一致,而计算结果符号不同,则判定OF成立,其他都不成立。
也有其他操作指令会导致溢出或进位,具体请参考Intel手册。
4.1.3 自增和自减
C++中使用“++”“--”来实现自增和自减操作。自增和自减有两种定义:一种为自增自减运算符在语句块之后,则先执行语句块,再执行自增自减;另一种恰恰相反,自增自减运算符在语句块之前,则先执行自增和自减,再执行语句块。通常,自增和自减是被拆分成两条汇编指令语句执行的,如代码清单4-15所示。
代码清单4-15 自增和自减
// C++源码 #include <stdio.h> int main(int argc, char* argv[]) { int n1 = argc; int n2 = argc; //变量定义并初始化 n2 = 5 + (n1++); //变量后缀自增参与表达式运算 n2 = 5 + (++n1); //变量前缀自增参与表达式运算 n1 = 5 + (n2--); //变量后缀自减参与表达式运算 n1 = 5 + (--n2); //变量前缀自减参与表达式运算 return 0; } //x86_vs对应汇编代码讲解 00401000 push ebp 00401001 mov ebp, esp 00401003 sub esp, 8 00401006 mov eax, [ebp+8] ;eax=argc 00401009 mov [ebp-8], eax ;n1=argc 0040100C mov ecx, [ebp+8] ;ecx=argc 0040100F mov [ebp-4], ecx ;n2=argc 00401012 mov edx, [ebp-8] 00401015 add edx, 5 00401018 mov [ebp-4], edx ;n2=5+n1 0040101B mov eax, [ebp-8] 0040101E add eax, 1 00401021 mov [ebp-8], eax ;n1+=1 00401024 mov ecx, [ebp-8] 00401027 add ecx, 1 0040102A mov [ebp-8], ecx ;n1+=1 0040102D mov edx, [ebp-8] 00401030 add edx, 5 00401033 mov [ebp-4], edx ;n2=5+n1 00401036 mov eax, [ebp-4] 00401039 add eax, 5 0040103C mov [ebp-8], eax ;n1=5+n2 0040103F mov ecx, [ebp-4] 00401042 sub ecx, 1 00401045 mov [ebp-4], ecx ;n2-=1 00401048 mov edx, [ebp-4] 0040104B sub edx, 1 0040104E mov [ebp-4], edx ;n2-=1 00401051 mov eax, [ebp-4] 00401054 add eax, 5 00401057 mov [ebp-8], eax ;n1=5+n2 0040105A xor eax, eax 0040105C mov esp, ebp 0040105E pop ebp 0040105F retn //x86_gcc对应汇编代码讲解 00401510 push ebp 00401511 mov ebp, esp 00401513 and esp, 0FFFFFFF0h 00401516 sub esp, 10h 00401519 call ___main ;调用初始化函数 0040151E mov eax, [ebp+8] ;eax=argc 00401521 mov [esp+0Ch], eax ;n1=argc 00401525 mov eax, [ebp+8] ;eax=argc 00401528 mov [esp+8], eax ;n2=argc 0040152C mov eax, [esp+0Ch] ;eax=n1 00401530 lea edx, [eax+1] ;edx=n1+1 00401533 mov [esp+0Ch], edx ;n1+=1 00401537 add eax, 5 0040153A mov [esp+8], eax ;n2=5+n1,n1为n1+=1之前的值 0040153E add dword ptr [esp+0Ch], 1 ;n1+=1 00401543 mov eax, [esp+0Ch] 00401547 add eax, 5 0040154A mov [esp+8], eax ;n2=5+n1, n1为n1+=1之后的值 0040154E mov eax, [esp+8] ;eax=n2 00401552 lea edx, [eax-1] ;edx=n2-1 00401555 mov [esp+8], edx ;n2-=1 00401559 add eax, 5 0040155C mov [esp+0Ch], eax ;n1=5+n2, n2为n2-=1之前的值 00401560 sub dword ptr [esp+8], 1 ;n2-=1 00401565 mov eax, [esp+8] 00401569 add eax, 5 0040156C mov [esp+0Ch], eax ;n1=5+n2, n2为n2-=1之后的值 00401570 mov eax, 0 00401575 leave 00401576 retn //x86_clang对应汇编代码讲解 00401000 push ebp 00401001 mov ebp, esp 00401003 push edi 00401004 push esi 00401005 sub esp, 14h 00401008 mov eax, [ebp+0Ch] ;eax=argv 0040100B mov ecx, [ebp+8] ;ecx=argc 0040100E xor edx, edx 00401010 mov dword ptr [ebp-0Ch], 0 00401017 mov esi, [ebp+8] ;esi=argc 0040101A mov [ebp-10], esi ;n1=argc 0040101D mov esi, [ebp+8] 00401020 mov [ebp-14], esi ;n2=argc 00401023 mov esi, [ebp-10h] ;esi=n1 00401026 mov edi, esi 00401028 add edi, 1 0040102B mov [ebp-10h], edi ;n1+=1 0040102E add esi, 5 00401031 mov [ebp-14], esi ;n2=5+n1,n1为n1+=1之前的值 00401034 mov esi, [ebp-10h] 00401037 add esi, 1 0040103A mov [ebp-10h], esi ;n1+=1 0040103D add esi, 5 00401040 mov [ebp-14h], esi ;n2=5+n1,n1为n1+=1之后的值 00401043 mov esi, [ebp-14h] ;esi=n2 00401046 mov edi, esi 00401048 add edi, 0FFFFFFFFh ;edi=n2+-1=n2-1 0040104B mov [ebp-14h], edi ;n2-=1 0040104E add esi, 5 00401051 mov [ebp-10h], esi ;n1=5+n2,n2为n2-=1之前的值 00401054 mov esi, [ebp-14h] 00401057 add esi, 0FFFFFFFFh 0040105A mov [ebp-14h], esi ;n2-=1 0040105D add esi, 5 00401060 mov [ebp-10h], esi ;n1=5+n2,n2为n2-=1之后的值 00401063 mov [ebp-18h], eax 00401066 mov eax, edx 00401068 mov [ebp-1Ch], ecx 0040106B add esp, 14h 0040106E pop esi 0040106F pop edi 00401070 pop ebp 00401071 retn //x64_vs对应汇编代码讲解 0000000140001000 mov [rsp+10h], rdx 0000000140001005 mov [rsp+8], ecx 0000000140001009 sub rsp, 18h 000000014000100D mov eax, [rsp+20h] ;eax=argc 0000000140001011 mov [rsp+4], eax ;n1=argc 0000000140001015 mov eax, [rsp+20h] ;eax=argc 0000000140001019 mov [rsp], eax ;n2=argc 000000014000101C mov eax, [rsp+4] 0000000140001020 add eax, 5 0000000140001023 mov [rsp], eax ;n2=5+n1 0000000140001026 mov eax, [rsp+4] 000000014000102A inc eax 000000014000102C mov [rsp+4], eax ;n1+=1 0000000140001030 mov eax, [rsp+4] 0000000140001034 inc eax 0000000140001036 mov [rsp+4], eax ;n1+=1 000000014000103A mov eax, [rsp+4] 000000014000103E add eax, 5 0000000140001041 mov [rsp], eax ;n2=5+n1 0000000140001044 mov eax, [rsp] 0000000140001047 add eax, 5 000000014000104 Amov [rsp+4], eax ;n1=5+n2 000000014000104 Emov eax, [rsp] 0000000140001051 dec eax 0000000140001053 mov [rsp], eax ;n2-=1 0000000140001056 mov eax, [rsp] 0000000140001059 dec eax 000000014000105B mov [rsp], eax ;n2-=1 000000014000105E mov eax, [rsp] 0000000140001061 add eax, 5 0000000140001064 mov [rsp+4], eax ;n1=5+n2 0000000140001068 xor eax, eax 000000014000106A add rsp, 18h 000000014000106E retn //x64_gcc对应汇编代码讲解 0000000000401550 push rbp 0000000000401551 mov rbp, rsp 0000000000401554 sub rsp, 30h 0000000000401558 mov [rbp+10h], ecx 000000000040155B mov [rbp+18h], rdx 000000000040155F call __main 0000000000401564 mov eax, [rbp+10h] ;eax=argc 0000000000401567 mov [rbp-4], eax ;n1=argc 000000000040156A mov eax, [rbp+10h] ;eax=argc 000000000040156D mov [rbp-8], eax ;n2=argc 0000000000401570 mov eax, [rbp-4] ;eax=n1 0000000000401573 lea edx, [rax+1] 0000000000401576 mov [rbp-4], edx ;n1+=1 0000000000401579 add eax, 5 000000000040157C mov [rbp-8], eax ;n2=5+n1,n1为n1+=1之前的值 000000000040157F add dword ptr [rbp-4], 1 ;n1+=1 0000000000401583 mov eax, [rbp-4] 0000000000401586 add eax, 5 0000000000401589 mov [rbp-8], eax ;n2=5+n1,n1为n1+=1之后的值 000000000040158C mov eax, [rbp-8] ;eax=n2 000000000040158F lea edx, [rax-1] 0000000000401592 mov [rbp-8], edx ;n2-=1 0000000000401595 add eax, 5 0000000000401598 mov [rbp-4], eax ;n1=5+n2,n2为n2-=1之前的值 000000000040159B sub dword ptr [rbp-8], 1 ;n2-=1 000000000040159F mov eax, [rbp-8] 00000000004015A2 add eax, 5 00000000004015A5 mov [rbp-4], eax ;n1=5+n2,n2为n2-=1之后的值 00000000004015A8 mov eax, 0 00000000004015AD add rsp, 30h 00000000004015B1 pop rbp 00000000004015B2 retn //x64_clang对应汇编代码讲解 0000000140001000 sub rsp, 20h 0000000140001004 xor eax, eax 0000000140001006 mov dword ptr [rsp+1Ch], 0 000000014000100E mov [rsp+10h], rdx 0000000140001013 mov [rsp+0Ch], ecx 0000000140001017 mov ecx, [rsp+0Ch] ;ecx=argc 000000014000101B mov [rsp+8], ecx ;n1=argc 000000014000101F mov ecx, [rsp+0Ch] ;ecx=argc 0000000140001023 mov [rsp+4], ecx ;n2=argc 0000000140001027 mov ecx, [rsp+8] ;ecx=n1 000000014000102B mov r8d, ecx 000000014000102E add r8d, 1 0000000140001032 mov [rsp+8], r8d ;n1+=1 0000000140001037 add ecx, 5 000000014000103A mov [rsp+4], ecx ;n2=5+n1,n1为n1+=1之前的值 000000014000103E mov ecx, [rsp+8] 0000000140001042 add ecx, 1 0000000140001045 mov [rsp+8], ecx ;n1+=1 0000000140001049 add ecx, 5 000000014000104C mov [rsp+4], ecx ;n2=5+n1,n1为n1+=1之后的值 0000000140001050 mov ecx, [rsp+4] 0000000140001054 mov r8d, ecx 0000000140001057 add r8d, 0FFFFFFFFh ;r8d=n2+-1=n2-1 000000014000105B mov [rsp+4], r8d ;n2-=1 0000000140001060 add ecx, 5 0000000140001063 mov [rsp+8], ecx ;n1=5+n2,n2为n2-=1之前的值 0000000140001067 mov ecx, [rsp+4] 000000014000106B add ecx, 0FFFFFFFFh ;ecx=n2+-1=n2-1 000000014000106E mov [rsp+4], ecx ;n2-=1 0000000140001072 add ecx, 5 0000000140001075 mov [rsp+8], ecx ;n1=5+n2,n2为n2-=1之后的值 0000000140001079 add rsp, 20h 000000014000107D retn
从代码清单4-15中可以看出,先将自增自减运算进行分离,然后根据运算符的位置来决定执行顺序。将原语句块“n1=5+(n1++);”分解为“n2=5+n1;”和“n1=n1+1”,这样就实现了先参与语句块运算,再自增1。同理,前缀++的拆分过程只是执行顺序做了替换,先将自身加1,再参与表达式运算。在识别过程中,后缀++必然会保存计算前的变量值,在表达式计算完成后,才取出之前的值加1,这是个显著特点。