揭秘 BPF map 前生今世

简介: 揭秘 BPF map 前生今世

揭秘 BPF map 前生今世


本文地址:https://www.ebpf.top/post/map_internal



1. 前言


众所周知,map 可用于内核 BPF 程序和用户应用程序之间实现双向的数据交换, 为 BPF 技术中的重要基础数据结构。


在 BPF 程序中可以通过声明 struct bpf_map_def 结构完成创建,这其实带给我们一种错觉,感觉这和普通的 C 语言变量没有区别,然而事实真的是这样的吗? 事情远没有这么简单,读完本文以后相信你会有更大的惊喜。


struct bpf_map_def SEC("maps") my_map = {
  .type = BPF_MAP_TYPE_ARRAY,
   // ...
};


我们知道最终 BPF 程序是需要在内核中执行,但是 map 数据结构是用于用户空间和内核 BPF 程序双向的数据结构,那么问题来了:


  • 通过 struct bpf_map_def 定义的变量究竟是如何创建的,是在用户空间创建还是内核中直接创建的?
  • 如何实现创建后的 map 的结构,在用户空间与内核中 BPF 程序关联?你可能注意到在用户空间中对于 map 的访问是通过 map 文件句柄 fd 完成(类型为 int),但是在 BPF 程序中是通过 struct bpf_map * 结构完成的。


毕竟数据交换跨越了用户空间和内核空间,本文将从深入浅出为各位看官揭开 map 整个生命管理的 "大瓜"。


2. 简单的使用样例


本样例来自于 samples/bpf/sockex1_user.csockex1_kern.c,略有修改和删除。

sockex1_user.c 用户空间程序主要内容如下(为方便展示,部分内容有删除和修改):


int main(int argc, char **argv)
{
  struct bpf_object *obj;
  int map_fd, prog_fd;
  // ...
// 加载 BPF 程序至 bpf_object 对象中,
  bpf_prog_load("sockex_kern.o", BPF_PROG_TYPE_SOCKET_FILTER, &obj, &prog_fd))
// 获取 my_map 对应的 map_fd 句柄
  map_fd = bpf_object__find_map_fd_by_name(obj, "my_map"); // == 本次关注 ==
// 通过 setsockopt 将 BPF 字节码加载到内核中
  sock = open_raw_sock("lo");
  setsockopt(sock, SOL_SOCKET, SO_ATTACH_BPF, &prog_fd, sizeof(prog_fd));
  popen("ping -4 -c5 localhost", "r"); // 产生报文
// 从 my_map 中读取 5 次 IPPROTO_TCP 的统计
  for (i = 0; i < 5; i++) { 
    long long tcp_cnt;
    int key = IPPROTO_TCP;
    assert(bpf_map_lookup_elem(map_fd, &key, &tcp_cnt) == 0); // == 本次关注 ==
    // ...
    sleep(1);
  }
  return 0;
}


sockex1_user.c 文件中的 bpf_map_lookup_elem 调用的函数原型如下,定义在文件 tools/lib/bpf/bpf.c 中:


int bpf_map_lookup_elem(int fd, const void *key, void *value)


函数底层通过 sys_bpf(cmd=BPF_MAP_LOOKUP_ELEM,...) 实现,为我们方便 map 操作的用户空间封装函数, bpf 系统调用可参考 man 2 bpf


其中 sockex1_kern.c 主要内容如下:


// map 定义 
struct bpf_map_def SEC("maps") my_map = {
  .type = BPF_MAP_TYPE_ARRAY,
  .key_size = sizeof(u32),
  .value_size = sizeof(long),
  .max_entries = 256,
};
// BPF 程序,获取到报文协议类型并进行计数更新
SEC("socket1")
int bpf_prog1(struct __sk_buff *skb)
{
  int index = load_byte(skb, ETH_HLEN + offsetof(struct iphdr, protocol));
  long *value;
  value = bpf_map_lookup_elem(&my_map, &index);  // 查找索引并更新 map 对应的值,== 本次关注 ==
  if (value)
    __sync_fetch_and_add(value, skb->len);
  return 0;
}
char _license[] SEC("license") = "GPL";


sockex1_kern.c 文件中的 bpf_map_lookup_elem 函数为内核中提供的 BPF 辅助函数,原型声明如下,详情可参考 man 7 bpf-helper


void *bpf_map_lookup_elem(struct bpf_map *map, const void *key)


用户空间与内核 BPF 辅助函数参数对比


通过分析 sockex1_user.c 和 sockex1_kern.c 函数中的 bpf_map_lookup_elem 使用姿势,这里我们做个简单对比:


// 用户空间 map 查询函数
int bpf_map_lookup_elem(int fd, const void *key, void *value)
// 内核中 BPF 辅助函数 map 查询函数
void *bpf_map_lookup_elem(struct bpf_map *map, const void *key)


那么如何将 int fdstruct bpf_map *map 共同关联一个对象呢? 这需要我们通过分析 BPF 字节码来进行解密。


3. 深入指令分析


首先我们将 sockex1_kern.c 文件使用 llvm/clang 将之编译成 ELF 的 BPF 字节码。对于生成的 sockex1_kern.o 文件可以用 llvm-objdump 来查看相对应的文件格式,这里我们仅关注 map 相关的部分。


3.1 查看 BPF 指令


$ clang -O2 -target bpf -c sockex1_kern.c  -o sockex1_kern.o
$ llvm-objdump -S sockex1_kern.o
0000000000000000 <bpf_prog1>:
       // ...
        ;   value = bpf_map_lookup_elem(&my_map, &index); # 备注:编译的机器启用了 BTF 
       7: 18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll
       9: 85 00 00 00 01 00 00 00 call 1
       // ...


上述结果展示了 BPF 程序中 socket1 部分的函数 bpf_prog1 的 BPF 指令,但是其中对于涉及到的变量 my_map 的引用都未有解决。上述的反汇编部分打印了 map_lookup_elem() 函数调用涉及的指令:


  • 根据 BPF 程序调用的约定,寄存器 r1 为函数调用的第 1 个参数,这里即 bpf_map_lookup_elem(&my_map, &index) 调用中的 my_map

7:  18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll  # 64 位直接数赋值 , r1 = 0 
       9: 85 00 00 00 01 00 00 00 call 1                             # 调用 bpf_map_lookup_elem,编号为 1


上述 "7:" 行 表了为一条 16 个字节的 BPF 指令,表示加载一个 64 位立即数。

这里无需担心相关的 BPF 指令集,后续我们会详细展开解释。1 个 BPF 指令有 8 个字节组成,格式定义如下:


struct bpf_insn {
    __u8    code;         /* opcode */
    __u8    dst_reg:4;    /* dest register */
    __u8    src_reg:4;    /* source register */
    __s16   off;        /* signed offset */
    __s32   imm;        /* signed immediate constant */
};


通过上述结构对应拆解一下 ”7:“ 行(其中包含了 2 条 BPF 指令,为 BPF 指令中的特殊指令,运行时会被解析成 1 条指令执行) ,第 1 条 BPF 指令详细的信息如下:(这里忽略了 off 字段)


  • opcode 为 0x18,即 BPF_LD | BPF_IMM | BPF_DW。该 opcode 表示要将一个 64 位的立即数加载到目标寄存器。
  • dst_reg 是 1(4 个 bit 位),代表寄存器 r1
  • src_reg 是 0(4 个 bit 位),表示立即数在指令内。
  • imm 为 0,因为 my_map 的值在生成 BPF 字节码的时候还未进行创建


第 2 条指令主要负责保存 imm 的高 32 位。


3.2 加载器创建 map 对象


当加载器(loader)在加载 ELF 对象 sockex1_kern.o 时,其首先会从 ELF 格式的 maps 区域获取到定义的 map 对象 my_map 及相关的属性, 然后通过调用 bpf() 系统调用来创建 my_map 对象,如果创建成功,那么 bpf() 系统调用返回一个文件描述符 (map fd)。


同时,加载器也会对于基于 map 元信息(比如名称 my_map)与通过 bpf() 系统调用创建 map 后返回的 map fd 建立起对应关系,此后用户空间空间程序就可以使用 my_map 作为关键字获取到其对应的 fd,具体代码如下:


map_fd = bpf_object__find_map_fd_by_name(obj, "my_map");


用户空间获取到了 map 对象的 fd,后续可用于 map_lookup_elem(map_fd, ...) 函数进行 map 的查询等操作。


3.3 第一次变身: map fd 替换


以上完成了 my_map 对象的创建,但是在 BPF 字节码程序加载到内核前,还需要将 map fd 在 BPF 指令集中完成第一次变身,如函数 lib/bpf.c:

bpf_apply_relo_map() 的代码片段所示:


prog->insns[insn_off].src_reg = BPF_PSEUDO_MAP_FD; // 值在内核中定义为 1
        prog->insns[insn_off].imm = ctx->map_fds[map_idx]; // ctx->map_fds[map_idx] 即为保存的 map fd 值。


这里假设获取到的 map 文件描述符为 6,那么在加载的 BPF 程序完成 bpf_apply_relo_map 的替换后上述的指令对比如下:

ELF 文件中的字节码:


7:  18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll  # 64 位直接数赋值 , r1 = 0 
       9: 85 00 00 00 01 00 00 00 call 1                             # 调用 bpf_map_lookup_elem,编号为 1


替换 map fd 后的字节码:


7:  18 11 00 00 06 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll  # 64 位直接数赋值 , r1 = 6 
       9: 85 00 00 00 01 00 00 00 call 1                             # 调用 bpf_map_lookup_elem,编号为 1


3.4 第二次变身: map fd 替换成 map 结构指针


当上述经过第一次变身的 BPF 字节码加载到内核后,还需要进行一次变身,才能真正在内核中工作,这次 BPF 验证器(verifier)扛过大旗。


验证器将加载器注入到指令中的 map fd 替换成内核中的 map 对象指针。调用堆栈的情况如下:


sys_bpf()
    --> bpf_prog_load()
        --> bpf_check()
            --> replace_map_fd_with_map_ptr()
            --> do_check()
                --> check_ld_imm()
                ==> check_func_arg()
            --> convert_pseudo_ld_imm64()


函数 replace_map_fd_with_map_ptr() 通过以下代码完成第二次大变身,实现了内核中 BPF 字节码的 imm 摇身一变成为 map ptr 地址。


f = fdget(insn[0].imm);  // 从第 1 条指令中的 imm 字段获取到加载器设置的 map fd
map = __bpf_map_get(f);  // 基于 map fd 获取到 map 对象指针
        addr = (unsigned long)map;  
        insn[0].imm = (u32)addr;   // 将 map  对象指针低 32 位放入第一条指令中的 imm 字段
        insn[1].imm = addr >> 32;  // 将 map  对象指针高 32 位放入第二条指令中的 imm 字段


于此同时,函数 convert_pseudo_ld_imm64() 还需要清理加载器设置的 src_reg = BPF_PSEUDO_MAP_FD 操作( prog->insns[insn_off].src_reg = BPF_PSEUDO_MAP_FD;), 用于表明完成了整个指令的重写工作:


if (insn->code == (BPF_LD | BPF_IMM | BPF_DW))
                insn->src_reg = 0;


如果这里的 my_map 在内核中 64 位地址为 0xffff8881384aa200,那么验证器完成第二次变身后的 BPF 字节码对比如下。


替换 map fd 后的字节码:


7:  18 11 00 00 06 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll  # 64 位直接数赋值 , r1 = 6 
       9: 85 00 00 00 01 00 00 00 call 1                             # 调用 bpf_map_lookup_elem,编号为 1


替换为 map 对象指针后的字节码如下:


7: 18 01 00 00 00 a2 4a 38 00 00 00 00 81 88 ff ff           # 64 位直接数赋值 , r1 = 0xffff8881384aa200 
      9: 85 00 00 00 30 86 01 00                                   # 调用 bpf_map_lookup_elem,编号为 1


在完成了上述两次变身后,当在内核中调用 map_lookup_elem() 时,第一个参数 my_map 的值为 0xffff8881384aa200


从而实现了从最早的 ELF 中的 0 ,替换成了 map_fd (6),直到最后的 map 对象 struct bpf_map * (0xffff8881384aa200)


提示,内核中 bpf_map_lookup_elem 辅助函数的原型定义为:


static void *(*bpf_map_lookup_elem)(struct bpf_map *map, void *key)


4. 整个流程总结


通过上述 map 访问指令的 2 次大变身,我们可以清晰了解 map 创建、map fd 指令重写和 map ptr 对象的重写,也能够彻底明白用户空间 map fd 与内核中 map 对象指针的关联关系。


俗话说一图胜千言,这里我们用一张图进行整个流程的总结:



原始图片来自于这里 ,略有修改。


参考


目录
相关文章
|
6月前
|
SQL 存储 资源调度
UCB Data100:数据科学的原理和技巧:第十九章到第二十章
UCB Data100:数据科学的原理和技巧:第十九章到第二十章
85 0
|
6月前
|
机器学习/深度学习 资源调度 算法
UCB Data100:数据科学的原理和技巧:第二十一章到第二十六章
UCB Data100:数据科学的原理和技巧:第二十一章到第二十六章
101 0
|
监控 算法 区块链
Metaforce佛萨奇2.0系统开发(马蹄链)源码部署
共识机制是指在区块链网络中public boolean equals
|
编译器 Linux
荔枝派Zero(全志V3S)编译Kernel
上文我们讲述了uboot编译及配置,本文讲述了如何编译kernel,对编译过程中遇到的问题进行解决
248 0
|
存储 机器学习/深度学习 Dragonfly
龙蜥社区首次突破!高性能存储 SIG 现身 LSF/MM/BPF 2023 分享 EROFS 的演进路线
Gao Xiang 在 LSF/MM/BPF 会议中做了 EROFS 文件系统的介绍:EROFS 已成为安卓系统分区推荐方案,目前它越来越不局限于原始目标场景,不断突破自身应用边界。例如,在龙蜥社区合作伙伴的努力下,EROFS 的容器镜像场景也有许多应用落地。
|
区块链 开发者
深入分析Metaforce/Forsage/魔豹联盟/Polygon马蹄链Matic/佛萨奇2.0系统开发实现技术原理丨成熟及源码
 智能合约dapp开发技术主要由以太坊区块链网络提供支持,该网络提供了一系列的智能合约技术,这些智能合约可以让开发者快速、安全地构建出功能强大的dapp。智能合约dapp开发技术主要包括以太坊智能合约语言Solidity,以太坊智能合约框架Truffle,Web3.js,以太坊区块链浏览器Mist等
|
存储 监控 区块链
什么是Matic马蹄链polygon/MetaForce/Forsage/魔豹联盟/佛萨奇2.0系统开发(开发源码)丨详细规则
 去中心化存储技术的结构为去中心化节点网络,它采用分布式存储方式来存储数据并保护这些数据。分布式存储方式使用多个结点以多层结构来管理数据,使得每个结点都有能力参与到存储系统的监控、管理和数据同步行为中,从而改变传统的数据备份结构,使其能够保护用户的数据不被任何人或机构访问。
|
测试技术 区块链
佛萨奇2.0Polygon马蹄链Matic开发原理丨佛萨奇2.0Polygon马蹄链Matic系统开发(运营版)及案例详细
DAO代表去中心化自治组织(Decentralized Autonomous Organization),是一种在Web3中基于区块链技术的组织形式。DAO是一个智能合约的集合,旨在实现一组规则和程序来自动执行管理和决策的任务,而无需中心化的管理机构。
|
存储 并行计算 PyTorch
Matic马蹄链Polygon佛萨奇2.0开发详情版丨Matic马蹄链Polygon佛萨奇2.0系统开发(开发原理)丨atic马蹄链Polygon佛萨奇2.0源码版
本质上来说,智能合约是一段程序,它以计算机指令的方式实现了传统合约的自动化处理。智能合约程序不只是一个可以自动执行的计算机程序,它本身就是一个系统参与者,对接收到的信息进行回应,可以接收和储存价值,也可以向外发送信息和价值。这个程序就像一个可以被信任的人,可以临时保管资产,总是按照事先的规则执行操作。
|
人工智能 大数据 区块链
马蹄链MetaForce佛萨奇开发功能丨马蹄链MetaForce佛萨奇系统开发(2.0升级版)
  From a technical perspective,blockchain has entered the stage of platform-based,component-based and integrated development from the initial technological exploration.It is mainly reflected in the following aspects:First,the platform promotes the formation of urban chain network.City chains such as