Skip to content
Draft
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
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ crossterm = { version = "0.29", features = ["event-stream"] }
clap = { version = "4.0", features = ["derive"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
bincode = "1.3"
toml = "0.8"
dirs = "5.0"
object = "0.36"
Expand Down
9 changes: 9 additions & 0 deletions config-zh.toml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,15 @@ search_paths = [
# 也会继续使用该独立调试文件(会记录警告日志)。仅建议在排障或环境不规范时短期启用。
allow_loose_debug_match = false

[dwarf.analysis_cache]
# 读取由 --prepare 生成、与运行时目标无关的 DWARF 解析索引。PID filter、
# 模块映射、CFI map 发布和 eBPF attach 状态都不会写入这里。
enabled = true

# 未设置时使用 $XDG_CACHE_HOME/ghostscope/analysis 或
# ~/.cache/ghostscope/analysis。
# directory = "/home/user/.cache/ghostscope/analysis"

[dwarf.debuginfod]
# debuginfod 调试信息回退。
# 模式参考 GDB:
Expand Down
10 changes: 10 additions & 0 deletions config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,16 @@ search_paths = [
# ad-hoc environments; may cause inaccurate symbol/line info.
allow_loose_debug_match = false

[dwarf.analysis_cache]
# Read parsed, target-independent DWARF indices populated by --prepare. Runtime
# PID filters, module mappings, CFI map publication, and eBPF attachment are
# never stored here.
enabled = true

# Leave unset to use $XDG_CACHE_HOME/ghostscope/analysis or
# ~/.cache/ghostscope/analysis.
# directory = "/home/user/.cache/ghostscope/analysis"

[dwarf.debuginfod]
# debuginfod fallback for separate debug information.
# This follows GDB-style modes:
Expand Down
35 changes: 35 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,28 @@ ghostscope -p 1234 --debug-file /path/to/binary.debug
# directories that you still rely on.
```

### Preparing the Analysis Cache

```bash
# Runs without root or eBPF capabilities.
ghostscope --prepare --target /path/to/binary

# Use the same explicit debug file and cache directory during prepare and run.
ghostscope --prepare -t /path/to/binary -d /path/to/binary.debug \
--analysis-cache-dir /var/cache/ghostscope
ghostscope -p 1234 -t /path/to/binary --script-file trace.gs \
--analysis-cache-dir /var/cache/ghostscope
```

Prepare stores parsed DWARF, source-line, and symbol indices. A tracing run still
resolves PID namespaces, module load addresses, `dlopen` changes, CFI map state,
kernel capabilities, and eBPF attachment at runtime. Cache entries are rejected
when the target or selected debug file identity changes.

CLI startup reports and the TUI loading summary show cache hits, misses, and
rejected entries. Misses and rejected entries are non-fatal: GhostScope parses
the original DWARF data, while rejected entries also include a fallback reason.

### Logging Configuration

```bash
Expand Down Expand Up @@ -242,6 +264,9 @@ Behavior:
| `--backtrace-depth <N>` | | Max DWARF-unwound frames captured by each `bt`/`backtrace` instruction (`1..=128`) | 128 |
| `--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 |
| `--prepare` | | Parse `--target` debug data into the persistent analysis cache and exit. Does not require eBPF privileges. | Off |
| `--analysis-cache-dir <DIR>` | | Override the parsed DWARF analysis cache directory | User cache directory |
| `--no-analysis-cache` | | Disable persistent parsed DWARF analysis caching | 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 Expand Up @@ -372,6 +397,16 @@ search_paths = [
# this off unless you know what you are doing.
allow_loose_debug_match = false

[dwarf.analysis_cache]
# Read target-independent DWARF, source-line, and symbol indices populated by
# --prepare. Runtime PID filters, ASLR/module ranges, CFI map publication, and
# eBPF attach state are resolved again for every tracing session.
enabled = true

# Leave unset to use $XDG_CACHE_HOME/ghostscope/analysis or
# ~/.cache/ghostscope/analysis.
# directory = "/home/user/.cache/ghostscope/analysis"

[dwarf.debuginfod]
# Optional debuginfod fallback for separate debug information.
# Modes follow GDB's model:
Expand Down
34 changes: 34 additions & 0 deletions docs/zh/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,27 @@ ghostscope -p 1234 --debug-file /path/to/binary.debug
# 注意:如果在 config.toml 中覆盖 search_paths,请保留仍然依赖的系统调试目录。
```

### 预生成分析缓存

```bash
# 不需要 root 或 eBPF capabilities。
ghostscope --prepare --target /path/to/binary

# prepare 和实际运行使用相同的显式 debug file 与缓存目录。
ghostscope --prepare -t /path/to/binary -d /path/to/binary.debug \
--analysis-cache-dir /var/cache/ghostscope
ghostscope -p 1234 -t /path/to/binary --script-file trace.gs \
--analysis-cache-dir /var/cache/ghostscope
```

Prepare 保存解析后的 DWARF、源码行和符号索引。实际 tracing 时仍会解析 PID
namespace、模块加载地址、`dlopen` 变化、CFI map 状态、内核能力和 eBPF attach。
目标文件或选中的 debug file 身份变化后,旧缓存会被拒绝。

CLI 启动报告和 TUI 加载摘要会显示 cache hit、miss 和 rejected 数量。miss 与
rejected 都不会导致启动失败:GhostScope 会解析原始 DWARF;rejected 还会显示
fallback 原因。

### 日志配置

```bash
Expand Down Expand Up @@ -241,6 +262,9 @@ ghostscope bpffs prune --dry-run --json
| `--backtrace-depth <N>` | | 每条 `bt`/`backtrace` 指令最多采集的 DWARF unwind 栈帧数(`1..=128`) | 128 |
| `--dry-run` | | 编译脚本、解析 trace 目标,然后退出,不 attach uprobe。需要与真实运行相同的 eBPF 权限和内核能力。 | 关 |
| `--dry-run-details` | | 在 dry-run 输出中包含源码、inline 和变量诊断;需要同时使用 `--dry-run` | 关 |
| `--prepare` | | 将 `--target` 的调试数据解析到持久化分析缓存后退出;不需要 eBPF 权限 | 关 |
| `--analysis-cache-dir <DIR>` | | 覆盖 DWARF 解析缓存目录 | 用户缓存目录 |
| `--no-analysis-cache` | | 禁用持久化 DWARF 解析缓存 | 关 |
| `--status` | | 启用交互式 DWARF/脚本/attach stderr 状态提示 | 开 |
| `--no-status` | | 禁用交互式 DWARF/脚本/attach stderr 状态提示 | 关闭覆盖 |
| `--script-timestamp <FORMAT>` | | pretty 输出时间戳:local, boot, none | local |
Expand Down Expand Up @@ -366,6 +390,16 @@ search_paths = [
# 候选只要包含可用 .debug_info,仍会报告为 debuglink。仅建议在排障或环境不规范时短期启用。
allow_loose_debug_match = false

[dwarf.analysis_cache]
# 读取由 --prepare 生成、与运行时目标无关的 DWARF、源码行和符号索引。
# PID filter、ASLR/模块 ranges、CFI map 发布和 eBPF attach 状态会在每个
# tracing session 中重新解析。
enabled = true

# 未设置时使用 $XDG_CACHE_HOME/ghostscope/analysis 或
# ~/.cache/ghostscope/analysis。
# directory = "/home/user/.cache/ghostscope/analysis"

[dwarf.debuginfod]
# 可选的 debuginfod 调试信息回退。
# 模式参考 GDB:
Expand Down
144 changes: 144 additions & 0 deletions e2e-tests/tests/backtrace_execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,46 @@ fn signal_backtrace_target(target: &TargetHandle, signal: &str) -> anyhow::Resul
Ok(())
}

fn prepare_analysis_cache(binary_path: &Path, cache_dir: &Path) -> anyhow::Result<String> {
let sandbox = common::sandbox::SandboxHandle::default_ghostscope()?;
anyhow::ensure!(
sandbox.is_host_backend(),
"analysis-cache backtrace prepare requires host GhostScope"
);
let (program, sandbox_args) = sandbox.ghostscope_command()?;
anyhow::ensure!(
sandbox_args.is_empty(),
"host GhostScope command unexpectedly has sandbox arguments"
);
let output = Command::new(program)
.args([
OsString::from("--prepare"),
OsString::from("--target"),
binary_path.as_os_str().to_os_string(),
OsString::from("--analysis-cache-dir"),
cache_dir.as_os_str().to_os_string(),
OsString::from("--no-log"),
])
.output()?;
let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
anyhow::ensure!(
output.status.success(),
"analysis cache prepare failed with status {}\nSTDOUT:\n{}\nSTDERR:\n{}",
output.status,
stdout,
stderr
);
Ok(stdout)
}

fn analysis_cache_entry_count(cache_dir: &Path) -> anyhow::Result<usize> {
Ok(fs::read_dir(cache_dir.join("v2"))?
.filter_map(Result::ok)
.filter(|entry| entry.path().is_file())
.count())
}

async fn run_hot_backtrace_with_depth(
script: &str,
depth: u8,
Expand Down Expand Up @@ -1105,6 +1145,110 @@ trace cross_module_lib_leaf {
Ok(())
}

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

let ghostscope_sandbox = common::sandbox::SandboxHandle::default_ghostscope()?;
let target_sandbox = common::sandbox::SandboxHandle::default_target()?;
if !ghostscope_sandbox.is_host_backend() || !target_sandbox.is_host_backend() {
println!("skipping prepared backtrace cache e2e outside host->host topology");
return Ok(());
}

let binary_path = FIXTURES.get_test_binary("backtrace_cross_module_program")?;
let cache = tempfile::tempdir()?;
let prepare_stdout = prepare_analysis_cache(&binary_path, cache.path())?;
assert!(
prepare_stdout.contains("Prepared analysis cache for"),
"prepare should populate a new analysis cache\nSTDOUT:\n{prepare_stdout}"
);
assert_eq!(
analysis_cache_entry_count(cache.path())?,
1,
"preparing only the main executable should create one cache entry"
);

let script = r#"
trace cross_module_lib_leaf {
print "PREPARED_CROSS_MODULE_STACK";
bt full;
}
"#;
let log_path = cache.path().join("ghostscope.log");
let (count, stdout, stderr) = run_backtrace_binary_with_args_and_mapped_module(
&binary_path,
script,
250,
3,
vec![
OsString::from("--backtrace-depth"),
OsString::from("5"),
OsString::from("--analysis-cache-dir"),
cache.path().as_os_str().to_os_string(),
OsString::from("--log"),
OsString::from("--log-console"),
OsString::from("--log-level"),
OsString::from("info"),
OsString::from("--log-file"),
log_path.into_os_string(),
],
None,
Some("libbacktrace_cross_module.so"),
)
.await?;
if count == 0 && stderr.contains("BPF_PROG_LOAD") {
return Ok(());
}

let main_cache_hit = stderr.lines().any(|line| {
line.contains("DWARF analysis cache hit for")
&& line.contains("backtrace_cross_module_program")
&& !line.contains("libbacktrace_cross_module.so")
});
assert!(
main_cache_hit,
"runtime should consume the prepared main executable cache\nSTDERR:\n{stderr}"
);
assert!(
!stderr.lines().any(|line| {
line.contains("DWARF analysis cache hit for")
&& line.contains("libbacktrace_cross_module.so")
}),
"the shared library should remain uncached\nSTDERR:\n{stderr}"
);
assert_eq!(
analysis_cache_entry_count(cache.path())?,
1,
"runtime cache reads must not add an entry for the shared library"
);

let block = first_backtrace_block_after(&stdout, "PREPARED_CROSS_MODULE_STACK", 5)?;
assert_ordered_patterns(
block,
&[
"#0 cross_module_lib_leaf",
"#1 cross_module_lib_probe",
"#2 cross_module_main_caller",
"#3 cross_module_main_loop",
"#4 main",
],
)?;
assert!(
block.contains("[libbacktrace_cross_module.so+")
&& block.contains("[backtrace_cross_module_program+"),
"backtrace should load CFI and symbols from both cached and uncached modules\nBLOCK:\n{block}"
);
assert!(
!block.contains("stopped: no unwind rows for PC")
&& !block.contains("stopped: unsupported CFI"),
"uncached module CFI should still be published\nBLOCK:\n{block}\nSTDERR:\n{stderr}"
);

Ok(())
}

#[tokio::test]
#[serial(backtrace_t_mode)]
async fn test_t_mode_cross_module_backtrace_resolves_so_and_exe_frames() -> anyhow::Result<()> {
Expand Down
Loading
Loading