【问题标题】:How does defining an explicit destructor for a C++ struct affect calling conventions?为 C++ 结构定义显式析构函数如何影响调用约定?
【发布时间】:2012-10-06 20:04:11
【问题描述】:

一位同事在注意到 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.smain 的代码[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-64 ABI 中传递参数的寄存器。

  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] 我删除了一些我认为对于这个问题来说不必要的行。

【问题讨论】:

  • 如果用-O3编译,两者有区别吗?
  • 当你添加一个析构函数时,对象必须在调用foo 之后才能被销毁。至于第二个观察,这似乎是一个有意识的决定,类似于“尝试在合适的时候将参数放入寄存器,然后回退到堆栈”。
  • @Prætorian 使用-O3 在这两种情况下,整个调用都会被优化掉。
  • 再想一想——一旦定义了任何成员函数,就必须有一种方法可以将this 指针传递给它。如果您的对象位于寄存器中,就像您的第一个示例一样,您无法创建指向它的指针。
  • @DCoder 但是为什么对象不能在像 r12 这样的被调用者不能触及的寄存器中超过调用?我只能将异常和堆栈展开视为潜在原因。

标签: c++ assembly struct calling-convention


【解决方案1】:

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

【讨论】:

  • 在你的回答中你提到了C++03;这是否意味着C++11 标准的情况有所不同?
  • 在这种特殊情况下没有区别,因为在 C++11 中,您通过定义析构函数将平凡的类转变为非平凡的类。
猜你喜欢
  • 1970-01-01
  • 2015-07-01
  • 2018-01-10
  • 2014-08-26
  • 1970-01-01
  • 2021-01-28
  • 1970-01-01
相关资源
最近更新 更多