直接重载 operator new/delete 是起点,因所有 new/delete 均经此路径;需同时重载数组版本,禁用监控避免递归,用底层接口管理元数据,多线程加锁,指针作键索引记录,校验 nullptr,注意对齐与异常清理。

为什么直接重载 operator new 和 operator delete 是起点
因为所有 new / delete 行为都经过这两个函数,不侵入业务代码就能捕获分配点。全局重载时必须同时提供数组版本(operator new[] / operator delete[]),否则 new int[10] 会绕过监控。
关键点:重载函数里不能用 new 或 malloc 分配元数据(否则递归调用),得用 malloc 的底层接口(如 brk、mmap)或静态缓冲区。常见做法是预分配一块大内存,用链表管理已分配块的元信息。
- 每个分配记录需存:
ptr、size、file、line、stack trace(可选) - 用
__FILE__和__LINE__宏配合宏封装new,才能拿到调用位置 - 多线程下必须加锁(
std::mutex或原子操作),否则元数据链表可能被破坏
如何用指针记录堆块并避免双重释放检测失效
核心不是“用指针”,而是用指针作为哈希键或链表节点字段去索引元数据。比如定义结构体:
struct AllocRecord {
void* ptr;
size_t size;
const char* file;
int line;
AllocRecord* next;
};然后维护一个全局 AllocRecord* 链表头指针。
双重释放检测依赖每次 delete 时遍历该链表查找 ptr。但线性遍历慢,实际可用 std::unordered_map<void allocrecord></void> 加速——注意:std::unordered_map 自身分配内存会触发重载函数,必须在构造前禁用监控(如设标志位),否则死循环。
- 删除时先从 map 中 erase,再调用原始
free,顺序不能反 - 记录
ptr前要校验是否为 nullptr,否则delete nullptr会误报 - 对齐要求:若原分配地址被偏移(如在前面放 header),
delete传入的仍是原始ptr,但free必须传回真实内存起始地址
为什么 malloc_hook 在现代 glibc 中不可靠
__malloc_hook 等接口在 glibc 2.34+ 已被标记为废弃,且多线程下行为未定义。它无法捕获 C++ operator new(可能走不同路径),也不能区分 new 和 new[]。更严重的是,某些优化(如 small object cache)会让 hook 完全失效。
立即学习“C++免费学习笔记(深入)”;
替代方案只有两个:一是坚持重载 C++ 运算符(覆盖所有 new 场景),二是 LD_PRELOAD 拦截 malloc/free(但会漏掉 operator new 的非 malloc 实现)。混合使用两者最稳妥,但需注意重复计数——建议只监控 operator new,把 malloc 调用视为第三方库行为,单独统计。
- LD_PRELOAD 方案中,拦截函数里不能再调用
malloc,要用__libc_malloc绕过 hook - ASan(AddressSanitizer)虽能检测泄漏和越界,但运行时开销 >2x,不适合长期线上监控
- Linux
/proc/self/maps可读取当前进程内存映射,但无法关联到具体分配点
调试阶段如何快速验证指针记录逻辑是否生效
写个最小测试用例,强制触发分配和释放,然后检查全局记录链表或 map 是否增删正确。别依赖日志——日志本身可能分配内存。直接在 gdb 里打印:p *(AllocRecord*)record_head,或用 watch 监视链表头变化。
最容易被忽略的是异常路径:如果 new 抛异常(如 std::bad_alloc),对应 operator new 仍会被调用,但不会调用 operator delete(C++ 标准规定)。此时元数据必须在 operator new 中注册,在异常时主动清理,否则泄漏监控元数据本身。
- 在
operator new开头申请元数据失败,应直接抛异常,不要返回 nullptr - 用 RAII 封装监控开关(如
ScopedDisableMonitor),确保析构函数里恢复状态 - 监控工具自身内存占用要可控——例如限制最大记录条数,超限时转为采样模式
真正难的不是记录指针,是让记录过程不干扰原有行为、不引入新 bug、不拖垮性能。尤其当监控开启时,连 std::string 构造都可能触发几十次小分配,每处都要稳住。


















