eBPF学习记录(三)使用BCC开发eBPF程序
上一节,我们使用了bpftrace 开发eBPF程序跟踪内核和用户态的程序,bpftrace 简单易用,非常适合入门,可以带初学者轻松体验 eBPF 的各种跟踪特性。但是,bpftrace 并不适用于所有的 eBPF 应用,它本身的限制导致我们无法在需要复杂 eBPF 程序的场景中使用它。在复杂的应用中,还是推荐使用 BCC 或者 libbpf 进行开发。现在讲一下BCC 的开发,有问题可以看官方文档。
现在我们试试使用BCC开发一个eBPF程序,分以下3个步骤,
- 使用 C 开发一个 eBPF 程序
- 使用 Python 和 BCC 库开发一个用户态程序
- 执行 eBPF 程序
现在先来一个最简单的程序玩玩吧!
一、开发一个最简单的eBPF程序
- 使用 C 开发一个 eBPF 程序
新建一个 hello.c 文件,并输入下面的内容:
int hello_world(void *ctx)
{bpf_trace_printk("Hello, World!");return 0;
}
就像所有编程语言的“ Hello World ”示例一样,这段代码的含义就是打印一句 “Hello, World!” 字符串。其中, bpf_trace_printk() 是一个最常用的 BPF 辅助函数,它的作用是输出一段字符串。不过,由于 eBPF 运行在内核中,它的输出并不是通常的标准输出(stdout),而是内核调试文件 /sys/kernel/debug/tracing/trace_pipe ,你可以直接使用 cat 命令来查看这个文件的内容。
- 使用 Python 和 BCC 库开发一个用户态程序
新建一个 hello.py 文件,并输入下面的内容:
#!/usr/bin/env python3
# 1) import bcc library
from bcc import BPF# 2) load BPF program
b = BPF(src_file="hello.c")
# 3) attach kprobe
b.attach_kprobe(event="do_sys_openat2", fn_name="hello_world")
# 4) read and print /sys/kernel/debug/tracing/trace_pipe
b.trace_print()
让我们来看看每一处的具体含义:
第 1) 处导入了 BCC 库的 BPF 模块,以便接下来调用;
第 2) 处调用 BPF() 加载第一步开发的 BPF 源代码;
第 3) 处将 BPF 程序挂载到内核探针(简称 kprobe),其中 do_sys_openat2() 是系统调用 openat() 在内核中的实现;
第 4) 处则是读取内核调试文件 /sys/kernel/debug/tracing/trace_pipe的内容,并打印到标准输出中。
执行 eBPF 程序
用户态程序开发完成之后,最后一步就是执行它了。需要注意的是, eBPF 程序需要以 root 用户来运行,非 root 用户需要加上 sudo 来执行:
sudo python3 hello.py
稍等一会,你就可以看到如下的输出:
b' python3-9659 [000] d... 67871.172937: bpf_trace_printk: Hello, World!'
b' python3-9659 [000] d... 67871.173107: bpf_trace_printk: Hello, World!'
b' python3-9659 [000] d... 67871.173302: bpf_trace_printk: Hello, World!'
b' <...>-9661 [000] d... 67911.701439: bpf_trace_printk: Hello, World!'
b' gnome-shell-1758 [001] d... 67912.214565: bpf_trace_printk: Hello, World!'
b' gnome-shell-1758 [001] d... 67914.622574: bpf_trace_printk: Hello, World!'
b' gnome-shell-1758 [001] d... 67914.622619: bpf_trace_printk: Hello, World!'
输出字符串含义解析:
- python3-9659 表示进程的名字和 PID;
- [000] 表示 CPU 编号;
- d… 表示一系列的选项;
- 67871.172937表示时间戳;
- bpf_trace_printk 表示函数名;
- 最后的 “Hello, World!” 就是调用bpf_trace_printk() 传入的字符串。
到了这里,我们已经成功开发并运行了第一个 eBPF 程序。
二、开发 BPF 事件映射程序
刚刚开发的第一个程序是使用bpf_trace_printk()输出,他输出格式不够灵活,输出的内容不太符合我们平常的需要。现在我们尝试一下使用时间映射把我们需要的数据映射到用户态,然后在python中选择我们需要的信息打印到控制台中,这里以追踪我们打开文件为例子尝试一下吧。
先看 eBPF 程序open.c的代码:
// 包含头文件
#include <uapi/linux/openat2.h>
#include <linux/sched.h>// 定义数据结构
struct data_t {u32 pid;u64 ts;char comm[TASK_COMM_LEN];char fname[NAME_MAX];
};// 定义性能事件映射
BPF_PERF_OUTPUT(events);// 定义kprobe处理函数
int bcc_do_sys_openat2(struct pt_regs *ctx, int dfd, const char __user * filename, struct open_how *how)
{struct data_t data = { };// 获取PID和时间data.pid = bpf_get_current_pid_tgid();data.ts = bpf_ktime_get_ns();// 获取进程名if (bpf_get_current_comm(&data.comm, sizeof(data.comm)) == 0){bpf_probe_read(&data.fname, sizeof(data.fname), (void *)filename);}// 提交性能事件events.perf_submit(ctx, &data, sizeof(data));return 0;
}
我们使用了BCC 定义的一系列的库函数和辅助宏定义,下面讲解一下:
- BPF_PERF_OUTPUT:定义一个 Perf 事件类型的 BPF 映射,需要调用 perf_submit() 把数据提交到 BPF 映射中;
- bpf_get_current_pid_tgid 用于获取进程的 TGID 和 PID;
- bpf_ktime_get_ns 用于获取系统自启动以来的时间,单位是纳秒;
- bpf_get_current_comm 用于获取进程名,并把进程名复制到预定义的缓冲区中;
- bpf_probe_read 用于从指定指针处读取固定大小的数据,这里则用于读取进程打开的文件名。
现在写一个用户态程序open.py,用来读取 BPF 映射内容并输出到标准输出:
from bcc import BPF# 1) 加载eBPF代码
b = BPF(src_file="open.c")
b.attach_kprobe(event="do_sys_openat2", fn_name="bcc_do_sys_openat2")# 2) 输出头
print("%-18s %-16s %-6s %-16s" % ("TIME(s)", "COMM", "PID", "FILE"))# 3) 定义性能事件打印函数
start = 0
def print_event(cpu, data, size):global startevent = b["events"].event(data)if start == 0:start = event.tstime_s = (float(event.ts - start)) / 1000000000print("%-18.9f %-16s %-6d %-16s" % (time_s, event.comm, event.pid, event.fname))# 4) 绑定性能事件映射和输出函数,并从映射中循环读取数据
b["events"].open_perf_buffer(print_event)
while 1:try:b.perf_buffer_poll()except KeyboardInterrupt:exit()
- 第 1) 处加载 eBPF 程序并挂载到内核探针上;
- 第 2) 处输出一行 Header 字符串表示数据的格式;
- 第 3) 处print_event 定义一个数据处理的回调函数,打印进程的名字、PID 以及它调用 openat 时打开的文件;
- 第 4) 处则是使用poll读取内核调试文件 /sys/kernel/debug/tracing/trace_pipe的内容,并回调print_event打印到标准输出中。
jian@ubuntu:~/Desktop/bpf/sec$ sudo python3 open.py
...
TIME(s) COMM PID FILE
0.000000000 b'vmtoolsd' 802 b'/proc/meminfo'
0.000071315 b'vmtoolsd' 802 b'/proc/vmstat'
0.000133430 b'vmtoolsd' 802 b'/proc/stat'
0.000187572 b'vmtoolsd' 802 b'/proc/zoneinfo'
0.000282536 b'vmtoolsd' 802 b'/proc/uptime'
0.000294861 b'vmtoolsd' 802 b'/proc/diskstats'
相对于前面的 Hello World,它的输出不仅格式更为清晰,还把进程打开的文件名输出出来了,这在调试的时候尤其有用。
三、开发hash映射程序
刚刚使用事件映射已经满足我们平常的需要,但是如果我们需要同时跟踪几个函数,还想让跟踪点之间可以访问相互想要提交的数据,仅仅事件映射是不行的,这时候就需要使用哈希映射了。我们还是来个例子吧,我们追踪系统调用execve吧,那么他的进入追踪函数和返回函数分别是sys_enter_execve,sys_exit_execve;我们让他们共享data_t 数据,并且各自把数据填充进去,最后再一起提交。
同样,先看 eBPF 程序execsnoop.c的代码:
// 引入内核头文件
#include <uapi/linux/ptrace.h>
#include <linux/sched.h>
#include <linux/fs.h>// consts for arguments (ensure below stack size limit 512)
#define ARGSIZE 64
#define TOTAL_MAX_ARGS 5
#define FULL_MAX_ARGS_ARR (TOTAL_MAX_ARGS * ARGSIZE)
#define LAST_ARG (FULL_MAX_ARGS_ARR - ARGSIZE)// perf event map (sharing data to userspace) and hash map (sharing data between tracepoints)
struct data_t {u32 pid;char comm[TASK_COMM_LEN];int retval;unsigned int args_size;char argv[FULL_MAX_ARGS_ARR];
};
BPF_PERF_OUTPUT(events);
BPF_HASH(tasks, u32, struct data_t);// helper function to read string from userspace.
static int __bpf_read_arg_str(struct data_t *data, const char *ptr)
{if (data->args_size > LAST_ARG) {return -1;}int ret = bpf_probe_read_user_str(&data->argv[data->args_size], ARGSIZE,(void *)ptr);if (ret > ARGSIZE || ret < 0) {return -1;}// increase the args size. the first tailing '\0' is not counted and hence it// would be overwritten by the next call.data->args_size += (ret - 1);return 0;
}//定义sys_enter_execve跟踪点处理函数.
TRACEPOINT_PROBE(syscalls, sys_enter_execve)
{// 变量定义unsigned int ret = 0;const char **argv = (const char **)(args->argv);// 获取进程PID和进程名称struct data_t data = { };u32 pid = bpf_get_current_pid_tgid();data.pid = pid;bpf_get_current_comm(&data.comm, sizeof(data.comm));// 获取第一个参数(即可执行文件的名字)if (__bpf_read_arg_str(&data, (const char *)argv[0]) < 0) {goto out;}// 获取其他参数(限定最多5个)for (int i = 1; i < TOTAL_MAX_ARGS; i++) {if (__bpf_read_arg_str(&data, (const char *)argv[i]) < 0) {goto out;}}out:// 存储到哈希映射中tasks.update(&pid, &data);return 0;
}// 定义sys_exit_execve跟踪点处理函数.
TRACEPOINT_PROBE(syscalls, sys_exit_execve)
{// 从哈希映射中查询进程基本信息u32 pid = bpf_get_current_pid_tgid();struct data_t *data = tasks.lookup(&pid);// 填充返回值并提交到性能事件映射中if (data != NULL) {data->retval = args->ret;events.perf_submit(args, data, sizeof(struct data_t));// 最后清理进程信息tasks.delete(&pid);}return 0;
}
- struct data_t:定义了一个包含进程基本信息的数据结构,它将用在哈希映射的值中(其中的参数大小 args_size
会在读取参数内容的时候用到); - BPF_PERF_OUTPUT(events) : 定义了一个性能事件映射;
- BPF_HASH(tasks,u32, struct data_t) : 定义了一个哈希映射,其键(tasks)为 32 位的进程PID,而值则是进程基本信息 data_t。
- tasks.update(&pid, &data):把进程的基本信息存储到哈希映射中。
- tasks.lookup(&pid):从哈希映射中通过进程ID获取进程基本信息 data_t。
- tasks.delete(&pid):从哈希映射中通过进程ID删除这个映射。
再看execsnoop.py:
# 引入库函数
from bcc import BPF
from bcc.utils import printb# 1) 加载eBPF代码
b = BPF(src_file="execsnoop.c")# 2) print header
print("%-6s %-16s %-3s %s" % ("PID", "COMM", "RET", "ARGS"))# 3) 定义性能事件打印函数
def print_event(cpu, data, size):# BCC自动根据"struct data_t"生成数据结构event = b["events"].event(data)printb(b"%-6d %-16s %-3d %-16s" % (event.pid, event.comm, event.retval, event.argv))# 4) 绑定性能事件映射和输出函数,并从映射中循环读取数据
b["events"].open_perf_buffer(print_event)
while 1:try:b.perf_buffer_poll()except KeyboardInterrupt:exit()
最后通过 Python 运行,并在另一个终端中执行 ls 命令,你就可以得到如下的输出:
jian@ubuntu:~/share/bpf/third$ sudo python3 execsnoop.py
PID COMM RET ARGS
6178 bash 0 ls--color=auto
使用BCC开发eBPF程序就讲到这里了,其实BCC中自带了很多eBPF程序,我们也可以直接那来使用的。
eBPF学习记录(三)使用BCC开发eBPF程序相关推荐
- eBPF学习记录(四)使用libbpf开发eBPF程序
上一节,我们讲解了使用BCC开发eBPF程序,不过,在我们想要分发这个程序到客户环境时,又会碰到一个新的难题:BCC 依赖于 LLVM 和内核头文件才可以动态编译和加载 eBPF 程序,然而,我们在客 ...
- MySQL学习记录 (三) ----- SQL数据定义语句(DDL)
相关文章: <MySQL学习记录 (一) ----- 有关数据库的基本概念和MySQL常用命令> <MySQL学习记录 (二) ----- SQL数据查询语句(DQL)> &l ...
- WebService学习总结(三)——使用JDK开发WebService
一.WebService的开发手段 使用Java开发WebService时可以使用以下两种开发手段 1. 使用JDK开发(1.6及以上版本) 2.使用CXF框架开发(工作中) 二.使用JDK开发Web ...
- css学习记录三:文本属性
css学习记录三:CSS文本属性 一.文本属性的作用 二.文本颜色 三.文本对齐 四.装饰文本 五.文本缩进 六.行间距 一.文本属性的作用 CSSS Text(文本)属性可定义文本的外观,比如文本的 ...
- Kafka学习记录(三)——Broker
Kafka学习记录(三)--Broker 目录 Kafka学习记录(三)--Broker 对应课程 Zookeeper存储的Kafka信息 Broker总体工作流程 Broker的服役和退役 Kafk ...
- eBPF学习记录(一)eBPF介绍
一.什么是eBPF eBPF, 从它的全称"扩展的伯克利数据包过滤器 (Extended Berkeley Packet Filter)" 来看,它是一种数据包过滤技术,是从 BP ...
- webrtc学习记录三【创建基于RTCPeerConnection本机内的1v1音视频互通】
系列文章目录 webrtc学习记录一[媒体录制MediaRecorder] webrtc学习记录二[基于socket.io创建信令服务器聊天室] 目录 系列文章目录 前言 一.媒体能力的协商过程 1. ...
- MOOSE多物理场耦合平台入门学习记录一(稳态热传导程序实例)
MOOSE多物理场耦合平台入门学习记录 MOOSE的简介 MOOSE的安装 Linux和Mac Windows MOOSE程序的一般开发流程-以导热微分方程为例 简单问题的有限元处理 MOOSE程序的 ...
- ant java xml文件,ant 学习(2)-ant开发java程序,buile.xml文件分离
用Ant 开发java 程序----以及将build.xml文件拆分 用Ant 开发java 程序,以HelloWord 为例 1> 本次实例的目录结构: c:\ant_test 主目录 --- ...
最新文章
- android studio 无线手机调试插件,Android Studio ADB Wifi 无线调试
- IOS开发中发送Email的两种方法
- 利用kali的msf提取汇编机器码(shellcode)
- leetcode350. 两个数组的交集 II(hashmap)
- .net/c#中栈和堆的区别及代码在栈和堆中的执行流程详解之一(转)
- LAMP默认安装路径
- HTML_DOM学习
- 20.Linux 账号管理与 ACL 权限设置
- ortoiseGit--小乌龟git项目
- 软件体系结构期末复习总结
- Comdo安全防火墙
- 聊聊CMSIS-RTOS是什么东东
- Android:JNI 与 NDK到底是什么?(含实例教学)
- PHP base64转图片
- TiDB学习笔记(七)-数据库系统优化
- Python turtle画图之心形图案
- json 转 实体对象 报解析错误
- Bugku:Snowfall
- 解决ThinkPad X200找不到无线网卡硬件的问题
- Column 'parent_id' specified twice问题解决--insertable = false, updatable = false的使用