GCC vs Clang复制struct flexible数组成员

时间:2017-10-18 04:19:25

标签: c gcc clang

请考虑以下代码段。

#include <stdio.h>

typedef struct s {
    int _;
    char str[];
} s;
s first = { 0, "abcd" };

int main(int argc, const char **argv) {
    s second = first;
    printf("%s\n%s\n", first.str, second.str);
}

当我用GCC 7.2编译它时,我得到:

$ gcc-7 -o tmp tmp.c && ./tmp
abcd
abcd

但是当我用Clang(Apple LLVM版本8.0.0(clang-800.0.42.1))编译它时,我得到以下内容:

$ clang -o tmp tmp.c && ./tmp
abcd
# Nothing here

为什么编译器之间的输出不同?我希望字符串不被复制,因为它是一个灵活的数组成员(类似于this question)。为什么GCC实际上会复制它?

修改

一些评论和答案表明这可能是由于优化。 GCC可能会secondfirst的别名,因此更新second应该禁止GCC进行优化。我添加了一行:

second._ = 1;

但这不会改变输出。

2 个答案:

答案 0 :(得分:2)

以下是gcc正在进行的真实答案。正如您所期望的那样,second被分配在堆栈中。它不是first的别名。通过打印地址可以轻松验证这一点。

此外,声明s second = first;正在破坏堆栈,因为(a)gcc正在为second分配最小存储量,但(b)它正在复制所有 first成为第二个,破坏了堆栈。

以下是原始代码的修改版本,其中显示了:

#include <stdio.h>

typedef struct s {
    int _;
    char str[];
} s;
s first = { 0, "abcdefgh" };
int main(int argc, const char **argv) {
    char v[] = "xxxxxxxx";
    s second = first;
    printf("%p %p %p\n", (void *) v, (void *) &first, (void *) &second);
    printf("<%s> <%s> <%s>\n", v, first.str, second.str);
}

在我的32位Linux机器上,使用gcc,我得到以下输出:

0xbf89a303 0x804a020 0xbf89a2fc
<defgh> <abcdefgh> <abcdefgh>

正如您从地址中看到的那样,vsecond位于堆栈中,first位于数据部分。此外,很明显second的初始化已经覆盖了堆栈上的v,结果是代替预期的<xxxxxxxx>,而是显示<defgh>

这对我来说似乎是一个gcc bug。至少,它应该警告second的初始化会破坏堆栈,因为它显然有足够的信息在编译时知道这一点。

编辑:我对此进行了更多测试,并通过将second的声明拆分为:

获得了基本相同的结果
s second;
second = first;

真正的问题是作业。它复制了first所有,而不是结构类型的最小公共部分,这是我认为它应该做的。实际上,如果将first的静态初始化移动到单独的文件中,则赋值执行它应该执行的操作,v正确打印,second.str是未定义的垃圾。这是gcc应该产生的行为,无论first的初始化是否在同一个编译单元中可见。

答案 1 :(得分:-1)

因此,对于答案,两个编译器都表现正常,但您得到的答案是未定义的行为。

GCC
因为您永远不会修改second GCC只是在其查找表中创建secondfirst的别名。修改第二个,GCC无法进行优化,你会得到与Clang相同的答案/崩溃。


看来,Clang并没有自动应用相同的优化。因此,当它复制结构时,它会正确地执行:它复制单个int而不复制任何其他内容。

您很幸运,在您的本地second变量后,堆栈上的值为零,从而终止您的未知字符串。基本上,您使用的是未初始化的指针。如果没有零,你可能会得到很多垃圾和内存故障。

这个东西的目的是做一些低级的东西,比如通过向你的结构中添加一些内存来实现一个内存管理器等。编译器没有义务理解你在做什么;它只是有义务表现得好像你知道自己在做什么。如果您未能将结构类型转换为实际上具有该类型数据的内存,则所有投注均已关闭。

修改
所以,使用godbolt.org并查看程序集:

.LC0:
        .string "%s\n%s\n"
main:
        sub     rsp, 24
        mov     eax, DWORD PTR first[rip]
        mov     esi, OFFSET FLAT:first+4
        lea     rdx, [rsp+16]
        mov     edi, OFFSET FLAT:.LC0
        mov     DWORD PTR [rsp+12], eax
        xor     eax, eax
        call    printf
        xor     eax, eax
        add     rsp, 24
        ret
first:
        .long   0
        .string "abcd"

我们看到GCC实际上与OP的原始代码完全相同:将second视为first的别名。

Tom Karzes 对代码进行了重大修改,因此遇到了不同的问题。他报告的内容确实是一个错误;我没时间在ATM上弄清楚他的堆栈腐败任务究竟发生了什么。