Linux | 利用原生libbpf编写BPF程序
2022-8-29 20:0:59 Author: TahirSec(查看原文) 阅读量:92 收藏

      编写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/localwget https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/snapshot/linux-4.19.tar.gzwget https://gitee.com/openeuler/kernel/repository/archive/openEuler-20.03-LTS-SP3.zipcd linux-4.19make oldconfigmakemake modules_install && make install && make headers_installreboot
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

3.1 hostprogs-y

    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 programsalways := $(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是用于链接操作时指定的库。

  1. libelf 用来管理elf格式的文件,程序一般都会使用elf作为最终格式,因此需要加载这个library。

  2. librt,这个库其实很常用,一般含有#include<time.h>头文件的代码,都需要加载这个library,用来支持real time相关功能。

如下面代码中使用两个库,通过选项-l加到最终生成的可执行文件中:

3.4 编译BPF程序

    Makefile最后编译BPF程序:

# Trick to allow make to be run from this directoryall:        $(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
  1. 生成新的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/命令,生成自己的可执行文件。

  1. 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。

    执行成功。

  1. 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文件。

  1. 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/ -aperf 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系统和应用性能》


文章来源: http://mp.weixin.qq.com/s?__biz=MzkzNjIwMzM5Nw==&mid=2247483992&idx=1&sn=b2f0dbc8087c431c51ff05808959506c&chksm=c2a307b2f5d48ea46a11493d39068d16213901c3847e436e8b8eefb323742f3c123c81a0012f#rd
如有侵权请联系:admin#unsafe.sh