当前位置: 首页 > news >正文

CppCon 2017 学习:Undefined Behavior in 2017

我们来逐步列出、注释并分析你提供的代码与概念,涉及两个核心主题:

  • 未定义行为(UB) 的设计理念
  • 类型转换和内存对齐问题

1. √(-1) = ?

√(-1) = ?
选项:
• i
• NaN
• throw 异常
• abort 程序
• 返回任意值
• Undefined Behavior(UB)

正确答案:Undefined Behavior(UB)

分析:

在 C/C++ 中,如果你写:

#include <cmath>
std::sqrt(-1.0);

那么:

  • 返回值依赖于实现(平台 + 库):
    • 有时是 NaN
    • 有时会抛异常(仅启用浮点异常时)
    • 有时是 UB,特别是在禁用 errno 或不检测非法输入的模式下
  • 不会返回 i(那是复数领域)
  • C++ 标准不要求 sqrt(-1) 有定义行为 ⇒ 未定义行为是允许最优化的“空间”

2. UB 是为什么被选择的?

“UB 是最极端但最有效率的设计选择。”

原因:

  • 没有任何标准保证 ⇒ 编译器不需要处理边界情况
  • 不需要生成检查代码 ⇒ 更高效
  • 避免平台特定行为限制移植性
  • 使得优化器 可以大胆重排/删除看似无用的代码
    例如:
if (ptr != nullptr)*ptr = 10; // 如果 ptr 是未初始化变量 ⇒ UB ⇒ 编译器甚至可能删掉整个 if 块

3. 示例代码分析

#include <iostream>
int main(void) {int a[] = {1, 2, 3};int *p = (int *)(1 + (char *)a);  //  未定义行为std::cout << *p << "\n";return 0;
}

行为解释(逐行注释):

int a[] = {1, 2, 3};     
// 创建一个整数数组,通常 a 在栈上分配
(char*)a                
// 将 int* 强转为 char*,这样可以做逐字节偏移(因为 char 是 1 字节单位)
1 + (char*)a           
// 把 char* 加 1 ⇒ 向前偏移一个字节,结果是一个 misaligned pointer(未对齐的地址)
(int*)(...)             
// 把非对齐的 char* 强转回 int* ⇒ 违反对齐要求(int 通常要求4字节对齐)
*p                     
// 解引用一个未对齐的 int 指针 ⇒  未定义行为(UB)
std::cout << *p << "\n";
// 输出可能值、崩溃、或垃圾

为什么这段代码有未定义行为?

标准明确要求:*解引用类型为 T 的指针时,该指针必须对齐到 alignof(T)**

  • int* 访问必须是 4 字节对齐(32 位)或 8 字节(64 位)
  • char* 偏移 1 字节 ⇒ 不再对齐
  • x86/x86_64 架构支持非对齐访问,但这只是行为未定义 ≠ 实际会报错
  • 换个平台(如 ARM)会直接崩溃

总结 C/C++ 中为什么有这么多 UB?

原因目的
✂ 不强制检查避免性能损失
不要求处理异常输入避免复杂性
不限制平台自由提高移植性
允许优化器自由推理提高执行效率

安全版本(避免 UB)

如果你想安全地访问整数的字节:

#include <iostream>
#include <cstring>
int main() {int a[] = {1, 2, 3};unsigned char* bytes = reinterpret_cast<unsigned char*>(a);int val;std::memcpy(&val, bytes + 1, sizeof(int)); // OK:不会违反对齐std::cout << val << "\n";return 0;
}

或者使用 std::byte(C++17+)。

未定义行为(Undefined Behavior, UB) 在 C 和 C++ 中的典型实例分析,尤其是以移位操作(如 x << y)为切入点的 UB 案例。我们来逐步 添加注释、理解并分析 你提供的内容,涵盖三个关键点:

1. x << y 中的未定义行为

代码示例(思考):

int x = 1;
int y = 32;
int z = x << y; //  UB if int is 32-bit

为什么这是 UB?

根据 C/C++ 标准:如果 移位位数 y ≥ 目标类型中位数(例如 32 位整型),则行为是 未定义的

例如:

int x = 1;
int y = 32; // 如果 int 是 32 位,y 等于或大于位宽,就会触发 UB。
int z = x << y; //  未定义行为

各种 CPU 行为不同:

  • x86/x86-64:移位位数自动 mod 32(只看低 5 位)
  • ARM:移位位数超过上限可能结果为 0
  • MIPS/SPARC:直接崩溃或返回垃圾值

为什么 C/C++ 标准选择 UB?

避免自动插入“移位位数合法性”检查,使编译器生成更高效的机器码。

如果语言强制修正 y:

x << (y % 32);

那么编译器必须始终插入 % 运算 ⇒ 额外一条指令,影响所有平台性能。

所以 C/C++ 说:“你保证合法,我就优化到底。”

C11 标准的 UB 统计

  • C11 标准附录 J 罗列了 199 种未定义行为
  • 但不是完整的列表(只是示例)
  • C++ 没有官方统计,因为:
    • 有些 UB 没有标成 “undefined behavior”
    • 版本演进会不断引入新的 UB

程序执行 UB 的三种可能后果

案例表现示例
Case 1程序立即崩溃段错误、浮点异常
Case 2程序继续运行但会晚点崩内存损坏、随机行为、数据破坏
Case 3程序表现正常但换编译器/选项后突然崩溃 ⇒ 定时炸弹

最危险的是 Case 3:

程序一切看起来都好,但你升级了 GCC 或开启了 -O3 优化,它就:

  • 删除了你的判断
  • 重排了顺序
  • 推理出你的代码“不可能到达”
  • 结果:崩溃或潜在漏洞

攻击者也能利用 UB

你以为 UB 没触发,但编译器生成了“假设某条件恒真”的代码,攻击者利用该路径(如类型越界、内存重解释),可能造成:

  • 越界写入
  • 代码注入
  • 破坏栈结构
  • 信息泄露

最佳实践建议

建议说明
使用标准库函数(如 std::bit_cast避免手动类型惩罚
保证移位合法:assert(y < 32)对于定宽整数,保护移位
不访问未初始化或非法地址即便在 x86 上“能跑”
编译时启用 -fsanitize=undefined检查未定义行为
使用 clang, gcc 的警告:-Wall -Wextra能发现很多潜在错误

总结一句话

未定义行为不是 bug,而是设计策略,用来换取性能。但对程序员来说,它是最危险的陷阱。

提供的内容围绕 C/C++ 中“未定义行为(Undefined Behavior, UB)”的现状、趋势、实际编译器表现和优化行为,讲解得非常深入。这些代码和文字都揭示了现代编译器在面对 UB 时的优化激进程度与潜在风险。下面我将逐条添加注释、解释与分析,帮助你全面理解这些案例。

总体趋势(过去 25 年)

• UB 检测工具不断进步(如早期的 Purify、后来的 Valgrind、Sanitizers)。
• 编译器变得越来越聪明,善于用 UB 优化生成的代码。
• UB 像“定时炸弹”,很多老代码看似运行正常,但在优化器更聪明后就出问题了。
• 安全性成为关键考量,UB 也成了攻击面之一。

开发者与编译器的“对话”

Q: 提高优化等级后程序坏了,怎么办?
A: 编译器作者:- 建议你读标准。- 建议你不要写 UB。- 祝你好运。

关键点:执行 UB 的代码是程序员的责任,编译器没有义务保证结果稳定或可预测。

我们的现实困境

• 旧代码充满 UB。
• 方案一:回头修所有代码(代价大)
• 方案二:让优化器“收敛”,别那么激进
• 方案三:继续让优化器激进(现实选择)

UB 示例分析合集(带注释)

1⃣ 溢出引发 UB

int foo (int x) {return (x + 1) > x;
}
int main() {cout << ((INT_MAX + 1) > INT_MAX) << "\n"; // UBcout << foo(INT_MAX) << "\n";              // 正常,常量折叠,返回 truereturn 0;
}
分析:
  • INT_MAX + 1 是有符号整型溢出 ⇒ UB
  • foo(INT_MAX)(x + 1) > x 在优化时可直接编译为 true常量推理成立
  • 实际输出:
    0   // 因为 +1 溢出,结果行为不确定
    1   // 优化器常量折叠
    

2⃣ Google Native Client 的移位漏洞

return addr & ~(uintptr_t)((1 << nap->align_boundary) - 1);
// 如果 align_boundary = 32,则是 1 << 32 ⇒ UB
分析:
  • C/C++ 中对 32 位整数执行 1 << 32UB
  • 优化器认为此表达式“无意义”,将其优化为 NOP
  • 整个安全检查被移除,沙箱失效 ⇒ 漏洞产生

3⃣ 使用未初始化指针

int *p = (int*)malloc(sizeof(int));
int *q = (int*)realloc(p, sizeof(int));
*p = 1;
*q = 2;
if (p == q)printf("%d %d\n", *p, *q);
分析:
  • 指针 prealloc 后可能失效。
  • 即便 p == q*p 仍未定义。
  • 实际表现:可能输出 1 2,但仍是 UB。

4⃣ 条件编译影响控制流

void foo(char *p) {
#ifdef DEBUGprintf("%s\n", p); // 如果 p 为 NULL,这里崩溃
#endifif (p) bar(p);
}
汇编对比分析:
  • -DDEBUG:编译器优化掉 if (p),直接跳过 bar() 调用。
  • -DDEBUG:必须输出字符串,所以保留 p 判断。
    ** UB 导致行为依赖宏定义!**

5⃣ memcpy + 空指针

void foo(int *p, int *q, size_t n) {memcpy(p, q, n);if (!q) abort(); // 但可能已经 dereference 了 null!
}
分析:
  • 即使 n == 0,调用 memcpy(p, q, 0) 也不合法 如果 q 是 NULL
  • 编译器可能优化掉判断 if (!q),直接调用 memcpy ⇒ 崩!

6⃣ 类型转换引发推断失误

int check(int *h, long *k) {*h = 5;*k = 6;return *h;
}
编译器优化:
movl $5, (%rdi)
movq $6, (%rsi)
movl $5, %eax
  • 因为 int 与 long 不可能 alias,优化器认为 *h 不受 *k 修改影响,直接返回 5

7⃣ 外部可见副作用顺序问题

void bar(); 
int foo(int z) {bar();return 100 % z;
}
如果 z == 0:
  • 整除 0 是运行时错误
  • 但编译器认为 bar() 没有 observable side effect ⇒ 调整执行顺序,导致程序提前崩溃或崩溃后不输出 “HELLO”

UB 可以“穿越时间”

• UB 可能使编译器提前优化“本来未来才触发的问题”
• 比如函数执行顺序、变量访问顺序
• 所以称之为:“UB 可以穿越时间”

总结建议

建议原因
永远不要依赖 UB它不可预测,后果可能随编译器/版本变化
-fsanitize=undefined 检查编译时检测大量 UB
尽可能用标准库、现代 C++(如 std::optionalbit_cast避免底层 hack
理解并使用 aliasing-safe 类型 punning 技术memcpystd::byte*
避免整数溢出、移位过界等位运算错误位操作优化器依赖很多假设

这部分内容深入探讨了 C/C++ 中未定义行为(Undefined Behavior, UB) 的“回溯性影响”、编译器优化行为、以及我们程序员能做些什么来应对 UB。以下我将逐段解析并添加代码注释与解释,帮助你全面理解。

UB Can Travel Back in Time!

int fermat() {const int MAX = 1000;int a = 1, b = 1, c = 1;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;
}
分析与注释:
  • 这段代码 构造性地尝试“反驳费马大定理”
  • 但实际上这是一个 无限循环,没有任何副作用(如打印、I/O、全局状态改动)。
  • 在 C++ 中,编译器允许优化掉不含副作用的无限循环

所以 Clang 在开启优化后可能移除整个 while(1) 循环,直接返回 1。输出变成:

Fermat's Last Theorem disproved!
结论:

UB 可以“向前传播”影响编译器对代码前面部分的重写或移除

为什么编译器能这样干?

因为 C++ 不像 C11 那样禁止移除常量控制的无限循环,所以如果没有可观察的副作用,编译器可以大胆优化。

  • C11 禁止移除 while(1) 类型常量控制表达式的循环(可见 n1528 提案)
  • C++ 没有这个限制,优化空间更大,但更危险

我们的处境

• 开发者必须理解并遵守 200+ 条 UB 规则
• 默认情况下没人告诉你哪里写错了
• 这是 bug 和漏洞滋生的土壤

开发者能做什么?(总结)

1⃣ 明确前置条件(Preconditions)

int foo(int x, int y) {return x << y; // y 必须满足 0 ≤ y < width(x)
}
  • y 超过位宽(如 x << 32)将导致 UB
  • 需要你在代码中 显式验证参数的合法性

2⃣ 静态分析(Static Analysis)

不需要运行程序也能发现潜在 UB
  • 不完备但实用工具(找部分 bug):
    • -Wall -Wextra -Werror 编译器警告
    • Clang Static Analyzer
    • Coverity、Klocwork、Cppcheck
  • 完备工具(特定类别保证无 bug):
    • Polyspace
    • TrustInSoft
    • Frama-C(偏 C)
建议:
  • 在 CI 中强制开启 -Werror
  • 将静态分析纳入代码审核流程
  • 对于核心库或关键模块使用 Polyspace/TrustInSoft

3⃣ 动态分析(运行时分析)

利用 Sanitizer 系列工具在运行时检测 UB:

工具功能
ASan地址空间错误(越界、use-after-free)
UBSan所有未定义行为(溢出、非法转换等)
MSan未初始化内存访问
TSan多线程数据竞争
TySan类型别名/强制转换检测(开发中)
示例编译方式:
clang++ -fsanitize=address -g your_code.cpp
clang++ -fsanitize=undefined -g your_code.cpp

4⃣ 代码审查强化:关注 UB

• 常见 UB 前置条件(应检查):- 除法除数 ≠ 0- 移位位数在合法范围内- 内存未释放后不要访问- 指针必须对齐
  • 代码审查时强制写出前置条件
  • 尤其要关注 循环内变量变化是否保证不越界

示例:编译器优化打破预期逻辑

int check(int *h, long *k) {*h = 5;*k = 6;return *h;
}
  • 编译器认为 int*long* 不可能 alias ⇒ 直接返回 5
  • 这在别名分析下是合法优化
  • 若你强制让 h 和 k 指向同一块内存,程序行为是 UB!

总结建议表

类别工具/建议
避免 UB不要写移位越界、访问未初始化、越界内存等代码
静态分析编译器警告、Coverity、Clang Static Analyzer
动态分析ASan、UBSan、TSan、MSan
编码习惯写函数时注释清楚 precondition,尤其是涉及位运算或指针
单元测试对可能触发 UB 的代码覆盖边界情况
编译选项-fsanitize-Werror-fno-strict-aliasing(慎用)
如果你有一个项目或代码库,我也可以帮你:
  • 检查出可能的 UB 区域
  • 自动加注释说明前置条件
  • 或者生成 clang-tidy 规则集来持续检测这些问题

动态分析的优缺点、未定义行为(UB)的缓解策略、当前面临的挑战以及对 C++ 开发者的建议。我帮你分点总结、解释和分析:

动态分析的优缺点

优点
  • 找出代码中“热门路径”的真实 bug
    动态分析通过运行时检测,能发现程序实际执行路径上的问题。
  • 通常没有误报(False positives)
    只要检测到错误,通常是真实存在的,而不是静态分析中常见的假阳性。
  • 一般不会因为细枝末节的问题烦死你
    动态工具倾向于报告真正严重或易复现的问题,而不会爆炸性地报告大量模糊警告。
缺点
  • 测试覆盖决定了发现的广度和深度
    动态分析只能检测程序运行过的路径,测试不到的代码分支就没法检测。
  • 彻底的测试极其困难
    需要足够的用例覆盖,模糊测试(fuzzing)虽有帮助,但并非万能。

UB 检测类型和工具覆盖

错误类型检测工具说明
使用无效指针ASan, Valgrind检测越界、使用后释放、重复释放
数组越界ASan, Valgrind访问数组边界之外的内存
严格别名规则违规UBSan违反 C++ 严格别名规则导致的 UB
整数溢出UBSan有符号整数溢出等
未定义的变量访问解释器等使用未初始化变量
变长参数错误UBSan可变参数传递错误
变量访问无序UBSan代码中未定义顺序的变量访问导致 UB

UB 缓解(Mitigation)

  • 禁用某些编译器优化,如 Linux 使用 -fno-delete-null-pointer-checks 防止空指针检查被移除。
  • MySQL 用 -fwrapv 来让有符号整型溢出行为定义为环绕(wrap-around),消除溢出 UB。
  • 禁用严格别名规则优化-fno-strict-aliasing 让别名相关的 UB 降低风险。
  • Android 在部分组件中使用 UBSan,但要提供替代运行时保证部署安全。
  • Chrome Linux 版启用了控制流完整性(CFI),由 LLVM 支持,增加安全保障且性能损耗极小。

UB 缓解面临的问题

  • 缺乏标准化、可移植的解决方案,不同平台不同编译器行为差异大。
  • 并发错误的缓解仍无良好方案,数据竞争、死锁等并非 UB 缓解重点。
  • 内存安全缓解昂贵且有时破坏程序行为,ASan 主要是调试工具,不适合生产。
  • UBSan 可配置为硬化工具,但这意味着在潜在漏洞处程序直接崩溃,需权衡稳定性和安全。

未来方向

针对所有 200 多种 C++ UB,必须做到:

  • 明确定义运行时行为(让 UB 成为有定义的行为)
  • 编译器能够检测到并产生致命错误
  • 提供可靠的运行时检测工具(sani zer)

当前状态总结

项目状态
内存边界检测生产级方案仍有挑战
严格别名规则仍需改进
非终止循环细节问题
未定义的副作用执行顺序(unsequenced)细节问题

给 C++ 开发者的建议

  1. 充分了解 UB 的含义和影响
    UB 不只是错误,还可能导致不可预期的优化和行为。
  2. 在代码审查时有意识地考虑 UB
    关注函数的前置条件,代码边界,别名规则等。
  3. 尽可能多地进行测试
    使用覆盖率工具保证测试广度,使用模糊测试扩展测试深度。
  4. 使用动态分析工具(sanitizers)和静态分析工具
    不断修复报告的 bug,建立健壮代码基础。
  5. 熟悉并愿意使用更“硬核”的检测工具
    如 Polyspace、TrustInSoft 这类严谨的验证工具。

相关文章:

  • Redis 持久化之 AOF 策略
  • (LeetCode 面试经典 150 题 ) 134. 加油站 (贪心)
  • RedisVL Schema 官方手册详读
  • 用户行为序列建模(篇六)-【阿里】DSIN
  • BF的数据结构题单-省选根号数据结构 - 题单 - 洛谷 计算机科学教育新生态
  • SQL Server从入门到项目实践(超值版)读书笔记 19
  • 03【C++ 入门基础】函数重载
  • 使用ros2服务实现人脸检测4-客户端(适合0基础小白)
  • 通达信【MACD趋势增强系统】幅图(含支撑压力位)
  • D-FiNE:在DETR模型中重新定义回归任务为精细粒度分布细化
  • MySQL数据库的增删改查
  • SpringCloud系列(41)--SpringCloud Config分布式配置中心简介
  • 模拟多维物理过程与基于云的数值分析-AI云计算数值分析和代码验证
  • CppCon 2017 学习:The Asynchronous C++ Parallel Programming Model
  • 在线之家官网入口 - 免费高清海外影视在线观看平台
  • STM32之28BYJ-48步进电机驱动
  • 思二勋:算法稳定币的发展在于生态场景、用户和资产的丰富性
  • 打造地基: App拉起基础小程序容器
  • 大事件项目记录12-文章管理接口开发-总
  • 现代 JavaScript (ES6+) 入门到实战(一):告别 var!拥抱 let 与 const,彻底搞懂作用域