跳转至

Arithmetic and Logic Instructions⚓︎

8370 个字 206 行代码 预计阅读时间 44 分钟

Addition, Subtraction and Comparison⚓︎

任何微处理器中的算术指令主要包括加法、减法和比较。

Addition⚓︎

  • 加法ADD)在微处理器中以多种形式出现
  • 唯二不允许的加法类型是内存到内存,以及段寄存器

    • 段寄存器只能移动、压栈或弹出
  • 当算术和逻辑指令执行时,标志寄存器的内容会改变

    • 中断、陷阱和其他标志位不会因此改变
    • 发生改变的是符号、零、进位、辅助进位、奇偶校验和溢出标志位
  • 立即数加法:涉及到常量和已知数据的加法

    MOV DL, 12H
    ADD DL, 33H
    
  • 内存 - 寄存器加法

    • 将参与加法的内存数据移动到 AL(或别的)寄存器中
    • 示例:将存储在数据段偏移位置的 NUMB 的两个连续字节的数据相加,结果存储在 AL 寄存器中

      0000 BF 0000 R      MOV DI, OFFSET NUMB        ; address NUMB
      0003 BO 00          MOV AL, O ¡clear sum
      0005 02 05          ADD AL, [DI]               ; add NUMB
      0007 02 45 01       ADD AL, [DI+1]             ; ada NUMB + 1
      
  • 数组加法

    • 内存数组是连续的数据列表
    • 假设一个数据数组(ARRAY)包含 10 字节,从元素 0 编号到元素 9;下面展示了如何将数组元素 35 7 的内容相加:

      ARRAY DB 0, 1, 3, 2, ...
      ...
      MOV AL, O                      ; clear sum
      MOV SI, 3                      ; address element 3
      ADD AL, ARRAY [SI]             ; add element 3
      ADD AL, ARRAY [SI + 2]         ; add element 5
      ADD AL, ARRAY [SI + 4]         ; ada element 7
      
    • 假设一个数据数组包含数字的字,用于在寄存器 AX 中形成 16 位的和;下面的指令序列展示了用比例变址寻址添加名为 ARRAY 的内存区域中的元素 35 7

      ARRAY DW 0, 1, 3, 2, ...
      ...
      MOV EBX, OFFSET ARRAY         ; address ARRAY
      MOV ECX, 3                    ; address element 3
      MOV AX, [EBX + 2 * ECX]       ; get element 3
      MOV ECX, 5                    ; address element 5
      ADD AX, [EBX + 2 * ECX]       ; add element 5
      MOV ECX, 7                    ; address element 7
      ADD AX, [EBX + 2 * ECX]       ; add element 7
      
      • EBX 加载了 ARRAY 的地址
      • ECX 保存了数组元素编号
      • 比例因子用于将 ECX 寄存器的内容乘以 2 以定位数据字
  • 递增(increment) 指令(INC)将 1 加到任何寄存器或内存位置(段寄存器除外)

    • 保持 CF(进位)标志的状态不变

      MOV AX, 0FFFFh
      INC AX          ; 40 (opcode)
                      ; CF = 0
      
      MOV AX, 0FFFFh
      ADD AX, 1       ; 05 01 00
                      ; CF = 1
      
    • 使用间接内存递增时,数据的大小必须通过使用BYTE PTR , WORD PTRDWORD PTR 指令来描述

    • 汇编器无法确定 INC [DI] 是字节大小、字大小还是双字大小的递增

      汇编语言 操作
      INC BL BL = BL + 1
      INC SP SP = SP + 1
      INC EAX EAX = EAX + 1
      INC BYTE PTR[BX] BX 所寻址的数据段内存位置的字节内容加 1
      INC WORD PTR[SI] SI 所寻址的数据段内存位置的字内容加 1
      INC DWORD PTR[ECX] ECX 所寻址的数据段内存位置的双字内容加 1
      INC DATA1 对数据段内存位置 DATA1 的内容加 1
      INC RCX RCX 164 位模式)
  • ADC(带进位加法 (add-with-carry))将进位标志(C)中的位加到操作数数据上

    • 主要出现在 80386 Core2 中添加宽于 16 32 位数字的软件中
    • ADD 类似,ADC 在加法后会影响标志位
    • 下图展示了该指令的操作细节:

    例子

    使用 ADC 计算两个双字长的长整数之和。

    ; [int1][0:7] = [int1][O:7] + [int2][0:7]
    .data
    int1  DD 1,0,0,0,0,0,0,1       
    int2  DD 1,0,0,0,0,0,0,1
    
    .code
          MOV EDI, OFFSET int1
          MOV ESI, OFFSET int2
          MOV ECX, 8              ; ECX = 8
          CLC                     ; start out with carry flag cleared
    loop: MOV EAX, [ESI]
          ADC [EDI], EAX          ; with carry from previous loop pass
          LEA ESI, [ESI+4]        ; point to next source
          LEA EDI, [EDI+4]        ; point to next destination
          DEC ECX                 ; adjust loop count
          JNZ loop                ; if ECX > 0 then repeat
    

  • ADC 的变体:ADCXADOX

    • ADDADC 指令可用于加速大整数算数运算,通过形如右侧的代码序列实现

      • 问题:这些指令创建了一个依赖链(dependency chain),因此处理器无法并行执行算术运算
    • 为了解决这个问题,Intel 增加了第二条进位链,使得两个独立的进位链可同时发生;它们对应 ADC 的两个变体 ADCXADOX

    • 这两个变体不会相互影响,因为它们有单独的进位标志
      • ADCX 使用进位标志,并保留其他标志不变
      • ADOX 使用溢出标志,并保留其他标志不变
    例子

    • ADCXADOXMULX 指令为大型整数乘法带来了很大便利

      • 而大型整数算术在密码学(例如 RSA 公钥算法)和高性能计算中有许多应用场景

  • XADD交换和加(exchange and add))

    • 首次出现在 80486 中,并一直延续到 Core2
    • 语法:XADD des, src,对应操作如下:

      • 交换 des 操作数与 src 操作数
      • 将两个值的和加载到 des 操作数中:des = src + des
    • 是少数改变源操作数的指令之一

    • 例子:

      MOV AX, 1000H
      MOV BX, 2000H    ; AX = 1000H, BX = 2000H
      XADD AX, BX      ; AX = 3000H, BX = 1000H
      
    • 目标操作数可以是寄存器或内存位置,而源操作数只能是寄存器

    • 对于多处理器系统XADD 可以与 LOCK 前缀在多处理器系统中结合使用,以允许多个处理器执行一个 DO 循环
    • int atomic_xadd(atomic_t *v, int inc)
      • XADD 将给定的增量 inc 加到 *v 上,并原子性地返回 *v 的先前值
      • XADD 在原子值 *v 上执行原子交换和加法操作
      • 当多个 CPU 在线时,XADD 会被锁住
    • XADD 可以实现共享计数器和各种数据结构
    • XADD 可用于乐观锁(optimistic locking),主要适用于高并发系统中

      例子

      以下指令使用乐观锁来安全地通过多个线程更新共享版本。

      .data
      version DD 0                 ; shared version number initialized to 0
      
      .code
              MOV ECX, version     ; load the current value of version
              ......               ; working optimistically
              MOV EAX, 1           ; EAX = 1
              XADD version, EAX    ; version ⇔ EAX, version = version+1
              CMP EAX, ECX         ; check if the value was modified by another thread
              JNE retry            ; if version was updated then rollback
              ......
      retry: ......                ; handle the conflict
      

Subtraction⚓︎

  • 指令集中有许多形式的减法(SUB)指令的

    • 使用任何寻址模式,带有 8 位、16 位或 32 位数据
  • 寄存器加法:每次执行减法运算时,微处理器修改标志寄存器的内容

  • 立即数加法:允许常量数据作为减法中的立即操作数
  • DEC递减(decrement):从任何寄存器或内存位置减去 1

    • 影响除 CF 外的所有标志位
    • CMP 这样的指令会将所有标志位更新为执行结果,但#!asm INCDEC 会写入除 CF 之外的标志位
    • 因此,如果 JCC 直接使用来自INC / DEC的标志位,JCC 可能会因为意外指令而产生错误依赖,例如:

    • 所以编译器通常不会生成用于循环计数更新,或重用 JCC 上产生的INC / DEC的标志位

  • 带借位的减法(subtract-with-borrow) 指令(SBB

    • 功能与普通减法相同,但包含借位的进位标志(C)也要从差值中减去
    • 常用于(在 80386 - Core 2)执行在超过 16 位或 32 位的数字上的减法
    • 宽减法需要借位来传播,就像宽加法传播进位一样
    • 下面展示该指令的运算细节:

Comparison⚓︎

比较(comparison) 指令(CMP)是一种只改变标志位,而不会改变目标操作数的减法,用于检查寄存器或内存位置的内容与另一个值是否相等。后面通常会跟一个条件跳转指令,用于测试标志位的条件。

汇编语言 操作
CMP CL, BL CL - BL
CMP AX, SP AX - SP
CMP EBP, ESI EBP - ESI
CMP RDI, RSI RDI - RSI64 位模式)
CMP AX, 2000H AX - 2000H
  • 比较和交换指令(CMPXCHG)比较目的操作数累加器(accumulator)(隐式操作数 (implicit operand)

    • 例子:CMPXCHG des, src (AL/AX/EAX)

      • 如果 des == accu,则 des = srcZF = 1
      • 如果 des != accu,则 accu = desZF = 0
      • EFLAGS 中的 ZF 位相应地被赋值
    • 仅存在于 80486 - Core2 指令集

    • 指令功能与 8 位、16 位或 32 位数据相关
    • ZF 标志在目标操作数和寄存器 ALAXEAX 中的值相等时被设置,否则被清除

      • 案例 1

        • 执行前:(CX)=00FFH, (DX)=00EFH, (AX)=00FFH
        • 执行后:(CX)=00EFH, (DX)=00EFH, (AX)=00FFH, ZF=1
      • 案例 2

        • 执行前:(CX)=00FFH, (DX)=00EFH, (AX)=00EEH
        • 执行后:(CX)=00FFH, (DX)=00EFH, (AX)=00FFH, ZF=0
    • int atomic_cmpxchg(atomic_t *v, int new, int old)

      • 此函数使用给定的旧值和新值,在原子值 v 上执行原子比较交换操作,返回操作前原子变量 v 的旧值
      • 它在操作周围提供显式的内存屏障
      • 例子CMPXCHG CX, DX (AX)(三个寄存器分别表示原子值、新值和旧值)
        • CX == AXDX 被拷贝到 CX
        • CX != AXCX 被拷贝到 AX
        • AX 保留执行前 CX 的值
    例子

    • 顺序栈在可能存在竞态条件的并发系统中将无法工作
    • 自旋锁和读写锁可以帮助将锁从一个持有者转移到另一个持有者,但成本高

    我们使用 cmpxchg 函数解决这一问题:

  • CMPXCHG8B 指令:比较并交换八个字节

    • 语法:CMPXCHG8B [mem64-operand]

      • ECX:EBX64 位新值(隐式操作数)
      • EDX:EAX64 位旧值(隐式操作数)
      • 如果操作数 == EDX:EAX,则操作数 = ECX:EBXZF = 1,否则EDX:EAX = 操作数,ZF = 0Z 标志位表示比较后的值相等
      例子
      MOV EAX, [mem]                    ; low 32 bits
      MOV EDX, [mem + 4]                ; high 32 bits
      MOV EBX, new_low                  ; low 32 bits of the new value
      MOV ECX, new_high                 ; high 32 bits of the new value
      LOCK CMPXCHG8B QWORD PTR [mem]
      JZ SUCCESS                        ; if ZF = 1,then goto success
      
    • CMPXCHG8B 通常在多处理器环境中与 LOCK 前缀一起使用,以确保原子操作

  • CMPXCHG16B:将 RDX:RAX 中的 128 位值与内存中位于目的操作数的 128 位数字进行比较

    • 如果值相等,则将 RCX:RBX 中的 128 位值存储到目的操作数中,否则目的操作数中的值将加载到 RDX:RAX
    • CMPXCHG16B 要求目的(内存)操作数应为 16 字节对齐

Multiplication and Division⚓︎

Multiplication⚓︎

  • 早期的 8 位微处理器不能使用乘法或除法程序,只能通过一系列移位和加减运算来实现
    • 后来,制造商们意识到了这种不足,于是他们将乘法和除法纳入了最新的微处理器指令集中
  • Pentium – Core2 包含特殊电路,可以在最少的时钟周期内完成乘法

    • 而早期的处理器执行乘法需要超过 40 个时钟周期
  • 乘法在字节、字或双字上执行

    • 有无符号整数(MUL)和符号整数(IMUL)两个版本
    • 对于 MUL,乘数始终在AL / AX / EAX寄存器中作为隐式操作数,例如 MUL CL ; AX = AL*CL
  • 积始终是双宽度积(double-width product)

    • 两个 8 位数字相乘生成一个 16 位积;两个 16 位数字生成一个 32 位积;两个 32 位数字生成一个 64 位积
    • Pentium 4 64 位模式下,两个 64 位数字相乘生成一个 128 位积
  • 8 位乘法

    • 使用 8 位乘法时,乘数始终在 AL 寄存器中,符号 / 无符号整数
      • 乘数可以是任何 8 位寄存器或内存位置
      • 使用内存操作数时需要指令来指示操作数大小,例如 MUL WORD PTR [BX]
    • 没有立即数乘法的形式(形如 MUL 12H,除非是 IMUL 乘法的两个或三个操作数形式
    • 乘法完成后,积放在 AX
    • 下面列举一些 8 位乘法指令:

      汇编语言 操作
      MUL CL AL 乘以 CL;无符号积在 AX
      IMUL DH AL 乘以 DH;符号积在 AX
      IMUL BYTE PTR[BX] AL 乘以 BX 所寻址的数据段内存位置的字节内容;符号积在 AX
      MUL TEMP AL 乘以内存位置 TEMP 的字节内容;无符号积在 AX
  • 16 位乘法

    • 此时乘数放在 AX
    • 32 位积放在 DX-AX
      • DX 包含积的高 16
      • AX 包含积的低 16
    • 8 位乘法一样,乘数的选取取决于程序员
  • 32 位乘法

    • 80386 及以上版本中可以实现 32 位乘法,因为这些微处理器包含 32 位寄存器
    • EAX 寄存器的内容与指令指定的操作数相乘
    • 64 位积位于 EDX-EAX 中,其中 EAX 包含积的低 32
  • 64 位乘法

    • Pentium 4 中,64 位乘法的结果以 128 位积的形式出现在 RDX:RAX 寄存器对中
    • 尽管这种大小的乘法相对较少见,但 Pentium 4 Core 2 可以同时对符号和无符号数进行此操作
  • IMUL 指令有三种形式:

    • 单操作数形式:这种形式与 MUL 指令使用的相同
    • 双操作数形式:目的操作数是一个寄存器,源操作数是一个立即值、一个寄存器或一个内存位置,中间乘积(输入操作数大小的两倍)被截断并存储在目的操作数位置

      IMUL ECX, [EAX+4]    ; ECX = ECX * [EAX+4]
      IMUL ECX, 16         ; ECX = ECX * 16
      
    • 三操作数形式:第一个源操作数乘以第二个源操作数,中间乘积被截断并存储在目的操作数

      IMUL ECX, [EAX+4], 5ECX = [EAX+4] * 5
      
  • 一种特殊的立即数乘法

    • 在双操作数或三操作数形式的 IMUL 指令中,源操作数可以是一个立即值
    • 当使用立即值作为操作数时,它会被符号扩展到目标操作数格式的长度
    • 比如 IMUL CX, DX, 12H 指令将 12H 乘以 DX,并将一个 16 位符号积留在 CX
    • 注意,双操作数形式会被汇编为三操作数以支持立即数乘法,例如,IMUL BX, 16H -> IMUL BX, BX, 16H
  • MULIMUL 的差别:前者用零扩展填充高位,而后者用符号扩展填充高位

  • MUL 影响的标志

    • 当积完全位于积的下寄存器内时,MUL 指令清除 OFCF 标志,否则 OFCF 标志被设置

    • CFOF 标志表示积的上半部分是否包含有效数字

  • IMUL 影响的标志

    • 当中间乘积的符号整数值与符号扩展操作数大小的截断乘积不同时,CFOF 标志被设置,否则 CFOF 标志被清除

    • 使用两个和三个操作数形式时,由于截断,应检查 CFOF 标志以确保没有重要的位丢失

思考

为什么 MUL 只有单操作数形式,而 IMUL 有扩展的双操作数和三操作数形式?

  • 随着编程语言和编译器的开发,Intel 发现符号整数(如 C 中的 int)的使用频率远高于无符号整数(例如,表达式 x = y * 10x = y * z
  • 因此,Intel 引入了 IMUL 的双操作数和三操作数形式,以简化在汇编级别实现高级语言乘法语义的实现

为什么 IMUL 指令的两位操作数和三位操作数形式使用截断乘法而不是扩展乘法?

– 单操作数的 MULIMUL 最初是为高精度 / 大数字算术(例如加密、哈希计算)等场景设计的,这些场景更依赖于完整的 2N 位结果,因此它们使用扩展乘法 – 在编程语言中,对于像 c = a * b (其中 abc 都是相同的 int 类型)这样的常见表达式,程序员关注的是结果的低 N 位,因此 IMUL 的双操作数和三操作数形式使用截断乘法

Division⚓︎

  • 除法发生在 8 位、16 位和 32 位数字上,具体取决于微处理器
  • 有无符号整数(DIV)或有符号整数(IDIV)版本
  • 被除数始终是双倍宽度的,除以操作数
  • 没有任何微处理器有立即数除法指令可用
  • 64 位模式下的 Pentium 4 Core 2 中,可以实现 128 位数字除以 64 位数字
  • 一个除法操作可能导致两种类型的错误:

    • 尝试除以零
    • 除法溢出(发生在一个大数除以一个小数时)
  • 在任何情况下,如果发生除法错误,微处理器都会生成一个中断,在屏幕上显示错误信息

  • 8 位除法

    • 使用 AX 存储被任何 8 位寄存器或内存位置的内容除以的被除数(dividend),即用 r/m8 除以 AX
    • 结果存储在AL := (quotient),AH := 余数(remainder)

    例子

    MOV AX, 237     ; AX = 237
    MOV CL, 11      ; CL = 11
    DIV CL          ; AH: AL = AX÷CL
                    ; AH = 6, AL = 21
    
    • 商或正或负,而余数始终取被除数的符号;这种舍入模式称为向零舍入(round-to-zero)
    • 示例:IDIV BL

      • 对于AX=10H (+16) BL=0FDH (-3)
        • 结果:商为 -5 (AL ),余数为 1 (AH )
      • 对于AX=0FFF0H (-16) BL=03H (+3)
        • 结果:商为 -5 (AL ),余数为 -1 (AH )
    • 数字通常在 8 位除法中为 8 位宽。 – 被除数必须转换为 AX 中的 16 位宽数字;对于符号和无符号数字,实现方式不同

    • 以下示例说明如何将内存位置 NUMB 的无符号字节内容除以内存位置 NUMB1 的无符号内容(即,NUMB / NUMB1

      MOV AL, NUMB        ; get NUMB
      MOV AH, 0           ; zero-extend
      DIV NUMB1           ; divide by NUMB1
      MOV ANSQ, AL        ; save quotient
      MOV ANSR, AH        ; save remainder
      
      • 注意:位置 NUMB 的内容被零扩展,以形成一个 16 位无符号数,作为被除数
  • 16 位除法

    • 此时以 DX-AX 作为被除数(32 位)
    • 80386 以上的处理器中,使用 MOVZX 指令对数字进行零扩展

  • 32 位除法

    • 80386 - Pertium 4 可在无符号或符号数上执行 32 位除法
    • EDX-EAX 中的 64 位内容除以指令指定的操作数
    • 32 位的商和余数分别在 EAXEDX

  • 64 位除法

    • 处于 64 位模式的 Pertium 4 可在无符号或符号数上执行 32 位除法
    • 使用 RDX-RAX 来保存被除数
    • 商和余数分别在 RAXRDX
  • 3 种类型的符号扩展

    • CBW / CWDE / CDQE:将AL / AX / EAX中的符号字节 / / 双字转换为符号字 / 双字 / RAX 中的四字

      指令 Op/En 64 位模式 兼容 / 传统模式 描述
      CBW ZO 有效 有效 AX := AL 的符号扩展
      CWDE ZO 有效 有效 EAX := AX 的符号扩展
      CDQE ZO 有效 不适用 RAX := EAX 的符号扩展
    • CWD / CDQ / CQO:将 RAX 的符号位拷贝到 RDX 寄存器的所有位上

      • 以下例子展示了通过 CWD 指令对 2 16 位符号数的除法

        MOV AX, - 100           ; load a -100
        MOV CX, 9               ; load +9
        CWD                     ; convert the signed 16-bit number in AX
        IDIV CX                 ; to a 32-bit signed number in DX: AX
        
    • MOVSX / MOVSXD:将源操作数(寄存器 / 内存位置)的内容通过符号扩展拷贝到目标操作数(寄存器)

      MOVSX reg16, reg/mem8       MOVSX reg32, reg/mem16
      MOVSX reg32, reg/mem8       MOVSX reg64, reg/mem16
      MOVSX reg64, reg/mem8       MOVSXD reg64, reg/mem32
      
      • MOVZX:将源操作数(寄存器 / 内存位置)的内容通过零扩展拷贝到目标操作数(寄存器)

        MOVZX reg16, reg/mem8       MOVZX reg32, reg/mem16
        MOVZX reg32, reg/mem8       MOVZX reg64, reg/mem16
        MOVZX reg64, reg/mem8
        
  • 余数

    • 在除法后,余数有以下几种处理方式:
      • 截断求商(dropped to truncate the quotient):比如 13 / 2 = 6
      • 舍入求商(round the quotient):如果除法是无符号的,舍入需要将余数与除数的一半进行比较,以决定是否将商数向上取整(比如 13 / 2 = 7
      • 分数余数(fractional remainder):比如 13 / 2 = 6.5
例子

下面展示了一个将 AX 除以 BL 的程序,并舍入无符号结果(即,AX / BL

0000 F6 F3          DIV BL          ; divide
0002 02 E4          ADD AH, AH      ; double remainder
0004 ЗА ЕЗ          CMP AH, BL      ; test for rounding
0006 72 02          JB NEXT         ; if OK
0008 FE CO          INC AL          ; round
000A           NEXT:
  • 在比较之前将余数加倍,以决定是否舍入求商
  • INC 指令在比较后舍入 AL 的内容

下面展示了如何将 13 除以 2

0000 B8 000D        MOV AX, 13      ; load 13
0003 B3 02          MOV BL, 2       ; load 2
0005 F6 F3          DIV BL          ; 13/2
0007 A2 0003 R      MOV ANSQ, AL    ; save quotient
000A BO 00          MOV AL, O       ; clear AL
000C F6 F3          DIV BL          ; generate remainder
000E A2 0004 R      MOV ANSR, AL    ; save remainder
  • 8 位商被保存在内存位置 ANSQ 中,然后 AL 被清零
  • 接下来,AX 的内容再次除以 2,以生成一个分数余数
  • 在第二次除法后,AL 寄存器的值为 80H
  • 如果将二进制点(基数)放置在 AL 的最左边位之前,则 AL 中的小数余数为 0.510 0.100000002
  • 余数保存在内存位置 ANSR

BCD and ASCII Arithmetic⚓︎

微处理器同时支持 BCD(二进制编码的十进制 (binary-coded decimal))和 ASCII 数据的算术操作。

  • 但这些指令在 64 位模式下是无效的,强行使用会抛出无效操作码异常(invalid-opcode exception)。

BCD Arithmetic⚓︎

  • BCD 运算发生在如小型终端(例如,收银机)和其他很少需要复杂算术的系统中
  • 两种使用 BCD 数据进行运算的算术指令:

    • DAA(加法后十进制调整 (decimal just after addition))指令紧随 BCD 加法之后
    • DAS(减法后十进制调整 (decimal just after subtraction))紧随 BCD 减法之后
    • 两者都纠正加法或减法的结果,使其成为 BCD 数,并且这些指令使用寄存器 AX 作为源和目标
  • DAA 指令:

    • 调整两个打包(packed) BCD 值的总和,以创建一个打包 BCD 结果
    • 仅在跟随 ADDADC 指令后面时才有用,这些指令将两个 2 位数字的打包 BCD 值相加(以二进制方式相加,并将字节结果存储在 AL 寄存器中
    • 然后该指令调整 AL 寄存器的内容,使其包含正确的 2 位数字打包 BCD 结果
    • 如果检测到十进制进位,则 CFAF(在加法后保存进位(半进位)标志会相应设置
    例子
    计算 BCD 35+48
    MOV AL, 35H
    ADD AL, 48H     ; AL = 7DH, AF = 0
    DAA             ; AL = 83H, CF = 0
    
    计算 BCD 69+29
    MOV AL, 69H
    ADD AL, 29H     ; AL = 92H, AF = 1
    DAA             ; AL = 98H, CF = 0
    
    计算 BCD 35+65
    MOV AL, 35H
    ADD AL, 65H     ; AL = 9AH, AF = 0
    DAA             ; AL = 00H, CF = 1
    

    0000 BA 1234    MOV DX, 1234H       ; load 1234 BCD
    0003 BB 3099    MOV BX, 3099Н       ; load 3099 BCD
    0006 8A C3      MOV AL, BL          ; sum BL and DL
    0008 02 C2      ADD AL, DL          ; AL= CDH
    000A 27         DAA                 ; AL = 33H, CF=1
    000B 8A C8      MOV CL, AL          ; answer to CL
    000D 9A C7      MOV AL, BH          ; sum BH, DH and carry
    000F 12 C6      ADC AL, DH          ; AL = 43H
    0011 27         DAA
    0012 8A E8      MOV CH, AL          ; answer to CH
                                        ; CX= 4333H
    
  • DAS 指令:除了做减法运算外,和 DAA 指令基本一致

    例子
    0000 BA 1234    MOV DX, 1234H       ; load 1234 BCD
    0003 BB 3099    MOV BX, 3099Н       ; load 3099 BCD
    0006 8A C3      MOV AL, BL          ; subtract DL from BL
    0008 2A C2      SUB AL, DL
    000A 2F         DAS
    000B 8A C8      MOV CL, AL          ; answer to CL
    000D 9A C7      MOV AL, BH          ; subtract DH
    000F 1A C6      SBB AL, DH
    0011 2F         DAS
    0012 8A E8      MOV CH, AL          ; answer to CH
    

ASCII Arithmetic⚓︎

  • ASCII 算术指令与编码数字一起工作,值 30H39H 代表 0 9
  • 有以下四种指令:

    • AAAASCII 加法后调整)
    • AASASCII 减法后调整)
    • AAMASCII 乘法后调整)
    • AADASCII 除法前调整)
  • 这些指令使用寄存器 AX 作为源和目标

  • AAA 指令:

    • 在使用 ADD 指令后,使用 AAA 指令来添加两个未打包的(unpacked) BCD 数字
    • AL 寄存器是 AAA 指令的隐式源操作数和目标操作数
    • AAA 指令将 AL 中的值调整为未打包的 BCD 结果

      • 如果 AL[3:0] > 9AF = 1,则 AL = AL + 6AH = 1,并设置 CF = 1AF = 1
      • 否则,CF = 0AF = 0
      • 在任何情况下,AL[7:4] = 0,将正确的小数位留在 AL[3:0]
    • AAA 指令可以添加 ASCII 数字,而无需屏蔽掉高四位 '3'

    例子
    MOV AL, '3'     ; AL = 0x33 (ASCII for '3')
    ADD AL, '4'     ; AL = 0×67
                    ; ASCII result for '3' + '4' = 0x33 + 0x34 = 0x67
    AAA             ; AH = 0, AL = 07, CF = 0 and AF = 0
    
    MOV AX, '1'     ; AL = 0x31 (ASCII for '1')
    ADD AL, '9'     ; AL = 0xбA
                    ; ASCII result for '1' + '9' = 0x31 + 0x39 = 0x6A
    AAA             ; AH = 1, AL = 0, CF = 1 and AF = 1
    
    LLM 能否理解 AAA 指令

    应该是对本学期第二次实验的预告

  • AAS 指令:在 ASCII 减法后调整 AX 寄存器

  • AAM 指令:
    • 在对两个一位数的未打包 BCD 数字进行乘法运算后,遵循乘法指令
    • 该指令将二进制转换为未打包的 BCD
    • 如果 AX 中出现介于 0000H0063H 之间的二进制数字,该指令将其转换为 BCD
  • AAD 指令:
    • 出现在除法前
    • 要求 AX 寄存器在执行前包含两位未打包的 BCD 数字(非 ASCII

Logic Instructions⚓︎

逻辑运算在低级软件 (low-level software) 中提供二进制位控制,允许设置、清除或补位。

  • 低级软件以机器语言或汇编语言形式出现,并且常用于控制系统中的 I/O 设备

逻辑运算始终:

  • 清除 OFCF 标志
  • SFZFPF 标志更改,以反映结果
  • AF 标志的状态是未定义的

当在寄存器或内存位置中操作二进制数据时,最右边的位始终编号为位 0

所有逻辑指令都会影响标志位,除了标志位不变的 NOT 指令。

下面详细介绍各类逻辑指令。

AND⚓︎

  • 执行逻辑乘法,通过真值表说明
  • 如果所需速度不是太快,AND 可以替代离散的 AND
    • 通常保留用于嵌入式控制应用
  • 8086 中,AND 指令通常在约 1μs 内执行
    • 使用较新版本时,执行速度大大提高
  • 可用于实现掩码(masking) 操作,即清除二进制数的位

    • 可以使用 AND 掩码去除最左边的四个二进制位位置,将 ASCII 编码的数字转换为 BCD

      MOV BX, 3135H   ; load ASCII
      AND BX, OF0FH   ; mask BX
      
  • 使用除内存到内存和段寄存器寻址之外的所有模式

OR⚓︎

  • 执行逻辑加法,通常称为包含或(inclusive-or) 函数

    • 如果有任何输入为 1,就会生成逻辑 1 输出
    • 只有当所有输入均为 0 时,输出才为 0
  • OR 门及真值表:

  • 使用除段寄存器寻址以外的任何寻址模式

  • 使用 OR 指令将数字中的某些位(强行)置 1

Exclusive-OR⚓︎

  • OR 唯一不同的是,当输入为 1, 1 时,异或(exclusive-OR) 产生 0
  • 实际上可将规则总结为:若输入都是 0 或者都是 1,则输出为 0;如果输入不同,则输出为 1
  • 异或有时被称为比较器(comparator)
  • XOR 门及真值表:

  • 使用除段寄存器寻址以外的任何寻址模式

  • 可用于反转寄存器或内存位置的某些位

    • 1 \(X\) 进行异或时,结果是 \(\overline{X}\)
    • 0 \(X\) 进行异或时,结果是 \(X\)

  • 另一个常见用途是将寄存器清零

Test and Bit Test Instructions⚓︎

  • TEST 执行 AND 操作,但只影响标志寄存器的表示测试结果的条件

    • 工作方式与 CMP 指令类似
  • 通常后面跟着 JZ(如果为零则跳转 (jump if zero))或 JNZ(如果不为零则跳转 (jump if not zero))指令

  • 目标操作数通常与立即数进行比较
例子

测试 AL 寄存器最右边和最左边的位;用 1 选择最右边的位,128 选择最左边的位

TEST AL, 1      ; test right bit
JNZ RIGHT       ; if set
TEST AL, 128    ; test left bit
JNZ LEFT        ; if set
  • CMPTEST 是常用于比较的指令,这些指令被称为条件指令(conditionals)

    MOV EAX, 1
    CMP EAX, 1      ; C=0, Z=1, S=0, 0=0
    JE  LABEL       ; jump to LABEL
    
    MOV  EAX, 1
    TEST EAX, 1     ; C=0, Z=0, S=0, 0=0
    JE   LABEL      ; do not jump
    
  • TEST same, same 用于确定一个符号数是否大于 0

    TEST EAX, EAX       ; if EAX = 0 set Z = 1, if EAX < 0 set S = 1
    JLE ERROR           ; if EAX is equal or less than zero then jump
    
  • TEST EAX, EAXCMP EAX, 0 几乎相同,除了前者的指令比后者更短

    TEST EAX, EAX       ; 85 C0 
    CMP  EAX, 0         ; 83 F8 00
    
  • 80386-Pentium 4 包含四个额外的测试指令,用于测试单个位的位置

    • 所有位测试指令测试由源操作数选定的目标操作数中的位位置
    汇编语言 操作
    BT 测试目标操作数中由源操作数指定的位
    BTC 测试目标操作数中由源操作数指定的位,并对该位进行求反操作
    BTR 测试目标操作数中由源操作数指定的位,并重置(置为 0)该位
    BTS 测试目标操作数中由源操作数指定的位,并设置(置为 1)该位
    • 例子:BT AX, 4 指令测试 AX 的第 4

      • 测试结果位于进位标志位
      • 若第 4 位是 1CF 置位
      • 若第 4 位是 0CF 清零
    • 下面的例子分别展示了在 CX 上使用逻辑和位测试指令的位操作

      OR  CX, 0600H       ; set bits 9 and 10
      AND CX, 0FFFCH      ; clear bits 0 and 1
      XOR CX, 1000H       ; invert bit 12
      
      BTS CX, 9       ; set bit 9
      BTS CX, 10      ; set bit 10
      BTR CX, 0       ; clear bit 0
      BTR CX, 1       ; clear bit 1
      BTC CX, 12      ; complement bit 12
      

NOT and NEG⚓︎

  • NOTNEG 可以使用除段寄存器寻址以外的任何寻址模式
    • NOT 指令反转一个字节、字或双字的所有位
    • NEG 对一个数取二进制补码
  • NOT 被认为是逻辑操作,NEG 被认为是算术操作。
  • NOT 指令不影响任何标志位,而 NEG 指令影响的标志位有:

    • 如果操作数 = 0,则CF = 0,否则CF = 1
    • OFSFZFAFPF 标志位根据结果设置
  • 为什么 NOT 指令不修改任何标志位

    • NOT 操作通常用作中间操作(例如if (!(a < b) && (a == b)))而不是作为最终计算结果;因此,它不需要修改任何标志位
    • 如果需要测试 NOT 操作的结果,可以使用 CMPTEST 指令来执行检查

      NOT  AX             ; Invert all bits of AX
      TEST AX, AX         ; Perform bitwise AND between AX and itself
      JZ   is_zero        ; Jump to `is_zero` if ZF is set
      
      NOT AX              ; Invert all bits of AX
      CMP AX, 0           ; Compare AX with 0
      JE  is_zero         ; Jump to `is_zero` if ZF is set
      
例子

符号数函数:

signum 函数的最小代码由一个叫做 superoptimizer 的程序生成。

Shift⚓︎

  • 移位指令将数字左移或右移到寄存器或内存位置中

    • 还可以执行简单的算术运算,如乘以 2 +n 的幂(左移)和除以 2 -n 的幂(右移

  • 微处理器的指令集包含四种不同的移位指令:

    • 两个是逻辑(logical) 移位;两个是算术(arithmetic) 移位
    • 语法:SHL/SAL/SHR/SAR REG/MEM, Count
  • 下图展示了这四种移位运算:

    • SHLSAL 将目标操作数的位向左移动(所以两者执行相同的操作)
    • SHR0 填充左边空出来的位
    • SAR符号位填充左边空出来的位
  • 一些例子:

    汇编语言 操作
    SHL AX,1 AX 进行逻辑左移 1
    SHR BX,12 BX 进行逻辑右移 12
    SHR ECX,10 ECX 进行逻辑右移 10
    SHL RAX,50 RAX 进行逻辑左移 50 位(64 位模式)
    SAL DATA1,CL 数据段内存位置 DATA1 的内容进行算术左移,位移量由 CL 指定
    SHR RAX,CL RAX 进行逻辑右移,位移量由 CL 指定(64 位模式)
    SAR SI,2 SI 进行算术右移 2
    SAR EDX,14 EDX 进行算术右移 14
  • 计数操作数可以是一个立即值或 CL 寄存器

    • 以下示例展示了如何以两种不同方式将 DX 寄存器左移 14 位:

      SHL DX, 14
      
      MOV CL, 14
      SHL DX, CL
      
  • 16 位或 32 位模式下,移位计数是一个模 32 的计数,即计数范围在 0-31

  • 64 位模式下,移位计数是一个模 64 的计数,即计数范围在 0-63
  • SARSHR 指令可用于执行目标操作数按 2 的幂次的符号或无符号除法
  • 负数的 SAR 舍入:

    • 对于负数,IDIV 指令得到的商会被舍入至 0,而 SAR 指令会将其舍入至负无穷,两者结果并不一致 (inconsistency)

  • 双精度移位(从 80386 开始)

    • 两个指令SHLD(左移)和 SHRD(右移,本质上是跨寄存器的移位

    • 每条指令包含三个操作数(SHLD/SHRD D, S, Count,而不是 2

    • 例如指令 SHLD reg1, reg2, imm8 将寄存器 reg1reg2 连接起来,并将它们左移由 imm8 指定的数量
    • 两个函数都使用两个 16 位、32 位或 64 位寄存器,或者一个 16 位、32 位或 64 位内存位置和一个寄存器
    例子
    shld ebx, ecx, 16       ; The leftmost 16 bits of ecx fill the
                            ; rightmost 16 bits of ebx. The contents
                            ; of ecx remain unchanged.
    shrd ax, bx, 12         ; Logical right shift of ax by 12
                            ; rightmost 12 bits of bx into
                            ; leftmost 12 bits of ax. The contents
                            ; of bx remain unchanged.
    

    128 位值除以 8 的除法:

    __uint128_t u128div (__uint128_t x) {
        return x / 8;
    }
    
    x86-64 gcc 12.2 -O3
    u128div:
    mov rax, rdi            ; rax is lower 64 bit
    mov rdx, rsi            ; rdx is upper 64 bit
    shrd rax, rsi, 0x3      ; [rsi : rax] >> 3
    shr rdx, 0x3            ; rdx >> 3
    ret                     ; result = [rdx : rax] / 8
    

Rotation⚓︎

  • 通过在寄存器或内存位置中旋转信息来定位二进制数据,要么从一端到另一端,要么通过进位标志
  • 语法:ROL/ROR/RCL/RCR REG/MEM, Count

  • 左旋(ROL)和右旋(ROR)不包括 CF 标志

  • 而进位左旋 (rotate through carry left)RCL)和进位右旋 (rotate through carry right)RCR)将 CF 标志移入最高或最低有效位
  • 旋转计数可以是立即数,或寄存器 CL 的值;如果使用后者,其值不会改变
  • 旋转指令通常用于将宽数字向左或向右移动
例子

128 位值除以 2 的除法:

__uint128_t u128div (__uint128_t x) {
    return x / 2;
}
SHRD version
u128div:
    mov rax, rdi
    mov rdx, rsi
    shrd rax, rsi, 0x1
    shr rdx, 0x1
    ret
RCR version
u128div:
    mov rax, rdi    ; rax is lower 64 bit
    mov rdx, rsi    ; rdx is upper 64 bit
    shr rdx, 0x1    ; rdx >> 1, shift LSB of rdx into CF
    rcr rax, 0x1    ; rotate rax by 1, and shift CF into rax
    ret             ; result = [rdx : rax] / 2

Bit Scan Instructions⚓︎

  • 位扫描指令在数字中扫描,用于寻找其中的某个位

    • 通过移位数字实现
    • 80386 - Pentium 4 中可用
  • BSF(位向前扫描 (bit scan forward))从最低位向左扫描源数字

  • BSR(位反向扫描 (bit scan reverse))从最高位向右扫描源数字
  • 语法:BSF/BSR REG, REG/MEM
  • 如果未遇到指定位,则设置零标志(ZF = 1
  • 如果遇到指定位,则清除零标志(ZF = 0,并将该位的位位置编号放入目标操作数中
例子

EAX = 60000000H = 0110 0000 0000 0000 0000 0000 0000 0000B

  • BSF EBX, EAX

    • EBX = 29(位 29 1
    • ZF = 0
  • BSR EBX, EAX

    • EBX = 30(位 30 1
    • ZF = 0
  • 扩展:

    • TZCNT(尾部零计数 (tailing zero count))计算尾部零比特的数量
    • LZCNT(前导零计数 (leading zero count))返回前导零比特的数量
    • 它们和 BSF/BSR 之间的主要区别在于:
      • 如果源操作数 = 0(没有 1 ,则 BSF/BSR 中目标操作数的内容是未定义的,而 TZCNT/LZCNT 提供操作数大小作为输出
      • 如果源操作数 = 0BSF/BSR 仅影响 ZF,而 TZCNT/LZCNT 同时设置 ZFCFZF = 1CF = 1,否则清零
    例子

    EAX = 60000000H = 0110 0000 0000 0000 0000 0000 0000 0000B

    • LZCNT EBX, EAX

      • EBX = 1(一个前导位)
      • ZF = 0CF = 0
    • BSR EBX, EAX

      • EBX = 30(位 30 1
      • ZF = 0

String Comparisons⚓︎

  • 字符串指令非常强大,能让程序员相对轻松地操作大量数据块
  • 数据块操作通过 MOVS / LODS / STOS / INS / OUTS 进行
  • 另外一些字符串指令能将内存的一部分,与常量或另一部分内存进行测试,包括SCAS(字符串扫描)和 CMPS(字符串比较)

SCAS⚓︎

  • SCAS 指令将使用内存操作数指定的字节、字、双字或四字与 ALAXEAX 中的值(隐式操作数)进行比较,然后在 EFLAGS 中设置状态标志以记录结果
  • 内存操作数地址根据当前操作模式的地址大小属性从 ES:EDI 读取
  • 操作数的大小可根据指令选择:

    • SCASB(字节比较)
    • SCASW(字比较)
    • SCASD(双字比较)
  • SCAS 使用方向标志(D)来选择 DI/EDI 的自动增减操作

  • SCAS 可以在块比较前使用条件重复前缀 REPE(相等时重复)或 REPNE(不相等时重复)
例子
  • 假设内存的一个部分长度为 100 字节,并从位置 BLOCK 开始
  • 必须测试这个内存部分,以查看是否有任何位置包含 00H
  • 以下程序展示了如何使用 SCASB 指令搜索内存的这一部分以查找 00H
MOV DI, OFFSET BLOCK        ; address data
CLD                         ; auto-increment
MOV CX, 100                 ; load counter
XOR AL, AL                  ; clear AL
REPNE SCASB

SCASB 指令结束后

  • 如果 ZF = 1,则某个位置包含 00H
  • 如果 CX = 0ZF = 0,则所有数据都不匹配 00H

CMPS⚓︎

  • 始终比较两个内存区域,数据以字节(CMPSB、字(CMPSW)或双字(CMPSD)的形式进行比较

    • SI/ESI 寻址的数据段内存位置的内容,与由 DI/EDI 寻址的附加段内存的内容进行比较
    • CMPS 指令会递增 / 递减计数器
  • 通常与 REPEREPNE 前缀一起使用

    • 替代方案是 REPZ(在零时重复)和 REPNZ(在非零时重复)
例子

比较两个内存部分,用于寻找匹配:

MOV SI, OFFSET LINE         ; address LINE
MOV DI, OFFSET TABLE        ; address TABLE
CLD                         ; auto-increment
MOV CX, 10                  ; load counter
REPE CMPSB                  ; search
  • CMPSB 指令以 REPE 作为前缀。这导致搜索在存在相等条件的情况下继续
  • CX 寄存器变为 0 或存在不等条件时,CMPSB 指令停止执行
  • CMPSB 指令结束后
    • 如果 CX = 0ZF = 1,则两个字符串匹配
    • 如果 CX != 0ZF = 0,则字符串不匹配

评论区

如果大家有什么问题或想法,欢迎在下方留言~