上一节,我们使用了bpftrace 开发eBPF程序跟踪内核和用户态的程序,bpftrace 简单易用,非常适合入门,可以带初学者轻松体验 eBPF 的各种跟踪特性。但是,bpftrace 并不适用于所有的 eBPF 应用,它本身的限制导致我们无法在需要复杂 eBPF 程序的场景中使用它。在复杂的应用中,还是推荐使用 BCC 或者 libbpf 进行开发。现在讲一下BCC 的开发,有问题可以看官方文档。

现在我们试试使用BCC开发一个eBPF程序,分以下3个步骤,

  1. 使用 C 开发一个 eBPF 程序
  2. 使用 Python 和 BCC 库开发一个用户态程序
  3. 执行 eBPF 程序

现在先来一个最简单的程序玩玩吧!

一、开发一个最简单的eBPF程序

  1. 使用 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 命令来查看这个文件的内容。

  1. 使用 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程序相关推荐

  1. eBPF学习记录(四)使用libbpf开发eBPF程序

    上一节,我们讲解了使用BCC开发eBPF程序,不过,在我们想要分发这个程序到客户环境时,又会碰到一个新的难题:BCC 依赖于 LLVM 和内核头文件才可以动态编译和加载 eBPF 程序,然而,我们在客 ...

  2. MySQL学习记录 (三) ----- SQL数据定义语句(DDL)

    相关文章: <MySQL学习记录 (一) ----- 有关数据库的基本概念和MySQL常用命令> <MySQL学习记录 (二) ----- SQL数据查询语句(DQL)> &l ...

  3. WebService学习总结(三)——使用JDK开发WebService

    一.WebService的开发手段 使用Java开发WebService时可以使用以下两种开发手段 1. 使用JDK开发(1.6及以上版本) 2.使用CXF框架开发(工作中) 二.使用JDK开发Web ...

  4. css学习记录三:文本属性

    css学习记录三:CSS文本属性 一.文本属性的作用 二.文本颜色 三.文本对齐 四.装饰文本 五.文本缩进 六.行间距 一.文本属性的作用 CSSS Text(文本)属性可定义文本的外观,比如文本的 ...

  5. Kafka学习记录(三)——Broker

    Kafka学习记录(三)--Broker 目录 Kafka学习记录(三)--Broker 对应课程 Zookeeper存储的Kafka信息 Broker总体工作流程 Broker的服役和退役 Kafk ...

  6. eBPF学习记录(一)eBPF介绍

    一.什么是eBPF eBPF, 从它的全称"扩展的伯克利数据包过滤器 (Extended Berkeley Packet Filter)" 来看,它是一种数据包过滤技术,是从 BP ...

  7. webrtc学习记录三【创建基于RTCPeerConnection本机内的1v1音视频互通】

    系列文章目录 webrtc学习记录一[媒体录制MediaRecorder] webrtc学习记录二[基于socket.io创建信令服务器聊天室] 目录 系列文章目录 前言 一.媒体能力的协商过程 1. ...

  8. MOOSE多物理场耦合平台入门学习记录一(稳态热传导程序实例)

    MOOSE多物理场耦合平台入门学习记录 MOOSE的简介 MOOSE的安装 Linux和Mac Windows MOOSE程序的一般开发流程-以导热微分方程为例 简单问题的有限元处理 MOOSE程序的 ...

  9. ant java xml文件,ant 学习(2)-ant开发java程序,buile.xml文件分离

    用Ant 开发java 程序----以及将build.xml文件拆分 用Ant 开发java 程序,以HelloWord 为例 1> 本次实例的目录结构: c:\ant_test 主目录 --- ...

最新文章

  1. android studio 无线手机调试插件,Android Studio ADB Wifi 无线调试
  2. IOS开发中发送Email的两种方法
  3. 利用kali的msf提取汇编机器码(shellcode)
  4. leetcode350. 两个数组的交集 II(hashmap)
  5. .net/c#中栈和堆的区别及代码在栈和堆中的执行流程详解之一(转)
  6. LAMP默认安装路径
  7. HTML_DOM学习
  8. 20.Linux 账号管理与 ACL 权限设置
  9. ortoiseGit--小乌龟git项目
  10. 软件体系结构期末复习总结
  11. Comdo安全防火墙
  12. 聊聊CMSIS-RTOS是什么东东
  13. Android:JNI 与 NDK到底是什么?(含实例教学)
  14. PHP base64转图片
  15. TiDB学习笔记(七)-数据库系统优化
  16. Python turtle画图之心形图案
  17. json 转 实体对象 报解析错误
  18. Bugku:Snowfall
  19. 解决ThinkPad X200找不到无线网卡硬件的问题
  20. Column 'parent_id' specified twice问题解决--insertable = false, updatable = false的使用

热门文章

  1. 007-配置IP和DNS
  2. 高德百度坐标系转换方法
  3. 删除所有的.svn 文件
  4. 【Java】使用MapReduce程序统计PV数量
  5. html 页面怎么自动定位到某个标签,JS如何实现在页面上快速定位(锚点跳转问题)...
  6. php代码输出sql语句,教你在Laravel中轻松容易的输出完整的SQL语句
  7. php auth和rbac区别,THINKPHP中的AUTH权限管理介绍
  8. Python 函数(一)
  9. 简易 Python 3 运行 Python 2 代码
  10. 我们应该如何写好HTMLCSS