MSVC 汇编函数参数 C++ 与 _asm

MSVC Assembly function arguments C++ vs _asm

我有一个带有 3 个参数的函数,dest、src0、src1,每个参数都是一个指向大小为 12 的数据的指针。我做了两个版本。一个是用 C 编写并由编译器优化的,另一个是完全用 _asm 编写的。嗯是的。 3个参数?我自然会做这样的事情:

mov ecx, [src0]
mov edx, [src1]
mov eax, [dest]

我对编译器有点困惑,因为它认为添加以下内容是合适的:

_src0$ = -8                     ; size = 4
_dest$ = -4                     ; size = 4
_src1$ = 8                      ; size = 4
?vm_vec_add_scalar_asm@@YAXPAUvec3d@@PBU1@1@Z PROC  ; vm_vec_add_scalar_asm
; _dest$ = ecx
; _src0$ = edx

; 20   : {

sub esp, 8
mov DWORD PTR _src0$[esp+8], edx
mov DWORD PTR _dest$[esp+8], ecx

; 21   :    _asm
; 22   :    {
; 23   :        mov ecx, [src0]

mov ecx, DWORD PTR _src0$[esp+8]

; 24   :            mov edx, [src1]

mov edx, DWORD PTR _src1$[esp+4]

; 25   :            mov eax, [dest]

mov eax, DWORD PTR _dest$[esp+8]

Function body etc.

add esp, 8
ret 0

_src0$[esp+8] 等是什么意思?为什么它在我的代码之前做所有这些事情?为什么它试图 [显然] 如此糟糕地堆叠任何东西?

相比之下,C++版本在其主体之前只有以下内容,非常相似:

_src1$ = 8                      ; size = 4
?vm_vec_add@@YAXPAUvec3d@@PBU1@1@Z PROC         ; vm_vec_add
; _dest$ = ecx
; _src0$ = edx

mov eax, DWORD PTR _src1$[esp-4]

为什么这点就够了?

编译器已决定使用使用 "pass arguments in registers" 又名 __fastcall 的调用约定。这允许编译器将一些参数传递到寄存器中,而不是压入堆栈,这可以减少调用中的开销,因为从变量移动到寄存器比压入堆栈更快,而且现在已经在当我们到达被调用函数时注册,所以不需要从堆栈中读取它。

关于调用约定如何在 Web 上工作的更多信息。 x86 calling conventions 上的维基百科文章是一个很好的起点。

Mats Petersson 的回答解释了 __fastcall。但我想这不完全是你要问的......

其实_src0$[esp+8]就是[_src0$ + esp + 8],而_src0$就是上面定义的:

_src0$ = -8                     ; size = 4

所以,整个表达式 _src0$[esp+8] 只不过是 [esp] ...

要了解它为什么会做所有这些事情,您可能首先应该了解 Mats Petersson 在他的 post 中所说的内容,__fastcall,或者更笼统地说,什么是 调用约定。详见他post中的link

假设您已经理解__fastcall,现在让我们看看您的代码发生了什么。编译器正在使用 __fastcall。您的被调用函数是 f(dst, src0, src1),它需要 3 个参数,因此根据调用约定,当调用者调用 f 时,它会执行以下操作:

  1. dst 移动到 ecx 并将 src0 移动到 edx
  2. src1压入堆栈
  3. 将 4 个字节 return 地址压入堆栈
  4. 转到函数的起始地址f

而被调用者f,当它的代码开始时,就知道参数在哪里:dstsrc0在寄存器ecx和[=28中=], 分别; esp 指向 4 个字节的 return 地址,但它下面的 4 个字节(即 DWORD PTR[esp+4])正好是 src1.

因此,在您的 "C++ version" 中,函数 f 只是做了它应该做的事情:

mov eax, DWORD PTR _src1$[esp-4]

这里_src1$ = 8,所以_src1$[esp-4]正好是[esp+4]。看,它只是检索参数 src1 并将其存储在 eax.

然而这里有一个棘手的问题。在f的代码中,如果你想多次使用参数src1,你当然可以这样做,因为它总是存储在堆栈中,就在return地址的下面;但是如果你想多次使用 dstsrc0 怎么办?它们在寄存器中,可以随时销毁。

所以在那种情况下,编译器应该做以下事情:在进入函数 f 之后,它应该记住 ecxedx 的当前值(通过推送它们入栈)。这8个字节就是所谓的"shadow space"。它没有在你的 "C++ version" 中完成,可能是因为编译器肯定知道这两个参数不会被多次使用,或者它可以通过其他方式正确处理它。

现在,您的 _asm 版本会怎样?这里的问题是您使用的是内联汇编。然后编译器失去对寄存器的控制,它不能假设寄存器 ecxedx 在你的 _asm 块中是安全的(它们实际上不是,因为你在_asm 块)。因此它被迫在函数的开头保存它们。

保存过程如下:它首先将 esp 增加 8 个字节(sub esp, 8),然后将 edxecx 移动到 [esp] 并且[esp+4]分别。

然后它可以安全地进入你的_asm块。现在在它的脑海里(如果它有的话),图片是[esp]src0[esp+4]dst[esp+8]是4字节return地址,[esp+12]src1。它不再考虑 ecxedx.

因此,您在 _asm 块中的第一条指令 mov ecx, [src0] 应解释为 mov ecx, [esp],与

相同
mov ecx, DWORD PTR _src0$[esp+8]

其他两条指令也一样。

此时,您可能会说,啊哈,它在做愚蠢的事情,我不想让它浪费时间,space在这方面,有什么办法吗?

好吧,有一个方法 - 不要使用内联汇编...它很方便,但有一个折衷方案。

您可以在 .asm 源文件和 public 中编写汇编函数 f。在 C/C++ 代码中,声明为 extern 'C' f(...)。然后,当您开始组装函数 f 时,您可以直接使用 ecxedx.