大家度过了美好的周日。
我目前正在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的答案总是会产生编译错误或使我的函数返回'南'。 ChatGPT 将我的简单函数始终转换为可执行主块,该块使用 .data 部分作为全局变量,我认为这导致我走向完全错误的方向。
我不喜欢我必须使用 sub 并添加 esp 才能将 xmm0 放入 st0。
我也很感谢任何改进我的代码的技巧,甚至是从中学习的好资源。 我现在只想关注 32 位。 :)
从 XMM 传输到
st0
需要存储/重新加载。即使 MMX 寄存器是 x87 寄存器的别名,也无法使用 MOVDQ2Q mm0, xmm0
将 80 位 FP 位模式转换为 st0
,甚至除了在不清除寄存器的情况下从 MMX 切换回 x87 状态的问题之外。
相关:Intel 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 st0
/ ret
会更有效,因为我们使用的是在 st0
中返回的调用约定。
如果您坚持使用 32 位代码,那么默认的调用约定又旧又坏,在堆栈上传递参数并在
float
中返回 double
/st0
,而不是 xmm0
。
对于 Windows,有不太糟糕的 32 位调用约定。 32 位
vectorcall
传递 xmm 寄存器中的前 6 个 FP(或 SIMD 向量)参数,并在 xmm0
中返回。 regs 中的前 2 个整数参数如 fastcall
。 (64 位向量调用仅在 XMM 寄存器中传递 4 个参数,与标准 Windows x64 约定的不同之处仅在于处理 __m128i
和 __m256
等类型。)请参阅 https://learn.microsoft.com/en-us/ cpp/cpp/vectorcall?view=msvc-170 了解更多。
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) 进行编译。这是一个像
fastcall
这样的被调用者流行约定;请注意 ret 4
,因为我们有一个堆栈参数。但是,如果您没有任何堆栈参数,则仅使用普通的 ret
仍然是正确的。
_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 的答案总是会产生编译错误或使我的函数返回
。 ChatGPT 将我的简单函数始终转换为可执行主块,该块使用 .data 部分作为全局变量,我认为这导致我走向完全错误的方向。NAN
不足为奇; ChatGPT 非常不擅长汇编语言,有错误的代码很正常。 它不“理解”它在任何语言中所做的事情,但 x86 asm 在其训练数据中可能更罕见,并且/或者对于大型语言模型来说更困难,因为所有程序中都使用相同的寄存器名称和助记符。而且汇编语言有很多不同的风格(包括 x86 的多种语言),这可能没有帮助。