一、编译期 & 运行时工具
1 AddressSanitizer(ASan)
原理
- 编译器插桩(Clang / GCC)
- 维护 Shadow Memory
- 退出时自动检测未释放内存
使用方法
bash
# GCC / Clang
-fsanitize=address -g -O1
示例输出
==12345==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 128 byte(s) in 1 object(s) allocated from:
#0 operator new
#1 foo()
优点
非常快
精确到行号
同时检测越界 / UAF
缺点
程序变慢 ~2x
不能和 MSVC 原生兼容
适合
Linux / macOS
CI / 日常开发
SLAM / 长时间运行程序
2 Valgrind(Memcheck)
原理
- 虚拟 CPU 执行
- 跟踪每一条内存读写
使用方法
bash
valgrind --leak-check=full ./your_program
示例输出
==12345== 32 bytes in 1 blocks are definitely lost
优点
非常全面
无需重新编译
缺点
极慢(10--50x)
不适合实时系统
适合
深度排查顽固泄漏
小规模程序
好,这一段已经是专业级速查表 了,我来在你这个基础上往下"深度展开" ,补齐工程实战里真正会踩的坑和高阶用法 ,尤其偏向 C++ / SLAM / 长时间运行系统。
二、ASan / LSan 实用细节
很多人"会用",但没用到位,导致要么漏报、要么误报。
1 ASan ≠ 只查越界,它自带 LSan(泄漏检测)
关键点
-fsanitize=address默认包含 LeakSanitizer- 只有程序正常退出时才会报告泄漏
abort()/SIGKILL/exit before main end可能看不到泄漏
正确情况
bash
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 ./your_program
常用 ASAN_OPTIONS(开发中常用)
bash
ASAN_OPTIONS=\
detect_leaks=1\
:check_initialization_order=1\
:strict_init_order=1\
:detect_stack_use_after_return=1
含义速解
| 选项 | 作用 |
|---|---|
| detect_leaks | 开启泄漏检测 |
| strict_init_order | 查全局对象初始化顺序 bug(SLAM 常见) |
| detect_stack_use_after_return | 查"函数返回后还用栈变量" |
| halt_on_error | 立刻中断,方便 CI |
SLAM 工程强烈建议全开
2 为什么 ASan 有时"查不到泄漏"?
这是个经典误区。
常见原因
1. 内存仍被全局对象持有
cpp
static std::vector<Foo*> g_cache;
- 程序退出时仍有指针引用
- LSan 认为这是"reachable"
- 不会报泄漏
解决方式:
bash
ASAN_OPTIONS=detect_leaks=1:report_reachable=1
2. 自定义 allocator / Eigen / TBB
- Eigen aligned alloc
- TBB scalable allocator
- CUDA / OpenCL pinned memory
可能被当成"外部库持有"
对策:
- 用 suppressions
- 或在 shutdown 阶段显式释放
3 ASan + O1 是黄金组合(不是 O0)
bash
-fsanitize=address -g -O1
原因
| 选项 | 影响 |
|---|---|
| -O0 | 栈布局怪异,误报多 |
| -O1 | 最推荐 |
| -O2 | 有时 inline 影响回溯 |
CI / 本地调试:统一 O1
4 ASan 在 SLAM 中的典型"秒杀场景"
shared_ptr 循环引用
后端线程提前退出
地图对象生命周期混乱
回调里捕获裸指针
全局静态对象顺序问题
提示 Valgrind 很慢才能定位,ASan 秒出
三、Valgrind 深度用法
Valgrind 的价值在于:
"你以为 ASan 查不到的,它能查到"
1 必须加的参数(否则信息不全)
bash
valgrind \
--leak-check=full \
--show-leak-kinds=all \
--track-origins=yes \
--num-callers=30 \
./your_program
关键解释
| 参数 | 作用 |
|---|---|
| track-origins | 查"未初始化内存来源" |
| num-callers | 深栈回溯(模板地狱必开) |
2 Valgrind 泄漏分类
definitely lost 真泄漏
indirectly lost 连带泄漏
possibly lost 多为指针算术
still reachable 通常可忽略
重点盯 definitely + indirectly
3 Valgrind 最适合干的三件事
- ASan 报不出的 未初始化读取
- STL / Eigen / boost 内部错误
- 小 demo 精确内存统计
不适合:
- 实时 SLAM
- 大地图
- 长时间跑
四、Sanitizer 组合
真正的工程不是"选一个",而是组合使用
ASan + UBSan(强烈推荐)
bash
-fsanitize=address,undefined
能抓到:
- 整数溢出
- 空指针解引用
- vptr 错误
- enum 越界
比单 ASan 强一个量级
2 TSAN(线程杀手)
bash
-fsanitize=thread
适合:
- 后端优化
- 多线程地图更新
- 回调竞态
不能和 ASan 同时用
分两个 build
五、实践推荐配置
Debug Sanitizer 构建
cmake
set(CMAKE_CXX_FLAGS_DEBUG
"-O1 -g -fsanitize=address,undefined -fno-omit-frame-pointer")
运行时
bash
ASAN_OPTIONS=detect_leaks=1:strict_init_order=1 ./slam_node
六、Eigen 泄漏 & "假泄漏"实战
1 Eigen 看起来在泄漏,其实不是
典型 Valgrind 输出
still reachable: 1,024 bytes in 8 blocks
根因
Eigen 内部有:
- 静态缓存(packet math)
- 对齐用的全局 allocator
- lazy 初始化
程序退出时不释放
结论
不是 bug
不要为了消灭它乱 free
判断标准
| 类型 | 处理 |
|---|---|
| still reachable | 忽略 |
| definitely lost | 必查 |
| indirectly lost | 八成你自己的锅 |
2 真·Eigen 泄漏:new + Matrix 裸指针
错误写法(后端优化里常见)
cpp
Eigen::Matrix<double, 6, 6>* H =
new Eigen::Matrix<double, 6, 6>();
- 优化异常 return
- 多路径 exit
- 忘了 delete
ASan 报告
Direct leak of 288 byte(s)
正确写法
cpp
Eigen::Matrix<double, 6, 6> H;
或
cpp
auto H = std::make_unique<Eigen::Matrix<double,6,6>>();
Eigen 对象 99% 不该手动 new
3 Eigen 对齐导致的"free 崩溃"
症状
- ASan 报
alloc-dealloc-mismatch - 只在 Release / AVX 下崩
错误写法
cpp
void* p = malloc(sizeof(Eigen::Vector3d));
auto v = new (p) Eigen::Vector3d();
free(p); //
正解
cpp
Eigen::aligned_allocator<Eigen::Vector3d>
或干脆别玩 placement new
七、Sophus:泄漏几乎都不是 Sophus 的锅
99% 来自 "值类型被误当指针类型"
1 Sophus 对象被 new
错误示例
cpp
Sophus::SE3d* pose = new Sophus::SE3d();
ASan
Direct leak of 48 byte(s)
正确方式
cpp
Sophus::SE3d pose;
或
cpp
std::unique_ptr<Sophus::SE3d>
Sophus 设计就是 轻量值语义
2 Sophus + std::vector + 对齐陷阱
症状
- Release 下偶现崩溃
- ASan 报 heap-buffer-overflow
错误写法
cpp
std::vector<Sophus::SE3d> poses;
(在老 Eigen / 开 AVX)
正解
cpp
std::vector<Sophus::SE3d,
Eigen::aligned_allocator<Sophus::SE3d>> poses;
或(Eigen ≥ 3.4)
cpp
#define EIGEN_DONT_ALIGN_STATICALLY
3 Sophus"泄漏"但其实是 shared_ptr 环
经典结构
cpp
struct Frame {
std::shared_ptr<MapPoint> mp;
};
struct MapPoint {
std::shared_ptr<Frame> ref;
};
ASan:
Indirect leak of xxx bytes
正解
cpp
std::weak_ptr<Frame> ref;
这类是 SLAM 最大的真实泄漏来源
八、g2o:泄漏重灾区
g2o 本身大量裸指针 + 工厂模式
1 忘记释放 SparseOptimizer
错误模式
cpp
auto optimizer = new g2o::SparseOptimizer();
// addVertex / addEdge
return; // 💀
ASan
Indirect leak of 100k+ bytes
正解
cpp
g2o::SparseOptimizer optimizer;
栈对象,自动析构
2 Edge / Vertex 是谁来删?
易错点
cpp
optimizer.addVertex(v);
optimizer.addEdge(e);
optimizer 析构时才 delete
如果中途 new / delete optimizer:
- 边 / 点可能残留
- 产生间接泄漏
生命周期要包住整个 BA
3 Factory 注册导致的"全局泄漏"
Valgrind 报
still reachable: g2o::Factory
这是啥?
- g2o 插件注册
- 静态 map
- 进程级对象
可以忽略
不要试图手动清理
4 自定义 Edge / Vertex 忘了虚析构
症状
- ASan 报小块 definitely lost
- 只在 delete base pointer 时出现
错误
cpp
struct MyEdge : g2o::BaseUnaryEdge<...> {
~MyEdge() {} // 非虚
};
正确
cpp
virtual ~MyEdge() = default;
九、组合案例:SLAM 后端典型泄漏链
真实场景
Map
└─ shared_ptr<Frame>
└─ shared_ptr<g2o::VertexSE3>
└─ optimizer
optimizer 先析构
Frame 还活着
Vertex 永远删不掉
解决原则(铁律)
g2o 对象只属于 optimizer
- Frame 不持有 g2o 指针
- 只保存 ID / index
- BA 完全结束后整体销毁
十、总结
ASan = 日常开发主力
Valgrind = 最后一根银针
TSAN = 并发问题唯一解