eBPF原理介绍与C语言实现eBPF程序
eBPF原理介绍与C语言实现eBPF程序
之前的文章<<使用eBPF和BCC调查创建文件的进程>>介绍了基于BCC
来实现eBPF
程序。BCC
实现了对eBPF
的封装,用户态部分提供Python API, 内核态部分使用的eBPF
程序还是通过C
语言来实现。运行时BCC
会把eBPF
的C
程序编译成字节码、加载到内核执行,最后再通过用户空间的前端程序获取执行状态。
可以在BCC
的BPF
调用中,指定参数debug=4
, 我们可以看到BCC
的执行过程, 如:
1
|
from bcc import BPF
|
eBPF
编程的门槛还是比较高的,在当前还是快速发展的情况,API也还不稳定,对程序员的C
语言、编译过程和内核等知识都有比较高的要求。BCC
把这些都封装起来给用户提供了一个更为简单的使用框架。但本身也存在的一些问题,比如:
- 每次执行时都需要重新编译
- 执行程序的机器都需要安装内核头文件
eBPF: extended Berkeley Packet Filter
是对BPF
(现在称为cBPF: classic BPF
)的扩展, 现在尽管还叫做BPF
, 但应用场景已经远远超过了它的名称的范畴。它的应用范围的扩大主要得益于这几方面:
- 内核中
BPF
字节码虚拟机扩展为一个通用的执行引擎 - 执行可节码的安全校验
JIT
支持,可以直接将字节码指令转成内核可执行的原生指令运行
这样在安全性、可编程性和性能方面的提升都使得eBPF
在包过滤以外的其他领域获取巨大的应用空间。
eBPF
的整体架构图如下:
![](http://just4coding.com/images/2022-03-22/1.png)
图片来自: https://cloudnative.to/blog/bpf-intro/linux_ebpf_internals.png
eBPF
程序分为两部分:
- 内核态部分: 内核中的
eBPF
字节码程序负责在内核中处理特定事件,并可以将处理的结果通过maps
或者perf-event
发送到用户空间 - 用户态部分: 用户态部分主要有两方面作用:
- 加载
eBPF
字节码程序到内核中 - 与内核态
eBPF
程序之写读写信息
- 加载
eBPF
本身是事件驱动触发的机制,因而需要将特定的内核事件与eBPF
字节码程序进行关联。
eBPF
程序的开发及运行的一个典型过程如下:
- 编写
eBPF
程序,并编译成字节码,目前只能使用CLANG
和LLVM
编译成eBPF
字节码 - 将
eBPF
程序加载到内核中,内核会校验字节码避免内核崩溃 - 将内核事件与
eBPF
程序进行关联 - 内核事件发生时,
eBPF
程序执行,发送信息给用户态程序 - 用户态程序读取相关信息
我们还是以之前文章中的示例来说明上述过程。上文我们提到我们的系统是CentOS7.8
, 尽管CentOS7
上的eBPF
支持还是比较实验性的,但大多数功能还是能支持的。
由于要使用CLANG
和LLVM
来编译eBPF
程序,而CentOS7
上默认yum
安装的CLANG
和LLVM
的版本比较老,不支持eBPF
的编译。可以从这个repo
中安装, 在/etc/yum.repos.d/
下创建文件c7-llvm.repo
文件, 内容如下:
1
|
[c7-llvm-toolset-9]
|
然后执行以下命令进行安装并启用:
1
|
yum install llvm-toolset-9.0
|
准备好编译环境之后,我们开始编写eBPF
程序, vfs_create.c
内容如下:
1
|
|
程序逻辑和之前文章中类似,可以看到和写在BCC
中的C
代码是不同的。例如,这里使用的eBPF
程序的参数只有一个struct pt_regs *ctx
, 其他的参数需要从ctx
中获取到,而BCC
中的C
代码的函数参数中就已经带有相关参数了。因为BCC
会在编译代码前对代码进行处理,可以使用文章开头所说的debug=4
来查看这些细节。
然后编写Makefile
:
1
|
OBJS = vfs_create.o
|
编译我们编写的eBPF
程序,这需要提前安装好相应内核版本的kernel-devel
包:
1
|
make
|
这会在当前目录生成vfs_create.o
文件, 使用llvm-objdump
查看生成的eBPF
程序:
1
|
[root@default bpf]# llvm-objdump -S ./vfs_create.o
|
至此我们完成了上述典型过程的第一步。接下来看如何加载eBPF
程序。
可以直接使用bpftool
来加载程序,具体的使用信息可以参考man bpftool-prog
。
执行bpftool
之前需要加挂载/sys/fs/bpf
目录,这是因为内核中的eBPF
对象由一个文件描述符
引用,当bpftool
工具退出时,相应的文件描述符关闭时,eBPF
程序也就销毁了,因而内核提供了/sys/fs/bpf
机制,保证程序退出后,eBPF
程序依然存在,对细节感兴趣可以参考LWN
的这篇。
1
|
mount -t bpf none /sys/fs/bpf
|
加载eBPF
程序, 使用show
命令可以看到我们创建的eBPF
程序:
1
|
[root@default bpf]# bpftool prog load vfs_create.o /sys/fs/bpf/vfs_create
|
要删除eBPF
程序只需要移除bpffs
里的文件:
1
|
rm /sys/fs/bpf/vfs_create
|
为了更好的说明加载的过程,我们使用C
语言调用bpf
系统调用实现一个简单的加载器loader.c
:
1
|
|
为了简单没有在代码中处理ELF
文件格式,而直接使用eBPF
的字节码。我们可以使用dd
程序从vfs_create.o
这个ELF
文件中抽取出字节码。
使用llvm-readelf
查看vfs_create.o
, 可以看到kprobe/vfs_create
段的偏移量是000040
,也就是64
, 大小是0000c0
, 也就是192
字节:
1
|
[root@default bpf]# llvm-readelf -S ./vfs_create.o
|
使用dd
命令抽取字节码部分:
1
|
[root@default bpf]# dd if=./vfs_create.o of=vfs_create.bpf bs=1 count=192 skip=64
|
编译我们编写的loader
, 并加载eBPF
程序:
1
|
[root@default bpf]# ./loader ./vfs_create.bpf /sys/fs/bpf/vfs_create
|
此时使用bpftool
查看eBPF
程序, 可以看到我们的程序被成功加载:
1
|
[root@default user]# bpftool prog show
|
至此第二步完成。
第三步是要内核事件与加载的eBPF
程序进行关联。我们使用的事件源是kprobe
。每个kprobe
或kretprobe
被创建时都会关联一个id
, 存储在/sys/kernel/debug/tracing/events/[uk]probe/xxxxxx/id
或/sys/kernel/debug/tracing/events/[uk]retprobe/xxxxxx/id
中。具体怎样使用sysfs
创建kprobe
可以参考内核文档。我们需要使用这个id
打开一个perf_event
并启用它,关联到指定的eBPF
程序做为我们的事件处理程序。
首先创建vfs_create
的kprobe
, 并查看相应probe
的id
:
1
|
[root@default user]# echo 'p:vfs_create vfs_create' >> /sys/kernel/debug/tracing/kprobe_events
|
编写关联逻辑的代码attacher.c
:
1
|
|
1
|
[root@default user]# gcc attacher.c -o attacher
|
bpf_trace_printk
会将信息写入到文件/sys/kernel/debug/tracing/trace_pipe
。我们打开一个终端读取它的内容, 在另一个终端上,在/tmp
目录下创建文件:
1
|
touch /tmp/dummy.xxx
|
可以看到trace_pipe
内容中看到相应的文件记录:
1
|
[root@default tmp]# cat /sys/kernel/debug/tracing/trace_pipe
|
eBPF
现在发展很快,不同的内核版本上,这里所用的代码可能需要有所调整。想要具体的了解eBPF
编程相关的内容还是要熟悉内核中eBPF
相关的代码,而且在不同的内核版本上,文件目录可能都会有所不同。
参考链接:
- https://cloud.tencent.com/developer/article/1749470?from=article.detail.1757600
- https://goteleport.com/blog/what-is-ebpf/
- https://cloud.tencent.com/developer/article/1916561?from=article.detail.1749470
- https://mp.weixin.qq.com/s?__biz=MzA4MjM3NzE5MQ==&mid=2649662456&idx=1&sn=de53747b1ca74f0f15398f118b78026a&chksm=879ca370b0eb2a661bfd48242ec2f66b1f58e4ac1a76fe712fbf7181ddfb69f0a79509625f2d&scene=21#wechat_redirect
- https://ebpf.io/
- https://www.njcx.bid/posts/S6.html
- https://cyral.com/blog/how-to-ebpf-accelerating-cloud-native/
- https://qmonnet.github.io/whirl-offload/2020/04/12/llvm-ebpf-asm/
- https://arthurchiao.art/blog/ebpf-assembly-with-llvm-zh/
- https://cloudnative.to/blog/bpf-intro/
- https://cloudnative.to/blog/compile-bpf-examples/
- https://www.lijiaocn.com/%E6%8A%80%E5%B7%A7/2019/02/25/ebpf-introduction-1.html
- https://lwn.net/Articles/599755/
- https://www.collabora.com/news-and-blog/blog/2019/04/05/an-ebpf-overview-part-1-introduction/
- https://blog.raymond.burkholder.net/index.php?/archives/1000-eBPF-Basics.html
- https://lwn.net/Articles/664688/
- https://blogs.oracle.com/linux/post/bpf-a-tour-of-program-types
- https://blogs.oracle.com/linux/post/bpf-in-depth-bpf-helper-functions
- https://blogs.oracle.com/linux/post/bpf-in-depth-communicating-with-userspace
- https://blogs.oracle.com/linux/post/bpf-in-depth-building-bpf-programs
- https://blogs.oracle.com/linux/post/bpf-in-depth-the-bpf-bytecode-and-the-bpf-verifier
- https://blogs.oracle.com/linux/post/bpf-using-bpf-to-do-packet-transformation
- https://blogs.oracle.com/linux/post/notes-on-bpf-7-bpf-tc-and-generic-segmentation-offload
- http://terenceli.github.io/%E6%8A%80%E6%9C%AF/2020/01/18/ebpf-in-c
- https://stackoverflow.com/questions/48721256/error-while-loading-a-bpf-program-that-copies-a-buffer-to-the-bpf-stack
- https://www.kernel.org/doc/html/latest/bpf/verifier.html#understanding-ebpf-verifier-messages
- https://www.kernel.org/doc/Documentation/trace/kprobetrace.txt