https://eunomia.dev/zh/tutorials/16-memleak/
编译运行
- 安装 Rust 和 Cargo
1 | curl https://sh.rustup.rs -sSf | sh |
- 编译
1 | git clone https://github.com/eunomia-bpf/bpf-developer-tutorial.git |
- 运行
1 | $ ./test_memleak & |
调试内存泄露的挑战
- Valgrind memcheck: 模拟 CPU 来检查所有内存访问,但可能会导致应用程序运行速度大大减慢
- 堆分析器 libtcmalloc:相对较快,但仍可能使应用程序运行速度降低五倍以上
- gdb:可以获取应用程序的核心转储 (coredump),并进行后处理以分析内存使用情
- Address Sanitizer (ASAN)
这些工具通常在获取核心转储时需要暂停应用程序,或在应用程序终止后才能调用 free() 函数。
Memleak 实现原理
memleak 的工作方式类似于在内存分配和释放路径上安装监控设备。它通过在内存分配和释放函数中插入 eBPF 程序来达到这个目标。
- 当分配内存的函数被调用时,memleak 就会记录一些重要信息,如调用者的进程 ID(PID)、分配的内存地址以及分配的内存大小等。
- 当释放内存的函数被调用时,memleak 则会在其内部的映射表(map)中删除相应的内存分配记录。
这种机制使得 memleak 能够准确地追踪到哪些内存块已被分配但未被释放。
用户态
对于用户态的常用内存分配函数,如 malloc 和 calloc 等,memleak 利用了用户态探测(uprobe)技术来实现监控。
uprobe 是一种用于用户空间应用程序的动态追踪技术,它可以在运行时不修改二进制文件的情况下在任意位置设置断点,从而实现对特定函数调用的追踪。
内核态
对于内核态的内存分配函数,如 kmalloc 等,memleak 则选择使用了 tracepoint 来实现监控。
Tracepoint 是一种在 Linux 内核中提供的动态追踪技术,它可以在内核运行时动态地追踪特定的事件,而无需重新编译内核或加载内核模块。
Memleak 代码实现
数据结构
alloc_info: 内存分配的基本信息
1 | struct alloc_info { |
combined_alloc_info:
1 | union combined_alloc_info { |
total_size和number_of_allocs在存储时共有一个unsigned long long类型的变量bits- 可以通过在成员变量
bits上进行位运算,来访问和修改total_size和number_of_allocs
maps
sizes: 存储每个进程的分配大小
- key: pid
- value: 内存分配的大小
1 | struct { |
allocs: 存储每个内存分配的详细信息
- key: 内存地址
- value:
1 | struct { |
1 | // 存储所有未释放分配的总大小和总次数 |
1 | // 在用户空间和内核空间之间传递内存指针 |
1 | // 存储堆栈 ID |