eBPF
eBPF
目录
Try the Lab
先导
这个在线实验台在开始时会有些提示,这是因为这里要准备虚拟环境需要耗费比较多的时间(1min),给点东西给你看防止你那么闷。
但其实内容都是在官网中 “What is eBPF” 提到过了,没什么意义。
我会把这部分内容在 What is eBPF 笔记中重复一次,此处不再赘述
然后整个Lab实验一共有4个环节:
- 构建并运行 opensnoop
- 检查BPF对象文件
- 使用bpftool查看加载到内核中的BPF程序
- 添加您自己的跟踪消息
- eBPF入门测验
构建并运行 opensnoop
项目介绍
在左侧,您将看到一个选项卡 >_ 1️⃣ 终端 1。当前工作目录 ( /opt/ebpf/bcc/libbpf-tools
) 包含 BCC 项目中许多可观察性工具的源代码。
这个工作目录是开源的,是 iovisor/bcc 项目。
Makefile cachestat.bpf.c fsdist.c mountsnoop.c slabratetop.h tcptop.bpf.c
Makefile.btfgen cachestat.c fsdist.h mountsnoop.h softirqs.bpf.c tcptop.c
README.md capable.bpf.c fsslower.bpf.c numamove.bpf.c softirqs.c tcptop.h
arm64 capable.c fsslower.c numamove.c softirqs.h tcptracer.bpf.c
bashreadline.bpf.c capable.h fsslower.h offcputime.bpf.c solisten.bpf.c tcptracer.c
bashreadline.c compat.bpf.h funclatency.bpf.c offcputime.c solisten.c tcptracer.h
bashreadline.h compat.c funclatency.c offcputime.h solisten.h trace_helpers.c
bindsnoop.bpf.c compat.h funclatency.h oomkill.bpf.c stat.h trace_helpers.h
bindsnoop.c core_fixes.bpf.h gethostlatency.bpf.c oomkill.c statsnoop.bpf.c uprobe_helpers.c
bindsnoop.h cpudist.bpf.c gethostlatency.c oomkill.h statsnoop.c uprobe_helpers.h
biolatency.bpf.c cpudist.c gethostlatency.h opensnoop.bpf.c statsnoop.h vfsstat.bpf.c
biolatency.c cpudist.h hardirqs.bpf.c opensnoop.c syscall_helpers.c vfsstat.c
biolatency.h cpufreq.bpf.c hardirqs.c opensnoop.h syscall_helpers.h vfsstat.h
biopattern.bpf.c cpufreq.c hardirqs.h powerpc syscount.bpf.c wakeuptime.bpf.c
biopattern.c cpufreq.h javagc.bpf.c readahead.bpf.c syscount.c wakeuptime.c
biopattern.h drsnoop.bpf.c javagc.c readahead.c syscount.h wakeuptime.h
biosnoop.bpf.c drsnoop.c javagc.h readahead.h tcpconnect.bpf.c x86
biosnoop.c drsnoop.h kernel.config riscv tcpconnect.c
biosnoop.h drsnoop_example.txt klockstat.bpf.c runqlat.bpf.c tcpconnect.h
biostacks.bpf.c errno_helpers.c klockstat.c runqlat.c tcpconnlat.bpf.c
biostacks.c errno_helpers.h klockstat.h runqlat.h tcpconnlat.c
biostacks.h execsnoop.bpf.c ksnoop.bpf.c runqlen.bpf.c tcpconnlat.h
biotop.bpf.c execsnoop.c ksnoop.c runqlen.c tcplife.bpf.c
biotop.c execsnoop.h ksnoop.h runqlen.h tcplife.c
biotop.h exitsnoop.bpf.c llcstat.bpf.c runqslower.bpf.c tcplife.h
bitesize.bpf.c exitsnoop.c llcstat.c runqslower.c tcprtt.bpf.c
bitesize.c exitsnoop.h llcstat.h runqslower.h tcprtt.c
bitesize.h filelife.bpf.c map_helpers.c runqslower_example.txt tcprtt.h
bits.bpf.h filelife.c map_helpers.h sigsnoop.bpf.c tcpstates.bpf.c
blazesym filelife.h maps.bpf.h sigsnoop.c tcpstates.c
blk_types.h filetop.bpf.c mdflush.bpf.c sigsnoop.h tcpstates.h
bpftool filetop.c mdflush.c sigsnoop_example.txt tcpsynbl.bpf.c
btf_helpers.c filetop.h mdflush.h slabratetop.bpf.c tcpsynbl.c
btf_helpers.h fsdist.bpf.c mountsnoop.bpf.c slabratetop.c tcpsynbl.h
由于其简单性,我们将在本示例中只使用其中的 opensnoop
。opensnoop
可以跟踪系统范围内的 open()
系统调用,并打印各种详细信息。因此,它是展示简单 eBPF 用例的优秀第一个程序。
程序组成
eBPF 应用程序通常由至少两部分组成:
- 一个用户空间程序(USP),它声明内核空间程序并将其附加到相关的跟踪点/探针。
- 一旦满足跟踪点/探针,内核空间程序(KSP)就会被触发并在内核内部运行。这是实际的 eBPF 逻辑实现的地方。
由于这两个程序无法直接相互通信(根据设计),因此它们需要一个缓冲区来交换数据。对于 eBPF,它是通过 不同类型的 BPF 映射 实现的。
构建
让我们构建可执行文件,这大约需要 15 秒,然后会生成一个opensnoop可执行文件
make opensnoop
运行
要实际运行已编译的 opensnoop
二进制文件,需要 CAP_BPF
Linux 功能。这是强制性的,因为我们的逻辑使用特权 BPF 操作(例如,将 eBPF 代码加载到内核中),同时许多 Linux 发行版不允许非特权 eBPF。 CAP_BPF
自 Linux 内核 5.8 起可用,允许加载所有类型的 BPF 程序、创建大多数地图类型、加载 BTF、迭代程序和地图。引入它是为了将 BPF 功能与重载的 CAP_SYS_ADMIN
功能分开。
然而,由于在这个演示环境中我们无论如何都是以 root 身份运行的,所以这不是问题。运行 opensnoop
:
./opensnoop
使用程序
opensnoop
现在将在每次打开文件时显示输出。但由于像我们这样的小型虚拟机中几乎没有发生任何事情,因此我们将生成一些事件。
顶部左侧,切换到第二个>_2️⃣Terminal 2选项卡,执行:
cat /etc/os-release
# 输出:
PRETTY_NAME="Ubuntu 22.04.2 LTS"
NAME="Ubuntu"
VERSION_ID="22.04"
VERSION="22.04.2 LTS (Jammy Jellyfish)"
VERSION_CODENAME=jammy
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=jammy
切换回第一个>_1️⃣Terminal 1选项卡,查看输出:已经访问了多个文件,直到 cat
最终输出文件内容。
PID COMM FD ERR PATH
2903 cat 3 0 /etc/ld.so.cache
2902 opensnoop 21 0 /etc/localtime
2903 cat 3 0 /lib/x86_64-linux-gnu/libc.so.6
2903 cat 3 0 /usr/lib/locale/locale-archive
2903 cat 3 0 /usr/share/locale/locale.alias
2903 cat -1 2 /usr/lib/locale/C.UTF-8/LC_IDENTIFICATION
2903 cat 3 0 /usr/lib/locale/C.utf8/LC_IDENTIFICATION
2903 cat 3 0 /usr/lib/x86_64-linux-gnu/gconv/gconv-modules.cache
2903 cat -1 2 /usr/lib/locale/C.UTF-8/LC_MEASUREMENT
2903 cat 3 0 /usr/lib/locale/C.utf8/LC_MEASUREMENT
2903 cat -1 2 /usr/lib/locale/C.UTF-8/LC_TELEPHONE
2903 cat 3 0 /usr/lib/locale/C.utf8/LC_TELEPHONE
2903 cat -1 2 /usr/lib/locale/C.UTF-8/LC_ADDRESS
2903 cat 3 0 /usr/lib/locale/C.utf8/LC_ADDRESS
2903 cat -1 2 /usr/lib/locale/C.UTF-8/LC_NAME
2903 cat 3 0 /usr/lib/locale/C.utf8/LC_NAME
2903 cat -1 2 /usr/lib/locale/C.UTF-8/LC_PAPER
2903 cat 3 0 /usr/lib/locale/C.utf8/LC_PAPER
2903 cat -1 2 /usr/lib/locale/C.UTF-8/LC_MESSAGES
2903 cat 3 0 /usr/lib/locale/C.utf8/LC_MESSAGES
2903 cat 3 0 /usr/lib/locale/C.utf8/LC_MESSAGES/SYS_LC_MESSAGES
2903 cat -1 2 /usr/lib/locale/C.UTF-8/LC_MONETARY
2903 cat 3 0 /usr/lib/locale/C.utf8/LC_MONETARY
2903 cat -1 2 /usr/lib/locale/C.UTF-8/LC_COLLATE
2903 cat 3 0 /usr/lib/locale/C.utf8/LC_COLLATE
2903 cat -1 2 /usr/lib/locale/C.UTF-8/LC_TIME
2903 cat 3 0 /usr/lib/locale/C.utf8/LC_TIME
2903 cat -1 2 /usr/lib/locale/C.UTF-8/LC_NUMERIC
2903 cat 3 0 /usr/lib/locale/C.utf8/LC_NUMERIC
2903 cat -1 2 /usr/lib/locale/C.UTF-8/LC_CTYPE
2903 cat 3 0 /usr/lib/locale/C.utf8/LC_CTYPE
2903 cat 3 0 /etc/os-release
2495 bash 3 0 /root/.bash_history
如果您保持 opensnoop
运行,您有时可能会看到虚拟机上运行的其他进程生成的输出,例如 systemd
。
现在,您可以单击“检查”按钮并转到下一部分,我们将在其中进行更深入的研究。
检查BPF对象文件
笔记者:这部分 readelf 应该在汇编分析里是常规操作了,我不太熟悉,另外找了些资料:
- readelf是一个用于查看二进制可执行文件和共享库的程序。
它可以用来 查看 ELF(Executable and Linking Format)文件的信息 - ELF是一种用于Unix类操作系统中可执行文件和共享库的标准文件格式,它是一种自描述、可重定位、可扩展、可升级、跨平台的文件格式。
内容包含了:- 文件头信息
- 程序指令、数据
- 程序头表
- 节头表
- 符号表
- 动态节表
- 重定向表
- ……等各种详细信息
- .o文件是目标文件,包含了编译器编译源代码产生的汇编代码和符号表等信息,它通常是被编译器转换成可执行文件或库文件的中间过程。
因此,.o文件实际上是一种未完成的ELF文件。
它包含了ELF文件的部分信息,包括文件头、节头表、段数据等,这也是为什么可以使用readelf命令查看.o文件的原因。通过查看.o文件的信息,可以获得编译器将源代码和库文件编译为可执行文件或共享库的一些提示,比如符号表、重定位信息等,对于程序调试和分析非常有用。
readelf
在左侧的 >_ 1️⃣ 终端中,使用 readelf
检查我们之前作为 make
命令的一部分构建的 BPF 对象文件。
readelf --section-details --headers .output/opensnoop.bpf.o
# 输出:
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: Linux BPF
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 11960 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 20
Section header string table index: 1
Section Headers:
[Nr] Name
Type Address Offset Link
Size EntSize Info Align
Flags
[ 0]
NULL 0000000000000000 0000000000000000 0
0000000000000000 0000000000000000 0 0
[0000000000000000]:
[ 1] .strtab
STRTAB 0000000000000000 0000000000002c93 0
0000000000000224 0000000000000000 0 1
[0000000000000000]:
[ 2] .text
PROGBITS 0000000000000000 0000000000000040 0
0000000000000000 0000000000000000 0 4
[0000000000000006]: ALLOC, EXEC
[ 3] tracepoint/syscalls/sys_enter_open
PROGBITS 0000000000000000 0000000000000040 0
0000000000000170 0000000000000000 0 8
[0000000000000006]: ALLOC, EXEC
[ 4] .reltracepoint/syscalls/sys_enter_open
REL 0000000000000000 00000000000022d8 19
0000000000000040 0000000000000010 3 8
[0000000000000040]: INFO LINK
[ 5] tracepoint/syscalls/sys_enter_openat
PROGBITS 0000000000000000 00000000000001b0 0
0000000000000170 0000000000000000 0 8
[0000000000000006]: ALLOC, EXEC
[ 6] .reltracepoint/syscalls/sys_enter_openat
REL 0000000000000000 0000000000002318 19
0000000000000040 0000000000000010 5 8
[0000000000000040]: INFO LINK
[ 7] tracepoint/syscalls/sys_exit_open
PROGBITS 0000000000000000 0000000000000320 0
0000000000000330 0000000000000000 0 8
[0000000000000006]: ALLOC, EXEC
[ 8] .reltracepoint/syscalls/sys_exit_open
REL 0000000000000000 0000000000002358 19
0000000000000040 0000000000000010 7 8
[0000000000000040]: INFO LINK
[ 9] tracepoint/syscalls/sys_exit_openat
PROGBITS 0000000000000000 0000000000000650 0
0000000000000330 0000000000000000 0 8
[0000000000000006]: ALLOC, EXEC
[10] .reltracepoint/syscalls/sys_exit_openat
REL 0000000000000000 0000000000002398 19
0000000000000040 0000000000000010 9 8
[0000000000000040]: INFO LINK
[11] .rodata
PROGBITS 0000000000000000 0000000000000980 0
000000000000000d 0000000000000000 0 4
[0000000000000002]: ALLOC
[12] .maps
PROGBITS 0000000000000000 0000000000000990 0
0000000000000038 0000000000000000 0 8
[0000000000000003]: WRITE, ALLOC
[13] license
PROGBITS 0000000000000000 00000000000009c8 0
0000000000000004 0000000000000000 0 1
[0000000000000003]: WRITE, ALLOC
[14] .BTF
PROGBITS 0000000000000000 00000000000009cc 0
0000000000000d8e 0000000000000000 0 4
[0000000000000000]:
[15] .rel.BTF
REL 0000000000000000 00000000000023d8 19
0000000000000070 0000000000000010 14 8
[0000000000000040]: INFO LINK
[16] .BTF.ext
PROGBITS 0000000000000000 000000000000175c 0
00000000000008ac 0000000000000000 0 4
[0000000000000000]:
[17] .rel.BTF.ext
REL 0000000000000000 0000000000002448 19
0000000000000840 0000000000000010 16 8
[0000000000000040]: INFO LINK
[18] .llvm_addrsig
LOOS+0xfff4c03 0000000000000000 0000000000002c88 0
000000000000000b 0000000000000000 0 1
[0000000080000000]: EXCLUDE
[19] .symtab
SYMTAB 0000000000000000 0000000000002008 1
00000000000002d0 0000000000000018 19 8
[0000000000000000]:
ELF格式
目标文件为 ELF 格式(Executable and Linkable Format,可执行和可链接格式)。
它代表可执行文件、目标代码、共享库和核心转储的通用标准文件格式。它也是 x86 处理器上二进制文件的标准文件格式。
四个可执行节
需要观察一些有趣的事情:
- 机器是
Linux BPF
。因此,这个二进制代码应该在 BPF 内核虚拟机内运行。 - 该文件中包含 BTF 信息。 BTF 是元数据格式,对与 BPF 程序/映射相关的调试信息进行编码。此调试信息用于地图漂亮打印、函数签名等。
- 在表中名为
.text
的节头 (对应[2]那里) 之后,有四个以tracepoint
开头的可执行节。它们对应于四个 BPF 程序 (对应[3~10, 8条,每个追踪点2条])。
让我们在 BPF 源代码中找到这四个程序。在第二个选项卡 编辑器中,您可以打开文件 opensnoop.bpf.c
- 我们的内核空间程序 (KSP)。
向下滚动找到四个不同的函数,
// 其名称以 `int tracepoint__syscalls...` 开头。您应该在第 50、68、125 和 131 行找到它们
/** 程序解释
* SEC()宏:
* 对应于 `readelf` 列出的可执行部分,定义了代码应附加到的 eBPF [钩子](https://ebpf.io/what-is-ebpf/#hook-overview)
* 固定格式:SEC("tracepoint/<category>/<name>
* 跟踪点:
* eBPF 跟踪点 这里设置了4个
* 每当发出 open()/enpenat() 系统调用时,就会执行相应的eBPF代码函数。
* 跟踪点是内核代码中的静态标记,可用于在正在运行的内核中附加 (eBPF) 代码。这些跟踪点通常放置在有趣的位置或常见的位置来测量性能。
* 四个可执行点:
* 每当发出 open()/enpenat() 系统调用时,就会执行相应的eBPF代码函数。然后解析调用的参数(文件名等)并将此信息写入 BPF 映射。
* 从 BPF映射 那里,我们编译的 `opensnoop.c` 二进制部分 - 我们的用户空间程序 (USP) - 可以读取它并将其打印到 STDOUT
*/
SEC("tracepoint/syscalls/sys_enter_open")
int tracepoint__syscalls__sys_enter_open(struct trace_event_raw_sys_enter* ctx) {...} // 50行
SEC("tracepoint/syscalls/sys_enter_openat")
int tracepoint__syscalls__sys_enter_openat(struct trace_event_raw_sys_enter* ctx) {...} // 68行
SEC("tracepoint/syscalls/sys_exit_open")
int tracepoint__syscalls__sys_exit_open(struct trace_event_raw_sys_exit* ctx) {...} // 125行
SEC("tracepoint/syscalls/sys_exit_openat")
int tracepoint__syscalls__sys_exit_openat(struct trace_event_raw_sys_exit* ctx) {...} // 131行
其他
如果您想了解有关这四个 BPF 程序正在做什么的更多详细信息,请参阅 Liz Rice 所著的 《What is eBPF ?》by Liz Rice 一书的第 3 章。
但这如何与 Linux 内核结合在一起呢?单击“下一步”以了解下一部分的内容。
使用 bpftool
查看加载到内核中的BPF程序
查看内核中的 BPF 程序:现在我们知道 BPF 代码正在运行,让我们看一下内核方面的事情。为此,我们将使用 bpftool
来查看我们已加载到内核中的内容。
当前 opensnoop
未运行。让我们看看我们的机器上是否有运行任何 eBPF 程序。我们可以利用 bpftool
来实现这一点。
bpftool是与 eBPF 一起使用的瑞士军刀。它可以在 GitHub 上找到,或者对于某些 Linux 发行版(例如 Ubuntu 和 Fedora),可以直接在 Linux 内核存储库中找到。
查看运行的eBPF程序 bpftool prog list
bpftool prog list
# 输出
65: cgroup_device tag 28a890580b33b0dc gpl
loaded_at 2023-07-19T05:34:43+0000 uid 0
xlated 560B jited 352B memlock 4096B
pids systemd(1)
66: cgroup_device tag c8b47a902f1cc68b gpl
loaded_at 2023-07-19T05:34:43+0000 uid 0
xlated 464B jited 289B memlock 4096B
pids systemd(1)
70: cgroup_device tag e3dbd137be8d6168 gpl
loaded_at 2023-07-19T05:34:43+0000 uid 0
xlated 504B jited 310B memlock 4096B
pids systemd(1)
71: cgroup_device tag 0ecd07b7b633809f gpl
loaded_at 2023-07-19T05:34:43+0000 uid 0
xlated 496B jited 308B memlock 4096B
pids systemd(1)
72: cgroup_skb tag 6deef7357e7b4530 gpl
loaded_at 2023-07-19T05:34:43+0000 uid 0
xlated 64B jited 55B memlock 4096B
pids systemd(1)
73: cgroup_skb tag 6deef7357e7b4530 gpl
loaded_at 2023-07-19T05:34:43+0000 uid 0
xlated 64B jited 55B memlock 4096B
pids systemd(1)
74: cgroup_device tag e3dbd137be8d6168 gpl
loaded_at 2023-07-19T05:34:43+0000 uid 0
xlated 504B jited 310B memlock 4096B
pids systemd(1)
75: cgroup_skb tag 6deef7357e7b4530 gpl
loaded_at 2023-07-19T05:34:43+0000 uid 0
xlated 64B jited 55B memlock 4096B
pids systemd(1)
76: cgroup_skb tag 6deef7357e7b4530 gpl
loaded_at 2023-07-19T05:34:43+0000 uid 0
xlated 64B jited 55B memlock 4096B
pids systemd(1)
80: cgroup_device tag 8b9c33f36f812014 gpl
loaded_at 2023-07-19T05:34:43+0000 uid 0
xlated 744B jited 448B memlock 4096B
pids systemd(1)
81: cgroup_skb tag 6deef7357e7b4530 gpl
loaded_at 2023-07-19T05:34:43+0000 uid 0
xlated 64B jited 55B memlock 4096B
pids systemd(1)
82: cgroup_skb tag 6deef7357e7b4530 gpl
loaded_at 2023-07-19T05:34:43+0000 uid 0
xlated 64B jited 55B memlock 4096B
pids systemd(1)
一些解释:
- 您应该看到两种条目:
cgroup_skb
和cgroup_device
。两者均由 Systemd 组件 systemd.resource-control 管理,并用于管理 Systemd 单元对网络设备(请参阅示例)和文件系统的访问。 - 目前不应有任何
tracepoint
类型的条目
在第二个终端,我们运行 opensnoop
:
./opensnoop
当它运行时,切换回第一个终端,然后重新运行:
bpftool prog list
# 输出(相较于第一次调用时,多出来了四个追踪点)
98: tracepoint name tracepoint__syscalls__sys_enter_open tag 07014be5359438f8 gpl
loaded_at 2023-07-19T05:54:41+0000 uid 0
xlated 240B jited 137B memlock 4096B map_ids 9,6
btf_id 60
pids opensnoop(2941)
100: tracepoint name tracepoint__syscalls__sys_enter_openat tag 8ee3432dcd98ffc3 gpl
loaded_at 2023-07-19T05:54:41+0000 uid 0
xlated 240B jited 137B memlock 4096B map_ids 9,6
btf_id 60
pids opensnoop(2941)
101: tracepoint name tracepoint__syscalls__sys_exit_open tag 37f628f9e857b071 gpl
loaded_at 2023-07-19T05:54:41+0000 uid 0
xlated 792B jited 546B memlock 4096B map_ids 6,9,7
btf_id 60
pids opensnoop(2941)
102: tracepoint name tracepoint__syscalls__sys_exit_openat tag 37f628f9e857b071 gpl
loaded_at 2023-07-19T05:54:41+0000 uid 0
xlated 792B jited 546B memlock 4096B map_ids 6,9,7
btf_id 60
pids opensnoop(2941)
您会看到另外加载了四个 BPF 程序!这些对应于前面提到的四个 opensnoop BPF 程序。
请注意,名称被截断,因此您无法真正看到哪个是用于进入或退出的。
但是,它们每个都引用两个或三个地图 ID,例如 map_ids 11,8
(数字可能不同)。让我们利用这些信息吧!
加载到内核中的映射 bpftool map list
当 opensnoop 仍在第二个 >_2️⃣ 终端 2 中运行时,留在第一个 >_1️⃣ 终端 1 中并观察加载到内核中的映射
bpftool map list
# 输出,输出显示了一些现有的 BPF 映射
6: hash name start flags 0x0
key 4B value 16B max_entries 10240 memlock 245760B
btf_id 60
pids opensnoop(2941)
7: perf_event_array name events flags 0x0
key 4B value 4B max_entries 1 memlock 4096B
pids opensnoop(2941)
9: array name opensnoo.rodata flags 0x480
key 4B value 13B max_entries 1 memlock 4096B
btf_id 60 frozen
pids opensnoop(2941)
17: array name pid_iter.rodata flags 0x480
key 4B value 4B max_entries 1 memlock 4096B
btf_id 76 frozen
pids bpftool(2951)
18: array name libbpf_det_bind flags 0x0
key 4B value 32B max_entries 1 memlock 4096B
一些解释:
- 观察到有一个名为
start
的哈希表和一个名为events
的性能事件数组。这些在源代码中的~/bcc/libbpf-tools/opensnoop.bpf.c
第 13-24 行中定义。在 编辑器中查看一下。 - 还有一个用于 opensnoop 只读数据的数组 (
array name opensnoo.rodata
)。 - 另请注意,每行开头的地图 ID 与之前
bpftool prog list
引用的 ID 相对应。
观察程序字节码 bpftool prog dump xlated id 98 linum
让我们看一下其中一个程序的字节码。为此,让我们再次查看一下 prog 列表。在 >_ 1️⃣ 终端 1 中执行:
bpftool prog list
在每行的开头,您可以看到相应 BPF 程序的 ID。获取 tracepoint
程序的 ID,并转储字节码(在我们的运行中,编号为 46
,您的编号可能不同):
bpftool prog dump xlated id 98 linum
# 输出
int tracepoint__syscalls__sys_enter_open(struct trace_event_raw_sys_enter * ctx):
; int tracepoint__syscalls__sys_enter_open(struct trace_event_raw_sys_enter* ctx) [file:/opt/ebpf/bcc/libbpf-tools/opensnoop.bpf.c line_num:50 line_col:0]
0: (bf) r6 = r1
; u64 id = bpf_get_current_pid_tgid(); [file:/opt/ebpf/bcc/libbpf-tools/opensnoop.bpf.c line_num:52 line_col:11]
1: (85) call bpf_get_current_pid_tgid#186208
; u32 pid = id; [file:/opt/ebpf/bcc/libbpf-tools/opensnoop.bpf.c line_num:55 line_col:6]
2: (63) *(u32 *)(r10 -4) = r0
; if (targ_tgid && targ_tgid != tgid) [file:/opt/ebpf/bcc/libbpf-tools/opensnoop.bpf.c line_num:36 line_col:6]
3: (18) r1 = map[id:9][0]+4
5: (61) r2 = *(u32 *)(r1 +0)
; if (targ_pid && targ_pid != pid) [file:/opt/ebpf/bcc/libbpf-tools/opensnoop.bpf.c line_num:38 line_col:6]
6: (18) r1 = map[id:9][0]+0
8: (61) r2 = *(u32 *)(r1 +0)
; if (valid_uid(targ_uid)) { [file:/opt/ebpf/bcc/libbpf-tools/opensnoop.bpf.c line_num:40 line_col:16]
9: (18) r7 = map[id:9][0]+8
11: (61) r1 = *(u32 *)(r7 +0)
12: (18) r2 = 0xffffffff
; if (targ_uid != uid) { [file:/opt/ebpf/bcc/libbpf-tools/opensnoop.bpf.c line_num:42 line_col:7]
14: (b7) r1 = 0
; struct args_t args = {}; [file:/opt/ebpf/bcc/libbpf-tools/opensnoop.bpf.c line_num:59 line_col:17]
15: (7b) *(u64 *)(r10 -16) = r1
; args.fname = (const char *)ctx->args[0]; [file:/opt/ebpf/bcc/libbpf-tools/opensnoop.bpf.c line_num:60 line_col:30]
16: (79) r1 = *(u64 *)(r6 +16)
; args.fname = (const char *)ctx->args[0]; [file:/opt/ebpf/bcc/libbpf-tools/opensnoop.bpf.c line_num:60 line_col:14]
17: (7b) *(u64 *)(r10 -24) = r1
; args.flags = (int)ctx->args[1]; [file:/opt/ebpf/bcc/libbpf-tools/opensnoop.bpf.c line_num:61 line_col:21]
18: (79) r1 = *(u64 *)(r6 +24)
; args.flags = (int)ctx->args[1]; [file:/opt/ebpf/bcc/libbpf-tools/opensnoop.bpf.c line_num:61 line_col:14]
19: (63) *(u32 *)(r10 -16) = r1
20: (bf) r2 = r10
; struct args_t args = {}; [file:/opt/ebpf/bcc/libbpf-tools/opensnoop.bpf.c line_num:59 line_col:17]
21: (07) r2 += -4
22: (bf) r3 = r10
23: (07) r3 += -24
; bpf_map_update_elem(&start, &pid, &args, 0); [file:/opt/ebpf/bcc/libbpf-tools/opensnoop.bpf.c line_num:62 line_col:3]
24: (18) r1 = map[id:6]
26: (b7) r4 = 0
27: (85) call htab_map_update_elem#220464
; return 0; [file:/opt/ebpf/bcc/libbpf-tools/opensnoop.bpf.c line_num:64 line_col:2]
28: (b7) r0 = 0
29: (95) exit
如您所见,这显示了有关源代码的信息。切换到 编辑器选项卡并打开文件 opensnoop.bpf.c
。将 >_1️⃣ 终端 1 中输出的行号与源代码文件中的行号进行比较,看看它们是否匹配。这使得比较 BPF 编译对象和它背后的源代码变得相当容易!
(与原代码相比,这里有一些区别)
能够编写自己的 BPF 代码不是很棒吗?单击下一步并执行此操作!
添加您自己的跟踪消息
编写我们自己的代码:现在我们知道如何运行 eBPF 工具,如何观察它们的行为,检查内核中加载的内容,甚至获取与实际源代码相比实际发生的情况的信息。接下来,我们要编写实际的代码!为此,我们将把我们自己的跟踪消息添加到代码中!
两种调试打印方法
eBPF 程序可以出于调试目的编写跟踪消息。对于通常通过 trace_pipe
完成的快速示例,可以从 /sys/kernel/debug/tracing/trace_pipe
读取。
但是,它有一些限制:最大 3 个参数,trace_pipe 是全局共享的(因此并发程序将产生冲突的输出),等等。
因此,您不应该将其用于高效的 eBPF 代码。您应该通过 BPF_PERF_OUTPUT()
界面来完成此操作。
尽管如此,为了简单起见,我们在本实验中通过 trace_pipe
进行操作,并将我们自己的消息添加到 opensnoop
中。
修改程序并重新运行
在 编辑器选项卡的文件列表左侧,向下滚动并选择文件 opensnoop.bpf.c
。找到 cleanup:
标签,它应该位于第 119 行左右。在其中,在其上方添加以下行 bpf_printk("Hello world");
,修改后文件应如下所示:
/* emit event */
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU,
&event, sizeof(event));
bpf_printk("Hello world"); // 添加这一行,其他不动
cleanup: // 原来处于第119行
bpf_map_delete_elem(&start, &pid);
return 0;
}
单击 编辑器窗口顶部选项卡中的 💾 保存文件。
在 >_ 1️⃣ Terminal 1 选项卡中,重建现在更改的 opensnoop
:
make opensnoop
接下来,在执行新构建之前,开始读取第二个>_2️⃣终端 2 中的内核跟踪输出文件:
cat /sys/kernel/debug/tracing/trace_pipe
开始时不会显示任何输出 - 它正在等待数据
现在,在第一个>_1️⃣终端1中,运行 opensnoop
:
./opensnoop
现在我们只需要生成文件访问权限。在 编辑器中,单击任意文件。在编辑器中加载文件会生成很多事件。
切换到 >_ 1️⃣ 运行 opensnoop
的终端 1,观察有大量文件访问。
转到第二个 >_ 2️⃣ 终端 2,我们的 cat
进程仍在运行,并观察到打印了很多跟踪消息!
sandbox-agent-1778 [000] d...1 2066.381919: bpf_trace_printk: Hello world
sandbox-agent-1778 [000] d...1 2066.381932: bpf_trace_printk: Hello world
sandbox-agent-1778 [000] d...1 2066.381951: bpf_trace_printk: Hello world
sandbox-agent-1778 [000] d...1 2066.381970: bpf_trace_printk: Hello world
sandbox-agent-1778 [000] d...1 2066.381989: bpf_trace_printk: Hello world
补充
恭喜,您已经编写了一些 eBPF 代码并在内核中运行它!
请注意,除了显示您定义的字符串外,跟踪行还包括其他有用的上下文信息 - 例如,可执行文件的名称和触发运行程序的事件的进程 ID - 在本例中为 sandbox-agent
运行。
这说明了 eBPF 程序如何收集有关触发它的事件的有用信息 - 例如,可以出于可观察性目的将其报告给用户空间。
eBPF入门测验
(蒙了几次才蒙对了)
- ❌ eBPF 程序始终需要 Linux 功能“CAP_BPF” 和 “CAP_SYS_ADMIN”
- ✅ ELF 代表 x86 架构上可执行文件的通用标准文件格式
- ✅ eBPF 程序使用 BPF 映射与用户空间应用程序交换数据
- ❌ “BPF_PERF_OUTPUT()” 接口可用于通过向用户空间提供消息来对内核空间程序进行故障排除。
- ✅ 每当用户空间程序尝试与内核空间程序交换数据时,它们都需要使用 eBPF 映射