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
2 changes: 2 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,8 @@ Behavior:
| `--script-file <PATH>` | | Script file to execute | None |
| `--script-help` | | Print the embedded script language reference and exit | Off |
| `--script-output <MODE>` | | Script event stdout mode: pretty, plain | pretty |
| `--dry-run` | | Compile the script, resolve trace targets, and exit without attaching uprobes. Requires the same eBPF privileges and kernel capabilities as a real run. | Off |
| `--dry-run-details` | | Include source, inline, and variable diagnostics in dry-run output; requires `--dry-run` | Off |
| `--status` | | Enable interactive DWARF/script/attach stderr status prompts | On |
| `--no-status` | | Disable interactive DWARF/script/attach stderr status prompts | Off override |
| `--script-timestamp <FORMAT>` | | Pretty output timestamp: local, boot, none | local |
Expand Down
15 changes: 15 additions & 0 deletions docs/scripting.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,21 @@ GhostScope supports the following statements:

The `trace` statement is the top‑level construct used only at the script file level (not nested inside other trace blocks).

Before attaching uprobes, you can validate a script and inspect the resolved
targets:

```bash
ghostscope -p 1234 --script-file trace.gs --dry-run
ghostscope -p 1234 --script-file trace.gs --dry-run --dry-run-details
```

`--dry-run` parses DWARF, compiles the script, resolves PCs and uprobe file
offsets, then exits without attaching. It still performs the same startup
privilege and kernel capability checks as a real run, so use `sudo` or
equivalent eBPF privileges when your system requires them. `--dry-run-details`
adds source locations, inline classification, script-used variables, visible
variables, and optimized-out/unavailable variable diagnostics.

### Syntax

```ghostscope
Expand Down
2 changes: 2 additions & 0 deletions docs/zh/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,8 @@ ghostscope bpffs prune --dry-run --json
| `--script-file <PATH>` | | 要执行的脚本文件 | 无 |
| `--script-help` | | 输出内嵌的脚本语言参考并退出 | 关 |
| `--script-output <MODE>` | | 脚本事件 stdout 模式:pretty, plain | pretty |
| `--dry-run` | | 编译脚本、解析 trace 目标,然后退出,不 attach uprobe。需要与真实运行相同的 eBPF 权限和内核能力。 | 关 |
| `--dry-run-details` | | 在 dry-run 输出中包含源码、inline 和变量诊断;需要同时使用 `--dry-run` | 关 |
| `--status` | | 启用交互式 DWARF/脚本/attach stderr 状态提示 | 开 |
| `--no-status` | | 禁用交互式 DWARF/脚本/attach stderr 状态提示 | 关闭覆盖 |
| `--script-timestamp <FORMAT>` | | pretty 输出时间戳:local, boot, none | local |
Expand Down
9 changes: 9 additions & 0 deletions docs/zh/scripting.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ GhostScope 支持以下语句类型:

`trace` 语句是定义追踪点的顶层结构。它只在脚本文件中使用,不在追踪块内部使用。

在真正 attach uprobe 之前,可以先验证脚本并查看解析结果:

```bash
ghostscope -p 1234 --script-file trace.gs --dry-run
ghostscope -p 1234 --script-file trace.gs --dry-run --dry-run-details
```

`--dry-run` 会解析 DWARF、编译脚本、解析 PC 和 uprobe 文件偏移,然后直接退出,不 attach。它仍会执行与真实运行相同的启动权限和内核能力检查;如果当前系统要求,请使用 `sudo` 或等价 eBPF 权限。`--dry-run-details` 会追加源码位置、inline 分类、脚本用到的变量、当前 PC 可见变量,以及 optimized-out/不可用变量诊断。

### 语法

```ghostscope
Expand Down
316 changes: 316 additions & 0 deletions ghostscope/src/cli/dry_run.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,316 @@
use ghostscope_compiler::{CompilationResult, UProbeConfig};
use ghostscope_dwarf::{
Availability, DwarfAnalyzer, ModuleAddress, Provenance, RuntimeCapabilities,
VariableLoweringKind, VariableQueryDiagnostic, VariableReadPlan, VisibleVariable,
};

const VARIABLE_DISPLAY_LIMIT: usize = 40;

#[derive(Debug, Clone, Copy)]
pub struct DryRunReportOptions {
pub details: bool,
}

pub fn print_dry_run_report(
result: &CompilationResult,
analyzer: Option<&DwarfAnalyzer>,
runtime_capabilities: &RuntimeCapabilities,
options: DryRunReportOptions,
) {
let attachable_configs: Vec<_> = result
.uprobe_configs
.iter()
.filter(|config| is_attachable_config(config))
.collect();
let unattachable_configs: Vec<_> = result
.uprobe_configs
.iter()
.filter(|config| !is_attachable_config(config))
.collect();

println!("GhostScope dry run: no uprobes were attached.");
println!(
"Summary: {} attachable target(s), {} unattachable resolved target(s), {} failed target(s), next trace id {}",
attachable_configs.len(),
unattachable_configs.len(),
result.failed_targets.len(),
result.next_available_trace_id
);
if !result.target_info.is_empty() {
println!("Primary target: {}", result.target_info);
}

if attachable_configs.is_empty() {
println!("\nAttachable targets: none");
} else {
println!("\nAttachable targets:");
for (index, config) in attachable_configs.iter().enumerate() {
print_target(
index + 1,
config,
analyzer,
runtime_capabilities,
options.details,
);
}
}

if !unattachable_configs.is_empty() {
println!("\nResolved but not attachable targets:");
for (index, config) in unattachable_configs.iter().enumerate() {
print_target(
index + 1,
config,
analyzer,
runtime_capabilities,
options.details,
);
}
}

if !result.failed_targets.is_empty() {
println!("\nFailed targets:");
for failed in &result.failed_targets {
println!(
" - {} at 0x{:x}: {}",
failed.target_name, failed.pc_address, failed.error_message
);
}
}
}

fn is_attachable_config(config: &UProbeConfig) -> bool {
config.uprobe_offset.is_some()
}

fn print_target(
index: usize,
config: &UProbeConfig,
analyzer: Option<&DwarfAnalyzer>,
runtime_capabilities: &RuntimeCapabilities,
details: bool,
) {
let pc = config.function_address.unwrap_or(0);
let target = target_label(config);
let offset = config
.uprobe_offset
.map(|offset| format!("0x{offset:x}"))
.unwrap_or_else(|| "<unresolved>".to_string());

println!(
" [{index}] {} -> pc=0x{pc:x}, uprobe_offset={}, module={}",
target, offset, config.binary_path
);

if !details {
return;
}

println!(" trace_id: {}", config.assigned_trace_id);
println!(" eBPF program: {}", config.ebpf_function_name);
println!(" pattern: {}", trace_pattern_label(config));
println!(" attachable: {}", attachable_label(config));
if let Some(address_index) = config.resolved_address_index {
println!(" resolved address index: {address_index}");
}
if let Some(source) = source_label(config, analyzer) {
println!(" source: {source}");
}
if let Some(inline) = inline_label(config, analyzer) {
println!(" inline context: {inline}");
}
print_used_variables(config);
print_visible_variables(config, analyzer, runtime_capabilities);
}

fn target_label(config: &UProbeConfig) -> String {
config
.function_name
.clone()
.unwrap_or_else(|| format!("0x{:x}", config.function_address.unwrap_or(0)))
}

fn trace_pattern_label(config: &UProbeConfig) -> String {
use ghostscope_compiler::script::TracePattern;

match &config.trace_pattern {
TracePattern::FunctionName(name) => format!("function {name}"),
TracePattern::Wildcard(pattern) => format!("wildcard {pattern}"),
TracePattern::Address(address) => format!("address 0x{address:x}"),
TracePattern::AddressInModule { module, address } => {
format!("address {module}:0x{address:x}")
}
TracePattern::SourceLine {
file_path,
line_number,
} => format!("source {file_path}:{line_number}"),
}
}

fn attachable_label(config: &UProbeConfig) -> &'static str {
if config.uprobe_offset.is_some() && !config.ebpf_bytecode.is_empty() {
"yes (file offset resolved; eBPF bytecode generated)"
} else if config.uprobe_offset.is_some() {
"partial (file offset resolved; empty eBPF bytecode)"
} else {
"no (file offset unresolved)"
}
}

fn module_address(config: &UProbeConfig) -> Option<ModuleAddress> {
config.function_address.map(|address| {
ModuleAddress::new(
std::path::PathBuf::from(config.binary_path.clone()),
address,
)
})
}

fn source_label(config: &UProbeConfig, analyzer: Option<&DwarfAnalyzer>) -> Option<String> {
let analyzer = analyzer?;
let module_address = module_address(config)?;
let source = analyzer.lookup_source_location(&module_address)?;
let column = source
.column
.map(|column| format!(":{column}"))
.unwrap_or_default();
Some(format!(
"{}:{}{}",
source.file_path, source.line_number, column
))
}

fn inline_label(config: &UProbeConfig, analyzer: Option<&DwarfAnalyzer>) -> Option<&'static str> {
let analyzer = analyzer?;
let module_address = module_address(config)?;
analyzer
.is_inline_at(&module_address)
.map(|is_inline| if is_inline { "yes" } else { "no" })
}

fn print_used_variables(config: &UProbeConfig) {
if config.trace_context.variable_names.is_empty() {
println!(" variables used by script: none");
return;
}

println!(
" variables used by script: {}",
config.trace_context.variable_names.join(", ")
);
}

fn print_visible_variables(
config: &UProbeConfig,
analyzer: Option<&DwarfAnalyzer>,
runtime_capabilities: &RuntimeCapabilities,
) {
let Some(analyzer) = analyzer else {
println!(" visible variables: unavailable (no DWARF analyzer)");
return;
};
let Some(module_address) = module_address(config) else {
println!(" visible variables: unavailable (no PC)");
return;
};

let visible = analyzer
.resolve_pc(&module_address)
.and_then(|ctx| analyzer.visible_variables_with_diagnostics(&ctx));

match visible {
Ok(result) => {
print_visible_variable_list(&result.variables, runtime_capabilities);
print_variable_diagnostics(&result.diagnostics);
}
Err(error) => {
println!(" visible variables: unavailable ({error})");
}
}
}

fn print_visible_variable_list(
variables: &[VisibleVariable],
runtime_capabilities: &RuntimeCapabilities,
) {
if variables.is_empty() {
println!(" visible variables: none");
return;
}

println!(
" visible variables: {} shown{}",
variables.len().min(VARIABLE_DISPLAY_LIMIT),
if variables.len() > VARIABLE_DISPLAY_LIMIT {
format!(" of {}", variables.len())
} else {
String::new()
}
);

for variable in variables.iter().take(VARIABLE_DISPLAY_LIMIT) {
let plan = VariableReadPlan::from_visible_variable(variable.clone(), Provenance::DirectDie);
let materialization = plan.materialization_plan(runtime_capabilities);
println!(
" - {}{}: {} [{}; {}; {}]",
if variable.is_parameter { "param " } else { "" },
variable.name,
variable.type_name,
availability_label(&materialization.availability),
lowering_label(&materialization.lowering.kind),
variable.location
);
}

if variables.len() > VARIABLE_DISPLAY_LIMIT {
println!(
" ... {} more variable(s) omitted",
variables.len() - VARIABLE_DISPLAY_LIMIT
);
}
}

fn print_variable_diagnostics(diagnostics: &[VariableQueryDiagnostic]) {
if diagnostics.is_empty() {
return;
}

println!(" variable diagnostics:");
for diagnostic in diagnostics.iter().take(VARIABLE_DISPLAY_LIMIT) {
let name = diagnostic.name.as_deref().unwrap_or("<unnamed>");
println!(
" - {}: {} [{}]",
name,
diagnostic.detail,
availability_label(&diagnostic.availability)
);
}

if diagnostics.len() > VARIABLE_DISPLAY_LIMIT {
println!(
" ... {} more diagnostic(s) omitted",
diagnostics.len() - VARIABLE_DISPLAY_LIMIT
);
}
}

fn availability_label(availability: &Availability) -> String {
match availability {
Availability::Available => "available".to_string(),
Availability::PartiallyAvailable => "partially available".to_string(),
Availability::OptimizedOut => "optimized out".to_string(),
Availability::NotInScope => "not in scope".to_string(),
Availability::Unsupported(reason) => format!("unsupported: {reason:?}"),
Availability::Requires(requirement) => format!("requires: {requirement:?}"),
Availability::Ambiguous(reason) => format!("ambiguous: {reason:?}"),
}
}

fn lowering_label(kind: &VariableLoweringKind) -> &'static str {
match kind {
VariableLoweringKind::DirectValue => "direct value",
VariableLoweringKind::UserMemoryRead => "user memory read",
VariableLoweringKind::Composite => "composite",
VariableLoweringKind::Unavailable => "unavailable",
}
}
1 change: 1 addition & 0 deletions ghostscope/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

mod color;
mod docs;
mod dry_run;
mod loading_reporter;
pub mod script_output;
pub mod script_runtime;
Expand Down
Loading
Loading