19 KiB
eBPF内存泄露检测代码实现<完整版>
[toc]
说明:
eBPF内存泄露检测代码实现是在 libbpf-bootstrap 框架下开发,需要的基础知识请参考之前的ebpf系列视频
本节视频使用的是 Ubuntu18.04 x86-64
平台
目标
- 支持如下用户态内存分配接口的内存泄露检测
void *malloc(size_t size);
void *calloc(size_t nmemb, size_t size);
void *realloc(void *ptr, size_t size);
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
void *memalign(size_t alignment, size_t size);
void *valloc(size_t size);
void *pvalloc(size_t size);
void *aligned_alloc(size_t alignment, size_t size);
int posix_memalign(void **memptr, size_t alignment, size_t size);
- 实现可执行文件一启动就开始内存泄露检测
BPF_KRETPROBE 宏展开
因为 malloc uretprobe
中的 bpf_get_stackid
接口涉及到一个 struct pt_regs *ctx
的参数,
info.stack_id = bpf_get_stackid(ctx, &stack_traces, USER_STACKID_FLAGS);
参考 <eBPF示例:x86-64平台上的kprobe> 视频中 BPF_KPROBE
宏展开方法,对 BPF_KRETPROBE
宏进行展开:
SEC("uretprobe")
int BPF_KRETPROBE(malloc_exit, void * address)
{
/* malloc uretuprobe 的处理逻辑 */
}
//////////////////////// 宏展开 ////////////////////////
__attribute__((section("uretprobe"), used))
// 函数的声明
int malloc_exit(struct pt_regs *ctx);
static inline int ____malloc_exit(struct pt_regs *ctx, void * address);
// 函数的定义
int malloc_exit(struct pt_regs *ctx) {
return ____malloc_exit(ctx, (void *)((ctx)->ax));
}
static inline int ____malloc_exit(struct pt_regs *ctx, void * address)
{
/* malloc uretuprobe 的处理逻辑 */
}
/* 对于uprobe和uretprobe
* struct pt_regs: 保存用户态的寄存器上下文,可以存放用户态接口的参数或者返回值
* 不同的平台, struct pt_regs 中成员变量的定义不一样:
* x86-64平台(libbpf-bootstrap/vmlinux/x86/vmlinux.h):
struct pt_regs {
long unsigned int r15;
long unsigned int r14;
long unsigned int r13;
long unsigned int r12;
long unsigned int bp;
long unsigned int bx;
long unsigned int r11;
long unsigned int r10;
long unsigned int r9;
long unsigned int r8;
long unsigned int ax;
long unsigned int cx;
long unsigned int dx;
long unsigned int si;
long unsigned int di;
long unsigned int orig_ax;
long unsigned int ip;
long unsigned int cs;
long unsigned int flags;
long unsigned int sp;
long unsigned int ss;
};
* arm32 平台(libbpf-bootstrap/vmlinux/arm/vmlinux.h):
struct pt_regs {
long unsigned int uregs[18];
};
*
* 参考头文件: libbpf-bootstrap/libbpf/src/bpf_tracing.h
* 假设 uprobe 和 uretprobe attach 到 void *malloc(size_t size) 接口:
* malloc 的第一个参数可以通过 PT_REGS_PARM1(ctx)获取,在 x86-64 平台上:
* size = PT_REGS_PARM1(ctx) = ctx->di
* malloc 的返回值可以通过 PT_REGS_RC(ctx)获取,在 x86-64 平台上:
* malloc的返回值 void * address = PT_REGS_RC(ctx) = ctx->ax
* 使用 PT_REGS_PARM1 和 PT_REGS_RC 宏可以屏蔽平台之间的差异;
*/
提取公共的代码
针对 malloc
和 free
的内存泄露检测逻辑抽象图:
malloc
内存分配接口: 分配的内存大小size
,分配的内存指针ptr
,stack_id
,经过
malloc uprobe
malloc uretuprobe
free uprobe
处理逻辑后,最终输出一个内存统计信息 union combined_alloc_info
现在把malloc uprobe
,malloc uretuprobe
,free uprobe
处理逻辑提取成公共代码:
/* 通用的内存分配 uprobe的处理逻辑
* 内存分配接口(malloc, calloc等)进入后就会被调用
* size: 分配内存的大小, 比如 malloc 的第一个参数
*/
static int gen_alloc_enter(size_t size)
{
const pid_t pid = bpf_get_current_pid_tgid() >> 32;
bpf_map_update_elem(&sizes, &pid, &size, BPF_ANY);
return 0;
}
/* 通用的内存分配 uretprobe的处理逻辑
* 内存分配接口(malloc, calloc等)返回时就会被调用
* ctx: struct pt_regs 指针, 参考 BPF_KRETPROBE 的宏展开
* address: 分配成功的内存指针, 比如 malloc 的返回值
*/
static int gen_alloc_exit2(void *ctx, u64 address)
{
const u64 addr = (u64)address;
const pid_t pid = bpf_get_current_pid_tgid() >> 32;
struct alloc_info info;
const u64 * size = bpf_map_lookup_elem(&sizes, &pid);
if (NULL == size) {
return 0;
}
__builtin_memset(&info, 0, sizeof(info));
info.size = *size;
bpf_map_delete_elem(&sizes, &pid);
if (0 != address) {
info.stack_id = bpf_get_stackid(ctx, &stack_traces, USER_STACKID_FLAGS);
bpf_map_update_elem(&allocs, &addr, &info, BPF_ANY);
union combined_alloc_info add_cinfo = {
.total_size = info.size,
.number_of_allocs = 1
};
union combined_alloc_info * exist_cinfo = bpf_map_lookup_elem(&combined_allocs, &info.stack_id);
if (NULL == exist_cinfo) {
bpf_map_update_elem(&combined_allocs, &info.stack_id, &add_cinfo, BPF_NOEXIST);
}
else {
__sync_fetch_and_add(&exist_cinfo->bits, add_cinfo.bits);
}
}
return 0;
}
/* 把 gen_alloc_exit2 接口中的2个参数精简为1个参数
* 参考 BPF_KRETPROBE 的宏展开过程
*/
static int gen_alloc_exit(struct pt_regs *ctx)
{
return gen_alloc_exit2(ctx, PT_REGS_RC(ctx));
}
/* 通用的内存释放 uprobe的处理逻辑
* 内存释放接口(free, munmap等)进入后就会被调用
* address: 需要释放的内存指针, 比如 free 的第一个参数
*/
static int gen_free_enter(const void *address)
{
const u64 addr = (u64)address;
const struct alloc_info * info = bpf_map_lookup_elem(&allocs, &addr);
if (NULL == info) {
return 0;
}
union combined_alloc_info * exist_cinfo = bpf_map_lookup_elem(&combined_allocs, &info->stack_id);
if (NULL == exist_cinfo) {
return 0;
}
const union combined_alloc_info sub_cinfo = {
.total_size = info->size,
.number_of_allocs = 1
};
__sync_fetch_and_sub(&exist_cinfo->bits, sub_cinfo.bits);
bpf_map_delete_elem(&allocs, &addr);
return 0;
}
memleak.bpf.c
中 malloc
和 free
的 uprobe
和uretprobe
代码实现:
SEC("uprobe")
int BPF_KPROBE(malloc_enter, size_t size)
{
return gen_alloc_enter(size);
}
SEC("uretprobe")
int BPF_KRETPROBE(malloc_exit)
{
return gen_alloc_exit(ctx);
}
SEC("uprobe")
int BPF_KPROBE(free_enter, void * address)
{
return gen_free_enter(address);
}
用宏来简化 memleak.c
(用户态的ebpf)中的 bpf_program__attach_uprobe_opts
接口调用:
参考:https://github.com/eunomia-bpf/bpf-developer-tutorial 中 memleak 的实现
#define __ATTACH_UPROBE(skel, sym_name, prog_name, is_retprobe) \
do { \
LIBBPF_OPTS(bpf_uprobe_opts, uprobe_opts, \
.func_name = #sym_name, \
.retprobe = is_retprobe); \
skel->links.prog_name = bpf_program__attach_uprobe_opts( \
skel->progs.prog_name, \
attach_pid, \
binary_path, \
0, \
&uprobe_opts); \
} while (false)
#define __CHECK_PROGRAM(skel, prog_name) \
do { \
if (!skel->links.prog_name) { \
perror("no program attached for " #prog_name); \
return -errno; \
} \
} while (false)
#define __ATTACH_UPROBE_CHECKED(skel, sym_name, prog_name, is_retprobe) \
do { \
__ATTACH_UPROBE(skel, sym_name, prog_name, is_retprobe); \
__CHECK_PROGRAM(skel, prog_name); \
} while (false)
/* ATTACH_UPROBE_CHECKED 和 ATTACH_UPROBE 宏的区别是:
* ATTACH_UPROBE_CHECKED 会检查elf文件中(比如 libc.so)中是否存在 uprobe attach 的符号(比如malloc)
* 如果不存在,返回错误;
* ATTACH_UPROBE 发现符号不存在时不会返回错误,直接跳过这个符号的uprobe attach,继续往下执行;
*/
#define ATTACH_UPROBE(skel, sym_name, prog_name) __ATTACH_UPROBE(skel, sym_name, prog_name, false)
#define ATTACH_URETPROBE(skel, sym_name, prog_name) __ATTACH_UPROBE(skel, sym_name, prog_name, true)
#define ATTACH_UPROBE_CHECKED(skel, sym_name, prog_name) __ATTACH_UPROBE_CHECKED(skel, sym_name, prog_name, false)
#define ATTACH_URETPROBE_CHECKED(skel, sym_name, prog_name) __ATTACH_UPROBE_CHECKED(skel, sym_name, prog_name, true)
bpf_program__attach_uprobe_opts
替换:
int main(int argc, char **argv)
{
uprobe_opts.func_name = "malloc";
uprobe_opts.retprobe = false;
skel->links.malloc_enter = bpf_program__attach_uprobe_opts(skel->progs.malloc_enter,
attach_pid, binary_path, 0, &uprobe_opts);
if (!skel->links.malloc_enter) {
err = -errno;
fprintf(stderr, "Failed to attach uprobe: %d\n", err);
goto cleanup;
}
uprobe_opts.func_name = "malloc";
uprobe_opts.retprobe = true;
skel->links.malloc_exit = bpf_program__attach_uprobe_opts(
skel->progs.malloc_exit, attach_pid, binary_path, 0, &uprobe_opts);
if (!skel->links.malloc_exit) {
err = -errno;
fprintf(stderr, "Failed to attach uprobe: %d\n", err);
goto cleanup;
}
uprobe_opts.func_name = "free";
uprobe_opts.retprobe = false;
skel->links.free_enter = bpf_program__attach_uprobe_opts(skel->progs.free_enter,
attach_pid, binary_path, 0, &uprobe_opts);
if (!skel->links.free_enter) {
err = -errno;
fprintf(stderr, "Failed to attach uprobe: %d\n", err);
goto cleanup;
}
return 0;
}
//////////////////////// 替换为 ////////////////////////
int attach_uprobes(struct memleak_bpf *skel)
{
ATTACH_UPROBE_CHECKED(skel, malloc, malloc_enter);
ATTACH_URETPROBE_CHECKED(skel, malloc, malloc_exit);
ATTACH_UPROBE_CHECKED(skel, free, free_enter);
return 0;
}
int main(int argc, char **argv)
{
err = attach_uprobes(skel);
if (err) {
fprintf(stderr, "failed to attach uprobes\n");
goto cleanup;
}
return 0;
}
修改后的代码,编译验证通过;
posix_memalign 的内存泄露检测实现过程
posix_memalign
接口定义:
int posix_memalign(void **memptr, size_t alignment, size_t size);
void *malloc(size_t size);
//posix_memalign 对应的内存释放接口
void free(void *ptr);
posix_memalign
和 malloc
对于内存泄露检测来说,最大的区别就是 分配的内存指针 返回方式不一样:
malloc
: 通过接口返回值返回 分配的内存指针,直接通过 uretprobe
就可以获取 分配的内存指针;
posix_memalign
:把 分配的内存指针
保存到 用户态的指针变量中(memptr)
,因为 用户态的指针变量(memptr)
在uprobe
中获取,而把 分配的内存指针
保存到用户态的指针变量(memptr)
是在 uretprobe
中,所以需要一个 ebpf 的 hash map 来临时存储在uprobe
中获取到的 用户态的指针变量(memptr)
;
修改测试代码:
static void * alloc_v3(int alloc_size)
{
void * memptr = NULL;
posix_memalign(&memptr, 128, 1024);
return memptr;
}
memleak.bpf.c
中的代码实现:
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__type(key, u64); // pid
__type(value, u64); // 用户态指针变量 memptr
__uint(max_entries, 10240);
} memptrs SEC(".maps");
SEC("uprobe")
int BPF_KPROBE(posix_memalign_enter, void **memptr, size_t alignment, size_t size)
{
const u64 memptr64 = (u64)(size_t)memptr;
const u64 pid = bpf_get_current_pid_tgid() >> 32;
bpf_map_update_elem(&memptrs, &pid, &memptr64, BPF_ANY);
return gen_alloc_enter(size);
}
SEC("uretprobe")
int BPF_KRETPROBE(posix_memalign_exit)
{
const u64 pid = bpf_get_current_pid_tgid() >> 32;
u64 *memptr64;
void *addr;
memptr64 = bpf_map_lookup_elem(&memptrs, &pid);
if (!memptr64)
return 0;
bpf_map_delete_elem(&memptrs, &pid);
//通过 bpf_probe_read_user 读取保存在用户态指针变量(memptr64)中的 分配成功的内存指针
if (bpf_probe_read_user(&addr, sizeof(void*), (void*)(size_t)*memptr64))
return 0;
const u64 addr64 = (u64)(size_t)addr;
return gen_alloc_exit2(ctx, addr64);
}
memleak.c
中的代码实现:
// 在 attach_uprobes 接口中增加代码:
int attach_uprobes(struct memleak_bpf *skel)
{
/* ...... */
ATTACH_UPROBE_CHECKED(skel, posix_memalign, posix_memalign_enter);
ATTACH_URETPROBE_CHECKED(skel, posix_memalign, posix_memalign_exit);
/* ...... */
}
内存泄露检测的完整实现
//void *malloc(size_t size);
void *calloc(size_t nmemb, size_t size);
void *realloc(void *ptr, size_t size);
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
void *memalign(size_t alignment, size_t size);
void *valloc(size_t size);
void *pvalloc(size_t size);
void *aligned_alloc(size_t alignment, size_t size);
//int posix_memalign(void **memptr, size_t alignment, size_t size);
剩下的内存分配接口和 malloc
相差不大,都是通过返回值来获取分配成功的内存指针,就不一一讲解了;
memleak.bpf.c
文件中增加如下代码,uprobe 和 uretprobe 处理逻辑调用的都是之前定义的通用接口:
SEC("uprobe")
int BPF_KPROBE(calloc_enter, size_t nmemb, size_t size)
{
return gen_alloc_enter(nmemb * size);
}
SEC("uretprobe")
int BPF_KRETPROBE(calloc_exit)
{
return gen_alloc_exit(ctx);
}
SEC("uprobe")
int BPF_KPROBE(realloc_enter, void *ptr, size_t size)
{
gen_free_enter(ptr);
return gen_alloc_enter(size);
}
SEC("uretprobe")
int BPF_KRETPROBE(realloc_exit)
{
return gen_alloc_exit(ctx);
}
SEC("uprobe")
int BPF_KPROBE(mmap_enter, void *address, size_t size)
{
return gen_alloc_enter(size);
}
SEC("uretprobe")
int BPF_KRETPROBE(mmap_exit)
{
return gen_alloc_exit(ctx);
}
SEC("uprobe")
int BPF_KPROBE(munmap_enter, void *address)
{
return gen_free_enter(address);
}
SEC("uprobe")
int BPF_KPROBE(aligned_alloc_enter, size_t alignment, size_t size)
{
return gen_alloc_enter(size);
}
SEC("uretprobe")
int BPF_KRETPROBE(aligned_alloc_exit)
{
return gen_alloc_exit(ctx);
}
SEC("uprobe")
int BPF_KPROBE(valloc_enter, size_t size)
{
return gen_alloc_enter(size);
}
SEC("uretprobe")
int BPF_KRETPROBE(valloc_exit)
{
return gen_alloc_exit(ctx);
}
SEC("uprobe")
int BPF_KPROBE(memalign_enter, size_t alignment, size_t size)
{
return gen_alloc_enter(size);
}
SEC("uretprobe")
int BPF_KRETPROBE(memalign_exit)
{
return gen_alloc_exit(ctx);
}
SEC("uprobe")
int BPF_KPROBE(pvalloc_enter, size_t size)
{
return gen_alloc_enter(size);
}
SEC("uretprobe")
int BPF_KRETPROBE(pvalloc_exit)
{
return gen_alloc_exit(ctx);
}
memleak.c
文件中的 attach_uprobes
接口中增加如下代码:
int attach_uprobes(struct memleak_bpf *skel)
{
/* ...... */
ATTACH_UPROBE_CHECKED(skel, calloc, calloc_enter);
ATTACH_URETPROBE_CHECKED(skel, calloc, calloc_exit);
ATTACH_UPROBE_CHECKED(skel, realloc, realloc_enter);
ATTACH_URETPROBE_CHECKED(skel, realloc, realloc_exit);
ATTACH_UPROBE_CHECKED(skel, mmap, mmap_enter);
ATTACH_URETPROBE_CHECKED(skel, mmap, mmap_exit);
ATTACH_UPROBE_CHECKED(skel, memalign, memalign_enter);
ATTACH_URETPROBE_CHECKED(skel, memalign, memalign_exit);
ATTACH_UPROBE_CHECKED(skel, free, free_enter);
ATTACH_UPROBE_CHECKED(skel, munmap, munmap_enter);
// the following probes are intentinally allowed to fail attachment
// deprecated in libc.so bionic
ATTACH_UPROBE(skel, valloc, valloc_enter);
ATTACH_URETPROBE(skel, valloc, valloc_exit);
// deprecated in libc.so bionic
ATTACH_UPROBE(skel, pvalloc, pvalloc_enter);
ATTACH_URETPROBE(skel, pvalloc, pvalloc_exit);
// added in C11
ATTACH_UPROBE(skel, aligned_alloc, aligned_alloc_enter);
ATTACH_URETPROBE(skel, aligned_alloc, aligned_alloc_exit);
/* ...... */
}
修改后的代码,编译验证通过;
C++中的new可以被内存泄露检测吗?
测试代码:
static void * alloc_v3(int alloc_size)
{
void * ptr = new char[alloc_size];
return ptr;
}
// main 函数中
// free(ptr);
// 改为
// delete [] (char *)ptr;
检测结果:
stack_id=0x3d21 with outstanding allocations: total_size=8 nr_allocs=2
0 [<00007f71f1a1a298>] operator new(unsigned long)+0x18
1 [<000055deb2ba2741>] alloc_v2(int)+0x15 test_memleak.cpp:33
2 [<000055deb2ba2760>] alloc_v1(int)+0x15 test_memleak.cpp:40
3 [<000055deb2ba27a0>] main+0x36 test_memleak.cpp:53
4 [<00007f71f15b7c87>] __libc_start_main+0xe7
5 [<05a6258d4c544155>]
打印的堆栈不完整,没看到调用 new 的接口,也没有看到 new 对应的文件名和行号,用 ldd
命令查看测试程序使用的libstdc++
库:
# ldd test_memleak
linux-vdso.so.1 (0x00007fff74deb000)
libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f50e191e000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f50e152d000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f50e118f000)
/lib64/ld-linux-x86-64.so.2 (0x00007f50e1ea9000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f50e0f77000)
# ls -la /usr/lib/x86_64-linux-gnu/libstdc++.so.6
lrwxrwxrwx 1 root root 19 3月 10 2020 /usr/lib/x86_64-linux-gnu/libstdc++.so.6 -> libstdc++.so.6.0.25
# file /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.25
/usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.25: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, BuildID[sha1]=f2119a44a99758114620c8e9d8e243d7094f77f6, stripped
## 可以看到 libstdc++.so.6.0.25 被striped掉了,堆栈不完整应该是这个原因;
如何实现可执行文件一启动就开始内存泄露检测?
上图左边就是现在的内存泄露检测启动流程;
如何实现上图右边的启动流程?
参考 https://www.kernel.org/doc/html/latest/trace/ftrace.html 中的 Single thread tracing
章节,写个内存泄露检测的启动脚本start_memleak.sh
:
#!/bin/bash
# 使用示例:
# sh start_memleak.sh ./test/test_memleak
# $$ 表示脚本运行的当前进程ID号,使用示例中的 test_memleak 启动时,就会继承这个进程ID
# 启动内存泄露检测工具 memleak 时,就可以预先知道测试程序 test_memleak 的进程ID
sudo ./memleak $$ &
# $@ 表示传给脚本的所有参数的列表,即使用示例中的 ./test/test_memleak
exec "$@"
这个shell脚本就可以先启动 memleak
再启动测试程序 test_memleak
;
因为 memleak
用的是sudo
,且在后台运行,结束后需要执行 sudo killall memleak
把 memleak
进程退出;