为什么在 x64 中忽略 __stdcall 调用约定?

Why is the __stdcall calling convention ignored in x64?

我知道 __cdecl__stdcall 之间的区别是什么,但我不太确定为什么 __stdcall 在 x64 构建中被编译器忽略。

以下代码中的函数

int __stdcall stdcallFunc(int a, int b, int c, int d, int e, int f, int g)
{
    return a + b + c + d + e + f + g;
}

int __cdecl cdeclFunc(int a, int b, int c, int d, int e, int f, int g)
{
    return a + b + c + d + e + f + g;
}

int main()
{
    stdcallFunc(1, 2, 3, 4, 5, 6, 7);
    cdeclFunc(1, 2, 3, 4, 5, 6, 7);

    return 0;
}

有足够的参数超过可用的 CPU 寄存器。因此,一些参数必须通过堆栈传递。我不精通汇编,但我注意到 x86 和 x64 汇编之间存在一些差异。

x64

main    PROC
$LN3:
        sub     rsp, 72                             ; 00000048H
        mov     DWORD PTR [rsp+48], 7
        mov     DWORD PTR [rsp+40], 6
        mov     DWORD PTR [rsp+32], 5
        mov     r9d, 4
        mov     r8d, 3
        mov     edx, 2
        mov     ecx, 1
        call    ?stdcallFunc@@YAHHHHHHHH@Z          ; stdcallFunc
        mov     DWORD PTR [rsp+48], 7
        mov     DWORD PTR [rsp+40], 6
        mov     DWORD PTR [rsp+32], 5
        mov     r9d, 4
        mov     r8d, 3
        mov     edx, 2
        mov     ecx, 1
        call    ?cdeclFunc@@YAHHHHHHHH@Z                ; cdeclFunc
        xor     eax, eax
        add     rsp, 72                             ; 00000048H
        ret     0
main    ENDP

x86

_main   PROC
        push    ebp
        mov     ebp, esp
        push    7
        push    6
        push    5
        push    4
        push    3
        push    2
        push    1
        call    ?stdcallFunc@@YGHHHHHHHH@Z          ; stdcallFunc
        push    7
        push    6
        push    5
        push    4
        push    3
        push    2
        push    1
        call    ?cdeclFunc@@YAHHHHHHHH@Z                ; cdeclFunc
        add     esp, 28                             ; 0000001cH
        xor     eax, eax
        pop     ebp
        ret     0
_main   ENDP
  1. 正如预期的那样,前 4 个参数在 x64 中通过寄存器传递。
  2. 其余参数按与 x86 中相同的顺序放入堆栈。
  3. 与 x86 相反,在 x64 中我们不使用 push 指令。相反,我们在 main 的开头保留足够的堆栈 space 并使用 mov 指令将参数添加到堆栈。
  4. 在 x64 中,在 call 之后没有堆栈清理发生,但是在 main.
  5. 结束时

这让我想到了我的问题:

  1. 为什么 x64 使用 mov 而不是 push?我认为它只是更有效率并且在 x86 中不可用。
  2. 为什么在 x64 中 call 指令后没有堆栈清理?
  3. Microsoft 选择在 x64 程序集中忽略 __stdcall 的原因是什么? 来自 docs

    On ARM and x64 processors, __stdcall is accepted and ignored by the compiler

Here 是示例代码和程序集。

  1. Why does x64 use mov rather than push? I assume it's just more efficient and wasn't available in x86.

不是这个原因。这两条指令也存在于 x86 汇编语言中。

您的编译器没有为 x64 代码发出 push 指令的原因可能是因为它无论如何都必须直接调整堆栈指针,以创建 32 字节的“阴影 space " 用于被调用的函数。有关“影子 space”的更多信息,请参阅 this link(由 @NateEldredge 提供)。

使用 push 指令分配 32 个字节的“shadow space”将需要 4 个 64 位 push 指令,但只需要一个 sub 指令。这就是为什么它更喜欢使用 sub 指令。由于它无论如何都使用 sub 指令来创建 32 字节的影子 space,因此将 sub 指令的操作数从 32 更改为 72 没有任何惩罚,它分配了 72 字节的影子堆栈上的内存,足以在堆栈上传递 3 个参数(其他 4 个在 CPU 寄存器中传递)。

我不明白为什么它在堆栈上分配 72 个字节,但是,根据我的计算,它只需要 56 个字节(32 个字节的“shadow space”和 24 个在堆栈上传递的 3 个参数的字节)。编译器可能会为局部变量或异常处理保留额外的 16 个字节,当编译器优化处于活动状态时,这些字节可能会被优化掉。


  1. Why is there no stack cleanup after the call instructions in x64?

调用指令后有堆栈清理。这条线是这样的

add rsp, 72

会。

但是,由于某些原因(可能是为了提高性能),x64 编译器仅在调用函数结束时执行清理,而不是在每次函数调用之后执行清理。这意味着对于 x64 编译器,所有函数调用为其参数共享相同的堆栈 space,而对于 x86 编译器,堆栈 space 在每次函数调用时分配和清理。


  1. What's the reason that Microsoft chose to ignore __stdcall in x64 assembly?

关键字 _stdcall_cdecl 指定 32 位调用约定。这就是它们与 64 位程序(即 x64)无关的原因。在 x64 上,只有 standard calling convention and the extended __vectorcall 调用约定。