Add ebpf
This commit is contained in:
parent
98f4ba76a5
commit
23254b6b17
|
@ -0,0 +1,36 @@
|
||||||
|
# eBPF arm32 平台入门课程
|
||||||
|
|
||||||
|
这个系列视频讲解下使用libbpf在 arm32 平台上开发eBPF工具的过程;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
为什么会有这个系列视频?
|
||||||
|
|
||||||
|
目前工作中使用的比较多的是arm32平台的嵌入式Linux系统,eBPF这么好用,为什么不能移植到arm32平台?
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
因为嵌入式系统上的资源有限,使用 C语言的 libbpf-bootstrap 来开发eBPF程序;
|
||||||
|
|
||||||
|
arm32平台使用的Linux内核版本: 4.9.88
|
||||||
|
|
||||||
|
在移植到arm32平台之前,会在x86-64平台上先跑通;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
课程大纲(暂定):
|
||||||
|
|
||||||
|
1. 基础
|
||||||
|
|
||||||
|
讲解eBPF基础
|
||||||
|
|
||||||
|
2. 示例
|
||||||
|
|
||||||
|
移植 libbpf-bootstrap 自带的示例程序到arm32平台
|
||||||
|
|
||||||
|
3. 实践
|
||||||
|
|
||||||
|
3.1 简单版本的 内存泄露检测 eBPF工具
|
||||||
|
|
||||||
|
3.2 调试驱动的eBPF工具
|
||||||
|
|
|
@ -0,0 +1,214 @@
|
||||||
|
# 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示例图:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,163 @@
|
||||||
|
# eBPF内存泄露检测代码实现<一>
|
||||||
|
|
||||||
|
[toc]
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
这节视频的目标是用ebpf代码实现内存泄露检测工具的第一个简单版本,只检测 malloc 和 free,并打印如下的内存泄露堆栈;(指令地址对应符号名,文件名,行号的解析放到下节视频中讲解)
|
||||||
|
|
||||||
|
```
|
||||||
|
stack_id=0x2d81 with outstanding allocation: total_size=20 nr_alloc=5
|
||||||
|
[ 0] 0x55e8fbd196f2
|
||||||
|
[ 1] 0x55e8fbd19711
|
||||||
|
[ 2] 0x55e8fbd19730
|
||||||
|
[ 3] 0x55e8fbd19770
|
||||||
|
[ 4] 0x7f5bb944ec87
|
||||||
|
[ 5] 0x5f6258d4c544155
|
||||||
|
```
|
||||||
|
|
||||||
|
代码实现参考开源项目:
|
||||||
|
|
||||||
|
https://github.com/eunomia-bpf/bpf-developer-tutorial
|
||||||
|
|
||||||
|
bpf-developer-tutorial/src/16-memleak
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 测试代码
|
||||||
|
|
||||||
|
`test_memleak.c`
|
||||||
|
|
||||||
|
```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;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
编译:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
gcc -g test_memleak.c -o test_memleak
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 内存泄露检测工具的第一个版本
|
||||||
|
|
||||||
|
结构体和maps
|
||||||
|
|
||||||
|
```c
|
||||||
|
#ifndef __MEMLEAK_H
|
||||||
|
#define __MEMLEAK_H
|
||||||
|
|
||||||
|
#define ALLOCS_MAX_ENTRIES 1000000
|
||||||
|
#define COMBINED_ALLOCS_MAX_ENTRIES 10240
|
||||||
|
|
||||||
|
struct alloc_info {
|
||||||
|
__u64 size;
|
||||||
|
int stack_id;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* 为了节省内存和方便整形数据的原子操作,把 combined_alloc_info 定义为联合体
|
||||||
|
* 其中 total_size 占 40bit, number_of_allocs 占 24bit, 联合体总大小为 64bit
|
||||||
|
* 2个combined_alloc_info联合体的 bits 字段相加, 相当于对应的 total_size 相加,
|
||||||
|
* 和对应的 number_of_allocs 相加;
|
||||||
|
*/
|
||||||
|
union combined_alloc_info {
|
||||||
|
struct {
|
||||||
|
__u64 total_size : 40;
|
||||||
|
__u64 number_of_allocs : 24;
|
||||||
|
};
|
||||||
|
__u64 bits;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /* __MEMLEAK_H */
|
||||||
|
```
|
||||||
|
|
||||||
|
```c
|
||||||
|
#define KERN_STACKID_FLAGS (0 | BPF_F_FAST_STACK_CMP)
|
||||||
|
#define USER_STACKID_FLAGS (0 | BPF_F_FAST_STACK_CMP | BPF_F_USER_STACK)
|
||||||
|
|
||||||
|
struct {
|
||||||
|
__uint(type, BPF_MAP_TYPE_HASH);
|
||||||
|
__type(key, pid_t); // pid
|
||||||
|
__type(value, u64); // size for alloc
|
||||||
|
__uint(max_entries, 10240);
|
||||||
|
} sizes SEC(".maps");
|
||||||
|
|
||||||
|
struct {
|
||||||
|
__uint(type, BPF_MAP_TYPE_HASH);
|
||||||
|
__type(key, u64); /* alloc return address */
|
||||||
|
__type(value, struct alloc_info);
|
||||||
|
__uint(max_entries, ALLOCS_MAX_ENTRIES);
|
||||||
|
} allocs SEC(".maps");
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
/* value: stack id 对应的堆栈的深度
|
||||||
|
* max_entries: 最大允许存储多少个stack_id(每个stack id都对应一个完整的堆栈)
|
||||||
|
* 这2个值可以根据应用层的使用场景,在应用层的ebpf中open之后load之前动态设置
|
||||||
|
*/
|
||||||
|
struct {
|
||||||
|
__uint(type, BPF_MAP_TYPE_STACK_TRACE);
|
||||||
|
__type(key, u32); /* stack id */
|
||||||
|
//__type(value, xxx); memleak_bpf__open 之后再动态设置
|
||||||
|
//__uint(max_entries, xxx); memleak_bpf__open 之后再动态设置
|
||||||
|
} stack_traces SEC(".maps");
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
```c
|
||||||
|
static const int perf_max_stack_depth = 127;
|
||||||
|
static const int stack_map_max_entries = 10240;
|
||||||
|
```
|
||||||
|
|
|
@ -0,0 +1,413 @@
|
||||||
|
# eBPF内存泄露检测代码实现<二>
|
||||||
|
|
||||||
|
[toc]
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
把上节视频中获取到的堆栈中的指令地址解析成 `符号名`,`文件名`,`行号`
|
||||||
|
|
||||||
|
```
|
||||||
|
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>]
|
||||||
|
```
|
||||||
|
|
||||||
|
使用 `blazesym` 开源代码来完成解析工作: https://hub.njuu.cf/libbpf/blazesym
|
||||||
|
|
||||||
|
`libbpf-bootstrap` 开源项目中已经包含了 `blazesym` 子项目;
|
||||||
|
|
||||||
|
使用方法请参考: `libbpf-bootstrap/examples/c/profile.c`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**说明:**
|
||||||
|
|
||||||
|
**eBPF内存泄露检测代码实现是在 libbpf-bootstrap 框架下开发,需要的基础知识请参考之前的ebpf系列视频**
|
||||||
|
|
||||||
|
**本节视频使用的是 `Ubuntu18.04 x86-64` 平台**
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 手动解析
|
||||||
|
|
||||||
|
在使用 blazesym 开源代码之前,用手动解析来展示下 `符号名`,`文件名`,`行号` 解析的大概的处理流程;
|
||||||
|
|
||||||
|
需要使用的工具:`readelf` 和 `dwarfdump`
|
||||||
|
|
||||||
|
`readelf` 工具一般Ubuntu等操作系统都会自带,这里就不用开源代码来编译了;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### dwarfdump 是什么?
|
||||||
|
|
||||||
|
dwarf 相关的 `libdwarf` 和 `dwarfdump`
|
||||||
|
|
||||||
|
请参考:https://wiki.dwarfstd.org/Libdwarf_And_Dwarfdump.md
|
||||||
|
|
||||||
|
**dwarf** : 是一种调试信息格式,一般现代的 GCC 和 LLVM 编译器都可以自动生成 `dwarf` 格式的调试信息,调试信息中就包含了 `符号名`,`文件名`,`行号`
|
||||||
|
|
||||||
|
**libdwarf**:是C语言库,用于读写 DWARF2, DWARF3, DWARF4 and DWARF5 格式的调试信息;
|
||||||
|
|
||||||
|
**dwarfdump**:是使用`libdwarf`库开发的开源工具,以人类可读格式打印 `dwarf` 的调试信息;
|
||||||
|
|
||||||
|
使用方法:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
dwarfdump -a test_memleak
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 编译 dwarfdump
|
||||||
|
|
||||||
|
源码下载:https://www.prevanders.net/dwarf.html
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# 下载当前最新的版本, 比如当前最新版本:libdwarf-0.9.0.tar.xz
|
||||||
|
tar -axf libdwarf-0.9.0.tar.xz
|
||||||
|
cd libdwarf-0.9.0
|
||||||
|
./configure --prefix=$PWD/__install
|
||||||
|
make
|
||||||
|
make install
|
||||||
|
# 编译得到的 dwarfdump 和 libdwarf.a 在 libdwarf-0.9.0/__install/ 目录下
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 进程指令地址转换成elf文件指令地址
|
||||||
|
|
||||||
|
`ebpf`获取到的是运行中的进程指令地址,而符号名,文件名,行号 都是存储在`elf`文件中,所以解析时需要把进程指令地址转换成`elf`文件中的指令地址;
|
||||||
|
|
||||||
|
假设测试程序 `test_memleak`的进程号:22487
|
||||||
|
|
||||||
|
ebpf内存泄露检测工具打印出来的堆栈:
|
||||||
|
|
||||||
|
```
|
||||||
|
stack_id=0x2b2d with outstanding allocations: total_size=8 nr_allocs=2
|
||||||
|
[ 0] 0x56076afcb6f2
|
||||||
|
[ 1] 0x56076afcb711
|
||||||
|
[ 2] 0x56076afcb730
|
||||||
|
[ 3] 0x56076afcb770
|
||||||
|
[ 4] 0x7f5b32afac87
|
||||||
|
[ 5] 0x5f6258d4c544155
|
||||||
|
```
|
||||||
|
|
||||||
|
`0x56076afcb6f2` 怎么转换成 `test_memleak` elf文件中的指令地址?
|
||||||
|
|
||||||
|
通过 `cat /proc/进程号/maps` 获取进程号中具有可执行权限的指令地址的 起始地址 和 偏移地址:
|
||||||
|
|
||||||
|
`cat /proc/22487/maps` 如下所示:
|
||||||
|
|
||||||
|
```
|
||||||
|
起始地址 -结束地址 属性 偏移地址 主从设备号 inode编号 文件名
|
||||||
|
56076afcb000-56076afcc000 r-xp 00000000 08:01 32658117 test_memleak
|
||||||
|
56076b1cb000-56076b1cc000 r--p 00000000 08:01 32658117 test_memleak
|
||||||
|
56076b1cc000-56076b1cd000 rw-p 00001000 08:01 32658117 test_memleak
|
||||||
|
56076bd3d000-56076bd5e000 rw-p 00000000 00:00 0 [heap]
|
||||||
|
7f5b32ad9000-7f5b32cc0000 r-xp 00000000 103:02 11558371 /lib/x86_64-linux-gnu/libc-2.27.so
|
||||||
|
7f5b32cc0000-7f5b32ec0000 ---p 001e7000 103:02 11558371 /lib/x86_64-linux-gnu/libc-2.27.so
|
||||||
|
7f5b32ec0000-7f5b32ec4000 r--p 001e7000 103:02 11558371 /lib/x86_64-linux-gnu/libc-2.27.so
|
||||||
|
7f5b32ec4000-7f5b32ec6000 rw-p 001eb000 103:02 11558371 /lib/x86_64-linux-gnu/libc-2.27.so
|
||||||
|
7f5b32ec6000-7f5b32eca000 rw-p 00000000 00:00 0
|
||||||
|
7f5b32eca000-7f5b32ef3000 r-xp 00000000 103:02 11543405 /lib/x86_64-linux-gnu/ld-2.27.so
|
||||||
|
7f5b330c9000-7f5b330cb000 rw-p 00000000 00:00 0
|
||||||
|
7f5b330f3000-7f5b330f4000 r--p 00029000 103:02 11543405 /lib/x86_64-linux-gnu/ld-2.27.so
|
||||||
|
7f5b330f4000-7f5b330f5000 rw-p 0002a000 103:02 11543405 /lib/x86_64-linux-gnu/ld-2.27.so
|
||||||
|
7f5b330f5000-7f5b330f6000 rw-p 00000000 00:00 0
|
||||||
|
7ffd56894000-7ffd568b5000 rw-p 00000000 00:00 0 [stack]
|
||||||
|
7ffd569e0000-7ffd569e3000 r--p 00000000 00:00 0 [vvar]
|
||||||
|
7ffd569e3000-7ffd569e5000 r-xp 00000000 00:00 0 [vdso]
|
||||||
|
7fffffffe000-7ffffffff000 --xp 00000000 00:00 0 [uprobes]
|
||||||
|
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]
|
||||||
|
```
|
||||||
|
|
||||||
|
test_memleak 具有可执行权限的 起始地址=`0x56076afcb000` 偏移地址=`0x00000000`
|
||||||
|
|
||||||
|
参考 `blazesym/src/normalize/user.rs` 文件中的 normalize_elf_addr 接口中的计算公式:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let file_off = virt_addr as u64 - entry.range.start as u64 + entry.offset;
|
||||||
|
```
|
||||||
|
|
||||||
|
elf文件中的指令地址 = 进程中的指令地址 - 起始地址 + 偏移地址
|
||||||
|
|
||||||
|
`0x56076afcb6f2` 对应的elf文件指令地址 = `0x56076afcb6f2 - 0x56076afcb000 + 0x00000000` = `0x6f2`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### readelf 解析符号名
|
||||||
|
|
||||||
|
`readelf -s test_memleak | grep FUNC` 获取测试程序 `test_memleak` 的符号表中类型为 `FUNC` 的 `entries`
|
||||||
|
|
||||||
|
如下所示:
|
||||||
|
|
||||||
|
```
|
||||||
|
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND free@GLIBC_2.2.5 (2)
|
||||||
|
3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
|
||||||
|
5: 0000000000000000 0 FUNC GLOBAL DEFAULT UND malloc@GLIBC_2.2.5 (2)
|
||||||
|
7: 0000000000000000 0 FUNC GLOBAL DEFAULT UND sleep@GLIBC_2.2.5 (2)
|
||||||
|
8: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2.5 (2)
|
||||||
|
32: 0000000000000600 0 FUNC LOCAL DEFAULT 14 deregister_tm_clones
|
||||||
|
33: 0000000000000640 0 FUNC LOCAL DEFAULT 14 register_tm_clones
|
||||||
|
34: 0000000000000690 0 FUNC LOCAL DEFAULT 14 __do_global_dtors_aux
|
||||||
|
37: 00000000000006d0 0 FUNC LOCAL DEFAULT 14 frame_dummy
|
||||||
|
40: 00000000000006da 34 FUNC LOCAL DEFAULT 14 alloc_v3
|
||||||
|
41: 00000000000006fc 31 FUNC LOCAL DEFAULT 14 alloc_v2
|
||||||
|
42: 000000000000071b 31 FUNC LOCAL DEFAULT 14 alloc_v1
|
||||||
|
51: 0000000000000810 2 FUNC GLOBAL DEFAULT 14 __libc_csu_fini
|
||||||
|
52: 0000000000000000 0 FUNC GLOBAL DEFAULT UND free@@GLIBC_2.2.5
|
||||||
|
56: 0000000000000814 0 FUNC GLOBAL DEFAULT 15 _fini
|
||||||
|
57: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@@GLIBC_
|
||||||
|
62: 00000000000007a0 101 FUNC GLOBAL DEFAULT 14 __libc_csu_init
|
||||||
|
63: 0000000000000000 0 FUNC GLOBAL DEFAULT UND malloc@@GLIBC_2.2.5
|
||||||
|
65: 00000000000005d0 43 FUNC GLOBAL DEFAULT 14 _start
|
||||||
|
67: 000000000000073a 96 FUNC GLOBAL DEFAULT 14 main
|
||||||
|
70: 0000000000000000 0 FUNC GLOBAL DEFAULT UND sleep@@GLIBC_2.2.5
|
||||||
|
71: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@@GLIBC_2.2
|
||||||
|
72: 0000000000000560 0 FUNC GLOBAL DEFAULT 11 _init
|
||||||
|
```
|
||||||
|
|
||||||
|
`0x56076afcb6f2` 对应的elf文件指令地址 = `0x6f2`
|
||||||
|
|
||||||
|
`00000000000006da` (alloc_v3) < `6f2` < `00000000000006fc` (alloc_v2)
|
||||||
|
|
||||||
|
所以 `0x56076afcb6f2` 是执行到了 `alloc_v3` 这个符号名(函数)的内部了;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### dwarfdump 解析文件名和行号
|
||||||
|
|
||||||
|
`dwarfdump -i test_memleak | grep DW_TAG_subprogram -A11` 获取测试程序 `test_memleak` 中函数相关的调试信息,如下所示:
|
||||||
|
|
||||||
|
```
|
||||||
|
< 1><0x00000350> DW_TAG_subprogram
|
||||||
|
DW_AT_external yes(1)
|
||||||
|
DW_AT_name main
|
||||||
|
DW_AT_decl_file 0x00000001 /home/zhanglong/Desktop/ebpf/note/src/x86-64/libbpf-bootstrap/examples/c/test/test_memleak.c
|
||||||
|
DW_AT_decl_line 0x0000001b
|
||||||
|
DW_AT_prototyped yes(1)
|
||||||
|
DW_AT_type <0x00000062>
|
||||||
|
DW_AT_low_pc 0x0000073a
|
||||||
|
DW_AT_high_pc <offset-from-lowpc> 96 <highpc: 0x0000079a>
|
||||||
|
DW_AT_frame_base len 0x0001: 0x9c:
|
||||||
|
DW_OP_call_frame_cfa
|
||||||
|
DW_AT_GNU_all_tail_call_sites yes(1)
|
||||||
|
--
|
||||||
|
< 1><0x000003b6> DW_TAG_subprogram
|
||||||
|
DW_AT_name alloc_v1
|
||||||
|
DW_AT_decl_file 0x00000001 /home/zhanglong/Desktop/ebpf/note/src/x86-64/libbpf-bootstrap/examples/c/test/test_memleak.c
|
||||||
|
DW_AT_decl_line 0x00000014
|
||||||
|
DW_AT_prototyped yes(1)
|
||||||
|
DW_AT_type <0x0000008b>
|
||||||
|
DW_AT_low_pc 0x0000071b
|
||||||
|
DW_AT_high_pc <offset-from-lowpc> 31 <highpc: 0x0000073a>
|
||||||
|
DW_AT_frame_base len 0x0001: 0x9c:
|
||||||
|
DW_OP_call_frame_cfa
|
||||||
|
DW_AT_GNU_all_tail_call_sites yes(1)
|
||||||
|
DW_AT_sibling <0x000003f4>
|
||||||
|
--
|
||||||
|
< 1><0x000003f4> DW_TAG_subprogram
|
||||||
|
DW_AT_name alloc_v2
|
||||||
|
DW_AT_decl_file 0x00000001 /home/zhanglong/Desktop/ebpf/note/src/x86-64/libbpf-bootstrap/examples/c/test/test_memleak.c
|
||||||
|
DW_AT_decl_line 0x0000000d
|
||||||
|
DW_AT_prototyped yes(1)
|
||||||
|
DW_AT_type <0x0000008b>
|
||||||
|
DW_AT_low_pc 0x000006fc
|
||||||
|
DW_AT_high_pc <offset-from-lowpc> 31 <highpc: 0x0000071b>
|
||||||
|
DW_AT_frame_base len 0x0001: 0x9c:
|
||||||
|
DW_OP_call_frame_cfa
|
||||||
|
DW_AT_GNU_all_tail_call_sites yes(1)
|
||||||
|
DW_AT_sibling <0x00000432>
|
||||||
|
--
|
||||||
|
< 1><0x00000432> DW_TAG_subprogram
|
||||||
|
DW_AT_name alloc_v3
|
||||||
|
DW_AT_decl_file 0x00000001 /home/zhanglong/Desktop/ebpf/note/src/x86-64/libbpf-bootstrap/examples/c/test/test_memleak.c
|
||||||
|
DW_AT_decl_line 0x00000006
|
||||||
|
DW_AT_prototyped yes(1)
|
||||||
|
DW_AT_type <0x0000008b>
|
||||||
|
DW_AT_low_pc 0x000006da
|
||||||
|
DW_AT_high_pc <offset-from-lowpc> 34 <highpc: 0x000006fc>
|
||||||
|
DW_AT_frame_base len 0x0001: 0x9c:
|
||||||
|
DW_OP_call_frame_cfa
|
||||||
|
DW_AT_GNU_all_tail_call_sites yes(1)
|
||||||
|
< 2><0x0000044f> DW_TAG_formal_parameter
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
`DW_AT_low_pc` 和 `DW_AT_high_pc` 描述了 `DW_AT_name` 函数的指令地址范围:
|
||||||
|
|
||||||
|
`DW_AT_name`=`alloc_v3` 的指令地址范围: `0x000006da` 到 `0x000006fc`
|
||||||
|
|
||||||
|
`0x56076afcb6f2` 对应的elf文件指令地址 = `0x6f2`
|
||||||
|
|
||||||
|
`0x000006da` < `0x6f2` < `0x000006fc`
|
||||||
|
|
||||||
|
所以 `0x56076afcb6f2` 执行到了 `alloc_v3` 函数内部,查找 `alloc_v3` 对应的 `DW_AT_decl_file` 值,即可得知是
|
||||||
|
|
||||||
|
`test_memleak.c`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
`dwarfdump -l test_memleak` 获取测试程序 `test_memleak` 中行号相关的调试信息,如下所示:
|
||||||
|
|
||||||
|
```
|
||||||
|
.debug_line: line number info for a single cu
|
||||||
|
Source lines (from CU-DIE at .debug_info offset 0x0000000b):
|
||||||
|
|
||||||
|
NS new statement, BB new basic block, ET end of text sequence
|
||||||
|
PE prologue end, EB epilogue begin
|
||||||
|
IS=val ISA number, DI=val discriminator value
|
||||||
|
<pc> [lno,col] NS BB ET PE EB IS= DI= uri: "filepath"
|
||||||
|
0x000006da [ 7, 0] NS uri: "/home/zhanglong/Desktop/ebpf/note/src/x86-64/libbpf-bootstrap/examples/c/test/test_memleak.c"
|
||||||
|
0x000006e5 [ 8, 0] NS
|
||||||
|
0x000006f6 [ 10, 0] NS
|
||||||
|
0x000006fa [ 11, 0] NS
|
||||||
|
0x000006fc [ 14, 0] NS
|
||||||
|
0x00000707 [ 15, 0] NS
|
||||||
|
0x00000715 [ 17, 0] NS
|
||||||
|
0x00000719 [ 18, 0] NS
|
||||||
|
0x0000071b [ 21, 0] NS
|
||||||
|
0x00000726 [ 22, 0] NS
|
||||||
|
0x00000734 [ 24, 0] NS
|
||||||
|
0x00000738 [ 25, 0] NS
|
||||||
|
0x0000073a [ 28, 0] NS
|
||||||
|
0x00000749 [ 29, 0] NS
|
||||||
|
0x00000750 [ 30, 0] NS
|
||||||
|
0x00000758 [ 31, 0] NS
|
||||||
|
0x0000075f [ 33, 0] NS
|
||||||
|
0x00000766 [ 35, 0] NS
|
||||||
|
0x00000774 [ 37, 0] NS
|
||||||
|
0x0000077e [ 39, 0] NS
|
||||||
|
0x00000788 [ 41, 0] NS
|
||||||
|
0x00000794 [ 33, 0] NS
|
||||||
|
0x00000798 [ 35, 0] NS
|
||||||
|
0x0000079a [ 35, 0] NS ET
|
||||||
|
```
|
||||||
|
|
||||||
|
`0x000006e5 [ 8, 0] NS` 表示指令地址 `0x000006e5` 行号是 第8行
|
||||||
|
|
||||||
|
`0x56076afcb6f2` 对应的elf文件指令地址 = `0x6f2` ,通过二分查找法可知,
|
||||||
|
|
||||||
|
`0x000006e5` < `0x6f2` < `0x000006f6`
|
||||||
|
|
||||||
|
`0x000006e5` 指令地址属于 第 8 行,
|
||||||
|
|
||||||
|
所以 `0x56076afcb6f2` 指令地址执行到了 第 8 行;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## blazesym 自动解析
|
||||||
|
|
||||||
|
### rust 语言编译环境安装
|
||||||
|
|
||||||
|
`blazesym` 使用 `rust` 语言编写,使用前需要安装 `rust` 语言的编译环境
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# 安装前先配置国内镜像源(以下只是示例),可以加速下载
|
||||||
|
# 设置环境变量 RUSTUP_DIST_SERVER (用于更新 toolchain):
|
||||||
|
export RUSTUP_DIST_SERVER=https://mirrors.ustc.edu.cn/rust-static
|
||||||
|
# RUSTUP_UPDATE_ROOT (用于更新 rustup):
|
||||||
|
export RUSTUP_UPDATE_ROOT=https://mirrors.ustc.edu.cn/rust-static/rustup
|
||||||
|
|
||||||
|
# 安装 https://www.rust-lang.org/tools/install
|
||||||
|
# 请 不要 使用Ubuntu的安装命令: sudo apt install cargo,否则可能会出现莫名其妙的问题
|
||||||
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||||
|
|
||||||
|
# 修改 ~/.cargo/config 文件,配置 rust 使用的国内镜像源,这部分请自行上网查找
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### `blazesym` 编译
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cd libbpf-bootstrap/blazesym
|
||||||
|
cargo build --release
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### `blazesym` 命令行解析
|
||||||
|
|
||||||
|
假设 `rust` 使用的 `cargo` 可执行文件的绝对路径: `/home/zhanglong/.cargo/bin/cargo`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
进程指令地址解析,假设:
|
||||||
|
|
||||||
|
进程号:22487
|
||||||
|
|
||||||
|
进程指令地址:`0x56076afcb6f2`
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cd libbpf-bootstrap/blazesym
|
||||||
|
sudo /home/zhanglong/.cargo/bin/cargo run -p blazecli -- symbolize process \
|
||||||
|
--pid 28473 0x56076afcb6f2
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
elf文件指令地址解析,假设:
|
||||||
|
|
||||||
|
elf文件绝对路径:
|
||||||
|
|
||||||
|
`/home/zhanglong/Desktop/ebpf/note/src/x86-64/libbpf-bootstrap/examples/c/test/test_memleak`
|
||||||
|
|
||||||
|
elf文件指令地址: `0x6f2`
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cd libbpf-bootstrap/blazesym
|
||||||
|
sudo /home/zhanglong/.cargo/bin/cargo run -p blazecli -- symbolize elf \
|
||||||
|
--path /home/zhanglong/Desktop/ebpf/note/src/x86-64/libbpf-bootstrap/examples/c/test/test_memleak \
|
||||||
|
0x6f2
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## memleak中使用blazesym
|
||||||
|
|
||||||
|
使用方法参考: `libbpf-bootstrap/examples/c/profile.c`
|
||||||
|
|
||||||
|
```c
|
||||||
|
// memleak.c 文件中包含头文件
|
||||||
|
#include <assert.h>
|
||||||
|
#include "blazesym.h"
|
||||||
|
|
||||||
|
// 拷贝 libbpf-bootstrap/examples/c/profile.c 文件中的
|
||||||
|
// symbolizer 对象 和 show_stack_trace 接口
|
||||||
|
// 到 memleak.c
|
||||||
|
static struct blaze_symbolizer *symbolizer;
|
||||||
|
static void show_stack_trace(__u64 *stack, int stack_sz, pid_t pid);
|
||||||
|
|
||||||
|
// 在 memleak.c 中的 main 函数中初始化和销毁 symbolizer 对象
|
||||||
|
symbolizer = blaze_symbolizer_new();
|
||||||
|
if (!symbolizer) {
|
||||||
|
fprintf(stderr, "Fail to create a symbolizer\n");
|
||||||
|
err = -1;
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
|
||||||
|
blaze_symbolizer_free(symbolizer);
|
||||||
|
|
||||||
|
// 修改 libbpf-bootstrap/examples/c/Makefile 文件
|
||||||
|
// APPS 变量中去掉 memleak, BZS_APPS 变量中加上 memleak
|
||||||
|
// 编译 memleak 时,就会自动去编译 blazesym 的 C lib库
|
||||||
|
|
||||||
|
// 编译 memleak
|
||||||
|
cd libbpf-bootstrap/examples/c
|
||||||
|
make clean
|
||||||
|
make memleak
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### blazesym 说明
|
||||||
|
|
||||||
|
1. `blazesym ` 开源代码目前仅支持 `ELF64` 文件的解析,比如 `x86-64` 和 `arm64` 平台的 `elf` 都可以正常解析,但是还不支持 `ELF32` 文件的解析,比如 `arm32` 平台的 `elf` 就解析不了
|
||||||
|
2. `blazesym` 为了解析的效率,会缓存整个`elf`文件和符号表,会导致使用 `blazesym` 的 `ebpf` 程序运行后消耗很多内存,如果是在内存紧张的产品上调试内存泄漏,`ebpf`程序可以不解析`符号名`,`文件名`,`行号`,只打印堆栈中的指令地址和 `/proc/进程ID/maps`,然后再在内存充足的PC机上使用上面的手动解析方法对`elf`文件进行指令地址的解析(使用 shell 或者 python 批量处理?);
|
||||||
|
3. 如果`elf`可执行文件编译时没有 `-g` 选项,但是没有 `strip`,`blazesym` 就只能解析到 `符号名`,解析不了 `文件名` 和 `行号`; 如果被 `strip` 处理了,那 `符号名`,`文件名`,`行号` 都解析不了;
|
||||||
|
|
|
@ -0,0 +1,699 @@
|
||||||
|
# eBPF内存泄露检测代码实现<完整版>
|
||||||
|
|
||||||
|
[toc]
|
||||||
|
|
||||||
|
**说明:**
|
||||||
|
|
||||||
|
**eBPF内存泄露检测代码实现是在 libbpf-bootstrap 框架下开发,需要的基础知识请参考之前的ebpf系列视频**
|
||||||
|
|
||||||
|
**本节视频使用的是 `Ubuntu18.04 x86-64` 平台**
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
- 支持如下用户态内存分配接口的内存泄露检测
|
||||||
|
|
||||||
|
```c
|
||||||
|
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` 的参数,
|
||||||
|
|
||||||
|
```c
|
||||||
|
info.stack_id = bpf_get_stackid(ctx, &stack_traces, USER_STACKID_FLAGS);
|
||||||
|
```
|
||||||
|
|
||||||
|
参考 <eBPF示例:x86-64平台上的kprobe> 视频中 `BPF_KPROBE` 宏展开方法,对 `BPF_KRETPROBE` 宏进行展开:
|
||||||
|
|
||||||
|
```c
|
||||||
|
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` 处理逻辑提取成公共代码:
|
||||||
|
|
||||||
|
```c
|
||||||
|
/* 通用的内存分配 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`代码实现:
|
||||||
|
|
||||||
|
```c
|
||||||
|
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 的实现
|
||||||
|
|
||||||
|
```c
|
||||||
|
#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` 替换:
|
||||||
|
|
||||||
|
```c
|
||||||
|
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` 接口定义:
|
||||||
|
|
||||||
|
```c
|
||||||
|
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)`;
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
修改测试代码:
|
||||||
|
|
||||||
|
```c
|
||||||
|
static void * alloc_v3(int alloc_size)
|
||||||
|
{
|
||||||
|
void * memptr = NULL;
|
||||||
|
posix_memalign(&memptr, 128, 1024);
|
||||||
|
return memptr;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
`memleak.bpf.c` 中的代码实现:
|
||||||
|
|
||||||
|
```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 ` 中的代码实现:
|
||||||
|
|
||||||
|
```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);
|
||||||
|
/* ...... */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 内存泄露检测的完整实现
|
||||||
|
|
||||||
|
```c
|
||||||
|
//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 处理逻辑调用的都是之前定义的通用接口:
|
||||||
|
|
||||||
|
```c
|
||||||
|
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` 接口中增加如下代码:
|
||||||
|
|
||||||
|
```c
|
||||||
|
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可以被内存泄露检测吗?
|
||||||
|
|
||||||
|
测试代码:
|
||||||
|
|
||||||
|
```c++
|
||||||
|
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++`库:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# 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`:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
#!/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` 进程退出;
|
||||||
|
|
|
@ -0,0 +1,349 @@
|
||||||
|
# eBPF基础
|
||||||
|
|
||||||
|
[toc]
|
||||||
|
|
||||||
|
### eBPF是什么?
|
||||||
|
|
||||||
|
eBPF 是可以在内核虚拟机中运行的程序,可以在不重启内核,不加载内核模块的情况下动态安全的扩展内核功能;
|
||||||
|
|
||||||
|
BPF 和 eBPF 的区别:
|
||||||
|
|
||||||
|
BPF :
|
||||||
|
|
||||||
|
```tex
|
||||||
|
是 Berkeley Packet Filter(伯克利包过滤)的简写;
|
||||||
|
BPF一般也叫做:cBPF (classic BPF);
|
||||||
|
BPF允许用户空间程序链接到任何的网络套接字上,根据流过网络套接字的数据的类型进行过滤筛选;
|
||||||
|
网络抓包工具:tcpdump 或者 wireshark 就是BPF的经典应用案例;
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
eBPF: [e]xtended BPF
|
||||||
|
|
||||||
|
```
|
||||||
|
eBPF 在 BPF 的基础上进行改进增强:
|
||||||
|
1. 使用的寄存器个数从2个增加到10个
|
||||||
|
2. 寄存器从32bit扩展到64bit
|
||||||
|
3. 512 字节堆栈,无限制大小的 “map” 存储
|
||||||
|
4. 其它
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### eBPF有什么用?
|
||||||
|
|
||||||
|
主要用在:网络,安全,可视化;
|
||||||
|
|
||||||
|
简单的应用可以查看下开源代码: bpf-developer-tutorial 的 sample 程序描述
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### eBPF的数据流?
|
||||||
|
|
||||||
|
以 libbpf 为例:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
eBPF 数据流:
|
||||||
|
|
||||||
|
1. 用C语言编写 eBPF 的代码;
|
||||||
|
2. 用clang编译器把 C语言代码 编译成 eBPF 字节码;
|
||||||
|
3. 通过 bpf 的 syscall 系统调用,把eBPF字节码加载到内核;
|
||||||
|
4. 内核对eBPF字节码进行安全校验;是否有内存越界风险,复杂度是否过高,是否会进入死循环等;
|
||||||
|
5. 内核对eBPF字节码即时编译成本地机器码;
|
||||||
|
6. 把eBPF程序attach到挂接点,比如: system calls, function entry/exit, kernel tracepoints, network events, 等;
|
||||||
|
7. 挂接点上的eBPF程序被触发运行; eBPF 程序都是被动触发运行的;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
1. C语言编写的eBPF代码(这个示例代码是在 `libbpf-bootstrap` 框架下开发的):
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include "vmlinux.h" //需要内核支持BTF
|
||||||
|
#include <bpf/bpf_helpers.h>
|
||||||
|
#include <bpf/bpf_tracing.h>
|
||||||
|
|
||||||
|
// eBPF代码功能:只要应用层打开文件,就会打印文件名
|
||||||
|
// 这个eBPF程序要运行在内核态
|
||||||
|
// 这个eBPF程序有2个section
|
||||||
|
|
||||||
|
// 第1个 section: 指定 license,如果没有指定,内核拒绝加载这个eBPF程序
|
||||||
|
char LICENSE[] SEC("license") = "Dual BSD/GPL";
|
||||||
|
|
||||||
|
// 第2个 section: 在 do_sys_open 内核接口的入口处放置一个kprobe探测点
|
||||||
|
// 只要打开文件,就会触发这个eBPF程序,并且打印这个文件名
|
||||||
|
SEC("kprobe/do_sys_open")
|
||||||
|
int BPF_KPROBE(do_sys_open, int dfd, const char *filename)
|
||||||
|
{
|
||||||
|
bpf_printk("do_sys_open: name=%s\n", filename);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
2. 用clang编译器把 C语言代码 编译成 eBPF 字节码;字节码用elf格式存储;
|
||||||
|
|
||||||
|
2.1 编译:
|
||||||
|
|
||||||
|
`clang -O2 -target bpf -c hello.bpf.c -o hello.bpf.o`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
2.2 查看elf中的Section Headers:
|
||||||
|
|
||||||
|
`llvm-readelf -S hello.bpf.o `
|
||||||
|
|
||||||
|
```tex
|
||||||
|
There are 13 section headers, starting at offset 0x670:
|
||||||
|
|
||||||
|
Section Headers:
|
||||||
|
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
|
||||||
|
[ 0] NULL 0000000000000000 000000 000000 00 0 0 0
|
||||||
|
[ 1] .strtab STRTAB 0000000000000000 0005eb 000082 00 0 0 1
|
||||||
|
[ 2] .text PROGBITS 0000000000000000 000040 000000 00 AX 0 0 4
|
||||||
|
[ 3] kprobe/do_sys_open PROGBITS 0000000000000000 000040 000038 00 AX 0 0 8
|
||||||
|
[ 4] .relkprobe/do_sys_open REL 0000000000000000 000568 000010 10 I 12 3 8
|
||||||
|
[ 5] license PROGBITS 0000000000000000 000078 00000d 00 WA 0 0 1
|
||||||
|
[ 6] .rodata PROGBITS 0000000000000000 000085 000016 00 A 0 0 1
|
||||||
|
[ 7] .BTF PROGBITS 0000000000000000 00009c 0003af 00 0 0 4
|
||||||
|
[ 8] .rel.BTF REL 0000000000000000 000578 000020 10 I 12 7 8
|
||||||
|
[ 9] .BTF.ext PROGBITS 0000000000000000 00044c 00008c 00 0 0 4
|
||||||
|
[10] .rel.BTF.ext REL 0000000000000000 000598 000050 10 I 12 9 8
|
||||||
|
[11] .llvm_addrsig LLVM_ADDRSIG 0000000000000000 0005e8 000003 00 E 0 0 1
|
||||||
|
[12] .symtab SYMTAB 0000000000000000 0004d8 000090 18 1 4 8
|
||||||
|
Key to Flags:
|
||||||
|
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
|
||||||
|
L (link order), O (extra OS processing required), G (group), T (TLS),
|
||||||
|
C (compressed), x (unknown), o (OS specific), E (exclude),
|
||||||
|
R (retain), p (processor specific)
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
2.3 查看clang编译得到eBPF字节码:
|
||||||
|
|
||||||
|
`llvm-objdump -d hello.bpf.o`
|
||||||
|
|
||||||
|
```tex
|
||||||
|
.output/hello.bpf.o: file format elf64-bpf
|
||||||
|
|
||||||
|
Disassembly of section kprobe/do_sys_open:
|
||||||
|
|
||||||
|
0000000000000000 <do_sys_open>:
|
||||||
|
0: 79 13 68 00 00 00 00 00 r3 = *(u64 *)(r1 + 0x68)
|
||||||
|
1: 18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0x0 ll
|
||||||
|
3: b7 02 00 00 16 00 00 00 r2 = 0x16
|
||||||
|
4: 85 00 00 00 06 00 00 00 call 0x6
|
||||||
|
5: b7 00 00 00 00 00 00 00 r0 = 0x0
|
||||||
|
6: 95 00 00 00 00 00 00 00 exit
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
call 0x6:
|
||||||
|
表示调用 在 bpf_helper_defs.h 文件中定义的 bpf_trace_printk 枚举值对应的内核接口
|
||||||
|
这个内核接口是在内核代码中的 linux-5.4.150/kernel/trace/bpf_trace.c 文件中定义:
|
||||||
|
bpf_trace_printk_proto
|
||||||
|
r0 = 0x0:
|
||||||
|
表示把返回值存入 r0 寄存器
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
3. 通过 bpf 的 syscall 系统调用,把eBPF字节码加载到内核;
|
||||||
|
|
||||||
|
```c
|
||||||
|
// libbpf-bootstrap/libbpf/src/bpf.c
|
||||||
|
static inline int sys_bpf(enum bpf_cmd cmd, union bpf_attr *attr,
|
||||||
|
unsigned int size)
|
||||||
|
{
|
||||||
|
return syscall(__NR_bpf, cmd, attr, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int sys_bpf_fd(enum bpf_cmd cmd, union bpf_attr *attr,
|
||||||
|
unsigned int size)
|
||||||
|
{
|
||||||
|
int fd;
|
||||||
|
|
||||||
|
fd = sys_bpf(cmd, attr, size);
|
||||||
|
return ensure_good_fd(fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
int sys_bpf_prog_load(union bpf_attr *attr, unsigned int size, int attempts)
|
||||||
|
{
|
||||||
|
int fd;
|
||||||
|
|
||||||
|
do {
|
||||||
|
fd = sys_bpf_fd(BPF_PROG_LOAD, attr, size);
|
||||||
|
} while (fd < 0 && errno == EAGAIN && --attempts > 0);
|
||||||
|
|
||||||
|
return fd;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
4. 内核对eBPF字节码进行安全校验;
|
||||||
|
|
||||||
|
clang 编译通过,并不表示内核就可以检查通过;
|
||||||
|
|
||||||
|
```c
|
||||||
|
SEC("kprobe/vfs_write")
|
||||||
|
int BPF_KPROBE(vfs_write, struct file *file, const char *buf, size_t count, loff_t *pos)
|
||||||
|
{
|
||||||
|
char dst_buf[256];
|
||||||
|
|
||||||
|
/* 把 vfs_write 接口中的 buf 参数中的数据 读到 dst_buf 数组中,读取的数据长度为 count;
|
||||||
|
* count 是个变量,运行过程中,可能会大于 dst_buf 数组长度,会导致数组越界,可能会导致内核崩溃;
|
||||||
|
* 这样的eBPF代码,内核是拒绝执行的;
|
||||||
|
* 如果把count改成常量,比如256,这样就确定不会数组越界,就可以通过内核检查;
|
||||||
|
*/
|
||||||
|
bpf_probe_read_user(dst_buf, count, buf);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
5. 内核对eBPF字节码进行JIT编译成本地机器码
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# 以下命令可以查看加载到内核中的eBPF字节码
|
||||||
|
# 打印内核运行中的eBPF id
|
||||||
|
sudo bpftool prog show
|
||||||
|
|
||||||
|
# 打印 id = 53 的eBPF程序的汇编指令
|
||||||
|
sudo bpftool prog dump xlated id 53
|
||||||
|
sudo bpftool prog dump xlated id 53 opcodes # 增加 opcode(字节码) 的打印
|
||||||
|
|
||||||
|
# JIT 编译成本地机器码,暂时还没办法看到
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
6. 把eBPF程序attach到 `do_sys_open`
|
||||||
|
|
||||||
|
7. 挂接点上的eBPF程序被触发运行
|
||||||
|
|
||||||
|
观察下 eBPF 程序的输出结果;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
maps的作用:
|
||||||
|
|
||||||
|
负责内核层和应用层之间的数据交互,比如:
|
||||||
|
|
||||||
|
应用层通过map获取内核层eBPF程序收集到的数据;
|
||||||
|
|
||||||
|
应用层把参数通过map传递给内核层的eBPF程序;
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include "vmlinux.h" //需要内核支持BTF
|
||||||
|
#include <bpf/bpf_helpers.h>
|
||||||
|
#include <bpf/bpf_tracing.h>
|
||||||
|
|
||||||
|
// 现在的eBPF程序,不管什么进程打开文件,都会打印文件名称出来,
|
||||||
|
// 如果只打印用户指定进程ID打开的文件名称,要怎么做?
|
||||||
|
// 用户态程序通过map,把指定的进程ID传给内核态的eBPF程序
|
||||||
|
// 如果调用 do_sys_open 接口的当前进程ID不等于用户指定的进程ID,就不输出打印,否则打印;
|
||||||
|
|
||||||
|
// 定义1个数组类型的map,数组长度为1,key 为数组下标,value是用户指定的PID(进程ID)
|
||||||
|
struct {
|
||||||
|
__uint(type, BPF_MAP_TYPE_ARRAY);
|
||||||
|
__uint(max_entries, 1);
|
||||||
|
__type(key, u32); // array数组下标
|
||||||
|
__type(value, pid_t); // 用户指定的pid
|
||||||
|
} my_pid_map SEC(".maps");
|
||||||
|
|
||||||
|
char LICENSE[] SEC("license") = "Dual BSD/GPL";
|
||||||
|
|
||||||
|
SEC("kprobe/do_sys_open")
|
||||||
|
int BPF_KPROBE(do_sys_open, int dfd, const char *filename)
|
||||||
|
{
|
||||||
|
u32 index = 0;
|
||||||
|
|
||||||
|
// 获取调用 do_sys_open 内核接口的当前进程ID
|
||||||
|
pid_t pid = bpf_get_current_pid_tgid() >> 32;
|
||||||
|
|
||||||
|
// 查找数组下标为0的数组元素值,即应用层传下来的 PID
|
||||||
|
pid_t *my_pid = bpf_map_lookup_elem(&my_pid_map, &index);
|
||||||
|
|
||||||
|
// 如果当前进程ID不等于应用层指定的PID,就不打印
|
||||||
|
if (!my_pid || *my_pid != pid)
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
bpf_printk("do_sys_open: name=%s\n", filename);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
eBPF map的类型介绍,使用场景,程序示例,请参考:
|
||||||
|
|
||||||
|
http://arthurchiao.art/blog/bpf-advanced-notes-1-zh/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### eBPF的开发框架有哪些?
|
||||||
|
|
||||||
|
eBPF程序开发过程涉及 内核态代码编写,用户态代码编写,编译,加载,应用层和内核层数据交互等,很多繁琐的事情就可以交给开发框架来处理;目前主要有:
|
||||||
|
|
||||||
|
bcc,bpftrace,libbpf-bootstrap,Go eBPF library
|
||||||
|
|
||||||
|
bcc 和 bpftrace 是:python语言库;
|
||||||
|
|
||||||
|
Go eBPF library 是:GO语言库;
|
||||||
|
|
||||||
|
libbpf-bootstrap 是:C语言库;
|
||||||
|
|
||||||
|
参考:
|
||||||
|
|
||||||
|
https://ebpf.io/what-is-ebpf/#development-toolchains
|
||||||
|
|
||||||
|
https://github.com/eunomia-bpf/bpf-developer-tutorial
|
||||||
|
|
||||||
|
bpf-developer-tutorial/src/0-introduce/README.md
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### eBPF参考资料
|
||||||
|
|
||||||
|
基础概念:
|
||||||
|
Linux源码/Documentation/networking/filter.txt
|
||||||
|
https://ebpf.io/what-is-ebpf/
|
||||||
|
|
||||||
|
https://www.kernel.org/doc/html/latest/bpf/libbpf/index.html
|
||||||
|
|
||||||
|
开源代码:
|
||||||
|
|
||||||
|
libbpf-bootstrap:https://github.com/libbpf/libbpf-bootstrap
|
||||||
|
|
||||||
|
libbpf-bootstrap 中文介绍:https://blog.csdn.net/sinat_38816924/article/details/122259826
|
||||||
|
|
||||||
|
libbpf-bootstrap 英文原版介绍:https://nakryiko.com/posts/libbpf-bootstrap/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
bpf-developer-tutorial: https://github.com/eunomia-bpf/bpf-developer-tutorial
|
||||||
|
|
||||||
|
bpf-developer-tutorial 的readme文档中有非常丰富的资源介绍;
|
||||||
|
|
||||||
|
其它:
|
||||||
|
|
||||||
|
eBPF C编程:
|
||||||
|
|
||||||
|
https://www.bilibili.com/video/BV1f54y1h74r/?spm_id_from=333.999.0.0&vd_source=5c3a508420a1ab4902225c642abde096
|
||||||
|
|
||||||
|
ebpf工作原理浅析:
|
||||||
|
|
||||||
|
https://www.bilibili.com/video/BV1HW4y1674H/?spm_id_from=333.999.0.0&vd_source=5c3a508420a1ab4902225c642abde096
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,260 @@
|
||||||
|
# eBPF示例:x86-64平台上的kprobe
|
||||||
|
|
||||||
|
[toc]
|
||||||
|
|
||||||
|
## 回顾
|
||||||
|
|
||||||
|
前面2节视频讲了 eBPF基础 和 libbpf-bootstrap基础,这节视频讲下 libbpf-bootstrap中kprobe在x86-64平台上的示例;
|
||||||
|
|
||||||
|
重点讲:
|
||||||
|
|
||||||
|
- BPF_KPROBE 宏展开
|
||||||
|
- 低版本内核,不支持CO-RE,如何重新实现kprobe示例功能
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## kprobe作用
|
||||||
|
|
||||||
|
可以在几乎所有的函数中 **动态** 插入探测点,利用注册的回调函数,知道内核函数是否被调用,被调用上下文,入参以及返回值;
|
||||||
|
|
||||||
|
kretprobe 是在 kprobe 的基础上实现;
|
||||||
|
|
||||||
|
## kprobe原理
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- 如果用户没有注册kprobe探测点,指令流:`指令1(instr1)` 顺序执行到 `指令4(instr4)`
|
||||||
|
- 如果用户注册一个kprobe探测点到`指令2(instr2)`,`指令2`被备份,并把`指令2`的入口点替换为断点指令,断点指令是CPU架构相关,如x86-64是int3,arm是设置一个未定义指令;
|
||||||
|
- 当CPU执行到断点指令时,触发一个 `trap`,在`trap`流程中,
|
||||||
|
- 首先,执行用户注册的 `pre_handler` 回调函数;
|
||||||
|
- 然后,单步执行前面备份的`指令2(instr2)`;
|
||||||
|
- 单步执行完成后,执行用户注册的 `post_handler` 回调函数;
|
||||||
|
- 最后,执行流程回到被探测指令之后的正常流程继续执行;
|
||||||
|
|
||||||
|
参考:
|
||||||
|
|
||||||
|
https://blog.csdn.net/Rong_Toa/article/details/116643875
|
||||||
|
|
||||||
|
https://yoc.docs.t-head.cn/linuxbook/Chapter4/tracing.html
|
||||||
|
|
||||||
|
https://github.com/eunomia-bpf/bpf-developer-tutorial
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## libbpf-bootstrap 中的 kprobe 示例
|
||||||
|
|
||||||
|
通过示例代码来了解kprobe的使用方法:
|
||||||
|
|
||||||
|
```
|
||||||
|
libbpf-bootstrap/examples/c/ 目录下的:
|
||||||
|
kprobe.bpf.c
|
||||||
|
kprobe.c
|
||||||
|
```
|
||||||
|
|
||||||
|
- kprobe.bpf.c 的功能及演示
|
||||||
|
- kprobe示例代码的实现逻辑
|
||||||
|
- BPF_KPROBE 宏展开
|
||||||
|
- 如果不使用CO-RE,要怎么实现原来的功能
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### kprobe.bpf.c 的功能及演示
|
||||||
|
|
||||||
|
删除文件时,就会打印被删除文件的文件名,以及返回值
|
||||||
|
|
||||||
|
```
|
||||||
|
rm-12056 [002] .... 4188.004100: 0: KPROBE ENTRY pid = 12056, filename = a.txt
|
||||||
|
rm-12056 [002] d... 4188.004180: 0: KPROBE EXIT: pid = 12056, ret = 0
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### do_unlinkat 接口定义
|
||||||
|
|
||||||
|
```c
|
||||||
|
// linux-5.4.150/include/linux/syscalls.h
|
||||||
|
extern long do_unlinkat(int dfd, struct filename *name);
|
||||||
|
|
||||||
|
// linux-5.4.150/include/linux/fs.h
|
||||||
|
struct filename {
|
||||||
|
const char *name; /* pointer to actual string */
|
||||||
|
const __user char *uptr; /* original userland pointer */
|
||||||
|
int refcnt;
|
||||||
|
struct audit_names *aname;
|
||||||
|
const char iname[];
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### kprobe示例代码的实现逻辑
|
||||||
|
|
||||||
|
在kprobe.bpf.c 中
|
||||||
|
|
||||||
|
```c
|
||||||
|
SEC("kprobe/do_unlinkat") //在内核的 do_unlinkat 入口处注册一个 kprobe 探测点
|
||||||
|
SEC("kretprobe/do_unlinkat") //在内核的 do_unlinkat 返回时注册一个 kretprobe 探测点
|
||||||
|
|
||||||
|
// 使用 BPF_KPROBE 和 BPF_KRETPROBE 宏来定义探测点的回调函数
|
||||||
|
```
|
||||||
|
|
||||||
|
kprobe.c 中的 open, load, attach
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### BPF_KPROBE 宏展开
|
||||||
|
|
||||||
|
clang编译器预编译的示例:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
clang -E -c test.c -o test.i
|
||||||
|
```
|
||||||
|
|
||||||
|
clang编译器预编译 kprobe.bpf.c
|
||||||
|
|
||||||
|
```shell
|
||||||
|
/home/zhanglong/Desktop/clang-16/clang -g -O2 -target bpf -D__TARGET_ARCH_x86 \
|
||||||
|
-I.output -I../../libbpf/include/uapi -I../../vmlinux/x86/ -I/home/zhanglong/workdir/disk1/workdir/embeded_work/my_note/eBPF/note/src/x86-64/libbpf-bootstrap/blazesym/include -idirafter /home/zhanglong/workdir/disk1/software/llvm-clang/clang+llvm-16.0.0-x86_64-linux-gnu-ubuntu-18.04/lib/clang/16/include -idirafter /usr/local/include -idirafter /usr/include/x86_64-linux-gnu -idirafter /usr/include \
|
||||||
|
-E -c kprobe.bpf.c -o .output/kprobe.tmp.bpf.i
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
```c
|
||||||
|
__attribute__((section("kprobe/do_unlinkat"), used))
|
||||||
|
|
||||||
|
# 相当于kprobe原理中用户定义的回调函数
|
||||||
|
int do_unlinkat(struct pt_regs *ctx);
|
||||||
|
|
||||||
|
static inline int ____do_unlinkat(struct pt_regs *ctx, int dfd, struct filename *name);
|
||||||
|
|
||||||
|
int do_unlinkat(struct pt_regs *ctx)
|
||||||
|
{
|
||||||
|
return ____do_unlinkat(ctx, (void *)((ctx)->di), (void *)((ctx)->si));
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int ____do_unlinkat(struct pt_regs *ctx, int dfd, struct filename *name)
|
||||||
|
{
|
||||||
|
pid_t pid;
|
||||||
|
const char *filename;
|
||||||
|
|
||||||
|
pid = bpf_get_current_pid_tgid() >> 32;
|
||||||
|
filename = ({
|
||||||
|
typeof((name)->name) __r;
|
||||||
|
({
|
||||||
|
bpf_probe_read_kernel((void *)(&__r), sizeof(*(&__r)), (const void *)__builtin_preserve_access_index(&((typeof(((name))))(((name))))->name));
|
||||||
|
});
|
||||||
|
__r;
|
||||||
|
});
|
||||||
|
|
||||||
|
({
|
||||||
|
static const char ____fmt[] = "KPROBE ENTRY pid = %d, filename = %s\n";
|
||||||
|
bpf_trace_printk(____fmt, sizeof(____fmt), pid, filename);
|
||||||
|
});
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- 不借助 BPF_KPROBE 宏要怎么实现?
|
||||||
|
- do_unlinkat 函数名可改吗?
|
||||||
|
- 参数一定要定义2个吗?只有1个行不行?都不要行不行?
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
不借助BPF_KPROBE
|
||||||
|
|
||||||
|
```c
|
||||||
|
SEC("kprobe/do_unlinkat")
|
||||||
|
int do_unlinkat(struct pt_regs *ctx)
|
||||||
|
{
|
||||||
|
pid_t pid;
|
||||||
|
const char *filename;
|
||||||
|
|
||||||
|
struct filename *name = (struct filename *)((ctx)->si);
|
||||||
|
|
||||||
|
pid = bpf_get_current_pid_tgid() >> 32;
|
||||||
|
filename = BPF_CORE_READ(name, name);
|
||||||
|
bpf_printk("KPROBE ENTRY pid = %d, filename = %s\n", pid, filename);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 不使用CO-RE功能
|
||||||
|
|
||||||
|
如果内核版本过低,不支持CO-RE,要怎么实现?
|
||||||
|
|
||||||
|
```c
|
||||||
|
// libbpf-bootstrap/examples/c/.output/bpf/bpf_helper_defs.h
|
||||||
|
long (*bpf_probe_read_user)(void *dst, __u32 size, const void *unsafe_ptr);
|
||||||
|
long (*bpf_probe_read_kernel)(void *dst, __u32 size, const void *unsafe_ptr);
|
||||||
|
long (*bpf_probe_read_user_str)(void *dst, __u32 size, const void *unsafe_ptr);
|
||||||
|
long (*bpf_probe_read_kernel_str)(void *dst, __u32 size, const void *unsafe_ptr);
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
不使用CO-RE功能的代码实现:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
|
||||||
|
/* Copyright (c) 2021 Sartura */
|
||||||
|
#define __KERNEL__ //参考: bpf_tracing.h 中的定义
|
||||||
|
#include <linux/bpf.h> //参考: minimal_legacy.bpf.c
|
||||||
|
#include <bpf/bpf_helpers.h>
|
||||||
|
#include <bpf/bpf_tracing.h>
|
||||||
|
|
||||||
|
//参考: minimal_legacy.bpf.c
|
||||||
|
typedef unsigned int u32;
|
||||||
|
typedef int pid_t;
|
||||||
|
|
||||||
|
//拷贝: 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;
|
||||||
|
};
|
||||||
|
|
||||||
|
char LICENSE[] SEC("license") = "Dual BSD/GPL";
|
||||||
|
|
||||||
|
SEC("kprobe/do_unlinkat")
|
||||||
|
int BPF_KPROBE(do_unlinkat, int dfd, void *name)
|
||||||
|
{
|
||||||
|
pid_t pid;
|
||||||
|
const char *filename;
|
||||||
|
int refcnt;
|
||||||
|
|
||||||
|
pid = bpf_get_current_pid_tgid() >> 32;
|
||||||
|
|
||||||
|
bpf_probe_read_kernel(&filename, sizeof(filename), name+0);
|
||||||
|
bpf_probe_read_kernel(&refcnt, sizeof(refcnt), name+16);
|
||||||
|
|
||||||
|
bpf_printk("KPROBE ENTRY pid = %d, refcnt=%d filename = %s\n", pid, refcnt, filename);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
|
@ -0,0 +1,256 @@
|
||||||
|
# eBPF示例:x86-64平台上的uprobe
|
||||||
|
|
||||||
|
[toc]
|
||||||
|
|
||||||
|
**重点:示例代码中只适用于uprobe没有strip的可执行文件,如果可执行文件被strip了,怎么办?**
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 回顾
|
||||||
|
|
||||||
|
上节视频详细讲解了kprobe的原理和在 libbpf-bootstrap 框架上的示例代码分析;
|
||||||
|
|
||||||
|
这节视频讲下 libbpf-bootstrap 框架上的 uprobe 示例代码;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## libbpf-bootstrap 中的 uprobe 示例代码
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# 代码路径: libbpf-bootstrap/examples/c/
|
||||||
|
uprobe.bpf.c uprobe.c
|
||||||
|
```
|
||||||
|
|
||||||
|
uprobe 要能正常工作,需要满足2个条件:
|
||||||
|
|
||||||
|
- 被attach的接口名称
|
||||||
|
- 接口所在可执行文件路径
|
||||||
|
|
||||||
|
在 libbpf-bootstrap 框架中,有2种写法:
|
||||||
|
|
||||||
|
1. 在内核层的ebpf程序中,通过`SEC()` 给ebpf提供 被attach的接口名称和接口所在的文件路径;
|
||||||
|
2. 在用户层的ebpf程序中,通过 `bpf_program__attach_uprobe_opts` 接口提供 被attach的接口名称和接口所在的文件路径;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 改造示例代码
|
||||||
|
|
||||||
|
为了简单起见,`uprobed_sub` 改成和 `uprobe_add` 同一种写法;
|
||||||
|
|
||||||
|
为了模拟更真实的使用场景,把示例代码中的 `uprobed_add` 和 `uprobed_sub` 独立到一个测试代码中:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
/* It's a global function to make sure compiler doesn't inline it. */
|
||||||
|
int uprobed_add(int a, int b)
|
||||||
|
{
|
||||||
|
return a + b;
|
||||||
|
}
|
||||||
|
|
||||||
|
int uprobed_sub(int a, int b)
|
||||||
|
{
|
||||||
|
return a - b;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char * argv[])
|
||||||
|
{
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
for (i = 0;; i++) {
|
||||||
|
/* trigger our BPF programs */
|
||||||
|
uprobed_add(i, i + 1);
|
||||||
|
uprobed_sub(i * i, i);
|
||||||
|
|
||||||
|
/* 为了好验证结果 */
|
||||||
|
if (i >= 10) {
|
||||||
|
i = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
sleep(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
再改造下 uprobe.c 代码,把 `uprobed_add` 和 `uprobed_sub` 所在的测试代码进程ID通过参数传进来:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
|
||||||
|
/* Copyright (c) 2020 Facebook */
|
||||||
|
#include <errno.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/resource.h>
|
||||||
|
#include <bpf/libbpf.h>
|
||||||
|
#include "uprobe.skel.h"
|
||||||
|
|
||||||
|
static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
|
||||||
|
{
|
||||||
|
return vfprintf(stderr, format, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
struct uprobe_bpf *skel;
|
||||||
|
int err, i, attach_pid;
|
||||||
|
char binary_path[256] = {};
|
||||||
|
LIBBPF_OPTS(bpf_uprobe_opts, uprobe_opts);
|
||||||
|
|
||||||
|
if (2 != argc) {
|
||||||
|
fprintf(stderr, "usage:%s attach_pid\n", argv[0]);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
attach_pid = atoi(argv[1]);
|
||||||
|
sprintf(binary_path, "/proc/%d/exe", attach_pid);
|
||||||
|
|
||||||
|
/* Set up libbpf errors and debug info callback */
|
||||||
|
libbpf_set_print(libbpf_print_fn);
|
||||||
|
|
||||||
|
/* Load and verify BPF application */
|
||||||
|
skel = uprobe_bpf__open_and_load();
|
||||||
|
if (!skel) {
|
||||||
|
fprintf(stderr, "Failed to open and load BPF skeleton\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Attach tracepoint handler */
|
||||||
|
uprobe_opts.func_name = "uprobed_add";
|
||||||
|
uprobe_opts.retprobe = false;
|
||||||
|
/* uprobe/uretprobe expects relative offset of the function to attach
|
||||||
|
* to. libbpf will automatically find the offset for us if we provide the
|
||||||
|
* function name. If the function name is not specified, libbpf will try
|
||||||
|
* to use the function offset instead.
|
||||||
|
*/
|
||||||
|
skel->links.uprobe_add = bpf_program__attach_uprobe_opts(skel->progs.uprobe_add,
|
||||||
|
attach_pid, binary_path,
|
||||||
|
0 /* offset for function */,
|
||||||
|
&uprobe_opts /* opts */);
|
||||||
|
if (!skel->links.uprobe_add) {
|
||||||
|
err = -errno;
|
||||||
|
fprintf(stderr, "Failed to attach uprobe: %d\n", err);
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* we can also attach uprobe/uretprobe to any existing or future
|
||||||
|
* processes that use the same binary executable; to do that we need
|
||||||
|
* to specify -1 as PID, as we do here
|
||||||
|
*/
|
||||||
|
uprobe_opts.func_name = "uprobed_add";
|
||||||
|
uprobe_opts.retprobe = true;
|
||||||
|
skel->links.uretprobe_add = bpf_program__attach_uprobe_opts(
|
||||||
|
skel->progs.uretprobe_add, attach_pid, binary_path,
|
||||||
|
0 /* offset for function */, &uprobe_opts /* opts */);
|
||||||
|
if (!skel->links.uretprobe_add) {
|
||||||
|
err = -errno;
|
||||||
|
fprintf(stderr, "Failed to attach uprobe: %d\n", err);
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Attach tracepoint handler */
|
||||||
|
uprobe_opts.func_name = "uprobed_sub";
|
||||||
|
uprobe_opts.retprobe = false;
|
||||||
|
/* uprobe/uretprobe expects relative offset of the function to attach
|
||||||
|
* to. libbpf will automatically find the offset for us if we provide the
|
||||||
|
* function name. If the function name is not specified, libbpf will try
|
||||||
|
* to use the function offset instead.
|
||||||
|
*/
|
||||||
|
skel->links.uprobe_sub = bpf_program__attach_uprobe_opts(skel->progs.uprobe_sub,
|
||||||
|
attach_pid, binary_path,
|
||||||
|
0 /* offset for function */,
|
||||||
|
&uprobe_opts /* opts */);
|
||||||
|
if (!skel->links.uprobe_sub) {
|
||||||
|
err = -errno;
|
||||||
|
fprintf(stderr, "Failed to attach uprobe: %d\n", err);
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* we can also attach uprobe/uretprobe to any existing or future
|
||||||
|
* processes that use the same binary executable; to do that we need
|
||||||
|
* to specify -1 as PID, as we do here
|
||||||
|
*/
|
||||||
|
uprobe_opts.func_name = "uprobed_sub";
|
||||||
|
uprobe_opts.retprobe = true;
|
||||||
|
skel->links.uretprobe_sub = bpf_program__attach_uprobe_opts(
|
||||||
|
skel->progs.uretprobe_sub, attach_pid, binary_path,
|
||||||
|
0 /* offset for function */, &uprobe_opts /* opts */);
|
||||||
|
if (!skel->links.uretprobe_sub) {
|
||||||
|
err = -errno;
|
||||||
|
fprintf(stderr, "Failed to attach uprobe: %d\n", err);
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Let libbpf perform auto-attach for uprobe_sub/uretprobe_sub
|
||||||
|
* NOTICE: we provide path and symbol info in SEC for BPF programs
|
||||||
|
*/
|
||||||
|
err = uprobe_bpf__attach(skel);
|
||||||
|
if (err) {
|
||||||
|
fprintf(stderr, "Failed to auto-attach BPF skeleton: %d\n", err);
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("Successfully started! Please run `sudo cat /sys/kernel/debug/tracing/trace_pipe` "
|
||||||
|
"to see output of the BPF programs.\n");
|
||||||
|
|
||||||
|
for (i = 0;; i++) {
|
||||||
|
/* trigger our BPF programs */
|
||||||
|
fprintf(stderr, ".");
|
||||||
|
sleep(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup:
|
||||||
|
uprobe_bpf__destroy(skel);
|
||||||
|
return -err;
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 问题:可执行文件被strip,找不到被attach的接口符号,怎么办?
|
||||||
|
|
||||||
|
1. 在 strip 可执行文件前,先备份一下;
|
||||||
|
2. 对没有strip的可执行文件进行反汇编,并查找 uprobe_add 和 uprobe_sub 符号的汇编指令地址:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
objdump -d test_uprobe.unstriped
|
||||||
|
|
||||||
|
000000000000064a <uprobed_add>:
|
||||||
|
000000000000065e <uprobed_sub>:
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 通过命令 `cat /proc/attach_pid/maps` (attach_pid 是uprobe需要跟踪的进程ID) 获取有可执行属性的attach_pid 的偏移地址:
|
||||||
|
|
||||||
|
```
|
||||||
|
起始地址 -结束地址 属性 偏移地址 主从设备号 inode编号 文件名
|
||||||
|
55fde9ec5000-55fde9ec6000 r-xp 00000000 08:01 32672410 test_uprobe
|
||||||
|
55fdea0c5000-55fdea0c6000 r--p 00000000 08:01 32672410 test_uprobe
|
||||||
|
55fdea0c6000-55fdea0c7000 rw-p 00001000 08:01 32672410 test_uprobe
|
||||||
|
7fb077d9b000-7fb077f82000 r-xp 00000000 103:02 11558371 /lib/x86_64-linux-gnu/libc-2.27.so
|
||||||
|
7fb077f82000-7fb078182000 ---p 001e7000 103:02 11558371 /lib/x86_64-linux-gnu/libc-2.27.so
|
||||||
|
7fb078182000-7fb078186000 r--p 001e7000 103:02 11558371 /lib/x86_64-linux-gnu/libc-2.27.so
|
||||||
|
7fb078186000-7fb078188000 rw-p 001eb000 103:02 11558371 /lib/x86_64-linux-gnu/libc-2.27.so
|
||||||
|
7fb078188000-7fb07818c000 rw-p 00000000 00:00 0
|
||||||
|
7fb07818c000-7fb0781b5000 r-xp 00000000 103:02 11543405 /lib/x86_64-linux-gnu/ld-2.27.so
|
||||||
|
7fb07838b000-7fb07838d000 rw-p 00000000 00:00 0
|
||||||
|
7fb0783b5000-7fb0783b6000 r--p 00029000 103:02 11543405 /lib/x86_64-linux-gnu/ld-2.27.so
|
||||||
|
7fb0783b6000-7fb0783b7000 rw-p 0002a000 103:02 11543405 /lib/x86_64-linux-gnu/ld-2.27.so
|
||||||
|
7fb0783b7000-7fb0783b8000 rw-p 00000000 00:00 0
|
||||||
|
7ffed92ea000-7ffed930b000 rw-p 00000000 00:00 0 [stack]
|
||||||
|
7ffed9383000-7ffed9386000 r--p 00000000 00:00 0 [vvar]
|
||||||
|
7ffed9386000-7ffed9388000 r-xp 00000000 00:00 0 [vdso]
|
||||||
|
7fffffffe000-7ffffffff000 --xp 00000000 00:00 0 [uprobes]
|
||||||
|
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]
|
||||||
|
```
|
||||||
|
|
||||||
|
4. uprobe.c 文件中的 `bpf_program__attach_uprobe_opts` 接口:
|
||||||
|
|
||||||
|
```
|
||||||
|
uprobe_opts.func_name 不要赋值,因为二进制文件中已经找不到函数对应的符号
|
||||||
|
func_offset 参数赋值 = 第2步获取的函数符号汇编指令地址 + 第3步获取的偏移地址
|
||||||
|
```
|
||||||
|
|
|
@ -0,0 +1,253 @@
|
||||||
|
# eBPF:arm平台上的uprobe
|
||||||
|
|
||||||
|
[toc]
|
||||||
|
|
||||||
|
这节视频讲下在`arm 32`平台上的 ebpf uprobe 的使用方法;和 x86-64 上的使用方法有好些不同点;
|
||||||
|
|
||||||
|
Linux 内核版本:4.9.88
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 回顾
|
||||||
|
|
||||||
|
在上节视频中,交叉编译了 `zlib`, `libelf`, `libbpf-bootstrap`
|
||||||
|
|
||||||
|
交叉编译前需要先执行 `build_env.sh`
|
||||||
|
|
||||||
|
```shell
|
||||||
|
#!/bin/sh
|
||||||
|
# 指定交叉编译工具链的绝对地址
|
||||||
|
export PATH=$PATH:/home/zhanglong/Desktop/imx6ull_dev/sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/bin
|
||||||
|
|
||||||
|
# 指定目标平台类型
|
||||||
|
export ARCH=arm
|
||||||
|
|
||||||
|
# 指定交叉编译器
|
||||||
|
export CROSS_COMPILE=arm-buildroot-linux-gnueabihf-
|
||||||
|
|
||||||
|
# 指定交叉编译好的 zlib 和 libelf 库的头文件路径
|
||||||
|
export EXTRA_CFLAGS="-I/home/zhanglong/Desktop/ebpf/note/src/arm32/extra_libs/elfutils-0.189/_install/include -I/home/zhanglong/Desktop/ebpf/note/src/arm32/extra_libs/zlib-1.3/_install/include"
|
||||||
|
|
||||||
|
# 指定交叉编译好的 zlib 和 libelf 库(.a)文件路径
|
||||||
|
export EXTRA_LDFLAGS="-L/home/zhanglong/Desktop/ebpf/note/src/arm32/extra_libs/elfutils-0.189/_install/lib -L/home/zhanglong/Desktop/ebpf/note/src/arm32/extra_libs/zlib-1.3/_install/lib"
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
编译
|
||||||
|
|
||||||
|
```shell
|
||||||
|
source build_env.sh # 只需执行一次
|
||||||
|
make clean
|
||||||
|
make uprobe
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 移植 x86-64 平台上的uprobe代码
|
||||||
|
|
||||||
|
1. 把 `eBPF示例:x86-64平台上的uprobe` 这节视频讲解中改造过的代码拷贝过来;
|
||||||
|
|
||||||
|
涉及到3个文件:
|
||||||
|
|
||||||
|
- `test_uprobe.c`
|
||||||
|
|
||||||
|
应用层的测试代码,定义了 `uprobed_add` 和 `uprobed_sub` 2个函数,然后在 `main` 函数中循环调用;
|
||||||
|
|
||||||
|
测试目的:ebpf uprobe 在arm 32平台上是否可以正确获取 `uprobed_add` 和 `uprobed_sub` 2个函数的入口参数和返回值;
|
||||||
|
|
||||||
|
- `uprobe.bpf.c`
|
||||||
|
|
||||||
|
ebpf 在内核层的代码,定义 `uprobe` 探测点的回调函数:
|
||||||
|
|
||||||
|
应用层 `uprobed_add` 函数进入和返回时的回调函数;
|
||||||
|
|
||||||
|
应用层 `uprobed_sub` 函数进入和返回时的回调函数;
|
||||||
|
|
||||||
|
- `uprobe.c`
|
||||||
|
|
||||||
|
ebpf 在应用层的代码,负责把 `uprobe.bpf.c` 编译得到的字节码加载到内核,以及把 `uprobe.bpf.c` 文件中定义的回调函数 attach 到应用层测试代码中函数的 `uprobe` 探测点;
|
||||||
|
|
||||||
|
为了简单起见,不再 `strip` 测试代码的可执行程序,直接通过测试代码中的函数名来 `attach`;(如果对这部分内容不清楚的,可以再回去看看 `eBPF示例:x86-64平台上的uprobe` 这节视频)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
2. 直接编译 测试代码 和 ebpf
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# 编译测试代码
|
||||||
|
arm-buildroot-linux-gnueabihf-gcc test_uprobe.c -o test_uprobe
|
||||||
|
|
||||||
|
# 编译 ebpf
|
||||||
|
make clean
|
||||||
|
make uprobe
|
||||||
|
```
|
||||||
|
|
||||||
|
编译好后,直接运行,报错:
|
||||||
|
|
||||||
|
```
|
||||||
|
libbpf: failed to find valid kernel BTF
|
||||||
|
libbpf: Error loading vmlinux BTF: -3
|
||||||
|
```
|
||||||
|
|
||||||
|
查看BCC文档: https://github.com/iovisor/bcc/blob/master/docs/kernel-versions.md
|
||||||
|
|
||||||
|
BTF 需要 4.18 及以后的内核版本才支持;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
3. 修改代码:
|
||||||
|
|
||||||
|
在 `uprobe.bpf.c` 中,去掉 `#include "vmlinux.h"` ,改为 `#include <linux/bpf.h>`
|
||||||
|
|
||||||
|
再次编译ebpf,报错:
|
||||||
|
|
||||||
|
```
|
||||||
|
uprobe.bpf.c:11:5: error: incomplete definition of type 'struct pt_regs'
|
||||||
|
```
|
||||||
|
|
||||||
|
把 `libbpf-bootstrap/vmlinux/arm/vmlinux.h` 中的 `struct pt_regs` 定义拷贝到 `uprobe.bpf.c` 中;
|
||||||
|
|
||||||
|
再次编译ebpf,并执行,报错:
|
||||||
|
|
||||||
|
```
|
||||||
|
libbpf: prog 'uprobe_add': -- BEGIN PROG LOAD LOG --
|
||||||
|
0: (79) r4 = *(u64 *)(r1 +8)
|
||||||
|
1: (79) r3 = *(u64 *)(r1 +0)
|
||||||
|
2: (85) call 2001000000
|
||||||
|
invalid func 2001000000
|
||||||
|
-- END PROG LOAD LOG --
|
||||||
|
```
|
||||||
|
|
||||||
|
`bpf_helper_defs.h` 头文件中并没有定义 2001000000 的枚举值;
|
||||||
|
|
||||||
|
阅读下 libbpf-bootstrap 开源代码说明文档:https://hub.njuu.cf/libbpf/libbpf-bootstrap
|
||||||
|
|
||||||
|
**minimal_Legacy**
|
||||||
|
|
||||||
|
```
|
||||||
|
This version of minimal is modified to allow running on even older kernels that
|
||||||
|
do not allow global variables.
|
||||||
|
bpf_printk uses global variables unless BPF_NO_GLOBAL_DATA is defined before
|
||||||
|
including bpf_helpers.h.
|
||||||
|
```
|
||||||
|
|
||||||
|
`uprobe.bpf.c` 文件中,增加:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#define BPF_NO_GLOBAL_DATA
|
||||||
|
```
|
||||||
|
|
||||||
|
再次编译,并执行:
|
||||||
|
|
||||||
|
可以正常运行,但是 `uprobed_add` 打印的结果不对;
|
||||||
|
|
||||||
|
按照: `eBPF示例:x86-64平台上的kprobe` 视频中的方法,改写下`int BPF_KPROBE(uprobe_add, int a, int b)`
|
||||||
|
|
||||||
|
```c
|
||||||
|
int uprobe_add(struct pt_regs *ctx)
|
||||||
|
{
|
||||||
|
int a = ctx->uregs[0];
|
||||||
|
int b = ctx->uregs[1];
|
||||||
|
bpf_printk("uprobed_add ENTRY: a = %d, b = %d\n", a, b);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
在 `eBPF基础` 视频中讲过,ebpf寄存器是64 bit,但是arm32平台寄存器是32 bit,这2个事实存在冲突,用代码来验证下;
|
||||||
|
|
||||||
|
```c
|
||||||
|
int uprobe_add(struct pt_regs *ctx)
|
||||||
|
{
|
||||||
|
int a = ctx->uregs[0];
|
||||||
|
int b = ctx->uregs[1];
|
||||||
|
bpf_printk("uprobed_add ENTRY: a = %d, b = %d sizeof(uregs[0])=%d\n", a, b, sizeof(ctx->uregs[0]));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
打印结果
|
||||||
|
uprobed_add ENTRY: a = 10, b = 16 sizeof(uregs[0])=8
|
||||||
|
即内核层ebpf中, long unsigned int 是 64 bit
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
把变量 `a` 和 `b` 用64 bit的格式打印出来:
|
||||||
|
|
||||||
|
```c
|
||||||
|
int uprobe_add(struct pt_regs *ctx)
|
||||||
|
{
|
||||||
|
int a = ctx->uregs[0];
|
||||||
|
int b = ctx->uregs[1];
|
||||||
|
|
||||||
|
bpf_printk("uprobed_add ENTRY: a = 0x%llx, b = 0x%llx\n", a, b);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
打印结果:
|
||||||
|
uprobed_add ENTRY: a = 0x700000006, b = 0x700000010
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
根据打印结果和 `test_uprobe.c` 代码分析可知,变量 a 的低32bit是 `test_uprobe.c` 中 `uprobed_add` 函数的第一个参数,高32bit是第二个参数;
|
||||||
|
|
||||||
|
强制把 `struct pt_regs` 中的 `long unsigned int` 改为 `unsigned int`(强制变为32 bit)
|
||||||
|
|
||||||
|
```c
|
||||||
|
struct pt_regs {
|
||||||
|
unsigned int uregs[18];
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
结果正确;
|
||||||
|
|
||||||
|
再来解决编译警告的问题:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// 修改 libbpf-bootstrap/libbpf/src/bpf_tracing.h
|
||||||
|
#if defined(bpf_target_arm)
|
||||||
|
#define ___bpf_kretprobe_args0() ctx
|
||||||
|
#define ___bpf_kretprobe_args1(x) ___bpf_kretprobe_args0(), (unsigned int)PT_REGS_RC(ctx)
|
||||||
|
#define ___bpf_kretprobe_args(args...) ___bpf_apply(___bpf_kretprobe_args, ___bpf_narg(args))(args)
|
||||||
|
#else
|
||||||
|
#define ___bpf_kretprobe_args0() ctx
|
||||||
|
#define ___bpf_kretprobe_args1(x) ___bpf_kretprobe_args0(), (void *)PT_REGS_RC(ctx)
|
||||||
|
#define ___bpf_kretprobe_args(args...) ___bpf_apply(___bpf_kretprobe_args, ___bpf_narg(args))(args)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(bpf_target_arm)
|
||||||
|
#define ___bpf_kprobe_args0() ctx
|
||||||
|
#define ___bpf_kprobe_args1(x) ___bpf_kprobe_args0(), (unsigned int)PT_REGS_PARM1(ctx)
|
||||||
|
#define ___bpf_kprobe_args2(x, args...) ___bpf_kprobe_args1(args), (unsigned int)PT_REGS_PARM2(ctx)
|
||||||
|
#define ___bpf_kprobe_args3(x, args...) ___bpf_kprobe_args2(args), (unsigned int)PT_REGS_PARM3(ctx)
|
||||||
|
#define ___bpf_kprobe_args4(x, args...) ___bpf_kprobe_args3(args), (unsigned int)PT_REGS_PARM4(ctx)
|
||||||
|
#define ___bpf_kprobe_args5(x, args...) ___bpf_kprobe_args4(args), (unsigned int)PT_REGS_PARM5(ctx)
|
||||||
|
#define ___bpf_kprobe_args6(x, args...) ___bpf_kprobe_args5(args), (unsigned int)PT_REGS_PARM6(ctx)
|
||||||
|
#define ___bpf_kprobe_args7(x, args...) ___bpf_kprobe_args6(args), (unsigned int)PT_REGS_PARM7(ctx)
|
||||||
|
#define ___bpf_kprobe_args8(x, args...) ___bpf_kprobe_args7(args), (unsigned int)PT_REGS_PARM8(ctx)
|
||||||
|
#define ___bpf_kprobe_args(args...) ___bpf_apply(___bpf_kprobe_args, ___bpf_narg(args))(args)
|
||||||
|
#else
|
||||||
|
#define ___bpf_kprobe_args0() ctx
|
||||||
|
#define ___bpf_kprobe_args1(x) ___bpf_kprobe_args0(), (void *)PT_REGS_PARM1(ctx)
|
||||||
|
#define ___bpf_kprobe_args2(x, args...) ___bpf_kprobe_args1(args), (void *)PT_REGS_PARM2(ctx)
|
||||||
|
#define ___bpf_kprobe_args3(x, args...) ___bpf_kprobe_args2(args), (void *)PT_REGS_PARM3(ctx)
|
||||||
|
#define ___bpf_kprobe_args4(x, args...) ___bpf_kprobe_args3(args), (void *)PT_REGS_PARM4(ctx)
|
||||||
|
#define ___bpf_kprobe_args5(x, args...) ___bpf_kprobe_args4(args), (void *)PT_REGS_PARM5(ctx)
|
||||||
|
#define ___bpf_kprobe_args6(x, args...) ___bpf_kprobe_args5(args), (void *)PT_REGS_PARM6(ctx)
|
||||||
|
#define ___bpf_kprobe_args7(x, args...) ___bpf_kprobe_args6(args), (void *)PT_REGS_PARM7(ctx)
|
||||||
|
#define ___bpf_kprobe_args8(x, args...) ___bpf_kprobe_args7(args), (void *)PT_REGS_PARM8(ctx)
|
||||||
|
#define ___bpf_kprobe_args(args...) ___bpf_apply(___bpf_kprobe_args, ___bpf_narg(args))(args)
|
||||||
|
#endif
|
||||||
|
```
|
||||||
|
|
||||||
|
再重新编译 uprobe
|
||||||
|
|
||||||
|
```shell
|
||||||
|
make clean; make uprobe
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**ebpf kprobe 和 uprobe 在arm 32平台的处理方法一样,这里就不再详细讲了;**
|
||||||
|
|
|
@ -0,0 +1,223 @@
|
||||||
|
# libbpf-bootstrap交叉编译
|
||||||
|
|
||||||
|
[toc]
|
||||||
|
|
||||||
|
在嵌入式arm平台上开发产品时,经常遇到内存,性能的问题,可以用上ebpf这个功能强大的工具吗?
|
||||||
|
|
||||||
|
当然可以!
|
||||||
|
|
||||||
|
这节视频讲下 libbpf-bootstrap 在 arm32 平台上的交叉编译(arm64平台的交叉编译也类似);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 交叉编译方法
|
||||||
|
|
||||||
|
- 到 libbpf-bootstrap 开源仓库:
|
||||||
|
|
||||||
|
https://github.com/libbpf/libbpf-bootstrap
|
||||||
|
|
||||||
|
libbpf-bootstrap 的镜像仓库,访问速度比较快:
|
||||||
|
|
||||||
|
https://hub.njuu.cf/libbpf/libbpf-bootstrap
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
查找交叉编译的方法:
|
||||||
|
|
||||||
|
在 libbpf-bootstrap github开源仓库页面,点击 Issues 选项,搜索框中输入 `cross` 搜索:
|
||||||
|
|
||||||
|
[How to cross-compile programs from amd64 to arm64](https://github.com/libbpf/libbpf-bootstrap/issues/144)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# 交叉编译命令示例:
|
||||||
|
make EXTRA_CFLAGS="-IXXX" EXTRA_LDFLAGS="-LXXX" ARCH=arm64
|
||||||
|
# 或者:
|
||||||
|
make EXTRA_CFLAGS="-IXXX" EXTRA_LDFLAGS="-LXXX" ARCH=arm
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
`EXTRA_CFLAGS` 和 `EXTRA_LDFLAGS` 有什么用?
|
||||||
|
|
||||||
|
阅读下 libbpf-bootstrap 开源代码中的 `README.md` 文档,在 `Install Dependencies` 章节中的描述:
|
||||||
|
|
||||||
|
```
|
||||||
|
You will need clang (at least v11 or later), libelf and zlib to build
|
||||||
|
the examples, package names may vary across distros.
|
||||||
|
```
|
||||||
|
|
||||||
|
即libbpf-bootstrap需要依赖: `libelf` 和 `zlib`
|
||||||
|
|
||||||
|
所以,交叉编译时需要:
|
||||||
|
|
||||||
|
`EXTRA_CFLAGS` 指定 `libelf` 和 `zlib` 的头文件路径;
|
||||||
|
|
||||||
|
`EXTRA_LDFLAGS` 指定 `libelf` 和 `zlib` 的库文件路径;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 确定是否需要交叉编译 `libelf` 和 `zlib`
|
||||||
|
|
||||||
|
如果交叉编译工具链已经有 libelf 和 zlib,就不需要交叉编译这2个库;
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cd /your/toolchain/
|
||||||
|
find . -name libelf.*
|
||||||
|
find . -name libz.*
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 交叉编译 `zlib`
|
||||||
|
|
||||||
|
zlib源码下载:http://www.zlib.net/
|
||||||
|
|
||||||
|
```shell
|
||||||
|
tar -axf zlib-1.3.tar.gz
|
||||||
|
cd zlib-1.3
|
||||||
|
export PATH=$PATH:/home/xxx/Desktop/imx6ull_dev/sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/bin
|
||||||
|
export CC=arm-buildroot-linux-gnueabihf-gcc
|
||||||
|
./configure --prefix=$PWD/_install
|
||||||
|
make
|
||||||
|
make install
|
||||||
|
# 当前目录下的_install,就是编译出来的头文件和lib库
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 交叉编译 `libelf`
|
||||||
|
|
||||||
|
elfutils源码下载:https://sourceware.org/elfutils/
|
||||||
|
|
||||||
|
[elfutils-latest.tar.bz2](https://sourceware.org/elfutils/ftp/elfutils-latest.tar.bz2)
|
||||||
|
|
||||||
|
```shell
|
||||||
|
tar -axf elfutils-latest.tar.bz2
|
||||||
|
cd elfutils-0.189
|
||||||
|
# 参考当前目录下的 INSTALL 文档 和 网上资料
|
||||||
|
./configure --prefix=$PWD/_install --build=x86_64-linux-gnu \
|
||||||
|
--host=arm-buildroot-linux-gnueabihf \
|
||||||
|
CC=arm-buildroot-linux-gnueabihf-gcc CXX=arm-buildroot-linux-gnueabihf-g++ \
|
||||||
|
--disable-nls --disable-rpath --disable-libdebuginfod --disable-debuginfod \
|
||||||
|
--with-zlib
|
||||||
|
make
|
||||||
|
make install
|
||||||
|
|
||||||
|
# 当前目录下的_install,就是编译出来的头文件和lib库
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 交叉编译 `libbpf-bootstrap`
|
||||||
|
|
||||||
|
如果系统自带的 clang 编译器版本过低,
|
||||||
|
|
||||||
|
编译之前需要先修改: `libbpf-bootstrap/examples/c/Makefile` 文件,指定 clang 编译器:
|
||||||
|
|
||||||
|
```makefile
|
||||||
|
CLANG ?= clang
|
||||||
|
# 改为:
|
||||||
|
CLANG ?= /your-clang-version/clang # your-clang-version 指的是自己下载的clang版本
|
||||||
|
```
|
||||||
|
|
||||||
|
交叉编译命令:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# export 交叉编译工具链的路径
|
||||||
|
export PATH=$PATH:/home/xxx/Desktop/imx6ull_dev/sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/bin
|
||||||
|
|
||||||
|
# 编译选项中增加 V=1, 打印详细的编译命令
|
||||||
|
# make EXTRA_CFLAGS="-IXXX" EXTRA_LDFLAGS="-LXXX" ARCH=arm
|
||||||
|
make ARCH=arm \
|
||||||
|
EXTRA_CFLAGS="-I/home/xxx/Desktop/ebpf/note/src/arm32/extra_libs/zlib-1.3/_install/include -I/home/xxx/Desktop/ebpf/note/src/arm32/extra_libs/elfutils-0.189/_install/include" \
|
||||||
|
EXTRA_LDFLAGS="-L/home/xxx/Desktop/ebpf/note/src/arm32/extra_libs/zlib-1.3/_install -L/home/xxx/Desktop/ebpf/note/src/arm32/extra_libs/elfutils-0.189/_install/lib" \
|
||||||
|
minimal_legacy V=1
|
||||||
|
```
|
||||||
|
|
||||||
|
编译 libbpf 库时用的编译器是 `cc `,而不是交叉编译器 `arm-buildroot-linux-gnueabihf-gcc`
|
||||||
|
|
||||||
|
查看下 `libbpf-bootstrap/examples/c/Makefile` 文件中的 `CC` 变量相关的定义:
|
||||||
|
|
||||||
|
```makefile
|
||||||
|
$(call allow-override,CC,$(CROSS_COMPILE)cc)
|
||||||
|
$(call allow-override,LD,$(CROSS_COMPILE)ld)
|
||||||
|
```
|
||||||
|
|
||||||
|
需要 `CROSS_COMPILE` 变量的定义,才会把 `CC` 替换成交叉编译工具链,把交叉编译命令改为:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# export 交叉编译工具链的路径
|
||||||
|
export PATH=$PATH:/home/xxx/Desktop/imx6ull_dev/sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/bin
|
||||||
|
|
||||||
|
make ARCH=arm CROSS_COMPILE=arm-buildroot-linux-gnueabihf- \
|
||||||
|
EXTRA_CFLAGS="-I/home/xxx/Desktop/ebpf/note/src/arm32/extra_libs/zlib-1.3/_install/include -I/home/xxx/Desktop/ebpf/note/src/arm32/extra_libs/elfutils-0.189/_install/include" \
|
||||||
|
EXTRA_LDFLAGS="-L/home/xxx/Desktop/ebpf/note/src/arm32/extra_libs/zlib-1.3/_install -L/home/xxx/Desktop/ebpf/note/src/arm32/extra_libs/elfutils-0.189/_install/lib" \
|
||||||
|
minimal_legacy V=1
|
||||||
|
```
|
||||||
|
|
||||||
|
为了编译方便,可以写一个shell脚本配置编译环境:
|
||||||
|
|
||||||
|
`build_env.sh`
|
||||||
|
|
||||||
|
```shell
|
||||||
|
#!/bin/sh
|
||||||
|
export PATH=$PATH:/home/xxx/Desktop/imx6ull_dev/sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/bin
|
||||||
|
export ARCH=arm
|
||||||
|
export CROSS_COMPILE=arm-buildroot-linux-gnueabihf-
|
||||||
|
export EXTRA_CFLAGS="-I/home/xxx/Desktop/ebpf/note/src/arm32/extra_libs/elfutils-0.189/_install/include -I/home/xxx/Desktop/ebpf/note/src/arm32/extra_libs/zlib-1.3/_install/include"
|
||||||
|
export EXTRA_LDFLAGS="-L/home/xxx/Desktop/ebpf/note/src/arm32/extra_libs/elfutils-0.189/_install/lib -L/home/xxx/Desktop/ebpf/note/src/arm32/extra_libs/zlib-1.3/_install/lib"
|
||||||
|
```
|
||||||
|
|
||||||
|
交叉编译时先执行一次: `source build_env.sh`
|
||||||
|
|
||||||
|
之后就可以简单执行:`make clean; make minimal_legacy`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## eBPF 内核配置选项
|
||||||
|
|
||||||
|
Linux内核版本:4.9.88
|
||||||
|
|
||||||
|
要让ebpf跑起来,还需要内核的支持,需要打开如下的内核配置选项:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
[*] Networking support --->
|
||||||
|
Networking options --->
|
||||||
|
[*] enable BPF Just In Time compiler
|
||||||
|
[*] QoS and/or fair queueing --->
|
||||||
|
<*> BPF-based classifier
|
||||||
|
[*] Actions
|
||||||
|
<*> BPF based action
|
||||||
|
|
||||||
|
General setup --->
|
||||||
|
[*] Enable bpf() system call
|
||||||
|
[*] Kprobes
|
||||||
|
|
||||||
|
Kernel hacking --->
|
||||||
|
[*] Tracers --->
|
||||||
|
[*] Enable kprobes-based dynamic events (NEW)
|
||||||
|
[*] Enable uprobes-based dynamic events
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
arm32开发板上,`minimal_legacy` 跑不起来:
|
||||||
|
|
||||||
|
这个地方我调试了好久,好像这个版本的内核不支持ebpf 的 tracepoint/syscalls,不知道是不是我哪里搞错了,
|
||||||
|
|
||||||
|
先不管了,改成kprobe试一试,实现的功能是一样的;
|
||||||
|
|
||||||
|
```c
|
||||||
|
SEC("tp/syscalls/sys_enter_write")
|
||||||
|
int handle_tp(void *ctx)
|
||||||
|
//改为:
|
||||||
|
SEC("kprobe/sys_write")
|
||||||
|
int BPF_KPROBE(sys_write)
|
||||||
|
```
|
||||||
|
|
|
@ -0,0 +1,362 @@
|
||||||
|
# libbpf-bootstrap 基础
|
||||||
|
|
||||||
|
[TOC]
|
||||||
|
|
||||||
|
### 源码下载
|
||||||
|
|
||||||
|
https://github.com/libbpf/libbpf-bootstrap
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# 克隆源码,如果是手动下载,需要注意把子仓库也要下载下来
|
||||||
|
git clone --recurse-submodules https://github.com/libbpf/libbpf-bootstrap
|
||||||
|
|
||||||
|
# 如果是通 git clone 下载源码,可以查看修改记录
|
||||||
|
git log
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 源码目录
|
||||||
|
|
||||||
|
```shell
|
||||||
|
blazesym bpftool examples libbpf LICENSE README.md tools vmlinux
|
||||||
|
# blazesym -- Rust语言中的符号库,如果用C语言开发,就不用关注;
|
||||||
|
# bpftool -- 是 libbpf-bootstrap 框架的核心bpf工具,下面章节会介绍
|
||||||
|
# examples -- 示例代码,包括 c语言 和 Rust语言
|
||||||
|
# libbpf -- 开发eBPF的基础代码库,下面章节会介绍
|
||||||
|
# tools -- 生成 vmlinux.h 文件的工具
|
||||||
|
# vmlinux -- 存放CO-RE(Compile Once – Run Everywhere)依赖的 vmlinux.h 头文件
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 问题:libbpf 和 libbpf-bootstrap 有什么关系?
|
||||||
|
|
||||||
|
**libbpf**
|
||||||
|
|
||||||
|
是对bpf syscall(系统调用) 的基础封装,提供了 open, load, attach, maps操作, CO-RE, 等功能:
|
||||||
|
|
||||||
|
- open
|
||||||
|
|
||||||
|
从elf文件中提取 eBPF的字节码程序,maps等;
|
||||||
|
|
||||||
|
```c
|
||||||
|
LIBBPF_API struct bpf_object *bpf_object__open(const char *path);
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- load
|
||||||
|
|
||||||
|
把 eBPF字节码程序,maps等加载到内核
|
||||||
|
|
||||||
|
```c
|
||||||
|
LIBBPF_API int bpf_object__load(struct bpf_object *obj);
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- attach
|
||||||
|
|
||||||
|
把eBPF程序attch到挂接点
|
||||||
|
|
||||||
|
```c
|
||||||
|
LIBBPF_API struct bpf_link *bpf_program__attach(......);
|
||||||
|
LIBBPF_API struct bpf_link *bpf_program__attach_perf_event(......);
|
||||||
|
LIBBPF_API struct bpf_link *bpf_program__attach_kprobe(......);
|
||||||
|
LIBBPF_API struct bpf_link *bpf_program__attach_uprobe(......);
|
||||||
|
LIBBPF_API struct bpf_link *bpf_program__attach_ksyscall(......);
|
||||||
|
LIBBPF_API struct bpf_link *bpf_program__attach_usdt(......);
|
||||||
|
LIBBPF_API struct bpf_link *bpf_program__attach_tracepoint(......);
|
||||||
|
......
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- maps的操作
|
||||||
|
|
||||||
|
```c
|
||||||
|
LIBBPF_API int bpf_map__lookup_elem(......);
|
||||||
|
LIBBPF_API int bpf_map__update_elem(......);
|
||||||
|
LIBBPF_API int bpf_map__delete_elem(......);
|
||||||
|
......
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- CO-RE(Compile Once – Run Everywhere)
|
||||||
|
|
||||||
|
CO-RE可以实现eBPF程序一次编译,在不同版本的内核中正常运行;下面的章节会详细展开讲;
|
||||||
|
|
||||||
|
```c
|
||||||
|
bpf_core_read(dst, sz, src)
|
||||||
|
bpf_core_read_user(dst, sz, src)
|
||||||
|
BPF_CORE_READ(src, a, ...)
|
||||||
|
BPF_CORE_READ_USER(src, a, ...)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- 其它辅助功能
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**libbpf-bootstrap:**
|
||||||
|
|
||||||
|
基于 libbpf 开发出来的eBPF内核层代码,通过bpftool工具直接生成用户层代码操作接口,极大减少开发人员的工作量;
|
||||||
|
|
||||||
|
eBPF一般都是分2部分:内核层代码 + 用户层代码
|
||||||
|
|
||||||
|
内核层代码:跑在内核层,负责实现真正的eBPF功能
|
||||||
|
|
||||||
|
用户层代码:跑在用户层,负责 open, load, attach eBPF内核层代码到内核,并负责用户层和内核层的数据交互;
|
||||||
|
|
||||||
|
在 libbpf-bootstrap 框架中,开发一个eBPF功能,一般需要2个基础代码文件,比如需要开发个minimal的eBPF程序,需要 minimal.bpf.c 和 minimal.c,如果前面的2个文件还需要公共的头文件,可以定义头文件:minimal.h
|
||||||
|
|
||||||
|
minimal.bpf.c 是内核层代码,被 clang 编译器编译成 minimal.tmp.bpf.o
|
||||||
|
|
||||||
|
bpftool 工具通过 minimal.tmp.bpf.o 自动生成 minimal.skel.h 头文件:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
clang -g -O2 -target bpf -c minimal.bpf.c -o minimal.tmp.bpf.o
|
||||||
|
bpftool gen object minimal.bpf.o minimal.tmp.bpf.o
|
||||||
|
bpftool gen skeleton minimal.bpf.o > minimal.skel.h
|
||||||
|
```
|
||||||
|
|
||||||
|
minimal.skel.h 头文件中就包含了 minimal.bpf.c 对应的elf文件数据,以及用户层需要的 open, load, attach 等接口;
|
||||||
|
|
||||||
|
```c
|
||||||
|
// hello.bpf.c 对应的 elf 文件数据:
|
||||||
|
static inline const void *minimal_bpf__elf_bytes(size_t *sz);
|
||||||
|
|
||||||
|
// open load attach 操作接口:
|
||||||
|
static inline struct minimal_bpf *minimal_bpf__open(void);
|
||||||
|
static inline int minimal_bpf__load(struct minimal_bpf *obj);
|
||||||
|
static inline struct minimal_bpf *minimal_bpf__open_and_load(void);
|
||||||
|
static inline int minimal_bpf__attach(struct minimal_bpf *obj);
|
||||||
|
|
||||||
|
// 注意: 以上的接口都是 libbpf-bootstrap 根据开发人员编写的 minimal.bpf.c 文件,直接自动生成的接口;
|
||||||
|
// minimal.bpf.c --> bpftool --> 自动生成简洁的 minimal.skel.h
|
||||||
|
// minimal.skel.h 头文件中的接口可以非常简单的操作eBPF程序;
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### eBPF程序的生命周期
|
||||||
|
|
||||||
|
4个阶段: `open`, ` load`, ` attach`, ` destroy`
|
||||||
|
|
||||||
|
- open 阶段
|
||||||
|
|
||||||
|
从 clang 编译器编译得到的eBPF程序elf文件中抽取 maps, eBPF程序, 全局变量等;但是还未在内核中创建,所以还可以对 maps, 全局变量 进行必要的修改;如:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// libbpf-bootstrap/examples/c/minimal.c
|
||||||
|
|
||||||
|
/* Open BPF application */
|
||||||
|
skel = minimal_bpf__open();
|
||||||
|
|
||||||
|
/* eBPF内核层代码中定义的全局变量初始化 */
|
||||||
|
skel->bss->my_pid = getpid();
|
||||||
|
|
||||||
|
/* 还可以通过 bpf_map__set_value_size 和 bpf_map__set_max_entries 2个接口对eBPF内核层代码中
|
||||||
|
* 定义的 maps 进行修改;
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
- load 阶段
|
||||||
|
|
||||||
|
maps,全局变量 在内核中被创建,eBPF字节码程序加载到内核中,并进行校验;但这个阶段,eBPF程序虽然存在内核中,但还不会被运行,还可以对内核中的maps进行初始状态的赋值;
|
||||||
|
|
||||||
|
- attach 阶段
|
||||||
|
|
||||||
|
eBPF程序被attach到挂接点,eBPF相关功能开始运行,比如:eBPF程序被触发运行,更新maps, 全局变量等;
|
||||||
|
|
||||||
|
- destroy 阶段
|
||||||
|
|
||||||
|
eBPF程序被 detached,eBPF用到的资源将会被释放;
|
||||||
|
|
||||||
|
在 libbpf-bootstrap中,4个阶段对应的用户层接口:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// open 阶段,xxx:根据eBPF程序文件名而定
|
||||||
|
xxx_bpf__open(...);
|
||||||
|
|
||||||
|
// load 阶段,xxx:根据eBPF程序文件名而定
|
||||||
|
xxx_bpf__load(...);
|
||||||
|
|
||||||
|
// attach 阶段,xxx:根据eBPF程序文件名而定
|
||||||
|
xxx_bpf__attach(...);
|
||||||
|
|
||||||
|
// destroy 阶段,xxx:根据eBPF程序文件名而定
|
||||||
|
xxx_bpf__destroy(...);
|
||||||
|
|
||||||
|
//以上接口都是libbpf-bootstrap根据开发人员的eBPF文件自动生成,
|
||||||
|
//如果eBPF程序文件名为 hello.bpf.c,
|
||||||
|
//自动生成的用户层接口:
|
||||||
|
hello_bpf__open(...);
|
||||||
|
hello_bpf__load(...);
|
||||||
|
hello_bpf__attach(...);
|
||||||
|
hello_bpf__destroy(...);
|
||||||
|
|
||||||
|
//如果eBPF程序文件名为 minimal.bpf.c
|
||||||
|
//自动生成的用户层接口:
|
||||||
|
minimal_bpf__open(...);
|
||||||
|
minimal_bpf__load(...);
|
||||||
|
minimal_bpf__attach(...);
|
||||||
|
minimal_bpf__destroy(...);
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
eBPF程序生命周期更详细的介绍:
|
||||||
|
|
||||||
|
https://nakryiko.com/posts/bcc-to-libbpf-howto-guide/#bpf-skeleton-and-bpf-app-lifecycle
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### CO-RE(Compile Once – Run Everywhere)
|
||||||
|
|
||||||
|
一次编译,可以运行在不同版本的内核中
|
||||||
|
|
||||||
|
为什么需要这样的功能?
|
||||||
|
|
||||||
|
假设内核有个结构体 `struct foo`,但是在不同版本的内核,定义有变化:
|
||||||
|
|
||||||
|
```c
|
||||||
|
//4.x的内核版本
|
||||||
|
struct foo {
|
||||||
|
int a;
|
||||||
|
int b;
|
||||||
|
int c;
|
||||||
|
}
|
||||||
|
|
||||||
|
//5.x的内核版本
|
||||||
|
struct foo {
|
||||||
|
int a;
|
||||||
|
int b;
|
||||||
|
int x; //新版本内核中新增了一个字段
|
||||||
|
int c;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eBPF程序访问struct foo结构体中的字段c:
|
||||||
|
SEC("kprobe/xxx")
|
||||||
|
int BPF_KPROBE(xxx, struct foo * p_foo)
|
||||||
|
{
|
||||||
|
int read_c;
|
||||||
|
|
||||||
|
/* bpf_probe_read_kernel 的函数声明:
|
||||||
|
* long bpf_probe_read_kernel(void *dst, __u32 size, const void *unsafe_ptr);
|
||||||
|
*/
|
||||||
|
|
||||||
|
//如果是4.x内核
|
||||||
|
bpf_probe_read_kernel(&read_c, sizeof(int), p_foo + 2 * sizeof(int));
|
||||||
|
|
||||||
|
//如果是5.x内核
|
||||||
|
bpf_probe_read_kernel(&read_c, sizeof(int), p_foo + 3 * sizeof(int));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 因为不同内核版本中, struct foo 中的c字段偏移变了,所以不同版本的内核必须要编写2个不同的eBPF程序
|
||||||
|
// 这会对eBPF工具的发布造成非常大的问题
|
||||||
|
```
|
||||||
|
|
||||||
|
为了解决这个问题,需要3个方面的配合:
|
||||||
|
|
||||||
|
1. BTF (BPF Type Format)
|
||||||
|
|
||||||
|
运行中的内核提供当前内核中各种数据类型的BTF描述,用户空间可以通过 `/sys/kernel/btf/vmlinux` 访问当前内核的BTF信息;
|
||||||
|
|
||||||
|
通过bpftool工具,把BTF格式的 vmlinux 转化成C语言格式的头文件 vmlinux.h,vmlinux.h 包好了当前内核中的所有数据类型的定义;
|
||||||
|
|
||||||
|
```shell
|
||||||
|
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
|
||||||
|
```
|
||||||
|
|
||||||
|
BTF 详细介绍:https://www.kernel.org/doc/html/latest/bpf/btf.html#
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
2. clang 编译器需要支持记录结构体字段重定位的信息
|
||||||
|
|
||||||
|
```c
|
||||||
|
#define bpf_core_read(dst, sz, src) \
|
||||||
|
bpf_probe_read_kernel(dst, sz, (const void *)__builtin_preserve_access_index(src))
|
||||||
|
|
||||||
|
// __builtin_preserve_access_index 就是让 clang 编译器编译时增加结构体字段重定位的信息
|
||||||
|
// 比如:
|
||||||
|
bpf_core_read(&read_c, sizeof(int), p_foo->c)
|
||||||
|
//宏展开:
|
||||||
|
bpf_probe_read_kernel(&read_c, sizeof(int), __builtin_preserve_access_index(p_foo->c))
|
||||||
|
//clang编译器编译这段代码时,就会增加描述信息:访问 c 字段时需要根据当前内核的BTF信息重新计算偏移量
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
3. eBPF loader
|
||||||
|
|
||||||
|
libbpf 加载eBPF程序到内核时,会先找到 clang 编译器记录的重定位信息,根据当前运行中的内核提供的BTF信息,重新计算需要访问的字段的偏移量;
|
||||||
|
|
||||||
|
```tex
|
||||||
|
比如上面的 struct foo 结构体,如果内核开启了 CONFIG_DEBUG_INFO_BTF 的内核配置选项,在编译内核时,
|
||||||
|
struct foo 结构体就会被编译进内核的BTF信息中,内核运行时,可以通过访问 /sys/kernel/btf/vmlinux
|
||||||
|
文件就可以知道 struct foo 结构体具体的定义,也就可以动态计算得到 c 字段的偏移量;
|
||||||
|
|
||||||
|
如果在编写eBPF程序时,通过clang编译器的 __builtin_preserve_access_index 明确告诉libbpf加载
|
||||||
|
eBPF程序时,需要动态计算 c 字段的偏移量,从而避免在eBPF程序中手动写死 c 字段的偏移量;
|
||||||
|
|
||||||
|
eBPF程序就可以只编译一次,在不同版本的内核中正常运行!
|
||||||
|
```
|
||||||
|
|
||||||
|
如何在 libbpf-bootstrap 中使用或者不使用 CO-RE
|
||||||
|
|
||||||
|
```c
|
||||||
|
//在内核层的eBPF程序中,包含 vmlinux.h 头文件就说明需要使用 CO-RE 功能, 否则就是不使用
|
||||||
|
#include "vmlinux.h"
|
||||||
|
|
||||||
|
//使用CO-RE需要内核打开 CONFIG_DEBUG_INFO_BTF 配置选项,如果内核版本过低,不支持这个配置选项,
|
||||||
|
//就不要使用 CO-RE,即不要包含 vmlinux.h 头文件
|
||||||
|
|
||||||
|
// vmlinux.h 头文件,在 libbpf-bootstrap/vmlinux/ 目录下有预先提供特定版本内核相关的 vmlinux.h
|
||||||
|
// 使用过程中,运行中的内核版本没必要和libbpf-bootstrap/vmlinux/ 目录下预先提供的 vmlinux.h
|
||||||
|
// 对应的内核版本完全匹配上,不匹配上也可以用
|
||||||
|
|
||||||
|
// 手动生成自己的 vmlinux.h, 可以参考:libbpf-bootstrap/tools/gen_vmlinux_h.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
CO-RE 更详细的介绍:https://nakryiko.com/posts/bpf-portability-and-co-re/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### x86-64 平台上的编译
|
||||||
|
|
||||||
|
- clang 编译器
|
||||||
|
|
||||||
|
版本要求: at least v11 or later
|
||||||
|
|
||||||
|
不同版本 clang 编译下载: https://releases.llvm.org/download.html
|
||||||
|
|
||||||
|
在 ubuntu18.04 上,我下载了 16.0.0 版本的 clang 编译器
|
||||||
|
|
||||||
|
```shell
|
||||||
|
~/Desktop/clang-16/clang --version
|
||||||
|
```
|
||||||
|
|
||||||
|
- 修改 libbpf-bootstrap/examples/c/ 目录下的Makefile
|
||||||
|
|
||||||
|
```shell
|
||||||
|
CLANG ?= /home/zhanglong/Desktop/clang-16/clang
|
||||||
|
|
||||||
|
# 可以被编译的 sample 程序
|
||||||
|
APPS = minimal minimal_legacy bootstrap uprobe kprobe fentry usdt sockfilter tc ksyscall
|
||||||
|
```
|
||||||
|
|
||||||
|
- 编译 libbpf-bootstrap/examples/c/ 目录下的 uprobe 示例代码
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cd libbpf-bootstrap/examples/c/
|
||||||
|
make clean
|
||||||
|
make uprobe
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue