BPF C编程入门

Linux内核之旅-BPF C编程入门 的笔记

先说一下,这里的BPF实际上是指eBPF,不是传统的cBPF

1.搭建BPF程序运行环境

1.1.下载内核源码

下载的内核版本应与你系统的版本一致,查看当前内核版本 uname -r

然后在源码镜像站点(http://ftp.sjtu.edu.cn/sites/ftp.kernel.org/pub/linux/kernel)下载对应版本的内核源码

也可以通过Ubuntu apt仓库下载。Ubuntu官方自己维护了每个操作系统版本的背后的Linux内核代码,可以通过以下两种apt命令方式获取相关代码:

# 第一种方式
# 先搜索
> apt-cache search linux-source
    linux-source - Linux kernel source with Ubuntu patches
    linux-source-4.15.0 - Linux kernel source for version 4.15.0 with Ubuntu patches
    linux-source-4.18.0 - Linux kernel source for version 4.18.0 with Ubuntu patches
    linux-source-5.0.0 - Linux kernel source for version 5.0.0 with Ubuntu patches
    linux-source-5.3.0 - Linux kernel source for version 5.3.0 with Ubuntu patches
# 再安装
> apt install linux-source-4.15.0
    
# 第二种方式
> apt-get source linux
    Reading package lists... Done
    NOTICE: 'linux' packaging is maintained in the 'Git' version control system at:
    git://git.launchpad.net/~ubuntu-kernel/ubuntu/+source/linux/+git/bionic
    Please use:
    git clone git://git.launchpad.net/~ubuntu-kernel/ubuntu/+source/linux/+git/bionic
    to retrieve the latest (possibly unreleased) updates to the package.
    Need to get 167 MB of source archives.
    Get:2 https://mirrors.ustc.edu.cn/ubuntu bionic-updates/main linux 4.15.0-99.100 (tar) [158 MB]
    ...
    
# 以上两种方式,内核源代码均下载至/usr/src/目录下

1.2.安装依赖项

apt install libncurses5-dev flex bison libelf-dev binutils-dev libssl-dev

1.3.安装Clang和LLVM

然后使用以下两条命令分别安装 clang 和 llvm

apt install clang
apt install llvm

1.4.配置内核

在源码根目录下使用make defconfig生成.config<c/ode>文件

1.5.解决modpost: not found错误

因为直接make M=samples/bpf时,会报错缺少modules的错误。修复modpost的错误,以下两种解决方案二选一

make modules_prepare

make script

1.6.关联内核头文件

make headers_install

1.7.编译内核程序样例

在源码根目录下执行make M=samples/bpf,

此时进入linux-source-4.15.0/smaples/bpf中,会看到生成了BPF字节码文件*_kern.o和用户态的可执行文件

 

你可以运行几个试试,例如sockex1

2. 使用BPF C编写hello world程序

2.1.先了解一下原理吧

BPF程序经过Clang/LLVM编译成BPF字节码,然后通过BPF系统调用的方式加载进内核,然后交给BPF虚拟机来执行,也是JIT的方式动态转成机器码

内核有很多hook点,我们在写BPF程序时也会做事件源配置。当hook点上的事件发生时,就会执行我们的BPF程序。

我们还可以在BPF程序中创建一个Map,把我们想拿到的数据保存在Map中,然后用户态程序就可以拿到。

总之,就是我们可以通过BPF程序拿到内核的一些数据

2.2.hello world程序

进入samples/bpf目录,可以利用自带的Makefile编译,

编写hello_kern.c:

#include <linux/bpf.h>
#include "bpf_helpers.h"
#define SEC(NAME) __attribute__((section(NAME), used))

SEC("tracepoint/syscalls/sys_enter_execve")
int bpf_prog(void *ctx){
        char msg[] = "Hello World\n";
        bpf_trace_printk(msg, sizeof(msg));
        return 0;
}

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

这个程序的作用就是当发生系统调用(sys_enter_execve)时在终端输出"Hello World",其实bpf_trace_printk只是将msg写到一个管道文件中

编写hello_user.c:

#include <stdio.h>
#include "bpf_load.h"

int main(int argc, char **argv){
        if(load_bpf_file("hello_kern.o")!=0){
                printf("The kernel didn't load BPF program\n");
                return -1;
        }

        read_trace_pipe();
        return 0;
}

这个程序的作用是将包含BPF的文件hello_kern.o通过系统调用的方式加载进内核,read_trace_pipe()读取管道文件并打印到终端

2.3.修改Makefile

模仿原有的,有四处需要修改:

# List of programs to build
hostprogs-y += hello

# Libbpf dependencies
hello-objs := bpf_load.o $(LIBBPF) hello_user.o

# Tell kbuild to always build the programs
always += hello_kern.o

HOSTLOADLIBES_hello += -lelf

2.4.编译

可以返回源码根目录用 make M=samples/bpfmake samples/bpf/ 编译

或者直接在当前目录(samples/bpf) 执行make 编译

可以查看编译后的结果,生成了hello可执行文件

2.5.运行

3.进一步

进一步学习BPF程序是如何转换成字节码的

3.1.BPF程序中的节(section)

SEC宏会将宏里面的内容(kprobe/sys_write)作为节的名字放到elf文件中,也就是目标文件,可以用readelf工具查看

还用宏生成了一个名字为license的section

3.2.BPF程序中的字节码(bytecode)

可以用objdump工具查看

可见是将我们的bpf程序编译到elf文件的某个节中,右边黄框内就是常说的bpf字节码,对应左边灰色内容

接下来讲一下,bpf程序是如何转成字节码的

3.3.BPF内核辅助函数调用转换为BPF字节码的过程

我们用到的BPF内核辅助函数是bpf_trace_printk

bpg_prog是我们的elf函数名字,分析下call 6是怎么得到的?

 

BPF_FUNC_map_lookup_elem(BPF_FUNC_trace_printk类似)是在bpf.h中定义的,只不过是宏的形式,我们将其展开:

可见BPF_FUNC_trace_printk的相对位置是6,

一般BPF内核辅助函数转汇编是这样的:

就是BPF_call id,id就是bpf_func_id中的id;

进一步就是BPF_EMIT_CALL(func name)

例如,在内核中的某一处代码,调用bpf_map_lookup_elem,在BPF指令集编程中,就是使用BPF_EMIT_CALL来调用的

不难想象,我们调用bpf_trace_printk也是采用同样的调用方式

BPF_EMIT_CALL(func name)是如何转化成字节码的呢?

_bpf_call_base啥也没做,直接返回0,可见只是需要其地址,而差值就是在enum中的位置

进一步分析

 

所以,call 6对应的字节码就是85 00 00 00 06 00 00 00

我们还可以进一步查看JIT前后字节码的变化:

首先执行objdump -s hello_kern.o 得到JIT之前的字节码:

在一直运行hello 

进入linux-source-4.15.0/tools/bpf/bpftool目录,make,生成bpftool工具,

通过 ./bpftool prog show 显示加载了哪些BPF程序:

可见我们的hello程序对应的id为86,钩子类型为tracepoint

再使用./bpftool prog dump xlated id 86 opcodes 即可查看JIT之后的字节码:

 

对比起来看:

其他的没变,可以看到这个变化,这是因为JIT前call使用的id,JIT后成了调用函数到这个指令的距离

3.4.BPF程序到BPF字节码的编译过程:Clang与LLVM

 

LLVM支持很多后端,通过命令llc -version

bpf target有三种,不指定就根据系统的大小端法

有两种方式编译BPF程序:

gcc缺少BPF backend,幸运的是clang支持BPF. 之前的Makefile就是使用clang将hello_kern.c编译成hello_kern.o

右边的图表示一步到位和分布编译的结果是一样的,而且是之前用Makefile编译的也一样

分步编译是生成中间IR文件,默认是.ll格式

 

 

除视频外还参考了:

1.  https://blog.csdn.net/qq_34258344/article/details/108932912

2. https://cloud.tencent.com/developer/article/1644458

特别是修改Makefile的地方,网上的都只改了三个地方,CSDN杀我¥#……

有个小问题是SEC("kprobe/sys_write")这个hook用不了,我只能换一个试了.

posted @ 2021-10-13 16:59  Rogn  阅读(3157)  评论(0编辑  收藏  举报