揭秘 BPF map 前生今世_bpfdm
off999 2025-09-19 01:10 36 浏览 0 评论
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.c[1] 和 sockex1_kern.c[2],略有修改和删除。
sockex1_user.c[3] 用户空间程序主要内容如下(为方便展示,部分内容有删除和修改):
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[4] 中:
int bpf_map_lookup_elem(int fd, const void *key, void *value)
函数底层通过 sys_bpf(cmd=BPF_MAP_LOOKUP_ELEM,...) 实现,为我们方便 map 操作的用户空间封装函数, bpf 系统调用可参考 man 2 bpf[5]。
其中 sockex1_kern.c[6] 主要内容如下:
// 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[7]:
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 fd 与 struct bpf_map *map 共同关联一个对象呢?这需要我们通过分析 BPF 字节码来进行解密。
相关视频推荐
神奇的linux技术:Linux观测技术bpf,用bpf来观测tcp网络
学习地址:C/C++Linux服务器开发/后台架构师【零声教育】-学习视频教程-腾讯课堂
需要C/C++ Linux服务器架构师学习资料加qun812855908获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享
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 对象指针的关联关系。
俗话说一图胜千言,这里我们用一张图进行整个流程的总结:
相关推荐
- 浏览器最好用的(浏览器最好用的插件)
-
一、谷歌浏览器谷歌浏览器是公认最好用的,这个可以从市场占有率看出端倪,超过三分之二的用户使用谷歌浏览器。Chrome浏览器以简洁快速著称,不管是普通用户还是开发人员,chrome浏览器都是首选。Chr...
- fast路由器6位初始密码(fast路由器的密码)
-
答:fast路由器初始密码是admin;新款的迅捷无线路由器,管理界面没有初始密码。查看迅捷无线路由器底部标签,标签上标注了admin,说明初始密码就是admin;如果没有,说明该路由器没有初始密码。...
- 硬盘恢复软件哪个好(硬盘 恢复软件)
-
迷你兔数据恢复工具:支持恢复硬盘丢失的数据Pc3000数据恢复软件是一款非常专业的硬盘修复工具,能够对电脑硬盘资料数据进行修复,通过使用这个软件可以解决硬盘数据丢失故障,是一个用户进行硬盘资料修复好帮...
- 十大品牌监控摄像头排名(十大品牌监控摄像头排名第一)
-
答:1、华为/HUAWEI9.92、小米/MI9.63、罗技/Logitech9.64、海康威视/HIKVISION9.25、乔安/Jooan9.26、普联/TP-LINK9.27、乐橙8.98、萤石...
- 360手机助手老旧版本大全(360手机助手 老版本)
-
在设置里面找到历史记录就可能查看360手机助手·换机神器是安卓系统的换机软件,因为苹果的换机软件是爱思。1、打开360手机卫士,登陆账号,点击账号。2、进入个人中心,点击账户安全,在密保工具中,点击解...
- 系统应用工程师(系统工程的应用)
-
信息软件系统工程师有前途,毕业以后可以从事软件开发,软件系统的维护,运营等等,和计算机有关的工作内容,因为计算机专业可以说是一个非常热门的专业,很多的大型企业公司基本上都是以计算机研发为主的,薪资福利...
- qq恢复系统常见问题(qq恢复系统常见问题及答案)
-
QQ好友恢复系统一直显示服务器频繁有可能是系统的问题,你的qq重新更新一下,或者你卸载了重新下载一下就有可能好了。您好,很高兴为您解答:您可以试试清空下IE内存,然后关掉不必要的程序,尽量在电脑运...
- win10自带的office不见了(win10自带的office在哪个文件夹)
-
win10系统的office在电脑硬盘office的安装目录里,具体打开安装目录的操作如下:1、首先我们右键点击word,打开方式,选择默认程序打开。2、在默认框打钩,点击【浏览】。3、打开你安装of...
- w10系统我的电脑在哪里(windows10 我的电脑在哪)
-
首先,打开Windows10系统,可以看到当前桌面上没有“我的电脑”。二、然后,在桌面上鼠标右键都加空白处,在右键菜单中选择“个性化”,点击打开。三、然后,在窗口中左侧选择“更改桌面图标”,点击打开。...
- 2500电脑组装最强配置(2500左右组装电脑主机配置清单)
-
两千五百元组装电脑,但配置只能是一般。台式机分为主机和显示器两个主体,按这个价格,显示器只能配置一般的,大约两百多元价格,其余都用来组装主机,主机包含机箱,电源,排风扇,电脑主板及内存,电脑处理器,声...
-
- 小米手机定时关机怎么设置(如何让小米手机定时关机)
-
1、从设置菜单中找到电池与性能选项。 2、选择电池版块,点击定时开关机。 3、将定时开机右边的按钮开...
-
2025-12-28 02:51 off999
- 磁盘删除的文件怎么恢复(磁盘误删怎么恢复)
-
可以恢复,因为删除文件时,其实只是把表头删除了,后面的数据并没有删除,直到下一次进行写磁盘操作需要占用节点所在位置时,才会把相应的数据覆盖掉。所以,就算你误删了文件之后又进行了其他写磁盘操作,只要没有...
-
- qq自动回复内容古风(qq自动回复古文)
-
1、抽剑相助,搭救无辜,却引来杀身之祸。那女子故作柔弱,假装爱慕,只为那本剑谱。2、只缘感君一回顾,使我思君朝与暮。3、相知相惜若可谓缘,不负韶华年。4、看那天地日月,恒静无言;青山长河,世代绵延;就像在我心中,你从未离去,也从未改变。5、...
-
2025-12-28 01:51 off999
欢迎 你 发表评论:
- 一周热门
-
-
抖音上好看的小姐姐,Python给你都下载了
-
全网最简单易懂!495页Python漫画教程,高清PDF版免费下载
-
Python 3.14 的 UUIDv6/v7/v8 上新,别再用 uuid4 () 啦!
-
飞牛NAS部署TVGate Docker项目,实现内网一键转发、代理、jx
-
python入门到脱坑 输入与输出—str()函数
-
宝塔面板如何添加免费waf防火墙?(宝塔面板开启https)
-
Python三目运算基础与进阶_python三目运算符判断三个变量
-
(新版)Python 分布式爬虫与 JS 逆向进阶实战吾爱分享
-
失业程序员复习python笔记——条件与循环
-
系统u盘安装(win11系统u盘安装)
-
- 最近发表
- 标签列表
-
- python计时 (73)
- python安装路径 (56)
- python类型转换 (93)
- python进度条 (67)
- python吧 (67)
- python的for循环 (65)
- python格式化字符串 (61)
- python静态方法 (57)
- python列表切片 (59)
- python面向对象编程 (60)
- python 代码加密 (65)
- python串口编程 (77)
- python封装 (57)
- python写入txt (66)
- python读取文件夹下所有文件 (59)
- python操作mysql数据库 (66)
- python获取列表的长度 (64)
- python接口 (63)
- python调用函数 (57)
- python多态 (60)
- python匿名函数 (59)
- python打印九九乘法表 (65)
- python赋值 (62)
- python异常 (69)
- python元祖 (57)
