LLVM eBPF 汇编编程教程

Linux内核那些事

共 6018字,需浏览 13分钟

 ·

2021-12-10 00:39

1 引言

1.1 主流开发方式:从 C 代码直接生成 eBPF 字节码

eBPF 相比于 cBPF(经典 BPF)的优势之一是:Clang/LLVM 为它提供了一个编译后端, 能从 C 源码直接生成 eBPF 字节码(bytecode)。(写作本文时,GCC 也提供了一个类似 的后端,但各方面都没有 Clang/LLVM 完善,因此后者仍然是生成 eBPF 字节码 的最佳参考工具)。

将 C 代码编译成 eBPF 目标文件非常有用,因为 直接用字节码编写高级程序是非常耗时的。此外,截至本文写作时, 还无法直接编写字节码程序来使用 CO-RE 等复杂特性。

因此,Clang 和 LLVM 仍然是 eBPF 工作流不可或缺的部分。

1.2 特殊场景需求:eBPF 汇编编程更合适

但是,C 方式不适用于某些特殊的场景,例如:

  1. 只是想测试特定的 eBPF 指令流
  2. 对程序的某个特定部分进行深度调优

在这些情况下,就需要直接编写或修改 eBFP 汇编程序。

1.3 几种 eBPF 汇编编程方式

  1. 直接编写 eBPF 字节码程序。也就是编写可直接加载运行的 二进制 eBPF 程序,

    • 这肯定是可行的,但过程非常冗长无聊,对开发者极其不友好。
    • 此外,为保证与 tc 等工具的兼容,还要将写好的程序转换成目标文件(object file),因此工作量又多了一些。
  2. 直接用 eBPF 汇编语言编写,然后用专门的汇编器 (例如 ebpf_asm)将其汇编(assemble)成字节码。

    • 相比字节码(二进制),汇编语言(文本)至少可读性还是好很多的。
  3. 用 LLVM 将 C 编译成 eBPF 汇编,然后手动修改生成的汇编程序, 最后再将其汇编(assemble)成字节码放到对象文件。

  4. 在 C 中插入内联汇编,然后统一用 clang/llvm 编译。

以上几种方式 Clang/LLVM 都支持!先用可读性比较好的方式写, 然后再将其汇编(assembling)成另字节码程序。此外,甚至能 dump 对象文件中包含的程序。

本文将会展示第三种和第四种方式,第二种可以认为是第三种的更加彻底版,开发的流程 、步骤等已经包括在第三种了。

2 Clang/LLVM 编译 eBPF 基础

在开始汇编编程之前,先来熟悉一下 clang/llvm 将 C 程序编译成 eBPF 程序的过程。

2.1 将 C 程序编译成 BPF 目标文件

下面是个 eBPF 程序:没做任何事情,直接返回零,

// bpf.c
int func() {
    return 0;
}

如下命令可以将其编译成对象文件(目标文件):

# 注意 target 类型指定为 `bpf`
$ clang -target bpf -Wall -O2 -c bpf.c -o bpf.o

某些复杂的程序可能需要用下面的命令来编译:

$ clang -O2 -emit-llvm -c bpf.c -o - | \
 llc -march=bpf -mcpu=probe -filetype=obj -o bpf.o

以上命令会将 C 源码编译成字节码,然后生成一个 ELF 格式的目标文件。

1.2 查看 ELF 文件中的 eBPF 字节码

默认情况下,代码位于 ELF 的 .text 区域(section):

$ readelf -x .text bpf.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 汇编语法不熟悉,可参考:

  1. 简洁文档: (https://github.com/iovisor/bpf-docs/blob/master/eBPF.md)
  2. 详细文档: (https://www.kernel.org/doc/Documentation/networking/filter.txt)

有了以上基础,接下来看如何开发 eBPF 汇编程序。

3 方式一:C 生成 eBPF 汇编 + 手工修改汇编

本节需要 Clang/LLVM 6.0+ 版本(clang -v)。

译文基于 10.0,结果与原文略有差异。

C 源码:

// bpf.c
int func() {
 return 0;
}

3.1 将 C 编译成 eBPF 汇编(clang)

其实前面已经看到了,与将普通 C 程序编译成汇编类似,只是这里指定 target 类型是 bpf (bpf target 与默认 target 的不同,见 Cilium 文档 BPF 和 XDP 参考指南):

Cilium:BPF 和 XDP 参考指南:

http://docs.cilium.io/en/latest/bpf/#llvm

$ clang -target bpf -S -o bpf.s bpf.c

查看生成的汇编代码:

$ cat bpf.s
    .text
    .file   "bpf.c"
    .globl  func                 # -- Begin function func
    .p2align        3
    .type   func,@function
func:                           # @func
# %bb.0:
    r0 = 0
    exit
.Lfunc_end0:
    .size   func, .Lfunc_end0-func
                               # -- End function
    .addrsig

接下来就可以修改这段汇编代码了。

3.2 手工修改汇编程序

因为汇编程序是文本文件,因此编辑起来很容易。作为练手,我们在程序最后加上一行汇编指令 r0 = 3:

$ cat bpf.s
    .text
    .file   "bpf.c"
    .globl  func                    # -- Begin function func
    .p2align        3
    .type   func,@function
func:                               # @func
# %bb.0:
    r0 = 0
    exit
    r0 = 3                          # -- 这行是我们手动加的
.Lfunc_end0:
    .size   func, .Lfunc_end0-func
                                    # -- End function
    .addrsig

这行放在了 exit 之后,因此实际上没任何作用。

3.3 将汇编程序 assemble 成 ELF 对象文件(llvm-mc)

接下来将 bpf.s 汇编(assemble)成包含字节码的 ELF 对象文件。这 里需要用到 LLVM 自带的与机器码(machine code,mc)打交道的工具 llvm-mc:

$ llvm-mc -triple bpf -filetype=obj -o bpf.o bpf.s

bpf.o 就是生成的 ELF 文件!

3.4 查看对象文件中的 eBPF 字节码(readelf)

查看 bpf.o 中的字节码:

$ readelf -x .text bpf.o

Hex dump of section '.text':
  0x00000000 b7000000 00000000 95000000 00000000 ................
  0x00000010 b7000000 03000000                   ........

看到和之前相比,

  • 第一行(包含前两条指令)一样,
  • 第二行是新多出来的(对应的正是我们新加的一行汇编指令),作用:将常量 3 load 到寄存器 r0 中。

至此,我们已经成功地修改了指令流。接下来就可以用 bpftool 之 类的工具将这个程序加载到内核,任务完成!

3.5 以更加人类可读的方式查看 eBPF 字节码(llvm-objdump -d)

LLVM 还能以人类可读的方式 dump eBPF 对象文件中的指令,这里就要用到 llvm-objdump:

# -d           : alias for --disassemble
# --disassemble: display assembler mnemonics for the machine instructions
$ llvm-objdump -d bpf.o
bpf.o:  file format ELF64-BPF

Disassembly of section .text:

0000000000000000 func:
       0:       b7 00 00 00 00 00 00 00 r0 = 0
       1:       95 00 00 00 00 00 00 00 exit
       2:       b7 00 00 00 03 00 00 00 r0 = 3

最后一列显示了对应的 LLVM 使用的汇编指令(也是前面我们手工编辑时使用的 eBPF 指令)。

3.6 编译时嵌入调试符号或 C 源码(clang -g + llvm-objdump -S)

除了字节码和汇编指令,LLVM 还能将调试信息(debug symbols)嵌入到对象文件, 更具体说就是能在字节码旁边同时显示对应的 C 源码,对调试非常有用,也是 观察 C 指令如何映射到 eBPF 指令的好机会。

在 clang 编译时加上 -g 参数:

# -g: generate debug information.
$ clang -target bpf -g -S -o bpf.s bpf.c
$ llvm-mc -triple bpf -filetype=obj -o bpf.o bpf.s
# -S      : alias for --source
# --source: display source inlined with disassembly. Implies disassemble object
$ llvm-objdump -S bpf.o
Disassembly of section .text:

0000000000000000 func:
; int func() {
       0:       b7 00 00 00 00 00 00 00 r0 = 0
;     return 0;
       1:       95 00 00 00 00 00 00 00 exit

注意这里用的是 -S(显示源码),不是 -d(反汇编)。

4 方式二:内联汇编(inline assembly)

接下来看另一种生成和编译 eBPF 汇编的方式:直接在 C 程序中嵌入 eBPF 汇编。

4.1 C 内联汇编示例

下面是个非常简单的例子,受 Cilium 文档 BPF 和 XDP 参考指南的启发:

// inline_asm.c
int func() {
    unsigned long long foobar = 2, r3 = 3, *foobar_addr = &foobar;

    asm volatile("lock *(u64 *)(%0+0) += %1" : // 等价于:foobar += r3
         "=r"(foobar_addr) :
         "r"(r3), "0"(foobar_addr))
;

    return foobar;
}

关键字 asm 用于插入汇编代码。

4.2 编译及查看生成的字节码

$ clang -target bpf -Wall -O2 -c inline_asm.c -o inline_asm.o

反汇编:

$ llvm-objdump -d inline_asm.o
Disassembly of section .text:

0000000000000000 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

对应到最后一列的汇编,大家应该大致能看懂。

4.3 小结

这种方式的好处是:源码仍然是 C,因此无需像前一种方式那样必须手动执行编译( compile)和汇编(assemble)两个分开的过程。

5 结束语

本文通过两个极简的例子展示了两种 eBPF 汇编编程方式:

  1. 手动生成并修改一段特定的指令流
  2. 在 C 中插入内联汇编

这两种方式我认为都是有用的,比如在 Netronome,我们经常用前一种方式做单元测试, 检查 nfp 驱动中的 eBPF hw offload 特性。

LLVM 支持编写任意的 eBPF 汇编程序(但提醒一下:编译能通过是一回事,能不能通过校验器是另一回事)。有兴趣自己试试吧!


原文: https://arthurchiao.art/blog/ebpf-assembly-with-llvm-zh/

浏览 142
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报