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

Posted

技术标签:

【中文标题】linux / amd64 C与C ++上的abi差异【英文标题】:abi difference on linux/amd64 C vs C++ 【发布时间】:2017-12-14 22:33:41 【问题描述】:

我有带有此类 API 的 C 库:

#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++,然后在C 一侧我得到0x7fff905d2050 -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 vs %edi

为了清楚起见,我知道我做错了,我重写了代码 某种智能指针而不是真正的Foo。 但我想知道c++c 的ABI 在这种特殊情况下是如何工作的。

完整示例:

//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

【问题讨论】:

我建议删除 C 和 Linux 标签。成员函数不是 C。没有对 Linux 操作系统的调用。 IIRC c++ 有一个隐藏的“this”参数 @melpomene 我在最后添加了完整的代码。 @technosaurus 但我调用的不是成员函数 @ThomasMatthews Windows ABI x64 调用 C/C++ 函数可能会有所不同,这就是我添加 linux 标签的原因。还有关于c/c++合作的问题。 【参考方案1】:

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

// 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 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 类型,它禁止通过寄存器进行简单的按值返回。

【讨论】:

以上是关于linux / amd64 C与C ++上的abi差异的主要内容,如果未能解决你的问题,请参考以下文章

linux 操作系统下能用fprintf() 及fscanf()等函数吗?它们与c语言中的用法一样吗?

Linux常用服务部署与优化

Eclipse 中科学 Linux 上的 C++14

Linux 上的 Ctrl+C 等终止信号或中断

C 中静态内存分配与动态内存分配的成本

C ++从Linux上的剪贴板获取字符串