1.18. 浮点数单元
CPU中的FPU
1.18.1. IEEE 754
sign+significand+exponent
1.18.2. x86
一开始FPU和CPU是分开的,FWAIT用来转换CPU状态,等待FPU完成工作。FPU有一个栈,用来保存8个80比特的寄存器ST(0)……ST(7)。
1.18.3. ARM, MIPS, x86/x64 SIMD
ARM和MIPS不是栈,而是一些寄存器,在x86/x64的SIMD拓展也是。
1.18.4. C/C++
2种浮点类型,float(32bits)和double(64bits)。
1.18.5. 简单例子
return and printf a/3.15+b*4.1
x86
MSVC
fld QWORD PTR _a$[ebp]
fdiv QWORD PTR _real@40091eb851eb851f 即a/3.14
ST(0)=a, 用a/3.14放入ST(0)
ST(0)=b,先把ST(0)->ST(1)因为是栈
ST(0)=b*4.1 -> ST(0)
把ST(0)、ST(1)加起来->ST(0)
MSVC+OllyDbg
注意FPU的这个栈是循环的。
GCC
不一样的是,第一步ST(0)=3.14
fdivv [ebp+arg_0], 其中arg_0存放a
乘法也是,乘数与被乘数顺序与x86不同。
ARM:优化的Xcode(LLVM)(ARM模式)
VFP标准,没有栈,只有寄存器,D开头双精度,S开头单精度。
Thumb-2的代码是一样的。
ARM:优化的Keil(Thumb模式)
调用了一些库函数,模拟FPU,但其实是软件实现的。经济。
ARM64:优化的GCC
ldr d2, .LC25 ;3.14
fdiv d0, d0, d2 ;计算除法
ldr d2, .LC26 ;4.1
fwadd d0, d1, d2, d0 ;计算乘法和加法
ret
ARM64:非优化的GCC
没有必要地把值倒来倒去。
MIPS
最多可以支持4个coprocessor。也没有栈,用寄存器。
LWC1加载32位字到第一个coprocessor的寄存器。
DIV.D MUL.D ADD.D
1.18.6. 通过参数传递浮点数
一个简单的例子:
printf(“32.01 ^ 1.54 = %lf\n”, pow(32.01, 1.54));
x86
先给第一个变量分配空间,然后使用fld和fstp指令。这两个指令把变量在数据段和FPU栈之间进行移动。所以这两条语句实现了将32.01从数据段移到栈中。同样地,1.54也被移到栈中,然后调用了pow函数,函数返回值存放在ST(0)中。然后用fstp从ST(0)移动到本地栈,调用printf函数。
ARM和未优化的Xcode(LLVM)(Thumb-2模式)
64位的浮点数是用寄存器传递的,而不是栈。未优化的代码有点冗余。pow函数接收R0+R1为第一个参数,R2+R3为第二个参数。结果保存在R0+R1。pow函数的结果随后会被移动到D16中,然后再存到R1+R2里,printf就从这两个寄存器接受参数。
ARM和未优化的Keil(ARM模式)
还是用R0+R1存第一个参数,R2+R3存第二个参数。然后直接把pow函数结果放到了R3+R2,然后调用printf,不过这里没有使用D开头的寄存器,只使用了R开头的寄存器。
AMR64和优化的GCC
常数加载到D0和D1,这是pow的参数。结果放在D0中。然后会把D0直接传递给printf。实际上printf一般从X寄存器取整数类型值,从D寄存器取浮点数类型值。
MIPS
LUI指令将浮点数的32位放到 V0,但是这一步是多余的