eBPF 实践:使用 Uprobe 追踪用户态 Rust 应用
eBPF,即扩展的Berkeley包过滤器(Extended Berkeley Packet Filter),是Linux内核中的一种革命性技术,它允许开发者在内核态中运行自定义的“微程序”,从而在不修改内核代码的情况下改变系统行为或收集系统细粒度的性能数据。
本文讨论如何使用 Uprobe 和 eBPF 追踪用户态 Rust 应用,包括如何获取符号名称并 attach、获取函数参数、获取返回值等。本文是 eBPF 开发者教程的一部分,更详细的内容可以在这里找到:https://eunomia.dev/tutorials/ 源代码在 GitHub 仓库 中开源。
Uprobe
Uprobe是一种用户空间探针,uprobe探针允许在用户空间程序中动态插桩,插桩位置包括:函数入口、特定偏移处,以及函数返回处。当我们定义uprobe时,内核会在附加的指令上创建快速断点指令(x86机器上为int3指令),当程序执行到该指令时,内核将触发事件,程序陷入到内核态,并以回调函数的方式调用探针函数,执行完探针函数再返回到用户态继续执行后序的指令。
uprobe 适用于在用户态去解析一些内核态探针无法解析的流量,例如 http2 流量,https 流量,同时也可以分析程序运行时、业务逻辑等。关于 Uprobe 的更多信息,可以参考:
Uprobe 在内核态 eBPF 运行时,也可能产生比较大的性能开销,这时候也可以考虑使用用户态 eBPF 运行时,例如 bpftime。bpftime 是一个基于 LLVM JIT/AOT 的用户态 eBPF 运行时,它可以在用户态运行 eBPF Uprobe 程序,和内核态的 eBPF 兼容,由于避免了内核态和用户态之间的上下文切换,bpftime 的 Uprobe 开销比内核少约 10 倍,并且也更容易扩展。
Rust
Rust 是一种开源的系统编程语言,注重安全、速度和并行性。它于2010年由Graydon Hoare在Mozilla研究中心开发,并于2015年发布了第一个稳定版本。Rust 语言的设计哲学旨在提供C++的性能优势,同时大幅减少内存安全漏洞。Rust在系统编程领域逐渐受到欢迎,特别是在需要高性能、安全性和可靠性的应用场景,例如操作系统、文件系统、游戏引擎、网络服务等领域。许多大型技术公司,包括Mozilla、Google、Microsoft和Amazon等,都在使用或支持Rust语言。
可以参考 Rust 官方网站 了解更多 Rust 语言的信息,并安装 Rust 的工具链。
最简单的例子:Symbol name mangling
我们先来看一个简单的例子,使用 Uprobe 追踪 Rust 程序的 main 函数,代码如下:
pub fn hello() -> i32 {
println!("Hello, world!");
0
}
fn main() {
hello();
}构建和尝试获取符号:
$ cd helloworld
$ cargo build
$ nm -C target/debug/helloworld | grep 'helloworld::main'
0000000000013ec0 t helloworld::mainrustc 使用 符号名称修饰 为链接器编码唯一的符号名称。Rust 1.97 默认使用 v0 名称修饰,而 nm -C 会显示稳定的反修饰名称。将符号交给 bpftrace 之前,可以先通过地址解析原始符号:
$ main_address=$(nm -C target/debug/helloworld | awk '$2 ~ /^[tT]$/ && $3 ~ /^helloworld::main(::h[[]]+)?$/ && !matched { found = $1; matched = 1 } END { if (matched) print found }')
$ main_symbol=$(nm target/debug/helloworld | awk -v address="$main_address" '$1 == address && $2 ~ /^[tT]$/ && !matched { found = $3; matched = 1 } END { if (matched) print found }')
$ echo "$main_symbol"
_RNvCsiXtUZV4ocyZ_10helloworld4main-C symbol-mangling-version rustc 选项可以选择名称修饰方案,但从当前二进制解析可以避免依赖某一种方案。现在可以追踪 main:
$ sudo bpftrace -e "uprobe:target/debug/helloworld:${main_symbol} { printf(\"Function hello-world called\n\"); }"
Attaching 1 probe...
Function hello-world called追踪多次调用的函数并获取返回值
对于一个更复杂的例子,包含多次调用并演示如何获取返回值:
use std::env;
pub fn hello(i: i32, len: usize) -> i32 {
println!("Hello, world! {} in {}", i, len);
i + len as i32
}
fn main() {
let args: Vec<String> = env::args().collect();
// Skip the first argument, which is the path to the binary, and iterate over the rest
for arg in args.iter().skip(1) {
match arg.parse::<i32>() {
Ok(i) => {
let ret = hello(i, args.len());
println!("return value: {}", ret);
}
Err(_) => {
eprintln!("Error: Argument '{}' is not a valid integer", arg);
}
}
}
}首先,我们需要使用 debug 模式构建,因为 hello 函数在 release 模式下会被内联,不会有自己的符号:
$ cd args
$ cargo build
$ nm -C target/debug/helloworld | grep 'helloworld::hello'
0000000000016f90 t helloworld::hello注意,在 release 模式(cargo build --release)下,只会出现 main 函数符号,因为 hello 在优化过程中被内联了。
Rust 1.97 默认使用 v0 符号名称修饰。bpftrace 需要原始符号,因此先通过稳定的反修饰名称找到地址,再解析对应的原始符号:
$ hello_address=$(nm -C target/debug/helloworld | awk '$2 ~ /^[tT]$/ && $3 ~ /^helloworld::hello(::h[[]]+)?$/ && !matched { found = $1; matched = 1 } END { if (matched) print found }')
$ hello_symbol=$(nm target/debug/helloworld | awk -v address="$hello_address" '$1 == address && $2 ~ /^[tT]$/ && !matched { found = $3; matched = 1 } END { if (matched) print found }')
$ echo "$hello_symbol"
_RNvCsiXtUZV4ocyZ_10helloworld5hello
$ sudo bpftrace -e "uprobe:target/debug/helloworld:${hello_symbol} { printf(\"Function hello called\n\"); }"
Attaching 1 probe...
Function hello called
Function hello called
Function hello called
Function hello called当我们使用多个参数运行程序时,bpftrace 正确地捕获了所有对 hello 函数的调用:
$ ./target/debug/helloworld 1 2 3 4
Hello, world! 1 in 5
return value: 6
Hello, world! 2 in 5
return value: 7
Hello, world! 3 in 5
return value: 8
Hello, world! 4 in 5
return value: 9我们也可以使用 Uretprobe 和同一个已解析的符号来获取返回值:
$ sudo bpftrace -e "uretprobe:target/debug/helloworld:${hello_symbol} { printf(\"Function hello returned: %d\n\", retval); }"
Attaching 1 probe...
Function hello returned: 6
Function hello returned: 7
Function hello returned: 8
Function hello returned: 9通过反修饰名称解析原始符号的方式同时适用于 Rust 的 legacy 和 v0 名称修饰方案。
参考资料
继续阅读
返回索引
eBPF 开发实践教程:基于 CO-RE,通过小工具快速上手 eBPF 开发
这是一个基于 CO-RE(一次编译,到处运行)的 eBPF 的开发教程,提供了从入门到进阶的 eBPF 开发实践,包括基本概念、代码实例、实际应用等内容。和 BCC 不同的是,我们使用 libbpf、Cilium、libbpf-rs、eunomia-bpf 等框架进行开发,包含 C、Go、Rust 等语言的示例。
上一篇 / 上一页
用户空间 eBPF 运行时:深度解析与应用实践
本文旨在对用户空间的 eBPF 运行时和对应的一些应用场景进行剖析和总结。尽管大多数人对基于内核的 eBPF 已有所了解,用户空间 eBPF 的进展和应用实践同样引人注目。本文还将探讨用户空间 eBPF 运行时与 Wasm 运行时的技术比较,后者在云原生和边缘计算领域已获得广泛的关注。我们也新开源了一个用户态 eBPF 运行时 bpftime。通过 LLVM JIT/AOT 后端支持,我们的基准测试表明 bpftime
下一篇 / 下一页
借助 eBPF 和 BTF,让用户态也能一次编译、到处运行
在现代 Linux 系统中,eBPF(扩展的 Berkeley Packet Filter)是一项强大而灵活的技术。它允许在内核中运行沙盒化程序,类似于虚拟机环境,为扩展内核功能提供了一种既安全又不会导致系统崩溃或安全风险的方法。
- 最后更新
- 2026年7月14日
- 首次发布
- 2025年2月10日
- 贡献者
- github-actions[bot], yunwei37, 云微
这个页面有帮助吗?