- 第一章 CPU 简介
- 第二章 Hello,world!
- 第三章 函数开始和结束
- 第四章 栈
- Chapter 5 printf() 与参数处理
- Chapter 6 scanf()
- CHAPER7 访问传递参数
- Chapter 8 一个或者多个字的返回值
- Chapter 9 指针
- Chapter 10 条件跳转
- 第 11 章 选择结构 switch()/case/default
- 第 12 章 循环结构
- 第 13 章 strlen()
- Chapter 14 Division by 9
- chapter 15 用 FPU 工作
- Chapter 16 数组
- Chapter 17 位域
- 第 18 章 结构体
- 19 章 联合体
- 第二十章 函数指针
- 第 21 章 在 32 位环境中的 64 位值
- 第二十二章 SIMD
- 23 章 64 位化
- 24 章 使用 x64 下的 SIMD 来处理浮点数
- 25 章 温度转换
- 26 章 C99 的限制
- 27 章 内联函数
- 第 28 章 得到不正确反汇编结果
- 第 29 章 花指令
- 第 30 章 16 位 Windows
- 第 31 章 类
- 三十二 ostream
- 34.2.2 MSVC
- 34.2.3 C++ 11 std::forward_list
- 34.3 std::vector
- 34.4 std::map and std::set
15.1 简单实例
下面我们来研究一个简单的例子
#!bash
double f (double a, double b)
{
return a/3.14 + b*4.1;
}
15.1.1 x86
在 msvc2010 中编译
#!bash
CONST SEGMENT
__real@4010666666666666 DQ 04010666666666666r ; 4.1
CONST ENDS
CONST SEGMENT
__real@40091eb851eb851f DQ 040091eb851eb851fr ; 3.14
CONST ENDS
_TEXT SEGMENT
_a$ = 8 ; size = 8
_b$ = 16 ; size = 8
_f PROC
push ebp
mov ebp, esp
fld QWORD PTR _a$[ebp]
; current stack state: ST(0) = _a
fdiv QWORD PTR __real@40091eb851eb851f
; current stack state: ST(0) = result of _a divided by 3.13
fld QWORD PTR _b$[ebp]
; current stack state: ST(0) = _b; ST(1) = result of _a divided by 3.13
fmul QWORD PTR __real@4010666666666666
; current stack state: ST(0) = result of _b * 4.1; ST(1) = result of _a divided by 3.13
faddp ST(1), ST(0)
; current stack state: ST(0) = result of addition
pop ebp
ret 0
_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 中,这里没有栈,你只能使用寄存器。
#!bash
f
VLDR D16, =3.14
VMOV D17, R0, R1 ; load a
VMOV D18, R2, R3 ; load b
VDIV.F64 D16, D17, D16 ; a/3.14
VLDR D17, =4.1
VMUL.F64 D17, D18, D17 ; b*4.1
VADD.F64 D16, D17, D16 ; +
VMOV R0, R1, D16
BX LR
dbl_2C98 DCFD 3.14 ; DATA XREF: f
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 位,所以为了便于传递需要两个寄存器。
“VMOV D17,R0,R1”在最开始,将两个来自 R0 和 R1 的 32 位的值组成一个 64 位的值并且将它保存在 D17 中。
“VMOV R0,R1,D16”是一个逆操作,D16 中的值放回 R0,R1 中。
VDIV,VMUL,VADD 都是用于浮点数的处理计算的指令,分别为除法指令,乘法指令,加法指令。
thumb-2 的代码也是相同的。
15.1.3 ARM:优化 keil+thumb 模式
#!bash
f
PUSH {R3-R7,LR}
MOVS R7, R2
MOVS R4, R3
MOVS R5, R0
MOVS R6, R1
LDR R2, =0x66666666
LDR R3, =0x40106666
MOVS R0, R7
MOVS R1, R4
BL __aeabi_dmul
MOVS R7, R0
MOVS R4, R1
LDR R2, =0x51EB851F
LDR R3, =0x40091EB8
MOVS R0, R5
MOVS R1, R6
BL __aeabi_ddiv
MOVS R2, R7
MOVS R3, R4
BL __aeabi_dadd
POP {R3-R7,PC}
dword_364 DCD 0x66666666 ; DATA XREF: f+A
dword_368 DCD 0x40106666 ; DATA XREF: f+C
dword_36C DCD 0x51EB851F ; DATA XREF: f+1A
dword_370 DCD 0x40091EB8 ; DATA XREF: f+1C
keil 为处理器生成的代码不支持 FPU 和 NEON。因此,双精度浮点数通过通用 R 寄存器来传递双精度数字,与 FPU 指令不同的是,通过对库函数调用(如 aeabi_dmul, 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 分区必须根据调用惯例编译的原因。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论