【发布时间】: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.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
现在,我的组装知识有点生疏了,但在我看来:
在第一种情况下,结构是“按值”传递的。即,它的内存内容被复制到
%edi寄存器中,如果我没记错的话,它是第一个用于在x86-64ABI 中传递参数的寄存器。在第二种情况下,结构体是在堆栈上分配的,但
foo()函数是使用%rdi中的指针调用的。
为什么会有这样的差异?
注意事项:
如果使用
gcc-4.6.3或clang 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