能力值:
( LV4,RANK:50 )
4 楼
这两天下雨闹的都没什么时间上网逛逛了。这二伏没有想象中的那么难熬,大家要多注意身体啊。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指令编码这里将会给你问题的答案。谢谢大家阅读本文。最后对于排版我实在搞不定了。有些图表可能会看不出来是什么最好复制进文本文档里加以空格隔开。好像他们都挨在一起了。
能力值:
( LV2,RANK:10 )
8 楼
文章很好,能否提供一个WORD的格式,或PDF格式的文件呢,论坛贴的代码有点乱的感觉
能力值:
( LV3,RANK:20 )
14 楼
扩展精度的指数位应是15位, 尾数中的小数部分确实是63位(bit0-bit62), 还有1位(bit63)是尾数中的整数位, 这是与单精度和双精度格式不同的地方(单精度与双精度隐含认为尾数的整数部分一直为1)。请楼主核实。