为C++结构定义显式析构函数如何影响调用约定

How does defining an explicit destructor for a C++ struct affect calling conventions?

本文关键字:影响 约定 调用 何影响 结构 C++ 定义 析构函数      更新时间:2023-10-16

一位同事在注意到C++结构的一个奇怪行为后,向我提出了这个问题。

以这个琐碎的代码为例:

struct S {
int i;
#ifdef TEST
~S() {}
#endif
};
void foo (S s) {
(void)s;
}
int main () {
foo(S());
return 0;
}

我已经生成了一次没有显式析构函数的汇编代码:

g++-4.7.2 destructor.cc -S -O0 -o destructor_no.s

后来包括它:

g++-4.7.2 destructor.cc -DTEST -S -O0 -o destructor_yes.s

这是destructor_no.s:中main的代码[1]

main:
pushq   %rbp
movq    %rsp, %rbp
movl    $0, %eax
movl    %eax, %edi
call    _Z3foo1S   // call to foo()
movl    $0, %eax
popq    %rbp
ret

相反,如果析构函数是明确定义的:

main:
pushq   %rbp
movq    %rsp, %rbp
subq    $16, %rsp
movl    $0, -16(%rbp)
leaq    -16(%rbp), %rax
movq    %rax, %rdi
call    _Z3foo1S   // call to foo()
leaq    -16(%rbp), %rax
movq    %rax, %rdi
call    _ZN1SD1Ev  // call to S::~S()
movl    $0, %eax
leave
ret

现在,我的组装知识有点生疏,但在我看来:

  1. 在第一种情况下,结构是"按值"传递的。也就是说,它的内存内容被复制到%edi寄存器中,如果我没有记错的话,它是x86-64ABI中用于传递参数的第一个寄存器。

  2. 在第二种情况下,结构是在堆栈上分配的,但foo()函数是用%rdi中的指针调用的。

为什么会有这样的差异


注意:

  • 如果使用gcc-4.6.3clang 3.1,则会确认相同的行为。

  • 当然,如果启用了优化,那么对函数foo()的调用在任何情况下都会被完全优化掉。

  • 如果没有显式提供析构函数,则在向struct添加更多变量时会出现一个有趣的模式。

通过参数寄存器最多可传递4个ints(=16字节):

pushq   %rbp
movq    %rsp, %rbp
subq    $16, %rsp
movl    $0, -16(%rbp)
movl    $0, -12(%rbp)
movl    $0, -8(%rbp)
movl    $0, -4(%rbp)
movq    -16(%rbp), %rdx
movq    -8(%rbp), %rax
movq    %rdx, %rdi
movq    %rax, %rsi
call    _Z3foo1S

但是,当我向结构中添加第五个int时,仍然"按值"传递的函数的参数现在就在堆栈上了:

pushq   %rbp
movq    %rsp, %rbp
subq    $56, %rsp
movl    $0, -32(%rbp)
movl    $0, -28(%rbp)
movl    $0, -24(%rbp)
movl    $0, -20(%rbp)
movl    $0, -16(%rbp)
movq    -32(%rbp), %rax
movq    %rax, (%rsp)
movq    -24(%rbp), %rax
movq    %rax, 8(%rsp)
movl    -16(%rbp), %eax
movl    %eax, 16(%rsp)
call    _Z3foo1S

[1] 我删除了一些我认为不必要的行。

在C++03中,如果定义了析构函数,则结构不再是POD类型。变量中没有析构函数的对象的行为类似于C结构变量(因此它只是按值传递),而具有用户定义的对象的操作类似于C++对象。