type
Post
status
Published
slug
2023/01/06/bpf-example-program-uprobes
summary
tags
开发
BPF
eBPF
category
BPF
icon
password
Property
Jan 6, 2023 01:15 PM
created days
用户空间探针允许在用户空间运行的程序中设置动态标志。它们等同于内核探针,用户空间探针是运行在用户空间的监测程序。当我们定义 uprobe 时,内核会在附加的指令上创建陷阱。当程序执行到该指令时,内核将触发事件以回调函数的方式调用探针函数。uprobes 也可以访问程序链接到的任何库只要知道指令的名称,就可以跟踪对应的调用。
与内核探针非常相似,用户空间探针也分为两类: uprobesuretprobes,依赖于插人 BPF 程序在指令执行周期的哪个阶段。接下来,让我们直接演示一些示例。

1 uprobes

般来说,uprobes 是内核在程序特定指令执行之前插入该指令集的钩子。附加 uprobes 到程序的不同版本时要注意,因为在不同版本之间函数签名可能会有所变化。如果你想在程序不同版本上运行 BPF 程序,唯一的方法是确保程序不同版本中函数签名是相同的。在 Linux 中你可以使用 nm 命令列出 ELF 对象文件中包括的所有符号,并检查跟踪指令在程序中是否仍然存在
下面是示例程序:

1.1 main.go

package main import "fmt" func main() { fmt.Println("Hello, BPF") }
我们可以使用go build -o hello-bpf main.go 编译这个Go程序。你能使用命令 nm 获取二进制文件中包括所有的指令点信息。nm 程序是 GNU开发工具包中的程序,可以用来列出目标文件中包括的符号。如果使用 main关键字对符号进行过滤,将得到与下面类似的列表:
notion image
有了符号列表后,你可以在指令执行时进行跟踪,即使多个进程同时执行一个二进制程序,我们也能够使用该方法对程序指令进行跟踪。
为了跟踪上面 Go 程序中的 main 函数什么时候执行,我们可以编写 BPF 程序并将其附加到 uprobe 上,在任何进程调用该指令之前 uprobe 将产生中断:

1.2 example.py

from bcc import BPF bpf_source = """ int trace_go_main(struct pt_regs *ctx) { u64 pid = bpf_get_current_pid_tgid(); bpf_trace_printk("New hello-bpf process running with PID: %d\\n", pid); return 0; } """ bpf = BPF(text = bpf_source) bpf.attach_uprobe(name = "./hello-bpf", sym = "main.main", fn_name = "trace_go_main") bpf.trace_print()
使用函数 bpf_get_current_pid_tgid 获取 hello-bpf 程序的进程标识符 (PID)。
将该程序附加到 uprobe。这个调用需要知道要跟踪的对象 hello-bpf 此为目标文件的绝对路径。程序还需要设置正在跟踪对象的符号 main.main,及要运行的BPF 程序。这样,每次系统中运行 hello-bpf 时我们将在跟踪中获得一条新日志。

1.3 运行结果展示

notion image

2 uretprobes

uretprobeskretprobes 并行探针,适用于用户空间程序使用。它将 BPF 程序附加到指令返回值之上,允许通过 BPF 代码从寄存器中访问返回值。
uprobesuretprobes 的结合使用可以编写更复杂的 BPF 程序。两者的结合可以为我们提供应用程序运行时的全面了解。你可以在函数运行前及结束后注入跟踪代码,则能够收集更多数据来衡量应用程序行为。一个常见的用例是在无须修改应用程序的前提下,衡量一个函数执行所需的时间。
我们将再次使用介绍 “uprobes” 时的 Go程序示例,测量主函数的执行时间。这个 BPF 程序比前面的示例要长,因此,我们将它分为不同的代码块:

2.1 example.py

from bcc import BPF bpf_source = """ BPF_HASH(cache, u64, u64); int trace_start_time(struct pt_regs *ctx) { u64 pid = bpf_get_current_pid_tgid(); u64 start_time_ns = bpf_ktime_get_ns(); cache.update(&pid, &start_time_ns); return 0; } """ bpf_source += """ int print_duration(struct pt_regs *ctx) { u64 pid = bpf_get_current_pid_tgid(); u64 *start_time_ns = cache.lookup(&pid); if (start_time_ns == 0) { return 0; } u64 duration_ns = bpf_ktime_get_ns() - *start_time_ns; bpf_trace_printk("Function call duration: %d\\n", duration_ns); return 0; } """ bpf = BPF(text = bpf_source) bpf.attach_uprobe(name = "./hello-bpf", sym = "main.main", fn_name = "trace_start_time") bpf.attach_uretprobe(name = "./hello-bpf", sym = "main.main", fn_name = "print_duration") bpf.trace_print()
  1. 创建一个 BPF 哈希映射。该映射允许在 uprobe 和uretprobe 函数之间共享数据。在这种情况下,我们使用应用程序 PID 作为键,并将函数的启动时间存储为值。uprobe 函数的两个最有趣的操作如下所述。
  1. 像内核探针一样,以纳秒为单位捕获系统的当前时间。
  1. 在 cache 中创建一个元素保存程序 PID 和当前时间。假设当前时间是应用程序的启动时间。下面是 uretprobe 函数的声明
uretprobe 函数实现指令完成后的附加功能。uretprobe 功能与介绍 kretprobes 时看到的类似:
  1. 获取应用程序的 PID。下面需要使用 PID 找到函数开始时间,我们能够使用映射查找函数获取函数运行前保存的启动时间。
  1. 通过当前时间减去启动时间计算出函数的执行时间。
  1. 在跟踪日志中打印延迟时间,以便我们可以在终端中看到。
最后,将这两个 BPF 函数附加到正确的 bpf探针上:
我们在原始的 uprobe 示例中增加了一行,将打印函数附加到程序的 uretprobe 上。

2.2 运行结果展示

notion image
 
 
欢迎加入喵星计算机技术研究院,原创技术文章第一时间推送。
notion image
 
BPF 学习系列之 - 内核探针 - kprobes 与 kretprobes使用 CoreDNS 对抗 DNS 污染

  • Waline
  • Utterance