汇编之浮点数处理(CrackMe003前置知识)

版权声明:本文作者为鬼手56,您可在网络上任意转载,但请注明出处、作者并保持文章的完整性,谢谢 https://blog.csdn.net/qq_38474570/article/details/87872839

浮点数的二进制表示

十进制浮点数有三个部分组成:符号,有效数字和阶码。比如,在-1.23154*105中,符号为负,有效数字为1.23154,阶码为5

IEEE二进制浮点数的表示

x86处理器使用的三种浮点数二进制存储格式都是由IEEE标准754-1985——二进制浮点运算一一所制定。下表列出了他们的特点

精度 范围
单精度 32位:1位符号位,8位阶码,23位为有效数字的小数部分。大致的规格化范围:2-126—2127 也被称为短实数
双精度 64位:1位符号位,11位阶码,52位为有效数字的小数部分。大致的规格化范围为:2-1022—21023 也被称为长实数
扩展双精度 80位:1位符号位,15位阶码,1位为整数部分,63位为有效数字的小数部分。大致的规格化范围:2-16382—216383 也被称为扩展实数

由于三种格式比较相似,因此本节将重点关注单精度格式。

1.符号位

如果符号位为1,则该数为负;如果符号位为0,则该数为正。

2.有效数字

浮点数的有效数字由小数点的左右的十进制数字构成。十进制的数123.154用加权位计数法可以表示为下面的累加和形式

123.154=(1x102)+(2x101)+(3x100)+(1x10-1)+(5x10-2)+(4x10-3)

小数点左边的数字的阶码都位正,右边的数字阶码都为负

小数点右边数字还有一种表达方式,即把他们列为分数之和,其中分母为2的幂,例如:

.1011=1/2+0/4+1/8+1/16=11/16

3.有效数字的精度

用有限位数表示的任何浮点数都无法表示完整的连续的实数。例如:假设一个简单的浮点数格式有5位有效数字,那么将无法表示范围在1.1111-10.000之间的二进制数。比如,二进制数1.11111就需要更精确的有效数字。将这个思想扩展到IEEE双精度格式,就会发现53位有效数字无法表示需要54位或更多二进制数值。

阶码

单精度数用8位无符号整数存放阶码,引入的偏差为127,因此必须在数的实际阶码上再加上127

规格化二进制浮点数

大多数二进制浮点数都以规格化格式存放,以便将有效数字的精度最大化。给定任意二进制浮点数,都可以进行规格化,方法是将小数点移位,直到小数点左边只有一个1。阶码表示的是二进制小数点向左或向右移动的位数。示例如下:

非规格化 规格化
1110.1 1.1101x23
000101 1.01x2-4
1010001 1.010001x2-6

反规格化数:规格化操作的逆操作是将二进制浮点数反规格化。移动二进制小数点,直到阶码为0。如果阶码为正数,则将小数点右移,如果阶码为负数,则将二进制小数点左移,并在需要的位置前填充导数0。

新建IEEE表示

实数编码

一旦符号位 阶码和有效数字字段完成格式化和编码后,生成一个完整的二进制IEEE段实数就很容易了。首先设置符号位,然后是阶码字段,最后是有效数字部分。例如:下面表示的是二进制1.101x20

  • 符号位:0
  • 阶码:01111111
  • 小数部分:10100000000000000000000

偏移码(01111111)是十进制数127的二进制形式。所有规格化有效数字在二进制小数点的左边都有个1,因此,不需要对这一位进行显示编码。

单精度数位编码示例

二进制数值 偏移阶码 符号 阶码 小数部分
-1.11 127 1 01111111 11000000000000000000000
1101.101 130 0 10000010 10110100000000000000000

IEEE规范包含了多钟实数和非数字编码

  • 正零和负零
  • 非规格化有限数
  • 规格化有限数
  • 正无穷和负无穷
  • 非数字
  • 不定数

规格化和非规格化: 规格化有限数是指所有非零有限值,这些数能被编码为零到无穷之间的规格化实数。尽管看上去全部有限非零浮点数都应被规格化,但若数值接近于零,则无法规格化,当阶码范围造成的限制使得FPU不能将二进制小数点移动到规格化位置时,就会发生这种情况。假设FPU计算结果为1.0101111x2-129,其阶码太小,无法用单精度数形式存放。此时产生一个下溢异常,数值则每次将二进制小数点左移一位逐步进行非规格化,直到阶码达到有效范围

正无穷和负无穷:正无穷表示最大正实数,负无穷表示最大负实数。无穷可以和其他数值比较。负无穷小于正无穷,负无穷小于任意有限实数。任一无穷都可以表示浮点溢出条件。运算结果不能格式化的原因是,结果的阶码太大而无法用有效阶码的位数来表示。

NaN:NaN是不表示任何有效实数的位模式

特定编码:在浮点运算中,常常会出现一些特定的数值编码

单精度数转换为十进制

IEEE单精度数转换为十进制时,建议步骤如下:

  1. 若MSB为1,该数为负,否则该数为正
  2. 其后8位为阶码。从中减去127,生成无偏差阶码,将无偏差阶码转换为十进制
  3. 其后23位表示有效数字。添加1. 后面紧跟有效数字位,尾随零可以忽略。用形成的有效数字,第一步得到的符号和第二步算出来的阶码,就构成了一个二进制浮点数
  4. 对第三步生成的二进制数进行非格式化(按照阶码的值移动二进制小数点,如果阶码为正,则右移,如果阶码为负,则左移)
  5. 利用权位计数法,从左到右,将二进制浮点数转换为2的幂之和,形成十进制数

示例IEEE(0 10000010 01011000000000000000000)转换为十进制:

  1. 该数为正数
  2. 无偏差阶码的二进制值为11 十进制为3
  3. 将符号 阶码和有效数字组合起来即得该二进制数为1.01011x23
  4. 非规格化二进制数为1010.11
  5. 则该数的十进制值为10.75

浮点单元

Inter8086处理器设计使之只能处理整数运算。这对于使用浮点运算的图形和计算密集型软件来说就变成了麻烦。尽管也可以纯粹地通过软件来模拟浮点运算,但这样会带来严重的性能损失

FPU寄存器栈

FPU不使用通用寄存器,反之,它有自己的一组寄存器,称为寄存器栈。数值从内存加载到寄存器栈,然后执行计算,再将堆栈数值保存到内存。FPU指令用后缀形式计算算术表达式,这和惠普计算器的方法大致相同。比如,现有一个中缀表达式:(5*6)+4,其后缀表达式为:5 6 *4 +

中缀表达式(A+B)*C要用括号来覆盖默认的优先规则,与之等效的后缀表达式则不需要括号:A B + C *

中缀转为后缀的例子

中缀 后缀 中缀 后缀
A+B AB+ (A+B)*(C+D) AB+CD+*
(A-B)/D AB-D/ ((A+B)/C)*(E-F) AB+C/EF-*

表达式堆栈:在计算后缀表达式的过程中,用堆栈来保存中间结果

FPU寄存器

FPU有8个独立的 可寻址的80位数据寄存器R0-R7,这些寄存器合称为寄存器栈。FPU状态字中名为TOP的一个3位字段给出了当前处于栈顶的寄存器编号。例如 当TOP=011时 表示栈顶为R3。在编写浮点指令时,这个位置也称为ST(0)。最后一个寄存器为ST(7)

如同想的一样,入栈操作将top-1,并把操作数复制到标识为ST(0)的寄存器中,如果在入栈之前,TOP等于0,那么TOP就回绕到寄存器R7。出栈操作把ST(0)的数据复制到操作数,再将TOP+1。如果在出栈之前TOP=7,则TOP就回绕到寄存器R0。如果加载到堆栈的数值覆盖了寄存器栈内的原有数据,就会产生一个浮点异常

尽管理解FPU如何利用一组有限数量的寄存器实现堆栈很有意思,但这里只需要关注ST(n),其中ST(0)总是表示栈顶。从这里开始,引用栈寄存器时将使用ST(0) ST(1),以此类推。指令操作数不能直接引用寄存器编号

寄存器中浮点数使用的是IEEE10字节扩展实数格式,也被称为临时实数。当FPU把算术运算结果存入内存时,它会把结果转换成如下格式之一:整数 长整数 单精度 双精度 或者压缩二进制编码的十进制数

专用寄存器

FPU有6个专用寄存器

  • 操作码寄存器:保存最后执行的非控制指令的操作码
  • 控制寄存器:执行运算时,控制精度以及FPU使用的舍入方法,还可以用这个寄存器来屏蔽单个浮点异常
  • 状态寄存器:包含栈顶指针 条件码和异常警告
  • 标识寄存器:指明FPU数据寄存器栈内每个寄存器的内容。其中每个寄存器都用两位来表示该寄存器包含的是一个有效数 零 特殊数值还是为空
  • 最后指令指针寄存器:保存指向最后执行的非控制指令的指针
  • 最后数据(操作数)指针寄存器:保存指向数据操作数的指针,如果存在那么该数被最后执行的指令所使用

舍入

FPU尝试从浮点运算中产生非常精确的运算结果,但是在很多情况下这是不可能的,因为目标操作数可能无法精确表示计算结果。FPU可以在四种舍入方法中进行选择

  1. 舍入到最接近的偶数
  2. 向负无穷舍入
  3. 向正无穷舍入
  4. 向0舍入

FPU控制字

FPU控制字用两位指明使用的舍入方法,这两位被称为RC字段。字段数值如下:

  • 00:舍入到最接近的偶数(默认)
  • 01:向负无穷舍入
  • 10:向正无穷舍入
  • 11:向0舍入

浮点数异常

每个程序都可能出错,而FPU就需要处理这些结果。因而,它要识别并检测6种类型的异常条件:无效操作 除零 非规格化操作数 数字上溢 数字下溢以及模糊精度。前三个在全部运算操作发生前进行检测,后三个在操作发生后进行检测。

每种异常都有对应的标志位和屏蔽位。当检测到浮点异常时,处理器将与之匹配的标志位置1。每个被处理器标志的异常都有两种可能的操作:

  • 如果相应的屏蔽位置1 那么处理器自动处理异常并继续执行程序
  • 如果相应的屏蔽位清0,那么处理器将调用软件异常处理程序

大多数程序普遍都可以接受处理器的屏蔽响应。如果应用程序需要特殊响应,那么可以使用自定义异常处理程序,一条指令能触发多个异常,因此处理器要持续保存自上一次异常清零后所发生的全部异常。完成一系列计算后,可以检测是否发生了异常。

浮点数指令集

FPU指令集有些复杂,因此本节尝试对齐功能进行概述,并用具体例子给出编译器通常会生成的代码。此外,本节还将看到如何通过改变舍入模式来控制FPU。指令集包括如下基本指令类型:

  • 数据传送
  • 基本算术运算
  • 比较
  • 超越函数
  • 常数加载
  • x87FPU控制
  • x87FPU和SIMD状态管理

浮点指令名用字母F开头,以区别CPU指令,指令助记符的第二个字母(通常为B或I)指明如何解释内存操作数:B表示BCD操作数,I表示二进制整数操作数。如果这两个字母都没有使用,则内存操作数被认为是实数。比如,FBLD操作对象为BCD数值,FILD操作对象为整数,而FLD操作对象为实数

操作数:浮点指令可以包含零操作数 单操作数和双操作数。如果是双操作数,那么其中一个必然为浮点寄存器。指令中没有立即操作数,但是某些预定义常数可以加载到堆栈。通用寄存器EAX EBX…不能作为操作数。

整数操作数从内存加载到FPU,并自动转换为浮点格式。同样,将浮点数保存到整数内存操作数时,该数值也会被自动截断或舍入为整数。

1.初始化(FINIT)

FINIT指令对FPU进行初始化。将FPU控制字设置为037Fh,即屏蔽了所有浮点异常,舍入模式设置为最近偶数,计算精度设置为64位。建议在程序开始时调用FINIT,这样就可以了解处理器的其实状态

2.浮点数据类型

MASM支持的浮点类型有:

  • QWORD 64位整数
  • TBYTE 80位整数
  • REAL4 32位IEEE短实数
  • REAL8 64位IEEE长实数
  • REAL10 80位IEEE扩展实数

3.加载浮点数值 FLD

FLD指令将浮点操作数复制到FPU堆栈栈顶(ST(0))。操作数可以是32位 64位 80位的内存操作数或另一个FPU寄存器。FLD支持的内存操作数类型与MOV指令一样

FILD

FILD指令将16位 32位或者64位有符号整数源操作数转换为双精度浮点数,并加载到ST(0)。源操作数符号保留。FILD支持的内存操作数类型和MOV一致

加载常数

下面的指令将特定常数加载到堆栈,这些指令没有操作数

  • FLD1指令将1.0压入寄存器堆栈
  • FLDL2T指令将log210压入寄存器堆栈
  • FLDL2E指令将log2e压入寄存器堆栈
  • FLDPI指令将π压入寄存器堆栈
  • FLDLG2指令将log102压入寄存器堆栈
  • FLDLN2指令将loge2压入寄存器堆栈
  • FLDZ(加载零)指令将0.0压入FPU堆栈

保存浮点数值(FST FSTP FIST)

FST指令将浮点操作数从FPU栈顶复制到内存。FST支持的内存操作数类型和FLD一致。操作数可以为32位 64位 80位内存操作数或另外一个FPU寄存器

FSTP(保存浮点值并将其出栈)指令将ST(0)的值复制到内存并将ST(0)弹出堆栈

FIST(保存整数)指令将ST(0)的值转换为有符号整数,并把结果保存到目标操作数。保存的值可以为字或者双字。FIST支持的内存操作数类型与FST一致

算术运算指令

下表列出了基本算术运算操作。所有算术运算指令支持的内存操作数类型与FLD(加载)和FST(保存)一致,因此操作数可以是间接操作数 变址操作数和基址变址操作数等等

指令 作用
FCHS 修改符号
FADD 源操作数与目的操作数相加
FSUB 从目的操作数中减去源操作数
FSUBR 从源操作数中减去目的操作数
FMUL 源操作数和目的操作数相乘
FDIV 目的操作数除以源操作数
FDIVR 源操作数除以目的操作数

FCHS和FABS

FCHS(修改符号)指令将ST(0)中的浮点值的符号取反。FABS(绝对值)指令清除ST(0)中数值的符号,以得到它的绝对值,这两条指令都没有操作数

FADD FADDP FIADD

FADD(加法),如果FADD没有操作数,则ST(0)与ST(1)相加,结果暂存在ST(1)。然后ST(0)弹出堆栈,把加法结果保留在栈顶。如果是寄存器操作数,从同样的栈开始,将ST(0)加到ST(1)。如果是内存操作数,FADD将操作数与ST(0)相加

FADDP(相加并出栈)指令先执行加法操作,再将ST(0)弹出堆栈

FIADD(整数加法)指令先将源操作数转换为扩展双精度浮点数,再与ST(0)相加

FSUB FSUBP FISUB

FUSB指令从目的操作数中减去源操作数,并把结果保存到目的操作数。目的操作数总是一个FPU寄存器,源操作数可以是FPU寄存器或内存操作数。该指令操作数类型和FADD指令一致。

FUSB的操作与FADD相似,只不过它进行的是减法而不是加法。比如,无参数FUSB实现ST(1)-ST(0),结果暂存与ST(1)。然后ST(0)弹出堆栈,将减法结果留在栈顶。若FSUB使用内存操作数,则从ST(0)中减去内存操作数,且不再弹出堆栈

FSUBP(相减并出栈)指令先执行减法,再将ST(0)弹出堆栈

FISUB(整数减法)指令先把源操作数转为扩展双精度浮点数,再从ST(0)中减去该操作数

FMUL FMULP FIMUL

FMUL指令将源操作数与目的操作数相乘,乘积保存在目的操作数中。目的操作数总是一个FPU寄存器,源操作数可以为寄存器或者内存操作数。除了执行的是乘法不是加法外,FMUL的操作与FADD相同。比如,无参数FMUL将ST(0)与ST(1)相乘,乘积暂存于ST(1),然后将ST(0)弹出堆栈,将乘积留在栈顶。

FMULP(相乘并出栈)指令先执行乘法,再将ST(0)弹出堆栈

FIMUL与FIADD相同,只是它执行的是乘法不是加法

FDIV FDIVP FIDIV

FDIV指令执行目的操作数除以源操作数,被除数保存在目的操作数中。目的操作数总是一个寄存器,源操作数可以为寄存器或者内存操作数。其语法与FADD和FSUB相同。

除了执行的是除法不是加法外,FDIV的操作和FADD相同。比如,无参数FDIV执行ST(1)除以ST(0)。然后ST(0)弹出堆栈,将被除数留在栈顶。使用内存操作数的FDIV将ST(0)除以内存操作数。

若操作数为零 则产生除零异常。若源操作数为正 负无穷 零 或者NaN,则使用一些特殊情况

FIDIV指令先将整数源操作数转换为扩展双精度浮点数,再执行与ST(0)的除法

比较浮点数值

浮点数不能使用CMP进行比较,因为CMP是通过整数减法来执行比较的。取而代之,必须使用FCOM指令,执行FCOM。执行FCOM指令后,还需要采取特殊步骤,然后再使用JCC跳转指令。由于所有的浮点数都为隐含的有符号数,因此FCOM执行的是有符号的比较。

FCOM FCOMP FCOMPP

FCOM(比较浮点数)指令将源操作数与ST(0)进行比较。源操作数可以为内存操作数或者FPU寄存器

FCOMP指令的操作数类型和执行的操作与FCOM指令相同,但是它要将ST(0)弹出堆栈

FCOMPP指令与FCOMP相同,但是它有两次出栈操作

条件码

FPU条件码标识有三个:C3 C2和C0,用以说明浮点数的比较结果。C3 C2和C0的功能分别与零标志位(ZF) 奇偶标志位(PF)和进位标志位(CF)相同。

在比较了两个数值并设置了FPU条件码之后,遇到的主要挑战就是怎样根据条件分支到相应标号。这包括两个步骤

  • 用FNSTSW指令把FPU状态字送入AX
  • 用SAHF指令把AH复制到EFLAGS寄存器

条件码送入EFLAGS之后,就可以根据ZF CF和PF进行条件跳转

P6处理器的改进

浮点数比较的运行时开销大于整数比较。考虑到这一点,InterP6系列引入了FCOMI指令。该指令比较浮点数值,并直接设置ZF PF CF

读写浮点数值

  • ReadFloat:从键盘读取一个浮点数,并将其压入浮点堆栈
  • WriteFloat:将ST(0)中的浮点数以阶码形式写到控制台窗口

异常同步

整数(CPU)和FPU是相互独立的单元,因此,在执行整数和系统指令的同时可以执行浮点指令。这个功能被称为并行性,当发生未屏蔽的浮点异常时,它可能是一个潜在的问题。反之,已屏蔽异常则不成问题,因为FPU总是可以完成当前操作并保存结果。

发生未屏蔽异常时,中断当前的浮点指令,FPU发异常事件信号。当下一条浮点指令或者FWAIT指令将要被执行时,FPU检查待处理的异常。如果发现有这样的异常,FPU就调用浮点异常处理程序

如果引发异常的浮点指令后面跟的是整数或系统指令,则指令不会检查待处理异常——它们会立即执行

展开阅读全文

没有更多推荐了,返回首页