当程序需要将一个数的位从一部分移动到另一部分时,汇编语言是非常合适的工具。有时,把数的位元子集移动到位 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,再使用乘法分配律:
下图展示了乘法 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,然后屏蔽与该字段无关的位:
要提取月份字段,就把位 5〜 位 8 移到 AL 的低位部分,再清除其他无关位,最后把 AL 复制到变量中:
年份字段(位 9〜 位 15)完全包含在 DH 寄存器中,将其复制到 AL,再右移 1 位: