总览

libbpf是内核提供的一个功能库,学习libbpf有助于理解bcc/bpftrace等功能。eBPF程序的运行流程如图所示:
libbpf_ebpf_0

  • 生成eBPF程序字节码,字节码最终被内核中的「虚拟机」执行。
  • 通过系统调用加载eBPF字节码到内核中,将eBPF程序attach到各个事件、函数。
  • 创建map,用于在内核态与用户态进行数据交互。
  • 「事件」、TP点触发时,调用attacheBPF字节码并执行其功能。

本文例子为:统计一段时间中syscall调用次数,项目文件结构如下:

libbpf_ebpf_1

  • syscall_count_kern.ceBPF程序的c代码,使用clang编译为eBPF字节码。
  • syscall_count_user.cpp为用户态程序,用于调用libbpf加载eBPF字节码。

eBPF 字节码生成

字节码生成有多种方式:

  • 手动编写字节码(手写汇编指令),使用libbpf中的内存加载模式。
  • 编写c代码,由clang生成,使用libbpf解析elf文件获取相关字节码信息。
  • 一些其他工具能够解析脚本语言生成字节码(bpftrace)。

本文使用clang进行编译,创建eBPF程序:

// syscall_count_kern.c
#include "vmlinux.h"
#include "bpf/bpf_helpers.h"

// 大小为1的数组,用于存放计数值
struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __type(key, u32);
    __type(value, u32);
    __uint(max_entries, 1);
} data SEC(".maps");

// 不需要处理参数默认直接 void* 指针即可
SEC("tracepoint/raw_syscalls/sys_enter")
int trace_enter_open(void *arg) {
    u32 key = 0;
    u32 init_val = 0;
    // 查找arr[0]并+1,不存在则创建
    u32 *value = bpf_map_lookup_elem(&data, &key);
    if (value) {
        *value += 1;
    } else {
        bpf_map_update_elem(&data, &key, &init_val, BPF_NOEXIST);
    }
    return 0;
}

char LICENSE[] SEC("license") = "GPL";

需要注意:

  • 头文件使用vmlinux.h,不用手动引入内核头文件。如果内核不支持生成该头文件,请手动引入所需要的内核头文件。
  • bpf/bpf_helpers.hlibbpf提供一些有用的宏、函数和定义。

生成上述文件(需要内核源码、或者使用单独构建的libbpf库和bpftool):

# 构建 vmlinux.h 文件
# 进入内核 bpftool 目录
# 这里使用交叉编译
cd tools/bpf/bpftool
make -j8 ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- 
# 生成 vmlinux.h 在 tools/bpf/bpftool 文件夹下
# 生成 libbpf 在 tools/bpf/bpftool/libbpf 文件夹下

# 拷贝到我们的工程目录下
cp ${LINUX_SRC}/tools/bpf/bpftool/vmlinux.h ./
cp -r ${LINUX_SRC}/tools/bpf/bpftool/libbpf/ ./

编译生成字节码(注意要使用-g -O2否则加载时会报错)

clang -g -O2 -target bpf -c syscall_count_kern.c -o syscall_count_kern.o -I$PWD/libbpf/include

libbpf 库使用

使用libbpf库加载eBPF程序代码如下:

// syscall_count_user.cpp
#include <bpf/libbpf.h>
#include <iostream>
#include <string>
#include <unistd.h>

const std::string file_name = "syscall_count_kern.o";

int main() {
    // 打开文件为 obj
    bpf_object *obj = bpf_object__open_file(file_name.c_str(), nullptr);
    if (libbpf_get_error(obj)) {
        std::cout << "open file error:" << strerror(errno) << std::endl;
        return 0;
    }
    // 解析elf查找其中的 program
    bpf_program *prog = bpf_object__find_program_by_name(obj, "trace_enter_open");
    if (!prog) {
        std::cout << "not find the program" << std::endl;
        return 0;
    }
    // 加载
    if (bpf_object__load(obj)) {
        std::cout << "load object error: " << strerror(errno) << std::endl;
        return 0;
    }
    // 建立 map
    bpf_map *map = bpf_object__find_map_by_name(obj, "data");
    if (map < 0) {
        std::cout << "finding map error: " << strerror(errno) << std::endl;
        return 0;
    }
    // attach
    bpf_link *link = bpf_program__attach(prog);
    if (libbpf_get_error(link)) {
        std::cout << "attach failed: " << strerror(errno) << std::endl;
        return 0;
    }
    while (true) {
        __u32 key = 0;
        __u32 val = 0;
        // 等待统计数据
        sleep(5);
        // 读取
        if (bpf_map__lookup_elem(map, &key, sizeof(key), &val, sizeof(val), 0) != 0) {
            std::cout << "loopup error: " << strerror(errno) << std::endl;
        }
        std::cout << "syscall count: " << val << std::endl;
        // 写入
        val = 0;
        if (bpf_map__update_elem(map, &key, sizeof(key), &val, sizeof(val), BPF_ANY) !=
            0) {
            std::cout << "update error: " << strerror(errno) << std::endl;
        }
    }
    return 0;
}

编译时还需要linux头文件支持,从内核源码中安装到当前目录。

mkdir linux-headers
cd ${LINUX_SRC}
make headers_install INSTALL_HDR_PATH=${WORKSPACE}/linux-headers

整个工程目录如下:

libbpf_ebpf_2

libbpf库依赖libelflibz,需要交叉编译相关库:

# 下载libz
wget https://zlib.net/zlib-1.2.13.tar.gz
tar -xvf zlib-1.2.13.tar.gz
cd ./zlib-1.2.13/ && mkdir install
CC=riscv64-linux-gnu-gcc CXX=riscv64-linux-gnu-g++ ./configure --prefix=$PWD/install
make 
make install
cd ..
# 下载libelf
wget https://sourceware.org/elfutils/ftp/0.188/elfutils-0.188.tar.bz2
tar -xvf elfutils-0.188.tar.bz2
cd elfutils-0.188 && mkdir install
CC=riscv64-linux-gnu-gcc CXX=riscv64-linux-gnu-g++ LDFLAGS="-L$PWD/../zlib-1.2.13/install/lib" LIBS="-lz" ./configure --prefix=$PWD/install --build=x86_64-linux-gnu --host=riscv64-linux-gnu --disable-libdebuginfod --disable-debuginfod
make -j8
make -C libelf install

编译用户态eBPF加载程序,链接之前编译好的依赖库:

riscv64-linux-gnu-g++ -o syscall_count syscall_count_user.cpp ./libbpf/libbpf.a -lelf -lz -I$PWD/libbpf/include -I$PWD/linux-headers/include/ -L$PWD/zlib-1.2.13/install/lib -L$PWD/elfutils-0.188/install/lib

注意:内核要开启相关功能(若加载失败可以使用 gdb 进行内核调试,定位根因)

# Kernel hacking
#     -> Tracers 这里开启
# 还需要挂载 debugfs
mount -t debugfs none /sys/kernel/debug

syscall_count_kern.osyscall_count和动态库拷贝到之前搭建的内核调试环境中。

libbpf_ebpf_3

启动虚拟机并运行,即可开始调试内核bpf模块功能。

libbpf_ebpf_4

最后修改:2023 年 01 月 05 日
如果觉得我的文章对你有用,请随意赞赏