编写eBPF程序,通常通过Cilium、bcc或bpftrace等项目间接使用,这些项目提供eBPF抽象,使用预定义功能和帮助函数实现eBPF相关功能。
利用原生libbpf编写BPF程序,需要编写用户态和内核态程序,利用内核自带的Makefile进行编译。
1. 利用C语言编写BPF程序
使用C语言开发(而不是BCC、BPFTrace)的原因:
降低启动开销
没有庞大的编译器依赖
降低运行时开销
BPF hacking : )
使用perf
使用C语言开发整个跟踪工具注意:
不能进行无界循环或内核函数调用,只能通过预定义的bpf_*内核辅助函数、BPF尾部调用、以及一些编译器内置函数。
必须通过bpf_probe_read()读取内存,bpf_probe_read()对栈内存进行必要的检查,可以使用BPF映射存储。
从内核将数据输出到用户态的三种方法:
bpf_perf_event_output() (BPF_FUNC_perf_event_output):通过自定义结构将每个时间的详细信息发送到用户空间的方式。
BPF_MAP_TYPE.*以及映射辅助函数(bpf_map_update_elem()):映射是键值哈希,可以从中构建更高级的数据结构。映射可用于摘要统计或制作直方图,并定期从用户空间读取。
bpf_trace_printk():仅用于调试,写入trace_pipe,并可能与其他程序和跟踪器冲突。
尽量使用静态插桩(跟踪点、USDT),而不是动态插桩(kprobes、uprobes),因为静态插桩提供了稳定的接口。
使用BCC、BPFTrace中重写程序进行调试,可以显示debug信息。例如,BCC的BEBUG_PREPROCESSOR模式在预处理器之后显示C代码。
开发新的BPF功能时,通常提供示例程序/内核自测试用例,用于演示其用法。C程序被存放在Linux源代码的samples/bpf下,自测程序在tools/testing/selftests/bpf中。
BPF指令:作为嵌入在C程序中的BPF指令的数组,传递到bpf()系统调用。
C程序:作为可以编译为BPF的C程序,该程序随后被传递给bpf()系统调用。
Linux 4.x系列的旧API是一个包含常见函数库,定义在samples/bpf下的bpf_load.c和bpf_load.h中。
旧API已经在内核中被弃用了,以后这个旧bpf_load API可能会被删除。大部分项目已经被转换为使用libbpf了,libbpf是和内核功能同步开发的,并被外部项目使用(BCC、BPFTrace)。
2.编译内核
系统版本 openEuler 20.03 LTS SP3,编译内核。
cd /usr/local
wget https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/snapshot/linux-4.19.tar.gz
wget https://gitee.com/openeuler/kernel/repository/archive/openEuler-20.03-LTS-SP3.zip
cd linux-4.19
make oldconfig
make
make modules_install && make install && make headers_install
reboot
make samples/bpf
make samples/bpf,./tools/perf/perf-sys.h代码报错,报错的那一行是test开头的。
内核开发大佬们的邮件:
https://www.spinics.net/lists/netdev/msg608676.html 由于是测试相关的代码,所以可以跳过。
增加变量,注释掉测试代码:
#ifndef HAVE_ATTR_TEST
#define HAVE_ATTR_TEST 0
#endif
重新make。
3. sample/bpf/Makefile
sample/bpf/Makefile 开头定义变量 hostprogs-y
此种编译阶段称为:Host Program support,目的是在编译阶段就构建出在本机直接运行的可执行文件,需要两个步骤:
第一步告诉 kbuild 存在哪些需要生成的可执行文件,通过变量hostprogs-y
来指定:
hostprogs-y := test_lru_dist
第二步是将显式依赖关系添加到可执行文件中。这可以通过两种方式来完成,一种是为Makefile中某个target添加这个可执行文件,作为prerequisites依赖关系,另一种是直接利用变量 always,即无需指定第一种方式中的依赖关系,只要Makefile被执行,变量 always
中包含的可执行文件都会被构建。
# Tell kbuild to always build the programs
always := $(hostprogs-y)
可以看到Makefile使用第二种方式,保证这些可执行文件被构建。
3.2 <executeable>-objs
前两行声明并初始化了两个变量LIBBPF和CGROUP_HELPERS。
可执行文件可以由多个其他文件复合组成,通过<executeable>-objs语法,指定所有用于生成最终可执行文件(命名为executeable)的文件清单。
例如,可执行文件sockex1是由bpf_load.o和sockex1_user.o链接生成的。
3.3 HOSTCFLAGS与HOSTLOADLIBES
变量HOSTCFLAGS是在编译host program(即可执行文件)时,为编译操作指定的特殊选项,使用-I参数指定依赖的头文件所在目录。默认情况,这个变量的配置会作用到当前Makefile所有host program。
变量HOSTLOADLIBES是用于链接操作时指定的库。
libelf 用来管理elf格式的文件,程序一般都会使用elf作为最终格式,因此需要加载这个library。
librt,这个库其实很常用,一般含有#include<time.h>头文件的代码,都需要加载这个library,用来支持real time相关功能。
如下面代码中使用两个库,通过选项-l加到最终生成的可执行文件中:
3.4 编译BPF程序
Makefile最后编译BPF程序:
# Trick to allow make to be run from this directory
all:
$(MAKE) -C ../../ $(CURDIR)/ BPF_SAMPLES_PATH=$(CURDIR)
clean:
$(MAKE) -C ../../ M=$(CURDIR) clean
@rm -f *~
$(LIBBPF): FORCE
# Fix up variables inherited from Kbuild that tools/ build system won't like
$(MAKE) -C $(dir [email protected]) RM='rm -rf' LDFLAGS= srctree=$(BPF_SAMPLES_PATH)/../../ O=
$(obj)/syscall_nrs.s: $(src)/syscall_nrs.c
$(call if_changed_dep,cc_s_c)
$(obj)/syscall_nrs.h: $(obj)/syscall_nrs.s FORCE
$(call filechk,offsets,__SYSCALL_NRS_H__)
clean-files += syscall_nrs.h
FORCE:
# Verify LLVM compiler tools are available and bpf target is supported by llc
.PHONY: verify_cmds verify_target_bpf $(CLANG) $(LLC)
verify_cmds: $(CLANG) $(LLC)
@for TOOL in $^ ; do \
if ! (which -- "$${TOOL}" > /dev/null 2>&1); then \
echo "*** ERROR: Cannot find LLVM tool $${TOOL}" ;\
exit 1; \
else true; fi; \
done
verify_target_bpf: verify_cmds
@if ! (${LLC} -march=bpf -mattr=help > /dev/null 2>&1); then \
echo "*** ERROR: LLVM (${LLC}) does not support 'bpf' target" ;\
echo " NOTICE: LLVM version >= 3.7.1 required" ;\
exit 2; \
else true; fi
$(BPF_SAMPLES_PATH)/*.c: verify_target_bpf $(LIBBPF)
$(src)/*.c: verify_target_bpf $(LIBBPF)
$(obj)/tracex5_kern.o: $(obj)/syscall_nrs.h
FORCE:
# Verify LLVM compiler tools are available and bpf target is supported by llc
.PHONY: verify_cmds verify_target_bpf $(CLANG) $(LLC)
verify_cmds: $(CLANG) $(LLC)
@for TOOL in $^ ; do \
if ! (which -- "$${TOOL}" > /dev/null 2>&1); then \
echo "*** ERROR: Cannot find LLVM tool $${TOOL}" ;\
exit 1; \
else true; fi; \
done
verify_target_bpf: verify_cmds
@if ! (${LLC} -march=bpf -mattr=help > /dev/null 2>&1); then \
echo "*** ERROR: LLVM (${LLC}) does not support 'bpf' target" ;\
echo " NOTICE: LLVM version >= 3.7.1 required" ;\
exit 2; \
else true; fi
$(BPF_SAMPLES_PATH)/*.c: verify_target_bpf $(LIBBPF)
$(src)/*.c: verify_target_bpf $(LIBBPF)
$(obj)/tracex5_kern.o: $(obj)/syscall_nrs.h
# asm/sysreg.h - inline assembly used by it is incompatible with llvm.
# But, there is no easy way to fix it, so just exclude it since it is
# useless for BPF samples.
$(obj)/%.o: $(src)/%.c
@echo " CLANG-bpf " [email protected]
$(Q)$(CLANG) $(NOSTDINC_FLAGS) $(LINUXINCLUDE) $(EXTRA_CFLAGS) -I$(obj) \
-I$(srctree)/tools/testing/selftests/bpf/ \
-D__KERNEL__ -D__BPF_TRACING__ -Wno-unused-value -Wno-pointer-sign \
-D__TARGET_ARCH_$(ARCH) -Wno-compare-distinct-pointer-types \
-Wno-gnu-variable-sized-type-not-at-end \
-Wno-address-of-packed-member -Wno-tautological-compare \
-Wno-unknown-warning-option $(CLANG_ARCH_ARGS) \
-O2 -emit-llvm -c $< -o -| $(LLC) -march=bpf $(LLC_FLAGS) -filetype=obj -o [email protected]
ifeq ($(DWARF2BTF),y)
$(BTF_PAHOLE) -J [email protected]
endif
首先是定义了很多target,其中包括默认的入口target,$(CURDIR)是系统变量,其值是当前工作目录的绝对路径,作用跟$(pwd)类似。
all:
$(MAKE) -C ../../ $(CURDIR)/ BPF_SAMPLES_PATH=$(CURDIR)
clean:
$(MAKE) -C ../../ M=$(CURDIR) clean
@rm -f *~
编译BPF程序,其中有两个系统变量:第一个[email protected]代表的是target所指的文件名;第二个$<代表的是第一个prerequisite的文件名。把所有.c源代码文件,通过clang全部编译成.o目标文件。
# asm/sysreg.h - inline assembly used by it is incompatible with llvm.
# But, there is no easy way to fix it, so just exclude it since it is
# useless for BPF samples.
$(obj)/%.o: $(src)/%.c
@echo " CLANG-bpf " [email protected]
$(Q)$(CLANG) $(NOSTDINC_FLAGS) $(LINUXINCLUDE) $(EXTRA_CFLAGS) -I$(obj) \
-I$(srctree)/tools/testing/selftests/bpf/ \
-D__KERNEL__ -D__BPF_TRACING__ -Wno-unused-value -Wno-pointer-sign \
-D__TARGET_ARCH_$(ARCH) -Wno-compare-distinct-pointer-types \
-Wno-gnu-variable-sized-type-not-at-end \
-Wno-address-of-packed-member -Wno-tautological-compare \
-Wno-unknown-warning-option $(CLANG_ARCH_ARGS) \
-O2 -emit-llvm -c $< -o -| $(LLC) -march=bpf $(LLC_FLAGS) -filetype=obj -o [email protected]
ifeq ($(DWARF2BTF),y)
$(BTF_PAHOLE) -J [email protected]
endif
生成新的BPF可执行文件
利用Linux内核环境来编译自己的BPF程序,修改samples/bpf/目录下的Makefile即可。
# BPF程序如下所示:
# 内核空间代码:my_bpf_kern.c
# 用户空间代码:my_bpf_user.c
# 1. 追加新的一行至hostprogs-y开头的代码块最后,保证自己的BPF程序能够生成可执行文件
hostprogs-y += my_bpf
# 2. 一般BPF程序使用以下命令即可,具体取决于你的程序是否依赖其他特殊头文件
my_bpf-objs := bpf_load.o $(LIBBPF) my_bpf_user.o
# 3. 追加新的一行至always开头的代码块最后,保证触发生成可执行文件的任务
always += my_bpf_kern.o
修改完成后,使用make samples/bpf/命令,生成自己的可执行文件。
Hello World
hello_world.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <linux/version.h>
#include <bpf/bpf.h>
#include <bcc/libbpf.h>
#define DEBUGFS "/sys/kernel/debug/tracing/"
char bpf_log_buf[BPF_LOG_BUF_SIZE];
int main(int argc, char *argv[]){
int prog_fd, probe_fd;
/*使用BPF指令帮助程序宏将BPF程序声明为prog数组*/
struct bpf_insn prog[] = {
/*把 Hello,World!\n 存放在BPF堆栈上。为了提高效率,将四个字符组成的组进行声明并存储一个32位整数(单词的类型为BPF_W)
,而不是一次存储一个字符。最后两个字节存储为一个16位整数(半个单词的类型为BPF_H)*/
BPF_MOV64_IMM(BPF_REG_1, 0xa21), /* '!\n' */
BPF_STX_MEM(BPF_H, BPF_REG_10, BPF_REG_1, -4),
BPF_MOV64_IMM(BPF_REG_1, 0x646c726f), /* 'orld' */
BPF_STX_MEM(BPF_W, BPF_REG_10, BPF_REG_1, -8),
BPF_MOV64_IMM(BPF_REG_1, 0x57202c6f), /* 'o, W' */
BPF_STX_MEM(BPF_W, BPF_REG_10, BPF_REG_1, -12),
BPF_MOV64_IMM(BPF_REG_1, 0x6c6c6548), /* 'Hell' */
BPF_STX_MEM(BPF_W, BPF_REG_10, BPF_REG_1, -16),
/*准备并调用BPF_FUNC_trace_printk,该调用把字符串写到共享的跟踪缓冲区中*/
BPF_MOV64_IMM(BPF_REG_1, 0),
BPF_STX_MEM(BPF_B, BPF_REG_10, BPF_REG_1, -2),
BPF_MOV64_REG(BPF_REG_1, BPF_REG_10),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_1, -16), /* r1 = fp - 16 */
BPF_MOV64_IMM(BPF_REG_2, 15),
BPF_RAW_INSN(BPF_JMP| BPF_CALL, 0, 0, 0, BPF_FUNC_trace_printk),
BPF_MOV64_IMM(BPF_REG_0, 0), /* r0 = 0 */
BPF_EXIT_INSN(),
};
size_t insns_cnt = sizeof(prog) / sizeof(struct bpf_insn);
/*调用libbpf的bpf_load_program函数,将其附加到一个kprobe上,返回一个文件描述符*/
prog_fd = bpf_load_program(BPF_PROG_TYPE_KPROBE, prog, insns_cnt,
"GPL", LINUX_VERSION_CODE, bpf_log_buf, BPF_LOG_BUF_SIZE);
if (prog_fd < 0) {
printf("ERROR: failed to load prog '%s'\n", strerror(errno));
return 1;
}
/*调用libbcc的bpf_attach_kprobe函数,把程序附加到do_nanossleep()函数的入口kprobe。事件名称为hello_world。函数返回一个文件描述符*/
probe_fd = bpf_attach_kprobe(prog_fd, BPF_PROBE_ENTRY, "hello_world",
"do_nanosleep", 0, 0);
if (probe_fd < 0) {
return 2;
}
/*调用system()对共享跟踪管道调用cat命令,打印输出消息*/
system("cat " DEBUGFS "/trace_pipe");
close(probe_fd);
bpf_detach_kprobe("hello_world");
close(prog_fd);
return 0;
}
编译。
# BPF程序如下所示:
# 内核空间代码:hello_world.c
# 1. 追加新的一行至hostprogs-y开头的代码块最后,保证自己的BPF程序能够生成可执行文件
hostprogs-y += hello_world
# 2. 一般BPF程序使用以下命令即可,具体取决于你的程序是否依赖其他特殊头文件
hello_world-objs := hello_world.o
# 3. 添加编译选项
HOSTLDLIBS_hello_world += -lbcc
报错,安装bcc-devel。
执行成功。
bigreads
bigreads跟踪vfs_read()返回,并为大于1MB的读操作打印消息。bigreads与下列使用BPFTrace工具的单行程序作用一致。
bpftrace -e 'kr:vfs_read /retval > 1024*1024/{printf("Read: %d bytes\n", retval); }'
内核态代码bigreads_kern.c:
#include <uapi/linux/bpf.h>
#include <uapi/linux/ptrace.h>
#include <linux/version.h>
#include "bpf_helpers.h"
/*定义字节阈值*/
#define MIN_BYTES (1024 * 1024)
/*声明一个名为kretprobe/vfs_read的ELF节*/
SEC("kretprobe/vfs_read")
/*调用kretprobe事件的函数。结构体参数pt_regs包含寄存器状态和BPF上下文。
函数参数和返回值可以从寄存器被读取。这个结构体指针也是数个BPF辅助函数的必需函数*/
int bpf_myprog(struct pt_regs *ctx)
{
char fmt[] = "READ: %d bytes\n";
/*使用PT_REGS_RC宏定义将ctx映射到ctx->rax,从pt_regs结构体寄存器中取得返回值*/
int bytes = PT_REGS_RC(ctx);
if (bytes >= MIN_BYTES){
bpf_trace_printk(fmt, sizeof(fmt), bytes, 0, 0);
}
return 0;
}
char _license[] SEC("license") = "GPL";
u32 _version SEC("version") = LINUX_VERSION_CODE;
用户态代码bigreads_user.c:
//bigreads_user.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/resource.h>
#include "bpf/libbpf.h"
#define DEBUGFS "/sys/kernel/debug/tracing/"
int main()
{
struct bpf_object *obj;
struct bpf_program *prog;
struct bpf_link *link;
struct rlimit lim = {
.rlim_cur = RLIM_INFINITY, /*设置RLIM_INFINITY为无穷大,避免内存分配问题*/
.rlim_max = RLIM_INFINITY,
};
char filename[256];
snprintf(filename, sizeof(filename), "%s_kern.o", argv[0]);
setrlimit(RLIMIT_MEMLOCK, &lim);
obj = bpf_object__open(filename);
if(libbpf_get_error(obj)){
printf("ERROR: failed to open prog: '%s'\n", strerror(errno));
return 1;
}
prog = bpf_object__find_program_by_title(obj, "kretprobe/vfs_read");
bpf_program__set_type(prog, BPF_PROG_TYPE_KPROBE);
if(bpf_object__load(obj)){
printf("ERROR: failed to load prog: '%s'\n", strerror(errno));
return 1;
}
link = bpf_program__attach_kprobe(prog, true /*retprobe*/, "vfs_read");
if(libbpf_get_error(obj))
return 2;
system("cat " DEBUGFS "/trace_pipe");
bpf_link__destroy(link);
bpf_object__close(obj);
return 0;
}
修改Makefile。
编译C程序,bigreads_user.o读取的部分中创建了bigreads_kern.o文件。
perf C
Linux perf 两个接口:
perf record:对于在事件上运行的程序,可以应用自定义过滤器并向perf.data文件发出其他记录。
perf trace:为了美化跟踪输出,使用BPF程序过滤并增强输出的perf跟踪事件
yum install pref
bigreads.c
#include <uapi/linux/bpf.h>
#include <uapi/linux/ptrace.h>
#include <linux/types.h>
#define SEC(NAME) __attribute__((section(NAME), used))
struct bpf_map_def{
unsigned int type;
unsigned int key_size;
unsigned int value_size;
unsigned int max_entries;
};
static int (*perf_event_output)(void *, struct bpf_map_def *, int, void *, unsigned long) = (void *)BPF_FUNC_perf_event_output;
struct bpf_map_def SEC("maps") channel = {
.type = BPF_MAP_TYPE_PERF_EVENT_ARRAY,
.key_size = sizeof(int),
.value_size = sizeof(__u32),
.max_entries = __NR_CPUS__,
};
#define MIN_BYTES (1024 * 1024)
SEC("func=vfs_read")
int bpf_myprog(struct pt_regs *ctx)
{
long bytes = ctx->rdx;
if (bytes >= MIN_BYTES){
perf_event_output(ctx, &channel, BPF_F_CURRENT_CPU, &bytes, sizeof(bytes));
}
return 0;
}
char _license[] SEC("license") = "GPL";
u32 _version SEC("version") = LINUX_VERSION_CODE;
使用dd命令进行读取4M,触发bigreads记录。
dd if=/dev/zero of=sun.txt bs=4M count=1
perf.data记录文件大于1MB的读取项,输出事件。
perf record -e bpf-output/no-inherit,name=evt/ -e ./bigreads.c/map:channel.event=evt/ -a
perf script
00 00 40是4MB,0x400000,采用小端序。
reference
http://www.ruanyifeng.com/blog/2015/02/make.html
https://github.com/nevermosby/linux-bpf-learning/blob/master/bpf/perf-sys.h
https://davidlovezoe.club/wordpress/archives/988
https://www.spinics.net/lists/netdev/msg608676.html
《BPF之巅洞悉Linux系统和应用性能》