linux / amd64 C与C ++的abi差异

时间:2017-12-14 22:33:42

标签: c++ c linux gcc

我的C库有这样的API:

#ifdef __cplusplus
extern "C" {
#endif
struct Foo {
    void *p;
    int len;
};

struct Foo f(void *opaque, int param);
void foo_free(struct Foo *);
#ifdef __cplusplus
}
#endif

为了简化C++生活,我决定做一件简单的事情:

 struct Foo {
    void *p;
    int len;
#ifdef __cplusplus
    ~Foo() { foo_free(this); }
#endif
};

然后事情变得疯狂:例如,如果我打电话 f(0xfffeeea0, 40)中的C++,然后C0x7fff905d2050 -69984 0x000055555555467a <+0>: push %rbp 0x000055555555467b <+1>: mov %rsp,%rbp 0x000055555555467e <+4>: sub $0x10,%rsp 0x0000555555554682 <+8>: mov $0x28,%esi 0x0000555555554687 <+13>: mov $0xfffeeea0,%edi 0x000055555555468c <+18>: callq 0x5555555546a0 <f> 0x0000555555554691 <+23>: mov %rax,-0x10(%rbp) 0x0000555555554695 <+27>: mov %rdx,-0x8(%rbp) 0x0000555555554699 <+31>: mov $0x0,%eax 0x000055555555469e <+36>: leaveq 0x000055555555469f <+37>: retq

没有析构函数的声明:

   0x00000000000006da <+0>: push   %rbp
   0x00000000000006db <+1>: mov    %rsp,%rbp
   0x00000000000006de <+4>: sub    $0x20,%rsp
   0x00000000000006e2 <+8>: mov    %fs:0x28,%rax
   0x00000000000006eb <+17>:    mov    %rax,-0x8(%rbp)
   0x00000000000006ef <+21>:    xor    %eax,%eax
   0x00000000000006f1 <+23>:    lea    -0x20(%rbp),%rax
   0x00000000000006f5 <+27>:    mov    $0x28,%edx
   0x00000000000006fa <+32>:    mov    $0xfffeeea0,%esi
   0x00000000000006ff <+37>:    mov    %rax,%rdi
   0x0000000000000702 <+40>:    callq  0x739 <f>
   0x0000000000000707 <+45>:    lea    -0x20(%rbp),%rax
   0x000000000000070b <+49>:    mov    %rax,%rdi
   0x000000000000070e <+52>:    callq  0x72e <Foo::~Foo()>
   0x0000000000000713 <+57>:    mov    $0x0,%eax
   0x0000000000000718 <+62>:    mov    -0x8(%rbp),%rcx
   0x000000000000071c <+66>:    xor    %fs:0x28,%rcx
   0x0000000000000725 <+75>:    je     0x72c <main()+82>
   0x0000000000000727 <+77>:    callq  0x5c0 <__stack_chk_fail@plt>
   0x000000000000072c <+82>:    leaveq 
   0x000000000000072d <+83>:    retq   

使用析构函数声明:

%esi

我想知道发生了什么事? 我可以理解为什么编译器应该处理不同的返回 方式,但为什么它在不同的寄存器中移动参数%edi vs Foo

为了清楚起见,我明白我做错了,我用了重写代码 某种智能指针而不触及真正的c++。 但我想知道c//test.cpp extern "C" { struct Foo { void *p; int len; ~Foo() {/*call free*/} }; struct Foo f(void *opaque, int param); } int main() { auto foo = f(reinterpret_cast<void *>(0xfffeeea0), 40); } //test.c #include <stdio.h> struct Foo { void *p; int len; }; struct Foo f(void *opaque, int param) { printf("!!! %p %d\n", opaque, param); struct Foo ret = {0, 0}; return ret; } #makefile: prog: test.cpp test.c gcc -Wall -ggdb -std=c11 -c -o test.c.o test.c g++ -Wall -ggdb -std=c++11 -o $@ test.cpp test.c.o ./prog 的ABI在这种特殊情况下是如何起作用的。

完整示例:

PUT _xpack/watcher/watch/error_report
{
  "trigger" : {
    "schedule": {
      "interval": "1h"
    }
  },
  "actions" : {
    "email_admin" : { 
      "email": {
        "to": "'Recipient Name <recipient@example.com>'",
        "subject": "Error Monitoring Report",
        "attachments" : {
          "error_report.pdf" : {
            "reporting" : {
              "url": "http://0.0.0.0:5601/api/reporting/generate/dashboard/Error-Monitoring?_g=(time:(from:now-1d%2Fd,mode:quick,to:now))", 
              "retries":6, 
              "interval":"1s", 
              "auth":{ 
                "basic":{
                  "username":"elastic",
                  "password":"changeme"
                }
              }
            }
          }
        }
      }
    }
  }
}

1 个答案:

答案 0 :(得分:2)

在代码的第一个版本(没有析构函数)中,我们有:

// allocate 16 bytes on the stack (for a Foo instance)
sub    $0x10,%rsp

// load two (constant) arguments into %edi and %esi
mov    $0x28,%esi
mov    $0xfffeeea0,%edi

// call f
callq  0x5555555546a0 <f>

// a 2-word struct was returned by value (in %rax/%rdx).
// move the values to the corresponding slots on the stack
mov    %rax,-0x10(%rbp)
mov    %rdx,-0x8(%rbp)

在第二个版本中(带有析构函数):

// load address of Foo instance into %rax
lea    -0x20(%rbp),%rax

// load three arguments:
//  - 40 in %edx
//  - 0xfffeeea0 in %esi
//  - &foo in %rdi
mov    $0x28,%edx
mov    $0xfffeeea0,%esi
mov    %rax,%rdi

// ... and call f
callq  0x739 <f>

// ignore f's return value; load &foo into %rax again
lea    -0x20(%rbp),%rax

// call ~Foo on &foo
mov    %rax,%rdi
callq  0x72e <Foo::~Foo()>

我的猜测是,如果没有析构函数,结构将被视为一个普通的2字元组,并按值返回。

但是使用析构函数,编译器假定它不能只复制成员值,因此它将struct返回值转换为隐藏的指针参数:

struct Foo f(void *opaque, int param);

// actually implemented as:
void f(struct Foo *_hidden, void *opaque, int param);

通常f会将返回值写入*_hidden

因为函数的调用者和实现者看到不同的返回类型,所以他们不同意函数实际具有的参数数量。 C ++代码传递了3个参数,但C代码只查看其中的两个参数。它将Foo实例的地址误解为opaque指针,应该是opaque指针的内容最终位于param

换句话说,析构函数的存在意味着Foo不再是POD类型,它禁止通过寄存器进行简单的按值返回。