bpftool 是什么

bpftool 是一个用来检查 BPF 程序和映射的内核工具。

如何编译安装 bpftool

bpftool 源码在 tools/bpf/bpftool 中,直接 cd 到这个路径中,然后执行 make && make install 即可。
这里我使用了 V=1 来输出更详细的信息,过程记录如下:

  1. root@debian-10:12:12:04] bpftool # make && make V=1 install
  2. make[1]: 进入目录“/home/longyu/linux-git/tools/lib/bpf
  3. make[1]: 离开目录“/home/longyu/linux-git/tools/lib/bpf
  4. make -C /home/longyu/linux-git/tools/lib/bpf/ OUTPUT= libbpf.a
  5. make[1]: 进入目录“/home/longyu/linux-git/tools/lib/bpf
  6. make -f /home/longyu/linux-git/tools/build/Makefile.build dir=. obj=libbpf
  7. make[1]: 离开目录“/home/longyu/linux-git/tools/lib/bpf
  8. install -m 0755 -d /usr/local/sbin
  9. install bpftool /usr/local/sbin/bpftool
  10. install -m 0755 -d /usr/share/bash-completion/completions
  11. install -m 0644 bash-completion/bpftool /usr/share/bash-completion/completions
  12. 1
  13. 2
  14. 3
  15. 4
  16. 5
  17. 6
  18. 7
  19. 8
  20. 9
  21. 10
  22. 11

执行 bpftool version 命令查看版本信息:

  1. [root@debian-10:12:13:46] bpftool # bpftool version
  2. bpftool v5.0.0
  3. 1
  4. 2

v5.0.0 版本与我使用的内核版本对应!

上手 bpftool

bpftool 帮助信息

单独执行 bpftool 命令会输出如下帮助信息:

  1. Usage: bpftool [OPTIONS] OBJECT { COMMAND | help }
  2. bpftool batch file FILE
  3. bpftool version
  4. OBJECT := { prog | map | cgroup | perf | net }
  5. OPTIONS := { {-j|--json} [{-p|--pretty}] | {-f|--bpffs} |
  6. {-m|--mapcompat} | {-n|--nomount} }
  7. 1
  8. 2
  9. 3
  10. 4
  11. 5
  12. 6
  13. 7

与 《Linux内核观测技术BPF》一书中不同的是,我这个版本并不支持 feature 选项。

bpftool prog

bpftool prog 可以用来检查系统中运行程序的情况,在我的系统中执行相关命令得到了如下信息:

  1. [root@debian-10:12:14:52] bpftool # bpftool prog show
  2. 3: cgroup_skb tag 7be49e3934a125ba gpl
  3. loaded_at 2020-11-21T19:16:17+0800 uid 0
  4. xlated 296B not jited memlock 4096B map_ids 2,3
  5. 4: cgroup_skb tag 2a142ef67aaad174 gpl
  6. loaded_at 2020-11-21T19:16:17+0800 uid 0
  7. xlated 296B not jited memlock 4096B map_ids 2,3
  8. 5: cgroup_skb tag 7be49e3934a125ba gpl
  9. loaded_at 2020-11-21T19:16:17+0800 uid 0
  10. xlated 296B not jited memlock 4096B map_ids 4,5
  11. 6: cgroup_skb tag 2a142ef67aaad174 gpl
  12. loaded_at 2020-11-21T19:16:17+0800 uid 0
  13. xlated 296B not jited memlock 4096B map_ids 4,5
  14. 7: cgroup_skb tag 7be49e3934a125ba gpl
  15. loaded_at 2020-11-21T19:16:17+0800 uid 0
  16. xlated 296B not jited memlock 4096B map_ids 6,7
  17. 8: cgroup_skb tag 2a142ef67aaad174 gpl
  18. loaded_at 2020-11-21T19:16:17+0800 uid 0
  19. xlated 296B not jited memlock 4096B map_ids 6,7
  20. 1
  21. 2
  22. 3
  23. 4
  24. 5
  25. 6
  26. 7
  27. 8
  28. 9
  29. 10
  30. 11
  31. 12
  32. 13
  33. 14
  34. 15
  35. 16
  36. 17
  37. 18
  38. 19

上面的输出中最左侧的数字表示程序标识符,可以用来获取更详细的信息。
可以使用 —json 来生成 json 格式的输出,同时指定 id 来过滤掉不需要的项目。
执行示例如下:

  1. [root@debian-10:12:21:01] bpftool # bpftool prog show --json id 3 | jq
  2. {
  3. "id": 3,
  4. "type": "cgroup_skb",
  5. "tag": "7be49e3934a125ba",
  6. "gpl_compatible": true,
  7. "loaded_at": 1605957377,
  8. "uid": 0,
  9. "bytes_xlated": 296,
  10. "jited": false,
  11. "bytes_memlock": 4096,
  12. "map_ids": [
  13. 2,
  14. 3
  15. ]
  16. }
  17. [root@debian-10:12:21:05] bpftool # bpftool prog show --json id 4 | jq
  18. {
  19. "id": 4,
  20. "type": "cgroup_skb",
  21. "tag": "2a142ef67aaad174",
  22. "gpl_compatible": true,
  23. "loaded_at": 1605957377,
  24. "uid": 0,
  25. "bytes_xlated": 296,
  26. "jited": false,
  27. "bytes_memlock": 4096,
  28. "map_ids": [
  29. 2,
  30. 3
  31. ]
  32. }
  33. 1
  34. 2
  35. 3
  36. 4
  37. 5
  38. 6
  39. 7
  40. 8
  41. 9
  42. 10
  43. 11
  44. 12
  45. 13
  46. 14
  47. 15
  48. 16
  49. 17
  50. 18
  51. 19
  52. 20
  53. 21
  54. 22
  55. 23
  56. 24
  57. 25
  58. 26
  59. 27
  60. 28
  61. 29
  62. 30
  63. 31
  64. 32

这个 jq 程序我的系统中并没有预装,可以执行下面这条命令来安装:

  1. apt-get install jq
  2. 1

bpf prog dump

获取到了程序标识符后,可以执行 bpf prog dump 来获取整个程序的数据,能够看到由编译器生成的字节码。
我首先运行如下程序:

  1. from bcc import BPF
  2. bpf_source = """
  3. int trace_go_main(struct pt_regs *ctx) {
  4. u64 pid = bpf_get_current_pid_tgid();
  5. bpf_trace_printk("New hello-bpf process running with PID: %d\\n", pid);
  6. return 0;
  7. }
  8. """
  9. bpf = BPF(text = bpf_source)
  10. bpf.attach_uprobe(name = "/home/longyu/linux-observability-with-bpf/code/chapter-4/uprobes/hello-bpf", sym = "main.main", fn_name = "trace_go_main")
  11. bpf.trace_print()
  12. 1
  13. 2
  14. 3
  15. 4
  16. 5
  17. 6
  18. 7
  19. 8
  20. 9
  21. 10
  22. 11
  23. 12
  24. 13

再次执行 bpftool prog show 发现增加了如下信息:

  1. 39: kprobe name trace_go_main tag 76ea4b5c961566b3 gpl
  2. loaded_at 2020-11-22T12:30:12+0800 uid 0
  3. xlated 200B not jited memlock 4096B
  4. 1
  5. 2
  6. 3

然后我使用 bpftool prog dump xlated 来 dump id 为 39 的程序的内容,相关信息如下:

  1. [root@debian-10:12:30:23] bpftool # bpftool prog dump xlated id 39
  2. 0: (85) call bpf_get_current_pid_tgid#84176
  3. 1: (b7) r1 = 680997
  4. 2: (63) *(u32 *)(r10 -8) = r1
  5. 3: (18) r1 = 0x203a444950206874
  6. 5: (7b) *(u64 *)(r10 -16) = r1
  7. 6: (18) r1 = 0x697720676e696e6e
  8. 8: (7b) *(u64 *)(r10 -24) = r1
  9. 9: (18) r1 = 0x757220737365636f
  10. 11: (7b) *(u64 *)(r10 -32) = r1
  11. 12: (18) r1 = 0x7270206670622d6f
  12. 14: (7b) *(u64 *)(r10 -40) = r1
  13. 15: (18) r1 = 0x6c6c65682077654e
  14. 17: (7b) *(u64 *)(r10 -48) = r1
  15. 18: (bf) r1 = r10
  16. 19: (07) r1 += -48
  17. 20: (b7) r2 = 44
  18. 21: (bf) r3 = r0
  19. 22: (85) call bpf_trace_printk#-46368
  20. 23: (b7) r0 = 0
  21. 24: (95) exit
  22. 1
  23. 2
  24. 3
  25. 4
  26. 5
  27. 6
  28. 7
  29. 8
  30. 9
  31. 10
  32. 11
  33. 12
  34. 13
  35. 14
  36. 15
  37. 16
  38. 17
  39. 18
  40. 19
  41. 20
  42. 21

第一行 call 指令调用 bpf_get_current_pid_tgid 函数,这个函数是内核函数,它返回 pid 这个值。
从第 1 行指令到第 21 行指令都是在给 bpf_trace_printk 制作参数,注意第 21 行的 r3 = r0 操作,这个 r0 其实就是 bpf_get_current_pid_tgit 获取到的 pid 值。
为啥说 r0 就是 pid 的值呢?
可以看到第 23 行将 r0 赋值为 0,然后第 24 行调用 exit 退出,其实对应的就是 trace_go_main 函数中的 return 0 操作。
这样看来它传递参数应该有这样的规则:

r0 作为返回值 r1-rN 作为函数的参数

bpf_trace_printk 函数看来并不像表面那样使用了两个参数,其实它的参数有三个。
第一个是 r1,指向 “New hello-bpf process running with PID: %d\n” 字符串的存储位置,第二个是 r2,它被赋值为 44,这个 44 并不存在于代码中,它其实是 r1 指向的字符串长度,r3 被赋值为 r0,它其实就是 pid 的值。
从上面这些 bpf 指令中没有看到对栈的使用,但是调用 bpf_get_current_pid_tgid 这个函数应当是要使用栈的,我觉得这里没有用到到使用栈的指令能够说明 bpf_get_current_pid_tgid 函数并不会被编译成 bpf 指令
bpf_get_current_pid_tgid 的实现在内核源码树 kernel/bpf/helpers.c 中,相关内容如下:

  1. BPF_CALL_0(bpf_get_current_pid_tgid)
  2. {
  3. struct task_struct *task = current;
  4. if (unlikely(!task))
  5. return -EINVAL;
  6. return (u64) task->tgid << 32 | task->pid;
  7. }
  8. 1
  9. 2
  10. 3
  11. 4
  12. 5
  13. 6
  14. 7
  15. 8
  16. 9

反汇编 helpers.o 得到了如下信息:

  1. 00000000000000c0 <bpf_get_current_pid_tgid>:
  2. c0: e8 00 00 00 00 callq c5 <bpf_get_current_pid_tgid+0x5>
  3. c5: 65 48 8b 14 25 00 00 mov %gs:0x0,%rdx
  4. cc: 00 00
  5. ce: 48 c7 c0 ea ff ff ff mov $0xffffffffffffffea,%rax
  6. d5: 48 85 d2 test %rdx,%rdx
  7. d8: 74 15 je ef <bpf_get_current_pid_tgid+0x2f>
  8. da: 48 63 82 cc 04 00 00 movslq 0x4cc(%rdx),%rax
  9. e1: 48 63 92 c8 04 00 00 movslq 0x4c8(%rdx),%rdx
  10. e8: 48 c1 e0 20 shl $0x20,%rax
  11. ec: 48 09 d0 or %rdx,%rax
  12. ef: c3 retq
  13. 1
  14. 2
  15. 3
  16. 4
  17. 5
  18. 6
  19. 7
  20. 8
  21. 9
  22. 10
  23. 11
  24. 12

这个反汇编的结果能够证明此函数是非 bpf 指令,但是这个函数的反汇编信息看上去很奇怪,与标准函数的汇编代码有所区别。
写到这里我想到了一个问题,既然这个 bpf_get_current_pid_tgid 函数是 x86 的机器码,按照物理机器的执行过程,这个函数执行完了就要返回到调用点的下一行代码,也就是下面这行 bpf 指令:

  1. 1: (b7) r1 = 680997
  2. 1

想想从一个 x86 的机器码返回到 bpf 的机器码,这完全不科学。其实 bpf 的机器码也是用 x86 的机器码模拟的,这样这个问题就不存在了。
___bpf_prog_run 中对 bpf 的 call 指令进行下面的处理:

  1. /* CALL */
  2. JMP_CALL:
  3. /* Function call scratches BPF_R1-BPF_R5 registers,
  4. * preserves BPF_R6-BPF_R9, and stores return value
  5. * into BPF_R0.
  6. */
  7. BPF_R0 = (__bpf_call_base + insn->imm)(BPF_R1, BPF_R2, BPF_R3,
  8. BPF_R4, BPF_R5);
  9. CONT;
  10. 1
  11. 2
  12. 3
  13. 4
  14. 5
  15. 6
  16. 7
  17. 8
  18. 9

JMP_CALL 对应 bpf 的机器码,BPF_R0 存储了这个调用的返回值,它与我上文提到的 r0 寄存器是一致的,其实不过是一个局部变量数组中的某个项目,其它的寄存器也有类似的特征。
bpf 虚拟机实际是提供了一套解析执行 bpf 指令码的环境,这个环境由软件模拟,它最终也必须通过硬件指令来执行
写到这里,我想到了《The AWK Programming Language》中的第 6 章,内容是讲用 awk 来实现一些简单的语言,第一个例子就是汇编器与解析器,这个例子其实与这里的 bpf 虚拟机的原理有某种相似之处,它也是用软件模拟了指令的执行过程。
也许有人会这样想,既然这种虚拟机器最终都要通过实际的物理机器指令来执行,那么为什么还要转化成另外一种中间的机器码呢?
对一个硬件来说,它的指令集一般是固定的,这样我们不能在同一个硬件环境中执行其它架构的指令,要执行 x86 架构的指令需要使用 x86 架构的机器,要执行 arm 架构的指令需要使用 arm 架构的机器,如果我要同时执行 x86 架构与 arm 架构的程序,我们需要有这两种架构的机器。
而使用虚拟机将会让我们打破硬件的这种限定,不同的虚拟机通过软件来解析不同架构的指令,然后转化为目标硬件的物理架构指令执行,这样我们就能在同一个物理硬件架构上运行多个结构的程序了,这无疑极大的扩展了硬件的边界!

对机器的思考

物理机器一般会使用栈来存储临时变量并维持函数调用的层次,这其实是由函数执行的特点决定的,对于汇编语言来说其实根本不存在函数调用这种说法,函数算是高级语言最具代表性的抽象。
每一个函数其实对应都是一组机器码,它们的执行依赖的机器是共享的,这样就带来了一个问题,在函数嵌套调用过程中如何保存这些共享的内容,栈其实就是一个很好的解决方案。
栈的增减通过简单的拨动栈顶指针完成,基本上也就是一行机器指令,尽管如此简单,但是栈却是整个程序运行的基础元素。
操作系统中任务的切换其实跟函数嵌套调用有类似的特征,不过它保存的是一个完整的寄存器状态,那上下文切换又在干嘛呢?其实说白了也就是在切换栈帧而已!
好了,再次回到 bpftool 中!

将 bpf 程序持久化到 bpf 文件系统

bpftool 可以将 bpf 程序持久化保存到 bpf 文件系统,这样当程序终止后它也会存在。
示例命令如下:

  1. bpftool prog load ./bpf_program.o /sys/fs/bpf/bpf_prog
  2. 1

ls 查看 bpf_prog 文件,确定成功创建,信息如下;

  1. [root@debian-10:15:47:50] hello_world # ls -lh /sys/fs/bpf/bpf_prog
  2. -rw------- 1 root root 0 11 22 15:44 /sys/fs/bpf/bpf_prog
  3. 1
  4. 2

使用 bpftool show 来观察程序仍旧存在,命令输出信息如下:

  1. 44: tracepoint name bpf_prog tag c6e8e35bea53af79 gpl
  2. loaded_at 2020-11-22T15:44:25+0800 uid 0
  3. xlated 112B not jited memlock 4096B
  4. 1
  5. 2
  6. 3

bpftool 检查 bpf 映射

bpftool 可以访问程序正在使用的 bpf 映射,命令类似上面 show 程序的形式。
执行示例如下:

  1. [root@debian-10:15:49:55] hello_world # bpftool map show
  2. 2: lpm_trie flags 0x1
  3. key 8B value 8B max_entries 1 memlock 4096B
  4. 3: lpm_trie flags 0x1
  5. key 20B value 8B max_entries 1 memlock 4096B
  6. 4: lpm_trie flags 0x1
  7. key 8B value 8B max_entries 1 memlock 4096B
  8. 5: lpm_trie flags 0x1
  9. key 20B value 8B max_entries 1 memlock 4096B
  10. 6: lpm_trie flags 0x1
  11. key 8B value 8B max_entries 1 memlock 4096B
  12. 7: lpm_trie flags 0x1
  13. key 20B value 8B max_entries 1 memlock 4096B
  14. 1
  15. 2
  16. 3
  17. 4
  18. 5
  19. 6
  20. 7
  21. 8
  22. 9
  23. 10
  24. 11
  25. 12
  26. 13

bpftool 也可以用来创建、更新、查看映射信息,不进一步描述。

批量加载 bpftool 命令

bpftool 支持批处理模式,可以将命令写入到一个文件中,然后使用 bpftool batch file filepath 来执行。
在 /tmp/bpf 文件中写入如下两行:

  1. prog show
  2. map show
  3. 1
  4. 2

然后执行 bpftool batch file /tmp/bpf 来运行,操作记录如下:

  1. [root@debian-10:15:57:14] hello_world # bpftool batch file /tmp/bpf
  2. 3: cgroup_skb tag 7be49e3934a125ba gpl
  3. loaded_at 2020-11-21T19:16:17+0800 uid 0
  4. xlated 296B not jited memlock 4096B map_ids 2,3
  5. 4: cgroup_skb tag 2a142ef67aaad174 gpl
  6. loaded_at 2020-11-21T19:16:17+0800 uid 0
  7. xlated 296B not jited memlock 4096B map_ids 2,3
  8. ..........
  9. 2: lpm_trie flags 0x1
  10. key 8B value 8B max_entries 1 memlock 4096B
  11. 3: lpm_trie flags 0x1
  12. key 20B value 8B max_entries 1 memlock 4096B
  13. ..........
  14. processed 2 commands
  15. 1
  16. 2
  17. 3
  18. 4
  19. 5
  20. 6
  21. 7
  22. 8
  23. 9
  24. 10
  25. 11
  26. 12
  27. 13
  28. 14

总结

bpf 的核心是 bpf 虚拟机,对 bpf 相关程序的学习是为了研究 bpf 虚拟机做准备,在上手 bpftool 的过程中有了初步的研究,后续需要进一步深化!同时在看内核代码的时候发现 trace 模块是我一直没有看到的内容,貌似很多书中也都没有讲这部分内容,也需要抽时间学习一下!