快速上手 eBPF
eBPF 是一个运行在 kernel 中的虚拟机,通过它我们可以轻松的向内核各处注入一些自定义的代码(虽然受到很多限制)。如果你已经具备一定的 Linux 使用经验,那么你很有可能已经使用过 tcpdump、strace 之类的工具,用来分析网络报文、进程的系统调用等,那么 eBPF 可以看作是这些工具的加强版,具备更高的可编程能力、性能和细致的数据颗粒度,因为 eBPF 能让我们直接在内核中获取数据!
Brendan Gregg 的博客中推荐 eBPF 的学习路径是:
- 初学者:运行 bcc 工具,主要是这些工具都提供源码,可以看代码;
- 中级:开发 bpftrace 工具;
- 高级:开发 bcc 工具;
根据我的学习经验,推荐下面的学习顺序:
- 开发 bpftrace 工具;
- 运行 bcc 工具,研读代码;
- 开发 bcc 工具。
原因很简单,我觉得 bpfstrace 比 bcc 简单多了!
这里简单介绍一下 bcc 和 bpfstace,前面介绍过 eBPF 采用了 VM 机制,其加载的代码是 bytecode,而通常我们并不直接编写 bytecode,直接编写 bytecode 与编写汇编代码无异,即无必要,一般开发者短期也不具备这样的精力,因此 eBPF 的 bytecode 通常是由其他语言编译而来。
比较常用的前端编程语言是 C(pseudo-C)、Go等,通过 LLVM 作为后端编译得到 bytecode。这样就极大的降低了开发门槛。bcc 是就是这样的前端,它是一个 Python 库,其中以字符串形式混入 C 代码,bcc 会自动解决 C 代码的编译、提交到 eBPF 核心、与 eBPF 核心通信等工作。你可以在 Brendan Gregg 的博客中找到 bcc 项目的 github 地址,下面是一个 bcc/examples/hello_world.py 实例:
#!/usr/bin/python
# Copyright (c) PLUMgrid, Inc.
# Licensed under the Apache License, Version 2.0 (the "License")
from bcc import BPF
# This may not work for 4.17 on x64, you need replace kprobe__sys_clone with kprobe____x64_sys_clone
BPF(text='int kprobe__sys_clone(void *ctx) { bpf_trace_printk("Hello, World!\\n"); return 0; }').trace_print()
删除了一些注释,但保留了版权声明
这是一个 hello world 实例,如果浏览更多的 bcc examples,初学者会发现代码多、不容易理解、要求同时具备 C 和 Python 开发经验,因此我不推荐一开始就玩 bcc。
bpfstrace 对 bcc 进行了更进一步封装,基于 eBPF 提供了一种更高级、更抽象的跟踪语言(trace language),越抽象就越容易理解、使用,虽然灵活性受限,但作为上手实验是不二之选。bpfstrace 像是 AWK 和 C 语言的混合,下面是与前文 bcc 相同功能的代码:
bpfstrace -e 'BEGIN{printf("Hello, World!\n")}'
非常简单吧!如果你用过 awk,你会发现,这不能说一模一样,简直就是十分相似。
eBPF 对内核预定义了一组 hook 点(如同上一篇文章所说的 hook callback),涉及到内核调用、内核函数的出入点、内核跟踪点(tracepoints)、网络事件等等,如果这些位置都不满足需要,那么可以在运行时生成内核探针(kprobe)、应用探针(uprobe),如此一来,内核、用户态应用几乎所有的公开符号(global symbols,非 static 函数)都可以被 eBPF 注入,非常强大不是么!
下面来两个简单的例子。
第一个例子
是内核探针的例子,当用户态进程退出时,会调用内核函数 do_exit,下面的代码实现的功能是,只要 do_exit 被调用,就打印退出进程的名称。
bpftrace -e 'kprobe:do_exit{printf("%s quit.\n", comm)}'
这样,每当系统中有进程退出时,终端都会立刻打印刚刚结束的进程名称,可以看到我调用的很多工具的退出记录。
第二个例子
是一个用户进程探针。下面是一段 C 代码,注意其中定义了函数 test,参数为一个字符串。
/* main.c */
#include <stdio.h>
int test(const char * str){
printf("%s\n", str);
return 0;
}
int main(){
char * p = NULL;
test("this is a string.");
*p = '1';
sleep(10000);
return 0;
}
main 函数中向 test 函数传入参数 “this is a string.”,这段代码会编译为可执行文件 main。
下面是 bpfstrace 代码,实现的功能是打印 test 函数的第一个参数:
bpftrace -e 'uprobe:/home/git/test/main:test{printf("%s\n", str(arg0))}'
只要运行 main,即使倒数第 3 行的
*p = '1';
会使得 main 段错误(非正常终止),bpfstrace 依然捕获了 test 函数的参数是什么,而我们不需要对 main 程序做任何修改,甚至不需要借助调试工具,太神奇了!
总结
显然,bpfstrace 非常简单,但是它可以帮助我们快速的建立 eBPF 对 hook 点的分类,尽快理解它的工作原理,建立基本的知识框架,明天就快速进入 bpfstrace 学习,其中最有意思的是,bpfstrace 只需要一行代码,就能实现非常强大的内核 trace 功能,不能不说十分极客了!