1. 前言

在本系列的第 1 部分和第 2 部分,我们介绍了 eBPF 虚拟机内部工作原理,在第 3 部分我们研究了基于底层虚拟机机制之上开发和使用 eBPF 程序的主流方式。

在这一部分中,我们将从另外一个视角来分析项目,尝试解决嵌入式 Linux 系统所面临的一些独特的问题:如需要非常小的自定义操作系统镜像,不能容纳完整的 BCC LLVM 工具链/python 安装,或试图避免同时维护主机的交叉编译(本地)工具链和交叉编译的目标编译器工具链,以及其相关的构建逻辑,即使在使用像 OpenEmbedded/Yocto 这样的高级构建系统时也很重要。

2. 关于可移植性

在第 3 部分研究的运行 eBPF/BCC 程序的主流方式中,可移植性并不是像在嵌入式设备上面临的问题那么大:eBPF 程序是在被加载的同一台机器上编译的,使用已经运行的内核,而且头文件很容易通过发行包管理器获得。嵌入式系统通常运行不同的 Linux 发行版和不同的处理器架构,与开发人员的计算机相比,有时具有重度修改或上游分歧的内核,在构建配置上也有很大的差异,或还可能使用了只有二进制的模块。

eBPF 虚拟机的字节码是通用的(并未与特定机器相关),所以一旦编译好 eBPF 字节码,将其从 x86_64 移动到 ARM 设备上并不会引起太多问题。当字节码探测内核函数和数据结构时,问题就开始了,这些函数和数据结构可能与目标设备的内核不同或者会不存在,所以至少目标设备的内核头文件必须存在于构建 eBPF 程序字节码的主机上。新的功能或 eBPF 指令也可能被添加到以后的内核中,这可以使 eBPF 字节码向前兼容,但不能在内核版本之间向后兼容(参见内核版本与 eBPF 功能)。建议将 eBPF 程序附加到稳定的内核 ABI 上,如跟踪点 tracepoint,这可以缓解常见的可移植性。

最近一个重要的工作已经开始,通过在 LLVM 生成的 eBPF 对象代码中嵌入数据类型信息,通过增加 BTF(BTF 类型格式)数据,以增加 eBPF 程序的可移植性(CO-RE 一次编译,到处运行)。更多信息见这里的 补丁文章。这很重要,因为 BTF 涉及到 eBPF 软件技术栈的所有部分(内核虚拟机和验证器、clang/LLVM 编译器、BCC 等),但这种方式可带来很大的便利,允许重复使用现有的 BCC 工具,而不需要特别的 eBPF 交叉编译和在嵌入式设备上安装 LLVM 或运行 BPFd。截至目前,CO-RE BTF 工作仍处于早期开发阶段,还需要付出相当多的工作才能可用【译者注:当前在高版本内核已经可以使用或者编译内核时启用了 BTF 编译选项】。也许我们会在其完全可用后再发表一篇博文。

3. BPFd

BPFd (项目地址 https://github.com/joelagnel/bpfd )更像是一个为 Android 设备开发的概念验证,后被放弃,转而通过 adeb 包运行一个完整的设备上的 BCC 工具链【译者注:BCC 在 adeb 的编译文档参见这里】。如果一个设备足够强大,可以运行 Android 和 Java,那么它也可能可以安装 BCC/LLVM/python。尽管这个实现有些不完整(通信是通过 Android USB 调试桥或作为一个本地进程完成的,而不是通过一个通用的传输层),但这个设计很有趣,有足够时间和资源的人可以把它拿起来合并,继续搁置的 PR 工作

简而言之,BPFd 是一个运行在嵌入式设备上的守护程序,作为本地内核/libbpf 的一个远程过程调用(RPC)接口。Python 在主机上运行,调用 BCC 来编译/部署 eBPF 字节码,并通过 BPFd 创建/读取 map。BPFd 的主要优点是,所有的 BCC 基础设施和脚本都可以工作,而不需要在目标设备上安装 BCC、LLVM 或 python,BPFd 二进制文件只有 100kb 左右的大小,并依赖 libc。

4. Ply

ply 项目实现了一种与 BPFtrace 非常相似的高级领域特定语言(受到 AWK 和 C 的启发),其明确的目的是将运行时的依赖性降到最低。它只依赖于一个现代的 libc(不一定是 GNU 的 libc)和 shell(与 sh 兼容)。Ply 本身实现了一个 eBPF 编译器,需要根据目标设备的内核头文件进行构建,然后作为一个单一的二进制库和 shell 包装器部署到目标设备上。

为了更好解释 ply,我们把第 3 部分中的 BPFtrace 例子和与 ply 实现进行对比:

  • BPFtrace:要运行该例子,你需要数百 MB 的 LLVM/clang、libelf 和其他依赖项:
bpftrace -e 'tracepoint:raw_syscalls:sys_enter {@[pid, comm] = count();}'
  • ply:你只需要一个 ~50kb 的二进制文件,它产生的结果是相同的,语法几乎相同:
ply 'tracepoint:raw_syscalls/sys_enter {@[pid, comm] = count();}'

Ply 仍在大量开发中(最近的 v2.0 版本是完全重写的)【译者注:当前最新版本为 2.1.1,最近一次代码提交是 8 个月前,活跃度一般】,除了一些示例之外,该语言还不不稳定或缺乏文档,它不如完整的 BCC 强大,也没有 BPFtrace 丰富的功能特性,但它对于通过 ssh 或串行控制台快速调试远程嵌入式设备仍然非常有用。

5. Gobpf

Gobpf 及其合并的子项目(goebpf, gobpf-elf-loader),是 IOVisor 项目的一部分,为 BCC 提供 Golang 语言绑定。eBPF 的内核逻辑仍然用 “限制性 C” 编写,并由 LLVM 编译,只有标准的 python/lua 用户空间脚本被 Go 取代。这个项目对嵌入式设备的意义在于它的 eBPF elf 加载模块,其可以被交叉编译并在嵌入式设备上独立运行,以加载 eBPF 程序至内核并与与之交互。

值得注意的是,go 加载器可以被写成通用的(我们很快就会看到),因此它可以加载和运行任何 eBPF 字节码,并在本地重新用于多个不同的跟踪会话。

使用 gobpf 很痛苦的,主要是因为缺乏文档。目前最好的 “文档” 是 tcptracer 的源代码,它相当复杂(他们使用 kprobes 而不依赖于特定的内核版本!),但从它可以学到很多。Gobpf 本身也是一项正在进行的工作:虽然 elf 加载器相当完整,并支持加载带有套接字、(k|u)probes、tracepoints、perf 事件等加载的 eBPF ELF 对象,但 bcc go 绑定模块还不容易支持所有这些功能。例如,尽管你可以写一个 socket_ilter ebpf 程序,将其编译并加载到内核中,但你仍然不能像 BCC 的 python 那样从 go 用户空间轻松地与 eBPF 进行交互,BCC 的 API 更加成熟和用户友好。无论如何,gobpf 仍然比其他具有类似目标的项目处于更好的状态。

让我们研究一个简单的例子来说明 gobpf 如何工作的。首先,我们将在本地 x86_64 机器上运行它,然后交叉编译并在 32 位 ARMv7 板上运行它,比如流行的 Beaglebone 或 Raspberry Pi。我们的文件目录结构如下:

$ find . -type f
./src/open-example.go
./src/open-example.c
./Makefile

open-example.go:这是建立在 gobpf/elf 之上的 eBPF ELF 加载器。它把编译好的 “限制性 C” ELF 对象作为参数,加载到内核并运行,直到加载器进程被杀死,这时内核会自动卸载 eBPF 逻辑【译者注:通常情况是这样的,也有场景加载器退出,ebpf 程序继续运行的】。我们有意保持加载器的简单性和通用性(它加载在对象文件中发现的任何探针),因此加载器可以被重复使用。更复杂的逻辑可以通过使用 gobpf 绑定 模块添加到这里。

package main

import (
"fmt"
"os"
"os/signal"
"github.com/iovisor/gobpf/elf"
) func main() {mod := elf.NewModule(os.Args[1]) err := mod.Load(nil);
if err != nil {fmt.Fprintf(os.Stderr, "Error loading'%s'ebpf object: %v\n", os.Args[1], err)os.Exit(1)
} err = mod.EnableKprobes(0)
if err != nil {fmt.Fprintf(os.Stderr, "Error loading kprobes: %v\n", err)
os.Exit(1)
} sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt, os.Kill)
// ...
}

open-example.c:这是上述加载器加载至内核的 “限制性 C” 源代码。它挂载在 do_sys_open 函数,并根据 ftrace format 将进程命令、PID、CPU、打开文件名和时间戳打印到跟踪环形缓冲区,(详见 “输出格式” 一节)。打开的文件名作为 do_sys_open call 的第二个参数传递,可以从代表函数入口的 CPU 寄存器的上下文结构中访问。


#include <uapi/linux/bpf.h>
#include <uapi/linux/ptrace.h>
#include <bpf/bpf_helpers.h> SEC("kprobe/do_sys_open")
int kprobe__do_sys_open(struct pt_regs *ctx)
{char file_name[256]; bpf_probe_read(file_name, sizeof(file_name), PT_REGS_PARM2(ctx)); char fmt[] = "file %s\n";
bpf_trace_printk(fmt, sizeof(fmt), &file_name); return 0;
} char _license[] SEC("license") = "GPL";
__u32 _version SEC("version") = 0xFFFFFFFE;

在上面的代码中,我们定义了特定的 “SEC” 区域,这样 gobpf 加载器就可获取到哪里查找或加载内容的信息。在我们的例子中,区域为 kprobe、license 和 version。特殊的 0xFFFFFFFE 值告诉加载器,这个 eBPF 程序与任何内核版本都是兼容的,因为打开系统调用而破坏用户空间的机会接近于 0。

Makefile:这是上述两个文件的构建逻辑。注意我们是如何在 include 路径中加入 “arch/x86/…” 的;在 ARM 上它将是 “arch/arm/…"。

SHELL=/bin/bash -o pipefail
LINUX_SRC_ROOT="/home/adi/workspace/linux"
FILENAME="open-example" ebpf-build: clean go-build
clang \
-D__KERNEL__ -fno-stack-protector -Wno-int-conversion \
-O2 -emit-llvm -c "src/${FILENAME}.c" \
-I ${LINUX_SRC_ROOT}/include \
-I ${LINUX_SRC_ROOT}/tools/testing/selftests \
-I ${LINUX_SRC_ROOT}/arch/x86/include \
-o - | llc -march=bpf -filetype=obj -o "${FILENAME}.o" go-build:
go build -o ${FILENAME} src/${FILENAME}.go clean:
rm -f ${FILENAME}*

运行上述 makefile 在当前目录下产生两个新文件:

  • open-example:这是编译后的 src/*.go 加载器。它只依赖于 libc 并且可以被复用来加载多个 eBPF ELF 文件运行多个跟踪。

  • open-example.o:这是编译后的 eBPF 字节码,将在内核中加载。

“open-example” 和 “open-example.o” ELF 二进制文件可以进一步合并成一个;加载器可以包括 eBPF 二进制文件作为资产,也可以像 tcptracer 那样在其源代码中直接存储为字节数。然而,这超出了本文的范围。

运行例子显示以下输出(见 ftrace 文档 中的 “输出格式” 部分)。

# (./open-example open-example.o &) && cat /sys/kernel/debug/tracing/trace_pipe
electron-17494 [007] ...3 163158.937350: 0: file /proc/self/maps
systemd-1 [005] ...3 163160.120796: 0: file /proc/29261/cgroup
emacs-596 [006] ...3 163163.501746: 0: file /home/adi/
(...)

沿用我们在本系列的第 3 部分中定义的术语,我们的 eBPF 程序有以下部分组成:

  • 后端:是 open-example.o ELF 对象。它将数据写入内核跟踪环形缓冲区。

  • 加载器:这是编译过的 open-example 二进制文件,包含 gobpf/elf 加载器模块。只要它运行,数据就会被添加到跟踪缓冲区中。

  • 前端:这就是 cat /sys/kernel/debug/tracing/trace_pipe。非常 UNIX 风格。

  • 数据结构:内核跟踪环形缓冲区。

现在将我们的例子交叉编译为 32 位 ARMv7。 基于你的 ARM 设备运行的内核版本:

  • 内核版本>=5.2:只需改变 makefile,就可以交叉编译与上述相同的源代码。
  • 内核版本<5.2:除了使用新的 makefile 外,还需要将 PT_REGS_PARM* 宏从 这个 patch 复制到 “受限制 C” 代码。

新的 makefile 告诉 LLVM/Clang,eBPF 字节码以 ARMv7 设备为目标,使用 32 位 eBPF 虚拟机子寄存器地址模式,以便虚拟机可以正确访问本地处理器提供的 32 位寻址内存(还记得第 2 部分中介绍的所有 eBPF 虚拟机寄存器默认为 64 位宽),设置适当的包含路径,然后指示 Go 编译器使用正确的交叉编译设置。在运行这个 makefile 之前,需要一个预先存在的交叉编译器工具链,它被指向 CC 变量。

SHELL=/bin/bash -o pipefail
LINUX_SRC_ROOT="/home/adi/workspace/linux"
FILENAME="open-example" ebpf-build: clean go-build
clang \
--target=armv7a-linux-gnueabihf \
-D__KERNEL__ -fno-stack-protector -Wno-int-conversion \
-O2 -emit-llvm -c "src/${FILENAME}.c" \
-I ${LINUX_SRC_ROOT}/include \
-I ${LINUX_SRC_ROOT}/tools/testing/selftests \
-I ${LINUX_SRC_ROOT}/arch/arm/include \
-o - | llc -march=bpf -filetype=obj -o "${FILENAME}.o" go-build:
GOOS=linux GOARCH=arm CGO_ENABLED=1 CC=arm-linux-gnueabihf-gcc \
go build -o ${FILENAME} src/${FILENAME}.go clean:
rm -f ${FILENAME}*

运行新的 makefile,并验证产生的二进制文件已经被正确地交叉编译:

[adi@iwork]$ file open-example*
open-example: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter (...), stripped
open-example.o: ELF 64-bit LSB relocatable, *unknown arch 0xf7* version 1 (SYSV), not stripped

然后将加载器和字节码复制到设备上,与在 x86_64 主机上使用上述相同的命令来运行。记住,只要修改和重新编译 C eBPF 代码,加载器就可以重复使用,用于运行不同的跟踪。

[root@ionelpi adi]# (./open-example open-example.o &) && cat /sys/kernel/debug/tracing/trace_pipe
ls-380 [001] d..2 203.410986: 0: file /etc/ld-musl-armhf.path
ls-380 [001] d..2 203.411064: 0: file /usr/lib/libcap.so.2
ls-380 [001] d..2 203.411922: 0: file /
zcat-397 [002] d..2 432.676010: 0: file /etc/ld-musl-armhf.path
zcat-397 [002] d..2 432.676237: 0: file /usr/lib/libtinfo.so.5
zcat-397 [002] d..2 432.679431: 0: file /usr/bin/zcat
gzip-397 [002] d..2 432.693428: 0: file /proc/
gzip-397 [002] d..2 432.693633: 0: file config.gz

由于加载器和字节码加起来只有 2M 大小,这是一个在嵌入式设备上运行 eBPF 的相当好的方法,而不需要完全安装 BCC/LLVM。

6. 总结

在本系列的第 4 部分,我们研究了可以用于在小型嵌入式设备上运行 eBPF 程序的相关项目。不幸的是,当前使用这些项目还是比较很困难的:它们有的被遗弃或缺乏人力,在早期开发时一切都在变化,或缺乏基本的文档,需要用户深入到源代码中并自己想办法解决。正如我们所看到的,gobpf 项目作为 BCC/python 的替代品是最有活力的,而 ply 也是一个有前途的 BPFtrace 替代品,其占用空间最小。随着更多的工作投入到这些项目中以降低使用者的门槛,eBPF 的强大功能可以用于资源受限的嵌入式设备,而无需移植/安装整个 BCC/LLVM/python/Hover 技术栈。

eBPF 概述:第 4 部分:在嵌入式系统运行的更多相关文章

  1. 《嵌入式系统原理与接口技术》——嵌入式系统接口应用基础

    本文为我负责编写的电子工业出版社出版的<嵌入式系统原理与接口技术>一书第七章部分,这里整理的仍然是修改稿,供需要的同学参考,本书为普通高等教育"十二五"规划教材,电子信 ...

  2. 【swupdate文档 三】SWUpdate: 嵌入式系统的软件升级

    SWUpdate: 嵌入式系统的软件升级 概述 本项目被认为有助于从存储媒体或网络更新嵌入式系统.但是,它应该主要作为一个框架来考虑,在这个框架中可以方便地向应用程序添加更多的协议或安装程序(在SWU ...

  3. C、C++、boost、Qt在嵌入式系统开发中的使用

    概述 嵌入式系统开发相对来说属于偏底层的开发,也就是与硬件结合比较紧密,只能使用C/C++语言.对于做平台开发的人来说,C语言真的是很"古老"的语言,属于操作系统语言!好多人会觉得 ...

  4. 嵌入式Linux驱动学习之路(一)嵌入式系统的软硬件架构

    硬件资料: 操作系统:(非虚拟机) zws@z-pc:~$ lsb_release -aNo LSB modules are available.Distributor ID: Ubuntu Desc ...

  5. 热烈庆祝华清远见2014嵌入式系统(Linux&Android)开发就业培训课程全面升级

    近日,华清远见公开宣布:2014嵌入式系统 (Linux&Android)开发就业培训课程再次升级!据悉,华清远见如今已经持续10年,一直保持课程每年2次的更新的频率.华清远见的每 次课程更新 ...

  6. 嵌入式系统coredump设计

    阴沟翻船,马失前蹄,说明凡事皆有可能.自然,程序设计的再好,也会有crash的时候.开发期还还说,正式交付的系统crash自然更是难以承受的.无论何时,死一次就够了,得有方法查个水落石出. 几年前哥去 ...

  7. 嵌入式系统Linux内核开发工程师必须掌握的三十道题(转)

    嵌入式系统Linux内核开发工程师必须掌握的三十道题 如果你能正确回答以下问题并理解相关知识点原理,那么你就可以算得上是基本合格的Linux内核开发工程师,试试看! 1) Linux中主要有哪几种内核 ...

  8. 基于s5pv210嵌入式系统busybox文件系统移植

    基于s5pv210嵌入式系统busybox文件系统移植 1.下载源码 busybox.net/downloads下载最新版的busybox源码,最新源码为1.21.1 2.解压源码文件 tar xvf ...

  9. Linux嵌入式系统与硬件平台的关系

    一. Linux嵌入式系统 操作系统是一种在计算机上运行的软件,它的主要任务是管理计算机上的系统资源,为用户提供使用计算机及其外部设备的接口.它存在的目的是为了管理所有硬件资源,并且提供应用软件一个合 ...

  10. 嵌入式系统图形库GUI核心模块介绍

    本文转载自:http://blog.csdn.net/xteda/article/details/6575278 (作者 冯青华 信庭嵌入式工作室(www.xteda.com)- CEO Blog:h ...

随机推荐

  1. Python 实现行为驱动开发 (BDD) 自动化测试详解

    ​ 在当今的软件开发领域,行为驱动开发(Behavior Driven Development,BDD)作为一种新兴的测试方法,逐渐受到越来越多开发者的关注和青睐.Python作为一门功能强大且易于使 ...

  2. 关于MultipartFile

    首先,他来自spring框架,用于处理文件上传的问题 一般来讲,这个接口主要是实现以表单形式上传文件的功能 常用方法: getOriginalFileName:获取文件名+拓展名 getContent ...

  3. Scratch源码下载 | 3D钻石

    程序说明: <3D钻石>是一个利用Scratch平台创作的独特艺术作品.此程序在屏幕上呈现一个精致的3D钻石模型,允许用户通过鼠标操作来旋转和查看钻石的不同角度.该程序还提供了修改钻石参数 ...

  4. 《最新出炉》系列入门篇-Python+Playwright自动化测试-57- 上传文件 - 番外篇

    1.简介 前边的三篇文章基本上对文件上传的知识介绍和讲解的差不多了,今天主要是来分享宏哥在文件上传的实际操作中发现的一个问题:input控件和非input控件的上传API对其都可以上传成功.废话不多说 ...

  5. 【WSDL】01 JAX-WS 入门案例

    去年这个时候工作遇见时暂时总结的笔记: https://www.cnblogs.com/mindzone/p/14777493.html 当时也不是很清楚,直到最近前同事又遇上了这项技术, 除了WSD ...

  6. 【RabbitMQ】12 日志监控 & 消息追踪

    一.日志和监控 RabbitMQ日志存放目录 [root@localhost ~]# ll /var/log/rabbitmq/ 总用量 176 -rw-r-----. 1 rabbitmq rabb ...

  7. 使用 abortNavigation 阻止导航

    title: 使用 abortNavigation 阻止导航 date: 2024/8/3 updated: 2024/8/3 author: cmdragon excerpt: 摘要:在Nuxt3中 ...

  8. 绝对要收藏!!! JavaEE开发常用注解

    目录 前言 1.Mybatis常用注解 2.SpringMVC常用注解 3.Spring常用注解 1. IoC注解 2. DI注解 3. 事务注解 4.SpringBoot常用注解 5.Lombok注 ...

  9. linux终端alacritty导致计算机死机的解决方式——Ubuntu18.04系统Nvidia显卡

    如题所述,近日在自己Ubuntu18.04的系统上安装了alacritty终端,安装这个终端主要原因就是可以出现透明桌面,说白了就是漂亮,beautiful,但是这个终端安装后系统就变得极不稳定,经常 ...

  10. CPU端多进程/多线程调用CUDA是否可以加速???

    相关: NVIDIA显卡cuda的多进程服务--MPS(Multi-Process Service) tensorflow1.x--如何在C++多线程中调用同一个session会话 tensorflo ...