点击上方蓝字【囧囧妹】一起学习,一起成长!
对于 LLVM,可以检查 BPF 目标支持与否,例如,通过以下方式:
$ llc --versionLLVM (http://llvm.org/):LLVM version 3.8.1Optimized build.Default target: x86_64-unknown-linux-gnuHost CPU: skylakeRegistered Targets: [...] bpf - BPF (host endian) bpfeb - BPF (big endian) bpfel - BPF (little endian) [...]
bpf目标使用 CPU 的字节序进行编译,这意味着如果 CPU 的字节序是小端字节序,则程序也以小端字节序格式表示,如果 CPU 的字节序为大端字节序,则程序表示为大端。这也与 BPF 的运行时行为相匹配,它是通用的并使用它运行的 CPU 字节序,以便不损害任何格式的架构。
bpfeb和
bpfel,这要归功于 BPF 程序可以在以一种字节序运行的节点上编译(例如 x86 上的小字节序)并在以另一种字节序格式(例如 arm 上的大字节序)的节点上运行. 请注意,前端(clang)也需要以目标字节序运行。
bpf目标作为是首选方式。例如,对
x86_64目标进行编译会产生相同的输出,
bpf并且
bpfel由于是小端,因此触发编译的脚本也不必是端感知的。
一个最小的、独立的 XDP 放置程序可能类似于以下示例 ( xdp-example.c
):
#include <linux/bpf.h>#ifndef __section# define __section(NAME) \ __attribute__((section(NAME), used))#endif__section("prog")int xdp_drop(struct xdp_md *ctx){ return XDP_DROP;}char __license[] __section("license") = "GPL";
然后可以按如下方式编译并加载到内核中:
$ clang -O2 -Wall -target bpf -c xdp-example.c -o xdp-example.o# ip link set dev em1 xdp obj xdp-example.o
注意:将 XDP BPF 程序附加到上述网络设备需要 Linux 4.11 和支持 XDP 的设备,或者 Linux 4.12 或更高版本。
对于生成的目标文件 LLVM (>= 3.9) 使用官方的 BPF 机器值,即EM_BPF
(十进制:247
/十六进制:)0xf7
。在此示例中,程序已使用 下的bpf
目标进行编译x86_64
,因此LSB
(相对于MSB
)显示关于字节序:
$ file xdp-example.oxdp-example.o: ELF 64-bit LSB relocatable, *unknown arch 0xf7* version 1 (SYSV), not stripped
readelf -a xdp-example.o将转储有关 ELF 文件的更多信息,这些信息有时可用于内省生成的节标题、重定位条目和符号表。
$ git clone https://github.com/llvm/llvm-project.git$ cd llvm-project$ mkdir build$ cd build$ cmake -DLLVM_ENABLE_PROJECTS=clang -DLLVM_TARGETS_TO_BUILD="BPF;X86" -DBUILD_SHARED_LIBS=OFF -DCMAKE_BUILD_TYPE=Release -DLLVM_BUILD_RUNTIME=OFF -G "Unix Makefiles" ../llvm$ make -j $(getconf _NPROCESSORS_ONLN)$ ./bin/llc --versionLLVM (http://llvm.org/):LLVM version x.y.zsvnOptimized build.Default target: x86_64-unknown-linux-gnuHost CPU: skylakeRegistered Targets:bpf - BPF (host endian)bpfeb - BPF (big endian)bpfel - BPF (little endian)x86 - 32-bit X86: Pentium-Pro and abovex86-64 - 64-bit X86: EM64T and AMD64$ export PATH=$PWD/bin:$PATH # add to ~/.bashrc
--version使用了Optimized build否则当 LLVM 处于调试模式时,程序的编译时间将显着增加(例如,增加 10 倍或更多)。
$ clang -O2 -S -Wall -target bpf -c xdp-example.c -o xdp-example.S$ cat xdp-example.S.text.section prog,"ax",@progbits.globl xdp_drop.p2align 3xdp_drop: # @xdp_drop# BB#0:r0 = 1exit.section license,"aw",@progbits.globl __license # @__license__license:.asciz "GPL"
llvm-mc -triple bpf -filetype=obj -o xdp-example.o xdp-example.S
-g编译来完成。
$ clang -O2 -g -Wall -target bpf -c xdp-example.c -o xdp-example.o$ llvm-objdump -S --no-show-raw-insn xdp-example.oxdp-example.o: file format ELF64-BPFDisassembly of section prog:xdp_drop:; {0: r0 = 1; return XDP_DROP;1: exit
llvm-objdump工具可以使用编译中使用的原始 C 代码对汇编器输出进行注释。本例中的简单示例不包含太多 C 代码,但是,显示为
0:和的行号
1:直接对应于内核的验证程序日志。
llvm-objdump可以帮助将指令关联回原始 C 代码,这对于分析非常有用。
# ip link set dev em1 xdp obj xdp-example.o verbProg section 'prog' loaded (5)!- Type: 6- Instructions: 2 (0 over limit)- License: GPLVerifier analysis:0: (b7) r0 = 11: (95) exitprocessed 2 insns
llvm-objdump输出转储了与内核相同的 BPF 汇编代码。
--no-show-raw-insn选项也会将原始数据 作为十六进制转储到程序集前面:
struct bpf_insn$ llvm-objdump -S xdp-example.oxdp-example.o: file format ELF64-BPFDisassembly of section prog:xdp_drop:; {0: b7 00 00 00 01 00 00 00 r0 = 1; return foo();1: 95 00 00 00 00 00 00 00 exit
xdp-example.bc,然后可以将其传递给 llc:
$ clang -O2 -Wall -target bpf -emit-llvm -c xdp-example.c -o xdp-example.bc$ llc xdp-example.bc -march=bpf -filetype=obj -o xdp-example.o
clang -O2 -Wall -emit-llvm -S -c xdp-example.c -o -
-mattr=dwarfris选项添加到
llc命令中:
$ llc -march=bpf -mattr=help |& grep dwarfrisdwarfris - Disable MCAsmInfo DwarfUsesRelocationsAcrossSections.[...]
-mattr=dwarfris的原因是因为标志
dwarfris(
dwarf relocation in section)禁用 DWARF 和 ELF 符号表之间的 DWARF 横截面重定位,因为 libdw 没有适当的 BPF 重定位支持,因此像这样的工具
pahole将无法从对象中正确转储结构。
-mattr=dwarfris选项也可以实现相同的功能。从目标文件中转储结构可以通过 DWARF 或 BTF 信息来完成。
pahole此时使用 LLVM 发出的 DWARF 信息,但是,
pahole如果可用,未来的版本可能会依赖 BTF。
git clone https://git.kernel.org/pub/scm/devel/pahole/pahole.git
pahole附带
-J将 DWARF 从目标文件转换为 BTF 的选项。
pahole可以按如下方式探测 BTF 支持(请注意,该
llvm-objcopy工具也是必需的
pahole,因此也要检查它的存在):
$ pahole --help | grep BTF-J, --btf_encode Encode as BTF
-g到
clang命令行来生成源级调试信息。请注意,这
-g与是否使用
llc'
dwarfris选项无关。生成目标文件的完整示例:
$ clang -O2 -g -Wall -target bpf -emit-llvm -c xdp-example.c -o xdp-example.bc$ llc xdp-example.bc -march=bpf -mattr=dwarfris -filetype=obj -o xdp-example.o
clang -target bpf -O2 -g -c -Xclang -target-feature -Xclang +dwarfris -c xdp-example.c -o xdp-example.o
pahole,可以根据 DWARF 信息正确转储 BPF 程序的结构:
$ pahole xdp-example.ostruct xdp_md {__u32 data; /* 0 4 */__u32 data_end; /* 4 4 */__u32 data_meta; /* 8 4 *//* size: 12, cachelines: 1, members: 3 *//* last cacheline: 12 bytes */};
-Jpahole最终可以从 DWARF 生成 BTF。在对象文件中,DWARF 数据仍将与新添加的 BTF 数据一起保留。完整
clang和
pahole示例结合:
$ clang -target bpf -O2 -Wall -g -c -Xclang -target-feature -Xclang +dwarfris -c xdp-example.c -o xdp-example.o$ pahole -J xdp-example.o
readelf可以看到
.BTF的一部分的存在:
$ readelf -a xdp-example.o[...][18] .BTF PROGBITS 0000000000000000 00000671[...]
-mcpu用于 BPF 后端的选择器,以便选择不同版本的 BPF 指令集,即在 BPF 基本指令集之上的指令集扩展,以便生成更高效和更小的代码。
-mcpu选项可通过以下方式查询:
$ llc -march bpf -mcpu=helpAvailable CPUs for this target:generic - Select the generic processor.probe - Select the probe processor.v1 - Select the v1 processor.v2 - Select the v2 processor.[...]
generic
处理器是默认处理器,也是 BPF 的基本指令集v1
。选项v1
和v2
通常在 BPF 程序被交叉编译并且加载程序的目标主机与编译它的目标主机不同的环境中很有用(因此可用的 BPF 内核特性也可能不同)。
Cilium 内部也使用的推荐-mcpu
选项是 -mcpu=probe
!在这里,LLVM BPF 后端向内核查询 BPF 指令集扩展的可用性,当发现可用时,LLVM 将在适当的时候使用它们来编译 BPF 程序。
带有 llc 的-mcpu=probe
完整命令行示例:
$ clang -O2 -Wall -target bpf -emit-llvm -c xdp-example.c -o xdp-example.bc$ llc xdp-example.bc -march=bpf -mcpu=probe -filetype=obj -o xdp-example.o
通常,LLVM IR 生成与架构无关。然而,使用clang -target bpf与省略-target bpf时存在一些差异,因此使用 clang 的默认目标,取决于底层架构,可能是 x86_64arm64
或其他。
引用内核的Documentation/bpf/bpf_devel_QA.txt
:
BPF 程序可以递归地包含带有文件范围内联汇编代码的头文件。默认目标可以很好地处理这个问题,而如果 bpf 后端汇编器不理解这些汇编代码,则 bpf 目标可能会失败,这在大多数情况下是正确的。
在没有 -g 的情况下编译时,附加的 elf 部分,例如
.eh_frame
和.rela.eh_frame
,可能会出现在具有默认目标的目标文件中,但不会出现在 bpf 目标中。默认目标可能会将 C switch 语句转换为 switch 表查找和跳转操作。由于切换表放在全局只读部分,bpf程序将无法加载。bpf 目标不支持切换表优化。clang 选项
-fno-jump-tables
可用于禁用切换表生成。对于 clang
-target bpf
,无论底层 clang 二进制文件还是默认目标(或内核)是 32 位,都可以保证指针或 long unsigned long 类型的宽度始终为 64 位。但是,当使用原生 clang 目标时,它将根据底层架构的约定编译这些类型,这意味着在 32 位架构的情况下,指针或长 无符号长类型(例如 BPF 上下文结构中)将具有 32 位的宽度,而BPF LLVM 后端仍然以 64 位运行。
在跟踪映射 CPU 寄存器的内核struct pt_regs
或其他 CPU 寄存器宽度很重要的内核结构的情况下,最需要本机目标。在所有其他情况下(例如联网),使用clang -target bpf是首选。
此外,自 LLVM 7.0 版以来,LLVM 开始支持 32 位子寄存器和 BPF ALU32 指令。添加了一个新的代码生成属性alu32
。启用后,LLVM 将尽可能尝试使用 32 位子寄存器,通常是在对 32 位类型进行操作时。与 32 位子寄存器相关的 ALU 指令将成为 ALU32 指令。例如,对于以下示例代码:
$ cat 32-bit-example.cvoid cal(unsigned int *a, unsigned int *b, unsigned int *c){unsigned int sum = *a + *b;*c = sum;}
在默认代码生成时,汇编器将如下所示:
$ clang -target bpf -emit-llvm -S 32-bit-example.c$ llc -march=bpf 32-bit-example.ll$ cat 32-bit-example.scal:r1 = *(u32 *)(r1 + 0)r2 = *(u32 *)(r2 + 0)r2 += r1*(u32 *)(r3 + 0) = r2exit
使用 64 位寄存器,因此加法意味着 64 位加法。现在,如果您通过指定启用新的 32 位子寄存器支持-mattr=+alu32
,那么汇编器将如下所示:
$ llc -march=bpf -mattr=+alu32 32-bit-example.ll$ cat 32-bit-example.scal:w1 = *(u32 *)(r1 + 0)w2 = *(u32 *)(r2 + 0)w2 += w1*(u32 *)(r3 + 0) = w2exit
w
寄存器,意思是 32 位子寄存器,将被用来代替 64 位r
寄存器。
启用 32 位子寄存器可能有助于减少类型扩展指令序列。它还可以帮助内核 eBPF JIT 编译器用于 32 位架构,其中寄存器对用于对 64 位 eBPF 寄存器进行建模,并且需要额外的指令来操作高 32 位。给定从 32 位子寄存器读取保证仅从低 32 位读取,即使写入仍需要清除高 32 位,如果 JIT 编译器知道一个寄存器的定义只有子寄存器读取,则设置指令可以消除目标的高 32 位。




