Update README.md

This commit is contained in:
云微
2023-02-23 04:17:20 +08:00
committed by GitHub
parent 7dbf035eb3
commit c817a06b42

View File

@@ -1,104 +1,90 @@
# eBPF 入门开发实践教程一:介绍 eBPF 的基本概念、常见的开发工具
<!-- TOC -->
## 1. eBPF简介安全和有效地扩展内核
- [eBPF 入门开发实践教程一:介绍 eBPF 的基本概念、常见的开发工具](#ebpf-入门开发实践教程一介绍-ebpf-的基本概念常见的开发工具)
- [1. 为什么会有 eBPF 技术?](#1-为什么会有-ebpf-技术)
- [1.1. 起源](#11-起源)
- [1.2. 执行逻辑](#12-执行逻辑)
- [1.3. 架构](#13-架构)
- [1.3.1. 寄存器设计](#131-寄存器设计)
- [1.3.2. 指令编码格式](#132-指令编码格式)
- [1.4. 本节参考文章](#14-本节参考文章)
- [2. 如何使用eBPF编程](#2-如何使用ebpf编程)
- [编写 eBPF 程序](#编写-ebpf-程序)
- [2.1. BCC](#21-bcc)
- [2.2. libbpf-bootstrap](#22-libbpf-bootstrap)
- [2.3 eunomia-bpf](#23-eunomia-bpf)
- [参考资料](#参考资料)
eBPF 是一项革命性的技术,起源于 Linux 内核可以在操作系统的内核中运行沙盒程序。它被用来安全和有效地扩展内核的功能而不需要改变内核的源代码或加载内核模块。eBPF 通过允许在操作系统内运行沙盒程序应用程序开发人员可以在运行时可编程地向操作系统动态添加额外的功能。然后操作系统保证安全和执行效率就像在即时编译JIT编译器和验证引擎的帮助下进行本地编译一样。eBPF 程序在内核版本之间是可移植的,并且可以自动更新,从而避免了工作负载中断和节点重启。
<!-- /TOC -->
今天eBPF被广泛用于各类场景在现代数据中心和云原生环境中可以提供高性能的网络包处理和负载均衡以非常低的资源开销做到对多种细粒度指标的可观测性帮助应用程序开发人员跟踪应用程序为性能故障排除提供洞察力保障应用程序和容器运行时的安全执行等等。可能性是无穷的而 eBPF 在操作系统内核中所释放的创新才刚刚开始[3]。
## 1. 为什么会有 eBPF 技术?
### eBPF 的未来:内核的 JavaScript 可编程接口
Linux内核一直是实现监控/可观测性、网络和安全功能的理想地方但是直接在内核中进行监控并不是一个容易的事情。在传统的Linux软件开发中实现这些功能往往都离不开修改内核源码或加载内核模块。修改内核源码是一件非常危险的行为稍有不慎可能便会导致系统崩溃并且每次检验修改的代码都需要重新编译内核耗时耗力
对于浏览器而言JavaScript 的引入带来的可编程性开启了一场巨大的革命,使浏览器发展成为几乎独立的操作系统。现在让我们回到 eBPF为了理解 eBPF 对 Linux 内核的可编程性影响,对 Linux 内核的结构以及它如何与应用程序和硬件进行交互有一个高层次的理解是有帮助的[4]
加载内核模块虽然来说更为灵活,不需要重新编译源码,但是也可能导致内核崩溃,且随着内核版本的变化,模块也需要进行相应的修改,否则将无法使用。
!(kernel-arch](kernel-arch.webp)
在这一背景下eBPF技术应运而生。它是一项革命性技术能在内核中运行沙箱程序sandbox programs而无需修改内核源码或者加载内核模块。用户可以使用其提供的各种接口实现在内核中追踪、监测系统的作用。
Linux 内核的主要目的是抽象出硬件或虚拟硬件并提供一个一致的API系统调用允许应用程序运行和共享资源。为了实现这个目的我们维护了一系列子系统和层以分配这些责任[5]。每个子系统通常允许某种程度的配置,以考虑到用户的不同需求。如果不能配置所需的行为,就需要改变内核,从历史上看,改变内核的行为,或者让用户编写的程序能够在内核中运行,就有两种选择:
### 1.1. 起源
| 本地支持内核模块 | 写一个内核模块 |
| ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| 改变内核源代码并说服Linux内核社区相信这种改变是必要的。等待几年让新的内核版本成为一种商品。 | 定期修复它因为每个内核版本都可能破坏它。由于缺乏安全边界冒着破坏你的Linux内核的风险 |
eBPF的雏形是BPF(Berkeley Packet Filter, 伯克利包过滤器)。BPF于
1992年被Steven McCanne和Van Jacobson在其[论文](https://www.tcpdump.org/papers/bpf-usenix93.pdf)
提出。二人提出BPF的初衷是是提供一种新的数据包过滤方法该方法的模型如下图所示。
![original_bpf](../imgs/original_bpf.png)
实际上,两种方案都不常用,前者成本太高,后者则几乎没有可移植性。
相较于其他过滤方法BPF有两大创新点首先是它使用了一个新的虚拟机可以有效地工作在基于寄存器结构的CPU之上。其次是其不会全盘复制数据包的所有信息只会复制相关数据可以有效地提高效率。这两大创新使得BPF在实际应用中得到了巨大的成功在被移植到Linux系统后其被上层的`libcap`
`tcpdump`等应用使用,是一个性能卓越的工具。
有了 eBPF就有了一个新的选择可以重新编程 Linux 内核的行为,而不需要改变内核的源代码或加载内核模块,同时保证在不同内核版本之间一定程度上的行为一致性和兼容性、以及安全性[6]。为了实现这个目的eBPF 程序也需要有一套对应的 API允许用户定义的应用程序运行和共享资源 --- 换句话说,某种意义上讲 eBPF 虚拟机也提供了一套类似于系统调用的机制,借助 eBPF 和用户态通信的机制Wasm 虚拟机和用户态应用也可以获得这套“系统调用”的完整使用权,一方面能可编程地扩展传统的系统调用的能力,另一方面能在网络、文件系统等许多层次实现更高效的可编程 IO 处理。
传统的BPF是32位架构其指令集编码格式为
![new-os](new-os-model.jpg)
- 16 bit: 操作指令
- 8 bit: 下一条指令跳向正确目标的偏移量
- 8 bit: 下一条指令跳往错误目标的偏移量
正如上图所示,当今的 Linux 内核正在向一个新的内核模型演化:用户定义的应用程序可以在内核态和用户态同时执行,用户态通过传统的系统调用访问系统资源,内核态则通过 BPF Helper Calls 和系统的各个部分完成交互。截止 2023 年初,内核中的 eBPF 虚拟机中已经有 220 多个Helper 系统接口,涵盖了非常多的应用场景。
经过十余年的沉积后2013年Alexei Starovoitov对BPF进行了彻底地改造改造后的BPF被命名为eBPF(extended BPF)于Linux Kernel 3.15中引入Linux内核源码
eBPF相较于BPF有了革命性的变化。首先在于eBPF支持了更多领域的应用它不仅支持网络包的过滤还可以通过
`kprobe``tracepoint`,`lsm`等Linux现有的工具对响应事件进行追踪。另一方面其在使用上也更为
灵活更为方便。同时其JIT编译器也得到了升级解释器也被替换这直接使得其具有达到平台原生的
执行性能的能力。
值得注意的是BPF Helper Call 和系统调用二者并不是竞争关系,它们的编程模型和有性能优势的场景完全不同,也不会完全替代对方。对 Wasm 和 Wasi 相关生态来说,情况也类似,专门设计的 wasi 接口需要经历一个漫长的标准化过程,但可能在特定场景能为用户态应用获取更佳的性能和可移植性保证,而 eBPF 在保证沙箱本质和可移植性的前提下,可以提供一个快速灵活的扩展系统接口的方案
### 1.2. 执行逻辑
目前的 eBPF 仍然处于早期阶段,但是借助当前 eBPF 提供的内核接口和用户态交互的能力,经由 Wasm-bpf 的系统接口转换Wasm 虚拟机中的应用已经几乎有能力获取内核以及用户态任意一个函数调用的数据和返回值kprobeuprobe...以很低的代价收集和理解所有系统调用并获取所有网络操作的数据包和套接字级别的数据tracepointsocket...在网络包处理解决方案中添加额外的协议分析器并轻松地编程任何转发逻辑XDPTC...以满足不断变化的需求而无需离开Linux内核的数据包处理环境。
eBPF在执行逻辑上和BPF有相似之处eBPF也可以认为是一个基于寄存器的使用自定义的64位RISC指令集的
微型"虚拟机"。它可以在Linux内核中以一种安全可控的方式运行本机编译的eBPF程序并且访问内核函数和内存的子集。
不仅如此eBPF 还有能力往用户空间任意进程的任意地址写入数据bpf_probe_write_user[7]有限度地修改内核函数的返回值bpf_override_return[8]),甚至在内核态直接执行某些系统调用[9]所幸的是eBPF 在加载进内核之前对字节码会进行严格的安全检查,确保没有内存越界等操作,同时,许多可能会扩大攻击面、带来安全风险的功能都是需要在编译内核时明确选择启用才能使用的;在 Wasm 虚拟机将字节码加载进内核之前,也可以明确选择启用或者禁用某些 eBPF 功能,以确保沙箱的安全性。
在写好程序后我们将代码使用llvm编译得到使用BPF指令集的ELF文件解析出需要注入的部分后调用函数将其
注入内核。用户态的程序和注入内核态中的字节码公用一个位于内核的eBPF Map进行通信实现数据的传递。同时
为了防止我们写入的程序本身不会对内核产生较大影响编译好的字节码在注入内核之前会被eBPF校验器严格地检查。
## 2. 关于如何学习 eBPF 相关的开发的一些建议
eBPF程序是由事件驱动的我们在程序中需要提前确定程序的执行点。编译好的程序被注入内核后如果提前确定的执行点
被调用,那么注入的程序就会被触发,按照既定方式处理。
本文不会对 eBPF 的原理做更详细的介绍,不过这里有一个学习规划和参考资料,也许会有一些价值:
### 1.3. 架构
### eBPF 入门5-7h
#### 1.3.1. 寄存器设计
- Google 或者其他搜索引擎查找eBPF
- 询问 ChatGPT 之类的东西eBPF 是什么?
eBPF有11个寄存器分别是R0~R10每个寄存器均是64位大小有相应的32位子寄存器其指令集是固定的64位宽。
推荐:
#### 1.3.2. 指令编码格式
- 阅读 ebpf 简介https://ebpf.io/30min
- 简要了解一下 ebpf 内核相关文档https://prototype-kernel.readthedocs.io/en/latest/bpf/ (知道有问题去哪里查询: 30min
- 阅读 ebpf 中文入门指南https://www.modb.pro/db/3915701h
- 有大量的参考资料https://github.com/zoidbergwill/awesome-ebpf2-3h
- 可以选自己感兴趣的 PPT 翻一翻https://github.com/gojue/ebpf-slide1-2h
eBPF指令编码格式为
回答三个问题
- 8 bit: 存放真实指令码
- 4 bit: 存放指令用到的目标寄存器号
- 4 bit: 存放指令用到的源寄存器号
- 16 bit: 存放偏移量,具体作用取决于指令类型
- 32 bit: 存放立即数
1. 了解 eBPF 是什么东西?为啥要有这个玩意,不能用内核模块?
2. 它有什么功能?能在 Linux 内核里面完成哪些事情?有哪些 eBPF 程序的类型和 helper不需要知道全部但是需要知道去哪里找
3. 能拿来做什么?比如说在哪些场景中进行运用?网络、安全、可观测性?
### 1.4. 本节参考文章
### 了解如何开发 eBPF 程序10-15h
[A thorough introduction to eBPF](https://lwn.net/Articles/740157/)
[bpf简介](https://www.collabora.com/news-and-blog/blog/2019/04/05/an-ebpf-overview-part-1-introduction/)
[bpf架构知识](https://www.collabora.com/news-and-blog/blog/2019/04/15/an-ebpf-overview-part-2-machine-and-bytecode/)
了解并尝试一下 eBPF 开发框架:
## 2. 如何使用eBPF编程
- BCC 开发各类小工具的例子https://github.com/iovisor/bcc/blob/master/docs/tutorial_bcc_python_developer.md跑一遍3-4h
- libbpf 的一些例子: https://github.com/libbpf/libbpf-bootstrap选感兴趣的运行一下并阅读一下源代码2h
- 基于 libbpf 和 eunomia-bpf 的教程: https://github.com/eunomia-bpf/bpf-developer-tutorial阅读 1-10 的部分: 3-4h
原始的eBPF程序编写是非常繁琐和困难的。为了改变这一现状
llvm于2015年推出了可以将由高级语言编写的代码编译为eBPF字节码的功能同时其将`bpf()`
等原始的系统调用进行了初步地封装,给出了`libbpf`库。这些库会包含将字节码加载到内核中
的函数以及一些其他的关键函数。在Linux的源码包的`samples/bpf/`目录下有大量Linux
提供的基于`libbpf`的eBPF样例代码。
其他开发框架Go 语言或者 Rust 语言请自行搜索并且尝试0-2h
一个典型的基于 `libbpf` 的eBPF程序具有`*_kern.c``*_user.c`两个文件,
`*_kern.c`中书写在内核中的挂载点以及处理函数,`*_user.c`中书写用户态代码,
完成内核态代码注入以及与用户交互的各种任务。 更为详细的教程可以参考[该视频](https://www.bilibili.com/video/BV1f54y1h74r?spm_id_from=333.999.0.0)
然而由于该方法仍然较难理解且入门存在一定的难度因此现阶段的eBPF程序开发大多基于一些工具比如
有任何问题或者想了解的东西,不管是不是和本项目相关,都可以在本项目的 discussions 里面开始讨论。
回答一些问题并且进行一些尝试2-5h
1. 如何开发一个最简单的 eBPF 程序?
2. 如何用 eBPF 追踪一个内核功能或函数?有很多种方法,举出对应的代码;
3. 有哪些方案能通过用户态和内核态通信?如何从用户态向内核态传送信息?如何从内核态向用户态传递信息?举出代码示例;
4. 编写一个你自己的 eBPF 程序,实现一个功能;
5. eBPF 程序的整个生命周期里面,分别在用户态和内核态做了哪些事情?
## 3. 如何使用eBPF编程
原始的eBPF程序编写是非常繁琐和困难的。为了改变这一现状llvm于2015年推出了可以将由高级语言编写的代码编译为eBPF字节码的功能同时eBPF 社区将 `bpf()` 等原始的系统调用进行了初步地封装,给出了`libbpf`库。这些库会包含将字节码加载到内核中的函数以及一些其他的关键函数。在Linux的源码包的`samples/bpf/`目录下有大量Linux提供的基于`libbpf`的eBPF样例代码。
一个典型的基于 `libbpf` 的eBPF程序具有`*_kern.c``*_user.c`两个文件,`*_kern.c`中书写在内核中的挂载点以及处理函数,`*_user.c`中书写用户态代码,完成内核态代码注入以及与用户交互的各种任务。 更为详细的教程可以参考[该视频](https://www.bilibili.com/video/BV1f54y1h74r?spm_id_from=333.999.0.0)然而由于该方法仍然较难理解且入门存在一定的难度因此现阶段的eBPF程序开发大多基于一些工具比如
- BCC
- BPFtrace
- libbpf-bootstrap
- Go eBPF library
以及还有比较新的工具,例如 `eunomia-bpf`.
@@ -117,7 +103,7 @@ eBPF 程序由内核态部分和用户态部分构成。内核态部分包含程
- tracepoint_return跟踪点函数返回在指定的内核跟踪点返回时执行。
- raw_tracepoint_return原始跟踪点函数返回在指定的内核原始跟踪
### 2.1. BCC
### BCC
BCC全称为BPF Compiler Collection该项目是一个python库
包含了完整的编写、编译、和加载BPF程序的工具链以及用于调试和诊断性能问题的工具。
@@ -134,7 +120,11 @@ eBPF程序每次执行时候都需要进行编译编译则需要用户配置
相信大家也会有体会编译依赖问题是一个很棘手的问题。也正是因此在本项目的开发中我们放弃了BCC
选择了可以做到一次编译-多次运行的libbpf-bootstrap工具。
### 2.2. libbpf-bootstrap
### eBPF Go library
eBPF Go库提供了一个通用的eBPF库它解耦了获取 eBPF 字节码的过程和 eBPF 程序的加载和管理,并实现了类似 libbpf 一样的 CO- 功能。eBPF程序通常是通过编写高级语言创建的然后使用clang/LLVM编译器编译为eBPF字节码。
### libbpf
`libbpf-bootstrap`是一个基于`libbpf`库的BPF开发脚手架从其
[github](https://github.com/libbpf/libbpf-bootstrap) 上可以得到其源码。
@@ -152,7 +142,7 @@ eBPF程序每次执行时候都需要进行编译编译则需要用户配置
字节码装载到内核中。同样的,`libbpf-bootstrap`也有非常完备的入门教程,用户可以在[该处](https://nakryiko.com/posts/libbpf-bootstrap/)
得到详细的入门操作介绍。
### 2.3 eunomia-bpf
### eunomia-bpf
开发、构建和分发 eBPF 一直以来都是一个高门槛的工作,使用 BCC、bpftrace 等工具开发效率高、可移植性好,但是分发部署时需要安装 LLVM、Clang等编译环境每次运行的时候执行本地或远程编译过程资源消耗较大使用原生的 CO-RE libbpf时又需要编写不少用户态加载代码来帮助 eBPF 程序正确加载和从内核中获取上报的信息,同时对于 eBPF 程序的分发、管理也没有很好地解决方案。
@@ -160,7 +150,7 @@ eBPF程序每次执行时候都需要进行编译编译则需要用户配置
使用 eunomia-bpf ,可以:
- 在编写 eBPF 程序或工具时只编写内核态代码,自动获取内核态导出信息;
- 在编写 eBPF 程序或工具时只编写内核态代码,自动获取内核态导出信息,并作为模块动态加载
- 使用 WASM 进行用户态交互程序的开发,在 WASM 虚拟机内部控制整个 eBPF 程序的加载和执行,以及处理相关数据;
- eunomia-bpf 可以将预编译的 eBPF 程序打包为通用的 JSON 或 WASM 模块,跨架构和内核版本进行分发,无需重新编译即可动态加载运行。