EBPF(Berkeley Packet Filter)学习记录


相关资料

EBPF相关要点介绍

在内核中的 BPF 代码

/* Register numbers */
enum {
        BPF_REG_0 = 0,
        BPF_REG_1,
        BPF_REG_2,
        BPF_REG_3,
        BPF_REG_4,
        BPF_REG_5,
        BPF_REG_6,
        BPF_REG_7,
        BPF_REG_8,
        BPF_REG_9,
        BPF_REG_10,
        __MAX_BPF_REG,
};
  • 指令格式:
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 */
};
  • 指令宏:
/* instruction classes */
#define BPF_JMP32        0x06        /* jmp mode in word width */
#define BPF_ALU64        0x07        /* alu mode in double word width */
/* ld/ldx fields */
#define BPF_DW                0x18        /* double word (64-bit) */
#define BPF_ATOMIC        0xc0        /* atomic memory ops - op type in immediate */
#define BPF_XADD        0xc0        /* exclusive add - legacy name */
/* alu/jmp fields */
#define BPF_MOV                0xb0        /* mov reg to reg */
#define BPF_ARSH        0xc0        /* sign extending arithmetic shift right */
/* change endianness of a register */
#define BPF_END                0xd0        /* flags for endianness conversion: */
#define BPF_TO_LE        0x00        /* convert to little-endian */
#define BPF_TO_BE        0x08        /* convert to big-endian */
#define BPF_FROM_LE        BPF_TO_LE
#define BPF_FROM_BE        BPF_TO_BE
/* jmp encodings */
#define BPF_JNE                0x50        /* jump != */
#define BPF_JLT                0xa0        /* LT is unsigned, '<' */
#define BPF_JLE                0xb0        /* LE is unsigned, '<=' */
#define BPF_JSGT        0x60        /* SGT is signed '>', GT in x86 */
#define BPF_JSGE        0x70        /* SGE is signed '>=', GE in x86 */
#define BPF_JSLT        0xc0        /* SLT is signed, '<' */
#define BPF_JSLE        0xd0        /* SLE is signed, '<=' */
#define BPF_CALL        0x80        /* function call */
#define BPF_EXIT        0x90        /* function return */
/* atomic op type fields (stored in immediate) */
#define BPF_FETCH        0x01        /* not an opcode on its own, used to build others */
#define BPF_XCHG        (0xe0 | BPF_FETCH)        /* atomic exchange */
#define BPF_CMPXCHG        (0xf0 | BPF_FETCH)        /* atomic compare-and-write */
  • 指令码:

64-bit

Opcode 操作码 Mnemonic 助记符 Pseudocode 伪代码
0x07 add dst, imm dst += imm
0x0f add dst, src dst += src
0x17 sub dst, imm dst -= imm
0x1f sub dst, src dst -= src
0x27 mul dst, imm dst *= imm
0x2f mul dst, src dst *= src
0x37 div dst, imm dst /= imm
0x3f div dst, src dst /= src
0x47 or dst, imm dst |= imm
0x4f or dst, src dst |= src
0x57 and dst, imm dst &= imm
0x5f and dst, src dst &= src
0x67 lsh dst, imm dst <<= imm
0x6f lsh dst, src dst <<= src
0x77 rsh dst, imm dst >>= imm (logical)
0x7f rsh dst, src dst >>= src (logical)
0x87 neg dst dst = -dst
0x97 mod dst, imm dst %= imm
0x9f mod dst, src dst %= src
0xa7 xor dst, imm dst ^= imm
0xaf xor dst, src dst ^= src
0xb7 mov dst, imm dst = imm
0xbf mov dst, src dst = src
0xc7 arsh dst, imm dst >>= imm (arithmetic)
0xcf arsh dst, src dst >>= src (arithmetic)

32-bit

这些指令只对low 32位有效,会把high 32位清空

Opcode Mnemonic Pseudocode
0x04 add32 dst, imm dst += imm
0x0c add32 dst, src dst += src
0x14 sub32 dst, imm dst -= imm
0x1c sub32 dst, src dst -= src
0x24 mul32 dst, imm dst *= imm
0x2c mul32 dst, src dst *= src
0x34 div32 dst, imm dst /= imm
0x3c div32 dst, src dst /= src
0x44 or32 dst, imm dst |= imm
0x4c or32 dst, src dst |= src
0x54 and32 dst, imm dst &= imm
0x5c and32 dst, src dst &= src
0x64 lsh32 dst, imm dst <<= imm
0x6c lsh32 dst, src dst <<= src
0x74 rsh32 dst, imm dst >>= imm (logical)
0x7c rsh32 dst, src dst >>= src (logical)
0x84 neg32 dst dst = -dst
0x94 mod32 dst, imm dst %= imm
0x9c mod32 dst, src dst %= src
0xa4 xor32 dst, imm dst ^= imm
0xac xor32 dst, src dst ^= src
0xb4 mov32 dst, imm dst = imm
0xbc mov32 dst, src dst = src
0xc4 arsh32 dst, imm dst >>= imm (arithmetic)
0xcc arsh32 dst, src dst >>= src (arithmetic)

Byteswap instructions

Opcode Mnemonic Pseudocode
0xd4 (imm == 16) le16 dst dst = htole16(dst)
0xd4 (imm == 32) le32 dst dst = htole32(dst)
0xd4 (imm == 64) le64 dst dst = htole64(dst)
0xdc (imm == 16) be16 dst dst = htobe16(dst)
0xdc (imm == 32) be32 dst dst = htobe32(dst)
0xdc (imm == 64) be64 dst dst = htobe64(dst)

Memory Instructions

Opcode Mnemonic Pseudocode
0x18 lddw dst, imm dst = imm
0x20 ldabsw src, dst, imm See kernel documentation
0x28 ldabsh src, dst, imm
0x30 ldabsb src, dst, imm
0x38 ldabsdw src, dst, imm
0x40 ldindw src, dst, imm
0x48 ldindh src, dst, imm
0x50 ldindb src, dst, imm
0x58 ldinddw src, dst, imm
0x61 ldxw dst, [src+off] dst = *(uint32_t *) (src + off)
0x69 ldxh dst, [src+off] dst = *(uint16_t *) (src + off)
0x71 ldxb dst, [src+off] dst = *(uint8_t *) (src + off)
0x79 ldxdw dst, [src+off] dst = *(uint64_t *) (src + off)
0x62 stw [dst+off], imm *(uint32_t *) (dst + off) = imm
0x6a sth [dst+off], imm *(uint16_t *) (dst + off) = imm
0x72 stb [dst+off], imm *(uint8_t *) (dst + off) = imm
0x7a stdw [dst+off], imm *(uint64_t *) (dst + off) = imm
0x63 stxw [dst+off], src *(uint32_t *) (dst + off) = src
0x6b stxh [dst+off], src *(uint16_t *) (dst + off) = src
0x73 stxb [dst+off], src *(uint8_t *) (dst + off) = src
0x7b stxdw [dst+off], src *(uint64_t *) (dst + off) = src

Branch Instructions

Opcode Mnemonic Pseudocode
0x05 ja +off PC += off
0x15 jeq dst, imm, +off PC += off if dst == imm
0x1d jeq dst, src, +off PC += off if dst == src
0x25 jgt dst, imm, +off PC += off if dst > imm
0x2d jgt dst, src, +off PC += off if dst > src
0x35 jge dst, imm, +off PC += off if dst >= imm
0x3d jge dst, src, +off PC += off if dst >= src
0xa5 jlt dst, imm, +off PC += off if dst < imm
0xad jlt dst, src, +off PC += off if dst < src
0xb5 jle dst, imm, +off PC += off if dst <= imm
0xbd jle dst, src, +off PC += off if dst <= src
0x45 jset dst, imm, +off PC += off if dst & imm
0x4d jset dst, src, +off PC += off if dst & src
0x55 jne dst, imm, +off PC += off if dst != imm
0x5d jne dst, src, +off PC += off if dst != src
0x65 jsgt dst, imm, +off PC += off if dst > imm (signed)
0x6d jsgt dst, src, +off PC += off if dst > src (signed)
0x75 jsge dst, imm, +off PC += off if dst >= imm (signed)
0x7d jsge dst, src, +off PC += off if dst >= src (signed)
0xc5 jslt dst, imm, +off PC += off if dst < imm (signed)
0xcd jslt dst, src, +off PC += off if dst < src (signed)
0xd5 jsle dst, imm, +off PC += off if dst <= imm (signed)
0xdd jsle dst, src, +off PC += off if dst <= src (signed)
0x85 call imm Function call
0x95 exit return r0
#define __BPF_PROG_RUN(prog, ctx, dfunc)        ({                        \
        u32 __ret;                                                        \
        cant_migrate();                                                        \
        if (static_branch_unlikely(&bpf_stats_enabled_key)) {                \
                struct bpf_prog_stats *__stats;                                \
                u64 __start = sched_clock();                                \
                __ret = dfunc(ctx, (prog)->insnsi, (prog)->bpf_func);        \
                __stats = this_cpu_ptr(prog->stats);                        \
                u64_stats_update_begin(&__stats->syncp);                \
                __stats->cnt++;                                                \
                __stats->nsecs += sched_clock() - __start;                \
                u64_stats_update_end(&__stats->syncp);                        \
        } else {                                                        \
                __ret = dfunc(ctx, (prog)->insnsi, (prog)->bpf_func);        \
        }                                                                \
        __ret; })
#define BPF_PROG_RUN(prog, ctx)                                                \
        __BPF_PROG_RUN(prog, ctx, bpf_dispatcher_nop_func)
  • BPF 相关的 主要的代码片断linux/kernel/bpf/目录下面。系统调用的不同操作许可,比如,程序加载或者映射管理是在文件 syscall.c 中实现,而 core.c 包含了 解析器。其它文件的命名显而易见:verifier.c 包含 校验器arraymap.c 的代码用于与数组类型的 映射 交互,等等。

syscall.c

static int bpf_prog_load(union bpf_attr *attr, union bpf_attr __user *uattr)
SYSCALL_DEFINE3(bpf, int, cmd, union bpf_attr __user *, uattr, unsigned int, size)
  • 有几个与网络(及 tc、XDP )相关的函数和 helpers 是用户可用,其实现在 linux/net/core/filter.c中。它也包含了移植 cBPF 字节码到 eBPF 的代码(因为在运行之前,内核中的所有的 cBPF 程序被转换成 eBPF)。

  • 相关于 事件跟踪 的函数和 helpers 都在 linux/kernel/trace/bpf_trace.c 中。

/**
 * trace_call_bpf - invoke BPF program
 * @call: tracepoint event
 * @ctx: opaque context pointer
 *
 * kprobe handlers execute BPF programs via this helper.
 * Can be used from static tracepoints in the future.
 *
 * Return: BPF programs always return an integer which is interpreted by
 * kprobe handler as:
 * 0 - return from kprobe (event is filtered out)
 * 1 - store kprobe event into ring buffer
 * Other values are reserved and currently alias to 1
 */
unsigned int trace_call_bpf(struct trace_event_call *call, void *ctx)
{
        unsigned int ret;

        cant_sleep();

        if (unlikely(__this_cpu_inc_return(bpf_prog_active) != 1)) {
                /*
                 * since some bpf program is already running on this cpu,
                 * don't call into another bpf program (same or different)
                 * and don't send kprobe event into ring-buffer,
                 * so return zero here
                 */
                ret = 0;
                goto out;
        }

        /*
         * Instead of moving rcu_read_lock/rcu_dereference/rcu_read_unlock
         * to all call sites, we did a bpf_prog_array_valid() there to check
         * whether call->prog_array is empty or not, which is
         * a heuristic to speed up execution.
         *
         * If bpf_prog_array_valid() fetched prog_array was
         * non-NULL, we go into trace_call_bpf() and do the actual
         * proper rcu_dereference() under RCU lock.
         * If it turns out that prog_array is NULL then, we bail out.
         * For the opposite, if the bpf_prog_array_valid() fetched pointer
         * was NULL, you'll skip the prog_array with the risk of missing
         * out of events when it was updated in between this and the
         * rcu_dereference() which is accepted risk.
         */
        ret = BPF_PROG_RUN_ARRAY_CHECK(call->prog_array, ctx, BPF_PROG_RUN);

 out:
        __this_cpu_dec(bpf_prog_active);

        return ret;
}
bpf_tracing_func_proto(enum bpf_func_id func_id, const struct bpf_prog *prog)

XDP 钩子代码

一旦装载进内核的 BPF 虚拟机,由一个 Netlink 命令将 XDP 程序从用户空间钩入到内核网络路径中。接收它的是在 linux/net/core/dev.c 文件中的 dev_change_xdp_fd() 函数,它被调用并设置一个 XDP 钩子。钩子被放在支持的网卡的驱动程序中。例如,用于 Netronome 硬件钩子的 ntp 驱动程序实现放在 drivers/net/ethernet/netronome/nfp/ 中。文件 nfp_net_common.c 接受 Netlink 命令,并调用 nfp_net_xdp_setup(),它会转而调用 nfp_net_xdp_setup_drv() 实例来安装该程序。

在 bcc 中的 BPF 逻辑

在 bcc 的 GitHub 仓库能找到的 bcc 工具集的代码。其 Python 代码,包含在 BPF 类中,最初它在文件 bcc/src/python/bcc/init.py 中。但是许多我觉得有意思的东西,比如,加载 BPF 程序到内核中,出现在 libbcc 的 C 库中。

使用 tc 去管理 BPF 的代码

当然,这些代码与 iproute2 包中的 tc 中的 BPF 相关。其中的一些在 iproute2/tc/ 目录中。文件 f_bpf.cm_bpf.c(和 e_bpf.c)各自用于处理 BPF 的过滤器和动作的(和 tc exec 命令,等等)。文件 q_clsact.c 定义了为 BPF 特别创建的 clsact qdisc。但是,大多数的 BPF 用户空间逻辑 是在 iproute2/lib/bpf.c库中实现的,因此,如果你想去使用 BPF 和 tc,这里可能是会将你搞混乱的地方(它是从文件 iproute2/tc/tc_bpf.c 中移动而来的,你也可以在旧版本的包中找到相同的代码)。

BPF 实用工具

内核中也带有 BPF 相关的三个工具的源代码(bpf_asm.cbpf_dbg.cbpf_jit_disasm.c),根据你的版本不同,在 linux/tools/net/(直到 Linux 4.14)或者 linux/tools/bpf/目录下面:

  • bpf_asm 是一个极小的 cBPF 汇编程序。

  • bpf_dbg 是一个很小的 cBPF 程序调试器。

  • bpf_jit_disasm 对于两种 BPF 都是通用的,并且对于 JIT 调试来说非常有用。

  • bpftool 是由 Jakub Kicinski 写的通用工具,它可以与 eBPF 程序交互并从用户空间的映射,例如,去展示、转储、pin 程序、或者去展示、创建、pin、更新、删除映射。

阅读在源文件顶部的注释可以得到一个它们使用方法的概述。

与 eBPF 一起工作的其它必需的文件是来自内核树的两个用户空间库,它们可以用于管理 eBPF 程序或者映射来自外部的程序。这个函数可以通过 linux/tools/lib/bpf/ 目录中的头文件 bpf.hlibbpf.h(更高层面封装)来访问。比如,工具 bpftool 主要依赖这些库。

eBPF程序类型

加载BPF_PROG_LOAD的程序的类型决定了四件事:可以在何处附加程序,可以调用验证程序的内核内辅助函数,是否可以直接访问网络数据包数据,以及作为第一个传递的对象的类型该程序的参数。实际上,程序类型本质上定义了一个API。甚至纯粹是创建新程序类型来区分允许的可调用函数的不同列表(例如,BPF_PROG_TYPE_CGROUP_SKB与 BPF_PROG_TYPE_SOCKET_FILTER)。

内核支持的当前eBPF程序类型集为:

  • BPF_PROG_TYPE_SOCKET_FILTER:网络数据包过滤器

  • BPF_PROG_TYPE_KPROBE:确定是否应触发kprobe

  • BPF_PROG_TYPE_SCHED_CLS:网络流量控制分类器

  • BPF_PROG_TYPE_SCHED_ACT:网络流量控制操作

  • BPF_PROG_TYPE_TRACEPOINT:确定是否应触发跟踪点

  • BPF_PROG_TYPE_XDP:从设备驱动程序接收路径运行的网络数据包筛选器

  • BPF_PROG_TYPE_PERF_EVENT:确定是否应该触发性能事件处理程序

  • BPF_PROG_TYPE_CGROUP_SKB:用于控制组的网络数据包过滤器

  • BPF_PROG_TYPE_CGROUP_SOCK:用于控制组的网络数据包筛选器,允许修改套接字选项

  • BPF_PROG_TYPE_LWT_ *:用于轻型隧道的网络数据包过滤器

  • BPF_PROG_TYPE_SOCK_OPS:用于设置套接字参数的程序

  • BPF_PROG_TYPE_SK_SKB:网络数据包过滤器,用于在套接字之间转发数据包

  • BPF_PROG_CGROUP_DEVICE:确定是否应该允许设备操作

随着添加了新的程序类型,内核开发人员也发现也需要添加新的数据结构。

eBPF数据结构

eBPF程序使用的主要数据结构是eBPF映射,eBPF映射是一种通用数据结构,它允许在内核内或内核与用户空间之间来回传递数据。顾名思义,“map”使用键存储和检索数据。

使用bpf()系统调用创建和处理map。成功创建映射后,将返回与该映射关联的文件描述符。通常,通过关闭关联的文件描述符来销毁map。每个映射由四个值定义:类型,最大元素数,值大小(以字节为单位)和键大小(以字节为单位)。有不同的map类型,每种map类型提供不同的行为和权衡方案:

  • BPF_MAP_TYPE_HASH:哈希表

  • BPF_MAP_TYPE_ARRAY:数组映射,已针对快速查找速度进行了优化,通常用于计数器

  • BPF_MAP_TYPE_PROG_ARRAY:对应于eBPF程序的文件描述符数组;用于实现跳转表和子程序以处理特定的数据包协议

  • BPF_MAP_TYPE_PERCPU_ARRAY:每个CPU的阵列,用于实现延迟的直方图

  • BPF_MAP_TYPE_PERF_EVENT_ARRAY:存储指向struct perf_event的指针,用于读取和存储perf事件计数器

  • BPF_MAP_TYPE_CGROUP_ARRAY:存储指向控制组的指针

  • BPF_MAP_TYPE_PERCPU_HASH:每个CPU的哈希表

  • BPF_MAP_TYPE_LRU_HASH:仅保留最近使用项目的哈希表

  • BPF_MAP_TYPE_LRU_PERCPU_HASH:每个CPU的哈希表,仅保留最近使用的项目

  • BPF_MAP_TYPE_LPM_TRIE:最长前缀匹配树,适用于将IP地址匹配到某个范围

  • BPF_MAP_TYPE_STACK_TRACE:存储堆栈跟踪

  • BPF_MAP_TYPE_ARRAY_OF_MAPS:map中map数据结构

  • BPF_MAP_TYPE_HASH_OF_MAPS:map中map数据结构

  • BPF_MAP_TYPE_DEVICE_MAP:用于存储和查找网络设备引用

  • BPF_MAP_TYPE_SOCKET_MAP:存储和查找套接字,并允许使用BPF帮助函数进行套接字重定向

可以使用bpf_map_lookup_elem()和 bpf_map_update_elem()函数从eBPF或用户空间程序访问所有map 。某些映射类型(例如套接字映射)可以与执行特殊任务的其他eBPF帮助器功能一起使用。

如何编写eBPF程序

从历史上看,有必要手动编写eBPF汇编并使用内核的bpf_asm汇编器生成BPF字节码。幸运的是,LLVM Clang编译器已经增加了对eBPF后端的支持,该后端将C编译为字节码。然后可以使用bpf()系统调用和 BPF_PROG_LOAD命令直接加载包含该字节码的目标文件。

您可以使用-march = bpf参数与Clang一起编译,从而用C编写自己的eBPF程序。内核的samples / bpf / 目录中有许多eBPF程序示例;大多数文件名的后缀为“ _kern.c ”。Clang发出的目标文件(eBPF字节码)需要由计算机上本地运行的程序加载(这些示例的文件名通常带有“ _user.c ”)。为了使编写eBPF程序更容易,内核提供了libbpf库,该库包括用于加载程序以及创建和操作eBPF对象的帮助程序函数。例如,使用libbpf的eBPF程序和用户程序的高级流程 可能会像这样:

  • 将eBPF字节码读取到用户应用程序的缓冲区中,并将其传递给bpf_load_program()。

  • eBPF程序在由内核运行时,将调用 bpf_map_lookup_elem()在地图中查找元素并将其存储新值。

  • 用户应用程序调用bpf_map_lookup_elem()来读取eBPF程序存储在内核中的值。

但是,所有示例代码都有一个主要缺点:您需要从内核源代码树中编译eBPF程序。幸运的是,创建了BCC项目来解决此问题。它包括一个完整的工具链,用于编写eBPF程序并加载它们,而无需链接内核源代码树。

《BPF内核观测技术》笔录

BPF跟踪

探针

内核探针:提供内核中内部组件的动态访问

跟踪点:提供内核中内部组件的静态访问

用户空间探针:提供用户空间运行的程序的动态访问

用户静态定义跟踪点:提供用户空间运行的程序的静态访问

内核探针

kprobes–允许执行任何内核指令之前插入BPF程序

kretprobes–在内核指令有返回值时插入BPF程序

kprobes 探针在内核函数入口被调用,示例程序如下:

from bcc import BPF 

bpf_source = """ 
#include <uapi/linux/ptrace.h>                                                                                                                                               

int do_sys_execve(struct pt_regs *ctx) {
  char comm[16];
  bpf_get_current_comm(&comm, sizeof(comm));
  bpf_trace_printk("executing program: %s\\n", comm);
  return 0;
}
"""

bpf = BPF(text=bpf_source)
execve_function = bpf.get_syscall_fnname("execve")
bpf.attach_kprobe(event=execve_function, fn_name="do_sys_execve")
bpf.trace_print() 
[longyu@debian-10:17:12:19] kprobes $ sudo python example.py 
[sudo] longyu 的密码:
           guake-21900 [003] .... 19913.271490: 0: executing program: guake
           guake-21901 [001] .... 19913.273462: 0: executing program: guake
            bash-21902 [000] .... 19913.274109: 0: executing program: bash
            bash-21903 [005] .... 19913.277194: 0: executing program: bash
            bash-21906 [002] .... 19913.279022: 0: executing 

kretprobes 探测点在内核函数返回的时候被调用,示例程序如下:

from bcc import BPF

bpf_source = """
#include <uapi/linux/ptrace.h>

int ret_sys_execve(struct pt_regs *ctx) {
  int return_value;
  char comm[16];
  bpf_get_current_comm(&comm, sizeof(comm));
  return_value = PT_REGS_RC(ctx);

  bpf_trace_printk("program: %s, return: %d\\n", comm, return_value);
  return 0;
}
"""

bpf = BPF(text=bpf_source)
execve_function = bpf.get_syscall_fnname("execve")
bpf.attach_kretprobe(event=execve_function, fn_name="ret_sys_execve")
bpf.trace_print()
[longyu@debian-10:17:15:35] kretprobes $ sudo python example.py 
            bash-22471 [007] d... 20099.730870: 0: program: bash, return: 0
              id-22472 [005] d... 20099.732045: 0: program: id, return: 0
        utempter-22473 [004] d... 20099.732546: 0: program: utempter, return: 0
       dircolors-22474 [005] d... 20099.733757: 0: program: dircolors, return: 0
       dircolors-22475 [005] d... 20099.735221: 0: program: dircolors, return: 0
             lua-22476 [005] d... 20099.736689: 0: program: lua, return: 0
跟踪点

tracepoints 跟踪点是内核代码的静态标记,用于将代码附加到运行内核中,它由内核开发人员在内核中编写与修改,内核探针与跟踪点都提供了在用户空间的完全访问

查看 /sys/kernel/debug/tracing/events 目录下的内容可以获取系统中所有可用的跟踪点。

用户空间探针

uprobes–内核在程序特定指令执行之前插入该指令集的钩子

uretprobes–与kretprobes并行探针,适用于用户空间使用,在将bpf程序附加到指令返回值之上,允许通过bpf代码从寄存器中访问返回值

一个示例 patch 内容如下:

diff --git a/code/chapter-4/uprobes/example.py b/code/chapter-4/uprobes/example.py
old mode 100644
new mode 100755
index 4f2c76f..c0a93de
--- a/code/chapter-4/uprobes/example.py
+++ b/code/chapter-4/uprobes/example.py
@@ -9,5 +9,5 @@ int trace_go_main(struct pt_regs *ctx) {
 """

 bpf = BPF(text = bpf_source)
-bpf.attach_uprobe(name = "hello-bpf", sym = "main.main", fn_name = "trace_go_main")
+bpf.attach_uprobe(name = "/home/longyu/linux-observability-with-bpf/code/chapter-4/uprobes/hello-bpf", sym = "main.main", fn_name = "trace_go_main")
 bpf.trace_print()
[longyu@debian-10:17:52:59] uprobes $ sudo python ./example.py 
       hello-bpf-25911 [005] .... 22341.336969: 0: New hello-bpf process running with PID: 25911
       hello-bpf-25927 [001] .... 22354.651541: 0: New hello-bpf process running with PID: 25927
用户静态定义跟踪点

USDT为用户应用程序提供静态跟踪点,是检测应用程序的便捷方法

USDT 是用户态程序静态定义的跟踪点,类似于内核中的 tracepoint,它需要在程序的源代码中添加代码

示例 demo 源码如下:

#include <sys/sdt.h>

int main(int argc, char const *argv[]) {
    DTRACE_PROBE("hello-usdt", "probe-main");
    return 0;
}

直接编译,发现会报 sys/sdt.h 头文件不存在的问题。可以通过执行如下命令来解决:

sudo apt-get install systemtap-sdt-dev

修改 example.py 后执行有如下信息:

[longyu@debian-10:18:36:53] usdt $ sudo python example.py 
/virtual/main.c:7:1: warning: control reaches end of non-void function [-Wreturn-type]
}
^
1 warning generated. 

跟踪数据可视化

火焰图

对系统耗时进行可视化的图表

直方图
Perf事件

Perf事件的数组映射,允许将数据放入环形缓存区,以便实现用户空间实时同步

BPF运行时的体系架构

BPF API

include/uapi/linux/bpf.h

Some Graph

BPF helper function:

BPF Syscall

BPF tracing program types

注:BPF Type Format (BTF)

EBPF程序运行流程案例分析

EBPF程序运行

eBPF 在 Linux 内核中将 C 代码编译成 BPF 字节码,挂在 kprobe/tracepoint 等 hook 上,当 hook 触发时,Linux 内核运行字节码来追踪性能。

EBPF框架

在 Linux 内核中的 sample/bpf 目录存在许多 bpf 程序的样例,以 tracex4_kern.ctracex4_user.c 为例。

下图是 eBPF 程序的框架,分为程序执行流和数据通信流。

对于程序执行流来说,

  • trace_kern.c 是分配 slab,释放 slab 时调用的代码,其中申明了 hook 处理函数和数据 map,数据 map 用于内核态和用户态之间的数据通信,使用 LLVM/clang 编译器编译成 bpf 字节码

  • trace_user.c 用来加载 bpf 字节码,陷入内核态,通过 JIT(just in time) 编译器将 bpf 字节码转换成机器汇编码,当 kprobe 或 tracepoint 追踪到某类事件时执行上述申明的 hook 处理函数并获取数据 map,将数据传到 userspace

tracex4_kern.c

其中定义了 “my_map” 的一个数据 map,数据 map 由 key/value 键值对组成,这里的 value 是一个结构体的变量,用于获得当前运行时间和 ip 寄存器,数据 map 由__attribute__ 声明,是一个单独的 section,编译成 ELF 格式的文件时该结构体变量存在 “maps” 段中。

接下来申明的是分配 slab(内存分配),释放 slab 的钩子处理函数,并单独放在 kprobe/kmem_cache_freekretprobe/kmem_cache_alloc_node 两个代码段中。

#include <linux/version.h>
#include <uapi/linux/bpf.h>
#include "bpf_helpers.h"

struct pair {
        u64 val;
        u64 ip;
};

struct bpf_map_def SEC("maps") my_map = {
        .type = BPF_MAP_TYPE_HASH,
        .key_size = sizeof(long),
        .value_size = sizeof(struct pair),
        .max_entries = 1000000,
};
SEC("kprobe/kmem_cache_free")
int bpf_prog1(struct pt_regs *ctx)
{
        long ptr = PT_REGS_PARM2(ctx);

        bpf_map_delete_elem(&my_map, &ptr);
        return 0;
}

SEC("kretprobe/kmem_cache_alloc_node")
int bpf_prog2(struct pt_regs *ctx)
{
        long ptr = PT_REGS_RC(ctx);
        long ip = 0;

        /* get ip address of kmem_cache_alloc_node() caller */
        BPF_KRETPROBE_READ_RET_IP(ip, ctx);

        struct pair v = {
                .val = bpf_ktime_get_ns(),
                .ip = ip,
        };

        bpf_map_update_elem(&my_map, &ptr, &v, BPF_ANY);
        return 0;
}
char _license[] SEC("license") = "GPL";
u32 _version SEC("version") = LINUX_VERSION_CODE;

使用 clang 编译出 .o 文件,同样属于 ELF 文件,可以使用 llvm dump 出各个 section table,如上述自定义的几个段,用 SEC 就可自定义一个 section:

#define SEC(NAME) __attribute__((section(NAME), used))

tracex4_user.c

其中定义了一个 pair 结构体用来接受 hook 处理函数的数据,首先加载 tracex4_kern.o,然后在死循环中轮询获取数据,然后用 printf 打印出来:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <stdbool.h>
#include <string.h>
#include <time.h>
#include <linux/bpf.h>
#include <sys/resource.h>

#include <bpf/bpf.h>
#include "bpf_load.h"

struct pair {
        long long val;
        __u64 ip;
};

static __u64 time_get_ns(void)
{
        struct timespec ts;

        clock_gettime(CLOCK_MONOTONIC, &ts);
        return ts.tv_sec * 1000000000ull + ts.tv_nsec;
}

static void print_old_objects(int fd)
{
        long long val = time_get_ns();
        __u64 key, next_key;
        struct pair v;

        key = write(1, "\e[1;1H\e[2J", 12); /* clear screen */

        key = -1;
        while (bpf_map_get_next_key(map_fd[0], &key, &next_key) == 0) {
                bpf_map_lookup_elem(map_fd[0], &next_key, &v);
                key = next_key;
                if (val - v.val < 1000000000ll)
                        /* object was allocated more then 1 sec ago */
                        continue;
                printf("obj 0x%llx is %2lldsec old was allocated at ip %llx\n",
                       next_key, (val - v.val) / 1000000000ll, v.ip);
        }
}

int main(int ac, char **argv)
{
        struct rlimit r = {RLIM_INFINITY, RLIM_INFINITY};
        char filename[256];
        int i;

        snprintf(filename, sizeof(filename), "%s_kern.o", argv[0]);

        if (setrlimit(RLIMIT_MEMLOCK, &r)) {
                perror("setrlimit(RLIMIT_MEMLOCK, RLIM_INFINITY)");
                return 1;
        }

        if (load_bpf_file(filename)) {
                printf("%s", bpf_log_buf);
                return 1;
        }

        for (i = 0; ; i++) {
                print_old_objects(map_fd[1]);
                sleep(1);
        }
}

通过 readelf 和 llvm-objdump 解析目标文件

读取 ELF 文件头
wu@ubuntu:~/linux/samples/bpf$ readelf -h tracex4_kern.o
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: Linux BPF
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 8344 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 27
Section header string table index: 1

在 readelf 的输出中:

第 1 行,ELF Header: 指名 ELF 文件头开始。 第 2 行,Magic 魔数,用来指名该文件是一个 ELF 目标文件。第一个字节 7F 是个固定的数;后面的 3 个字节正是 E, L, F 三个字母的 ASCII 形式。 第 3 行,CLASS 表示文件类型,这里是 64位的 ELF 格式。 第 4 行,Data 表示文件中的数据是按照什么格式组织(大端或小端)的,不同处理器平台数据组织格式可能就不同,如x86平台为小端存储格式。 第 5 行,当前 ELF 文件头版本号,这里版本号为 1 。 第 6 行,OS/ABI ,指出操作系统类型,ABI 是 Application Binary Interface 的缩写。 第 7 行,ABI 版本号,当前为 0 。 第 8 行,Type 表示文件类型。ELF 文件有 3 种类型,一种是如上所示的 Relocatable file 可重定位目标文件,一种是可执行文件(Executable),另外一种是共享库(Shared Library) 。 第 9 行,机器平台类型。 这里是bpf虚拟机 第 10 行,当前目标文件的版本号。 第 11 行,程序的虚拟地址入口点,因为这还不是可运行的程序,故而这里为零。 第 12 行,与 11 行同理,这个目标文件没有 Program Headers。 第 13 行,sections 头开始处,这里 8344 是十进制,表示从地址偏移 0x2098 处开始。 第 14 行,是一个与处理器相关联的标志,x86 平台上该处为 0 。 第 15 行,ELF 文件头的字节数。 第 16 行,因为这个不是可执行程序,故此处大小为 0 第 19 行,一共有多少个 section 头,这里是 27个。

打印各个段的内容
wu@ubuntu:~/linux/samples/bpf$ readelf -S tracex4_kern.o
There are 27 section headers, starting at offset 0x2098:

Section Headers:
  [Nr] Name                 Type                  Address            Offset
       Size                 EntSize          Flags  Link  Info  Align
  [ 0]                         NULL                  0000000000000000  00000000
       0000000000000000  0000000000000000            0          0        0
  [ 1] .strtab                 STRTAB           0000000000000000  00001f80
       0000000000000115  0000000000000000            0          0        1
  [ 2] .text                 PROGBITS          0000000000000000  00000040
       0000000000000000  0000000000000000  AX            0          0        4
  [ 3] kprobe/kmem_cache PROGBITS          0000000000000000  00000040
       0000000000000048  0000000000000000  AX            0          0        8
  [ 4] .relkprobe/kmem_c REL                  0000000000000000  00001870
       0000000000000010  0000000000000010           26          3        8
  [ 5] kretprobe/kmem_ca PROGBITS          0000000000000000  00000088
       00000000000000c0  0000000000000000  AX            0          0        8
  [ 6] .relkretprobe/kme REL                  0000000000000000  00001880
       0000000000000010  0000000000000010           26          5        8
  [ 7] maps                 PROGBITS          0000000000000000  00000148
       000000000000001c  0000000000000000  WA            0          0        4
  [ 8] license                 PROGBITS          0000000000000000  00000164
       0000000000000004  0000000000000000  WA            0          0        1
  [ 9] version                 PROGBITS          0000000000000000  00000168
       0000000000000004  0000000000000000  WA            0          0        4
  [10] .debug_str         PROGBITS          0000000000000000  0000016c
       00000000000001e9  0000000000000001  MS            0          0        1
  [11] .debug_loc         PROGBITS          0000000000000000  00000355
       0000000000000150  0000000000000000            0          0        1
  [12] .rel.debug_loc         REL                  0000000000000000  00001890
       0000000000000050  0000000000000010           26         11        8
  [13] .debug_abbrev         PROGBITS          0000000000000000  000004a5
       0000000000000101  0000000000000000            0          0        1
  [14] .debug_info         PROGBITS          0000000000000000  000005a6
       0000000000000376  0000000000000000            0          0        1
  [15] .rel.debug_info         REL                  0000000000000000  000018e0
       00000000000004b0  0000000000000010           26         14        8
  [16] .debug_ranges         PROGBITS          0000000000000000  0000091c
       0000000000000030  0000000000000000            0          0        1
  [17] .rel.debug_ranges REL                  0000000000000000  00001d90
       0000000000000040  0000000000000010           26         16        8
  [18] .BTF                 PROGBITS          0000000000000000  0000094c
       0000000000000569  0000000000000000            0          0        1
  [19] .rel.BTF          REL                  0000000000000000  00001dd0
       0000000000000030  0000000000000010           26         18        8
  [20] .BTF.ext          PROGBITS          0000000000000000  00000eb5
       0000000000000178  0000000000000000            0          0        1
  [21] .rel.BTF.ext         REL                  0000000000000000  00001e00
       0000000000000140  0000000000000010           26         20        8
  [22] .eh_frame         PROGBITS          0000000000000000  00001030
       0000000000000050  0000000000000000   A            0          0        8
  [23] .rel.eh_frame         REL                  0000000000000000  00001f40
       0000000000000020  0000000000000010           26         22        8
  [24] .debug_line         PROGBITS          0000000000000000  00001080
       0000000000000147  0000000000000000            0          0        1
  [25] .rel.debug_line         REL                  0000000000000000  00001f60
       0000000000000020  0000000000000010           26         24        8
  [26] .symtab                 SYMTAB           0000000000000000  000011c8
       00000000000006a8  0000000000000018            1         66        8

其中,第三列代表类型(Type):

  • ”NULL”:未使用,如段表的第一个空段

  • “PROGBITS”:程序数据,如 .text、.data、.rodata;

  • “REL”:重定位表,如 .rel.text;

  • “NOBITS”:暂时没有数据的程序空间,如 .bss;

  • “STRTAB”:字符串表,如 .strtab、.shstrtab;

  • “SYMTAB”:符号表,如 .symtab,包括所有用到的相关符号信息,如函数名、变量名。

通过 llvm-objdump 解析 BPF ELF 格式文件
wu@ubuntu:~/linux/samples/bpf$ llvm-objdump -h tracex4_kern.o

tracex4_kern.o: file format ELF64-BPF

Sections:
Idx Name                                Size         VMA                  Type
  0                                        00000000 0000000000000000
  1 .strtab                                00000115 0000000000000000
  2 .text                                00000000 0000000000000000 TEXT
  3 kprobe/kmem_cache_free                00000048 0000000000000000 TEXT
  4 .relkprobe/kmem_cache_free                00000010 0000000000000000
  5 kretprobe/kmem_cache_alloc_node        000000c0 0000000000000000 TEXT
  6 .relkretprobe/kmem_cache_alloc_node 00000010 0000000000000000
  7 maps                                0000001c 0000000000000000 DATA
  8 license                                00000004 0000000000000000 DATA
  9 version                                00000004 0000000000000000 DATA
... ...

可以使用 llvm 工具为 eBPF 程序进行反编译,tracex4_kern.o 是 ELF 格式的文件,分为两个代码段,如下所示 :

wu@ubuntu:~/linux/samples/bpf$ llvm-objdump -d -r -print-imm-hex tracex4_kern.o

tracex4_kern.o: file format ELF64-BPF


Disassembly of section kprobe/kmem_cache_free:

0000000000000000 bpf_prog1:
       0:        79 11 68 00 00 00 00 00 r1 = *(u64 *)(r1 + 0x68)
       1:        7b 1a f8 ff 00 00 00 00 *(u64 *)(r10 - 0x8) = r1
       2:        bf a2 00 00 00 00 00 00 r2 = r10
       3:        07 02 00 00 f8 ff ff ff r2 += -0x8
       4:        18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0x0 ll
                0000000000000020:  R_BPF_64_64        my_map
       6:        85 00 00 00 03 00 00 00 call 0x3
       7:        b7 00 00 00 00 00 00 00 r0 = 0x0
       8:        95 00 00 00 00 00 00 00 exit

Disassembly of section kretprobe/kmem_cache_alloc_node:

0000000000000000 bpf_prog2:
       0:        79 12 50 00 00 00 00 00 r2 = *(u64 *)(r1 + 0x50)
       1:        7b 2a f8 ff 00 00 00 00 *(u64 *)(r10 - 0x8) = r2
       2:        b7 02 00 00 00 00 00 00 r2 = 0x0
       3:        7b 2a f0 ff 00 00 00 00 *(u64 *)(r10 - 0x10) = r2
       4:        79 13 20 00 00 00 00 00 r3 = *(u64 *)(r1 + 0x20)
       5:        07 03 00 00 08 00 00 00 r3 += 0x8
       6:        bf a1 00 00 00 00 00 00 r1 = r10
       7:        07 01 00 00 f0 ff ff ff r1 += -0x10
       8:        b7 02 00 00 08 00 00 00 r2 = 0x8
       9:        85 00 00 00 04 00 00 00 call 0x4
      10:        85 00 00 00 05 00 00 00 call 0x5
      11:        7b 0a e0 ff 00 00 00 00 *(u64 *)(r10 - 0x20) = r0
      12:        79 a1 f0 ff 00 00 00 00 r1 = *(u64 *)(r10 - 0x10)
      13:        7b 1a e8 ff 00 00 00 00 *(u64 *)(r10 - 0x18) = r1
      14:        bf a2 00 00 00 00 00 00 r2 = r10
      15:        07 02 00 00 f8 ff ff ff r2 += -0x8
      16:        bf a3 00 00 00 00 00 00 r3 = r10
      17:        07 03 00 00 e0 ff ff ff r3 += -0x20
      18:        18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0x0 ll
                0000000000000090:  R_BPF_64_64        my_map
      20:        b7 04 00 00 00 00 00 00 r4 = 0x0
      21:        85 00 00 00 02 00 00 00 call 0x2
      22:        b7 00 00 00 00 00 00 00 r0 = 0x0
      23:        95 00 00 00 00 00 00 00 exit

             ........

接下来查看其他段的内容,比如 maps 段和 license 段:

wu@ubuntu:~/linux/samples/bpf$ llvm-objdump --section=maps  -s tracex4_kern.o

tracex4_kern.o: file format ELF64-BPF

Contents of section maps:
 0000 01000000 08000000 10000000 40420f00  ............@B..
 0010 00000000 00000000 00000000           ............
wu@ubuntu:~/linux/samples/bpf$ llvm-objdump --section=license  -s tracex4_kern.o

tracex4_kern.o: file format ELF64-BPF

Contents of section license:
 0000 47504c00                                   GPL.

上述 bpf 程序的字节码,是不能在 x86_64 平台上直接执行的,当加载 bpf 程序时需要使用 JIT(just in time) 编译器将 bpf 字节码翻译成主机能识别的汇编码,然而对于大多数操作码,eBPF 指令集可以和 x86 或 aarch64 指令集一一映射。

bpf 程序自定义了一套指令,有别于 x86,ARM64 等,而且指令集没这两者丰富,没有浮点计算等。但寄存器功能大同小异, 功能如下所示:

无法复制加载中的内容

通过 strace 工具追踪分析 eBPF 程序行为

执行如下命令可以看到 slab 对象的分配地址以及分配时间:

wu@ubuntu:~/linux/samples/bpf$ sudo strace -v -f -s 128 -o tracex4.txt ./tracex4
obj 0xffff9637e3175cc0 is  1sec old was allocated at ip ffffffff99679a9a
obj 0xffff9637e31750c0 is  1sec old was allocated at ip ffffffff99679a9a
obj 0xffff9637e3175e00 is  1sec old was allocated at ip ffffffff99679a9a
obj 0xffff9637e3175780 is  1sec old was allocated at ip ffffffff99679a9a
... ...

strace 追踪到的关键系统调用如下:

execve("./tracex4", ["./tracex4"], ["LANG=en_US.UTF-8", "LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca"..., "TERM=xterm", "DISPLAY=localhost:12.0", "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin", "MAIL=/var/mail/root", "LOGNAME=root", "USER=root", "HOME=/root", "SHELL=/bin/bash", "SUDO_COMMAND=/usr/bin/strace -v -f -s 128 -o tracex4.txt ./tracex4", "SUDO_USER=wu", "SUDO_UID=1000", "SUDO_GID=1000"]) = 0
... ...
bpf(BPF_MAP_CREATE, {map_type=BPF_MAP_TYPE_HASH, key_size=8, value_size=16, max_entries=1000000, map_flags=0, inner_map_fd=0, map_name="my_map", map_ifindex=0, btf_fd=0, btf_key_type_id=0, btf_value_type_id=0}, 112) = 4

bpf(BPF_PROG_LOAD, {prog_type=BPF_PROG_TYPE_KPROBE, insn_cnt=9, insns=[{code=BPF_LDX|BPF_DW|BPF_MEM, dst_reg=BPF_REG_1, src_reg=BPF_REG_1, off=104, imm=0}, {code=BPF_STX|BPF_DW|BPF_MEM, dst_reg=BPF_REG_10, src_reg=BPF_REG_1, off=-8, imm=0}, {code=BPF_ALU64|BPF_X|BPF_MOV, dst_reg=BPF_REG_2, src_reg=BPF_REG_10, off=0, imm=0}, {code=BPF_ALU64|BPF_K|BPF_ADD, dst_reg=BPF_REG_2, src_reg=BPF_REG_0, off=0, imm=0xfffffff8}, {code=BPF_LD|BPF_DW|BPF_IMM, dst_reg=BPF_REG_1, src_reg=BPF_REG_1, off=0, imm=0x4}, {code=BPF_LD|BPF_W|BPF_IMM, dst_reg=BPF_REG_0, src_reg=BPF_REG_0, off=0, imm=0}, {code=BPF_JMP|BPF_K|BPF_CALL, dst_reg=BPF_REG_0, src_reg=BPF_REG_0, off=0, imm=0x3}, {code=BPF_ALU64|BPF_K|BPF_MOV, dst_reg=BPF_REG_0, src_reg=BPF_REG_0, off=0, imm=0}, {code=BPF_JMP|BPF_K|BPF_EXIT, dst_reg=BPF_REG_0, src_reg=BPF_REG_0, off=0, imm=0}], license="GPL", log_level=0, log_size=0, log_buf=NULL, kern_version=KERNEL_VERSION(5, 4, 0), prog_flags=0, prog_name="", prog_ifindex=0, expected_attach_type=BPF_CGROUP_INET_INGRESS, prog_btf_fd=0, func_info_rec_size=0, func_info=NULL, func_info_cnt=0, line_info_rec_size=0, line_info=NULL, line_info_cnt=0, attach_btf_id=0}, 112) = 5

openat(AT_FDCWD, "/sys/kernel/debug/tracing/kprobe_events", O_WRONLY|O_APPEND) = 6
write(6, "p:kmem_cache_free kmem_cache_free", 33) = 33
close(6)
openat(AT_FDCWD, "/sys/kernel/debug/tracing/events/kprobes/kmem_cache_free/id", O_RDONLY) = 6
read(6, "2145\n", 256)                 = 5
close(6)
perf_event_open({type=PERF_TYPE_TRACEPOINT, size=0 /* PERF_ATTR_SIZE_??? */, config=2145, sample_period=1, sample_type=PERF_SAMPLE_RAW, read_format=0, disabled=0, inherit=0, pinned=0, exclusive=0, exclusive_user=0, exclude_kernel=0, exclude_hv=0, exclude_idle=0, mmap=0, comm=0, freq=0, inherit_stat=0, enable_on_exec=0, task=0, watermark=0, precise_ip=0 /* arbitrary skid */, mmap_data=0, sample_id_all=0, exclude_host=0, exclude_guest=0, exclude_callchain_kernel=0, exclude_callchain_user=0, mmap2=0, comm_exec=0, use_clockid=0, context_switch=0, write_backward=0, namespaces=0, wakeup_events=1, config1=0}, -1, 0, -1, 0) = 6
... ...
openat(AT_FDCWD, "/sys/kernel/debug/tracing/kprobe_events", O_WRONLY|O_APPEND) = 8
write(8, "r:kmem_cache_alloc_node kmem_cache_alloc_node", 45) = 45
openat(AT_FDCWD, "/sys/kernel/debug/tracing/events/kprobes/kmem_cache_alloc_node/id", O_RDONLY) = 8
read(8, "2146\n", 256)                 = 5
close(8)                         = 0
perf_event_open({type=PERF_TYPE_TRACEPOINT, size=0 /* PERF_ATTR_SIZE_??? */, config=2146, sample_period=1, sample_type=PERF_SAMPLE_RAW, read_format=0, disabled=0, inherit=0, pinned=0, exclusive=0, exclusive_user=0, exclude_kernel=0, exclude_hv=0, exclude_idle=0, mmap=0, comm=0, freq=0, inherit_stat=0, enable_on_exec=0, task=0, watermark=0, precise_ip=0 /* arbitrary skid */, mmap_data=0, sample_id_all=0, exclude_host=0, exclude_guest=0, exclude_callchain_kernel=0, exclude_callchain_user=0, mmap2=0, comm_exec=0, use_clockid=0, context_switch=0, write_backward=0, namespaces=0, wakeup_events=1, config1=0}, -1, 0, -1, 0) = 8
ioctl(8, PERF_EVENT_IOC_ENABLE, 0) = 0
ioctl(8, PERF_EVENT_IOC_SET_BPF, 7) = 0
write(1, "\33[1;1H\33[2J\0\0", 12) = 12
bpf(BPF_MAP_GET_NEXT_KEY, {map_fd=4, key=0x7ffffaf162b0, next_key=0x7ffffaf162b8}, 112) = 0
bpf(BPF_MAP_LOOKUP_ELEM, {map_fd=4, key=0x7ffffaf162b8, value=0x7ffffaf162d0, flags=BPF_ANY}, 112) = 0
... ...
BPF 字节码加载分析

当 bpf 系统第一个参数 cmds=BPF_PROG_LOAD 时,表示加载 ELF64-BPF 格式的文件,仔细分析下第二个参数 attr,这个结构体的原型如下,发现就是保存了 bpf 字节码:

struct {    /* Used by BPF_PROG_LOAD */
        __u32              prog_type;
        __u32              insn_cnt;
        __aligned_u64 insns;          /* 'const struct bpf_insn *' */
        __aligned_u64 license;          /* 'const char *' */
        __u32              log_level;  /* verbosity level of verifier */
        __u32              log_size;   /* size of user buffer */
        __aligned_u64 log_buf;          /* user supplied 'char *'
buffer */
        __u32              kern_version;
                                /* checked when prog_type=kprobe
(since Linux 4.1) */
};
 __attribute__((aligned(8)));

当加载 bpf 程序时,BPF_PROG_LOAD 表示的是该程序的具体 bpf 指令,对应 bpf_prog1 这个代码段。

strace 追踪到的指令如下所示,每条指令的操作码由六部分组成:

  • code(操作码)

  • dst_reg(目标寄存器)

  • src_reg(源寄存器)

  • off(偏移)

  • imm(立即数)

详见:

insns=[
{code=BPF_LDX|BPF_DW|BPF_MEM, dst_reg=BPF_REG_1, src_reg=BPF_REG_1, off=104, imm=0},
{code=BPF_STX|BPF_DW|BPF_MEM, dst_reg=BPF_REG_10, src_reg=BPF_REG_1, off=-8, imm=0},
{code=BPF_ALU64|BPF_X|BPF_MOV, dst_reg=BPF_REG_2, src_reg=BPF_REG_10, off=0, imm=0},
{code=BPF_ALU64|BPF_K|BPF_ADD, dst_reg=BPF_REG_2, src_reg=BPF_REG_0, off=0, imm=0xfffffff8},
{code=BPF_LD|BPF_DW|BPF_IMM, dst_reg=BPF_REG_1, src_reg=BPF_REG_1, off=0, imm=0x4},
{code=BPF_LD|BPF_W|BPF_IMM, dst_reg=BPF_REG_0, src_reg=BPF_REG_0, off=0, imm=0},
{code=BPF_JMP|BPF_K|BPF_CALL, dst_reg=BPF_REG_0, src_reg=BPF_REG_0, off=0, imm=0x3},
{code=BPF_ALU64|BPF_K|BPF_MOV, dst_reg=BPF_REG_0, src_reg=BPF_REG_0, off=0, imm=0},
{code=BPF_JMP|BPF_K|BPF_EXIT, dst_reg=BPF_REG_0, src_reg=BPF_REG_0, off=0, imm=0}
]

指令格式如下图所示,BPF 当前拥有 102 个指令,主要包括三大类:

  • ALU (64bit and 32bit)

  • 内存操作

  • 分支操作

其中指令的格式主要由下面这几部分组成:

opcode 的低 3 位表示指令类型,BPF_LDXBPF_REG_10 等这些宏在 kernel 目录 tools/include/uapi/linux/bpf.h 中定义:

#define         BPF_LDX         0x01
#define         BPF_MEM         0x60
#define         BPF_DW                0x18
... ...

以第一条 bpf 指令为例子:

{code=BPF_LDX|BPF_DW|BPF_MEM, dst_reg=BPF_REG_1, src_reg=BPF_REG_1, off=104, imm=0}

正好对应 llvm-objdump 解析出来的第一条指令,为内存访问指令:

0:       79 11 68 00 00 00 00 00 r1 = *(u64 *)(r1 + 0x68)
BPF_LDX|BPF_MEM|BPF_DW=0x79

该条指令在 kernel 中的定义为:

/* Memory load, dst_reg = *(uint *) (src_reg + off16) */

#define BPF_LDX_MEM(SIZE, DST, SRC, OFF)                        \
        ((struct bpf_insn) {                                        \
                .code  = BPF_LDX | BPF_SIZE(SIZE) | BPF_MEM,        \
                .dst_reg = DST,                                 \
                .src_reg = SRC,                                 \
                .off   = OFF,                                        \
                .imm   = 0 })
MAP 数据通信分析

当 bpf 系统第一个参数 cmds=BPF_MAP_CREATE 时,表示创建一个数据 map,仔细分析下第二个参数 attr,这个结构体的原型包含了 map 类型,key 的大小,valu e大小等:

union bpf_attr {
struct {    /* Used by BPF_MAP_CREATE */
        __u32              map_type;
        __u32              key_size;    /* size of key in bytes */
        __u32              value_size;  /* size of value in bytes */
        __u32              max_entries; /* maximum number of entries
in a map */
};

用 strace 抓取的 log 分析来看 map 的类型为 BPF_MAP_TYPE_HASH,key 的大小为 8,value大小为 16,访问 map 是 bpf_prog1 第5条字节码,其中的 imm 立即数为 4 代表 map_fd,这是一条伪指令,这条指令是可重定位指令:

bpf(BPF_MAP_CREATE, {map_type=BPF_MAP_TYPE_HASH, key_size=8, value_size=16, max_entries=1000000, map_flags=0, inner_map_fd=0, map_name="my_map", map_ifindex=0, btf_fd=0, btf_key_type_id=0, btf_value_type_id=0}, 112) = 4

 0000000000000020:  R_BPF_64_64  my_map
{code=BPF_LD|BPF_DW|BPF_IMM, dst_reg=BPF_REG_1, src_reg=BPF_REG_1, off=0, imm=0x4}

bpf 程序访问所有类型的 map 都可以使用 bpf_map_lookup_elem()bpf_map_update_elem() 函数,socket maps 和一些其他额外的 map 当作特殊用途。

当 bpf 系统第一个参数 cmds=BPF_MAP_GET_NEXT_KEYBPF_MAP_LOOKUP_ELEM 时,表示遍历 map,仔细分析下第二个参数 attr,这个结构体的原型包含了 map_fd,是 bpf 系统调用第一个参数 cmd=BPF_MAP_CREATE 返回值,key 值,value 值等:

struct {    /* Used by BPF_MAP_*_ELEM and BPF_MAP_GET_NEXT_KEY
commands */
        __u32              map_fd;
        __aligned_u64 key;
        union {
        __aligned_u64 value;
        __aligned_u64 next_key;
        };
        __u64              flags;
};

bpf(BPF_MAP_GET_NEXT_KEY, {map_fd=4, key=0x7ffffaf162b0, next_key=0x7ffffaf162b8}, 112) = 0
bpf(BPF_MAP_LOOKUP_ELEM, {map_fd=4, key=0x7ffffaf162b8, value=0x7ffffaf162d0, flags=BPF_ANY}, 112) = 0

bpftool 用法简介

bpftool 在内核的 tools/bpf/bpftool/ 目录下,使用 make 编译就可使用,查看当前运行的 bpf 程序,如下所示,可以看到当前运行的是 kprobe event 还有 map id

wu@ubuntu:~/linux/samples/bpf$ sudo bpftool prog show
[sudo] password for wu:
... ...
205: kprobe  tag a6cfc4a29f52a193  gpl
        loaded_at 2021-01-19T11:51:26+0000  uid 0
        xlated 72B  jited 62B  memlock 4096B  map_ids 72
206: kprobe  tag d16c41919f3b767a  gpl
        loaded_at 2021-01-19T11:51:26+0000  uid 0
        xlated 192B  jited 119B  memlock 4096B        map_ids 72

查看 map 的id,可以看到当前使用的是 hash map:

wu@ubuntu:~/linux$ sudo bpftool map show
72: hash  name my_map  flags 0x0
        key 8B        value 16B  max_entries 1000000        memlock 88788992B

查看 map 的所有的内容,并查看对应 key 的value:

wu@ubuntu:~/linux$ sudo bpftool map dump id 72
key: 80 35 43 e5 26 95 ff ff  value: b9 f3 f3 9e 87 6b 00 00  9a 9a c7 86 ff ff ff ff
key: 80 9c 5f f6 25 95 ff ff  value: 98 85 ac c7 8c 6b 00 00  9a 9a c7 86 ff ff ff ff
key: 00 4c 9b e4 25 95 ff ff  value: e6 8d c2 b7 7e 6b 00 00  9a 9a c7 86 ff ff ff ff
key: 80 21 15 f8 26 95 ff ff  value: 60 df 01 fc 5c 6b 00 00  9a 9a c7 86 ff ff ff ff
key: 80 f5 46 5f 26 95 ff ff  value: 5e e1 73 7b 8d 6b 00 00  9a 9a c7 86 ff ff ff ff
... ...
wu@ubuntu:~/linux$ sudo bpftool map lookup id 72 key 0x80 0x35 0x43 0xe5 0x26 0x95 0xff 0xff
key: 80 35 43 e5 26 95 ff ff  value: b9 f3 f3 9e 87 6b 00 00  9a 9a c7 86 ff ff ff ff

eBPF 程序装载、翻译与运行过程详解

BPF 程序格式为 ELF

加载 bpf 程序实质上是加载 ELF 格式文件,Linux 加载普通 ELF 格式的文件在通过 load_elf_binary 来实现,而 Linux 加载 bpf elf 其实在用户态实现的,使用的是开源的 libelf 库实现的,调用过程不太一样,而且只是把 ELF 格式的指令 dump 出来,接下来还需要 JIT 编译器翻译出机器汇编码才能执行,这个调用过程比 Linux 加载普通 ELF 格式文件简单。

libelf 库实现的各个 API 可参考如下 链接 以及我们专门为此准备的 libelf 开源库用法详解

ELF 格式的详解可参考社区创始人撰写的开源书籍 C 语言编程透视 和配套视频课程《360° 剖析 Linux ELF》

ELF 文件大体结构

ELF 文件大体结构如下所示,包含 ELF 头部,程序头表,各个段和程序段表:

ELF Header                 #程序头,有该文件的Magic number(参考man magic),类型等
Program Header Table         #对可执行文件和共享库有效,它描述下面各个节(section)组成的段
Section1
Section2
Section3
.....
Program Section Table        #仅对可重定位目标文件和静态库有效,用于描述各个Section的重定位信息等。

BPF 程序 Section 解析:get_sec

samples/bpf/bpf_load.c 中通过 get_sec 函数调用 libelf 库的 API 获取 section 内容,其中第四个参数是传入段的名字,最后一个参数获得的是该段的数据:

static int get_sec(Elf *elf, int i, GElf_Ehdr *ehdr, char **shname,
                   GElf_Shdr *shdr, Elf_Data **data)
{
        Elf_Scn *scn;

        scn = elf_getscn(elf, i);  //从elf描述符获取按照节索引获取节接口
        if (!scn)
                return 1;

        if (gelf_getshdr(scn, shdr) != shdr) // 通过节结构复制节表头
                return 2;

        *shname = elf_strptr(elf, ehdr->e_shstrndx, shdr->sh_name); // 从指定的字符串表中通过偏移获取字符串
        if (!*shname || !shdr->sh_size)
                return 3;

        *data = elf_getdata(scn, 0);  //从节中获取节数据(经过了字节序的转换)
        if (!*data || elf_getdata(scn, *data) != NULL)
                return 4;

        return 0;
}

BPF 程序装载与解析:load_bpf_file

tracex4_user.c 通过 load_bpf_file 加载 .o 文件,我们来分析一下。

load_bpf_file 实质是调用 do_load_bpf_file,在这个函数里首先打开 .o 文件,do_load_bpf_file 会将输入的 .o 文件作为 ELF 格式文件,逐个 section 进行分析:

  • 如 section 的名字是特殊的(比如 ‘kprobe’),那么就会将这个 section 的内容作为 load_and_attach 的参数。

  • 如 section 的名字是 “license” 或 “version” 则保存 license 或 version。

  • 如 section 是 map 则解析出 map 段

samples/bpf/bpfload.c 中的相关代码如下:

static int do_load_bpf_file(const char *path, fixup_map_cb fixup_map)
{
        int fd, i, ret, maps_shndx = -1, strtabidx = -1;
        Elf *elf;
        GElf_Ehdr ehdr;
        GElf_Shdr shdr, shdr_prog;
        Elf_Data *data, *data_prog, *data_maps = NULL, *symbols = NULL;
        char *shname, *shname_prog;
        int nr_maps = 0;
        ... ...
        fd = open(path, O_RDONLY, 0);  //打开elf文件
        if (fd < 0)
                return 1;

        elf = elf_begin(fd, ELF_C_READ, NULL);//获取elf描述符,使用‘读取’的方式
        ... ...
        if (gelf_getehdr(elf, &ehdr) != &ehdr)        //获取elf文件头副本
                return 1;
        ... ...
        /* scan over all elf sections to get license and map info */
        for (i = 1; i < ehdr.e_shnum; i++) {                       //遍历各个section
                if (get_sec(elf, i, &ehdr, &shname, &shdr, &data))  // shname 为"section"的名字
                        continue;

                if (0) /* helpful for llvm debugging */        //打印各个section 对应的数据保存在data->d_buf中
                        printf("section %d:%s data %p size %zd link %d flags %d\n",
                        i, shname, data->d_buf, data->d_size,
                        shdr.sh_link, (int) shdr.sh_flags);

                if (strcmp(shname, "license") == 0) {             //如果是"license"段
                        processed_sec[i] = true;
                        memcpy(license, data->d_buf, data->d_size); //把 data->d_buf 拷贝到license数组
                } else if (strcmp(shname, "version") == 0) { //如果是"version"段
                        processed_sec[i] = true;
                        if (data->d_size != sizeof(int)) {
                                printf("invalid size of version section %zd\n",
                                data->d_size);
                                return 1;
                }
                memcpy(&kern_version, data->d_buf, sizeof(int));//把 data->d_buf 拷贝到kern_version变量
        } else if (strcmp(shname, "maps") == 0) {      //如果是map 段
                int j;

                maps_shndx = i;
                data_maps = data;
                for (j = 0; j < MAX_MAPS; j++)
                        map_data[j].fd = -1;
        } else if (shdr.sh_type == SHT_SYMTAB) {
                strtabidx = shdr.sh_link;
                symbols = data;
        }

        ... ...

        if (data_maps) {     //对map段的处理
                nr_maps = load_elf_maps_section(map_data, maps_shndx,elf, symbols, strtabidx);         //获取map段内容
                if (nr_maps < 0) {
                        printf("Error: Failed loading ELF maps (errno:%d):%s\n",
                        nr_maps, strerror(-nr_maps));
                        goto done;
                }
                if (load_maps(map_data, nr_maps, fixup_map))  //这里加载map
                        goto done;
                map_data_count = nr_maps;

                processed_sec[maps_shndx] = true;
        }

        /* process all relo sections, and rewrite bpf insns for maps */
        for (i = 1; i < ehdr.e_shnum; i++) {  //遍历所有的重定向段,
                if (processed_sec[i])  ////flag 置位表示已经是处理了的段 ,跳过去
                        continue;

                if (get_sec(elf, i, &ehdr, &shname, &shdr, &data))
                        continue;

                if (shdr.sh_type == SHT_REL) {
                        struct bpf_insn *insns;

                        /* locate prog sec that need map fixup (relocations) */
                        if (get_sec(elf, shdr.sh_info, &ehdr, &shname_prog,
                                &shdr_prog, &data_prog))  //该段保存到data_prog
                                continue;

                        if (shdr_prog.sh_type != SHT_PROGBITS ||
                        !(shdr_prog.sh_flags & SHF_EXECINSTR))
                                continue;

                        insns = (struct bpf_insn *) data_prog->d_buf;  //得到bpf字节码对应的结构体
                        processed_sec[i] = true; /* relo section */

                        if (parse_relo_and_apply(data, symbols, &shdr, insns,
                                                map_data, nr_maps))
                                continue;
                }
        }

        /* load programs */
        for (i = 1; i < ehdr.e_shnum; i++) {
                if (processed_sec[i])  //flag 置位表示已经是处理了的段 ,跳过去
                        continue;

                if (get_sec(elf, i, &ehdr, &shname, &shdr, &data))
                        continue;

                if (memcmp(shname, "kprobe/", 7) == 0 ||
                        memcmp(shname, "kretprobe/", 10) == 0 ||
                        memcmp(shname, "tracepoint/", 11) == 0 ||
                        memcmp(shname, "raw_tracepoint/", 15) == 0 ||
                        memcmp(shname, "xdp", 3) == 0 ||
                        memcmp(shname, "perf_event", 10) == 0 ||
                        memcmp(shname, "socket", 6) == 0 ||
                        memcmp(shname, "cgroup/", 7) == 0 ||
                        memcmp(shname, "sockops", 7) == 0 ||
                        memcmp(shname, "sk_skb", 6) == 0 ||
                        memcmp(shname, "sk_msg", 6) == 0) {
                        ret = load_and_attach(shname, data->d_buf,
                                        data->d_size);  //事件类型 字节码 字节码大小
                        if (ret != 0)
                                goto done;
                }
        }

done:
        close(fd);
        return ret;
}

打开 ELF 调试 log,可以得到该 ELF 文件各个段的内容首地址,大小,属性等信息。

wu@ubuntu:~/linux/samples/bpf$ sudo ./tracex4
[sudo] password for wu:
section 1:.strtab data 0x556034a3d070 size 277 link 0 flags 0
section 3:kprobe/kmem_cache_free data 0x556034a3d5a0 size 72 link 0 flags 6
section 4:.relkprobe/kmem_cache_free data 0x556034a3d5f0 size 16 link 26 flags 0
section 5:kretprobe/kmem_cache_alloc_node data 0x556034a3d610 size 192 link 0 flags 6
section 6:.relkretprobe/kmem_cache_alloc_node data 0x556034a3d6e0 size 16 link 26 flags 0
section 7:maps data 0x556034a3d700 size 28 link 0 flags 3
section 8:license data 0x556034a3d730 size 4 link 0 flags 3
section 9:version data 0x556034a3d750 size 4 link 0 flags 3
section 10:.debug_str data 0x556034a3d770 size 489 link 0 flags 48
section 11:.debug_loc data 0x556034a3d970 size 336 link 0 flags 0
section 12:.rel.debug_loc data 0x556034a3dad0 size 80 link 26 flags 0
section 13:.debug_abbrev data 0x556034a3db30 size 257 link 0 flags 0
section 14:.debug_info data 0x556034a3dc40 size 886 link 0 flags 0
section 15:.rel.debug_info data 0x556034a3dfc0 size 1200 link 26 flags 0
section 16:.debug_ranges data 0x556034a3e480 size 48 link 0 flags 0
section 17:.rel.debug_ranges data 0x556034a3e4c0 size 64 link 26 flags 0
section 18:.BTF data 0x556034a3e510 size 1384 link 0 flags 0
section 19:.rel.BTF data 0x556034a3ea80 size 48 link 26 flags 0
section 20:.BTF.ext data 0x556034a3eac0 size 376 link 0 flags 0
section 21:.rel.BTF.ext data 0x556034a3ec40 size 320 link 26 flags 0
section 22:.eh_frame data 0x556034a3ed90 size 80 link 0 flags 2
section 23:.rel.eh_frame data 0x556034a3edf0 size 32 link 26 flags 0
section 24:.debug_line data 0x556034a3ee20 size 327 link 0 flags 0
section 25:.rel.debug_line data 0x556034a3ef70 size 32 link 26 flags 0
section 26:.symtab data 0x556034a3efa0 size 1704 link 1 flags 0

BPF 字节码加载过程

接下来调用 load_and_attach,第一个参数是 event,本例就是 “kprobe/” ,第二个参数是 bpf 字节码,第三个参数是字节码大小。

BPF 指令结构

bpf_insn 是一个结构体,代表一条 eBPF 指令,包含 5 个字段组成:

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 */
};

每一个 eBPF 程序都是由若干个 bpf 指令构成,就是一个一个 bpf_insn 数组,使用 bpf 系统调用将其载入内核。

把 BPF 字节码装载到内核空间

接着调用 bpf_load_program,填入的参数为程序类型 prog_type, 和虚拟机指令 insns_cnt 等。

如果判断 events 是 kprobe/kretprobe,那么填充 buf 为 debugfs 相关路径。打开该路径,然后调用 sys_perf_event_open ioctl 设置等,这个和 strace 追踪到的调用过程基本一致。

static int load_and_attach(const char *event, struct bpf_insn *prog, int size)
{
    bool is_socket = strncmp(event, "socket", 6) == 0;


    ......

    fd = bpf_load_program(prog_type, prog, insns_cnt, license, kern_version,
                            bpf_log_buf, BPF_LOG_BUF_SIZE);

    ......
           if (is_kprobe || is_kretprobe) {
                bool need_normal_check = true;
                const char *event_prefix = "";

                if (is_kprobe)
                        event += 7;
                else
                        event += 10;

                if (*event == 0) {
                        printf("event name cannot be empty\n");
                        return -1;
                }

                if (isdigit(*event))
                        return populate_prog_array(event, fd);

#ifdef __x86_64__
                if (strncmp(event, "sys_", 4) == 0) {
                        snprintf(buf, sizeof(buf), "%c:__x64_%s __x64_%s",
                                is_kprobe ? 'p' : 'r', event, event);
                        err = write_kprobe_events(buf);
                        if (err >= 0) {
                                need_normal_check = false;
                                event_prefix = "__x64_";
                        }
                }
#endif
                if (need_normal_check) {
                        snprintf(buf, sizeof(buf), "%c:%s %s",
                                is_kprobe ? 'p' : 'r', event, event);
                        err = write_kprobe_events(buf);
                        if (err < 0) {
                                printf("failed to create kprobe '%s' error '%s'\n",
                                       event, strerror(errno));
                                return -1;
                        }
                }

                strcpy(buf, DEBUGFS);
                strcat(buf, "events/kprobes/");
                strcat(buf, event_prefix);
                strcat(buf, event);
                strcat(buf, "/id");
        }

        efd = open(buf, O_RDONLY, 0);
        if (efd < 0) {
                printf("failed to open event %s\n", event);
                return -1;
        }

        err = read(efd, buf, sizeof(buf));
        if (err < 0 || err >= sizeof(buf)) {
                printf("read from '%s' failed '%s'\n", event, strerror(errno));
                return -1;
        }

        close(efd);

        buf[err] = 0;
        id = atoi(buf);
        attr.config = id;

        efd = sys_perf_event_open(&attr, -1/*pid*/, 0/*cpu*/, -1/*group_fd*/, 0);
        ... ...
        event_fd[prog_cnt - 1] = efd;
        err = ioctl(efd, PERF_EVENT_IOC_ENABLE, 0);
        ... ...
        err = ioctl(efd, PERF_EVENT_IOC_SET_BPF, fd);
        ... ...

        return 0;
}

其中 bpf_load_program 会通过 BPF_PROG_LOAD 系统调用,将字节码传入内核,返回一个文件描述符 fdattr->insns 就是下面这种 bpf 字节码:

code=BPF_ALU64|BPF_X|BPF_MOV, dst_reg=BPF_REG_6, src_reg=BPF_REG_1, off=0, imm=0

kernel/bpf/syscall.c 中定义的相应系统调用如下:

SYSCALL_DEFINE3(bpf, int, cmd, union bpf_attr __user *, uattr, unsigned int, size)
{
    ......
        case BPF_MAP_CREATE:
                err = map_create(&attr);
                break;
    case BPF_PROG_LOAD:
        err = bpf_prog_load(&attr);  //attr包含字节码
    ... ...
}

bpf_prog_load 真正的加载 bpf 字节码,首先从 bpf 字节码中获得 license,判断是不是 GPL license。

然后分配内核 bpf_prog 程序数据结构空间,将 bpf 虚拟机指令从用户空间拷贝到内核空间,把指令保存在 struct bpf_prog 结构体中。

然后运行 bpf_check 验证 bpf 指令在注入内核是否安全,比如检查栈是否会溢出,除数是否为零,否则不检测安不安全容易造成内核 panic 等严重问题,这一部分内容很多,就暂时不分析了。

把 BPF 字节码翻译为机器码

验证通过之后,核心调用是运行 bpf_prog_select_runtime 里的 do_jit 把 bpf 字节码转换成机器汇编码,最后运行 bpf_prog_kallsyms_add 将机器汇编码添加到 kallsyms,在 /proc/kallsyms 中会看到 bpf 程序的符号表:

static int bpf_prog_load(union bpf_attr *attr)
{
        enum bpf_prog_type type = attr->prog_type;
        struct bpf_prog *prog;
        int err;
        char license[128];
        bool is_gpl;
        ... ...
        /* copy eBPF program license from user space */
        if (strncpy_from_user(license, u64_to_user_ptr(attr->license),
                        sizeof(license) - 1) < 0)  //拷贝license attr->license
                return -EFAULT;
        license[sizeof(license) - 1] = 0;  //最后一位设空字符

        /* eBPF programs must be GPL compatible to use GPL-ed functions */
        is_gpl = license_is_gpl_compatible(license);

        /* plain bpf_prog allocation */
        prog = bpf_prog_alloc(bpf_prog_size(attr->insn_cnt), GFP_USER); /* 分配内核 bpf_prog 程序数据结构空间 */
        if (!prog)
                return -ENOMEM;

        prog->expected_attach_type = attr->expected_attach_type;

        prog->aux->offload_requested = !!attr->prog_ifindex;

        err = security_bpf_prog_alloc(prog->aux);
        if (err)
                goto free_prog_nouncharge;

        err = bpf_prog_charge_memlock(prog);
        if (err)
                goto free_prog_sec;

        prog->len = attr->insn_cnt;

        err = -EFAULT;
        if (copy_from_user(prog->insns, u64_to_user_ptr(attr->insns),
                        bpf_prog_insn_size(prog)) != 0)  //将若干指令从用户态拷贝到内核态
                goto free_prog;

        prog->orig_prog = NULL;
        prog->jited = 0;

        atomic_set(&prog->aux->refcnt, 1);
        prog->gpl_compatible = is_gpl ? 1 : 0;        //设置gpl_compatible字段

        ... ...
        /* run eBPF verifier */
        err = bpf_check(&prog, attr);  //运行verifier 检查字节码安全性
        if (err < 0)
                goto free_used_maps;

        prog = bpf_prog_select_runtime(prog, &err); //这里调用do_jit 将bpf字节码转换成汇编码
        if (err < 0)
                goto free_used_maps;

        err = bpf_prog_alloc_id(prog);
        if (err)
                goto free_used_maps;

        bpf_prog_kallsyms_add(prog);  //添加kallsyms

        err = bpf_prog_new_fd(prog);
        if (err < 0)
                bpf_prog_put(prog);
        return err;
        ... ...
}

运行 BPF 机器码

JIT 编译器将机器汇编码的首地址转换成一个函数指针,保存到 prog->bpf_func,再看看哪里调用 prog->bpf_func 这个函数指针的呢?

在 debugfs 中创建 kprobe events,执行 init_kprobe_trace 加载 BPF 字节码的时候就调用了 trace_kprobe_create 继而调用 kprobe_dispatcher,因为定义了 CONFIG_PERF_EVENTS 而后调用 kprobe_perf_func,相关代码如下:

static struct dyn_event_operations trace_kprobe_ops = {
        .create = trace_kprobe_create,
        .show = trace_kprobe_show,
        .is_busy = trace_kprobe_is_busy,
        .free = trace_kprobe_release,
        .match = trace_kprobe_match,
};

/* Make a tracefs interface for controlling probe points */
static __init int init_kprobe_trace(void)
{
        ... ...

        ret = dyn_event_register(&trace_kprobe_ops);
        if (ret)
                return ret;

        if (register_module_notifier(&trace_kprobe_module_nb))
                return -EINVAL;

        d_tracer = tracing_init_dentry();
        if (IS_ERR(d_tracer))
                return 0;

        entry = tracefs_create_file("kprobe_events", 0644, d_tracer,
                                NULL, &kprobe_events_ops);

        ... ...
        return 0;
}
fs_initcall(init_kprobe_trace);

trace_kprobe_create
{

   ... ...
   kprobe_dispatcher
   ... ...

}

static int kprobe_dispatcher(struct kprobe *kp, struct pt_regs *regs)
{
        struct trace_kprobe *tk = container_of(kp, struct trace_kprobe, rp.kp);
        int ret = 0;

        raw_cpu_inc(*tk->nhit);

        if (trace_probe_test_flag(&tk->tp, TP_FLAG_TRACE))
                kprobe_trace_func(tk, regs);
        #ifdef CONFIG_PERF_EVENTS
        if (trace_probe_test_flag(&tk->tp, TP_FLAG_PROFILE))
                ret = kprobe_perf_func(tk, regs);
        #endif
        return ret;
}

kprobe_perf_func 会调用 trace_call_bpf,在这里会执行 bpf 程序。BPF_PROG_RUN_ARRAY_CHECK 是一个宏,其实质上执行 BPF_PROG_RUN 里的一个函数:

/* Kprobe profile handler */
static int
kprobe_perf_func(struct trace_kprobe *tk, struct pt_regs *regs)
{
        if (bpf_prog_array_valid(call)) {
                unsigned long orig_ip = instruction_pointer(regs);
                int ret;

                ret = trace_call_bpf(call, regs);

                /*
* We need to check and see if we modified the pc of the
* pt_regs, and if so return 1 so that we don't do the
* single stepping.
*/
                if (orig_ip != instruction_pointer(regs))
                        return 1;
                if (!ret)
                        return 0;
        }

        return 0;
}


/ * trace_call_bpf - invoke BPF program
 * @call: tracepoint event
 * @ctx: opaque context pointer
 *
 * kprobe handlers execute BPF programs via this helper.
 * Can be used from static tracepoints in the future.
 *
 * Return: BPF programs always return an integer which is interpreted by
 * kprobe handler as:
 * 0 - return from kprobe (event is filtered out)
 * 1 - store kprobe event into ring buffer
 * Other values are reserved and currently alias to 1
 */
unsigned int trace_call_bpf(struct trace_event_call *call, void *ctx)
{
        unsigned int ret;

        ... ...
        ret = BPF_PROG_RUN_ARRAY_CHECK(call->prog_array, ctx, BPF_PROG_RUN);  //运行bpf程序

out:
        __this_cpu_dec(bpf_prog_active);
        preempt_enable();

        return ret;
}

其中的 trace_event_call 结构体定义了 bpf_prog_array,该结构体数组中包含了要执行的函数指针:

struct trace_event_call {
        struct list_head        list;
        struct trace_event_class *class;
        union {
                char                        *name;
                /* Set TRACE_EVENT_FL_TRACEPOINT flag when using "tp" */
                struct tracepoint        *tp;
        };
        ... ...
#ifdef CONFIG_PERF_EVENTS
        int                                perf_refcount;
        struct hlist_head __percpu        *perf_events;
        struct bpf_prog_array __rcu        *prog_array;

        int        (*perf_perm)(struct trace_event_call *,
                                struct perf_event *);
#endif
};

struct bpf_prog_array {
        struct rcu_head rcu;
        struct bpf_prog_array_item items[0];
};

struct bpf_prog_array_item {
        struct bpf_prog *prog;
        struct bpf_cgroup_storage *cgroup_storage[MAX_BPF_CGROUP_STORAGE_TYPE];
};

BPF_PROG_RUN_ARRAY_CHECKBPF_PROG_RUN 宏展开如下所示,实质是在 BPF_PROG_RUN 中调用 ret = (*(prog)->bpf_func)(ctx, (prog)->insnsi) 这个函数指针来执行 bpf 指令:

#define BPF_PROG_RUN_ARRAY_CHECK(array, ctx, func)        \
        __BPF_PROG_RUN_ARRAY(array, ctx, func, true)


#define __BPF_PROG_RUN_ARRAY(array, ctx, func, check_non_null)        \
        ({                                                \
                struct bpf_prog_array_item *_item;        \
                struct bpf_prog *_prog;                 \
                struct bpf_prog_array *_array;                \
                u32 _ret = 1;                                \
                preempt_disable();                        \
                rcu_read_lock();                        \
                _array = rcu_dereference(array);        \
                if (unlikely(check_non_null && !_array))\
                        goto _out;                        \
                _item = &_array->items[0];                \
                while ((_prog = READ_ONCE(_item->prog))) {                \
                        bpf_cgroup_storage_set(_item->cgroup_storage);        \
                        _ret &= func(_prog, ctx);        \
                        _item++;                        \
                }                                        \
_out:                                                        \
                rcu_read_unlock();                        \
                preempt_enable();                        \
                _ret;                                        \
         })



#define BPF_PROG_RUN(prog, ctx) ({                                \
        u32 ret;                                                \
        cant_sleep();                                                \
        if (static_branch_unlikely(&bpf_stats_enabled_key)) {        \
                struct bpf_prog_stats *stats;                        \
                u64 start = sched_clock();                        \
                ret = (*(prog)->bpf_func)(ctx, (prog)->insnsi); \
                stats = this_cpu_ptr(prog->aux->stats);         \
                u64_stats_update_begin(&stats->syncp);                \
                stats->cnt++;                                        \
                stats->nsecs += sched_clock() - start;                \
                u64_stats_update_end(&stats->syncp);                \
        } else {                                                \
                ret = (*(prog)->bpf_func)(ctx, (prog)->insnsi); \
        }                                                        \
        ret; })

文章作者: 杰克成
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 杰克成 !
评论
  目录