bpftool 是什么
bpftool 是一个用来检查 BPF 程序和映射的内核工具。
如何编译安装 bpftool
bpftool 源码在 tools/bpf/bpftool 中,直接 cd 到这个路径中,然后执行 make && make install 即可。
这里我使用了 V=1 来输出更详细的信息,过程记录如下:
root@debian-10:12:12:04] bpftool # make && make V=1 install
make[1]: 进入目录“/home/longyu/linux-git/tools/lib/bpf”
make[1]: 离开目录“/home/longyu/linux-git/tools/lib/bpf”
make -C /home/longyu/linux-git/tools/lib/bpf/ OUTPUT= libbpf.a
make[1]: 进入目录“/home/longyu/linux-git/tools/lib/bpf”
make -f /home/longyu/linux-git/tools/build/Makefile.build dir=. obj=libbpf
make[1]: 离开目录“/home/longyu/linux-git/tools/lib/bpf”
install -m 0755 -d /usr/local/sbin
install bpftool /usr/local/sbin/bpftool
install -m 0755 -d /usr/share/bash-completion/completions
install -m 0644 bash-completion/bpftool /usr/share/bash-completion/completions
• 1
• 2
• 3
• 4
• 5
• 6
• 7
• 8
• 9
• 10
• 11
执行 bpftool version 命令查看版本信息:
[root@debian-10:12:13:46] bpftool # bpftool version
bpftool v5.0.0
• 1
• 2
上手 bpftool
bpftool 帮助信息
单独执行 bpftool 命令会输出如下帮助信息:
Usage: bpftool [OPTIONS] OBJECT { COMMAND | help }
bpftool batch file FILE
bpftool version
OBJECT := { prog | map | cgroup | perf | net }
OPTIONS := { {-j|--json} [{-p|--pretty}] | {-f|--bpffs} |
{-m|--mapcompat} | {-n|--nomount} }
• 1
• 2
• 3
• 4
• 5
• 6
• 7
与 《Linux内核观测技术BPF》一书中不同的是,我这个版本并不支持 feature 选项。
bpftool prog
bpftool prog 可以用来检查系统中运行程序的情况,在我的系统中执行相关命令得到了如下信息:
[root@debian-10:12:14:52] bpftool # bpftool prog show
3: cgroup_skb tag 7be49e3934a125ba gpl
loaded_at 2020-11-21T19:16:17+0800 uid 0
xlated 296B not jited memlock 4096B map_ids 2,3
4: cgroup_skb tag 2a142ef67aaad174 gpl
loaded_at 2020-11-21T19:16:17+0800 uid 0
xlated 296B not jited memlock 4096B map_ids 2,3
5: cgroup_skb tag 7be49e3934a125ba gpl
loaded_at 2020-11-21T19:16:17+0800 uid 0
xlated 296B not jited memlock 4096B map_ids 4,5
6: cgroup_skb tag 2a142ef67aaad174 gpl
loaded_at 2020-11-21T19:16:17+0800 uid 0
xlated 296B not jited memlock 4096B map_ids 4,5
7: cgroup_skb tag 7be49e3934a125ba gpl
loaded_at 2020-11-21T19:16:17+0800 uid 0
xlated 296B not jited memlock 4096B map_ids 6,7
8: cgroup_skb tag 2a142ef67aaad174 gpl
loaded_at 2020-11-21T19:16:17+0800 uid 0
xlated 296B not jited memlock 4096B map_ids 6,7
• 1
• 2
• 3
• 4
• 5
• 6
• 7
• 8
• 9
• 10
• 11
• 12
• 13
• 14
• 15
• 16
• 17
• 18
• 19
上面的输出中最左侧的数字表示程序标识符,可以用来获取更详细的信息。
可以使用 —json 来生成 json 格式的输出,同时指定 id 来过滤掉不需要的项目。
执行示例如下:
[root@debian-10:12:21:01] bpftool # bpftool prog show --json id 3 | jq
{
"id": 3,
"type": "cgroup_skb",
"tag": "7be49e3934a125ba",
"gpl_compatible": true,
"loaded_at": 1605957377,
"uid": 0,
"bytes_xlated": 296,
"jited": false,
"bytes_memlock": 4096,
"map_ids": [
2,
3
]
}
[root@debian-10:12:21:05] bpftool # bpftool prog show --json id 4 | jq
{
"id": 4,
"type": "cgroup_skb",
"tag": "2a142ef67aaad174",
"gpl_compatible": true,
"loaded_at": 1605957377,
"uid": 0,
"bytes_xlated": 296,
"jited": false,
"bytes_memlock": 4096,
"map_ids": [
2,
3
]
}
• 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
这个 jq 程序我的系统中并没有预装,可以执行下面这条命令来安装:
apt-get install jq
• 1
bpf prog dump
获取到了程序标识符后,可以执行 bpf prog dump 来获取整个程序的数据,能够看到由编译器生成的字节码。
我首先运行如下程序:
from bcc import BPF
bpf_source = """
int trace_go_main(struct pt_regs *ctx) {
u64 pid = bpf_get_current_pid_tgid();
bpf_trace_printk("New hello-bpf process running with PID: %d\\n", pid);
return 0;
}
"""
bpf = BPF(text = bpf_source)
bpf.attach_uprobe(name = "/home/longyu/linux-observability-with-bpf/code/chapter-4/uprobes/hello-bpf", sym = "main.main", fn_name = "trace_go_main")
bpf.trace_print()
• 1
• 2
• 3
• 4
• 5
• 6
• 7
• 8
• 9
• 10
• 11
• 12
• 13
再次执行 bpftool prog show 发现增加了如下信息:
39: kprobe name trace_go_main tag 76ea4b5c961566b3 gpl
loaded_at 2020-11-22T12:30:12+0800 uid 0
xlated 200B not jited memlock 4096B
• 1
• 2
• 3
然后我使用 bpftool prog dump xlated 来 dump id 为 39 的程序的内容,相关信息如下:
[root@debian-10:12:30:23] bpftool # bpftool prog dump xlated id 39
0: (85) call bpf_get_current_pid_tgid#84176
1: (b7) r1 = 680997
2: (63) *(u32 *)(r10 -8) = r1
3: (18) r1 = 0x203a444950206874
5: (7b) *(u64 *)(r10 -16) = r1
6: (18) r1 = 0x697720676e696e6e
8: (7b) *(u64 *)(r10 -24) = r1
9: (18) r1 = 0x757220737365636f
11: (7b) *(u64 *)(r10 -32) = r1
12: (18) r1 = 0x7270206670622d6f
14: (7b) *(u64 *)(r10 -40) = r1
15: (18) r1 = 0x6c6c65682077654e
17: (7b) *(u64 *)(r10 -48) = r1
18: (bf) r1 = r10
19: (07) r1 += -48
20: (b7) r2 = 44
21: (bf) r3 = r0
22: (85) call bpf_trace_printk#-46368
23: (b7) r0 = 0
24: (95) exit
• 1
• 2
• 3
• 4
• 5
• 6
• 7
• 8
• 9
• 10
• 11
• 12
• 13
• 14
• 15
• 16
• 17
• 18
• 19
• 20
• 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 中,相关内容如下:
BPF_CALL_0(bpf_get_current_pid_tgid)
{
struct task_struct *task = current;
if (unlikely(!task))
return -EINVAL;
return (u64) task->tgid << 32 | task->pid;
}
• 1
• 2
• 3
• 4
• 5
• 6
• 7
• 8
• 9
反汇编 helpers.o 得到了如下信息:
00000000000000c0 <bpf_get_current_pid_tgid>:
c0: e8 00 00 00 00 callq c5 <bpf_get_current_pid_tgid+0x5>
c5: 65 48 8b 14 25 00 00 mov %gs:0x0,%rdx
cc: 00 00
ce: 48 c7 c0 ea ff ff ff mov $0xffffffffffffffea,%rax
d5: 48 85 d2 test %rdx,%rdx
d8: 74 15 je ef <bpf_get_current_pid_tgid+0x2f>
da: 48 63 82 cc 04 00 00 movslq 0x4cc(%rdx),%rax
e1: 48 63 92 c8 04 00 00 movslq 0x4c8(%rdx),%rdx
e8: 48 c1 e0 20 shl $0x20,%rax
ec: 48 09 d0 or %rdx,%rax
ef: c3 retq
• 1
• 2
• 3
• 4
• 5
• 6
• 7
• 8
• 9
• 10
• 11
• 12
这个反汇编的结果能够证明此函数是非 bpf 指令,但是这个函数的反汇编信息看上去很奇怪,与标准函数的汇编代码有所区别。
写到这里我想到了一个问题,既然这个 bpf_get_current_pid_tgid 函数是 x86 的机器码,按照物理机器的执行过程,这个函数执行完了就要返回到调用点的下一行代码,也就是下面这行 bpf 指令:
1: (b7) r1 = 680997
• 1
想想从一个 x86 的机器码返回到 bpf 的机器码,这完全不科学。其实 bpf 的机器码也是用 x86 的机器码模拟的,这样这个问题就不存在了。
___bpf_prog_run 中对 bpf 的 call 指令进行下面的处理:
/* CALL */
JMP_CALL:
/* Function call scratches BPF_R1-BPF_R5 registers,
* preserves BPF_R6-BPF_R9, and stores return value
* into BPF_R0.
*/
BPF_R0 = (__bpf_call_base + insn->imm)(BPF_R1, BPF_R2, BPF_R3,
BPF_R4, BPF_R5);
CONT;
• 1
• 2
• 3
• 4
• 5
• 6
• 7
• 8
• 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 文件系统,这样当程序终止后它也会存在。
示例命令如下:
bpftool prog load ./bpf_program.o /sys/fs/bpf/bpf_prog
• 1
ls 查看 bpf_prog 文件,确定成功创建,信息如下;
[root@debian-10:15:47:50] hello_world # ls -lh /sys/fs/bpf/bpf_prog
-rw------- 1 root root 0 11月 22 15:44 /sys/fs/bpf/bpf_prog
• 1
• 2
使用 bpftool show 来观察程序仍旧存在,命令输出信息如下:
44: tracepoint name bpf_prog tag c6e8e35bea53af79 gpl
loaded_at 2020-11-22T15:44:25+0800 uid 0
xlated 112B not jited memlock 4096B
• 1
• 2
• 3
bpftool 检查 bpf 映射
bpftool 可以访问程序正在使用的 bpf 映射,命令类似上面 show 程序的形式。
执行示例如下:
[root@debian-10:15:49:55] hello_world # bpftool map show
2: lpm_trie flags 0x1
key 8B value 8B max_entries 1 memlock 4096B
3: lpm_trie flags 0x1
key 20B value 8B max_entries 1 memlock 4096B
4: lpm_trie flags 0x1
key 8B value 8B max_entries 1 memlock 4096B
5: lpm_trie flags 0x1
key 20B value 8B max_entries 1 memlock 4096B
6: lpm_trie flags 0x1
key 8B value 8B max_entries 1 memlock 4096B
7: lpm_trie flags 0x1
key 20B value 8B max_entries 1 memlock 4096B
• 1
• 2
• 3
• 4
• 5
• 6
• 7
• 8
• 9
• 10
• 11
• 12
• 13
bpftool 也可以用来创建、更新、查看映射信息,不进一步描述。
批量加载 bpftool 命令
bpftool 支持批处理模式,可以将命令写入到一个文件中,然后使用 bpftool batch file filepath 来执行。
在 /tmp/bpf 文件中写入如下两行:
prog show
map show
• 1
• 2
然后执行 bpftool batch file /tmp/bpf 来运行,操作记录如下:
[root@debian-10:15:57:14] hello_world # bpftool batch file /tmp/bpf
3: cgroup_skb tag 7be49e3934a125ba gpl
loaded_at 2020-11-21T19:16:17+0800 uid 0
xlated 296B not jited memlock 4096B map_ids 2,3
4: cgroup_skb tag 2a142ef67aaad174 gpl
loaded_at 2020-11-21T19:16:17+0800 uid 0
xlated 296B not jited memlock 4096B map_ids 2,3
..........
2: lpm_trie flags 0x1
key 8B value 8B max_entries 1 memlock 4096B
3: lpm_trie flags 0x1
key 20B value 8B max_entries 1 memlock 4096B
..........
processed 2 commands
• 1
• 2
• 3
• 4
• 5
• 6
• 7
• 8
• 9
• 10
• 11
• 12
• 13
• 14
总结
bpf 的核心是 bpf 虚拟机,对 bpf 相关程序的学习是为了研究 bpf 虚拟机做准备,在上手 bpftool 的过程中有了初步的研究,后续需要进一步深化!同时在看内核代码的时候发现 trace 模块是我一直没有看到的内容,貌似很多书中也都没有讲这部分内容,也需要抽时间学习一下!