我正在阅读以下关于 C 中序列点的文章:https://www.geeksforgeeks.org/sequence-points-in-c-set-1/
其中有几个未定义行为的示例,例如调用两个修改单个全局变量的函数的表达式,或多次递增同一变量的单个表达式。
从理论上讲,我理解这个概念。 但是,无论我尝试运行示例多少次,行为都是相同的,并且永远不会"令人惊讶"。
为了亲身体验未定义的行为,让示例"令人惊讶"的最简单方法是什么?
(如果重要,我正在使用MINGW64。
这是我在短时间内能想到的最好的:
源代码:
#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("%dn", 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 "%dn"
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()
中的代码如何尝试使其正确:它内联,然后优化对undefined()
的调用,而是在对printf
调用上方执行xor eax,eax
时z
假设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
,但gcc和clang在给定前者时错过了允许的优化机会,而后者则被利用。 这很可能是因为作者认识到利用前一个机会是允许的,但不可否认的是,这是愚蠢的,以至于迫使人们认识到该标准从未打算鼓励它所允许的所有优化。