运行时拦截 malloc/new 调用并记录调用栈、大小、地址等信息,Linux 用 LD_PRELOAD 重定义内存函数,Windows 用 Detour 或重载 operator new,配合文件行号宏和元数据头,退出前安全 dump 泄漏摘要。

用 malloc / new 拦截 + 全局钩子就能拿到分配现场
核心思路不是靠事后扫描内存,而是运行时拦截所有堆分配调用,记录调用栈、大小、地址。C++ 没有标准 API 做这个,但 Linux 下可改写 __libc_malloc、__libc_free,Windows 下用 DetourAttach 或 MSVC 的 _malloc_hook(注意:后者在较新 CRT 中已弃用)。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- Linux 优先用
LD_PRELOAD注入共享库,重定义malloc/free/realloc/calloc,内部调用__libc_malloc等真实函数,避免递归 - Windows 下若用 MSVC,
_malloc_hook仅支持 debug 版 CRT,且不捕获new——必须同时替换全局operator new和operator delete - 记录调用栈别依赖
backtrace()(信号安全问题),改用libunwind或 Windows 的CaptureStackBackTrace - 每个分配记录至少存:
ptr、size、file、line、stack_hash(避免重复打印同一泄漏点)
如何让 operator new 自动带文件/行号信息?
标准 operator new 不传位置信息,但 C++17 起支持 operator new(std::size_t, std::source_location),不过主流编译器还没完全落地。更稳的方案是宏替换 + 重载带参版本。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 定义宏:
#define NEW new (__FILE__, __LINE__),再声明void* operator new(std::size_t, const char*, int) - 该重载函数里调用
malloc,并把文件/行号存入分配头(比如在ptr前面放 16 字节元数据) - 注意:类内自定义
operator new会屏蔽全局重载,所以必须确保所有类没声明自己的new,或统一继承一个基类来接管 - 别忘了配对实现
operator delete(void*, const char*, int),否则delete时无法定位元数据
程序退出时 dump 未释放块,但 atexit 太晚了
atexit 注册的函数在静态析构之后执行,此时部分全局对象(比如日志模块)可能已销毁,导致写文件失败或崩溃。真正安全的时机是 main 返回后、CRT 清理前。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- Linux 下用
__cxa_atexit注册函数,并指定一个比默认优先级更高的序列值(如 -1),让它早于静态析构执行 - Windows 下用
onexit(仅 MSVC)或直接 hookexit函数,但需小心绕过 CRT 的双重检查 - dump 内容别写磁盘——用
write(2)直接写 stderr 或临时 fd,避免触发std::ofstream构造 - 只打印泄漏块的
stack_hash和总字节数,按哈希聚合,否则上万次分配会输出几 MB 无意义日志
为什么 valgrind 不够用,还得自己写?
valgrind 是重量级动态二进制插桩,启动慢、性能降 20–50 倍,且不支持某些内联汇编或硬件加速路径;更重要的是它看不到 C++ 对象语义(比如某 std::vector 的 capacity 是否被反复 realloc 却未 shrink_to_fit)。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 自研工具要轻量:单个共享库注入,运行时开销控制在 5% 以内(用 per-CPU 缓存分配记录,避免锁)
- 加白名单机制:
std::string内部缓存、std::shared_ptr控制块这类“合法长期持有”内存要过滤,否则误报爆炸 - 支持运行时开关:
setenv("LEAK_CHECK=0", 1, 1)可在关键路径关闭检测,而不是重新编译 - 最易忽略的一点:多线程下
pthread_atfork必须注册 fork 后的清理逻辑,否则子进程会继承父进程的分配表并误报泄漏
真正难的不是记下每次 malloc,而是判断“什么时候算漏”。这取决于你的业务语义——比如网络模块里一个连接对象存活期间申请的 buffer,连接 close 后没释放才算漏。工具只能给原始数据,判定逻辑得你写。









