diff --git a/ghostscope-process/src/offsets.rs b/ghostscope-process/src/offsets.rs index 05d6598..2f6ca53 100644 --- a/ghostscope-process/src/offsets.rs +++ b/ghostscope-process/src/offsets.rs @@ -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); @@ -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)) } diff --git a/ghostscope/src/cli/runtime.rs b/ghostscope/src/cli/runtime.rs index 32fe08e..0e2df2b 100644 --- a/ghostscope/src/cli/runtime.rs +++ b/ghostscope/src/cli/runtime.rs @@ -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 @@ -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) diff --git a/ghostscope/tests/globals_execution.rs b/ghostscope/tests/globals_execution.rs index a5e6b30..90315b5 100644 --- a/ghostscope/tests/globals_execution.rs +++ b/ghostscope/tests/globals_execution.rs @@ -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::().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(); diff --git a/ghostscope/tests/rust_script_execution.rs b/ghostscope/tests/rust_script_execution.rs index 39477e5..a65b007 100644 --- a/ghostscope/tests/rust_script_execution.rs +++ b/ghostscope/tests/rust_script_execution.rs @@ -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!( @@ -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}"); @@ -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}"); @@ -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::() { + 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(()) +}