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
8 changes: 6 additions & 2 deletions docs/scripting.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,12 +120,12 @@ GhostScope's script syntax is source-language agnostic, but real-world support d
| --- | --- | --- |
| C | Best | This is the primary target. Plain locals, globals, x86_64 executable static thread-local variables, pointers, arrays, structs, enums, and C strings map most directly to the current DWARF readers and script operators. |
| C++ | Limited | Automatic demangling is supported for function names in `trace ...` patterns and for global/static variable lookup. Beyond name resolution, most C++-specific language features are not modeled yet, so the best results come from simple, C-like layouts and scalar fields. |
| Rust | Limited | Automatic demangling is supported for function names in `trace ...` patterns and for global/static variable lookup. Beyond name resolution, most Rust-specific language features are not modeled yet, so the best results come from plain globals, scalar fields, and straightforward struct layouts. |
| Rust | Limited | Automatic demangling is supported for function names in `trace ...` patterns and for global/static variable lookup. Numeric tuple fields such as `value.0` are supported for tuples and tuple structs when Rust DWARF type identity is available. Most other Rust-specific features are not modeled yet. |

Practical guidance:
- Prefer C targets when you need the highest success rate for complex DWARF expressions.
- GhostScope recognizes `DW_OP_form_tls_address`, which is used for both static and dynamic TLS. Runtime address resolution currently supports only x86_64 executable static TLS; dynamic/shared-library TLS is not modeled yet.
- For C++ and Rust, think of GhostScope as "DWARF layout aware" rather than "language semantics aware".
- C++ remains primarily DWARF-layout aware. Rust tuple fields are the first language-aware projection, but broader Rust semantics are not modeled yet.
- In C++ and Rust, start from demangled function/global names, then probe simple fields first. If name lookup is ambiguous, fall back to line- or address-based trace patterns.

## Variables
Expand Down Expand Up @@ -215,6 +215,10 @@ print x;
print person.name;
print config.settings.timeout;

// Rust tuple and tuple-struct fields
print GLOBAL_TUPLE.0;
print GLOBAL_PAIR.1;

// Array access (literal and expression indices)
print arr[0];
print arr[1];
Expand Down
8 changes: 6 additions & 2 deletions docs/zh/scripting.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,12 @@ GhostScope 的脚本语法本身与源语言无关,但实际效果取决于被
| --- | --- | --- |
| C | 最好 | 这是当前的主要优化目标。局部变量、全局变量、x86_64 可执行文件中的 static 线程局部变量、指针、数组、结构体、枚举和 C 字符串这类 C 风格布局,与现有 DWARF 读取和脚本运算最匹配。 |
| C++ | 有限 | `trace ...` 里的函数名匹配,以及全局/静态变量查找,支持自动 demangle(符号名自动反混淆)。除名字解析外,C++ 语言特性目前大多还没有建模,实际更接近“按 DWARF 布局访问”,适合简单、接近 C 的数据布局和标量字段。 |
| Rust | 有限 | `trace ...` 里的函数名匹配,以及全局/静态变量查找,支持自动 demangle(符号名自动反混淆)。除名字解析外,Rust 语言特性目前大多还没有建模,实际更接近“按 DWARF 布局访问”,适合简单全局、标量字段和较直白的结构体布局。 |
| Rust | 有限 | `trace ...` 里的函数名匹配,以及全局/静态变量查找,支持自动 demangle(符号名自动反混淆)。Rust DWARF 类型身份可用时,tuple 和 tuple struct 支持 `value.0` 这类数字成员访问;其他大多数 Rust 语言特性仍未建模。 |

实践建议:
- 需要较高成功率的复杂 DWARF 表达式时,优先选择 C 目标。
- GhostScope 能识别 `DW_OP_form_tls_address`,这个操作既会用于 static TLS,也会用于 dynamic TLS。运行时地址解析目前只支持 x86_64 可执行文件 static TLS;dynamic/shared-library TLS 尚未建模。
- C++ Rust,建议把 GhostScope 理解为“理解 DWARF 布局”,而不是“理解语言语义”
- C++ 目前仍主要按 DWARF 布局访问。Rust tuple 成员是第一个语言语义投影,但更广泛的 Rust 语义仍未建模
- 在 C++ 和 Rust 上,先从 demangle 后的函数名/全局名入手,再逐步验证简单字段;若名字解析有歧义,优先退回到按源码行号或地址下探。

## 变量
Expand Down Expand Up @@ -211,6 +211,10 @@ print x;
print person.name;
print config.settings.timeout;

// Rust tuple 与 tuple struct 成员
print GLOBAL_TUPLE.0;
print GLOBAL_PAIR.1;

// 数组访问(支持字面量和表达式下标)
print arr[0];
print arr[1];
Expand Down
2 changes: 2 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 @@ -91,6 +91,7 @@ static DATA_STRINGS: [&[u8]; 2] = [DATA_ALPHA, DATA_OMEGA];

pub static mut GLOBAL_TUPLE: (i32, bool) = (1, true);
pub static mut GLOBAL_PAIR: Pair = Pair(2, 3);
pub static GLOBAL_PAIRS: [Pair; 2] = [Pair(5, 8), Pair(13, 21)];
pub static mut GLOBAL_UNION: NumberUnion = NumberUnion { int_value: 10 };
pub static mut GLOBAL_SLICE: &'static [u8] = DATA_ALPHA;
pub static mut GLOBAL_NICHE: Option<NonZeroU32> = NonZeroU32::new(7);
Expand Down Expand Up @@ -169,6 +170,7 @@ fn touch_globals() -> i32 {

let total = CONFIG.a as i64
+ G_MESSAGE.len() as i64
+ GLOBAL_PAIRS[0].0 as i64
+ union_value as i64
+ pinned_value as i64
+ GLOBAL_PHANTOM.value as i64
Expand Down
15 changes: 15 additions & 0 deletions e2e-tests/tests/member_pointer_compilation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,16 @@ async fn test_plan_variable_by_name_uses_pc_context() -> anyhow::Result<()> {
assert_eq!(id_header_pos.name, "r.header_in.pos");
assert!(id_header_pos.availability.is_available());
assert!(id_header_pos.dwarf_type.is_some());
assert!(id_header_pos.type_id.is_some());

let semantic_type = analyzer
.semantic_type_for_plan(&id_header_pos)?
.ok_or_else(|| anyhow::anyhow!("expected semantic type for 'r.header_in.pos'"))?;
assert_eq!(semantic_type.id, id_header_pos.type_id);
assert_eq!(
semantic_type.origin.map(|origin| origin.language),
Some(ghostscope_dwarf::SourceLanguage::C)
);

let header_pos = analyzer
.plan_variable_access_by_name(
Expand All @@ -308,6 +318,11 @@ async fn test_plan_variable_by_name_uses_pc_context() -> anyhow::Result<()> {
header_pos.dwarf_type.is_some(),
"access plan should carry final member type: {header_pos:?}"
);
assert_eq!(header_pos.type_id, id_header_pos.type_id);

let indexed = analyzer.plan_pointer_element_index(&plan, 1)?;
assert!(indexed.type_id.is_some(), "indexed plan: {indexed:?}");
assert_ne!(indexed.type_id, plan.type_id);

Ok(())
}
Expand Down
109 changes: 109 additions & 0 deletions e2e-tests/tests/rust_script_execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,115 @@ async fn spawn_rust_global_program() -> anyhow::Result<common::targets::TargetHa
Ok(target)
}

#[tokio::test]
async fn test_rust_tuple_projection_plan_preserves_semantic_identity() -> anyhow::Result<()> {
init();

let binary_path = FIXTURES.get_test_binary("rust_global_program")?;
let analyzer = ghostscope_dwarf::DwarfAnalyzer::from_exec_path(&binary_path).await?;
let tuple_path = ghostscope_dwarf::VariableAccessPath::new(vec![
ghostscope_dwarf::VariableAccessSegment::TupleIndex(0),
]);
let (_, plan) = analyzer
.plan_global_access_read_plan(&binary_path, "GLOBAL_TUPLE", &tuple_path)?
.ok_or_else(|| anyhow::anyhow!("expected GLOBAL_TUPLE.0 read plan"))?;

assert_eq!(plan.name, "GLOBAL_TUPLE.0");
assert_eq!(plan.access_path, tuple_path);
assert!(plan.type_id.is_some(), "plan: {plan:?}");
let semantic_type = analyzer
.semantic_type_for_plan(&plan)?
.ok_or_else(|| anyhow::anyhow!("expected semantic type for GLOBAL_TUPLE.0"))?;
assert_eq!(semantic_type.id, plan.type_id);
assert_eq!(
semantic_type.origin.map(|origin| origin.language),
Some(ghostscope_dwarf::SourceLanguage::Rust)
);

let (_, pair_plan) = analyzer
.plan_global_access_read_plan(
&binary_path,
"GLOBAL_PAIR",
&ghostscope_dwarf::VariableAccessPath::default(),
)?
.ok_or_else(|| anyhow::anyhow!("expected GLOBAL_PAIR read plan"))?;
let pair_type = pair_plan
.dwarf_type
.as_ref()
.ok_or_else(|| anyhow::anyhow!("expected GLOBAL_PAIR type"))?;
let layout = analyzer.tuple_member_layout_in_module(&binary_path, pair_type, 1)?;
assert_eq!(layout.offset, 4);
assert_eq!(layout.member_type.type_name(), "i32");

Ok(())
}

#[tokio::test]
async fn test_rust_script_print_tuple_fields() -> anyhow::Result<()> {
init();

let target = spawn_rust_global_program().await?;
let script = r#"
trace do_stuff {
let pair_index = 1;
print "RTUP:{}:{}", GLOBAL_TUPLE.0, GLOBAL_TUPLE.1;
print "RPAIR:{}:{}", GLOBAL_PAIR.0, GLOBAL_PAIR.1;
print "DPAIR:{}", GLOBAL_PAIRS[pair_index].0;
print "CPAIR:{}", cast(&GLOBAL_PAIR, "Pair *").0;
}
"#;

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("RTUP:") && (line.contains(":true") || line.contains(":false"))
}),
"Expected typed tuple output: {stdout}"
);
assert!(
stdout.lines().any(|line| {
let Some((_, payload)) = line.split_once("RPAIR:") else {
return false;
};
let mut fields = payload.split(':');
let Some(left) = fields.next() else {
return false;
};
let Some(right) = fields.next() else {
return false;
};
left.trim().parse::<i64>().is_ok()
&& right
.split_whitespace()
.next()
.is_some_and(|value| value.parse::<i64>().is_ok())
}),
"Expected typed tuple struct output: {stdout}"
);
assert!(
!stdout.contains("ExprError"),
"Unexpected ExprError: {stdout}"
);
assert!(
stdout.contains("DPAIR:13"),
"Expected dynamic-index tuple output: {stdout}"
);
assert!(
stdout.lines().any(|line| {
line.split_once("CPAIR:")
.and_then(|(_, value)| value.split_whitespace().next())
.is_some_and(|value| value.parse::<i64>().is_ok())
}),
"Expected cast tuple output: {stdout}"
);

Ok(())
}

#[tokio::test]
async fn test_rust_script_print_globals() -> anyhow::Result<()> {
init();
Expand Down
10 changes: 6 additions & 4 deletions ghostscope-compiler/src/ebpf/codegen/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,7 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> {

// 5) Complex lvalue shapes -> DWARF runtime read
expr @ (E::MemberAccess(_, _)
| E::TupleAccess(_, _)
| E::ArrayAccess(_, _)
| E::PointerDeref(_)
| E::ChainAccess(_)) => {
Expand Down Expand Up @@ -516,9 +517,8 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> {
.as_ref()
.is_some_and(ghostscope_dwarf::is_c_pointer_or_array_type)
{
let pointed_plan = var
.plan_pointer_element_index(index)
.map_err(|err| CodeGenError::DwarfError(err.to_string()))?;
let pointed_plan =
self.plan_dwarf_pointer_element_index(&var, index)?;
let pc_address = self.get_compile_time_context()?.pc_address;
let materialized = self
.variable_read_plan_to_materialization(pointed_plan, pc_address)?;
Expand Down Expand Up @@ -794,6 +794,7 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> {
match e {
E::Variable(s) => s.clone(),
E::MemberAccess(obj, field) => format!("{}.{field}", inner(obj)),
E::TupleAccess(obj, index) => format!("{}.{index}", inner(obj)),
E::ArrayAccess(arr, idx) => format!("{}[{}]", inner(arr), inner(idx)),
E::PointerDeref(p) => format!("*{}", inner(p)),
E::AddressOf(p) => format!("&{}", inner(p)),
Expand Down Expand Up @@ -865,7 +866,8 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> {
| E::UnaryBitNot(inner)
| E::PointerDeref(inner)
| E::AddressOf(inner)
| E::MemberAccess(inner, _) => Self::expr_contains_builtin(inner),
| E::MemberAccess(inner, _)
| E::TupleAccess(inner, _) => Self::expr_contains_builtin(inner),
E::Cast { expr, .. } => Self::expr_contains_builtin(expr),
E::ArrayAccess(base, index) => {
Self::expr_contains_builtin(base) || Self::expr_contains_builtin(index)
Expand Down
55 changes: 55 additions & 0 deletions ghostscope-compiler/src/ebpf/dwarf_bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1345,6 +1345,10 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> {
let base = expand_aliases(ctx, obj, visited, depth + 1)?;
E::MemberAccess(Box::new(base), field.clone())
}
E::TupleAccess(obj, index) => {
let base = expand_aliases(ctx, obj, visited, depth + 1)?;
E::TupleAccess(Box::new(base), *index)
}
E::ArrayAccess(arr, idx) => {
let base = expand_aliases(ctx, arr, visited, depth + 1)?;
let idx2 = expand_aliases(ctx, idx, visited, depth + 1)?;
Expand Down Expand Up @@ -1426,6 +1430,7 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> {
match &expanded {
Expr::Variable(var_name) => self.query_dwarf_for_variable_plan(var_name),
Expr::MemberAccess(_, _)
| Expr::TupleAccess(_, _)
| Expr::ArrayAccess(_, _)
| Expr::ChainAccess(_)
| Expr::PointerDeref(_) => {
Expand Down Expand Up @@ -1533,6 +1538,17 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> {
self.query_dwarf_for_variable_plan(var_name)
}

pub(super) fn plan_dwarf_pointer_element_index(
&self,
plan: &VariableReadPlan,
index: i64,
) -> Result<VariableReadPlan> {
self.process_analyzer
.ok_or_else(|| CodeGenError::DwarfError("No DWARF analyzer available".to_string()))?
.plan_pointer_element_index(plan, index)
.map_err(|err| CodeGenError::DwarfError(err.to_string()))
}

fn query_dwarf_for_pc_access_plan(
&mut self,
base_name: &str,
Expand Down Expand Up @@ -1596,6 +1612,10 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> {
out.push('.');
out.push_str(field);
}
VariableAccessSegment::TupleIndex(index) => {
out.push('.');
out.push_str(&index.to_string());
}
VariableAccessSegment::ArrayIndex(index) => {
out.push('[');
out.push_str(&index.to_string());
Expand Down Expand Up @@ -1632,6 +1652,13 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> {
segments.push(VariableAccessSegment::Field(field.clone()));
Ok(Some(base))
}
crate::script::Expr::TupleAccess(obj, index) => {
let Some(base) = append_segments(obj, segments)? else {
return Ok(None);
};
segments.push(VariableAccessSegment::TupleIndex(*index));
Ok(Some(base))
}
crate::script::Expr::ArrayAccess(array, index) => {
let crate::script::Expr::Int(index) = index.as_ref() else {
return Err(CodeGenError::NotImplemented(
Expand Down Expand Up @@ -1758,6 +1785,34 @@ mod tests {
);
}

#[test]
fn access_path_from_expr_preserves_tuple_indices() {
let expr = Expr::TupleAccess(
Box::new(Expr::TupleAccess(
Box::new(Expr::Variable("nested".to_string())),
1,
)),
0,
);

let (base, path) = EbpfContext::<'static, 'static>::access_path_from_expr(&expr)
.expect("access path should parse")
.expect("expression should be flattenable");

assert_eq!(base, "nested");
assert_eq!(
path.segments,
vec![
VariableAccessSegment::TupleIndex(1),
VariableAccessSegment::TupleIndex(0),
]
);
assert_eq!(
EbpfContext::<'static, 'static>::access_path_to_string(&base, &path),
"nested.1.0"
);
}

#[test]
fn access_path_from_expr_rejects_dynamic_array_index() {
let expr = Expr::ArrayAccess(
Expand Down
Loading
Loading