Skip to content

Latest commit

Β 

History

History
566 lines (474 loc) Β· 31 KB

debug-information.md

File metadata and controls

566 lines (474 loc) Β· 31 KB

Debug information

Debug-information providers translate format- and language-specific data into the neutral model used by the debugger. ELF, DWARF, Go tables, native register layouts, and ABI rules stay behind this boundary.

Code map

Path What lives there
src/debug_info/mod.rs, src/debug_info/dispatch.rs Provider interfaces and format dispatch
src/debug_info/dwarf.rs, src/debug_info/dwarf/ DWARF loading, variables, locations, and type facts
src/debug_info/elf.rs, src/debug_info/separate.rs ELF metadata and separate debug-file discovery
src/debug_info/gopclntab.rs, src/debug_info/gopclntab/ Go table decoding and Go-specific unwind data
src/debug_info/x86_64.rs Architecture-specific register and unwind rules
src/debug_info/x86_64/frames.rs Frames of undescribed code, proved from its instructions
src/debug_info/perf_map.rs Perf maps: names and frames of code a compiler generated
src/debug_info/gdb_jit.rs, src/backend/linux/jit_objects.rs Objects a JIT compiler registers through the GDB JIT interface
src/backend/linux/code_copies.rs Copies of an image's code a program made in anonymous memory, and V8's compiled code
src/debug_info/v8.rs, src/debug_info/runtime_code.rs V8's builtins and the frames of code V8 generates, unwound by its frame-pointer convention
src/debug_info/cpython.rs, src/debug_info/cpython/ CPython's runtime layout: its offsets table, generated layouts, and their checks
src/debug_info/v8.rs, src/debug_info/v8/, src/image/v8.rs V8's runtime layout: its v8dbg_ constants, layouts generated from Torque, and their checks
src/image/ Validated immutable table image and checked views
src/model/image.rs, src/model/image/ Public image model and location types
src/cache.rs Persistent image cache and cache-key construction
src/unwind/ Generic caller iteration and sampled-stack unwinding
src/source_map.rs Source path resolution and remapping
src/debug_info/js_source_map.rs, src/debug_info/js_source_map/ Source Map v3 (ECMA-426): decoding, generated-to-original lookups and back, and finding a script's map; tested against Node's own decoder
src/image/tests.rs, src/cache/tests.rs, tests/debugger/debug_files.rs Image, cache, and external debug-file coverage

Images and bindings

A load produces one validated image of immutable tables under src/image. ModuleImage and providers read those tables through checked views. Validate all offsets, indexes, counts, and cross-table references before publishing the image.

A module binding holds the program path, identifier, mapping, and separate debug-file path. Do not put those values in the image. Keeping them separate allows one image to serve several mappings and makes the cache safe to share.

The cache in src/cache.rs keys an entry by the bytes that affect its result and by revisions of the loader sources. Tests use only a cache path they create or one explicitly named with MICROSCOPE_CACHE_DIR.

Providers

Normalize data at the provider boundary. The evaluator and clients must not interpret DWARF expressions, ELF segments, Go pclntab records, or native register numbers.

Data layout belongs to providers. They resolve field offsets, base classes, variants, calling conventions, and register locations. If the available debug information cannot support an answer, return an explicit absence or partial result.

Unwinding

The generic unwind loop iterates caller contexts. gimli owns DWARF CFI interpretation. Runtime-specific stack transitions report enough information to continue or return a typed reason why the stack ended.

Do not fill unknown registers with zero or assume a frame rule. A convincing wrong stack is worse than a short stack with a reason.

Sources

Resolve source paths while loading metadata. Read source contents lazily, outside the ptrace controller thread. Path remapping changes source lookup, not the identity or bytes of a module image.

Loading pipeline

Loading separates file discovery, parsing, normalization, sealing, and binding. Keep those phases visible because they fail for different reasons.

  1. Resolve the executable or module file and any separate debug file.
  2. Read object metadata, symbols, debug sections, unwind data, and language-specific tables.
  3. Normalize provider output into table builders under src/image.
  4. Seal the builders into one byte image.
  5. Validate the complete image as if it came from an untrusted cache.
  6. Create a ModuleImage view over the validated bytes.
  7. Bind that image to a loaded module's virtual ranges.

No partially loaded image enters the public model. If one input cannot be used, report why and decide explicitly whether the whole module fails or a documented feature remains unavailable.

The image container

src/image stores dense immutable tables and pools. Records use integer IDs and offsets instead of Rust pointers. This keeps images relocatable, validates all references once, and allows memory mapping and cache reuse.

The container has a header, table directory, table bytes, and trailer. Table kinds identify their schema. String and path pools deduplicate text. Indexes provide deterministic lookup without rebuilding hash maps for every binding.

Adding a table or field requires answers to four questions:

  • What provider-neutral fact does it represent?
  • How is every integer, range, flag, and cross-reference validated?
  • What revision makes older cache entries incompatible?
  • Can a public view read it without allocating or trusting unchecked bytes?

Byte validity is only the first validation layer. A record can have valid Rust layout while its offset points outside a pool, its range is reversed, its ID names the wrong table, or two indexes disagree. Validate semantic structure before exposing views.

Image revisions and cache keys

src/image::revisions() contributes source revisions to the cache key. Bump the relevant revision when a loader or schema change can alter the meaning of cached bytes. Do not bump it for a refactor that provably leaves output and validation unchanged.

The cache key names every input whose bytes affect the image: module content, separate debug content, supplementary data, and loader revision. Paths do not identify content. The same bytes at another path should reuse an image; new bytes at the same path must not.

A cache hit still validates the image. Cache files are not trusted merely because microscope wrote them. Failed or interrupted writes must not leave an entry that later appears complete.

Tests run with the user cache disabled unless a recipe explicitly supplies MICROSCOPE_CACHE_DIR. Cache tests create their own scratch directory. just test warms and uses target/image-cache; just stress loads afresh to exercise the loader repeatedly.

Module discovery and identity

For a live process, mappings establish runtime address ranges and file bindings. Files in another mount namespace may need to be opened through /proc/PID/root. Match the mapped file by device and inode evidence rather than trusting a path that resolves differently for the debugger.

For a core dump, notes and mappings describe the original process. Use build IDs and recorded identity to find matching files under their original paths, a sysroot, or module search directories. allow-module-mismatch is a public opt-out from proof, not permission for ordinary loading to guess.

Two kinds of module live only in memory. The vDSO is one. The other is each object a JIT compiler registers through the GDB JIT interface, as CPython 3.15's JIT does. The controller walks the list in each __jit_debug_descriptor at every published stop, through debug_info::gdb_jit. It reads each object's ELF image, loads it uncached at load bias zero, and names it for its address and a hash of its bytes, so replaced code is a new module. Such an image has no program headers, so its address range is its allocated sections'. A list read while the compiler changes it is read as far as it is sound; what could not be read is recorded, and a frame in unread code stays unnamed.

A program may also copy part of a loaded image's code into anonymous memory and run it there, as V8 copies its embedded builtins next to each code range. src/backend/linux/code_copies.rs finds such copies at each module refresh, and in a core when it opens. A candidate mapping is executable and not writable, and no file backs it. Its first bytes must equal a function that begins a page of a loaded image. The proof is a full comparison of the mapping against the image's code, cached per mapping until the mapping changes. A proven copy becomes a second binding of the same image whose LoadedModule::window is the copied range of image addresses. virtual_address and image_address refuse addresses outside a window, so breakpoint resolution plants a location in a copy only for code the copy holds. The copy's symbols, roles, and unwind rules are the image's.

A trap the debugger installed in the source before the program copied it is in the copy too. The comparison accepts a 0xCC where the source has an installed site and records it. The trap comes out before the copy's own sites go in. A thread that hits such a trap first is classified as a carried trap, rewound, and resumed once the refresh has registered the copy.

An ELF build ID identifies file content for lookup; it is not a runtime module ID. ModuleId identifies one public session binding. ModuleImageId identifies immutable image content in the model. Keep these roles separate.

ELF boundary

ELF parsing owns load segments, sections, symbols, build IDs, interpreter facts, and core notes. Translate these into neutral addresses and module records before handing them inward.

Do not assume section headers exist in a runtime image or that symbols have complete extents. Record the evidence used to infer a boundary so disassembly and symbol lookup can report conflicts and shortfalls.

Separate debug files can be found through explicit directories, system debug directories, build-ID layouts, supplementary links, and debuginfod. Keep network fetching outside the controller. Verify that fetched or discovered files match the module before loading them.

DWARF boundary

DWARF providers own compilation units, DIE traversal, line programs, location lists, range lists, expressions, CFI, and producer-specific repairs. Generic code should see source files, functions, types, variable descriptions, locations, and unwind rules.

DWARF is frequently partial or internally inconsistent. Bound recursion, references, expression work, ranges, and table sizes. A malformed entry should carry enough unit and offset context to diagnose the producer without exposing raw DWARF concepts through every public API.

Type identity is not a DIE offset. Compilers may emit declarations and definitions in several units, imported types, aliases, split debug data, and equivalent types at different offsets. Keep canonical identity and named relationships in the image model.

Variable locations

A source variable can be constant, live in memory, live in one or more register pieces, depend on a frame base, require a DWARF procedure, or be unavailable over part of its scope. Capture the description statically, then evaluate it against one frame and stop.

Do not read a location while loading an image. Do not discard an unsupported operation and continue with the stack as if it succeeded. Preserve the difference between optimized out, malformed debug information, unsupported producer behavior, unreadable process state, and a variable outside its live range.

Entry values and caller-state reconstruction need explicit evidence. Never substitute the current register value for a requested entry value.

A thread-local variable's location is an offset in its module's TLS block, and the C library's structures say where each thread's copy of the block is. src/backend/linux/tls.rs reads musl's structures, whose layout is fixed ABI, and glibc's through the _thread_db_* descriptors glibc exports for debuggers, following glibc's own lookup. glibc defines the descriptors in libc since 2.34 and in libpthread before. The TLS fixtures run on Ubuntu's glibc 2.27, 2.31, and 2.35, which the dev shell provides in MICROSCOPE_UBUNTU_GLIBC, as well as on the toolchain's glibc and musl.

A runtime model reads a thread-local variable without debug information, through the image's ThreadLocals table, which elf::load_thread_locals fills from TLS symbols. A ThreadLocal is where the image's own code finds the variable. Offset is a fixed offset from the thread pointer, as for an executable's variables. Slot is a GOT slot the loader fills with that offset (initial exec). Dynamic is an offset in the module's own block, which __tls_get_addr reaches through the thread's DTV (general and local dynamic). RuntimeStop::thread_local_address resolves all three; Dynamic goes through the C library lookup above. A stripped library may name no symbol for a variable. elf::accessor_thread_local then decodes the placement from the code of a function that reads it, by the x86-64 TLS ABI's sequences. V8 uses this for g_current_isolate_ in Debian's libnode, through the exported v8::Isolate::GetCurrent.

Types and layout

Providers resolve ABI and language layout into explicit model facts:

  • byte size, alignment where known, and byte order;
  • record members, bases, offsets, and bit ranges;
  • arrays, dimensions, ordering, and dynamic bounds;
  • pointer, reference, slice, and function representations;
  • enums, discriminants, variants, and storage;
  • calling conventions and return-value capture;
  • language-specific attributes such as Go kind information.

The evaluator asks providers for these facts. It must not reproduce C, C++, Rust, or Go layout rules. When a dynamic offset needs stopped program state, represent that operation explicitly rather than hiding it in a nominal type.

Source lines and functions

Line tables map image addresses to source statements. Functions and inline instances form a related but distinct index. Optimized code can have several source rows at one address, non-contiguous function ranges, inline chains, and instructions with no source row.

Keep all useful evidence and let stepping or presentation choose according to its documented rule. Do not force the loader to invent one canonical line for every instruction.

Source paths are normalized and remapped while metadata is loaded so equal files compare consistently. File contents stay lazy. A missing source file must not prevent symbols, types, disassembly, or unwinding from loading.

Unwinding design

The generic loop in src/unwind operates on FrameContext, register files, memory reads, and a CallerProvider. The DWARF provider uses gimli to interpret CFI. Go stack transitions and signal frames add explicit transition rules at their edges. PLT stubs, which mold and lld leave without CFI, get a rule decoded at load: how many words each stub instruction finds pushed.

V8's code has no CFI either, and its frames follow V8's convention instead: code calls only from a frame it has entered, whose rbp holds the address of the word that saved the caller's rbp, 16 bytes below the canonical frame address. Two pieces apply it. src/debug_info/v8.rs finds an image's embedded builtins by their Builtins_ symbols past v8_Default_embedded_blob_code_. The ProvedFrames table, decoded at load, holds each builtin's frame from its entry, as far as x86_64::frames::CodeFrames proves it from the instructions, then the convention's frame at each call it makes. A frame in the copy of the builtins (see module discovery) uses the image's table. Code V8 compiles for JavaScript lies in anonymous memory that the process can write. In a process that has loaded a V8 image, code_copies.rs makes each such mapping a pseudo-module, debug_info::runtime_code, with no symbols. It unwinds by the convention, except where the instructions at the frame's own address prove otherwise: a push rbp; mov rbp, rsp prologue that has not run yet, or a ret. A caller's address is the end of the call it made, which is never either.

Debian's shared libnode names no builtin and has no CFI for them, so no table describes them. There, RuntimeModel::process_code gives the code a runtime owns as the process says at a stop, in process addresses: the Node model reads the pointer and 32-bit size at the embedded_blob variables, relocated by the stop's load bias. It is empty before V8 stores them, where they cannot be read, and wherever Builtins_ symbols bound the builtins, so a symbolled build is unchanged. The backend reads the ranges once per walk (runtimes.rs runtime_process_code), with where each lies in the carrying module's image. Every unwinder is built by Controller::caller_provider, which carries them: where a module's rules give NoUnwindInfo for an address in such a range, or in a binding of the same image at the same image address, runtime_code::caller_at unwinds it by the convention. code_frames.rs takes such a frame to be the runtime's, not generated, so its interpreted frames are named. code_copies.rs also tries the start of that image range as a copy's candidate, so a worker isolate's copy of the builtins, which begins with no symbol, binds libnode's image rather than becoming V8's compiled code.

GCC's epilogues pop the registers a caller keeps but leave the CFI rules that name their slots, now below the stack pointer, until the function returns. A sample's copy of the stack begins at the stack pointer, so those slots are unreadable there. The RestoredFrames table, decoded at load, records the instructions where the code proves the registers already hold the caller's values. Only rows whose frame is gone (CFA = rsp+8) yet still name a slot are read, from the row's first instruction. Code that runs straight to ret without writing rsp or a callee-saved register proves all of them. Otherwise, a pop or leave that ends the previous row and loads the slot the row names at CFA-16 proves that one register, until the code writes it or may branch. The proofs only decide what a failed slot read means; memory that holds the slot, as a live process's does, is still read.

Each step yields a caller or a typed termination. Terminations include normal stack ends as well as missing memory, missing registers, unsupported rules, cycles, and bounds. Keep the frames already established when the next one cannot be proven.

A core dump can leave out a thread's stack while still listing it as saved. gcore, and a kernel that skips pages it cannot read, write such a range as a hole in a sparse file, which reads back as zeros, and a walk over zeros ends at once and looks complete. CoreDump::open records the file's holes with SEEK_HOLE/SEEK_DATA. A stack pointer whose byte lies in one ends the walk with StackNotSaved, so the missing callers show as missing.

Frame-pointer fallback is a named policy with target rules. It is not a silent replacement for arbitrary failed CFI. Profile unwinding has different available memory from a stopped debugger and uses the same generic loop with a sampled-state reader.

Language metadata

Language providers can add static facts without changing clients:

  • Go pclntab and runtime symbols identify code roles, stack transitions, and goroutine support.

  • Rust compiler output identifies coroutine state, captures, and resume points used by the async runtime model.

  • Type-name normalization handles compiler spellings used by expressions and views.

  • CPython's .PyRuntime section and its symbols give the layout the Python runtime model reads, and the roles of the evaluation loop and its call machinery.

  • V8's v8dbg_ constants and version symbols, with layouts generated from V8's Torque output, give the layout of V8's heap objects and frames.

These facts belong in image tables when they depend only on module bytes. Live runtime traversal belongs in the runtime model and reads them through the bound module.

CPython

debug_info::cpython finds the runtime in a module's .PyRuntime section and its version in Py_Version. From 3.13 the section begins with _Py_DebugOffsets, whose offsets are read by name under the minor version's schema in cpython/schemas.rs. The fields it lacks come from cpython/layouts.gen.rs, which scripts/cpython-layouts.sh generates by compiling scripts/cpython-layouts.c against each interpreter's headers in MICROSCOPE_CPYTHON. Patch releases change structures the table does not describe: 3.14.8's interpreter state is 24 bytes longer than 3.14.4's from its garbage collector's state on. So the generated layout is the one, among the builds of the minor version, that agrees with the table on every field both name. A table no build agrees with is refused, and so is one that builds agree with but lay out differently where the table is silent. A prerelease is read only by a layout generated for that exact release. 3.12 has no table, so its layout is the generated one for its exact release. Every layout is validated before it is stored: each required field is present and lies within its structure, and no structure read whole exceeds a bound.

Each layout is checked three ways, so a new release that moves a field fails in one place:

  • against its headers, by tests/python/layouts.rs: the generated file is current, each schema is its version's _Py_DebugOffsets, and the widths and tags the readers assume are the headers';
  • against the target's own table, by each_interpreter_in_the_matrix_agrees_with_its_headers;
  • against DWARF, for each interpreter with a debug output, by cpython/conformance.rs.

When a new CPython release arrives, add it to the flake's interpreter set, run scripts/cpython-layouts.sh in the dev shell, add a schema if its minor version is new, and run these tests.

V8

debug_info::v8::runtime makes the image of a Node executable or a libnode one V8Runtime row (src/image/v8.rs): V8's version, and a layout of Field offsets and Constant values, or the reason the module cannot be read. A module is V8's when it defines v8dbg_off_fp_function. The provider reads every v8dbg_ symbol from the module's own file, from its symbol table or, as in Debian's libnode, its dynamic table only. Each is an int in the module's data; one in a section with no file bytes, such as .bss, is 0. The version is v8::internal::Version's major_, minor_, build_, and patch_.

The constants leave out most fields the runtime model reads, such as a bytecode array's position table and a shared function info's function data. Those come from v8/layouts.gen.rs, generated for each V8 major.minor version from the Torque output in MICROSCOPE_V8_TORQUE: class-debug-readers.cc gives each field's offset, instance-types.h every instance type Torque assigns, and bit-fields.h the shift and mask of each bit field of the flags of SharedFunctionInfo, ScopeInfo (its flags and a context local's variable properties), Script, FeedbackVector (its flags and OSR state), Map's three bit fields, Symbol, JSPromise, JSArrayBuffer, JSArrayBufferView, JSRegExp, and CallSiteInfo. A field Torque renamed has one Field, named by its newest name, such as ByteArray::Values, which 12.4 calls ByteArray::Bytes.

The rest comes from the V8 headers beside the Torque output, parsed by v8/headers.rs, which fails on any construct, size, or name it does not know:

  • Code's and CodeWrapper's fields, from code.h's CODE_DATA_FIELDS and FIELD_LIST, which have no Torque reader. Their sizes depend on configuration macros, which headers::configuration resolves for the configuration binding requires, each with its evidence: no sandbox, no embedded constant pool (PPC only), leaptiering from 13.6, no builtin jump table info in 13.6 (an off-by-default build argument), jump table info in 14.6 (ENABLE_DISASSEMBLER, which every 14.6 build's Code::Disassemble shows), and no lower-limits mode;
  • the code kinds of code-kind.h, the slots of deoptimization-data.h, an InliningPosition's C layout and SourcePosition's bit fields, the slots of hash-table.h and dictionary.h, the weak-reference encoding, and, from 13.6, the JS dispatch table's entry layout;
  • what reading JavaScript's values needs: BigIntBase's fields and bits (bigint.h) and, from 14.6, AccessorPair's (struct.h), each laid out from HeapObjectLayout's header with its exact members asserted; the enumerators of ElementsKind (its typed arrays expanded from their macro lists), FunctionKind, PromiseState, VariableMode, InitializationFlag, and, from 14.6, PrivateSymbolKind and the raw CallSiteInfo::Fields; the oddball kinds; kHoleNanInt64 from globals.h's non-MIPS branch; the ordered hash tables' layout; the number and global dictionaries' entries; and the context slots, with the native context's expanded from NATIVE_CONTEXT_FIELDS;
  • the fp-relative slots of standard and unoptimized frames from frame-constants.h, its frame macros expanded from their own definitions, and kJSArgcReceiverSlots;
  • what reading an optimized frame's deoptimization translation needs, by v8/deoptimization.rs: both safepoint tables' headers and entry configuration bits (safepoint-table.h, maglev-safepoint-table.h; 12.4's headers have no stack slots' count), a deoptimization entry's size and its translation index's slot in a release build (the parser drops #ifdef DEBUG branches), kFunctionEntryBytecodeOffset, and BytecodeOffset::kFirstBuiltinContinuationId. It also checks the facts the runtime model relies on but takes no number from: that the safepoint table begins the code's metadata just past its instructions and runs to its handler table, each table's entry order, the VLQ encoding of base/vlq.h, and that --turbo-compress-frame-translations is off by default. The flake copies src/deoptimizer/translation-opcode.h, src/base/vlq.h, src/flags/flag-definitions.h, and src/utils/utils.h for it;
  • the values of v8dbg_ constants the headers define too, kept in each generated layout's checked list: the descriptor and property-details prop_ constants, the dictionary shapes, the code kinds, the deoptimization data slots, the oddball kinds, three elements kinds, a context's first slots, and the bits of a map's elements kind and a detached array buffer.

Two things only the module's machine code says are decoded from it by v8/accessors.rs, each into its own Result on the runtime, so that a failure refuses only what needs it:

  • dispatch_table, where the JS dispatch table's base is kept, from ExternalReference::js_dispatch_table_address: a word of each isolate in 14.6, or of the isolate group a global points to in 13.6; None before 13.6. That function is local, so a module stripped of its symbols, as Debian's libnode, does not name it. For 13.6 both facts then come from functions V8's API exports. The global is the one v8::IsolateGroup::GetDefault() loads before it increments the group's reference count. The table's offset in the group comes from v8::IsolateGroup::~IsolateGroup(), followed through Release and delete into the destructor: it is the word the table's TearDown clears after freeing its first segment and before deleting the address space in the next word. A 14.6 module without the symbol has an error here;
  • embedded_blob, the variables that hold the embedded builtins' bounds, from the exported v8::Isolate::GetEmbeddedCodeRange, by following which call's result it stores through each argument, then the one rip-relative load of each accessor it calls.

Each decoder accepts only the instruction shapes it names and reports the first instruction it did not expect.

Binding a module checks, in order, and refuses it with the first reason:

  • its major.minor version has a generated layout. Any other version is refused by name, as V8 12.3 has no layout in this build of microscope, and never read by the nearest version's layout;
  • v8dbg_TaggedSize is 8: a build with pointer compression is refused;
  • v8dbg_SmiShiftSize is 31: a build with 31-bit Smis is refused;
  • it is not built with V8's sandbox. A sandboxed build declares the v8::V8::*Sandbox* functions of V8's public API, which every build exports in its dynamic table, and the members of v8::internal::Sandbox. The sandbox also requires pointer compression;
  • each v8dbg_class_ constant that names a Field gives the generated offset, and the layout has that field;
  • each exported v8dbg_type_ constant whose name Torque assigns has Torque's number. A build configured unlike the release, such as one without Intl or Temporal, numbers its types differently;
  • each v8dbg_ constant the headers define too has the headers' value;
  • the layout has every field and constant its version needs.

v8/conformance.rs checks the layouts against both sources. layouts_match_torque regenerates the file from MICROSCOPE_V8_TORQUE and fails when the checked-in file differs, or when the Torque output lacks a field or constant that a version must have. Rerun it with MICROSCOPE_UPDATE_V8=1 to rewrite the file. translations_match_torque does the same for src/image/v8/translation.gen.rs, each version's translation opcodes with their operand counts, from translation-opcode.h, which image::v8::Translations serves to the runtime model by version and refuses by name for any other. bytecode_sizes_match_torque does the same for src/image/v8/bytecodes.gen.rs: v8/bytecodes.rs computes each version's Bytecodes::kBytecodeSizes from src/interpreter/bytecodes.h and bytecode-operands.h. The sizes map a Sparkplug frame's instruction to its bytecode. V8's own table is a local symbol, which Debian's libnode strips. each_builds_bytecode_sizes_are_the_generated_ones checks the generated table byte for byte against each build's own table and against the one copy in Debian's read-only data. every_build_in_the_matrix_binds_to_its_layout binds every build in MICROSCOPE_NODE, Debian's libnode included, and checks its version, the counts of agreeing fields, instance types, and header constants, the code fields the cores of the tiers fixture confirm, and its dispatch table and embedded blob variables, against the local symbols where the build has them. When a new Node line arrives, add its V8 version's Torque output to the flake, rerun the generator, and extend the version tables in debug_info/v8.rs where the new version adds or drops a field.

Load testing

Use unit tests for table validation, indexing, address boundaries, and pure provider algorithms. Use real fixtures for producer behavior. Compiler output is the contract being tested, so cover the compact GCC/Clang, optimization, PIE, frame-pointer, language, and runtime matrix where it changes results.

Fuzz entry points validate object parsing, image validation, DWARF expressions, dispatch, symbols, Go tables, disassembly, and capture formats. A fuzz target must remain bounded and run through the contained recipe.

For a schema or loader change, run the focused image and debug-information tests, cache tests, just bench-smoke, and the normal gate. Inspect allocation movement before accepting a baseline change.