汇编语言移位和循环移位的应用
当程序需要将一个数的位从一部分移动到另一部分时,汇编语言是非常合适的工具。有时,把数的位元子集移动到位 0,便于分离这些位的值。本节将展示一些易于实现的常见移位和循环移位的应用。
其工作方式如下:将数组的最低字节存放到它的起始地址,然后,从该字节开始依序把高字节存放到下一个顺序的内存地址中。除了可以将数组作为字节序列存放外,还可以将其作为字序列和双字序列存放。如果是后两种形式,则字节和字节之间仍然是小端顺序,因为 x86 机器是按照小端顺序存放字和双字的。
下面的步骤说明了怎样将一个字节数组右移一位。
步骤 1):把位于 [ESI+2] 的最高字节右移一位,其最低位自动复制到进位标志位。
步骤 2):把 [ESI+1] 循环右移一位,即用进位标志位填充最高位,而将最低位移入进位标志位:
步骤 3) :把 [ESI] 循环右移一位,即用进位标志位填充最高位,而将最低位移入进位标志位:
步骤 3 完成后,所有的位都向右移动了一位:
实现的是上述 3 个步骤,代码如下:
一个无符号数左移 n 位就是将其乘以 2n。其他任何乘数都可以表示为 2 的幂之和。例如,若将 EAX 中的无符号数乘以 36,则可以将 36 写为 25+22,再使用乘法分配律:
请注意这里有个有趣的现象,乘数 (36) 的位 2 和位 5 都为 1,而整数 2 和 5 又是需要移位的次数。利用这个现象,下面的代码片段使用 SHL 和 ADD 指令实现了 123 乘以 36:
(日期戳显示的是该文件最后被修改的日期。)其中,位 0〜 位 4 表示的是 1〜31 内的日期;位 5〜 位 8 表示的是月份;位 9〜 位 15 表示的是年份。如果一个文件最后被修改的日期是 1999 年 3 月 10 日,则 DX 寄存器中该文件的日期戳就如下图所示(年份以 1980 为基点):
要提取一个位串,就把这些位移到寄存器的低位部分,再清除掉其他无关的位。下面的代码示例从一个日期戳中提取日期字段,方法是:复制 DL,然后屏蔽与该字段无关的位:
多个双字的移位
对于已经被分割为字节、字或双字数组的扩展精度整数可以进行移位操作。在此之前,必须知道该数组元素是如何存放的。保存整数的常见方法之一被称为小端顺序 (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)
请注意这里有个有趣的现象,乘数 (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
mov ax, dx ;复制 DX
shr ax, 5 ;右移5位
and al, 00001111b ;清除位 4 〜位 7
mov month, al ;结果存入变量month
mov al, dh ;复制 DH
shr al, 1 ;右移1位
mov ah, 0 ;将 AH 清零
add ax, 1980 ;年份基点为1980
mov year, ax ;结果存入变量year