掌控内核 - ebpf整体概念和入门

这篇其实算是ebpf0x0

什么是ebpf

ebpf由bpf演变而来,其最初的作用是数据链路层的报文过滤框架,之后随着bpf的功能不断拓展有了ebpf框架,除了报文过滤外还添加了追踪/拦截系统调用的功能
根据docs.ebpf.io中的介绍,ebpf主要有以下几个核心内容

Programs

ebpf程序,ebpf本质上是一个c语言编写,llvm(通常来说是这样)编译的.o二进制文件,使用linux的loader library(libbpf)动态加载到内核中,从而实现用户态和内核态的交互以及监视内核行为
与通常的c语言程序不同,为了确保内核安全,loader会对程序做检查,禁用循环等可能产生危险的行为(你也不想你内核里有个程序卡死吧)

Helper functions

一些内置的函数,用于在ebpf程序中执行一些高级操作

KFuncs

作用和Helper functions相同,都是提供一些高级操作,因为Helper functions属于UAPI,为了稳定性考虑Helper functions实际上是不更新的,所以KFuncs负责拓展功能,但与之相对的KFuncs在不同linux版本之间不保证api兼容性

Maps

分为array_map和hash_map(任意键值对),实际上就是ebpf中管理内存的方式(malloc是危险的),但是Maps提供的功能远比管理内存强大,Maps是内核共享的,所以能实现bpf程序之间,bpf程序和用户态程序之间的数据交互

Objects

ebpf程序和maps都是对象,由ebpf loader创建,并且每个对象都会获得一个fd作为它的唯一标识符,对象的生命周期采用一种智能的管理方法,会在所有指向其的引用消失后自动回收,fd是内核共享的,用户态程序可以通过这些fd共享ebpf程序的信息,fd还可以通过pining储存在bpf file system中,pining的对象会由bpf file system维持其引用,也就是不会被回收

使用环境

ebpf作为一个比较新的框架功能迭代非常快,如果条件允许可以尽量使用新的内核版本,一般来说建议使用5.10以上的版本,然后发行版镜像一般不会开启完整的ebpf功能,如果有条件可以自行编译内核开启完整功能(比如syscall 和 ftrace),不过依靠kprobe已经能实现不少功能,这个系列文章基于的设备是内核序列号为oriole-bp2a.250605.031.a2的pixel 6,使用官方出厂镜像,只使用KernelSU添加了root,无其他修改

考虑到bcc本身搭建环境比较复杂,而且在编写内核态程序时使用的是类c的语法,所以本文章不使用bcc,而是采用在linux虚拟机(桌面平台)中交叉编译后直接push可执行文件到手机的方案,采用的加载器是libbpf,这样所有编码都采用c/c++实现(我不会写go所以用不了cilium)

交叉编译

关于交叉编译,如果想简单体验的话可以直接用这个项目开始libbpf-bootstrap-android,这是libbpf-bootstrap的安卓迁移版本,修改了makefile配置使其支持交叉编译,同时也准备了zlib.a和libelf.a两个静态链接库,除了要自己下载ndk工具链外基本不需要准备任何依赖

如果想研究自行交叉编译,可以参考下面的脚本,这应该是ebpf最简单的交叉编译方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
aarch64-linux-android35-clang \  # 使用ndk-clang编译内核态.o文件
-I./libbpf_include \ # 给编译器添加libbpf相关的头文件搜索路径
-I./zlib_include \ # 添加zlib相关头文件搜索路径
-I./libelf_include \ # 添加libelf相关头文件搜索路径
-I/home/sgsg/android-ndk-r27c/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include/aarch64-linux-android \ # 添加arm64 linux c头文件搜索路径,不然的话编译器会去/usr下找x86的头文件,然后编译就挂了
-g -O2 -Wall -target bpf -D__TARGET_ARCH_aarch64 \ # 指定目标格式为bpf,指定架构为arm64
-c bpf_test.bpf.c \ # 添加源代码文件
-o bpf_test.bpf.o -static # 静态链接,避免依赖问题

bpftool gen skeleton bpf_test.bpf.o > bpf_test.skel.h # libbpf建议使用skeleton加载,具体就是在内核态elf生成完毕后,使用bpftool生成.h文件,这个.h文件里包含了内核态文件完整的字节码和对应的加载函数,直接在用户态中引入这个头文件调用预生成的函数就可以加载程序到内核了

aarch64-linux-gnu-gcc test_bpf_loader.c \ # 添加源代码文件
-I./libbpf_include \ # 同样添加头文件路径
-I./zlib_include \
-I./libelf_include \
-I/home/sgsg/android-ndk-r27c/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include/aarch64-linux-android \
./libs/libbpf.a \ # 静态链接依赖
./libs/libz.a \
./libs/libelf.a \
-o loader -static # 静态编译,可以省去很多跨平台兼容性问题,就是产物会稍微大一点

项目文件结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
── bpf_test.bpf.c
├── bpf_test.bpf.o
├── bpf_test.skel.h
├── build.sh
├── libbpf_include
│   └── bpf
│   ├── bpf_core_read.h
│   ├── bpf_endian.h
│   ├── bpf.h
│   ├── bpf_helper_defs.h
│   ├── bpf_helpers.h
│   ├── bpf_tracing.h
│   ├── btf.h
│   ├── hashmap.h
│   ├── libbpf_common.h
│   ├── libbpf.h
│   ├── libbpf_internal.h
│   ├── libbpf_legacy.h
│   ├── libbpf_version.h
│   ├── relo_core.h
│   ├── skel_internal.h
│   └── usdt.bpf.h
├── libelf_include
│   ├── elf-knowledge.h
│   ├── gelf.h
│   ├── libelf.h
│   ├── nlist.h
│   └── version.h
├── libs
│   ├── libbpf.a
│   ├── libelf.a
│   └── libz.a
├── test_bpf_loader.c
├── vmlinux.h
└── zlib_include
├── blast.h
├── crc32.h
├── crypt.h
├── deflate.h
├── gzguts.h
├── gzlog.h
├── infback9.h
├── inffast.h
├── inffix9.h
├── inffixed.h
├── inflate9.h
├── inflate.h
├── inftree9.h
├── inftrees.h
├── ioapi.h
├── iowin32.h
├── mztools.h
├── puff.h
├── trees.h
├── unzip.h
├── zconf.h
├── zconf.h.cmakein
├── zconf.h.in
├── zfstream.h
├── zip.h
├── zlib.h
├── zlib_how.html
├── zstream.h
└── zutil.h

其中这些依赖和头文件可以去libbpf-bootstrap-android中获取,libbpf.a需要运行一次make后在example/c/libbpf中获取,vmlinux.h需要使用bpftool在手机上运行bpftool btf dump file /sys/kernel/btf/vmlinux format c,如果你的手机没有该文件说明不支持bpf头文件(稍微新一点的基本都支持),那就要使用libbpf-bootstrap-android中提供的头文件
想要查看手机支持ebpf的哪些特性可以通过运行bpftool feature查看

demo

1
2
3
4
5
6
7
8
9
10
11
12
// bpf_test.bpf.c
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>

SEC("kprobe/kernel_clone")
int bpf_prog(void *ctx)
{
bpf_printk("Hello from eBPF with Skeleton!");
return 0;
}

char _license[] SEC("license") = "GPL";
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// bpf_test_loader.c
#include <stdio.h>
#include <stdlib.h>
#include "bpf_test.skel.h"

int main(int argc, char **argv)
{
struct bpf_test_bpf *skel;
int err;

skel = bpf_test_bpf__open_and_load();
bpf_test_bpf__attach(skel);

printf("BPF skeleton loaded and attached successfully. Press enter to exit...\n");
getchar();

bpf_test_bpf__destroy(skel);
return 0;
}

这是一个简单的demo,用来跟踪clone命令内置的kprobe挂载点,只要clone命令触发(任何创建新进程的行为都需要调用clone)就会在sys/kernel/tracing/trace_pipe这个文件(比较新的内核都是这个路径,老一点的是在debug路径下的,如果没有的话手动挂载一下tracingFS和debugFS)中输出Hello from eBPF with Skeleton!
在使用前需要在/sys/kernel/tracing/tracing_on中输入1开启跟踪echo 1 > tracing_on
然后运行loader程序就可以看跟踪结果了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
130|oriole:/sys/kernel/tracing # echo 1 > tracing_on
oriole:/sys/kernel/tracing # cat trace_pipe
Lite Thread #2-10604 [003] d..3 36892.084632: bpf_trace_printk: Hello from eBPF with Skeleton!
Lite Thread #1-10088 [005] d..3 36892.087729: bpf_trace_printk: Hello from eBPF with Skeleton!
lowpool[756]-1210 [000] d..3 36892.151596: bpf_trace_printk: Hello from eBPF with Skeleton!
servicemanager-480 [003] d..3 36892.805585: bpf_trace_printk: Hello from eBPF with Skeleton!
roid.apps.turbo-6207 [003] d..3 36892.929320: bpf_trace_printk: Hello from eBPF with Skeleton!
BG Thread #0-6234 [000] d..3 36892.942382: bpf_trace_printk: Hello from eBPF with Skeleton!
BG Thread #0-6234 [000] d..3 36892.944007: bpf_trace_printk: Hello from eBPF with Skeleton!
servicemanager-480 [001] d..3 36893.813645: bpf_trace_printk: Hello from eBPF with Skeleton!
servicemanager-480 [001] d..3 36894.820207: bpf_trace_printk: Hello from eBPF with Skeleton!
servicemanager-480 [002] d..3 36895.824630: bpf_trace_printk: Hello from eBPF with Skeleton!
servicemanager-480 [002] d..3 36896.828603: bpf_trace_printk: Hello from eBPF with Skeleton!
POSIX timer 3-2540 [003] d..3 36897.530348: bpf_trace_printk: Hello from eBPF with Skeleton!
servicemanager-480 [003] d..3 36897.832404: bpf_trace_printk: Hello from eBPF with Skeleton!
POSIX timer 3-2540 [003] d..3 36898.688762: bpf_trace_printk: Hello from eBPF with Skeleton!
servicemanager-480 [002] d..3 36898.840582: bpf_trace_printk: Hello from eBPF with Skeleton!
servicemanager-480 [002] d..3 36899.845768: bpf_trace_printk: Hello from eBPF with Skeleton!
POSIX timer 3-2540 [000] d..3 36900.606367: bpf_trace_printk: Hello from eBPF with Skeleton!
servicemanager-480 [000] d..3 36900.850164: bpf_trace_printk: Hello from eBPF with Skeleton!
servicemanager-480 [002] d..3 36901.857614: bpf_trace_printk: Hello from eBPF with Skeleton!
POSIX timer 3-2540 [001] d..3 36902.128525: bpf_trace_printk: Hello from eBPF with Skeleton!
servicemanager-480 [001] d..3 36902.862111: bpf_trace_printk: Hello from eBPF with Skeleton!
servicemanager-480 [001] d..3 36903.866426: bpf_trace_printk: Hello from eBPF with Skeleton!
servicemanager-480 [003] d..3 36904.872046: bpf_trace_printk: Hello from eBPF with Skeleton!
servicemanager-480 [003] d..3 36905.876475: bpf_trace_printk: Hello from eBPF with Skeleton!
POSIX timer 3-2540 [000] d..3 36906.276747: bpf_trace_printk: Hello from eBPF with Skeleton!
kthreadd-2 [004] d..3 36906.461608: bpf_trace_printk: Hello from eBPF with Skeleton!

可以看到系统服务创建新进程的行为都被捕捉到了

掌控内核 - ebpf整体概念和入门

https://sgsgsama.github.io/ctf/ebpf/ebpf0x1/

Author

SGSG

Posted on

2025-05-18

Updated on

2025-07-16

Licensed under