45 / 80 · C++11 · 约 11 分钟
内存错误排查 Memory safety diagnostics
把内存错误拆成空间越界、时间失效、初始化缺失、释放协议错误和并发竞争,再选择对应证据。崩溃地点往往只是后果;应追踪第一次非法访问、分配与释放栈,并以明确边界和拥有型接口修复根因。
先分类再追踪第一次错误
空间错误是访问超出合法对象边界,时间错误是在构造前或销毁后使用对象,初始化错误是读取尚未取得有效值的状态。释放协议错误包括重复释放、释放栈地址和 new/delete[] 不匹配;数据竞争则可能让原本单线程正确的逻辑失效。
堆损坏可能到下一次分配才触发崩溃,因此最后一个 malloc 或容器操作不一定有错。保留能够复现的输入,优先阅读检测器报告的首次非法读写,以及相关分配和释放调用栈。后续报错可能只是同一破坏造成的连锁反应。
工具覆盖范围并不相同
ASan 擅长发现许多越界、释放后使用、重复释放和无效释放;UBSan 补充部分未定义行为检查;MemorySanitizer 面向未初始化值使用;线程竞争需要适合的并发检测工具。不要因一个 sanitizer 没报告,就推断其他类别也已经安全。
可从带调试信息的 ASan 构建开始,使用 -fsanitize=address -g -O1 -fno-omit-frame-pointer 编译与链接。未初始化问题要单独安排 MemorySanitizer 及其所需的依赖插桩环境。动态工具只覆盖执行路径,也受平台和库插桩支持影响,不是一份形式化正确性证明。
修复输入边界与所有权契约
示例用 vector::at 接受边界检查:合法下标读取成功,等于 size 的下标抛出 out_of_range,程序捕获并验证这一结果,而不是执行未定义的下标访问。这适合边界尚未由上层证明的路径;若采用无检查访问,必须让有效范围成为明确前置条件。
消除越界通常需要修正长度计算、循环终止条件或输入验证;消除时间错误通常需要调整拥有关系和借用范围。增加空指针检查无法修复非空悬空,扩大缓冲区也无法修复错误的长度协议。修复后应重新运行相同输入,并覆盖空集合与边界相邻值。
容易答错的地方
- 断言可能在特定构建模式下关闭,不能用 assert 代替处理不可信输入的运行时检查。
- 未定义行为不保证立即崩溃;某次输出看起来正确不构成可依赖的行为,也不适合写成固定预期输出。
运行一个例子
最低标准 C++11 · 完整程序 · 下载 .cpp
#include <cassert>
#include <stdexcept>
#include <vector>
int main() {
std::vector<int> values{4, 8, 12};
assert(values.at(2) == 12);
bool rejected = false;
try {
(void)values.at(values.size());
} catch (const std::out_of_range&) {
rejected = true;
}
assert(rejected);
assert(values.size() == 3 && values.at(0) == 4);
}
在本地编译
g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread memory-memory-errors.cpp -o example && ./example预期结果
预期:正常退出、无输出;所有 assert 通过。
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
报告指出一次 heap-use-after-free,访问指针非空。应先加空检查,还是追踪释放栈?给出修复方向。
查看参考答案
先追踪该对象的分配栈、释放栈和这次访问所在路径,确定哪个观察者活过了拥有者。空检查不能识别这类非空悬空。修复可把使用移到释放前、让异步任务持有必要的拥有型副本,或以 weak_ptr::lock 取得共享对象的临时所有权。随后运行原复现,确认失效路径不再发生。
继续查证
标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。