Linux之ebpf(1)根本使用

王柳  金牌会员 | 2024-6-22 20:19:58 | 来自手机 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 541|帖子 541|积分 1623

Linux之ebpf(1)根本使用

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


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


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


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


  参考文章:
  

  • eBPF 完全入门指南.pdf(万字长文) - 知乎 (zhihu.com)
  • Linux新技能基石 |eBPF and XDP (qq.com)
  • 什么是 eBPF ? An Introduction and Deep Dive into the eBPF Technology
  • 聊聊迩来很火的eBPF - 知乎 (zhihu.com)
  • BPF and XDP Reference Guide — Cilium 1.15.4 documentation
  • bpf-helpers(7) - Linux manual page (man7.org)
  

  
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 支持各种范例的步伐,包括网络相干的过滤和监控,体系调用的审计和监控,以及性能分析。
  • 可编程性:eBPF 提供了一种根本设施,可以在不改变内核代码的情况下,插入自定义的代码片段来扩展内核的功能。
  • 工具生态:随着 eBPF 的流行,出现了许多工具和项目(如 BCC, bpftrace, Cilium)来简化编写和摆设 eBPF 步伐的过程。
eBPF的工作流程

  • 编写 eBPF 步伐:通常使用 C 语言编写,并针对 eBPF 虚拟机的指令集举行编译。
  • 加载到内核:编译后的 eBPF 步伐通过特定的体系调用被加载到内核。
  • 验证:内核中的验证器查抄步伐是否安全执行(不会崩溃内核或访问不该访问的内存区域)。
  • JIT 编译:为了进步执行服从,内核可以将 eBPF 字节码即时(JIT)编译成本机代码。
  • 附加到 Hook 点:eBPF 步伐附加到内核的各种 hook 点,比方网络变乱、体系调用或其他内核函数。
  • 运行:当相干的变乱发生时,eBPF 步伐将被执行。
eBPF的使用场景


  • 网络监控与安全: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 的工作流程可以总结如下

  • 编写阶段:开发者使用 C 语言编写 BPF 步伐,包括用户空间步伐和内核中的 BPF 字节码步伐。
  • 编译阶段:使用 LLVM 或 GCC 工具将编写的 BPF 代码编译成 BPF 字节码。
  • 加载阶段:用户空间步伐使用加载步伐(Loader)将编译好的 BPF 字节码加载到内核中。
  • 验证阶段:内核中的验证器(Verifier)组件会查抄加载的 BPF 字节码的安全性,以确保它不会对内核造成危害。
  • 执行阶段:验证通过后,BPF 字节码步伐会附加到内核的特定变乱或函数上,当变乱被触发时,BPF 步伐就会在内核中执行。
  • 通讯阶段:内核中运行的 BPF 步伐可以通过两种方式与用户空间步伐举行通讯:

    • 使用 maps 结构将内核中的统计信息、状态等数据通报给用户空间步伐。
    • 通过 perf-event 将内核中的变乱实时发送给用户空间步伐举行分析。

  • 分析阶段:用户空间步伐可以读取和分析从内核中通报来的数据,实现对体系的监控、追踪、性能分析等功能。
上述过程中也有一些限制条件:

  • 不能调用任意的内核参数,只限于内核模块中列出的 BPF Helper 函数。
  • 不答应包罗无法到达的指令,防止加载无效代码,延迟步伐的终止。
  • 循环次数限制且必须在有限时间内竣事,防止锁住整个体系。
  • 堆栈大小被限制在 MAX_BPF_STACK(include/linux/filter.h),一般为512字节。
  • 字节码大小最初被限制为BPF_COMPLEXITY_LIMIT_INSNS(include/linux/bpf.h)条指令,现在支持一百万条指令。
1.4 eBPF在Linux内核中的应用


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


  • 灵活性和可编程性:eBPF 提供了一个独立的指令集,答应开发者编写自定义的内核级步伐,可以在内核运行时动态加载和执行。
  • 高效性:eBPF 步伐在内核中执行,可以直接访问内核数据结构和函数,避免了用户态和内核态之间的上下文切换开销。
  • 安全性:eBPF 引入了验证器机制,确保加载到内核中的 eBPF 步伐是安全的,不会对内核造成危害。验证器会对 eBPF 步伐举行严格的查抄,比方防止无限循环、非法内存访问等。别的,eBPF 还提供了一些安全加固原语,如 helper 函数,用于与内核功能安全地交互。
  • 可扩展性:eBPF 提供了多种机制来扩展其功能。比方,通过 helper 函数,eBPF 步伐可以与内核功能交互并利用内核功能;通过尾调用,eBPF 步伐可以调用其他 eBPF 步伐,实现功能的组合和复用;通过伪文件体系,可以方便地管理 eBPF 对象(如 maps 和步伐)。
  • 工具链支持:eBPF 得到了 LLVM 编译器工具链的支持。开发者可以使用 C 语言编写 eBPF 步伐,然后使用 clang 等工具将其编译为 BPF 目标文件,再加载到内核中执行。
  • 内核集成:eBPF 与 Linux 内核紧麋集成,eBPF 步伐可以在不捐躯本机内核性能的情况下,实现完全可编程的功能扩展和优化。
  • 硬件卸载:eBPF 还提供了将其功能卸载到网卡硬件的根本设施,可以进一步进步性能并减轻主机 CPU 的负担。
eBPF 的计划思想围绕着提供一个灵活、高效、安全、可扩展的内核级编程框架,与内核紧麋集成,并得到了成熟的工具链支持
2. eBPF特性介绍

2.1 eBPF钩子位点(hook)

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

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

  • 在用户空间步伐的特定位置插入 uprobe 钩子。这可以通过在步伐的源代码中添加特殊的宏或标志,或者通过动态二进制插桩技能来实现。
  • 编写 eBPF 步伐,定义在 uprobe 钩子触发时要执行的利用。eBPF 步伐可以访问 uprobe 上下文,获取函数参数、返回值等信息,并根据这些信息举行过滤和处理。
  • 将 eBPF 步伐加载到内核中,并将其附加到 uprobe 钩子上。当用户空间步伐执行到 uprobe 位置时,eBPF 步伐就会被触发执行。
  • eBPF 步伐在内核中执行,对用户空间步伐的相干数据举行过滤和处理。它可以根据预定义的规则或条件对数据举行筛选、聚合、统计等利用,并将结果通报回用户空间或存储在内核的 map 中。
  • 用户空间步伐可以通过与 eBPF 步伐共享的 map 或其他通讯机制获取 eBPF 步伐处理的结果,并举行进一步的分析和利用。
kprobe(kernel probe)是一种内核探测机制,答应在内核函数的入口或返回点插入一个钩子(hook),eBPF 可以与 kprobe 技能协同工作,一般步调如下:

  • 确定要跟踪或监控的内核函数,并在其入口或返回点插入 kprobe 钩子。
  • 编写 eBPF 步伐,定义在 kprobe 钩子触发时要执行的利用,如记录变乱、过滤数据、更新统计信息等。
  • 将 eBPF 步伐加载到内核中,并将其附加到 kprobe 钩子上。
  • 当内核函数被调用或返回时,kprobe 钩子会被触发,eBPF 步伐就会执行相应的利用。
  • 通过与 eBPF 步伐共享的数据结构(如 map)或其他通讯机制,用户空间步伐可以获取 eBPF 步伐处理的结果,并举行进一步的分析和利用。
2.2 eBPF字节码验证


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


  • 默认情况下,只有具有特权级的进程才气加载 eBPF 步伐,除非节点开启了 unprivileged 特性。
  • 内核提供了一个配置项 /proc/sys/kernel/unprivileged_bpf_disabled 来控制非特权用户是否可以或许使用 bpf(2) 体系调用。
  • 这个配置项是一次性开关(one-time kill switch),一旦将其设置为 1,就无法再改回 0,除非重启内核。
  • 当配置项被设置为 1 后,只有初始定名空间中具有 CAP_SYS_ADMIN 特权的进程才气调用 bpf(2) 体系调用。
  • Cilium 等工具在启动后会将该配置项设置为 1,以限制非特权用户的访问。
(2) 步伐的安全性查抄


  • 验证器会分析 eBPF 步伐的指令,确保步伐不会导致内核崩溃或体系出现故障。
  • 验证器会查抄 eBPF 步伐是否存在无限循环或递归调用,确保步伐可以或许在有限时间内完成执行(runs to completion)。
  • 验证器会查抄 eBPF 步伐的大小是否超过体系答应的限制,过大的步伐将被拒绝加载。
(3) 步伐复杂度分析


  • 验证器会评估 eBPF 步伐的所有可能执行路径,以确定步伐的复杂度。
  • 验证器需要在有限的时间内完成复杂度分析,如果超时,步伐将被拒绝加载。
  • 复杂度分析确保了 eBPF 步伐的行为是可预测和可控的,避免了步伐对体系资源的过分斲丧。
(4) 内存访问和资源限制


  • 验证器会查抄 eBPF 步伐对内存的访问是否合法,防止步伐访问未授权的内存区域。
  • eBPF 步伐只能通过特定的 helper 函数来访问内核数据结构和资源,验证器会确保步伐遵照这些规则。
  • 验证器会对 eBPF 步伐使用的资源举行限制,如栈大小、指令数量等,以防止步伐耗尽体系资源。
(5) 范例和参数查抄


  • 验证器会对 eBPF 步伐的范例和参数举行查抄,确保范例匹配和参数使用精确。
  • 验证器会查抄 eBPF 步伐中的 map 范例、大小和键值对是否符合规范。
  • 验证器会查抄 eBPF 步伐调用的 helper 函数是否合法,并且参数范例和数量是否精确。
通过这些全面的验证措施,内核确保了 eBPF 步伐的安全性和可靠性,防止了恶意或错误的步伐对体系造成危害。
2.3 eBPF即时编译JIT

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


  • JIT 编译器可以将 eBPF 指令直接映射为目标架构的当地指令,通常是 1:1 的映射关系。
  • 相比解释器逐条解释执行指令,JIT 编译后的当地指令可以直接在 CPU 上执行,减少了每条指令的执行开销。
(2) 减小可执行镜像大小


  • JIT 编译器生成的当地指令通常比 eBPF 字节码更加紧凑,因此生成的可执行镜像大小更小。
  • 较小的可执行镜像对 CPU 的指令缓存更加友好,可以进步缓存命中率,进一步提升执行性能。
(3) 针对 CISC 指令集的优化


  • 对于 CISC(复杂指令集)架构,如 x86,JIT 编译器会举行特殊优化。
  • JIT 编译器会为给定的 eBPF 指令生成尽可能短的利用码,以减少步伐翻译过程所需的空间。
  • 这种优化可以进一步减小生成的当地指令的大小,进步执行服从。
现在,多个主流架构都内置了 in-kernel eBPF JIT 编译器,包括:


  • 64 位架构:x86_64、arm64、ppc64、s390x、mips64、sparc64
  • 32 位架构:arm、x86_32
这些架构上的 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 的交互场景有以下几种:

  • 内核空间与用户空间通讯,用户空间步伐可以通过Maps与内核中的eBPF步伐举行数据交换。比方,用户空间步伐可以将配置参数存储在Map中,供内核中的eBPF步伐读取和使用。
  • 不同eBPF步伐之间的数据共享,多个eBPF步伐可以通过Maps共享数据,实现协作和通讯。比方,一个eBPF步伐可以将处理结果写入Map,另一个eBPF步伐可以从该Map中读取数据举行后续处理。
  • 数据统计和监控,Maps可以用于统计和监控内核中的各种指标和变乱。比方,可以使用Maps来统计网络数据包的数量、范例等信息,或者跟踪进程的资源使用情况。
  • 数据缓存和加快,Maps可以充当缓存,存储频繁访问的数据,进步步伐的性能。比方,可以将常用的路由表信息缓存在Map中,加快数据包的转发过程。

   eBPF键值数据存储和共享(来自ebpf.io)  常见的Map范例如下所示

  • Hash Map,哈希表结构,支持键值对的快速查找和更新。适用于需要频繁查找和更新的场景,如流量统计、路由表等。
  • Array Map,数组结构,通过索引访问元素。适用于固定大小的数据集合,如配置参数、统计计数器等。
  • LRU Map,基于Least Recently Used (LRU)算法的Map,自动淘汰迩来最少使用的元素。适用于需要缓存和淘汰机制的场景,如连接跟踪、流量控制等。
  • Per-CPU Array Map,为每个CPU核心提供独立的数组副本,避免并发访问的竞争。适用于需要每个CPU核心独立统计和处理的场景,如网络数据包的接收和发送。
  • Stack Trace Map,用于存储函数调用栈信息,方便举行性能分析和故障排查。配合其他工具如bpftrace,可以实现高效的内核级别调试。
  • Sockmap/Sockhash Map,用于实现高效的数据包转发和负载均衡。Sockmap以索引的方式访问socket,Sockhash以哈希的方式访问socket。
  • Ringbuf Map,环形缓冲区,支持在eBPF步伐和用户空间之间高效传输数据。适用于需要接二连三地将数据从内核传输到用户空间的场景,如变乱记录、日记采集等。
eBPF Maps提供了灵活而强大的数据交互和共享机制,使得eBPF步伐可以或许与内核和用户空间举行高效的通讯和协作。
2.5 eBPF辅助函数Helpers

   eBPF 步伐不直接调用内核函数。这样做会将 eBPF 步伐绑定到特定的内核版本,会使步伐的兼容性复杂化。而对应地,eBPF 步伐改为调用 helper 函数达到结果,这是内核提供的通用且稳定的 API。(来自ebpf.io)
  eBPF Helpers的功能和作用

  • 数据包处理,访问和利用数据包的元数据,如网络接口、协议头等。示例Helpers:bpf_skb_load_bytes()、bpf_skb_store_bytes()、bpf_clone_redirect()。
  • 追踪和变乱处理,跟踪内核函数的调用和返回,发送和接收自定义变乱。示例Helpers:bpf_probe_read()、bpf_perf_event_output()、bpf_get_current_comm()。
  • 网络协议剖析,剖析和访问各种网络协议头,如IP、TCP、UDP等。示例Helpers:bpf_skb_load_bytes()、bpf_l3_csum_replace()、bpf_l4_csum_replace()。
  • 安全性和访问控制,举行权限查抄和访问控制。示例Helpers:bpf_get_current_uid_gid()、bpf_get_cgroup_classid()。
  • 时间戳和计时器,获取当前时间,设置和管理计时器。示例Helpers:bpf_ktime_get_ns()、bpf_timer_init()、bpf_timer_set_callback()。
  • Maps利用,对Maps举行查找、更新、删除等利用。示例Helpers:bpf_map_lookup_elem()、bpf_map_update_elem()、bpf_map_delete_elem()
使用Helpers的注意事项

  • 内核版本依赖,不同的内核版本可能支持不同的Helpers集合,需要确保目标内核版本支持所使用的Helpers。
  • 安全性考虑,需要审慎使用Helpers,避免不妥利用导致内核崩溃或安全毛病。
  • 性能影响,过分使用Helpers可能对性能产生影响,需要权衡功能需求和性能影响,适度使用Helpers。
  • 上下文限制,某些Helpers只能在特定的上下文中调用,如网络数据包处理步伐。所以不同范例的 BPF 步伐可以或许使用的辅助函数可能是不同的
  • 参数合法性,通报给Helpers的参数需要合法且与预期一致。
所有的 BPF 辅助函数都是核心内核的一部分,无法通过内核模块来扩展或添加。可以在 bpf-helpers(7) - Linux manual page (man7.org) 看到当前 Linux 支持的 Helper functions。
2.6 eBPF尾调用和函数调用

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


  • 函数调用答应在 eBPF 步伐内部完成定义和调用函数。
  • 尾调用可以调用和执行另一个 eBPF 步伐并替换执行上下文,类似于 execve() 体系调用对常规进程的利用方式。

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


  • 在尾调用中,当前eBPF步伐的执行环境和堆栈状态会被销毁,直接跳转到目标eBPF步伐。
  • 避免单个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)固定到文件体系中,使其可以或许在不同的进程、步伐或体系重启之间共享和持久化。
通过将对象固定到文件体系,可以实现以下功能:


  • 持久化,固定的对象可以在体系重启后继续存在,并可以或许被其他进程访问。
  • 共享,固定的对象可以在不同的进程之间共享,实现进程间通讯和数据交换。
  • 全局可见性,固定的对象在整个体系范围内可见,可以被其他eBPF步伐和用户空间步伐访问。
对象固定通过bpf()体系调用和BPF_OBJ_PIN下令来实现。以下是固定Maps和Programs的根本步调:

  • 创建eBPF对象(Map或Program)。
  • 使用bpf()体系调用的BPF_OBJ_PIN下令将对象固定到指定的文件体系路径。
    1. bpf(BPF_OBJ_PIN, &attr, sizeof(attr));
    复制代码
    此中,attr是一个bpf_attr结构体,包罗了要固定的对象文件形貌符和目标文件体系路径。
  • 其他进程或步伐可以使用bpf()体系调用的BPF_OBJ_GET下令从指定路径获取固定的对象。
    1. bpf(BPF_OBJ_GET, &attr, sizeof(attr));
    复制代码
    此中,attr包罗了要获取的对象的文件体系路径。
2.9 eBPF安全增强功能

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


  • 步伐执行掩护(Protection Execution Protection), 内核中保存 eBPF 步伐的内存受到掩护并变为只读。如果出于任何缘故起因,无论是内核错误还是恶意利用,试图修改 eBPF 步伐,内核将会崩溃,而不是答应它继续执行粉碎/被利用的步伐。
  • 缓解Spectre毛病(Mitigation Against Spectre): 根据推断,CPU 可能会错误地预测分支并留下可观察到的副作用,这些副作用可以通过旁路(side channel)提取。
    比方,eBPF 步伐可以屏蔽内存访问,以便在临时指令下将访问重定向到受控区域,验证器也遵照仅在推测执行(speculative execution)下可访问的步伐路径,JIT 编译器在尾调用不能转换为直接调用的情况下发出 Retpoline。
  • 常量盲化(Constant blinding):代码中的所有常量都是盲化的,以防止JIT spraying 攻击。这可以防止攻击者将可执行代码作为常量注入,在存在另一个内核错误的情况下,这可能答应攻击者跳转到 eBPF 步伐的内存部分来执行代码。
   将 /proc/sys/net/core/bpf_jit_harden 设置为 1 会为非特权用户的 JIT 编译做一些额外的加固工作。这些额外加固会稍微降低步伐的性能,但在有非受信用户在体系上举行利用的情况下,可以或许有用地减小潜伏的受攻击面。但与完全切换到解释器相比,这些性能损失还是比力小的。(来自eBPF 完全入门指南.pdf(万字长文) - 知乎 (zhihu.com))
  盲化 JIT 常量通过对真实指令举行随机化(randomizing the actual instruction)实现 。在这种方式中,通过对指令举行重写,将原来基于立刻数的利用转换成基于寄存器的利用,指令重写将加载值的过程分解为两部分:

  • 加载一个盲化后的(blinded)立刻数 rnd ^ imm 到寄存器
  • 将寄存器和 rnd 举行异或利用(xor)
3. eBPF工具链介绍

3.1 BCC(BPF Compiler Collection)

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

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


  • 简化了 BPF 步伐的编写和使用,提供了高级语言绑定和工具集。
  • 可以实时、动态地对内核和应用步伐举行监控和分析,无需重启体系或修改源代码。
  • 相比传统的内核工具和调试方法,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使用类似于awk和C语言的语法,易于学习和使用。
  • 内核级别的跟踪,通过eBPF技能,bpftrace可以跟踪内核函数、体系调用、跟踪点(tracepoints)等,提供低开销、高精度的性能分析。
  • 用户级别的跟踪,bpftrace也支持跟踪用户空间的函数和库调用,实现全面的体系性能分析。
  • 动态插装,bpftrace可以在运行时动态地插入跟踪代码,无需重启体系或应用步伐。
  • 灵活的输出格式,bpftrace支持多种输出格式,如表格、直方图、火焰图等,方便用户分析和可视化数据。
bpftrace常用场景如下:


  • 识别体系瓶颈,跟踪关键体系调用、内核函数的执行时间和频率,发现性能瓶颈。
  • 分析应用步伐行为,跟踪应用步伐的函数调用、库函数使用情况,优化应用性能。
  • 诊断体系标题,通过跟踪非常变乱、错误信息,快速定位体系标题的根因。
  • 安全监控,实时监控体系调用、网络运动等,发现可疑行为和潜伏威胁。
3.3 libbpf库

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

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


  • 简化eBPF步伐开发,libbpf提供了一组高条理的API,封装了与eBPF步伐加载、验证、附加到内核探针等相干的底层细节,使得开发职员可以更专注于eBPF步伐的逻辑。
  • 与内核交互,libbpf处理了与内核的通讯,包括eBPF步伐的加载、卸载、参数通报和数据读取等,简化了用户空间和内核空间的交互。
  • 封装eBPF Maps,eBPF Maps是内核空间和用户空间共享数据的重要机制,libbpf提供了一组API来创建、更新、删除和查询eBPF Maps,方便数据的存储和交换。
  • CO-RE(Compile Once – Run Everywhere)支持,libbpf支持CO-RE特性,答应eBPF步伐在编译时与内核相干的数据结构解耦,使得编译后的eBPF步伐可以在不同版本的内核上运行,进步了可移植性。
  • 与bpftrace和BCC等工具集成,libbpf是bpftrace和BCC(BPF Compiler Collection)等高条理eBPF工具的根本库,这些工具在libbpf的根本上提供了更加易用和专业的eBPF开发环境。
使用libbpf的一般步调如下

  • 编写eBPF步伐,使用C语言编写eBPF步伐,定义数据结构、Maps和步伐逻辑。
  • 加载eBPF步伐,使用libbpf提供的API将eBPF步伐加载到内核中,并举行验证和优化。
  • 附加到内核探针,将加载的eBPF步伐附加到内核的探针(如kprobes、tracepoints等)上,以便在特定的变乱发生时执行。
  • 与eBPF Maps交互,通过libbpf提供的API与eBPF Maps举行数据交换,如存储统计信息、配置参数等。
  • 读取和分析数据,从eBPF Maps中读取数据,并在用户空间举行分析和处理,生成性能报告、日记等。
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) 头文件包罗:


  • <linux/bpf.h>: 这个头文件包罗了 BPF 步伐所需的根本定义和结构体。
  • <bpf/bpf_helpers.h>: 这个头文件包罗了 BPF 步伐可以使用的辅助函数,如 bpf_trace_printk。
(2) SEC 宏定义:


  • 这个宏用于指定 BPF 步伐的代码段(section)和属性。
  • 在这个例子中,SEC("tracepoint/syscalls/sys_enter_execve") 表示这个 BPF 步伐将附加到 execve 体系调用的入口点。
(3) bpf_prog 函数:


  • 这是 BPF 步伐的主函数,当 execve 体系调用被调用时,这个函数将被执行。
  • 函数的参数 void *ctx 表示 BPF 上下文,可以用于获取体系调用的参数和其他信息。
  • 在这个例子中,函数体只是简单地打印一条消息。
(4) 打印消息:


  • 使用 bpf_trace_printk 函数打印一条消息到跟踪输出。
  • 消息内容为Hello BPF from onceday!\n,包括换行符。
  • sizeof(msg) 用于指定消息的长度,包括空字符。
(5) 返回值:


  • BPF 步伐必须返回一个整数值。
  • 在这个例子中,返回值为 0,表示答应体系调用继续执行。
(6) 许可声明:


  • char _license[] SEC("license") = "GPL"; 用于声明 BPF 步伐的许可证。
  • 这个声明是必须的,否则 BPF 步伐将无法加载到内核中。
  • 在这个例子中,许可证为 “GPL”(GNU General Public License)。
要使用这个 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 步伐交互。下面是对这段代码的扼要介绍:


  • <bpf/libbpf.h>: libbpf 库的头文件,提供了与 BPF 步伐交互的函数和结构体。
  • 全局变量,local_obj: 表示 BPF 对象文件,local_link: 表示 BPF 步伐与内核的链接。
  • 信号处理函数 signal_handler,当接收到 SIGINT 信号(如按下 Ctrl+C)时,会执行此函数。函数内部会卸载 BPF 步伐,关闭 BPF 对象文件,并退出步伐。
  • 设置信号处理函数,打开 BPF 对象文件 “hello_kern.o”,在 BPF 对象文件中查找名为 “bpf_prog” 的 BPF 步伐。
  • 加载 BPF 步伐到内核中,将 BPF 步伐附加到内核的跟踪点上。
  • 调用 read_trace_pipe 函数(代码中未提供实现)读取跟踪输出。
  • 整理资源,卸载 BPF 步伐,关闭 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

  

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

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

王柳

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表