diff --git a/0-introduce/README.md b/0-introduce/README.md index f3d171a..fc40f79 100644 --- a/0-introduce/README.md +++ b/0-introduce/README.md @@ -1,104 +1,90 @@ # eBPF 入门开发实践教程一:介绍 eBPF 的基本概念、常见的开发工具 - +## 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 程序在内核版本之间是可移植的,并且可以自动更新,从而避免了工作负载中断和节点重启。 - +今天,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 虚拟机中的应用已经几乎有能力获取内核以及用户态任意一个函数调用的数据和返回值(kprobe,uprobe...);以很低的代价收集和理解所有系统调用,并获取所有网络操作的数据包和套接字级别的数据(tracepoint,socket...);在网络包处理解决方案中添加额外的协议分析器,并轻松地编程任何转发逻辑(XDP,TC...),以满足不断变化的需求,而无需离开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/391570(1h) +- 有大量的参考资料:https://github.com/zoidbergwill/awesome-ebpf(2-3h) +- 可以选自己感兴趣的 PPT 翻一翻:https://github.com/gojue/ebpf-slide(1-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 模块,跨架构和内核版本进行分发,无需重新编译即可动态加载运行。