使用libbpf-bootstrap构建第一个libbpf+BPF CO-RE程序

前言

本文参考了相关博客:Building BPF applications with libbpf-bootstrap

选择libbpf+BPF CO-RE的理由

libbpf 是一个比BCC更新的 BPF 开发库,也是最新的 BPF 开发推荐方式,以下是从BCC转向libbpf+BPF CO-RE的理由:

BCC 依靠运行时间汇编,将整个大型 LLVM/Clang 库带入并嵌入其中。这有许多后果,所有这些都不太理想:

  • 编译过程中资源利用量大(内存和 CPU),可能会中断繁忙服务器上的主要工作流;
  • 依赖于内核头包,该包必须安装在每个目标主机上。即便如此,如果您需要从内核中获取不通过public headers暴露的东西,您也需要手工将类型定义复制/粘贴到 BPF 代码中,以完成您的工作;
  • 即使是微不足道的编译时间错误也只在运行时间(在您完全重建并重新启动用户空间应用程序后)检测到:这显著缩短了开发迭代时间(并增加了挫折感水平。。。)

文章BPF Portability and CO-RE 指出,为了提高BPF程序的便携性,即在不同内核版本上正常工作,而无需为每个特定内核重新编译的能力,社区提出了一个称为BPF CO-RE(Compile Once – Run Everywhere)的解决方案。

Libbpf+BPF CO-RE的理念是,BPF程序与任何"正常"用户空间程序没有太大区别:它们应该汇编成小型二进制文件,然后以紧凑的形式进行部署,以瞄准主机。Libbpf 扮演 BPF 程序装载机的角色,执行平凡的设置工作(重定位、加载和验证 BPF 程序、创建 BPF map、连接到 BPF 挂钩等),让开发人员只担心 BPF 程序的正确性和性能。这种方法将开销保持在最低水平,消除沉重的依赖关系,使整体开发人员体验更加愉快。

使用libbpf-bootstrap的理由

开始使用 BPF 在很大程度上仍然令人生畏,因为即使为简单的"Hello World"般的 BPF 应用程序设置构建工作流,也需要一系列步骤,对于新的 BPF 开发人员来说,这些步骤可能会令人沮丧和令人生畏。这并不复杂,但知道必要的步骤是一个(不必要的)困难的部分。

libbpf-bootstrap 就是这样一个 BPF 游乐场,它已经尽可能地为初学者配置好了环境,帮助他们可以直接步入到 BPF 程序的书写。它综合了 BPF 社区多年来的最佳实践,并且提供了一个现代化的、便捷的工作流。libbpf-bootstrap 依赖于 libbpf 并且使用了一个很简单的 Makefile。对于需要更高级设置的用户,它也是一个好的起点。即使这个 Makefile不会被直接使用到,也可以很轻易地迁移到别的构建系统上。

Libbpf-bootstrap结构

Libbpf-bootstrap的内容如下:

$ tree -d
.
├── examples
│   ├── c
│   │   ├── CMakeFiles
│   │   │   ├── 3.18.4
│   │   │   │   ├── CompilerIdC
│   │   │   │   │   └── tmp
│   │   │   │   └── CompilerIdCXX
│   │   │   │       └── tmp
│   │   │   ├── bootstrap.dir
│   │   │   ├── CMakeTmp
│   │   │   ├── fentry.dir
│   │   │   ├── kprobe.dir
│   │   │   ├── libbpf-build.dir
│   │   │   ├── libbpf.dir
│   │   │   ├── minimal.dir
│   │   │   └── uprobe.dir
│   │   └── libbpf
│   │       ├── bpf
│   │       ├── libbpf
│   │       │   └── staticobjs
│   │       ├── pkgconfig
│   │       ├── src
│   │       │   └── libbpf-stamp
│   │       └── tmp
│   └── rust
│       ├── tracecon
│       │   └── src
│       │       └── bpf
│       └── xdp
│           └── src
│               └── bpf
├── libbpf
│   ├── include
│   │   ├── asm
│   │   ├── linux
│   │   └── uapi
│   │       └── linux
│   ├── scripts
│   ├── src
│   │   ├── sharedobjs
│   │   └── staticobjs
│   └── travis-ci
│       ├── managers
│       └── vmtest
│           └── configs
│               ├── blacklist
│               └── whitelist
├── tools
│   └── cmake
└── vmlinux

51 directories

libbpf-bootstrap捆绑libbpf作为子目录中的子模块,以避免取决于全系统的libbpf可用性和版本。这意味着,拉取时拉不了libbpf这个文件下,需要你自己定制(再拉一次…)。

libbpf/tools/包含二进制文件,用于构建BPF代码的BPF骨架。与 libbpf 类似,它被捆绑以避免取决于全系统的 bpftool 可用性及其版本是否足够最新。

此外,bpftool 可用于生成具有所有 Linux 内核类型定义的您自己的头文件vmlinux.h,文章【踩坑】使用libbpfgo构建你的第一个eBPF项目就有类似操作。

但很可能你不需要这样做, 因为利布普夫引导已经在子目录中提供了预生成的vmlinux. h 。它基于 Linux 5.8 的默认内核配置,启用了一系列额外的 BPF 相关功能。这意味着它应该已经有很多常用的内核类型和常数。由于BPF CO-RE,不必完全匹配您的内核配置和版本。但是,如果您确实需要生成自定义内核,请随时查看工具/gen_vmlinux_h.sh脚本,看看如何做到这一点。

Makefile定义了必要的生成规则,以编译所有提供的(和自定义的)BPF 应用程序。它遵循一个简单的文件命名惯例:

  • <app>.bpf.c文件是包含在内核上下文中执行的逻辑的 BPF C 代码;
  • <app>.c是用户空间 C 代码,它加载 BPF 代码并在应用程序的整个使用寿命内与它交互:
  • 可选的是具有通用类型定义的标头文件,由应用的 BPF 和用户空间代码共享。<app>.h

Minimal:最小应用程序分析

minimal是一个很好的尝试。把它当作一个极简主义的游乐场, 尝试 Bpf 的东西。它不使用BPF CO-RE,因此您可以使用较旧的内核,只需将系统内核头用于内核类型定义。这不是构建生产应用和工具的最佳方法,但对于本地实验来说已经足够了。

minimal使用bpf_printk()这一经典方法,将内容输出到/sys/kernel/debug/tracing/trace_pipe,使用root权限可以进行查看。

运行minimal:

按照官方教程

$ cd examples/c
$ make minimal
$ sudo ./minimal
$ sudo cat /sys/kernel/debug/tracing/trace_pipe
           <...>-3840345 [010] d... 3220701.101143: bpf_trace_printk: BPF triggered from PID 3840345.
           <...>-3840345 [010] d... 3220702.101265: bpf_trace_printk: BPF triggered from PID 3840345.

minimal是一个几乎仅有骨架的BPF程序,适合做新程序的实验。

代码分析:BPF side

以下是BPF side的应用程序代码minimal.bpf.c的内容:

// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
/* Copyright (c) 2020 Facebook */
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>

char LICENSE[] SEC("license") = "Dual BSD/GPL";

int my_pid = 0;

SEC("tp/syscalls/sys_enter_write")
int handle_tp(void *ctx)
{
   
	int pid = bpf_get_current_pid_tgid() >> 32;

	if (pid != my_pid)
		return 0;

	bpf_printk("BPF triggered from PID %d.\n", pid);

	return 0;
}

#include <linux/bpf.h>包括一些基本的 BPF 相关类型和常数,这些类型和常数是使用内核侧 BPF API(例如,BPF helper 函数标志)所必需的。由最常用的宏、常数和 BPF 助手定义提供并包含这些定义,几乎每个现有 BPF 应用程序都使用这些定义。

LICENSE变量定义了BPF代码的许可证。指定许可证是强制性的,由内核强制执行。某些 BPF 功能不可用于非 GPL 兼容代码。注意特殊注释。 (由 )将变量和功能放入指定部分。,沿着一些其他部分的名称,是惯例所决定的,所以一定要坚持下去。

接下来,我们看到使用令人兴奋的BPF功能:global变量。 做你所期望的:它定义了一个global变量,BPF代码可以读取和更新,就像任何用户空间C代码将做一个global变量。使用 BPF global变量来维护 BPF 计划的状态非常方便,而且性能也非常出色。此外,此类global变量可以从用户空间方面读取和编写。此功能可从 Linux 5.5 版本开始。它经常用于诸如配置具有额外设置的 BPF 应用程序、低开销统计信息等。它还可用于在内核 BPF 代码和用户空间控制代码之间来回传递数据。int my_pid = 0;

SEC("tp/syscalls/sys_enter_write") int handle_tp(void *ctx) { ... }定义将加载到内核中的BPF程序。在特别命名的部分(使用宏)中,它表示为普通 C 函数。节名称定义了 BPF 程序 libbpf 应该创建的类型以及如何/在哪里可以将其附加到内核中。在这种情况下,我们定义了一个跟踪点 BPF 程序,每次从任何用户空间应用程序调用系统呼叫时,都会将其调用。

可能在同一 BPF C 代码文件中定义了许多 BPF 程序。它们可能具有不同的类型(即注释)。例如,您可以有几个不同的 BPF 程序,每个程序用于不同的跟踪点或其他一些内核事件(例如,正在处理的网络数据包等)。您还可以定义具有相同属性的多个 BPF 程序:将处理得很好。在同一 BPF C 代码文件中定义的所有 BPF 程序共享所有全球状态(如变量,但使用时还可以共享任何 BPF 地图)。这经常被用于协调很少的合作 BPF 程序。

接下来看handle_tp()打算做什么:

	int pid = bpf_get_current_pid_tgid() >> 32;
	if (pid !=
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值