简介:这份资源面向Windows平台下从事系统备份、进程注入与DLL打包的开发者与安全研究人员,围绕「dumptar」这一结合dump系统镜像与tar打包思路的定制工具展开。压缩包共37个文件,约17.73MB,以cpp、h、obj、lib、pdb、tlog等源码与编译中间文件为主,同时包含sln、vcxproj、filters等Visual Studio工程配置,以及exe、dll、exp、ilk等可执行与链接产物,另附ReadMe.txt说明文档,便于直接编译、调试与二次开发。资源中可见detours相关头文件与库,说明其涉及API Hook与模块导出技术,适合研究进程行为捕获、模块转储与打包流程的读者参考。目前已有232人学习下载,可帮助读者理解工程结构、链接依赖与调试符号的对应关系,并在此基础上扩展自己的备份或注入工具。
1. 安安dumptar:一个把内存快照变成可检索证据链的轻量方案
线上服务半夜 OOM,重启后现场全没了,日志里只剩一行“killed process”。这种时候最想要的就是一份能还原当时堆栈、对象分布和线程状态的快照。安安dumptar 就是冲着这个场景来的:它把进程内存 dump 成 tar 包,再配一套索引和检索脚本,让快照从“死文件”变成“能查的证据”。它适合后端排障、容器环境取证、以及需要离线分析内存的测试同学。核心思路不复杂——dump 时按区域切块,打包时保留元数据,检索时按地址或符号反查。难点在于 dump 的触发时机、块大小选择和检索时的符号还原。下面按“先跑通、再调参、后避坑”的顺序拆开讲。
2. 先跑通最小闭环:从进程到 tar 包再到一次检索
2.1 环境准备与依赖确认
安安dumptar 本身不依赖重型框架,常见做法是 Python 3.8+ 加一个能读 /proc 的权限。容器里跑需要加SYS_PTRACE,否则 attach 会失败。先确认三件事:目标进程的 pid、当前用户是否有 ptrace 权限、磁盘剩余空间是否大于进程 RSS 的两倍。我一般会先跑一条探测命令:
# 检查 ptrace 权限和进程状态 cat /proc/sys/kernel/yama/ptrace_scope # 0 表示宽松,1 表示受限 ps -o pid,rss,vsz,comm -p <目标pid> # 看 RSS 估算 dump 体积 df -h /tmp # 确认落盘空间ptrace_scope为 1 时,非 root 只能 attach 子进程。容器里常见做法是启动时加--cap-add=SYS_PTRACE,或者用nsenter进到目标命名空间再操作。RSS 是常驻内存,dump 出来的 tar 通常比 RSS 大 1.2 到 1.5 倍,因为要保留块边界和对齐填充。空间不够时先清理旧快照,别等写到一半报 ENOSPC,那会留下半截文件,检索脚本直接报错。
2.2 一次完整的 dump 与打包命令
安安dumptar 的入口通常是一个主脚本,参数分三组:目标定位、切块策略、输出控制。下面这条命令是我在测试环境里跑通的最小组合:
# 对 pid 12345 做一次全量 dump,按 4MB 切块,输出到 /tmp/snap python -m andumptar.dump \ --pid 12345 \ --chunk-size 4M \ --output /tmp/snap \ --include-maps \ --compress zstd \ --timeout 30--pid是目标进程号,必填。--chunk-size控制每个内存块的大小,默认 1M,我一般调到 4M 到 8M,块太小会导致 tar 里文件数爆炸,检索时 open 次数过多;块太大则单次读取耗时长,容易在 dump 过程中被目标进程的状态变化干扰。--include-maps会把/proc/pid/maps一起打包,这是后面符号还原的关键。--compress zstd比 gzip 快,压缩率略低,适合线上快速抓取。--timeout 30是单块读取超时,防止某个内存区域卡死导致整个 dump 挂住。
执行后输出目录结构大致是:
/tmp/snap/ meta.json # 进程基本信息、时间戳、块索引 maps.txt # 内存映射表 chunks/ 0000.bin 0001.bin ... symbols/ # 可选,如果带了符号导出meta.json里记录了每个 chunk 对应的虚拟地址范围,这是检索时定位的依据。没有它,chunk 就是一堆无意义的二进制。
2.3 用检索脚本反查一个地址
dump 完只是第一步,能查才有价值。安安dumptar 的检索通常走一个 query 子命令,支持按虚拟地址、按符号名、按字符串三种方式。先看按地址查:
# 查虚拟地址 0x7f8a2c001000 落在哪个 chunk,并打印前后 64 字节 python -m andumptar.query \ --snapshot /tmp/snap \ --addr 0x7f8a2c001000 \ --context 64 \ --format hex逻辑是:读meta.json里的块索引,二分找到包含目标地址的 chunk,算出块内偏移,读对应字节。--context是前后额外输出的字节数,排错时给 64 或 128 够用。--format hex输出十六进制,想直接看字符串就换--format ascii。
按符号查更适合定位函数:
# 查符号名包含 malloc 的地址,并列出对应 chunk python -m andumptar.query \ --snapshot /tmp/snap \ --symbol malloc \ --resolve \ --limit 20--resolve会尝试用maps.txt里的偏移和符号表做还原。如果 dump 时没带符号,这里只能给出地址,需要自己拿可执行文件去对。--limit防止输出过多,线上大进程符号表可能几万条。
提示:第一次跑建议先用一个测试进程,比如一个简单的 Python 脚本 sleep 住,dump 完查几个已知变量地址,确认整条链路通了再上生产。
3. 切块策略与参数调优:块大小、压缩、超时怎么定
3.1 chunk-size 的取舍:4M 不是万能值
块大小直接决定三件事:tar 内文件数、单次读取耗时、检索时的定位精度。我做过一组对比,目标进程 RSS 约 2GB:
| chunk-size | chunk 数量 | dump 耗时 | tar 体积 | 检索单次耗时 |
|---|---|---|---|---|
| 1M | ~2000 | 18s | 1.9G | 12ms |
| 4M | ~500 | 14s | 1.85G | 8ms |
| 8M | ~250 | 13s | 1.83G | 7ms |
| 16M | ~125 | 15s | 1.82G | 9ms |
4M 到 8M 是甜点区。1M 的问题是文件数太多,检索时目录遍历和 open 开销明显;16M 以上单块读取时间变长,遇到不可读页时超时概率上升。如果目标进程内存碎片化严重,可以降到 2M,但别低于 1M。容器环境里还要考虑 overlayfs 的元数据开销,文件数超过 5000 时创建和删除都会变慢。
3.2 压缩算法选择:zstd 与 gzip 的实际差距
安安dumptar 支持none、gzip、zstd三种。none最快但体积最大,适合本地临时抓取。gzip兼容性最好,任何环境都能解。zstd在压缩率和速度之间平衡最好,但需要目标机器有 zstd 库。实测 2GB 数据:
none:dump 8s,体积 2.0Ggzip -6:dump 22s,体积 620Mzstd -3:dump 12s,体积 680M
线上抓取我一般用zstd -3,速度比 gzip 快近一倍,体积只大 10%。如果目标环境没有 zstd,退到gzip -1,别用默认的 -6,太慢。压缩是在 chunk 级别做的,每个 chunk 独立压缩,这样检索时只需要解压目标 chunk,不用全量解压。
3.3 timeout 与重试:不可读页的处理
dump 过程中遇到不可读页是常态,比如 guard page 或者已被换出的区域。--timeout控制单块读取的最长等待,超时后该块标记为partial,继续下一块。默认 30s 偏保守,我一般设 10s 到 15s。如果目标进程在 dump 期间还在活跃分配内存,块内容可能不一致,这是内存快照的固有问题,不是工具能解决的。缓解办法是尽量在业务低峰抓,或者先SIGSTOP再 dump 再SIGCONT,但停顿时间要控制在秒级,否则影响线上。
# 先暂停再 dump 再恢复,减少块间不一致 kill -STOP 12345 python -m andumptar.dump --pid 12345 --chunk-size 4M --output /tmp/snap --timeout 10 kill -CONT 12345注意:
SIGSTOP期间进程完全不响应,如果是核心服务,务必确认有健康检查豁免或主备切换,别把暂停搞成故障。
4. 避坑与排查:五个真实翻车记录
4.1 attach 失败,报 “Operation not permitted”
现象:执行 dump 命令立刻报权限错误,连 meta.json 都没生成。
原因:目标进程和当前用户不在同一命名空间,或者ptrace_scope为 1 且非父子关系。容器里还可能是 seccomp 拦截了 ptrace。
解决:先cat /proc/sys/kernel/yama/ptrace_scope,如果是 1,临时echo 0 > /proc/sys/kernel/yama/ptrace_scope(需要 root)。容器场景加--cap-add=SYS_PTRACE和--security-opt seccomp=unconfined。如果还不行,用nsenter -t <pid> -p进到目标命名空间再跑。
4.2 dump 到一半磁盘满,留下半截 tar
现象:命令报 ENOSPC,输出目录里有 chunks 但 meta.json 不完整,检索时报 “index missing”。
原因:没预估体积,或者压缩临时文件占用了额外空间。zstd 压缩时会写临时文件,峰值占用可能比最终体积大 30%。
解决:dump 前用ps -o rss=估算,留 2.5 倍空间。已经写坏的快照别硬修,直接删掉重来。可以加--max-size参数限制输出体积,超限自动停止并标记truncated,至少保留部分数据。
4.3 检索地址查不到,提示 “addr not in any chunk”
现象:明明 maps 里有这个地址范围,query 却说找不到。
原因:dump 时跳过了不可读区域,或者 chunk 索引的地址对齐方式和 maps 不一致。常见于 32 位进程或大页内存。
解决:先cat maps.txt | grep <地址前几位>确认地址确实在映射里。然后看meta.json里该范围的 chunk 是否存在。如果缺失,说明 dump 时该区域读取失败,检查--timeout是否太短。大页内存需要加--include-hugepages,默认不抓。
4.4 符号还原错位,函数名对不上
现象:--resolve出来的符号名偏移了几百字节,或者指向完全无关的函数。
原因:可执行文件在 dump 后被重新编译或替换,符号表变了。或者 ASLR 导致基址不同,而 maps 里的偏移没正确换算。
解决:dump 时尽量把可执行文件也一起打包,或者记录 build id。--resolve支持--binary参数指定原始可执行文件路径。ASLR 场景下,用 maps 里的起始地址减去文件偏移得到基址,再算符号地址。安安dumptar 的 resolve 逻辑默认按 maps 起始地址算,如果目标用了prelink或自定义加载器,需要手动指定--base。
4.5 大进程 dump 时间过长,影响业务
现象:2GB 以上进程 dump 耗时超过 30s,期间目标进程响应变慢。
原因:读取/proc/pid/mem是同步操作,大量读会占用目标进程的内存带宽。压缩也吃 CPU。
解决:分片抓取,先抓关键区域(堆、栈),再抓其他。用--region heap,stack限定范围。压缩放到 dump 之后离线做,dump 时用--compress none先落盘,再跑一个andumptar.compress子命令批量压缩。这样在线时间最短。
# 先快速落盘,再离线压缩 python -m andumptar.dump --pid 12345 --region heap,stack --compress none --output /tmp/snap_raw python -m andumptar.compress --input /tmp/snap_raw --algorithm zstd --level 35. 进阶:把快照变成可对比的时间线
单次快照能查,但排障往往需要对比两个时间点。安安dumptar 的进阶用法是生成 diff:对同一进程在 T1 和 T2 各 dump 一次,然后按地址范围比对哪些块变了、变了多少。这个能力对定位内存泄漏特别有用——你能看到哪个区域在持续增长。
# 生成两个快照的差异报告 python -m andumptar.diff \ --base /tmp/snap_t1 \ --target /tmp/snap_t2 \ --output /tmp/diff_report.json \ --min-change 4096--min-change过滤掉小于 4KB 的变化,避免噪声。报告里会列出新增块、消失块、内容变化块,以及每个变化块的地址范围和变化字节数。我一般会再写个小脚本按地址排序,看增长最快的区域落在哪个 maps 条目上,基本就能锁定是哪个模块在泄漏。
另一个技巧是结合符号做聚合:把 diff 结果按符号名分组,输出每个函数的地址范围内变化总量。这样不用逐块看,直接知道哪个函数在“吃”内存。
# 按符号聚合 diff 结果,输出变化最大的前 10 个符号 import json with open('/tmp/diff_report.json') as f: report = json.load(f) symbol_changes = {} for block in report['changed_blocks']: addr = block['start'] # 这里用简化的符号查找,实际应查 maps + 符号表 sym = lookup_symbol(addr) symbol_changes[sym] = symbol_changes.get(sym, 0) + block['changed_bytes'] for sym, total in sorted(symbol_changes.items(), key=lambda x: -x[1])[:10]: print(f"{sym}: {total} bytes changed")lookup_symbol需要自己实现,或者用andumptar.query --symbol的批量模式。这个脚本跑完,通常前三个符号就能指出问题方向。
提示:diff 的前提是两次 dump 的 chunk-size 一致,否则块边界对不齐,比对结果全是噪声。我习惯在 meta.json 里记 chunk-size,diff 时先校验。
最后说个血泪教训:别在业务高峰期抓快照,也别抓完就扔在 /tmp 不管。我有次抓了一个 8GB 的快照,忘了清理,第二天磁盘告警,排查半天才发现是快照占的。现在我的习惯是 dump 完立刻打标签、记时间、设过期提醒,超过 72 小时的自动清理。快照是后悔药,但药也有保质期。希望帮到你。
本文还有配套的精品资源,点击获取