02月22, 2022

bpftrace 入门(1)

bpftrace 通过高度抽象的封装来使用 eBPF,大多数功能只需要寥寥几笔就可以运行起来,可以很快让我们搞清楚 eBPF 是什么样的,而暂时不关心 eBPF 复杂的内部机理。由于 bpftrace 深受 AWK 和 c 的影响,bpftrace 使用起来于 AWK 非常相似,那些内核 hook 注入点几乎可以按普通字符串匹配来理解,非常容易上手。

安装 bpftrace

首先,bpftrace 依赖 eBPF,而 eBPF 的一些特性需要较新的内核,如果你的内核版本小于 4,最好先升级内核。你可以通过如下指令确认自己的内核版本:

uname -a

我的内核是 4.18 版本,CentOS 8。

[root@localhost ~]# uname -a
Linux localhost.localdomain 4.18.0-348.7.1.el8_5.x86_64 #1 SMP Wed Dec 22 13:25:12 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

下面开始安装 bpftrace,bpftrace 的项目文档给出了一些常见系统:

  • Ubuntu
  • Fedora
  • Gentoo
  • Debian
  • openSUSE
  • CentOS
  • Arch

的安装方法。 我使用的是 CentOS 系统,文档中提供了社区维护的源,安装过程也非常简单:

curl https://repos.baslab.org/rhel/7/bpftrace-daily/bpftrace-daily.repo --output /etc/yum.repos.d/bpftools.repo
curl https://repos.baslab.org/rhel/7/bpftools/bpftools.repo --output /etc/yum.repos.d/bpftrace-daily.repo
yum install bpftrace bpftrace-tools bpftrace-doc

安装完毕,可以确认一下是否安装成功:

[root@localhost ~]# bpftrace -V
bpftrace v0.14.0-50-g4228-dirty

可以看到我安装的 bpftrace 版本。

另外还可以利用 docker 容器运行:

docker run -ti -v /usr/src:/usr/src:ro \
       -v /lib/modules/:/lib/modules:ro \
       -v /sys/kernel/debug/:/sys/kernel/debug:rw \
       --net=host --pid=host --privileged \
       quay.io/iovisor/bpftrace:latest \
       tcplife.bt

虽然非常方便,作为新手最好还是在虚拟机里部署一套原生环境学习为佳。

虽然费了些笔墨,但总体来说安装过程还是比较简单的。

bpftrace 脚本的结构

我说 bpftrace 简单好用是有原因的,它可以只用一行就实现一些很有用的功能。回顾之前文章里提到的 Hello world 例子:

  • bcc 版本
from bcc import BPF
BPF(text='int kprobe__sys_clone(void *ctx) { bpf_trace_printk("Hello, World!\\n"); return 0; }').trace_print()
  • bpftrace 版本
bpftrace -e 'BEGIN{printf("Hello, World!\n")}'

好用是一目了然的。

bpftrace 本质上是一种脚本语言,风格与 AWK 非常相似,脚本的结构像:

// 这些换行不是必须的
/*采用与 C 语言类似的注释*/

BEGIN{
    // 开始时执行一次的代码
}
probe/filter/ {
    // 事件与 prob 和 filter 匹配时执行
}
END{
    // 结束时执行
}

eBPF 的运行是基于内核的 “事件” 驱动的,这些事件就是内核收到某个 Event、进入或退出某个函数之类。换言之,我们编写的 eBPF 或者 bpftrace 程序将会被注入 eBPF VM,告诉内核在发生指定 “事件” 时执行对应的代码,而不是在注入时立刻执行。bpftrace 通过 过滤器 + 代码块 的形式表示事件和对应的代码。

除此之外,额外提供了 BEGIN 和 END 用于执行一些与事件无关的代码,比如做一些初始化工作,或者结束时输出结果。

前面的 Hello World 例子,可以看到脚本只有 BEGIN 块,probe 块 和 END 块都缺省,这是没问题的,说明这段脚本只需要开始时无条件执行一次,之后就没有其他的事了。同理,也可以指定 END 块,这样更容易看到 BEGIN 和 END 块的关系:

[root@localhost ~]# bpftrace -e 'BEGIN{printf("Hello, World!\n");}END{printf("Bye, World!\n")}'
Attaching 2 probes...
Hello, World!
^CBye, World!

可以看到,脚本运行起来立刻输出了 BEGIN 块的结果 “Hello, World!”,当我们按下 Ctl+C 退出时,END 块的代码输出了 “Bye, World!”,BEGIN 和 END 分别在脚本的最开始和退出前执行。

那么能不能像前文中一样,将脚本组织为更容易读的格式呢?可以的,有两种方式:

  • 利用 shell 换行:

    [root@localhost ~]# bpftrace -e '
    > BEGIN{
    > printf("Hello, World!\n");
    > }
    > 
    > END{
    > printf("Bye, World!\n");
    > }'
    

    在脚本尾部的单引号之前,按照需要换行即可。

  • 在文件中编写脚本

    // hello.bt
    // 这是一段注释
    /* 这是另一种注释 */
    
    BEGIN{
        printf("Hello, World!\n");
    }
    
    END{
        printf("Bye, World!\n");
    }
    

    保存为 hello.bt,这看起来就更像一般的脚本文件了,可以这样让它运行起来:

    [root@localhost ~]# bpftrace hello.bt
    

上述两种方式都可以得到完全相同的结果。

执行 bpftrace 的两种方式

我们注意到前文运行 bpftrace 脚本有两种不同的方式:

  • bpftrace -e ‘cmds’ 执行单行指令;
  • bpftrace filename 执行脚本文件;

单行指令模式是即用即弃,执行一些简短的指令非常方便,所有指令都包含在最后一个参数中,需要注意的是,虽然单行指令模式的指令可能很长,但对于 bpftrace 来说,它只接受了 2 个参数:

  1. '-e';
  2. 一个包含指令的字符串;

通常第三个参数总是包裹在单引号内,其实是 shell 的要求,确保所有指令都被识别为一个完整的字符串!

对于那些稍复杂的脚本,就要用脚本文件的形式,便于编辑、维护。另外,在脚本文件头部增加一行代码:

#!/bin/env bpftrace
// hello.bt
// 这是一段注释
/* 这是另一种注释 */

BEGIN{
    printf("Hello, World!\n");
}

END{
    printf("Bye, World!\n");
}

并赋予脚本文件可执行权限, 它可以像一个普通的可执行文件一样被执行:

[root@localhost ~]# chmod +x hello.bt
[root@localhost ~]# ./hello.bt 
Attaching 2 probes...
Hello, World!
^CBye, World!

eBPF 支持哪些探针?

bpftrace 是 eBPF 的高级封装,借助 bpftrace 可以一窥 eBPF 轮廓。

bpftrace 脚本的 BEGIN 和 END 块都很简单,probe 块我打算放到明天再分享,在此之前先解决一个问题,bpftrace 或者说 eBPF 支持哪些探针(内核 hook)呢?

bpftrace 具备良好的自文档,通过

bpftrace -l

即可列出大部分支持的探针,下面是部分探针和统计数据:

[root@localhost ~]# bpftrace -l | head -n 10
hardware:backend-stalls:
hardware:branch-instructions:
hardware:branch-misses:
hardware:bus-cycles:
hardware:cache-misses:
hardware:cache-references:
hardware:cpu-cycles:
hardware:frontend-stalls:
hardware:instructions:
hardware:ref-cycles:
[root@localhost ~]# bpftrace -l  | wc -l 
82013
[root@localhost ~]# bpftrace -l  | awk -F ":" '{print $1}' | sort | uniq -c
     10 hardware
      2 iter
  35083 kfunc
  45102 kprobe
     11 software
   1805 tracepoint

bpftrace 约有 82013 个探针,其中包含:

  • hardware
  • iter
  • kfunc
  • kprobe
  • software
  • tracepoint

六个大类,这是很庞大的数量了,几乎涵盖了内核的方方面面,为后续透视内核提供强大的支持。

这里的探针分类与 eBPF 一致,在使用 bpftrace 大致了解这些探针后,再用 bcc 直接操作这些探针一定会更得心应手。

本文链接:http://www.thinkinpython.com/post/bpftrace_tutorial_1.html

-- EOF --