相关资料
EBPF相关要点介绍
在内核中的 BPF 代码
文件 linux/include/linux/bpf.h及其相对的 linux/include/uapi/bpf.h包含有关 eBPF 的 定义,它们分别用在内核中和用户空间程序的接口。
linux/include/uapi/bpf.h bpf虚拟机参数
寄存器:
/* Register numbers */
enum {
BPF_REG_0 = 0,
BPF_REG_1,
BPF_REG_2,
BPF_REG_3,
BPF_REG_4,
BPF_REG_5,
BPF_REG_6,
BPF_REG_7,
BPF_REG_8,
BPF_REG_9,
BPF_REG_10,
__MAX_BPF_REG,
};
- 指令格式:
struct bpf_insn {
__u8 code; /* opcode */
__u8 dst_reg:4; /* dest register */
__u8 src_reg:4; /* source register */
__s16 off; /* signed offset */
__s32 imm; /* signed immediate constant */
};
- 指令宏:
/* instruction classes */
#define BPF_JMP32 0x06 /* jmp mode in word width */
#define BPF_ALU64 0x07 /* alu mode in double word width */
/* ld/ldx fields */
#define BPF_DW 0x18 /* double word (64-bit) */
#define BPF_ATOMIC 0xc0 /* atomic memory ops - op type in immediate */
#define BPF_XADD 0xc0 /* exclusive add - legacy name */
/* alu/jmp fields */
#define BPF_MOV 0xb0 /* mov reg to reg */
#define BPF_ARSH 0xc0 /* sign extending arithmetic shift right */
/* change endianness of a register */
#define BPF_END 0xd0 /* flags for endianness conversion: */
#define BPF_TO_LE 0x00 /* convert to little-endian */
#define BPF_TO_BE 0x08 /* convert to big-endian */
#define BPF_FROM_LE BPF_TO_LE
#define BPF_FROM_BE BPF_TO_BE
/* jmp encodings */
#define BPF_JNE 0x50 /* jump != */
#define BPF_JLT 0xa0 /* LT is unsigned, '<' */
#define BPF_JLE 0xb0 /* LE is unsigned, '<=' */
#define BPF_JSGT 0x60 /* SGT is signed '>', GT in x86 */
#define BPF_JSGE 0x70 /* SGE is signed '>=', GE in x86 */
#define BPF_JSLT 0xc0 /* SLT is signed, '<' */
#define BPF_JSLE 0xd0 /* SLE is signed, '<=' */
#define BPF_CALL 0x80 /* function call */
#define BPF_EXIT 0x90 /* function return */
/* atomic op type fields (stored in immediate) */
#define BPF_FETCH 0x01 /* not an opcode on its own, used to build others */
#define BPF_XCHG (0xe0 | BPF_FETCH) /* atomic exchange */
#define BPF_CMPXCHG (0xf0 | BPF_FETCH) /* atomic compare-and-write */
- 指令码:
64-bit
Opcode 操作码 | Mnemonic 助记符 | Pseudocode 伪代码 |
---|---|---|
0x07 | add dst, imm | dst += imm |
0x0f | add dst, src | dst += src |
0x17 | sub dst, imm | dst -= imm |
0x1f | sub dst, src | dst -= src |
0x27 | mul dst, imm | dst *= imm |
0x2f | mul dst, src | dst *= src |
0x37 | div dst, imm | dst /= imm |
0x3f | div dst, src | dst /= src |
0x47 | or dst, imm | dst |= imm |
0x4f | or dst, src | dst |= src |
0x57 | and dst, imm | dst &= imm |
0x5f | and dst, src | dst &= src |
0x67 | lsh dst, imm | dst <<= imm |
0x6f | lsh dst, src | dst <<= src |
0x77 | rsh dst, imm | dst >>= imm (logical) |
0x7f | rsh dst, src | dst >>= src (logical) |
0x87 | neg dst | dst = -dst |
0x97 | mod dst, imm | dst %= imm |
0x9f | mod dst, src | dst %= src |
0xa7 | xor dst, imm | dst ^= imm |
0xaf | xor dst, src | dst ^= src |
0xb7 | mov dst, imm | dst = imm |
0xbf | mov dst, src | dst = src |
0xc7 | arsh dst, imm | dst >>= imm (arithmetic) |
0xcf | arsh dst, src | dst >>= src (arithmetic) |
32-bit
这些指令只对low 32位有效,会把high 32位清空
Opcode | Mnemonic | Pseudocode |
---|---|---|
0x04 | add32 dst, imm | dst += imm |
0x0c | add32 dst, src | dst += src |
0x14 | sub32 dst, imm | dst -= imm |
0x1c | sub32 dst, src | dst -= src |
0x24 | mul32 dst, imm | dst *= imm |
0x2c | mul32 dst, src | dst *= src |
0x34 | div32 dst, imm | dst /= imm |
0x3c | div32 dst, src | dst /= src |
0x44 | or32 dst, imm | dst |= imm |
0x4c | or32 dst, src | dst |= src |
0x54 | and32 dst, imm | dst &= imm |
0x5c | and32 dst, src | dst &= src |
0x64 | lsh32 dst, imm | dst <<= imm |
0x6c | lsh32 dst, src | dst <<= src |
0x74 | rsh32 dst, imm | dst >>= imm (logical) |
0x7c | rsh32 dst, src | dst >>= src (logical) |
0x84 | neg32 dst | dst = -dst |
0x94 | mod32 dst, imm | dst %= imm |
0x9c | mod32 dst, src | dst %= src |
0xa4 | xor32 dst, imm | dst ^= imm |
0xac | xor32 dst, src | dst ^= src |
0xb4 | mov32 dst, imm | dst = imm |
0xbc | mov32 dst, src | dst = src |
0xc4 | arsh32 dst, imm | dst >>= imm (arithmetic) |
0xcc | arsh32 dst, src | dst >>= src (arithmetic) |
Byteswap instructions
Opcode | Mnemonic | Pseudocode |
---|---|---|
0xd4 (imm == 16) | le16 dst | dst = htole16(dst) |
0xd4 (imm == 32) | le32 dst | dst = htole32(dst) |
0xd4 (imm == 64) | le64 dst | dst = htole64(dst) |
0xdc (imm == 16) | be16 dst | dst = htobe16(dst) |
0xdc (imm == 32) | be32 dst | dst = htobe32(dst) |
0xdc (imm == 64) | be64 dst | dst = htobe64(dst) |
Memory Instructions
Opcode | Mnemonic | Pseudocode |
---|---|---|
0x18 | lddw dst, imm | dst = imm |
0x20 | ldabsw src, dst, imm | See kernel documentation |
0x28 | ldabsh src, dst, imm | … |
0x30 | ldabsb src, dst, imm | … |
0x38 | ldabsdw src, dst, imm | … |
0x40 | ldindw src, dst, imm | … |
0x48 | ldindh src, dst, imm | … |
0x50 | ldindb src, dst, imm | … |
0x58 | ldinddw src, dst, imm | … |
0x61 | ldxw dst, [src+off] | dst = *(uint32_t *) (src + off) |
0x69 | ldxh dst, [src+off] | dst = *(uint16_t *) (src + off) |
0x71 | ldxb dst, [src+off] | dst = *(uint8_t *) (src + off) |
0x79 | ldxdw dst, [src+off] | dst = *(uint64_t *) (src + off) |
0x62 | stw [dst+off], imm | *(uint32_t *) (dst + off) = imm |
0x6a | sth [dst+off], imm | *(uint16_t *) (dst + off) = imm |
0x72 | stb [dst+off], imm | *(uint8_t *) (dst + off) = imm |
0x7a | stdw [dst+off], imm | *(uint64_t *) (dst + off) = imm |
0x63 | stxw [dst+off], src | *(uint32_t *) (dst + off) = src |
0x6b | stxh [dst+off], src | *(uint16_t *) (dst + off) = src |
0x73 | stxb [dst+off], src | *(uint8_t *) (dst + off) = src |
0x7b | stxdw [dst+off], src | *(uint64_t *) (dst + off) = src |
Branch Instructions
Opcode | Mnemonic | Pseudocode |
---|---|---|
0x05 | ja +off | PC += off |
0x15 | jeq dst, imm, +off | PC += off if dst == imm |
0x1d | jeq dst, src, +off | PC += off if dst == src |
0x25 | jgt dst, imm, +off | PC += off if dst > imm |
0x2d | jgt dst, src, +off | PC += off if dst > src |
0x35 | jge dst, imm, +off | PC += off if dst >= imm |
0x3d | jge dst, src, +off | PC += off if dst >= src |
0xa5 | jlt dst, imm, +off | PC += off if dst < imm |
0xad | jlt dst, src, +off | PC += off if dst < src |
0xb5 | jle dst, imm, +off | PC += off if dst <= imm |
0xbd | jle dst, src, +off | PC += off if dst <= src |
0x45 | jset dst, imm, +off | PC += off if dst & imm |
0x4d | jset dst, src, +off | PC += off if dst & src |
0x55 | jne dst, imm, +off | PC += off if dst != imm |
0x5d | jne dst, src, +off | PC += off if dst != src |
0x65 | jsgt dst, imm, +off | PC += off if dst > imm (signed) |
0x6d | jsgt dst, src, +off | PC += off if dst > src (signed) |
0x75 | jsge dst, imm, +off | PC += off if dst >= imm (signed) |
0x7d | jsge dst, src, +off | PC += off if dst >= src (signed) |
0xc5 | jslt dst, imm, +off | PC += off if dst < imm (signed) |
0xcd | jslt dst, src, +off | PC += off if dst < src (signed) |
0xd5 | jsle dst, imm, +off | PC += off if dst <= imm (signed) |
0xdd | jsle dst, src, +off | PC += off if dst <= src (signed) |
0x85 | call imm | Function call |
0x95 | exit | return r0 |
相同的方式,文件 linux/include/linux/filter.h和 linux/include/uapi/filter.h包含了用于 运行 BPF 程序 的信息。
linux/include/linux/filter.h 内核run bpf程序
Bpf_prog_run宏:
#define __BPF_PROG_RUN(prog, ctx, dfunc) ({ \
u32 __ret; \
cant_migrate(); \
if (static_branch_unlikely(&bpf_stats_enabled_key)) { \
struct bpf_prog_stats *__stats; \
u64 __start = sched_clock(); \
__ret = dfunc(ctx, (prog)->insnsi, (prog)->bpf_func); \
__stats = this_cpu_ptr(prog->stats); \
u64_stats_update_begin(&__stats->syncp); \
__stats->cnt++; \
__stats->nsecs += sched_clock() - __start; \
u64_stats_update_end(&__stats->syncp); \
} else { \
__ret = dfunc(ctx, (prog)->insnsi, (prog)->bpf_func); \
} \
__ret; })
#define BPF_PROG_RUN(prog, ctx) \
__BPF_PROG_RUN(prog, ctx, bpf_dispatcher_nop_func)
- BPF 相关的 主要的代码片断 在 linux/kernel/bpf/目录下面。系统调用的不同操作许可,比如,程序加载或者映射管理是在文件
syscall.c
中实现,而core.c
包含了 解析器。其它文件的命名显而易见:verifier.c
包含 校验器,arraymap.c
的代码用于与数组类型的 映射 交互,等等。
syscall.c
:
static int bpf_prog_load(union bpf_attr *attr, union bpf_attr __user *uattr)
SYSCALL_DEFINE3(bpf, int, cmd, union bpf_attr __user *, uattr, unsigned int, size)
有几个与网络(及 tc、XDP )相关的函数和 helpers 是用户可用,其实现在 linux/net/core/filter.c中。它也包含了移植 cBPF 字节码到 eBPF 的代码(因为在运行之前,内核中的所有的 cBPF 程序被转换成 eBPF)。
相关于 事件跟踪 的函数和 helpers 都在 linux/kernel/trace/bpf_trace.c 中。
/**
* trace_call_bpf - invoke BPF program
* @call: tracepoint event
* @ctx: opaque context pointer
*
* kprobe handlers execute BPF programs via this helper.
* Can be used from static tracepoints in the future.
*
* Return: BPF programs always return an integer which is interpreted by
* kprobe handler as:
* 0 - return from kprobe (event is filtered out)
* 1 - store kprobe event into ring buffer
* Other values are reserved and currently alias to 1
*/
unsigned int trace_call_bpf(struct trace_event_call *call, void *ctx)
{
unsigned int ret;
cant_sleep();
if (unlikely(__this_cpu_inc_return(bpf_prog_active) != 1)) {
/*
* since some bpf program is already running on this cpu,
* don't call into another bpf program (same or different)
* and don't send kprobe event into ring-buffer,
* so return zero here
*/
ret = 0;
goto out;
}
/*
* Instead of moving rcu_read_lock/rcu_dereference/rcu_read_unlock
* to all call sites, we did a bpf_prog_array_valid() there to check
* whether call->prog_array is empty or not, which is
* a heuristic to speed up execution.
*
* If bpf_prog_array_valid() fetched prog_array was
* non-NULL, we go into trace_call_bpf() and do the actual
* proper rcu_dereference() under RCU lock.
* If it turns out that prog_array is NULL then, we bail out.
* For the opposite, if the bpf_prog_array_valid() fetched pointer
* was NULL, you'll skip the prog_array with the risk of missing
* out of events when it was updated in between this and the
* rcu_dereference() which is accepted risk.
*/
ret = BPF_PROG_RUN_ARRAY_CHECK(call->prog_array, ctx, BPF_PROG_RUN);
out:
__this_cpu_dec(bpf_prog_active);
return ret;
}
bpf_tracing_func_proto(enum bpf_func_id func_id, const struct bpf_prog *prog)
JIT 编译器 在它们各自的架构目录下面,比如,x86 架构的在 linux/arch/x86/net/bpfjitcomp.c中。例外是用于硬件卸载的 JIT 编译器,它们放在它们的驱动程序下,例如 Netronome NFP 网卡的就放在 linux/drivers/net/ethernet/netronome/nfp/bpf/jit.c 。
在 linux/net/sched/目录下,你可以找到 tc 的 BPF 组件 相关的代码,尤其是在文件
act_bpf.c
(action)和cls_bpf.c
(filter)中。我并没有在 BPF 上深入到 事件跟踪 中,因此,我并不真正了解这些程序的钩子。在 linux/kernel/trace/bpf_trace.c那里有一些东西。如果你对它感兴趣,并且想去了解更多,你可以在 Brendan Gregg 的演示或者博客文章上去深入挖掘。
我也没有使用过 seccomp-BPF,不过你能在 linux/kernel/seccomp.c找到它的代码,并且可以在 linux/tools/testing/selftests/seccomp/seccomp_bpf.c中找到一些它的使用示例。
XDP 钩子代码
一旦装载进内核的 BPF 虚拟机,由一个 Netlink 命令将 XDP 程序从用户空间钩入到内核网络路径中。接收它的是在 linux/net/core/dev.c 文件中的 dev_change_xdp_fd()
函数,它被调用并设置一个 XDP 钩子。钩子被放在支持的网卡的驱动程序中。例如,用于 Netronome 硬件钩子的 ntp 驱动程序实现放在 drivers/net/ethernet/netronome/nfp/ 中。文件 nfp_net_common.c
接受 Netlink 命令,并调用 nfp_net_xdp_setup()
,它会转而调用 nfp_net_xdp_setup_drv()
实例来安装该程序。
在 bcc 中的 BPF 逻辑
在 bcc 的 GitHub 仓库能找到的 bcc 工具集的代码。其 Python 代码,包含在 BPF
类中,最初它在文件 bcc/src/python/bcc/init.py 中。但是许多我觉得有意思的东西,比如,加载 BPF 程序到内核中,出现在 libbcc 的 C 库中。
使用 tc 去管理 BPF 的代码
当然,这些代码与 iproute2 包中的 tc 中的 BPF 相关。其中的一些在 iproute2/tc/ 目录中。文件 f_bpf.c
和 m_bpf.c
(和 e_bpf.c
)各自用于处理 BPF 的过滤器和动作的(和 tc exec
命令,等等)。文件 q_clsact.c
定义了为 BPF 特别创建的 clsact
qdisc。但是,大多数的 BPF 用户空间逻辑 是在 iproute2/lib/bpf.c库中实现的,因此,如果你想去使用 BPF 和 tc,这里可能是会将你搞混乱的地方(它是从文件 iproute2/tc/tc_bpf.c 中移动而来的,你也可以在旧版本的包中找到相同的代码)。
BPF 实用工具
内核中也带有 BPF 相关的三个工具的源代码(bpf_asm.c
、 bpf_dbg.c
、 bpf_jit_disasm.c
),根据你的版本不同,在 linux/tools/net/(直到 Linux 4.14)或者 linux/tools/bpf/目录下面:
bpf_asm
是一个极小的 cBPF 汇编程序。bpf_dbg
是一个很小的 cBPF 程序调试器。bpf_jit_disasm
对于两种 BPF 都是通用的,并且对于 JIT 调试来说非常有用。bpftool
是由 Jakub Kicinski 写的通用工具,它可以与 eBPF 程序交互并从用户空间的映射,例如,去展示、转储、pin 程序、或者去展示、创建、pin、更新、删除映射。
阅读在源文件顶部的注释可以得到一个它们使用方法的概述。
与 eBPF 一起工作的其它必需的文件是来自内核树的两个用户空间库,它们可以用于管理 eBPF 程序或者映射来自外部的程序。这个函数可以通过 linux/tools/lib/bpf/ 目录中的头文件 bpf.h
和 libbpf.h
(更高层面封装)来访问。比如,工具 bpftool
主要依赖这些库。
eBPF程序类型
加载BPF_PROG_LOAD的程序的类型决定了四件事:可以在何处附加程序,可以调用验证程序的内核内辅助函数,是否可以直接访问网络数据包数据,以及作为第一个传递的对象的类型该程序的参数。实际上,程序类型本质上定义了一个API。甚至纯粹是创建新程序类型来区分允许的可调用函数的不同列表(例如,BPF_PROG_TYPE_CGROUP_SKB与 BPF_PROG_TYPE_SOCKET_FILTER)。
内核支持的当前eBPF程序类型集为:
BPF_PROG_TYPE_SOCKET_FILTER:网络数据包过滤器
BPF_PROG_TYPE_KPROBE:确定是否应触发kprobe
BPF_PROG_TYPE_SCHED_CLS:网络流量控制分类器
BPF_PROG_TYPE_SCHED_ACT:网络流量控制操作
BPF_PROG_TYPE_TRACEPOINT:确定是否应触发跟踪点
BPF_PROG_TYPE_XDP:从设备驱动程序接收路径运行的网络数据包筛选器
BPF_PROG_TYPE_PERF_EVENT:确定是否应该触发性能事件处理程序
BPF_PROG_TYPE_CGROUP_SKB:用于控制组的网络数据包过滤器
BPF_PROG_TYPE_CGROUP_SOCK:用于控制组的网络数据包筛选器,允许修改套接字选项
BPF_PROG_TYPE_LWT_ *:用于轻型隧道的网络数据包过滤器
BPF_PROG_TYPE_SOCK_OPS:用于设置套接字参数的程序
BPF_PROG_TYPE_SK_SKB:网络数据包过滤器,用于在套接字之间转发数据包
BPF_PROG_CGROUP_DEVICE:确定是否应该允许设备操作
随着添加了新的程序类型,内核开发人员也发现也需要添加新的数据结构。
eBPF数据结构
eBPF程序使用的主要数据结构是eBPF映射,eBPF映射是一种通用数据结构,它允许在内核内或内核与用户空间之间来回传递数据。顾名思义,“map”使用键存储和检索数据。
使用bpf()系统调用创建和处理map。成功创建映射后,将返回与该映射关联的文件描述符。通常,通过关闭关联的文件描述符来销毁map。每个映射由四个值定义:类型,最大元素数,值大小(以字节为单位)和键大小(以字节为单位)。有不同的map类型,每种map类型提供不同的行为和权衡方案:
BPF_MAP_TYPE_HASH:哈希表
BPF_MAP_TYPE_ARRAY:数组映射,已针对快速查找速度进行了优化,通常用于计数器
BPF_MAP_TYPE_PROG_ARRAY:对应于eBPF程序的文件描述符数组;用于实现跳转表和子程序以处理特定的数据包协议
BPF_MAP_TYPE_PERCPU_ARRAY:每个CPU的阵列,用于实现延迟的直方图
BPF_MAP_TYPE_PERF_EVENT_ARRAY:存储指向struct perf_event的指针,用于读取和存储perf事件计数器
BPF_MAP_TYPE_CGROUP_ARRAY:存储指向控制组的指针
BPF_MAP_TYPE_PERCPU_HASH:每个CPU的哈希表
BPF_MAP_TYPE_LRU_HASH:仅保留最近使用项目的哈希表
BPF_MAP_TYPE_LRU_PERCPU_HASH:每个CPU的哈希表,仅保留最近使用的项目
BPF_MAP_TYPE_LPM_TRIE:最长前缀匹配树,适用于将IP地址匹配到某个范围
BPF_MAP_TYPE_STACK_TRACE:存储堆栈跟踪
BPF_MAP_TYPE_ARRAY_OF_MAPS:map中map数据结构
BPF_MAP_TYPE_HASH_OF_MAPS:map中map数据结构
BPF_MAP_TYPE_DEVICE_MAP:用于存储和查找网络设备引用
BPF_MAP_TYPE_SOCKET_MAP:存储和查找套接字,并允许使用BPF帮助函数进行套接字重定向
可以使用bpf_map_lookup_elem()和 bpf_map_update_elem()函数从eBPF或用户空间程序访问所有map 。某些映射类型(例如套接字映射)可以与执行特殊任务的其他eBPF帮助器功能一起使用。
如何编写eBPF程序
从历史上看,有必要手动编写eBPF汇编并使用内核的bpf_asm汇编器生成BPF字节码。幸运的是,LLVM Clang编译器已经增加了对eBPF后端的支持,该后端将C编译为字节码。然后可以使用bpf()系统调用和 BPF_PROG_LOAD命令直接加载包含该字节码的目标文件。
您可以使用-march = bpf参数与Clang一起编译,从而用C编写自己的eBPF程序。内核的samples / bpf / 目录中有许多eBPF程序示例;大多数文件名的后缀为“ _kern.c ”。Clang发出的目标文件(eBPF字节码)需要由计算机上本地运行的程序加载(这些示例的文件名通常带有“ _user.c ”)。为了使编写eBPF程序更容易,内核提供了libbpf库,该库包括用于加载程序以及创建和操作eBPF对象的帮助程序函数。例如,使用libbpf的eBPF程序和用户程序的高级流程 可能会像这样:
将eBPF字节码读取到用户应用程序的缓冲区中,并将其传递给bpf_load_program()。
eBPF程序在由内核运行时,将调用 bpf_map_lookup_elem()在地图中查找元素并将其存储新值。
用户应用程序调用bpf_map_lookup_elem()来读取eBPF程序存储在内核中的值。
但是,所有示例代码都有一个主要缺点:您需要从内核源代码树中编译eBPF程序。幸运的是,创建了BCC项目来解决此问题。它包括一个完整的工具链,用于编写eBPF程序并加载它们,而无需链接内核源代码树。
《BPF内核观测技术》笔录
BPF跟踪
探针
内核探针:提供内核中内部组件的动态访问
跟踪点:提供内核中内部组件的静态访问
用户空间探针:提供用户空间运行的程序的动态访问
用户静态定义跟踪点:提供用户空间运行的程序的静态访问
内核探针
kprobes–允许执行任何内核指令之前插入BPF程序
kretprobes–在内核指令有返回值时插入BPF程序
kprobes 探针在内核函数入口被调用,示例程序如下:
from bcc import BPF
bpf_source = """
#include <uapi/linux/ptrace.h>
int do_sys_execve(struct pt_regs *ctx) {
char comm[16];
bpf_get_current_comm(&comm, sizeof(comm));
bpf_trace_printk("executing program: %s\\n", comm);
return 0;
}
"""
bpf = BPF(text=bpf_source)
execve_function = bpf.get_syscall_fnname("execve")
bpf.attach_kprobe(event=execve_function, fn_name="do_sys_execve")
bpf.trace_print()
[longyu@debian-10:17:12:19] kprobes $ sudo python example.py
[sudo] longyu 的密码:
guake-21900 [003] .... 19913.271490: 0: executing program: guake
guake-21901 [001] .... 19913.273462: 0: executing program: guake
bash-21902 [000] .... 19913.274109: 0: executing program: bash
bash-21903 [005] .... 19913.277194: 0: executing program: bash
bash-21906 [002] .... 19913.279022: 0: executing
kretprobes 探测点在内核函数返回的时候被调用,示例程序如下:
from bcc import BPF
bpf_source = """
#include <uapi/linux/ptrace.h>
int ret_sys_execve(struct pt_regs *ctx) {
int return_value;
char comm[16];
bpf_get_current_comm(&comm, sizeof(comm));
return_value = PT_REGS_RC(ctx);
bpf_trace_printk("program: %s, return: %d\\n", comm, return_value);
return 0;
}
"""
bpf = BPF(text=bpf_source)
execve_function = bpf.get_syscall_fnname("execve")
bpf.attach_kretprobe(event=execve_function, fn_name="ret_sys_execve")
bpf.trace_print()
[longyu@debian-10:17:15:35] kretprobes $ sudo python example.py
bash-22471 [007] d... 20099.730870: 0: program: bash, return: 0
id-22472 [005] d... 20099.732045: 0: program: id, return: 0
utempter-22473 [004] d... 20099.732546: 0: program: utempter, return: 0
dircolors-22474 [005] d... 20099.733757: 0: program: dircolors, return: 0
dircolors-22475 [005] d... 20099.735221: 0: program: dircolors, return: 0
lua-22476 [005] d... 20099.736689: 0: program: lua, return: 0
跟踪点
tracepoints 跟踪点是内核代码的静态标记,用于将代码附加到运行内核中,它由内核开发人员在内核中编写与修改,内核探针与跟踪点都提供了在用户空间的完全访问
查看 /sys/kernel/debug/tracing/events 目录下的内容可以获取系统中所有可用的跟踪点。
用户空间探针
uprobes–内核在程序特定指令执行之前插入该指令集的钩子
uretprobes–与kretprobes并行探针,适用于用户空间使用,在将bpf程序附加到指令返回值之上,允许通过bpf代码从寄存器中访问返回值
一个示例 patch 内容如下:
diff --git a/code/chapter-4/uprobes/example.py b/code/chapter-4/uprobes/example.py
old mode 100644
new mode 100755
index 4f2c76f..c0a93de
--- a/code/chapter-4/uprobes/example.py
+++ b/code/chapter-4/uprobes/example.py
@@ -9,5 +9,5 @@ int trace_go_main(struct pt_regs *ctx) {
"""
bpf = BPF(text = bpf_source)
-bpf.attach_uprobe(name = "hello-bpf", sym = "main.main", fn_name = "trace_go_main")
+bpf.attach_uprobe(name = "/home/longyu/linux-observability-with-bpf/code/chapter-4/uprobes/hello-bpf", sym = "main.main", fn_name = "trace_go_main")
bpf.trace_print()
[longyu@debian-10:17:52:59] uprobes $ sudo python ./example.py
hello-bpf-25911 [005] .... 22341.336969: 0: New hello-bpf process running with PID: 25911
hello-bpf-25927 [001] .... 22354.651541: 0: New hello-bpf process running with PID: 25927
用户静态定义跟踪点
USDT为用户应用程序提供静态跟踪点,是检测应用程序的便捷方法
USDT 是用户态程序静态定义的跟踪点,类似于内核中的 tracepoint,它需要在程序的源代码中添加代码
示例 demo 源码如下:
#include <sys/sdt.h>
int main(int argc, char const *argv[]) {
DTRACE_PROBE("hello-usdt", "probe-main");
return 0;
}
直接编译,发现会报 sys/sdt.h 头文件不存在的问题。可以通过执行如下命令来解决:
sudo apt-get install systemtap-sdt-dev
修改 example.py 后执行有如下信息:
[longyu@debian-10:18:36:53] usdt $ sudo python example.py
/virtual/main.c:7:1: warning: control reaches end of non-void function [-Wreturn-type]
}
^
1 warning generated.
跟踪数据可视化
火焰图
对系统耗时进行可视化的图表
直方图
Perf事件
Perf事件的数组映射,允许将数据放入环形缓存区,以便实现用户空间实时同步
BPF运行时的体系架构
BPF API
include/uapi/linux/bpf.h
Some Graph
BPF helper function:
BPF Syscall
BPF tracing program types
注:BPF Type Format (BTF)
EBPF程序运行流程案例分析
EBPF程序运行
eBPF 在 Linux 内核中将 C 代码编译成 BPF 字节码,挂在 kprobe/tracepoint
等 hook 上,当 hook
触发时,Linux 内核运行字节码来追踪性能。
EBPF框架
在 Linux 内核中的 sample/bpf
目录存在许多 bpf 程序的样例,以 tracex4_kern.c
和 tracex4_user.c
为例。
下图是 eBPF 程序的框架,分为程序执行流和数据通信流。
对于程序执行流来说,
trace_kern.c
是分配 slab,释放 slab 时调用的代码,其中申明了 hook 处理函数和数据 map,数据 map 用于内核态和用户态之间的数据通信,使用 LLVM/clang 编译器编译成 bpf 字节码trace_user.c
用来加载 bpf 字节码,陷入内核态,通过 JIT(just in time) 编译器将 bpf 字节码转换成机器汇编码,当 kprobe 或 tracepoint 追踪到某类事件时执行上述申明的 hook 处理函数并获取数据 map,将数据传到 userspace
tracex4_kern.c
其中定义了 “my_map” 的一个数据 map,数据 map 由 key/value
键值对组成,这里的 value 是一个结构体的变量,用于获得当前运行时间和 ip 寄存器,数据 map 由__attribute__
声明,是一个单独的 section,编译成 ELF 格式的文件时该结构体变量存在 “maps” 段中。
接下来申明的是分配 slab(内存分配),释放 slab 的钩子处理函数,并单独放在 kprobe/kmem_cache_free
,kretprobe/kmem_cache_alloc_node
两个代码段中。
#include <linux/version.h>
#include <uapi/linux/bpf.h>
#include "bpf_helpers.h"
struct pair {
u64 val;
u64 ip;
};
struct bpf_map_def SEC("maps") my_map = {
.type = BPF_MAP_TYPE_HASH,
.key_size = sizeof(long),
.value_size = sizeof(struct pair),
.max_entries = 1000000,
};
SEC("kprobe/kmem_cache_free")
int bpf_prog1(struct pt_regs *ctx)
{
long ptr = PT_REGS_PARM2(ctx);
bpf_map_delete_elem(&my_map, &ptr);
return 0;
}
SEC("kretprobe/kmem_cache_alloc_node")
int bpf_prog2(struct pt_regs *ctx)
{
long ptr = PT_REGS_RC(ctx);
long ip = 0;
/* get ip address of kmem_cache_alloc_node() caller */
BPF_KRETPROBE_READ_RET_IP(ip, ctx);
struct pair v = {
.val = bpf_ktime_get_ns(),
.ip = ip,
};
bpf_map_update_elem(&my_map, &ptr, &v, BPF_ANY);
return 0;
}
char _license[] SEC("license") = "GPL";
u32 _version SEC("version") = LINUX_VERSION_CODE;
使用 clang 编译出 .o
文件,同样属于 ELF 文件,可以使用 llvm dump 出各个 section table,如上述自定义的几个段,用 SEC
就可自定义一个 section:
#define SEC(NAME) __attribute__((section(NAME), used))
tracex4_user.c
其中定义了一个 pair 结构体用来接受 hook 处理函数的数据,首先加载 tracex4_kern.o
,然后在死循环中轮询获取数据,然后用 printf
打印出来:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <stdbool.h>
#include <string.h>
#include <time.h>
#include <linux/bpf.h>
#include <sys/resource.h>
#include <bpf/bpf.h>
#include "bpf_load.h"
struct pair {
long long val;
__u64 ip;
};
static __u64 time_get_ns(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return ts.tv_sec * 1000000000ull + ts.tv_nsec;
}
static void print_old_objects(int fd)
{
long long val = time_get_ns();
__u64 key, next_key;
struct pair v;
key = write(1, "\e[1;1H\e[2J", 12); /* clear screen */
key = -1;
while (bpf_map_get_next_key(map_fd[0], &key, &next_key) == 0) {
bpf_map_lookup_elem(map_fd[0], &next_key, &v);
key = next_key;
if (val - v.val < 1000000000ll)
/* object was allocated more then 1 sec ago */
continue;
printf("obj 0x%llx is %2lldsec old was allocated at ip %llx\n",
next_key, (val - v.val) / 1000000000ll, v.ip);
}
}
int main(int ac, char **argv)
{
struct rlimit r = {RLIM_INFINITY, RLIM_INFINITY};
char filename[256];
int i;
snprintf(filename, sizeof(filename), "%s_kern.o", argv[0]);
if (setrlimit(RLIMIT_MEMLOCK, &r)) {
perror("setrlimit(RLIMIT_MEMLOCK, RLIM_INFINITY)");
return 1;
}
if (load_bpf_file(filename)) {
printf("%s", bpf_log_buf);
return 1;
}
for (i = 0; ; i++) {
print_old_objects(map_fd[1]);
sleep(1);
}
}
通过 readelf 和 llvm-objdump 解析目标文件
读取 ELF 文件头
wu@ubuntu:~/linux/samples/bpf$ readelf -h tracex4_kern.o
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: Linux BPF
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 8344 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 27
Section header string table index: 1
在 readelf 的输出中:
第 1 行,ELF Header: 指名 ELF 文件头开始。 第 2 行,Magic 魔数,用来指名该文件是一个 ELF 目标文件。第一个字节 7F 是个固定的数;后面的 3 个字节正是 E, L, F 三个字母的 ASCII 形式。 第 3 行,CLASS 表示文件类型,这里是 64位的 ELF 格式。 第 4 行,Data 表示文件中的数据是按照什么格式组织(大端或小端)的,不同处理器平台数据组织格式可能就不同,如x86平台为小端存储格式。 第 5 行,当前 ELF 文件头版本号,这里版本号为 1 。 第 6 行,OS/ABI ,指出操作系统类型,ABI 是 Application Binary Interface 的缩写。 第 7 行,ABI 版本号,当前为 0 。 第 8 行,Type 表示文件类型。ELF 文件有 3 种类型,一种是如上所示的 Relocatable file 可重定位目标文件,一种是可执行文件(Executable),另外一种是共享库(Shared Library) 。 第 9 行,机器平台类型。 这里是bpf虚拟机 第 10 行,当前目标文件的版本号。 第 11 行,程序的虚拟地址入口点,因为这还不是可运行的程序,故而这里为零。 第 12 行,与 11 行同理,这个目标文件没有 Program Headers。 第 13 行,sections 头开始处,这里 8344 是十进制,表示从地址偏移 0x2098 处开始。 第 14 行,是一个与处理器相关联的标志,x86 平台上该处为 0 。 第 15 行,ELF 文件头的字节数。 第 16 行,因为这个不是可执行程序,故此处大小为 0 第 19 行,一共有多少个 section 头,这里是 27个。
打印各个段的内容
wu@ubuntu:~/linux/samples/bpf$ readelf -S tracex4_kern.o
There are 27 section headers, starting at offset 0x2098:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .strtab STRTAB 0000000000000000 00001f80
0000000000000115 0000000000000000 0 0 1
[ 2] .text PROGBITS 0000000000000000 00000040
0000000000000000 0000000000000000 AX 0 0 4
[ 3] kprobe/kmem_cache PROGBITS 0000000000000000 00000040
0000000000000048 0000000000000000 AX 0 0 8
[ 4] .relkprobe/kmem_c REL 0000000000000000 00001870
0000000000000010 0000000000000010 26 3 8
[ 5] kretprobe/kmem_ca PROGBITS 0000000000000000 00000088
00000000000000c0 0000000000000000 AX 0 0 8
[ 6] .relkretprobe/kme REL 0000000000000000 00001880
0000000000000010 0000000000000010 26 5 8
[ 7] maps PROGBITS 0000000000000000 00000148
000000000000001c 0000000000000000 WA 0 0 4
[ 8] license PROGBITS 0000000000000000 00000164
0000000000000004 0000000000000000 WA 0 0 1
[ 9] version PROGBITS 0000000000000000 00000168
0000000000000004 0000000000000000 WA 0 0 4
[10] .debug_str PROGBITS 0000000000000000 0000016c
00000000000001e9 0000000000000001 MS 0 0 1
[11] .debug_loc PROGBITS 0000000000000000 00000355
0000000000000150 0000000000000000 0 0 1
[12] .rel.debug_loc REL 0000000000000000 00001890
0000000000000050 0000000000000010 26 11 8
[13] .debug_abbrev PROGBITS 0000000000000000 000004a5
0000000000000101 0000000000000000 0 0 1
[14] .debug_info PROGBITS 0000000000000000 000005a6
0000000000000376 0000000000000000 0 0 1
[15] .rel.debug_info REL 0000000000000000 000018e0
00000000000004b0 0000000000000010 26 14 8
[16] .debug_ranges PROGBITS 0000000000000000 0000091c
0000000000000030 0000000000000000 0 0 1
[17] .rel.debug_ranges REL 0000000000000000 00001d90
0000000000000040 0000000000000010 26 16 8
[18] .BTF PROGBITS 0000000000000000 0000094c
0000000000000569 0000000000000000 0 0 1
[19] .rel.BTF REL 0000000000000000 00001dd0
0000000000000030 0000000000000010 26 18 8
[20] .BTF.ext PROGBITS 0000000000000000 00000eb5
0000000000000178 0000000000000000 0 0 1
[21] .rel.BTF.ext REL 0000000000000000 00001e00
0000000000000140 0000000000000010 26 20 8
[22] .eh_frame PROGBITS 0000000000000000 00001030
0000000000000050 0000000000000000 A 0 0 8
[23] .rel.eh_frame REL 0000000000000000 00001f40
0000000000000020 0000000000000010 26 22 8
[24] .debug_line PROGBITS 0000000000000000 00001080
0000000000000147 0000000000000000 0 0 1
[25] .rel.debug_line REL 0000000000000000 00001f60
0000000000000020 0000000000000010 26 24 8
[26] .symtab SYMTAB 0000000000000000 000011c8
00000000000006a8 0000000000000018 1 66 8
其中,第三列代表类型(Type):
”NULL”:未使用,如段表的第一个空段
“PROGBITS”:程序数据,如 .text、.data、.rodata;
“REL”:重定位表,如 .rel.text;
“NOBITS”:暂时没有数据的程序空间,如 .bss;
“STRTAB”:字符串表,如 .strtab、.shstrtab;
“SYMTAB”:符号表,如 .symtab,包括所有用到的相关符号信息,如函数名、变量名。
通过 llvm-objdump 解析 BPF ELF 格式文件
wu@ubuntu:~/linux/samples/bpf$ llvm-objdump -h tracex4_kern.o
tracex4_kern.o: file format ELF64-BPF
Sections:
Idx Name Size VMA Type
0 00000000 0000000000000000
1 .strtab 00000115 0000000000000000
2 .text 00000000 0000000000000000 TEXT
3 kprobe/kmem_cache_free 00000048 0000000000000000 TEXT
4 .relkprobe/kmem_cache_free 00000010 0000000000000000
5 kretprobe/kmem_cache_alloc_node 000000c0 0000000000000000 TEXT
6 .relkretprobe/kmem_cache_alloc_node 00000010 0000000000000000
7 maps 0000001c 0000000000000000 DATA
8 license 00000004 0000000000000000 DATA
9 version 00000004 0000000000000000 DATA
... ...
可以使用 llvm 工具为 eBPF 程序进行反编译,tracex4_kern.o
是 ELF 格式的文件,分为两个代码段,如下所示 :
wu@ubuntu:~/linux/samples/bpf$ llvm-objdump -d -r -print-imm-hex tracex4_kern.o
tracex4_kern.o: file format ELF64-BPF
Disassembly of section kprobe/kmem_cache_free:
0000000000000000 bpf_prog1:
0: 79 11 68 00 00 00 00 00 r1 = *(u64 *)(r1 + 0x68)
1: 7b 1a f8 ff 00 00 00 00 *(u64 *)(r10 - 0x8) = r1
2: bf a2 00 00 00 00 00 00 r2 = r10
3: 07 02 00 00 f8 ff ff ff r2 += -0x8
4: 18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0x0 ll
0000000000000020: R_BPF_64_64 my_map
6: 85 00 00 00 03 00 00 00 call 0x3
7: b7 00 00 00 00 00 00 00 r0 = 0x0
8: 95 00 00 00 00 00 00 00 exit
Disassembly of section kretprobe/kmem_cache_alloc_node:
0000000000000000 bpf_prog2:
0: 79 12 50 00 00 00 00 00 r2 = *(u64 *)(r1 + 0x50)
1: 7b 2a f8 ff 00 00 00 00 *(u64 *)(r10 - 0x8) = r2
2: b7 02 00 00 00 00 00 00 r2 = 0x0
3: 7b 2a f0 ff 00 00 00 00 *(u64 *)(r10 - 0x10) = r2
4: 79 13 20 00 00 00 00 00 r3 = *(u64 *)(r1 + 0x20)
5: 07 03 00 00 08 00 00 00 r3 += 0x8
6: bf a1 00 00 00 00 00 00 r1 = r10
7: 07 01 00 00 f0 ff ff ff r1 += -0x10
8: b7 02 00 00 08 00 00 00 r2 = 0x8
9: 85 00 00 00 04 00 00 00 call 0x4
10: 85 00 00 00 05 00 00 00 call 0x5
11: 7b 0a e0 ff 00 00 00 00 *(u64 *)(r10 - 0x20) = r0
12: 79 a1 f0 ff 00 00 00 00 r1 = *(u64 *)(r10 - 0x10)
13: 7b 1a e8 ff 00 00 00 00 *(u64 *)(r10 - 0x18) = r1
14: bf a2 00 00 00 00 00 00 r2 = r10
15: 07 02 00 00 f8 ff ff ff r2 += -0x8
16: bf a3 00 00 00 00 00 00 r3 = r10
17: 07 03 00 00 e0 ff ff ff r3 += -0x20
18: 18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0x0 ll
0000000000000090: R_BPF_64_64 my_map
20: b7 04 00 00 00 00 00 00 r4 = 0x0
21: 85 00 00 00 02 00 00 00 call 0x2
22: b7 00 00 00 00 00 00 00 r0 = 0x0
23: 95 00 00 00 00 00 00 00 exit
........
接下来查看其他段的内容,比如 maps 段和 license 段:
wu@ubuntu:~/linux/samples/bpf$ llvm-objdump --section=maps -s tracex4_kern.o
tracex4_kern.o: file format ELF64-BPF
Contents of section maps:
0000 01000000 08000000 10000000 40420f00 ............@B..
0010 00000000 00000000 00000000 ............
wu@ubuntu:~/linux/samples/bpf$ llvm-objdump --section=license -s tracex4_kern.o
tracex4_kern.o: file format ELF64-BPF
Contents of section license:
0000 47504c00 GPL.
上述 bpf 程序的字节码,是不能在 x86_64 平台上直接执行的,当加载 bpf 程序时需要使用 JIT(just in time) 编译器将 bpf 字节码翻译成主机能识别的汇编码,然而对于大多数操作码,eBPF 指令集可以和 x86 或 aarch64 指令集一一映射。
bpf 程序自定义了一套指令,有别于 x86,ARM64 等,而且指令集没这两者丰富,没有浮点计算等。但寄存器功能大同小异, 功能如下所示:
无法复制加载中的内容
通过 strace 工具追踪分析 eBPF 程序行为
执行如下命令可以看到 slab 对象的分配地址以及分配时间:
wu@ubuntu:~/linux/samples/bpf$ sudo strace -v -f -s 128 -o tracex4.txt ./tracex4
obj 0xffff9637e3175cc0 is 1sec old was allocated at ip ffffffff99679a9a
obj 0xffff9637e31750c0 is 1sec old was allocated at ip ffffffff99679a9a
obj 0xffff9637e3175e00 is 1sec old was allocated at ip ffffffff99679a9a
obj 0xffff9637e3175780 is 1sec old was allocated at ip ffffffff99679a9a
... ...
strace 追踪到的关键系统调用如下:
execve("./tracex4", ["./tracex4"], ["LANG=en_US.UTF-8", "LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca"..., "TERM=xterm", "DISPLAY=localhost:12.0", "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin", "MAIL=/var/mail/root", "LOGNAME=root", "USER=root", "HOME=/root", "SHELL=/bin/bash", "SUDO_COMMAND=/usr/bin/strace -v -f -s 128 -o tracex4.txt ./tracex4", "SUDO_USER=wu", "SUDO_UID=1000", "SUDO_GID=1000"]) = 0
... ...
bpf(BPF_MAP_CREATE, {map_type=BPF_MAP_TYPE_HASH, key_size=8, value_size=16, max_entries=1000000, map_flags=0, inner_map_fd=0, map_name="my_map", map_ifindex=0, btf_fd=0, btf_key_type_id=0, btf_value_type_id=0}, 112) = 4
bpf(BPF_PROG_LOAD, {prog_type=BPF_PROG_TYPE_KPROBE, insn_cnt=9, insns=[{code=BPF_LDX|BPF_DW|BPF_MEM, dst_reg=BPF_REG_1, src_reg=BPF_REG_1, off=104, imm=0}, {code=BPF_STX|BPF_DW|BPF_MEM, dst_reg=BPF_REG_10, src_reg=BPF_REG_1, off=-8, imm=0}, {code=BPF_ALU64|BPF_X|BPF_MOV, dst_reg=BPF_REG_2, src_reg=BPF_REG_10, off=0, imm=0}, {code=BPF_ALU64|BPF_K|BPF_ADD, dst_reg=BPF_REG_2, src_reg=BPF_REG_0, off=0, imm=0xfffffff8}, {code=BPF_LD|BPF_DW|BPF_IMM, dst_reg=BPF_REG_1, src_reg=BPF_REG_1, off=0, imm=0x4}, {code=BPF_LD|BPF_W|BPF_IMM, dst_reg=BPF_REG_0, src_reg=BPF_REG_0, off=0, imm=0}, {code=BPF_JMP|BPF_K|BPF_CALL, dst_reg=BPF_REG_0, src_reg=BPF_REG_0, off=0, imm=0x3}, {code=BPF_ALU64|BPF_K|BPF_MOV, dst_reg=BPF_REG_0, src_reg=BPF_REG_0, off=0, imm=0}, {code=BPF_JMP|BPF_K|BPF_EXIT, dst_reg=BPF_REG_0, src_reg=BPF_REG_0, off=0, imm=0}], license="GPL", log_level=0, log_size=0, log_buf=NULL, kern_version=KERNEL_VERSION(5, 4, 0), prog_flags=0, prog_name="", prog_ifindex=0, expected_attach_type=BPF_CGROUP_INET_INGRESS, prog_btf_fd=0, func_info_rec_size=0, func_info=NULL, func_info_cnt=0, line_info_rec_size=0, line_info=NULL, line_info_cnt=0, attach_btf_id=0}, 112) = 5
openat(AT_FDCWD, "/sys/kernel/debug/tracing/kprobe_events", O_WRONLY|O_APPEND) = 6
write(6, "p:kmem_cache_free kmem_cache_free", 33) = 33
close(6)
openat(AT_FDCWD, "/sys/kernel/debug/tracing/events/kprobes/kmem_cache_free/id", O_RDONLY) = 6
read(6, "2145\n", 256) = 5
close(6)
perf_event_open({type=PERF_TYPE_TRACEPOINT, size=0 /* PERF_ATTR_SIZE_??? */, config=2145, sample_period=1, sample_type=PERF_SAMPLE_RAW, read_format=0, disabled=0, inherit=0, pinned=0, exclusive=0, exclusive_user=0, exclude_kernel=0, exclude_hv=0, exclude_idle=0, mmap=0, comm=0, freq=0, inherit_stat=0, enable_on_exec=0, task=0, watermark=0, precise_ip=0 /* arbitrary skid */, mmap_data=0, sample_id_all=0, exclude_host=0, exclude_guest=0, exclude_callchain_kernel=0, exclude_callchain_user=0, mmap2=0, comm_exec=0, use_clockid=0, context_switch=0, write_backward=0, namespaces=0, wakeup_events=1, config1=0}, -1, 0, -1, 0) = 6
... ...
openat(AT_FDCWD, "/sys/kernel/debug/tracing/kprobe_events", O_WRONLY|O_APPEND) = 8
write(8, "r:kmem_cache_alloc_node kmem_cache_alloc_node", 45) = 45
openat(AT_FDCWD, "/sys/kernel/debug/tracing/events/kprobes/kmem_cache_alloc_node/id", O_RDONLY) = 8
read(8, "2146\n", 256) = 5
close(8) = 0
perf_event_open({type=PERF_TYPE_TRACEPOINT, size=0 /* PERF_ATTR_SIZE_??? */, config=2146, sample_period=1, sample_type=PERF_SAMPLE_RAW, read_format=0, disabled=0, inherit=0, pinned=0, exclusive=0, exclusive_user=0, exclude_kernel=0, exclude_hv=0, exclude_idle=0, mmap=0, comm=0, freq=0, inherit_stat=0, enable_on_exec=0, task=0, watermark=0, precise_ip=0 /* arbitrary skid */, mmap_data=0, sample_id_all=0, exclude_host=0, exclude_guest=0, exclude_callchain_kernel=0, exclude_callchain_user=0, mmap2=0, comm_exec=0, use_clockid=0, context_switch=0, write_backward=0, namespaces=0, wakeup_events=1, config1=0}, -1, 0, -1, 0) = 8
ioctl(8, PERF_EVENT_IOC_ENABLE, 0) = 0
ioctl(8, PERF_EVENT_IOC_SET_BPF, 7) = 0
write(1, "\33[1;1H\33[2J\0\0", 12) = 12
bpf(BPF_MAP_GET_NEXT_KEY, {map_fd=4, key=0x7ffffaf162b0, next_key=0x7ffffaf162b8}, 112) = 0
bpf(BPF_MAP_LOOKUP_ELEM, {map_fd=4, key=0x7ffffaf162b8, value=0x7ffffaf162d0, flags=BPF_ANY}, 112) = 0
... ...
BPF 字节码加载分析
当 bpf 系统第一个参数 cmds=BPF_PROG_LOAD
时,表示加载 ELF64-BPF
格式的文件,仔细分析下第二个参数 attr
,这个结构体的原型如下,发现就是保存了 bpf 字节码:
struct { /* Used by BPF_PROG_LOAD */
__u32 prog_type;
__u32 insn_cnt;
__aligned_u64 insns; /* 'const struct bpf_insn *' */
__aligned_u64 license; /* 'const char *' */
__u32 log_level; /* verbosity level of verifier */
__u32 log_size; /* size of user buffer */
__aligned_u64 log_buf; /* user supplied 'char *'
buffer */
__u32 kern_version;
/* checked when prog_type=kprobe
(since Linux 4.1) */
};
__attribute__((aligned(8)));
当加载 bpf 程序时,BPF_PROG_LOAD
表示的是该程序的具体 bpf 指令,对应 bpf_prog1
这个代码段。
strace 追踪到的指令如下所示,每条指令的操作码由六部分组成:
code(操作码)
dst_reg(目标寄存器)
src_reg(源寄存器)
off(偏移)
imm(立即数)
详见:
insns=[
{code=BPF_LDX|BPF_DW|BPF_MEM, dst_reg=BPF_REG_1, src_reg=BPF_REG_1, off=104, imm=0},
{code=BPF_STX|BPF_DW|BPF_MEM, dst_reg=BPF_REG_10, src_reg=BPF_REG_1, off=-8, imm=0},
{code=BPF_ALU64|BPF_X|BPF_MOV, dst_reg=BPF_REG_2, src_reg=BPF_REG_10, off=0, imm=0},
{code=BPF_ALU64|BPF_K|BPF_ADD, dst_reg=BPF_REG_2, src_reg=BPF_REG_0, off=0, imm=0xfffffff8},
{code=BPF_LD|BPF_DW|BPF_IMM, dst_reg=BPF_REG_1, src_reg=BPF_REG_1, off=0, imm=0x4},
{code=BPF_LD|BPF_W|BPF_IMM, dst_reg=BPF_REG_0, src_reg=BPF_REG_0, off=0, imm=0},
{code=BPF_JMP|BPF_K|BPF_CALL, dst_reg=BPF_REG_0, src_reg=BPF_REG_0, off=0, imm=0x3},
{code=BPF_ALU64|BPF_K|BPF_MOV, dst_reg=BPF_REG_0, src_reg=BPF_REG_0, off=0, imm=0},
{code=BPF_JMP|BPF_K|BPF_EXIT, dst_reg=BPF_REG_0, src_reg=BPF_REG_0, off=0, imm=0}
]
指令格式如下图所示,BPF 当前拥有 102 个指令,主要包括三大类:
ALU (64bit and 32bit)
内存操作
分支操作
其中指令的格式主要由下面这几部分组成:
opcode 的低 3 位表示指令类型,BPF_LDX
,BPF_REG_10
等这些宏在 kernel 目录 tools/include/uapi/linux/bpf.h
中定义:
#define BPF_LDX 0x01
#define BPF_MEM 0x60
#define BPF_DW 0x18
... ...
以第一条 bpf 指令为例子:
{code=BPF_LDX|BPF_DW|BPF_MEM, dst_reg=BPF_REG_1, src_reg=BPF_REG_1, off=104, imm=0}
正好对应 llvm-objdump 解析出来的第一条指令,为内存访问指令:
0: 79 11 68 00 00 00 00 00 r1 = *(u64 *)(r1 + 0x68)
BPF_LDX|BPF_MEM|BPF_DW=0x79
该条指令在 kernel 中的定义为:
/* Memory load, dst_reg = *(uint *) (src_reg + off16) */
#define BPF_LDX_MEM(SIZE, DST, SRC, OFF) \
((struct bpf_insn) { \
.code = BPF_LDX | BPF_SIZE(SIZE) | BPF_MEM, \
.dst_reg = DST, \
.src_reg = SRC, \
.off = OFF, \
.imm = 0 })
MAP 数据通信分析
当 bpf 系统第一个参数 cmds=BPF_MAP_CREATE
时,表示创建一个数据 map,仔细分析下第二个参数 attr
,这个结构体的原型包含了 map 类型,key 的大小,valu e大小等:
union bpf_attr {
struct { /* Used by BPF_MAP_CREATE */
__u32 map_type;
__u32 key_size; /* size of key in bytes */
__u32 value_size; /* size of value in bytes */
__u32 max_entries; /* maximum number of entries
in a map */
};
用 strace 抓取的 log 分析来看 map 的类型为 BPF_MAP_TYPE_HASH
,key 的大小为 8,value大小为 16,访问 map 是 bpf_prog1
第5条字节码,其中的 imm 立即数为 4 代表 map_fd,这是一条伪指令,这条指令是可重定位指令:
bpf(BPF_MAP_CREATE, {map_type=BPF_MAP_TYPE_HASH, key_size=8, value_size=16, max_entries=1000000, map_flags=0, inner_map_fd=0, map_name="my_map", map_ifindex=0, btf_fd=0, btf_key_type_id=0, btf_value_type_id=0}, 112) = 4
0000000000000020: R_BPF_64_64 my_map
{code=BPF_LD|BPF_DW|BPF_IMM, dst_reg=BPF_REG_1, src_reg=BPF_REG_1, off=0, imm=0x4}
bpf 程序访问所有类型的 map 都可以使用 bpf_map_lookup_elem()
和 bpf_map_update_elem()
函数,socket maps 和一些其他额外的 map 当作特殊用途。
当 bpf 系统第一个参数 cmds=BPF_MAP_GET_NEXT_KEY
或 BPF_MAP_LOOKUP_ELEM
时,表示遍历 map,仔细分析下第二个参数 attr
,这个结构体的原型包含了 map_fd,是 bpf 系统调用第一个参数 cmd=BPF_MAP_CREATE
返回值,key 值,value 值等:
struct { /* Used by BPF_MAP_*_ELEM and BPF_MAP_GET_NEXT_KEY
commands */
__u32 map_fd;
__aligned_u64 key;
union {
__aligned_u64 value;
__aligned_u64 next_key;
};
__u64 flags;
};
bpf(BPF_MAP_GET_NEXT_KEY, {map_fd=4, key=0x7ffffaf162b0, next_key=0x7ffffaf162b8}, 112) = 0
bpf(BPF_MAP_LOOKUP_ELEM, {map_fd=4, key=0x7ffffaf162b8, value=0x7ffffaf162d0, flags=BPF_ANY}, 112) = 0
bpftool 用法简介
bpftool 在内核的 tools/bpf/bpftool/
目录下,使用 make 编译就可使用,查看当前运行的 bpf 程序,如下所示,可以看到当前运行的是 kprobe event
还有 map id
:
wu@ubuntu:~/linux/samples/bpf$ sudo bpftool prog show
[sudo] password for wu:
... ...
205: kprobe tag a6cfc4a29f52a193 gpl
loaded_at 2021-01-19T11:51:26+0000 uid 0
xlated 72B jited 62B memlock 4096B map_ids 72
206: kprobe tag d16c41919f3b767a gpl
loaded_at 2021-01-19T11:51:26+0000 uid 0
xlated 192B jited 119B memlock 4096B map_ids 72
查看 map 的id,可以看到当前使用的是 hash map:
wu@ubuntu:~/linux$ sudo bpftool map show
72: hash name my_map flags 0x0
key 8B value 16B max_entries 1000000 memlock 88788992B
查看 map 的所有的内容,并查看对应 key 的value:
wu@ubuntu:~/linux$ sudo bpftool map dump id 72
key: 80 35 43 e5 26 95 ff ff value: b9 f3 f3 9e 87 6b 00 00 9a 9a c7 86 ff ff ff ff
key: 80 9c 5f f6 25 95 ff ff value: 98 85 ac c7 8c 6b 00 00 9a 9a c7 86 ff ff ff ff
key: 00 4c 9b e4 25 95 ff ff value: e6 8d c2 b7 7e 6b 00 00 9a 9a c7 86 ff ff ff ff
key: 80 21 15 f8 26 95 ff ff value: 60 df 01 fc 5c 6b 00 00 9a 9a c7 86 ff ff ff ff
key: 80 f5 46 5f 26 95 ff ff value: 5e e1 73 7b 8d 6b 00 00 9a 9a c7 86 ff ff ff ff
... ...
wu@ubuntu:~/linux$ sudo bpftool map lookup id 72 key 0x80 0x35 0x43 0xe5 0x26 0x95 0xff 0xff
key: 80 35 43 e5 26 95 ff ff value: b9 f3 f3 9e 87 6b 00 00 9a 9a c7 86 ff ff ff ff
eBPF 程序装载、翻译与运行过程详解
BPF 程序格式为 ELF
加载 bpf 程序实质上是加载 ELF 格式文件,Linux 加载普通 ELF 格式的文件在通过 load_elf_binary
来实现,而 Linux 加载 bpf elf 其实在用户态实现的,使用的是开源的 libelf 库实现的,调用过程不太一样,而且只是把 ELF 格式的指令 dump 出来,接下来还需要 JIT 编译器翻译出机器汇编码才能执行,这个调用过程比 Linux 加载普通 ELF 格式文件简单。
libelf 库实现的各个 API 可参考如下 链接 以及我们专门为此准备的 libelf 开源库用法详解。
ELF 格式的详解可参考社区创始人撰写的开源书籍 C 语言编程透视 和配套视频课程《360° 剖析 Linux ELF》。
ELF 文件大体结构
ELF 文件大体结构如下所示,包含 ELF 头部,程序头表,各个段和程序段表:
ELF Header #程序头,有该文件的Magic number(参考man magic),类型等
Program Header Table #对可执行文件和共享库有效,它描述下面各个节(section)组成的段
Section1
Section2
Section3
.....
Program Section Table #仅对可重定位目标文件和静态库有效,用于描述各个Section的重定位信息等。
BPF 程序 Section 解析:get_sec
samples/bpf/bpf_load.c
中通过 get_sec
函数调用 libelf 库的 API 获取 section 内容,其中第四个参数是传入段的名字,最后一个参数获得的是该段的数据:
static int get_sec(Elf *elf, int i, GElf_Ehdr *ehdr, char **shname,
GElf_Shdr *shdr, Elf_Data **data)
{
Elf_Scn *scn;
scn = elf_getscn(elf, i); //从elf描述符获取按照节索引获取节接口
if (!scn)
return 1;
if (gelf_getshdr(scn, shdr) != shdr) // 通过节结构复制节表头
return 2;
*shname = elf_strptr(elf, ehdr->e_shstrndx, shdr->sh_name); // 从指定的字符串表中通过偏移获取字符串
if (!*shname || !shdr->sh_size)
return 3;
*data = elf_getdata(scn, 0); //从节中获取节数据(经过了字节序的转换)
if (!*data || elf_getdata(scn, *data) != NULL)
return 4;
return 0;
}
BPF 程序装载与解析:load_bpf_file
tracex4_user.c
通过 load_bpf_file
加载 .o
文件,我们来分析一下。
load_bpf_file
实质是调用 do_load_bpf_file
,在这个函数里首先打开 .o
文件,do_load_bpf_file
会将输入的 .o
文件作为 ELF 格式文件,逐个 section 进行分析:
如 section 的名字是特殊的(比如 ‘kprobe’),那么就会将这个 section 的内容作为
load_and_attach
的参数。如 section 的名字是 “license” 或 “version” 则保存 license 或 version。
如 section 是 map 则解析出 map 段
samples/bpf/bpfload.c
中的相关代码如下:
static int do_load_bpf_file(const char *path, fixup_map_cb fixup_map)
{
int fd, i, ret, maps_shndx = -1, strtabidx = -1;
Elf *elf;
GElf_Ehdr ehdr;
GElf_Shdr shdr, shdr_prog;
Elf_Data *data, *data_prog, *data_maps = NULL, *symbols = NULL;
char *shname, *shname_prog;
int nr_maps = 0;
... ...
fd = open(path, O_RDONLY, 0); //打开elf文件
if (fd < 0)
return 1;
elf = elf_begin(fd, ELF_C_READ, NULL);//获取elf描述符,使用‘读取’的方式
... ...
if (gelf_getehdr(elf, &ehdr) != &ehdr) //获取elf文件头副本
return 1;
... ...
/* scan over all elf sections to get license and map info */
for (i = 1; i < ehdr.e_shnum; i++) { //遍历各个section
if (get_sec(elf, i, &ehdr, &shname, &shdr, &data)) // shname 为"section"的名字
continue;
if (0) /* helpful for llvm debugging */ //打印各个section 对应的数据保存在data->d_buf中
printf("section %d:%s data %p size %zd link %d flags %d\n",
i, shname, data->d_buf, data->d_size,
shdr.sh_link, (int) shdr.sh_flags);
if (strcmp(shname, "license") == 0) { //如果是"license"段
processed_sec[i] = true;
memcpy(license, data->d_buf, data->d_size); //把 data->d_buf 拷贝到license数组
} else if (strcmp(shname, "version") == 0) { //如果是"version"段
processed_sec[i] = true;
if (data->d_size != sizeof(int)) {
printf("invalid size of version section %zd\n",
data->d_size);
return 1;
}
memcpy(&kern_version, data->d_buf, sizeof(int));//把 data->d_buf 拷贝到kern_version变量
} else if (strcmp(shname, "maps") == 0) { //如果是map 段
int j;
maps_shndx = i;
data_maps = data;
for (j = 0; j < MAX_MAPS; j++)
map_data[j].fd = -1;
} else if (shdr.sh_type == SHT_SYMTAB) {
strtabidx = shdr.sh_link;
symbols = data;
}
... ...
if (data_maps) { //对map段的处理
nr_maps = load_elf_maps_section(map_data, maps_shndx,elf, symbols, strtabidx); //获取map段内容
if (nr_maps < 0) {
printf("Error: Failed loading ELF maps (errno:%d):%s\n",
nr_maps, strerror(-nr_maps));
goto done;
}
if (load_maps(map_data, nr_maps, fixup_map)) //这里加载map
goto done;
map_data_count = nr_maps;
processed_sec[maps_shndx] = true;
}
/* process all relo sections, and rewrite bpf insns for maps */
for (i = 1; i < ehdr.e_shnum; i++) { //遍历所有的重定向段,
if (processed_sec[i]) ////flag 置位表示已经是处理了的段 ,跳过去
continue;
if (get_sec(elf, i, &ehdr, &shname, &shdr, &data))
continue;
if (shdr.sh_type == SHT_REL) {
struct bpf_insn *insns;
/* locate prog sec that need map fixup (relocations) */
if (get_sec(elf, shdr.sh_info, &ehdr, &shname_prog,
&shdr_prog, &data_prog)) //该段保存到data_prog
continue;
if (shdr_prog.sh_type != SHT_PROGBITS ||
!(shdr_prog.sh_flags & SHF_EXECINSTR))
continue;
insns = (struct bpf_insn *) data_prog->d_buf; //得到bpf字节码对应的结构体
processed_sec[i] = true; /* relo section */
if (parse_relo_and_apply(data, symbols, &shdr, insns,
map_data, nr_maps))
continue;
}
}
/* load programs */
for (i = 1; i < ehdr.e_shnum; i++) {
if (processed_sec[i]) //flag 置位表示已经是处理了的段 ,跳过去
continue;
if (get_sec(elf, i, &ehdr, &shname, &shdr, &data))
continue;
if (memcmp(shname, "kprobe/", 7) == 0 ||
memcmp(shname, "kretprobe/", 10) == 0 ||
memcmp(shname, "tracepoint/", 11) == 0 ||
memcmp(shname, "raw_tracepoint/", 15) == 0 ||
memcmp(shname, "xdp", 3) == 0 ||
memcmp(shname, "perf_event", 10) == 0 ||
memcmp(shname, "socket", 6) == 0 ||
memcmp(shname, "cgroup/", 7) == 0 ||
memcmp(shname, "sockops", 7) == 0 ||
memcmp(shname, "sk_skb", 6) == 0 ||
memcmp(shname, "sk_msg", 6) == 0) {
ret = load_and_attach(shname, data->d_buf,
data->d_size); //事件类型 字节码 字节码大小
if (ret != 0)
goto done;
}
}
done:
close(fd);
return ret;
}
打开 ELF 调试 log,可以得到该 ELF 文件各个段的内容首地址,大小,属性等信息。
wu@ubuntu:~/linux/samples/bpf$ sudo ./tracex4
[sudo] password for wu:
section 1:.strtab data 0x556034a3d070 size 277 link 0 flags 0
section 3:kprobe/kmem_cache_free data 0x556034a3d5a0 size 72 link 0 flags 6
section 4:.relkprobe/kmem_cache_free data 0x556034a3d5f0 size 16 link 26 flags 0
section 5:kretprobe/kmem_cache_alloc_node data 0x556034a3d610 size 192 link 0 flags 6
section 6:.relkretprobe/kmem_cache_alloc_node data 0x556034a3d6e0 size 16 link 26 flags 0
section 7:maps data 0x556034a3d700 size 28 link 0 flags 3
section 8:license data 0x556034a3d730 size 4 link 0 flags 3
section 9:version data 0x556034a3d750 size 4 link 0 flags 3
section 10:.debug_str data 0x556034a3d770 size 489 link 0 flags 48
section 11:.debug_loc data 0x556034a3d970 size 336 link 0 flags 0
section 12:.rel.debug_loc data 0x556034a3dad0 size 80 link 26 flags 0
section 13:.debug_abbrev data 0x556034a3db30 size 257 link 0 flags 0
section 14:.debug_info data 0x556034a3dc40 size 886 link 0 flags 0
section 15:.rel.debug_info data 0x556034a3dfc0 size 1200 link 26 flags 0
section 16:.debug_ranges data 0x556034a3e480 size 48 link 0 flags 0
section 17:.rel.debug_ranges data 0x556034a3e4c0 size 64 link 26 flags 0
section 18:.BTF data 0x556034a3e510 size 1384 link 0 flags 0
section 19:.rel.BTF data 0x556034a3ea80 size 48 link 26 flags 0
section 20:.BTF.ext data 0x556034a3eac0 size 376 link 0 flags 0
section 21:.rel.BTF.ext data 0x556034a3ec40 size 320 link 26 flags 0
section 22:.eh_frame data 0x556034a3ed90 size 80 link 0 flags 2
section 23:.rel.eh_frame data 0x556034a3edf0 size 32 link 26 flags 0
section 24:.debug_line data 0x556034a3ee20 size 327 link 0 flags 0
section 25:.rel.debug_line data 0x556034a3ef70 size 32 link 26 flags 0
section 26:.symtab data 0x556034a3efa0 size 1704 link 1 flags 0
BPF 字节码加载过程
接下来调用 load_and_attach
,第一个参数是 event,本例就是 “kprobe/” ,第二个参数是 bpf 字节码,第三个参数是字节码大小。
BPF 指令结构
bpf_insn
是一个结构体,代表一条 eBPF 指令,包含 5 个字段组成:
struct bpf_insn {
__u8 code; /* opcode */
__u8 dst_reg:4; /* dest register */
__u8 src_reg:4; /* source register */
__s16 off; /* signed offset */
__s32 imm; /* signed immediate constant */
};
每一个 eBPF 程序都是由若干个 bpf 指令构成,就是一个一个 bpf_insn
数组,使用 bpf 系统调用将其载入内核。
把 BPF 字节码装载到内核空间
接着调用 bpf_load_program
,填入的参数为程序类型 prog_type
, 和虚拟机指令 insns_cnt
等。
如果判断 events 是 kprobe/kretprobe
,那么填充 buf 为 debugfs 相关路径。打开该路径,然后调用 sys_perf_event_open ioctl
设置等,这个和 strace 追踪到的调用过程基本一致。
static int load_and_attach(const char *event, struct bpf_insn *prog, int size)
{
bool is_socket = strncmp(event, "socket", 6) == 0;
......
fd = bpf_load_program(prog_type, prog, insns_cnt, license, kern_version,
bpf_log_buf, BPF_LOG_BUF_SIZE);
......
if (is_kprobe || is_kretprobe) {
bool need_normal_check = true;
const char *event_prefix = "";
if (is_kprobe)
event += 7;
else
event += 10;
if (*event == 0) {
printf("event name cannot be empty\n");
return -1;
}
if (isdigit(*event))
return populate_prog_array(event, fd);
#ifdef __x86_64__
if (strncmp(event, "sys_", 4) == 0) {
snprintf(buf, sizeof(buf), "%c:__x64_%s __x64_%s",
is_kprobe ? 'p' : 'r', event, event);
err = write_kprobe_events(buf);
if (err >= 0) {
need_normal_check = false;
event_prefix = "__x64_";
}
}
#endif
if (need_normal_check) {
snprintf(buf, sizeof(buf), "%c:%s %s",
is_kprobe ? 'p' : 'r', event, event);
err = write_kprobe_events(buf);
if (err < 0) {
printf("failed to create kprobe '%s' error '%s'\n",
event, strerror(errno));
return -1;
}
}
strcpy(buf, DEBUGFS);
strcat(buf, "events/kprobes/");
strcat(buf, event_prefix);
strcat(buf, event);
strcat(buf, "/id");
}
efd = open(buf, O_RDONLY, 0);
if (efd < 0) {
printf("failed to open event %s\n", event);
return -1;
}
err = read(efd, buf, sizeof(buf));
if (err < 0 || err >= sizeof(buf)) {
printf("read from '%s' failed '%s'\n", event, strerror(errno));
return -1;
}
close(efd);
buf[err] = 0;
id = atoi(buf);
attr.config = id;
efd = sys_perf_event_open(&attr, -1/*pid*/, 0/*cpu*/, -1/*group_fd*/, 0);
... ...
event_fd[prog_cnt - 1] = efd;
err = ioctl(efd, PERF_EVENT_IOC_ENABLE, 0);
... ...
err = ioctl(efd, PERF_EVENT_IOC_SET_BPF, fd);
... ...
return 0;
}
其中 bpf_load_program
会通过 BPF_PROG_LOAD
系统调用,将字节码传入内核,返回一个文件描述符 fd
,attr->insns
就是下面这种 bpf 字节码:
code=BPF_ALU64|BPF_X|BPF_MOV, dst_reg=BPF_REG_6, src_reg=BPF_REG_1, off=0, imm=0
kernel/bpf/syscall.c
中定义的相应系统调用如下:
SYSCALL_DEFINE3(bpf, int, cmd, union bpf_attr __user *, uattr, unsigned int, size)
{
......
case BPF_MAP_CREATE:
err = map_create(&attr);
break;
case BPF_PROG_LOAD:
err = bpf_prog_load(&attr); //attr包含字节码
... ...
}
而 bpf_prog_load
真正的加载 bpf 字节码,首先从 bpf 字节码中获得 license,判断是不是 GPL license。
然后分配内核 bpf_prog
程序数据结构空间,将 bpf 虚拟机指令从用户空间拷贝到内核空间,把指令保存在 struct bpf_prog
结构体中。
然后运行 bpf_check
验证 bpf 指令在注入内核是否安全,比如检查栈是否会溢出,除数是否为零,否则不检测安不安全容易造成内核 panic 等严重问题,这一部分内容很多,就暂时不分析了。
把 BPF 字节码翻译为机器码
验证通过之后,核心调用是运行 bpf_prog_select_runtime
里的 do_jit
把 bpf 字节码转换成机器汇编码,最后运行 bpf_prog_kallsyms_add
将机器汇编码添加到 kallsyms
,在 /proc/kallsyms
中会看到 bpf 程序的符号表:
static int bpf_prog_load(union bpf_attr *attr)
{
enum bpf_prog_type type = attr->prog_type;
struct bpf_prog *prog;
int err;
char license[128];
bool is_gpl;
... ...
/* copy eBPF program license from user space */
if (strncpy_from_user(license, u64_to_user_ptr(attr->license),
sizeof(license) - 1) < 0) //拷贝license attr->license
return -EFAULT;
license[sizeof(license) - 1] = 0; //最后一位设空字符
/* eBPF programs must be GPL compatible to use GPL-ed functions */
is_gpl = license_is_gpl_compatible(license);
/* plain bpf_prog allocation */
prog = bpf_prog_alloc(bpf_prog_size(attr->insn_cnt), GFP_USER); /* 分配内核 bpf_prog 程序数据结构空间 */
if (!prog)
return -ENOMEM;
prog->expected_attach_type = attr->expected_attach_type;
prog->aux->offload_requested = !!attr->prog_ifindex;
err = security_bpf_prog_alloc(prog->aux);
if (err)
goto free_prog_nouncharge;
err = bpf_prog_charge_memlock(prog);
if (err)
goto free_prog_sec;
prog->len = attr->insn_cnt;
err = -EFAULT;
if (copy_from_user(prog->insns, u64_to_user_ptr(attr->insns),
bpf_prog_insn_size(prog)) != 0) //将若干指令从用户态拷贝到内核态
goto free_prog;
prog->orig_prog = NULL;
prog->jited = 0;
atomic_set(&prog->aux->refcnt, 1);
prog->gpl_compatible = is_gpl ? 1 : 0; //设置gpl_compatible字段
... ...
/* run eBPF verifier */
err = bpf_check(&prog, attr); //运行verifier 检查字节码安全性
if (err < 0)
goto free_used_maps;
prog = bpf_prog_select_runtime(prog, &err); //这里调用do_jit 将bpf字节码转换成汇编码
if (err < 0)
goto free_used_maps;
err = bpf_prog_alloc_id(prog);
if (err)
goto free_used_maps;
bpf_prog_kallsyms_add(prog); //添加kallsyms
err = bpf_prog_new_fd(prog);
if (err < 0)
bpf_prog_put(prog);
return err;
... ...
}
运行 BPF 机器码
JIT 编译器将机器汇编码的首地址转换成一个函数指针,保存到 prog->bpf_func
,再看看哪里调用 prog->bpf_func
这个函数指针的呢?
在 debugfs 中创建 kprobe events,执行 init_kprobe_trace
加载 BPF 字节码的时候就调用了 trace_kprobe_create
继而调用 kprobe_dispatcher
,因为定义了 CONFIG_PERF_EVENTS
而后调用 kprobe_perf_func,相关代码如下:
static struct dyn_event_operations trace_kprobe_ops = {
.create = trace_kprobe_create,
.show = trace_kprobe_show,
.is_busy = trace_kprobe_is_busy,
.free = trace_kprobe_release,
.match = trace_kprobe_match,
};
/* Make a tracefs interface for controlling probe points */
static __init int init_kprobe_trace(void)
{
... ...
ret = dyn_event_register(&trace_kprobe_ops);
if (ret)
return ret;
if (register_module_notifier(&trace_kprobe_module_nb))
return -EINVAL;
d_tracer = tracing_init_dentry();
if (IS_ERR(d_tracer))
return 0;
entry = tracefs_create_file("kprobe_events", 0644, d_tracer,
NULL, &kprobe_events_ops);
... ...
return 0;
}
fs_initcall(init_kprobe_trace);
trace_kprobe_create
{
... ...
kprobe_dispatcher
... ...
}
static int kprobe_dispatcher(struct kprobe *kp, struct pt_regs *regs)
{
struct trace_kprobe *tk = container_of(kp, struct trace_kprobe, rp.kp);
int ret = 0;
raw_cpu_inc(*tk->nhit);
if (trace_probe_test_flag(&tk->tp, TP_FLAG_TRACE))
kprobe_trace_func(tk, regs);
#ifdef CONFIG_PERF_EVENTS
if (trace_probe_test_flag(&tk->tp, TP_FLAG_PROFILE))
ret = kprobe_perf_func(tk, regs);
#endif
return ret;
}
kprobe_perf_func
会调用 trace_call_bpf
,在这里会执行 bpf 程序。BPF_PROG_RUN_ARRAY_CHECK
是一个宏,其实质上执行 BPF_PROG_RUN
里的一个函数:
/* Kprobe profile handler */
static int
kprobe_perf_func(struct trace_kprobe *tk, struct pt_regs *regs)
{
if (bpf_prog_array_valid(call)) {
unsigned long orig_ip = instruction_pointer(regs);
int ret;
ret = trace_call_bpf(call, regs);
/*
* We need to check and see if we modified the pc of the
* pt_regs, and if so return 1 so that we don't do the
* single stepping.
*/
if (orig_ip != instruction_pointer(regs))
return 1;
if (!ret)
return 0;
}
return 0;
}
/ * trace_call_bpf - invoke BPF program
* @call: tracepoint event
* @ctx: opaque context pointer
*
* kprobe handlers execute BPF programs via this helper.
* Can be used from static tracepoints in the future.
*
* Return: BPF programs always return an integer which is interpreted by
* kprobe handler as:
* 0 - return from kprobe (event is filtered out)
* 1 - store kprobe event into ring buffer
* Other values are reserved and currently alias to 1
*/
unsigned int trace_call_bpf(struct trace_event_call *call, void *ctx)
{
unsigned int ret;
... ...
ret = BPF_PROG_RUN_ARRAY_CHECK(call->prog_array, ctx, BPF_PROG_RUN); //运行bpf程序
out:
__this_cpu_dec(bpf_prog_active);
preempt_enable();
return ret;
}
其中的 trace_event_call
结构体定义了 bpf_prog_array
,该结构体数组中包含了要执行的函数指针:
struct trace_event_call {
struct list_head list;
struct trace_event_class *class;
union {
char *name;
/* Set TRACE_EVENT_FL_TRACEPOINT flag when using "tp" */
struct tracepoint *tp;
};
... ...
#ifdef CONFIG_PERF_EVENTS
int perf_refcount;
struct hlist_head __percpu *perf_events;
struct bpf_prog_array __rcu *prog_array;
int (*perf_perm)(struct trace_event_call *,
struct perf_event *);
#endif
};
struct bpf_prog_array {
struct rcu_head rcu;
struct bpf_prog_array_item items[0];
};
struct bpf_prog_array_item {
struct bpf_prog *prog;
struct bpf_cgroup_storage *cgroup_storage[MAX_BPF_CGROUP_STORAGE_TYPE];
};
BPF_PROG_RUN_ARRAY_CHECK
, BPF_PROG_RUN
宏展开如下所示,实质是在 BPF_PROG_RUN
中调用 ret = (*(prog)->bpf_func)(ctx, (prog)->insnsi)
这个函数指针来执行 bpf 指令:
#define BPF_PROG_RUN_ARRAY_CHECK(array, ctx, func) \
__BPF_PROG_RUN_ARRAY(array, ctx, func, true)
#define __BPF_PROG_RUN_ARRAY(array, ctx, func, check_non_null) \
({ \
struct bpf_prog_array_item *_item; \
struct bpf_prog *_prog; \
struct bpf_prog_array *_array; \
u32 _ret = 1; \
preempt_disable(); \
rcu_read_lock(); \
_array = rcu_dereference(array); \
if (unlikely(check_non_null && !_array))\
goto _out; \
_item = &_array->items[0]; \
while ((_prog = READ_ONCE(_item->prog))) { \
bpf_cgroup_storage_set(_item->cgroup_storage); \
_ret &= func(_prog, ctx); \
_item++; \
} \
_out: \
rcu_read_unlock(); \
preempt_enable(); \
_ret; \
})
#define BPF_PROG_RUN(prog, ctx) ({ \
u32 ret; \
cant_sleep(); \
if (static_branch_unlikely(&bpf_stats_enabled_key)) { \
struct bpf_prog_stats *stats; \
u64 start = sched_clock(); \
ret = (*(prog)->bpf_func)(ctx, (prog)->insnsi); \
stats = this_cpu_ptr(prog->aux->stats); \
u64_stats_update_begin(&stats->syncp); \
stats->cnt++; \
stats->nsecs += sched_clock() - start; \
u64_stats_update_end(&stats->syncp); \
} else { \
ret = (*(prog)->bpf_func)(ctx, (prog)->insnsi); \
} \
ret; })