带有LLVM的eBPF组件

目录

从C到目标文件

深入说明

eBPF与LLVM循序渐进

从C编译到eBPF程序集

组装到ELF目标文件

llvm-objdump的人性化输出

内联汇编

结论


 

此职位作为草稿留了很长时间。它的大部分内容是在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格式创建一个目标文件,其中包含已编译的字节码。默认情况下,代码在.textELF部分下。让我们转储它:

$ 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目标是 bpf2

$ 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

请注意,我们传递-gclang,并且还更改了传递给的命令(现在 -S改为-dllvm-objdump。该return 0;指令出现,并映射(放置在eBPF程序中的相关指令上方)。好的。

内联汇编

由于clang和LLVM知道如何生成和编译eBPF程序集,因此存在另一种处理指令的方法。现在可以直接在C程序中内联使用eBPF程序集,再次在字节码中生成特定序列。请参阅以下示例,该示例在某种程度上受到CiliumBPF和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指令序列。包括错误的程序。不要忘记:即使编译,它仍然必须通过验证程序。祝好运并玩得开心点!


  1. 当我完成本文时,现在也有一个GCC后端,但似乎没有clang / LLVM版本完整,后者显然仍然是产生eBPF字节码的参考工具。 

  2. 有关目标以及与默认目标的区别的更多信息,请参见CiliumBPF和XDP参考指南bpf。 

 

 

 

 

 

 

上一篇:2021 新的征程


下一篇:6S模型从安装配置到第一个例子