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
68 changes: 68 additions & 0 deletions e2e-tests/tests/cpp_script_execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,24 @@ mod common;

use common::{init, FIXTURES};

const CPP_NESTED_MEMBER_TRACE_LINE: u32 = 44;

async fn compile_cpp_complex_script(
script: &str,
) -> anyhow::Result<ghostscope_compiler::CompilationResult> {
let binary_path = FIXTURES.get_test_binary("cpp_complex_program")?;
let mut analyzer = ghostscope_dwarf::DwarfAnalyzer::from_exec_path(&binary_path)
.await
.map_err(|e| anyhow::anyhow!("failed to load DWARF for cpp_complex_program: {e}"))?;
let compile_options = ghostscope_compiler::CompileOptions {
binary_path_hint: Some(binary_path.to_string_lossy().into_owned()),
..Default::default()
};

ghostscope_compiler::compile_script(script, &mut analyzer, None, Some(1), &compile_options)
.map_err(|e| anyhow::anyhow!("compile_script failed: {e}"))
}

async fn run_ghostscope_with_script_for_target(
script_content: &str,
timeout_secs: u64,
Expand Down Expand Up @@ -31,6 +49,56 @@ async fn spawn_cpp_complex_program() -> anyhow::Result<common::targets::TargetHa
Ok(target)
}

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

let binary_path = FIXTURES.get_test_binary("cpp_complex_program")?;
let source_path = binary_path
.parent()
.ok_or_else(|| anyhow::anyhow!("cpp_complex_program has no parent directory"))?
.join("main.cpp");

let valid_script = format!(
r#"
trace {}:{CPP_NESTED_MEMBER_TRACE_LINE} {{
print o.nested.shadow;
}}
"#,
source_path.display()
);
let valid = compile_cpp_complex_script(&valid_script).await?;
assert!(
!valid.uprobe_configs.is_empty(),
"expected valid o.nested.shadow to compile; target_info={} failed_targets={:?}",
valid.target_info,
valid.failed_targets
);

let invalid_script = format!(
r#"
trace {}:{CPP_NESTED_MEMBER_TRACE_LINE} {{
print o.shadow;
}}
"#,
source_path.display()
);
if let Ok(invalid) = compile_cpp_complex_script(&invalid_script).await {
assert!(
invalid.uprobe_configs.is_empty(),
"expected o.shadow to be rejected because shadow is only a member of o.nested; target_info={} failed_targets={:?}",
invalid.target_info,
invalid.failed_targets
);
assert!(
!invalid.failed_targets.is_empty(),
"expected at least one failed target for invalid o.shadow access"
);
}

Ok(())
}

#[tokio::test]
async fn test_cpp_script_print_globals() -> anyhow::Result<()> {
init();
Expand Down
24 changes: 24 additions & 0 deletions e2e-tests/tests/fixtures/cpp_complex_program/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include <string>
#include <thread>
#include <chrono>
#include <cstdint>

int g_counter = 0;
const char* g_msg = "hello cpp";
Expand All @@ -10,6 +11,17 @@ static int s_internal = 123;
namespace ns1 {
struct Point { int x; int y; };

struct Outer {
struct Nested {
int shadow;
int payload;
};

int tag;
Nested nested;
int tail;
};

class Foo {
public:
static int s_val;
Expand All @@ -22,6 +34,17 @@ int Foo::s_val = 7;
__attribute__((noinline)) int add(int a, int b) { return a + b; }
__attribute__((noinline)) int add(double a, double b) { return (int)(a + b); }

__attribute__((noinline)) int nested_member_probe(int v) {
volatile Outer outer = {
101,
{202 + v, 303},
404,
};
Outer* o = (Outer*)&outer;
volatile std::uintptr_t sink = (std::uintptr_t)o + (std::uintptr_t)o->nested.shadow;
return (int)sink;
}

// Variables purposely ending with ::h and ::h264 to validate demangled leaf handling
int h = 5;
int h264 = 7;
Expand All @@ -40,6 +63,7 @@ int main() {
acc += f.bar(i);
acc += ns1::add(i, i+1);
acc += ns1::add(1.5, 2.5);
acc += ns1::nested_member_probe(i);
touch_globals();
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
}
Expand Down
73 changes: 68 additions & 5 deletions e2e-tests/tests/optimized_inline_call_value_execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,6 @@ use std::time::Duration;
// Keep this on the first executable line before consume_pair() is called.
const INLINE_BEFORE_CALL_TRACE_LINE: u32 = 20;
// Keep this on the first executable line after consume_pair() returns.
// This is intentionally a negative regression point: at this PC the inline
// parameters have already fallen out of their own location-list coverage, so
// we only assert that GhostScope does not misreport them as consume_pair's
// argument registers. We do not expect post-call value recovery to work until
// full DW_OP_entry_value + caller-side call-site evaluation is implemented.
const INLINE_AFTER_CALL_TRACE_LINE: u32 = 22;

async fn spawn_inline_call_value_program(
Expand Down Expand Up @@ -272,3 +267,71 @@ async fn test_optimized_inline_parameters_survive_internal_call_sites() -> anyho

Ok(())
}

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

let binary_path = FIXTURES.get_test_binary("inline_call_value_program")?;
let mut analyzer = ghostscope_dwarf::DwarfAnalyzer::from_exec_path(&binary_path).await?;
let addrs = analyzer.lookup_addresses_by_source_line(
"inline_call_value_program.c",
INLINE_AFTER_CALL_TRACE_LINE,
);
anyhow::ensure!(
!addrs.is_empty(),
"No DWARF addresses found for inline_call_value_program.c:{INLINE_AFTER_CALL_TRACE_LINE}"
);
for module_address in &addrs {
anyhow::ensure!(
analyzer.is_inline_at(module_address) == Some(true),
"Expected inline address at 0x{:x}",
module_address.address
);
}

let target = spawn_inline_call_value_program(&binary_path).await?;
let script = format!(
"trace inline_call_value_program.c:{INLINE_AFTER_CALL_TRACE_LINE} {{\n print \"POSTCALL:{{}}:{{}}\", seed, after_call;\n}}\n"
);
let (exit_code, stdout, stderr) =
run_ghostscope_with_script_for_target(&script, 4, &target).await?;
target.terminate().await?;

if should_skip_for_ebpf_env(exit_code, &stderr) {
return Ok(());
}

assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");
assert!(
!stdout.contains("ExprError"),
"Expected exact entry_value recovery inside the inline body. STDOUT: {stdout}\nSTDERR: {stderr}"
);
assert!(
!stdout.contains("<optimized_out>"),
"Inline post-call entry_value should not be optimized out. STDOUT: {stdout}\nSTDERR: {stderr}"
);

let re = Regex::new(r"POSTCALL:([0-9-]+):([0-9-]+)")?;
let mut seen = 0;
for caps in re.captures_iter(&stdout) {
let seed: i64 = caps[1].parse()?;
let after_call: i64 = caps[2].parse()?;
let original_x = seed * 7;
let original_y = seed + 11;
let combined = (original_x + original_y) * (original_x - original_y);
assert_eq!(
after_call,
combined + 7,
"Expected seed/after_call to match wrapper(seed) on the first post-call line. STDOUT: {stdout}"
);
seen += 1;
}

assert!(
seen >= 2,
"Expected multiple post-call entry_value events. STDOUT: {stdout}\nSTDERR: {stderr}"
);
Ok(())
}
89 changes: 2 additions & 87 deletions ghostscope-dwarf/src/index/block_index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,29 +172,9 @@ impl FunctionBlocks {
out
}

/// Find the nearest caller-side call-site parameter binding whose return_pc
/// is at or before `pc` and whose callee entry register matches `register`.
pub fn entry_value_parameter_for_pc(
&self,
pc: u64,
register: u16,
) -> Option<&CallSiteParameter> {
for (_, records) in self.call_sites.range(..=pc).rev() {
for record in records.iter().rev() {
if let Some(parameter) = record
.parameters
.iter()
.find(|parameter| parameter.callee_register == register)
{
return Some(parameter);
}
}
}
None
}

/// Collect all incoming caller-side call-site bindings for a callee
/// register. These are used for non-inline DW_OP_entry_value recovery.
/// register. These drive DW_OP_entry_value recovery without consulting
/// nested outgoing call sites inside the current function body.
pub fn incoming_entry_value_parameters(&self, register: u16) -> Vec<(u64, &CallSiteParameter)> {
let mut out = Vec::new();
for (return_pc, records) in &self.incoming_call_sites {
Expand All @@ -208,17 +188,6 @@ impl FunctionBlocks {
}
out
}

/// True when `pc` is inside an inlined-subroutine scope in this function.
pub fn is_inline_context_at(&self, pc: u64) -> bool {
if !self.function_contains_pc(pc) {
return false;
}
self.block_path_for_pc(pc)
.into_iter()
.skip(1)
.any(|idx| self.nodes[idx].entry_pc.is_some())
}
}

/// Global per-module block index
Expand Down Expand Up @@ -1400,58 +1369,4 @@ mod tests {
assert_eq!(incoming[0].call_origin, None);
assert_eq!(incoming[0].call_target, Some(0x1200));
}

#[test]
fn entry_value_parameter_lookup_uses_nearest_prior_return_pc() {
let mut function = FunctionBlocks::new(gimli::DebugInfoOffset(0), gimli::UnitOffset(0));
function.call_sites.insert(
0x1018,
vec![CallSiteRecord {
cu_offset: gimli::DebugInfoOffset(0),
die_offset: gimli::UnitOffset(1),
return_pc: 0x1018,
call_origin: None,
call_target: None,
parameters: vec![CallSiteParameter {
callee_register: 5,
caller_value_steps: vec![ComputeStep::PushConstant(11)],
}],
}],
);
function.call_sites.insert(
0x1030,
vec![CallSiteRecord {
cu_offset: gimli::DebugInfoOffset(0),
die_offset: gimli::UnitOffset(2),
return_pc: 0x1030,
call_origin: None,
call_target: None,
parameters: vec![CallSiteParameter {
callee_register: 5,
caller_value_steps: vec![ComputeStep::PushConstant(22)],
}],
}],
);

let parameter = function
.entry_value_parameter_for_pc(0x1034, 5)
.expect("nearest call-site parameter should be found");
assert_eq!(
parameter.caller_value_steps,
vec![ComputeStep::PushConstant(22)]
);

let earlier = function
.entry_value_parameter_for_pc(0x1019, 5)
.expect("earlier call-site parameter should be found");
assert_eq!(
earlier.caller_value_steps,
vec![ComputeStep::PushConstant(11)]
);

assert!(
function.entry_value_parameter_for_pc(0x1017, 5).is_none(),
"call sites after the current PC must not match"
);
}
}
Loading
Loading