19个版本 (12个重大更改)

0.13.0 2024年7月24日
0.12.0 2024年5月28日
0.9.0 2024年2月7日
0.7.2 2022年12月8日
0.2.0 2022年3月14日

#40 in 调试

Download history 1028/week @ 2024-05-03 986/week @ 2024-05-10 960/week @ 2024-05-17 738/week @ 2024-05-24 844/week @ 2024-05-31 511/week @ 2024-06-07 579/week @ 2024-06-14 277/week @ 2024-06-21 516/week @ 2024-06-28 316/week @ 2024-07-05 668/week @ 2024-07-12 432/week @ 2024-07-19 428/week @ 2024-07-26 301/week @ 2024-08-02 308/week @ 2024-08-09 260/week @ 2024-08-16

每月1,376次下载
用于 4 crate

MIT/Apache

245KB
4.5K SLoC

crates.io page docs.rs page

framehop

Framehop是一个100%用Rust编写的堆栈帧回溯器。它能够在多个平台和架构上以高速度生成高质量的堆栈,而不需要进行昂贵的回溯信息预处理。这使得它适合用于采样分析器。

它目前支持x86_64和aarch64的回溯,并使用Windows、macOS、Linux和Android上常用的回溯信息格式。

你给framehop提供寄存器值、堆栈内存和回溯数据,framehop将生成一个返回地址列表。

Framehop可用于以下场景

  • 远程进程的实时回溯。这是samply使用它的方式。
  • 从保存的寄存器和堆栈字节中进行离线回溯,即使在不同的机器、不同的操作系统或不同的CPU架构上也可以。
  • 同一进程内的实时回溯。这目前尚未得到验证,但只要在采样之前可以进行堆分配,以便分配缓存和更新模块列表,应该可以正常工作。实际的回溯不需要任何堆分配,并且只要使用MustNotAllocateDuringUnwind,即使在信号处理程序内部也应该可以正常工作。

作为framehop的用户,你的责任包括以下内容

  • 你需要提前列出在采样进程中加载的模块(库),或者理想情况下维护一个活着的列表,该列表在模块加载/卸载时更新。
  • 你需要为这些模块提供地址范围和回溯节区数据。
  • 在采样时,你需要提供寄存器值和一个回调来读取任意堆栈内存而不会引发段错误。
  • 在aarch64上,选择正确的掩码来剥离返回地址的指针认证位取决于你。
  • 如果您需要函数名,则需要自行进行符号解析。Framehop只能生成地址,不进行任何符号化。

反过来,Framehop解决了以下问题

  • 它解析多种不同的展开信息格式。目前,它支持以下格式
    • 苹果的压缩展开格式,在 __unwind_info 中(macOS)
    • DWARF CFI 在 .eh_frame 中(如果可用,使用 .eh_frame_hdr 作为索引)
    • DWARF CFI 在 .debug_frame
    • PE 展开信息在 .pdata.rdata.xdata 中(用于 Windows x86_64)
  • 它支持在程序在函数前导或后导中中断时进行正确的展开。在 macOS 上,它必须分析汇编指令才能完成此操作。
  • 在 x86_64 和 aarch64 上,如果找不到地址的展开信息,它会回退到帧指针展开。
  • 它将每个地址的展开规则缓存到固定大小的缓存中,以便从同一地址重复展开时速度更快。
  • 它为没有二分搜索索引的展开信息格式生成二分搜索索引。特别是对于 .debug_frame 和没有 .eh_frame_hdr.eh_frame
  • 它可以合理地检测栈的末尾,因此您可以区分正确结束的栈和提前截断的栈。

Framehop不适用于调试器或实现异常处理。调试器通常需要恢复每个帧的所有寄存器值,而Framehop只关心返回地址。异常处理还需要调用析构函数的能力,这也是Framehop的非目标。

速度

Framehop的速度如此之快,以至于在两种我都尝试过的情况下,堆栈跟踪只是采样中一个非常小的部分。

在这个单线程Rust应用程序分析示例中,跟踪堆栈所需的时间是查询macOS线程寄存器值所需时间的四分之一。在另一个分析没有帧指针的Firefox构建的samply示例中,DWARF展开所需的时间是查询寄存器值的4倍,但总体上仍然比thread_suspend + thread_get_state + thread_resume的成本低。

在这个处理perf.data文件示例中,瓶颈是读取磁盘上的字节,而不是堆栈跟踪。在带有预热文件缓存的情况下,堆栈跟踪的成本仍然与从文件缓存复制字节的成本相当,而大部分堆栈跟踪时间都花在从堆栈字节中读取返回地址上。

Framehop以以下方式实现这种速度

  1. 它只恢复用于计算返回地址所需的寄存器。在x86_64上是 riprsprbp,在aarch64上是 lrspfp。所有其他寄存器都不需要 - 理论上它们可以用作DWARF CFI表达式的输入,但在实践中并不需要。
  2. 它尽可能使用零拷贝解析。例如,__unwind_info 中的字节只在展开期间访问,二分搜索直接在原始 __unwind_info 内存中进行。对于DWARF展开,Framehop使用专注于性能的优秀的gimli crate
  3. 它使用二分搜索在所有支持的展开信息格式中找到正确的展开规则。对于没有内置索引的格式,在模块添加时创建索引。
  4. 它根据地址缓存展开规则。在实践中,509槽位的缓存在像Firefox(缓存在所有Firefox进程中共享)这样的复杂代码上达到了大约80%的命中率。在分析更简单的应用程序时,命中率可能要高得多。

此外,添加模块也非常快,因为framehop仅执行最小的前置解析和处理——实际上,它所做的唯一一件事就是为 .eh_frame / .debug_frame 创建FDE偏移量的索引。

当前状态和路线图

Framehop仍然是一个正在进行中的项目。其API可能会发生变化。API的更新可能至少要到我们实现一个或两个32位架构时才会平静下来。

尽管如此,framehop在支持的平台上的表现非常出色,如果你能忍受频繁的API中断,它绝对值得一试。如果你遇到任何问题或有建议,请提交问题。

最终,我希望将framehop用作Gecko分析器(Firefox内置分析器)中Lul的替代品。为此,我们还需要添加x86支持(针对32位Windows和Linux)和EHABI / EXIDX支持(针对32位ARM Android)。

示例

use framehop::aarch64::{CacheAarch64, UnwindRegsAarch64, UnwinderAarch64};
use framehop::{ExplicitModuleSectionInfo, FrameAddress, Module};

let mut cache = CacheAarch64::<_>::new();
let mut unwinder = UnwinderAarch64::new();

let module = Module::new(
    "mybinary".to_string(),
    0x1003fc000..0x100634000,
    0x1003fc000,
    ExplicitModuleSectionInfo {
        base_svma: 0x100000000,
        text_svma: Some(0x100000b64..0x1001d2d18),
        text: Some(vec![/* __text */]),
        stubs_svma: Some(0x1001d2d18..0x1001d309c),
        stub_helper_svma: Some(0x1001d309c..0x1001d3438),
        got_svma: Some(0x100238000..0x100238010),
        unwind_info: Some(vec![/* __unwind_info */]),
        eh_frame_svma: Some(0x100237f80..0x100237ffc),
        eh_frame: Some(vec![/* __eh_frame */]),
        text_segment_svma: Some(0x1003fc000..0x100634000),
        text_segment: Some(vec![/* __TEXT */]),
        ..Default::default()
    },
);
unwinder.add_module(module);

let pc = 0x1003fc000 + 0x1292c0;
let lr = 0x1003fc000 + 0xe4830;
let sp = 0x10;
let fp = 0x20;
let stack = [
    1, 2, 3, 4, 0x40, 0x1003fc000 + 0x100dc4,
    5, 6, 0x70, 0x1003fc000 + 0x12ca28,
    7, 8, 9, 10, 0x0, 0x0,
];
let mut read_stack = |addr| stack.get((addr / 8) as usize).cloned().ok_or(());

use framehop::Unwinder;
let mut iter = unwinder.iter_frames(
    pc,
    UnwindRegsAarch64::new(lr, sp, fp),
    &mut cache,
    &mut read_stack,
);

let mut frames = Vec::new();
while let Ok(Some(frame)) = iter.next() {
    frames.push(frame);
}

assert_eq!(
    frames,
    vec![
        FrameAddress::from_instruction_pointer(0x1003fc000 + 0x1292c0),
        FrameAddress::from_return_address(0x1003fc000 + 0x100dc4).unwrap(),
        FrameAddress::from_return_address(0x1003fc000 + 0x12ca28).unwrap()
    ]
);

以下是我开发过程中发现的有用文章列表

我非常频繁地使用以下工具

  • Hopper反汇编器,用于查看汇编代码。
  • llvm-dwarfdump --eh-frame mylib.so 用于显示DWARF展开信息。
  • llvm-objdump --section-headers mylib.so 用于显示部分信息。
  • unwindinfodump mylib.dylib 用于显示紧凑的展开信息。(使用 cargo install --examples macho-unwind-info 安装,见 macho-unwind-info。)

许可证

以下任一许可证下授权

根据您的选择。

除非您明确表示,否则根据Apache-2.0许可证的定义,您提交的任何旨在包含在该作品中的贡献,应以上述方式双授权,不附加任何额外条款或条件。

依赖项

约2MB
约46K SLoC