汇编基础
浮点数处理
浮点数二进制表示
十进制浮点数有三个组成部分:符号,有效数字,阶码。比如:\(-1.23154*10^5\)中,符号位负,有效数字为1.23154,阶码为5(虽然不太正确,有时用术语尾数(mantissa)来代替有效数字(significand))
IEEE二进制浮点数表示
x86处理器使用的三种浮点数二进制存储格式都是由IEEE标准754-1985所指定
- 单精度,32位:1位符号位,8位阶码,23位为有效数字的小数部分
- 双精度,64位:1位符号位,11位阶码,52位为有效数字的小数部分
符号位
如果符号位为1,该数为负。如果符号位为0,该数为正。零被认为是正数。
有效数字
二进制浮点数可以使用加权位计数法。
需要注意的是小数点左面的阶为正,右面的为负
同样有效数字存在精度。例如假设一个简单的浮点数格式有5位有效数字,那么将无法表示范围在1.1111~10.000之间的数
阶码
单精度数用8位无符号整数存放阶码,引入的偏差为127,因此必须在数的实际阶码上再加127,实际都是这个偏移码被保存,偏移码总是正数,范围为1~254,实际阶码的范围是-126~127
规格化二进制浮点数
大多数二进制浮点数都以规格化格式(normalized form)存放,以便将有效数字的精度最大化。给定任意的二进制浮点数,都可以进行规格化,方法是:
将二进制小数点移位,直到小数点左边只有一个‘1’,阶码为二进制小数点向左(正阶码)或向右(负阶码)移动的位数
新建IEEE表示
实数
一旦符号位,阶码,有效数字字段完成规格化和编码后,生成一个完整的二进制IEEE段实数就很容易了。如\(1.101x2^0\):
- 符号位:0
- 阶码:0111111(127)
- 小数部分:10100000000000000000000
无穷 和 NaN
任一无穷都可以表示浮点溢出条件,运算结果不能规格化的原因是,阶码太大而无法用有效的阶码位数来表示
NaN表示Not a Number。x86有两种NaN:quiet NaN可以通过大多数算术运算符来传递,不会引起异常;signaling NaN则被用于产生一个浮点无效操作异常
十进制小数转为二进制实数
1/2 – .1;
1/4 – .01;
…
也可以使用长除法,将十进制的除数被除数全部转为二进制,再执行长除
如何转换回去呢,看下面的例子:
0 10000010 01011000000000000000000
1)该数为正数
2)无偏差阶码的二进制值为00000011,十进制为3
3)将符号阶码有效数字组合起来即得该二进制数为\(+1.01011x2^3\)
4)非格式化二进制数为+1010.11
5)十进制为10.75
浮点单元
Intel8086的处理器无法处理浮点运算,后来,Intel486出现时,浮点硬件就被集成到主CPU中,称为FPU
FPU寄存器栈
FPU不使用通用寄存器,它有自己的一组寄存器,称为寄存器栈(register stack)。数值从内存加载到寄存器栈,然后执行运算,再将堆栈数值保存到内存
FPU使用后缀(postfix)表达式计算算术表达式,如:
(A+B)* C --> A B + C *
-
表达式堆栈:
在计算后缀表达式的过程中,用堆栈来保存中间结果。ST(0)表示堆栈指针通常所指的位置
A入栈 -> B入栈 -> 弹出A,B做加法,结果入栈 -> C入栈 -> 弹出C和后面的数做乘法 -
FPU数据寄存器
FPU有8个独立的,可寻址的80位数据寄存器R0~R7,FPU状态字中名为TOP的一个3位字段给出了当前出于栈顶的寄存器编号,如011表示栈顶为R3,这个位置也称为ST(0),最后一个寄存器为ST(7)
入栈操作将TOP减1,并把操作数复制到表示为ST(0)的寄存器中。如果入栈前,TOP就是0了,那么会绕回到寄存器R7
- FPU专用寄存器
FPU有6个专用(special-purpose)寄存器- 操作码寄存器:保存最后执行的非控制指令的操作码
- 控制寄存器:执行运算时控制精度和FPU的舍入
- 状态寄存器:包含栈顶指针,条件码和异常警告
- 标识寄存器:指明FPU数据寄存器栈内每个寄存器的内容
- 最后指令指针寄存器:保存指向最后执行的非控制指令的指针
- 最后数据指针寄存器:保存指向数据操作数的指针
舍入
方法 | 精确结果 | 舍入结果 |
---|---|---|
舍入到最接近的偶数 | 1.0111 | 1.100 |
向-∞舍入 | 1.0111 | 1.011 |
向+∞舍入 | 1.0111 | 1.100 |
向0舍入 | 1.0111 | 1.011 |
同时FPU控制字会指明使用的舍入方法,这两位被称为RC段
00:舍入到最近的偶数(默认)
01:向负无穷舍入
10:向正无穷舍入
11:向0舍入(截断)
浮点数指令集
初始化 FINT
FINT指令对FPU进行初始化。将FPU控制字节设置为037Fh,即隐蔽了所有浮点异常,舍入模式设置为最近偶数,计算精度设置为64位
浮点数数据类型
REAL4 -> 32位(4字节)IEEE短实数
REAL8 -> 64位(8字节)IEEE长实数
REAL10 -> 80位(10字节)IEEE扩展实数
加载浮点数值 FLD
指令将浮点数复制到FPU堆栈栈顶,即压入栈顶
保存浮点数值 FST FSTP
FST将浮点操作数从FPU栈顶复制到内存,但并不出栈
FSTP将完成复制+弹出
算数指令
FCHS和FABS
FCHS(修改符号)指令将ST(0)中的浮点数数值的符号取反。FABS(绝对值)指令清除ST(0)中数值的符号,这两条指令没操作数
FADD,FADDP, FIADD
- FADD没有操作数时,为ST(0)和ST(1)相加,结果暂存在ST(1),ST(0)弹出堆栈,加法保留到栈顶
- 寄存器操作数时就是相加,不进行弹出操作
- 内存操作数,会将操作数和ST(0)相加
- FADDP相加并弹出
- FIADD(整数加法),先将源操作数转换为扩展双精度浮点数,再与ST(0)相加
FSUB,FSUBP,FISUB
类似加法,不再赘述
对应的乘除版本也类似
FSQRT
对ST(0)中的数值求平方根,结果返回ST(0)
FCOM,FCOMP,FCOMPP
- FCOM
指令 | 说明 |
---|---|
FCOM | 比较ST(0)和ST(1) |
FCOM m32fp | 比较ST(0)和m32fp |
FCOM m64fp | 比较ST(0)和m64fp |
FCOM ST(i) | 比较ST(0)和ST(i) |
- FCOMP会将ST(0)弹出
- FCOMPP会弹出两次
- FPU条件码标识有3个,C3,C2,C0分别对应ZF(零标志位),PF(奇偶标志位),CF(进位标志位)
在比较了两个数值并且设置了FPU条件码后,如何根据条件分支跳转呢?
这就包括了两个步骤:
1.用FNSTSW指令把FPU状态字送入AX
2.用SAHF指令把AH复制到EFLAGS
1 | double x = 1.2; |
与之等效的汇编代码:
1 | .data |
需要注意的是,在相等比较的时候,不要使用浮点数相等来进行比较,因为会在计算中存在舍入误差,一般使用差的绝对值小于一个极小的数时就认为相等
1 | .data |
异常同步
整数(CPU)和FPU是相互独立的单元,因此在执行整数和系统指令的同时可以执行浮点指令,这被称为并行性(concurrency),当发生未屏蔽的浮点异常时,可能会是一个潜在的问题。
发生未屏蔽异常,中断当前的浮点指令,FPU发异常事件信号。当下一条浮点指令或FWAIT(WAIT)指令将要执行时,FPU检查待处理的异常。
1 | fild intVal ;整数加载到ST(0) |
设置WAIT和FWAIT指令是为了在执行下一条指令前,强制处理器检查待处理且未屏蔽的浮点异常。直到异常处理结束,才执行INC
如果不设置,引发异常的浮点指令后跟的是整数指令或者系统指令,很遗憾,不会检查待处理的异常——会立即执行。第一条指令送入一个内存操作数,第二条指令又要修改它,异常处理的程序就无法正确执行