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

问题描述 投票:0回答:1

我知道

__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
    的开头保留足够的堆栈空间,并使用
    mov
    指令将参数添加到堆栈中。
  4. 在 x64 中,在两个
    call
    之后,但在
    main
    结束时,不会发生堆栈清理。

这让我想到了我的问题:

  1. 为什么 x64 使用
    mov
    而不是
    push
    ?我认为它只是更高效,并且在 x86 中不可用。
  2. 为什么 x64 中的
    call
    指令之后没有堆栈清理?
  3. 微软选择在 x64 汇编中忽略
    __stdcall
    的原因是什么? 来自文档

    在 ARM 和 x64 处理器上,编译器接受并忽略 __stdcall

这里是示例代码和汇编。

c++ visual-c++ x86-64 calling-convention
1个回答
7
投票
  1. 为什么 x64 使用
    mov
    而不是
    push
    ?我认为它只是更高效,并且在 x86 中不可用。

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

编译器没有为 x64 代码发出

push
指令的原因可能是因为无论如何它都必须直接调整堆栈指针,以便为被调用函数创建 32 字节的“影子空间”。有关“影子空间”的更多信息,请参阅此链接(由@NateEldredge 提供)。

使用

push
指令分配 32 字节的“影子空间”需要 4 个 64 位
push
指令,但只需要 1 个
sub
指令。这就是为什么它更喜欢使用
sub
指令。由于无论如何它都是使用
sub
指令来创建 32 字节的影子空间,因此将
sub
指令的操作数从 32 更改为 72 不会造成任何损失,这会在堆栈上分配 72 字节的内存,这已经足够了还在堆栈上传递 3 个参数(其他 4 个参数在 CPU 寄存器中传递)。

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


  1. 为什么 x64 中调用指令后没有堆栈清理?

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

add rsp, 72

是的。

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


  1. 微软选择忽略 x64 程序集中的 __stdcall 的原因是什么?

关键字

_stdcall
_cdecl
指定 32 位调用约定。这就是为什么它们与 64 位程序(即 x64)无关。在 x64 上,只有 标准调用约定 和扩展
__vectorcall
调用约定。

© www.soinside.com 2019 - 2024. All rights reserved.