返回介绍

15.1 简单实例

发布于 2025-02-22 14:00:45 字数 4602 浏览 0 评论 0 收藏 0

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

#!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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文