简介 浮点处理器也称x87处理器,在 486 之前是和 386 处理器分开的。到了 486 之后就把浮点处理器集成到了 x86 中。浮点处理器的功能就是处理浮点数的运算。浮点数运算主要是了解一些浮点数指令,在逆向过程中这块内容基本不会关注,只要知道浮点数结果是多少就行。
浮点数二进制表示 浮点数由三部分构成:符号、尾数和指数。以数字 -123.154 为例,该校书分解成 -1.23154 x 10^2为例,其中 “-“是符号,表示该浮点数是负数,1.23154是尾数,2是指数。
IEEE 二进制浮点数的表示
符号 二进制浮点数的符号由一个符号位表示,如果该位为 1 表示附属,为 0 表示正数。浮点数 0 是正数。
尾数
尾数的精度
指数
二进制浮点数的正规化
IEEE 表示法
把十进制分数转换为二进制实数
浮点单元
浮点寄存器栈
浮点数据寄存器
特殊寄存器的用途
近似 FPU 在进行浮点计算时试图产生准确的结果,步过在许多情况下这是不可能的,因为目的操作数根本就不能准确表示计算的结果。例如:假设某种存储格式只允许3个小数位,这种格式就只能存储 1.011 或 1.101,但不能存储1.0101。如果计算产生的精确结果是 +1.0111(十进制数1.4275),我们就必须通过加 .0001 或减去 .0001 向上或向下近似: (a) 1.0111 —> 1.100 (b) 1.0111 —> 1.011
如果精确结果是负数,那么加 -.0001 会使近似值趋向 -∞,减去 -.0001 会使近似值趋向 0 和 +∞
浮点异常
浮点指令集
初始化(FINIT) FINIT指令初始化浮点单元,把 FPU 的控制字设为 037Fh,掩盖所有的浮点异常,把近似方法设置为最接近的偶数,并把计算精度设为 64 位。一般程序的开始会调用 FINIT,使 FPU 处于一个固定的初始状态。
浮点数据类型 在定义 FPU 指令使用的数据类型时,会用到这些类型的数据。例如:在加载一个浮点变量至 FPU 堆栈时,变量可定义为 REAL4,REAL8,REAL10。
加载浮点值(FLD) FLD(加载浮点值)指令复制一个浮点数至 FPU 的栈顶 [即ST(0)],操作数可以是 32 位、64 位、或 80 位的内存操作数(REAL4、REAL8、REAL10)或另外浮点寄存器:
1 2 3 4 FLD m32fp FLD m64fp FLD m80fp FLD ST(i)
内存操作数类型:FLD 支持的内存操作数的类型和 MOV 是一样的。下面的一些例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 ;--------------------------- ;程序名:fld.asm ;功能:演示 FLD 加载浮点值指令 ;作者:9unk ;编写时间:2023-4-25 ;---------------------------- INCLUDE Irvine32.inc .data array REAL8 10 DUP(?) dblOne REAL8 234.56 dblTwo REAL8 10.1 .code main PROC 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堆栈 fld dblOne fld dblTwo main ENDP END main
FILD 指令 FILD 指令把 16 位、32位、64位的整数源操作数转换成双精度浮点数并把其加载到 ST(0),源操作数的符号位保留。FILD支持的内存操作数类型(间接操作数、变址操作数、基址变址操作数等)同 MOV 指令。
加载常量: 下面的指令在堆栈上加载特定的常量,这些指令无操作数:
FLD1 指令在寄存器堆栈上压入 1.0
FLDL2T 指令在寄存器堆栈上压入 1b 10(即log2 10)
FLDL2E 指令在寄存器堆栈上压入 1b e(即log2 e)
FLDPI 指令在寄存器堆栈上压入 Π(圆周率)
FLDLG2 指令在寄存器堆栈上压入 lg 2(即log10 2)
FLDLN2 指令在寄存器堆栈上压入 ln 2(即log2 e)
FLDZ 指令在寄存器堆栈上压入 0.0(把 +0.0 压入压入堆栈中)
存储浮点值(FST、FSTP) FST 指令(存储浮点值)复制FPU的栈顶的操作数至内存中,操作数可以是 32 位、64 位或 80 位的内存操作数(REAL4、REAL8、REAL10)或另外一个浮点寄存器:
1 2 3 FST m32fp FST m64fp FST ST(i)
FST 不会弹出栈顶元素,下面的指令把 ST(0) 存储到内存中,如下面的例子 ST(0) 等于 10.1 并且 ST(1) 等于 234.56:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 ;--------------------------- ;程序名:fst.asm ;功能:演示 FST 把 ST(0) 存储到内存中 ;作者:9unk ;编写时间:2023-4-26 ;---------------------------- INCLUDE Irvine32.inc .data dblOne REAL8 234.56 dblTwo REAL8 10.1 dblThree REAL8 0.0 dblFOur REAL8 0.0 .code main PROC FINIT ;浮点单元初始化 ;加载两个直接操作数至FPU堆栈 fld dblOne fld dblTwo ;FST存储浮点值 fst dblThree fst dblFOur main ENDP END main
FSTP: 复制 ST(0) 至内存并弹出 ST(0),如下面的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 ;--------------------------- ;程序名:fst.asm ;功能:演示 FSTP 把 ST(0) 存储到内存中,并弹出ST(0) ;作者:9unk ;编写时间:2023-4-26 ;---------------------------- INCLUDE Irvine32.inc .data dblOne REAL8 234.56 dblTwo REAL8 10.1 dblThree REAL8 0.0 dblFOur REAL8 0.0 .code main PROC FINIT ;浮点单元初始化 ;加载两个直接操作数至FPU堆栈 fld dblOne fld dblTwo ;FST存储浮点值 fst dblThree fst dblFOur ;FSTP 存储浮点值 fstp dblThree fstp dblFOur main ENDP END main
执行后,从逻辑上来讲两个值已经从堆栈上移除了。从物理上来讲,每次 FSTP 指令执行后,TOP指针增1,改变了 ST(0) 的位置。
FIST:把 ST(0) 中的值转换成有符号数整数并把结果存储到目的操作数中,值可以存储在字或双字中。FIST 支持的内存操作数格式同 FST
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 ;---------------------------------------------------- ;程序名:fst.asm ;功能:演示 FIST 把 ST(0) 中的值转换成有符号整数并把结果存储到目的操作数中,值可以存储在字或双字中。 ;作者:9unk ;编写时间:2023-4-26 ;---------------------------------------------------- INCLUDE Irvine32.inc .data dblOne REAL8 234.56 dblTwo REAL8 10.1 dblThree REAL8 0.0 dblFOur REAL8 0.0 num dw 0 .code main PROC FINIT ;浮点单元初始化 ;加载两个直接操作数至FPU堆栈 fld dblOne fld dblTwo ;FST存储浮点值 fst dblThree fst dblFOur ;FIST 存储浮点值 fist num fstp dblFOur fist num main ENDP END main
算数运算指令 基本的算数运算指令如下表所示:
算数运算指令支持的内存操作数类型同 FLD(加载)和 FST(存储),因此操作数类型可以是间接操作数、变址操作数、基址变址操作数等。
FCHS 和 FABS
FADD,FADDP,FIADD
FSUB,FSUBP,FISUB FSUB 指令从目的操作数中减去源操作数,把差存储到目的操作数中。目的操作数是一个 FPU 寄存器,源可以是 FPU 寄存器或内存操作数,其操作数格式同FADD:
1 2 3 4 5 FSUB FSUB m32fp FSUB m64fp FSUB ST(0),ST(i) FSUB ST(i),ST(0)
FMUL,FMULP,FIMUL
FDIV,FDIVP,FIDIV
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ;---------------------------------------------------- ;程序名:fdiv.asm ;功能:演示 FDIV 指令 ;作者:9unk ;编写时间:2023-4-25 ;---------------------------------------------------- INCLUDE Irvine32.inc .data dblOne REAL8 234.56 dblTwo REAL8 10.1 dblQuot REAL8 ? .code main PROC FINIT ;浮点单元初始化 ;加载两个直接操作数至FPU堆栈 fld dblOne fdiv dblTwo fstp dblQuot main ENDP END main
浮点值比较 浮点值比较不能使用 cmp 指令,应使用 FCOM 指令。 在执行完 FCOM 指令之后,使用条件跳转指令之前还要执行一些必须的指令。 FCOM,FCOMP,FCOMPP:指令比较 ST(0) 和 源操作数,源草组数可以是内存操作数或 FPU 寄存器,其格式如下:
条件码: C3,C2,C0 这三个 FPU 条件码标志说明了浮点值比较的结果,下表中的标题栏列出了各个浮点标志对应的 CPU 状态标志,这是因为 C3,C2,C0 分别与零标志、奇偶表示和进位标志在功能上类似。
例子:假如下 C++ 代码:
1 2 3 4 5 6 7 double X = 1.2 ;double Y = 3.0 ;int N = 0 ;if (X < Y>) { N = 1 ; }
反汇编:
在 C/C++ 中用的是 CMOISD 指令,COMISD 指令可以接受内存操作数,且比较结果会直接传到 EFLAGS 寄存器中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 ;--------------------------- ;程序名:fcmop.asm ;功能:演示浮点比较指令 fcmop ;作者:9unk ;编写时间:2023-5-4 ;---------------------------- INCLUDE Irvine32.inc .data X REAL8 1.2 Y REAL8 3.0 N DWORD 0 .code main PROC fld X fcomp Y fnstsw ax ;把 FPU 状态字传送到 AX 寄存器中 sahf ;把 AH 复制到 EFLAGS 寄存器中 jnb L1 mov N,1 L1: exit main ENDP END main
P6的改进: 对于前面的例子,需要注意的是:浮点数比较比整数比较运行时间开销更大,因此 Intel 的 P6 处理器引入了 FCOMI 指令,该指令比较两个浮点值并直接设置零标志、奇偶标志和进位标志。FOMI 的 格式如下:
FCOMI ST(0),ST(i)
下面使用 FCOMI 指令重写前面例子的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 ;--------------------------- ;程序名:fcmoi.asm ;功能:演示浮点比较指令 fcmoi ;作者:9unk ;编写时间:2023-5-4 ;---------------------------- INCLUDE Irvine32.inc .data X REAL8 1.2 Y REAL8 3.0 N DWORD 0 .code main PROC ;if(X<Y),注意 X 要放在 ST(0) 的位置 fld Y fld X fcomi ST(0),ST(1) jnb L1 mov N,1 L1: exit main ENDP END main
FCOMI 指令代替了前面例子中的三条指令,不过需要一套额外的 FLD 指令。FCOMI 指令不接受内存操作数。
比较是否相等
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 ;--------------------------- ;程序名:fequal.asm ;功能:演示比较两个浮点数是否相等 ;作者:9unk ;编写时间:2023-5-4 ;---------------------------- INCLUDE Irvine32.inc INCLUDE macros.inc .data epsilon REAL8 1.0E-12 val2 REAL8 0.0 ;val3 REAL8 1.001E-13 val3 REAL8 1.001E-12 ;如果使 val3 大于临界值,则val3和val2将不再相等 .code main PROC ;if(X<Y),注意 X 要放在 ST(0) 的位置 fld epsilon fld val2 fsub val3 fabs fcomi ST(0),ST(1) ja skip mWrite <"Values are equal",0dh,0ah> skip: exit main ENDP END main
读写浮点值
例子程序:下面的例子程序在 FPU 堆栈上压入两个浮点值,然后显示,接下来读入两个用户输入的值,相乘并显示其乘积:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 ;--------------------------- ;程序名:floatTest32.asm ;功能:下面的例子程序在 FPU 堆栈上压入两个浮点值,然后显示,接下来读入两个用户输入的值,相乘并显示其乘积 ;作者:9unk ;编写时间:2023-5-4 ;---------------------------- INCLUDE Irvine32.inc INCLUDE macros.inc .data first REAL8 123.456 second REAL8 10.0 third REAL8 ? .code main PROC finit ;初始化 FPU ;压入两个浮点数到 FPU 栈中,并显示 FPU 栈 fld first fld second call ShowFPUStack ;输入两个浮点是并显示其乘积 mWrite "Please enter a real number: " call ReadFloat mWrite "Please enter a real number: " call ReadFloat fmul ST(0),ST(1) ;相乘 mWrite "Their product is: " call WriteFloat call Crlf exit main ENDP END main
异常的同步
代码示例
混合模式运算
屏蔽和未屏蔽异常 浮点异常默认是屏蔽的,因此在浮点异常发生时,处理器给结果赋一个默认值,并继续安静地执行。
因此我在异常的程序这一块,试验的两个例子都没出现报错,我一开始还以为是系统比较新的原因。
如果在 FPU 控制字中未屏蔽异常,处理器将自动执行合适的异常处理程序。关闭异常屏蔽是通过清除 FPU 控制字中向合适的位完成的。想要关闭对除 0 异常的屏蔽,需要执行下面的步骤:
存储 CPU 控制字到一个 16 位变量中
清除位 2(除零标志位)
加载变量到控制字中
案例:屏蔽除零异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 ;--------------------------- ;程序名:floaterr.asm ;功能:浮点异常默认是屏蔽的,演示关闭浮点异常 ;作者:9unk ;编写时间:2023-2-13 ;---------------------------- INCLUDE Irvine32.inc .data ctrlWord WORD ? val1 DWORD 1 val2 REAL8 0.0 .code main PROC fstcw ctrlWord ;获取控制字 and ctrlWord,1111111111111011b ;关闭对除零异常的屏蔽 fldcw ctrlWord ;加载会 FPU 中 fild val1 fdiv val2 fst val2 exit main ENDP END main