https://eunomia.dev/zh/tutorials/16-memleak/

编译运行

  • 安装 Rust 和 Cargo
1
curl https://sh.rustup.rs -sSf | sh
  • 编译
1
2
3
4
5
git clone https://github.com/eunomia-bpf/bpf-developer-tutorial.git
cd bpf-developer-tutorial
git submodule update --init --recursive # Synchronize submodule
cd src/16-memleak
make
  • 运行
1
2
$ ./test_memleak &
$ sudo ./memleak -p $(pidof test_memleak)

调试内存泄露的挑战

  • Valgrind memcheck: 模拟 CPU 来检查所有内存访问,但可能会导致应用程序运行速度大大减慢
  • 堆分析器 libtcmalloc:相对较快,但仍可能使应用程序运行速度降低五倍以上
  • gdb:可以获取应用程序的核心转储 (coredump),并进行后处理以分析内存使用情
  • Address Sanitizer (ASAN)

这些工具通常在获取核心转储时需要暂停应用程序,或在应用程序终止后才能调用 free() 函数。

Memleak 实现原理

memleak 的工作方式类似于在内存分配和释放路径上安装监控设备。它通过在内存分配和释放函数中插入 eBPF 程序来达到这个目标。

  • 当分配内存的函数被调用时,memleak 就会记录一些重要信息,如调用者的进程 ID(PID)、分配的内存地址以及分配的内存大小等。
  • 当释放内存的函数被调用时,memleak 则会在其内部的映射表(map)中删除相应的内存分配记录。

这种机制使得 memleak 能够准确地追踪到哪些内存块已被分配但未被释放。

用户态

对于用户态的常用内存分配函数,如 malloccalloc 等,memleak 利用了用户态探测(uprobe)技术来实现监控。

uprobe 是一种用于用户空间应用程序的动态追踪技术,它可以在运行时不修改二进制文件的情况下在任意位置设置断点,从而实现对特定函数调用的追踪。

内核态

对于内核态的内存分配函数,如 kmalloc 等,memleak 则选择使用了 tracepoint 来实现监控。

Tracepoint 是一种在 Linux 内核中提供的动态追踪技术,它可以在内核运行时动态地追踪特定的事件,而无需重新编译内核或加载内核模块。

Memleak 代码实现

数据结构

alloc_info: 内存分配的基本信息

1
2
3
4
5
struct alloc_info {
__u64 size; // 分配的内存大小
__u64 timestamp_ns; // 分配发生时的时间戳
int stack_id; // 分配时的调用堆栈 ID
};

combined_alloc_info:

1
2
3
4
5
6
7
union combined_alloc_info {
struct {
__u64 total_size : 40; // 所有未释放的总内存大小
__u64 number_of_allocs : 24; // 所有未释放的总分配次数
};
__u64 bits;
};
  • total_sizenumber_of_allocs 在存储时共有一个 unsigned long long 类型的变量 bits
  • 可以通过在成员变量 bits 上进行位运算,来访问和修改 total_sizenumber_of_allocs

maps

sizes: 存储每个进程的分配大小

  • key: pid
  • value: 内存分配的大小
1
2
3
4
5
6
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__type(key, pid_t);
__type(value, u64);
__uint(max_entries, 10240);
} sizes SEC(".maps");

allocs: 存储每个内存分配的详细信息

  • key: 内存地址
  • value:
1
2
3
4
5
6
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__type(key, u64); /* address */
__type(value, struct alloc_info);
__uint(max_entries, ALLOCS_MAX_ENTRIES);
} allocs SEC(".maps");
1
2
3
4
5
6
7
// 存储所有未释放分配的总大小和总次数
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__type(key, u64); /* stack id */
__type(value, union combined_alloc_info);
__uint(max_entries, COMBINED_ALLOCS_MAX_ENTRIES);
} combined_allocs SEC(".maps");
1
2
3
4
5
6
7
// 在用户空间和内核空间之间传递内存指针
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__type(key, u64);
__type(value, u64);
__uint(max_entries, 10240);
} memptrs SEC(".maps");
1
2
3
4
5
// 存储堆栈 ID
struct {
__uint(type, BPF_MAP_TYPE_STACK_TRACE);
__type(key, u32);
} stack_traces SEC(".maps");