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

bpf架构 - 指令系统篇

囧囧妹 2022-07-26
229

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

一、指令系统

BPF 是一个通用的 RISC 指令集,最初是为了在 C 的子集中编写程序而设计的,这些程序可以通过编译器后端(例如 LLVM)编译成 BPF 指令,以便内核稍后可以将它们映射到内核中的 JIT 编译器到本机操作码中,以实现内核内的最佳执行性能。

将这些指令推送到内核的优点包括:

  • 使内核可编程,而无需跨越内核/用户空间边界。例如,与网络相关的 BPF 程序,如 Cilium 的情况,可以实现灵活的容器策略、负载平衡和其他手段,而无需将数据包移动到用户空间并返回内核。BPF 程序和内核/用户空间之间的状态仍然可以在需要时通过映射共享。

  • 鉴于可编程数据路径的灵活性,还可以通过编译程序解决的用例不需要的功能来对程序进行性能优化。例如,如果容器不需要 IPv4,则可以构建 BPF 程序以仅处理 IPv6,以节省快速路径中的资源。

  • 在联网的情况下(例如 tc 和 XDP),BPF 程序可以自动更新,而无需重新启动内核、系统服务或容器,也不会中断流量。此外,还可以通过 BPF 映射在整个更新过程中维护任何程序状态。

  • BPF 为用户空间提供了稳定的 ABI,并且不需要任何第三方内核模块。BPF 是随处可见的 Linux 内核的核心部分,它保证现有的 BPF 程序在更新的内核版本上继续运行。这种保证与内核为用户空间应用程序的系统调用提供的保证相同。此外,BPF 程序可以跨不同的架构移植。

  • BPF 程序与内核协同工作,它们利用现有的内核基础设施(例如驱动程序、网络设备、隧道、协议栈、套接字)和工具(例如 iproute2)以及内核提供的安全保证。与内核模块不同,BPF 程序通过内核验证器进行验证,以确保它们不会使内核崩溃、始终终止等。例如,XDP 程序重用现有的内核驱动程序并在提供的 DMA 缓冲区上运行包含数据包帧,而不像在其他模型中那样将它们或整个驱动程序暴露给用户空间。此外,XDP 程序重用现有堆栈而不是绕过它。BPF 可以被认为是内核设施的通用“胶水代码”,用于制作程序以解决特定用例。

内核中 BPF 程序的执行始终是事件驱动的,举例

  • 一旦接收到数据包,在其入口路径上附加了 BPF 程序的网络设备将触发程序的执行。

  • 一旦该地址处的代码被执行,内核地址将捕获带有附加 BPF 程序的 kprobe,然后调用 kprobe 的回调函数进行检测,随后触发附加 BPF 程序的执行。

BPF 由 11 个 64 位寄存器和 32 位子寄存器、一个程序计数器和一个 512 字节的大 BPF 堆栈空间组成。寄存器被命名为r0
r10
操作模式默认为 64 位,32 位子寄存器只能通过特殊的 ALU(算术逻辑单元)操作访问。低 32 位子寄存器在被写入时零扩展为 64 位。

寄存器r10
是唯一的只读寄存器,它包含帧指针地址,以便访问 BPF 堆栈空间。其余的r0
-r9
 寄存器是通用的,具有读/写性质。

BPF 程序可以调用预定义的辅助函数,该函数由核心内核定义(从不由模块定义)。BPF 调用约定定义如下:

  • r0
    包含辅助函数调用的返回值。

  • r1
    -r5
    将 BPF 程序的参数保存到内核辅助函数。

  • r6
    -r9
    是被调用者保存的寄存器,将在辅助函数调用时保留。

BPF 调用约定足够通用,可以直接映射到x86_64
arm64
 其他 ABI,因此所有 BPF 寄存器都一对一映射到 HW CPU 寄存器,因此 JIT 只需要发出调用指令,而不需要额外的额外移动来放置函数参数. 这种调用约定被建模为涵盖常见的调用情况而不会降低性能。当前不支持具有 6 个或更多参数的调用。内核中专用于 BPF(函数)的辅助函数是专门为考虑BPF_CALL_0()
BPF_CALL_5()
这个约定而设计的。

寄存器r0
也是包含 BPF 程序退出值的寄存器。退出值的语义由程序类型定义。此外,当将执行交还给内核时,退出值作为 32 位值传递。

寄存器r1
-r5
是临时寄存器,这意味着 BPF 程序需要将它们溢出到 BPF 堆栈或将它们移动到被调用者保存的寄存器,如果这些参数要在多个辅助函数调用中重用。溢出意味着寄存器中的变量被移动到 BPF 堆栈中。将变量从 BPF 堆栈移动到寄存器的反向操作称为填充。溢出/填充的原因是由于寄存器数量有限。

在进入 BPF 程序的执行时,寄存器r1
最初包含程序的上下文。上下文是程序的输入参数(类似于argc/argv
典型 C 程序的 pair)。BPF 仅限于在单个上下文中工作。上下文由程序类型定义,例如,网络程序可以将网络数据包 ( skb
) 的内核表示作为输入参数。

BPF 的一般操作是 64 位,以遵循 64 位架构的自然模型,以便执行指针算术、传递指针以及将 64 位值传递给辅助函数,并允许 64 位原子操作。

每个程序的最大指令限制为 4096 条 BPF 指令,根据设计,这意味着任何程序都将快速终止。对于 5.1 之后的内核,这个限制被提升到 100 万条 BPF 指令。尽管指令集包含前向和后向跳转,但内核 BPF 验证器将禁止循环,以便始终保证终止。由于 BPF 程序在内核中运行,验证者的工作是确保这些程序可以安全运行,而不影响系统的稳定性。这意味着从指令集的角度来看,可以实现循环,但验证器会限制它。但是,还有一个尾调用的概念,它允许一个 BPF 程序跳转到另一个 BPF 程序。这也带有 33 个调用的嵌套上限,

指令格式被建模为两个操作数指令,这有助于在 JIT 阶段将 BPF 指令映射到本机指令。指令集是固定大小的,这意味着每条指令都有 64 位编码。目前,已经实现了 87 条指令,并且编码还允许在需要时使用更多指令来扩展集合。大端机器上单个 64 位指令的指令编码定义为从最高有效位 (MSB) 到最低有效位 (LSB) 的位序列op:8
dst_reg:4
src_reg:4
off:16
imm:32
off
并且imm
是有符号的类型。编码是内核标头的一部分,并在标头中定义linux/bpf.h
,其中还包括linux/bpf_common.h
.

op
定义要执行的实际操作。大部分的编码op
 都从 cBPF 中重用。该操作可以基于寄存器或立即操作数。自身的编码op
提供了有关使用哪种模式的信息(分别BPF_X
用于表示基于寄存器的操作和BPF_K
基于立即数的操作)。在后一种情况下,目标操作数始终是寄存器。两者都dst_reg
提供src_reg
有关要用于操作的寄存器操作数的附加信息(例如r0
r9
)。off
 在某些指令中用于提供相对偏移量,例如,用于寻址堆栈或 BPF 可用的其他缓冲区(例如映射值、数据包数据等),或跳转指令中的跳转目标。imm
包含一个常量/立即值。

可用op
指令可以分类为各种指令类别。这些类也在op
字段内编码。op
字段分为(从MSB到LSB)code:4
source:1
class:3
class
 是更通用的指令类,code
表示该类中的特定操作代码,并source
说明源操作数是寄存器还是立即值。可能的教学课程包括:

  • BPF_LD
    BPF_LDX
    : 这两个类都用于加载操作。BPF_LD
    用于将双字加载为由于imm:32
    拆分而跨越两条指令的特殊指令,以及数据包数据的字节/半字/字加载。后者是从 cBPF 继承而来的,主要是为了保持 cBPF 到 BPF 的转换效率,因为它们优化了 JIT 代码。对于本机 BPF,这些数据包加载指令如今已不太相关。BPF_LDX
    类保存字节/半字/字/双字从内存中加载的指令。此上下文中的内存是通用的,可以是堆栈内存、映射值数据、数据包数据等。

  • BPF_ST
    BPF_STX
    : 这两个类都用于存储操作。与存储对应物类似,BPF_LDX
     用于BPF_STX
    将数据从寄存器存储到内存中,同样可以是堆栈内存、映射值、数据包数据等。BPF_STX
    还包含用于执行基于字和双字的原子的特殊指令add 操作,例如可用于计数器。该类 BPF_ST
    类似于BPF_STX
    提供将数据存储到内存中的指令,只是源操作数是立即值。

  • BPF_ALU
    BPF_ALU64
    : 两个类都包含 ALU 操作。通常, BPF_ALU
    操作在 32 位模式和BPF_ALU64
    64 位模式下。两个 ALU 类都具有基于寄存器和基于立即数的对应源操作数的基本操作。两者都支持add( +
    )、sub( -
    )、and( &
    )、or( |
    )、左移( <<
    )、右移( >>
    )、xor( ^
    )、mul( *
    )、div( /
    )、mod( %
    )、neg( ~
    )操作. mov ( ) 也被添加为两种操作数模式下两个类的特殊 ALU 操作。 还包含有符号右移。另外包含给定源寄存器上半字/字/双字的字节顺序转换指令。<X> := <Y>
    BPF_ALU64
    BPF_ALU

  • BPF_JMP
    :这个类专门用于跳转操作。跳转可以是无条件的和有条件的。无条件跳转只是将程序计数器向前移动,因此相对于当前指令要执行的下一条指令是 ,其中是指令中编码的常量偏移量。由于 是有符号的,只要不创建循环并且在程序范围内,也可以向后执行跳转。条件跳转对基于寄存器和基于立即数的源操作数进行操作。如果跳转操作中的条件导致,则执行相对跳转,否则执行下一条指令 (off + 1
    off
    off
    true
    off + 1
    0 + 1
    ) 被执行。与 cBPF 相比,这种直通跳转逻辑有所不同,并且允许更好的分支预测,因为它更自然地适合 CPU 分支预测器逻辑。可用条件有 jeq( ==
    )、jne( !=
    )、jgt( >
    )、jge( >=
    )、jsgt(有符号>
    )、jsge(有符号>=
    )、jlt( <
    )、jle( <=
    )、jslt(有符号<
    )、jsle(有符号 <=
    )和jset(跳转如果)。除此之外,这个类中还有三个特殊的跳转操作:退出指令,它将离开 BPF 程序并将当前值作为返回码返回,调用指令,它将向可用的 BPF 之一发出函数调用辅助函数和隐藏的尾调用指令,它将跳转到不同的 BPF 程序。DST & SRC
    r0

Linux 内核附带一个 BPF 解释器,它执行以 BPF 指令汇编的程序。甚至 cBPF 程序在内核中也被透明地转换为 eBPF 程序,除了仍然带有 cBPF JIT 并且尚未迁移到 eBPF JIT 的架构。
目前x86_64
arm64
ppc64
s390x
,mips64
架构都带有内核内 eBPF JIT 编译器sparc64
。 arm
所有 BPF 处理,例如将程序加载到内核或创建 BPF 映射,都通过中央bpf()
系统调用进行管理。它还用于管理映射条目(查找/更新/删除),并通过固定使程序和映射在 BPF 文件系统中持久化。
觉得不错,点击“分享”,“赞”,“在看”传播给更多热爱嵌入式的小伙伴吧!

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

评论