Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

VariableWatch

virtuals/variable_watch/ emulates source-level software watchpoints. It keeps the user-facing identity as a source variable when DWARF is available, while also supporting traditional raw-address ranges.

Enable it through the generic plugin table:

[CPU.cpu0.plugins.variable_watch]
enabled = true
variable = "motor_state.temperature"
access = "write"       # read, write, or read_write
changes_only = true

For a global, variable watches its full DWARF extent. For a structure field, use a dotted path such as motor_state.temperature; the host preprocessor resolves the parent object, member offset, member size, and type. The runtime only fires when an actual guest access overlaps that precise field range, so a write to motor_state.rpm does not trigger a temperature watchpoint.

Raw ranges do not require DWARF:

[CPU.cpu0.plugins.variable_watch]
enabled = true
address = "0x20001420"
size = 4
access = "read_write"

Exactly one of variable or address is required. size is required and positive for address mode.

Runtime behavior

The host creates a single normalized watch target and a conservative list of candidate memory instructions. The list comes from shared virtuals/utils/object_access_analysis.py, also used by ObjectSan. Its initial correctness baseline includes every ARM Thumb instruction with a memory operand; it does not discard an indirect access because pointer flow is uncertain.

At runtime, VariableWatch compares actual access ranges, not just their start addresses:

access_start < watch_end && watch_start < access_end

Partial overlap therefore triggers correctly. read, write, and read_write filter the matching access direction. For writes, the event log contains the previously observed and current watched bytes; changes_only suppresses a write whose resulting watched bytes equal the previous observed value.

Artifacts are namespaced beneath the run work directory:

run-artifacts/variable_watch/
  watch.tsv               resolved WatchTarget
  candidate_accesses.tsv  shared conservative candidate PCs
  events.tsv              source-facing access records

events.tsv includes the variable/range name, PC, containing ELF function, access direction, actual address/width, old/new bytes, and DWARF type. Values are currently represented as target-byte-order hexadecimal to keep the event format correct for scalar, aggregate, and partial accesses.

Runtime callbacks for plugin developers

Compiled-in plugins can subscribe to structured watch events instead of polling or parsing events.tsv:

#include <variable_watch.h>

static void observe(const VariableWatchEvent *event, void *userdata)
{
    if (event->access == VARIABLE_WATCH_WRITE && event->changed) {
        /* Inspect event->name, PC, function, values, or guest state here. */
    }
    (void)userdata;
}

static int my_plugin_init(const VirtualContext *ctx)
{
    (void)ctx;
    return variable_watch_register_callback(observe, NULL);
}

variable_watch_register_callback() is intended for a compiled plugin’s initializer, before QEMU executes. It returns -1 for an invalid callback or when its bounded registry is full. A callback receives every matching filtered read/write access, including an unchanged write when changes_only = true would suppress the TSV row. The event’s value-buffer pointers are transient: copy them during the callback if they must outlive it.

Runnable fixtures

fastdyn run -c configs/variable_watch.toml -o /tmp/variable-watch
fastdyn run -c configs/variable_watch_raw.toml -o /tmp/variable-watch-raw

The variable fixture watches motor_state.temperature. It confirms direct and pointer-mediated writes, rejects an adjacent rpm write, and records the containing function. The raw fixture watches the same four-byte range without using variable resolution.

Current scope

The conservative candidate planner and runtime are implemented for ARM Thumb/Cortex-M. ObjectSan and VariableWatch share the candidate-access API; future proof-based points-to pruning belongs in that shared utility, not in either plugin. Heap allocation-site watchpoints and RTOS task annotations will reuse ObjectSan’s object manager and Introspection artifacts when those cross-plugin runtime contracts are explicitly added. They are not duplicated inside VariableWatch.