最近,在看汇编。其中的一章引起了我的兴趣。那就是浮点处理和指令编码。大家其实在编写代码时候经常会用到浮点处理。大家又想过处理器是怎么处理这些数据的?希望通过本帖的简单介绍能对你有所帮助。
    
                                    浮点数的几种表示方法
浮点二进制表示
   浮点数是由三部分组成的:符号、尾数、指数。以数字 -1.23456X10^5为例,其中“-”是符号,表示该浮点数是负数,1.23456是尾数,5是指数。

IEEE二进制浮点数的表示
   Intel微处理器使用IEEE发布的Standard754-1985 for Binary Floating-Point Arithmetic中规定的三种浮点二进制存储格式。见下表。

  格式                               说明
 单精度                        32位:符号占1位、指数占8位、尾数中的小数部分占23位。
                               可表示的大致范围2^-126~2^127,也称为短实数

 双精度                        64位:符号占1位、指数占11位、尾数中的小数部分占52位。
                               可表示的大致范围2^-1022~2^1023,也称为长实数

 扩展精度                      80位:符号占1位、指数占16位、尾数中的小数部分占63位。
                               可表示的大致范围是2^-16382~2^16383,也称为扩展实数

三种格式非常相似,因此这里重点讲述单精度格式。见下图。图中最高有效位(MSB)在左边:
           1            8                                         23
             符号        指数                     尾数中的小数部分

符号
   二进制浮点数的符号由一个符号位便是,如果该位为1表示负数,为0表示正数。浮点数的0是正数。
尾数
   在形如mXb*e的表达式表示的浮点数中,m称为尾数,b是基数,e是指数。尾数部分m是一个十进制小数。大家知道的二进制,十进制和十六进制的加权进位表示法。现在可以把同样的概念扩展到数字的小数部分。例如十进制数123.154可以表示为表达式的和:
123.154=(1X10^2)+(2X10^1)+(3X10^-0)+(1X10^-1)+(5X10^-2)+(4X10^-3)
二进制浮点数的表示方法类似,只要用2为基数计算位权值即可。例如浮点数二进制值11.1011可表示为:
11.1011=(1X2^1)+(1X2^0)+(1X2^-1)+(0X2^-2)+(1X2^-3)+(1X2^-4)
另一种表示小数点后面的值的方法是用以2的幕为分母的分数之和表示,例如 
  .1011=1/2+0/4+1/8+1/16=11/16
得到分数表示的方法是相当直观的。二进制数1011实际上是十进制数11,可用1011(十进制数11)做分数的分子。假设e是二进制小数点后的有效数字的位数,那么分母就是2^e。在该例子中e=4,因此分母2^e=16。下表中给出了另外一些二进制浮点表示转换成十进制分数的例子。表1中的最后一项是23位规格化尾数所以能存储的最小分数。表2中列出了另一些例子,例子中分别给出了二进制浮点数以及与其等价的分数和十进制数。
表1   二进制浮点数转换成分数
二进制浮点数                                           十进制分数值
11.11                                                                          3   3/4
101.0011                                                                    5   3/16
1101.100101                                                            13  37/64
0.00101                                                                           5/32
1.011                                                                         1    3/8
0.00000000000000000000001                                      1/8388608
表2  二进制和十进制分数
二进制浮点数                       十进制分数值                   十进制值
.1                                                    1/2                                         .5
.01                                                  1/4                                         .25
.001                                                1/8                                         .125
.0001                                              1/16                                       .0625
.00001                                            1/32                                       .03125

尾数的精度
     连续实数的整体不可能以有限个数据位的浮点格式表示。例如,假设使用一种简化的格式表示浮点数,其中尾数占5个数据位,那么就没有办法表示二进制数1.1111和二进制数10.0000之间的实数了。因此表示二进制数1.11111就需要更高精度的尾数。类似的结论可以推广到IEEE双精度格式,53位的尾数不能表示需要54或更多个数据位的二进制数字。

指数
    单精度浮点数的指数是以8位无符号正数的格式存储的,实际存储的是指数值与127相加的和。以数字1.101X2^5为例,指数(5)与127相加的和(十进制132,二进制10100010)存储在指数部分。下表给出了一些指数的例子。其中第一列是有符号十进制整数,第二列是调整后的值,最后一列是调整后的整数对应的二进制值。调整后的指数部分总是正数(在1~254之间),
实际指数范围是-126~+127。之所以选择这个范围,是为了避免最小可能的指数的倒数发生溢出(这里由于如果最小的指数选择了-127,那么-127+127=0)
指数(E)                  调整后(E+127)               二进制值
+5                                          132                                      10000100
0                                            127                                       01111111
-10                                         117                                       01110101
+127                                      254                                       11111110
-126                                       1                                           00000001
-1                                           126                                       01111110

二进制浮点数的正规化
     大多数浮点二进制数都是以正规化的格式存储,以便能够使得尾数的精度最大化。对于任何给定的浮点二进制数,可通过移动小数点,使小数点前仅有一个数字“1”从而使其正规化,指数表示小数点向左(正数)或向右移动(负数)的位数。看下面例子
未正规化格式                                       正规化格式
1110.1                                                                 1.1101X2^3
.000101                                                               1.01X2^-4
101001.                                                                1.010001X2^6

非正规化值:二进制浮点正规化操作的逆过程称为逆正规化,可通过移动二进制小数点直到指数为零。如果指数部分使正数n,那么向右移动小数点n位;如果指数部分是负数n,那么向左移动小数点n位。如果需要,开头的空位应以0填充。

IEEE表示法
实数编码
    一旦符号位、指数和尾数域进行了正规化和编码,得到一个完整的二进制IEEE短实数就非常容易了。例如,二进制值1.101X2^0可表示如下:
符号位:0 (因为是正数)
指数  :01111111 (因为指数是0  0+127=127=01111111)
小数  :10100000000000000000000 (因为单精度短实数尾数中小数部分占23位)
   调整后的指数01111111是十进制数127,所有正规化后的二进制尾数在小数点前面都有一个1,因此对该位就无需显式编码了。下表中给出了几个例子:
单精度浮点数位编码的例子
二进制           调整后的指数值      符号、指数和尾数
-1.11                       127                  1      01111111   11000000000000000000000
+1101.101              130                  0      10000010   10110100000000000000000
-.00101                   124                  1      01111100   01000000000000000000000
+100111.0              132                  0      10000100   00111000000000000000000
+0000001101011   120                  0      01111000   10101100000000000000000

IEEE规范中还规定了几类实数和非实数的编码:
正数0和负数0
反向正规化的有限数
正规化的有限数
正无穷和负无穷
非数字值(NaN)
不确定数

Intel浮点单元使用不确定数响应一些无效的浮点操作。

正规化和反向正规化:正规化的有限数是0到无穷大之间的所有可以编码为正规化实数的非0有限值。尽管乍看起来所有非零浮点数都可以正规化,不过事实上在值非常接近于0时这是不可能的。由于指数范围的限制,FPU有可能不能把二进制的小数点移动到正规化的位置。假设FPU的计算结果是1.0101111X2^-129,其指数太小无法在单精度实数中存储,这时将产生一个下溢出异常,可通过向左移动小数点(每次一位)使其正规化,直到指数落在有效的范围内为止:例
1.01011110000000000001111 X 2^-129
0.10101111000000000000111 X 2^-128
0.01010111100000000000011 X 2^-127
0.00101011110000000000001 X 2^-126
在这个例子中,由于移动了小数点,精度会有一些损失。

正无穷和负无穷:正无穷(+∞)表示最大的正实数,负无穷(-∞)表示最小的负实数。可以比较无穷值和其他值:-∞小于+∞,-∞小于任何有限数,+∞大于任何有限数。正无穷和负无穷都能用来表示溢出。

非数字(NaN):NaN是不表示任何有效实数的位序列。IA-32体系结构包含两种类型的NaN:quiet NaN可通过大多数算术运算而不会导致任何异常;signalling NaN可用于产生一个无效操作异常。编译器可以用Signalling NaN值填充未初始化的数组,对该数组进行任何计算的企图都会引发一个异常。quiet NaN可用于存放调试会话产生的诊断信息。浮点单元不会试图对NaN执行操作。IA-32手册详细描述了一套规则,用于确定以这两种类型的NaN组合作为操作数时指令执行的结果。

特殊值的编码:下表列出了浮点操作中经常遇到的几个特殊值的编码。标记位的位置既可以是0,也可以试1。QNaN表示quiet NaN  , SNaN表示 signalling NaN。
特殊的单精度浮点值的编码
值                        符号、指数、尾数
正数0                                0      00000000     00000000000000000000000
负数0                                1      00000000     00000000000000000000000
正无穷                     0     11111111      00000000000000000000000
负无穷                     1     11111111      00000000000000000000000
QNaN                                x     11111111     1xxxxxxxxxxxxxxxxxxxxxxxxx
SNaN                                x      11111111     1xxxxxxxxxxxxxxxxxxxxxxxxx

*SNaN的尾数域以0开始,但是其余x表示的域中至少要有一个为1。

把十进制分数转换为二进制实数
    如果十进制分数可以很容易地表示为1/2+1/4+1/8+......的格式,那么就很容易得到其对应的二进制实数。下表中的左边一列中的大部分分数一眼上去都不容易转换成二进制,不过如果写成第二列的格式就容易多了。
十进制分数和二进制实数的例子
十进制分数               分解为                  二进制实数
1/2                                1/2                               .1
1/4                                1/4                               .01
3/4                                1/2+1/4                       .11
1/8                                1/8                              .001
7/8                                1/2+1/4+1/8               .111
3/8                                1/4+1/8                       .011
1/16                              1/16                            .0001
3/16                              1/8+1/16                    .0011
5/16                              1/4+1/16                    .0101
      很多实数,如1/10(0.1)或1/100(.01),不能用有限个二进制数字位表示,这样的分数只能近似表示为若干以2的幕为分母的分数之和。想想货币值如$39.95会受什么影响。

另一种方法:使用二进制长除法。把十进制分数转换成二进制数时如果涉及到的十进制值比较小,可以使用一种很方便的方法:首先把分子和分母都转换成二进制值,然后再进行除法操作。例如,十进制值0.5可表示成分数5/10,十进制5是二进制值0101,十进制值10是二进制值1010,执行下面的除法,我们发现商是二进制值0.1:
                                  _____.1__________     
                       1010   |  0101.0
                                     - 101 0
                                    ___________
                                              0
       在被除数减去1010后余数为0时,除法终止。因此十进制分数5/10等于二进制值0.1,我们称上面这种方法为二进制长除法。PS来自于DePaul大学的Harvey Nice。
例子:以二进制数表示0.2。下面把十进制的0.2(2/10)转换成二进制值,使用长除法。首先,把二进制值10除以1010(十进制值10):
               ___.00110011(等等)_________________             
         1010 | 10.00000000             
                      1 010
                       __________
                         1100
                         1010
                     ___________
                             10000
                               1010
                      ____________
                                 1100
                                 1010
                      _____________
                                         等等
     第一个足够大的余数是10000,在除以1010之后,余数是110,在末尾添加一个0,新的被除数是1100,除以1010后,余数是10,再添加3个0,新的被除数是10000,这等于进行第一次除法操作时被除数的值。从这点开始,商中重复出现0011,因此我们得知准确的商是无法得到的。0.2不能用有限个二进制位表示。0.2的单精度编码的尾数部分是00110011001100110011001

把单精度值转换成十进制数
下面是在把IEEE单精度值转换成十进制数时的推荐步骤:
1。如果MSB(最高有效位)是1,该值为负数,否则为正数。
2。接下来的8位表示指数,减去二进制值01111111(十进制值127),得到未调整的值数值,把未调整的指数值转换成十进制数。
3。接下来的23位表示尾数,注意尾数小数点前面有一个被省略的“1.”,尾数尾部的0可以忽略。这时就可以使用第一步中得到的符号位、第二步中得到的指数以及本步骤中得到的尾数创建一个浮点二进制数了。
4。对第三步得到的浮点数进行逆规格化操作(根据指数相应地移动小数点,指数为正则右移动,指数为负则左移动)。
5。从左到右,使用位权表示方法得到浮点数的2的次幕之和的表示,进而得到相应的十进制数。
例子:把IEEE浮点数(0  10000010   01011000000000000000000)   转换成十进制数
1. 该数是正数。
2. 未调整的指数值是00000011,也就是十进制数3。
3. 组合符号位、指数和尾数得到二进制数+1.01011X2^3
4. 逆正规化后的二进制数是+1010.11
5. 对应的十进制值是+10   3/4或+10.75

好长的一帖。我打字打的都快晕了。不过这里只是简单介绍了浮点数的转换。当然后面还有应用。今天就先到这里吧。我休息一下。大家也能消化一下。还有表中数值对齐问题是在弄晕我了。希望斑竹帮忙调试一下。谢谢阅读。下一篇介绍FPU   未完待续......     

  • 标 题:答复
  • 作 者:install
  • 时 间:2009-07-27 14:29:21

这两天下雨闹的都没什么时间上网逛逛了。这二伏没有想象中的那么难熬,大家要多注意身体啊。OK多余的话就不说了。现在书接上回。话说上次浅谈了一下浮点数的常用表示方法。这回咱们探讨一下FPU也就是浮点单元。
     
浮点单元
    Intel 8086处理器是为处理整数运算而设计的,不过,事实证明这对于大量需要使用浮点运算的图形处理程序和运算密集型的程序有个严重的限制。尽管可以用纯软件仿真的方法模拟浮点运算,但会导致运算性能严重下降。这种局面从Intel486处理器开始得到了改变。浮点处理硬件集成进了主CPU,这就是称为浮点单元的FPU,Floating Point Unit。
    
浮点寄存器栈
    FPU不使用通用寄存器(EAX,EBX等等),FPU有自己的一套寄存器,称为寄存器栈。FPU从内存中把值加载到寄存器栈,执行计算,然后再把栈上的值存储到内存中。FPU指令以后缀格式计算数学表达式。例如,中缀格式的表达式:(5*6)+4的后缀格式是:5 6 * 4 +
中缀表达式(A+B)*C使用圆括号覆盖了默认的优先级规则(默认的是先乘后加),但其等价的后缀格式不需要圆括号: A B + C *
      表达式栈:在对后缀表达式求值的时候可使用堆栈存放中间值。表1显示了对表达式“5 6 * 4 -”求值所需的步骤,堆栈项以ST(0)和ST(1)标识,ST(0)通常表示堆栈指针指向的位置(栈顶)。
    从左到右                      堆栈                              动作
      5                                        5      --ST(0)                            push 5
         5 6                                     5      --ST(1)                            push 6
                                                   6      --ST(0)
         5 6 *                                  30    --ST(0)                            Mulitply ST(1)by
                                                                                                     ST(0)and pop
                                                                                                     ST(0)off the stack.
         5 6 * 4                               30   --ST(1)                             push4
                                                   4     --ST(0)
         5 6 * 4 -                             26   --ST(0)                            Subtract ST(0) from
                                                                                                     ST(1)and pop
                                                                                                     ST(1)off the stack.
以上就是对后缀表达式求值,堆栈变化过程。

    把中缀表达式转换成后缀表达式的常用方法在网上GOOGLE一下会有很多教程,这里就不重复了。有兴趣的朋友可以自己找来看看。这里给出一些中缀表达式和后缀表达式的例子,供大家参考一下。
中缀格式                                             后缀格式
A+B                                                                         A B +
(A-B)/D                                                                    A B - D /
(A+B)*(C+D)                                                           A B + C D + *
((A+B)/C)*(E-F)                                                       A B + C / E F - *

浮点数据寄存器
    FPU有8个可独立寻址的80位寄存器,分别名为R0,R1,R2......R7。他们以堆栈形式组织在一起,统称为寄存器栈。栈顶由FPU状态字中的一个名为TOP的域(占三个二进制位)来标识,对寄存器的引用都是相对于栈顶而言的。例如下面的例子。TOP等于二进制值011,并指向了R3,这就说明R3是栈顶。在编写浮点指令时栈顶也写成ST(0)或者ST,最后一个寄存器(栈低)写成ST(7)。
                         79                                           0
                                R7                                                    ST(4)
       ^                      R6                                                     ST(3)
      Pop                    R5                                                    ST(2)
                                R4                                                     ST(1)
                                R3                                                     ST(0) <--Top =011
     Push                   R2                                                     ST(7)
      V                     R1                                                     ST(6)
                               R0                                                      ST(5)

注意增长方向。

   压栈(PUSH)操作把指示栈顶的TOP域值减1并把结果复制到由ST(0)标识的寄存器中,如果在压栈操作之前TOP等于0,压栈操作使TOP回滚指示寄存器R7。出栈(POP)操作把ST(0)中的数据复制到一个操作数中并把TOP域的值增1。如果在出栈之前TOP等于7,出栈操作使TOP回滚指示寄存器R0。加载一个值至浮点栈中时如果会覆盖已存在的数据,就会产生一个浮点异常。
   浮点寄存器中的值以10字节的IEEE扩展实数格式(也称为临时实数格式)存储,FPU在内存中保存算术运算的结果时,自动把结果转换为一下格式之一:整数、长整数、单精度(短实数)、双精度(长实数)、或压缩的二进制编码的十进制整数。
    图例显示同一个浮点栈在两个浮点数压栈后的情况。压入1.0和2.0
     在压人1.0后                                                    在压入2.0后  

          79                0                                                                     79         0
          R7                       ST(4)                                         ^            R7                ST(4)
^       R6                        ST(3)                                        pop         R6                ST(3)
Pop   R5                        ST(2)                                                       R5                 ST(2)
         R4                        ST(1)                                                       R4                 ST(1)
         R3                        ST(0) <--Top                                           R3                  ST(0)
Push R2                        ST(7)                                         PUSH     R2                  ST(7)<--TOP
V       R1                        ST(6)                                         V           R1                   ST(6)
         R0                        ST(5)                                                      R0                   ST(5)
这就是同一浮点栈压入两个数据的变化。

特殊用途寄存器:
FPU有6个特殊用途的寄存器。
一个10位的操作码寄存器:存储最后一条执行的非控制指令。
一个16位的控制寄存器:在执行计算时控制FPU的精度和使用的近似方法。
一个16位的状态寄存器:存放着栈顶指针、条件码以及相关异常的警告。
一个16位的标记字寄存器:指示FPU数据寄存器栈中每个寄存器的内容的状态,每个寄存器使用两位指示寄存器是否包含一个有效的数字、零、特殊值(NaN、无穷数、反规格化或不支持的格式)或是否为空。
一个48位的最后指令指针寄存器:存放最后执行的非控制指令的指针。
一个48位的最后数据(操作数)指针寄存器:存放最后执行的指令使用的数据操作数(如果有的话)。
    操作系统在任务切换时使用这些特殊用途的寄存器保存浮点单元的状态信息。

近似
    FPU在进行浮点计算时试图产生准确的结果,不过在许多情况下这是不可能的。因为目的操作数根本就不能准确表示计算的结果。例如,假设某种存储格式只允许3个小数位,这种格式就只能存储1.011或1.101,但不能存储1.0101 如果计算产生的精确结果是+1.0111(十进制1.4375),我们就必须通过加.0001或减去.0001向上或向下近似:(a)1.0111-----> 1.100     (b) 1.0111------->1.011
如果精确结果是负数,那么加-.0001会使近似值趋向-∞,减去-.0001会使近似值趋向0和+∞。
FPU允许选择下面4种近似方法:
1。近似到最接近的偶数:近似结果最接近准确结果,如果有两个值域精确结果近似度相同,则选取最接近的偶数(最低有效位是0)。
2。向下近似趋向于-∞:近似结果小于或等于精确结果。
3。向上近似趋向于+∞:近似结果大于或等于精确结果。
4。近似趋向于0:也称为剪裁,近似结果的绝对值小于或等于精确结果。

FPU控制字:FPU的状态字中包含了一个名为RC的域,包含两个数据位,该域指定使用何种近似方法。该域取值如下:
二进制00:近似到最近的偶数(默认)。
二进制01:向下近似趋向于-∞。
二进制10:向上近似趋向于+∞。
二进制11:近似趋向于0(剪裁)。
近似到最近的偶数是默认的,被认为是最准确的,适合大多数应用程序。下面两个表分别给出了二进制值+1.0111如何应用4种近似方法的例子。和二进制值-1.0111可能的近似值。
                  +1.0111的近似
方法                                  精确结果                                近似后
近似到最后的偶数                      1.0111                                                 1.100
向下近似趋向于-∞                    1.0111                                                 1.011
向上近似趋向于+∞                    1.0111                                                 1.100
近似趋向于0(剪裁)                   1.0111                                                 1.011

                          -1.0111的近似
方法                                  精确结果                                近似后
近似到最近的偶数                      -1.0111                                              -1.100
向下近似趋向于-∞                    -1.0111                                              -1.100
向上近似趋向于+∞                    -1.0111                                              -1.011
近似趋向于0(剪裁)                   -1.0111                                              -1.011

浮点异常
    每个程序都有可能出错,FPU必须能够处理错误。FPU能够识别和检查6种类型的异常:
无效操作#I 、除零#Z、反规格化操作数#D、数值溢出#O、数值下溢#U、无效精度#P。前三种异常(#I #Z #D)是在算术操作发生之前检查的,后三种异常(#O #U #P)是在算术操作发生之后检查的。
    每种异常类型都有相应的标志位和掩码,检测到浮点异常时,处理器设置相应的标志位。对于标识出来的每种异常,根据其掩码位的不用,处理器可采用两种不同的动作:
1。如果相应的掩码位置位,处理器自动处理异常并允许程序继续。
2。如果相应的掩码位清零,处理器调用软件异常处理程序。
    处理器的掩码响应方式对大多数程序而言都是可接受的。定制的异常处理程序可在应用程序要响应特定异常的情况下使用。单条指令可引发多个异常,因此处理器保留自上次异常清除后发生的异常记录,因此在一系列计算完成之后,可以检查是否发生了异常。

浮点指令集
    浮点指令集有些复杂,这里只能简单介绍一下其功能。浮点指令集包含下面几类指令:
数据传送指令
基本的算术运算指令
比较指令
超越指令
常量加载指令(特殊的预定义常量)
x87FPU控制指令
x87FPU和SIMD状态管理指令

    浮点指令总是以字母F开头的。以便于CPU指令区分。指令的第二个字母(通常是B或I)说明了内存操作数应如何理解:B表示二/十进制(BCD)操作数,I表示二进制整数操作数。如果没有指定B或I,就便是操作数是实数格式。例如FBLD对BCD数进行操作,FILD对整数进行操作,FLD对实数进行操作。如果需要详细了解可以参考IA-32浮点指令手册。
    操作数:浮点指令最多可以有两个操作数,也可以无操作数或只有一个操作数。如果有两个操作数,其中一个必须是浮点寄存器。没有立即数操作数,但可加载一些预定义的值(如0.0 ,π派以及Ib 10等)。通用寄存器EAX,EBX,ECX,EDX等不能作为操作数,不允许内存到内存操作。
    整数操作数必须从内存加载至FPU(绝对不能从CPU寄存器中加载),FPU把整数自动转换成浮点数的格式。类似的,在内存中存储浮点值的时候,浮点值自动剪裁或近似取整。

初始化(FINIT)
     FINIT指令初始化浮点单元,把FPU的控制字设为037Fh,掩盖所有的浮点异常,把近似方法设置位最接近的偶数,并把计算精度设为64位。强烈建议在程序的开始调用FINIT以使FPU处于一个固定的初始状态。
浮点数据类型
    下面大家来回想一下MASM支持的浮点数据类型QWORD, TBYTE, REAL4, REAL8, REAL10。
内部数据类型说明
类型                                             用途
QWORD                                                            64位整数
TBYTE                                                               80位(10字节)整数
REAL4                                                              32位(4字节)IEEE短实数
REAL8                                                              64位(8字节)IEEE长实数
REAL10                                                            80位(10字节)IEEE扩展实数
在定义FPU指令时会用到以上数据类型。例如在加载一个浮点便领至FPU堆栈时,变量可以定义为REAL4 REAL8 REAL10:
.data
bigVal REAL10 1.211342342234234233E+587
.code
fld bigVal                              ;加载变量至浮点栈

加载浮点值(FLD)
    FLD(加载浮点值)指令复制一个浮点数至FPU的栈顶[既ST(0)],操作数可以时32位,64位或80位的内存操作数(REAL4 REAL8 REAL10)或另外一个浮点寄存器:
FLD m32fp
FLD m64fp
FLD m80fp
FLD ST(i)

内存操作数的类型:FLD支持的内存操作数的类型和MOV时一样的。下面给出一些例子:
.data
array REAL8 10 DUP(?)
.code
fld array
fld [array+16]
fld REAL8 PTR[esi]
fld array[esi]
fld array[esi*8]
fld array[esi*TYPE array]
fld REAL8 PTR[ebx+esi]
fld array[ebx+esi]
fld array[ebx+esi*TYPE array]
例子:下面的例子加载两个直接操作数至FPU堆栈:
.data
db1One REAL8   234.56
db1Two REAL8   10.1
.code
fld db1One
fld db1Two
下面用图示显示每条指令在执行后堆栈的内容:
fld db1One                    ST(0)    234.56
fld db1Two                    ST(1)    234.56
                                     ST(0)    10.1
在第二条FLD执行的时候,TOP域减1,导致原来标记为ST(0)的元素变成了ST(1)。

  FILD:FILD指令把16位、32位、64位的整数源操作数转换成双精度浮点数并把其加载到ST(0),源操作数的符号位保留,在后面会详细介绍该命令在混合模式算术运算中的运用。FILD支持的内存操作数类型(间接操作数,变址操作数,基址变址操作数等)同MOV指令。
加载常量:下面的指令在堆栈上加载特定的常量,这些指令无操作数:
FLD1指令在寄存器堆栈上压入1.0
FLD L2T指令在寄存器堆栈上压入lb 10
FLD L2E指令在寄存器堆栈上压入lb e
FLDPI指令在寄存器堆栈上压入π
FLDLG2指令在寄存器堆栈上压人lg 2
FLDLN2指令在寄存器堆栈上压入In 2
FLDZ指令在寄存器堆栈上压人0.0

存储浮点值(FST,FSTP)
    FST指令(存储浮点值)复制FPU的栈顶的操作数至内存中,操作撒可以是32位,64位或80位的内存操作数REAL4 REAL8 REAL10或另外一个浮点寄存器:
FST m32fp
FST m64fp
FST ST(i)
FST不会弹出栈顶元素,下面的指令把ST(0)存储到内存中,假设ST(0)等于10.1并且ST(1)等于234.56:
fst dblThree                    ;10.1
fst dblFour                      ;10.1
      单凭直观感觉,我们可能期望dblFour等于234.56,不过由于第一条FST指令把10.1留在ST(0)中,因此结果正好相反。如果想把ST(1)复制到dblFour中,第一条指令就必须用FSTP。
    FSTP:FSTP(存储浮点值并出栈)复制ST(0)至内存中并弹出ST(0),假设在执行下面的指令之前ST(0)等于10.1并且ST(1)等于234.56:
fstp dblThree           ;10.1
fstp dblFour             ;234.56
      在执行之后,从逻辑上讲这两个值已经从堆栈上移除了。从物理上来讲,每次FSTP指令执行后,TOP指针增1,改变了ST(0)的位置。
    FIST(存储整数)指令把ST(0)中的值转换成有符号整数并把结果存储到目的操作数中,值可以存储在字或双字中。具体使用方法在后面的混合模式算术运算中详细讲述。FIST支持的内存操作数格式同FST。

算术运算指令
    基本的算术运算指令在下表列出。算术运算指令支持的内存操作数类型同FLD(加载)和FST(存储),因为操作数类型可以是间接操作数,变址操作说,基址变址操作数等。
                            基本的算术运算指令
指令                                          说明
FCHS                                                             改变符号
FADD                                                             源和目的相加
FSUB                                                             从目的中减去源
FSUBR                                                           源乘以目的
FMUL                                                            目的除以源
FDIV                                                             源除以目的
FDIVR                                                            目的除以源

FCHS和FABS
        FCHS(改变符号)指令把ST(0)中的值的符号变反,FABS(绝对值)指令取ST(0)中值的绝对值,这两条指令都不需要操作数:
FCHS
FABS

FADD FADDP FIADD
        FADD(加法)指令的格式如下,其中m32fp是一个REAL4类型的内存操作数,m64fp是一个REAL8类型的操作数,i是寄存器号:
FADD                                 ;MASM使用无参数的FADD指令执行和无参数的Intel FADDP指令同样的操作
FADD m32fp
FADD m64fp
FADD ST(0),ST(i)
FADD ST(i),ST(0)
无操作数:如果FADD不带操作数那么ST(0)和ST(1)相加,结果临时存储在ST(1)中,然后ST(0)弹出堆栈,最终结果存储在栈顶。下面图示给除无操作数的FADD指令执行的情况。假设堆栈中已经包含了两个变量。
fadd                  之前                ST(1)       234.56
                                                       ST(0)       10.1
                       
                         之后                ST(0)       244.66

寄存器操作数:假设浮点栈的内容和上例相同,下面图示ST(0)和ST(1)相加的过程。
fadd st(1),st(0)         之前                  ST(1)    234.56
                                                                  ST(0)    10.1
                                之后                   ST(1)    244.66
                                                                   ST(0)    10.1
内存操作数:FADD在使用内存操作数时,把操作数和ST(0)相加,下面时一些例子:
fadd mySingle                            ;ST(0)+=mySingle
fadd REAL8 PTR[esi]                  ;ST(0)+=[esi]

FADDP:FADDP指令(相加并出栈)指令在执行完加法后从堆栈上弹出ST(0),其格式如下:
FADD ST(i),ST(0)
下面图示FADDP如何运作的。
faddp st(1),st(0)                  之前                ST(1)      234.56
                                                                          ST(0)      10.1
   
                                            之后                ST(0)      244.66

FIADD:FIADD(与整数相加)指令把源操作数转换成扩展精度浮点数格式,然后再和ST(0)相加。其格式如下:
FIADD m16int
FIADD m32int
例子:
.data
myInteger DWORD 1
.code 
fiadd myInteger                               ;ST(0)+=myInteger

FSUB  FSUBP  FISUB
       FSUB指令从目的操作数中减去源操作数,把差存储在目的操作数中。目的应总是一个FPU寄存器,源可以是FPU寄存器或内存操作数,其操作数格式同:FADD:
FSUB                                ;MASM使用的无参数的FSUB指令执行和无参数的IntelFSUBP指令同样的操作
FSUB m32fp
FSUB m64fp
FSUB ST(0),ST(i)
FSUB ST(i),ST(0)
      除了执行的是减法操作二不是加法操作之外,FSUB的操作和FADD很相似。例如,无操作数的FSUB从ST(1)中减去ST(0),结果临时存储在ST(1)中,然后从堆栈中弹出ST(0),这样最后的结果就保存在栈顶了。FSUB在使用内存操作数时从ST(0)中减去内存操作数,不会弹出栈顶元素:
fsub mySingle                        ;ST(0)-=mySingle
fsub array[edi*8]                   ;ST(0)-=array[edi*8]

FSUBP:FSUBP(相减并出栈)指令在执行完减法后从堆栈中弹出ST(0) MASM支持下面的格式:
FSUBP ST(i),ST(0)

FISUB:FISUB(减整数)指令把源操作数转换成扩展精度的浮点数格式,然后再从ST(0)中减去源操作数:
FISUB m16int
FISUB m32int

FMUL  FMULP   FIMUL
       FMUL指令把源操作数和目的操作数相乘,结果存储再目的操作数中。目的应总是FPU寄存器,源可以时寄存器或内存操作数。其格式同FADD和FSUB是一样的:
FMUL                           ;MASM使用无参数的FMUL指令执行的无参数的Intel FMULP指令同样的操作。
FMUL m32fp
FMUL m64fp
FMUL ST(0),ST(i)
FUML ST(i),ST(0)
除执行的操作是乘法而不是加法之外,FMUL的操作和FADD非常相似。例如,无操作数的FMUL把ST(1)和ST(0)相乘,积临时存储再ST(1)中,然后从堆栈中弹出ST(0),这样最后的结果就保存再栈顶了。FMUL再使用内存操作数时把ST(0)和内存操作数相乘,不会弹出栈顶元素:
fmul mySingle                            ;ST(0)*=mySingle

FMULP:FMULP(相乘并出栈)指令再执行完乘法后从堆栈中弹出ST(0)  MASM支持下面格式:
FMULP ST(i),ST(0)
FIMUL与FIADD的格式基本相同,不过执行的操作是乘法而非加法:
FIMUL m16int
FIMUL m32int

FDIV   FDIVP   FIDIV
      FDIV指令把源操作数和目的操作数相除,结果存储再目的操作数中。目的应总是FPU寄存器,源可以是寄存器或内存操作数。其格式同FADD和FSUB是一样的:
FDIV                   ;MASM使用无参数的FDIV指令执行和无参数的Intel FDIVP指令同样的操作
FDIV m32fp
FDIV m64fp
FDIV ST(0),ST(i)
FDIV ST(i),ST(0)
除执行的操作是除法而不是加法外,FDIV的操作数和FADD非常相似。例如,无操作数的FDIV把ST(1)和ST(0)相除,商临时存储再ST(1)中,然后从堆栈中弹出ST(0),这样最后的结果就保存再栈顶了。FDIV在使用内存操作数时把ST(0)和内存操作数相除,不会弹出栈顶元素。下面代码中dblOne除以dblTwo,商存储在dblQuot中:
.data
dblOne REAL8 1234.56
dblTwo REAL8 10.0
dblQuot REAL8 ?
.code
fld dblOne                                   ;加载至ST(0)
fdiv dblTwo                                 ;ST(0)除以dblTwo
fstp dblQuot                               ;把ST(0)存储到dblQuot中
如果源操作数是0,就会产生一个除零异常。有很多特殊情况,如正无穷、负无穷、正数0、负数0、NaN作为被除数时。这些细节有兴趣的朋友可以翻阅IA-32指令手册。
FIDIV:FIDIV指令把整数源操作数转换成扩展精度的浮点数格式,然后再把ST(0)和源操作数相除,格式如下:
FIDIV m16int
FIDIV m32int

浮点值的比较
    浮点值的比较不能使用CMP指令(执行比较时使用整数减法操作),应该使用FCOM指令。在执行完FCOM指令之后,使用条件跳转指令(JA  JB   JE等等)之前还要执行一些必需的指令。
FCOM,FCOMP,FCOMPP:FCOM指令(比较浮点值)比较ST(0)和源操作数,源操作数可以时内存操作数或FPU寄存器,其格式如下:
指令                                                     描述
FCOM                                                                 比较ST(0)和ST(1)
FCOM m32fp                                                      比较ST(0)和m32fp
FCOM m64fp                                                      比较ST(0)和m64fp
FCOM ST(i)                                                         比较ST(0)和ST(i)

FCOMP的操作数格式和FCOM相同,对于每种类型的操作数,FCOMP执行的动作和FCOM基本相同,不过最后还要从堆栈上弹出ST(0)。FCOMPP和FCOMP基本相同。最后还要再一次从堆栈上弹出ST(0)。
 
    条件码:C3,C2,C0这三个FPU条件码标志说明了浮点值比较的结果。下表中给出。表格的标题栏中列出了各个浮点标志对应的CPU状态标志,这时因为C3,C2,C0分别与零标志、奇偶标志和进位标志在功能上类似。
          FCOM,FCOMP,FCOMPP设置的条件码
条件          C3(零标志)       C2(奇偶标志)    C0(进位标志)    应使用的条件跳转指令
ST(0)>SRC         0                                0                            0                 JA,JNBE
ST(0)<SRC         0                                0                            1                 HB,JNAE
ST(0)=SRC         1                                0                            0                 JE,JZ
无序             1                                1                            1                   无
无序的解释如果抛出了无效算术操作数异常(由于无操作数)并且异常被屏蔽掉了,那么C3,C2,C0的值根据该行设置。
    在比较了两个值并设置了FPU条件码之后,主要的挑战在于找到一种方法以根据条件码分支跳转到目的标号处,这涉及两个步骤:
1。使用FNSTSW指令把FPU状态字送AX
2。使用SAHF指令把AH复制到EFLAGS寄存器中。
    一旦条件码复制到ELFAGS寄存器之后,就可以使用基于零标志、奇偶标志和进位标志的跳转指令了。出了上面表里给出的条件码和对应的跳转指令。对于其他的条件码的组合,还可以使用其他的条件跳转指令,如JAE指令在CF=0时跳转。JBE在CF=1或ZF=1时跳转。JNE在ZF=0时跳转。
例子: 假设有如下C++代码:
double X=1.2;
double Y=3.0;
int N=0;
if(X<Y)
    N=1;
下面是等价的汇编语言代码:
.data
X REAL8 1.2
Y REAL8 3.0
N DWORD 0
.code
;if(X<Y)
;  N=1
      fld    X                    ;st(0)=X
      fcomp Y                  ;compare ST(0)to Y
      fnstsw ax               ;move status word into AX
      sahf                       ;copy AH into EFLAGS
      jnb  L1                   ;X not < Y? skip
      mov N,1                 ;N=1
L1:

基本是这样的。不过在随着CPU的发展对于浮点运算也在变化。对于上面的代码。如果用P6系列CPU处理代码会得到简化。因为Intel的P6系列处理器引入了FCOMI指令,该指令比较两个浮点值并直接设置零标志,奇偶标志和进位标志(P6系列CPU始于奔腾Pro和奔腾II处理器)。FCOMI的格式如下:
FCOMI ST(0),ST(i)
下面就用FCOMI指令重写上面代码:
.code
;if(X<Y)
;  N=1
     fld    Y                       ;ST(0)=Y
     fld    X                       ;ST(0)=X,ST(1)=Y
     fcomi  ST(0),ST(1)     ; compare ST(0) to ST(1)
     jnb   L1                     ;ST(0) not < ST(1)? skip
     mov N,1                    ;N=1
L1:

     FCOMI指令替代了前面代码中的三条指令,不过需要一条额外的FLD指令。FCOMI指令不接受内存操作数。


比较是否相等
    几乎所有的程序入门教材都会警告读者不要去比较浮点值是否相等,这是由于计算过程中的近似可能导致错误。这个问题可以通过计算下面的表达式说明:(sqrt(2.0)*sqrt(2.0))-2.0从数学上讲,这个表达式的结果应该是0。但是世界的结果却出乎你的意料。大约为4.4408921E-016 为什么是这个结果呢下面看看这个表达式在FPU堆栈里的情况。
vall REAL8 2.0
指令                                          FPU堆栈
fld vall                                                          ST(0):+2.0000000E+000
fsqrt                                                             ST(0):+1.4142135E+000
fmul ST(0),ST(0)                                           ST(0):+2.0000000E+000
fsud vall                                                       ST(0):+4.4408921E+016

比较浮点值X和Y是否相等的正确做法是取其差值的绝对值(|x-y|)并和用户自定义的一个小的正数相比较。下面的汇编代码是实现类似功能的。其中使用了一个小正数作为在认为这两个值相等时其差值的临界值:
.data
epsilon REAL8 1.0E-12
val2 REAL8 0.0
val3 REAL8 1.001E-13
.code
;if(val2==val3),display"Values are equal".
fld epsilon
fld val2
fsub val3
fabs
fcomi ST(0),ST(1)
ja skip
mWrite <"Values are equal",0dh,0ah>
skip:

下面给出了程序执行过程和每条指令执行后浮点栈的情况
指令                             FPU堆栈
fld epsilon                                  ST(0):+1.0000000E-012
fld val2                                       ST(0):+0.0000000E+000
                                                   ST(1):+1.0000000E-012
fsub val3                                     ST(0):-1.0010000E-013
                                                   ST(1):+1.0010000E-012
fabs                                             ST(0):+1.0010000E-013
                                                    ST(1):+1.0000000E-012
fcomi ST(0),ST(1)                         ST(0)<ST(1);   CF=1, ZF=0

如果重定义val3,使其大于临界值,则val3和val2将不再相等:
val3 REAL8 1.001E-12                  ;不相等

异常的同步
    CPU和FPU分别是独立的单元,因此浮点指令可以和整数及系统指令同时执行,这称为并行。并行执行浮点指令在发生未屏蔽的异常时可能会导致问题,屏蔽的异常不会导致问题。因为FPU总是会执行完当前的操作并储存结果。
    未屏蔽的异常发生时,当前执行的浮点指令中断,FPU产生异常事件信号。下一条浮点指令或FWAIT(WAIT)指令要执行时,FPU检查是否有未决异常,如果有,则调用浮点异常处理程序。(一个子过程)。
    如果产生异常的浮点指令后跟的是一条整数指令或系统指令又会出现什么情况呢?很遗憾,如果是这种情况指令不会检查未决异常它们将立即执行。假设第一条浮点指令要把其输出存储到一个内存操作数中,第二条整数指令修改该内存操作数,如果第一条指令发生异常,那么异常处理程序就不能执行,这通常会导致错误的结果。例如:
.data
intVal DWORD 25
.code
fild intVal                                         ;存储ST(0)至intVal
inc intVal                                         ;整数值增1

       WAIT和FWAIT指令正是用来强制处理器在执行下一条指令之前检查未决的未屏蔽浮点异常的,这两条指令能解决这里潜在的同步问题。在下面的代码中,直到异常处理程序执行完后INC指令才能执行:
fild intVal                         ;存储ST(0)至intVal
fwait                               ;等待未决异常
inc intVal                         ;整数值增1

好了说了这么多下面大家来阅读几个浮点算术运算指令的例子代码。
表达式
      下面编码实现表达式valD=-valA+(valB*valC)按部就班的方法是:加载valA至浮点栈并求反,加载valB至ST(0),这时-valA保存在ST(1)中,valC和ST(0)相乘,乘积保存在ST(0)中,ST(0)和ST(1)相加并把和存储在valD中。代码如下:
.data 
valA REAL8 1.5
valB REAL8 2.5
valC REAL8 3.0
valD REAL8 ?
.code
fld  valA                                ;ST(0)=valA
fchs                                      ;改变ST(0)中值的符号
fld valB                                 ;加载valB至ST(0)
fmul valC                              ;ST(0)*=valC
fadd                                     ;ST(0)+=ST(1)
fstp valD                              ;存储ST(0)至valD中


数组之和
     下面代码计算一个双精度实数数组之和并显示:
ARRAY_SIZE=20
.data
sngArray REAL8 ARRAY_SIZE DUP(?)
.code 
        mov  esi,0                                     ;数组的索引
     fldz                                                ;在浮点栈上压入0.0
       mov  ecx,ARRAY_SIZE                    
L1:  fld sngArray[esi]                            ;加载内存操作数至ST(0)
       fadd                                               ;ST(0)和ST(1)相加后ST(0)出栈
    add esi,TYPE REAL8                        ;下一个数组元素
    loop L1
      call WriteFloat                                ;显示ST(0)中的和


平方根之和
    FSQRT指令计算ST(0)的平方根并把结果存储在ST(0)中,下面的代码计算了两个平方根之和:
.data
valA REAL8 25.0
valB REAL8 36.0
.code
fld valA                                         ;PUSH valA
fsqrt                                             ;ST(0)=sqrt(valA)
fld valB                                         ;PUSH valB
fsqrt                                             ;ST(0)=sqrt(valB)
fadd                                             ;add ST(0),ST(1)


数组的点积
    下面的代码计算表达式(array[0]*array[1])+(array[2]*array[3]),这种计算有时也称未点积。(dot product)。下面给出了每条指令执行后FPU栈的内容。
指令                                             FPU堆栈
fld array                                                            ST(0):+6.0000000E+000
fmul[array+4]                                                    ST(0):+1.2000000E+001
fld[array+8]                                                       ST(0):+4.5000000E+000
                                                                         ST(1):+1.2000000E+001
fmul[array+12]                                                 ST(0):+1.4400000E+001
                                                                         ST(1):+1.2000000E+001
fadd                                                                  ST(0):+2.6400000E+001


混合模式算术运算
    到现在为止,涉及到的算术运算至包含实数,应用程序经常涉及到混合算术运算:同时包括整数和实数的运算。ADD和MUL等整数算术运算指令不能处理实数,因为唯一的选择就是使用浮点指令。Intel指令集中提供了提升整数至实数的指令以及把值加载至浮点栈的指令。
例子:下面的C++代码把一个整数和一个双精度数相加,其和储存在一个双精度数中。在执行加法之前C++自动把整数提升到实数:
int N=20;
double X=3.5;
double Z=N+X;
下面汇编代码和上面C++代码等价:
.data
N SDWORD 20
X REAL8 3.5
Z REAL8 ?
.code
fild N                          ;加载整数至ST(0)中
fadd X                       ;内存操作数和ST(0)相加
fstp Z                        ;存储ST(0)至内存操作数中


例子:下面C++代码把N提升成双精度数,然后计算实数表达式的值,最后再把结果存储到一个整数变量中:
int N=20;
double X=3.5;
int Z=(int)(N+X);
Visual C++生成的代码在Z中存储剪裁的结果之前调用了一个转换函数FTOL。如果以汇编语言编写实现该表达式的代码,就可以使用FIST替代函数FTOL,Z向上近似(默认)到24。代码如下:
fild N
fadd X
fist Z

改变近似模式:FPU控制字的RC域允许指定近似的类型。可使用FSTCW把控制字存储到一个变量中,修改RC域(位10和位11),然后再使用FLDCW指令把变量加载回控制字中:
fstcw ctrlWord                                 ;存储控制字
or ctrlWord,110000000000b           ;设置RC=剪裁方式
fldcw ctrlWord                                 ;加载控制字
对于前面的例子,如果使用剪裁的近似方法执行计算,得到的结果是Z=23:
fild N
fadd X
fist Z
此外,还可以重置近似模式至默认模式(近似到最近的偶数):
fstcw ctrlWord                                 ;存储控制字
and ctrlWord,001111111111b        ;重置近似模式至默认模式
fldcw ctrlWord                                 ;加载控制字


屏蔽和未屏蔽的异常
     浮点异常默认是屏蔽的,因此在浮点异常发生时,处理器给结果赋一个默认值并继续安静地执行。例如,浮点数除0地寄过是无穷大而不会终止程序:
.data
val1 DWORD 1
val2 REAL8 0.0
.code
fild val1                      ;加载整数至ST(0)中
fdiv val2                     :ST(0)=正无穷大
    如果在FPU控制字中未屏蔽异常,处理器将进入执行合适地异常处理程序。关闭异常屏蔽是通过清除FPU控制字中向合适地位完成的具体FPU状态字中域在下表给出。假设想要关闭对除零异常的屏蔽,下面是所需步骤:
1。存储FPU控制字至一个16位变量中
2。清除位2(除零标志)
3。加载变量至控制字中

下面代码关闭对除零异常的屏蔽:
.data
ctrlWord WORD ?
.code
fstcw ctrlWord                                                 ;获取控制字
and ctrWord,1111111111111011b                 ;关闭对除零异常的屏蔽
fldcw ctrlWord                                                 ;加载回FPU中

下表是具体FPU状态字中域的具体解释:
位                                  描述
0                                                    无操作异常屏蔽位
1                                                    反规格化操作数异常屏蔽位
2                                                    除零异常屏蔽位
3                                                    溢出异常屏蔽位
4                                                    下溢异常屏蔽位
5                                                     精度异常屏蔽位
8~9                                               精度控制
10~11                                           近似控制
12                                                  无穷大控制

现在,如果执行下面的除零代码,就会产生一个未屏蔽的异常:
fild val1
fdiv val2            ;除零
fst val2
FST指令一开始执行,MS-Windows就会显示一个对话框提示错误。

屏蔽异常:要屏蔽某种异常,应设置FPU控制字中的相应位,下面的代码屏蔽了除零异常:
.data
ctrlWord WORD?
.code
fstcw ctrlWord                           ;获取控制字
or ctrlWrord,100b                      ;屏蔽除零异常
fldcw ctrlWord                           ;加载回FPU中

终于把浮点处理的指令编码介绍完了。天气闷热,阴天又不下雨。至此对于浮点处理和指令编码就都介绍完了。希望大家多提意见。共同探讨和学习。这部分就到这里了。可能很多人会对OD里翻译出来的汇编码不陌生,但是对于每条汇编码对应的机器码是怎么形成的就不太清除了。比如:PUSH CX这条汇编语句的机器指令就是50 51为什么会是50 51而不是别的呢?请关注下回 Intel指令编码这里将会给你问题的答案。谢谢大家阅读本文。最后对于排版我实在搞不定了。有些图表可能会看不出来是什么最好复制进文本文档里加以空格隔开。好像他们都挨在一起了。