目录
SEC("uprobe//proc/self/exe:uprobed_sub")
代码分析
BPF 程序分析
// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
/* Copyright (c) 2020 Facebook */
#include <linux/bpf.h>
#include <linux/ptrace.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
char LICENSE[] SEC("license") = "Dual BSD/GPL";
SEC("uprobe")
int BPF_KPROBE(uprobe_add, int a, int b)
{
bpf_printk("uprobed_add ENTRY: a = %d, b = %d", a, b);
return 0;
}
SEC("uretprobe")
int BPF_KRETPROBE(uretprobe_add, int ret)
{
bpf_printk("uprobed_add EXIT: return = %d", ret);
return 0;
}
SEC("uprobe//proc/self/exe:uprobed_sub")
int BPF_KPROBE(uprobe_sub, int a, int b)
{
bpf_printk("uprobed_sub ENTRY: a = %d, b = %d", a, b);
return 0;
}
SEC("uretprobe//proc/self/exe:uprobed_sub")
int BPF_KRETPROBE(uretprobe_sub, int ret)
{
bpf_printk("uprobed_sub EXIT: return = %d", ret);
return 0;
}
功能分析
主要定义了一些 uprobe 和 uretprobe 函数,用于在用户空间程序的特定函数进入和返回时进行跟踪。
头文件引入
- #include <linux/bpf.h>: 这个头文件定义了 BPF 的基本数据类型和函数,如 bpf_map_update_elem、bpf_map_lookup_elem 等。此外,它还定义了 BPF 程序类型(如 BPF_PROG_TYPE_KPROBE)和一些宏(如 BPF_ANY、BPF_NOEXIST 等)。
- #include <linux/ptrace.h>: 这个头文件定义了 struct pt_regs 结构体,这个结构体在 BPF 程序中常用于访问 CPU 寄存器的值。在你的 BPF 程序中,struct pt_regs 是 BPF_KPROBE 和 BPF_KRETPROBE 函数的参数类型。
- #include <bpf/bpf_helpers.h>: 这个头文件定义了一些 BPF 辅助函数,如 bpf_printk。在你的 BPF 程序中,bpf_printk 函数用于打印调试信息。
- #include <bpf/bpf_tracing.h>: 这个头文件定义了一些宏,这些宏用于从 struct pt_regs 结构体中提取参数和返回值。在你的 BPF 程序中,例如 PT_REGS_PARM1(ctx) 和 PT_REGS_RC(ctx) 使用了这个头文件提供的宏。
SEC("uprobe//proc/self/exe:uprobed_sub")
SEC 宏用于定义 BPF 程序的一个 section,这个 section 的名字是 "uprobe//proc/self/exe:uprobed_sub"。
"uprobe//proc/self/exe:uprobed_sub" 这个字符串有三部分:
- "uprobe": 这表示这个 section 是一个 uprobe 类型的 BPF 程序。
- "/proc/self/exe": 这是目标程序的路径。/proc/self/exe 是一个在 Linux 系统中的特殊路径,它是一个符号链接,指向当前进程的可执行文件。
- "uprobed_sub": 这是目标程序中要跟踪的函数的名字。
所以,SEC("uprobe//proc/self/exe:uprobed_sub") 这段代码的意思是:定义一个 uprobe 类型的 BPF 程序,用于在当前进程的可执行文件中的 uprobed_sub 函数入口处设置一个跟踪点。
BPF_KPROBE&BPF_KRETPROBE
BPF_KPROBE 是一个宏,用于定义 BPF (Berkeley Packet Filter) 程序中的 kprobe 或者 uprobe 函数。这个宏在 BPF 辅助库(如 libbpf)中定义。
当 BPF_KPROBE 用于定义一个函数时,它接受两个参数:
- 第一个参数是你的 BPF 函数的名字。
- 第二个参数及其后的参数是你的 BPF 函数的参数。
在实际使用中,BPF_KPROBE 定义的函数会在相应的 kprobe 或 uprobe 事件发生时被调用,例如,当目标内核函数(对于 kprobe)或用户空间函数(对于 uprobe)被调用时。这个函数的参数通常包含一个 struct pt_regs *ctx 指针,这个指针可以用于访问 CPU 寄存器的值。
请注意,BPF_KPROBE 只是一个宏,实际的函数定义需要由你自己提供。这个宏只是用于帮助你更容易地创建 BPF 程序。
用户态程序分析
// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
/* Copyright (c) 2020 Facebook */
#include <errno.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/resource.h>
#include <bpf/libbpf.h>
#include "uprobe.skel.h"
static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
{
return vfprintf(stderr, format, args);
}
ssize_t get_uprobe_offset(const void *addr)
{
size_t start, end, base;
char buf[256];
bool found = false;
FILE *f;
f = fopen("/proc/self/maps", "r");
if (!f)
return -errno;
while (fscanf(f, "%zx-%zx %s %zx %*[^\n]\n", &start, &end, buf, &base) == 4) {
if (buf[2] == 'x' && (uintptr_t)addr >= start && (uintptr_t)addr < end) {
found = true;
break;
}
}
fclose(f);
if (!found)
return -ESRCH;
return (uintptr_t)addr - start + base;
}
/* It's a global function to make sure compiler doesn't inline it. */
int uprobed_add(int a, int b)
{
return a + b;
}
int uprobed_sub(int a, int b)
{
return a - b;
}
int main(int argc, char **argv)
{
struct uprobe_bpf *skel;
long uprobe_offset;
int err, i;
/* Set up libbpf errors and debug info callback */
libbpf_set_print(libbpf_print_fn);
/* Load and verify BPF application */
skel = uprobe_bpf__open_and_load();
if (!skel) {
fprintf(stderr, "Failed to open and load BPF skeleton\n");
return 1;
}
/* uprobe/uretprobe expects relative offset of the function to attach
* to. This offset is relateve to the process's base load address. So
* easy way to do this is to take an absolute address of the desired
* function and substract base load address from it. If we were to
* parse ELF to calculate this function, we'd need to add .text
* section offset and function's offset within .text ELF section.
*/
uprobe_offset = get_uprobe_offset(&uprobed_add);
/* Attach tracepoint handler */
skel->links.uprobe_add =
bpf_program__attach_uprobe(skel->progs.uprobe_add, false /* not uretprobe */,
0 /* self pid */, "/proc/self/exe", uprobe_offset);
if (!skel->links.uprobe_add) {
err = -errno;
fprintf(stderr, "Failed to attach uprobe: %d\n", err);
goto cleanup;
}
/* we can also attach uprobe/uretprobe to any existing or future
* processes that use the same binary executable; to do that we need
* to specify -1 as PID, as we do here
*/
skel->links.uretprobe_add =
bpf_program__attach_uprobe(skel->progs.uretprobe_add, true /* uretprobe */,
-1 /* any pid */, "/proc/self/exe", uprobe_offset);
if (!skel->links.uretprobe_add) {
err = -errno;
fprintf(stderr, "Failed to attach uprobe: %d\n", err);
goto cleanup;
}
/* Let libbpf perform auto-attach for uprobe_sub/uretprobe_sub
* NOTICE: we provide path and symbol info in SEC for BPF programs
*/
err = uprobe_bpf__attach(skel);
if (err) {
fprintf(stderr, "Failed to auto-attach BPF skeleton: %d\n", err);
goto cleanup;
}
printf("Successfully started! Please run `sudo cat /sys/kernel/debug/tracing/trace_pipe` "
"to see output of the BPF programs.\n");
for (i = 0;; i++) {
/* trigger our BPF programs */
fprintf(stderr, ".");
uprobed_add(i, i + 1);
uprobed_sub(i * i, i);
sleep(1);
}
cleanup:
uprobe_bpf__destroy(skel);
return -err;
}
功能说明
- get_uprobe_offset: 这是一个辅助函数,用于通过读取 /proc/self/maps 文件来计算 uprobes 需要的函数偏移量。这个文件包含了当前进程内存映射的信息。
- uprobed_add 和 uprobed_sub: 这两个函数是会被 uprobes 附加的目标。在这个例子中,他们都是简单的函数,一个执行加法,另一个执行减法。
- main 函数: 这是程序的主入口点。
- 首先,它设置了 libbpf 的错误和调试信息回调,然后加载并验证 BPF 程序。
- 接下来,它调用 get_uprobe_offset 函数计算 uprobed_add 函数的偏移量,然后将 uprobes 附加到这个偏移量上。其中 bpf_program__attach_uprobe 函数用于将 Uprobe 附加到指定的程序上。
- 然后,它尝试将一个返回 Uprobe 附加到同一个偏移量上,但这次是为任何使用相同二进制可执行文件的现有或未来的进程。
- 然后,它使用 uprobe_bpf__attach 函数让 libbpf 自动附加 uprobe_sub 和 uretprobe_sub。
- 最后,它进入一个无限循环,定期触发这两个被 uprobes 附加的函数。
/proc/self/maps 文件
/proc/self/maps 文件是 Linux 中的一个虚拟文件,它包含了当前进程的内存映射信息。这个文件是只读的,它列出了进程的各种内存区域,包括可执行文件的各个段(如代码段、数据段)、动态库的映射、堆、栈、内核映射等。
每一行代表一个内存区域,以下是一行的格式示例:
00400000-0040b000 r-xp 00000000 08:01 1971274 /bin/cat
这一行中的各部分含义如下:
- 00400000-0040b000: 这是内存区域的起始和结束地址。
- r-xp: 这表明该内存区域的权限。r 代表可读,w 代表可写,x 代表可执行,p 代表私有(写时复制)。
- 00000000: 这是该区域在文件中的偏移量。
- 08:01: 这是该区域所在设备的主设备号和次设备号。
- 1971274: 这是该区域相关联的 inode 号。
- /bin/cat: 如果该区域是映射到了文件,这里会显示文件的路径。
如果你想查看其他进程的内存映射信息,你可以通过 /proc/<pid>/maps 来查看,其中 <pid> 是你感兴趣的进程的进程 ID。不过,要注意的是,只有当你拥有足够的权限时(通常是 root 权限),你才能查看其他进程的内存映射信息。
在代码中
f = fopen("/proc/self/maps", "r");
if (!f)
return -errno;
while (fscanf(f, "%zx-%zx %s %zx %*[^\n]\n", &start, &end, buf, &base) == 4) {
if (buf[2] == 'x' && (uintptr_t)addr >= start && (uintptr_t)addr < end) {
found = true;
break;
}
}
本质上就是用函数的当前地址,在/proc/self/maps 中找到本函数的执行的内存区域,然后用这个地址去换算偏移量
在
skel->links.uprobe_add =
bpf_program__attach_uprobe(skel->progs.uprobe_add, false /* not uretprobe */,
0 /* self pid */, "/proc/self/exe", uprobe_offset);
中对uprobe_add 函数进行绑定
执行效果
uprobe-53665 [004] d..21 72478.224978: bpf_trace_printk: uprobed_sub ENTRY: a = 49, b = 7
uprobe-53665 [004] d..21 72478.224979: bpf_trace_printk: uprobed_sub EXIT: return = 42
uprobe-53665 [004] d..21 72479.225086: bpf_trace_printk: uprobed_add ENTRY: a = 8, b = 9
uprobe-53665 [004] d..21 72479.225090: bpf_trace_printk: uprobed_add EXIT: return = 17
uprobe-53665 [004] d..21 72479.225091: bpf_trace_printk: uprobed_sub ENTRY: a = 64, b = 8
uprobe-53665 [004] d..21 72479.225093: bpf_trace_printk: uprobed_sub EXIT: return = 56
uprobe-53665 [004] d..21 72480.225396: bpf_trace_printk: uprobed_add ENTRY: a = 9, b = 10
uprobe-53665 [004] d..21 72480.225400: bpf_trace_printk: uprobed_add EXIT: return = 19
uprobe-53665 [004] d..21 72480.225401: bpf_trace_printk: uprobed_sub ENTRY: a = 81, b = 9
uprobe-53665 [004] d..21 72480.225402: bpf_trace_printk: uprobed_sub EXIT: return = 72
uprobe-53665 [004] d..21 72481.225544: bpf_trace_printk: uprobed_add ENTRY: a = 10, b = 11
uprobe-53665 [004] d..21 72481.225547: bpf_trace_printk: uprobed_add EXIT: return = 21
uprobe-53665 [004] d..21 72481.225549: bpf_trace_printk: uprobed_sub ENTRY: a = 100, b = 10
uprobe-53665 [004] d..21 72481.225550: bpf_trace_printk: uprobed_sub EXIT: return = 90
uprobe-53665 [004] d..21 72482.225697: bpf_trace_printk: uprobed_add ENTRY: a = 11, b = 12
uprobe-53665 [004] d..21 72482.225702: bpf_trace_printk: uprobed_add EXIT: return = 23
uprobe-53665 [004] d..21 72482.225705: bpf_trace_printk: uprobed_sub ENTRY: a = 121, b = 11
uprobe-53665 [004] d..21 72482.225707: bpf_trace_printk: uprobed_sub EXIT: return = 110
uprobe-53665 [004] d..21 72483.225810: bpf_trace_printk: uprobed_add ENTRY: a = 12, b = 13
uprobe-53665 [004] d..21 72483.225815: bpf_trace_printk: uprobed_add EXIT: return = 25
uprobe-53665 [004] d..21 72483.225818: bpf_trace_printk: uprobed_sub ENTRY: a = 144, b = 12
uprobe-53665 [004] d..21 72483.225819: bpf_trace_printk: uprobed_sub EXIT: return = 132
uprobe-53665 [004] d..21 72484.225973: bpf_trace_printk: uprobed_add ENTRY: a = 13, b = 14
uprobe-53665 [004] d..21 72484.225978: bpf_trace_printk: uprobed_add EXIT: return = 27
uprobe-53665 [004] d..21 72484.225980: bpf_trace_printk: uprobed_sub ENTRY: a = 169, b = 13
uprobe-53665 [004] d..21 72484.225982: bpf_trace_printk: uprobed_sub EXIT: return = 156