LLVM,联合,指针转换和未定义的行为

时间:2016-12-29 11:44:24

标签: c clang llvm undefined-behavior

Clang似乎将联合转换为最严格对齐的成员类型,然后自由使用指针强制转换,例如。

union U {
  double x;
  int y;
};

int f(union U *u) { return u->y; }

编译到

%union.U = type { double }

; Function Attrs: nounwind uwtable
define i32 @f(%union.U* %u) #0 {
  %1 = alloca %union.U*, align 8
  store %union.U* %u, %union.U** %1, align 8
  %2 = load %union.U*, %union.U** %1, align 8
  %3 = bitcast %union.U* %2 to i32*
  %4 = load i32, i32* %3, align 8
  ret i32 %4
}

我很惊讶,因为将指针指向另一种类型然后解除引用它通常是未定义的行为。当然,LLVM IR没有义务遵循与C相同的UB规则,但在大多数情况下它确实如此 - Clang如何遵循C UB规则,它只是将代码直接转录到IR中并让后端处理它。

那么,如何/为什么这是一种处理工会的有效方式呢?

添加一些说明:上述IR基本上与以下C生成的相同:

struct U {
  double x;
};

int f(struct U *u) { return *(int*)u; }

唯一的区别是最终align 8变为align 4。我希望第二个C代码片段是UB,但第一个不是,所以第二个不能是。那么为什么第二个C代码片段不是UB?

2 个答案:

答案 0 :(得分:3)

第二个例子是未定义的行为。在某些现实世界的体系结构中,double具有比int更严格的对齐要求。甚至可以想象一些深奥的体系结构,其中整数和浮点变量存储在不同的内存区域中,以便在单独的ALU和FPU上更高效地运行。反过来说,当int不属于同一double* int时,将union的地址转换为double并取消引用它例如,可能会在32位Sparc Solaris上以SIGBUS错误导致程序崩溃。

即使对不正确对齐的指针进行转换也是UB(因为只是将无效指针加载到寄存器中可能会使某些系统上的程序崩溃,例如旧版x86保护模式下的无效段选择器)。见J.2和§6.3.2.3。请注意,您注意到的一个更改,将对齐限制从8字节放宽到4字节,允许指针的低位位为100而不是000,并且转换指针以100结尾到必须以000结尾的指针类型已经是未定义的行为。 (为了迂腐,唯一的例外是,向任何其他指针类型转换空指针总是安全的,并为您提供新类型的空指针。)

未定义的行为意味着允许编译器执行任何操作,包括执行您的字面意思并执行您的意思。由于你在第二个例子中明确地施放了指针,因此Clang可能会让你自己陷入困境。

你的第一个例子,有两个工会成员怎么样?您可以保证获得有效int对象的地址。根据{{​​3}}(第6.2.5.28节),“所有指向联合类型的指针应具有相同的表示和对齐要求。指向其他类型的指针不需要具有相同的表示或对齐要求。“第40页的脚注41具体说明,”相同的表示和对齐要求意味着可互换性作为函数的参数,函数的返回值和联合的成员。 “在§6.7.2.1.16中,”指向适当转换的联合对象的指针指向其每个成员[...],反之亦然。“

实现合适的转换作为身份功能肯定是有效的!编译器可以在该架构上以任何有意义的方式表示指针,并且标准保证指针的表示对两个对象都有效。

也就是说,如果它读取了联合的非活动成员,则该值未指定。如果设置u.y并读取u.x,则在int小于64位宽的目标上,u.x的对象表示的其余位可能是任何内容,包括陷阱表示。或者,如果您设置u.x并阅读u.y,则该值将取决于intdouble的表示方式的详细信息。

答案 1 :(得分:1)

定义了第一个例子。如果读取了最后未写入的成员,则会在新类型中重新解释此成员表示的字节。类型可能是陷阱表示,如果您将获得未定义的行为,但在现代机器上不太可能。

由于别名规则,第二个示例是未定义的行为。 union可以通过int类型访问,它与struct U或double类型不兼容。

正确的代码是未定义行为的可能结果之一。