【问题标题】:Is the following C union access pattern undefined behavior?以下 C 联合访问模式是未定义的行为吗?
【发布时间】:2018-09-12 08:11:22
【问题描述】:

以下不是现代 C 中未定义的行为:

union foo
{
    int i;
    float f;
};
union foo bar;
bar.f = 1.0f;
printf("%08x\n", bar.i);

并打印 1.0f 的十六进制表示。

但以下是未定义的行为:

int x;
printf("%08x\n", x);

这个呢?

union xyzzy
{
    char c;
    int i;
};
union xyzzy plugh;

这应该是未定义的行为,因为尚未写入 plugh 的任何成员。

printf("%08x\n", plugh.i);

但是这个呢。这是未定义的行为吗?

plugh.c = 'A';
printf("%08x\n", plugh.i);

现在大多数 C 编译器都有sizeof(char) < sizeof(int)sizeof(int) 是 2 或 4。这意味着在这些情况下,最多 50% 或 25% 的 plugh.i 将被写入,但读取剩余的字节将读取未初始化的数据,因此应该是未定义的行为。在此基础上,是不是整个读取未定义行为?

【问题讨论】:

  • @Tom's - 访问不确定值是 UB。点空白。
  • @Tom's 那行没有强制转换,为什么你认为使用未初始化的变量不是 UB?
  • @Tom's - “但它永远不会崩溃,但行为不同” 这里的主题是 C 标准,它不保证任何类型的东西。这就是未定义行为的全部意义所在。
  • @Tom's A cast 是一种显式类型转换。这里没有演员。甚至没有隐式转换,因为 varargs 没有为您提供已知的类型上下文。还是您认为printf("%f", 42) 可以,因为42 可以隐式转换为double
  • @Stargateur 通过联合的类型双关语在 C 中得到了很好的定义,尽管我只会在 C++ 中使用 memcpy 和 bit_cast 请参阅 my answer here for more details。我个人feel that unions are meant for variant types 但那艘船很久以前就离开了。

标签: c language-lawyer undefined-behavior unions type-punning


【解决方案1】:

Defect report 283: Accessing a non-current union member ("type punning") 涵盖了这一点,并告诉我们如果存在陷阱表示,则存在未定义的行为。

缺陷报告要求:

在对应于 6.5.2.3#5 的段落中,C89 包含了这个 句子:

除了一个例外,如果在将值存储到对象的不同成员之后访问联合对象的成员, 行为是实现定义的。

与这句话相关的是这个脚注:

标量类型的“字节顺序”对于不沉迷于类型双关的孤立程序是不可见的(例如,通过 分配给工会的一名成员并通过以下方式检查存储 访问另一个成员,该成员是一个适当的六元数组 字符类型),但在符合时必须考虑 外部强加的存储布局。

C99 is 6.2.6.1#7中唯一对应的措辞:

当一个值存储在联合类型对象的成员中时,与该对象不对应的字节表示 成员但确实对应于其他成员取未指定的值,但 联合对象的价值不应因此成为陷阱 表示。

不完全清楚C99词有相同的 含义为 C89 词。

缺陷报告添加了以下脚注:

在 6.5.2.3#3 中的“指定成员”一词上附加一个新的脚注 78a:

78a 如果用于访问联合对象内容的成员与上次用于在对象中存储值的成员不同,则该值的对象表示的适当部分被重新解释为新类型中的对象表示,如 6.2.6 中所述(有时称为“类型双关语”的过程)。 这可能是一个陷阱表示。

C11 6.2.6.1 General 告诉我们:

某些对象表示不需要表示对象类型的值。如果对象的存储值具有这样的表示形式,并且由不具有字符类型的左值表达式读取,则行为是未定义的。如果这样的表示是由修改的副作用产生的对象的全部或任何部分由不具有字符类型的左值表达式,行为未定义。50)这种表示称为陷阱表示。

【讨论】:

  • @PotentialUpvoters 为什么这只是 +2?我会尽力的。
  • 注意我特别引用缺陷报告是因为脚注是非规范性的,而缺陷报告我们可以看到基本原理并且更有信心我们没有错误,这确实发生在非不时制定规范性部分。
【解决方案2】:

从 6.2.6.1 §7 开始:

当一个值存储在联合类型对象的成员中时,对象表示中与该成员不对应但与其他成员对应的字节采用未指定的值。

所以,plugh.i 的值在设置plugh.c 后将是未指定的。

从脚注到 6.5.2.3 §3 :

如果用于读取联合对象内容的成员与上次用于在对象中存储值的成员不同,则将值的对象表示的适当部分重新解释为对象表示中的对象表示6.2.6 中描述的新类型(有时称为“类型双关”的过程)。这可能是一个陷阱表示。

这表示特别允许类型双关语(正如您在问题中断言的那样)。但它可能会导致陷阱表示,在这种情况下,根据 6.2.6.1 §5 读取值具有未定义的行为:

某些对象表示不需要表示对象类型的值。如果对象的存储值具有这样的表示形式并且由不具有字符类型的左值表达式读取,则行为未定义。如果这种表示是由通过不具有字符类型的左值表达式修改对象的全部或任何部分的副作用产生的,则行为未定义。 50) 这种表示称为 陷阱表示。

如果它不是陷阱表示,那么标准中似乎没有任何内容会导致这种未定义的行为,因为从 4 §3 开始,我们得到:

在所有其他方面都正确、对正确数据进行操作、包含未指定行为的程序应是正确的程序并按照 5.1.2.3 执行。

【讨论】:

    【解决方案3】:

    其他答案解决了以下主要问题:当 plugh 未初始化且仅分配了 plugh.c 时,读取 plugh.i 是否会产生未定义的行为。简而言之:不,除非plugh.i 的字节在读取时构成陷阱表示。

    但我想直接谈谈问题中的初步断言:

    现在大多数 C 编译器都有sizeof(char) < sizeof(int),带有 sizeof(int) 是 2 或 4。这意味着在这些情况下 plugh.i 的大部分 50% 或 25% 将被写入

    问题似乎是假设为plugh.c 赋值将使plugh 中与c 不对应的那些字节不受干扰,但标准绝不支持该命题。事实上,它明确否认任何此类保证,因为正如其他人所指出的那样:

    当一个值存储在联合类型对象的成员中时, 不对应的对象表示的字节 成员但确实对应于其他成员采用未指定的值

    C2011, 6.2.6.1/7;强调)

    虽然这不能保证这些字节所采用的未指定值与分配之前的值不同,但它明确规定它们可能不同。在某些实现中,它们通常会是完全合理的。例如,在仅支持字大小写入内存的平台上,或者此类写入比字节大小的写入更有效,对plugh.c 的分配很可能是通过字大小的写入实现的,而无需先加载另一个plugh.i 的字节,以便保留它们的值。

    【讨论】:

    • 我认为wobbly values的概念包含了这个问题。
    • @ShafikYaghmour:在某些情况下,保证写入结构成员不会干扰该成员之外的任何存储将是有用的,并且解决缺乏这样的保证会很昂贵;在其他情况下,这种保证会很昂贵。同样,在某些平台上,这种保证几乎可以免费得到维护,而在其他平台上,这种保证会很昂贵。我不认为标准的作者打算应用上述许可,除非实施者判断后者的费用将超过前者...
    • ...实现旨在服务的任务类型,但不幸的是,标准没有提供需要保证的程序可以安全地拒绝在不提供保证的实现上运行的方法。
    • @ShafikYaghmour,据我了解该术语的含义,我认为不需要“摇摆不定的价值观”来理解我所描述的内容。也就是说,plugh.i 的某些字节在分配给plugh.c 时所取的未指定值是否更不稳定是另外一个单独的考虑因素。
    • @JohnBollinger:在某些情况下,实现最佳性能的唯一方法是识别“摆动字节”的概念。例如,假设该代码按该顺序写入unionArray[i].struct1.member2unionArray[j].struct2unionArray[i].struct1.member1,然后返回unionArray[i].struct1。我认为如果没有检查返回结构的第一个成员或unionArray[i].struct1 之外的任何内容,则该序列应该具有定义的行为,并且看不到通过字符类型进行此类检查以调用 UB 的基础,但除了“摇摆不定的值”之外没有其他基础对于...
    【解决方案4】:

    C11 §6.2.6.1 p7 说:

    当一个值存储在联合类型对象的成员中时, 不对应的对象表示的字节 成员,但确实对应于其他成员,取 unspecified 值。

    因此,plugh.i 将是未指定的。

    【讨论】:

      【解决方案5】:

      在有用的优化可能导致程序执行的某些方面以与标准不一致的方式表现的情况下(例如,同一字节的两次连续读取产生不一致的结果),标准通常会尝试描述这种影响可能发生的情况被观察,然后将这种情况分类为调用未定义的行为。它没有做太多努力来确保其特征不会“诱捕”某些行为显然应该以可预测的方式处理的行为,因为它希望编译器编写者避免在这种情况下表现得迟钝。

      不幸的是,在某些极端情况下,这种方法确实不能很好地工作。例如,考虑:

      struct c8 { uint32_t u; unsigned char arr[4]; };
      union uc { uint32_t u; struct c8 dat; } uuc1,uuc2;
      
      void wowzo(void)
      {
        union uc u;
        u.u = 123;
        uuc1 = u;
        uuc2 = u;
      }
      

      我认为很明显,标准不要求 uuc1.dat.arruuc2.dat.arr 中的字节包含任何特定值,并且对于四个字节 i==0.. 3、将uuc1.dat.arr[i]复制到uuc2.dat.arr[i],将uuc2.dat.arr[i]复制到uuc1.dat.arr[i],或者同时写入uuc1.dat.arr[i]uuc2.dat.arr[i]匹配值。我认为标准是否打算要求编译器选择其中一种行动方案,而不是简单地让这些字节保留它们碰巧持有的东西,这还不清楚。

      显然,如果没有观察到 uuc1.dat.arruuc2.dat.arr 的内容,则代码应该具有完全定义的行为,并且没有任何迹象表明检查这些数组应该调用 UB。此外,没有定义的方法可以使u.dat.arr 的值在分配给uuc1uuc2 之间改变。这表明uuc1.dat.arruuc2.dat.arr 应该包含匹配值。另一方面,对于某些类型的程序,将明显无意义的数据存储到uuc1.dat.arr 和/或uuc1.dat.arr 很少有任何用处。我不认为标准的作者特别打算要求这样的存储,但是说字节采用“未指定”值使得它们成为必要。我希望这样的行为保证会被弃用,但我不知道有什么可以替代它。

      【讨论】:

        猜你喜欢
        • 2019-08-12
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-05-01
        • 1970-01-01
        • 1970-01-01
        • 2018-06-24
        相关资源
        最近更新 更多