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 {