大纲: 
- 介绍
- 什么是 eBPF
- Uprobes
- 构建追踪程序
- 番外
  - Installing BCC
  - too many arguments 编译错误

最新的 Go Weekly 推送了这篇文章, eBPF 作为新时代的剖析工具正在如火如荼发展, 读完感觉用来入门很好, 就根据自己理解编译了这篇文章. 做实验过程遇到一些问题, 在最后加了一个番外章节可参考.

下面正式开始.

不用重新编译/部署线上程序而是借助 eBPF 即可实现对程序进行调试, 接下来我们会用一个系列文章介绍我们是怎么做的, 这是开篇. 本篇描述了如何使用 gobpf 和 uprobe 来构建一个跟踪 Go 程序函数入口参数变化的应用. 这里介绍的技术可以扩展到其它编译型语言, 如 C++, Rust 等等. 本系列文章后续将会讨论如何使用 eBPF 来跟踪 HTTP/gRPC 数据和 SSL 等等.

介绍

当调试程序时, 我们一般对捕获程序的运行时状态非常感兴趣. 因为这可以让我们检查程序在干什么, 并能让我们确定 bug 出现在程序的哪一块. 观察运行时状态的一个简单方式是使用调试器. 比如针对 Go 程序, 我们可以使用 Delve 和 gdb.

Delve 和 gdb 在开发环境中做调试表现没得说, 但是我们一般不会在线上使用此类工具. 它们的长处同时也是它们的短处, 因为调试器会导致线上程序中断, 甚至如果在调试过程中不小心改错某个变量的值而导致线上程序出现异常.

为了让线上调试过程的侵入和影响更小, 我们将会探索使用增强版的 BPF(eBPF, Linux 4.x+ 内核可用)和更高级的 Go 库 gobpf 来达成目标.

什么是 eBPF

扩展型 BPF(eBPF) 是一项在 Linux 4.x+ 内核可用的技术. 你可以把它看作一个轻量级的沙箱 VM, 它运行在 Linux 内核中并且提供了针对内核内存的可信访问.

就像下面要说的, eBPF 允许内核运行 BPF 字节码. 虽然可用的前端(这里指的是编译器前端)语言多样, 但通常都是 C 语言的真子集. 通常 C 代码先通过 Clang 被编译为 BPF 字节码, 然后字节被验证以确保可以安全执行. 这些严格的验证保证了机器码不会有意或无意地危及 Linux 内核, 同时也确保了 BPF 探针在每次被触发时将会执行有限数目的指令. 这些保证确保了 eBPF 可以被用于性能敏感的应用中, 比如包过滤, 网络监控等等.

从功能上说, eBPF 允许你针对某些事件(如定时器事件, 网络事件或是函数调用事件)运行受限的 C 代码. 当因为一个函数调用事件被触发时, 我们把这些 eBPF 代码叫做探针. 这些探针既可以针对内核函数调用事件被触发(这时叫 kprobe, k 即 kernelspace), 也可以针对用户空间的函数调用事件被触发(这时叫 uprobe, u 即 userspace). 本篇文章讲解如何通过 uprobe 实现函数参数的动态追踪.

Uprobes
int3

编译和验证过的 BPF 程序作为 uprobe 的一部分被执行, 同时执行结果写入到一个 buffer 中.

下面让我们研究下 uprobes 如何起作用的. 为了演示部署 uprobes 并捕获函数参数, 我们会用到这个简单的 demo 应用. 该 demo 相关部分下面介绍.

main()ecomputeEcomputeE
func computeE(iterations int64) float64 {
  res := 2.0
  fact := 1.0

  for i := int64(2); i < iterations; i++ {
    fact *= float64(i)
    res += 1 / fact
  }
  return res
}

func main() {
  http.HandleFunc("/e", func(w http.ResponseWriter, r *http.Request) {
    // ... 省略代码用于从 get 请求中解析 iters 参数, 若为空则使用默认值
    w.Write([]byte(fmt.Sprintf("e = %0.4f\n", computeE(iters))))
  })
  // 启动 server...
}

为了进行后面的实验以及为最后采用 gdb 验证修改生效, 我们采用如下指令编译该代码:

$ go build  -gcflags "-N -l" app.go
objdump
# 执行下面命令之前需要你先将上面 go 程序编译为名为 app 的二进制文件.
# objdump --syms 可以从可执行程序中导出全部符号, 然后通过 grep 查找 computeE.
# 具体输出可能与你机器上不同, 这没什么问题.
$ objdump --syms app | grep computeE
00000000000x6600e0 g     F .text  000000000000004b             main.computeE
computeE0x0x6600e0objdump-d
$ objdump -d app | grep -A 1 0x6600e0
00000000000x6600e0 <main.computeE>:
  0x6600e0:       48 8b 44 24 08          mov    0x8(%rsp),%rax
computeEmov 0x8(%rsp),%raxrspcomputeE0x8raxcomputeEiterations
computeE
构建追踪程序

我们给这个追踪程序起个名叫 Tracer. 为了捕获前面提到的事件, 我们需要注册一个 uprobe 函数, 并且还得有个用户态函数负责去读 uprobe 的输出, 具体如下图所示:

tracerperf-buffer
int3main.computeEcomputeEtracer

就我们这个需求来说, 相应的 BPF 代码很简单, C 代码如下:

#include <uapi/linux/ptrace.h>
BPF_PERF_OUTPUT(trace);
// 该函数将会被注册, 以便每次 main.computeE 被调用时该函数也会被调用
inline int computeECalled(struct pt_regs *ctx) {
  // main.computeE 的入参保存在了 ax 寄存器里.
  long val = ctx->ax;
  trace.perf_submit(ctx, &val, sizeof(val));
  return 0;
}
main.computeE
main.computeE

上述动图执行步骤如下:

./appcurl http://localhost:9090/e?iters=10sudo ./trace --binary ../app/app
0x0x6600e0trace
$ gdb ./app
(gdb) display /4i 0x6600e0
1: x/4i 0x6600e0
   0x6600e0 <main.computeE>:    sub    $0x20,%rsp
   0x6600e4 <main.computeE+4>:  mov    %rbp,0x18(%rsp)
   0x6600e9 <main.computeE+9>:  lea    0x18(%rsp),%rbp
   0x6600ee <main.computeE+14>: xorps  %xmm0,%xmm0
trace
$ gdb ./app
(gdb) display /4i 0x65fecf
2: x/4i 0x6600e0
   0x6600e0 <main.computeE>:    int3   
   0x6600e1 <main.computeE+1>:  sub    $0x20,%esp
   0x6600e4 <main.computeE+4>:  mov    %rbp,0x18(%rsp)
   0x6600e9 <main.computeE+9>:  lea    0x18(%rsp),%rbp
0x6600e0int3
番外

Installing BCC

trace
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 4052245BD4284CDD
echo "deb https://repo.iovisor.org/apt/$(lsb_release -cs) $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/iovisor.list
sudo apt-get update
sudo apt-get install bcc-tools libbcc-examples linux-headers-$(uname -r)
/etc/sudoersDefaults env_keep = "http_proxy https_proxy"

too many arguments 编译错误

# github.com/iovisor/gobpf/bcc
../../../../go/pkg/mod/github.com/iovisor/gobpf@v0.0.0-20200614202714-e6b321d32103/bcc/module.go:98:40: too many arguments in call to _Cfunc_bpf_module_create_c_from_string
        have (*_Ctype_char, number, **_Ctype_char, _Ctype_int, _Ctype__Bool, nil)
        want (*_Ctype_char, _Ctype_uint, **_Ctype_char, _Ctype_int, _Ctype__Bool)
../../../../go/pkg/mod/github.com/iovisor/gobpf@v0.0.0-20200614202714-e6b321d32103/bcc/module.go:230:28: too many arguments in call to _C2func_bcc_func_load
        have (unsafe.Pointer, _Ctype_int, *_Ctype_char, *_Ctype_struct_bpf_insn, _Ctype_int, *_Ctype_char, _Ctype_uint, _Ctype_int, *_Ctype_char, _Ctype_uint, nil)
        want (unsafe.Pointer, _Ctype_int, *_Ctype_char, *_Ctype_struct_bpf_insn, _Ctype_int, *_Ctype_char, _Ctype_uint, _Ctype_int, *_Ctype_char, _Ctype_uint)

原因为这一行增加的特性 Update bcc_func_load to libbcc 0.11 with hardware offload support, 以及这一行增加的特性 bcc: update bpf_module_create_c_from_string for bcc 0.11.0 (fixes #202).

module.gonil

--End--