使用 eBPF 替换任意程序读取或写入的文本
-完整源代码:https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/27-replace
+完整源代码:https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/27-replace
关于如何安装依赖,请参考:https://eunomia.dev/tutorials/11-bootstrap/
编译:
make
diff --git a/28-detach/index.html b/28-detach/index.html
index 4babce4..df5b231 100644
--- a/28-detach/index.html
+++ b/28-detach/index.html
@@ -175,8 +175,8 @@
在应用程序退出后运行 eBPF 程序:eBPF 程序的生命周期
eBPF(Extended Berkeley Packet Filter)是 Linux 内核中的一项重大技术创新,允许用户在内核空间中执行自定义程序,而无需修改内核源代码或加载任何内核模块。这为开发人员提供了极大的灵活性,可以观察、修改和控制 Linux 系统。
-本文将介绍 eBPF 程序的生命周期,以及如何在用户空间应用程序退出后继续运行 eBPF 程序的方法,还将介绍如何使用 "pin" 在不同进程之间共享 eBPF 对象。本文是 eBPF 开发者教程的一部分,更多详细信息可以在 https://github.com/eunomia-bpf/bpf-developer-tutorial 和 https://eunomia.dev/tutorials 中找到。
-通过使用 "detach" 方法来运行 eBPF 程序,用户空间加载程序可以在不停止 eBPF 程序的情况下退出。另外,使用 "pin" 的方法可以在进程之间共享 eBPF 对象,使其保持活动状态。
+本文将介绍 eBPF 程序的生命周期,以及如何在用户空间应用程序退出后继续运行 eBPF 程序的方法,还将介绍如何使用 "pin" 在不同进程之间共享 eBPF 对象。本文是 eBPF 开发者教程的一部分,更多详细信息可以在 https://github.com/eunomia-bpf/bpf-developer-tutorial 和 https://eunomia.dev/tutorials 中找到。
+通过使用 "detach" 方法来运行 eBPF 程序,用户空间加载程序可以在不停止 eBPF 程序的情况下退出。另外,使用 "pin" 的方法可以在进程之间共享 eBPF 对象,使其保持活动状态。
eBPF 程序的生命周期
BPF对象(包括程序、映射和调试信息)通过文件描述符(FD)进行访问,并具有引用计数器。每个对象都有一个引用计数器,用于追踪对象被引用的次数。例如,当创建一个映射时,内核会分配一个struct bpf_map对象,并将其引用计数器初始化为1。然后,将映射的文件描述符返回给用户空间进程。如果进程退出或崩溃,文件描述符将被关闭,并且映射的引用计数将减少。当引用计数为零时,内存将被释放。
BPF程序使用 maps 有两个阶段。首先,创建 maps 并将其文件描述符存储为BPF_LD_IMM64指令的一部分。当内核验证程序时,它会增加程序使用的 maps 的引用计数,并将程序的引用计数初始化为1。此时,用户空间可以关闭与maps 相关的文件描述符,但 maps 不会被销毁,因为程序仍然在使用它们。当程序文件描述符关闭且引用计数为零时,销毁逻辑将减少 maps 的引用计数。这允许多个不同类型的程序同时使用同一个 maps。
@@ -199,7 +199,7 @@
int err;
err = bpf_program__pin(prog, path);
if (err) {
- fprintf(stdout, "could not pin prog %s: %d\n", path, err);
+ fprintf(stdout, "could not pin prog %s: %d\n", path, err);
return err;
}
return err;
@@ -210,7 +210,7 @@ int pin_map(struct bpf_map *map, const char* path)
int err;
err = bpf_map__pin(map, path);
if (err) {
- fprintf(stdout, "could not pin map %s: %d\n", path, err);
+ fprintf(stdout, "could not pin map %s: %d\n", path, err);
return err;
}
return err;
@@ -221,7 +221,7 @@ int pin_link(struct bpf_link *link, const char* path)
int err;
err = bpf_link__pin(link, path);
if (err) {
- fprintf(stdout, "could not pin link %s: %d\n", path, err);
+ fprintf(stdout, "could not pin link %s: %d\n", path, err);
return err;
}
return err;
diff --git a/29-sockops/index.html b/29-sockops/index.html
index a72dec9..62a575f 100644
--- a/29-sockops/index.html
+++ b/29-sockops/index.html
@@ -186,7 +186,7 @@
示例程序
此示例程序从发送者的套接字(出口)重定向流量至接收者的套接字(入口),跳过 TCP/IP 内核网络栈。在这个示例中,我们假定发送者和接收者都在同一台机器上运行。这个示例程序有两个部分,它们共享一个 map 定义:
bpf_sockmap.h
-#include "vmlinux.h"
+#include "vmlinux.h"
#include <bpf/bpf_endian.h>
#include <bpf/bpf_helpers.h>
@@ -205,7 +205,7 @@ struct {
__uint(max_entries, 65535);
__type(key, struct sock_key);
__type(value, int);
-} sock_ops_map SEC(".maps");
+} sock_ops_map SEC(".maps");
这个示例程序中的 BPF 程序被分为两个部分 bpf_redirect.bpf.c 和 bpf_contrack.bpf.c。
@@ -227,11 +227,11 @@ struct {
这个示例程序就是通过 BPF 实现了在本地通信时,快速将消息从发送者的套接字重定向到接收者的套接字,从而绕过了内核网络栈,以提高传输效率。
bpf_redirect.bpf.c
-
#include "bpf_sockmap.h"
+#include "bpf_sockmap.h"
-char LICENSE[] SEC("license") = "Dual BSD/GPL";
+char LICENSE[] SEC("license") = "Dual BSD/GPL";
-SEC("sk_msg")
+SEC("sk_msg")
int bpf_redir(struct sk_msg_md *msg)
{
if(msg->remote_ip4 != LOCALHOST_IPV4 || msg->local_ip4!= LOCALHOST_IPV4)
@@ -248,11 +248,11 @@ int bpf_redir(struct sk_msg_md *msg)
}
bpf_contrack.bpf.c
-#include "bpf_sockmap.h"
+#include "bpf_sockmap.h"
-char LICENSE[] SEC("license") = "Dual BSD/GPL";
+char LICENSE[] SEC("license") = "Dual BSD/GPL";
-SEC("sockops")
+SEC("sockops")
int bpf_sockops_handler(struct bpf_sock_ops *skops){
u32 family, op;
@@ -275,7 +275,7 @@ int bpf_sockops_handler(struct bpf_sock_ops *skops){
.family = skops->family,
};
- bpf_printk(">>> new connection: OP:%d, PORT:%d --> %d\n", op, bpf_ntohl(key.sport), bpf_ntohl(key.dport));
+ bpf_printk(">>> new connection: OP:%d, PORT:%d --> %d\n", op, bpf_ntohl(key.sport), bpf_ntohl(key.dport));
bpf_sock_hash_update(skops, &sock_ops_map, &key, BPF_NOEXIST);
return BPF_OK;
@@ -298,28 +298,28 @@ set -e
sudo mount -t bpf bpf /sys/fs/bpf/
# check if old program already loaded
-if [ -e "/sys/fs/bpf/bpf_sockops" ]; then
- echo ">>> bpf_sockops already loaded, uninstalling..."
+if [ -e "/sys/fs/bpf/bpf_sockops" ]; then
+ echo ">>> bpf_sockops already loaded, uninstalling..."
./unload.sh
- echo ">>> old program already deleted..."
+ echo ">>> old program already deleted..."
fi
# load and attach sock_ops program
sudo bpftool prog load bpf_contrack.bpf.o /sys/fs/bpf/bpf_sockops type sockops pinmaps /sys/fs/bpf/
-sudo bpftool cgroup attach "/sys/fs/cgroup/" sock_ops pinned "/sys/fs/bpf/bpf_sockops"
+sudo bpftool cgroup attach "/sys/fs/cgroup/" sock_ops pinned "/sys/fs/bpf/bpf_sockops"
# load and attach sk_msg program
-sudo bpftool prog load bpf_redirect.bpf.o "/sys/fs/bpf/bpf_redir" map name sock_ops_map pinned "/sys/fs/bpf/sock_ops_map"
+sudo bpftool prog load bpf_redirect.bpf.o "/sys/fs/bpf/bpf_redir" map name sock_ops_map pinned "/sys/fs/bpf/sock_ops_map"
sudo bpftool prog attach pinned /sys/fs/bpf/bpf_redir msg_verdict pinned /sys/fs/bpf/sock_ops_map
这是一个 BPF 的加载脚本。它的主要功能是加载和附加 BPF 程序到内核系统中,并将关联的 BPF map 一并存储(pin)到 BPF 文件系统中,以便 BPF 程序能访问和操作这些 map。
让我们详细地看一下脚本的每一行是做什么的。
sudo mount -t bpf bpf /sys/fs/bpf/ 这一行用于挂载 BPF 文件系统,使得 BPF 程序和相关的 map 可以被系统访问和操作。
-- 判断条件
[ -e "/sys/fs/bpf/bpf_sockops" ] 是检查是否已经存在 /sys/fs/bpf/bpf_sockops 文件,如果存在,则说明 bpf_sockops 程序已经被加载到系统中,那么将会通过 ./unload.sh 脚本将其卸载。
+- 判断条件
[ -e "/sys/fs/bpf/bpf_sockops" ] 是检查是否已经存在 /sys/fs/bpf/bpf_sockops 文件,如果存在,则说明 bpf_sockops 程序已经被加载到系统中,那么将会通过 ./unload.sh 脚本将其卸载。
sudo bpftool prog load bpf_contrack.bpf.o /sys/fs/bpf/bpf_sockops type sockops pinmaps /sys/fs/bpf/ 这一行是加载上文中 bpf_contrack.bpf.c 编译得到的 BPF 对象文件 bpf_contrack.bpf.o 到 BPF 文件系统中,存储至 /sys/fs/bpf/bpf_sockops,并且指定它的类型为 sockops。pinmaps /sys/fs/bpf/ 是指定将加载的 BPF 程序相关的 map 存储在 /sys/fs/bpf/ 下。
-sudo bpftool cgroup attach "/sys/fs/cgroup/" sock_ops pinned "/sys/fs/bpf/bpf_sockops" 这一行是将已经加载到 BPF 文件系统的 bpf_sockops 程序附加到 cgroup(此路径为"/sys/fs/cgroup/")。附加后,所有属于这个 cgroup 的套接字操作都会受到 bpf_sockops 的影响。
-sudo bpftool prog load bpf_redirect.bpf.o "/sys/fs/bpf/bpf_redir" map name sock_ops_map pinned "/sys/fs/bpf/sock_ops_map" 这一行是加载 bpf_redirect.bpf.c 编译得到的 BPF 对象文件 bpf_redirect.bpf.o 到 BPF 文件系统中,存储至 /sys/fs/bpf/bpf_redir ,并且指定它的相关 map为 sock_ops_map,这个map在 /sys/fs/bpf/sock_ops_map 中。
+sudo bpftool cgroup attach "/sys/fs/cgroup/" sock_ops pinned "/sys/fs/bpf/bpf_sockops" 这一行是将已经加载到 BPF 文件系统的 bpf_sockops 程序附加到 cgroup(此路径为"/sys/fs/cgroup/")。附加后,所有属于这个 cgroup 的套接字操作都会受到 bpf_sockops 的影响。
+sudo bpftool prog load bpf_redirect.bpf.o "/sys/fs/bpf/bpf_redir" map name sock_ops_map pinned "/sys/fs/bpf/sock_ops_map" 这一行是加载 bpf_redirect.bpf.c 编译得到的 BPF 对象文件 bpf_redirect.bpf.o 到 BPF 文件系统中,存储至 /sys/fs/bpf/bpf_redir ,并且指定它的相关 map为 sock_ops_map,这个map在 /sys/fs/bpf/sock_ops_map 中。
sudo bpftool prog attach pinned /sys/fs/bpf/bpf_redir msg_verdict pinned /sys/fs/bpf/sock_ops_map 这一行是将已经加载的 bpf_redir 附加到 sock_ops_map 上,附加方式为 msg_verdict,表示当该 map 对应的套接字收到消息时,将会调用 bpf_redir 程序处理。
综上,此脚本的主要作用就是将两个用于处理本地套接字流量的 BPF 程序分别加载到系统并附加到正确的位置,以便它们能被正确地调用,并且确保它们可以访问和操作相关的 BPF map。
diff --git a/3-fentry-unlink/index.html b/3-fentry-unlink/index.html
index 73a04c1..3438b50 100644
--- a/3-fentry-unlink/index.html
+++ b/3-fentry-unlink/index.html
@@ -179,29 +179,29 @@
Fentry
fentry(function entry)和 fexit(function exit)是 eBPF(扩展的伯克利包过滤器)中的两种探针类型,用于在 Linux 内核函数的入口和退出处进行跟踪。它们允许开发者在内核函数执行的特定阶段收集信息、修改参数或观察返回值。这种跟踪和监控功能在性能分析、故障排查和安全分析等场景中非常有用。
与 kprobes 相比,fentry 和 fexit 程序有更高的性能和可用性。在这个例子中,我们可以直接访问函数的指针参数,就像在普通的 C 代码中一样,而不需要使用各种读取帮助程序。fexit 和 kretprobe 程序最大的区别在于,fexit 程序可以访问函数的输入参数和返回值,而 kretprobe 只能访问返回值。从 5.5 内核开始,fentry 和 fexit 对 eBPF 程序可用。
-#include "vmlinux.h"
+#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
-char LICENSE[] SEC("license") = "Dual BSD/GPL";
+char LICENSE[] SEC("license") = "Dual BSD/GPL";
-SEC("fentry/do_unlinkat")
+SEC("fentry/do_unlinkat")
int BPF_PROG(do_unlinkat, int dfd, struct filename *name)
{
pid_t pid;
pid = bpf_get_current_pid_tgid() >> 32;
- bpf_printk("fentry: pid = %d, filename = %s\n", pid, name->name);
+ bpf_printk("fentry: pid = %d, filename = %s\n", pid, name->name);
return 0;
}
-SEC("fexit/do_unlinkat")
+SEC("fexit/do_unlinkat")
int BPF_PROG(do_unlinkat_exit, int dfd, struct filename *name, long ret)
{
pid_t pid;
pid = bpf_get_current_pid_tgid() >> 32;
- bpf_printk("fexit: pid = %d, filename = %s, ret = %ld\n", pid, name->name, ret);
+ bpf_printk("fexit: pid = %d, filename = %s, ret = %ld\n", pid, name->name, ret);
return 0;
}
diff --git a/30-sslsniff/index.html b/30-sslsniff/index.html
index 10aa8a1..f35b2fa 100644
--- a/30-sslsniff/index.html
+++ b/30-sslsniff/index.html
@@ -321,12 +321,12 @@ struct probe_SSL_data_t {
- 最后,将数据发送到用户空间。
注意:我们使用了两个用户返回探针 uretprobe 来分别 hook SSL_read 和 SSL_write 的返回:
-SEC("uretprobe/SSL_read")
+SEC("uretprobe/SSL_read")
int BPF_URETPROBE(probe_SSL_read_exit) {
return (SSL_exit(ctx, 0)); // 0 表示读操作
}
-SEC("uretprobe/SSL_write")
+SEC("uretprobe/SSL_write")
int BPF_URETPROBE(probe_SSL_write_exit) {
return (SSL_exit(ctx, 1)); // 1 表示写操作
}
@@ -336,7 +336,7 @@ int BPF_URETPROBE(probe_SSL_write_exit) {
进入握手
我们使用 uprobe 为 do_handshake 设置一个 probe:
-SEC("uprobe/do_handshake")
+SEC("uprobe/do_handshake")
int BPF_UPROBE(probe_SSL_do_handshake_enter, void *ssl) {
u64 pid_tgid = bpf_get_current_pid_tgid();
u32 pid = pid_tgid >> 32;
@@ -362,7 +362,7 @@ int BPF_UPROBE(probe_SSL_do_handshake_enter, void *ssl) {
退出握手
同样,我们为 do_handshake 的返回设置了一个 uretprobe:
-SEC("uretprobe/do_handshake")
+SEC("uretprobe/do_handshake")
int BPF_URETPROBE(probe_SSL_do_handshake_exit) {
u32 zero = 0;
u64 pid_tgid = bpf_get_current_pid_tgid();
@@ -427,18 +427,18 @@ int BPF_URETPROBE(probe_SSL_do_handshake_exit) {
上述代码片段中,根据环境变量 env 的设定,程序可以选择针对三种常见的加密库(OpenSSL、GnuTLS 和 NSS)进行挂载。这意味着我们可以在同一个工具中对多种库的调用进行追踪。
为了实现这一功能,首先利用 find_library_path 函数确定库的路径。然后,根据库的类型,调用对应的 attach_ 函数来将 eBPF 程序挂载到库函数上。
if (env.openssl) {
- char *openssl_path = find_library_path("libssl.so");
- printf("OpenSSL path: %s\n", openssl_path);
- attach_openssl(obj, "/lib/x86_64-linux-gnu/libssl.so.3");
+ char *openssl_path = find_library_path("libssl.so");
+ printf("OpenSSL path: %s\n", openssl_path);
+ attach_openssl(obj, "/lib/x86_64-linux-gnu/libssl.so.3");
}
if (env.gnutls) {
- char *gnutls_path = find_library_path("libgnutls.so");
- printf("GnuTLS path: %s\n", gnutls_path);
+ char *gnutls_path = find_library_path("libgnutls.so");
+ printf("GnuTLS path: %s\n", gnutls_path);
attach_gnutls(obj, gnutls_path);
}
if (env.nss) {
- char *nss_path = find_library_path("libnspr4.so");
- printf("NSS path: %s\n", nss_path);
+ char *nss_path = find_library_path("libnspr4.so");
+ printf("NSS path: %s\n", nss_path);
attach_nss(obj, nss_path);
}
@@ -498,7 +498,7 @@ int attach_nss(struct sslsniff_bpf *skel, const char *lib) {
while (!exiting) {
err = perf_buffer__poll(pb, PERF_POLL_TIMEOUT_MS);
if (err < 0 && err != -EINTR) {
- warn("error polling perf buffer: %s\n", strerror(-err));
+ warn("error polling perf buffer: %s\n", strerror(-err));
goto cleanup;
}
err = 0;
@@ -514,13 +514,13 @@ void print_event(struct probe_SSL_data_t *event, const char *evt) {
char hex_data[MAX_BUF_SIZE * 2 + 1] = {0};
buf_to_hex((uint8_t *)buf, buf_size, hex_data);
- printf("\n%s\n", s_mark);
+ printf("\n%s\n", s_mark);
for (size_t i = 0; i < strlen(hex_data); i += 32) {
- printf("%.32s\n", hex_data + i);
+ printf("%.32s\n", hex_data + i);
}
- printf("%s\n\n", e_mark);
+ printf("%s\n\n", e_mark);
} else {
- printf("\n%s\n%s\n%s\n\n", s_mark, buf, e_mark);
+ printf("\n%s\n%s\n%s\n\n", s_mark, buf, e_mark);
}
}
}
diff --git a/31-goroutine/Makefile b/31-goroutine/Makefile
deleted file mode 100644
index 93aef19..0000000
--- a/31-goroutine/Makefile
+++ /dev/null
@@ -1,141 +0,0 @@
-# SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
-OUTPUT := .output
-CLANG ?= clang
-LIBBPF_SRC := $(abspath ../third_party/libbpf/src)
-BPFTOOL_SRC := $(abspath ../third_party/bpftool/src)
-LIBBPF_OBJ := $(abspath $(OUTPUT)/libbpf.a)
-BPFTOOL_OUTPUT ?= $(abspath $(OUTPUT)/bpftool)
-BPFTOOL ?= $(BPFTOOL_OUTPUT)/goroutine/bpftool
-LIBBLAZESYM_SRC := $(abspath ../third_party/blazesym/)
-LIBBLAZESYM_OBJ := $(abspath $(OUTPUT)/libblazesym.a)
-LIBBLAZESYM_HEADER := $(abspath $(OUTPUT)/blazesym.h)
-ARCH ?= $(shell uname -m | sed 's/x86_64/x86/' \
- | sed 's/arm.*/arm/' \
- | sed 's/aarch64/arm64/' \
- | sed 's/ppc64le/powerpc/' \
- | sed 's/mips.*/mips/' \
- | sed 's/riscv64/riscv/' \
- | sed 's/loongarch64/loongarch/')
-VMLINUX := ../third_party/vmlinux/$(ARCH)/vmlinux.h
-# Use our own libbpf API headers and Linux UAPI headers distributed with
-# libbpf to avoid dependency on system-wide headers, which could be missing or
-# outdated
-INCLUDES := -I$(OUTPUT) -I../third_party/libbpf/include/uapi -I$(dir $(VMLINUX))
-CFLAGS := -g -Wall
-ALL_LDFLAGS := $(LDFLAGS) $(EXTRA_LDFLAGS)
-
-APPS = goroutine # minimal minimal_legacy uprobe kprobe fentry usdt sockfilter tc ksyscall
-
-CARGO ?= $(shell which cargo)
-ifeq ($(strip $(CARGO)),)
-BZS_APPS :=
-else
-BZS_APPS := # profile
-APPS += $(BZS_APPS)
-# Required by libblazesym
-ALL_LDFLAGS += -lrt -ldl -lpthread -lm
-endif
-
-# Get Clang's default includes on this system. We'll explicitly add these dirs
-# to the includes list when compiling with `-target bpf` because otherwise some
-# architecture-specific dirs will be "missing" on some architectures/distros -
-# headers such as asm/types.h, asm/byteorder.h, asm/socket.h, asm/sockios.h,
-# sys/cdefs.h etc. might be missing.
-#
-# Use '-idirafter': Don't interfere with include mechanics except where the
-# build would have failed anyways.
-CLANG_BPF_SYS_INCLUDES ?= $(shell $(CLANG) -v -E - &1 \
- | sed -n '/<...> search starts here:/,/End of search list./{ s| \(/.*\)|-idirafter \1|p }')
-
-ifeq ($(V),1)
- Q =
- msg =
-else
- Q = @
- msg = @printf ' %-8s %s%s\n' \
- "$(1)" \
- "$(patsubst $(abspath $(OUTPUT))/%,%,$(2))" \
- "$(if $(3), $(3))";
- MAKEFLAGS += --no-print-directory
-endif
-
-define allow-override
- $(if $(or $(findstring environment,$(origin $(1))),\
- $(findstring command line,$(origin $(1)))),,\
- $(eval $(1) = $(2)))
-endef
-
-$(call allow-override,CC,$(CROSS_COMPILE)cc)
-$(call allow-override,LD,$(CROSS_COMPILE)ld)
-
-.PHONY: all
-all: $(APPS)
-
-.PHONY: clean
-clean:
- $(call msg,CLEAN)
- $(Q)rm -rf $(OUTPUT) $(APPS)
-
-$(OUTPUT) $(OUTPUT)/libbpf $(BPFTOOL_OUTPUT):
- $(call msg,MKDIR,$@)
- $(Q)mkdir -p $@
-
-# Build libbpf
-$(LIBBPF_OBJ): $(wildcard $(LIBBPF_SRC)/*.[ch] $(LIBBPF_SRC)/Makefile) | $(OUTPUT)/libbpf
- $(call msg,LIB,$@)
- $(Q)$(MAKE) -C $(LIBBPF_SRC) BUILD_STATIC_ONLY=1 \
- OBJDIR=$(dir $@)/libbpf DESTDIR=$(dir $@) \
- INCLUDEDIR= LIBDIR= UAPIDIR= \
- install
-
-# Build bpftool
-$(BPFTOOL): | $(BPFTOOL_OUTPUT)
- $(call msg,BPFTOOL,$@)
- $(Q)$(MAKE) ARCH= CROSS_COMPILE= OUTPUT=$(BPFTOOL_OUTPUT)/ -C $(BPFTOOL_SRC) goroutine
-
-
-$(LIBBLAZESYM_SRC)/target/release/libblazesym.a::
- $(Q)cd $(LIBBLAZESYM_SRC) && $(CARGO) build --features=cheader,dont-generate-test-files --release
-
-$(LIBBLAZESYM_OBJ): $(LIBBLAZESYM_SRC)/target/release/libblazesym.a | $(OUTPUT)
- $(call msg,LIB, $@)
- $(Q)cp $(LIBBLAZESYM_SRC)/target/release/libblazesym.a $@
-
-$(LIBBLAZESYM_HEADER): $(LIBBLAZESYM_SRC)/target/release/libblazesym.a | $(OUTPUT)
- $(call msg,LIB,$@)
- $(Q)cp $(LIBBLAZESYM_SRC)/target/release/blazesym.h $@
-
-# Build BPF code
-$(OUTPUT)/%.bpf.o: %.bpf.c $(LIBBPF_OBJ) $(wildcard %.h) $(VMLINUX) | $(OUTPUT) $(BPFTOOL)
- $(call msg,BPF,$@)
- $(Q)$(CLANG) -g -O2 -target bpf -D__TARGET_ARCH_$(ARCH) \
- $(INCLUDES) $(CLANG_BPF_SYS_INCLUDES) \
- -c $(filter %.c,$^) -o $(patsubst %.bpf.o,%.tmp.bpf.o,$@)
- $(Q)$(BPFTOOL) gen object $@ $(patsubst %.bpf.o,%.tmp.bpf.o,$@)
-
-# Generate BPF skeletons
-$(OUTPUT)/%.skel.h: $(OUTPUT)/%.bpf.o | $(OUTPUT) $(BPFTOOL)
- $(call msg,GEN-SKEL,$@)
- $(Q)$(BPFTOOL) gen skeleton $< > $@
-
-# Build user-space code
-$(patsubst %,$(OUTPUT)/%.o,$(APPS)): %.o: %.skel.h
-
-$(OUTPUT)/%.o: %.c $(wildcard %.h) | $(OUTPUT)
- $(call msg,CC,$@)
- $(Q)$(CC) $(CFLAGS) $(INCLUDES) -c $(filter %.c,$^) -o $@
-
-$(patsubst %,$(OUTPUT)/%.o,$(BZS_APPS)): $(LIBBLAZESYM_HEADER)
-
-$(BZS_APPS): $(LIBBLAZESYM_OBJ)
-
-# Build application binary
-$(APPS): %: $(OUTPUT)/%.o $(LIBBPF_OBJ) | $(OUTPUT)
- $(call msg,BINARY,$@)
- $(Q)$(CC) $(CFLAGS) $^ $(ALL_LDFLAGS) -lelf -lz -o $@
-
-# delete failed targets
-.DELETE_ON_ERROR:
-
-# keep intermediate (.skel.h, .bpf.o, etc) targets
-.SECONDARY:
diff --git a/31-goroutine/go-server-http/main b/31-goroutine/go-server-http/main
new file mode 100755
index 0000000..b84185a
Binary files /dev/null and b/31-goroutine/go-server-http/main differ
diff --git a/31-goroutine/go-server-http/main.go b/31-goroutine/go-server-http/main.go
new file mode 100644
index 0000000..3398fa9
--- /dev/null
+++ b/31-goroutine/go-server-http/main.go
@@ -0,0 +1,34 @@
+package main
+
+import (
+ "crypto/rand"
+ "fmt"
+ "log"
+ "net/http"
+ "os"
+ "strconv"
+)
+
+var http_body []byte
+
+func handler(w http.ResponseWriter, r *http.Request) {
+ w.Write(http_body)
+}
+
+func main() {
+ args := os.Args
+ if len(args) > 1 {
+ body_len, _ := strconv.ParseInt(args[1], 10, 64)
+ http_body = make([]byte, body_len)
+ rand.Read(http_body)
+ fmt.Println("Body set to", body_len, "bytes of random stuff")
+ } else {
+ http_body = []byte("Hello,World!")
+ }
+ http.HandleFunc("/", handler)
+ fmt.Println("Server started!")
+ err := http.ListenAndServe(":447", nil)
+ if err != nil {
+ log.Fatalf("Failed to start server: %v", err)
+ }
+}
diff --git a/31-goroutine/goroutine.bpf.c b/31-goroutine/goroutine.bpf.c
index 5f800b4..5fc152f 100644
--- a/31-goroutine/goroutine.bpf.c
+++ b/31-goroutine/goroutine.bpf.c
@@ -1,7 +1,7 @@
/*
* This code runs using bpf in the Linux kernel.
* Copyright 2022- The Yunshan Networks Authors.
- *
+ *
* Modify from https://github.com/deepflowio/deepflow
* By Yusheng Zheng <1067852565@qq.com>
*
@@ -17,359 +17,44 @@
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+ * USA.
*
* SPDX-License-Identifier: GPL-2.0
*/
-#include "vmlinux.h"
+#include
+#include "goroutine.h"
+#include
#include
#include
-#include
-#define NAME(N) __##N
-#define HASH_ENTRIES_MAX 40960
-#define MAX_SYSTEM_THREADS 40960
-struct sched_comm_fork_ctx {
- __u64 __pad_0;
- char parent_comm[16];
- __u32 parent_pid;
- char child_comm[16];
- __u32 child_pid;
-};
+#define GOID_OFFSET 0x98
-struct sched_comm_exit_ctx {
- __u64 __pad_0; /* 0 8 */
- char comm[16]; /* offset:8; size:16 */
- pid_t pid; /* offset:24; size:4 */
- int prio; /* offset:28; size:4 */
-};
+struct {
+ __uint(type, BPF_MAP_TYPE_RINGBUF);
+ __uint(max_entries, 256 * 1024);
+} rb SEC(".maps");
-// struct ebpf_proc_info -> offsets[] arrays index.
-enum offsets_index {
- OFFSET_IDX_GOID_RUNTIME_G,
- OFFSET_IDX_CONN_TLS_CONN,
- OFFSET_IDX_SYSFD_POLL_FD,
- OFFSET_IDX_CONN_HTTP2_SERVER_CONN,
- OFFSET_IDX_TCONN_HTTP2_CLIENT_CONN,
- OFFSET_IDX_CC_HTTP2_CLIENT_CONN_READ_LOOP,
- OFFSET_IDX_CONN_GRPC_HTTP2_CLIENT,
- OFFSET_IDX_CONN_GRPC_HTTP2_SERVER,
- OFFSET_IDX_FRAMER_GRPC_TRANSPORT_LOOPY_WRITER,
- OFFSET_IDX_WRITER_GRPC_TRANSPORT_FRAMER,
- OFFSET_IDX_CONN_GRPC_TRANSPORT_BUFWRITER,
- OFFSET_IDX_SIDE_GRPC_TRANSPORT_LOOPY_WRITER,
- OFFSET_IDX_FIELDS_HTTP2_META_HEADERS_FRAME,
- OFFSET_IDX_STREAM_HTTP2_CLIENT_CONN,
- OFFSET_IDX_STREAM_ID_HTTP2_FRAME_HEADER,
- OFFSET_IDX_MAX,
-};
-
-// Store the ebpf_proc_info to eBPF Map.
-struct ebpf_proc_info {
- __u32 version;
- __u16 offsets[OFFSET_IDX_MAX];
-
- // In golang, itab represents type, and in interface, struct is represented
- // by the address of itab. We use itab to judge the structure type, and
- // find the fd representing the connection after multiple jumps. These
- // types are not available in Go ELF files without a symbol table.
- // Go 用 itab 表示类型, 在 interface 中通过 itab 确定具体的 struct, 并根据
- // struct 找到表示连接的 fd.
- __u64 net_TCPConn_itab;
- __u64 crypto_tls_Conn_itab; // TLS_HTTP1,TLS_HTTP2
- __u64 credentials_syscallConn_itab; // gRPC
-};
-
-#define GO_VERSION(a, b, c) (((a) << 16) + ((b) << 8) + ((c) > 255 ? 255 : (c)))
-
-// Go implements a new way of passing function arguments and results using
-// registers instead of the stack. We need the go version and the computer
-// architecture to determine the parameter locations
-static __inline bool is_register_based_call(struct ebpf_proc_info *info)
-{
-#if defined(__x86_64__)
- // https://go.dev/doc/go1.17
- return info->version >= GO_VERSION(1, 17, 0);
-#elif defined(__aarch64__)
- // https://groups.google.com/g/golang-checkins/c/SO9OmZYkOXU
- return info->version >= GO_VERSION(1, 18, 0);
-#else
-_Pragma("error \"Must specify a BPF target arch\"");
-#endif
+SEC("uprobe/./go-server-http/main:runtime.casgstatus")
+int uprobe_runtime_casgstatus(struct pt_regs *ctx) {
+ int newval = ctx->cx;
+ void *gp = ctx->ax;
+ struct goroutine_execute_data *data;
+ u64 goid;
+ if (bpf_probe_read_user(&goid, sizeof(goid), gp + GOID_OFFSET) == 0) {
+ data = bpf_ringbuf_reserve(&rb, sizeof(*data), 0);
+ if (data) {
+ u64 pid_tgid = bpf_get_current_pid_tgid();
+ data->pid = pid_tgid;
+ data->tgid = pid_tgid >> 32;
+ data->goid = goid;
+ data->state = newval;
+ bpf_ringbuf_submit(data, 0);
+ }
+ }
+ return 0;
}
-struct bpf_map_def {
- unsigned int type;
- unsigned int key_size;
- unsigned int value_size;
- unsigned int max_entries;
-};
-
-// Process ID and coroutine ID, marking the coroutine in the system
-struct go_key {
- __u32 tgid;
- __u64 goid;
-} __attribute__((packed));
-
-// The mapping of coroutines to ancestors, the map is updated when a new
-// coroutine is created
-// key : current gorouting (struct go_key)
-// value : ancerstor goid
-struct bpf_map_def SEC("maps") go_ancerstor_map = {
- .type = BPF_MAP_TYPE_LRU_HASH,
- .key_size = sizeof(struct go_key),
- .value_size = sizeof(__u64),
- .max_entries = HASH_ENTRIES_MAX,
-};
-
-// Used to determine the timeout, as a termination condition for finding
-// ancestors.
-// key : current gorouting (struct go_key)
-// value: timestamp when the data was inserted into the map
-struct bpf_map_def SEC("maps") go_rw_ts_map = {
- .type = BPF_MAP_TYPE_LRU_HASH,
- .key_size = sizeof(struct go_key),
- .value_size = sizeof(__u64),
- .max_entries = HASH_ENTRIES_MAX,
-};
-
-/*
- * The binary executable file offset of the GO process
- * key: pid
- * value: struct ebpf_proc_info
- */
-struct bpf_map_def SEC("maps") proc_info_map = {
- .type = BPF_MAP_TYPE_HASH,
- .key_size = sizeof(int),
- .value_size = sizeof(struct ebpf_proc_info),
- .max_entries = HASH_ENTRIES_MAX,
-};
-
-// Pass data between coroutine entry and exit functions
-struct go_newproc_caller {
- __u64 goid;
- void *sp; // stack pointer
-} __attribute__((packed));
-
-struct bpf_map_def SEC("maps") pid_tgid_callerid_map = {
- .type = BPF_MAP_TYPE_HASH,
- .key_size = sizeof(__u64),
- .value_size = sizeof(struct go_newproc_caller),
- .max_entries = HASH_ENTRIES_MAX,
-};
-
-/*
- * Goroutines Map
- * key: {tgid, pid}
- * value: goroutine ID
- */
-struct bpf_map_def SEC("maps") goroutines_map = {
- .type = BPF_MAP_TYPE_HASH,
- .key_size = sizeof(__u64),
- .value_size = sizeof(__u64),
- .max_entries = MAX_SYSTEM_THREADS,
-};
-
-SEC("uprobe/runtime.execute")
-int runtime_execute(struct pt_regs *ctx)
-{
- __u64 pid_tgid = bpf_get_current_pid_tgid();
- __u32 tgid = pid_tgid >> 32;
-
- struct ebpf_proc_info *info = bpf_map_lookup_elem(&proc_info_map, &tgid);
- if (!info) {
- return 0;
- }
- int offset_g_goid = info->offsets[OFFSET_IDX_GOID_RUNTIME_G];
- if (offset_g_goid < 0) {
- return 0;
- }
-
- void *g_ptr;
-
- if (is_register_based_call(info)) {
- g_ptr = (void *)PT_GO_REGS_PARM1(ctx);
- } else {
- bpf_probe_read(&g_ptr, sizeof(g_ptr), (void *)(PT_REGS_SP(ctx) + 8));
- }
-
- __s64 goid = 0;
- bpf_probe_read(&goid, sizeof(goid), g_ptr + offset_g_goid);
- bpf_map_update_elem(&goroutines_map, &pid_tgid, &goid, BPF_ANY);
-
- return 0;
-}
-
-// This function creates a new go coroutine, and the parent and child
-// coroutine numbers are in the parameters and return values respectively.
-// Pass the function parameters through pid_tgid_callerid_map
-//
-// go 1.15 ~ 1.17: func newproc1(fn *funcval, argp unsafe.Pointer, narg int32, callergp *g, callerpc uintptr) *g
-// go1.18+ :func newproc1(fn *funcval, callergp *g, callerpc uintptr) *g
-SEC("uprobe/enter_runtime.newproc1")
-int enter_runtime_newproc1(struct pt_regs *ctx)
-{
- __u64 pid_tgid = bpf_get_current_pid_tgid();
- __u32 tgid = pid_tgid >> 32;
-
- struct ebpf_proc_info *info =
- bpf_map_lookup_elem(&proc_info_map, &tgid);
- if (!info) {
- return 0;
- }
-
- // go less than 1.15 cannot get parent-child coroutine relationship
- // ~ go1.14: func newproc1(fn *funcval, argp unsafe.Pointer, narg int32, callergp *g, callerpc uintptr)
- if (info->version < GO_VERSION(1, 15, 0)) {
- return 0;
- }
-
- int offset_g_goid = info->offsets[OFFSET_IDX_GOID_RUNTIME_G];
- if (offset_g_goid < 0) {
- return 0;
- }
-
- void *g_ptr;
- if (is_register_based_call(info)) {
- // https://github.com/golang/go/commit/8e5304f7298a0eef48e4796017c51b4d9aeb52b5
- if (info->version >= GO_VERSION(1, 18, 0)) {
- g_ptr = (void *)PT_GO_REGS_PARM2(ctx);
- } else {
- g_ptr = (void *)PT_GO_REGS_PARM4(ctx);
- }
- } else {
- if (info->version >= GO_VERSION(1, 18, 0)) {
- bpf_probe_read(&g_ptr, sizeof(g_ptr),
- (void *)(PT_REGS_SP(ctx) + 16));
- } else {
- bpf_probe_read(&g_ptr, sizeof(g_ptr),
- (void *)(PT_REGS_SP(ctx) + 32));
- }
- }
-
- __s64 goid = 0;
- bpf_probe_read(&goid, sizeof(goid), g_ptr + offset_g_goid);
- if (!goid) {
- return 0;
- }
-
- struct go_newproc_caller caller = {
- .goid = goid,
- .sp = (void *)PT_REGS_SP(ctx),
- };
- bpf_map_update_elem(&pid_tgid_callerid_map, &pid_tgid, &caller,
- BPF_ANY);
- return 0;
-}
-
-// The mapping relationship between parent and child coroutines is stored in go_ancerstor_map
-//
-// go 1.15 ~ 1.17: func newproc1(fn *funcval, argp unsafe.Pointer, narg int32, callergp *g, callerpc uintptr) *g
-// go1.18+ :func newproc1(fn *funcval, callergp *g, callerpc uintptr) *g
-SEC("uprobe/exit_runtime.newproc1")
-int exit_runtime_newproc1(struct pt_regs *ctx)
-{
- __u64 pid_tgid = bpf_get_current_pid_tgid();
- __u32 tgid = pid_tgid >> 32;
-
- struct ebpf_proc_info *info =
- bpf_map_lookup_elem(&proc_info_map, &tgid);
- if (!info) {
- return 0;
- }
-
- if(info->version < GO_VERSION(1, 15, 0)){
- return 0;
- }
-
- int offset_g_goid = info->offsets[OFFSET_IDX_GOID_RUNTIME_G];
- if (offset_g_goid < 0) {
- return 0;
- }
-
- struct go_newproc_caller *caller =
- bpf_map_lookup_elem(&pid_tgid_callerid_map, &pid_tgid);
- if (!caller) {
- return 0;
- }
-
- void *g_ptr;
- if (is_register_based_call(info)) {
- g_ptr = (void *)PT_GO_REGS_PARM1(ctx);
- } else {
- if (info->version >= GO_VERSION(1, 18, 0)) {
- bpf_probe_read(&g_ptr, sizeof(g_ptr), caller->sp + 32);
- } else {
- bpf_probe_read(&g_ptr, sizeof(g_ptr), caller->sp + 48);
- }
- }
-
- __s64 goid = 0;
- bpf_probe_read(&goid, sizeof(goid), g_ptr + offset_g_goid);
- if (!goid) {
- bpf_map_delete_elem(&pid_tgid_callerid_map, &pid_tgid);
- return 0;
- }
-
- struct go_key key = { .tgid = tgid, .goid = goid };
- goid = caller->goid;
- bpf_map_update_elem(&go_ancerstor_map, &key, &goid, BPF_ANY);
-
- bpf_map_delete_elem(&pid_tgid_callerid_map, &pid_tgid);
- return 0;
-}
-
-// /sys/kernel/debug/tracing/events/sched/sched_process_exit/format
-SEC("tracepoint/sched/sched_process_exit")
-int bpf_func_sched_process_exit(struct sched_comm_exit_ctx *ctx)
-{
- pid_t pid, tid;
- __u64 id;
-
- id = bpf_get_current_pid_tgid();
- pid = id >> 32;
- tid = (__u32)id;
-
- // If is a process, clear proc_info_map element and submit event.
- if (pid == tid) {
- bpf_map_delete_elem(&proc_info_map, &pid);
- struct process_event_t data;
- data.pid = pid;
- data.meta.event_type = EVENT_TYPE_PROC_EXIT;
- bpf_get_current_comm(data.name, sizeof(data.name));
- // int ret = bpf_perf_event_output(ctx, &NAME(socket_data),
- // BPF_F_CURRENT_CPU, &data,
- // sizeof(data));
-
- // if (ret) {
- // bpf_debug
- // ("bpf_func_sched_process_exit event output failed: %d\n",
- // ret);
- // }
- }
-
- bpf_map_delete_elem(&goroutines_map, &id);
- return 0;
-}
-
-// /sys/kernel/debug/tracing/events/sched/sched_process_fork/format
-SEC("tracepoint/sched/sched_process_fork")
-int bpf_func_sched_process_fork(struct sched_comm_fork_ctx *ctx)
-{
- struct process_event_t data;
-
- data.meta.event_type = EVENT_TYPE_PROC_EXEC;
- data.pid = ctx->child_pid;
- bpf_get_current_comm(data.name, sizeof(data.name));
- // int ret = bpf_perf_event_output(ctx, &NAME(socket_data),
- // BPF_F_CURRENT_CPU, &data, sizeof(data));
-
- // if (ret) {
- // bpf_debug(
- // "bpf_func_sys_exit_execve event output() failed: %d\n",
- // ret);
- // }
- return 0;
-}
+char LICENSE[] SEC("license") = "GPL";
diff --git a/31-goroutine/goroutine.h b/31-goroutine/goroutine.h
index 76b9fbe..98b12ec 100644
--- a/31-goroutine/goroutine.h
+++ b/31-goroutine/goroutine.h
@@ -1,33 +1,24 @@
#ifndef EBPF_EXAMPLE_GOROUTINE_H
#define EBPF_EXAMPLE_GOROUTINE_H
-
-enum {
- /*
- * 0 ~ 16 for L7 socket event (struct socket_data_buffer),
- * indicates the number of socket data in socket_data_buffer.
- */
-
- /*
- * For event registrion
- */
- EVENT_TYPE_MIN = 1 << 5,
- EVENT_TYPE_PROC_EXEC = 1 << 5,
- EVENT_TYPE_PROC_EXIT = 1 << 6
- // Add new event type here.
+enum goroutine_state {
+ IDLE,
+ RUNNABLE,
+ RUNNING,
+ SYSCALL,
+ WAITING,
+ MORIBUND_UNUSED,
+ DEAD,
+ ENQUEUE_UNUSED,
+ COPYSTACK,
+ PREEMPTED,
};
-// Description Provides basic information about an event
-struct event_meta {
- __u32 event_type;
+struct goroutine_execute_data {
+ enum goroutine_state state;
+ unsigned long goid;
+ int pid;
+ int tgid;
};
-// Process execution or exit event data
-struct process_event_t {
- struct event_meta meta;
- __u32 pid; // process ID
- __u8 name[TASK_COMM_LEN]; // process name
-};
-
-
#endif
diff --git a/34-syscall/index.html b/34-syscall/index.html
index 6bca0ab..652a581 100644
--- a/34-syscall/index.html
+++ b/34-syscall/index.html
@@ -186,7 +186,7 @@
如果该技术被恶意软件利用,攻击者可以重定向文件操作,导致数据泄漏或者破坏数据完整性。例如,程序写入日志文件时,攻击者可能将数据重定向到控制的文件中,干扰审计跟踪。
内核态代码(部分,完整内容请参考 Github bpf-developer-tutorial):
-SEC("tracepoint/syscalls/sys_enter_openat")
+SEC("tracepoint/syscalls/sys_enter_openat")
int tracepoint__syscalls__sys_enter_openat(struct trace_event_raw_sys_enter *ctx)
{
u64 pid = bpf_get_current_pid_tgid() >> 32;
@@ -202,7 +202,7 @@ int tracepoint__syscalls__sys_enter_openat(struct trace_event_raw_sys_enter *ctx
args.fname = (const char *)ctx->args[1];
args.flags = (int)ctx->args[2];
if (rewrite) {
- bpf_probe_write_user((char*)ctx->args[1], "hijacked", 9);
+ bpf_probe_write_user((char*)ctx->args[1], "hijacked", 9);
}
bpf_map_update_elem(&start, &pid, &args, 0);
return 0;
@@ -213,7 +213,7 @@ int tracepoint__syscalls__sys_enter_openat(struct trace_event_raw_sys_enter *ctx
bpf_get_current_pid_tgid() 获取当前进程ID。
- 如果指定了
target_pid 并且不匹配当前进程ID,函数直接返回。
- 我们创建一个
args_t 结构来存储文件名和标志。
-- 使用
bpf_probe_write_user 修改用户空间内存中的文件名为 "hijacked"。
+- 使用
bpf_probe_write_user 修改用户空间内存中的文件名为 "hijacked"。
eunomia-bpf 是一个开源的 eBPF 动态加载运行时和开发工具链,它的目的是简化 eBPF 程序的开发、构建、分发、运行。可以参考 https://github.com/eunomia-bpf/eunomia-bpf 或 https://eunomia.dev/tutorials/1-helloworld/ 下载和安装 ecc 编译工具链和 ecli 运行时。我们使用 eunomia-bpf 编译运行这个例子。
编译:
@@ -222,27 +222,27 @@ int tracepoint__syscalls__sys_enter_openat(struct trace_event_raw_sys_enter *ctx
使用 make 构建一个简单的 victim 程序,用来测试:
int main()
{
- char filename[100] = "my_test.txt";
+ char filename[100] = "my_test.txt";
// print pid
int pid = getpid();
- std::cout << "current pid: " << pid << std::endl;
- system("echo \"hello\" > my_test.txt");
- system("echo \"world\" >> hijacked");
+ std::cout << "current pid: " << pid << std::endl;
+ system("echo \"hello\" > my_test.txt");
+ system("echo \"world\" >> hijacked");
while (true) {
- std::cout << "Opening my_test.txt" << std::endl;
+ std::cout << "Opening my_test.txt" << std::endl;
int fd = open(filename, O_RDONLY);
assert(fd != -1);
- std::cout << "test.txt opened, fd=" << fd << std::endl;
+ std::cout << "test.txt opened, fd=" << fd << std::endl;
usleep(1000 * 300);
// print the file content
char buf[100] = {0};
int ret = read(fd, buf, 5);
- std::cout << "read " << ret << " bytes: " << buf << std::endl;
- std::cout << "Closing test.txt..." << std::endl;
+ std::cout << "read " << ret << " bytes: " << buf << std::endl;
+ std::cout << "Closing test.txt..." << std::endl;
close(fd);
- std::cout << "test.txt closed" << std::endl;
+ std::cout << "test.txt closed" << std::endl;
}
return 0;
}
@@ -257,7 +257,7 @@ test.txt closed
可以使用以下命令指定应修改其 openat 系统调用参数的目标进程ID:
sudo ./ecli run package.json --rewrite --target_pid=$(pidof victim)
-然后就会发现输出变成了 world,可以看到我们原先想要打开 "my_test.txt" 文件,但是实际上被劫持打开了 hijacked 文件:
+然后就会发现输出变成了 world,可以看到我们原先想要打开 "my_test.txt" 文件,但是实际上被劫持打开了 hijacked 文件:
test.txt opened, fd=3
read 5 bytes: hello
Closing test.txt...
@@ -274,7 +274,7 @@ read 5 bytes: world
包含测试用例的完整代码可以在 https://github.com/eunomia-bpf/bpf-developer-tutorial 找到。
修改 bash execve 的进程名称
这段功能用于当 execve 系统调用进行时修改执行程序名称。在一些审计或监控场景,这可能用于记录特定进程的行为或修改其行为。然而,此类篡改可能会造成混淆,使得用户或管理员难以确定系统实际执行的程序是什么。最严重的风险是,如果恶意用户能够控制 eBPF 程序,他们可以将合法的系统命令重定向到恶意软件,造成严重的安全威胁。
-SEC("tp/syscalls/sys_enter_execve")
+SEC("tp/syscalls/sys_enter_execve")
int handle_execve_enter(struct trace_event_raw_sys_enter *ctx)
{
size_t pid_tgid = bpf_get_current_pid_tgid();
@@ -294,11 +294,11 @@ int handle_execve_enter(struct trace_event_raw_sys_enter *ctx)
bpf_probe_read_user(&prog_name, TASK_COMM_LEN, (void*)ctx->args[0]);
bpf_probe_read_user(&prog_name_orig, TASK_COMM_LEN, (void*)ctx->args[0]);
prog_name[TASK_COMM_LEN-1] = '\x00';
- bpf_printk("[EXECVE_HIJACK] %s\n", prog_name);
+ bpf_printk("[EXECVE_HIJACK] %s\n", prog_name);
// Program can't be less than out two-char name
if (prog_name[1] == '\x00') {
- bpf_printk("[EXECVE_HIJACK] program name too small\n");
+ bpf_printk("[EXECVE_HIJACK] program name too small\n");
return 0;
}
diff --git a/35-user-ringbuf/index.html b/35-user-ringbuf/index.html
index 8f0603a..cb478ee 100644
--- a/35-user-ringbuf/index.html
+++ b/35-user-ringbuf/index.html
@@ -192,7 +192,7 @@
下面,我们将通过一段代码示例,详细展示如何利用 user ring buffer,实现从用户态向内核传送数据,并以 kernel ring buffer 相应地从内核态向用户态传送数据。
一、实现:在用户态和内核态间使用 ring buffer 传送数据
-借助新的 BPF MAP,我们可以实现在用户态和内核态间通过环形缓冲区传送数据。在这个示例中,我们将详细说明如何在用户空间创建一个 "用户环形缓冲区" (user ring buffer) 并向其写入数据,然后在内核空间中通过 bpf_user_ringbuf_drain 函数来消费这些数据。同时,我们也会使用 "内核环形缓冲区" (kernel ring buffer) 来从内核空间反馈数据到用户空间。为此,我们需要在用户空间和内核空间分别创建并操作这两个环形缓冲区。
+借助新的 BPF MAP,我们可以实现在用户态和内核态间通过环形缓冲区传送数据。在这个示例中,我们将详细说明如何在用户空间创建一个 "用户环形缓冲区" (user ring buffer) 并向其写入数据,然后在内核空间中通过 bpf_user_ringbuf_drain 函数来消费这些数据。同时,我们也会使用 "内核环形缓冲区" (kernel ring buffer) 来从内核空间反馈数据到用户空间。为此,我们需要在用户空间和内核空间分别创建并操作这两个环形缓冲区。
完整的代码可以在 https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/35-user-ringbuf 中找到。
创建环形缓冲区
在内核空间,我们创建了一个类型为 BPF_MAP_TYPE_USER_RINGBUF 的 user_ringbuf,以及一个类型为 BPF_MAP_TYPE_RINGBUF 的 kernel_ringbuf。在用户空间,我们创建了一个 struct ring_buffer_user 结构体的实例,并通过 ring_buffer_user__new 函数和对应的操作来管理这个用户环形缓冲区。
@@ -201,7 +201,7 @@
if (!rb)
{
err = -1;
- fprintf(stderr, "Failed to create ring buffer\n");
+ fprintf(stderr, "Failed to create ring buffer\n");
goto cleanup;
}
user_ringbuf = user_ring_buffer__new(bpf_map__fd(skel->maps.user_ringbuf), NULL);
@@ -211,25 +211,25 @@
// SPDX-License-Identifier: GPL-2.0
/* Copyright (c) 2022 Meta Platforms, Inc. and affiliates. */
-#include "vmlinux.h"
+#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
-#include "user_ringbuf.h"
+#include "user_ringbuf.h"
-char _license[] SEC("license") = "GPL";
+char _license[] SEC("license") = "GPL";
struct
{
__uint(type, BPF_MAP_TYPE_USER_RINGBUF);
__uint(max_entries, 256 * 1024);
-} user_ringbuf SEC(".maps");
+} user_ringbuf SEC(".maps");
struct
{
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024);
-} kernel_ringbuf SEC(".maps");
+} kernel_ringbuf SEC(".maps");
int read = 0;
@@ -255,7 +255,7 @@ do_nothing_cb(struct bpf_dynptr *dynptr, void *context)
return 0;
}
-SEC("tracepoint/syscalls/sys_exit_kill")
+SEC("tracepoint/syscalls/sys_exit_kill")
int kill_exit(struct trace_event_raw_sys_exit *ctx)
{
long num_samples;
@@ -282,9 +282,9 @@ int kill_exit(struct trace_event_raw_sys_exit *ctx)
}
entry->i = getpid();
- strcpy(entry->comm, "hello");
+ strcpy(entry->comm, "hello");
- int read = snprintf(entry->comm, sizeof(entry->comm), "%u", i);
+ int read = snprintf(entry->comm, sizeof(entry->comm), "%u", i);
if (read <= 0)
{
/* Assert on the error path to avoid spamming logs with
@@ -308,8 +308,8 @@ done:
write_samples(user_ringbuf);
/* Process events */
- printf("%-8s %-5s %-16s %-7s %-7s %s\n",
- "TIME", "EVENT", "COMM", "PID", "PPID", "FILENAME/EXIT CODE");
+ printf("%-8s %-5s %-16s %-7s %-7s %s\n",
+ "TIME", "EVENT", "COMM", "PID", "PPID", "FILENAME/EXIT CODE");
while (!exiting)
{
err = ring_buffer__poll(rb, 100 /* timeout, ms */);
@@ -321,7 +321,7 @@ done:
}
if (err < 0)
{
- printf("Error polling perf buffer: %d\n", err);
+ printf("Error polling perf buffer: %d\n", err);
break;
}
}
diff --git a/36-userspace-ebpf/index.html b/36-userspace-ebpf/index.html
index fb0b3f9..6d1f0c0 100644
--- a/36-userspace-ebpf/index.html
+++ b/36-userspace-ebpf/index.html
@@ -178,7 +178,7 @@
本文旨在对用户空间的 eBPF 运行时和对应的一些应用场景进行剖析和总结。尽管大多数人对基于内核的 eBPF 已有所了解,用户空间 eBPF 的进展和应用实践同样引人注目。本文还将探讨用户空间 eBPF 运行时与 Wasm 运行时的技术比较,后者在云原生和边缘计算领域已获得广泛的关注。我们也新开源了一个用户态 eBPF 运行时 bpftime。通过 LLVM JIT/AOT 后端支持,我们的基准测试表明 bpftime 是最快的用户空间 eBPF 运行时之一,同时还可以让内核中间的 eBPF Uprobe 无缝在用户空间运行,获得近十倍的性能提升。
eBPF:内核的动态扩展运行时与字节码
eBPF 究竟是何方神圣?
-eBPF,全称 "extended Berkeley Packet Filter",是一项允许在不更改内核源代码或重启系统的情况下动态干预和修改内核行为的革命性技术。虽然 eBPF 起初是作为网络数据包过滤工具而设计,但如今已广泛应用于从性能分析到安全策略等多个方面,逐渐成为系统管理员的得力助手。
+eBPF,全称 "extended Berkeley Packet Filter",是一项允许在不更改内核源代码或重启系统的情况下动态干预和修改内核行为的革命性技术。虽然 eBPF 起初是作为网络数据包过滤工具而设计,但如今已广泛应用于从性能分析到安全策略等多个方面,逐渐成为系统管理员的得力助手。
eBPF 的前身,Berkeley Packet Filter (BPF) —— 20 世纪 90 年代初的产物,主要用于网络数据包的高效过滤。尽管 BPF 已被广大用户所认可,eBPF 的出现则为其带来了更为广泛的指令集,并能直接与内核数据结构互动。自 2014 年 Linux 内核引入 eBPF 以后,它的影响力迅速扩张。Linux 的核心开发团队不断地完善 eBPF,使其从一个基础的网络数据包过滤器逐渐演变为一个功能强大的字节码引擎。
eBPF 对现代计算和网络的深远影响
随着现代计算环境日益复杂,实时数据的采集和深入分析显得尤为重要。在这一背景下,eBPF 凭借其卓越的动态性,为开发者和管理员提供了实时干预系统行为的强大工具。eBPF 以其卓越的灵活性在现代网络解决方案中占据核心地位。它为流量控制、负载均衡及安全策略在内核级别提供了细致的控制手段,确保了系统的性能优化和安全稳定。同时,eBPF 在系统可观察性上也做出了显著贡献,为各种系统调用和硬件事件提供了详细的可编程追踪方案,促进了问题的迅速定位和解决。
@@ -291,7 +291,7 @@ continue malloc...
然后附加到该进程:
$ sudo bpftime attach 101771 # 您可能需要以root身份运行make install
-Inject: "/root/.bpftime/libbpftime-agent.so"
+Inject: "/root/.bpftime/libbpftime-agent.so"
成功注入。ID: 1
您可以看到原始程序的输出:
diff --git a/37-uprobe-rust/index.html b/37-uprobe-rust/index.html
index 1b9d49a..4737c97 100644
--- a/37-uprobe-rust/index.html
+++ b/37-uprobe-rust/index.html
@@ -191,7 +191,7 @@
最简单的例子:Symbol name mangling
我们先来看一个简单的例子,使用 Uprobe 追踪 Rust 程序的 main 函数,代码如下:
pub fn hello() -> i32 {
- println!("Hello, world!");
+ println!("Hello, world!");
0
}
@@ -214,7 +214,7 @@ $ rustfilt -i name.txt | grep hello
0000000000008b60 t helloworld::main
接下来我们可以尝试使用 bpftrace 跟踪对应的函数:
-$ sudo bpftrace -e 'uprobe:helloworld/target/release/helloworld:_ZN10helloworld4main17h2dce92cb81426b91E { printf("Function hello-world called\n"); }'
+$ sudo bpftrace -e 'uprobe:helloworld/target/release/helloworld:_ZN10helloworld4main17h2dce92cb81426b91E { printf("Function hello-world called\n"); }'
Attaching 1 probe...
Function hello-world called
@@ -223,7 +223,7 @@ Function hello-world called
use std::env;
pub fn hello(i: i32, len: usize) -> i32 {
- println!("Hello, world! {} in {}", i, len);
+ println!("Hello, world! {} in {}", i, len);
i + len as i32
}
@@ -235,16 +235,16 @@ fn main() {
match arg.parse::<i32>() {
Ok(i) => {
let ret = hello(i, args.len());
- println!("return value: {}", ret);
+ println!("return value: {}", ret);
}
Err(_) => {
- eprintln!("Error: Argument '{}' is not a valid integer", arg);
+ eprintln!("Error: Argument '{}' is not a valid integer", arg);
}
}
}
}
我们再次进行类似的操作,会发现一个奇怪的现象:
-$ sudo bpftrace -e 'uprobe:args/target/release/helloworld:_ZN10helloworld4main17h2dce92cb81426b91E { printf("Function hello-world called\n"); }'
+$ sudo bpftrace -e 'uprobe:args/target/release/helloworld:_ZN10helloworld4main17h2dce92cb81426b91E { printf("Function hello-world called\n"); }'
Attaching 1 probe...
Function hello-world called
@@ -260,7 +260,7 @@ Hello, world! 4 in 5
return value: 9
而且看起来 bpftrace 并不能正确获取参数:
-$ sudo bpftrace -e 'uprobe:args/target/release/helloworld:_ZN10helloworld4main17h2dce92cb81426b91E { printf("Function hello-world called %d\n"
+$ sudo bpftrace -e 'uprobe:args/target/release/helloworld:_ZN10helloworld4main17h2dce92cb81426b91E { printf("Function hello-world called %d\n"
, arg0); }'
Attaching 1 probe...
Function hello-world called 63642464
@@ -268,8 +268,8 @@ Function hello-world called 63642464
Uretprobe 捕捉到了第一次调用的返回值:
$ sudo bpftrace -e 'uretprobe:args/tar
get/release/helloworld:_ZN10helloworld4main17h2dce92
-cb81426b91E { printf("Function hello-world called %d
-\n", retval); }'
+cb81426b91E { printf("Function hello-world called %d
+\n", retval); }'
Attaching 1 probe...
Function hello-world called 6
diff --git a/38-btf-uprobe/index.html b/38-btf-uprobe/index.html
index b17c174..aa98295 100644
--- a/38-btf-uprobe/index.html
+++ b/38-btf-uprobe/index.html
@@ -214,17 +214,17 @@ struct data {
int d;
};
-SEC("uprobe/examples/btf-base:add_test")
+SEC("uprobe/examples/btf-base:add_test")
int BPF_UPROBE(add_test, struct data *d)
{
int a = 0, c = 0;
bpf_probe_read_user(&a, sizeof(a), &d->a);
bpf_probe_read_user(&c, sizeof(c), &d->c);
- bpf_printk("add_test(&d) %d + %d = %d\n", a, c, a + c);
+ bpf_printk("add_test(&d) %d + %d = %d\n", a, c, a + c);
return a + c;
}
-char LICENSE[] SEC("license") = "Dual BSD/GPL";
+char LICENSE[] SEC("license") = "Dual BSD/GPL";
然后,我们有两个不同版本的用户空间程序,examples/btf-base和examples/btf-base-new。两个版本中的struct data是不同的。
examples/btf-base:
@@ -241,7 +241,7 @@ int add_test(struct data *d) {
int main(int argc, char **argv) {
struct data d = {1, 3, 4};
- printf("add_test(&d) = %d\n", add_test(&d));
+ printf("add_test(&d) = %d\n", add_test(&d));
return 0;
}
@@ -259,7 +259,7 @@ int add_test(struct data *d) {
int main(int argc, char **argv) {
struct data d = {1, 2, 3, 4};
- printf("add_test(&d) = %d\n", add_test(&d));
+ printf("add_test(&d) = %d\n", add_test(&d));
return 0;
}
@@ -311,17 +311,17 @@ struct data {
#pragma clang attribute pop
#endif
-SEC("uprobe/examples/btf-base:add_test")
+SEC("uprobe/examples/btf-base:add_test")
int BPF_UPROBE(add_test, struct data *d)
{
int a = 0, c = 0;
bpf_probe_read_user(&a, sizeof(a), &d->a);
bpf_probe_read_user(&c, sizeof(c), &d->c);
- bpf_printk("add_test(&d) %d + %d = %d\n", a, c, a + c);
+ bpf_printk("add_test(&d) %d + %d = %d\n", a, c, a + c);
return a + c;
}
-char LICENSE[] SEC("license") = "Dual BSD/GPL";
+char LICENSE[] SEC("license") = "Dual BSD/GPL";
struct data的记录在eBPF程序中被保留下来。然后,我们可以使用 btf-base.btf来编译eBPF程序。
将用户btf与内核btf合并,这样我们就有了一个完整的内核和用户空间的btf:
@@ -370,7 +370,7 @@ Successfully started! Press Ctrl+C to stop.
);
LIBBPF_OPTS(bpf_uprobe_opts, uprobe_opts);
if (argc != 3 && argc != 2) {
- fprintf(stderr, "Usage: %s <example-name> [<external-btf>]\n", argv[0]);
+ fprintf(stderr, "Usage: %s <example-name> [<external-btf>]\n", argv[0]);
return 1;
}
if (argc == 3)
@@ -386,7 +386,7 @@ Successfully started! Press Ctrl+C to stop.
/* Load and verify BPF application */
skel = uprobe_bpf__open_opts(&opts);
if (!skel) {
- fprintf(stderr, "Failed to open and load BPF skeleton\n");
+ fprintf(stderr, "Failed to open and load BPF skeleton\n");
return 1;
}
diff --git a/4-opensnoop/index.html b/4-opensnoop/index.html
index 1e34e95..caa29f4 100644
--- a/4-opensnoop/index.html
+++ b/4-opensnoop/index.html
@@ -183,10 +183,10 @@
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
-/// @description "Process ID to trace"
+/// @description "Process ID to trace"
const volatile int pid_target = 0;
-SEC("tracepoint/syscalls/sys_enter_openat")
+SEC("tracepoint/syscalls/sys_enter_openat")
int tracepoint__syscalls__sys_enter_openat(struct trace_event_raw_sys_enter* ctx)
{
u64 id = bpf_get_current_pid_tgid();
@@ -195,23 +195,23 @@ int tracepoint__syscalls__sys_enter_openat(struct trace_event_raw_sys_enter* ctx
if (pid_target && pid_target != pid)
return false;
// Use bpf_printk to print the process information
- bpf_printk("Process ID: %d enter sys openat\n", pid);
+ bpf_printk("Process ID: %d enter sys openat\n", pid);
return 0;
}
-/// "Trace open family syscalls."
-char LICENSE[] SEC("license") = "GPL";
+/// "Trace open family syscalls."
+char LICENSE[] SEC("license") = "GPL";
这段 eBPF 程序实现了:
- 引入头文件:<vmlinux.h> 包含了内核数据结构的定义,<bpf/bpf_helpers.h> 包含了 eBPF 程序所需的辅助函数。
- 定义全局变量
pid_target,用于过滤指定进程 ID。这里设为 0 表示捕获所有进程的 sys_openat 调用。
-- 使用
SEC 宏定义一个 eBPF 程序,关联到 tracepoint "tracepoint/syscalls/sys_enter_openat"。这个 tracepoint 会在进程发起 sys_openat 系统调用时触发。
+- 使用
SEC 宏定义一个 eBPF 程序,关联到 tracepoint "tracepoint/syscalls/sys_enter_openat"。这个 tracepoint 会在进程发起 sys_openat 系统调用时触发。
- 实现 eBPF 程序
tracepoint__syscalls__sys_enter_openat,它接收一个类型为 struct trace_event_raw_sys_enter 的参数 ctx。这个结构体包含了关于系统调用的信息。
- 使用
bpf_get_current_pid_tgid() 函数获取当前进程的 PID 和 TID(线程 ID)。由于我们只关心 PID,所以将其值右移 32 位赋值给 u32 类型的变量 pid。
- 检查
pid_target 变量是否与当前进程的 pid 相等。如果 pid_target 不为 0 且与当前进程的 pid 不相等,则返回 false,不对该进程的 sys_openat 调用进行捕获。
- 使用
bpf_printk() 函数打印捕获到的进程 ID 和 sys_openat 调用的相关信息。这些信息可以在用户空间通过 BPF 工具查看。
-- 将程序许可证设置为 "GPL",这是运行 eBPF 程序的必要条件。
+- 将程序许可证设置为 "GPL",这是运行 eBPF 程序的必要条件。
这个 eBPF 程序可以通过 libbpf 或 eunomia-bpf 等工具加载到内核并执行。它将捕获指定进程(或所有进程)的 sys_openat 系统调用,并在用户空间输出相关信息。
eunomia-bpf 是一个结合 Wasm 的开源 eBPF 动态加载运行时和开发工具链,它的目的是简化 eBPF 程序的开发、构建、分发、运行。可以参考 https://github.com/eunomia-bpf/eunomia-bpf 下载和安装 ecc 编译工具链和 ecli 运行时。我们使用 eunomia-bpf 编译运行这个例子。完整代码请查看 https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/4-opensnoop 。
diff --git a/5-uprobe-bashreadline/index.html b/5-uprobe-bashreadline/index.html
index 19f6b08..4f66935 100644
--- a/5-uprobe-bashreadline/index.html
+++ b/5-uprobe-bashreadline/index.html
@@ -197,11 +197,11 @@
* binary can be an absolute/relative path or a filename; the latter is resolved to a
* full binary path via bpf_program__attach_uprobe_opts.
*
- * Specifying uprobe+ ensures we carry out strict matching; either "uprobe" must be
+ * Specifying uprobe+ ensures we carry out strict matching; either "uprobe" must be
* specified (and auto-attach is not possible) or the above format is specified for
* auto-attach.
*/
-SEC("uretprobe//bin/bash:readline")
+SEC("uretprobe//bin/bash:readline")
int BPF_KRETPROBE(printret, const void *ret)
{
char str[MAX_LINE_SIZE];
@@ -216,18 +216,18 @@ int BPF_KRETPROBE(printret, const void *ret)
pid = bpf_get_current_pid_tgid() >> 32;
bpf_probe_read_user_str(str, sizeof(str), ret);
- bpf_printk("PID %d (%s) read: %s ", pid, comm, str);
+ bpf_printk("PID %d (%s) read: %s ", pid, comm, str);
return 0;
};
-char LICENSE[] SEC("license") = "GPL";
+char LICENSE[] SEC("license") = "GPL";
这段代码的作用是在 bash 的 readline 函数返回时执行指定的 BPF_KRETPROBE 函数,即 printret 函数。
在 printret 函数中,我们首先获取了调用 readline 函数的进程的进程名称和进程 ID,然后通过 bpf_probe_read_user_str 函数读取了用户输入的命令行字符串,最后通过 bpf_printk 函数打印出进程 ID、进程名称和输入的命令行字符串。
除此之外,我们还需要通过 SEC 宏来定义 uprobe 探针,并使用 BPF_KRETPROBE 宏来定义探针函数。
在 SEC 宏中,我们需要指定 uprobe 的类型、要捕获的二进制文件的路径和要捕获的函数名称。例如,上面的代码中的 SEC 宏的定义如下:
-SEC("uprobe//bin/bash:readline")
+SEC("uprobe//bin/bash:readline")
这表示我们要捕获的是 /bin/bash 二进制文件中的 readline 函数。
接下来,我们需要使用 BPF_KRETPROBE 宏来定义探针函数,例如:
@@ -244,7 +244,7 @@ char LICENSE[] SEC("license") = "GPL";
bpf_probe_read_user_str(str, sizeof(str), ret);
最后使用 bpf_printk 函数输出 PID、任务名称和用户输入的字符串。
- bpf_printk("PID %d (%s) read: %s ", pid, comm, str);
+ bpf_printk("PID %d (%s) read: %s ", pid, comm, str);
eunomia-bpf 是一个结合 Wasm 的开源 eBPF 动态加载运行时和开发工具链,它的目的是简化 eBPF 程序的开发、构建、分发、运行。可以参考 https://github.com/eunomia-bpf/eunomia-bpf 下载和安装 ecc 编译工具链和 ecli 运行时。我们使用 eunomia-bpf 编译运行这个例子。
编译运行上述代码:
diff --git a/6-sigsnoop/index.html b/6-sigsnoop/index.html
index 10d5bab..ccaf6ce 100644
--- a/6-sigsnoop/index.html
+++ b/6-sigsnoop/index.html
@@ -198,7 +198,7 @@ struct {
__uint(max_entries, MAX_ENTRIES);
__type(key, __u32);
__type(value, struct event);
-} values SEC(".maps");
+} values SEC(".maps");
static int probe_entry(pid_t tpid, int sig)
@@ -228,9 +228,9 @@ static int probe_exit(void *ctx, int ret)
return 0;
eventp->ret = ret;
- bpf_printk("PID %d (%s) sent signal %d ",
+ bpf_printk("PID %d (%s) sent signal %d ",
eventp->pid, eventp->comm, eventp->sig);
- bpf_printk("to PID %d, ret = %d",
+ bpf_printk("to PID %d, ret = %d",
eventp->tpid, ret);
cleanup:
@@ -238,7 +238,7 @@ cleanup:
return 0;
}
-SEC("tracepoint/syscalls/sys_enter_kill")
+SEC("tracepoint/syscalls/sys_enter_kill")
int kill_entry(struct trace_event_raw_sys_enter *ctx)
{
pid_t tpid = (pid_t)ctx->args[0];
@@ -247,13 +247,13 @@ int kill_entry(struct trace_event_raw_sys_enter *ctx)
return probe_entry(tpid, sig);
}
-SEC("tracepoint/syscalls/sys_exit_kill")
+SEC("tracepoint/syscalls/sys_exit_kill")
int kill_exit(struct trace_event_raw_sys_exit *ctx)
{
return probe_exit(ctx, ctx->ret);
}
-char LICENSE[] SEC("license") = "Dual BSD/GPL";
+char LICENSE[] SEC("license") = "Dual BSD/GPL";
上面的代码定义了一个 eBPF 程序,用于捕获进程发送信号的系统调用,包括 kill、tkill 和 tgkill。它通过使用 tracepoint 来捕获系统调用的进入和退出事件,并在这些事件发生时执行指定的探针函数,例如 probe_entry 和 probe_exit。
在探针函数中,我们使用 bpf_map 存储捕获的事件信息,包括发送信号的进程 ID、接收信号的进程 ID、信号值和进程的可执行文件名称。在系统调用退出时,我们将获取存储在 bpf_map 中的事件信息,并使用 bpf_printk 打印进程 ID、进程名称、发送的信号和系统调用的返回值。
@@ -284,7 +284,7 @@ Runing eBPF program...
__uint(max_entries, MAX_ENTRIES);
__type(key, __u32);
__type(value, struct event);
-} values SEC(".maps");
+} values SEC(".maps");
并使用一些对应的 API 进行访问,例如 bpf_map_lookup_elem、bpf_map_update_elem、bpf_map_delete_elem 等。
更多的例子和详细的开发指南,请参考 eunomia-bpf 的官方文档:https://github.com/eunomia-bpf/eunomia-bpf
diff --git a/7-execsnoop/index.html b/7-execsnoop/index.html
index 14472ea..2fba885 100644
--- a/7-execsnoop/index.html
+++ b/7-execsnoop/index.html
@@ -202,15 +202,15 @@ struct event {
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_core_read.h>
-#include "execsnoop.h"
+#include "execsnoop.h"
struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
__uint(key_size, sizeof(u32));
__uint(value_size, sizeof(u32));
-} events SEC(".maps");
+} events SEC(".maps");
-SEC("tracepoint/syscalls/sys_enter_execve")
+SEC("tracepoint/syscalls/sys_enter_execve")
int tracepoint__syscalls__sys_enter_execve(struct trace_event_raw_sys_enter* ctx)
{
u64 id;
@@ -232,7 +232,7 @@ int tracepoint__syscalls__sys_enter_execve(struct trace_event_raw_sys_enter* ctx
return 0;
}
-char LICENSE[] SEC("license") = "GPL";
+char LICENSE[] SEC("license") = "GPL";
这段代码定义了个 eBPF 程序,用于捕获进程执行 execve 系统调用的入口。
在入口程序中,我们首先获取了当前进程的进程 ID 和用户 ID,然后通过 bpf_get_current_task 函数获取了当前进程的 task_struct 结构体,并通过 bpf_probe_read_str 函数读取了进程名称。最后,我们通过 bpf_perf_event_output 函数将进程执行事件输出到 perf buffer。
@@ -261,7 +261,7 @@ TIME PID PPID UID COMM
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
__uint(key_size, sizeof(u32));
__uint(value_size, sizeof(u32));
-} events SEC(".maps");
+} events SEC(".maps");
就可以往用户态直接发送信息。
更多的例子和详细的开发指南,请参考 eunomia-bpf 的官方文档:https://github.com/eunomia-bpf/eunomia-bpf
diff --git a/8-exitsnoop/index.html b/8-exitsnoop/index.html
index 84d1b55..7dcf89b 100644
--- a/8-exitsnoop/index.html
+++ b/8-exitsnoop/index.html
@@ -214,20 +214,20 @@ struct event {
#endif /* __BOOTSTRAP_H */
源文件:exitsnoop.bpf.c
-#include "vmlinux.h"
+#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
-#include "exitsnoop.h"
+#include "exitsnoop.h"
-char LICENSE[] SEC("license") = "Dual BSD/GPL";
+char LICENSE[] SEC("license") = "Dual BSD/GPL";
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024);
-} rb SEC(".maps");
+} rb SEC(".maps");
-SEC("tp/sched/sched_process_exit")
+SEC("tp/sched/sched_process_exit")
int handle_exit(struct trace_event_raw_sched_process_template* ctx)
{
struct task_struct *task;
@@ -267,7 +267,7 @@ int handle_exit(struct trace_event_raw_sched_process_template* ctx)
这段代码展示了如何使用 exitsnoop 监控进程退出事件并使用 ring buffer 向用户态打印输出:
- 首先,我们引入所需的头文件和 exitsnoop.h。
-- 定义一个名为 "LICENSE" 的全局变量,内容为 "Dual BSD/GPL",这是 eBPF 程序的许可证要求。
+- 定义一个名为 "LICENSE" 的全局变量,内容为 "Dual BSD/GPL",这是 eBPF 程序的许可证要求。
- 定义一个名为 rb 的 BPF_MAP_TYPE_RINGBUF 类型的映射,它将用于将内核空间的数据传输到用户空间。指定 max_entries 为 256 * 1024,代表 ring buffer 的最大容量。
- 定义一个名为 handle_exit 的 eBPF 程序,它将在进程退出事件触发时执行。传入一个名为 ctx 的 trace_event_raw_sched_process_template 结构体指针作为参数。
- 使用 bpf_get_current_pid_tgid() 函数获取当前任务的 PID 和 TID。对于主线程,PID 和 TID 相同;对于子线程,它们是不同的。我们只关心进程(主线程)的退出,因此在 PID 和 TID 不同时返回 0,忽略子线程退出事件。
diff --git a/9-runqlat/index.html b/9-runqlat/index.html
index 9fefc14..e5c6cb1 100644
--- a/9-runqlat/index.html
+++ b/9-runqlat/index.html
@@ -177,7 +177,7 @@
eBPF (Extended Berkeley Packet Filter) 是 Linux 内核上的一个强大的网络和性能分析工具。它允许开发者在内核运行时动态加载、更新和运行用户定义的代码。
runqlat 是一个 eBPF 工具,用于分析 Linux 系统的调度性能。具体来说,runqlat 用于测量一个任务在被调度到 CPU 上运行之前在运行队列中等待的时间。这些信息对于识别性能瓶颈和提高 Linux 内核调度算法的整体效率非常有用。
runqlat 原理
-本教程是 eBPF 入门开发实践系列的第九部分,主题是 "捕获进程调度延迟"。在此,我们将介绍一个名为 runqlat 的程序,其作用是以直方图的形式记录进程调度延迟。
+本教程是 eBPF 入门开发实践系列的第九部分,主题是 "捕获进程调度延迟"。在此,我们将介绍一个名为 runqlat 的程序,其作用是以直方图的形式记录进程调度延迟。
Linux 操作系统使用进程来执行所有的系统和用户任务。这些进程可能被阻塞、杀死、运行,或者正在等待运行。处在后两种状态的进程数量决定了 CPU 运行队列的长度。
进程有几种可能的状态,如:
@@ -190,7 +190,7 @@
等待资源或其他函数信号的进程会处在可中断或不可中断的睡眠状态:进程被置入睡眠状态,直到它需要的资源变得可用。然后,根据睡眠的类型,进程可以转移到可运行状态,或者保持睡眠。
即使进程拥有它需要的所有资源,它也不会立即开始运行。它会转移到可运行状态,与其他处在相同状态的进程一起排队。CPU可以在接下来的几秒钟或毫秒内执行这些进程。调度器为 CPU 排列进程,并决定下一个要执行的进程。
根据系统的硬件配置,这个可运行队列(称为 CPU 运行队列)的长度可以短也可以长。短的运行队列长度表示 CPU 没有被充分利用。另一方面,如果运行队列长,那么可能意味着 CPU 不够强大,无法执行所有的进程,或者 CPU 的核心数量不足。在理想的 CPU 利用率下,运行队列的长度将等于系统中的核心数量。
-进程调度延迟,也被称为 "run queue latency",是衡量线程从变得可运行(例如,接收到中断,促使其处理更多工作)到实际在 CPU 上运行的时间。在 CPU 饱和的情况下,你可以想象线程必须等待其轮次。但在其他奇特的场景中,这也可能发生,而且在某些情况下,它可以通过调优减少,从而提高整个系统的性能。
+进程调度延迟,也被称为 "run queue latency",是衡量线程从变得可运行(例如,接收到中断,促使其处理更多工作)到实际在 CPU 上运行的时间。在 CPU 饱和的情况下,你可以想象线程必须等待其轮次。但在其他奇特的场景中,这也可能发生,而且在某些情况下,它可以通过调优减少,从而提高整个系统的性能。
我们将通过一个示例来阐述如何使用 runqlat 工具。这是一个负载非常重的系统:
# runqlat
Tracing run queue latency... Hit Ctrl-C to end.
@@ -213,7 +213,7 @@ Tracing run queue latency... Hit Ctrl-C to end.
16384 -> 32767 : 809 |****************************************|
32768 -> 65535 : 64 |*** |
-在这个输出中,我们看到了一个双模分布,一个模在0到15微秒之间,另一个模在16到65毫秒之间。这些模式在分布(它仅仅是 "count" 列的视觉表示)中显示为尖峰。例如,读取一行:在追踪过程中,809个事件落入了16384到32767微秒的范围(16到32毫秒)。
+在这个输出中,我们看到了一个双模分布,一个模在0到15微秒之间,另一个模在16到65毫秒之间。这些模式在分布(它仅仅是 "count" 列的视觉表示)中显示为尖峰。例如,读取一行:在追踪过程中,809个事件落入了16384到32767微秒的范围(16到32毫秒)。
在后续的教程中,我们将深入探讨如何利用 eBPF 对此类指标进行深度跟踪和分析,以更好地理解和优化系统性能。同时,我们也将学习更多关于 Linux 内核调度器、中断处理和 CPU 饱
runqlat 的实现利用了 eBPF 程序,它通过内核跟踪点和函数探针来测量进程在运行队列中的时间。当进程被排队时,trace_enqueue 函数会在一个映射中记录时间戳。当进程被调度到 CPU 上运行时,handle_switch 函数会检索时间戳,并计算当前时间与排队时间之间的时间差。这个差值(或 delta)被用于更新进程的直方图,该直方图记录运行队列延迟的分布。该直方图可用于分析 Linux 内核的调度性能。
runqlat 代码实现
@@ -225,10 +225,10 @@ Tracing run queue latency... Hit Ctrl-C to end.
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_core_read.h>
#include <bpf/bpf_tracing.h>
-#include "runqlat.h"
-#include "bits.bpf.h"
-#include "maps.bpf.h"
-#include "core_fixes.bpf.h"
+#include "runqlat.h"
+#include "bits.bpf.h"
+#include "maps.bpf.h"
+#include "core_fixes.bpf.h"
#define MAX_ENTRIES 10240
#define TASK_RUNNING 0
@@ -245,24 +245,24 @@ struct {
__type(key, u32);
__type(value, u32);
__uint(max_entries, 1);
-} cgroup_map SEC(".maps");
+} cgroup_map SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, MAX_ENTRIES);
__type(key, u32);
__type(value, u64);
-} start SEC(".maps");
+} start SEC(".maps");
static struct hist zero;
-/// @sample {"interval": 1000, "type" : "log2_hist"}
+/// @sample {"interval": 1000, "type" : "log2_hist"}
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, MAX_ENTRIES);
__type(key, u32);
__type(value, struct hist);
-} hists SEC(".maps");
+} hists SEC(".maps");
static int trace_enqueue(u32 tgid, u32 pid)
{
@@ -346,7 +346,7 @@ cleanup:
return 0;
}
-SEC("raw_tp/sched_wakeup")
+SEC("raw_tp/sched_wakeup")
int BPF_PROG(handle_sched_wakeup, struct task_struct *p)
{
if (filter_cg && !bpf_current_task_under_cgroup(&cgroup_map, 0))
@@ -355,7 +355,7 @@ int BPF_PROG(handle_sched_wakeup, struct task_struct *p)
return trace_enqueue(BPF_CORE_READ(p, tgid), BPF_CORE_READ(p, pid));
}
-SEC("raw_tp/sched_wakeup_new")
+SEC("raw_tp/sched_wakeup_new")
int BPF_PROG(handle_sched_wakeup_new, struct task_struct *p)
{
if (filter_cg && !bpf_current_task_under_cgroup(&cgroup_map, 0))
@@ -364,13 +364,13 @@ int BPF_PROG(handle_sched_wakeup_new, struct task_struct *p)
return trace_enqueue(BPF_CORE_READ(p, tgid), BPF_CORE_READ(p, pid));
}
-SEC("raw_tp/sched_switch")
+SEC("raw_tp/sched_switch")
int BPF_PROG(handle_sched_switch, bool preempt, struct task_struct *prev, struct task_struct *next)
{
return handle_switch(preempt, prev, next);
}
-char LICENSE[] SEC("license") = "GPL";
+char LICENSE[] SEC("license") = "GPL";
这其中定义了一些常量和全局变量,用于过滤对应的追踪目标:
#define MAX_ENTRIES 10240
@@ -390,14 +390,14 @@ const volatile pid_t targ_tgid = 0;
__type(key, u32);
__type(value, u32);
__uint(max_entries, 1);
-} cgroup_map SEC(".maps");
+} cgroup_map SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, MAX_ENTRIES);
__type(key, u32);
__type(value, u64);
-} start SEC(".maps");
+} start SEC(".maps");
static struct hist zero;
@@ -406,7 +406,7 @@ struct {
__uint(max_entries, MAX_ENTRIES);
__type(key, u32);
__type(value, struct hist);
-} hists SEC(".maps");
+} hists SEC(".maps");
这些映射包括:
@@ -464,9 +464,9 @@ struct {
这些入口点分别处理不同的调度事件,但都会调用 handle_switch 函数来计算进程的调度延迟并更新直方图数据。
最后,程序包含一个许可证声明:
-char LICENSE[] SEC("license") = "GPL";
+char LICENSE[] SEC("license") = "GPL";
-这一声明指定了 eBPF 程序的许可证类型,这里使用的是 "GPL"。这对于许多内核功能是必需的,因为它们要求 eBPF 程序遵循 GPL 许可证。
+这一声明指定了 eBPF 程序的许可证类型,这里使用的是 "GPL"。这对于许多内核功能是必需的,因为它们要求 eBPF 程序遵循 GPL 许可证。
runqlat.h
然后我们需要定义一个头文件runqlat.h,用来给用户态处理从内核态上报的事件:
/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
diff --git a/bcc-documents/kernel-versions.html b/bcc-documents/kernel-versions.html
index c2071e3..8c7a183 100644
--- a/bcc-documents/kernel-versions.html
+++ b/bcc-documents/kernel-versions.html
@@ -265,7 +265,7 @@
性能事件 4.9 0515e5999a46BPF_PROG_TYPE_PERF_EVENT
cgroup套接字过滤 4.10 0e33661de493BPF_PROG_TYPE_CGROUP_SKB
cgroup套接字修改 4.10 610236587600BPF_PROG_TYPE_CGROUP_SOCK
-轻量级隧道(IN) 4.10 3a0af8fd61f9BPF_PROG_TYPE_LWT_IN".lightweight tunnel (OUT)
+轻量级隧道(IN) 4.10 3a0af8fd61f9BPF_PROG_TYPE_LWT_IN".lightweight tunnel (OUT)
轻量级隧道 (OUT) 4.10 3a0af8fd61f9BPF_PROG_TYPE_LWT_OUT