-
-
[原创]关于IEEE754的一点总结
-
发表于: 4天前 639
-
定义
这是一个关于浮点数的存储,运算,以及异常处理的标准。
一个浮点数 (Value) 的表示其实可以这样表示:
Value=sign×exponent×fraction
(-1)^sign × 1.fraction × 2^(exponent - bias)
也就是浮点数的实际值,等于符号位(sign bit)乘以指数偏移值(exponent bias,指数 )再乘以分数值(fraction,尾数)
几乎所有现代语言最寻该标准,构成以下浮点数类型:
- float (32-bit)
- double (64-bit)
- long double(实现相关,部分没有)
结构
[ 符号位 ][ 指数 ][ 尾数 ] 1bit 11bit 52bit
例子
转化 5.0
- 转二进制:5.0 的二进制是 101.0。
- 规格化:移动小数点,使其变成 1.01 \times 2^2。
- Sign: 0(正数)。
- Fraction: 01(省略开头的 1,后面补 0 至 23 位)。
- Exponent: 实际指数是 2,所以存储值 = 2 + 127 = 129 (10000001_2)。
- 最终存储:
0 10000001 01000000000000000000000
特殊值
这里有三个特殊值需要指出:
- 如果指数是0,而尾数非0,则为非规约形式,代表极小值(正负与符号位相关)
- 如果指数是0并且尾数的小数部分是0,这个数±0(和符号位相关)
- 如果指数 = 2e−1并且尾数的小数部分是0,这个数是±∞(同样和符号位相关)
- 如果指数 = 2e−1并且尾数的小数部分非0,这个数表示为非数(NaN)。
列表如下:
| 状态类型 | 指数位 (E) | 尾数位 (M) | 数值范围 / 特征 | 含义 |
|---|---|---|---|---|
| 非规约形式 (Denormalized) | 全 0 | 非 0 | (0, 1) \times 2^{-bias+1} | 极接近 0 的数,不隐含开头的 1. |
| 真零 (Zero) | 全 0 | 全 0 | \pm 0.0 | 符号位决定正负零 |
| 无穷大 (Infinity) | 全 1 (2^e-1) | 全 0 | \pm \infty | 计算溢出或除以 0 的结果 |
| NaN (非数) | 全 1 (2^e-1) | 非 0 | N/A | 非法运算结果(如 0 \div 0) |
利用方式
对IEEE-754的利用主要在特殊值上。
1. NaN 判断绕过
NAN,与其他所有数的任何比较(包括<,>,==,<=,>=)返回值都为false。
NAN值取得方式主要有以下两类:
- 数学运算触发 (运行时产生)
在程序运行时,以下逻辑运算通常会返回 NaN: - 零除以零:
0.0 / 0.0 - 无穷大减无穷大:
inf - inf - 无穷大乘以零:
inf * 0.0 - 对负数开平方根:
sqrt(-1.0) - 对负数取对数:
log(-1.0) - 直接在内存中构造
- 符号位:
0 - 指数位:
11111111111(11 个 1) - 尾数位:
1000...0(非零) - 符号位:
0 - 指数位:
11111111(全 1) - 尾数位:
10000000000000000000000(非零)
双精度 (double, 64-bit) 示例 - 十六进制:
0x7fc00000(这是一个常见的 Quiet NaN) - 二进制结构:
- 十六进制:
0x7ff8000000000000 - 二进制结构
- 指数位 (Exponent):必须全为 1。
- 尾数位 (Fraction/Mantissa):必须非零(如果是全 0,那就是 \infty)。
单精度 (float, 32-bit) 示例
在硬件层面,NaN 还细分为两种:
- qNaN (Quiet NaN):最常见。进行运算时不会抛出异常,只是静默地把结果传下去。通常尾数的最高位是 1。
- sNaN (Signaling NaN):进行运算时会触发 CPU 异常(Exception)。通常尾数的最高位是 0(但其余位不全为 0)。
在写exp时,可以直接封装上面的十六字节数,也可以用struct包:
from pwn import *
import struct
nan_value = float('nan')
# 'd' 代表 double (8 bytes), 'f' 代表 float (4 bytes)
raw_bytes = struct.pack('<d', nan_value)
print(f"NaN bytes: {enhex(raw_bytes)}")
#或直接(具体值见上)
raw_bytes = p64(`0x7fc00000`)
print(f"NaN bytes: {enhex(raw_bytes)}")由于编译器编译时,在汇编层面对浮点数的特殊值的特殊处理方式,所以在ida中的反汇编会与源码不同,所以在看执行逻辑时要多看汇编,而且要理解汇编如何用标志位进行执行流控制,以及浮点数的特殊值处理。
x86 条件跳转(Jcc)标志位要求一览表(Intel/AMD 标准,EFLAGS)
| 跳转指令 | 别名 | 跳转条件(标志位) | 含义(整数语境) | 浮点语境常用对应 |
|---|---|---|---|---|
| JA / JNBE | Jump Above | CF=0 且 ZF=0 | 无符号 > | 浮点 a > b(有序时) |
| JAE / JNB | Jump Above or Equal | CF=0 | 无符号 ≥ | 浮点 a ≥ b(极少用) |
| JB / JNAE / JC | Jump Below | CF=1 | 无符号 < | 浮点 a < b |
| JBE / JNA | Jump Below or Equal | CF=1 或 ZF=1 | 无符号 ≤ | 浮点 a ≤ b(最常见!) |
| JG / JNLE | Jump Greater | ZF=0 且 SF=OF | 有符号 > | 几乎不用(浮点不用 SF/OF) |
| JGE / JNL | Jump Greater or Equal | SF=OF | 有符号 ≥ | 几乎不用 |
| JL / JNGE | Jump Less | SF ≠ OF | 有符号 < | 几乎不用 |
| JLE / JNG | Jump Less or Equal | ZF=1 或 SF≠OF | 有符号 ≤ | 几乎不用 |
| JE / JZ | Jump Equal | ZF=1 | 等于 | 浮点 a == b |
| JNE / JNZ | Jump Not Equal | ZF=0 | 不等于 | 浮点 a != b |
| JP / JPE | Jump Parity Even | PF=1 | 奇偶校验偶 | 浮点 unordered(NaN 时常用) |
| JNP / JPO | Jump Parity Odd | PF=0 | 奇偶校验奇 | 浮点 ordered |
浮点比较逻辑(UCOMISS 与 COMISS)
标志位设置规则(Intel SDM)
执行 UCOMISS xmm1, xmm2/m32 或 COMISS xmm1, xmm2/m32 时(比较 xmm1[31:0] ? xmm2[31:0]):
| 比较结果 (RESULT) | CF | ZF | PF | OF | SF | AF | 说明(IEEE 754 语义) |
|---|---|---|---|---|---|---|---|
| GREATER_THAN (有序 >) | 0 | 0 | 0 | 0 | 0 | 0 | src1 > src2 |
| EQUAL (相等,包括 +0 == -0) | 0 | 1 | 0 | 0 | 0 | 0 | src1 == src2 |
| LESS_THAN (有序 <) | 1 | 0 | 0 | 0 | 0 | 0 | src1 < src2 |
| UNORDERED (无序) | 1 | 1 | 1 | 0 | 0 | 0 | 任意操作数是 NaN(QNaN 或 SNaN) |
区别:NaN 时的异常行为
| 指令 | NaN 类型 | 是否触发 #I(无效操作异常) | EFLAGS 是否更新(如果异常发生且未屏蔽) | 典型使用场景 |
|---|---|---|---|---|
| UCOMISS | SNaN( Signaling NaN ) | 是(触发 #I) | 如果异常未屏蔽 → 不更新 EFLAGS | 默认选择,大多数编译器(gcc/clang/MSVC)生成的浮点比较代码几乎都用 UCOMISS |
| UCOMISS | QNaN( Quiet NaN ) | 否(安静通过) | 正常更新(CF=ZF=PF=1) | — |
| COMISS | SNaN | 是 | 如果未屏蔽 → 不更新 | 很少用 |
| COMISS | QNaN | 是(也触发 #I) | 如果未屏蔽 → 不更新 | 想让所有 NaN 都显式报错时用 |
2. NaN 传播
NAN可以通过一些运算传播到目标变量,如下:
NaN + 5.0 = NaN10.0 * NaN = NaNNaN / inf = NaNpow(NaN, 2) = NaN(幂运算)- 部分数学库函数(如cos、sin)运算NAN的返回值也为NAN
3.INF判断绕过
INF(无穷大),大于任何数,同时满足INF == INF。
获得方式:
- 在程序逻辑中,以下操作会产生 INF:
- 除以零:
1.0 / 0.0(注意:整数除以零会崩溃,但浮点数除以零会得到 INF)。 - 数值溢出:计算结果超过了该类型能表示的最大值。例如,对一个巨大的数进行幂运算。
- 内存中构造,同样,直接按如下数值封包即可
| 类型 | 符号位 (S) | 指数位 (E) | 尾数位 (M) | 十六进制 (正无穷) |
|---|---|---|---|---|
| 单精度 (float) | 0 或 1 | 8位全为 1 | 23位全为 0 | 0x7f800000 |
| 双精度 (double) | 0 或 1 | 11位全为 1 | 52位全为 0 | 0x7ff0000000000000 |
4.类型转换
如果该类型数被强制类型转换,很容易得到一个很大的值(具体参考上述内存中布局形式,与其他类型布局形式),同样可以进行一些绕过
5.精度绕过
看如下程序:
#include <math.h>
#include <assert.h>
void main()
{
double x = 0.2;
double y = x / 2;
assert(x == 0.1);
}众所周知,浮点数计算时会损失精度,所以毫无疑问会触发断言。
正常程序一般是这样:
#include <math.h>
#include <assert.h>
void main() {
double x = 0.2;
double y = x / 2;
double epsilon = 1e-15; // 定义一个极小的容差
// 判断 y 和 0.1 的差距是否在误差范围内
assert(fabs(y - 0.1) < epsilon);
}赞赏
赞赏
雪币:
留言: