Return 来自使用 x87 FPU 的 64 位汇编函数的浮点数

Return a float from a 64-bit assembly function that uses x87 FPU

我正在尝试编写一个程序来计算使用 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;
}

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

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

main.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

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


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

如果您的问题是 "How do I write 64-bit assembler code that uses 32-bit floats using the x87 FPU?" 那么您的 C++ 代码看起来不错,但您的汇编代码需要一些工作。您的 C++ 代码表明该函数的输出类型是 32 位浮点数:

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

我们需要创建一个 return 是 32 位浮点数的函数。您的汇编代码可以按以下方式修改。我在你的代码中保留了堆栈帧代码和 RBX 的 push/pop,因为我假设你只是给我们一个最小的例子,而你的真实代码正在使用 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(又名 register 参数area) 在调用之前分配。该区域可用作草稿区。由于我们设置了栈帧,RBPRBP+0,return地址在RBP+8,暂存区从[=20开始=].如果你没有使用堆栈帧,那么 return 地址在 RSP+0,影子 space 将从 RSP+8 开始 我们可以存储浮点数的结果操作在那里而不是在 QWORD 你标记为 bla.

展开浮点堆栈是一个合理的想法,这样在我们退出我们的函数之前就不会在上面留下任何东西。我使用 FPU 浮点函数,在我们使用完它们后弹出寄存器。

64-bit Microsoft calling convention requires floating point values to be returned in XMM0。我们使用 SSE 指令 MOVSS 将标量单个(32 位浮点数)移动到 XMM0登记。这就是 C++ 代码期望该值被 returned.

的地方

使用 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 converts a scalar integer to a scalar single (float). In this case the 64-bit integer value in RCX is converted to a 32-bit float and stored in XMM0. XMM0 is the register we'll be placing our returned value into. XMM0 to XMM5 are considered 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++、[=24= 中也称为 double 类型](或 QWORD)在汇编程序中,scalar doubleSSE2 中。由于我们现在使用 double 作为 return 类型,我们必须将 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 中 return 适当的大小浮动很重要。如果您使用 MOVSS 值 returned 到 C++ 函数可能不正确。


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

这是第二个示例的变体,但现在我们使用的是 64 位浮点数,在 C++、[=24= 中也称为 double 类型](或 QWORD)在汇编程序中,scalar doubleSSE2 中。 C++ 代码应使用上一节中的代码,以便使用 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 instead of CVTSI2SS. SD in the instruction means we are converting to a scalar double (64-bit double float). Similarly we use the MULSD 指令使用标量双精度进行乘法运算。 XMM0 将保存将 returned 到调用函数的 64 位双精度标量(双浮点数)。