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

第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。

在数学描述中,106-2表示对x向下取整。

在标准C语言的math.h中定义了floor函数,其作用就是向下取整,也有人称之为地板取整。

向下取整的除法,当除数为2的幂时,可以直接用带符号右移指令(sar)完成。但是,向下取整存在一个问题:

可能是因为这个问题,C语言的整数除法没有使用floor方式。

b. 向上取整

所谓对x向上取整,就是取得在+∞方向最接近x的整数值,换言之,就是取得不小于x的最小整数。

例如,+3.5向上取整得到4;-3.5向上取整得到-3。

在我们的数学描述中,106-4表示对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为实数,有

108-1

进而可推导:

x不为整数时:

定理2:x为整数,则

108-4

定理3:由于上下取整相对于0点是对称的,所以

108-5

进而可推导:

定理4:x为实数,n为整数,有

结合定理1,可推导:

g108-8,若x<n,则108-9;若108-10,则108-11,因108-12,可得x<n

推导6:有整数ab和实数xabb≠0,有

  • 108-13,则108-14
  • 108-15,则108-16

证明:设qa÷b的商,r为余数(0≤|r|<|b|,且qr均为整数),则

109-2,可得0≤x·|b|<1;

因0≤|r|<|b|,可得109-3

因|r|+1≤|b|,且|bx<1,结合上式可得0≤|r|+|bx<|r|+1≤|b|;

因0≤|r|+|bx<|b|,故109-4

rb同号时,109-5

r>0,b<0,109-6时,

-1<bx<0,r+1≤-br+bx<-b

得到0<r+bx<-b109-7

109-8,得109-9

r<0,b>0,109-10时,

0≤bx<1,r+bx<0,-b<r+bx

得到-b<r+bx<0,109-11

109-12,由此得109-13

由于109-14,且已证109-15,可得

110-1

同理可证:当110-2时,则110-3

推导7:设有ab两整数,当b>0时,有

b<0时,有

证明1:qa÷b的商,r为余数,

根据定理4,有

b>0,|r|<b,有

因此得110-9

同理可证110-10,过程略。

证明2:qa÷b的商,r为余数,

根据定理4,有

b<0,|r|<|b|,有

111-2

r>0时,111-3111-4

r=0时,111-5

r<0时,111-6

得到:111-7,故:

代入得:

同理可证111-10,过程略。

(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,相当于向下取整。

对于113-1,当x≥0时,有:

举例说明一下,比如x为4,113-3等价于4>>1,结果为2;但当x为5呢?113-4处理为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的取值由编译器选择,所以115-2的值在编译期间可以计算出来。对于n的取值,在32位除法中通常都会大于等于32,在64位除法中通常都会大于等于64。这样就可以直接调整使用乘法结果的高位了。

如此一来,115-3就是一个编译期间先行计算出来的常量值了,这个值常被称为Magic Number(魔数、幻数),我们先用M代替115-4这个Magic常量,于是又有:

简单证明如下:

115-6xc皆为整数,c为正数,当x≥0时,有:

115-7

反推过程:

(eax×argc)>>33

等价于:

115-8

由此得:

116-1

解方程得:116-2(注:此处的“约等于”在后面讨论除法优化原则处详细解释)。

于是,我们反推出优化前的高级代码如下:

argc/3

总结

当遇到数学优化公式116-3,且使用mul无符号乘法时,基本可判定为除法优化后的代码,其除法原型为x除以常量c,其操作数是优化前的被除数x,接下来统计右移次数以确定公式中的n值,然后使用公式116-4将魔数作为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处疑似118-1,看后面的代码,不符合前面例子得出的结论,所以不能使用前面推导的公式。接着一边看后面的指令,一边先写出等价于上述步骤的数学表达式。

M为常量24924925h,以上代码等价于:

0040101B处的sub ecx,edx直接减去乘法结果的高32位,数学表达式等价于118-2

其后shr ecx,1相当于除以2:118-3

其后add ecx,edx再次加上乘法结果的高32位:118-4

其后shr ecx,2等价于把加法的结果再次除以4:118-5

最后直接使用ecx,乘法结果低32位ecx弃而不用。

先简化表达式:

至此,我们可以看出除法优化的原型,232+M是带进位值的魔数。编译器作者在实现除法优化的过程中,通过计算得到的魔数超过了4字节整数范围,为了避免大数运算的开销,对此做了数学转换,于是得到最开始的表达式,规避了所有的大数计算问题。

若有119-3n=3,x为ecx,则有:

数数看一共移动了几次。ecx一共右移了3位,因为是直接与edx运算并作为结果,所以还要加上乘法的低32位,共计35位,最终n的取值为3。已知M值为常量24924925h,根据上述推导可得:

解方程求c

于是,我们反推出优化前的高级代码为:

argc/7

在计算魔数后,如果值超出4字节整数的表达范围,编译器会对其进行调整。如上例中的argc/7,在计算魔数时,编译器选择119-7,但是其结果超出了4字节整数的表达范围,所以编译器调整魔数的取值为120-1,导致整个除法的推导也随之产生变化。

综上所证,可推导出除法等价优化公式如下:

120-2

总结

当遇到数学优化公式120-4时,基本可判定其是除法优化后的代码,除法原型为x除以常量c,mul可表明是无符号计算,操作数是优化前的被除数x,接下来统计右移的总次数以确定公式中的n值,然后使用公式120-5将魔数作为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,相当于向下取整。

对于122-1,当x≥0时,有:

122-2

x<0时,有:

122-3

根据推导7可得:

例如:122-5

总结

当遇到数学优化公式:如果x≥0,则122-6,如果x<0,则时122-7,基本可判定是除法优化后的代码,根据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。简单证明如下:

125-1xc皆为整数,c为正数,当x≥0时,有:

125-2

x < 0时,根据推导3:

125-3

反推过程:

(eax×arg c)>> 33

等价于:

125-4

由此得:

125-5

解方程得:

于是,我们反推出优化前的高级代码为:

argc/9

总结

当遇到数学优化公式:如果x≥0,则126-1;如果x<0,则126-2时,基本可判定是除法优化后的代码,其除法原型为x除以常量c,imul可表明是有符号计算,其操作数是优化前的被除数x,接下来统计右移的总次数以确定公式中的n值,然后使用公式126-3,将魔数作为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],这是个有符号数的乘法。请注意,编译器在计算魔数时是作为无符号数处理的,而代入到除法转换乘法的代码里又是作为有符号乘数处理的。因为有符号数的最高位不是数值,而是符号位,所以,对应的有符号乘法指令是不会让最高位参与数值计算的,这样会导致乘数的数学意义和魔数不一致。

于是,在有符号乘法中,如果128-1的取值大于0x80000000(最高位为1,补码形式为负数),实际参与乘法计算的是个负数,其值应等价于128-2,证明如下:

设有符号整数x、无符号整数yy≥0x80000000,我们定义y的有符号补码值表示为y(补),y的无符号值表示为y(无)。比如当y=0xffffffff时,y(补)的真值为-1,y(无)=232-1。

xy进行有符号乘法,根据求补计算规则可推出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、无符号整数yy≥0x80000000,当xy进行有符号乘法计算时,其结果等于x·[y(无)-232],若期望的乘法结果为x·y(无),则需要调整为:

x·y(无)=x·[y(无)-232]+232·x=x·y(补)+232·x

对于前面例题中的129-1在计算机补码中表示为负数,根据以上推导,129-2等价于129-3,故其除法等价优化公式也相应调整:

完全理解以上证明后,我们回过头来分析代码清单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等价于129-5,然后加上被除数argc,对edx右移两位,负数调整后直接使用edx中的值,那么舍弃了低32位,相当于一共右移了34位,于是可推导原来的除数如下。

将以上代码转换为公式:

上式等价于129-7c为某常量,eax为魔数,arg c为被除数。

解方程得:

于是,我们反推出优化前的高级代码为:

argc / 7

注意,这里的arg c是有符号整型,因为指令中使用的是imul有符号乘法指令。

总结

当遇到数学优化公式:如果x≥0,则130-1;如果x<0,则130-2时,基本可判定是除法优化后的代码,其除法原型为x除以常量c,imul表明是有符号计算,其操作数是优化前的被除数x,接下来统计右移的总次数以确定公式中的n值,然后使用公式130-3,将魔数作为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的出现很合理132-1。证明过程可参考除数为正的2的幂的示例,这里不再赘述。

总结

当遇到数学优化公式:如果x≥0,则132-2;如果x < 0,则132-3时,基本可判定是除法优化后的代码,根据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编译器采用求正数的除法,再对结果求补就可以算出除数为负的结果,即134-1。对于非求补除数为负的求值过程,有什么需要注意的呢?我们先看除法转乘法的过程。

c为正数时,设134-2,有:

134-3

c为负数时,有:

我们再来看编译器产生的代码。eax是魔数,argc为被除数,根据代码体现以下表达式:

135-2

下面我们介绍一个分析编译器行为的好方法。步骤很简单,先写出高级语言,然后看对应的汇编代码,接着论证数学模型,最后归纳出还原的办法和依据。在这个例子中,我们出于研究目的,分析自己写的代码,所以对应的运算是已知的。

135-3

eax保存了魔的数值,据上式可得:

接下来,我们求解|c|:

分析以上代码时,很容易与除数为正的情况混淆,我们先看这两者之间的重要差异。关键在于上述代码中魔数的值。在前面讨论的除以7的例子中,当魔数最高位为1时,对于正除数,编译器会在imul和sar之间产生调整作用的add指令,而本例没有,故结合上下流程可分析魔数为补码形式,除数为负。这点应作为区分负除数的重要依据。

于是,我们反推出优化前的高级代码如下。

argc / -5

总结

当遇到数学优化公式:如果x≥0,则135-6;如果x<0,则135-7时,则基本可判定是除法优化后的代码,其除法原型为x除以常量c,imul可表明是有符号计算,其操作数是优化前的被除数x。由于魔数取值大于7fffffffh,而imul和sar之间未见任何调整代码,故可认定除数为负,且魔数为补码形式,接下来统计右移的总次数,以确定公式中的n值,然后使用公式135-8,将魔数作为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的公式做正数的除法,再对结果求补,就可以算出除数为负的结果,即138-1。对于非求补除数为负的求值过程,回忆前面除数等于+7的讨论,对于正除数,魔数大于0x7fffffff的处理如下。

魔数为138-3

当除数c为负数时,我们可以直接对上式的魔数取负,设魔数为M

对应调整除法转换公式:

理解以上推导后,可先将以上代码转换为公式:

由上式可得:

接下来,我们求解|c|:

于是,我们反推出优化前的高级代码为:

argc / -7

总结

当遇到数学优化公式:如果x≥0,则139-2,如果x<0,则139-3,可判定是除法优化后的汇编代码,其除法原型为x除以常量c,imul可表明是有符号计算,其操作数是优化前的被除数x。由于魔数取值小于等于7fffffffh,而imul和sar之间有sub指令调整乘积,故可认定除数为负,且魔数为补码形式,接下来统计右移的总次数以确定公式中的n值,然后使用公式139-4将魔数作为M值代入公式求解常量除数|c|,即可恢复除法原型。

j. 除法优化的原则

看到这里,大家应该注意到了,在以上讨论中,还原所得的除数是近似值,说明给出的公式还不够严格。我们接下来可以好好思考一下其值近似但不等的原因,先看看余数是多少。

回忆一下除法和余数的关系,根据性质3,有:

b=(a-r)÷q

代入139-5,公式改为139-6

以除以9为例。

139-7

解方程求r

r=38E38E39h×9−233=200000001h−200000000h=1

139-8

于是找到不等于的原因:139-9,这里的139-10是存在错误的,魔数是整数值,而139-11是实数值。

于是修改推导公式为:

140-1

现在又出现了“新问题”,在反汇编代码流程中,还原的公式为:

(eax * arg c)>> 33

等价于:

140-2

38E38E39h是140-3的值,代入得到:

140-4

看起来前面的步骤都前功尽弃了。

现在我们来解决这个问题,当x≥0时,根据C语言除法向零取整规则,有:

证明:

在编译器计算魔数时,如果给出一个合适的n值,使下式成立:

140-7

则根据推导6可得:

140-8

举例说明一下,以前面讨论的arg c/9为例,设arg c/9商为q

140-9

当arg c≥0时,有

显然arg c<233,于是得到140-11,根据推导6可以得到:

x<0时,有:

证明:

根据推导3,

根据推导7,

在编译器计算魔数时,如果给出一个合适的n值,使下式成立:

141-5

则根据推导6可得:

141-6

举例说明一下,以前面讨论的arg c/9为例,设arg c/9商为q

141-7

当arg c<0时,有

根据推导7:

显然141-10,根据推导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,这是个显著特点。