闲言少叙,本文记录了如何零基础通过 BCC 框架,入门 eBPF 程序的开发,并实现几个简易的程序。

有关 eBPF 的介绍,网络上的资料有很多,本文暂且先不深入讨论,后面会再出一篇文章详细分析其原理和功能。

我们目前只需要知道,eBPF 实际上是一种过滤器,这种过滤器几乎可以插入内核源码的任意的流程和环节中,实现自定义的逻辑。由于 eBPF 自身的若干限制,使它最常见的用法是,附着在内核某些关键流程上,抓取一些关键数据,用于监控、统计和分析。

1 一个简单的例子

下面是一个简单的例子,我想实现一个程序,用来实时监控内核可执行文件(ELF)的加载。这个程序运行如下:

如图所示,每当有一个 ELF 文件被加载时,可以显示这个 ELF 加载时的一些内核信息,如:加载时间、加载进程名、加载进程 PID、以及被加载的 ELF 文件名。

这个程序就是基于 eBPF 实现的。接下来,我们就逐步了解一下,如何通过 BCC 框架,成功编写运行这个 eBPF 程序。

2 BCC 框架

进行 eBPF 编程,有很多种方式。例如:

1)libbpf:使用原生的 C 语言,基于 libbpf 库,编写用户态程序和 BPF 程序的加载;

2)libbpf-bootstrap:使用 libbpf-bootstrap 脚手架,轻而易举地编写 BPF 程序;

3)BCC:使用 BCC 框架,基于 python/Lua 脚本,实现 BPF 和用户态程序,上手容易,简化了 BPF 的开发;

4)Bpftrace:一种用于eBPF的高级跟踪语言,使用LLVM作为后端,将脚本编译为BPF字节码;

5)eunomia-bpf:较新的基于 libbpf 的 CO-RE 轻量级框架,简化了 eBPF 程序的开发、构建、分发、运行

选择 BCC 框架作为第一个学习的框架的原因是,BCC 封装较好,上手容易,用户态和内核态的区分明显,用户态支持 Python,易于理解。

安装过程很简单,直接通过对应软件包管理器安装即可。

本文的实验环境是 REHL 8(x86),因此,执行 yum 命令来安装。

yum install -y python3-bcc.x86_64

2.1 编写 hello world

安装好 Python BCC 依赖包后,在工作目录中创建一个 py 脚本文件,输入以下代码:

#!/bin/python3
from bcc import BPF bpf_code = '''
int kprobe__sys_clone(void *ctx) {
bpf_trace_printk("Hello world!\\n");
return 0;
}
''' b = BPF(text=bpf_code)
b.trace_print()

运行这个 py 脚本,当有进程被创建时,打印一条 Hello world 记录。

这就是一个最简单的 eBPF 程序。

3 扩展这个 Hello world

上面给出的这个程序结构很清晰,分为两个部分:以 C 编写的 eBPF 内核态程序,和以 Python 编写的用户态控制程序。eBPF 内核态程序被 BCC 框架编译到内核中,等待预设的触发条件,——这里是 sys_clone 即进程创建的系统调用,eBPF 被执行时,将会返回数据给用户态控制程序。

流程可以描述如下:

接下来我们对这个程序进行亿点点扩展,让它变得规范一些,代码如下:

#!/bin/python3
from bcc import BPF
from bcc.utils import printb # define BPF program
prog = """
int hello(void *ctx) {
bpf_trace_printk("Hello, World!\\n");
return 0;
}
""" # load BPF program
b = BPF(text=prog)
b.attach_kprobe(event=b.get_syscall_fnname("clone"), fn_name="hello") # header
print("%-18s %-16s %-6s %s" % ("TIME(s)", "COMM", "PID", "MESSAGE")) # format output
while 1:
try:
(task, pid, cpu, flags, ts, msg) = b.trace_fields()
except ValueError:
continue
except KeyboardInterrupt:
exit()
printb(b"%-18.9f %-16s %-6d %s" % (ts, task, pid, msg))

在这段程序中,我们做出了以下几点变动:

1)使用 event=b.get_syscall_fnname("clone") 来绑定内核中的系统调用监视点,这里绑定了 clone 进程创建调用;使用 fn_name="hello" 绑定了 eBPF 程序中的自定义检查逻辑;使用 b.attach_kprobe() 函数将 eBPF 程序加载到内核中。

2)使用 b.trace_fields() 函数按字段的形式,接收内核 eBPF 程序传出的输出信息;其中,msgbpf_trace_printk() 的打印信息。

3)通过无限循环,监测 clone 系统调用的执行;增加了异常输出。

这段程序运行后,输出结果如下:

4 进一步扩展,监视 do_execve

第 3 节的代码,输出内核字段的方式是 bpf_trace_printk() + trace_fields(),比较灵活,但性能较差。实际上,还有一种比较常见的输出方式,那就是通过一段共享内存 Ring buffer 来实现。

此外,这次我们更换一个内核监视点,不再关注进程的创建,而关注进程的执行。

接下来,对上面的代码进行大刀阔斧的修改吧。

文件拆分:

// do_execve.c
#include <uapi/linux/limits.h> // #define NAME_MAX 255
#include <linux/fs.h> // struct filename;
#include <linux/sched.h> // #define TASK_COMM_LEN 16 // 定义 Buffer 中的数据结构,用于内核态和用户态的数据交换
struct data_t {
u32 pid;
char comm[TASK_COMM_LEN];
char fname[NAME_MAX];
};
BPF_PERF_OUTPUT(events);
// 自定义 hook 函数
int check_do_execve(struct pt_regs *ctx, struct filename *filename,
const char __user *const __user *__argv,
const char __user *const __user *__envp) {
truct data_t data = { }; data.pid = bpf_get_current_pid_tgid();
bpf_get_current_comm(&data.comm, sizeof(data.comm));
bpf_probe_read_kernel_str(&data.fname, sizeof(data.fname), (void *)filename->name);
// 提交 buffer 数据
events.perf_submit(ctx, &data, sizeof(data));
return 0;
}
# do_execve.py
#!/bin/python3
from bcc import BPF
from bcc.utils import printb
# 指定 eBPF 源码文件
b = BPF(src_file="do_execve.c")
# 以内核函数的方式绑定 eBPF 探针
b.attach_kprobe(event="do_execve", fn_name="check_do_execve") print("%-6s %-16s %-16s" % ("PID", "COMM", "FILE"))
# 自定义回调函数
def print_event(cpu, data, size):
event = b["events"].event(data)
printb(b"%-6d %-16s %-16s" % (event.pid, event.comm, event.fname)) # 指定 buffer 名称,为 buffer 的修改添加回调函数
b["events"].open_perf_buffer(print_event)
while 1:
try:
# 循环监听
b.perf_buffer_poll()
except KeyboardInterrupt:
exit()

这一次,我们又进行了亿点点修改:

1)首先,对 eBPF BCC 程序的用户态和内核态代码进行拆分,并在用户态程序中,通过 b = BPF(src_file="do_execve.c") 对内核态源码文件进行绑定。

2)以内核函数的方式绑定 eBPF 程序,绑定点为 do_execve(),自定义处理函数为 check_do_execve()

注意:

可以看到,check_do_execve() 函数的参数分为两部分:

① struct pt_regs *ctx;
② struct filename *filename, const char __user *const __user *__argv, const char __user *const __user *__envp

这是因为,②所代表的,正是内核 do_execve()函数的参数。do_execve()函数签名如下:

// fs/exec.c
int do_execve(struct filename *filename, const char __user *const __user *__argv, const char __user *const __user *__envp) {...}

是的,通过这种方式,几乎可以监控任意一个内核中的函数

3)内核态程序中,使用了一些 eBPF Helper 函数来进行一些基础的操作和数据获取,例如:

bpf_get_current_pid_tgid()								// 获取当前进程 pid
bpf_get_current_comm(&data.comm, sizeof(data.comm)); // 获取当前进程名 comm
bpf_probe_read_kernel_str(&data.fname, sizeof(data.fname), (void *)filename->name); // 将数据从内核空间拷贝到用户空间

4)内核态程序中,使用 BPF_PERF_OUTPUT(events) 声明 buffer 中的共享变量;使用 events.perf_submit(ctx, &data, sizeof(data)) 提交数据。

用户态程序中,使用 b["events"].open_perf_buffer(print_event) 指定 buffer 名称,为 buffer 的修改添加回调函数 print_event

运行这段程序,输出如下:

可以看到,这段程序可以实时监控内核进程执行,并输出执行的进程和被执行的文件名。

5 总结

本文通过几个程序 demo,简单介绍了 eBPF BCC 框架的编程方法,并最终实现了一个简单的进程执行的监视工具,可以实时打印被执行的进程信息。

本文开篇所引出的实时监控内核可执行文件(ELF)的加载程序,也就没那个高深莫测了。

【eBPF-01】初见:基于 BCC 框架的第一个 eBPF 程序的更多相关文章

  1. 基于Struts2框架实现登录案例 之 程序国际化

    国际化牵涉的知识非常多,这里只能简单的介绍,程序国际化的一般做法是:在jsp页面时, 不是直接输出信息,而是输出一个key值,该key值在不同语言环境下找到对应资源文件下的 对应信息,因此首先要创建满 ...

  2. 网络编程应用:基于TCP协议【实现一个聊天程序】

    要求: 基于TCP协议实现一个聊天程序,客户端发送一条数据,服务器端发送一条数据 客户端代码: package Homework1; import java.io.IOException; impor ...

  3. 文件系统(01):基于SpringBoot框架,管理Excel和PDF文件类型

    本文源码:GitHub·点这里 || GitEE·点这里 一.文档类型简介 1.Excel文档 Excel一款电子表格软件.直观的界面.出色的计算功能和图表工具,在系统开发中,经常用来把数据转存到Ex ...

  4. 基于gin框架搭建的一个简单的web服务

    刚把go编程基础知识学习完了,学习的时间很短,可能还有的没有完全吸收.不过还是在项目中发现知识,然后在去回顾已学的知识,现在利用gin这个web框架做一个简单的CRUD操作. 1.Go Web框架的技 ...

  5. 基于NopCommerce框架开发的微信小程序UrShop

    Urshop小程序商城 介绍 UrShop小程序商城 2.0发布啦,发布地址https://gitee.com/urselect/urshop UrShop 根据NopCommerce框架开发的,基于 ...

  6. 基于uniapp框架开发飞书小程序总结

    前期准备 飞书官方客户端文档:https://open.feishu.cn/document/home/intro 飞书官方工具资源文档:https://open.feishu.cn/document ...

  7. 基于vue框架手写一个notify插件,实现通知功能

    简单编写一个vue插件,当点击时触发notify插件,dom中出现相应内容并且在相应时间之后清除,我们可以在根组件中设定通知内容和延迟消失时间. 1. 基础知识 我们首先初始化一个vue项目,删除不需 ...

  8. Flask框架 之第一个Flask程序

    from flask import Flask # 创建flask应用对象 # __name__ 代表当前模块名称 # flask以当前目录为总目录,static目录为静态目录,templates为模 ...

  9. 01 基础版web框架

    01 基础版web框架 服务器server端python程序(基础版): import socket server=socket.socket() server.bind(("127.0.0 ...

  10. 基于Dubbo框架构建分布式服务

    Dubbo是Alibaba开源的分布式服务框架,我们可以非常容易地通过Dubbo来构建分布式服务,并根据自己实际业务应用场景来选择合适的集群容错模式,这个对于很多应用都是迫切希望的,只需要通过简单的配 ...

随机推荐

  1. 【项目源码】基于Spring + Spring MVC + MyBatis的图书馆管理系统

    基于Spring + Spring MVC + MyBatis的图书馆管理系统.主要功能包括:图书查询.图书管理.图书编辑.读者管理.图书的借阅与归还以及借还日志记录等​,非常适合学习研究. 运行配置 ...

  2. 关于MySQL获取自增ID的几种方法

    1. Select Max(id) From Table; 通过取表字段最大值的方式来获取最近一次自增id 缺点: 这种方法在多人操作数据库的软件上不可靠, 举个例子, 你刚插入一条记录. 当你在查询 ...

  3. 2017-A

    2017-A 题目描述: 输入一个字符串,要求输出能把所有的小写字符放前面,大写字符放中间,数字放后面,并且中间用空格隔开,如果同种类字符间有不同种类的字符,输出后也要用字符隔开. 例: 输入 12a ...

  4. MySQL实战实战系列 04 深入浅出索引(上)

    提到数据库索引,我想你并不陌生,在日常工作中会经常接触到.比如某一个 SQL 查询比较慢,分析完原因之后,你可能就会说"给某个字段加个索引吧"之类的解决方案.但到底什么是索引,索引 ...

  5. Oracle中数据的约束

  6. Oracle12C登录PDB容器

    Oracle12C登录PDB用户,此为12C的新特性 ①首先管理员身份登录 sqlplus / as sysdba;--管理员身份登录 show con_name;--查看此时连接容器 显示:CDB$ ...

  7. 【sqli-labs】学习--待续

    预备知识: 数字型注入: 这种sql语句中处理的是整型,不需要使用单引号来闭合变量的值. 首先输入id=1',此时因为不是整型,sql语句会执行出错,抛出异常. 然后输入id=1 and 1=1,此时 ...

  8. 自定义过滤器配置 Shiro 认证失败返回 json 数据

    by emanjusaka from ​ https://www.emanjusaka.top/archives/11 彼岸花开可奈何 本文欢迎分享与聚合,全文转载请留下原文地址. Shiro权限框架 ...

  9. Python拆分列中文和 字符

    需求描述:我们日常实际的工作中经常需要把一列数据按中文和 数字或者字母单独拆分出来 导入所需的库: import pandas as pd 定义函数 extract_characters,该函数接受三 ...

  10. macbook通过虚拟机连接远程linux

    之前操作远程虚拟机,都是用window系统,现第一次用linux命令操作一下linux系统. 苹果启动term 输入ssh root@192.168.3.154连接linux,ssh 用户名@服务器i ...