linux下最轻量内存快照起点是用__malloc_hook拦截堆分配,需在main开头原子注册全部钩子、避免递归调用、加锁防多线程冲突,并结合backtrace记录调用栈。

用 malloc_hook 拦截所有堆分配(仅 Linux)
Linux 下最轻量的内存快照起点不是自己遍历堆,而是让系统在每次 malloc/free 时主动通知你。GNU libc 提供了 __malloc_hook 等钩子函数,虽然已标记为 deprecated,但在调试场景下依然可靠——它不依赖符号重写或 LD_PRELOAD,也不需要修改编译选项。
常见错误现象:直接赋值 __malloc_hook = my_malloc 导致程序崩溃。这是因为 glibc 内部会检查钩子函数是否由主线程首次设置,且要求同步修改 __free_hook 和 __realloc_hook,否则后续任意一次 free 都可能跳转到野指针。
- 必须在
main()开头、任何malloc调用前完成全部钩子注册 - 钩子函数内部禁止再调用
malloc/free(可用静态缓冲区或 mmap 分配内存) - 记录时保存地址、大小、调用栈(用
backtrace()+backtrace_symbols()) - 注意多线程:钩子是全局的,需加锁(如
pthread_mutex_t静态初始化)或改用 per-thread 记录 +__malloc_initialize_hook初始化
手动 dump 堆内存并比对(跨平台基础方案)
如果不能依赖 glibc 钩子(比如 macOS / Windows),或者想捕获栈/全局区变化,就得自己扫内存区域。核心思路是:获取当前进程的内存映射(/proc/self/maps on Linux, VirtualQueryEx on Windows),过滤出可读的堆段(如 [heap] 或 MEM_HEAP 标志),然后 mmap(Linux)或 VirtualAlloc(Windows)一份只读副本做快照。
容易踩的坑:直接 memcpy 整个 [heap] 区域会失败——该区域包含大量未分配页(PROT_NONE),访问触发 SIGSEGV。必须逐页 mincore()(Linux)或 VirtualQuery()(Windows)判断页是否已提交。
立即学习“C++免费学习笔记(深入)”;
- 快照粒度建议设为 4KB(一页),避免漏掉小对象变化
- 两次快照比对不用全量 memcmp:先用
std::unordered_map<uintptr_t std::vector>></uintptr_t>按页地址索引,只比对“已存在且内容不同”的页 - Windows 上记得用
PROCESS_QUERY_INFORMATION+PROCESS_VM_READ权限打开自身句柄 - macOS 需用
task_for_pid(仅调试签名二进制可用,发布版不可行)
std::set 存地址 + std::map 存内容的最小可行模型
不需要完整内存分析器?一个能标出“哪些地址被新分配/释放/改写”的简易对比就够用了。这时候别碰 valgrind 或 AddressSanitizer 的输出解析——它们太重。直接用两个容器管理快照:
- 第一次快照:
std::set<uintptr_t></uintptr_t>存所有活跃堆地址(来自钩子或扫描);std::map<uintptr_t std::vector>></uintptr_t>存每个地址起始处 16 字节(够看 magic number 或指针偏移) - 第二次快照后,用
set_difference找新增/消失地址;对交集地址,用vector的==判断内容是否变化 - 注意:地址比较必须用
uintptr_t,不能用void*直接比较(避免指针算术干扰) - 若要支持多次快照,把每次的
map存进std::vector,用索引当版本号,避免深拷贝大内存块
为什么不用 std::shared_ptr 或 RAII 自动追踪?
因为你要对比的是实际内存状态,不是 C++ 对象生命周期。RAII 管理的是逻辑资源,而内存快照要捕捉的是物理字节变化——比如 std::vector 的 capacity 扩容导致底层 new[] 分配新内存、旧内存尚未 free,此时 RAII 完全感知不到“旧地址还在”;又比如裸指针 int* p = (int*)malloc(100),RAII 根本不介入。
更隐蔽的问题:std::shared_ptr 的控制块本身也占堆内存,它的分配/释放会污染你的观测目标。除非你明确只想跟踪某几个类的实例,否则自动追踪只会让你更难看清真实内存行为。
真正关键的不是“怎么封装”,是“在哪一刻取快照”——必须在业务逻辑关键节点(比如网络包处理前后、渲染帧开始/结束)手动调用 take_snapshot(),而不是指望智能指针替你决定时机。










