ToB企服应用市场:ToB评测及商务社交产业平台

标题: Linux之ebpf(1)根本使用 [打印本页]

作者: 王柳    时间: 2024-6-22 20:19
标题: Linux之ebpf(1)根本使用
Linux之ebpf(1)根本使用

  
   Author: Once Day Date: 2024年4月20日


  一位热衷于Linux学习和开发的菜鸟,试图谱写一场冒险之旅,大概终点只是一场白日梦…


   漫漫长路,有人对你微笑过嘛…


  全系列文章可以参考专栏:Linux根本知识_Once-Day的博客-CSDN博客。


  参考文章:
  
  

  
1. 概述

1.1 eBPF背景介绍

eBPF (extended Berkeley Packet Filter) 是一种先进的技能,答应在无需更改内核源代码或加载内核模块的情况下,以安全的方式动态地在内核中执行预编译和沙箱化的步伐。它最初是为了可以或许在内核层面高效地过滤网络包而计划的,但现在它的用途已经大大扩展,可以用于各种体系级编程任务。
eBPF 是 Berkeley Packet Filter (BPF) 的扩展,BPF 最初是在 1992 年为了进步网络包过滤的服从而引入的。2014 年,eBPF 被引入 Linux 内核,从其时起,它的能力和用途不断扩展。

   eBPF技能全局概览图(来自ebpf.io)  eBPF的核心特性

eBPF的工作流程
eBPF的使用场景

随着技能的发展,eBPF 正在成为 Linux 体系监控和管理中不可或缺的工具,它的重要性和应用范围只会不断增长。
1.2 eBPF和cBPF的接洽和区别

   BPF 最初代表伯克利包过滤器 (Berkeley Packet Filter),但是现在 eBPF(extended BPF) 可以做的不仅仅是包过滤,这个缩写不再有意义了。eBPF 现在被认为是一个独立的术语,不代表任何东西。在 Linux 源代码中,术语 BPF 持续存在,在工具和文档中,术语 BPF 和 eBPF 通常可以互换使用。最初的 BPF 有时被称为 cBPF(classic BPF),用以区别于 eBPF。(来自ebpf.io)
  eBPF和cBPF都是Linux内核中用于数据包过滤和处理的技能,但eBPF是cBPF的增强升级版本。它们的主要区别和接洽如下:
eBPFcBPF起源于2014年由Alexei Starovoitov实现,是cBPF的升级版。起源于1992年,由Steven McCanne和Van Jacobson提出,旨在进步网络数据包过滤的服从。性能优化eBPF针对当代硬件举行了优化,生成的指令集比cBPF的解释器生成的呆板码执行速率更快。
eBPF将虚拟机中的寄存器数量从cBPF的2个32位寄存器增长到10个64位寄存器,使得开发职员可以更自由地交换信息,编写更复杂的步伐。原有实现,无进一步优化。功能扩展eBPF不再局限于网络栈,已成为内核顶级子体系,可用于性能分析、软件定义网络等多种场景。cBPF主要用于网络数据包过滤。内核支持eBPF最早出现在Linux 3.18内核中。现在Linux内核只运行eBPF,加载的cBPF字节码会被透明地转换成eBPF再执行。用户空间支持2014年6月,eBPF扩展到用户空间,标志着BPF技能的重要转折点。在tcpdump等报文过滤场景仍在使用。 现在可以使用tcpdump -d "icmp or arp"来查看tcpdump底层使用的cBPF字节码,这些字节码会在Linux内核中透明转换为eBPF表示。如下所示:
  1. onceday->ease-shoot:# tcpdump -d "icmp or arp"
  2. Warning: assuming Ethernet
  3. (000) ldh      [12]
  4. (001) jeq      #0x800           jt 2    jf 4
  5. (002) ldb      [23]
  6. (003) jeq      #0x1             jt 5    jf 6
  7. (004) jeq      #0x806           jt 5    jf 6
  8. (005) ret      #262144
  9. (006) ret      #0
复制代码
1.3 eBPF架构和限制介绍

   BPF 是一个通用目的 RISC 指令集,其最初的计划目标是:用 C 语言的一个子集编写步伐,然后用一个编译器后端(比方 LLVM)将其编译成 BPF 指令,稍后内核再通过一个位于内核中的(in-kernel)即时编译器(JIT Compiler)将 BPF 指令映射成处理器的原生指令(opcode ),以取得在内核中的最佳执行性能。(来自Linux新技能基石 |eBPF and XDP (qq.com))
  

   eBPF技能架构图, 使用libbpf运行eBPF步伐(来自ebpf.io)  eBPF 的工作流程可以总结如下
上述过程中也有一些限制条件:
1.4 eBPF在Linux内核中的应用


   eBPF总体计划图(来自 极客重生 - Linux新技能基石 |eBPF and XDP (qq.com))  eBPF 的计划思想可以总结如下:

eBPF 的计划思想围绕着提供一个灵活、高效、安全、可扩展的内核级编程框架,与内核紧麋集成,并得到了成熟的工具链支持
2. eBPF特性介绍

2.1 eBPF钩子位点(hook)

钩子位点是内核中特定的点,eBPF步伐可以在这些点“挂载”本身,以便在特定变乱发生时执行。预定义的钩子包括体系调用、函数入口/退出、内核跟踪点、网络变乱等。通过这些钩子,eBPF可以或许提供极高的灵活性和强大的监控能力,而且由于它的运行时服从,对体系性能的影响极小。

   eBPF在钩子位点由变乱举行触发(来自ebpf.io)  如果预定义的钩子不能满足特定需求,则可以创建内核探针(kprobe)或用户探针(uprobe),以便在内核或用户应用步伐的几乎任何位置附加 eBPF 步伐。
uprobe(user-level probe)是一种在用户空间步伐中动态插入探测点的机制。eBPF 可以与 uprobe 技能联合使用,实现在用户空间步伐中的过滤和处理功能:
kprobe(kernel probe)是一种内核探测机制,答应在内核函数的入口或返回点插入一个钩子(hook),eBPF 可以与 kprobe 技能协同工作,一般步调如下:
2.2 eBPF字节码验证


   eBPF步伐运行之前需要举行验证(来自ebpf.io)  eBPF 步伐在加载到内核之前,需要颠末严格的验证(Verification)过程,以确保步伐的安全性和可靠性。验证过程主要包括以下几个方面:
(1) 特权级查抄

(2) 步伐的安全性查抄

(3) 步伐复杂度分析

(4) 内存访问和资源限制

(5) 范例和参数查抄

通过这些全面的验证措施,内核确保了 eBPF 步伐的安全性和可靠性,防止了恶意或错误的步伐对体系造成危害。
2.3 eBPF即时编译JIT

eBPF 的 JIT(Just-In-Time)编译器是一种动态编译技能,用于将通用的 eBPF 字节码实时转换为与呆板相干的当地指令集。JIT 编译器极大地进步了 eBPF 步伐的执行性能,相比解释器执行方式有以下上风:
(1) 降低指令开销

(2) 减小可执行镜像大小

(3) 针对 CISC 指令集的优化

现在,多个主流架构都内置了 in-kernel eBPF JIT 编译器,包括:

这些架构上的 eBPF JIT 编译器功能一致,可以通过以下方式启用:
  1. $ echo 1 > /proc/sys/net/core/bpf_jit_enable
复制代码
某些 32 位架构,如 mips、ppc 和 sparc,当前内置的是 cBPF JIT 编译器,而不是 eBPF JIT 编译器。对于这些只支持 cBPF JIT 编译器的架构,以及完全没有 BPF JIT 编译器的架构,需要通过内核中的解释器(in-kernel interpreter)来执行 eBPF 步伐,性能相对较低。
可以通过在内核源代码中搜索 HAVE_EBPF_JIT 宏来判断哪些平台支持 eBPF JIT 编译器。
  1. onceday->ease-shoot:# cat /boot/config-5.15.0-56-generic |grep HAVE_EBPF_JIT
  2. CONFIG_HAVE_EBPF_JIT=y
复制代码
2.4 eBPF数据映射Maps

eBPF Maps用于在内核空间和用户空间之间共享数据,以及在不同的eBPF步伐之间通报数据。
BPF Map 的交互场景有以下几种:

   eBPF键值数据存储和共享(来自ebpf.io)  常见的Map范例如下所示
eBPF Maps提供了灵活而强大的数据交互和共享机制,使得eBPF步伐可以或许与内核和用户空间举行高效的通讯和协作。
2.5 eBPF辅助函数Helpers

   eBPF 步伐不直接调用内核函数。这样做会将 eBPF 步伐绑定到特定的内核版本,会使步伐的兼容性复杂化。而对应地,eBPF 步伐改为调用 helper 函数达到结果,这是内核提供的通用且稳定的 API。(来自ebpf.io)
  eBPF Helpers的功能和作用
使用Helpers的注意事项
所有的 BPF 辅助函数都是核心内核的一部分,无法通过内核模块来扩展或添加。可以在 bpf-helpers(7) - Linux manual page (man7.org) 看到当前 Linux 支持的 Helper functions。
2.6 eBPF尾调用和函数调用

eBPF 步伐可以通过尾调用和函数调用的概念来组合。


   eBPF尾调用和函数调用(来自ebpf.io)  Tail Calls尾调用是一种特殊的函数调用方式,它答应一个eBPF步伐在竣事时直接跳转到另一个eBPF步伐,而不是返回到调用方:

使用方法,使用bpf_tail_call()辅助函数举行尾调用。需要预先定义一个存储eBPF步伐引用的prog_array Map,并将目标步伐的索引作为参数通报给bpf_tail_call()。尾调用的深度有限制,通常为32层,超过限制会导致步伐终止。
BPF调用BPF是指在一个eBPF步伐中直接调用另一个eBPF步伐,类似于函数调用。与尾调用不同,BPF调用BPF答应被调用的步伐返回到调用方,并继续执行后续的代码,被调用的eBPF步伐可以访问调用方的上下文和参数。
在BPF调用BPF特性引入内核之前,典型的 BPF C 步伐必须将所有需要复用的代码举行 always_inline处理。当 LLVM 编译和生成 BPF 对象文件时,会在生成的对象文件中重复多次相同代码,导致指令数尺寸膨胀。
从 Linux 4.16 和 LLVM 6.0 开始,BPF支持函数函数调用,BPF 步伐也不再需要随处使用 always_inline 声明,减小了生成的 BPF 代码大小,因此对CPU指令缓存(instruction cache,i-cache)更友好
2.7 eBPF对象固定(Object Pinning)

对象固定(Object Pinning)答应将eBPF对象(如Maps和Programs)固定到文件体系中,使其可以或许在不同的进程、步伐或体系重启之间共享和持久化。
通过将对象固定到文件体系,可以实现以下功能:

对象固定通过bpf()体系调用和BPF_OBJ_PIN下令来实现。以下是固定Maps和Programs的根本步调:
2.9 eBPF安全增强功能

在成功完成验证后,eBPF 步伐将根据步伐是从特权进程还黑白特权进程加载而运行一个加固过程。这一步包括:

   将 /proc/sys/net/core/bpf_jit_harden 设置为 1 会为非特权用户的 JIT 编译做一些额外的加固工作。这些额外加固会稍微降低步伐的性能,但在有非受信用户在体系上举行利用的情况下,可以或许有用地减小潜伏的受攻击面。但与完全切换到解释器相比,这些性能损失还是比力小的。(来自eBPF 完全入门指南.pdf(万字长文) - 知乎 (zhihu.com))
  盲化 JIT 常量通过对真实指令举行随机化(randomizing the actual instruction)实现 。在这种方式中,通过对指令举行重写,将原来基于立刻数的利用转换成基于寄存器的利用,指令重写将加载值的过程分解为两部分:
3. eBPF工具链介绍

3.1 BCC(BPF Compiler Collection)

BCC 是 BPF 的编译工具集合,前端提供 Python/Lua API,本身通过 C/C++ 语言实现,集成 LLVM/Clang 对 BPF 步伐举行重写、编译和加载等功能, 提供一些更人性化的函数给用户使用。

   BPF开发编译套件(来自ebpf.io)  BCC工具优点如下:

固然 BCC 简化了 BPF 步伐的编写,但仍然需要一定的内核知识和编程技能,并且BCC 的某些功能和工具可能依赖于特定的内核版本和配置,跨平台和兼容性方面有一定限制。
3.2 bpftrace

bpftrace是一款基于eBPF(Extended Berkeley Packet Filter)技能的Linux体系性能分析和跟踪工具。它答应用户编写简单而强大的脚本,以动态地跟踪内核和应用步伐的行为,而无需修改源代码或重新编译内核。
   bpftrace 是一种用于 Linux eBPF 的高级跟踪语言,可在较新的 Linux 内核(4.x)中使用。bpftrace 使用 LLVM 作为后端,将脚本编译为 eBPF 字节码,并利用 BCC 与 Linux eBPF 子体系以及现有的 Linux 跟踪功能(内核动态跟踪(kprobes)、用户级动态跟踪(uprobes)和跟踪点)举行交互。bpftrace 语言的灵感来自于 awk、C 和之前的跟踪步伐,如 DTrace 和 SystemTap。
  

   bpftrace工具(来自ebpf.io)  bpftrace的主要特点包括:

bpftrace常用场景如下:

3.3 libbpf库

libbpf 库是一个基于 C/ c++ 的通用 eBPF 库,它可以资助解耦将 clang/LLVM 编译器生成的 eBPF 对象文件的加载到内核中的这个过程,并通过为应用步伐提供易于使用的库 API 来抽象与 BPF 体系调用的交互。
也有一个基于Go语言实现的eBPF库,支持在Go语言下管理eBPF步伐。

libbpf的主要特点和上风包括:

使用libbpf的一般步调如下
4. eBPF实践(hello world)

4.1 eBPF环境搭建

起首需要根据当前使用的Linux内核版本下载源码,如下所示:
  1. onceday->~:# uname -a
  2. Linux VM-4-17-ubuntu 5.15.0-56-generic #62-Ubuntu SMP Tue Nov 22 19:54:14 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux
  3. onceday->~:# apt search linux-source
  4. Sorting... Done
  5. Full Text Search... Done
  6. linux-source/jammy-updates 5.15.0.105.102 all
  7.   Linux kernel source with Ubuntu patches
  8. linux-source-5.15.0/jammy-updates 5.15.0-105.115 all
  9.   Linux kernel source for version 5.15.0 with Ubuntu patches
  10. ......
复制代码
这里选择当前内核版本的源码(linux-source-5.15.0),然后下载:
  1. onceday->~:# apt install linux-source-5.15.0
复制代码
源码在/usr/src目录下面,直接解压即可:
  1. onceday->~:# cd /usr/src/
  2. onceday->~:# tar -jxvf linux-source-5.15.0.tar.bz2
  3. onceday->~:# cd linux-source-5.15.0/
复制代码
然后拷贝当前体系的config配置到目录下面,并且完成初始编译测试:
  1. onceday->linux-source-5.15.0:# cp /boot/config-5.15.0-56-generic .config
  2. onceday->linux-source-5.15.0:# make scripts
  3. onceday->linux-source-5.15.0:# make headers_install
  4. onceday->linux-source-5.15.0:# make scripts
复制代码
然后是准备编译bpf相干的文件和代码:
  1. onceday->linux-source-5.15.0:# apt install llvm clang libcap-dev libbpf-dev
  2. onceday->linux-source-5.15.0:# make M=samples/bpf
复制代码
编译eBPF相干的示例步伐时,错误还黑白常多的,比方缺少库依赖,路径错误等等,需要逐一寻找解决方案。
4.2 eBPF步伐(kern)

eBPF 通常由内核空间步伐和用户空间步伐两部分组成,内核空间步伐以 _kern.c 结尾,用户空间步伐以 _user.c 结尾。
起首写一个内核中运行的eBPF步伐,如下(hello_kern.c):
  1. #include <linux/bpf.h>
  2. #include <bpf/bpf_helpers.h>
  3. #define SEC(NAME) __attribute__((section(NAME), used))
  4. SEC("tracepoint/syscalls/sys_enter_execve")
  5. int bpf_prog(void *ctx)
  6. {
  7.     char msg[] = "Hello BPF from onceday!\n";
  8.     bpf_trace_printk(msg, sizeof(msg));
  9.     return 0;
  10. }
  11. char _license[] SEC("license") = "GPL";
复制代码
(1) 头文件包罗:

(2) SEC 宏定义:

(3) bpf_prog 函数:

(4) 打印消息:

(5) 返回值:

(6) 许可声明:

要使用这个 BPF 步伐,需要将其编译为 BPF 字节码,并使用 BPF 加载器(如 bpftool 或 libbpf)将其加载到内核中。加载后,每当 execve 体系调用被调用时,就会在跟踪输出中看到这条消息。
4.3 eBPF用户空间步伐

用户空间代码主要使用libbpf来加载bpf步伐,然后读取输出,退出时也要卸载bpf步伐。
  1. #include <bpf/libbpf.h>
  2. #include <signal.h>
  3. #include <stdio.h>
  4. #include <unistd.h>
  5. static struct bpf_object *local_obj;
  6. static struct bpf_link *local_link = NULL;
  7. /* 收到signal时, 主动卸载bpf程序 */
  8. static void signal_handler(int signo) {
  9.   if (signo == SIGINT) {
  10.     printf("Unload bpf program\n");
  11.     bpf_link__destroy(local_link);
  12.     bpf_object__close(local_obj);
  13.     exit(0);
  14.   }
  15. }
  16. int main(int ac, char **argv) {
  17.   struct bpf_program *prog;
  18.   char filename[256];
  19.   FILE *f;
  20.   signal(SIGINT, signal_handler);
  21.   snprintf(filename, sizeof(filename), "hello_kern.o");
  22.   local_obj = bpf_object__open_file(filename, NULL);
  23.   if (libbpf_get_error(local_obj)) {
  24.     fprintf(stderr, "ERROR: opening BPF object file failed\n");
  25.     return 0;
  26.   }
  27.   prog = bpf_object__find_program_by_name(local_obj, "bpf_prog");
  28.   if (!prog) {
  29.     fprintf(stderr, "ERROR: finding a prog in obj file failed\n");
  30.     goto cleanup;
  31.   }
  32.   /* load BPF program */
  33.   if (bpf_object__load(local_obj)) {
  34.     fprintf(stderr, "ERROR: loading BPF object file failed\n");
  35.     goto cleanup;
  36.   }
  37.   local_link = bpf_program__attach(prog);
  38.   if (libbpf_get_error(local_link)) {
  39.     fprintf(stderr, "ERROR: bpf_program__attach failed\n");
  40.     local_link = NULL;
  41.     goto cleanup;
  42.   }
  43.   read_trace_pipe();
  44. cleanup:
  45.   bpf_link__destroy(local_link);
  46.   bpf_object__close(local_obj);
  47.   return 0;
  48. }
复制代码
这段代码是一个用户空间步伐,用于加载和管理 BPF 步伐。它使用 libbpf 库与 BPF 步伐交互。下面是对这段代码的扼要介绍:

在samples/bpf/Makefile文件里面添加三行配置即可:
  1. tprogs-y += hello
  2. hello-objs := hello_user.o $(TRACE_HELPERS)
  3. always-y += hello_kern.o
复制代码
然后重新编译并运行用户空间步伐:
  1. onceday->bpf:# make M=samples/bpf
  2. onceday->bpf:# ./hello
  3. libbpf: elf: skipping unrecognized data section(4) .rodata.str1.16
  4.            <...>-490371  [002] d...1 8467660.788803: bpf_trace_printk: Hello BPF from onceday!
  5.      barad_agent-490372  [000] d...1 8467660.791449: bpf_trace_printk: Hello BPF from onceday!
  6.               sh-490394  [002] d...1 8467664.792088: bpf_trace_printk: Hello BPF from onceday!
  7. ......
复制代码
可以看到,部分步伐触发execve体系调用后,便会打印一个消息,该消息可以在下述管道直接读取:
  1. sudo cat /sys/kernel/debug/tracing/trace_pipe
复制代码
5. 总结

本文简单总结和介绍了eBPF技能历史背景和发展现状,以及几种重要的特性,终极在Linux内核环境下举行了一个hello world的小实验。 eBPF技能对于网络领域开发者来说,学习价值很大,可以或许提升网络流量的可观测性,在不侵入内核的情况下,提供高性能的过滤和处理能力。这里只是一个开始,eBPF学习还是不能浮在表面,必须基于内核源码深入分析,了解流程和思想,才气掌握精髓。
一起开始这趟路程吧!







   Once Day

  

    也信美人终作土,不堪幽梦太匆匆......
    如果这篇文章为您带来了资助或启发,不妨点个赞




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4