15.1 简单实例

下面我们来研究一个简单的例子

  1. double f (double a, double b)
  2. {
  3. return a/3.14 + b*4.1;
  4. }

15.1.1 x86

在msvc2010中编译

  1. CONST SEGMENT
  2. __real@4010666666666666 DQ 04010666666666666r ; 4.1
  3. CONST ENDS
  4. CONST SEGMENT
  5. __real@40091eb851eb851f DQ 040091eb851eb851fr ; 3.14
  6. CONST ENDS
  7. _TEXT SEGMENT
  8. _a$ = 8 ; size = 8
  9. _b$ = 16 ; size = 8
  10. _f PROC
  11. push ebp
  12. mov ebp, esp
  13. fld QWORD PTR _a$[ebp]
  14. ; current stack state: ST(0) = _a
  15. fdiv QWORD PTR __real@40091eb851eb851f
  16. ; current stack state: ST(0) = result of _a divided by 3.13
  17. fld QWORD PTR _b$[ebp]
  18. ; current stack state: ST(0) = _b; ST(1) = result of _a divided by 3.13
  19. fmul QWORD PTR __real@4010666666666666
  20. ; current stack state: ST(0) = result of _b * 4.1; ST(1) = result of _a divided by 3.13
  21. faddp ST(1), ST(0)
  22. ; current stack state: ST(0) = result of addition
  23. pop ebp
  24. ret 0
  25. _f ENDP

FLD从栈中取8个字节并将这个数字放入ST(0)寄存器中,自动将它转换成内部80位格式的扩展操作数。

FDIV除存储在ST(0)中地址指向的数值 __real@40091eb851eb851f —3.14 就放在那里。

汇编语法丢失浮点数,因此,我们这里看到的是64位IEEE754编码的16进制表示的3.14。

执行FDIV执行后,ST(0)将保存除法的结果。

另外,这里也有FDIVP指令,用ST(0)除ST(1),从栈中将将这些值抛出来,然后将结果压栈。如果你懂forth语言,你会很快意识到这是堆栈机。

FLD指令将b的值压入栈中之后,商放入ST(1)寄存器中,ST(0)中保存b的值。

接下来FMUL指令将来自ST(0)的b值和在__real@4010666666666666 (4.1 的值在那里)相乘,然后将结果放入ST(0)中。

最后,FADDP指令将栈顶的两个值相加,将结果存储在ST(1)寄存器中,然后从ST(1)中弹出,再放入ST(0)中。

这个函数必须返回ST(0)寄存器中的值,因此,在执行FADDP命令后,没有其他额外的的指令了需要执行了。

GCC 4.4.1(选项03)生成基本同样的代码,有小小的不同之处。

不同之处在于,首先,3.14被压入栈中(进入ST(0)),然后arg_0的值除以ST(0)寄存器中的值

FDIVR 意味着逆向除法 被除数和除数交换。

因为乘法两个乘数可交换,所以没有这样的指令,我们只有FMUL而没有逆乘。

FADDP也是将两个值相加,其中一个来自栈。然后ST(0)保存它们的和。

这段反编译代码的碎片是由IDA产生的,ST(0)简称为ST。

15.1.2 ARM: Xcode优化模式(LLVM)+ARM 模式

直到ARM有标准化的浮点数支持后,几家处理器厂商才将其加入到他们自己指令扩展中。然后,VFP(向量浮点运算单元)标准化了。

与x86相比,一个重要的不同是,在x86中使用fpu栈工作,而在ARM中,这里没有栈,你只能使用寄存器。

  1. f
  2. VLDR D16, =3.14
  3. VMOV D17, R0, R1 ; load a
  4. VMOV D18, R2, R3 ; load b
  5. VDIV.F64 D16, D17, D16 ; a/3.14
  6. VLDR D17, =4.1
  7. VMUL.F64 D17, D18, D17 ; b*4.1
  8. VADD.F64 D16, D17, D16 ; +
  9. VMOV R0, R1, D16
  10. BX LR
  11. dbl_2C98 DCFD 3.14 ; DATA XREF: f
  12. dbl_2CA0 DCFD 4.1 ; DATA XREF: f+10

可以看到,这里我们使用了新的寄存器,并以D开头。这些是64位寄存器,有32个,他们既可以用作浮点数(double)运算也可以用作SIMD(在ARM中称为NEON)。

它们同时也可以作为32个32位的S寄存器使用,它们被用于单精度操作浮点数(float)运算。

记住它们很容易:D系列寄存器用于双精度数字,S寄存器用于单精度数字,记住Double和Single的首字母就可以了。

两个常量(3.14和4.1)都是以IEEE 754的形式存储在内存中。

VLDR和VMOV指令,容易推断,类似LDR和MOV指令,但是它们使用D系列寄存器,需要注意的就是这些指令不就之后也会展现出,就像D系列寄存器一样,不仅可以进行浮点数运算而且也可以用于SIMD(NEON)运算,参数传递的方式仍旧是通过R系列寄存器传递,但是每个具有双精度的数值有64位,所以为了便于传递需要两个寄存器。

  1. VMOV D17,R0,R1在最开始,将两个来自R0R132位的值组成一个64位的值并且将它保存在D17中。
  2. VMOV R0,R1,D16是一个逆操作,D16中的值放回R0,R1中。
  3. VDIV,VMUL,VADD都是用于浮点数的处理计算的指令,分别为除法指令,乘法指令,加法指令。

thumb-2的代码也是相同的。

15.1.3 ARM:优化 keil+thumb 模式

  1. f
  2. PUSH {R3-R7,LR}
  3. MOVS R7, R2
  4. MOVS R4, R3
  5. MOVS R5, R0
  6. MOVS R6, R1
  7. LDR R2, =0x66666666
  8. LDR R3, =0x40106666
  9. MOVS R0, R7
  10. MOVS R1, R4
  11. BL __aeabi_dmul
  12. MOVS R7, R0
  13. MOVS R4, R1
  14. LDR R2, =0x51EB851F
  15. LDR R3, =0x40091EB8
  16. MOVS R0, R5
  17. MOVS R1, R6
  18. BL __aeabi_ddiv
  19. MOVS R2, R7
  20. MOVS R3, R4
  21. BL __aeabi_dadd
  22. POP {R3-R7,PC}
  23. dword_364 DCD 0x66666666 ; DATA XREF: f+A
  24. dword_368 DCD 0x40106666 ; DATA XREF: f+C
  25. dword_36C DCD 0x51EB851F ; DATA XREF: f+1A
  26. dword_370 DCD 0x40091EB8 ; DATA XREF: f+1C

keil为处理器生成的代码不支持FPU和NEON。因此,双精度浮点数通过通用R寄存器来传递双精度数字,与FPU指令不同的是,通过对库函数调用(如_aeabidmul, aeabi_ddiv, aeabi_dadd)用来实现乘法,除法,浮点数加法。当然,这比FPU协处理器慢,但总比没有强。

另外,在x86的世界中,当协处理器少而贵并且只安装昂贵的计算机上时,在FPU模拟库非常受欢迎。

在ARM的世界中,FPU处理器模拟称为soft float 或者armel,用协处理器的FPU指令的称为hard float和armhf。

举个例子,树莓派的linux内核用两种变量编译。如果是soft float,参数就会通过R系列寄存器编码,hard float则会通过D系列寄存器。

这就是不让你使用例子中来自armel编码的armhf库原因,反之亦然。那也是linux分区必须根据调用惯例编译的原因。