汇编语言移位和循环移位的应用

< 上一页SHLD和SHRD指令 MUL指令下一页 >
当程序需要将一个数的位从一部分移动到另一部分时,汇编语言是非常合适的工具。有时,把数的位元子集移动到位 0,便于分离这些位的值。本节将展示一些易于实现的常见移位和循环移位的应用。

多个双字的移位

对于已经被分割为字节、字或双字数组的扩展精度整数可以进行移位操作。在此之前,必须知道该数组元素是如何存放的。保存整数的常见方法之一被称为小端顺序 (little-endian order)。

其工作方式如下:将数组的最低字节存放到它的起始地址,然后,从该字节开始依序把高字节存放到下一个顺序的内存地址中。除了可以将数组作为字节序列存放外,还可以将其作为字序列和双字序列存放。如果是后两种形式,则字节和字节之间仍然是小端顺序,因为 x86 机器是按照小端顺序存放字和双字的。

下面的步骤说明了怎样将一个字节数组右移一位。

步骤 1):把位于 [ESI+2] 的最高字节右移一位,其最低位自动复制到进位标志位。


步骤 2):把 [ESI+1] 循环右移一位,即用进位标志位填充最高位,而将最低位移入进位标志位:


步骤 3) :把 [ESI] 循环右移一位,即用进位标志位填充最高位,而将最低位移入进位标志位:


步骤 3 完成后,所有的位都向右移动了一位:


实现的是上述 3 个步骤,代码如下:
.data
ArraySize = 3
array BYTE ArraySize DUP(99h)          ; 每个半字节的值都是 1001

.code
main PROC
    mov esi,0
    shr array[esi+2],1                 ; 高字节
    rcr array[esi+1],1                 ; 中间字节,包括进位标志位
    rcr array[esi],1                   ; 低字节,包括进位标志位
虽然这个例子只有 3 个字节进行了移位,但是它能很容易被修改成执行字数组或双字数组的移位操作。利用循环,可以对任意大小的数组进行移位操作。

二进制乘法

有时程序员会压榨出任何可以获得的性能优势,他们会使用移位而非 MUL 指令来实现整数乘法。当乘数是 2 的幂时,SHL 指令执行的是无符号数乘法。

一个无符号数左移 n 位就是将其乘以 2n。其他任何乘数都可以表示为 2 的幂之和。例如,若将 EAX 中的无符号数乘以 36,则可以将 36 写为 25+22,再使用乘法分配律:

EAX * 36 = EAX * (2⁵ + 2²)
         = EAX * (32 + 4)
         = (EAX * 32) + (EAX * 4)

下图展示了乘法 123*36 得到结果 4428 的过程:


请注意这里有个有趣的现象,乘数 (36) 的位 2 和位 5 都为 1,而整数 2 和 5 又是需要移位的次数。利用这个现象,下面的代码片段使用 SHL 和 ADD 指令实现了 123 乘以 36:
mov eax, 123
mov ebx, eax
shl eax, 5                ; 乘以 2⁵
shl ebx, 2                ; 乘以 2²
add eax, ebx            ; 乘积相力口

显示二进制位

将二进制整数转换为 ASCII 码的位串,并显示出来是一种常见的编程任务。SHL 指令适用于这个要求,因为每次操作数左移时,它都会把操作数的最高位复制到进位标志位。下面的 BinToAsc 过程是该功能一个简单的实现:
;---------------------------------------------------------
BinToAsc PROC
;
; 将 32 位二进制整数转换为 ASCII 码的二进制形式。
; 接收:EAX = 二进制整数,EST 为缓冲区指针
; 返回:包含 ASCII 码二进制数字的缓冲区
;---------------------------------------------------------
    push    ecx
    push    esi
   
    mov    ecx,32                    ; EAX 中的位数

L1:    shl    eax,1                  ; 最高位移入进位标志位
    mov    BYTE PTR [esi],'0'        ; 选择0作为默认数字
    jnc    L2                        ; 如果进位标志位为0,则跳转到L2
    mov    BYTE PTR [esi],'1'        ; 否则将1送入缓冲区

L2:    inc    esi                    ; 指向下一个缓冲区位置
    loop    L1                       ; 下一位进行左移

    pop    esi
    pop    ecx
    ret
BinToAsc ENDP

提取文件日期字段

当存储空间非常宝贵的时候,系统软件常常将多个数据字段打包为一个整数。要获得这些数据,应用程序就需要提取被称为位串(bit string)的位序列。例如,在实地址模式下,MS-DOS 函数 57h 用 DX 返回文件的日期戳。

(日期戳显示的是该文件最后被修改的日期。)其中,位 0〜 位 4 表示的是 1〜31 内的日期;位 5〜 位 8 表示的是月份;位 9〜 位 15 表示的是年份。如果一个文件最后被修改的日期是 1999 年 3 月 10 日,则 DX 寄存器中该文件的日期戳就如下图所示(年份以 1980 为基点):


要提取一个位串,就把这些位移到寄存器的低位部分,再清除掉其他无关的位。下面的代码示例从一个日期戳中提取日期字段,方法是:复制 DL,然后屏蔽与该字段无关的位:

mov al, dl                       ; 复制 DL
and al, 00011111b         ; 清除位 5 〜 位 7
mov day, al                    ; 结果存入变量 day

要提取月份字段,就把位 5〜 位 8 移到 AL 的低位部分,再清除其他无关位,最后把 AL 复制到变量中:

mov ax, dx                     ;复制 DX
shr ax, 5                         ;右移5位
and al, 00001111b         ;清除位 4 〜位 7
mov month, al               ;结果存入变量month

年份字段(位 9〜 位 15)完全包含在 DH 寄存器中,将其复制到 AL,再右移 1 位:

mov al, dh                 ;复制 DH
shr al, 1                     ;右移1位
mov ah, 0                  ;将 AH 清零
add ax, 1980             ;年份基点为1980
mov year, ax             ;结果存入变量year

< 上一页SHLD和SHRD指令 MUL指令下一页 >