前面的章节介绍过BCD码,虽然很多高级的BCD运算操作是在FPU中完成的,但是在处理器中也提供了一些可以完成简单的BCD码运算的指令...

    本文由zengl.com站长对
    http://pan.baidu.com/share/link?shareid=3717576860&uk=940392313 汇编教程英文版相应章节进行翻译得来。
    另外再附加一个英特尔英文手册的共享链接地址:
    http://pan.baidu.com/share/link?shareid=2345340326&uk=940392313 (在某些例子中会用到)

    本篇翻译对应汇编教程英文原著的第258页到第265页,翻译完前8章 (注意这里的页数不是页脚的页数,而是pdf电子档顶部,分页输入框中的页数,也就是包含了目录,前言部分的总页数,pdf电子档总页数是577,当前已翻译完的页数为265/ 577)。

Decimal Arithmetic 十进制运算:

    前面的章节介绍过BCD码,虽然很多高级的BCD运算操作是在FPU中完成的,但是在处理器中也提供了一些可以完成简单的BCD码运算的指令。

Unpacked BCD arithmetic 普通非压缩BCD码的运算指令:

    普通的非压缩BCD码是指一个字节对应一个0到9的数字,ADD,SUB等指令产生的结果是二进制的格式,如果要将这些结果转为BCD码格式,就可以使用下面的指令:
  • AAA: Adjusts the result of an addition process
    AAA指令:将加法运算的结果调整为BCD码
  • AAS: Adjusts the result of a subtraction process
    AAS指令:将减法运算的结果调整为BCD码
  • AAM: Adjusts the result of a multiplication process
    AAM指令:将乘法运算的结果调整为BCD码
  • AAD: Prepares the dividend of a division process
    AAD指令:为除法运算准备二进制格式的被除数
    也就是当被除数是BCD码格式时
    AAD指令就可以将其转为二进制格式,以用于接下来的除法运算
    这些指令必须配合普通无符号数的ADD,ADC,SUB,SBB,MUL及DIV指令来使用的。其中AAA,AAS和AAM指令是用在对应的加减乘运算之后,用于将这些运算的结果由常规的二进制格式转为非压缩BCD码格式,而AAD指令则有些不同,它必须用在DIV指令之前,提前将被除数由BCD格式转二进制格式,才能让DIV指令产生出正确的结果来。

    AAA,AAS和AAM指令会假定操作数位于AL寄存器中,然后会将AL里的值转为非压缩BCD格式,所以ADD之类的加减乘运算需要将结果存储到AL里。AAD指令则假定要操作的被除数位于AX寄存器,并且会假定该被除数是BCD格式,然后会将AX里的被除数转为二进制格式,以用于后面的除法运算。

    当计算多字节的非压缩BCD码时,必须将carry进位标志和overflow溢出标志也考虑进去,才能计算出正确的结果,如下图所示:


图1

    上图显示当某一对数值加法发生进位时,carry进位标志需要带到下一对数值的计算当中,AAA指令在将结果转为BCD时,如果结果大于9,就会向AH进一位,同时设置carry进位标志,剩下的值就留在AL里 (AAA会忽略结果的高4位,只考虑低4位,例如AL寄存器里的结果无论是0x1a还是0xa,在AAA指令操作后,AX都会变为0x100)

    下面的aaatest.s程式就演示了AAA指令的用法:

# aaatest.s - An example of using the AAA instruction
.section .data
value1:
    .byte 0x05, 0x02, 0x01, 0x08, 0x02
value2:
    .byte 0x03, 0x03, 0x09, 0x02, 0x05
.section .bss
    .lcomm sum, 6
.section .text
.globl _start
_start:
    nop
    xor %edi, %edi
    movl $5, %ecx
    clc
loop1:
    movb value1(, %edi, 1), %al
    adcb value2(, %edi, 1), %al
    aaa
    movb %al, sum(, %edi, 1)
    inc %edi
    loop loop1
    movb $0, sum(, %edi, 1)
    adcb $0, sum(, %edi, 1)
    movl $1, %eax
    movl $0, %ebx
    int $0x80
 

    代码中,非压缩BCD码是以小字节序存储在内存里的,一次从内存里读取一个BCD值,通过ADC指令将value1和value2里的值相加,同时将carry标志也加进去,ADC生成的结果默认是二进制格式的,所以再由AAA指令将AL里的结果转为BCD格式,再存储到sum对应的位置,例如value1里的0x01和value2里的0x09相加后,结果就是0xa,AAA指令执行后,AL的值就变为0,同时AH的值就变为0x1,由于向AH进了一位,所以carry标志就会被设置,当循环执行下一个BCD码的加法运算时,前面设置的carry标志就会加进来,这样循环5次,就可以将value1和value2里的5个BCD值相加,结果也以BCD格式存储到sum内存位置,在loop1循环之前的CLC指令是用于将carry标志清零,以防止对下面的ADC指令产生影响,在循环处理完5个数后(ECX计数器一开始设置为5,则loop1的循环体就会被执行5次),最后一个数0x02和0x05加法所产生的进位将被存储到sum的最后一个字节。

    在调试器中,当ADC执行完第三个数即0x01和0x09的加法操作后,在执行AAA指令之前,EAX寄存器的值如下:

(gdb) info reg
eax 0xa 10
........................................

(gdb)

    结果是二进制格式的0xa,在执行完AAA指令后,EAX寄存器的值为:

(gdb) info reg
eax 0x100 256
........................................
(gdb)

    AL里的值为0,AH里的值为进位的1,当所有计算都执行完后,sum里的结果为:

(gdb) x/6bx &sum
0x80490b8 <sum>: 0x08 0x05 0x00 0x01 0x08 0x00
(gdb)

    因为都已经转为BCD格式了,所以28,125与52,933的加法结果就是81,058 。

    如果上面说明不够详细,可以在google中进行类似如下的搜索:

AAD site:www.jaist.ac.jp

    上面的搜索结果,第一项就是www.jaist.ac.jp网站有关AAD指令的详细说明,绝大部分的汇编指令都可以这么搜索到。

Packed BCD arithmetic 压缩BCD码的数学运算:

    对于压缩BCD码,只有如下两个指令可用:
  • DAA: Adjusts the result of the ADD or ADC instructions
    DAA指令:将ADD或ADC指令的结果调整为压缩BCD格式
  • DAS: Adjusts the result of the SUB or SBB instructions
    DAS指令:将SUB或SBB指令的结果调整为压缩BCD格式
    这些指令和前面的AAA与AAS指令的用法差不多,只不过是用于压缩BCD码的运算,压缩BCD码是用每4位二进制来表示一位十进制,非压缩BCD则是每8位二进制来表示一位十进制,DAA与DAS指令的隐示操作数也是位于AL寄存器里,转换后的结果也存储在AL里,进位的值将被存储到AH寄存器与carry标志位。

    下图演示了压缩BCD码运算的例子:


图2

    下面的dastest.s程式就演示了上图的例子:

# dastest.s - An example of using the DAS instruction
.section .data
value1:
    .byte 0x25, 0x81, 0x02
value2:
    .byte 0x33, 0x29, 0x05
.section .bss
    .lcomm result, 4
.section .text
.globl _start
_start:
    nop
    xor %edi, %edi
    movl $3, %ecx
loop1:
    movb value2(, %edi, 1), %al
    sbbb value1(, %edi, 1), %al
    das
    movb %al, result(, %edi, 1)
    inc %edi
    loop loop1
    sbbb $0, result(, %edi, 1)
    movl $1, %eax
    movl $0, %ebx
    int $0x80
 

    上面的代码和之前aaatest的例子在结构上差不多,ECX计数器在此处被设置为3,表示对value1和value2里的3对值依次进行减法运算,使用SBB指令,从而将carry借位标志也考虑进去,得到的结果存储在AL里,该结果默认是二进制格式,通过DAS指令将其转为压缩BCD格式,再将BCD格式的结果存储到result内存位置。

    调试器中在对第一对值0x33与0x25进行SBB操作后,EAX里的值如下:

(gdb) info reg
eax 0x0e 14
.....................................

(gdb)

    在执行DAS指令后,EAX的值就变为:

(gdb) info reg
eax 0x08 8
.....................................
(gdb)

    和前面图2演示的结果一致,其他的结果也是以此类推。

Logical Operations 逻辑运算指令:

    除了标准的加减乘除运算外,汇编还提供了一些指令用于对字节里的原始二进制位进行相关操作,下面就介绍布尔逻辑运算指令和位测试指令。

Boolean logic 布尔逻辑运算指令:

    下面是汇编中可用的布尔逻辑指令:
  • AND 逻辑且
  • NOT 逻辑取反
  • OR 逻辑或
  • XOR 逻辑异或
    其中AND,OR和XOR的指令格式如下:

and source, destination

    上面格式里的source源操作数可以是一个8位、16位、32位的立即数,寄存器,或者内存位置里的值,destination目标操作数可以是一个8位、16位、32位的寄存器或者内存位置里的值(当然,源和目标操作数不能同时都为内存位置)。

    NOT逻辑取反指令使用的是单一的操作数(8位、16位、32位的寄存器或者内存位置),它会将操作数里的所有原始二进制位进行反转,1变0,0变1,例如1001经过NOT指令反转后就会变为0110 。

    这些逻辑指令都是对操作数逐位的进行布尔运算的,比较常见的像XOR指令,就是按照相同则为0,不同则为1的原则,例如对于1001和1111两操作数进行XOR运算,结果是0110,第一位和第四位都是相同的1,所以第一和第四位就被清零,中间两位不同,所以中间两位就是1 。XOR指令经常用于将寄存器清零,例如xor %edx,%edx指令就可以将EDX寄存器清零,使用XOR清零比MOV指令加载0到寄存器的速度要快。

    AND指令只有两个操作数对应二进制位上都为1时,该二进制位才为1,否则就为0,例如1001和1111进行AND运算,结果就是1001 ,OR指令则只有都为0时,该二进制位才为0,否则就为1,例如10010和11110进行OR运算,结果就是11110 ,只有最后一位都为0,其他位上有一个操作数为1则该位就为1。

Bit testing 位测试指令:

    下面是www.jaist.ac.jp网站上有关TEST指令的Intel语法:

Opcode

指令操作码

Instruction

Intel格式的指令

Description

指令描述

A8  ib

TEST AL,imm8

AND imm8 with AL;
set SF, ZF, PF according to result

A9 iw

TEST AX,imm16

AND imm16 with AX;
set SF, ZF, PF according to result

A9 id

TEST EAX,imm32

AND imm32 with EAX;
set SF, ZF, PF according to result

F6 /0 ib

TEST r/m8,imm8

AND imm8 with r/m8;
set SF, ZF, PF according to result

F7 /0 iw

TEST r/m16,imm16

AND imm16 with r/m16;
set SF, ZF, PF according to result

F7 /0 id

TEST r/m32,imm32

AND imm32 with r/m32;
set SF, ZF, PF according to result

84 /r

TEST r/m8,r8

AND r8 with r/m8;
set SF, ZF, PF according to result

85 /r

TEST r/m16,r16

AND r16 with r/m16;
set SF, ZF, PF according to result

85 /r

TEST r/m32,r32

AND r32 with r/m32;
set SF, ZF, PF according to result


    可以看出来,TEST指令其实就是将源操作数和目标操作数进行模拟AND运算,之所以是模拟,是因为它不会像AND那样修改目标操作数的值,在模拟AND后,会根据结果来设置对应的SF(符号位),ZF(是否为零),PF(奇偶)标志位,上面表格中是Intel的写法,即第一个是目标操作数,第二个是源操作数,可以看出源操作数可以是8位、16位、32位的立即数或者各尺寸的寄存器,目标操作数可以是寄存器或内存位置。

    TEST指令常见的用途是可以用来检测操作数中某个二进制位的设置情况,例如可以用来检测EFLAGS标志寄存器里的各标志位的设置情况,在EFLAGS寄存器中有一个ID标志位,它可以用于检测CPUID指令是否可用(CPUID指令前面介绍过,可以用于获取一些处理器的信息),如果ID标志位可以被修改的话,就说明处理器支持CPUID汇编指令,否则就不支持,下面的cpuidtest程式就演示了检测处理器是否支持CPUID指令的例子:

# cpuidtest.s - An example of using the TEST instruction
.section .data
output_cpuid:
    .asciz "This processor supports the CPUID instruction\n"
output_nocpuid:
    .asciz "This processor does not support the CPUID instruction\n"
.section .text
.globl _start
_start:
    nop
    pushfl
    popl %eax
    movl %eax, %edx
    xor $0x00200000, %eax
    pushl %eax
    popfl
    pushfl
    popl %eax
    xor %edx, %eax
    test $0x00200000, %eax
    jnz cpuid
    pushl $output_nocpuid
    call printf
    add $4, %esp
    pushl $0
    call exit
cpuid:
    pushl $output_cpuid
    call printf
    add $4, %esp
    pushl $0
    call exit
 

    上面的代码里,pushf指令用于将EFLAGS寄存器里的标志压入寄存器栈,然后就可以通过pop指令将栈中的EFLAGS寄存器值弹出给EAX寄存器(EFLAGS寄存器不可以直接使用MOV指令,只能通过压栈和弹出栈的方式进行设置和获取值的操作),再将EAX赋值给EDX(后面在设置完EFLAGS寄存器后,会将EFLAGS里的值和EDX进行TEST指令测试),接着通过XOR异或指令,将EAX里的ID标志位所在位置(位21)进行反转操作,即原来ID标志位为0,就改为1,原来为1,就改为0,从而修改ID标志位,当然现在操作的只是EAX里的副本,通过将EAX这个副本压入栈,再由popf指令将栈中修改过的值写入EFLAGS寄存器,从而完成修改EFLAGS寄存器的目的,最后再pushfl和popl %eax,将EFLAGS里的值再次赋值给EAX ,将此时的EAX与原始的EDX进行XOR运算,如果EFLAGS里的ID标志位不能被修改,则EAX与原始的EDX值应该相同,XOR运算后,EAX值就会是0,后面的TEST指令测试后,就会设置ZF(是否为零)标志,JNZ指令根据ZF标志就不会进行跳转,如果EFLAGS里的ID标志位被成功修改了,则XOR后,EAX就不为0,TEST指令测试后,就不会设置ZF标志,JNZ就会跳转到cpuid标签处,printf也就会显示处理器支持CPUID指令的信息。

    剩下就是一些总结部分,就不多说了。

    本篇就到这里,下一篇开始进入高级数学运算部分,转载请注明来源:www.zengl.com

    OK,休息,休息一下 o(∩_∩)o~~
上下篇

下一篇: 高级数学运算 (一) FPU寄存器介绍

上一篇: 基本数学运算 (三) 除法和移位指令

相关文章

汇编函数的定义和使用 (二)

汇编中使用文件 (二)

汇编函数的定义和使用 (三) 汇编函数结束篇

使用内联汇编 (二)

汇编开发相关工具 (三) 工具介绍结束篇,kdbg,gprof,mepis

汇编里使用Linux系统调用 (一)