Linux/AMD64 C与C 的ABI差异



我有具有这样的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)中的CC_3,然后在C侧我得到0x7fff905d2050 -69984

无击构函数的Assember:

   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

为了清晰,我知道我做错了事,并且我使用代码重写某种智能指针而没有触摸实际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 %dn", 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

在您的代码的第一个版本(无驱动器)中,我们有:

// 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类型,它可以通过寄存器限制简单的返回。

最新更新