总览
libbpf
是内核提供的一个功能库,学习libbpf
有助于理解bcc/bpftrace
等功能。eBPF
程序的运行流程如图所示:
- 生成
eBPF
程序字节码,字节码最终被内核中的「虚拟机」执行。 - 通过系统调用加载
eBPF
字节码到内核中,将eBPF
程序attach
到各个事件、函数。 - 创建
map
,用于在内核态与用户态进行数据交互。 - 「事件」、
TP
点触发时,调用attach
的eBPF
字节码并执行其功能。
本文例子为:统计一段时间中syscall
调用次数,项目文件结构如下:
syscall_count_kern.c
为eBPF
程序的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.h
为libbpf
提供一些有用的宏、函数和定义。
生成上述文件(需要内核源码、或者使用单独构建的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
库依赖libelf
和libz
,需要交叉编译相关库:
# 下载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.o
、syscall_count
和动态库拷贝到之前搭建的内核调试环境中。
启动虚拟机并运行,即可开始调试内核bpf
模块功能。