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差异的主要内容,如果未能解决你的问题,请参考以下文章