Memray attach 到一个运行中的 Python 进程时,在创建辅助线程的过程中触发了 SIGSEGV。沿着调用栈检查,问题落在 libc 的 mmap 入口:首字节是 libc 文件中的原始内容,紧接着的四字节却能解出一条指向 UCX 的跳转位移。
调查记录中的独立映射实验复现了这个变化。实际使用的 memleak 安装 uprobe 时,把入口首字节从 E9 改成 CC,退出时又恢复成 F3。最终留下的 16 字节与故障进程完全一致。本文围绕这段字节的来源,记录故障指令、UCX hook、内核恢复路径与对照实验之间的关系。字节损坏机制已经复现,原进程的历史写入过程没有被直接记录。
故障环境与 Memray 的调用路径
此前的使用 eBPF 监控内存分配主要讨论分配统计与 RSS 等指标的关系。这次是在一个包含 Python 和原生库的推理环境中排查内存增长,接入 Memray 时发生了异常,需要先查清辅助线程为什么创建失败。
以下版本、地址和实验输出来自 2026 年 10 月 19 日的调查记录。目标是容器中的 PID 1,命令行以 /usr/bin/python /usr/local/bin/torchrun --nproc_per_node 8 开头,进程名为 pt_elastic,从 10 月 17 日起运行。这个进程负责启动和管理 worker,是 launcher。本文取证的地址与字节属于这个 launcher,其他 worker 有各自的 PID 和地址空间。
| 项目 | 现场信息 |
|---|---|
| 架构与内核 | x86-64,5.15.0-94-generic |
| 容器用户空间 | Ubuntu 24.04 noble |
| Python | 3.12.3 |
| Memray | 1.20.0 |
| GDB | 15.1-1ubuntu1~24.04.1 |
| glibc 包 | libc6 2.39-0ubuntu8.5 |
| UCX 内存事件库 | /opt/hpcx/ucx/lib/libucm.so.0.0.0,精确发行版本未取得 |
| 实际成功运行的 memleak | /usr/sbin/memleak,日志与行为对应 libbpf-tools 实现,精确构建版本未取得 |
这里容器中的用户空间是 Ubuntu 24.04,使用的宿主机内核是 5.15.0-94-generic。
Memray 最初只报告:
1 | An unexpected error occurred. Run with --verbose to debug the failure. |
补充 GDB 诊断后,相关调用栈如下:
1 | #0 0x00007fc3785a32f0 in mmap64 () from libc.so.6 |
这条 attach 路径通过 GDB 在目标进程中执行函数,GDB 把这种操作称为 inferior call,其中 inferior 就是被调试进程。Memray 1.20.0 的注入实现在 memray_spawn_client() 中调用 pthread_create()。源码注释说明,attach 时解释器可能正在修改 GC 链表等不可重入的状态,因此创建一个辅助线程,由它取得 GIL(CPython 的全局解释器锁)后再执行 Python 代码。
这次创建线程需要为线程栈建立映射。现场寄存器中的映射长度是 0x801000:
1 | 0x801000 = 8 × 1024 × 1024 + 4096 字节 |
这个长度与本次调用中 8 MiB 栈加一页保护空间的分配路径相符。调查记录对精确版本 libc 的离线反汇编还确认,栈中返回地址之前是一条直接调用 mmap 入口的指令。
发生异常的是已有主线程 LWP 1,它正在执行 GDB 发起的函数调用,辅助线程尚未创建成功。这次到达故障入口的具体路径是 memray_spawn_client → pthread_create → mmap。
mmap 入口的字节与故障地址
内存中的入口与 libc 文件不同
现场停在 mmap64 的第一条指令:
1 | 0x7fc3785a32f0 <mmap64>: repz mov -0x2(%rbx,%rdi,2),%eax |
为核对这条指令,调查继续读取进程内存中的字节。GDB 会管理软件断点,部分内存读取接口会展示被断点替换之前的内容,因此这里采用调试器退出后的独立读取。
在 GDB detach 之后,通过 /proc/1/mem 直接读取同一地址,并与目标进程根目录下的 libc 文件比较,得到:
1 | memory: f3 8b 44 7b fe f7 c1 ff 0f 00 00 75 33 55 48 89 |
这里选择读取 16 字节,是为了同时覆盖前五字节补丁和紧接着的指令。检查前还核对了 /proc/1/stat 的启动时间字段,避免 PID 被复用后读到另一个进程。
文件中的 F3 0F 1E FA 是 ENDBR64,用于 x86 控制流保护技术 CET 中的间接分支跟踪。启用间接分支跟踪时,这条指令标记允许到达的间接分支目标。本例需要核对的是它占据的四个字节。后面的 41 F7 C1 ... 则是检查 %r9d 低位的指令。
比较文件内容还需要把进程虚拟地址换算成文件偏移。现场包含故障地址的映射从 0x7fc3785a3000 开始,对应文件偏移 0x125000,因此:
1 | 入口文件偏移 = 映射文件偏移 +(入口虚拟地址 - 映射起始地址) |
诊断包记录了映射所对应的文件与目标根目录下的 libc 具有相同 inode 和 Build ID。离线取得的 Ubuntu 官方库也匹配这个 Build ID,其文件标识如下:
1 | Build ID: 282c2c16e7b6600b0b22ea0c99010d2795752b5f |
文件身份核对确认了这些字节属于同一构建的 libc,detach 后的直接读取确认了错误字节仍留在进程内存中。
错误指令解释了本次 SIGSEGV
现场寄存器与信号信息为:
1 | rbx = 0xfffffffffffff000 # 按有符号数解释为 -4096 |
按这条 mov 的寻址方式,读取地址是:
1 | rbx + rdi × 2 - 2 = -4096 + 0 × 2 - 2 = -4098 |
结果与信号中的 si_addr 完全一致。这使得故障栈、实际入口字节、寄存器与非法访问地址相互对应:本次 SIGSEGV 就发生在错误入口的第一次内存读取处。
残留位移与 UCX 内联 hook
内存中的前五字节是 F3 8B 44 7B FE。如果把首字节暂时按 E9 解释,后面的四字节恰好能构成一条 x86-64 相对跳转。
E9 rel32 总共占五字节,E9 是操作码,rel32 是小端编码的有符号 32 位位移。跳转基准是这条指令结束后的地址,因此可以离线计算:
1 | 候选指令:E9 8B 44 7B FE |
候选目标位于已经加载的 libucm.so.0.0.0 中。根据现场映射换算到库内,位置是 0xb780,恰好对应:
1 | 000000000000b780 <ucm_override_mmap>: |
这四字节位移恰好指向 ucm_override_mmap 的入口。整个计算使用已经取得的地址与字节,在离线条件下完成。
libucm 的内存事件机制
UCX 是面向高性能通信的软件框架,libucm 是其中负责内存事件的组件。内联 hook 指直接改写已有函数的入口,把调用转到替代实现。在这个例子中,libc 的 mmap 入口被改成跳转,原本进入 libc 的调用就会先到达 ucm_override_mmap。
UCX 需要知道映射何时建立、释放或改变。例如,在 RDMA 场景中,注册一段内存需要向通信设备登记地址范围与访问权限。注册缓存保存已经完成注册的区域,后续操作可以复用它们。应用释放一段映射后,同一虚拟地址可以映射到另一组物理页,因此覆盖这段地址的旧缓存项需要失效。libucm 提供 mmap、munmap 等内存事件,供注册缓存处理这些变化。
在 UCX v1.18.0 的注册缓存实现中,ucs_rcache_unmapped_callback() 收到 UCM_EVENT_VM_UNMAPPED 后,从事件中取得地址和长度,算出待失效范围。随后按锁与配置条件立即处理相交缓存项,或把这个范围放入失效队列。这就是内存事件与注册缓存之间的具体连接。
在 UCX v1.18.0 的安装代码中,内存事件表通过 UCM_MMAP_RELOC_ENTRY(mmap) 将 mmap 与 ucm_override_mmap 对应起来。在称为 bistro 的内联改写模式下,安装逻辑取得原函数地址,然后调用 ucm_bistro_patch()。x86-64 实现在位移允许且未强制远跳转时,使用上述 E9 rel32 补丁。
实际建立映射仍然由内核完成。bistro 模式的原始操作路径可以直接发起系统调用,避免重新进入刚刚改写的 libc 入口。
安装代码用 installed_events 记录已经安装的事件,遍历事件表时会跳过对应项。跳转写入入口后,后续调用直接执行这条跳转。本次分析的起点就是探针安装前已经存在的五字节跳转。
现场没有取得 UCX 的精确发行版本,v1.18.0 用来核对实现路径。对现场库的判断依据是残留位移、进程映射以及
ucm_override_mmap的符号位置。调查记录还列出了已加载的 PyTorch、Open MPI、UCC 等库,具体业务请求的通信调用栈未被采集。
uprobe 保存与恢复的原始指令
接下来的问题是,谁会保留跳转的四字节位移,却把 E9 变回文件中的 F3。先前运行过的 memleak 是一条需要核查的路径。
memleak 的用户态分配追踪在 malloc、mmap 等函数进入和返回时采集分配信息,再与释放事件关联。这里使用的 uprobe 是 Linux 的用户态动态探针机制,探测位置以文件 inode 和文件偏移标识,再应用到目标进程的相应映射。eBPF 程序负责探针命中后的采集逻辑,代码位置上的探针由内核安装。
在本次 x86-64 路径中,内核将探测位置的首字节改成 CC,即一字节的 INT3 软件断点。执行到这里时,CPU 触发断点异常,内核识别 uprobe,调用关联处理逻辑,再执行或模拟被替换的原指令。需要异地执行时会使用 XOL(execute out of line) 指令区,即在独立位置执行保存的指令,然后继续原执行流。
这里选择 mmap 的入口作为探测位置。原来的 ENDBR64 占四字节,UCX 跳转占五字节,INT3 占一字节。安装探针时只需用 CC 覆盖这条入口指令的第一个字节。
保存值来自文件页缓存
在上游 Linux v5.15 的 copy_insn() 中,读取来源是:
1 | struct address_space *mapping = uprobe->inode->i_mapping; |
它通过 __copy_insn(),从文件页缓存读取对应偏移的指令。对于这里的普通文件,该函数使用 read_mapping_page() 取得页面,然后把内容复制到 uprobe->arch.insn。
这份保存值与进程私有代码页中当前的指令可以不同。UCX 改写的是进程中的映射,libc 文件仍以 F3 0F 1E FA 开头,所以内核保存的原指令也从 F3 开始。
内核在准备探针时保存文件页缓存中的指令,撤销时使用这份保存值。 文件页已经在缓存中时,读取直接使用该页。
写入保留了当前页面的其他字节
实际修改进程代码走的是 uprobe_write_opcode()。其中两个连续操作说明了新页面从哪里来,以及改多少字节:
1 | copy_highpage(new_page, old_page); |
先复制进程当前的页面,再只替换断点覆盖的部分。x86 定义为:
1 |
安装时写入 CC。撤销时,set_orig_insn()传入保存的原指令起始字节,通过同一写入路径恢复。于是,读取“原始内容”的来源是文件页缓存,保留其余字节的来源却是进程当前页面。
将这两个来源放到现场的五字节上,得到:
| 状态 | libc 文件中的入口 | 进程内存中的入口 |
|---|---|---|
| 未改写 | F3 0F 1E FA 41 |
F3 0F 1E FA 41 |
| UCX 安装跳转后 | F3 0F 1E FA 41 |
E9 8B 44 7B FE |
| uprobe 安装后 | F3 0F 1E FA 41 |
CC 8B 44 7B FE |
| uprobe 撤销后 | F3 0F 1E FA 41 |
F3 8B 44 7B FE |
内核保存的是 F3,安装时覆盖的却是 E9。撤销只恢复首字节,后面仍是 UCX 跳转的位移,最终组成了一条混合指令。
撤销代码还会比较恢复后的页面与文件页缓存是否完全一致。如果一致,可以让映射回到文件页。本例保留了四字节跳转位移,整页相等的条件不成立,进程继续使用包含这些字节的私有页面。
这几次写入可以严格按顺序发生:UCX 安装跳转,memleak 安装探针,再撤销探针。每一步读取和写入的内容由上表确定。即使目标始终没有执行过这个入口,完成安装与撤销也会留下 F3 8B 44 7B FE。
verify_opcode()在撤销时检查当前位置是否仍是软件断点。当前位置已经被改成其他字节时,它会返回并跳过恢复。本例撤销前的字节为CC,满足继续恢复的条件。上述源码来自上游 v5.15,现场 Ubuntu5.15.0-94的完整补丁集尚未核对,后面的实验用于验证现场内核实际写入了什么。
memleak 的实现与运行记录
调查记录中出现了两个 memleak 命令,它们的实现和运行结果不同。
| 命令 | 实现 | 记录到的结果 |
|---|---|---|
/usr/sbin/memleak-bpfcc |
BCC Python 脚本 | 缺少 kernel headers,在编译 BPF 模块时失败 |
/usr/sbin/memleak -p 1 5 3 |
日志与行为对应 libbpf-tools 实现 | 正常输出过周期统计 |
第一条命令的错误停在:
1 | Unable to find kernel headers. |
BCC v0.29.1 的 Python 实现先执行 BPF(text=bpf_source),成功后才依次调用 attach_probes()。因此,记录中的这次失败发生在 mmap 探针安装之前。
第二条命令中的 -p 1 指向目标进程,5 是报告间隔秒数,3 是报告次数。这是当时的操作记录。随后用于对照实验的也是这个 /usr/sbin/memleak。
两种实现对安装失败的处理有一处与本次调查直接相关的差异。Python 版调用 attach_probes("mmap", can_fail=True),允许该探针安装失败后继续运行。libbpf-tools 的对应代码则为 mmap 入口和返回探针使用检查错误的宏:
1 | ATTACH_UPROBE_CHECKED(skel, mmap, mmap_enter); |
在所核对的实现中,任一安装失败都会离开 attach_uprobes(),主程序进入清理流程。成功后才进入周期报告循环。退出时,程序释放持有的探针连接,内核据此移除相应探针。因此,正常输出周期统计提供了探针安装已通过的线索。
这里引用 v0.29.1,是因为日志文本、探针逻辑,以及下一节的路径截断行为与现场相符。实际二进制的包归属、版本和哈希尚未取得,本文保留这个版本信息缺口。
独立映射上的对照实验
实验要测量的是三个时刻的入口字节:安装之前、探针存在期间、撤销之后。为此,可以只映射 libc 副本并观察代码页,把实际函数调用留在实验之外。
实验脚本可下载:verify-uprobe-restore.py。它在原容器中运行,使用现场的 libc 和 /usr/sbin/memleak,以脚本自身的 PID 为探测目标。
文件与映射的安排
脚本先检查 libc 文件的 SHA256,再检查文件偏移 0x1252f0 处的 16 字节是否与现场原始入口相同。这个偏移由前面的映射换算得到,哈希检查将它限定在已核对的 libc 构建上。
接着,脚本普通复制一份 libc,并检查副本与源文件的设备号、inode 组合不同。因为 uprobe 用 inode 和文件偏移定位探测位置,副本的独立 inode 能让本次安装作用于副本对应的映射。
同一个副本建立两份 MAP_PRIVATE 映射:
| 映射 | 初始入口 | 用途 |
|---|---|---|
| control | F3 0F 1E FA 41 ... |
检查原始入口能否在撤销后恢复 |
| patched | E9 8B 44 7B FE ... |
检查已有跳转在撤销后留下哪些字节 |
实验组起初以可读、可写权限映射,写入五字节补丁后,调用 mprotect() 改成只读、可执行,即 RX。这是实验成立的一个具体条件:上游 v5.15 的 valid_vma()在安装探针时排除带 VM_WRITE 的映射。脚本会检查 /proc/self/maps,确认两份映射的权限均为 r-xp 后再启动 memleak。
探针通过 -p 指定脚本 PID,通过 -O 指定 libc 副本。报告间隔设为一秒,共报告两次,以留出观察 CC 的时间窗口。脚本在 memleak 运行前、运行期间和退出后读取两份映射,并在结束时检查磁盘副本的内容。
实验组中的跳转位移原样取自现场。由于映射地址已经改变,这份位移在实验进程中没有对应的 UCX 调用目标。脚本仅将映射中的字节作为数据读取,全程没有调用其中的代码。
首次运行中的对象路径截断
首次运行没有观察到字节变化,结果为 INCONCLUSIVE。memleak 日志给出了安装失败的位置:
1 | libbpf: elf: failed to open /tmp/uprobe-restore-0b5_rgb8/li: No such file or directory |
传入的完整路径为:
1 | /tmp/uprobe-restore-0b5_rgb8/libc-copy.so |
这个路径包含 41 个 ASCII 字节,日志中的路径只剩下前 31 字节。libbpf-tools 的对象路径字段定义为 char object[32],处理 -O 时使用:
1 | strncpy(env.object, arg, sizeof(env.object) - 1); |
最多复制 31 字节,最后一个位置留给 NUL。被截断的文件路径不存在,因此 memleak 在第一个 malloc_enter 探针处退出。这轮运行尚未到达 mmap 探针的安装步骤。
随后把临时目录前缀缩短,得到 /tmp/ur-xxxxxxxx/c.so 这样的路径。其中随机目录名部分为 8 个字符,整个路径共 21 字节。脚本同时加入长度检查,再进行下一轮实验。
安装与撤销期间的实测字节
调查记录中,在原容器执行:
1 | python3 verify-uprobe-restore.py /usr/sbin/memleak |
输出如下,PID、临时路径、设备号和 inode 均为这次运行的实际值:
1 | libc SHA256: de259f5276c4a991f78bf87225d6b40e56edbffe0dcbc0ffca36ec7fe30f3f77 |
对照组完成了 F3 → CC → F3。实验组完成了 E9 → CC → F3,后四字节一直是 8B 44 7B FE。最终实验组的 16 字节与 PID 1 中观察到的内容相同。
脚本要求以下条件同时成立,才输出 REPRODUCED:观察到实验组入口为 CC,memleak 在超时前以状态码 0 退出,磁盘副本保持原样,对照组恢复原始 16 字节,实验组得到准确的损坏字节。这样,现场内核与实际工具怎样安装和撤销探针,就有了直接的字节观测。
两份映射中都没有执行过代码,这次实验仍然得到了损坏入口。形成这些字节所需的操作已经缩小到:在已有五字节跳转的映射上安装探针,再撤销探针。
原进程故障与实验结果的对应
将源码与观测结果放在一起,原进程最符合现有证据的故障过程如下:
1 | UCX 将 mmap 入口改为 E9 8B 44 7B FE |
每个判断对应的证据可以分别列出:
| 判断 | 依据 |
|---|---|
| 故障发生在错误入口的内存读取处 | 指令的有效地址计算与 si_addr 相同 |
| 入口中四字节位移指向 UCX hook | E9 rel32 的目标地址与现场 ucm_override_mmap 符号重合 |
| 实际 memleak 能留下这些损坏字节 | 独立映射实验记录了 E9 → CC → F3,最终 16 字节匹配 |
| 业务 PID 上曾成功运行过 memleak | 操作记录与周期统计输出 |
| 该次运行就是原进程入口损坏的写入来源 | 根据以上事实作出的历史归因,缺少原进程改写前的快照与写入瞬间记录 |
实验直接验证了安装、撤销与字节变化之间的关系。原进程的历史归因还需要把那次成功运行的 memleak 与具体写入时刻对应起来,现有材料缺少第一次 Memray attach 之前的入口快照,也没有针对该地址的历史写入审计。因此,本文将它保留为最符合现有证据的解释。
GDB 是调查中检查过的另一条写入路径。Memray 1.20.0 的 attach 脚本在 malloc、calloc、Python 分配函数等位置设置断点,命中后删除这些断点,再加载注入库并调用 memray_spawn_client()。脚本没有在 mmap 入口设置断点,已有日志也未记录 GDB 在该地址恢复 F3。对 memleak 的判断则有独立映射中相同字节变化的直接支持。
本次实验覆盖了上述流程中的探针安装和撤销步骤。真实 UCX 初始化、业务进程中的历史执行顺序,以及从初始化一直运行到 Memray 触发异常的完整复现,仍属于尚未完成的验证。
调用与调试状态补充
launcher 的后续执行路径
损坏字节位于 launcher 的地址空间。已有线程可以继续等待,或执行不经过该入口的代码。worker 位于其他进程,它们的执行状态需要分别检查。调查前后 PID 和启动时间没有变化,说明这段观察期间未发现对应进程重启。
还需要按实际调用地址区分映射路径。直接发起 mmap 系统调用的代码会从自己的系统调用指令进入内核,复用已有内存的分配也可以跳过这次映射。现场 libc 中的 mmap、mmap64、__mmap 则是同地址别名,通过这些符号进入时都会执行同一段字节。
错误入口的后续指令
第一次错误读取的地址由 %rbx、%rdi 决定。在本次调用中,它恰好是非法地址 0xffffffffffffeffe。另一组寄存器值若使读取地址落在可访问页面中,CPU 还会继续执行后面的指令。
后面的字节同样需要重新解码。文件原始入口中的 41 是 REX 前缀,它使紧接着的 F7 C1 ... 使用 %r9d。这个前缀被五字节跳转覆盖后,从偏移 5 开始的指令变成:
1 | test $0xfff, %ecx |
在这里的 x86-64 函数调用约定中,mmap 的第四个参数 flags 位于 %ecx,第六个参数 offset 位于 %r9。原始入口检查 offset 的低位,损坏入口检查的则是 flags 的低位。
0xfff 覆盖低 12 位,来自精确 libc 反汇编中的立即数。调查记录显示,随后条件跳转的目标是设置 EINVAL 的路径。现场 flags 为 0x20022,所以:
1 | 0x20022 & 0xfff = 0x22 |
如果执行到这条 test,结果非零,就会跳到该错误返回路径。本次实际执行在前一条 mov 已经触发 SIGSEGV,上述分支是根据字节与寄存器推导出的后续行为。
GDB 对 inferior call 信号的处理
本次 attach 脚本设置了 unwindonsignal。在 inferior call 收到 SIGSEGV 后,GDB 恢复调用前的上下文,然后 detach。它恢复的是被调试调用的执行上下文,函数执行期间已经发生的内存写入、库加载,以及其他线程的动作会留在进程中。
现场进程在这次调试操作后继续存活,与上述信号处理过程相符。通常未被处理的 SIGSEGV 会终止整个进程,因此用进程内新建线程执行同一错误入口,仍然会影响整个进程。
探针存在期间的执行
本次实验将映射用于字节读取,探针没有被命中。由此得到的直接观察覆盖安装与撤销期间的字节变化。探针存在期间,内核执行或模拟保存指令的行为,需要另设执行实验验证。
这里保存的指令来自文件,进程原本准备执行的指令来自 UCX 补丁,两者已经不同。检查这一组合的兼容性时,还需要观察命中探针后是否按预期进入 ucm_override_mmap,以及后续参数和返回值是否正确。
处置与后续验证
继续调查应先保留当前进程的映射、入口字节、Build ID、工具版本和操作时间,再在独立进程中测试同一内核、UCX 与 profiler 的组合。新测试应在 UCX 安装后、探针安装后、撤销后分别读取入口,同时记录实际函数调用的结果。这样可以逐步补齐本次缺少的执行过程。
已经损坏的业务入口需要结合进程恢复流程处理。在线写回 E9 要确认 UCX hook 仍然有效、线程当前是否正在执行被改写的区域,以及指令修改如何同步到其他线程。本次没有验证这样的在线修补流程。重建进程会重新映射库并执行初始化,恢复前还需要移除已知的触发组合,恢复后核对入口字节与服务行为。调查记录没有包含实际业务重启结果。
后续选择 profiling 工具时,可以具体检查它如何接入目标:是否改写用户态函数入口、是否向目标注入代码、是否在目标中创建线程。本次 Memray 的调用栈确认了它会创建线程,memleak 的对照实验确认了它会通过内核改写入口。对同一版本组合,还应检查两个工具是否覆盖同一段代码,以及撤销探针后原有跳转的五个字节是否完整。
复现脚本补充
下载完整实验脚本后,显式指定现场实际使用的 memleak 运行:
1 | python3 verify-uprobe-restore.py /usr/sbin/memleak |
原脚本省略命令行参数时会查找 memleak-bpfcc。复现实测组合时,按上面的命令显式传入 /usr/sbin/memleak。
脚本限定 Linux x86-64,并在使用固定偏移前校验 libc 哈希。其他 libc 构建需要重新解析符号与文件偏移,再相应修改校验值。运行环境需要已经具备加载和附着用户态 BPF 探针的条件。
脚本每 20 毫秒读取一次入口字节,在一秒报告间隔内多次观察探针状态。轮询以 60 秒为期限,超过期限后进入终止测试子进程的流程,依次尝试 SIGINT、SIGTERM、SIGKILL,每次最多等待 5 秒。这些数值都是用于控制实验时长与观察频率的参数。轮询未捕获到 CC 时,脚本会输出 INCONCLUSIVE,此时需要结合日志确认探针是否安装,以及观察窗口是否覆盖了它存在的时间。
输出目录保留 maps.txt、memleak.log、result.json 与 libc 副本。result.json 记录内核版本、目标 PID、设备号与 inode、实际命令、观测字节和退出状态,便于与下一次运行比较。
参考资料
- Memray 1.20.0:GDB attach 脚本、辅助线程注入代码。
- UCX v1.18.0:内存事件与 mmap hook 安装、x86-64 跳转补丁、bistro 模式的系统调用路径、注册缓存的映射释放回调。
- Linux v5.15:原指令读取、进程页改写、恢复入口、撤销前检查、映射条件、x86 断点定义。
- BCC v0.29.1:Python memleak、libbpf-tools 的探针安装与对象路径字段。
- Ubuntu libc6 2.39-0ubuntu8.5 amd64 官方包,调查中用于核对现场 Build ID、符号和指令。