
简介dmalloc-5.5.2.tgz 是开源内存调试与分配库 dmalloc 的 5.5.2 版本源码包面向从事 C/C 开发、需要排查内存问题的程序员与系统调试人员。它可帮助定位内存泄漏、越界访问、重复释放等难以察觉的缺陷适用于大型复杂程序的质量保障与性能优化。压缩包共 69 个文件约 651KB以 18 个 h 头文件与 15 个 c 源文件为主体另含 configure 与 Makefile.in 等构建脚本、readme/install 等安装说明、texi 与 pdf/html 文档、示例程序及多平台适配笔记结构完整便于编译定制。目前已有 147 人学习下载。通过该源码包读者可获得可自行编译的完整库实现、configure 自动检测环境并生成 Makefile 的构建流程以及内存统计、调试日志、多线程支持与自定义选项等核心能力为深入理解内存管理机制、积累排错思路提供实用参考。1. dmalloc-5.5.2.tgz一个被遗忘的内存调试利器为什么现在仍值得翻出来用如果你在 Linux 下写过 C 程序大概率经历过这种场景程序跑着跑着段错误了core dump 一看栈是 malloc 内部或者服务跑了一周RSS 缓慢上涨重启就好但就是找不到哪里漏了。Valgrind 太重ASan 要重新编译整个工程而 dmalloc-5.5.2.tgz 这个只有几百 KB 的压缩包恰好卡在一个很舒服的位置——它用链接期替换的方式接管 malloc/free运行时通过环境变量控制检查级别几乎不需要改代码就能定位越界写、重复释放、内存泄漏和未初始化使用。它不新5.5.2 是它最后一个稳定发布版本但正因为足够老它在嵌入式交叉编译、老旧构建系统、不能随便换编译器的场景里反而比现代工具更好落地。这篇笔记面向的是需要在一线排查 C 内存问题、又不想大动干戈的工程师从解压配置一路讲到怎么把日志缩到能看。2. 把 dmalloc-5.5.2.tgz 跑起来从解压到第一次抓到越界写2.1 解压与配置三个必须显式指定的选项拿到 dmalloc-5.5.2.tgz 之后不要急着./configure make。dmalloc 的默认配置是给「轻量检查」用的真正要抓越界和泄漏需要在 configure 阶段就把重检查打开。我一般会建一个独立的构建目录避免污染源码树。tar -xzf dmalloc-5.5.2.tgz cd dmalloc-5.5.2 mkdir build cd build ../configure \ --prefix/opt/dmalloc \ --enable-threads \ --enable-shlib \ --enable-cxx make make install--enable-threads让 dmalloc 在多线程程序里也能正确记录每个线程的分配归属不加这个选项pthread 程序里日志会串。--enable-shlib生成动态库版本方便用LD_PRELOAD做无侵入注入--enable-cxx则补上new/delete的包装纯 C 项目可以不加。安装完在/opt/dmalloc下会有include/dmalloc.h、lib/libdmalloc.a和bin/dmalloc这个日志分析工具。2.2 两种接入方式链接期替换和 LD_PRELOAD 注入接入 dmalloc 有两条路选哪条取决于你能不能改构建脚本。第一种是链接期替换适合自己掌控 Makefile 的项目。在编译每个源文件时加上-DDMALLOC链接时把-ldmalloc放在所有目标文件之后gcc -DDMALLOC -I/opt/dmalloc/include -c main.c -o main.o gcc -DDMALLOC -I/opt/dmalloc/include -c worker.c -o worker.o gcc main.o worker.o -L/opt/dmalloc/lib -ldmalloc -o app-DDMALLOC的作用是让dmalloc.h里的宏把malloc重定向到dmalloc_malloc如果不加这个宏头文件不会做替换链接了库也没用。链接顺序上-ldmalloc必须靠后否则符号解析会先命中 libc 的 malloc。第二种是LD_PRELOAD适合不能重新编译、只有二进制的情况LD_PRELOAD/opt/dmalloc/lib/libdmalloc.so \ DMALLOC_OPTIONSdebug0x4a4403,log/tmp/dmalloc.log \ ./app这种方式不需要改任何代码但要求 dmalloc 编译时开了--enable-shlib。注意LD_PRELOAD对静态链接的程序无效对 setuid 程序也会被系统忽略这是内核的安全策略不是 dmalloc 的问题。2.3 第一次运行用 debug 位掩码控制检查强度dmalloc 的核心控制手段是DMALLOC_OPTIONS环境变量其中debug是一个十六进制位掩码。新手最容易翻车的地方就是直接抄一个巨大的掩码结果日志几百 MB 根本没法看。我建议第一次只用下面这个组合export DMALLOC_OPTIONSdebug0x4000c5,log/tmp/dmalloc.log,inter100 ./app0x4000c5拆开看0x1是记录每次分配0x4是检查越界写0x40是检查重复释放0x400000是启动时打印配置摘要。inter100表示每 100 次分配做一次完整性校验这个值太小会让程序慢十倍以上太大又可能错过现场100 是个折中。跑完之后用 dmalloc 自带的工具把日志转成可读格式/opt/dmalloc/bin/dmalloc /tmp/dmalloc.log它会按「未释放块」「越界写」「重复释放」分类汇总并给出分配时的调用栈。如果日志里出现ra0x...这样的返回地址用addr2line -e app -f -C 0x...就能定位到源码行。3. 读懂 dmalloc 日志从十六进制掩码到具体泄漏点3.1 debug 掩码的常用组合与代价dmalloc 的 debug 掩码有二十多个位全开会让程序慢到无法接受。下面这张表是我在实际排查中总结出的几组常用配置按「查什么」来选而不是按「开多少」来选。排查目标debug 掩码说明只查内存泄漏0x1记录分配退出时报告未释放块开销最小查越界写0x1 | 0x4 0x5在每个块尾部加哨兵字节写越界会被发现查重复释放0x1 | 0x40 0x41维护已释放块表二次 free 立即报错查未初始化使用0x1 | 0x2 0x3分配时填充固定模式读取未写区域会触发校验综合排查0x4000c5上面几项加上启动摘要日常够用掩码每多一位dmalloc 就要在每次 malloc/free 时多做一次检查0x5相比0x1大约慢 2 到 3 倍0x4000c5在分配密集的程序里可能慢 10 倍。所以正确做法是先用小掩码确认有没有问题再逐步加位缩小范围而不是一上来全开。3.2 用 dmalloc 工具把日志变成可操作的线索原始日志是逐条记录的直接看很痛苦。dmalloc工具支持按类型过滤# 只看未释放的内存块 /opt/dmalloc/bin/dmalloc -u /tmp/dmalloc.log # 只看越界写 /opt/dmalloc/bin/dmalloc -b /tmp/dmalloc.log # 统计各调用点的分配次数和总字节数 /opt/dmalloc/bin/dmalloc -s /tmp/dmalloc.log-s这个统计视图特别有用。它会按分配时的返回地址聚合列出「哪个调用点分配了多少块、总共多少字节、有多少没释放」。如果某个调用点分配了 10000 次但只释放了 9990 次那 10 次差额就是泄漏源。拿到返回地址后addr2line -e ./app -f -C 0x4012a3输出会直接给出文件名和行号。如果程序编译时带了-g还能看到函数名。注意 dmalloc 记录的地址是运行时地址如果程序开了 ASLR每次运行地址会变但 addr2line 用的是相对偏移所以只要二进制没变同一个调用点的地址是稳定的。3.3 在自动化测试里嵌入 dmalloc 检查手动跑一次只能抓一次把 dmalloc 接进 CI 或 nightly 测试才能持续发现回归。我一般会在测试脚本里这样写#!/bin/bash export DMALLOC_OPTIONSdebug0x1,log/tmp/dmalloc_$$.log ./run_tests RET$? if [ -s /tmp/dmalloc_$$.log ]; then LEAKS$(/opt/dmalloc/bin/dmalloc -u /tmp/dmalloc_$$.log | grep -c not freed) if [ $LEAKS -gt 0 ]; then echo 发现 $LEAKS 处未释放内存 /opt/dmalloc/bin/dmalloc -u /tmp/dmalloc_$$.log exit 1 fi fi exit $RET这里用debug0x1而不是全掩码是因为 CI 里跑的是功能测试主要防泄漏回归越界检查交给专门的单元测试。log文件名带上进程号避免并行测试互相覆盖。退出码的处理要注意dmalloc 本身不会改变程序退出码所以泄漏检测要自己判断日志内容。4. 避坑dmalloc 用错比不用更浪费时间4.1 现象程序启动就报一堆「invalid pointer」但代码没改过原因通常是链接顺序错了。如果-ldmalloc出现在目标文件之前或者同时链接了静态库和动态库符号解析会混乱dmalloc 的包装函数和 libc 的 malloc 会交替生效导致 dmalloc 拿到的是 libc 分配的指针自然认不出来。解决方法是确保-ldmalloc在链接命令的最后并且只链接一种版本。用ldd ./app | grep dmalloc确认动态库被正确加载。4.2 现象日志文件巨大程序跑几分钟就几十 GB这是inter设得太小加上debug掩码开太多导致的。inter1意味着每次分配都做全量校验分配密集的程序每秒可能产生上万条日志。解决方法是把inter调到 100 到 1000 之间并且用log指定一个独立文件而不是输出到 stderr。如果已经产生了大文件不要用编辑器打开用dmalloc工具直接分析或者head -c 1000000截取前 1MB 看格式。4.3 现象多线程程序里日志显示「分配线程」和「释放线程」不一致dmalloc 默认不记录线程信息需要编译时加--enable-threads运行时还要在DMALLOC_OPTIONS里加debug的0x8000位。如果没开这个位dmalloc 只记录全局的分配释放跨线程传递指针时看起来就像「在 A 线程分配、在 B 线程释放」这本身不是错误但会干扰判断。开了线程支持后日志里会多一列线程 ID能清楚看到指针的流转路径。4.4 现象用 LD_PRELOAD 注入后程序行为异常甚至崩溃LD_PRELOAD对某些程序不适用静态链接的程序不会加载动态库setuid 程序会忽略LD_PRELOAD有些程序自己实现了 malloc 池dmalloc 的包装会和它冲突。另外如果程序在main之前就有分配比如 C 全局对象的构造函数dmalloc 的初始化可能还没完成这时分配会走 libc后续释放走 dmalloc导致误报。解决方法是尽量用链接期替换或者在LD_PRELOAD场景下先用一个最小程序验证 dmalloc 能正常工作。4.5 现象dmalloc 报告泄漏但代码里明明 free 了常见原因是 free 之后指针被重新赋值或者 free 的是指针的副本。dmalloc 记录的是分配时的返回地址和块地址如果free的地址和malloc返回的地址不一致比如指针被偏移过dmalloc 会认为这块没释放。另一种情况是程序用了reallocdmalloc 对realloc的处理是「释放旧块、分配新块」如果旧块地址和新块地址相同日志里会看起来像没释放。用dmalloc -u看未释放块的地址再和代码里的free调用对比通常能发现是地址不匹配。5. 进阶把 dmalloc 日志压缩成可对比的基线以及一个我常用的技巧dmalloc 最被低估的能力是「可对比」。单次运行的日志只能告诉你「现在有没有问题」但把两次运行的统计视图做 diff就能看出「这次改动引入了哪些新分配点」。我一般会在代码 review 前后各跑一次测试用dmalloc -s导出统计然后按调用点排序对比。# 基线 DMALLOC_OPTIONSdebug0x1,log/tmp/base.log ./run_tests /opt/dmalloc/bin/dmalloc -s /tmp/base.log | sort /tmp/base.stat # 改动后 DMALLOC_OPTIONSdebug0x1,log/tmp/new.log ./run_tests /opt/dmalloc/bin/dmalloc -s /tmp/new.log | sort /tmp/new.stat # 只看新增的分配点 comm -13 /tmp/base.stat /tmp/new.statcomm -13会输出只在 new 里出现的行也就是这次改动新增的分配调用点。如果这些新增点没有对应的释放基本可以锁定是新引入的泄漏。这个方法的代价是每次要跑两遍测试但对于核心模块的回归测试是值得的。另一个技巧是结合dmalloc的malloc_limit选项做「分配上限告警」。在DMALLOC_OPTIONS里加malloc_limit10485760当程序累计分配超过 10MB 时 dmalloc 会打印一条警告。这不是硬限制程序不会因此失败但能在日志里留下一个时间戳帮你判断内存是在哪个阶段涨上去的。对于长时间运行的服务这个选项比事后分析 core dump 更早发现问题。最后说一个我踩过的坑dmalloc 的日志分析工具默认按地址排序但不同运行之间地址会变所以做 diff 之前一定要用-s的统计视图它按调用点聚合地址变化不影响。如果直接 diff 原始日志你会看到满屏的差异实际上什么都没变。这个习惯我保持了几年现在每次改完内存相关的代码都会先跑一遍基线对比再决定要不要深入看某一条日志。希望帮到你。本文还有配套的精品资源点击获取