首页 文章

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

提问于
浏览
11

在尝试to optimize return values on 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}; }

Clang 3.8 outputs f 的汇编代码:

movl $1, %eax
movl $2, %edx
retq

这对于 g

movl $2, %eax
movl $1, %edx
retq

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

movq %rdi, %rax
movq $2, (%rdi)
movq $1, 8(%rdi)
ret

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

相关:

另见

2 回答

  • 9

    正如davmac的回答所示,libstdc std::tuple 可以简单地复制构造,但不能简单地移动构造 . 两个编译器不一致关于移动构造函数是否应该影响传递约定的参数 .

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

    总而言之,Clang完全实现了ABI规范所说的内容,但G实现了它应该说的内容,但实际上还没有更新 .

  • 4

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

    如果聚合体的大小超过单个八字节,则每个都单独分类 . 每个八字节都被初始化为NO_CLASS类 . 对象的每个字段都是递归分类的,因此总是考虑两个字段 . 生成的类根据八字节中的字段类计算:

    在这种情况下,每个字段(对于元组或一对)都是 uint64_t 类型,因此占用整个"eightbyte" . 那么,每个八字节中要考虑的"two fields"是"NO_CLASS"(按照3)和 uint64_t 字段,它被归类为INTEGER .

    还有,与参数传递有关:

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

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

    最后,有:

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

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

    在返回值时,文字说:

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

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

    如果类是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;
    }
    

    使用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
    

    这意味着海湾合作委员会错了 . 返回值应在%rax,%rdx中传递 .

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

相关问题