未定义行为
C 语言标准精确指定了 C 语言程序的可观察行为,除了下列分类之一:
- 未定义行为 - 程序的该行为没有限制。未定义行为的例子是越过数组边界的访问、有符号整数溢出、空指针解引用、在表达式中超过一次修改标量而其中无顺序点、通过不同类型的指针访问对象,等等。编译器不要求诊断未定义行为(尽管多数简单情形是得到诊断的),且编译后的程序不要求做任何有意义的事。
- 实现定义行为 - 在未指定行为之上,实现规范了如何选择。例如,字节中的位数,或有符号整数右移是算术还是逻辑。
(注意:严格遵从的程序不依赖任何未指定、未定义或实现定义行为)
要求编译器对违背任何 C 语法规则或语义约束的任何程序发布诊断消息(错误或警告),即使其行为被指定为未定义或实现定义,或者编译器可提供语言扩展以允许此种程序被接受。另外,不要求对未定义行为诊断。
UB 与优化
因为正确的 C 程序是没有未定义行为的,编译器可以在启用优化的条件下编译确实有 UB 的程序时,生成不期待的结果:
例如,
有符号溢出
int foo(int x) { return x+1 > x; // 真或为有符号溢出导致的 UB }
可以编译成(演示)
foo: movl $1, %eax ret
越界访问
int table[4] = {0}; int exists_in_table(int v) { // 在最初的 4 个迭代中返回真或因为越界访问 UB for (int i = 0; i <= 4; i++) { if (table[i] == v) return 1; } return 0; }
可以编译成(演示)
exists_in_table: movl $1, %eax ret
未初始化标量
可能产生下列输出(可在一个旧版本 gcc 观察到):
p is true p is false
可以编译成(演示)
f: mov eax, 42 ret
非法标量
int f(void) { _Bool b = 0; unsigned char* p =(unsigned char*)&b; *p = 10; // 从 b 读取现在是 UB return b == 0; }
可编译成(演示)
f(): movl $11, %eax ret
空指针解引用
int foo(int* p) { int x = *p; if(!p) return x; // 为上述 UB ,或绝不采用此分支 else return 0; } int bar() { int* p = NULL; return *p; // 无条件 UB }
可以编译成( foo 用 gcc , bar 用 clang )
foo: xorl %eax, %eax ret bar: retq
访问传递给 realloc 的指针
选择 Clang 以观察示出的输出
运行此代码
可能的输出:
12
无副效应的无限循环
选择 Clang 以观察示出的输出
运行此代码
#include <stdio.h> int fermat() { const int MAX = 1000; int a=1,b=1,c=1; // 无副效应的无限循环是 UB while (1) { if (((a*a*a) == ((b*b*b)+(c*c*c)))) return 1; a++; if (a>MAX) { a=1; b++; } if (b>MAX) { b=1; c++; } if (c>MAX) { c=1;} } return 0; } int main(void) { if (fermat()) puts("Fermat's Last Theorem has been disproved."); else puts("Fermat's Last Theorem has not been disproved."); }
可能的输出:
Fermat's Last Theorem has been disproved.
引用
- C11 standard (ISO/IEC 9899:2011):
- 3.4 Behavior (p: 3-4)
- 4/2 Undefined behavior (p: 8)
- C99 standard (ISO/IEC 9899:1999):
- 3.4 Behavior (p: 3-4)
- 4/2 Undefined behavior (p: 7)
- C89/C90 standard (ISO/IEC 9899:1990):
- 1.6 DEFINITIONS OF TERMS
外部链接
- 每个 C 程序员都该知道关于未定义行为的事 #1/3
- 每个 C 程序员都该知道关于未定义行为的事 #2/3
- 每个 C 程序员都该知道关于未定义行为的事 #3/3
- 未定义行为能导致时间旅行(在所有事项中,时间旅行可是最惊人的)
- 了解 C/C++ 中的整数溢出
- 未定义行为及费马最后定理
- 空指针的趣事, part 1 ( Linux 2.6.30 中由空指针解引用导致的未定义行为所引发的局部滥用)