《Reverse Engineering for Beginners》 - 第1章 代码模式 - 笔记(1.18)

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位放到 V0MFC11FPU1CPUpow A3和$A2。printf就从这两个寄存器读取参数。

1.18.7. 比较例子

比较两个浮点数的大小。

x86

未优化的MSVC

比较的时候,b放在ST(0)中,a放在栈上。首先FLD将b加载到ST(0)中。FCOMP比较ST(0)和a的值,然后设置FPU状态字寄存器中的C3/C2/C0。这是一个16位的寄存器,保存FPU的状态。设置好位之后,FCOMP从栈上弹出变量,这就是与FCOM的不同点。不过这是老CPU的做法,现代CPU使用FCOMI/FCOMIP/FUCOMI/FUCOMIP指令,做同样的事,但修改的是ZF/PF/CF的flags。

FNSTSW指令将FPU的状态字寄存器中的内容复制给AX。C3/C2/C0放在14/10/8位置,与AX寄存器相同位置,都放在高地址部分AX-AH。

如果b>a,C3/C2/C0设置为0,0,0
a>b,0,0,1
a=b,1,0,0
结果出错,则是1,1,1

下面的指令test ah, 5,只考虑C0和C2位,其他的都被忽略了。然后会根据AX的值设置PF的值,根据PF值判断是否跳转。
如果条件跳转触发,FLD会将b的值加载到ST(0)。如果没有触发,就加载a的值。

注意C2主要是存放错误信息的位。

优化的MSVC2010

FCOM与FCOMP不一样,没有POP这一步,不更改FPU栈。与上一个例子不同,这里操作数是反的顺序。

如果b>a,C3/C2/C0设置为0,0,0
a>b,0,0,1
a=b,1,0,0

test ah, 65检查2个位,即C3和C0。然后FSTP ST(1)把值从ST(0)复制到ST(1),如果a>b就是a。

注意这个比较的两个数放在ST(0)和ST(1)中。

FSTP ST相当于pop这个FPU栈。

GCC

FUCOMPP与FCOM差不多,从栈中弹出两个变量。FPU可以处理非数字的值,如NaN。FCOM会触发异常,如果操作数是NaN。
SAHF将AH值存储到flags中。

fnstsw ax/sahf指令将C3/C2/C0移动到ZF, PF, CF。其实具体的操作与上面的都差不多。

如果a>b,CPU设置为ZF=0, PF=0, CF=0。
a

优化的GCC

与上面的差不多,除了在SAHF之后使用JA。JA是在CF和ZF都为0的情况下触发,其实与上面的结果一样,但是节省了大量的代码。

GCC:-03优化打开

FUCOMI比较ST(0)中的a值和ST(1)中的b值,然后在CPU设置一些FLAGS。FCMOVBE检查flags并将ST(1)的b值复制给ST(0),如果a

ARM

优化的Xcode(LLVM)(ARM模式)

输入值a和b分别放在D16和D17中。然后使用VCMPE指令进行比较。ARM有自己的flag寄存器,即FPSCR。VMRS将4位(N, Z, C, V)从处理器的状态字拷贝到通用状态寄存器APSR。

VMOVGT是D寄存器的指令,如果比较两个操作数,前者大于后者就执行(GT)。如果执行,a的值写入的D16(原本存在D17)。否则就是b在D16中。

VMOV准备好D16中的值,通过R0和R1返回值。

优化的Xcode(LLVM)(Thumb-2模式)

与之前的例子基本相同,但有一点不一样。我们已经知道ARM模式的很多指令可以通过条件谓词支持,但是Thumb模式中没有。IT GT指令表示if-then条件。ITE是if-then-else。

优化的Xcode(LLVM)(ARM模式)

基本相同,但是有很多冗余代码,因为a和b变量存在局部栈上。

优化的Keil(Thumb模式)

Keil不生成FPU指令,调用外部库来完成比较:_aeabi_cdrcmple。

ARM64

优化的GCC

有FPU指令。首先是FCMPE,比较D0和D1中的值,然后设置APSR标签(N, Z, C, V)。FCSEL根据条件不同将D0或D1的值赋值给D0。条件比较要根据APSR。

未优化的GCC

把参数存放在本地栈。然后从本地栈放到X0/X1,然后再放到D0/D1进行比较。本质上是使用了传统的BLE进行跳转,借用寄存器X0。最后把X0返回到D0进行printf的调用。

练习

试着手动优化上面的代码。

优化的GCC-float

把double换成float。这时没有用D寄存器,而是用了S寄存器。

MIPS

在FPU里设置条件,在CPU中检查。检查位放在FCCR中。C.LT.D比较两个值。LT是条件小于的意思。D表示数据类型为double。依据比较结果,FCC0是条件位被设置。

BC1T检查FCC0的位,然后跳到相关位置。T表示如果为True则跳转。因此存在BC1F。

1.18.8. 栈,计算器和逆向波兰注释

一些计算器使用reverse Polish notation。比如12,34的加法,先输入12和34,再输入+。因为计算器的实现就像栈一样。

1.18.9. x64

1.18.10. 练习

http://challenges.re/60
http://challenges.re/61
求5个数的平均数。

你可能感兴趣的:(《Reverse Engineering for Beginners》 - 第1章 代码模式 - 笔记(1.18))