x86 汇编 如何正确将 XMM0 导入 ST0?

x86 Assembly How to properly get XMM0 into ST0?

提问人:Zvend 提问时间:8/6/2023 最后编辑:Sep RolandZvend 更新时间:8/11/2023 访问量:119

问:

祝大家星期天愉快。

我目前正在 32 位环境(目前为 Windows)中学习大量汇编。我正在为此使用 FASM。

我有以下成功制作的代码,但我对将 XMM0 加载到 ST0 的方式非常不满意:

GetDistance: ;(__cdecl*)(float x1, float y1, float x2, float y2)
        push    ebp
        mov     ebp, esp        
        sub     esp, 0x4
        
        movss   xmm0, DWORD [ebp + 0x0014] ; Load x2
        subss   xmm0, DWORD [ebp + 0x000C] ; Subtract x1

        movss   xmm1, DWORD [ebp + 0x0010] ; Load y2
        subss   xmm1, DWORD [ebp + 0x0008]  ; Subtract y1

        mulss   xmm0, xmm0             ; Square of the x difference
        mulss   xmm1, xmm1             ; Square of the y difference

        addss   xmm0, xmm1             ; Sum of squared differences

        sqrtss  xmm0, xmm0             ; Square root
                                 
        movss   dword [ebp - 0x0004], xmm0
        fld     dword [ebp - 0x0004]    
        
        add     esp, 0x4
        
        pop     ebp
        ret     0

它确实有效,但我已经在谷歌上搜索了整整 2 个小时(甚至问过 ChatGPT)如何将我的 XMM0 值放入 ST0,但我无法搜索我猜的正确问题,ChatGPT 的答案总是造成编译错误或使我的函数返回“NAN”。ChatGPT 总是将我的简单函数转换为一个可执行的主块,它使用部分变量,因此使用全局变量,我认为这会导致我进入一个完全错误的方向。.data

我不喜欢我必须使用 from 和 to ESP 将 XMM0 放入 ST0。subadd

我也很欣赏任何改进代码的技巧,甚至是从中学习的好资源。 我现在只想专注于 32 位。:)

程序集 浮点 SSE FASM x87

评论

3赞 Jester 8/6/2023
你不需要 ,但如果你确实设置了一个堆栈框架,你可以在末尾使用而不是 /。 不幸的是,需要一个内存操作数,所以没有办法解决这个问题,当然,除非你使用 FPU 指令而不是 SSE。ebpleaveaddpopfld
1赞 Peter Cordes 8/6/2023
如果您坚持使用 32 位代码,那么您将被困在返回而不是 .但是,您可以尝试在 xmm 寄存器中传递前几个参数,并在 xmm0: godbolt.org/z/aozfG5PTh 中返回。或者,如果您不想这样做,则被调用方拥有其传入的堆栈参数,因此您可以将其用作暂存空间来存储/重新加载。正如 Jester 所说,您不需要浪费设置 EBP 的说明。floatst0xmm0vectorcall[esp+4]
0赞 fuz 8/6/2023
是的,在 X87 和 SSE 寄存器之间传输数据时,无法避免内存跳闸。
5赞 Eric Postpischil 8/6/2023
不要将 ChatGPT 用于学习或研究。
1赞 Peter Cordes 8/6/2023
如果您不想立即学习传统的 x87 FPU 指令,则可以使用 64 位代码,其中标准调用约定在 XMM 注册表中传递/返回浮点数。标准的 32 位调用约定又旧又坏,并且使用 x87 堆栈。

答:

4赞 Peter Cordes 8/6/2023 #1

存储/重新加载是从 XMM 传输到 所必需的。即使 MMX 寄存器别名 x87 寄存器,也无法将 80 位 FP 位模式放入 ,即使除了在不清除寄存器的情况下从 MMX 切换回 x87 状态的问题。st0MOVDQ2Q mm0, xmm0st0

相关新闻: 英特尔x86_64程序集, 如何在 x87 和 SSE2 之间移动?(计算双精度的反正切值)

但是,您不需要浪费将 EBP 设置为帧指针的指令,尤其是在像这样的简单函数中,它很容易跟踪相对于 ESP 的偏移量。

在具有堆栈参数的函数中,被调用方(您的函数)“拥有”它们,因此您可以将其用作暂存空间,而不是保留新空间。这就是为什么当使用相同的参数调用同一函数两次时,调用者必须再次存储参数。例如[esp+4]

square:                   ; float square(float a); legacy cdecl convention
 movss  xmm0, [esp+4]
 mulss  xmm0, xmm0
 movss  [esp+4], xmm0      ; reuse the incoming arg as scratch space
 fld    dword [esp+4]
 ret

在这种情况下,使用 / / 会更有效,因为我们使用的是返回 .fld dword [esp+4]fmul st0retst0


如果您坚持使用 32 位代码,那么默认的调用约定是旧的和糟糕的,在堆栈上传递参数并返回 / in 而不是 .floatdoublest0xmm0

不过,对于 Windows,32 位调用约定不那么糟糕。32 位向量调用在 xmm 寄存器中传递前 6 个 FP(或 SIMD 向量)参数,并在 xmm0 中返回。regs 中的前 2 个整数参数,例如 .(64 位 vectorcall 在 XMM regs 中仅传递 4 个参数,与标准 Windows x64 约定的不同之处仅在于处理 和 等类型。有关详细信息,请参阅 https://learn.microsoft.com/en-us/cpp/cpp/vectorcall?view=msvc-170fastcall__m128i__m256

float _vectorcall 
 foo(float a, float b, float c, float d, float e, float f, float g, int i){
    return a+b+c+d+e+f+g + i;
}

使用 x86 MSVC 19.10 (Godbolt) 进行编译。这是一个像 callee-pops 这样的约定;请注意,由于我们有一个堆栈参数。但是,如果您没有任何堆栈参数,则仅正常值仍然是正确的。fastcallret 4ret

_g$ = 8                                       ; size = 4
float foo(float,float,float,float,float,float,float,int) PROC                                ; foo, COMDAT
        addss   xmm0, xmm1
        movd    xmm1, ecx
        cvtdq2ps xmm1, xmm1          ; avoids a false dependency vs. cvtsi2ss xmm1, ecx which is also 2 uops
        addss   xmm0, xmm2
        addss   xmm0, xmm3
        addss   xmm0, xmm4
        addss   xmm0, xmm5
        addss   xmm0, DWORD PTR _g$[esp-4]   ; 7th FP arg comes from the stack.
                                             ; with _g$ = 8, this is actually [esp+4]
        addss   xmm0, xmm1                   ; +i  converted earlier
        ret     4
float foo(float,float,float,float,float,float,float,int) ENDP                                ; foo

如果呼叫者也是手写的 asm,则不必遵循标准呼叫约定;您可以在方便的寄存器中传递/返回参数,并基于每个函数使用注释进行记录。


ChatGPT 的答案总是会造成编译错误或使我的函数返回“NAN”。ChatGPT 总是将我的简单函数转换为一个可执行的主块,它使用部分变量,因此使用全局变量,我认为这会导致我进入一个完全错误的方向。.data

不足为奇;ChatGPT在汇编语言方面很差,有bug的代码很正常。
它不“理解”它在任何语言中所做的事情,但 x86 asm 的训练数据可能更罕见,并且/或更难用于大型语言模型,因为所有程序都使用相同的寄存器名称和助记符。而且有这么多不同风格的汇编语言(包括 x86 的多个)可能无济于事。

评论

0赞 Zvend 8/6/2023
哦,哇!这澄清了很多,非常感谢!我知道我的下一个问题有点横穿,但我想让你知道我不是懒惰或任何一种。我想看看并问你如何在 32 位汇编中编写我的函数。当有人拿起我的职能并用他的专业经验重写它时,我学到了很多东西。但你实际上说服我去掉堆栈框架,以获得更小的代码。其次,你有很好的资源来存放我的物品吗?(Windows 上的 32 位程序集)GetDistance
1赞 Peter Cordes 8/6/2023
@Zvend:假设您在编写必须在 st0 中返回的小型非内联函数时,要使用 SSE 而不是 x87 进行标量 FP 数学运算,您的代码看起来很高效。我只是将部分更改为使用 EBP 删除内容后,并更改将 XMM0 反弹到 st0 的方式。回复:学习资源,请参阅 stackoverflow.com/tags/x86/info 特别 agner.org/optimize[ebp+...][esp+4 + ...]
1赞 Peter Cordes 8/6/2023
@Zvend:对于堆栈上的 args,我们可以考虑使用成对加载它们,允许 / , 位,那么你需要洗牌来提取第二个浮点数。像 SSE3 / .因此,较少的 ILP(指令级并行性),但节省几条指令,可能有利于吞吐量。或者可能不是:如果调用方使用单独的 32 位存储存储堆栈参数,则 64 位加载将导致存储转发停滞,而无序 exec 可能无法完全隐藏,具体取决于其他代码。movsdsubps xmm0, xmm1mulps xmm0, xmm0movshdup xmm1, xmm0addss xmm0, xmm1