暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

bpf工具链 - LLVM篇

囧囧妹 2022-08-03
1320

点击上方蓝字【囧囧妹】一起学习,一起成长!

LLVM 是目前唯一提供 BPF 后端的编译器套件。gcc 目前不支持 BPF。
BPF 后端被合并到 LLVM 的 3.7 版本中。主要发行版在打包 LLVM 时默认启用 BPF 后端,因此在最近的发行版上安装 clang 和 llvm 足以开始将 C 编译为 BPF 目标文件。
典型的工作流程是 BPF 程序用 C 语言编写,通过 LLVM 编译成object/ELF 文件,由用户空间 BPF ELF 加载器(如 iproute2 或其他)解析,并通过 BPF 系统调用推送到内核中。内核验证 BPF 指令并对它们进行 JIT,为程序返回一个新的文件描述符,然后可以将其附加到子系统(例如网络),子系统可以进一步将 BPF 程序卸载到硬件(例如 NIC)。可参考前面bpf架构篇来了解各个模块。

对于 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.o
          xdp-example.o: ELF 64-bit LSB relocatable, *unknown arch 0xf7* version 1 (SYSV), not stripped
          readelf -a xdp-example.o
          将转储有关 ELF 文件的更多信息,这些信息有时可用于内省生成的节标题、重定位条目和符号表。
          在不太可能需要从头开始编译 clang 和 LLVM 的情况下,可以使用以下命令:
            $ 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 --version
            LLVM (http://llvm.org/):
            LLVM version x.y.zsvn
            Optimized build.
            Default target: x86_64-unknown-linux-gnu
            Host CPU: skylake


            Registered Targets:
            bpf - BPF (host endian)
            bpfeb - BPF (big endian)
            bpfel - BPF (little endian)
            x86 - 32-bit X86: Pentium-Pro and above
            x86-64 - 64-bit X86: EM64T and AMD64


            $ export PATH=$PWD/bin:$PATH # add to ~/.bashrc


            确保--version
            使用了Optimized build否则当 LLVM 处于调试模式时,程序的编译时间将显着增加(例如,增加 10 倍或更多)。
            对于调试,clang 可以生成汇编器输出如下:
              $ 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 3
              xdp_drop: # @xdp_drop
              # BB#0:
              r0 = 1
              exit


              .section license,"aw",@progbits
              .globl __license # @__license
              __license:
              .asciz "GPL"


              从 LLVM 的 6.0 版开始,会有汇编器解析器的支持。您可以直接使用 BPF 汇编器进行编程,然后使用 llvm-mc 将其组装成目标文件。例如,您可以使用以下命令将上面列出的 xdp-example.S 组装回目标文件:
                llvm-mc -triple bpf -filetype=obj -o xdp-example.o xdp-example.S
                此外,更新的 LLVM 版本 (>= 4.0) 还可以将调试信息以 dwarf 格式存储到目标文件中。这可以通过通常的工作流程通过添加-g
                编译来完成。
                  $ clang -O2 -g -Wall -target bpf -c xdp-example.c -o xdp-example.o
                  $ llvm-objdump -S --no-show-raw-insn xdp-example.o


                  xdp-example.o: file format ELF64-BPF


                  Disassembly of section prog:
                  xdp_drop:
                  ; {
                  0: r0 = 1
                  ; return XDP_DROP;
                  1: exit
                  然后,该llvm-objdump
                  工具可以使用编译中使用的原始 C 代码对汇编器输出进行注释。本例中的简单示例不包含太多 C 代码,但是,显示为0:
                   和的行号1:
                  直接对应于内核的验证程序日志。
                  这意味着,如果 BPF 程序被验证者拒绝,llvm-objdump
                   可以帮助将指令关联回原始 C 代码,这对于分析非常有用。
                    # ip link set dev em1 xdp obj xdp-example.o verb


                    Prog section 'prog' loaded (5)!
                    - Type: 6
                    - Instructions: 2 (0 over limit)
                    - License: GPL


                    Verifier analysis:


                    0: (b7) r0 = 1
                    1: (95) exit
                    processed 2 insns


                    从验证器分析中可以看出,llvm-objdump
                    输出转储了与内核相同的 BPF 汇编代码。
                    省略该--no-show-raw-insn
                    选项也会将原始数据 作为十六进制转储到程序集前面:struct bpf_insn
                      $ llvm-objdump -S xdp-example.o


                      xdp-example.o: file format ELF64-BPF


                      Disassembly 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


                      对于 LLVM IR 调试,BPF 的编译过程可以分为两个步骤,生成二进制 LLVM IR 中间文件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


                        生成的 LLVM IR 也可以通过以下方式以用户可读的格式转储:
                          clang -O2 -Wall -emit-llvm -S -c xdp-example.c -o -
                          LLVM 能够将调试信息(例如程序中使用的数据类型的描述)附加到生成的 BPF 目标文件中。默认情况下,这是 DWARF 格式。
                          BPF 使用的一个高度简化的版本称为 BTF(BPF 类型格式)。生成的 DWARF 可以转换为 BTF,然后通过 BPF 对象加载器加载到内核中。然后内核将验证 BTF 数据的正确性并跟踪 BTF 数据包含的数据类型。
                          然后可以使用 BTF 数据中的键和值类型对 BPF 映射进行注释,以便稍后的映射转储导出映射数据以及相关的类型信息。这允许更好的调试和打印。请注意,BTF 数据是一种通用调试数据格式,因此可以加载任何 DWARF 到 BTF 转换的数据(例如,内核的 vmlinux DWARF 数据可以转换为 BTF 并加载)。后者对于将来的 BPF 跟踪特别有用。
                          为了从 DWARF 调试信息生成 BTF,需要 elfutils (>= 0.173)。如果这不可用,则需要在编译期间将-mattr=dwarfris
                          选项添加到llc
                          命令中:
                            $ llc -march=bpf -mattr=help |& grep dwarfris
                            dwarfris - Disable MCAsmInfo DwarfUsesRelocationsAcrossSections.
                            [...]


                            使用-mattr=dwarfris
                            的原因是因为标志dwarfris
                            (dwarf relocation in section
                            禁用 DWARF 和 ELF 符号表之间的 DWARF 横截面重定位,因为 libdw 没有适当的 BPF 重定位支持,因此像这样的工具 pahole
                             
                            将无法从对象中正确转储结构。
                            lfutils (>= 0.173) 实现了适当的 BPF 重定位支持,因此没有该-mattr=dwarfris
                            选项也可以实现相同的功能。从目标文件中转储结构可以通过 DWARF 或 BTF 信息来完成。pahole
                            此时使用 LLVM 发出的 DWARF 信息,但是,pahole
                            如果可用,未来的版本可能会依赖 BTF。
                            要将 DWARF 转换为 BTF,需要最新的 pahole 版本 (>= 1.12)。如果无法从其中一个分发包中获得最新的 pahole 版本,也可以从其官方 git 存储库获得:
                              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 构建带有调试信息的 BPF 程序(同样,当具有正确的 elfutils 版本时,可以省略 dwarfris 标志):
                                    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.o
                                      struct 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 */
                                      };


                                      通过该选项-J
                                       pahole
                                      最终可以从 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
                                          [...]


                                          iproute2 等 BPF 加载器将检测并加载 BTF 部分,以便 BPF 映射可以使用类型信息进行注释。
                                          LLVM 默认使用 BPF 基本指令集来生成代码,以确保生成的目标文件也可以加载旧内核,例如长期稳定的内核(例如 4.9+)。
                                          但是,LLVM 有一个-mcpu
                                          用于 BPF 后端的选择器,以便选择不同版本的 BPF 指令集,即在 BPF 基本指令集之上的指令集扩展,以便生成更高效和更小的代码。
                                          可用-mcpu
                                          选项可通过以下方式查询:
                                            $ llc -march bpf -mcpu=help
                                            Available 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_64
                                              arm64
                                              或其他。

                                              引用内核的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.c
                                                void 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.s
                                                  cal:
                                                  r1 = *(u32 *)(r1 + 0)
                                                  r2 = *(u32 *)(r2 + 0)
                                                  r2 += r1
                                                  *(u32 *)(r3 + 0) = r2
                                                  exit


                                                  使用 64 位寄存器,因此加法意味着 64 位加法。现在,如果您通过指定启用新的 32 位子寄存器支持-mattr=+alu32
                                                  ,那么汇编器将如下所示:

                                                    $ llc -march=bpf -mattr=+alu32 32-bit-example.ll
                                                    $ cat 32-bit-example.s
                                                    cal:
                                                    w1 = *(u32 *)(r1 + 0)
                                                    w2 = *(u32 *)(r2 + 0)
                                                    w2 += w1
                                                    *(u32 *)(r3 + 0) = w2
                                                    exit


                                                    w
                                                    寄存器,意思是 32 位子寄存器,将被用来代替 64 位r
                                                     寄存器。

                                                    启用 32 位子寄存器可能有助于减少类型扩展指令序列。它还可以帮助内核 eBPF JIT 编译器用于 32 位架构,其中寄存器对用于对 64 位 eBPF 寄存器进行建模,并且需要额外的指令来操作高 32 位。给定从 32 位子寄存器读取保证仅从低 32 位读取,即使写入仍需要清除高 32 位,如果 JIT 编译器知道一个寄存器的定义只有子寄存器读取,则设置指令可以消除目标的高 32 位。



                                                    觉得不错,点击“分享”,“赞”,“在看”传播给更多热爱嵌入式的小伙伴吧!

                                                    文章转载自囧囧妹,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                                                    评论