重看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/

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM