一、汇编代码
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:规格化(科学计数法)
- 移动小数点至首位为
1
:1101.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模式下,编译器可能会分配额外的栈空间,即使这些空间并不完全被使用。这通常是为了:
- 调试方便:分配的空间可以用于存储临时变量或用于调试检查(例如用0xCC填充以检测未初始化内存的使用)。
- 栈对齐:x86-64架构要求栈在函数调用时保持16字节对齐(特别是在调用某些需要对齐的指令如SSE指令之前)。虽然在这个函数中,分配16字节之前栈已经是16字节对齐的(因为进入函数时RSP是16字节对齐的,然后push rdi(8字节)使RSP变为8字节对齐,再减去16字节(16的倍数)后,RSP又变为16字节对齐),但分配16字节可以确保后续操作满足对齐要求。
- 填充初始化:在Debug模式下,编译器可能会分配额外的空间并用特定值(如0xCC)填充,以便在调试时更容易识别未初始化的内存访问。
为什么用0xCC填充?
- 0xCC是调试模式下常用的填充值,它有两个作用:
- 在调试器中,当程序执行到0xCC时,会触发断点(因为0xCC是
int 3
指令的机器码)。- 在内存中填充0xCC,如果程序错误地执行了这些区域,会立即触发断点,便于发现错误。
- 另外,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;
}