Skip to content

Commit 5cef595

Browse files
committed
fix: recover inline entry values via incoming call sites
1 parent 247bbc0 commit 5cef595

3 files changed

Lines changed: 82 additions & 114 deletions

File tree

e2e-tests/tests/optimized_inline_call_value_execution.rs

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,6 @@ use std::time::Duration;
99
// Keep this on the first executable line before consume_pair() is called.
1010
const INLINE_BEFORE_CALL_TRACE_LINE: u32 = 20;
1111
// Keep this on the first executable line after consume_pair() returns.
12-
// This is intentionally a negative regression point: at this PC the inline
13-
// parameters have already fallen out of their own location-list coverage, so
14-
// we only assert that GhostScope does not misreport them as consume_pair's
15-
// argument registers. We do not expect post-call value recovery to work until
16-
// full DW_OP_entry_value + caller-side call-site evaluation is implemented.
1712
const INLINE_AFTER_CALL_TRACE_LINE: u32 = 22;
1813

1914
async fn spawn_inline_call_value_program(
@@ -272,3 +267,71 @@ async fn test_optimized_inline_parameters_survive_internal_call_sites() -> anyho
272267

273268
Ok(())
274269
}
270+
271+
#[tokio::test]
272+
async fn test_entry_value_recovers_outer_parameter_inside_optimized_inline_after_internal_call(
273+
) -> anyhow::Result<()> {
274+
init();
275+
276+
let binary_path = FIXTURES.get_test_binary("inline_call_value_program")?;
277+
let mut analyzer = ghostscope_dwarf::DwarfAnalyzer::from_exec_path(&binary_path).await?;
278+
let addrs = analyzer.lookup_addresses_by_source_line(
279+
"inline_call_value_program.c",
280+
INLINE_AFTER_CALL_TRACE_LINE,
281+
);
282+
anyhow::ensure!(
283+
!addrs.is_empty(),
284+
"No DWARF addresses found for inline_call_value_program.c:{INLINE_AFTER_CALL_TRACE_LINE}"
285+
);
286+
for module_address in &addrs {
287+
anyhow::ensure!(
288+
analyzer.is_inline_at(module_address) == Some(true),
289+
"Expected inline address at 0x{:x}",
290+
module_address.address
291+
);
292+
}
293+
294+
let target = spawn_inline_call_value_program(&binary_path).await?;
295+
let script = format!(
296+
"trace inline_call_value_program.c:{INLINE_AFTER_CALL_TRACE_LINE} {{\n print \"POSTCALL:{{}}:{{}}\", seed, after_call;\n}}\n"
297+
);
298+
let (exit_code, stdout, stderr) =
299+
run_ghostscope_with_script_for_target(&script, 4, &target).await?;
300+
target.terminate().await?;
301+
302+
if should_skip_for_ebpf_env(exit_code, &stderr) {
303+
return Ok(());
304+
}
305+
306+
assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");
307+
assert!(
308+
!stdout.contains("ExprError"),
309+
"Expected exact entry_value recovery inside the inline body. STDOUT: {stdout}\nSTDERR: {stderr}"
310+
);
311+
assert!(
312+
!stdout.contains("<optimized_out>"),
313+
"Inline post-call entry_value should not be optimized out. STDOUT: {stdout}\nSTDERR: {stderr}"
314+
);
315+
316+
let re = Regex::new(r"POSTCALL:([0-9-]+):([0-9-]+)")?;
317+
let mut seen = 0;
318+
for caps in re.captures_iter(&stdout) {
319+
let seed: i64 = caps[1].parse()?;
320+
let after_call: i64 = caps[2].parse()?;
321+
let original_x = seed * 7;
322+
let original_y = seed + 11;
323+
let combined = (original_x + original_y) * (original_x - original_y);
324+
assert_eq!(
325+
after_call,
326+
combined + 7,
327+
"Expected seed/after_call to match wrapper(seed) on the first post-call line. STDOUT: {stdout}"
328+
);
329+
seen += 1;
330+
}
331+
332+
assert!(
333+
seen >= 2,
334+
"Expected multiple post-call entry_value events. STDOUT: {stdout}\nSTDERR: {stderr}"
335+
);
336+
Ok(())
337+
}

ghostscope-dwarf/src/index/block_index.rs

Lines changed: 2 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -172,29 +172,9 @@ impl FunctionBlocks {
172172
out
173173
}
174174

175-
/// Find the nearest caller-side call-site parameter binding whose return_pc
176-
/// is at or before `pc` and whose callee entry register matches `register`.
177-
pub fn entry_value_parameter_for_pc(
178-
&self,
179-
pc: u64,
180-
register: u16,
181-
) -> Option<&CallSiteParameter> {
182-
for (_, records) in self.call_sites.range(..=pc).rev() {
183-
for record in records.iter().rev() {
184-
if let Some(parameter) = record
185-
.parameters
186-
.iter()
187-
.find(|parameter| parameter.callee_register == register)
188-
{
189-
return Some(parameter);
190-
}
191-
}
192-
}
193-
None
194-
}
195-
196175
/// Collect all incoming caller-side call-site bindings for a callee
197-
/// register. These are used for non-inline DW_OP_entry_value recovery.
176+
/// register. These drive DW_OP_entry_value recovery without consulting
177+
/// nested outgoing call sites inside the current function body.
198178
pub fn incoming_entry_value_parameters(&self, register: u16) -> Vec<(u64, &CallSiteParameter)> {
199179
let mut out = Vec::new();
200180
for (return_pc, records) in &self.incoming_call_sites {
@@ -208,17 +188,6 @@ impl FunctionBlocks {
208188
}
209189
out
210190
}
211-
212-
/// True when `pc` is inside an inlined-subroutine scope in this function.
213-
pub fn is_inline_context_at(&self, pc: u64) -> bool {
214-
if !self.function_contains_pc(pc) {
215-
return false;
216-
}
217-
self.block_path_for_pc(pc)
218-
.into_iter()
219-
.skip(1)
220-
.any(|idx| self.nodes[idx].entry_pc.is_some())
221-
}
222191
}
223192

224193
/// Global per-module block index
@@ -1400,58 +1369,4 @@ mod tests {
14001369
assert_eq!(incoming[0].call_origin, None);
14011370
assert_eq!(incoming[0].call_target, Some(0x1200));
14021371
}
1403-
1404-
#[test]
1405-
fn entry_value_parameter_lookup_uses_nearest_prior_return_pc() {
1406-
let mut function = FunctionBlocks::new(gimli::DebugInfoOffset(0), gimli::UnitOffset(0));
1407-
function.call_sites.insert(
1408-
0x1018,
1409-
vec![CallSiteRecord {
1410-
cu_offset: gimli::DebugInfoOffset(0),
1411-
die_offset: gimli::UnitOffset(1),
1412-
return_pc: 0x1018,
1413-
call_origin: None,
1414-
call_target: None,
1415-
parameters: vec![CallSiteParameter {
1416-
callee_register: 5,
1417-
caller_value_steps: vec![ComputeStep::PushConstant(11)],
1418-
}],
1419-
}],
1420-
);
1421-
function.call_sites.insert(
1422-
0x1030,
1423-
vec![CallSiteRecord {
1424-
cu_offset: gimli::DebugInfoOffset(0),
1425-
die_offset: gimli::UnitOffset(2),
1426-
return_pc: 0x1030,
1427-
call_origin: None,
1428-
call_target: None,
1429-
parameters: vec![CallSiteParameter {
1430-
callee_register: 5,
1431-
caller_value_steps: vec![ComputeStep::PushConstant(22)],
1432-
}],
1433-
}],
1434-
);
1435-
1436-
let parameter = function
1437-
.entry_value_parameter_for_pc(0x1034, 5)
1438-
.expect("nearest call-site parameter should be found");
1439-
assert_eq!(
1440-
parameter.caller_value_steps,
1441-
vec![ComputeStep::PushConstant(22)]
1442-
);
1443-
1444-
let earlier = function
1445-
.entry_value_parameter_for_pc(0x1019, 5)
1446-
.expect("earlier call-site parameter should be found");
1447-
assert_eq!(
1448-
earlier.caller_value_steps,
1449-
vec![ComputeStep::PushConstant(11)]
1450-
);
1451-
1452-
assert!(
1453-
function.entry_value_parameter_for_pc(0x1017, 5).is_none(),
1454-
"call sites after the current PC must not match"
1455-
);
1456-
}
14571372
}

ghostscope-dwarf/src/parser/expression_evaluator.rs

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -347,12 +347,9 @@ impl ExpressionEvaluator {
347347
debug!("Found DW_OP_stack_value - this is a computed value");
348348
}
349349
match &op {
350-
// TODO(entry_value): This is only a minimal fallback.
351-
// For inline parameters, a correct DW_OP_entry_value implementation must
352-
// walk back to the caller and evaluate the matching call_site_parameter's
353-
// DW_AT_call_value / DW_AT_GNU_call_site_value. Merely stripping
354-
// entry_value(reg) to a bare register only preserves the simplest cases
355-
// where the parameter is still equivalent to the entry register.
350+
// Lower supported DW_OP_entry_value forms through caller-side
351+
// call-site metadata. This keeps optimized parameters usable
352+
// after their entry registers have been clobbered.
356353
Operation::EntryValue { expression } => {
357354
let mut inner = *expression;
358355
let mut inner_ops: Vec<Operation<_>> = Vec::new();
@@ -1297,18 +1294,6 @@ impl ExpressionEvaluator {
12971294
let function_context = function_context.ok_or_else(|| {
12981295
anyhow::anyhow!("DW_OP_entry_value requires function call-site context")
12991296
})?;
1300-
if function_context.is_inline_context_at(current_pc) {
1301-
if let Some(parameter) =
1302-
function_context.entry_value_parameter_for_pc(current_pc, register)
1303-
{
1304-
return Self::materialize_caller_value_steps(
1305-
&parameter.caller_value_steps,
1306-
current_pc,
1307-
cfi_index,
1308-
);
1309-
}
1310-
}
1311-
13121297
Self::build_incoming_entry_value_lookup(
13131298
current_pc,
13141299
register,
@@ -1703,7 +1688,7 @@ mod tests {
17031688
}
17041689

17051690
#[test]
1706-
fn entry_value_uses_nearest_call_site_parameter_steps_in_inline_context() {
1691+
fn entry_value_ignores_outgoing_call_sites_in_inline_context() {
17071692
let mut function = FunctionBlocks {
17081693
cu_offset: gimli::DebugInfoOffset(0),
17091694
die_offset: gimli::UnitOffset(0),
@@ -1758,15 +1743,20 @@ mod tests {
17581743
}],
17591744
);
17601745

1761-
let steps = ExpressionEvaluator::resolve_entry_value_register(
1746+
let error = ExpressionEvaluator::resolve_entry_value_register(
17621747
0x1034,
17631748
5,
17641749
None,
17651750
Some(&function),
17661751
None,
17671752
)
1768-
.expect("entry_value should resolve to the nearest call site");
1769-
assert_eq!(steps, vec![ComputeStep::PushConstant(22)]);
1753+
.expect_err("inline entry_value must not reuse nested outgoing call-site bindings");
1754+
assert!(
1755+
error
1756+
.to_string()
1757+
.contains("DW_OP_entry_value register recovery needs CFI"),
1758+
"unexpected error: {error}"
1759+
);
17701760
}
17711761

17721762
#[test]

0 commit comments

Comments
 (0)