【汇编逆向系列】三、函数调用包含单个参数之float类型-xmm0寄存器,sub,rep,stos,movss,mulss,addss指令

发布于:2025-06-06 ⋅ 阅读:(20) ⋅ 点赞:(0)

一、汇编代码

single_float_param:
  0000000000000060: F3 0F 11 44 24 08  movss       dword ptr [rsp+8],xmm0
  0000000000000066: 57                 push        rdi
  0000000000000067: 48 83 EC 10        sub         rsp,10h
  000000000000006B: 48 8B FC           mov         rdi,rsp
  000000000000006E: B9 04 00 00 00     mov         ecx,4
  0000000000000073: B8 CC CC CC CC     mov         eax,0CCCCCCCCh
  0000000000000078: F3 AB              rep stos    dword ptr [rdi]
  000000000000007A: F3 0F 10 44 24 20  movss       xmm0,dword ptr [rsp+20h]
  0000000000000080: F3 0F 59 05 00 00  mulss       xmm0,dword ptr [__real@3fc00000]
                    00 00
  0000000000000088: F3 0F 58 05 00 00  addss       xmm0,dword ptr [__real@3f000000]
                    00 00
  0000000000000090: 48 83 C4 10        add         rsp,10h
  0000000000000094: 5F                 pop         rdi
  0000000000000095: C3                 ret
  0000000000000096: CC                 int         3

二、汇编分析

1. xmm0寄存器

xmm0 是 Intel x86 架构中 ​​16 个 128 位 XMM 寄存器​​的首个寄存器,属于 SSE(Streaming SIMD Extensions)指令集的核心组件

在这里是在函数调用中传递浮点参数或返回值(如 Windows fastcall 约定),用于函数参数中的第一个浮点型参数

2. movss 指令

movss(Move Scalar Single-Precision Floating-Point)是 SSE 指令集中的关键指令,用于操作单精度浮点数(32 位)。其核心特性如下:

​数据传输​​:将单精度浮点数从源操作数(内存或 XMM 寄存器)复制到目标操作数(XMM 寄存器或内存)的低 32 位

​高位处理​​:目标操作数的高位(96 位)保持不变(若目标为 XMM 寄存器)或忽略(若目标为内存)

3. float类型的内存空间

了解浮点类型传参首先要知道一个浮点类型的数在内存空间内是如何表示的,Float 类型(单精度浮点数)在内存中的存储遵循 ​​IEEE 754 标准​​,占用 ​​4 字节(32 位)​​,其结构分为三个部分:​​符号位(Sign)、指数位(Exponent)和尾数位(Mantissa)​​。以下是详细解析:

部分 ​比特位​ ​作用​
​符号位(S) 最高位(第 31 位) 0 表示正数,1 表示负数。
​指数位(E)​ 第 30–23 位(8 位) 存储​​科学计数法的指数值​​,采用​​偏移编码​​(实际指数 = 存储值 - 127)
​尾数位(M) 第 22–0 位(23 位) 存储​​规格化后的小数部分​​,隐含整数部分为 1(不直接存储)

数字转换成浮点类型存储

下面以13.625这个数为例说明如何拆解float类型

步骤 1:转换为二进制​
  • ​整数部分 13​:
    13 ÷ 2 = 6...1 → 6 ÷ 2 = 3...0 → 3 ÷ 2 = 1...1 → 1 ÷ 2 = 0...1
    结果为 1101(从最后一个余数向前读)。
  • ​小数部分 0.625​:
    0.625 × 2 = 1.25 → 取整 1,余 0.25
    0.25 × 2 = 0.5 → 取整 0,余 0.5
    0.5 × 2 = 1.0 → 取整 1,余 0
    结果为 .101
  • ​合并​​:13.625 = 1101.101₂。
​步骤 2:规格化(科学计数法)​
  • 移动小数点至首位为 11101.101 → 1.101101 × 2³(左移 3 位)。
  • ​隐含整数 1​:尾数仅存储小数点后的部分 101101(补足 23 位:10110100000000000000000)。
​步骤 3:计算指数位​
  • 实际指数 3 → ​​存储值 = 3 + 127 = 130​​(偏移值 127 是 IEEE 754 固定规则)。
  • 130 的二进制:10000010₂。
​步骤 4:组合三部分​
  • ​符号位​​:0(正数)。
  • ​指数位​​:10000010
  • ​尾数位​​:10110100000000000000000
  • ​完整二进制​​:
    0 10000010 10110100000000000000000 → 十六进制为 0x415A0000

浮点类型转化成数字

下面以0x3FC00000 为例,将二进制转换成浮点数字

步骤一:​​拆分十六进制值
  • 十六进制:3FC00000 → 二进制:0011 1111 1100 0000 0000 0000 0000 0000
  • 按 IEEE 754 标准划分:
  • ​符号位 S​​(1位):0(正数)
  • ​指数位 E​​(8位):01111111(十进制 127)
  • ​尾数位 M​​(23位):10000000000000000000000
步骤二:计算指数偏移值

     实际指数 = E - 127 = 127 - 127 = 0

步骤三:计算尾数值

    尾数隐含整数部分 1(IEEE 754 规范),因此实际尾数为:

 1 + M = 1 + 0.10000000000000000000000₂

 M的二进制小数部分10000000000000000000000转化为10进制,套入公式:

   

  示例:

示例​​(单精度尾数 100000000000011110000000):

第 1 位:1×2−1=0.5
第 13–16 位:1×(2−13+2−14+2−15+2−16)≈0.0002289
总和:1+0.5+0.0002289=1.5002289

    二进制小数 0.1₂ = 十进制 0.5(因 1×2⁻¹ = 0.5

       最终尾数 = 1 + 0.5 = 1.5

    步骤四:组合结果

    • 浮点数值 = (-1)^S × 尾数 × 2^{实际指数}
      1 × 1.5 × 2^0
      = ​​1.5​​。

    4. sub指令

    减法指令, sub         rsp,10h 指在rsp栈顶指针上分配16字节的内存空间,栈布局将会进行如下变化

    执行 sub rsp,10h 后:
    +-----------------+ <-- RSP
    | 16字节栈空间    |  // 初始化为 0xCC
    +-----------------+
    | 保存的 RDI      |  // push rdi
    +-----------------+ <-- 原始 RSP
    | 浮点参数        |  // [rsp+8] → [新RSP+20h]
    +-----------------+

    这里有个小细节:

    为什么分配16字节?

    在Debug模式下,编译器可能会分配额外的栈空间,即使这些空间并不完全被使用。这通常是为了:

    1. ​调试方便​​:分配的空间可以用于存储临时变量或用于调试检查(例如用0xCC填充以检测未初始化内存的使用)。
    2. ​栈对齐​​:x86-64架构要求栈在函数调用时保持16字节对齐(特别是在调用某些需要对齐的指令如SSE指令之前)。虽然在这个函数中,分配16字节之前栈已经是16字节对齐的(因为进入函数时RSP是16字节对齐的,然后push rdi(8字节)使RSP变为8字节对齐,再减去16字节(16的倍数)后,RSP又变为16字节对齐),但分配16字节可以确保后续操作满足对齐要求。
    3. ​填充初始化​​:在Debug模式下,编译器可能会分配额外的空间并用特定值(如0xCC)填充,以便在调试时更容易识别未初始化的内存访问。

    为什么用0xCC填充?

    • 0xCC是调试模式下常用的填充值,它有两个作用:
      1. 在调试器中,当程序执行到0xCC时,会触发断点(因为0xCC是int 3指令的机器码)。
      2. 在内存中填充0xCC,如果程序错误地执行了这些区域,会立即触发断点,便于发现错误。
      3. 另外,0xCC在内存中显示为连续的“烫”字(在GBK编码中),便于肉眼识别未初始化内存。

     5. rep指令

    重复前缀,作用​​:根据 ECX(32位)或 RCX(64位)寄存器的值重复执行后续指令,每次执行后计数器减1,直到计数器为

    6. stos指令

    存储字符串指令,将 EAX(32位)或 RAX(64位)寄存器的值存储到 [EDI] 或 [RDI] 指向的内存地址,并根据方向标志(DF)更新指针

    7. 重复操作

      000000000000006B: 48 8B FC           mov         rdi,rsp
      000000000000006E: B9 04 00 00 00     mov         ecx,4
      0000000000000073: B8 CC CC CC CC     mov         eax,0CCCCCCCCh
      0000000000000078: F3 AB              rep stos    dword ptr [rdi]

    这几行代码的核心就是rep stos指令, 首先将rsp栈顶指针的地址给到rdi寄存器,给ecx寄存器赋值为4, eax寄存器赋值为0xCCCCCCCC

    req指令为将 stos dword ptr [rdi] 指令重复操作 ecx次,即4次

    stos指令为将eax的数据存储到rdi的地址,即栈顶指针,就是前面sub指令分配的16字节内存内

    8. mulss,addss指令

    故名思意,mulss即将两个数相乘,放在第一个数的地址

    addss则为相加

    9. 内存释放

    在函数结尾处的 add rsp, 10h 指令与函数开头的 sub rsp, 10h 严格对应, 调整栈顶指针的位置。

    三、汇编转化

    single_float_param:
      0000000000000060: F3 0F 11 44 24 08  movss   dword ptr [rsp+8],xmm0  ; 保存浮点参数到栈
      0000000000000066: 57                 push    rdi                     ; 保存 RDI
      0000000000000067: 48 83 EC 10        sub     rsp,10h                 ; 分配 16 字节栈空间
      000000000000006B: 48 8B FC           mov     rdi,rsp                 ; RDI = 栈顶指针
      000000000000006E: B9 04 00 00 00     mov     ecx,4                   ; 循环计数 = 4
      0000000000000073: B8 CC CC CC CC     mov     eax,0CCCCCCCCh           ; 填充值 = 0xCC
      0000000000000078: F3 AB              rep stos dword ptr [rdi]        ; 用 0xCC 填充栈空间
      000000000000007A: F3 0F 10 44 24 20  movss   xmm0,dword ptr [rsp+20h] ; 加载参数到 XMM0
      0000000000000080: F3 0F 59 05 00 00  mulss   xmm0,dword ptr [__real@3fc00000] ; 乘以 1.5f
      0000000000000088: F3 0F 58 05 00 00  addss   xmm0,dword ptr [__real@3f000000] ; 加上 0.5f
      0000000000000090: 48 83 C4 10        add     rsp,10h                 ; 释放栈空间
      0000000000000094: 5F                 pop     rdi                     ; 恢复 RDI
      0000000000000095: C3                 ret                             ; 返回结果

    其中很大部分的操作都是debug调试添加的,release都会优化掉,优化后即:

    Release 模式优化版本
    mulss xmm0, [__real@3fc00000]
    addss xmm0, [__real@3f000000]
    ret

    转化为C语言:

    // 函数声明
    float single_float_param(float param);
    
    // 实际实现
    float single_float_param(float param) {
        // Debug 模式专用:分配并初始化栈空间
        volatile unsigned char stack_buf[16]; 
        for (int i = 0; i < sizeof(stack_buf); i++) {
            stack_buf[i] = 0xCC; // 调试填充值
        }
        
        // 实际计算:1.5 * param + 0.5
        float result = param * 1.5f + 0.5f;
        
        return result;
    }


    网站公告

    今日签到

    点亮在社区的每一天
    去签到