Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion docs/scripting.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>` 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<str>` and supported `Box<CStr>` 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<CStr>` use
Expand Down Expand Up @@ -242,6 +258,8 @@ Request { name: String } Request keeps DWARF layout; name captures string bytes
Vec<Request> each Request element adapts its recognized fields
Rc<Vec<i32>> Rc summary plus captured Vec elements
Cell<String> Cell wrapper plus captured String bytes
*Box<String> explicit dereference captures String bytes
*Box<Request> explicit dereference adapts Request fields
Vec<String> each captured element uses the String adapter
Vec<CString> each captured element omits its trailing NUL
Vec<Vec<i32>> each captured element uses the Vec adapter
Expand Down Expand Up @@ -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
Expand Down
21 changes: 20 additions & 1 deletion docs/zh/scripting.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,21 @@ GhostScope 自身使用 Rust 1.88 构建,但这不会限制被追踪目标使
`Rc<str>` 和 `Arc<str>` 会显示目标地址以及对外可见的 strong/weak 计数。
GhostScope 不会通过这些所有者读取动态大小字符串的内容。

rustc 会把 sized `Box<T>` 作为普通的 `T` 指针写入 DWARF,不保留 `Box`
类型标识。GhostScope 无法安全地区分该指针与引用或裸指针,因此直接打印根值
时会保留原生指针展示。显式解引用后,可以检查被所有权指针指向的值,并应用
pointee 的语义展示:

```ghostscope
trace handle_request {
print "text={} request={}", *boxed_text, *boxed_request;
}
```

标量、字符串、struct 以及其他已识别的 pointee 类型都支持这种写法。
unsized `Box<str>` 和布局验证通过的 `Box<CStr>` 会保留明确的包装元数据,
因此可以直接打印。

Rust C 字符串展示时不包含结尾 NUL;非 UTF-8 内容与平台原生字节串一样使用
`\xNN` 转义。兼容性矩阵中的所有版本都会验证 `CString`;`&CStr` 和
`Box<CStr>` 从 Rust 1.81 起使用语义展示,因为这些版本的目标 DWARF
Expand Down Expand Up @@ -225,6 +240,8 @@ Request { name: String } Request 保留 DWARF 布局,name 采集字符串字
Vec<Request> 每个 Request 元素都会适配其中已识别的字段
Rc<Vec<i32>> 显示 Rc 摘要并采集内部 Vec 元素
Cell<String> 显示 Cell 包装并采集 String 字节
*Box<String> 显式解引用后采集 String 字节
*Box<Request> 显式解引用后适配 Request 字段
Vec<String> 每个已采集元素使用 String 适配器
Vec<CString> 每个已采集元素都会排除结尾 NUL
Vec<Vec<i32>> 每个已采集元素使用 Vec 适配器
Expand Down Expand Up @@ -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` 参数。暂不支持:多维数组。

#### 显式类型转换
Expand Down
26 changes: 26 additions & 0 deletions e2e-tests/tests/fixtures/rust_global_program/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,24 @@ pub mod math {
value.len() + empty.len()
}

#[inline(never)]
pub fn observe_box_values(
number: Box<i32>,
text: Box<String>,
record: Box<crate::AggregateRecord>,
) -> 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,
Expand Down Expand Up @@ -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']),
Expand Down
33 changes: 33 additions & 0 deletions e2e-tests/tests/rust_script_execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> output: {stdout}"
);
assert!(
!stdout.contains("ExprError"),
"Unexpected ExprError: {stdout}"
);

Ok(())
}

#[tokio::test]
async fn test_rust_script_print_os_string_values() -> anyhow::Result<()> {
init();
Expand Down
6 changes: 6 additions & 0 deletions ghostscope-dwarf/src/language/rust/value/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> 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<str> and Box<CStr> 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(&current.summary) else {
return ValueLayoutResolution::NotApplicable;
};
Expand Down
21 changes: 21 additions & 0 deletions ghostscope-dwarf/tests/fixtures/rust_value_compat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -62,6 +67,9 @@ pub fn observe_values(
path_buf: PathBuf,
text: &str,
boxed_text: Box<str>,
boxed_i32: Box<i32>,
boxed_string: Box<String>,
boxed_record: Box<CompatBoxRecord>,
slice: &[i32],
vector: Vec<i32>,
deque: VecDeque<i32>,
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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(),
Expand All @@ -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,
Expand Down
74 changes: 74 additions & 0 deletions ghostscope-dwarf/tests/rust_version_compatibility.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ enum ExpectedAdapter {
RefCell,
RefGuard,
NonZero,
Aggregate,
NativeDwarf,
}

Expand All @@ -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),
Expand All @@ -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),
Expand Down Expand Up @@ -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, .. },
Expand Down Expand Up @@ -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<Option<ValueReadPlan>> {
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(&parameter_plan)?
.ok_or_else(|| anyhow::anyhow!("{toolchain}: missing type for {function}::{parameter}"))?;
anyhow::ensure!(
matches!(
ghostscope_dwarf::strip_type_aliases(&parameter_type.summary),
TypeInfo::PointerType { .. }
),
"{toolchain}: sized Box parameter {function}::{parameter} was not \
emitted as a pointer: {parameter_type:#?}"
);
let projected = analyzer.project_resolved_type(
&parameter_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,
Expand Down Expand Up @@ -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 {
Expand Down
Loading