GCC/Clang x86_64 C++ ABI在返回元组时不匹配?

jot*_*tik 12 c++ tuples x86-64 abi compiler-bug

在尝试优化x86_64上的返回值时,我注意到一件奇怪的事情.即,给出代码:

#include <cstdint>
#include <tuple>
#include <utility>

using namespace std;

constexpr uint64_t a = 1u;
constexpr uint64_t b = 2u;

pair<uint64_t, uint64_t> f() { return {a, b}; }
tuple<uint64_t, uint64_t> g() { return tuple<uint64_t, uint64_t>{a, b}; }
Run Code Online (Sandbox Code Playgroud)

Clang 3.8输出此汇编代码f:

movl $1, %eax
movl $2, %edx
retq
Run Code Online (Sandbox Code Playgroud)

这个用于g:

movl $2, %eax
movl $1, %edx
retq
Run Code Online (Sandbox Code Playgroud)

哪个看起来最佳.但是,使用GCC 6.1编译时,生成的程序集f与Clang输出相同,生成的程序集为g:

movq %rdi, %rax
movq $2, (%rdi)
movq $1, 8(%rdi)
ret
Run Code Online (Sandbox Code Playgroud)

看起来返回值的类型被GCC归类为MEMORY,而Clang归类为INTEGER.我可以确认将Clang代码与GCC代码链接这样的代码可能会导致分段错误(Clang调用GCC编译g(),写入指向任何地方%rdi),并返回无效值(GCC调用Clang编译g()).哪个编译器有问题?

有关:

也可以看看

dav*_*mac 9

ABI声明参数值根据特定算法分类.相关的是:

  1. 如果聚合体的大小超过单个八字节,则每个都单独分类.每个八字节都被初始化为NO_CLASS类.

  2. 对象的每个字段都是递归分类的,因此总是考虑两个字段.生成的类根据八字节中的字段类计算:

在这种情况下,每个字段(对于元组或一对)都是类型的uint64_t,因此占用整个"八字节".然后,在每个八字节中要考虑的"两个字段"是"NO_CLASS"(按照3)和uint64_t字段,其被分类为INTEGER.

还有,与参数传递有关:

如果C++对象具有非平凡的复制构造函数或非平凡的析构函数,则它通过不可见的引用传递(该对象在具有类INTEGER的指针的参数列表中被替换)

不满足这些要求的对象必须具有地址,因此需要在内存中,这就是存在上述要求的原因.对于返回值也是如此,尽管这似乎在规范中被省略(可能是偶然的).

最后,有:

(c)如果聚合的大小超过两个八字节且第一个八字节不是SSE或任何其他八字节不是SSEUP,则整个参数在内存中传递.

这显然不适用于此; 聚合的大小恰好是两个八字节.

在返回值时,文字说:

  1. 使用分类算法对返回类型进行分类

这意味着,如上所述,元组应该被分类为INTEGER.然后:

  1. 如果类是INTEGER,则使用序列%rax,%rdx的下一个可用寄存器.

这很清楚.

唯一尚未解决的问题是类型是否是非平凡的可复制构造/可破坏的.如上所述,这种类型的值不能在寄存器中传递或返回,即使规范似乎没有认识到返回值的问题.但是,我们可以使用以下程序轻松地显示元组和对可以是简单的可复制构造和简单可破坏的:

测试程序:

#include <utility>
#include <cstdint>
#include <tuple>
#include <iostream>

using namespace std;

int main(int argc, char **argv)
{
    cout << "pair is trivial? : " << is_trivial<pair<uint64_t, uint64_t> >::value << endl;
    cout << "pair is trivially_copy_constructible? : " << is_trivially_copy_constructible<pair<uint64_t, uint64_t> >::value << endl;
    cout << "pair is standard_layout? : " << is_standard_layout<pair<uint64_t, uint64_t> >::value << endl;
    cout << "pair is pod? : " << is_pod<pair<uint64_t, uint64_t> >::value << endl;
    cout << "pair is trivially_destructable? : " << is_trivially_destructible<pair<uint64_t, uint64_t> >::value << endl;
    cout << "pair is trivially_move_constructible? : " << is_trivially_move_constructible<pair<uint64_t, uint64_t> >::value << endl;

    cout << "tuple is trivial? : " << is_trivial<tuple<uint64_t, uint64_t> >::value << endl;
    cout << "tuple is trivially_copy_constructible? : " << is_trivially_copy_constructible<tuple<uint64_t, uint64_t> >::value << endl;
    cout << "tuple is standard_layout? : " << is_standard_layout<tuple<uint64_t, uint64_t> >::value << endl;
    cout << "tuple is pod? : " << is_pod<tuple<uint64_t, uint64_t> >::value << endl;
    cout << "tuple is trivially_destructable? : " << is_trivially_destructible<tuple<uint64_t, uint64_t> >::value << endl;
    cout << "tuple is trivially_move_constructible? : " << is_trivially_move_constructible<tuple<uint64_t, uint64_t> >::value << endl;
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

使用GCC或Clang编译时的输出:

pair is trivial? : 0
pair is trivially_copy_constructible? : 1
pair is standard_layout? : 1
pair is pod? : 0
pair is trivially_destructable? : 1
pair is trivially_move_constructible? : 1
tuple is trivial? : 0
tuple is trivially_copy_constructible? : 1
tuple is standard_layout? : 0
tuple is pod? : 0
tuple is trivially_destructable? : 1
tuple is trivially_move_constructible? : 0
Run Code Online (Sandbox Code Playgroud)

这意味着GCC弄错了.返回值应在%rax,%rdx中传递.

(类型之间的主要显着差异pair是标准布局,并且可以简单地移动构造,而tuple不是,因此GCC总是可以通过指针返回非平凡移动构造值(例如)).


Jon*_*ely 4

正如 davmac 的答案所示,libstdc++std::tuple可以简单地复制构造,但不能简单地移动构造。两个编译器对于移动构造函数是否应该影响参数传递约定存在分歧。

您链接到的 C++ ABI 线程似乎解释了这种分歧: http://sourcerytools.com/pipermail/cxx-abi-dev/2016-February/002891.html

总之,Clang 完全实现了 ABI 规范所说的内容,但 G++ 实现了它应该所说的内容,但没有更新为实际所说的内容。