汇编基础

汇编基础

浮点数处理

浮点数二进制表示

十进制浮点数有三个组成部分:符号,有效数字,阶码。比如:\(-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

入栈操作.jpg

  • 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
2
3
4
5
6
double x = 1.2;
double y = 3.0;
int n = 0;
if(x < y){
n = 1;
}

与之等效的汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
.data
x REAL8 1.2
y REAL8 3.0
n DWORD 0
.code
fld x ;ST(0)=x
fcomp y ;比较ST(0)和y
fnstsw ax ;状态字送入AX
sahf ;AH复制到EFLAGS
jnb L1
mov n,1
L1:

需要注意的是,在相等比较的时候,不要使用浮点数相等来进行比较,因为会在计算中存在舍入误差,一般使用差的绝对值小于一个极小的数时就认为相等

1
2
3
4
5
6
7
8
9
10
11
12
13
.data
epsilon REAL8 1.0E-12
val2 REAL8 0
val3 REAL8 1.001E-13
.code
fld epsilon
fld val2
fsub val3
fabs
fcomi ST(0), ST(1) ;代替之前的三条指令
ja skip
...
skip:

异常同步

整数(CPU)和FPU是相互独立的单元,因此在执行整数和系统指令的同时可以执行浮点指令,这被称为并行性(concurrency),当发生未屏蔽的浮点异常时,可能会是一个潜在的问题。
发生未屏蔽异常,中断当前的浮点指令,FPU发异常事件信号。当下一条浮点指令或FWAIT(WAIT)指令将要执行时,FPU检查待处理的异常。

1
2
3
fild intVal  ;整数加载到ST(0)
fwait ;等待处理异常
inc intVal

设置WAIT和FWAIT指令是为了在执行下一条指令前,强制处理器检查待处理且未屏蔽的浮点异常。直到异常处理结束,才执行INC
如果不设置,引发异常的浮点指令后跟的是整数指令或者系统指令,很遗憾,不会检查待处理的异常——会立即执行。第一条指令送入一个内存操作数,第二条指令又要修改它,异常处理的程序就无法正确执行

Author

Ctwo

Posted on

2019-08-09

Updated on

2020-10-25

Licensed under

Comments