Skip to content

Commit 97361cc

Browse files
authored
Debugging: allow breakpoints to be set at "function start" by slipping forward to first opcode. (#12791)
LLDB, when instructed to `break main`, looks at the DWARF metadata for `main` and finds its PC range, then sets a breakpoint at the first PC. This is reasonable behavior for native ISAs! That PC better be a real instruction! On Wasm, however, (i) toolchains typically emit the PC range as *including* the *locals count*, a leb128 value that precedes the first opcode and any types of locals; (ii) our gdbstub component that bridges LLDB to our debug APIs (#12771) only supports *exact* PCs for breakpoints, so when presented with a PC that does not actually point to an opcode, setting the breakpoint is effectively a no-op. There will always be a difference of at least 1 byte between the start-of-function offset and first-opcode offset (for a leb128 of `0` for no locals), so a breakpoint "on" a function will never work. I initially prototyped a fix that adds a sequence point at the start of every function (which, again, is *guaranteed* to be distinct from the first opcode), and the branch is [here], but I didn't like the developer experience: this meant that when a breakpoint at a function start fired, LLDB had a weird interstitial state where no line-number applied. The behavior that would be closer in line with "native" debug expectations is that we add a bit of fuzzy-ish matching: setting a breakpoint at function start should break at the first opcode, even if that's a few (or many) bytes later. There are two options here: special-case function start, or generally change the semantics of our breakpoint API so that "add breakpoint at `pc`" means "add breakpoint at next opcode at or after `pc`". I opted for the latter in this PR because it's more consistent. The logic is a little subtle because we're effectively defining an n-to-1 mapping with this "snap-to-next" behavior, so we have to refcount each breakpoint (consider setting a breakpoint at function start *and* at the first opcode, then deleting them, one at a time). I believe the result is self-consistent, even if a little more complicated now. And, importantly, with #12771 on top of this change, it produces the expected behavior for the (very simple!) debug script "`b main`; `continue`". [here]: https://github.com/cfallin/wasmtime/tree/breakpoint-at-func-start
1 parent e8cb875 commit 97361cc

3 files changed

Lines changed: 163 additions & 21 deletions

File tree

crates/environ/src/frame_table.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,23 @@ impl<'a> FrameTable<'a> {
297297
range.map(|i| self.breakpoint_patch(i))
298298
}
299299

300+
/// Find the nearest breakpoint PC at or after the given PC.
301+
pub fn nearest_breakpoint(&self, pc: u32) -> Option<u32> {
302+
match self
303+
.breakpoint_pcs
304+
.binary_search_by_key(&pc, |p| p.get(LittleEndian))
305+
{
306+
Ok(_) => Some(pc),
307+
Err(i) => {
308+
if i < self.breakpoint_pcs.len() {
309+
Some(self.breakpoint_pcs[i].get(LittleEndian))
310+
} else {
311+
None
312+
}
313+
}
314+
}
315+
}
316+
300317
/// Return an iterator over all breakpoint patches.
301318
///
302319
/// Returned tuples are (Wasm PC, breakpoint data).

crates/wasmtime/src/runtime/debug.rs

Lines changed: 63 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -943,8 +943,16 @@ pub trait DebugHandler: Clone + Send + Sync + 'static {
943943
pub(crate) struct BreakpointState {
944944
/// Single-step mode.
945945
single_step: bool,
946-
/// Breakpoints added individually.
947-
breakpoints: BTreeSet<BreakpointKey>,
946+
/// Breakpoints added individually. Maps from the actual
947+
/// (possibly slipped-forward) breakpoint key to a reference
948+
/// count. Multiple requested PCs may map to the same actual
949+
/// breakpoint when they are slipped forward.
950+
breakpoints: BTreeMap<BreakpointKey, usize>,
951+
/// When a requested breakpoint PC does not exactly match an
952+
/// opcode boundary, we "slip" it forward to the next available
953+
/// PC. This map records the redirect from the requested key to
954+
/// the actual key so that `remove_breakpoint` can undo it.
955+
breakpoint_redirects: BTreeMap<BreakpointKey, BreakpointKey>,
948956
}
949957

950958
/// A breakpoint.
@@ -1003,7 +1011,7 @@ impl BreakpointState {
10031011
&'a self,
10041012
registry: &'a ModuleRegistry,
10051013
) -> impl Iterator<Item = Breakpoint> + 'a {
1006-
self.breakpoints.iter().map(|key| key.get(registry))
1014+
self.breakpoints.keys().map(|key| key.get(registry))
10071015
}
10081016

10091017
pub(crate) fn is_single_step(&self) -> bool {
@@ -1068,18 +1076,36 @@ impl<'a> BreakpointEdit<'a> {
10681076
/// Add a breakpoint in the given module at the given PC in that
10691077
/// module.
10701078
///
1079+
/// If the requested PC does not fall exactly on an opcode
1080+
/// boundary, the breakpoint is "slipped" forward to the next
1081+
/// available opcode PC.
1082+
///
10711083
/// No effect if the breakpoint is already set.
10721084
pub fn add_breakpoint(&mut self, module: &Module, pc: u32) -> Result<()> {
1073-
let key = BreakpointKey::from_raw(module, pc);
1074-
self.state.breakpoints.insert(key);
1075-
log::trace!("patching in breakpoint {key:?}");
1076-
let mem =
1077-
Self::get_code_memory(self.state, self.registry, &mut self.dirty_modules, module)?;
10781085
let frame_table = module
10791086
.frame_table()
10801087
.expect("Frame table must be present when guest-debug is enabled");
1081-
let patches = frame_table.lookup_breakpoint_patches_by_pc(pc);
1082-
Self::patch(patches, mem, true);
1088+
let actual_pc = frame_table.nearest_breakpoint(pc).unwrap_or(pc);
1089+
let requested_key = BreakpointKey::from_raw(module, pc);
1090+
let actual_key = BreakpointKey::from_raw(module, actual_pc);
1091+
1092+
if actual_pc != pc {
1093+
log::trace!("slipping breakpoint from {requested_key:?} to {actual_key:?}");
1094+
self.state
1095+
.breakpoint_redirects
1096+
.insert(requested_key, actual_key);
1097+
}
1098+
1099+
let refcount = self.state.breakpoints.entry(actual_key).or_insert(0);
1100+
*refcount += 1;
1101+
if *refcount == 1 {
1102+
// First reference: actually patch the code.
1103+
log::trace!("patching in breakpoint {actual_key:?}");
1104+
let mem =
1105+
Self::get_code_memory(self.state, self.registry, &mut self.dirty_modules, module)?;
1106+
let patches = frame_table.lookup_breakpoint_patches_by_pc(actual_pc);
1107+
Self::patch(patches, mem, true);
1108+
}
10831109
Ok(())
10841110
}
10851111

@@ -1088,16 +1114,32 @@ impl<'a> BreakpointEdit<'a> {
10881114
///
10891115
/// No effect if the breakpoint was not set.
10901116
pub fn remove_breakpoint(&mut self, module: &Module, pc: u32) -> Result<()> {
1091-
let key = BreakpointKey::from_raw(module, pc);
1092-
self.state.breakpoints.remove(&key);
1093-
if !self.state.single_step {
1094-
let mem =
1095-
Self::get_code_memory(self.state, self.registry, &mut self.dirty_modules, module)?;
1096-
let frame_table = module
1097-
.frame_table()
1098-
.expect("Frame table must be present when guest-debug is enabled");
1099-
let patches = frame_table.lookup_breakpoint_patches_by_pc(pc);
1100-
Self::patch(patches, mem, false);
1117+
let requested_key = BreakpointKey::from_raw(module, pc);
1118+
let actual_key = self
1119+
.state
1120+
.breakpoint_redirects
1121+
.remove(&requested_key)
1122+
.unwrap_or(requested_key);
1123+
let actual_pc = actual_key.1;
1124+
1125+
if let Some(refcount) = self.state.breakpoints.get_mut(&actual_key) {
1126+
*refcount -= 1;
1127+
if *refcount == 0 {
1128+
self.state.breakpoints.remove(&actual_key);
1129+
if !self.state.single_step {
1130+
let mem = Self::get_code_memory(
1131+
self.state,
1132+
self.registry,
1133+
&mut self.dirty_modules,
1134+
module,
1135+
)?;
1136+
let frame_table = module
1137+
.frame_table()
1138+
.expect("Frame table must be present when guest-debug is enabled");
1139+
let patches = frame_table.lookup_breakpoint_patches_by_pc(actual_pc);
1140+
Self::patch(patches, mem, false);
1141+
}
1142+
}
11011143
}
11021144
Ok(())
11031145
}
@@ -1141,7 +1183,7 @@ impl<'a> BreakpointEdit<'a> {
11411183
let mem =
11421184
Self::get_code_memory(self.state, self.registry, &mut self.dirty_modules, &module)?;
11431185
Self::apply_single_step(mem, &module, enabled, |key| {
1144-
self.state.breakpoints.contains(key)
1186+
self.state.breakpoints.contains_key(key)
11451187
})?;
11461188
}
11471189

tests/all/debug.rs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1420,3 +1420,86 @@ async fn early_epoch_yield_still_has_vmctx() -> wasmtime::Result<()> {
14201420

14211421
Ok(())
14221422
}
1423+
1424+
#[tokio::test]
1425+
#[cfg_attr(miri, ignore)]
1426+
async fn breakpoint_slips_to_first_opcode() -> wasmtime::Result<()> {
1427+
let _ = env_logger::try_init();
1428+
1429+
// Breakpoints set at the function body start (which includes the
1430+
// local declarations and precedes the first opcode) should be
1431+
// "slipped" forward to the first opcode. This matches how LLDB
1432+
// sets breakpoints using DWARF `DW_AT_low_pc`.
1433+
//
1434+
// For the WAT below, `wasm-objdump -d` shows:
1435+
//
1436+
// ```
1437+
// 000023 func[0] <main>:
1438+
// 000024: 20 00 | local.get 0
1439+
// 000026: 20 01 | local.get 1
1440+
// 000028: 6a | i32.add
1441+
// 000029: 0b | end
1442+
// ```
1443+
//
1444+
// 0x23 is the function body start (locals count byte), while 0x24
1445+
// is the first opcode. Setting a breakpoint at 0x23 should slip
1446+
// to 0x24.
1447+
let (module, mut store) = get_module_and_store(
1448+
|_config| {},
1449+
r#"
1450+
(module
1451+
(func (export "main") (param i32 i32) (result i32)
1452+
local.get 0
1453+
local.get 1
1454+
i32.add))
1455+
"#,
1456+
)?;
1457+
1458+
debug_event_checker!(
1459+
D, store,
1460+
{ 0 ;
1461+
wasmtime::DebugEvent::Breakpoint => {
1462+
let stack = store.debug_exit_frames().next().unwrap();
1463+
let (func, pc) = stack.wasm_function_index_and_pc(&mut store).unwrap().unwrap();
1464+
assert_eq!(func.as_u32(), 0);
1465+
// The breakpoint should fire at the first opcode
1466+
// (0x24), not at the function body start (0x23).
1467+
assert_eq!(pc, 0x24);
1468+
}
1469+
}
1470+
);
1471+
1472+
let (handler, counter) = D::new_and_counter();
1473+
store.set_debug_handler(handler);
1474+
1475+
store
1476+
.edit_breakpoints()
1477+
.unwrap()
1478+
.add_breakpoint(&module, 0x23)?;
1479+
1480+
let instance = Instance::new_async(&mut store, &module, &[]).await?;
1481+
let func = instance.get_func(&mut store, "main").unwrap();
1482+
let mut results = [Val::I32(0)];
1483+
func.call_async(&mut store, &[Val::I32(1), Val::I32(2)], &mut results)
1484+
.await?;
1485+
assert_eq!(counter.load(Ordering::Relaxed), 1);
1486+
assert_eq!(results[0].unwrap_i32(), 3);
1487+
1488+
// The actual breakpoint stored should be at the slipped PC.
1489+
let breakpoints = store.breakpoints().unwrap().collect::<Vec<_>>();
1490+
assert_eq!(breakpoints.len(), 1);
1491+
assert_eq!(breakpoints[0].pc, 0x24);
1492+
1493+
// Removing with the originally requested PC should work.
1494+
store
1495+
.edit_breakpoints()
1496+
.unwrap()
1497+
.remove_breakpoint(&module, 0x23)?;
1498+
func.call_async(&mut store, &[Val::I32(1), Val::I32(2)], &mut results)
1499+
.await?;
1500+
// Counter should not have incremented now that we removed the
1501+
// breakpoint.
1502+
assert_eq!(counter.load(Ordering::Relaxed), 1);
1503+
1504+
Ok(())
1505+
}

0 commit comments

Comments
 (0)