【问题标题】:C - non-standard struct "compatibility"C - 非标准结构“兼容性”
【发布时间】:2015-03-06 07:01:55
【问题描述】:

简而言之,我的问题是:

C 标准明确规定结构成员应具有按声明顺序增长的相对地址。它也没有说明结构成员应该如何对齐的任何细节。显然,这样做是为了允许填充结构和打包结构实现。但是,理论上,可以有一个符合标准的编译器,只要它们的增长顺序与声明成员的顺序相同,它就会为结构成员提供完全随机的地址。但是这样的编译器存在吗?

这里有一些细节。考虑以下两个结构:

struct s1 {
    int var1;
    char var2;
    long var3;
};

struct s2 {
    int var1;
    char var2;
    long var3;
    char var4;
    int var5;
};

以及以下代码:

printf("offsetof(struct s1, var2) = %d\n", 
        offsetof(struct s1, var2));

printf("offsetof(struct s2, var2) = %d\n", 
        offsetof(struct s2, var2));

printf("offsetof(struct s1, var3) = %d\n", 
        offsetof(struct s1, var3));

printf("offsetof(struct s2, var3) = %d\n", 
        offsetof(struct s2, var3));

gcc (GCC) 4.8.3 20140911 产生以下输出:

offsetof(struct s1, var2) = 4                                                 
offsetof(struct s2, var2) = 4                                                 
offsetof(struct s1, var3) = 8                                                 
offsetof(struct s2, var3) = 8

这很有意义:常规的符合标准的编译器(不重新排序结构成员的编译器)在对结构成员执行填充时,只考虑前一个结构成员的大小和偏移量。这意味着具有相应类型的两个结构的第一个成员的相对地址在此类编译器上将始终相同。反过来,这意味着在我们的示例中,我们可以安全地执行以下操作:

struct s2 test_s2, *ptest_s2;
struct s1 test_s1, *ptest_s1;

ptest_s2 = &test_s2;
ptest_s1 = &test_s1;

ptest_s2->var1 = 1;
ptest_s2->var2 = '2';

ptest_s1 = (struct s1*)ptest_s2;

printf("ptest_s1->var1 = %d\n", ptest_s1->var1);
printf("ptest_s1->var2 = %c\n", ptest_s1->var2);

编译和运行良好,并在同一个编译器上给出输出

ptest_s1->var1 = 1                                                            
ptest_s1->var2 = 2

由于所有指向结构的指针在标准中都具有相同的表示和对齐方式,因此此处 UB 的唯一来源实际上是期望具有相应类型的第一个结构成员的相对地址在两个结构中相同。

现在,真正的问题来了:现实世界的编译器(那些不重新排序结构成员的编译器)的相对地址可以不同吗?

PS 我知道在 C11 中,通过将第二个结构中的第一个结构的成员替换为第一个结构的匿名实例(其中,顺便说一下,据我了解,内部应该以相同的方式工作),但我想编写可以在不支持匿名结构的编译器版本上执行相同操作的代码。

【问题讨论】:

  • 我会说'关于主题'-'这样的编译器是否存在'相当于'这个逻辑是否可移植'。
  • 今天不存在的东西明天可能会存在,并在下周成为主流。你打算每天问这个问题吗?坚持标准更容易。
  • @Dan Allen:比题外话,如要求一个非现场工具,编译器只是什么。
  • @zaibis 我会说“是以下便携式设备”几乎总是热门话题。
  • @LuisColorado:它是一个宏,自 C89 以来一直是标准的一部分,它在 stddef.h (en.wikipedia.org/wiki/Offsetof) 中定义

标签: c struct casting undefined-behavior


【解决方案1】:
struct s3 {
    int var1;
    int var2;
    int var3;
};

struct s4 {
    int var1;
    int var2;
    int var3;
    long long var4;
};

当你添加一个具有更强对齐要求的类型时,你会改变整个结构的对齐方式。

然后当你转换和取消引用指针时,它就是 UB。

在上面的代码中,我相信在最后添加一个var4 会将var1 从字对齐更改为双字对齐,假设int 是字对齐并且long long 是双字对齐.

long 是一个非常糟糕的例子,因为它在 32 位 gcc 中是 32 位,但在 64 位 gcc 中是 64 位。

【讨论】:

    【解决方案2】:

    这个问题比你想象的要多。 据我所知,答案是合格的“不”。

    大家的共识似乎是编译器没有真正的理由来填充成员,除了确保它们正确对齐到它们的开始并且可以占据数组中的连续位置。

    标准要求第一个成员位于struct 的开头。

    我只能找到一些人(在此处、网络等)认为以下是确定类型 T 对齐的最便携的已知方法,并且没有人提供过不兼容的平台。

    #include<stddef.h>
    
    #define alignment(T) (offsetof(struct {char w;T v;},v))
    

    编译器开发人员不会无缘无故浪费内存。然而,理论上有可能(比如说)有人可以决定将未对齐的成员放置在填充区域的末尾而不是开头。 甚至可以想象,调试编译器可以在数组类型的末尾添加“覆盖标记”。

    但是我找不到一个编译器的样本(或声明),它(当不打包数据时)除了从第一个成员开始,为下一个成员填充最少,然后为最严格对齐的成员结束填充。

    但是,即使在单一架构上,不同的编译器也可能对原始类型做出不同的决定,因此即使在相同的硬件架构上,struct 也可能具有不同的布局。

    因此,您不能依赖它来实现互操作性。

    【讨论】:

      猜你喜欢
      • 2020-07-31
      • 2020-07-01
      • 2012-01-31
      • 1970-01-01
      • 1970-01-01
      • 2016-05-24
      • 1970-01-01
      • 1970-01-01
      • 2021-01-14
      相关资源
      最近更新 更多