重看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的胖子
原文地址:https://www.cnblogs.com/codestack/p/14733074.html