notes/article/ebpf/eBPF 内存泄露检测原理.md

215 lines
5.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# eBPF 内存泄露检测原理
[toc]
## 内存泄露检测的任务
```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static void * alloc_v3(int alloc_size)
{
void * ptr = malloc(alloc_size);
return ptr;
}
static void * alloc_v2(int alloc_size)
{
void * ptr = alloc_v3(alloc_size);
return ptr;
}
static void * alloc_v1(int alloc_size)
{
void * ptr = alloc_v2(alloc_size);
return ptr;
}
int main(int argc, char * argv[])
{
const int alloc_size = 4;
void * ptr = NULL;
int i = 0;
for (i = 0; ; i++)
{
ptr = alloc_v1(alloc_size);
sleep(2);
if (0 == i % 2)
{
free(ptr);
}
}
return 0;
}
```
内存泄露检测的任务,需要输出下面的报告:
```
stack_id=0x3f3 with outstanding allocations: total_size=12 nr_alloc=3
0 [<0000555b65a096f2>] alloc_v3+0x18 test_memleak.c:8
1 [<0000555b65a09711>] alloc_v2+0x15 test_memleak.c:15
2 [<0000555b65a09730>] alloc_v1+0x15 test_memleak.c:22
3 [<0000555b65a09770>] main+0x36 test_memleak.c:35
4 [<00007fe8c8a7bc87>] __libc_start_main+0xe7
5 [<05f6258d4c544155>]
```
报告的内容包含下面3个信息
1. 定位到内存泄露的代码段:函数名,文件名,行号;
2. 泄露的内存总大小;
3. 内存泄露的次数;
## ebpf如何获取用户态堆栈
ebpf获取到的堆栈示例一组指令地址
```
//stack_id=0x3f3
[0000555b65a096f2,
0000555b65a09711,
0000555b65a09730,
0000555b65a09770,
00007fe8c8a7bc87,
05f6258d4c544155]
```
第1种方法获取堆栈和`stack_id`
```c
////// 内核态的ebpf
long bpf_get_stackid(void *ctx, void *map, __u64 flags);
/* stack_traces 是 BPF_MAP_TYPE_STACK_TRACE 类型的 maps
* flags = 0 | BPF_F_FAST_STACK_CMP 表示获取内核态的堆栈
* flags = 0 | BPF_F_FAST_STACK_CMP | BPF_F_USER_STACK 表示获取用户态的堆栈
*/
stack_id = bpf_get_stackid(ctx, &stack_traces, 0 | BPF_F_FAST_STACK_CMP | BPF_F_USER_STACK);
////// 用户态的ebpf
/* 通过 bpf_map__lookup_elem 接口在 stack_traces maps中查找 stack_id 对应的 完整堆栈信息
* stack_traces 是内核态ebpf代码中定义的maps
* stack_id 是内核态ebpf通过 bpf_get_stackid 接口获取到的堆栈ID
* stack_key_size 是stack_traces这个maps的key类型大小
* out_stacks_arry 是用来存储 stack_id 对应的完整调用栈(一组指令地址)
* out_stacks_arry_size 是 out_stacks_arry 的数组长度
*/
bpf_map__lookup_elem(skel->maps.stack_traces, \
&stack_id, stack_key_size, \
out_stacks_arry, out_stacks_arry_size, 0);
```
第2种方法 只获取堆栈,不计算 `stack_id`
```c
//参考libbpf-bootstrap/examples/c/profile.bpf.c
long bpf_get_stack(void *ctx, void *buf, __u32 size, __u64 flags);
```
`BPF_MAP_TYPE_STACK_TRACE` 的解释:
`http://arthurchiao.art/blog/bpf-advanced-notes-2-zh/#1-bpf_map_type_stack_trace`
```
内核程序能通过 bpf_get_stackid() helper 存储 stack 信息。
将 stack 信息关联到一个 id而这个 id 是对当前栈的
指令指针地址instruction pointer address进行 32-bit hash 得到的。
```
## 内存泄露检测原理
以 malloc 和 free 为例:
```c
void *malloc(size_t size);
void free(void *ptr);
```
从 malloc 调用中可以提取到3个关键信息
- malloc 的内存大小 size (通过 uprobe 获取)
- malloc 返回的内存指针 ptr (通过 uretprobe 获取)
- 执行到 malloc 的 `stack_id`(在 uretprobe 中通过 `bpf_get_stackid` 接口获取)
从 free 调用中可以提取最关注的1个信息
- free 释放的内存指针 ptr通过 uprobe 获取)
基本思路:
在每一个 malloc 调用的地方都记录一个统计信息,包括分配的内存总大小 和 分配次数
(如果代码中有2个不同地方调用malloc就会有2个不同的统计信息)
- 如果 malloc 成功更新统计信息增加内存总大小分配次数加1
- 如果 free 成功更新统计信息减少内存总大小分配次数减1
最后通过遍历每一个malloc调用地方记录的统计信息如果内存总大小或者分配次数不是0就是有内存泄露
```c
static void * foo(int alloc_size)
{
/* 1. alloc_size
* 2. ptr
* 3. 调用到这的 stack_id1
* 统计信息1: 每一个malloc都对应1个stack_id, 每一个stack_id都对应1个统计信息
*/
void * ptr = malloc(alloc_size);
return ptr;
}
static void * bar(int alloc_size)
{
/* 1. alloc_size
* 2. ptr
* 3. 调用到这的 stack_id2
* 统计信息2: 每一个malloc都对应1个stack_id, 每一个stack_id都对应1个统计信息
*/
void * ptr = malloc(alloc_size);
return ptr;
}
int main(int argc, char * argv[])
{
void * p1 = foo(4);
void * p2 = bar(8);
// 释放的内存指针 p1
free(p1);
return 0;
}
```
结构体和maps示例图
![memleak处理流程.jpg](./pictures/memleak处理流程.jpg)