为什么此代码的结果在使用和不使用&qot;-fsanitize=UNDEFINED,ADDRESS&QOT;时有所不同? [英] Why are the results of this code different with and without "-fsanitize=undefined,address"?
本文介绍了为什么此代码的结果在使用和不使用&qot;-fsanitize=UNDEFINED,ADDRESS&QOT;时有所不同?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!
问题描述
我发现此代码使用";-fsanitize=unfinded、address";和不使用它会产生不同的结果。
int printf(const char *, ...);
union {
long a;
short b;
int c;
} d;
int *e = &d.c;
int f, g;
long *h = &d.a;
int main() {
for (; f <= 0; f++) {
*h = g;
*e = 6;
}
printf("%d
", d.b);
}
命令行为:
$ clang -O0 -fsanitize=undefined,address a.c -o out0
$ clang -O1 -fsanitize=undefined,address a.c -o out1
$ clang -O1 a.c -o out11
$ ./out0
6
$ ./out1
6
$ ./out11
0
Clang版本为:
$ clang -v
clang version 13.0.0 (/data/src/llvm-dev/llvm-project/clang 3eb2158f4fea90d56aeb200a5ca06f536c1df683)
Target: x86_64-unknown-linux-gnu
Thread model: posix
InstalledDir: /data/bin/llvm-dev/bin
Found candidate GCC installation: /opt/rh/devtoolset-7/root/usr/lib/gcc/x86_64-redhat-linux/7
Selected GCC installation: /opt/rh/devtoolset-7/root/usr/lib/gcc/x86_64-redhat-linux/7
Candidate multilib: .;@m64
Candidate multilib: 32;@m32
Selected multilib: .;@m64
Found CUDA installation: /usr/local/cuda, version 10.2
操作系统和平台为:
CentOS Linux release 7.8.2003 (Core).0, x86_64 GNU/Linux
我的问题:
- 我的代码是否有问题?取多个工会成员的地址在C中是否无效?
- 如果我的代码有问题,怎么让llvm(或GCC)来提醒我?我用过-WALL-WEXTRA,但LLVM和GCC没有提示。
推荐答案
代码是否有问题?
出于实际目的,是的。
相同的根本问题正如Eric Postpischil指出的那样,从字面上看,C标准似乎允许您的代码,并要求它输出6(假设这与您的实现表示整数类型和布局联合的方式一致)。然而,这种字面上的解读将使strict aliasing rule几乎完全无能为力,因此在我看来,这不是标准作者所希望的。
严格别名规则的精神在于,不能通过指向不同类型(字符类型等的某些例外情况)的指针访问同一对象,并且编译器可以在假设这种情况从未发生的情况下进行优化。虽然d.a
和d.c
严格地说不是同一个对象&,但它们确实有重叠的存储,我认为编译器作者将该规则解释为也不允许通过指向不同类型的指针访问重叠的对象。在这种解释下,您的代码将具有未定义的行为。在Defect Report 236中,委员会考虑了一个类似的示例,并指出它具有未定义的行为,因为它使用的指针具有不同的类型,但指定了相同的存储区域。然而,澄清这一点的措辞似乎从未出现在该标准的任何后续版本中。
无论如何,我认为实际的结果是,您不能期望您的代码在现代编译器(强制执行其对严格别名规则的解释)下正确工作。这是否是一个clang bug还是个见仁见智的问题,但即使您确实认为它是,这也是一个他们可能永远无法修复的bug。
它为什么会这样?
如果使用-fno-strict-aliasing
标志,则返回到6行为。我的猜测是,消毒器碰巧抑制了某些优化,这就是为什么您在使用这些选项时看不到0行为的原因。
-O1
在幕后似乎发生了什么,编译器假设*h
和*e
的存储不交互(因为它们的类型不同),因此可以自由重新排序。因此它将*h = g
提升到循环之外,因为毕竟到同一地址的多个存储(没有中间负载)是冗余的,并且只需要保留最后一个。它恰好放在循环之后,大概是因为它不能证明e
不指向g
,所以需要在循环之后重新加载g
的值。因此,d.b
的最终值派生自*h = g
,有效地实现了d.a = 0
。如何获取警告?
不幸的是,编译器不擅长静态或在运行时检查是否违反(他们对严格别名规则的解释)。我不知道有什么方法可以对这样的代码发出警告。使用clang,您可以使用-Weverything
启用它支持的每个警告选项(其中许多选项是无用的或适得其反的),即使这样,它也不会给出有关您的程序的相关警告。
另一个示例
如果有人好奇,这里有另一个测试用例,它不依赖于任何类型双关、重新解释或其他实现定义的行为。
#include <stdio.h>
short int zero = 0;
void a(int *pi, long *pl) {
for (int x = 0; x < 1000; x++) {
*pl = x;
*pi = zero;
}
}
int main(void) {
union { int i; long l; } u;
a(&u.i, &u.l);
printf("%d
", u.i);
}
从字面上看,这段代码似乎在任何实现上都打印0:a()
中的最后一个赋值是给u.i
,因此u.i
应该是活动成员,printf
应该输出分配给它的值0。但是,使用clang -O2
时,存储被重新排序,程序输出999
。
这篇关于为什么此代码的结果在使用和不使用&qot;-fsanitize=UNDEFINED,ADDRESS&QOT;时有所不同?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!
查看全文