浮点数(floating point numbers)
在这里会看到非整数在汇编中是怎样工作的。计算机中有一对与浮点数的工作有关的东西:
首先,我们看一下浮点数是怎么存储在内存中的。浮点数的类型有这么几种:
- 单精度(single-precision)
- 双精度(double-precision)
- 双精度拓展(double-extended precision)
Intel 的 64-ia-32-architecture-software-developer-vol-1-manual 中有这么一个描述:
这些数据类型的数据格式符合 IEEE Standard 754 对二进制浮点算术的规范。
单精度浮点数在内存中这么表示:
- 符号(sign) – 1 比特
- 指数(exponent) – 8 比特
- 尾数(mantissa) – 23 比特
举个栗子吧,这么一个数:
sign | exponent | mantissa |
---|---|---|
0 | 00001111 | 110000000000000000000000 |
指数是有符号的 8 比特数据,有符号整数范围为 -128 - 127,无符号整数则为 0 - 255。符号位为 0,表示的数为正,否则为负。指数若编码为 00001111b,十进制表示为 15;对单精度浮点数而言,指数的偏移量为 127,换句话说,实际的指数值应该这么得到: eponent - 127,或者 15 - 127 = -112。在尾数中,规约形式的浮点数的整数部分始终为 1,因此它被省略,只记录小数部分,因此尾数的二进制数实际上为 1,110000000000000000000000。这个数值十进制表示为:
1 | value = mantissa * 2^-112 |
双精度浮点数为 64 比特大小:
- 符号 – 1 bit
- 指数 – 11 bits
- 尾数 – 52 bits
若转换为十进制,可以这么计算:
1 | value = (-1)^sign * (1 + mantissa / 2 ^ 52) * 2 ^ exponent - 1023) |
双精度拓展为 80 比特:
- sign – 1 bit
- exponent – 15 bits
- mantissa – 112 bit
更多信息在这里。
注:自己感觉作者说得不清。754 标准都差不多,以单精度实数为例:
1 | +------------+-------------------+----------------------+ |
一个单精度实数为 32 bits 大小。比特位使用情况如上图。符号为 0 代表正数,1 代表负数。指数使用移码表示,不过偏移量为 127 而不是通常的 128。尾数使用原码表示(忽略符号,或者叫浮点数绝对值的原码),与二进制科学计数法形似,区别为:因整数部分始终为 1,被省略,尾数表示的是小数部分。
x87 FPU
x87 浮点运算器(Floating-Point Unit, FPU)提供了高性能的浮点数运算。它支持浮点数、整数与压缩 BCD 整数类型与浮点处理算法。x87 提供了这些指令集:
- 数据传送指令(Data transfer instructions)
- 基本算术指令(Basic arithmetic instructions)
- 比较指令(Comparison instructions)
- 载入常量指令(Load constant instructions)
- 超越指令(Transcendental instructions)
- x87 FPU 控制指令(x87 FPU control instructions)
在这肯定不会看到 x87 提供的所有指令,想要获取更多的信息,64-ia-32-architecture-software-developer-vol-1-manual Chapter 8 欢迎您。这里有一对数据传输指令:
- FDL – 载入浮点数
- FST – 存储浮点数(在 ST(0) 寄存器)
- FSTP – 存储浮点数与弹出(在 ST(0) 寄存器)
算术指令:
- FADD – 浮点数加法运算
- FIADD – 浮点数与整数加法运算
- FSUB – 浮点数减法运算
- FISUB – 浮点数减法,从浮点数中减去整数
- FABS – 取浮点数绝对值
- FIMUL – 浮点数与整数乘法
- FIDIV – device integer and floating point(我猜,含义是除数为整数或浮点数)
FPU 有 10 字节大小的寄存器,这些寄存器构成一个环形堆栈。堆栈的顶端为寄存器 **ST(0)**,其余的寄存器为 ST(1), ST(2)…ST(7)。当使用浮点数的时候便会使用他们。例如:
1 | section .data |
把 x 的值放入堆栈。x 可以为 32 bits, 64 bits, 80 bits。它的工作方式就像常用的堆栈一样。若用 fld 把另一个数 y 放入堆栈,x 的值会在 ST(1),y 会在 ST(0)。FPU 指令可以使用这些寄存器,例如:
1 | ;; |
这有个简单的栗子。我们知道圆的半径,求它的面积:
1 | extern printResult |
它是这么工作的:data section 里预定义了 radius(半径) 与 result,result 将会存储运算结果。接着是两个用于调用系统退出函数的常量。接下来是程序的入口 – _start。使用 fld 指令把 radius 的值放入 st0 与 st1,并使用 fmul 使他们相乘。这样操作之后,在 st0 寄存器中便有了 radius 与 radius 相乘的结果。接着,使用 fldpi 指令载入 π 到 st0 寄存器,此时 radiusradius 的结果会在 st1 寄存器中。*fmul 指令作用在 st0(值为 π) 与 st1(radius*radius 的结果),结果会存放在 st0。好了,现在在 st0 寄存器中便有了圆型的面积,可以使用 fstp 指令把结果放入 result 了。下一部分是我们把结果传入 C 函数并调用它。还知道我们先前怎样在汇编中调用 C 函数吗?我们需要知道 x86_64 体系是怎样传参的,通常我们使用 rdi(arg1), rsi(arg2) 等传参,但是这里是浮点数。不过有这些特殊的寄存器: 由 sse提供的 xmm0 - xmm15。首先我们需要把 xmmN 寄存器的序列号放入 rax(这里为 0),之后把结果放入 xmm0 寄存器。现在,我们可以调用 C 函数来打印了:
1 |
|
编译他们:
1 | build: |
执行:
结语
这是这系列文章的第八篇了。这里我们介绍了浮点数在汇编中是怎样使用的。