消失的八字节-bcc memleak修复经历分享

288 阅读4分钟

memleak

在日常的开发中,我们经常会遇到内存泄漏这样的老大难问题。随着eBPF的发展,我们可以使用eBPF来进行内存泄露的检测。在bcc中,就自带了一个memleak工具来帮助我们进行c/c++的内存泄漏检测。

memleak原理简单解析

memleak的思路比较明确:利用插桩统计分配的字节和释放的字节,两者进行抵消,从而寻找没有释放的、可能是泄露的调用栈

malloc分配为例,当malloc申请时:

malloc申请

malloc申请

会记录一个<pid, size>对,记录下哪个进程申请了多少内存;接着当分配完成以后:

malloc申请完成

malloc申请完成

会记录下<address, info>对,记录下虚拟地址以及相关的调用栈等信息;当我们调用free释放对应地址的时候:

free

free

我们就基于address来进行抵消,那么剩下的没有抵消的部分就可能是泄露的地方:

泄露

泄露

消失的八字节

在了解了memleak的原理后,我们发现有这样的一个patchFix data race on --combined-only(github.com/iovisor/bcc…

测试代码

测试代码

可以看到,这里调用malloc分配了800000个字节,但是在修复完race condition后,结果却和我们想的不一样:

缺少8字节

缺少8字节

竟然少了8个字节。笔者尝试将分配的字节从800000变成80,发现也会缺少八个字节,可见这不是一个线性变化的事情。那问题出在哪里呢?

追踪

为了便捷说明,这里将分配次数调整成了10次,一共分配80字节。

为了进行调试,我们尝试打印pid等信息,并进行追踪,重点观察分配4字节的部分:

trace打印追踪

trace打印追踪

好像找到一点线索了,这里同一个tid=2600823怎么会连续分配两次呢?我们尝试加了更多的打印信息,来看看是谁调用了gen_alloc_enter

追踪调用者

追踪调用者

可以看到,这里在由malloc第一次调用gen_alloc_enter之后,mmap也调用了gen_alloc_enter,由于我们的BPF_HASH(sizes, u32, u64)的主键是tid,所以原来为四字节的部分就被冲刷掉了,自然而然就少统计了四个字节。

理想中的统计抵消过程如下图(表示map的映射):

理想的抵消

理想的抵消

理想的统计泄露过程如下图(表示map的映射):

理想的泄露发现过程

理想的泄露发现过程

实际的统计泄露过程(表示map的映射):

实际的情况

实际的情况

那么为什么会覆盖呢?这是因为malloc在进行内存分配的时候,如果内存分配器自身管理的内存也不够,就会通过mmap或者其他的方式来申请内存,所以会出现malloc调用后mmap调用的情况。

修复

现在我们已经知道了问题所在,就是mapkey冲突导致的,那么我们怎么解决这个问题呢?

二级Map

笔者本来想尝试通过BPF_ARRAY_OF_MAPS或者BPF_HASH_OF_MAPS来实现二级map,一级mapkey就是入口任务类别,二级map才是真正的存储,如图所示:

二级Map实现

二级Map实现

但是笔者查阅资料以后发现,想要实现动态的二级Map有点困难,不能动态的设置Array或者一级Map的大小,所以暂时放弃了这种写法。

如果有了解的读者可以私信或者留言指导下。

新的key定义

由于第一种方式行不动,我们只能寻找第二种方式。既然问题的因为key冲突导致的,而key是进程的pid,类型是u32,比较容易冲突,那我们改变key的定义,让它不冲突就可以了。于是笔者尝试将key设置成如下的值:

利用高32位来邦

利用高32位来作为区分位,这样,整个的泄露检测过程就变成了:

高32位区分

高32位区分

修复效果检验

完成以后,我们尝试重新运行一下:

修复成功

修复成功

可以看到已成功修复。

总结

本文主要介绍了memleak的简单原理和对其的修复过程,该pr已经合入到bcc仓库中,详见tools/memleak: Fix the data error caused by the same key in map(github.com/iovisor/bcc…](github.com/AshinZ/perf…