未定义行为 (UB) 是 C/C++ 最危险的概念。不是”程序会崩溃”——是”编译器可以做任何事,包括让程序看起来正常工作,直到某天突然不正常”。
AI 对 UB 的理解程度如何?我用三个 sanitizer(UBSan、ASan、MSan)扫描了 200 段 AI 生成的 C/C++ 代码。
实验设计
200 段代码分 4 类:指针操作(50 段)、整数运算(50 段)、类型转换(50 段)、并发代码(50 段)。
每段代码先编译通过,然后分别用以下工具检查: - UBSan (Undefined Behavior Sanitizer):检测整数溢出、空指针解引用、类型不匹配等 - ASan (Address Sanitizer):检测越界、use-after-free、double free 等 - MSan (Memory Sanitizer):检测使用未初始化内存
结果
| 类别 | UBSan 报错 | ASan 报错 | MSan 报错 | 至少一个 UB |
|---|---|---|---|---|
| 指针操作 | 22% | 28% | 18% | 42% |
| 整数运算 | 35% | 2% | 0% | 35% |
| 类型转换 | 30% | 8% | 12% | 38% |
| 并发代码 | 15% | 20% | 25% | 40% |
| 总计 | 25% | 14% | 14% | 38% |
38% 的代码包含至少一个未定义行为。
高频 UB 模式
1. 有符号整数溢出(整数运算类 #1 常见 UB)
// AI 生成
int sum(int a, int b) {
return a + b; // 当 a + b > INT_MAX 时,这是 UB
}C
标准规定有符号整数溢出是未定义行为。编译器可以假设它不会发生,并基于这个假设做优化。比如
if (a + b < a)
这种溢出检查可能被编译器直接删掉——因为”有符号整数不会溢出”。
AI 在 70% 的情况下不处理整数溢出。因为在 Python(AI 训练数据的大头)里整数不会溢出。
2. 空指针解引用(指针操作类 #1)
// AI 生成
struct Node *find(struct Node *head, int val) {
while (head->val != val) { // 如果 head 是 NULL,UB
head = head->next;
}
return head;
}AI 默认输入是有效的。它不加 NULL 检查——不是因为忘了,而是因为训练数据里的教学代码大部分也不加。
3. strict aliasing 违反(类型转换类 #1)
// AI 生成:把 float 的位模式当 int 读
float f = 3.14;
int i = *(int *)&f; // UB: 违反 strict aliasing rule
// 正确做法: memcpy(&i, &f, sizeof(i));这是 AI 最系统性的 UB 来源。AI 经常生成
*(type *)&var 形式的类型双关,这在 C
标准里是 UB(除了 char *)。正确做法是用
memcpy,但大量训练数据里的旧代码用的就是指针转换。
4. 使用未初始化变量(并发类 #1)
// AI 生成
int result;
pthread_create(&thread, NULL, compute, &result);
pthread_join(thread, NULL);
printf("%d\n", result); // 如果 compute 里没写 result,MSan 报错AI 假设线程函数会写入
result。但如果线程函数提前返回(错误路径),result
就是未初始化的。
为什么 AI 不理解 UB
训练数据里 UB 是沉默的。UB 代码能编译、能运行(大部分时候)。AI 从训练数据里学到的是”这段代码在实践中能用”,而不是”这段代码在标准里是未定义的”。
UB 的后果是非局部的。有符号溢出的 UB 可能导致完全不相关的代码被优化掉。这种非局部效应超出了 AI 的上下文窗口——它看到的是局部代码,看不到编译器对整个函数的优化决策。
不同优化级别不同表现。
-O0下 UB 代码通常”正常工作”,-O2下才暴露问题。AI 的训练数据里很多代码是在-O0下测试的。
防御措施
如果你使用 AI 生成 C/C++ 代码:
# 编译时开启所有 sanitizer
gcc -O2 -fsanitize=undefined,address -fno-sanitize-recover=all \
-Wall -Wextra -Werror -o myapp myapp.c
# Rust 用 Miri (无 UB by design,但 unsafe 块需要检查)
cargo miri test规则:AI 生成的每一段 C/C++ 代码都必须在
-fsanitize=undefined,address
下编译和测试。 没有例外。
延伸阅读:
- 让 LLM 帮你写系统代码:哪些能信,哪些会死 – 更全面的 AI 代码信任等级分析
- unsafe Rust:当编译器不再替你扛枪 – Rust 怎么把 UB 限制在 unsafe 块里
- Linux 内核的内存屏障 – volatile 不是内存屏障,这是 UB 的一个来源