重看ebpf -代码载入执行点-hook
先看看之前的sockmap sockmap_ebpf sock_map2 ipvs-ebpf
EBPF:本质上它是一种内核代码注入的技术
- 内核中实现了一个cBPF/eBPF虚拟机
- 用户态可以用C来写运行的代码,再通过一个Clang&LLVM的编译器将C代码编译成BPF目标码
- 用户态通过系统调用bpf()将BPF目标码注入到内核当中
- 内核通过JIT(Just-In-Time)将BPF目编码转换成本地指令码;如果当前架构不支持JIT转换内核则会使用一个解析器(interpreter)来模拟运行,这种运行效率较低;
- 内核在packet filter和tracing等应用中提供了一系列的钩子来运行BPF代码。目前支持以下类型的BPF代码
提供了一种在不修改内核代码的情况下,可以灵活修改内核处理策略的方法
#include <uapi/linux/bpf.h> #include <uapi/linux/if_ether.h> #include <uapi/linux/if_packet.h> #include <uapi/linux/ip.h> #include <bpf/bpf_helpers.h> #include "bpf_legacy.h" struct { __uint(type, BPF_MAP_TYPE_ARRAY); __type(key, u32); __type(value, long); __uint(max_entries, 256); } my_map SEC(".maps"); SEC("socket1") int bpf_prog1(struct __sk_buff *skb) { int index = load_byte(skb, ETH_HLEN + offsetof(struct iphdr, protocol)); long *value; if (skb->pkt_type != PACKET_OUTGOING) return 0; value = bpf_map_lookup_elem(&my_map, &index); if (value) __sync_fetch_and_add(value, skb->len); return 0; } char _license[] SEC("license") = "GPL";
只有一个 my_map 数据结构和 bpf_prog1 函数;bpf_prog1 就是我们在内核执行的程序片段,它的入参是报文 skb。这个函数完成了以下功能:
- 统计各个协议报文的数据量
// SPDX-License-Identifier: GPL-2.0 #include <stdio.h> #include <assert.h> #include <linux/bpf.h> #include <bpf/bpf.h> #include <bpf/libbpf.h> #include "sock_example.h" #include <unistd.h> #include <arpa/inet.h> int main(int ac, char **argv) { struct bpf_object *obj; int map_fd, prog_fd; char filename[256]; int i, sock; FILE *f; snprintf(filename, sizeof(filename), "%s_kern.o", argv[0]); /* 装载文件 sockex1_kern.o */ if (bpf_prog_load(filename, BPF_PROG_TYPE_SOCKET_FILTER, &obj, &prog_fd)) return 1; map_fd = bpf_object__find_map_fd_by_name(obj, "my_map"); sock = open_raw_sock("lo"); /* 创建一个 socket, bind 到环回口设备 */ /* 设置 socket 的 SO_ATTACH_BPF 选项,传入 prog_fd */ assert(setsockopt(sock, SOL_SOCKET, SO_ATTACH_BPF, &prog_fd, sizeof(prog_fd)) == 0); f = popen("ping -4 -c5 localhost", "r"); (void) f; for (i = 0; i < 5; i++) { long long tcp_cnt, udp_cnt, icmp_cnt; int key; key = IPPROTO_TCP; assert(bpf_map_lookup_elem(map_fd, &key, &tcp_cnt) == 0); key = IPPROTO_UDP; assert(bpf_map_lookup_elem(map_fd, &key, &udp_cnt) == 0); key = IPPROTO_ICMP; assert(bpf_map_lookup_elem(map_fd, &key, &icmp_cnt) == 0); printf("TCP %lld UDP %lld ICMP %lld bytes\n", tcp_cnt, udp_cnt, icmp_cnt); sleep(1); } return 0; }
sock_user:代码核心分析:
- bpf_prog_load的入参 sockex1_user.o 是如何转换成虚拟机机器码注入内核的?
- 内核代码何时执行,执行的上下文是什么?
- 用户空间和内核空间的程序是如何通过 map 进行通信?
bpf_prog_load是 libbpf苦衷提供的函数;最后会调用 sys_bpf(BPF_PROG_LOAD, &attr, sizeof(attr)); 将code 注入到内核!!、
SYSCALL_DEFINE3(bpf, int, cmd, union bpf_attr __user *, uattr, unsigned int, size){
...... case BPF_PROG_LOAD: err = bpf_prog_load(&attr); } static int bpf_prog_load(union bpf_attr *attr) { struct bpf_prog *prog; ...... /* 分配内核 bpf_prog 程序数据结构空间 */ prog = bpf_prog_alloc(bpf_prog_size(attr->insn_cnt), GFP_USER); ..... /* 将 bpf 虚拟机指令从用户空间拷贝到内核空间 */ copy_from_user(prog->insns, u64_to_user_ptr(attr->insns), bpf_prog_insn_size(prog)); ..... /* 分配一个 fd 与 prog 关联,最终这个 fd 将返回用户空间 * /
/*此时 file->private_data = priv; 也就是 file->private_data = prog
表示 注入内核的BPF程序--字节码 关联到 fd的priva_data上
所以后续当内核执行hook的时候,根据hook的fd查找到private-data找到bpf代码执行 */
err = bpf_prog_new_fd(prog); ..... return err; }
用户空间通过系统调用陷入内核后,内核也会分配相应的数据结构 struct bpf_prog,并从用户空间拷贝虚拟机指令。然后分配一个文件系统的 inode 节点,将它与 bpf_prog 关联起来,最后将文件描述符返回给用户空间。
eBPF 程序指令都是在内核的特定 Hook 点执行,不同类型的程序有不同的钩子,有不同的上下文
将指令 load 到内核时,内核会创建 bpf_prog 存储指令,但只是第一步,成功运行这些指令还需要完成以下两个步骤:
- 将 bpf_prog 与内核中的特定 Hook 点关联起来,也就是将程序挂到钩子上。
- 在 Hook 点被访问到时,取出 bpf_prog,执行这些指令。
比如:
SOCKET FILTER 类型 eBPF 程序通过 SO_ATTACH_BPF 选项完成设置
XDP 类型的 eBPF 程序,则通过 Netlink 的方式设置 Hook 点
每一个 load 到内核的 eBPF 程序都有一个 fd 会返回给用户,它对应一个 bpf_prog。
XDP 程序设置 Hook 点的方式就是将这个 fd 与 一个网卡联系起来,通过 Netlink 消息告诉内核。
int main(int argc, char **argv) { struct rlimit r = {RLIM_INFINITY, RLIM_INFINITY}; struct bpf_prog_load_attr prog_load_attr = { .prog_type = BPF_PROG_TYPE_XDP, }; int prog_fd, map_fd, opt; struct bpf_object *obj; struct bpf_map *map; ---------------------- snprintf(filename, sizeof(filename), "%s_kern.o", argv[0]); prog_load_attr.file = filename; if (bpf_prog_load_xattr(&prog_load_attr, &obj, &prog_fd)) return 1; map = bpf_map__next(NULL, obj); map_fd = bpf_map__fd(map); signal(SIGINT, int_exit); signal(SIGTERM, int_exit); if (bpf_set_link_xdp_fd(ifindex, prog_fd, xdp_flags) < 0) { } err = bpf_obj_get_info_by_fd(prog_fd, &info, &info_len); prog_id = info.id; poll_stats(map_fd, 2); return 0; }
其中 ifindex 为网卡的标识,而 prog_fd 为 load 的 eBPF 程序时返回的 fd
int bpf_set_link_xdp_fd(int ifindex, int fd, __u32 flags) { // code omitted ... nla->nla_type = NLA_F_NESTED | IFLA_XDP; // code omitted ... nla_xdp->nla_type = IFLA_XDP_FD; // code omitted ...
bpf_set_link_xdp_fd 打包 Netlink 消息,消息类型为 IFLA_XDP,子类型为 IFLA_XDP_FD, 表示要关联 bpf_prog
内核收到该 Netlink 消息后, 根据消息类型,最终调用到 dev_change_xdp_fd
do_setlink { // code omitted ... if (tb[IFLA_XDP]) { // code omitted ... if (xdp[IFLA_XDP_FD]) { err = dev_change_xdp_fd(dev, extack, nla_get_s32(xdp[IFLA_XDP_FD]),
expected_fd, xdp_flags); } }
dev_change_xdp_fd 意为为 dev 关联一个 XDP 程序的 fd。它使用网卡设备驱动程序的 do_bpf 方法,进行 XDP 程序的安装
/** * dev_change_xdp_fd - set or clear a bpf program for a device rx path * @dev: device * @extack: netlink extended ack * @fd: new program fd or negative value to clear * @expected_fd: old program fd that userspace expects to replace or clear * @flags: xdp-related flags * * Set or clear a bpf program for a device */ int dev_change_xdp_fd(struct net_device *dev, struct netlink_ext_ack *extack, int fd, int expected_fd, u32 flags) { // return f.file->private_data; 同时检测prog 是否为 TYPE_XDP prog = bpf_prog_get_type_dev(fd, BPF_PROG_TYPE_XDP, bpf_op == ops->ndo_bpf); err = dev_xdp_install(dev, bpf_op, extack, flags, prog); }
每个支持 XDP 的网卡都有自己的 ndo_bpf 实现,以 Intel i40e 为例,其实现为 i40e_xdp
static const struct net_device_ops i40e_netdev_ops = { // code omitted ... .ndo_bpf = i40e_xdp, } static int i40e_xdp(struct net_device *dev, struct netdev_bpf *xdp) { struct i40e_netdev_priv *np = netdev_priv(dev); struct i40e_vsi *vsi = np->vsi; switch (xdp->command) { case XDP_SETUP_PROG: return i40e_xdp_setup(vsi, xdp->prog); // add/remove an XDP program // code omitted ... } static int i40e_xdp_setup(struct i40e_vsi *vsi, struct bpf_prog *prog) { // code omitted ... old_prog = xchg(&vsi->xdp_prog, prog); // code omitted ... for (i = 0; i < vsi->num_queue_pairs; i++) WRITE_ONCE(vsi->rx_rings[i]->xdp_prog, vsi->xdp_prog); }
运行 Hook 点上设置的 eBPF 程序
i40e_clean_rx_irq
|
|- if (!skb) {
xdp.data = page_address(rx_buffer->page) +
rx_buffer->page_offset;
xdp.data_hard_start = (void *)((u8 *)xdp.data -
i40e_rx_offset(rx_ring));
xdp.data_end = (void *)((u8 *)xdp.data + size);
skb = i40e_run_xdp(rx_ring, &xdp);i40e_xdp_setup
}
static struct sk_buff *i40e_run_xdp(struct i40e_ring *rx_ring, struct xdp_buff *xdp) { int result = I40E_XDP_PASS; #ifdef HAVE_XDP_SUPPORT struct i40e_ring *xdp_ring; struct bpf_prog *xdp_prog; u32 act; int err; rcu_read_lock(); xdp_prog = READ_ONCE(rx_ring->xdp_prog); if (!xdp_prog) goto xdp_out; prefetchw(xdp->data_hard_start); /* xdp_frame write */ act = bpf_prog_run_xdp(xdp_prog, xdp); // 运行 eBPF 程序 switch (act) { case XDP_PASS: rx_ring->xdp_stats.xdp_pass++; break; case XDP_TX: xdp_ring = rx_ring->vsi->xdp_rings[rx_ring->queue_index]; result = i40e_xmit_xdp_ring(xdp, xdp_ring); rx_ring->xdp_stats.xdp_tx++; break; case XDP_REDIRECT: ----------------------------- case XDP_DROP: result = I40E_XDP_CONSUMED; rx_ring->xdp_stats.xdp_drop++; break; } xdp_out: rcu_read_unlock(); return (struct sk_buff *)ERR_PTR(-result); }
运行 eBPF 程序就是使用 BPF_PROG_RUN,对于 XDP 类型的程序来说,其参数除了指令(prog->insnsi)外,就是报文(struct xdp_buff* xdp )
#define BPF_PROG_RUN(filter, ctx) (*(filter)->bpf_func)(ctx, (filter)->insnsi) static u32 bpf_prog_run_xdp(const struct bpf_prog *prog, struct xdp_buff *xdp) { return BPF_PROG_RUN(prog, xdp); }
来自;https://switch-router.gitee.io/blog/bpf-3/
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
2020-05-05 对“线上问题 不能gdb调试怎么处理??“”的思考