学底层浮点数运算?x86 FPU 指令与堆栈操作指南
- 作者: 刘杰
- 来源: 技术那些事
- 阅读:173
- 发布: 2025-07-14 20:02
- 最后更新: 2025-07-14 20:02
浮点数的计算
浮点运算单元是从80486处理器开始才被集成到CPU中的,该运算单元被称为FPU浮点运算模块,FPU不使用CPU中的通用寄存器,其有自己的一套寄存器,被称为浮点数寄存器栈,FPU将浮点数从内存中加载到寄存器栈中,完成计算后在回写到内存中。
FPU有8个可独立寻址的80位寄存器,分别名为R0-R7他们以堆栈的形式组织在一起,栈顶由FPU状态字中的一个名为TOP的域组成,对寄存器的引用都是相对于栈顶而言的,栈顶通常也被叫做ST(0)最后一个栈底则被记作ST(7)其使用方式与堆栈一致。
浮点数运算通常会使用一些更长的数据类型,如下就是MASM汇编器定义的常用数据类型.
assembly
.data
var1 QWORD 10.1 ; 64位整数
var2 TBYTE 10.1 ; 80位(10字节)整数
var3 REAL4 10.2 ; 32位(4字节)短实数
var4 REAL8 10.8 ; 64位(8字节)长实数
var5 REAL10 10.10 ; 80位(10字节)扩展实数
此外浮点数对于指令的命名规范也遵循一定的格式,浮点数指令总是以F开头,而指令的第二个字母则表示操作位数,例如:B表示二十进制操作数,I表示二进制整数操作,如果没有指定则默认则是针对实数的操作fld等.
FLD/FSTP
FLD 和 FSTP 是x86架构处理器中的浮点操作指令,FLD指令用于将浮点数从内存装载进浮点寄存器,或者FSTP指令从浮点寄存器存储到内存中。
FLD 指令用于从内存中读取单精度浮点数(32位)或双精度浮点数(64位),并将其存储到浮点栈中。FLD 指令的语法如下:
assembly
FLD source
其中,source 可以是内存地址、寄存器或立即数。例如,要将双精度浮点数3.14159存储到浮点栈中,可以使用以下指令:
assembly
movsd xmm0, [pi] ; 将pi常量的值放入xmm0寄存器中
movsd [esp], xmm0 ; 将xmm0寄存器中的值存储到栈顶
fld qword ptr [esp] ; 将栈顶的值从内存中装载到浮点栈中
其中,xmm0 是双精度浮点寄存器,pi 是一个双精度浮点常量的地址,esp 是堆栈指针寄存器,qword ptr标记用于指示要读取的内存单元的数据大小。
FSTP 指令用于将浮点栈顶的值弹出,并将其存储到内存中。FSTP指令的语法如下:
assembly
FSTP destination
其中,destination 可以是内存地址、寄存器或立即数。例如,将浮点栈顶的值存储到内存单元 x 中,可以使用以下指令:
assembly
fstp qword ptr [x] ; 将浮点栈顶的值存储到 x 变量的内存单元中
需要注意,FSTP 指令会将浮点栈顶部的值弹出,在栈顶的值被存储到目标地址之后,浮点栈顶部的指针将自动下移。
FADD/FADDP/FIADD
浮点数加法系列指令,该系列可分为FADD/FADDP/FIADD,这些指令分别针对不同的场景使用,此外还会区分无操作数模式,寄存器操作数,内存操作数,整数相加等。这些指令用于不同的场景下进行操作,如下所述:
FADD 指令用于将两个浮点数相加,并将结果存储到浮点寄存器中。FADD指令支持多种操作数类型,包括无操作数模式、寄存器操作数和内存操作数等。例如,将一个双精度浮点数和一个32位整数相加,可以使用以下指令:
assembly
fld qword ptr [x] ; 将双精度浮点数x装载到栈顶
fiadd dword ptr [y] ; 将32位整数y装载到浮点寄存器中,并与栈顶的浮点数相加
fstp qword ptr [z] ; 将浮点栈顶的值存储到双精度浮点数z中
FADDP 指令也是用于将两个浮点数相加,但是会将结果弹出并存储到目标寄存器或内存中。FADDP指令与FADD指令最大的区别在于它弹出了浮点栈顶的值。例如,将两个单精度浮点数相加并将结果存储到内存中,可以使用以下指令:
assembly
fld dword ptr [x] ; 将单精度浮点数x1装载到栈顶
fadd dword ptr [y] ; 将单精度浮点数x2装载到栈顶,并与栈顶的数相加
fstp dword ptr [z] ; 将浮点栈顶的值存储到单精度浮点数z中,同时弹出栈顶
FIADD 指令用于将一个整数加到浮点寄存器的值中。与FADD指令不同,其支持的数据类型只有整数类型,而没有浮点数类型。使用FIADD指令时,要将操作数用一个寄存器或内存地址表示。例如,将一个16位有符号整数加到浮点数中,可以使用以下指令:
assembly
fild word ptr [x] ; 将16位有符号整数x装载到浮点寄存器中
fadd dword ptr [y] ; 将32位浮点数y装载到栈顶,并与浮点寄存器中的整数相加
fstp dword ptr [z] ; 将浮点栈顶的值存储到双精度浮点数z中
如下汇编代码将分别总结四种不同的浮点数计算方式,读者可自行根据提示信息理解这其中的含义。
- 第一种:无操作数模式,执行FADD时,ST(0)寄存器和ST(1)寄存器相加后,结果临时存储在ST(1)中,然后将ST(0)弹出堆栈,最终结果就会存储在栈顶部,使用FST指令即可取出来。
- 第二种:则是两个浮点寄存器相加,最后的结果会存储在源操作数ST(0)中。
- 第三种:则是内存操作数,就是ST寄存器与内存相加。
- 第四种:是与整数相加,默认会将整数扩展为双精度,然后在于ST(0)相加。
assembly
.386p
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
include msvcrt.inc
includelib msvcrt.lib
.data
Array QWORD 10.0,20.0,30.0,40.0,50.0
IntA DWORD 10
Result QWORD ?
szFmt BYTE 'ST寄存器: %f ',0dh,0ah,0
.code
main PROC
finit
fld qword ptr ds:[Array]
fld qword ptr ds:[Array + 8]
fld qword ptr ds:[Array + 16]
fld qword ptr ds:[Array + 24]
fld qword ptr ds:[Array + 32]
; 第一种:无操作数 fadd = faddp
;fadd
;faddp
; 第二种:两个浮点寄存器相加
fadd st(0),st(1) ; st(0) = st(0) + st(1)
fst qword ptr ds:[Result] ; 取出结果
invoke crt_printf,addr szFmt,qword ptr ds:[Result]
fadd st(0),st(2) ; st(0) = st(0) + st(2)
fst qword ptr ds:[Result] ; 取出结果
invoke crt_printf,addr szFmt,qword ptr ds:[Result]
; 第三种:寄存器与内存相加
fadd qword ptr ds:[Array] ; st(0) = st(0) + Array
fst qword ptr ds:[Result] ; 取出结果
invoke crt_printf,addr szFmt,qword ptr ds:[Result]
fadd real8 ptr ds:[Array + 8]
fst qword ptr ds:[Result] ; 取出结果
invoke crt_printf,addr szFmt,qword ptr ds:[Result]
; 第四种:与整数相加
fiadd dword ptr ds:[IntA]
fst qword ptr ds:[Result] ; 取出结果
invoke crt_printf,addr szFmt,qword ptr ds:[Result]
int 3
main ENDP
END main
FSUB/FSUBP/FISUB
x86架构处理器的浮点数减法指令有FSUB/FSUBP/FISUB该系列指令从目的操作数中减去原操作数,把差存储在目的操作数中,目的操作数必须是ST寄存器,源操作数可以是寄存器或内存,运算的过程与加法指令完全一致。
FMUL/FMULP/FIMUL
针对浮点数乘法指令有三种FMUL/FMULP/FIMUL第一个指令用于将堆栈上的浮点数相乘返回值放入到堆栈上,第二个指令则是相乘后将结果从堆栈中弹出,第三个指令则是将浮点数相乘并将结果存储回堆栈中,针对浮点数乘法指令总结如下:
- FMUL指令:将堆栈上的两个浮点数相乘,并将结果存储回堆栈中。它可以只在ST0和ST1之间执行乘法操作。例如,执行FMUL ST1, ST0将ST0和ST1中的两个数相乘,并将结果存储回ST1中。 FMUL指令使用栈操作数。
- FMULP指令:将堆栈上的两个浮点数相乘,但是不同于FMUL,它会从栈中弹出一个浮点数。例如,执行FMULP ST1, ST0将ST0和ST1中的两个数相乘,并将结果存储回ST1中,然后将ST0从堆栈中弹出。 FMULP指令使用栈操作数。
- FIMUL指令:将堆栈上的两个浮点数(或整数)相乘,并将结果存储回堆栈中。它只在ST0和ST1之间执行乘法操作,但是当它们的值为整数时,使用的密度为16位(计算2个字)。例如,执行FIMULWORD PTR [eax]通常用于使用16位整数执行浮点数乘法。 FIMUL指令使用栈操作数。
FDIV/FDIVP/FIDIV
对于浮点数除法运算其调用原理与乘法运算完全一致,对于浮点数除指令同样包含有FDIV/FDIVP/FIDIV这三种类型,如下则是三种类型的说明:
- FDIV指令:将堆栈上的ST1浮点数除以ST0浮点数,并将结果存储回ST1中。 FDIV指令使用栈操作数。
- FDIVP指令:将堆栈上的ST1浮点数除以ST0浮点数,不同于FDIV,它还将ST0从堆栈中弹出。例如,执行FDIVP ST1, ST0将ST1除以ST0,将结果存储回ST1中,然后将ST0从堆栈中弹出。 FDIVP指令使用栈操作数。
- FIDIV指令:将堆栈上的浮点数(或整数)ST0被ST1浮点数乘除,并将结果存储回堆栈中。 FIDIV指令使用栈操作数。
FCOM/FCOMP/FCOMPP
浮点数比较指令包括FCOM/FCOMP/FCOMPP这三个指令都是比较ST(0)和源操作数,源操作数可以是内存操作数或FPU寄存器,FCOM和FCOMP格式基本一致,唯一区别在于FCOMP在执行对比后还要从堆栈中弹出元素,而FCOMP和FCOMPP也基本一致,最后都是要从堆栈中弹出元素。