zoukankan      html  css  js  c++  java
  • 重看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
    ",
                   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/

    http代理服务器(3-4-7层代理)-网络事件库公共组件、内核kernel驱动 摄像头驱动 tcpip网络协议栈、netfilter、bridge 好像看过!!!! 但行好事 莫问前程 --身高体重180的胖子
  • 相关阅读:
    Apache、NGINX支持中文URL
    JS中关于clientWidth offsetWidth scrollWidth 等的含义
    设置apache登陆密码验证
    通过java代码访问远程主机
    win7
    Netty从没听过到入门 -- 服务器端详解
    分块分段
    数论-佩尔方程
    数论-毕达哥拉斯三元组
    HDU 5613-Baby Ming and Binary image
  • 原文地址:https://www.cnblogs.com/codestack/p/14733074.html
Copyright © 2011-2022 走看看