From bbfa690e7300e35d289d6add0a97694e59880b0a Mon Sep 17 00:00:00 2001 From: swananan Date: Mon, 27 Jul 2026 18:13:13 +0800 Subject: [PATCH] feat: support explicit Rust Box value inspection rustc emits sized Box as ordinary pointer DWARF, so automatic classification would confuse owners with references and raw pointers. Document explicit dereference as the safe inspection path and pin the behavior across the supported rustc matrix and host e2e. --- docs/scripting.md | 23 +++++- docs/zh/scripting.md | 21 +++++- .../fixtures/rust_global_program/src/main.rs | 26 +++++++ e2e-tests/tests/rust_script_execution.rs | 33 +++++++++ .../src/language/rust/value/mod.rs | 6 ++ .../tests/fixtures/rust_value_compat.rs | 21 ++++++ .../tests/rust_version_compatibility.rs | 74 +++++++++++++++++++ 7 files changed, 202 insertions(+), 2 deletions(-) diff --git a/docs/scripting.md b/docs/scripting.md index 39530244..6c5b5dea 100644 --- a/docs/scripting.md +++ b/docs/scripting.md @@ -166,6 +166,22 @@ The currently supported value families are: strong and weak counts. GhostScope does not read the dynamically sized string contents through these owners. +rustc emits a sized `Box` as an ordinary pointer to `T` in DWARF, without +retaining the `Box` identity. GhostScope cannot safely distinguish that pointer +from a reference or raw pointer, so printing the root keeps the native pointer +presentation. Dereference it explicitly to inspect the owned value and apply +the pointee's semantic presentation: + +```ghostscope +trace handle_request { + print "text={} request={}", *boxed_text, *boxed_request; +} +``` + +This supports scalar, string, struct, and other recognized pointee types. +Unsized `Box` and supported `Box` layouts retain explicit wrapper +metadata and can be printed directly. + Rust C strings are rendered without their trailing NUL. Non-UTF-8 content uses the same `\xNN` escaping as platform byte strings. `CString` is validated across every release in the compatibility matrix; `&CStr` and `Box` use @@ -242,6 +258,8 @@ Request { name: String } Request keeps DWARF layout; name captures string bytes Vec each Request element adapts its recognized fields Rc> Rc summary plus captured Vec elements Cell Cell wrapper plus captured String bytes +*Box explicit dereference captures String bytes +*Box explicit dereference adapts Request fields Vec each captured element uses the String adapter Vec each captured element omits its trailing NUL Vec> each captured element uses the Vec adapter @@ -384,7 +402,10 @@ print obj.field.items[i + 1]; ``` Tips: -- Auto‑dereference is supported for locals/params/globals. You don’t need to write `*ptr` or `->`; when safe, pointers are read and dereferenced automatically. +- Auto-dereference is supported for member access on locals, parameters, and + globals. You do not need to write `*ptr` or `->`; when safe, pointers are + read and dereferenced automatically. Printing a pointer itself still uses + pointer presentation, so use `*ptr` to print its pointee. - Array access: supported for one-dimensional arrays such as `arr[index]`, chain-tail `a.b.c[index]`, and array elements followed by member access such as `arr[index].field` or `a.b[index].c`, where `index` can be an integer literal or an integer expression such as `i + 1`. Address-of forms like `&arr[i]` and `&a.b.c[i]` can be printed as pointers, used as memory-format inputs (`{:x.N}`/`{:s.N}`), or passed to `memcmp`. Not supported: multi-dimensional arrays. #### Explicit Casts diff --git a/docs/zh/scripting.md b/docs/zh/scripting.md index 70cf24ba..9f3cb8ce 100644 --- a/docs/zh/scripting.md +++ b/docs/zh/scripting.md @@ -155,6 +155,21 @@ GhostScope 自身使用 Rust 1.88 构建,但这不会限制被追踪目标使 `Rc` 和 `Arc` 会显示目标地址以及对外可见的 strong/weak 计数。 GhostScope 不会通过这些所有者读取动态大小字符串的内容。 +rustc 会把 sized `Box` 作为普通的 `T` 指针写入 DWARF,不保留 `Box` +类型标识。GhostScope 无法安全地区分该指针与引用或裸指针,因此直接打印根值 +时会保留原生指针展示。显式解引用后,可以检查被所有权指针指向的值,并应用 +pointee 的语义展示: + +```ghostscope +trace handle_request { + print "text={} request={}", *boxed_text, *boxed_request; +} +``` + +标量、字符串、struct 以及其他已识别的 pointee 类型都支持这种写法。 +unsized `Box` 和布局验证通过的 `Box` 会保留明确的包装元数据, +因此可以直接打印。 + Rust C 字符串展示时不包含结尾 NUL;非 UTF-8 内容与平台原生字节串一样使用 `\xNN` 转义。兼容性矩阵中的所有版本都会验证 `CString`;`&CStr` 和 `Box` 从 Rust 1.81 起使用语义展示,因为这些版本的目标 DWARF @@ -225,6 +240,8 @@ Request { name: String } Request 保留 DWARF 布局,name 采集字符串字 Vec 每个 Request 元素都会适配其中已识别的字段 Rc> 显示 Rc 摘要并采集内部 Vec 元素 Cell 显示 Cell 包装并采集 String 字节 +*Box 显式解引用后采集 String 字节 +*Box 显式解引用后适配 Request 字段 Vec 每个已采集元素使用 String 适配器 Vec 每个已采集元素都会排除结尾 NUL Vec> 每个已采集元素使用 Vec 适配器 @@ -362,7 +379,9 @@ print obj.field.items[i + 1]; ``` 提示: -- 目前“局部变量、参数、全局变量”均已支持自动解引用(无需显式 `*ptr`,也不需要 `->`,统一使用 `.`,在安全范围内会自动加载并解引用指针值,类似于 Rust 的自动解引用)。 +- 局部变量、参数和全局变量在成员访问时支持自动解引用,无需显式 + `*ptr`,也不需要 `->`,统一使用 `.`。直接打印指针本身时仍按指针展示; + 若要打印 pointee,需要显式使用 `*ptr`。 - 数组访问:已支持一维数组,如顶层 `arr[index]`、“链尾”`a.b.c[index]`,以及数组元素后继续取成员的 `arr[index].field` 或 `a.b[index].c`,其中 `index` 可以是整数字面量,也可以是 `i + 1` 这类整数表达式。`&arr[i]`、`&a.b.c[i]` 这类取地址形式可以按指针打印,也可以用于内存格式化(`{:x.N}`/`{:s.N}`)或作为 `memcmp` 参数。暂不支持:多维数组。 #### 显式类型转换 diff --git a/e2e-tests/tests/fixtures/rust_global_program/src/main.rs b/e2e-tests/tests/fixtures/rust_global_program/src/main.rs index 2326e250..6359fc86 100644 --- a/e2e-tests/tests/fixtures/rust_global_program/src/main.rs +++ b/e2e-tests/tests/fixtures/rust_global_program/src/main.rs @@ -370,6 +370,24 @@ pub mod math { value.len() + empty.len() } + #[inline(never)] + pub fn observe_box_values( + number: Box, + text: Box, + record: Box, + ) -> usize { + std::hint::black_box(( + *number, + text.as_str(), + record.title.as_str(), + record.count, + )); + number.unsigned_abs() as usize + + text.len() + + record.title.len() + + record.count.unsigned_abs() as usize + } + #[inline(never)] pub fn observe_os_string( value: std::ffi::OsString, @@ -853,6 +871,14 @@ fn main() { for _ in 0..50000 { acc += math::do_stuff(3) as i64; acc += math::observe_boxed_str("boxed from rust".into(), "".into()) as i64; + acc += math::observe_box_values( + Box::new(-101), + Box::new(String::from("boxed string")), + Box::new(AggregateRecord { + title: String::from("boxed record"), + count: 103, + }), + ) as i64; acc += math::observe_os_string( OsString::from("os from rust"), OsString::from_vec(vec![b'o', b's', 0xff, b'x']), diff --git a/e2e-tests/tests/rust_script_execution.rs b/e2e-tests/tests/rust_script_execution.rs index 470a3a9b..8c15dab4 100644 --- a/e2e-tests/tests/rust_script_execution.rs +++ b/e2e-tests/tests/rust_script_execution.rs @@ -2795,6 +2795,39 @@ trace observe_boxed_str { Ok(()) } +#[tokio::test] +async fn test_rust_script_print_sized_box_values_through_dereference() -> anyhow::Result<()> { + init(); + + let target = spawn_rust_global_program().await?; + let script = r#" +trace observe_box_values { + print "RBOXVALUE:{}:{}:{}", *number, *text, *record; +} +"#; + + let (exit_code, stdout, stderr) = + run_ghostscope_with_script_for_target(script, 9, &target).await?; + target.terminate().await?; + + assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}"); + assert!( + stdout.lines().any(|line| { + line.contains( + "RBOXVALUE:-101:\"boxed string\":AggregateRecord { \ + title: \"boxed record\", count: 103 }", + ) + }), + "Expected explicitly dereferenced Rust Box output: {stdout}" + ); + assert!( + !stdout.contains("ExprError"), + "Unexpected ExprError: {stdout}" + ); + + Ok(()) +} + #[tokio::test] async fn test_rust_script_print_os_string_values() -> anyhow::Result<()> { init(); diff --git a/ghostscope-dwarf/src/language/rust/value/mod.rs b/ghostscope-dwarf/src/language/rust/value/mod.rs index 86fdad09..86bfa0a9 100644 --- a/ghostscope-dwarf/src/language/rust/value/mod.rs +++ b/ghostscope-dwarf/src/language/rust/value/mod.rs @@ -61,6 +61,12 @@ pub(super) fn diagnose_value_layout( // while the target only requests the generic loader through // `.debug_gdb_scripts`. GhostScope records the producer for diagnostics, // but every adapter below validates identity and physical target DWARF. + // + // Sized Box is intentionally absent here. rustc emits it as the same + // DW_TAG_pointer_type used by references and raw pointers, with no Box + // identity left to validate. Only unsized Box and Box retain a + // recognizable aggregate. A sized Box can still be inspected safely by + // explicitly dereferencing it in the script. let TypeInfo::StructType { name, .. } = strip_type_aliases(¤t.summary) else { return ValueLayoutResolution::NotApplicable; }; diff --git a/ghostscope-dwarf/tests/fixtures/rust_value_compat.rs b/ghostscope-dwarf/tests/fixtures/rust_value_compat.rs index 52159f61..dcc05847 100644 --- a/ghostscope-dwarf/tests/fixtures/rust_value_compat.rs +++ b/ghostscope-dwarf/tests/fixtures/rust_value_compat.rs @@ -51,6 +51,11 @@ pub enum CompatUnsigned { High = 0x8000_0000_0000_0000, } +pub struct CompatBoxRecord { + pub name: String, + pub count: u16, +} + #[inline(never)] pub fn observe_values( string: String, @@ -62,6 +67,9 @@ pub fn observe_values( path_buf: PathBuf, text: &str, boxed_text: Box, + boxed_i32: Box, + boxed_string: Box, + boxed_record: Box, slice: &[i32], vector: Vec, deque: VecDeque, @@ -112,6 +120,10 @@ pub fn observe_values( + path_buf.as_os_str().len() + text.len() + boxed_text.len() + + *boxed_i32 as usize + + boxed_string.len() + + boxed_record.name.len() + + boxed_record.count as usize + slice.len() + vector.len() + deque.len() @@ -214,6 +226,12 @@ fn main() { let slice = [29, 31]; let path = PathBuf::from("borrowed/path"); let c_str_owner = CString::new("borrowed-c-string").unwrap(); + let boxed_i32 = Box::new(101); + let boxed_string = Box::new(String::from("boxed-string")); + let boxed_record = Box::new(CompatBoxRecord { + name: String::from("boxed-record"), + count: 103, + }); let value = observe_values( String::from("string"), CString::new("owned-c-string").unwrap(), @@ -226,6 +244,9 @@ fn main() { PathBuf::from("owned/path"), "text", String::from("boxed-text").into_boxed_str(), + boxed_i32, + boxed_string, + boxed_record, &slice, vec![3], deque, diff --git a/ghostscope-dwarf/tests/rust_version_compatibility.rs b/ghostscope-dwarf/tests/rust_version_compatibility.rs index d47dc593..a2a77743 100644 --- a/ghostscope-dwarf/tests/rust_version_compatibility.rs +++ b/ghostscope-dwarf/tests/rust_version_compatibility.rs @@ -24,6 +24,7 @@ enum ExpectedAdapter { RefCell, RefGuard, NonZero, + Aggregate, NativeDwarf, } @@ -34,6 +35,9 @@ const ALL_ADAPTERS: &[(&str, ExpectedAdapter)] = &[ ("path_buf", ExpectedAdapter::OsBytes), ("text", ExpectedAdapter::Utf8Bytes), ("boxed_text", ExpectedAdapter::Utf8Bytes), + ("boxed_i32", ExpectedAdapter::NativeDwarf), + ("boxed_string", ExpectedAdapter::NativeDwarf), + ("boxed_record", ExpectedAdapter::NativeDwarf), ("slice", ExpectedAdapter::Sequence), ("vector", ExpectedAdapter::Sequence), ("deque", ExpectedAdapter::RingSequence), @@ -59,6 +63,9 @@ const RUST_135_ADAPTERS: &[(&str, ExpectedAdapter)] = &[ // applies that provider's semantics to Rust 1.35's concrete fat-pointer // DIE rather than implying it was in the bundled 1.35 script. ("boxed_text", ExpectedAdapter::Utf8Bytes), + ("boxed_i32", ExpectedAdapter::NativeDwarf), + ("boxed_string", ExpectedAdapter::NativeDwarf), + ("boxed_record", ExpectedAdapter::NativeDwarf), ("slice", ExpectedAdapter::Sequence), ("vector", ExpectedAdapter::Sequence), ("deque", ExpectedAdapter::RingSequence), @@ -205,6 +212,11 @@ fn adapter_matches(plan: Option<&ValueReadPlan>, expected: ExpectedAdapter) -> b ValuePresentation::Dwarf, ValueCapturePlan::ProjectedValue { .. }, ExpectedAdapter::NonZero, + ) + | ( + ValuePresentation::Dwarf, + ValueCapturePlan::InlineView { .. }, + ExpectedAdapter::Aggregate, ) => true, ( ValuePresentation::BTree { entry, .. }, @@ -424,6 +436,53 @@ async fn assert_parameter_adapter( Ok(plan) } +async fn assert_dereferenced_parameter_adapter( + analyzer: &DwarfAnalyzer, + binary: &Path, + function: &str, + parameter: &str, + expected: ExpectedAdapter, + toolchain: &str, +) -> anyhow::Result> { + let context = analyzer + .lookup_function_addresses(function) + .into_iter() + .find_map(|address| analyzer.resolve_pc(&address).ok()) + .ok_or_else(|| anyhow::anyhow!("{toolchain}: missing function {function}"))?; + let parameter_plan = analyzer + .plan_variable_by_name(&context, parameter)? + .ok_or_else(|| anyhow::anyhow!("{toolchain}: missing {function}::{parameter}"))?; + let parameter_type = analyzer + .resolved_type_for_plan(¶meter_plan)? + .ok_or_else(|| anyhow::anyhow!("{toolchain}: missing type for {function}::{parameter}"))?; + anyhow::ensure!( + matches!( + ghostscope_dwarf::strip_type_aliases(¶meter_type.summary), + TypeInfo::PointerType { .. } + ), + "{toolchain}: sized Box parameter {function}::{parameter} was not \ + emitted as a pointer: {parameter_type:#?}" + ); + let projected = analyzer.project_resolved_type( + ¶meter_type, + &ghostscope_dwarf::VariableAccessSegment::Dereference, + Some(binary), + )?; + anyhow::ensure!( + projected.layout == ghostscope_dwarf::TypeProjectionLayout::Dereference, + "{toolchain}: unexpected projection for {function}::{parameter}: \ + {projected:#?}" + ); + let plan = analyzer.value_read_plan(&projected.resolved_type, Some(binary))?; + anyhow::ensure!( + adapter_matches(plan.as_ref(), expected), + "{toolchain}: unexpected dereferenced adapter for \ + {function}::{parameter}: {plan:#?}\nresolved type: {:#?}", + projected.resolved_type + ); + Ok(plan) +} + async fn resolved_parameter_type( analyzer: &DwarfAnalyzer, function: &str, @@ -1084,6 +1143,21 @@ async fn rust_value_adapters_follow_pinned_toolchain_dwarf() -> anyhow::Result<( )); } } + for (parameter, expected) in [ + ("boxed_i32", ExpectedAdapter::NativeDwarf), + ("boxed_string", ExpectedAdapter::Utf8Bytes), + ("boxed_record", ExpectedAdapter::Aggregate), + ] { + assert_dereferenced_parameter_adapter( + &analyzer, + &binary, + "observe_values", + parameter, + expected, + &toolchain, + ) + .await?; + } let c_str_expected = if matches!(toolchain.as_str(), "1.35.0" | "1.49.0") { ExpectedAdapter::NativeDwarf } else {