Skip to content

Commit cdbf3fe

Browse files
committed
fix: recover non-inline entry values at runtime
Map caller-side call-site metadata back to callees and lower non-inline DW_OP_entry_value(reg) recovery to a runtime caller-PC lookup. This preserves the existing inline path while allowing tracing to materialize caller-provided DW_AT_call_value bindings for normal function calls.
1 parent a3879e7 commit cdbf3fe

6 files changed

Lines changed: 824 additions & 19 deletions

File tree

e2e-tests/tests/entry_value_recovery_execution.rs

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH};
1717

1818
const FIXTURE_NAME: &str = "entry_value_recovery_program";
1919
const FIXTURE_SOURCE: &str = "entry_value_recovery_program.c";
20+
const TOUCH_TRACE_LINE: u32 = 16;
2021
const POST_CALL_TRACE_LINE: u32 = 21;
2122

2223
static CLANG_FIXTURE: OnceLock<Result<PathBuf, String>> = OnceLock::new();
@@ -141,6 +142,104 @@ fn shell_quote(path: &Path) -> String {
141142
format!("'{}'", raw.replace('\'', "'\"'\"'"))
142143
}
143144

145+
#[tokio::test]
146+
async fn test_non_inline_entry_value_recovers_touch_parameters_at_runtime() -> anyhow::Result<()> {
147+
init();
148+
if !fixture_compiler_available(FixtureCompiler::ClangDwarf5) {
149+
eprintln!("Skipping non-inline entry_value runtime test because clang is unavailable");
150+
return Ok(());
151+
}
152+
153+
let binary_path = compile_entry_value_recovery_program_clang()?;
154+
let analyzer = ghostscope_dwarf::DwarfAnalyzer::from_exec_path(&binary_path).await?;
155+
let addrs = analyzer.lookup_addresses_by_source_line(FIXTURE_SOURCE, TOUCH_TRACE_LINE);
156+
anyhow::ensure!(
157+
!addrs.is_empty(),
158+
"No DWARF addresses found for {FIXTURE_SOURCE}:{TOUCH_TRACE_LINE}"
159+
);
160+
161+
let target = spawn_logged_target(&binary_path).await?;
162+
let script = format!(
163+
"trace {FIXTURE_SOURCE}:{TOUCH_TRACE_LINE} {{
164+
print \"TOUCH:{{}}:{{}}:{{}}\", x, state.total_bytes, state.stream_id;
165+
}}
166+
"
167+
);
168+
let (exit_code, ghostscope_stdout, ghostscope_stderr) = GhostscopeRunner::new()
169+
.with_script(&script)
170+
.attach_to(&target.target)
171+
.timeout_secs(4)
172+
.enable_sysmon_shared_lib(false)
173+
.run()
174+
.await?;
175+
let (target_stdout, target_stderr) = target.terminate_and_collect().await?;
176+
177+
if should_skip_for_ebpf_env(exit_code, &ghostscope_stderr) {
178+
return Ok(());
179+
}
180+
181+
assert_eq!(
182+
exit_code, 0,
183+
"ghostscope stderr={ghostscope_stderr} ghostscope stdout={ghostscope_stdout} target stderr={target_stderr}"
184+
);
185+
assert!(
186+
!ghostscope_stdout.contains("ExprError"),
187+
"Expected exact non-inline entry_value recovery inside touch(). STDOUT: {ghostscope_stdout}\nSTDERR: {ghostscope_stderr}"
188+
);
189+
assert!(
190+
!ghostscope_stdout.contains("<optimized out>"),
191+
"touch() parameters should not be optimized out. STDOUT: {ghostscope_stdout}\nSTDERR: {ghostscope_stderr}"
192+
);
193+
194+
let actual_re = Regex::new(r"ACTUAL:([0-9-]+):([0-9-]+):([0-9-]+):([0-9-]+)")?;
195+
let mut actual_by_seed = HashMap::new();
196+
for caps in actual_re.captures_iter(&target_stdout) {
197+
actual_by_seed.insert(
198+
caps[1].parse::<i64>()?,
199+
(
200+
caps[2].parse::<i64>()?,
201+
caps[3].parse::<i64>()?,
202+
caps[4].parse::<i64>()?,
203+
),
204+
);
205+
}
206+
anyhow::ensure!(
207+
!actual_by_seed.is_empty(),
208+
"fixture stdout did not contain ACTUAL lines: {target_stdout}"
209+
);
210+
211+
let trace_re = Regex::new(r"TOUCH:([0-9-]+):([0-9-]+):([0-9-]+)")?;
212+
let mut seen = 0;
213+
for caps in trace_re.captures_iter(&ghostscope_stdout) {
214+
let seed = caps[1].parse::<i64>()?;
215+
let total_bytes = caps[2].parse::<i64>()?;
216+
let stream_id = caps[3].parse::<i64>()?;
217+
let actual = actual_by_seed.get(&seed).ok_or_else(|| {
218+
anyhow::anyhow!("missing ACTUAL record for seed={seed}; target stdout={target_stdout}")
219+
})?;
220+
let mut expected_result = total_bytes + (seed * 3);
221+
if (expected_result & 1) != 0 {
222+
expected_result += stream_id;
223+
}
224+
assert_eq!(
225+
(total_bytes, stream_id),
226+
(actual.0, actual.1),
227+
"touch() recovered the wrong state for seed={seed}; ghostscope stdout={ghostscope_stdout}"
228+
);
229+
assert_eq!(
230+
actual.2, expected_result,
231+
"touch() recovered an inconsistent seed/result for seed={seed}; ghostscope stdout={ghostscope_stdout}"
232+
);
233+
seen += 1;
234+
}
235+
236+
assert!(
237+
seen >= 2,
238+
"Expected multiple touch() trace events. GhostScope STDOUT: {ghostscope_stdout}\nTarget STDOUT: {target_stdout}"
239+
);
240+
Ok(())
241+
}
242+
144243
#[tokio::test]
145244
async fn test_post_call_entry_value_recovers_state_members_at_runtime() -> anyhow::Result<()> {
146245
init();

ghostscope-compiler/src/ebpf/dwarf_bridge.rs

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -994,6 +994,20 @@ impl<'ctx> EbpfContext<'ctx> {
994994
}
995995
}
996996

997+
ComputeStep::EntryValueLookup {
998+
caller_pc_steps,
999+
cases,
1000+
} => {
1001+
let value = self.generate_entry_value_lookup(
1002+
caller_pc_steps,
1003+
cases,
1004+
pt_regs_ptr,
1005+
_result_size,
1006+
status_ptr,
1007+
)?;
1008+
stack.push(value);
1009+
}
1010+
9971011
// Add catch-all for unimplemented operations
9981012
_ => {
9991013
warn!("Unimplemented ComputeStep: {:?}", step);
@@ -1014,6 +1028,154 @@ impl<'ctx> EbpfContext<'ctx> {
10141028
}
10151029
}
10161030

1031+
fn generate_entry_value_lookup(
1032+
&mut self,
1033+
caller_pc_steps: &[ComputeStep],
1034+
cases: &[ghostscope_dwarf::core::EntryValueCase],
1035+
pt_regs_ptr: PointerValue<'ctx>,
1036+
result_size: Option<MemoryAccessSize>,
1037+
status_ptr: Option<PointerValue<'ctx>>,
1038+
) -> Result<IntValue<'ctx>> {
1039+
if cases.is_empty() {
1040+
return Err(CodeGenError::LLVMError(
1041+
"EntryValueLookup requires at least one case".to_string(),
1042+
));
1043+
}
1044+
1045+
let caller_pc = self
1046+
.generate_compute_steps(
1047+
caller_pc_steps,
1048+
pt_regs_ptr,
1049+
Some(MemoryAccessSize::U64),
1050+
status_ptr,
1051+
None,
1052+
)?
1053+
.into_int_value();
1054+
1055+
let current_block = self.builder.get_insert_block().ok_or_else(|| {
1056+
CodeGenError::LLVMError("No insertion block for EntryValueLookup".to_string())
1057+
})?;
1058+
let current_fn = current_block.get_parent().ok_or_else(|| {
1059+
CodeGenError::LLVMError("No parent function for EntryValueLookup".to_string())
1060+
})?;
1061+
let merge_bb = self
1062+
.context
1063+
.append_basic_block(current_fn, "entry_value_merge");
1064+
let default_bb = self
1065+
.context
1066+
.append_basic_block(current_fn, "entry_value_default");
1067+
1068+
let module_for_offsets = {
1069+
let ctx = self.get_compile_time_context()?;
1070+
self.current_resolved_var_module_path
1071+
.clone()
1072+
.unwrap_or_else(|| ctx.module_path.clone())
1073+
};
1074+
let module_cookie = self.cookie_for_module_or_fallback(&module_for_offsets);
1075+
let mut incoming_values = Vec::with_capacity(cases.len() + 1);
1076+
let mut any_missing_offsets = None;
1077+
1078+
for (index, case) in cases.iter().enumerate() {
1079+
let st_code = self.section_code_for_address(&module_for_offsets, case.caller_return_pc);
1080+
let link_pc = self
1081+
.context
1082+
.i64_type()
1083+
.const_int(case.caller_return_pc, false);
1084+
let (runtime_return_pc, found_flag) =
1085+
self.generate_runtime_address_from_offsets(link_pc, st_code, module_cookie)?;
1086+
let missing_offsets = self
1087+
.builder
1088+
.build_not(found_flag, &format!("entry_value_missing_{index}"))
1089+
.map_err(|e| CodeGenError::LLVMError(e.to_string()))?;
1090+
any_missing_offsets = Some(match any_missing_offsets {
1091+
Some(prev) => self
1092+
.builder
1093+
.build_or(
1094+
prev,
1095+
missing_offsets,
1096+
&format!("entry_value_missing_or_{index}"),
1097+
)
1098+
.map_err(|e| CodeGenError::LLVMError(e.to_string()))?,
1099+
None => missing_offsets,
1100+
});
1101+
1102+
let is_match = self
1103+
.builder
1104+
.build_int_compare(
1105+
inkwell::IntPredicate::EQ,
1106+
caller_pc,
1107+
runtime_return_pc,
1108+
&format!("entry_value_match_{index}"),
1109+
)
1110+
.map_err(|e| CodeGenError::LLVMError(e.to_string()))?;
1111+
let case_bb = self
1112+
.context
1113+
.append_basic_block(current_fn, &format!("entry_value_case_{index}"));
1114+
let next_bb = if index + 1 == cases.len() {
1115+
default_bb
1116+
} else {
1117+
self.context
1118+
.append_basic_block(current_fn, &format!("entry_value_check_{}", index + 1))
1119+
};
1120+
self.builder
1121+
.build_conditional_branch(is_match, case_bb, next_bb)
1122+
.map_err(|e| CodeGenError::LLVMError(e.to_string()))?;
1123+
1124+
self.builder.position_at_end(case_bb);
1125+
let case_value = self
1126+
.generate_compute_steps(
1127+
&case.value_steps,
1128+
pt_regs_ptr,
1129+
result_size,
1130+
status_ptr,
1131+
None,
1132+
)?
1133+
.into_int_value();
1134+
let case_value_block = self.builder.get_insert_block().ok_or_else(|| {
1135+
CodeGenError::LLVMError(
1136+
"No insertion block after EntryValueLookup case".to_string(),
1137+
)
1138+
})?;
1139+
self.builder
1140+
.build_unconditional_branch(merge_bb)
1141+
.map_err(|e| CodeGenError::LLVMError(e.to_string()))?;
1142+
incoming_values.push((case_value, case_value_block));
1143+
1144+
self.builder.position_at_end(next_bb);
1145+
}
1146+
1147+
self.builder.position_at_end(default_bb);
1148+
if let Some(sp) = status_ptr {
1149+
self.store_variable_read_status(
1150+
sp,
1151+
self.context.bool_type().const_int(1, false),
1152+
any_missing_offsets.unwrap_or_else(|| self.context.bool_type().const_zero()),
1153+
"entry_value_default",
1154+
)?;
1155+
}
1156+
let default_value = self.context.i64_type().const_zero();
1157+
let default_value_block = self.builder.get_insert_block().ok_or_else(|| {
1158+
CodeGenError::LLVMError("No default block for EntryValueLookup".to_string())
1159+
})?;
1160+
self.builder
1161+
.build_unconditional_branch(merge_bb)
1162+
.map_err(|e| CodeGenError::LLVMError(e.to_string()))?;
1163+
incoming_values.push((default_value, default_value_block));
1164+
1165+
self.builder.position_at_end(merge_bb);
1166+
let phi = self
1167+
.builder
1168+
.build_phi(self.context.i64_type(), "entry_value_phi")
1169+
.map_err(|e| CodeGenError::LLVMError(e.to_string()))?;
1170+
let incoming_refs: Vec<(&dyn inkwell::values::BasicValue<'ctx>, _)> = incoming_values
1171+
.iter()
1172+
.map(|(value, block)| (value as &dyn inkwell::values::BasicValue<'ctx>, *block))
1173+
.collect();
1174+
phi.add_incoming(&incoming_refs);
1175+
1176+
Ok(phi.as_basic_value().into_int_value())
1177+
}
1178+
10171179
/// Query DWARF for complex expression (supports member access, array access, etc.)
10181180
pub fn query_dwarf_for_complex_expr(
10191181
&mut self,

ghostscope-compiler/src/ebpf/helper_functions.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -912,7 +912,7 @@ impl<'ctx> EbpfContext<'ctx> {
912912
Ok(())
913913
}
914914

915-
fn store_variable_read_status(
915+
pub(crate) fn store_variable_read_status(
916916
&mut self,
917917
status_ptr: PointerValue<'ctx>,
918918
combined_fail: IntValue<'ctx>,

ghostscope-dwarf/src/core/evaluation.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,15 @@ pub struct PieceResult {
112112
pub bit_offset: Option<u64>,
113113
}
114114

115+
/// One caller-side case for recovering a callee's DW_OP_entry_value.
116+
#[derive(Debug, Clone, PartialEq)]
117+
pub struct EntryValueCase {
118+
/// Link-time caller return PC from DW_AT_call_return_pc.
119+
pub caller_return_pc: u64,
120+
/// Materialized ComputeStep[] that recover the original caller value.
121+
pub value_steps: Vec<ComputeStep>,
122+
}
123+
115124
/// Computation step for LLVM IR generation
116125
/// These map directly to LLVM IR operations that can be generated in eBPF
117126
#[derive(Debug, Clone, PartialEq)]
@@ -167,6 +176,13 @@ pub enum ComputeStep {
167176
then_branch: Vec<ComputeStep>,
168177
else_branch: Vec<ComputeStep>,
169178
},
179+
180+
/// Recover a DW_OP_entry_value at runtime by matching the recovered caller
181+
/// return PC against caller-side DW_AT_call_return_pc cases.
182+
EntryValueLookup {
183+
caller_pc_steps: Vec<ComputeStep>,
184+
cases: Vec<EntryValueCase>,
185+
},
170186
}
171187

172188
/// Memory access size for bpf_probe_read_user
@@ -461,6 +477,9 @@ impl DirectValueResult {
461477
_ = then_branch;
462478
_ = else_branch;
463479
}
480+
ComputeStep::EntryValueLookup { cases, .. } => {
481+
stack.push(format!("entry_value[{} cases]", cases.len()));
482+
}
464483
}
465484
}
466485

@@ -653,6 +672,9 @@ impl fmt::Display for ComputeStep {
653672
else_branch.len()
654673
)
655674
}
675+
ComputeStep::EntryValueLookup { cases, .. } => {
676+
write!(f, "entry_value_lookup[cases:{}]", cases.len())
677+
}
656678
}
657679
}
658680
}

0 commit comments

Comments
 (0)