工会的使用是否严格符合?

时间:2017-09-13 19:49:13

标签: c gcc clang strict-aliasing

鉴于代码:

struct s1 {unsigned short x;};
struct s2 {unsigned short x;};
union s1s2 { struct s1 v1; struct s2 v2; };

static int read_s1x(struct s1 *p) { return p->x; }
static void write_s2x(struct s2 *p, int v) { p->x=v;}

int test(union s1s2 *p1, union s1s2 *p2, union s1s2 *p3)
{
  if (read_s1x(&p1->v1))
  {
    unsigned short temp;
    temp = p3->v1.x;
    p3->v2.x = temp;
    write_s2x(&p2->v2,1234);
    temp = p3->v2.x;
    p3->v1.x = temp;
  }
  return read_s1x(&p1->v1);
}
int test2(int x)
{
  union s1s2 q[2];
  q->v1.x = 4321;
  return test(q,q+x,q+x);
}
#include <stdio.h>
int main(void)
{
  printf("%d\n",test2(0));
}

整个程序中存在一个联合对象 - q。其活动成员设置为v1,然后设置为v2,然后再设置为v1。代码仅在q.v1上使用address-of运算符,或者在该成员处于活动状态时使用结果指针,同样使用q.v2。由于p1p2p3属于同一类型,因此使用p3->v1访问p1->v1和{{1}应该完全合法}访问p3->v2

我没有看到任何可以证明编译器无法输出1234的原因,但是许多编译器(包括clang和gcc)生成的代码输出4321.我认为发生的事情是他们认为p3上的操作实际上不会更改内存中任何位的内容,它们可以完全被忽略,但我没有看到标准中有任何理由忽略p2->v2用于将数据从p3复制到p1->v1的事实p2->v2,反之亦然。

标准中是否有任何可以证明这种行为的理由,或者编制者是否只是不遵循它?

4 个答案:

答案 0 :(得分:10)

我相信你的代码是符合的,并且GCC和Clang的-fstrict-aliasing模式存在缺陷。

我找不到C标准的正确部分,但是在我为C ++模式编译代码时会出现同样的问题,我确实找到了C ++标准的相关段落。

在C ++标准中,[class.union] / 5定义了在union访问表达式上使用operator =时会发生什么。 C ++标准规定当联合涉及内置运算符=的成员访问表达式时,联合的活动成员将更改为表达式中涉及的成员(如果类型具有一个简单的构造函数) ,但因为这是C代码,它确实有一个简单的构造函数)。

请注意,write_s2x 无法更改联合的活动成员,因为赋值表达式中不包含联合。你的代码并不认为会发生这种情况,所以没关系。

即使我使用placement new显式更改哪个union成员处于活动状态,这应该是活动成员更改的编译器的提示,GCC仍会生成输出4321的代码。 / p>

这看起来像是GCC和Clang的一个错误,假设活动联盟成员的切换不能在这里发生,因为他们无法识别p1p2p3的可能性指向同一个对象。

GCC和Clang(以及几乎所有其他编译器)都支持C / C ++的扩展,您可以在其中读取union的非活动成员(获取任何可能的垃圾值),但前提是您执行此访问涉及union的成员访问表达式。 如果 v1不是活动成员,则read_s1x将不会在此特定于实现的规则下定义行为,因为联合不在成员访问表达式中。但由于v1是活跃成员,因此无关紧要。

这是一个复杂的案例,我希望我的分析是正确的,不是编译维护者或其中一个委员会的成员。

答案 1 :(得分:4)

对标准的严格解释,此代码可能不符合。让我们关注众所周知的§6.5p7的文本:

  

对象的存储值只能由具有其中一个的左值表达式访问   以下类型:
   - 与对象的有效类型兼容的类型,
   - 与对象的有效类型兼容的类型的限定版本,
   - 对应于有效类型的有符号或无符号类型的类型   对象,
   - 对应于合格版本的有符号或无符号类型的类型   有效的对象类型,
   - 一种聚合或联合类型,其中包含上述类型之一   成员(包括递归地,子集合或包含的联合的成员)或
   - 字符类型。

(强调我的)

您的功能read_s1x()write_s2x()执行我在整个代码的上下文中在上方标记为粗体的相反。只有这一段,你可以断定它是不允许的:指向union s1s2的指针将被允许别名指向struct s1的指针,但反之亦然。

如果您在test()中手动“内联”这些功能,那么这种解释当然意味着代码必须按预期工作。对于i686-w64-mingw32,gcc 6.2确实就是这种情况。

添加两个论据,支持上述严格解释:

  • 虽然总是允许使用char *对任何指针进行别名,但字符数组不能被任何其他类型别名化。

  • 考虑(此处不相关)§6.5.2.3p6

      

    为了简化工会的使用,我们提出了一项特殊保证:如果工会包含   几个结构共享一个共同的初始序列(见下文),如果是联盟   对象当前包含这些结构中的一个,允许检查公共结构   其中任何一个的初始部分声明已完成的工会类型的任何地方   是可见的。

    (再次强调我的) - 典型的解释是可见直接意味着在有关功能的范围内,而不是“在翻译单元的某个地方”...所以这个保证没有不包含一个函数,该函数指向struct成员的union之一。

答案 2 :(得分:0)

我没有阅读标准,但在严格别名模式下使用指针(即使用-fstrict-alising)是危险的。请参阅gcc online doc

  

特别注意这样的代码:

union a_union {
  int i;
  double d;
};

int f() {
  union a_union t;
  t.d = 3.0;
  return t.i;
}
  

从不同的工会成员阅读的做法比最近写的(称为type-punning)很常见。即使使用-fstrict-aliasing,也允许使用类型 - 双关语,前提是通过联合类型访问内存。因此,上面的代码按预期工作。请参阅结构联合枚举和位字段实现。但是,此代码可能不会:

int f() {
   union a_union t;
   int* ip;
   t.d = 3.0;
   ip = &t.i;
   return *ip;
}
  

类似地,通过获取地址,转换结果指针和取消引用结果的访问具有未定义的行为,即使转换使用联合类型,例如:

int f() {
  double d = 3.0;
  return ((union a_union *) &d)->i;
}
  

{ - 1}}选项在级别-O2,-O3,-Os。

处启用

在第二个例子中发现了类似的东西吗?

答案 3 :(得分:-2)

它不是符合或不符合 - 它是优化“陷阱”之一。您的所有数据结构都已经过优化,并且您将相同的指针传递给优化的输出数据,因此执行树将简化为值的简单printf。

  sub rsp, 8
  mov esi, 4321
  mov edi, OFFSET FLAT:.LC0
  xor eax, eax
  call printf
  xor eax, eax
  add rsp, 8
  ret

要改变它你需要使这个“转移”功能容易产生副作用并强制实际分配。它将强制优化器不减少执行树中的那些节点:

int test(union s1s2 *p1, union s1s2 *p2, volatile union s1s2 *p3)
/* ....*/

main:
  sub rsp, 8
  mov esi, 1234
  mov edi, OFFSET FLAT:.LC0
  xor eax, eax
  call printf
  xor eax, eax
  add rsp, 8
  ret

这是一个非常简单的测试,只是人为地制造了一点点复杂。