目录
此职位作为草稿留了很长时间。它的大部分内容是在2017年12月编写的。我希望它能在今天发布,尽管Cilium指南也涵盖了该功能。
eBPF(扩展的Berkeley数据包过滤器)相对于旧BPF版本(对于经典BPF而言,是cBPF)最有用的发展之一就是基于clang和LLVM的后端的可用性,从而可以从C源代码生成eBPF字节码。1个
从C到目标文件
例如,可以从以下代码编译返回零的简单eBPF程序:
$ cat my_bpf_program.c
int func()
{
return 0;
}
命令行如下所示:
$ clang -target bpf -Wall -O2 -c my_bpf_program.c -o my_bpf_objfile.o
注意:某些程序比本示例更先进,可能需要将该-mcpu
选项传递 给llc
,并使用更接近以下命令的内容:
$ clang -O2 -emit-llvm -c my_bpf_program.c -o - | \
llc -march=bpf -mcpu=probe -filetype=obj -o my_bpf_objfile.o
这将以ELF格式创建一个目标文件,其中包含已编译的字节码。默认情况下,代码在.text
ELF部分下。让我们转储它:
$ readelf -x .text my_bpf_objfile.o
Hex dump of section '.text':
0x00000000 b7000000 00000000 95000000 00000000 ................
有效!我们在这里有两个eBPF指令:
b7 0 0 0000 00000000 # r0 = 0
95 0 0 0000 00000000 # exit and return r0
如果您不熟悉eBPF汇编语法,则可能对此简短参考资料 (或 完整的文档,但内容密集)感兴趣 。
深入说明
从C编译为eBPF字节码作为目标文件非常有用。生成的ELF文件可直接用于将程序附加到各种钩子上:TC,XDP,kprobes等。将高级程序作为字节码编写将非常耗时,并且在2020年我完成对本文的编辑时,将更加复杂诸如 CO-RE之类的功能 根本无法手动完成。Clang和LLVM是eBPF工作流程的组成部分。
但是,对于需要测试非常特定的eBPF指令序列或微调程序特定方面的人员来说,这可能不是一个方便的解决方案。如果我想修改说明怎么办?如果我想在程序末尾添加第三条eBPF指令,以便在程序退出后为寄存器r0设置另一个值,该怎么办?当然,这个例子没有用,但是我仍然可以尝试一下!
为此,我们可以:
-
从头开始用eBPF字节码编写一个eBPF程序。这是完全可行的,但可能又长又乏味,而且绝对不友好。为了保持与tc之类工具的兼容性,无论如何该程序都必须转换为目标文件,这为该过程增加了额外的步骤。
-
使用汇编语言从头开始编写程序,至少不要以字节码编写程序,然后使用专用的汇编器对其进行编译(例如:
ebpf_asm
在Python中,由Solarflare编写 )。
作为LLVM近期改进的一部分,出现了另一个解决方案,我们还可以:
- 从C编译为eBPF汇编语言。编辑程序集,然后将其作为字节码组装到目标文件中。
Clang和LLVM现在可以做到这一点!在一侧生成程序的可读版本,然后在另一侧进行组装。奖励: llvm-objdump
甚至可以用来转储包含在目标文件中的程序。
eBPF与LLVM循序渐进
要使用本节中介绍的所有clang和LLVM功能,您需要在6.0版或更高版本中使用这些工具。当我开始起草本文时,它是开发分支,但是到我完成它时,版本10已经发布了,因此应该没有问题。
从C编译到eBPF程序集
让我们用clang编译从C到eBPF程序集的程序。实际上,这与通常使用C源代码为处理器生成汇编程序的方式相同,只是您告诉clang目标是 bpf
。2
$ cat bpf.c
int func()
{
return 0;
}
$ clang -target bpf -S -o bpf.s bpf.c
$ cat bpf.s
.text
.globl func # -- Begin function func
.p2align 3
func: # @func
# %bb.0:
r1 = 0
*(u32 *)(r10 - 4) = r1
r0 = r1
exit
# -- End function
太好了,现在让我们对其进行修改并在底部添加我们的说明!
$ sed -i '$a \\tr0 = 3' bpf.s
$ cat bpf.s
.text
.globl func # -- Begin function func
.p2align 3
func: # @func
# %bb.0:
r1 = 0
*(u32 *)(r10 - 4) = r1
r0 = r1
exit
# -- End function
r0 = 3
组装到ELF目标文件
我们可以将该文件汇编为包含该程序字节码的ELF目标文件。它需要llvm-mc
处理机器代码并与LLVM一起提供的工具。
$ llvm-mc -triple bpf -filetype=obj -o bpf.o foo.s
我们有ELF文件!让我们转储字节码:
$ readelf -x .text my_bpf_objfile.o
Hex dump of section '.text':
0x00000000 b7010000 00000000 631afcff 00000000 ........c.......
0x00000010 bf100000 00000000 95000000 00000000 ................
0x00000020 b7000000 03000000 b7000000 03000000 ................
相对于程序的原始版本,前两个说明没有变化。第三条指令对应于我们添加到汇编文件中的内容:它将3加载到寄存器r0中。我们已成功编辑了说明。例如,我们现在可以使用将程序加载到内核中bpftool
。任务完成!
llvm-objdump的人性化输出
请注意,LLVM还提供了一种以人类可读的方式转储eBPF对象文件的方法(如果我没有记错的话,从4.0版开始)。这可以通过以下方式完成llvm-objdump
:
$ llvm-objdump -d bpf.o
bpf.o: file format ELF64-BPF
Disassembly of section .text:
func:
0: b7 01 00 00 00 00 00 00 r1 = 0
1: 63 1a fc ff 00 00 00 00 *(u32 *)(r10 - 4) = r1
2: bf 10 00 00 00 00 00 00 r0 = r1
3: 95 00 00 00 00 00 00 00 exit
4: b7 00 00 00 03 00 00 00 r0 = 3
我们获得了LLVM使用的语法的汇编指令(我们需要编写或编辑eBPF汇编的语法,因此这很有用)。注意我们在程序末尾添加的无效指令。
除了字节码和汇编指令外,LLVM还可以嵌入调试符号,以便将其转储以进行检查。具体来说,我们可以将C指令与字节码同时使用。记住原始程序是很方便的,但是最重要的是了解C指令如何映射到eBPF代码非常有帮助。嵌入指令是通过从C编译并-g
传递给clang的标志来完成的。试一试吧:
$ clang -target bpf -g -S -o bpf.s bpf.c
$ llvm-mc -triple bpf -filetype=obj -o bpf.o bpf.s
$ llvm-objdump -S bpf.o
bpf.o: file format ELF64-BPF
Disassembly of section .text:
func:
; int func() {
0: b7 01 00 00 00 00 00 00 r1 = 0
1: 63 1a fc ff 00 00 00 00 *(u32 *)(r10 - 4) = r1
; return 0;
2: bf 10 00 00 00 00 00 00 r0 = r1
3: 95 00 00 00 00 00 00 00 exit
请注意,我们传递-g
给clang
,并且还更改了传递给的命令(现在 -S
改为-d
)llvm-objdump
。该return 0;
指令出现,并映射(放置在eBPF程序中的相关指令上方)。好的。
内联汇编
由于clang和LLVM知道如何生成和编译eBPF程序集,因此存在另一种处理指令的方法。现在可以直接在C程序中内联使用eBPF程序集,再次在字节码中生成特定序列。请参阅以下示例,该示例在某种程度上受到Cilium的BPF和XDP参考指南中示例的启发。
$ cat inline_asm.c
int func()
{
unsigned long long foobar = 2, r3 = 3, *foobar_addr = &foobar;
asm volatile("lock *(u64 *)(%0+0) += %1" :
"=r"(foobar_addr) :
"r"(r3), "0"(foobar_addr));
return foobar;
}
$ clang -target bpf -Wall -O2 -c inline_asm.c -o inline_asm.o
$ llvm-objdump -d inline_asm.o
inline_asm.o: file format ELF64-BPF
Disassembly of section .text:
func:
0: b7 01 00 00 02 00 00 00 r1 = 2
1: 7b 1a f8 ff 00 00 00 00 *(u64 *)(r10 - 8) = r1
2: b7 01 00 00 03 00 00 00 r1 = 3
3: bf a2 00 00 00 00 00 00 r2 = r10
4: 07 02 00 00 f8 ff ff ff r2 += -8
5: db 12 00 00 00 00 00 00 lock *(u64 *)(r2 + 0) += r1
6: 79 a0 f8 ff 00 00 00 00 r0 = *(u64 *)(r10 - 8)
7: 95 00 00 00 00 00 00 00 exit
它在r2指向的地址上产生值的原子增量。因为指令是用C源代码编写的,所以我们不需要中间步骤就可以进行汇编。
我们应该使用内联汇编还是中间编译?我认为这两种方法都很有用:在C源文件中以汇编形式插入几个指令,或者创建用于测试特定指令序列的小型程序。在Netronome,我们经常将后者用于单元测试,以检查nfp驱动程序的eBPF硬件卸载功能。
结论
简而言之,您无需将eBPF程序从C编译为ELF目标文件,而是可以将其编译为汇编语言,根据需要进行编辑,然后将此版本汇编为最终目标文件。为此,您需要6.0版或更高版本中的clang和LLVM,命令为:
$ clang -target bpf -S -o bpf.s bpf.c
$ llvm-mc -triple bpf -filetype=obj -o bpf.o foo.s
并以人类可读的格式转储该文件:
$ llvm-objdump -d bpf.o
$ llvm-objdump -S bpf.o # add C code, if -g was passed to clang
此外,该asm
关键字可用于在C程序中内联包括eBPF汇编程序。
LLVM中的eBPF程序集支持允许编写所需的任何eBPF指令序列。包括错误的程序。不要忘记:即使编译,它仍然必须通过验证程序。祝好运并玩得开心点!
-
当我完成本文时,现在也有一个GCC后端,但似乎没有clang / LLVM版本完整,后者显然仍然是产生eBPF字节码的参考工具。 ↩
-
有关目标以及与默认目标的区别的更多信息,请参见Cilium的BPF和XDP参考指南
bpf
。 ↩