From bcd34c11e7b0e44a9d907f17ddc69e342a6dd15f Mon Sep 17 00:00:00 2001 From: swananan Date: Mon, 24 Aug 2026 19:08:57 +0800 Subject: [PATCH] feat: add Rust backtrace symbol support Demangle Rust application, standard-library, and inline frames with concise symbols by default, while preserving compiler disambiguators with `bt full`. Document compact backtrace modes and cover legacy and v0 mangling, including optimized inline frames, in e2e tests. --- docs/scripting.md | 25 +- docs/zh/scripting.md | 25 +- e2e-tests/tests/common/rust_toolchain.rs | 23 +- .../fixtures/rust_backtrace_program/main.rs | 40 ++ e2e-tests/tests/rust_backtrace_execution.rs | 378 ++++++++++++++++++ .../src/ebpf/codegen/backtrace/plan.rs | 4 +- ghostscope-compiler/src/script/ast.rs | 3 + .../src/script/parser/statement.rs | 6 + .../src/script/parser/tests.rs | 19 + ghostscope-dwarf/src/analyzer/mod.rs | 18 +- ghostscope-dwarf/src/analyzer/plan_pc.rs | 30 +- ghostscope-dwarf/src/core/demangle.rs | 70 +++- .../src/objfile/function_lookup.rs | 158 +++++++- ghostscope-dwarf/src/semantics/mod.rs | 3 +- ghostscope-dwarf/src/semantics/origins.rs | 54 ++- ghostscope-protocol/src/trace_event.rs | 1 + ghostscope/src/trace/backtrace.rs | 8 +- 17 files changed, 813 insertions(+), 52 deletions(-) create mode 100644 e2e-tests/tests/fixtures/rust_backtrace_program/main.rs create mode 100644 e2e-tests/tests/rust_backtrace_execution.rs diff --git a/docs/scripting.md b/docs/scripting.md index 532e7284..d551d1f7 100644 --- a/docs/scripting.md +++ b/docs/scripting.md @@ -866,12 +866,12 @@ trace test_function { } ``` -Options: +The compact syntax is `bt [full|raw] [noinline];`. `backtrace` remains an alias for `bt`. -- `bt raw;` prints raw module cookie, module offset, and runtime IP without source symbolization. -- `bt full;` prints symbolized source-aware frames. Raw IP/cookie debug metadata is kept out of `bt full` and is only shown by `bt raw`. -- `bt inline;` enables inline call-chain rendering. This is the default. -- `bt noinline;` suppresses inline call-chain rendering. +- `bt;` prints source-aware, symbolized frames. Rust compiler disambiguators are hidden by default, so legacy `::h...` suffixes and v0 crate hashes do not clutter normal backtraces. +- `bt full;` keeps those Rust disambiguators in otherwise identical symbolized frames. `full` changes symbol presentation only: it does not collect more frames or variables, change unwinding, or add raw IP/cookie metadata. +- `bt raw;` skips source symbolization and prints the module cookie, module offset, and runtime IP. `raw` and `full` are mutually exclusive. +- Inline call-chain rendering is enabled by default. Append `noinline` to suppress inline pseudo-frames. The explicit `inline` option remains accepted. Backtrace depth is configured globally, not in the script. Use `--backtrace-depth ` or `[ebpf] backtrace_depth = N` in the config file. Valid range is `1..=128`; the default is `128`. In `--script-output pretty`, backtrace payload lines are colorized when `[script] color` enables ANSI output. `--script-output plain` always emits the raw payload text without ANSI color. @@ -880,8 +880,9 @@ Examples: ```ghostscope trace test_function { - bt full; - bt raw noinline; + bt; + bt full noinline; + bt raw; } ``` @@ -895,6 +896,16 @@ backtrace: complete, 4 frames (max 128) #3 at ?? [libc.so.6+0x2a1ca] ``` +For Rust frames, concise and full symbol display differ only in compiler-generated disambiguators: + +```text +bt: my_crate::worker +bt full: my_crate::worker::h05af221e174051e9 # legacy mangling +bt full: my_crate[a0b1c2d3]::worker # v0 mangling +``` + +The exact disambiguator is compiler-generated and may change between builds. Use `full` when distinguishing otherwise identical paths from different crate instances matters. + `bt raw;` keeps the same header but prints machine-facing fields for diagnosis: ```text diff --git a/docs/zh/scripting.md b/docs/zh/scripting.md index 8092421a..3d5d8eff 100644 --- a/docs/zh/scripting.md +++ b/docs/zh/scripting.md @@ -874,12 +874,12 @@ trace test_function { } ``` -参数: +紧凑语法为 `bt [full|raw] [noinline];`。`backtrace` 仍是 `bt` 的别名。 -- `bt raw;` 输出原始 module cookie、模块内偏移和运行时 IP,不做源码符号化。 -- `bt full;` 输出符号化的源码感知栈帧。raw IP/cookie 调试元数据不会出现在 `bt full` 中,只由 `bt raw` 显示。 -- `bt inline;` 输出 inline 调用链;这是默认行为。 -- `bt noinline;` 关闭 inline 调用链输出。 +- `bt;` 输出源码感知的符号化栈帧。默认隐藏 Rust 编译器消歧信息,避免 legacy `::h...` 后缀和 v0 crate hash 干扰普通回溯阅读。 +- `bt full;` 在相同的符号化栈帧中保留这些 Rust 消歧信息。`full` 只改变符号展示:不会采集更多栈帧或变量,不会改变 unwind,也不会增加 raw IP/cookie 元数据。 +- `bt raw;` 跳过源码符号化,输出 module cookie、模块内偏移和运行时 IP。`raw` 与 `full` 互斥。 +- 默认输出 inline 调用链;追加 `noinline` 可隐藏 inline 伪栈帧。显式的 `inline` 参数仍然可用。 Backtrace 深度是全局配置,不再写在脚本里。使用命令行 `--backtrace-depth `,或在配置文件 `[ebpf]` 中设置 `backtrace_depth = N`。合法范围是 `1..=128`,默认值是 `128`。 在 `--script-output pretty` 下,如果 `[script] color` 启用了 ANSI 输出,backtrace payload 会带颜色。`--script-output plain` 始终输出不带 ANSI 的原始 payload 文本。 @@ -888,8 +888,9 @@ Backtrace 深度是全局配置,不再写在脚本里。使用命令行 `--bac ```ghostscope trace test_function { - bt full; - bt raw noinline; + bt; + bt full noinline; + bt raw; } ``` @@ -903,6 +904,16 @@ backtrace: complete, 4 frames (max 128) #3 at ?? [libc.so.6+0x2a1ca] ``` +对于 Rust 栈帧,简洁输出与 full 输出的区别仅在编译器生成的消歧信息: + +```text +bt: my_crate::worker +bt full: my_crate::worker::h05af221e174051e9 # legacy mangling +bt full: my_crate[a0b1c2d3]::worker # v0 mangling +``` + +具体消歧值由编译器生成,可能随构建发生变化。需要区分来自不同 crate 实例、但源码路径相同的符号时再使用 `full`。 + `bt raw;` 使用相同的 header,但会输出面向排障的机器字段: ```text diff --git a/e2e-tests/tests/common/rust_toolchain.rs b/e2e-tests/tests/common/rust_toolchain.rs index 1503a379..e98ec5e1 100644 --- a/e2e-tests/tests/common/rust_toolchain.rs +++ b/e2e-tests/tests/common/rust_toolchain.rs @@ -23,7 +23,17 @@ pub fn compile_standalone_fixture( source: &Path, binary: &Path, ) -> anyhow::Result<()> { - compile_fixture(rustc, toolchain, source, binary, true) + compile_fixture(rustc, toolchain, source, binary, true, &["opt-level=0"]) +} + +pub fn compile_standalone_fixture_with_codegen_options( + rustc: &Path, + toolchain: &str, + source: &Path, + binary: &Path, + codegen_options: &[&str], +) -> anyhow::Result<()> { + compile_fixture(rustc, toolchain, source, binary, true, codegen_options) } pub fn compile_compact_standalone_fixture( @@ -32,7 +42,7 @@ pub fn compile_compact_standalone_fixture( source: &Path, binary: &Path, ) -> anyhow::Result<()> { - compile_fixture(rustc, toolchain, source, binary, false) + compile_fixture(rustc, toolchain, source, binary, false, &["opt-level=0"]) } fn compile_fixture( @@ -41,12 +51,13 @@ fn compile_fixture( source: &Path, binary: &Path, link_dead_code: bool, + codegen_options: &[&str], ) -> anyhow::Result<()> { let mut command = Command::new(rustc); - command - .args(["--edition=2018", "-g"]) - .arg("-C") - .arg("opt-level=0"); + command.args(["--edition=2018", "-g"]); + for option in codegen_options { + command.arg("-C").arg(option); + } if link_dead_code { command.arg("-C").arg("link-dead-code"); } diff --git a/e2e-tests/tests/fixtures/rust_backtrace_program/main.rs b/e2e-tests/tests/fixtures/rust_backtrace_program/main.rs new file mode 100644 index 00000000..87af5535 --- /dev/null +++ b/e2e-tests/tests/fixtures/rust_backtrace_program/main.rs @@ -0,0 +1,40 @@ +#![crate_name = "rust_backtrace_program"] + +use std::hint::black_box; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::thread; +use std::time::Duration; + +static RESULT: AtomicUsize = AtomicUsize::new(0); + +#[no_mangle] +#[inline(never)] +pub extern "C" fn rust_backtrace_probe(value: usize) -> usize { + black_box(value.wrapping_add(1)) +} + +#[inline(always)] +fn rust_backtrace_inline(value: usize) -> usize { + rust_backtrace_probe(value) // INLINE_BACKTRACE_TRACE_POINT +} + +#[inline(never)] +fn rust_backtrace_middle(value: usize) -> usize { + let result = rust_backtrace_inline(value); + black_box(result.wrapping_mul(3)) +} + +#[inline(never)] +fn rust_backtrace_outer(value: usize) -> usize { + let result = rust_backtrace_middle(value); + black_box(result.wrapping_add(value)) +} + +fn main() { + let mut value = 1usize; + loop { + value = rust_backtrace_outer(value); + RESULT.store(value, Ordering::Relaxed); + thread::sleep(Duration::from_millis(25)); + } +} diff --git a/e2e-tests/tests/rust_backtrace_execution.rs b/e2e-tests/tests/rust_backtrace_execution.rs new file mode 100644 index 00000000..637f5ebe --- /dev/null +++ b/e2e-tests/tests/rust_backtrace_execution.rs @@ -0,0 +1,378 @@ +//! Runtime coverage for native Rust backtraces. + +mod common; + +use std::ffi::OsString; +use std::path::{Path, PathBuf}; +use std::time::Duration; + +use common::{ + init, + rust_toolchain::{ + compile_standalone_fixture, compile_standalone_fixture_with_codegen_options, + fixture_tempdir, rustc_for_toolchain, + }, +}; +use serial_test::serial; + +const TOOLCHAIN: &str = "1.88.0"; +const REQUIRE_TOOLCHAIN_ENV: &str = "GHOSTSCOPE_REQUIRE_RUST_188_E2E"; +const BACKTRACE_DEPTH: u8 = 10; +const INLINE_TRACE_MARKER: &str = "INLINE_BACKTRACE_TRACE_POINT"; + +#[derive(Clone, Copy)] +enum BuildProfile { + DebugLegacy, + OptimizedV0, +} + +fn fixture_source_path() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/rust_backtrace_program/main.rs") +} + +fn inline_trace_line() -> anyhow::Result { + let source = std::fs::read_to_string(fixture_source_path())?; + let line = source + .lines() + .position(|line| line.contains(INLINE_TRACE_MARKER)) + .ok_or_else(|| anyhow::anyhow!("missing inline trace marker {INLINE_TRACE_MARKER:?}"))? + + 1; + Ok(u32::try_from(line)?) +} + +fn compile_fixture( + rustc: &Path, + output_dir: &Path, + profile: BuildProfile, +) -> anyhow::Result { + let source = fixture_source_path(); + let binary = match profile { + BuildProfile::DebugLegacy => output_dir.join("rust_backtrace_program"), + BuildProfile::OptimizedV0 => output_dir.join("rust_backtrace_program_optimized_v0"), + }; + match profile { + BuildProfile::DebugLegacy => { + compile_standalone_fixture(rustc, TOOLCHAIN, &source, &binary)?; + } + BuildProfile::OptimizedV0 => compile_standalone_fixture_with_codegen_options( + rustc, + TOOLCHAIN, + &source, + &binary, + &["opt-level=3", "symbol-mangling-version=v0"], + )?, + } + Ok(binary) +} + +fn first_backtrace_block_after<'a>(stdout: &'a str, marker: &str) -> anyhow::Result<&'a str> { + let marker_pos = stdout + .find(marker) + .ok_or_else(|| anyhow::anyhow!("missing marker {marker:?}\nSTDOUT: {stdout}"))?; + let after_marker = &stdout[marker_pos..]; + let header_pos = after_marker + .find("backtrace:") + .ok_or_else(|| anyhow::anyhow!("missing backtrace after {marker:?}\nSTDOUT: {stdout}"))?; + let block = &after_marker[header_pos..]; + let end = block.find("\n[").unwrap_or(block.len()); + Ok(&block[..end]) +} + +fn backtrace_blocks_after(stdout: &str, marker: &str) -> anyhow::Result> { + let marker_pos = stdout + .find(marker) + .ok_or_else(|| anyhow::anyhow!("missing marker {marker:?}\nSTDOUT: {stdout}"))?; + let mut blocks = Vec::new(); + let mut current: Option = None; + + for line in stdout[marker_pos..].lines() { + let trimmed = line.trim_start(); + if trimmed.starts_with("backtrace:") { + if let Some(block) = current.take() { + blocks.push(block); + } + current = Some(format!("{trimmed}\n")); + continue; + } + if line.starts_with('[') { + if let Some(block) = current.take() { + blocks.push(block); + } + continue; + } + if let Some(block) = current.as_mut() { + block.push_str(trimmed); + block.push('\n'); + } + } + if let Some(block) = current { + blocks.push(block); + } + + anyhow::ensure!( + !blocks.is_empty(), + "missing backtrace block after marker {marker:?}\nSTDOUT: {stdout}" + ); + for block in &blocks { + anyhow::ensure!( + block.contains(&format!("(max {BACKTRACE_DEPTH})")), + "backtrace block has wrong configured depth\nBLOCK:\n{block}" + ); + } + Ok(blocks) +} + +fn assert_ordered_patterns(block: &str, patterns: &[&str]) -> anyhow::Result<()> { + let mut cursor = 0usize; + for pattern in patterns { + let Some(relative) = block[cursor..].find(pattern) else { + anyhow::bail!("missing ordered pattern {pattern:?}\nBLOCK:\n{block}"); + }; + cursor += relative + pattern.len(); + } + Ok(()) +} + +async fn run_rust_backtrace_case(profile: BuildProfile) -> anyhow::Result<()> { + init(); + + let Some(rustc) = rustc_for_toolchain(TOOLCHAIN) else { + anyhow::ensure!( + std::env::var_os(REQUIRE_TOOLCHAIN_ENV).is_none(), + "required Rust toolchain {TOOLCHAIN} is not installed" + ); + eprintln!("skipping unavailable Rust toolchain {TOOLCHAIN}"); + return Ok(()); + }; + + let temp_dir = fixture_tempdir()?; + let binary = compile_fixture(&rustc, temp_dir.path(), profile)?; + let target = common::targets::TargetLauncher::binary(&binary) + .current_dir(temp_dir.path()) + .spawn() + .await?; + tokio::time::sleep(Duration::from_millis(750)).await; + + let backtrace_statement = match profile { + BuildProfile::DebugLegacy => "bt;", + BuildProfile::OptimizedV0 => "bt full;", + }; + let script = format!( + r#" +trace rust_backtrace_probe {{ + print "RUST_BACKTRACE"; + {backtrace_statement} +}} +"# + ); + let result = common::runner::GhostscopeRunner::new() + .with_script(&script) + .attach_to(&target) + .timeout_secs(6) + .enable_sysmon_for_target(false) + .with_cli_args(vec![ + OsString::from("--script-output-events-per-sec"), + OsString::from("2"), + OsString::from("--backtrace-depth"), + OsString::from(BACKTRACE_DEPTH.to_string()), + ]) + .run() + .await; + + target.terminate().await?; + let (exit_code, stdout, stderr) = result?; + assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}"); + + let block = first_backtrace_block_after(&stdout, "RUST_BACKTRACE")?; + assert!( + block.contains(&format!("(max {BACKTRACE_DEPTH})")), + "backtrace block has wrong configured depth\nBLOCK:\n{block}" + ); + assert_ordered_patterns( + block, + &[ + "#0 rust_backtrace_probe", + "#1 rust_backtrace_program", + "rust_backtrace_middle", + "#2 rust_backtrace_program", + "rust_backtrace_outer", + "#3 rust_backtrace_program", + "::main", + ], + )?; + if matches!(profile, BuildProfile::DebugLegacy) { + // Keep the unoptimized profile as an explicit contract for physical + // frames from core and std. Optimized builds may inline these frames. + assert_ordered_patterns( + block, + &[ + "#4 core::ops::function::FnOnce::call_once", + "#5 std::sys::backtrace::__rust_begin_short_backtrace", + "#6 std::rt::lang_start::{{closure}}", + "#7 std::rt::lang_start_internal", + "#8 std::rt::lang_start", + ], + )?; + assert!( + !block.contains("::h"), + "default bt should hide Rust symbol hashes\nBLOCK:\n{block}" + ); + } else { + assert!( + block.contains("rust_backtrace_program["), + "bt full should retain Rust v0 crate disambiguators\nBLOCK:\n{block}" + ); + } + assert!( + !block.contains("_ZN") && !block.contains(" _R"), + "Rust physical frames should be demangled\nBLOCK:\n{block}" + ); + + Ok(()) +} + +async fn run_rust_inline_backtrace_display_case() -> anyhow::Result<()> { + init(); + + let Some(rustc) = rustc_for_toolchain(TOOLCHAIN) else { + anyhow::ensure!( + std::env::var_os(REQUIRE_TOOLCHAIN_ENV).is_none(), + "required Rust toolchain {TOOLCHAIN} is not installed" + ); + eprintln!("skipping unavailable Rust toolchain {TOOLCHAIN}"); + return Ok(()); + }; + + let temp_dir = fixture_tempdir()?; + let binary = compile_fixture(&rustc, temp_dir.path(), BuildProfile::OptimizedV0)?; + let trace_line = inline_trace_line()?; + let analyzer = ghostscope_dwarf::DwarfAnalyzer::from_exec_path(&binary).await?; + let inline_address = analyzer + .lookup_addresses_by_source_line("main.rs", trace_line) + .into_iter() + .find(|address| { + analyzer.resolve_pc(address).ok().is_some_and(|context| { + context + .inline_chain + .iter() + .any(|frame| frame.function_name.as_deref() == Some("rust_backtrace_inline")) + }) + }) + .ok_or_else(|| { + anyhow::anyhow!("missing rust_backtrace_inline context for main.rs:{trace_line}") + })?; + + let concise_context = analyzer.resolve_pc_for_display(&inline_address, false)?; + let concise_inline_name = concise_context + .inline_chain + .iter() + .find_map(|frame| { + frame + .function_name + .as_deref() + .filter(|name| name.ends_with("::rust_backtrace_inline")) + }) + .ok_or_else(|| anyhow::anyhow!("missing concise Rust inline name: {concise_context:?}"))?; + assert_eq!( + concise_inline_name, "rust_backtrace_program::rust_backtrace_inline", + "concise inline name should hide the v0 crate disambiguator" + ); + + let full_context = analyzer.resolve_pc_for_display(&inline_address, true)?; + let full_inline_name = full_context + .inline_chain + .iter() + .find_map(|frame| { + frame + .function_name + .as_deref() + .filter(|name| name.ends_with("::rust_backtrace_inline")) + }) + .ok_or_else(|| anyhow::anyhow!("missing full Rust inline name: {full_context:?}"))?; + assert!( + full_inline_name.starts_with("rust_backtrace_program["), + "full inline name should retain the v0 crate disambiguator: {full_inline_name}" + ); + + let target = common::targets::TargetLauncher::binary(&binary) + .current_dir(temp_dir.path()) + .spawn() + .await?; + tokio::time::sleep(Duration::from_millis(750)).await; + + let source = fixture_source_path(); + let script = format!( + r#" +trace {}:{trace_line} {{ + print "RUST_INLINE_BACKTRACE"; + bt; + bt full; +}} +"#, + source.display() + ); + let result = common::runner::GhostscopeRunner::new() + .with_script(&script) + .attach_to(&target) + .timeout_secs(6) + .enable_sysmon_for_target(false) + .with_cli_args(vec![ + OsString::from("--script-output-events-per-sec"), + OsString::from("2"), + OsString::from("--backtrace-depth"), + OsString::from(BACKTRACE_DEPTH.to_string()), + ]) + .run() + .await; + + target.terminate().await?; + let (exit_code, stdout, stderr) = result?; + assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}"); + + let blocks = backtrace_blocks_after(&stdout, "RUST_INLINE_BACKTRACE")?; + anyhow::ensure!( + blocks.len() >= 2, + "expected concise and full inline backtraces\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}" + ); + let concise_inline = blocks[0] + .lines() + .find(|line| line.contains("#0.inline") && line.contains("rust_backtrace_inline")) + .ok_or_else(|| anyhow::anyhow!("missing concise inline frame\nBLOCK:\n{}", blocks[0]))?; + assert!( + !concise_inline.contains("rust_backtrace_program[") && !concise_inline.contains("::h"), + "default bt should hide inline Rust disambiguators\nLINE:\n{concise_inline}" + ); + + let full_inline = blocks[1] + .lines() + .find(|line| line.contains("#0.inline") && line.contains("rust_backtrace_inline")) + .ok_or_else(|| anyhow::anyhow!("missing full inline frame\nBLOCK:\n{}", blocks[1]))?; + assert!( + full_inline.contains("rust_backtrace_program[") + && full_inline.contains("::rust_backtrace_inline"), + "bt full should retain inline Rust disambiguators\nLINE:\n{full_inline}" + ); + + Ok(()) +} + +// Backtrace programs load verifier-heavy eBPF, so serialize them with the +// existing native backtrace suite. +#[tokio::test] +#[serial(backtrace_execution)] +async fn test_rust_188_backtrace_hides_hashes_in_application_and_std_frames() -> anyhow::Result<()> +{ + run_rust_backtrace_case(BuildProfile::DebugLegacy).await +} + +#[tokio::test] +#[serial(backtrace_execution)] +async fn test_rust_188_full_v0_backtrace_unwinds_with_disambiguators() -> anyhow::Result<()> { + run_rust_backtrace_case(BuildProfile::OptimizedV0).await +} + +#[tokio::test] +#[serial(backtrace_execution)] +async fn test_rust_188_inline_frames_follow_concise_and_full_display_modes() -> anyhow::Result<()> { + run_rust_inline_backtrace_display_case().await +} diff --git a/ghostscope-compiler/src/ebpf/codegen/backtrace/plan.rs b/ghostscope-compiler/src/ebpf/codegen/backtrace/plan.rs index 5362ab65..b1bc1034 100644 --- a/ghostscope-compiler/src/ebpf/codegen/backtrace/plan.rs +++ b/ghostscope-compiler/src/ebpf/codegen/backtrace/plan.rs @@ -292,12 +292,12 @@ mod tests { #[test] fn backtrace_flags_follow_statement_options() { let flags = backtrace_flags(&BacktraceStatement { - raw: true, + raw: false, full: true, inline: false, }); - assert_eq!(flags & BACKTRACE_FLAG_RAW, BACKTRACE_FLAG_RAW); + assert_eq!(flags & BACKTRACE_FLAG_RAW, 0); assert_eq!(flags & BACKTRACE_FLAG_FULL, BACKTRACE_FLAG_FULL); assert_eq!(flags & BACKTRACE_FLAG_INLINE, 0); } diff --git a/ghostscope-compiler/src/script/ast.rs b/ghostscope-compiler/src/script/ast.rs index b31e805d..c635ebb2 100644 --- a/ghostscope-compiler/src/script/ast.rs +++ b/ghostscope-compiler/src/script/ast.rs @@ -92,8 +92,11 @@ pub enum Statement { #[derive(Debug, Clone, PartialEq, Eq)] pub struct BacktraceStatement { + /// Emit machine-facing frame fields instead of symbolized names. pub raw: bool, + /// Retain compiler disambiguators in symbolized Rust frame names. pub full: bool, + /// Include source-level inline pseudo-frames. pub inline: bool, } diff --git a/ghostscope-compiler/src/script/parser/statement.rs b/ghostscope-compiler/src/script/parser/statement.rs index a131d499..c0360f86 100644 --- a/ghostscope-compiler/src/script/parser/statement.rs +++ b/ghostscope-compiler/src/script/parser/statement.rs @@ -26,6 +26,12 @@ fn parse_backtrace_stmt(pair: Pair) -> Result { } } + if stmt.raw && stmt.full { + return Err(ParseError::SyntaxError( + "Backtrace options 'raw' and 'full' are mutually exclusive".to_string(), + )); + } + Ok(stmt) } pub(super) fn parse_statement(pair: Pair) -> Result { diff --git a/ghostscope-compiler/src/script/parser/tests.rs b/ghostscope-compiler/src/script/parser/tests.rs index 13f8320e..bab54492 100644 --- a/ghostscope-compiler/src/script/parser/tests.rs +++ b/ghostscope-compiler/src/script/parser/tests.rs @@ -997,6 +997,25 @@ fn parse_backtrace_and_bt_statements() { } } +#[test] +fn parse_backtrace_rejects_raw_full_conflict() { + let result = parse( + r#" +trace foo { + bt raw full; +} +"#, + ); + + match result { + Err(ParseError::SyntaxError(message)) => { + assert!(message.contains("'raw' and 'full'"), "{message}"); + assert!(message.contains("mutually exclusive"), "{message}"); + } + other => panic!("expected SyntaxError, got {other:?}"), + } +} + #[test] fn parse_backtrace_rejects_named_depth_option() { let s = r#" diff --git a/ghostscope-dwarf/src/analyzer/mod.rs b/ghostscope-dwarf/src/analyzer/mod.rs index bd3996b9..eea30ec4 100644 --- a/ghostscope-dwarf/src/analyzer/mod.rs +++ b/ghostscope-dwarf/src/analyzer/mod.rs @@ -2,8 +2,8 @@ use crate::{ core::{ - mapping::ModuleMapping, CallerFrameRecovery, DebugInfoSource, ModuleAddress, Result, - SectionType, SourceLocation, + demangle::RustSymbolHashDisplay, mapping::ModuleMapping, CallerFrameRecovery, + DebugInfoSource, ModuleAddress, Result, SectionType, SourceLocation, }, loader::ExplicitDebugFile, objfile::LoadedObjfile, @@ -228,11 +228,23 @@ impl DwarfAnalyzer { fn find_function_name_by_module_address( &self, module_address: &ModuleAddress, + ) -> Option { + self.find_function_name_by_module_address_for_display( + module_address, + RustSymbolHashDisplay::Shown, + ) + } + + fn find_function_name_by_module_address_for_display( + &self, + module_address: &ModuleAddress, + rust_hashes: RustSymbolHashDisplay, ) -> Option { self.loaded_module_path_for(&module_address.module_path) .and_then(|module_path| self.modules.get(module_path)) .and_then(|module_data| { - module_data.find_function_name_by_address(module_address.address) + module_data + .find_function_name_by_address_for_display(module_address.address, rust_hashes) }) } diff --git a/ghostscope-dwarf/src/analyzer/plan_pc.rs b/ghostscope-dwarf/src/analyzer/plan_pc.rs index 858dce93..6c4253c0 100644 --- a/ghostscope-dwarf/src/analyzer/plan_pc.rs +++ b/ghostscope-dwarf/src/analyzer/plan_pc.rs @@ -1,6 +1,6 @@ use super::DwarfAnalyzer; use crate::{ - core::{ModuleAddress, Provenance, Result}, + core::{demangle::RustSymbolHashDisplay, ModuleAddress, Provenance, Result}, semantics::{ AddressSpaceInfo, FunctionParameter, PcContext, PcLineInfo, PcRange, PlanError, VariableAccessPath, VariableAccessSegment, VariableReadPlan, VisibleVariable, @@ -37,6 +37,34 @@ impl DwarfAnalyzer { Ok(context) } + /// Resolve a PC while selecting whether user-facing Rust names retain + /// compiler disambiguators. Semantic identity and address resolution are + /// unchanged; only physical and inline function names are reformatted. + pub fn resolve_pc_for_display( + &self, + module_address: &ModuleAddress, + show_rust_hashes: bool, + ) -> Result { + let mut context = self.resolve_pc(module_address)?; + let rust_hashes = if show_rust_hashes { + RustSymbolHashDisplay::Shown + } else { + RustSymbolHashDisplay::Hidden + }; + context.function_name = + self.find_function_name_by_module_address_for_display(module_address, rust_hashes); + if let Some(module_data) = self + .loaded_module_path_for(&module_address.module_path) + .and_then(|module_path| self.modules.get(module_path)) + { + for inline_frame in &mut context.inline_chain { + inline_frame.function_name = + module_data.find_inline_function_name_for_display(inline_frame, rust_hashes); + } + } + Ok(context) + } + fn resolve_pc_uncached(&self, module_address: &ModuleAddress) -> Result { let module_path = self .loaded_module_path_for(&module_address.module_path) diff --git a/ghostscope-dwarf/src/core/demangle.rs b/ghostscope-dwarf/src/core/demangle.rs index 0bd31bc5..48eea681 100644 --- a/ghostscope-dwarf/src/core/demangle.rs +++ b/ghostscope-dwarf/src/core/demangle.rs @@ -2,9 +2,23 @@ use gimli::DwLang; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum RustSymbolHashDisplay { + Hidden, + Shown, +} + /// Demangle a symbol string using language hint when available. /// Returns None if demangling fails or is not applicable. pub fn demangle_by_lang(lang: Option, s: &str) -> Option { + demangle_by_lang_for_display(lang, s, RustSymbolHashDisplay::Shown) +} + +pub(crate) fn demangle_by_lang_for_display( + lang: Option, + s: &str, + rust_hashes: RustSymbolHashDisplay, +) -> Option { let looks_rust = is_rust_mangled(s) || looks_like_legacy_rust(s); let looks_cpp = is_itanium_cpp_mangled(s); if !looks_rust && !looks_cpp { @@ -16,7 +30,7 @@ pub fn demangle_by_lang(lang: Option, s: &str) -> Option { // match the linkage symbol's mangling style. match lang { Some(gimli::DW_LANG_Rust) if looks_rust => { - if let Some(d) = demangle_rust(s) { + if let Some(d) = demangle_rust(s, rust_hashes) { return Some(d); } } @@ -36,7 +50,7 @@ pub fn demangle_by_lang(lang: Option, s: &str) -> Option { // Fall back heuristically when the hint was missing, mismatched, or failed. if looks_rust { - if let Some(d) = demangle_rust(s) { + if let Some(d) = demangle_rust(s, rust_hashes) { return Some(d); } } @@ -93,9 +107,12 @@ pub fn is_itanium_cpp_mangled(s: &str) -> bool { s.starts_with("_Z") } -fn demangle_rust(s: &str) -> Option { +fn demangle_rust(s: &str, hash_display: RustSymbolHashDisplay) -> Option { match rustc_demangle::try_demangle(s) { - Ok(sym) => Some(sym.to_string()), + Ok(sym) => Some(match hash_display { + RustSymbolHashDisplay::Hidden => format!("{sym:#}"), + RustSymbolHashDisplay::Shown => sym.to_string(), + }), Err(_) => None, } } @@ -109,7 +126,9 @@ fn demangle_cpp(s: &str) -> Option { #[cfg(test)] mod tests { - use super::{demangle_by_lang, is_likely_mangled}; + use super::{ + demangle_by_lang, demangle_by_lang_for_display, is_likely_mangled, RustSymbolHashDisplay, + }; #[test] fn skips_plain_names_even_with_language_hint() { @@ -159,4 +178,45 @@ mod tests { demangle_by_lang(Some(gimli::DW_LANG_Rust), rust_name) ); } + + #[test] + fn rust_display_can_hide_legacy_hashes_and_v0_disambiguators() { + let legacy = "_ZN4test4main17h05af221e174051e9E"; + let v0 = "_RNvCs73fAdSrgOJL_4test4main"; + + assert_eq!( + demangle_by_lang_for_display( + Some(gimli::DW_LANG_Rust), + legacy, + RustSymbolHashDisplay::Hidden, + ) + .as_deref(), + Some("test::main") + ); + assert_eq!( + demangle_by_lang_for_display( + Some(gimli::DW_LANG_Rust), + legacy, + RustSymbolHashDisplay::Shown, + ) + .as_deref(), + Some("test::main::h05af221e174051e9") + ); + assert_eq!( + demangle_by_lang_for_display( + Some(gimli::DW_LANG_Rust), + v0, + RustSymbolHashDisplay::Hidden, + ) + .as_deref(), + Some("test::main") + ); + let full_v0 = demangle_by_lang_for_display( + Some(gimli::DW_LANG_Rust), + v0, + RustSymbolHashDisplay::Shown, + ) + .expect("v0 symbol should demangle"); + assert!(full_v0.starts_with("test[") && full_v0.ends_with("::main")); + } } diff --git a/ghostscope-dwarf/src/objfile/function_lookup.rs b/ghostscope-dwarf/src/objfile/function_lookup.rs index 0c4f4354..317d847d 100644 --- a/ghostscope-dwarf/src/objfile/function_lookup.rs +++ b/ghostscope-dwarf/src/objfile/function_lookup.rs @@ -1,11 +1,17 @@ use super::LoadedObjfile; use crate::{ binary::DwarfReader, - core::{demangled_name, normalize_demangled_signature, symbol_name_matches_query, Result}, + core::{ + demangle::{demangle_by_lang_for_display, is_rust_mangled, RustSymbolHashDisplay}, + demangled_name, normalize_demangled_signature, symbol_name_matches_query, Result, + }, dwarf_expr::{errors as expr_errors, modes::DwarfExprMode}, index::{GdbSymbolKind, LightweightIndex}, parser::RangeExtractor, - semantics::{range_contains_pc, resolve_attr_with_unit_origins, resolve_origin_entry}, + semantics::{ + range_contains_pc, resolve_attr_with_unit_origins, resolve_linkage_name_with_origins, + resolve_origin_entry, InlineFrame, + }, }; use std::collections::HashSet; @@ -14,6 +20,29 @@ use std::collections::HashSet; // caller-side setup PCs still fall back to the range start. const MAX_INLINE_POINT_ENTRY_PC_GAP: u64 = 32; +fn function_name_for_display( + entry: &crate::core::IndexEntry, + rust_hashes: RustSymbolHashDisplay, +) -> String { + if !is_rust_mangled(&entry.name) { + return entry.name.to_string(); + } + + demangle_by_lang_for_display(entry.language, &entry.name, rust_hashes) + .unwrap_or_else(|| entry.name.to_string()) +} + +fn inline_function_name_for_display( + linkage_name: Option<&str>, + fallback_name: Option<&str>, + rust_hashes: RustSymbolHashDisplay, +) -> Option { + linkage_name + .filter(|name| is_rust_mangled(name)) + .and_then(|name| demangle_by_lang_for_display(Some(gimli::DW_LANG_Rust), name, rust_hashes)) + .or_else(|| fallback_name.map(str::to_owned)) +} + impl LoadedObjfile { pub(crate) fn lookup_function_addresses(&self, name: &str) -> Vec { tracing::debug!("LoadedObjfile: looking up function '{}'", name); @@ -812,16 +841,46 @@ impl LoadedObjfile { } pub(crate) fn find_function_name_by_address(&self, address: u64) -> Option { + self.find_function_name_by_address_for_display(address, RustSymbolHashDisplay::Shown) + } + + pub(crate) fn find_function_name_by_address_for_display( + &self, + address: u64, + rust_hashes: RustSymbolHashDisplay, + ) -> Option { self.find_function_index_entry_by_address(address) - .map(|entry| entry.name.to_string()) + .map(|entry| function_name_for_display(&entry, rust_hashes)) + } + + pub(crate) fn find_inline_function_name_for_display( + &self, + inline_frame: &InlineFrame, + rust_hashes: RustSymbolHashDisplay, + ) -> Option { + let cu_offset = gimli::DebugInfoOffset(inline_frame.concrete_die.cu.0 as usize); + let die_offset = gimli::UnitOffset(inline_frame.concrete_die.offset as usize); + let linkage_name = self.unit(cu_offset).ok().and_then(|unit| { + let entry = unit.entry(die_offset).ok()?; + resolve_linkage_name_with_origins(self.dwarf(), &unit, &entry) + .ok() + .flatten() + }); + + inline_function_name_for_display( + linkage_name.as_deref(), + inline_frame.function_name.as_deref(), + rust_hashes, + ) } } #[cfg(test)] mod tests { use super::super::LoadedObjfile; + use super::{function_name_for_display, inline_function_name_for_display}; use crate::binary::{dwarf_reader_from_arc, DwarfReader}; - use crate::core::{FunctionDieKind, IndexEntry, IndexFlags}; + use crate::core::{demangle::RustSymbolHashDisplay, FunctionDieKind, IndexEntry, IndexFlags}; use crate::index::LightweightIndex; use gimli::constants; use gimli::write::{ @@ -1477,6 +1536,97 @@ mod tests { ); } + #[test] + fn address_display_names_control_only_rust_disambiguators() { + let entry = |name: &str, language| IndexEntry { + name: Arc::::from(name), + die_offset: gimli::UnitOffset(0), + unit_offset: gimli::DebugInfoOffset(0), + tag: constants::DW_TAG_subprogram, + flags: IndexFlags { + is_linkage: true, + ..Default::default() + }, + language, + representative_addr: Some(0x1000), + entry_pc: Some(0x1000), + function_kind: FunctionDieKind::ConcreteSubprogram, + }; + + let legacy = entry( + "_ZN22rust_backtrace_program21rust_backtrace_middle17hbd24b59facb94bf3E", + Some(gimli::DW_LANG_Rust), + ); + let legacy_concise = function_name_for_display(&legacy, RustSymbolHashDisplay::Hidden); + assert_eq!( + legacy_concise, + "rust_backtrace_program::rust_backtrace_middle" + ); + let legacy_full = function_name_for_display(&legacy, RustSymbolHashDisplay::Shown); + assert!(legacy_full.ends_with("::hbd24b59facb94bf3")); + + let v0 = entry("_RNvCs73fAdSrgOJL_4test4main", Some(gimli::DW_LANG_Rust)); + assert_eq!( + function_name_for_display(&v0, RustSymbolHashDisplay::Hidden), + "test::main" + ); + let v0_full = function_name_for_display(&v0, RustSymbolHashDisplay::Shown); + assert!( + v0_full.starts_with("test[") && v0_full.ends_with("::main"), + "unexpected Rust v0 display name: {v0_full}" + ); + + let cpp = entry("_ZN2ns6Widget3runEv", Some(gimli::DW_LANG_C_plus_plus_17)); + assert_eq!( + function_name_for_display(&cpp, RustSymbolHashDisplay::Hidden), + cpp.name.as_ref() + ); + assert_eq!( + function_name_for_display(&cpp, RustSymbolHashDisplay::Shown), + cpp.name.as_ref() + ); + } + + #[test] + fn inline_display_names_control_rust_disambiguators_and_preserve_fallbacks() { + let v0 = "_RNvCs73fAdSrgOJL_4test4main"; + assert_eq!( + inline_function_name_for_display( + Some(v0), + Some("test::main"), + RustSymbolHashDisplay::Hidden, + ) + .as_deref(), + Some("test::main") + ); + let full = inline_function_name_for_display( + Some(v0), + Some("test::main"), + RustSymbolHashDisplay::Shown, + ) + .expect("v0 inline symbol should demangle"); + assert!(full.starts_with("test[") && full.ends_with("::main")); + + assert_eq!( + inline_function_name_for_display( + Some("_ZN2ns6Widget3runEv"), + Some("ns::Widget::run"), + RustSymbolHashDisplay::Shown, + ) + .as_deref(), + Some("ns::Widget::run") + ); + assert_eq!( + inline_function_name_for_display( + None, + Some("plain_inline"), + RustSymbolHashDisplay::Shown, + ) + .as_deref(), + Some("plain_inline") + ); + } + #[test] fn scan_fallback_matches_substitution_heavy_cpp_queries() { let mangled = "_ZNSt6vectorIiSaIiEE3endEv".to_string(); diff --git a/ghostscope-dwarf/src/semantics/mod.rs b/ghostscope-dwarf/src/semantics/mod.rs index 9babc9b8..07e9f85b 100644 --- a/ghostscope-dwarf/src/semantics/mod.rs +++ b/ghostscope-dwarf/src/semantics/mod.rs @@ -15,7 +15,8 @@ pub use c_integer::{ CIntegerComparisonPlan, CIntegerComparisonType, }; pub(crate) use origins::{ - resolve_attr_with_unit_origins, resolve_name_with_origins, resolve_origin_entry, + resolve_attr_with_unit_origins, resolve_linkage_name_with_origins, resolve_name_with_origins, + resolve_origin_entry, }; pub(crate) use pc::{range_contains_pc, ranges_contain_pc}; pub use pc_context::{ diff --git a/ghostscope-dwarf/src/semantics/origins.rs b/ghostscope-dwarf/src/semantics/origins.rs index 00cac953..eea5eb92 100644 --- a/ghostscope-dwarf/src/semantics/origins.rs +++ b/ghostscope-dwarf/src/semantics/origins.rs @@ -114,34 +114,39 @@ pub(crate) fn resolve_origin_entry( } } -fn read_name_attr( +fn read_string_attr( dwarf: &gimli::Dwarf, unit: &DwarfUnit, entry: &DwarfEntry, + attrs: &[gimli::DwAt], ) -> gimli::read::Result> { - if let Some(attr) = entry.attr(gimli::DW_AT_name) { - if let Ok(name) = dwarf.attr_string(unit, attr.value()) { - if let Ok(name) = name.to_string_lossy() { - return Ok(Some(name.into_owned())); + for attr_name in attrs { + if let Some(attr) = entry.attr(*attr_name) { + if let Ok(value) = dwarf.attr_string(unit, attr.value()) { + if let Ok(value) = value.to_string_lossy() { + return Ok(Some(value.into_owned())); + } } } } Ok(None) } -pub(crate) fn resolve_name_with_origins( +fn resolve_string_attr_with_origins( dwarf: &gimli::Dwarf, unit: &DwarfUnit, entry: &DwarfEntry, + attrs: &[gimli::DwAt], ) -> gimli::read::Result> { fn inner( dwarf: &gimli::Dwarf, unit: &DwarfUnit, entry: &DwarfEntry, + attrs: &[gimli::DwAt], visited: &mut HashSet, ) -> gimli::read::Result> { - if let Some(name) = read_name_attr(dwarf, unit, entry)? { - return Ok(Some(name)); + if let Some(value) = read_string_attr(dwarf, unit, entry, attrs)? { + return Ok(Some(value)); } for origin_attr in [ @@ -153,8 +158,10 @@ pub(crate) fn resolve_name_with_origins( resolve_origin_entry(dwarf, unit, value)? { if visited.insert(origin_abs) { - if let Some(name) = inner(dwarf, &origin_unit, &origin_entry, visited)? { - return Ok(Some(name)); + if let Some(value) = + inner(dwarf, &origin_unit, &origin_entry, attrs, visited)? + { + return Ok(Some(value)); } } } @@ -164,8 +171,8 @@ pub(crate) fn resolve_name_with_origins( Ok(None) } - if let Some(name) = read_name_attr(dwarf, unit, entry)? { - return Ok(Some(name)); + if let Some(value) = read_string_attr(dwarf, unit, entry, attrs)? { + return Ok(Some(value)); } let has_origin = entry @@ -182,5 +189,26 @@ pub(crate) fn resolve_name_with_origins( if let Some(entry_abs) = entry.offset().to_debug_info_offset(&unit.header) { visited.insert(entry_abs); } - inner(dwarf, unit, entry, &mut visited) + inner(dwarf, unit, entry, attrs, &mut visited) +} + +pub(crate) fn resolve_name_with_origins( + dwarf: &gimli::Dwarf, + unit: &DwarfUnit, + entry: &DwarfEntry, +) -> gimli::read::Result> { + resolve_string_attr_with_origins(dwarf, unit, entry, &[gimli::DW_AT_name]) +} + +pub(crate) fn resolve_linkage_name_with_origins( + dwarf: &gimli::Dwarf, + unit: &DwarfUnit, + entry: &DwarfEntry, +) -> gimli::read::Result> { + resolve_string_attr_with_origins( + dwarf, + unit, + entry, + &[gimli::DW_AT_linkage_name, gimli::DW_AT_MIPS_linkage_name], + ) } diff --git a/ghostscope-protocol/src/trace_event.rs b/ghostscope-protocol/src/trace_event.rs index 89b0b627..da0d7399 100644 --- a/ghostscope-protocol/src/trace_event.rs +++ b/ghostscope-protocol/src/trace_event.rs @@ -283,6 +283,7 @@ pub fn backtrace_error_label(error_code: u16) -> Option<&'static str> { } pub const BACKTRACE_FLAG_RAW: u8 = 0x01; +/// Retain compiler disambiguators in symbolized Rust frame names. pub const BACKTRACE_FLAG_FULL: u8 = 0x02; pub const BACKTRACE_FLAG_INLINE: u8 = 0x04; /// ExprError instruction data - structured warning for runtime expression failure diff --git a/ghostscope/src/trace/backtrace.rs b/ghostscope/src/trace/backtrace.rs index d0e33741..d426dc82 100644 --- a/ghostscope/src/trace/backtrace.rs +++ b/ghostscope/src/trace/backtrace.rs @@ -3,7 +3,7 @@ use ghostscope_process::ProcessManager; #[cfg(test)] use ghostscope_protocol::trace_event::backtrace_error_label; use ghostscope_protocol::trace_event::{ - BacktraceStatus, BACKTRACE_FLAG_INLINE, BACKTRACE_FLAG_RAW, + BacktraceStatus, BACKTRACE_FLAG_FULL, BACKTRACE_FLAG_INLINE, BACKTRACE_FLAG_RAW, }; use ghostscope_protocol::{ParsedBacktraceFrame, ParsedInstruction, ParsedTraceEvent}; use ghostscope_ui::{BacktraceDisplay, BacktraceDisplayFrame, TraceDisplayItem, UiTraceEvent}; @@ -402,6 +402,7 @@ impl BacktraceRenderer { } let raw = (flags & BACKTRACE_FLAG_RAW) != 0; + let full = (flags & BACKTRACE_FLAG_FULL) != 0; let inline = (flags & BACKTRACE_FLAG_INLINE) != 0; let module = resolve_frame_module(coordinator, analyzer, pids, frame); let frame_pc = module.as_ref().map(|module| module.pc).unwrap_or(frame.pc); @@ -423,7 +424,7 @@ impl BacktraceRenderer { module.pc.saturating_sub(1) }; let address = ModuleAddress::new(module.module_path.clone(), lookup_pc); - analyzer.resolve_pc(&address).ok() + analyzer.resolve_pc_for_display(&address, full).ok() }); let Some(ctx) = resolved else { @@ -492,6 +493,7 @@ impl BacktraceRenderer { } let raw = (flags & BACKTRACE_FLAG_RAW) != 0; + let full = (flags & BACKTRACE_FLAG_FULL) != 0; let inline = (flags & BACKTRACE_FLAG_INLINE) != 0; let module = resolve_frame_module(coordinator, analyzer, pids, frame); let frame_pc = module.as_ref().map(|module| module.pc).unwrap_or(frame.pc); @@ -513,7 +515,7 @@ impl BacktraceRenderer { module.pc.saturating_sub(1) }; let address = ModuleAddress::new(module.module_path.clone(), lookup_pc); - analyzer.resolve_pc(&address).ok() + analyzer.resolve_pc_for_display(&address, full).ok() }); let Some(ctx) = resolved else {