Embedded Systems · Embedded Tooling

Hardware Breakpoints, Watchpoints, and the Comparators Behind Them

Why a target supports only a handful of breakpoints, how an address comparator matches an instruction fetch, and what a data watchpoint does to the pipeline when it fires.

A breakpoint looks like a property of the source file. It is actually a hardware resource, present in a fixed and small quantity, allocated per debug session, and unavailable in some memory types entirely. The number of breakpoints a debugger can set is not a setting; it is a count of comparators on the silicon.

Two ways to stop at an address

The first is to replace the instruction with one that generates a debug event. On Arm this is the BKPT instruction, whose immediate field is ignored by the core but preserved for a debugger to use as an identifier. The debugger reads the original instruction, writes the breakpoint instruction in its place, and restores the original when the breakpoint is removed. The exception raised is a breakpoint instruction debug event, reported through the debug status register, and the core halts or vectors to a handler depending on configuration.

This approach scales to any number of breakpoints, since each costs one instruction slot in memory. It has three problems. Writing to the address requires the memory to be writable, so code executing from flash cannot be patched without a flash algorithm that erases and reprograms the page. It perturbs timing: the write itself takes time, and the substituted instruction occupies a different number of cycles from some originals. And a debugger that loses its session without restoring the instructions leaves them in memory, so the program crashes on the next boot with an instruction that the source does not contain.

The second is an address comparator. A debug unit holds a small set of registers, each with a value and a mask, and each configured to match on an instruction fetch, a data read, a data write, or both. When the address matches and the access type matches, the core reports a debug event. Nothing in memory changes, so flash-resident code can be stopped without reprogramming and timing is unaffected until the event fires.

The cost is the count. The architecture leaves the number implementation-defined, and each unit reports its own in a read-only field: the flash patch and breakpoint unit’s control register encodes how many instruction comparators exist, and the data watchpoint and trace unit reports its own comparator count in its control register. The values are fixed at manufacture, are not the same across parts in a family, and appear in the debugger as a limit on simultaneous breakpoints and watchpoints.

How the comparators are consumed

A debugger with a limited pool of instruction comparators and an unbounded desire for breakpoints uses hardware comparators for some addresses and instruction substitution or a patch unit for the rest, choosing by where the address lies and which resource is scarcer. That allocation is where most failures originate, because the debugger’s breakpoint list and the hardware’s actual state diverge silently.

The same comparators often serve both instruction fetches and data accesses. On the flash patch and breakpoint unit described below, comparators 0 to 5 can match either an instruction address or a literal data address, so a watchpoint taken from that range removes one instruction comparator with it, while comparators 6 and 7 match data addresses only and are the cheap place to spend watchpoints. A debugger that does not report the reduction leaves breakpoints listed in its interface that are not installed in hardware, and the program runs straight through them.

Some parts add a mask register that matches a range rather than a single address, which allows a comparator to cover a block of code. The granularity is a power of two, and the mask is expressed as a count of low bits to ignore. A masked comparator is how a debugger places a breakpoint on an inlined function that has been split across several address ranges, and it is also how a single comparator can catch every access within a buffer.

Data watchpoints and when they fire

A watchpoint compares the address of a data access rather than of an instruction fetch. The comparison happens when the load or store issues, which is before the instruction completes, and the debug event is reported as a watchpoint rather than a breakpoint. This timing has two consequences.

The first is that the reported program counter points at the instruction that performed the access. On Cortex-M the same is true of a breakpoint instruction: the core reports the address of the BKPT itself, not the following instruction, so a debugger stepping past either event must know which kind it stopped on and what the reported address refers to. A tool that assumes the wrong convention shows the source line before or after the one that actually faulted.

The second is that the halt does not rewind the access. The instruction that generated the address is the one reported, and the memory it touched may already reflect it: a store watchpoint can show the new value rather than the old one, because the store completes before the core stops and the previous contents are then not recoverable from the halted image. A debugger that presents every watchpoint stop as the state immediately before the access is describing an intention rather than the hardware.

Watchpoint matching uses the address and the access size. A comparator configured for a word access will not match a byte access to the same address on some implementations, and will on others. The architecture allows either, which is why a watchpoint on a variable that is accessed through a char * sometimes fires and sometimes does not. Widening the match to all sizes where the hardware supports it is the usual workaround.

The literal comparator on Cortex-M parts adds a further restriction: it compares against the data address computed by the instruction, so an access that spans a comparator’s word boundary is matched by whichever word contains the start address. A memcpy that writes a 64-byte block through word stores touches sixteen distinct addresses, so a single watchpoint catches the first store in the block and halts there, which is often the desired behaviour and sometimes is not.

Flash and the instruction-substitution path

Because flash cannot be written in place, breakpoints in flash-resident code require a different mechanism on parts that provide one. The flash patch and breakpoint unit holds a set of comparators that match addresses in the code region, and a remap register that names a base address in SRAM. When the core fetches from a matched address, the unit supplies the corresponding word from the remap area instead, so a BKPT can appear at a flash address without any write to flash. The Cortex-M3 and Cortex-M4 implementations provide eight comparators in total: comparators 0 to 5 can match instruction addresses or literal data addresses, and comparators 6 and 7 match literal data only.

Parts without a patch unit leave the debugger two options: use a hardware comparator, which consumes the scarce resource, or reprogram the flash page containing the address. The second is expensive and wears the cell, and it requires the flash algorithm to run with interrupts disabled and the code that performs the erase to execute from RAM or from a region the erase does not touch — the same arrangement described in in-circuit programming and flash algorithms. A debugger doing this repeatedly on the same page consumes the sector’s erase budget at a rate unrelated to how often the firmware is actually updated, which is why flash breakpoints are a poor choice for code under active iteration.

Self-hosted debugging adds a constraint that does not exist when the probe is external. A monitor running on the target that sets a breakpoint by writing to flash must erase the very page it is executing from unless it relocates itself first. The usual arrangement is a small monitor in RAM, with the erase routine and the breakpoint table both there, and the application in flash. That is the same split used by a trace monitor loaded into RAM, where the code must survive the memory it observes being reconfigured.

Limits worth knowing before a session

The count of comparators is discoverable but rarely displayed. Exceeding it is usually reported, sometimes silently substituted, and occasionally reported only when the breakpoint is hit rather than when it is set.

Watchpoints are more constrained than breakpoints because the address must be known at the time the comparator is programmed. A watchpoint on a stack variable in a function called from several places is one address only if the variable has a fixed frame offset; with variable-length frames or a different call depth, the address moves and the comparator misses. The correct tool for that case is a conditional breakpoint evaluated by the debugger, which is software and therefore unlimited but orders of magnitude slower per hit.

Finally, all of these mechanisms depend on the debug logic being powered and clocked. A part that gates the debug clock in a low-power mode loses comparators as well as bus access, and a breakpoint set before entering that mode does not fire on exit until the debug logic is re-enabled. The sequencing of that recovery belongs with the probe-level concerns in debug ports and memory access.

References

  1. ARMv7-M Architecture Reference Manual — Arm Ltd, DDI 0403
  2. Arm Debug Interface Architecture Specification (ADIv5) — Arm Ltd, IHI 0031
  3. CoreSight Architecture Specification — Arm Ltd, IHI 0029