ebpf简介
简介
概述: 允许用户在内核空间动态加载并执行自定义程序,从而实现对系统行为的深度监控与控制。其核心在于高性能、低侵入性和安全性。
发展:
- cbpf:最初用于网络数据包过滤,通过在虚拟机在内核执行过滤逻辑,性能远超过用户态方案
- ebpf:引入linux内核3.15,突破原有限制,支持更多事件类型(如系统调用、跟踪点)、更大指令集和复杂的数据结构
核心特性:
- 动态性:无需重启系统或修改内核代码,可实时附加/移除探测点
- 灵活性:支持几乎所有内核函数(部分保留函数如
kprobe
本身不可探测) - 低开销:ebpf程序在内核JIT编译为本地指令,执行效率高;回调函数运行时关闭抢占,避免上下文切换
核心运行机制:
- 状态机模型:ebpf程序以内核态虚拟机运行,通过事件触发(如网络包到达、系统调用执行)激活,执行后返回结果或更新状态
- 钩子机制:程序可挂载到内核关键位置(如
kprobe
、tracepoint
、XDP
),实现无侵入式监控,例如,tracepoint:syscalls:sys_enter_execve
可捕获所有execve
系统调用 - 验证器:加载前检查程序安全性,防止无限循环、非法内存访问等,确保内核稳定性
开发步骤:
- 编写程序:使用c编写逻辑,通过
BPF_HASH
等宏定义映射,挂钩事件处理函数。例如,跟踪TCP连接与断开 - 编译字节码:借助llvm/clang将c代码编译成ebpf字节码
- 加载与验证:通过bpf()系统调用加载到内核,验证器确保代码安全
应用场景:
- 性能监控与调优:
- 函数调用跟踪:通过
kprobe
捕获内核函数执行,分析cpu热点 - IO延迟分析:追踪块设备或网络IO延迟,定位性能瓶颈
- 函数调用跟踪:通过
- 网络优化与安全:
- DDos防护:在XDP层实时过滤恶意流量,降低内核处理开销
- 容器网络隔离:Cilium利用ebpf实现容器间网络策略,替代iptables
- 安全审计与防护:
- 系统调用监控:记录敏感操作(如文件访问),检测异常行为
- 内存保护:拦截非法内存访问,防止漏洞利用
概念
Hook
预定义钩子: ebpf程序是事件驱动的,当内核或应用程序通过某个钩子点时运行。预定义钩子包括系统调用、函数进入/退出、内核跟踪点、网络事件等
[kprobe](# kprobe)/uprobe: 如果不存在特定需求的预定义钩子,则可以创建内核探测器(kprobe)或用户探测器(uprobe),以将ebpf程序附加到内核或用户应用程序中的几乎任何位置
如何编写
-
一般不直接编写,而是通过cilium、bcc、bpftrace、libbpf等项目间接使用。
-
如果要直接编写,一般通过llvm将c代码编译成ebpf字节码
加载器&验证架构
概述: 当确定了所需的钩子时,可以使用bpf系统调用将ebpf程序加载到linux内核中。这通常是使用可用的ebpf库完成的
大致流程图:
当程序被加载到linux kernel中,会经过两个verifier验证和JIT编译步骤,然后被附加到请求的钩子上
验证
概述: 确保ebpf程序可以安全运行,验证以下条件:(这里是简单描述, 实际上挺复杂的)
- 加载ebpf程序的进程拥有所需的权限,除非启用了非特权的ebpf,否则只有特权进程才能加载ebpf程序
- 该程序不会崩溃或损害系统
- 程序始终运行完成,不会死循环
JIT编译
概述: Just-in-Time编译步骤将程序的通用字节码转换为特定于机器的指令集,以优化程序的执行速度。这使得ebpf程序的运行效率与本地编译的内核代码或作为内核模块加载的代码一样高效
Maps
概述: ebpf程序的一个重要能力是共享收集的信息和存储状态,ebpf程序可以利用ebpf maps的概念在各种数据结构中存储和检索数据。ebpf maps可以从ebpf程序访问,也可以通过系统调用从用户空间中的应用程序访问。
支持的map类型:
- 哈希表、数组
- LRU
- Ring Buffer
- 堆栈跟踪
- LPM(最长前缀匹配)
Helper Calls
概述: ebpf程序无法调用任意内核函数,允许这样做只会将ebpf程序绑定到特定的内核版本,并使程序的兼容性复杂化。相反,ebpf程序可以将函数调用转换为辅助函数,这是内核提供的众所周知且稳定的API。
可用的Helper Calls示例:
-
bpf_get_prandom_u32():生成一个32位无符号伪随机数
-
bpf_ktime_get_ns():返回系统启动以来的纳秒级单调时间(不受系统时间调整影响),常用于性能分析(如系统调用耗时)、事件时间戳记录
-
ebpf map access:
- bpf_map_lookup_elem():通过键查找Map中的值
- bpf_map_update_elem():更新或插入Map中的键值对
- bpf_map_delete_elem():删除Map中的键值对
- bpf_for_each_map_elem():遍历Map中的所有元素
-
get process/cgroup context:
- bpf_get_current_pid_tgid():获取当前进程ID和线程ID
- bpf_get_current_uid_gid():获取当前用户ID和组ID
- bpf_get_current_cgroup_id():获取当前cgroup ID
- bpf_get_current_ancestor_cgroup_id():获取祖先cgroup ID
-
manipute network packets and forwarding logic:
- bpf_skb_load_bytes()/bpf_skb_store_bytes():读取/修改数据包内容
- bpf_l3_csum_replace()/bpf_l4_csum_replace():更新校验和
- bpf_clone_redirect():克隆并重定向数据包
- bpf_redirect():直接重定向数据包
- bpf_skb_adjust_room():调整数据包空间
Tail&Function Call
概述: ebpf程序可以通过尾部调用和函数调用的概念进行组合,
- 函数调用:允许ebpf程序中定义和调用函数
- 尾调用:可以调用并执行另一个ebpf程序,并替换执行上下文,类似于execve()系统调用对常规进程的操作方式。
Tail Call
概述: 内核栈是很宝贵的,尾调用最大的优势就是其复用了当前栈帧并跳转至另外一个ebpf程序
ebpf程序都是独立验证的(调用者的对战和寄存器中的值被调用者不可访问),所以状态的传递一般可以使用per-cpu map传递
堆栈槽(slot)
概述: 指的是ebpf程序用于存储局部变量和临时数据的内存区域。BPF 程序拥有一个大小为 512 字节的固定堆栈空间,由特殊寄存器 R10 指向堆栈的顶部(高地址)。程序通过对 R10 进行负偏移来访问堆栈,例如 r10 - 8
表示堆栈顶部向下偏移 8 字节的位置。
开发工具链
bcc
概述: 使用户能够编写嵌入了ebpf程序的python程序,该框架主要针对涉及应用程序和系统分析跟踪的用例,其中ebpf程序用于收集统计数据或生成事件,而用户空间对应的程序收集数据并以人类可读的形式显示。运行该python程序会生成ebpf字节码并加载到内核中
bpftrace
概述: 是一种适用于linuc ebpf的高级跟踪语言,可用于半更新的linux内核(4.x)。bpftrace使用llvm作为后端将脚本编译为ebpf字节码,并利用bcc与linux ebpf子系统以及现有的linux跟踪功能进行交互:内核动态跟踪(kprobes)、用户级动态跟踪(uprobes)和跟踪点。
ebpf go library
概述: cilium社区开发了一个ebpf go library,作为通用的ebpf库,它将获取ebpf字节码的过程与ebpf程序的加载和管理解藕。ebpf程序通常是通过编写更高级的语言(比如go)来创建的,然后使用clang/llvm编译器编译为ebpf字节码
libbpf c/c++ library
概述: 是一个基于c/c++的通用ebpf库,它有助于clang/llvm编译器生成的ebpf目标文件加载到内核中,并且通过为应用程序提供易于使用的库API来抽象与bpf系统调用的交互