BPF 进阶笔记(四):调试 BPF 程序
2022-5-2 08:0:0 Author: arthurchiao.github.io(查看原文) 阅读量:77 收藏

Published at 2022-05-02 | Last Update 2022-05-02

本文是阅读一些 BPF 高级教程时所作的笔记。

关于 “BPF 进阶笔记” 系列

平时学习和使用 BPF 时所整理。由于是笔记而非教程,因此内容不会追求连贯,有基础的 同学可作查漏补缺之用。

文中涉及的代码,如无特殊说明,均基于内核 5.10 版本。



1.1 日志路径及格式

本节将介绍的几种打印日志方式最终都会输出到 debugfs 路径 /sys/kernel/debug/tracing/trace

$ sudo tail /sys/kernel/debug/tracing/trace
# 字段说明  <taskname>-<pid>    <cpuid>  <opts>  <timestamp>    <fake by bpf>  <log content>
            telnet-470          [001]     .N..   419421.045894: 0x00000001:    <formatted msg>

以上看到的是默认 trace 输出格式,

  1. 可通过 /sys/kernel/debug/tracing/trace_options 定制化 trace 输出格式(打印哪些列);
  2. 另外还可参考 /sys/kernel/debug/tracing/README,其中有更详细的说明。

字段说明

  1. telnet:进程名;
  2. 470:进程 ID;
  3. 001:进程所在的 CPU;
  4. .N..:每个字符表示一组配置选项,依次为,

    • 是否启用了中断(irqs);
    • 调度选项,这里 N 表示设置了 TIF_NEED_RESCHEDPREEMPT_NEED_RESCHED 标志位;
    • 硬中断/软中断是否正在运行;
    • level of preempt_disabled
  5. 419421.045894:时间戳;
  6. 0x00000001:BPF 使用的一个 fake value,for instruction pointer register;
  7. <formatted msg>:日志内容。

1.2 bpf_printk():kernel 5.2+

使用方式

这是内核 libbpf 库提供的一个宏:

// https://github.com/torvalds/linux/blob/v5.10/tools/lib/bpf/bpf_helpers.h#L17

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

使用非常方便,和 C 的 printf() 差不多,例如,

    bpf_printk("tcp_v4_connect latency_us: %u", latency_us);

使用限制

  1. 需要内核 5.2+,否则编译能通过,但执行时会报错:

     map .rodata: map create: read- and write-only maps not supported (requires >= v5.2)
    

    这个错误提示非常奇怪(实际上目前来说,大部分 BPF 错误提示都不那么直接)。

    简单来说,BPF 的栈空间非常小,每次调用 bpf_printk() 都会动态声明一个 char ____fmt[] = fmt; 并放到栈上,导致性能很差。 5.2 引入了 BPF global (and static) 变量,因此 clang 在编译时 可以直接将这些变量放到 ELF 的只读区域(.rodata,read-only data),libbpf 加载程序时将这些数据放到一个 .rodata BPF map 中,程序在用到这些变量时,背后执行一次 map lookup 即可。 相比于每次都在栈上创建一个字符数组(字符串),这样更加快速和高效。

    更多内容,见 Andrii Nakryiko 的博客 Improving bpf_printk()

  2. 最多只能带 3 个参数,即 bpf_printk(fmt, arg1, arg2, arg3)

    这是由 bpf_trace_printk() 的限制决定的,下一节有具体解释。

内核实现

前面已经看到 bpf_printk() 非常简单,只是单纯封装了一下 bpf_trace_printk(), 后者定义在 include/uapi/linux/bpf.h,具体实现见下文。

1.3 bpf_trace_printk()

对于 5.2 以下的内核,打印日志可以用 bpf_trace_printk(),它比 bpf_printk() 要麻烦一点:要提前声明格式字符串 fmt

使用方式

//  https://github.com/torvalds/linux/blob/v5.10/include/uapi/linux/bpf.h#L772

/**
 * long bpf_trace_printk(const char *fmt, u32 fmt_size, ...)
 */
  1. 功能与 printk() 类似,按指定格式将日志打印到 /sys/kernel/debug/tracing/trace 中; 但支持的格式比 printk() 少

    • 5.10 支持 %d, %i, %u, %x, %ld, %li, %lu, %lx, %lld, %lli, %llu, %llx, %p, %s。不支持指定字符串或数字长度等,否则会返回 -EINVAL(同时什么都不打印)。
    • 5.13 有进一步增强,见 Detecting full-powered bpf_trace_printk()
  2. 每次调用这个函数时,会往 trace 中追加一行;当 /sys/kernel/debug/tracing/trace is open,日志会被丢弃, 可使用 /sys/kernel/debug/tracing/trace_pipe 来避免这种情况;
  3. 这个函数执行很慢,因此只应在调试时使用
  4. fmt 格式串是否有默认换行

函数的返回值是写到 buffer 的字节数,出错时返回负的 error code。

例子:

    char fmt[] = "tcp_v4_connect latency_us: %u";
    bpf_printk(fmt, sizeof(fmt), latency_us);

使用限制

  1. 最多只能带 3 个参数(这是因为 eBPF helpers 最多只能带 5 个参数,前面 fmtfmt_size 已经占了两个了);
  2. 使用该函数的代码必须是 GPL 兼容的
  3. 前面已经提到,格式字符串支持的类型有限,但 5.13 有进一步改进,详见 Detecting full-powered bpf_trace_printk()

内核实现

实现:

// https://github.com/torvalds/linux/blob/v5.10/kernel/trace/bpf_trace.c#L428

BPF_CALL_5(bpf_trace_printk, char *, fmt, u32, fmt_size, u64, arg1, u64, arg2, u64, arg3)
{
    ...
}

其中 BPF_CALL_5 的定义:

// https://github.com/torvalds/linux/blob/v5.10/include/linux/filter.h#L485

#define BPF_CALL_x(x, name, ...)                           \
    static __always_inline u64 ____##name(__BPF_MAP(x, __BPF_DECL_ARGS, __BPF_V, __VA_ARGS__));   \
    typedef u64                (*btf_##name)(__BPF_MAP(x, __BPF_DECL_ARGS, __BPF_V, __VA_ARGS__)); \
    u64                        name(__BPF_REG(x, __BPF_DECL_REGS, __BPF_N, __VA_ARGS__));           \
    u64                        name(__BPF_REG(x, __BPF_DECL_REGS, __BPF_N, __VA_ARGS__)) {       \
        return ((btf_##name)____##name)(__BPF_MAP(x,__BPF_CAST,__BPF_N,__VA_ARGS__));\
    }                                       \
    static __always_inline u64 ____##name(__BPF_MAP(x, __BPF_DECL_ARGS, __BPF_V, __VA_ARGS__))

#define BPF_CALL_5(name, ...)    BPF_CALL_x(5, name, __VA_ARGS__)

2.1 使用场景

BPF trampoline 可以 作为内核函数之间、BPF 程序和其他 BPF 程序之间的桥梁。使用场景之一就是 tracing 其他 BPF 程序。这个功能来自 XDP 开发过程中的痛点。 现在能向任何网络类型的 BPF 程序 attach 类似 fentry/fexit 的 BPF 程序,因 此能够看到 XDP、TC、LWT、cgroup 等任何类型 BPF 程序中包的进进出出,而不会影 响到这些程序的执行,大大降低了基于 BPF 的网络排障难度。

一些 patch,如果感兴趣:

BPF trampoline 其他使用场景:

  1. fentry/fexit BPF 程序:功能与 kprobe/kretprobe 类似,但性能更好,几乎没有性能开销(practically zero overhead);
  2. 动态链接 BPF 程序(dynamicly link BPF programs)。

    在 tracing、networking、cgroup BPF 程序中,中,是比 prog array 和 prog link list 更加通用的机制。 在很多情况下,可直接作为基于 bpf_tail_call 程序链的一种替代方案。

这些特性都需要 root 权限。

2.2 依赖:kernel 5.5+

3.1 bpf_dbg(仅限 cBPF)

(译) Linux Socket Filtering (LSF, aka BPF)(Kernel,2021)


文章来源: https://arthurchiao.github.io/blog/bpf-advanced-notes-4-zh/
如有侵权请联系:admin#unsafe.sh