不一致的未定义行为
Inconsistent undefined behavior
对于 class,我想向学生展示 goto
的未定义行为。我想出了以下程序:
#include <stdio.h>
int main()
{
goto x;
for(int i = 0; i < 10; i++)
x: printf("%d\n", i);
return 0;
}
我希望编译器(gcc
版本 4.9.2)警告我访问 i
是未定义的行为,但没有警告,甚至没有:
gcc -std=c99 -Wall -Wextra -pedantic -O0 test.c
当运行程序时,i
显然被初始化为零。为了了解发生了什么,我用第二个变量 j
:
扩展了代码
#include <stdio.h>
int main()
{
goto x;
for(int i = 0, j = 1; i < 10; i++)
x: printf("%d %d\n", i, j);
return 0;
}
现在编译器警告我,我正在访问 j
,但它没有被初始化。我明白,但为什么 i
也没有未初始化?
Now the compiler warns me that I am accessing j without it being
initialized. I understand that, but why is i not uninitialized as
well?
这就是未定义行为的意义所在,它有时确实有效,或无效,或部分无效,或打印垃圾。问题是你无法知道你的编译器到底在做什么来做到这一点,这不是编译器产生不一致结果的错,因为正如你自己承认的那样,这种行为是未定义的。
在这一点上,唯一可以保证的是,对于这将如何进行,没有任何保证。不同的编译器甚至可能给出不同的结果,或者不同的优化级别可能。
编译器也不需要检查这个,也不需要处理这个,因此编译器不需要。无论如何,您不能使用编译器来可靠地检查未定义的行为。这就是单元测试和大量测试用例或统计分析的目的。
未定义行为是 运行 时间现象。因此,编译器很少能够为您检测到它。大多数未定义行为的情况都是在执行超出编译器范围的操作时调用的。
为了让事情变得更加复杂,编译器可能会优化代码。假设它决定将 i
放入 CPU 寄存器中,但将 j
放入堆栈中,反之亦然。然后假设在调试构建期间,它将所有堆栈内容设置为零。
如果您需要可靠地检测未定义的行为,您需要一个静态分析工具,它可以进行编译器无法完成的检查。
根据 C 标准,使用 "goto" 跳过变量初始化将允许编译器做任何它想做的事情,即使在它通常会产生不确定值的平台上,该值可能不会始终如一地运行,但不会'没有任何其他副作用。在这种情况下,gcc 的行为似乎没有像在例如情况下的行为那样下放。整数溢出,但它的优化虽然良性,但可能有点有趣。给定:
int test(int x)
{
int y;
if (x) goto SKIP;
y=x+1;
SKIP:
return y*2;
}
int test2(unsigned short y)
{
int q=0;int i;
for (i=0; i<=y; i++)
q+=test(i);
return q;
}
编译器将观察到在所有定义的情况下,test 将 return 2,因此可以通过为 test2 生成等效于以下代码来消除循环:
int test2(unsigned short y)
{
return (int)y << 1;
}
但是,这样的例子可能会给人一种印象,即编译器以一种良性的方式对待 UB。不幸的是,在 gcc 的情况下,一般情况下不再如此。过去,在没有硬件陷阱的机器上,编译器会将 Indeterminate Value 的使用视为简单地产生任意值,这些值可能会或可能不会以任何一致的方式运行,但没有任何其他副作用。我不确定在任何情况下使用 goto
跳过变量初始化会 yet 除了在变量中具有无意义的值之外会导致副作用,但事实并非如此意味着 gcc 的作者将来不会决定利用这种自由。
对于 class,我想向学生展示 goto
的未定义行为。我想出了以下程序:
#include <stdio.h>
int main()
{
goto x;
for(int i = 0; i < 10; i++)
x: printf("%d\n", i);
return 0;
}
我希望编译器(gcc
版本 4.9.2)警告我访问 i
是未定义的行为,但没有警告,甚至没有:
gcc -std=c99 -Wall -Wextra -pedantic -O0 test.c
当运行程序时,i
显然被初始化为零。为了了解发生了什么,我用第二个变量 j
:
#include <stdio.h>
int main()
{
goto x;
for(int i = 0, j = 1; i < 10; i++)
x: printf("%d %d\n", i, j);
return 0;
}
现在编译器警告我,我正在访问 j
,但它没有被初始化。我明白,但为什么 i
也没有未初始化?
Now the compiler warns me that I am accessing j without it being initialized. I understand that, but why is i not uninitialized as well?
这就是未定义行为的意义所在,它有时确实有效,或无效,或部分无效,或打印垃圾。问题是你无法知道你的编译器到底在做什么来做到这一点,这不是编译器产生不一致结果的错,因为正如你自己承认的那样,这种行为是未定义的。
在这一点上,唯一可以保证的是,对于这将如何进行,没有任何保证。不同的编译器甚至可能给出不同的结果,或者不同的优化级别可能。
编译器也不需要检查这个,也不需要处理这个,因此编译器不需要。无论如何,您不能使用编译器来可靠地检查未定义的行为。这就是单元测试和大量测试用例或统计分析的目的。
未定义行为是 运行 时间现象。因此,编译器很少能够为您检测到它。大多数未定义行为的情况都是在执行超出编译器范围的操作时调用的。
为了让事情变得更加复杂,编译器可能会优化代码。假设它决定将 i
放入 CPU 寄存器中,但将 j
放入堆栈中,反之亦然。然后假设在调试构建期间,它将所有堆栈内容设置为零。
如果您需要可靠地检测未定义的行为,您需要一个静态分析工具,它可以进行编译器无法完成的检查。
根据 C 标准,使用 "goto" 跳过变量初始化将允许编译器做任何它想做的事情,即使在它通常会产生不确定值的平台上,该值可能不会始终如一地运行,但不会'没有任何其他副作用。在这种情况下,gcc 的行为似乎没有像在例如情况下的行为那样下放。整数溢出,但它的优化虽然良性,但可能有点有趣。给定:
int test(int x)
{
int y;
if (x) goto SKIP;
y=x+1;
SKIP:
return y*2;
}
int test2(unsigned short y)
{
int q=0;int i;
for (i=0; i<=y; i++)
q+=test(i);
return q;
}
编译器将观察到在所有定义的情况下,test 将 return 2,因此可以通过为 test2 生成等效于以下代码来消除循环:
int test2(unsigned short y)
{
return (int)y << 1;
}
但是,这样的例子可能会给人一种印象,即编译器以一种良性的方式对待 UB。不幸的是,在 gcc 的情况下,一般情况下不再如此。过去,在没有硬件陷阱的机器上,编译器会将 Indeterminate Value 的使用视为简单地产生任意值,这些值可能会或可能不会以任何一致的方式运行,但没有任何其他副作用。我不确定在任何情况下使用 goto
跳过变量初始化会 yet 除了在变量中具有无意义的值之外会导致副作用,但事实并非如此意味着 gcc 的作者将来不会决定利用这种自由。