与 Kprobe 相比,uprobe 的核心区别在于其追踪的目标位置不同:它作用于用户空间,而非内核空间。这意味着 uprobe 只能访问用户进程的虚拟内存,通常需要借助 bpf_probe_read_user 等辅助函数来安全地读取用户栈、堆等内存区域的数据。
挂载点依赖
Uprobe 的挂载高度依赖于目标应用程序的符号表(Symbol Table)。如果程序在编译时移除了符号表(即 Stripped binary),定位具体的函数挂载点将变得困难。此时,可以使用 readelf、objdump 或 IDA 等工具分析二进制文件,通过计算匿名符号的相对偏移量来确定挂载位置。
样例代码分析
我们以 libbpf-bootstrap 项目中的 uprobe 示例进行分析:
eBPF 程序部分的关键定义如下:
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("uprobe") 这个段定义本身并未包含两个关键信息:目标文件路径和目标函数偏移量。因此,这个 eBPF 程序不会自动挂载,必须依靠用户态代码进行手动挂载配置。
如果目标文件的路径绝对确定且不会改变,可以尝试将路径直接写入 SEC 宏(例如 SEC("uprobe//usr/bin/bash:readline")),但这通常不够灵活。更通用的做法是在用户态手动挂载:
LIBBPF_OPTS(bpf_uprobe_opts, uprobe_opts);
skel->links.uprobe_add = bpf_program__attach_uprobe_opts(
skel->progs.uprobe_add,
0 /* self pid */,
"/proc/self/exe",
0 /* offset for function */,
&uprobe_opts /* opts */
);
代码解析:
LIBBPF_OPTS 宏用于初始化 bpf_uprobe_opts 结构体变量。
bpf_program__attach_uprobe_opts 是 libbpf 提供的用于手动挂载 uprobe 的核心函数。其内部实现通常包含两种方式:
- 现代方式:直接调用
perf_event_open 系统调用,通过传递特定的配置参数来创建一个匿名的 uprobe 事件。
- 传统方式:需要先向
/sys/kernel/debug/tracing/uprobe_events 文件写入一行格式化命令来注册探针,这源自 Ftrace 系统。
在 eBPF 技术流行之前,Ftrace 是 Linux 系统主要的动态追踪框架。所有的 kprobe 和 uprobe 事件都需要在 Ftrace 中“登记”。Ftrace 将所有的追踪点都抽象为文件系统接口:
- 添加 uprobe:向
uprobe_events 文件写入指令。
- 事件生成:Ftrace 会在
events/uprobes/ 目录下创建对应的子目录。
- 查看输出:读取
trace_pipe 文件获取文本格式的追踪日志。
uprobe 的实现经历了从依赖 Ftrace 到支持 perf_event_open 的演进。
实战工具解析
1. bashreadline
该工具的用户态代码关键部分展示了如何动态解析 ELF 文件以获取函数偏移:
func_off = get_elf_func_offset(readline_so_path, find_readline_function_name(readline_so_path));
if (func_off < 0) {
warn("cound not find readline in %s\n", readline_so_path);
goto cleanup;
}
find_readline_function_name 函数会读取 ELF 文件的符号表,尝试定位 readline_internal_teardown 符号,若未找到则回退到 readline 符号。
get_elf_func_offset 函数则负责计算找到的符号在文件内的偏移量。这是一种实用的动态解析方法,但需注意许多发布版的共享库会剥离符号表。
2. gethostlatency
该工具的 eBPF 程序使用了 uretprobe(uprobe 的返回探针):
SEC("uretprobe")
int BPF_URETPROBE(handle_return)
{
return probe_return(ctx);
}
其用户态挂载代码清晰地展示了如何同时挂载入口(uprobe)和返回(uretprobe)探针到同一个函数(getaddrinfo)上,这是实现函数耗时统计的常见模式:
// 挂载函数入口探针
links[0] = bpf_program__attach_uprobe(obj->progs.handle_entry, false,
target_pid ?: -1, libc_path, func_off);
// 挂载函数返回探针
links[1] = bpf_program__attach_uprobe(obj->progs.handle_return, true,
target_pid ?: -1, libc_path, func_off);
getaddrinfo 是当前最推荐、最标准的 POSIX 接口,用于将域名解析为 IP 地址。在实际的网络编程或性能分析工具开发中,为了全面覆盖,开发者有时需要采用“广撒网”策略,对多个相关的网络解析函数进行追踪。