从函数返回结构体可能存在GCC缺陷

133

我在实现O'Neill的PCG PRNG时,发现了GCC中的一个bug。(在Godbolt's Compiler Explorer上的初始代码

在将oldstate乘以MULTIPLIER之后(结果存储在rdi中),GCC没有将该结果加到INCREMENT上,而是将INCREMENT movabs到rdx中,然后将其用作rand32_ret.state的返回值。

最小可重现示例(Compiler Explorer):

#include <stdint.h>

struct retstruct {
    uint32_t a;
    uint64_t b;
};

struct retstruct fn(uint64_t input)
{
    struct retstruct ret;

    ret.a = 0;
    ret.b = input * 11111111111 + 111111111111;

    return ret;
}

生成的汇编代码(GCC 9.2, x86_64, -O3):

fn:
  movabs rdx, 11111111111     # multiplier constant (doesn't fit in imm32)
  xor eax, eax                # ret.a = 0
  imul rdi, rdx
  movabs rdx, 111111111111    # add constant; one more 1 than multiplier
     # missing   add rdx, rdi   # ret.b=... that we get with clang or older gcc
  ret
# returns RDX:RAX = constant 111111111111 : 0
# independent of input RDI, and not using the imul result it just computed

有趣的是,将结构体修改为uint64_t作为第一个成员会产生正确的代码,同时将两个成员都改为uint64_t也可以
当结构体可以被简单复制时,x86-64 System V会返回小于16字节的结构体到RDX:RAX寄存器中。在这种情况下,第二个成员位于RDX寄存器中,因为RAX的高半部分是对齐或.b的填充,当.a是更窄的类型时。(sizeof(retstruct)无论如何都是16;我们没有使用__attribute__((packed)),所以它遵循alignof(uint64_t) = 8。) 这段代码是否包含任何未定义的行为,使得GCC能够生成“不正确”的汇编代码? 如果没有,应该在https://gcc.gnu.org/bugzilla/上报告此问题。

评论不适合进行长时间的讨论;此对话已被移至聊天室 - Samuel Liew
3个回答

104
我这里看不到任何未定义行为;因为你的类型是无符号的,所以有符号溢出的未定义行为是不可能的,并且没有什么奇怪的地方。即使是带符号的,它也必须为那些引起溢出未定义行为的输入产生正确的输出,比如rdi=1。GCC的C++前端也存在问题。
此外,GCC8.2可以正确地编译AArch64和RISC-V(使用movk构造常量后转换为madd指令,或在加载常量后进行RISC-V乘法和加法)。如果GCC发现了UB,我们通常会预期它也会在其他ISA中找到并破坏您的代码,至少对于具有类似类型宽度和寄存器宽度的ISA。

Clang也可以正确编译它。

这似乎是从GCC 5到6的一个退步;GCC 5.4编译正确,6.1及更高版本则不行。(Godbolt)。

你可以使用你问题中的MCVE在GCC的bugzilla上报告此问题。 看起来这就像是x86-64 System V结构返回处理中的一个错误,可能是由于包含填充的结构体。 这就解释了为什么内联时它能够工作,并且当将a扩展为uint64_t(避免填充)时也能工作。

34
我已经报告了它。 - vitorhnn
11
看起来在“主分支”上已经修复了。 - S.S. Anne

22

这个问题已经在trunk/master上得到了修复。

这里是相关的提交

这是一个补丁来解决这个问题。

根据补丁中的一条评论,reload_combine_recognize_pattern函数正在尝试调整USE insns


14

这段代码是否存在任何未定义的行为,使得GCC会生成“错误”的汇编代码?

问题中提供的代码在C99及以后的C语言标准下是良好定义的。特别地,C允许函数无限制地返回结构体值。


2
GCC确实会生成函数的独立定义;无论是否与其他函数一起在翻译单元中编译,我们都在查看这个,这是我们正在寻找的。您可以通过将其单独编译到翻译单元中并在没有LTO的情况下链接,或者通过使用-fPIC进行编译来轻松测试它,这意味着所有全局符号(默认情况下)都是可互换的,因此不能内联到调用者中。但是,从生成的汇编代码中看,问题确实是可以检测到的,而不管调用者如何。 - Peter Cordes
好的,@PeterCordes,虽然我非常有信心这个细节是在Godbolt下改变了。 - John Bollinger
问题的第一个版本链接到了Godbolt,只有函数本身在翻译单元中,就像你回答时问题本身的状态一样。我没有检查你可能正在查看的所有修订或评论。水已经过桥了,但我认为从来没有声称独立的asm定义仅在源使用__attribute__((noinline))时才会出现问题。(这将是令人震惊的,不仅仅是GCC正确性错误的方式)。可能只是为了制作一个打印结果的测试调用者而提到了这一点。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接