我正在阅读以下有关C中序列点的文章:https://www.geeksforgeeks.org/sequence-points-in-c-set-1/
其中有几个未定义行为的示例,例如调用两个函数的表达式会修改单个全局变量,或者单个表达式会多次增加同一变量。
理论上,我理解这个概念。但是,无论我尝试运行这些示例多少次,其行为都是相同的,并且永远不会“令人惊讶”。
为了获得对未定义行为的动手理解的目的,使示例“令人惊讶”的最简单方法是什么?
(如果有问题,我正在使用MINGW64。)
答案 0 :(得分:0)
这是我可以在短时间内提出的最佳建议:
源代码:
#include <stdio.h>
int undefined(int *a, short *b)
{
*a = 1;
b[0] = 0;
b[1] = 0;
return *a;
}
int main()
{
int x;
short *y = (short*) &x;
int z = undefined(&x, y);
printf("%d\n", z);
return 0;
}
使用gcc 8.3 -O3生成的程序集
undefined(int*, short*):
mov DWORD PTR [rdi], 1
mov eax, 1
mov DWORD PTR [rsi], 0
ret
.LC0:
.string "%d\n"
main:
sub rsp, 8
mov esi, 1
mov edi, OFFSET FLAT:.LC0
xor eax, eax
call printf
xor eax, eax
add rsp, 8
ret
查看实际情况:https://godbolt.org/z/E0XDYt
尤其是,它依赖于将int
的地址强制转换为short*
所引起的不确定行为,该行为违反了严格的别名规则,因此会导致不确定行为。
从undefined()
的汇编开始。假设a
和b
是不同的类型,因此它们不能重叠,因此即使将return *a;
取到{内存中的值。它会关闭优化功能,因此这是那些真正隐患的问题之一,仅在优化的发行版中才会体现出来,而在您尝试使用非优化的调试版来进行调试时不会出现。
但是,请注意mov eax,1
中的代码如何尝试使其正确:它将内联,然后优化对main()
的调用,而假定{{1}中的undefined()
},则它会在对0
的调用上方执行z
。因此,它忽略了上面几行中刚刚算出的返回值,而是使用了另一个值。
总而言之,这是一个非常糟糕的程序。不确定行为可能给您带来的风险。
答案 1 :(得分:0)
测试gcc和clang时的一种有用模式是使用下标来访问数组,这些下标的值在一定范围内,但对于编译器未知,并使用标准描述为等同于下标表示法的指针语法。用类似的方法测试gcc和clang:
struct S1 {int x;};
struct S2 {int x;};
union foo { struct S1 arr1[8]; struct S2 arr2[8]; } u;
uint32_t test1(int i, int j)
{
if (sizeof u.arr1 != sizeof u.arr2)
return -99;
if (u.arr1[i].x)
u.arr2[j].x = 2;
return u.arr1[i].x;
}
uint32_t test2(int i, int j)
{
if (sizeof u.arr1 != sizeof u.arr2)
return -99;
if ((u.arr1+i)->x)
(u.arr2+j)->x = 2;
return (u.arr1+i)->x;
}
将显示,尽管标准将u.arr1[i].x
和u.arr2[j].x
的行为分别定义为与(u.arr1+i)->x
和(u.arr2+j)->x
等效,但是当给定前者,当给定后者时,他们会利用。这很可能是因为作者认识到利用以前的机会是可以的,但不可否认的是,它是如此愚蠢以至于迫使人们认识到该标准绝不是要鼓励它所允许的所有优化。