首页
社区
课程
招聘
[原创]关于IEEE754的一点总结
发表于: 4天前 639

[原创]关于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

  1. 转二进制:5.0 的二进制是 101.0。
  2. 规格化:移动小数点,使其变成 1.01 \times 2^2。
    • Sign: 0(正数)。
    • Fraction: 01(省略开头的 1,后面补 0 至 23 位)。
    • Exponent: 实际指数是 2,所以存储值 = 2 + 127 = 129 (10000001_2)。
  3. 最终存储0 10000001 01000000000000000000000

特殊值

这里有三个特殊值需要指出:

  1. 如果指数是0,而尾数非0,则为非规约形式,代表极小值(正负与符号位相关)
  2. 如果指数是0并且尾数的小数部分是0,这个数±0(和符号位相关)
  3. 如果指数 = 2e−1并且尾数的小数部分是0,这个数是±∞(同样和符号位相关)
  4. 如果指数 = 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)非 0N/A非法运算结果(如 0 \div 0)

利用方式

对IEEE-754的利用主要在特殊值上。

1. NaN 判断绕过

NAN,与其他所有数的任何比较(包括<,>,==,<=,>=)返回值都为false。

NAN值取得方式主要有以下两类:

  1. 数学运算触发 (运行时产生)
    在程序运行时,以下逻辑运算通常会返回 NaN:
    • 零除以零0.0 / 0.0
    • 无穷大减无穷大inf - inf
    • 无穷大乘以零inf * 0.0
    • 对负数开平方根sqrt(-1.0)
    • 对负数取对数log(-1.0)
  2. 直接在内存中构造
    • 符号位:0
    • 指数位:11111111111 (11 个 1)
    • 尾数位:1000...0 (非零)
    • 符号位:0
    • 指数位:11111111 (全 1)
    • 尾数位:10000000000000000000000 (非零)
      双精度 (double, 64-bit) 示例
    • 十六进制0x7fc00000 (这是一个常见的 Quiet NaN)
    • 二进制结构
    • 十六进制0x7ff8000000000000
    • 二进制结构
    1. 指数位 (Exponent):必须全为 1
    2. 尾数位 (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 / JNBEJump AboveCF=0 ZF=0无符号 >浮点 a > b(有序时)
JAE / JNBJump Above or EqualCF=0无符号 ≥浮点 a ≥ b(极少用)
JB / JNAE / JCJump BelowCF=1无符号 <浮点 a < b
JBE / JNAJump Below or EqualCF=1 ZF=1无符号 ≤浮点 a ≤ b(最常见!)
JG / JNLEJump GreaterZF=0 SF=OF有符号 >几乎不用(浮点不用 SF/OF)
JGE / JNLJump Greater or EqualSF=OF有符号 ≥几乎不用
JL / JNGEJump LessSF ≠ OF有符号 <几乎不用
JLE / JNGJump Less or EqualZF=1 SF≠OF有符号 ≤几乎不用
JE / JZJump EqualZF=1等于浮点 a == b
JNE / JNZJump Not EqualZF=0不等于浮点 a != b
JP / JPEJump Parity EvenPF=1奇偶校验偶浮点 unordered(NaN 时常用)
JNP / JPOJump Parity OddPF=0奇偶校验奇浮点 ordered

浮点比较逻辑(UCOMISS 与 COMISS)

标志位设置规则(Intel SDM)

执行 UCOMISS xmm1, xmm2/m32 或 COMISS xmm1, xmm2/m32 时(比较 xmm1[31:0] ? xmm2[31:0]):

比较结果 (RESULT)CFZFPFOFSFAF说明(IEEE 754 语义)
GREATER_THAN (有序 >)000000src1 > src2
EQUAL (相等,包括 +0 == -0)010000src1 == src2
LESS_THAN (有序 <)100000src1 < src2
UNORDERED (无序)111000任意操作数是 NaN(QNaN 或 SNaN)








区别:NaN 时的异常行为

指令NaN 类型是否触发 #I(无效操作异常)EFLAGS 是否更新(如果异常发生且未屏蔽)典型使用场景
UCOMISSSNaN( Signaling NaN )(触发 #I)如果异常未屏蔽 → 不更新 EFLAGS默认选择,大多数编译器(gcc/clang/MSVC)生成的浮点比较代码几乎都用 UCOMISS
UCOMISSQNaN( Quiet NaN )(安静通过)正常更新(CF=ZF=PF=1)
COMISSSNaN如果未屏蔽 → 不更新很少用
COMISSQNaN(也触发 #I)如果未屏蔽 → 不更新想让所有 NaN 都显式报错时用

2. NaN 传播

NAN可以通过一些运算传播到目标变量,如下:

  • NaN + 5.0 = NaN
  • 10.0 * NaN = NaN
  • NaN / inf = NaN
  • pow(NaN, 2) = NaN(幂运算)
  • 部分数学库函数(如cos、sin)运算NAN的返回值也为NAN

3.INF判断绕过

INF(无穷大),大于任何数,同时满足INF == INF

获得方式:

  1. 在程序逻辑中,以下操作会产生 INF:
    • 除以零1.0 / 0.0(注意:整数除以零会崩溃,但浮点数除以零会得到 INF)。
    • 数值溢出:计算结果超过了该类型能表示的最大值。例如,对一个巨大的数进行幂运算。
  2. 内存中构造,同样,直接按如下数值封包即可
类型符号位 (S)指数位 (E)尾数位 (M)十六进制 (正无穷)
单精度 (float)0 或 18位全为 123位全为 00x7f800000
双精度 (double)0 或 111位全为 152位全为 00x7ff0000000000000

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); 
}



传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 1
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回