Please enable Javascript to view the contents

经典 libbpf 范例: bootstrap 分析 - eBPF基础知识 Part3

 ·  ☕ 5 分钟

经典 libbpf 范例: bootstrap 分析 - eBPF基础知识 Part3

《eBPF基础知识》 系列简介:

《eBPF基础知识》系列目标是整理一下 BPF 相关的基础知识。主要聚焦程序与内核互动接口部分。文章使用了 libbpf,但如果你不直接使用 libbpf,看本系列还是有一定意义的,因为它聚焦于程序与内核互动接口部分,而非 libbpf 封装本身。而所有 bpf 开发框架,都要以相似的方式跟内核互动。甚至框架本身就是基于 libbpf。哪怕是 golang/rust/python/BCC/bpftrace。

  1. 《ELF 格式简述 - eBPF基础知识 Part1》
  2. 《BPF 系统接口 与 libbpf 示例分析 - eBPF基础知识 Part2》

上期 《BPF 系统接口 与 libbpf 示例分析 - eBPF基础知识 Part2》 介绍了一个最简的 BPF 程序如何与内核互动。

这期,将图解分析一个更为现实的实用的 BPF 程序与内核的互动过程。国际习惯:尽量多图少文字。以下假设读者已经对 BPF 有一定的了解,或者阅读过之前的 《eBPF基础知识》系列文章。

libbpf 提供了一个使用 libbpf 的示例:https://github.com/libbpf/libbpf-bootstrap。其中的 bootstrap 程序示范了一个最简单但现实实用的 BPF 程序加载、运行、与内核互动的过程。下面将图解分析这个程序与内核的互动过程。

动机:为何我想学习 BPF

开始分析前,我想说几句废话:为何我想学习 BPF?

因为是热点啊 :) 。看,当年的 Block-Chain、AI、CloudNative。如今的 ChatGPT。我承认,35 岁前的我真会这样考虑问题。而且,如果让我带着现在的认知回到 35 岁前的身体上,说不定心底也会是这个答案。

但现在,我更想运用 BPF 来有个更深远的底层技术观察力:

  1. 加速内核知识学习

    Linux 内核现在已经是个很复杂的怪兽。很难简单直接阅读源码去理解其中的设计思想了。但只看书的话,你有会一种“书上得来终觉淺”的感觉。BPF trace 内核,keep your hands dirty。是比较好的中间方法。可以保持学习的兴趣,让学习不失实用性。

  2. 内核可观察性

    这个不用多说,BPF 的强项。Cloud Native + 网络监控 + 安全 将会是 BPF 的杀手应用。

  3. 应用可观察性

bootstrap 程序功能

bootstrap 程序本身类似经典的 execsnoop。监听内核的 exec 调用,输出到程序终端:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ sudo ./bootstrap -d 50
TIME     EVENT COMM             PID     PPID    FILENAME/EXIT CODE
19:18:32 EXIT  timeout          3817109 402466  [0] (126ms)
19:18:32 EXIT  sudo             3817117 3817111 [0] (259ms)
19:18:32 EXIT  timeout          3817110 402466  [0] (264ms)
19:18:33 EXIT  python3.7        3817083 1       [0] (1026ms)
19:18:38 EXIT  python3          3817429 3817424 [1] (60ms)
19:18:38 EXIT  sh               3817424 3817420 [0] (79ms)
19:18:38 EXIT  timeout          3817420 402466  [0] (80ms)
...

程序架构

bootstrap 与内核互动概述

如上图排版有问题,请点这里用 Draw.io 打开。部分带互动链接和 hover tips

上图 file descriptor 之间的连线,反映了它们之间的关联。这里简单列一下上图的流程:

  1. 其它进程调用的 exec ,触发 sched_process_exec tracepoint
  2. 内核调用相关的 BPF 程序,更新 exec_start MAP
  3. BPF 程序把事件提交到 ringbuffer MAP
  4. 应用(bootstrap(user space)) 读取 ringbuffer

内核态 BPF

在 make 的过程中,实际上是执行了:

1
2
3
4
5
clang -g -O2 -target bpf -D__TARGET_ARCH_x86 -I.output -I../../libbpf/include/uapi -I../../vmlinux/x86/ 
-idirafter /usr/lib/llvm-14/lib/clang/14.0.0/include -idirafter /usr/local/include -idirafter 
/usr/include/x86_64-linux-gnu -idirafter /usr/include -c minimal.bpf.c -o .output/bootstrap.bpf.o

llvm-strip -g .output/bootstrap.bpf.o

最后一行就是重点。输入是 bootstrap.bpf.c。输出是 bootstrap.bpf.o。这是一个 ELF 格式的文件。这个文件将会嵌入到应用中。bootstrap.bpf.o section 如下:

$ readelf -aW examples/c/.output/bootstrap.bpf.o

Section Headers:
  [Nr] Name              Type            Address          Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            0000000000000000 000000 000000 00      0   0  0
  [ 1] .strtab           STRTAB          0000000000000000 008936 0000f2 00      0   0  1
  [ 2] .text             PROGBITS        0000000000000000 000040 000000 00  AX  0   0  4
  [ 3] tp/sched/sched_process_exec PROGBITS        0000000000000000 000040 0001f8 00  AX  0   0  8
  [ 4] .reltp/sched/sched_process_exec REL             0000000000000000 008370 000030 10   I 15   3  8
  [ 5] tp/sched/sched_process_exit PROGBITS        0000000000000000 000238 0002a8 00  AX  0   0  8
  [ 6] .reltp/sched/sched_process_exit REL             0000000000000000 0083a0 000050 10   I 15   5  8
  [ 7] license           PROGBITS        0000000000000000 0004e0 00000d 00  WA  0   0  1
  [ 8] .rodata           PROGBITS        0000000000000000 0004f0 000008 00   A  0   0  8
  [ 9] .maps             PROGBITS        0000000000000000 0004f8 000030 00  WA  0   0  8
  [10] .BTF              PROGBITS        0000000000000000 000528 0077a9 00      0   0  4
  [11] .rel.BTF          REL             0000000000000000 0083f0 000040 10   I 15  10  8
  [12] .BTF.ext          PROGBITS        0000000000000000 007cd4 00054c 00      0   0  4
  [13] .rel.BTF.ext      REL             0000000000000000 008430 000500 10   I 15  12  8
  [14] .llvm_addrsig     LOOS+0xfff4c03  0000000000000000 008930 000006 00   E  0   0  1
  [15] .symtab           SYMTAB          0000000000000000 008220 000150 18      1   8  8

如果你不太了解 ELF 格式,建议先看看,因为理解这个格式很重要。可以参考我的《ELF 格式简述 - eBPF 基础知识》

make 用户态应用

这里主要讲 skeleton 部分了。以前做过旧 RPC 的同学可能比较了解。用一些数据去生成一个 skeleton(骨架)代码 (主要是一些数据结构和函数定义),方便使用者基于这些 skeleton 再开发程序。对于 libbpf,也是一样的。

1
2
$ bpftool gen skeleton .output/bootstrap.bpf.o
Successfully remade target file '.output/bootstrap.skel.h'

bpftool 分析 BPF内核态的 ELF 文件(bootstrap.bpf.o),生成 skeleton 代码 。应用就可以基于这个 skeleton 去开发了。

需要注意的是,生成的 bootstrap.skel.h 其实嵌入了 bootstrap.bpf.o

1
2
3
4
5
6
7
static inline const void *bootstrap_bpf__elf_bytes(size_t *sz)
{
    *sz = 2432;
    return (const void *)"\
\x7f\x45\x4c\x46\x02\x01\x01\0\0\0\0\0\0\0\0\0\x01\0\xf7\0\x01\0\0\0\0\0\0\0\0\
\0\0\0\0\0\0\0\0\0\0\0\0\x06\0\0\0\0\0\0\0\0\0\0\x40\0\0\0\0\0\x40\0\x0e\0\x01\
...

跟踪 make 过程的小技巧

由于 c/c++ 我已经放下了快 20 年了。对 make 过程的 debug 已经忘记了。还好,有搜索引擎。

要知道 make 过程实际上发生了什么,执行了什么 clang/gcc 命令。你当然可以看 Makefile 。但如果我一直相信 trace > source code review 。如何 trace ? 我用了个老土的方法:

1
make clean && reset && make SHELL="/bin/bash -x" --debug=bvi 2>&1 | tee -a make.log

其中关键是 SHELL="/bin/bash -x" 了。

分析程序加载、运行、内核互动过程

终于回到我的初心了。

bootstrap 与内核互动过程

如上图排版有问题,请点这里用 Draw.io 打开。部分带互动链接和 hover tips

图中是我跟踪的结果。用 Draw.io 打开后,每一步均有 link,点击可看到代码。鼠标放到连接线上,会 hover 出 stack(调用堆栈)。

图中的说明已经比较详细。其中包括重要的数据结构和步骤。

我 fork 了项目到这里:

https://github.com/labilezhu/libbpf-bootstrap/tree/20230226

vscode debug 配置

你可以看到我用 vscode debug,其中 .vscode/launch.json 配置如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
{
    "configurations": [
        //bootstrap
        {
            "name": "gdb bootstrap",
            
            "type": "cppdbg",
            "request": "launch",
            "program": "${workspaceFolder}/examples/c/bootstrap",
            "args": [],
            "stopAtEntry": false,
            "cwd": "${fileDirname}",
            "environment": [],
            "externalConsole": false,
            "MIMode": "gdb",
            "setupCommands": [
                {
                    "description": "Enable pretty-printing for gdb",
                    "text": "-enable-pretty-printing",
                    "ignoreFailures": true
                },
                {
                    "description": "Set Disassembly Flavor to Intel",
                    "text": "-gdb-set disassembly-flavor intel",
                    "ignoreFailures": true
                }
            ],
            // "preLaunchTask": "C/C++: gcc build active file",
            "miDebuggerPath": "/usr/bin/sudo-gdb"
        },   
    "version": "2.0.0"
}

因为 bpf 程序需要 root 权限,所以要加上 "miDebuggerPath": "/usr/bin/sudo-gdb"。而 /usr/bin/sudo-gdb 内容如下:

1
2
$ cat /usr/bin/sudo-gdb
sudo /usr/bin/gdb "$@"

gdb 断点设置

由于目的是观察 bpf 加载过程相关的 syscall。可以配置 gdb 的 syscall 断点:

1
-exec catch syscall

其中 -exec 前缀是 vscode 对直接使用 gdb 命令要求加的前缀。

gdb 断点设置的一些坑

libbpf 自身时常会检查运行期内核的对 bpf 特性的支持情况。所以有一些 syscall 是要手工忽略的。如 stack 中有 kernel_supports(…) 的均是可以忽略的。有没方法让 gdb 加个断点条件?当然有:

1
2
3
-exec catch syscall
Catchpoint 3 (any syscall) //这里注意,3 是断点的 id ,下面的命令要引用这个 id。在您的环境可能数值不同。
-exec condition 3 !$_any_caller_matches("get_kernel_version|kernel_supports|bpf_object__probe_loading|btf_parse_raw|handle_event", 20)

即,stack 中已经包含 kernel_supports 等等的,就不断点。

后记

**这个后记和本文没什么相关了,不喜可跳过。**只是最近心情一般,春暖花开,本应该很快乐的。毕竟 blog 的本质是记录,所以也上图一张,记录一下心情。

page0 (1)

分享

Mark Zhu
作者
Mark Zhu
An old developer