原文:Andrii Nakryiko’s Blog --BPF tips & tricks: the guide to bpf_trace_printk() and bpf_printk()

任何BPF 程序总是需要一些调试才能使其正常工作。 不幸的是,目前还没有 BPF 调试器,所以下一个最好的办法是在周围撒上类似 printf() 的语句,看看 BPF 程序中发生了什么。 printf() 的 BPF 等价物是 bpf_trace_printk() 。 在这篇博文中,我们将了解如何使用它、它的局限性是什么以及如何解决这些局限性。 我们还将描述最近几个内核版本中 bpf_trace_printk() 发生的一些重要变化,以及如何使用 BPF CO-RE 来检测和处理这些变化。

预备知识

我将使用 libbpf-bootstrap 的minimal示例作为所有示例的基础。 它连接了所有东西并触发了一个简单的 BPF 程序,我们将用它来测试 bpf_trace_printk() 输出。 如果您想继续,请确保克隆 libbpf-bootstrap 并在您的编辑器中打开 minimap.bpf.c

$ # note --recursive to checkout libbpf submodule
$ git clone --recursive https://github.com/libbpf/libbpf-bootstrap
$ cd libbpf-bootstrap/examples/c
$ vim minimal.bpf.c
$ make minimal
$ sudo ./minimal

bpf_trace_printk() 介绍

Linux 内核提供了 BPF helper函数 bpf_trace_printk(),其定义如下:

long bpf_trace_printk(const char *fmt, __u32 fmt_size, ...);

它的第一个参数 fmt 是一个指向 printf 兼容格式字符串的指针(具有一些特定于内核的扩展和限制)。 fmt_size 是该字符串的大小,包括终止 \0varargs 是从格式字符串引用的参数。
bpf_trace_printk() 支持 libc 的 printf() 实现的有限子集。 诸如 %s%d%c 之类的基本内容有效,但是,比如说,位置参数 (%1$s) 无效。 参数宽度说明符(%10d%-20s 等)仅适用于最近的内核,但不适用于较早的内核。 此外,还支持一堆特定于内核的修饰符(如 %pi6 打印出 IPv6 地址或 %pks 内核字符串)。
如果格式字符串无效或使用不受支持的功能,bpf_trace_printk() 将返回负错误代码。
不幸的是,对 bpf_trace_printk() 的使用有一些更重要的限制。
首先,使用 bpf_trace_printk() 的 BPF 程序必须具有 GPL 兼容的许可证。 对于基于 libbpf 的 BPF 应用程序,这意味着使用特殊变量指定许可证:

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

为完整起见,以下是内核识别的所有GPL兼容许可证:

  • “GPL”;
  • “GPL v2”;
  • “GPL and additional rights”;
  • “Dual BSD/GPL”;
  • “Dual MIT/GPL”;
  • “Dual MPL/GPL”.

另一个硬性限制是 bpf_trace_printk() 最多只能接受 3 个输入参数(除了 fmtfmt_size)。 这通常是非常有限的,可能需要使用多个 bpf_trace_printk() 调用来记录所有数据。 这个限制源于 BPF helper总共只能接受最多 5 个输入参数的能力。
但是,一旦克服了这些限制, bpf_trace_printk() 会根据格式字符串尽职尽责地将数据发送到位于 /sys/kernel/debug/tracing/trace_pipe 的特殊文件中。 该文件需要以 root 身份阅读,因此请使用 sudo cat 查看调试日志:

$ sudo cat  /sys/kernel/debug/tracing/trace_pipe<...>-2328034 [007] d... 5344927.816042: bpf_trace_printk: Hello, world, from BPF! My PID is 2328034<...>-2328034 [007] d... 5344928.816147: bpf_trace_printk: Hello, world, from BPF! My PID is 2328034^C

让我们来剖析一下。 <...>-2328034 [007] d... 5344927.816042:bpf_trace_printk: 内核为每次 bpf_trace_printk() 调用自动发出部分。 它包含进程名称(有时缩写为 <...>)、PID (2328034)、系统启动后的时间戳 (5344927.816042) 等信息。但是Hello, world, from BPF! My PID is 2328034是由 BPF 程序控制的部分,并通过这样的简单代码发出:

int pid = bpf_get_current_pid_tgid() >> 32;
const char fmt_str[] = "Hello, world, from BPF! My PID is %d\n";bpf_trace_printk(fmt_str, sizeof(fmt_str), pid);

请注意 fmt_str 如何定义为堆栈上的变量。 不幸的是,由于 libbpf 的限制目前你不能做 bpf_trace_printk("Hello, world!", ...); 之类的事情。 但即使有可能,需要显式指定 fmt_size 也很不方便。 不过,Libbpf 提供了一个简单的包装宏 bpf_printk(fmt, ...),它负责处理这些细节。 它目前在 <bpf/bpf_helpers.h> 中定义如下:

/* Helper macro to print out debug messages */
#define bpf_printk(fmt, ...)                            \
({                                                      \char ____fmt[] = fmt;                           \bpf_trace_printk(____fmt, sizeof(____fmt),      \##__VA_ARGS__);                \
})

有了它,上面的"Hello, world!" 示例变得更加简洁和方便:

int pid = bpf_get_current_pid_tgid() >> 32;bpf_printk("Hello, world, from BPF! My PID is %d\n", pid);

好多了! 不幸的是,虽然方便,但这种实现并不理想,因为它必须在每次调用 bpf_printk() 时使用格式字符串的内容初始化堆栈上的 char 数组。 由于向后兼容性问题,libbpf 被困在这种次优实现上,因为它是唯一可以在旧内核上可靠地工作的实现,因此不会破坏任何 BPF 应用程序,这是 libbpf 作为通用库的高优先级。

另一方面,我在这篇博文中不受向后兼容性的限制。 因此,我可以并且将在本文的其余部分展示如何显着改进此实现。

换行行为改变

在 Linux 5.9 之前,bpf_trace_printk() 将采用格式字符串并按原样使用它。 因此,如果忘记(或选择不)将 \n 添加到格式字符串中,trace_pipe 输出会变得一团糟。 bpf_printk("Hello, world!") 执行几次将导致:

   <...>-179528 [065] .... 1863682.484368: 0: Hello, world!           <...>-179528 [065] .... 1863682.484381: 0: Hello, world!           <...>-179528 [065] .... 1863683.484447: 0: Hello, world!

从 ac5a72ea5c89 (“bpf: Use dedicated bpf_trace_printk event instead of trace_printk()”)开始(进入上游 Linux 5.9),bpf_trace_printk() 现在将始终在末尾附加换行符,因此对于 bpf_printk(“Hello, world!”); 你会看到一个整洁的输出:

   <...>-200501 [001] .... 1863840.478848: 0: Hello, world!<...>-200501 [002] .... 1863841.478916: 0: Hello, world!<...>-200501 [002] .... 1863842.478991: 0: Hello, world!

这很好,但是如果你之前小心(应该这样做)地在格式字符串的末尾添加 \n,那么在 Linux 5.9 之前的内核上使用 bpf_printk("Hello, world!\n") 会产生不错的输出像上面一样。 但是从 Linux 5.9 开始,你会得到一个令人讨厌的稀疏浪费的输出:

<...>-3658431 [048] d... 5362570.510814: bpf_trace_printk: Hello, world!<...>-3658431 [048] d... 5362571.510933: bpf_trace_printk: Hello, world!<...>-3658431 [048] d... 5362572.511048: bpf_trace_printk: Hello, world!

虽然不是世界末日,但在处理讨厌的 \n 时保持一致的行为并且不关心内核版本差异会很棒,不是吗?

好消息是,在 BPF CO-RE 的帮助下,我们可以透明地检测和适应这些内核差异。 如果您查看上面提到的提交 ac5a72ea5c89,您会看到它添加了一个新的内核跟踪点 bpf_trace_printk,并巧妙地使用它向 /sys/kernel/debug/tracing/trace_pipe 发送数据。 还要注意内核中的每个跟踪点都有一个对应的 struct trace_event_raw_<tracepointname> 类型。 我们将使用 struct trace_event_raw_bpf_trace_printk 的存在来检测 bpf_trace_printk() 是否添加了换行符。 如果没有,我们将确保在我们自己的 bpf_printk() 宏中默默地和透明地添加一个换行符。 让我们看看所有这些是如何组合在一起的:

[1] #include <bpf/bpf_core_read.h>/* define our own struct definition if our vmlinux.h is outdated */
[2] struct trace_event_raw_bpf_trace_printk___x {};#undef bpf_printk#define bpf_printk(fmt, ...)                                                    \({                                                                              \
[3]         static char ____fmt[] = fmt "\0";                                       \
[4]         if (bpf_core_type_exists(struct trace_event_raw_bpf_trace_printk___x)) {\
[5]                 bpf_trace_printk(____fmt, sizeof(____fmt) - 1, ##__VA_ARGS__);  \} else {                                                                \
[6]                 ____fmt[sizeof(____fmt) - 2] = '\n';                            \
[7]                 bpf_trace_printk(____fmt, sizeof(____fmt), ##__VA_ARGS__);      \}                                                                       \})

让我们把它分解一下。

[1] 包括 libbpf 的 bpf_core_read.h 头文件,它定义了所有 BPF CO-RE 宏。
[2] 定义了我们自己的 bpf_trace_printk 跟踪点结构的本地最小(空)定义,以避免依赖最新的 vmlinux.h。 这对于在 Linux 5.9 之前从内核 BTF 生成的 vmlinux.h 标头可能稍微过时的情况很重要。 添加 ___x 后缀确保它不会与最新的 vmlinux.h 中的定义冲突。 Libbpf 和 BPF CO-RE 将忽略 ___ 及其后的所有内容,因此这仍将与内核中的实际结构 trace_event_raw_bpf_trace_printk 匹配。 如果你确定你的 vmlinux.h 足够新,你可以跳过这一步。
[3] 有两个变化。 我们删除了 const 修饰符,因为我们将在运行时(在较旧的内核上)修改这个字符串,因此必须在可写的 .data ELF部分和相应的 BPF 映射中分配它。 我们还在末尾附加了额外的 \0 来为 \n 保留一个空格,如果我们碰巧需要的话。 替换现有字符比在运行时添加一个简单得多,所以这就是我们在这里要做的。
[4] 是基于 BPF CO-RE 的跟踪点存在检测。 如果内核中存在指定的结构体 bpf_core_type_exists() 计算结果为 1,否则替换为 0。
[5]是Linux 5.9+的情况,所以我们不需要添加换行符。 我们唯一应该注意的是不要在格式字符串中传递两个 \0,因为某些内核会在运行时拒绝它(并且您不会在 trace_pipe 文件中看到任何输出)。 这就是为什么将 sizeof(____fmt) - 1 指定为格式字符串的大小,跳过编译器在分配字符串时添加的隐式 '\0'
[6]-[7] 是较旧的 Linux 的情况,因此我们必须将显式保留的 \0 替换为 \n 以确保我们将获得正确包装的输出。 我们将完整的 ____fmt 大小传递给 bpf_trace_printk(),包括隐含的 \0
有了这个, bpf_printk("Hello, world!") 总是会在最后发出一个换行符,调用者不必关心内核版本。 只需要确保始终传递没有显式“\n”的格式字符串。

检测全功率的 bpf_trace_printk()

在(即将发布的)Linux 5.13 版本中,由于 Florent Revest 在 d9c9e4db186a (“bpf: Factorize bpf_trace_printk and bpf_seq_printf”).
中的工作,bpf_trace_printk() 实现的功能得到了非常好的提升。

以前,bpf_trace_printk() 只允许使用一个字符串 (%s) 参数,这是非常有限的。 Linux 5.13 版本解除了这个限制并允许多个字符串参数,只要总格式化输出不超过 512 字节。 另一个恼人的限制是缺乏对宽度说明符的支持,例如 %10d%-20s。 这个限制现在也没有了。 以下是其他重大改进的列表(来自上述提交的描述):

  • bpf_trace_printk 总是期望 fmt[fmt_size] 是终止的 NULL 字符,这不再是真的,第一个 0 是终止。
  • bpf_trace_printk 现在支持 %% (产生百分比字符)。
  • bpf_trace_printk 现在跳过宽度格式字段。
  • bpf_trace_printk 现在支持 X 修饰符(大写十六进制)。
  • bpf_trace_printk 现在支持 %pK、%px、%pB、%pi4、%pI4、%pi6 和 %pI6
  • bpf_trace_printk 现在支持 %ps 和 %pS 说明符来打印符号。

这意味着在最近的内核上,可以使用 bpf_trace_printk() 做更多的事情。 但是,如果想支持较旧的内核,则需要回退到更简单的逻辑。 问题是是否有可能可靠地检测是否可以预期更强大的 bpf_trace_printk() 行为。
BPF CO-RE 和 libbpf 实际上可以很好地帮助解决这个问题。 一种方法是使用 extern int LINUX_KERNEL_VERSION __kconfig; 变量显式检查上游 Linux 版本,但在 Linux 内核中存在向后移植的情况下,这不是很可靠。 对于此类向后移植的功能,Linux 内核版本与内核中包含的功能不对应。 因此,如果可能,最好直接检测所需的功能支持。
bpf_trace_printk()重构恰好与添加一个新的bpf helper 函数bpf_snprintf()相吻合,这些重构和改进是首先完成的。因此,我们不再依赖于内核版本检查,而是要检测对bpf_snprintf()helper函数的支持。
每个 BPF helper 在 enum bpf_func_id 中都有一个对应的 BPF_FUNC_<helpername> 枚举值。 因此,通过检查 vmlinux BTF 中是否存在给定的枚举值,可以确定相应的 BPF helper函数是否存在。 让我们看看如何在代码中做到这一点:

/* don't rely on up-to-date vmlinux.h */
[1] enum bpf_func_id___x { BPF_FUNC_snprintf___x = 42 /* avoid zero */ };[2] #define printk_is_powerful  \(bpf_core_enum_value_exists(enum bpf_func_id___x, BPF_FUNC_snprintf___x))...const char power[] = "POWER";int pid = bpf_get_current_pid_tgid() >> 32;if (printk_is_powerful)
[4]                 bpf_printk("I've got the =%%= %7s, %s, %-7s =%%=!", power, power, power);else
[5]                 bpf_printk("Sorry, NO %s! :( But my PID is %d", power, pid);

[1] 定义了我们自己的枚举 bpf_func_idBPF_FUNC_snprintf 枚举值的最小定义。 同样,这是为了避免依赖于最新的 vmlinux.h,所以如果这与您无关,请随意跳过它。 注意在枚举和枚举值上都使用了 ___x 后缀,在这两种情况下 ___x 后缀都将被 libbpf 忽略。 实际值 42 也无关紧要,但最好避免使用零(默认值,除非明确指定),因为某些旧版本的 Clang 存在问题。
[2] 使用 bpf_core_enum_value_exists() 来检测正在运行的内核中是否存在 BPF_FUNC_snprintf 枚举值。 除了适用于枚举之外,它类似于以前使用的 bpf_core_type_exists()。 如果枚举值存在,它将为 1,否则将返回 0。
[4] 处理了功能更丰富的 bpf_trace_printk() 实现的情况,并展示了使用 3 个字符串参数和一些更漂亮的格式。 此外,只是为了好玩,它使用 %% 转义。
[5] 是使用更原始和受限格式的后备案例。

就这样。 如果在 Linux 5.13+ 上运行,应该看到:

minimal-2167    [002] d..5 20804.858999: bpf_trace_printk: I've got the =%=   POWER, POWER, POWER   =%=!minimal-2167    [002] d..5 20805.859180: bpf_trace_printk: I've got the =%=   POWER, POWER, POWER   =%=!

总结

bpf_trace_printk()(或者更确切地说,实际上是 bpf_printk() 包装器)是一种非常有用的工具,可以极大地简化 BPF 应用程序的调试。 它允许你从 BPF 应用程序的 BPF 端转储大量有用的信息,并通过 trace_pipe 文件观察它。 不幸的是,bpf_trace_printk() 的行为和功能的逐渐变化会带来不便,但希望这篇博文展示了如何通过谨慎使用 BPF CO-RE 和其他 libbpf 功能来合理地、足够透明地抽象出来( 例如,BPF 静态变量)。 希望这些信息可以在将来为您节省一些时间,并让您从 BPF 应用程序中获得更多收益。
bpf_trace_printk() 演化的逻辑延续是支持传入 3 个以上的输入参数,类似于现代的 printf() 之类的 BPF helper, 例如bpf_seq_printf()bpf_snprintf() 来做到这一点。 毫无疑问,这将很快被添加,所以请留意 bpf@vger.kernel.org 邮件列表。

(译)BPF技巧和窍门:bpf_trace_printk() 和 bpf_printk() 指南相关推荐

  1. Visual Studio 2005 IDE 技巧和窍门

    发布日期: 2007-02-26 | 更新日期: 2007-02-26 James Lau Microsoft 项目经理 适用于: Microsoft Visual Studio 2005 摘要:Vi ...

  2. 在Kaggle上赢得大数据竞赛的技巧和窍门

    在Kaggle上赢得大数据竞赛的技巧和窍门 解决方案 平台 数据 应用 方法 阅读1906  原文:The tips and tricks I used to succeed on Kaggle  作 ...

  3. vue双击事件_我总结了12个Vue.js开发技巧和窍门

    我真的很喜欢使用Vue.js,每次使用框架时,我都会喜欢深入研究其功能和特性.通过这篇文章,我向你介绍了12个很酷的提示和技巧,你可能尚未意识到这些技巧和窍门,以帮助你成为更好的Vue开发人员. 更漂 ...

  4. selenium编写脚本_Selenium脚本编写技巧和窍门

    selenium编写脚本 如果您刚刚开始学习Selenium,则以下技巧和窍门将成为您的救星. 这些技巧和窍门具有您可能会忘记的所有基本知识,将帮助您记住所有这些. 您只需浏览一次,几秒钟后您便会了解 ...

  5. jsoup爬虫教程技巧_Jsoup V的幕后秘密:优化的技巧和窍门

    jsoup爬虫教程技巧 我们已经把事情做好了,现在是时候加快工作速度了. 我们会牢记Donald Knuth的警告:"大约97%的时间我们应该忘记效率低下:过早的优化是万恶之源". ...

  6. Selenium脚本编写技巧和窍门

    如果您刚刚开始学习硒,则以下技巧和窍门将成为您的救星. 这些技巧和窍门具有您可能会忘记的所有基本知识,将帮助您记住所有这些. 您只需浏览一下它们,几秒钟后您就会了解所有内容. 让我们一一看一下所有的技 ...

  7. Jsoup V的幕后秘密:优化的技巧和窍门

    我们做对了,现在是时候更快地做事了. 我们会牢记Donald Knuth的警告:"大约97%的时间,我们应该忘记效率低下:过早的优化是万恶之源". 根据Jonathan Hedle ...

  8. Jupyter Notebook的15个技巧和窍门,可简化您的编码体验

    Jupyter Notebook is a browser bases REPL (read eval print loop) built on IPython and other open-sour ...

  9. kaggle比赛数据_表格数据二进制分类:来自5个Kaggle比赛的所有技巧和窍门

    kaggle比赛数据 This article was originally written by Shahul ES and posted on the Neptune blog. 本文最初由 Sh ...

  10. mcu比较器技巧和诀窍_如何准备技术面试-技巧和窍门,以帮助您表现最好

    mcu比较器技巧和诀窍 Ah, the coding interview. 嗯,编码面试. "Dread it. Run from it. Destiny arrives all the s ...

最新文章

  1. 显卡不够时,如何训练大型网络
  2. 微信快速开发框架(六)-- 微信快速开发框架(WXPP QuickFramework)V2.0版本上线--源码已更新至github...
  3. Oracle创建数据库(手动)
  4. Caffe 深度学习框架介绍
  5. 如何用纯 CSS 创作一个精彩的彩虹 loading 特效
  6. boost::posix_time模块打印当天的剩余小时数的测试程序
  7. shell 查看磁盘和当前文件夹所有大小
  8. YbtOJ#791-子集最值【三维偏序】
  9. 如何用c语言编写工程文件夹,利用makefile实现c语言项目编译
  10. mybatis 学习一 建立maven项目
  11. [汇编与C语言关系]1.函数调用
  12. VS2022支持.net4.0和.net4.5SDK
  13. Centos 7.x 安装配置tomcat-8过程梳理
  14. zookeeper会话超时
  15. Macbook pro新手入门
  16. 凯恩帝绝对坐标清零_凯恩帝系统加工件数自动清零怎么设置
  17. 为什么我从 Google 辞职而为自己工作
  18. 2015上半年教师资格考试高中数学(404)- 用向量数量积推导两角差余弦公式
  19. 一文带你了解Room数据库
  20. python矩阵点乘和叉乘_NumPy点积:取向量积的乘积(而不是求和)

热门文章

  1. ie不能加载flash html,IE浏览器无法显示Flash怎么解决?解决的方法介绍
  2. 【UVM基础】+uvm_set_verbosity 使用介绍
  3. 设置小程序video标签宽高比例为9/16
  4. 他是清华大学唯一没学历教授,侵华日军都下令保护的大师级人物
  5. Dell重装系统之官方原版系统
  6. CVPR 2021 论文和开源项目合集
  7. SEO与SEM有什么区别?
  8. IT项目管理之第9章 项目沟通管理习题之案例分析汇总
  9. 一起学爬虫(Python) — 07
  10. 免费真实增加网站访问量的方法