C++与汇编——四则运算

表达式求值

算数运算和赋值

算术运算又称四则运算,计算机中的四则运算和数学上的有些不同。

赋值

赋值运算类似于数学中的“等于”,是将一个内存空间中的数据传递到另一个内存空间中。该操作必须经过处理器访问并中转,以实现两个单元的数据传输。

在C++中,算数运算与其他传递计算结果的代码组合后才被视为一条有效的语句,因为只进行计算而没有传递结果的运算不会对程序结果产生任何影响,编译器将忽略该语句。如

6+4;
int tmp = 0;

对应的汇编语句为

00401020 mov dword ptr [epb-4], 0

加法

加法运算对应的汇编指令为ADD,在执行加法运算时,针对不同类型的操作数,转换的指令也会有所不同。在Release选项下编译,执行效率最快,在Debug选项下编译更具有可读性。

语法:ADD DEST, SRCADC DEST,SRC

ADD是将源操作数与目的操作数相加,结果传送到目的操作数。ADC是将源操作数与目的操作数以及CF(低位进位)值相加,结果传送到目的操作数。   

源操作数可以是通用寄存器、存储器或立即数。目的操作数可以是通用寄存器或存储器操作数。 ADD,ADC指令影响标志位为OF,SF,ZF,AF,PF,CF。

Debug

在两常量相加的情况下,编译器在编译期间就计算出两常量相加后的结果,将这个结果值作为立即数参与运算。当有变量参与加法运算时,会先取出内存中的数据,放入通用寄存器中,再通过加法指令来完成计算过程得到结果,最后放回到变量所占用的内存空间中。

Release

开启优化选项后,由于效率优先,编译器会将无用的代码取出,并将可合并的代码进行归并处理。同时,编译器常常会采用“常量传播”和“常量折叠”两种方法对代码中的常量与变量进行优化

  • 常量传播:将编译期间可以计算出结果的变量转换为常量
  • 常量折叠:当计算公式中出现多个常量进行计算的情况,且编译器可以在编译期间计算出结果时,源码中所有的常量计算都将被计算结果代替。

减法

减法运算对应于汇编指令SUB,计算机通过补码将减法转换为加法的形式完成计算。

语法:SUB DEST, SRCSBB DEST,SRC

SUB将目的操作数减源操作数,结果送目的操作数。SBB将目的操作数减源操作数,还要减CF(低位借位)值,结果送目的操作数。   

源操作数可以是通用寄存器、存储器或立即数。目的操作数可以是通用寄存器或存储器操作数。 SUB,SBB指令影响标志位为OF,SF,ZF,AF,PF,CF。

在源代码中,当加数为负数的时候,执行的并非是加法而是减法操作

在优化的选项下,优化策略与加法一致

乘法

乘法运算对应的汇编指令为有符号的imul和无符号的mul两种,由于乘法指令的执行周期较长,编译器会先尝试将乘法转换成加法或移位等周期较短的指令。

  • 当乘数常量为2的幂时,编译器会执行左移运算shl来代替乘法指令
  • 当有符号变量乘以常量值,且常量非2的幂时,会直接使用imul指令
  • 当乘数与被乘数都是未知变量时,直接使用乘法指令完成计算

语法:

  • MUL SRCIMUL SRC
    • MUL为无符号数乘法指令,IMUL为带符号数乘法指令。源操作数为通用寄存器或存储器操作数。目的操作数缺省存放在ACC(AL,AX,EAX)中,乘积存AX,DX:AX,EDX:EAX中。   
    • 字节乘:AL SRC→AX   
    • 字乘:AX SRC→DX∶AX   
    • 双字乘:EAX SRC→EDX∶EAX  
    • MUL,IMUL指令执行后,CF=OF=0,表示乘积高位无有效数据;CF=OF=1表示乘积高位含有效数据,对其它标志位无定义。
  • IMUL DEST,SRC
    • 将目的操作数乘以源操作数,结果送目的操作数。目的操作数为16位或32位通用寄存器或存储器操作数。源操作数为16位或32位通用寄存器、存储器或立即数。   
    • 源操作数和目的操作数数据类型要求一致。乘积仅取和目的操作数相同的位数,高位部分将被舍去,并且CF=OF=1。其它标志位无定义。
  • IMUL DEST,SRC1,SRC2
    • 将源操作数SRC1与源操作数SRC2相乘,结果送目的操作数。目的操作数DEST为16位或32位,允许为通用寄存器。源操作数SRC1为16位或32位通用寄存器或存储器操作数。源操作数SRC2允许为立即数。

Debug

由于在Debug环境下侧重调试而非优化,遇到复杂四则运算时,编译器会先直接拆分,然后再进行计算。计算的过程与数学四则运算无异

Release

Release设置下,编译器会将计算步骤分解并优化,如:

  • 将常量乘数分解为2的倍数+自身,如3=2+1,15 = 3*5 = (2+1)*5 = (2+1)*(4+1)
  • 两常量相乘直接转换为常量值
  • 两变量相乘,无优化

除法

除法运算对应的汇编指令为有符号的idiv和无符号的div两种。除法指令的执行周期长,效率较低,编译器将尽可能避免使用除法指令。在C++中,对整数除法的定义为向0取整

语法:

  • DIV SRCIDIV SRC
    • DIV为无符号数除法,IDIV为带符号数除法。源操作数作为除数,为通用寄存器或存储器操作数。被除数缺省在目的操作数AX,DX:AX,EDX:EAX中。   

    • 字节除法:AX/SRC商→AL,余数→AH   

    • 字除法:DX·AX/SRC商→AX,余数→DX   

    • 双字除法:EDX·EAX/SRC商→EAX,余数→EDX   

    • 由于被除数必须是除数的双倍字长,一般应使用扩展指令进行高位扩展。当进行无符号数除法时,被除数高位按0扩展为双倍除数字长。当进行有符号数除法时,被除数以补码表示。可使用扩展指令CBW,CWD,CWDE,CDQ进行高位扩展。

    • 例如

    MOV AX,BLOCK   
    CWD;被除数高位扩展
    MOV BX,1000H;  
    IDIV BX;
    

    对于带符号除法,其商和余数均采用补码形式表示,余数与被除数同符号。当除数为零或商超过了规定数据类型所能表示的范围时,将会出现溢出现象,产生一个中断类型码为“0”的中断。执行除法指令后标志位无定义。

Debug

如果除数是变量,则只使用除法指令,如果除数为常量

  • 除数为2的幂,则使用右移指令sar
  • 除数非2的幂,使用IDIV

Release

  • 当除数为2的n次幂时,使用符号位扩展来避免分支结构。由于C++的除法结果是向0取整,因此当商为负数的时候要进行+1处理。编译器使用CDQ指令进行符号位扩展
    • EAX寄存器数据的最高位为1,那么EDX的值就置为-1(0XFFFFFFFF),否则为0。
    • 将除数与EDX进行与运算——计算机计算负数除法的方法为x/2^n = (x+2^n-1)/2^n,计算正数时不需要特殊处理
    • EAX减去EDX(注意,商存储于EAX中)
    • SAR右移完成除法
  • 当除数不为2的n次幂时,使用一个“幻数”来将除法化简为乘法与其他指令。思路为:x / o = x * 1/o = x * 2^n/(2^n) * o = x * 2^n/o * 1/2^n。由于n为常量,且2^n的取值由编译器取值,因此2^n/o的值可以在编译期间计算得出。2^n/o被称为“幻数”。
  • 进一步推导,设c = 2^n/o,则有x/o = x*c/2^n -> (x*c)向0位移取整等价于n,得到了各数的关系
  • 当遇到以下指令序列时,基本可判定是除法优化后的代码,其除法原型为变量a除以常量。imu的操作数是优化前的被除数a。接下来统计右移从次数n,然后利用公式a = 2^n/Magic Number可以还原出高级代码中的除数o
    mov eax, MagicNumber;
    imul ...;乘以乘数
    sar edx, ...;符号移位
    mov reg, edx;
    shr reg, 1Fh(不同编译器可能值不同);无符号移位
    add edx, reg;
    ;此后直接使用edx的值