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
47 changes: 31 additions & 16 deletions ghostscope-process/src/offsets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -385,18 +385,21 @@ impl ProcessManager {
}
}
let mut offsets = SectionOffsets::default();
if let Some(a0) = text_addr.and_then(find_bias_for) {
offsets.text = a0;
}
if let Some(a1) = rodata_addr.and_then(find_bias_for) {
offsets.rodata = a1;
}
if let Some(a2) = data_addr.and_then(find_bias_for) {
offsets.data = a2;
}
if let Some(a3) = bss_addr.and_then(find_bias_for) {
offsets.bss = a3;
}
// Each DW_OP_addr we encounter is an absolute link-time virtual address (e.g. 0x5798c for
// G_COUNTER). To rebase it we only need the ASLR bias `module_base`, not per-section
// runtime starts. Derive that bias from whichever segment we could match, then store it for
// all four slots so the eBPF helper can simply do `link_addr + bias`.
let module_base = text_addr
.and_then(find_bias_for)
.or_else(|| rodata_addr.and_then(find_bias_for))
.or_else(|| data_addr.and_then(find_bias_for))
.or_else(|| bss_addr.and_then(find_bias_for))
.unwrap_or(0);

offsets.text = module_base;
offsets.rodata = module_base;
offsets.data = module_base;
offsets.bss = module_base;
let cookie = crate::cookie::from_path(module_path);
let base = min_start.unwrap_or(0);
let size = max_end.unwrap_or(base).saturating_sub(base);
Expand All @@ -419,17 +422,29 @@ impl ProcessManager {
);
}
}
let runtime_text = text_addr
.map(|t| module_base.saturating_add(t))
.unwrap_or(0);
let runtime_ro = rodata_addr
.map(|r| module_base.saturating_add(r))
.unwrap_or(0);
let runtime_data = data_addr
.map(|d| module_base.saturating_add(d))
.unwrap_or(0);
let runtime_bss = bss_addr.map(|b| module_base.saturating_add(b)).unwrap_or(0);

tracing::debug!(
"computed offsets: pid={} module='{}' cookie=0x{:016x} base=0x{:x} size=0x{:x} text=0x{:x} rodata=0x{:x} data=0x{:x} bss=0x{:x}",
"computed offsets: pid={} module='{}' cookie=0x{:016x} base=0x{:x} size=0x{:x} module_bias=0x{:x} text=0x{:x} rodata=0x{:x} data=0x{:x} bss=0x{:x}",
pid,
module_path,
cookie,
base,
size,
offsets.text,
offsets.rodata,
offsets.data,
offsets.bss
runtime_text,
runtime_ro,
runtime_data,
runtime_bss
);
Ok((cookie, offsets, base, size))
}
Expand Down
6 changes: 6 additions & 0 deletions ghostscope/src/cli/runtime.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::config::MergedConfig;
use crate::core::GhostSession;
use anyhow::Result;
use std::io::{self, Write};
use tracing::{debug, error, info, warn};

/// Run GhostScope in command line mode with merged configuration
Expand Down Expand Up @@ -138,6 +139,11 @@ async fn run_cli_with_session(
for line in formatted_output {
println!(" {line}");
}
// When stdout is piped (as in tests), Rust switches to block buffering.
// Flush explicitly so short event bursts appear before the process exits.
if let Err(e) = io::stdout().flush() {
warn!("Failed to flush event output: {e}");
}
}

// Also show raw debug info if needed (can be removed later)
Expand Down
44 changes: 44 additions & 0 deletions ghostscope/tests/globals_execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2002,6 +2002,50 @@ trace globals_program.c:32 {
Ok(())
}

#[tokio::test]
async fn test_direct_bss_global_no_alias() -> anyhow::Result<()> {
// Focused regression: read the executable's .bss counter directly (without going through
// pointer aliases) to ensure rebasing logic works for zero-initialized globals.
init();

let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
let pid = prog
.id()
.ok_or_else(|| anyhow::anyhow!("Failed to get PID"))?;
tokio::time::sleep(Duration::from_millis(500)).await;

let script = r#"
trace globals_program.c:32 {
print "SBSS_ONLY:{}", s_bss_counter;
}
"#;
let (exit_code, stdout, stderr) = run_ghostscope_with_script_for_pid(script, 3, pid).await?;
let _ = prog.kill().await.is_ok();
assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");

let re = Regex::new(r"SBSS_ONLY:(-?\d+)").unwrap();
let mut vals = Vec::new();
for line in stdout.lines() {
if let Some(c) = re.captures(line) {
vals.push(c[1].parse::<i64>().unwrap_or(0));
}
}
assert!(
vals.len() >= 2,
"Insufficient SBSS_ONLY events. STDOUT: {stdout}"
);
for pair in vals.windows(2) {
assert_eq!(pair[1] - pair[0], 3, "s_bss_counter should +3 per tick");
}
Ok(())
}

#[tokio::test]
async fn test_direct_global_cross_module() -> anyhow::Result<()> {
init();
Expand Down
60 changes: 57 additions & 3 deletions ghostscope/tests/rust_script_execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ trace do_stuff {
}
"#;

let (exit_code, stdout, stderr) = run_ghostscope_with_script_for_pid(script, 4, pid).await?;
let (exit_code, stdout, stderr) = run_ghostscope_with_script_for_pid(script, 9, pid).await?;
let _ = prog.0.kill().await.is_ok();

assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");

assert!(
Expand Down Expand Up @@ -92,7 +93,7 @@ trace do_stuff {
print "RC:{}", G_COUNTER;
}
"#;
let (exit_code, stdout, stderr) = run_ghostscope_with_script_for_pid(script, 5, pid).await?;
let (exit_code, stdout, stderr) = run_ghostscope_with_script_for_pid(script, 9, pid).await?;
let _ = prog.0.kill().await.is_ok();
assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");

Expand Down Expand Up @@ -147,7 +148,7 @@ trace do_stuff {
print "&RC:{}", &G_COUNTER;
}
"#;
let (exit_code, stdout, stderr) = run_ghostscope_with_script_for_pid(script, 4, pid).await?;
let (exit_code, stdout, stderr) = run_ghostscope_with_script_for_pid(script, 9, pid).await?;
let _ = prog.0.kill().await.is_ok();
assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");

Expand All @@ -161,3 +162,56 @@ trace do_stuff {
);
Ok(())
}

#[tokio::test]
async fn test_rust_script_bss_counter_direct() -> anyhow::Result<()> {
// Regression coverage: ensure we can read a pure .bss global (G_COUNTER) directly, without
// relying on DWARF locals or pointer aliases.
init();

let binary_path = FIXTURES.get_test_binary("rust_global_program")?;
let bin_dir = binary_path.parent().unwrap();
struct KillOnDrop(tokio::process::Child);
impl Drop for KillOnDrop {
fn drop(&mut self) {
let _ = self.0.start_kill().is_ok();
}
}
let mut cmd = Command::new(&binary_path);
cmd.current_dir(bin_dir)
.stdout(Stdio::null())
.stderr(Stdio::null());
let child = cmd.spawn()?;
let pid = child.id().ok_or_else(|| anyhow::anyhow!("no pid"))?;
let mut prog = KillOnDrop(child);
tokio::time::sleep(Duration::from_millis(1500)).await;

let script = r#"
trace touch_globals {
print "BSSCNT:{}", G_COUNTER;
}
"#;

let (exit_code, stdout, stderr) = run_ghostscope_with_script_for_pid(script, 9, pid).await?;
let _ = prog.0.kill().await.is_ok();
assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");

let mut vals = Vec::new();
for line in stdout.lines() {
if let Some(pos) = line.find("BSSCNT:") {
if let Some(num_str) = line[pos + "BSSCNT:".len()..].split_whitespace().next() {
if let Ok(v) = num_str.parse::<i64>() {
vals.push(v);
}
}
}
}
assert!(
vals.len() >= 2,
"Insufficient BSSCNT events. STDOUT: {stdout}"
);
for pair in vals.windows(2) {
assert_eq!(pair[1] - pair[0], 1, "G_COUNTER should +1 per tick");
}
Ok(())
}