【问题标题】:Return a float from a 64-bit assembly function that uses x87 FPU从使用 x87 FPU 的 64 位汇编函数返回浮点数
【发布时间】:2016-01-16 23:12:23
【问题描述】:

我正在尝试制作一个程序来计算使用 64 位寄存器、浮点数和协处理器指令的方程(目前什么方程无关紧要)。不幸的是,我不知道如何将方程的最终结果作为浮点数访问。我能做到:

fist qword ptr [bla]
mov rax,bla

并将函数类型更改为 INT 并获取我的值,但我无法将其作为 FLOAT 访问。即使我将结果留在 ST(0)(协处理器堆栈的顶部)中,它也无法按预期工作,并且我的 C++ 程序得到错误的结果。我的汇编代码是:

public funct
.data
bla qword ?
bla2 qword 10.0
.code
funct PROC
push rbp
mov rbp, rsp
push rbx

mov bla,rcx
fild qword ptr[bla]

fld qword ptr [bla2]
fmul st(0), st(1)
fist dword ptr [bla]
pop rbx
pop rbp
ret
funct ENDP
END

我的C++代码是:

#include <stdlib.h>
#include <cstdlib>
#include <stdio.h>

extern "C" float funct(long long n);
int main(){

    float value1= funct(3);

    return 0;
}

有什么问题,我该如何解决?

【问题讨论】:

  • 您真的应该考虑使用 SSE2+ 在 64 位代码中执行浮点计算。浮点值必须根据Microsoft 64-bit calling convention在XMM0寄存器中返回
  • 是否仍然算作使用 FPU 命令?我注意到 XMM0 寄存器,但我不确定它们是否符合我的限制。基本上我想要一个从 C 语言调用的程序,它以 64 位运行并进行浮点计算,也许我误解了这个任务。
  • SSE2(所有 AMD 和 Intel 64 位处理器支持的最小公分母)是进行浮点计算的新方法。不,从技术上讲,它不是 FPU。我对您的任务/分配一无所知,我只是观察到现在在 64 位模式下几乎不赞成使用 X87 指令进行浮点数。您需要将结果从 X87 FPU 中弹出并存储在内存中,然后使用 SSE 指令将其移至 XMM0 寄存器。您可以使用 movss 指令将 32 位浮点数从内存移动到 XMM0 寄存器。
  • MOVSD 可用于将双精度标量(64 位浮点数)移动到 XMM0。
  • fst qword ptr [bla] movsd xmm0, qword ptr[bla] 我添加了这个,但它仍然不起作用 T.T,value1 显示为 0,000....

标签: assembly floating-point x86-64 masm x87


【解决方案1】:

您的问题有点模棱两可,您的代码也是如此。我将介绍一些使用 x87 FPU 和 SSE 指令的想法。不鼓励在 64 位代码中使用 x87 FPU 指令,首选 SSE/SSE2SSE/SSE2 适用于所有 64 位 AMD 和 64 位 Intel x86 处理器。


使用 x87 FPU 的 64 位代码中的 32 位浮点数

如果您的问题是“如何使用 x87 FPU 编写使用 32 位浮点数的 64 位汇编代码?” 那么您的 C++ 代码看起来不错,但是您的汇编代码需要一些工作。您的 C++ 代码表明函数的输出类型是 32 位浮点数:

extern "C" float funct(long long n);

我们需要创建一个返回 32 位浮点数的函数。您的汇编代码可以按以下方式修改。我在您的代码中保留堆栈帧代码和 RBX 的推送/弹出,因为我假设您只是给我们一个最小的示例,并且您的真实代码正在使用 RBX。考虑到这一点,以下代码应该可以工作:

public funct
.data
ten REAL4 10.0                     ; Define variable ten as 32-bit (4-byte float)
                                   ; REAL4 and DWORD are both same size. 
                                   ; REAL4 makes for more readable code when using floats
.code
funct PROC
    push rbp
    mov rbp, rsp                   ; Setup stack frame
                                   ; RSP aligned to 16 bytes at this point
    push rbx

    mov [rbp+16],rcx               ; 32 byte shadow space is just above the return address
                                   ; at RBP+16 (this address is 16 byte aligned). Rather 
                                   ; than use a temporary variable in the data section to 
                                   ; store the value of RCX, we just store it to the 
                                   ; shadow space on the stack.
    fild QWORD ptr[rbp+16]         ; Load and convert 64-bit integer into st(0)
    fld [ten]                      ; st(0) => st(1), st(0) = 10.0
    fmulp                          ; st(1)=st(1)*st(0), st(1) => st(0)
    fstp REAL4 ptr [rbp+16]        ; Store result to shadow space as 32-bit float
    movss xmm0, REAL4 ptr [rbp+16] ; Store single scalar (32-bit float) to xmm0
                                   ; XMM0 = return value for 32(and 64-bit) floats
                                   ;        in 64-bit code.

    pop rbx
    mov rsp, rbp                   ; Remove stack frame
    pop rbp
    ret
funct ENDP
END

我已经对代码进行了注释,但可能感兴趣的是我没有在 DATA 部分中使用第二个变量。 64 位 Windows 调用约定要求函数的调用者确保堆栈在 16 字节边界上对齐,并且之前分配了 32 字节 shadow space(AKA 寄存器参数区域)打电话。该区域可用作划痕区域。由于我们建立了一个栈帧,RBPRBP+0,返回地址在RBP+8,暂存区从RBP+16开始。如果您没有使用堆栈帧,则返回地址为RSP+0,影子空间将从RSP+8 开始我们可以将浮点运算的结果存储在那里而不是 QWORD em> 你标记为 bla

展开浮点堆栈是一个合理的想法,因此在我们退出函数之前它上面没有任何东西。我使用 FPU 浮点函数在我们使用完寄存器后弹出寄存器。

64-bit Microsoft calling convention 要求浮点值为returned in XMM0。我们使用 SSE 指令 MOVSS 将标量单个(32 位浮点数)移动到 XMM0 寄存器。这就是 C++ 代码期望返回该值的地方。


使用 SSE 的 64 位代码中的 32 位浮点数

基于上一节中的想法,我们可以修改代码以使用具有 32 位浮点数的 SSE 指令。此类代码的示例如下:

public funct
.data
ten REAL4 10.0                     ; Define variable ten as 32-bit (4-byte float)
                                   ; REAL4 and DWORD are both same size. 
                                   ; REAL4 makes for more readable code when using floats
.code
funct PROC
    push rbp
    mov rbp, rsp                   ; Setup stack frame
                                   ; RSP aligned to 16 bytes at this point
    push rbx
    cvtsi2ss xmm0, rcx             ; Convert scalar integer in RCX to 
                                   ;    scalar single(float) and store in XMM0
    mulss xmm0, [ten]              ; 32-bit float multiply by 10.0 store in XMM0
                                   ; XMM0 = return value for 32(and 64-bit) floats
                                   ;        in 64-bit code.
    pop rbx
    mov rsp, rbp                   ; Remove stack frame
    pop rbp
    ret
funct ENDP
END

此代码通过使用 SSE 指令删除了 x87 FPU 的使用。我们特别使用:

    cvtsi2ss xmm0, rcx             ; Convert scalar integer in RCX to 
                                   ;    scalar single(float) and store in XMM0

CVTSI2SS 将标量整数转换为单标量(浮点数)。在这种情况下,RCX 中的 64 位整数值被转换为 32 位浮点数并存储在 XMM0 中。 XMM0 是将返回值放入的寄存器。 XMM0XMM5 被视为volatile,因此我们不需要保存它们的值。

    mulss xmm0, [ten]              ; 32-bit float multiply by 10.0 store in XMM0
                                   ; XMM0 = return value for 32(and 64-bit) floats
                                   ;        in 64-bit code.

MULSS 是一个 SSE 指令,用于使用单标量(浮点数)进行 SSE 乘法。在这种情况下,MULSS 将执行 XMM0=XMM0*(32 位浮点内存操作数)。这将具有将 XMM0 乘以 10.0 的 32 位浮点数的 32 位浮点数的效果。由于 XMM0 也包含了我们的最终结果,所以我们无需再做任何事情,只需正确退出函数即可。


使用 x87 FPU 的 64 位代码中的 64 位双浮点

这是第一个示例的变体,但现在我们使用 64 位浮点数,也称为 C++ 中的 double 类型,汇编程序中的 REAL8(或 QWORD) ,以及 SSE2 中的 scalar double。由于我们现在使用 double 作为返回类型,我们必须将 C++ 代码修改为:

#include <stdlib.h>
#include <cstdlib>
#include <stdio.h>

extern "C" double funct(long long n);

int main() {    
    double value1 = funct(3);

    return 0;
}

汇编代码如下所示:

public funct
.data
ten REAL8 10.0                     ; Define variable ten as 64-bit (8-byte float)
                                   ; REAL8 and QWORD are both same size. 
                                   ; REAL8 makes for more readable code when using floats
.code
funct PROC
    push rbp
    mov rbp, rsp                   ; Setup stack frame
                                   ; RSP aligned to 16 bytes at this point
    push rbx

    mov [rbp+16],rcx               ; 32 byte shadow space is just above the return address
                                   ; at RBP+8 (this address is 16 byte aligned). Rather 
                                   ; than use a temporary variable in the data section to 
                                   ; store the value of RCX, we just store it to the 
                                   ; shadow space on the stack.
    fild QWORD ptr[rbp+16]         ; Load and convert 64-bit integer into st(0)
    fld [ten]                      ; st(0) => st(1), st(0) = 10.0
    fmulp                          ; st(1)=st(1)*st(0), st(1) => st(0)
    fstp REAL8 ptr [rbp+16]        ; Store result to shadow space as 64-bit float
    movsd xmm0, REAL8 ptr [rbp+16] ; Store double scalar (64-bit float) to xmm0
                                   ; XMM0 = return value for 32(and 64-bit) floats
                                   ;        in 64-bit code.

    pop rbx
    mov rsp, rbp                   ; Remove stack frame
    pop rbp
    ret
funct ENDP
END

此代码几乎与使用 32 位浮点数的 x87 代码相同。我们使用 REAL8(与 QWORD 相同)来存储 64 位浮点数并使用 MOVSD 将 64 位双浮点数(双精度标量)移动到 XMM0MOVSD 是一个 SSE2 指令。在 XMM0 中返回适当大小的浮点数很重要。如果您使用 MOVSS,则返回给 C++ 函数的值可能不正确。


使用 SSE2 的 64 位代码中的 64 位双浮点

这是第二个示例的变体,但现在我们使用 64 位浮点数,也称为 C++ 中的 double 类型,汇编程序中的 REAL8(或 QWORD) ,以及 SSE2 中的 scalar doubleC++ 代码应该使用上一节中的代码,以便使用 double 而不是 float。汇编代码类似于:

public funct
.data
ten REAL8 10.0                     ; Define variable ten as 64-bit (8-byte float)
                                   ; REAL8 and QWORD are both same size. 
                                   ; REAL8 makes for more readable code when using floats
.code
funct PROC
    push rbp
    mov rbp, rsp                   ; Setup stack frame
                                   ; RSP aligned to 16 bytes at this point
    push rbx
    cvtsi2sd xmm0, rcx             ; Convert scalar integer in RCX to 
                                   ;    scalar double(double float) and store in XMM0
    mulsd xmm0, [ten]              ; 64-bit float multiply by 10.0 store in XMM0
                                   ; XMM0 = return value for 32(and 64-bit) floats
                                   ;        in 64-bit code.
    pop rbx
    mov rsp, rbp                   ; Remove stack frame
    pop rbp
    ret
funct ENDP
END

与第二个示例的主要区别在于我们使用CVTSI2SD 而不是CVTSI2SS。指令中的 SD 表示我们正在转换为双精度标量(64 位双精度浮点数)。类似地,我们使用 MULSD 指令使用标量双精度进行乘法运算。 XMM0 将保存将返回给调用函数的 64 位双精度标量(双浮点)。

【讨论】:

    【解决方案2】:

    您可以将结果的地址作为参数传递:

    ma​​in.c:

    #include<stdio.h>
    
    extern "C" void funct(long long, float*);
    
    int main ( void )
    {
    
        float value1 = 0;           // float = DWORD ("double" would be QWORD)!
        funct(3, &value1);
        printf ("%f\n",value1);
    
        return 0;
    }
    

    callee.asm:

    .data
        bla qword ?
        bla2 qword 10.0
    
    .code
    funct PROC
        push rbp
        mov rbp, rsp
        push rbx
    
        mov bla,rcx
        fild qword ptr[bla]         ; -> st(1)
    
        fld qword ptr [bla2]        ; -> st(0)
        fmul st(0), st(1)
        fstp dword ptr [rdx]        ; pop the first value
        ffree st(0)                 ; pop the second value
    
        pop rbx
        pop rbp
        ret
    funct ENDP
    
    END
    

    【讨论】:

    • 你为什么要在被调用者中创建一个堆栈帧,尤其是。你为什么要保存/恢复rbx,你甚至不碰它? Win64 调用约定将前两个参数放在 rcxrdx 中,我假设?另外,你可以探测。使用fmulp 并避免使用ffree。如果整数不必是 64 位,您可以使用 fld qword [bla2] / fimul dword [bla] / fstp dword [rdx]
    • @PeterCordes:我总是尽量接近 OP 的 MCVE。如果有一个愚蠢的地方,我假设原始代码的错误减少。如果它不是一个明显的错误并且不会妨碍我的示例的清晰性,我通常不会更正它。
    猜你喜欢
    • 2015-07-31
    • 1970-01-01
    • 2013-12-15
    • 1970-01-01
    • 2013-10-24
    • 2012-06-08
    • 1970-01-01
    • 1970-01-01
    • 2013-08-10
    相关资源
    最近更新 更多