Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions docs/design/datacontracts/DebugInfo.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,10 @@ public enum DebugVarLocKind
DoubleStack,
FloatingPointStack,
FixedVarArg,
// The variable lives in a WebAssembly local (see "WebAssembly Variable Locations" below).
WasmLocal,
// The variable spans two WebAssembly locals.
WasmLocalPair,
}

public readonly struct DebugVarInfo
Expand All @@ -192,6 +196,9 @@ public readonly struct DebugVarInfo
public uint FloatingPointStackRegister { get; init; }
public uint FixedVarArgOffset { get; init; }
public uint CallReturnValueILOffset { get; init; }
// WASM only: Register and Register2 as unbiased local indices; null on every other architecture.
public uint? WasmLocal { get; init; }
public uint? WasmLocal2 { get; init; }
}

// Given a code pointer, return the variable location info for the method.
Expand Down Expand Up @@ -225,6 +232,99 @@ Each variable entry in the Vars section is nibble-encoded as follows:

Signed integers are encoded using the same unsigned scheme, with the sign bit stored in bit 0 (`value = unsigned >> 1`, negate if `unsigned & 1`). On x86, stack offsets are DWORD-aligned and stored divided by `sizeof(DWORD)`.

### WebAssembly Variable Register Encoding

WASM has no physical registers. Every register field in the Vars stream holds a WebAssembly local
index biased past the reserved register numbers:

```text
register = localIndex + WASM_LOCAL_REGNUM_BASE
```

`WASM_LOCAL_REGNUM_BASE` is `3`, one past `REGNUM_AMBIENT_SP`. Values `0` through `2` remain
`REGNUM_PC`, `REGNUM_COUNT`, and `REGNUM_AMBIENT_SP`. As on other targets, the value type is not
encoded: it is implied by the variable's type and by the local's declaration in the WebAssembly
function.

`WASM_LOCAL_REGNUM_BASE` (`3`) and the stack base register (`2`, described below) are fixed
constants of DebugInfo contract version `c1`; readers hardcode them. Changing either requires a new
DebugInfo contract version. Because ReadyToRun images persist this encoding, it also requires a
ReadyToRun format version change so static readers such as R2RDump do not misdecode older images.

### WebAssembly Stack Base Encoding

WASM `VLT_STK` and `VLT_STK2` records currently encode base register `2`.
`REG_FPBASE`, `REG_SPBASE`, and `REGNUM_AMBIENT_SP` all have that value on this target, so the
debug record identifies a logical frame-relative stack home; it does not identify a particular
WebAssembly engine local.

The absolute logical frame address is reconstructed by the runtime stack-walk and unwind
protocol from shadow-stack linear memory. The engine's current per-function SP/FP local allocation
is a separate code-generation detail:

* Frame access allocates an FP value when a method has frame locals, uses `localloc`, or has
funclets.
* Without `localloc`, the root function's FP aliases its SP, including methods that make calls.
* `localloc` gives the root a distinct FP so later SP movement does not change frame-relative
addresses.
* Funclets receive a distinct parent establishing FP; with `localloc`, that remains the root's
pre-adjustment frame base.

The numeric WebAssembly local indices holding those values can vary with function parameters and
compiler-created locals and are not part of this debug-info format. Readers must not infer the
logical frame address from a hardcoded `$varN`, and the producer does not advertise SP/FP engine
local indices. A future producer that changes stack records away from base `2` requires a
coordinated, fail-loud reader update.

### WebAssembly Variable Locations

On WASM the contract therefore reports:

* `VLT_REG` / `VLT_REG_BYREF` as `DebugVarLocKind.WasmLocal`, and `VLT_REG_REG` as
`DebugVarLocKind.WasmLocalPair`. WASM locals are engine-private frame state: they are not in
linear memory and cannot be read through the data target, so the contract names them rather than
resolving them. A consumer attached to the WASM engine (for example over the Chrome DevTools
Protocol) can fetch the value from the local index.
* Stack-based kinds unchanged, because their storage is linear memory and is readable. Only the
base register differs, as described below.

The frame pointer to add the offset to is the *logical* frame pointer, which for a method's root
function is its own frame base and for a funclet is the parent method's frame base.
`WasmContext.Unwind` and wasm explicit-frame seeding populate it by mirroring
`RtlVirtualUnwind` / `GetWasmFramePointerFromStackPointer` in
`src/coreclr/vm/wasm/helpers.cpp`; variable resolution consumes that context value rather than
independently unwinding again.

The JIT never emits `VLT_REG_FP` or `VLT_FPSTK` on WASM; `f32` and `f64` locals are reported as
`VLT_REG`, so `IsFloatingPoint` is not set for them.

`DebugVarInfo` exposes decoded register locations in `WasmLocal` and `WasmLocal2`, corresponding to
`Register` and `Register2` with `WASM_LOCAL_REGNUM_BASE` removed. A register-only location whose
register is below `WASM_LOCAL_REGNUM_BASE` names no local and is rejected. `Register` and
`Register2` retain the biased values from the Vars stream. Both are null on every other architecture. Stack base register `2` is
not a WASM local descriptor.

The managed cDAC reader reports engine-private WASM register locals with no native locations.
Existing `ClrDataValue` behavior then reports those values unavailable (`GetNumLocations == 0`,
`GetBytes`/`GetAddress` fail) instead of fabricating a register value of zero.

An unreadable or null WASM `VLT_STK_BYREF` indirection preserves one logical location, matching
the native DAC's location count, but its address/value/object accessors fail with
`CORDBG_E_READVIRTUAL_FAILURE`. This distinguishes a failed indirection from a direct
stack-homed null reference, which remains one readable location containing zero. This deliberately
differs from the native DAC (`DereferenceByRefVar` in `vm/util.cpp`), which reports a failed byref read as
address `0` and relies on address `0` being unmapped; WASM linear address `0` is readable, so the
same representation would read unrelated bytes as the variable's value.

There is no native DAC build on WASM, so live `_legacyImpl` comparison assertions cannot validate
this representation. DacDbi coverage is structural managed unit coverage.

WASM local index spaces are per function, and a method's funclets are separate WASM functions from
its root (`WasmRegAlloc` in `src/coreclr/jit/regallocwasm.cpp`). Variable ranges, by contrast, are
method-relative, and a funclet's virtual IP is rebased onto its root function. A consumer must
therefore establish which WASM function the current virtual IP belongs to before interpreting a
local index.

### Async Suspension Point APIs

We also support decoding async suspension points (and their captured continuation-object locals) from the `AsyncInfo` chunk of the debug info blob. The chunk is present only for methods that the JIT compiled with runtime-async suspension points; for all other methods, `AsyncInfoSize` is `0` in the FAT header and the API returns an empty list.
Expand Down
6 changes: 6 additions & 0 deletions src/coreclr/inc/cordebuginfo.h
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,12 @@ class ICorDebugInfo

};

#ifdef TARGET_WASM
// WASM has no physical registers. A register field in a variable location holds a wasm
// local index biased past the reserved RegNum values above.
static constexpr uint32_t WASM_LOCAL_REGNUM_BASE = REGNUM_AMBIENT_SP + 1;
#endif // TARGET_WASM

// VarLoc describes the location of a native variable. Note that currently, VLT_REG_BYREF and VLT_STK_BYREF
// are only used for value types on X64.
// [cDAC]: Mirrored in managed code (IDacDbiInterface.cs).
Expand Down
6 changes: 6 additions & 0 deletions src/coreclr/jit/codegen.h
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,12 @@ class CodeGen final : public CodeGenInterface

IL_OFFSET siLastEndOffs; // IL offset of the (exclusive) end of the last block processed

#if defined(TARGET_WASM)
// Wasm opens its method-wide scopes in the first emitted block rather than relying on the
// monotonic enter/exit cursors used by other targets.
bool siWasmScopesOpened;
#endif // defined(TARGET_WASM)

/*
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Expand Down
7 changes: 0 additions & 7 deletions src/coreclr/jit/compiler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2901,13 +2901,6 @@ void Compiler::compInitOptions(JitFlags* jitFlags)

opts.compScopeInfo = opts.compDbgInfo;

#ifdef TARGET_WASM
// Wasm uses virtual registers that cannot be encoded in the
// ICorDebugInfo register scheme, and there is no native debugger
// to consume scope info, so disable it entirely.
opts.compScopeInfo = false;
#endif

#ifdef LATE_DISASM
codeGen->getDisAssembler().disOpenForLateDisAsm(info.compMethodName, info.compClassName,
info.compMethodInfo->args.pSig);
Expand Down
4 changes: 4 additions & 0 deletions src/coreclr/jit/ee_il_dll.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -866,7 +866,11 @@ void Compiler::eeDispVar(ICorDebugInfo::NativeVarInfo* var)
{
case CodeGenInterface::VLT_REG:
case CodeGenInterface::VLT_REG_BYREF:
#if defined(TARGET_WASM)
printf("$%u", (unsigned)var->loc.vlReg.vlrReg - ICorDebugInfo::WASM_LOCAL_REGNUM_BASE);
#else
printf("%s", getRegName(var->loc.vlReg.vlrReg));
#endif
if (var->loc.vlType == (ICorDebugInfo::VarLocType)CodeGenInterface::VLT_REG_BYREF)
{
printf(" byref");
Expand Down
7 changes: 6 additions & 1 deletion src/coreclr/jit/lclvars.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1281,7 +1281,7 @@ unsigned Compiler::compMap2ILvarNum(unsigned varNum) const
}

#if defined(TARGET_WASM)
if (varNum == lvaWasmSpArg)
if ((varNum == lvaWasmSpArg) || (varNum == lvaWasmPortableEntryPointArg))
{
return (unsigned)ICorDebugInfo::UNKNOWN_ILNUM;
}
Expand Down Expand Up @@ -1319,6 +1319,11 @@ unsigned Compiler::compMap2ILvarNum(unsigned varNum) const
{
varNum--;
}

if ((lvaWasmPortableEntryPointArg != BAD_VAR_NUM) && (originalVarNum > lvaWasmPortableEntryPointArg))
{
varNum--;
}
#endif

if (varNum >= info.compLocalsCount)
Expand Down
94 changes: 89 additions & 5 deletions src/coreclr/jit/scopeinfo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,28 @@ XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
#include "emit.h"
#include "codegen.h"

#if defined(TARGET_WASM)
// Stack VarLoc records use the reserved ambient SP register number, not a wasm local.
static_assert(REG_FPBASE == REG_SPBASE);
static_assert(REG_FPBASE == REG_NA);
static_assert(static_cast<int>(REG_NA) == static_cast<int>(ICorDebugInfo::REGNUM_AMBIENT_SP));
static_assert(static_cast<int>(ICorDebugInfo::REGNUM_AMBIENT_SP) == 2);

//------------------------------------------------------------------------
// siWasmDebugRegNum: Encode a wasm local as a debug-info register number.
//
// Arguments:
// reg - the JIT register representing a wasm local.
//
// Return Value:
// The wasm local index biased past the reserved debug-info register numbers.
//
static regNumber siWasmDebugRegNum(regNumber reg)
{
return static_cast<regNumber>(ICorDebugInfo::WASM_LOCAL_REGNUM_BASE + WasmRegToIndex(reg));
}
#endif // defined(TARGET_WASM)

//============================================================================
// siVarLoc functions
//============================================================================
Expand Down Expand Up @@ -208,6 +230,30 @@ void CodeGenInterface::siVarLoc::storeVariableInRegisters(regNumber reg, regNumb
// Note: mask registers (K0-K7) and XMM16+ are accepted but will produce
// VLT_INVALID since they can't be encoded in debug info.

#if defined(TARGET_WASM)
if (reg == REG_NA)
{
vlType = VLT_INVALID;
return;
}

assert(genIsValidReg(reg));

if (otherReg == REG_NA)
{
vlType = VLT_REG;
vlReg.vlrReg = siWasmDebugRegNum(reg);
}
else
{
assert(genIsValidReg(otherReg));
vlType = VLT_REG_REG;
vlRegReg.vlrrReg1 = siWasmDebugRegNum(reg);
vlRegReg.vlrrReg2 = siWasmDebugRegNum(otherReg);
}
return;
#endif // defined(TARGET_WASM)

if (otherReg == REG_NA)
{
if (genIsValidFloatReg(reg))
Expand Down Expand Up @@ -462,6 +508,12 @@ void CodeGenInterface::siVarLoc::siFillStackVarLoc(
void CodeGenInterface::siVarLoc::siFillRegisterVarLoc(
const LclVarDsc* varDsc, var_types type, regNumber baseReg, int offset, bool isFramePointerUsed)
{
#if defined(TARGET_WASM)
this->vlType = VLT_REG;
this->vlReg.vlrReg = siWasmDebugRegNum(varDsc->GetRegNum());
return;
#endif // defined(TARGET_WASM)

switch (type)
{
case TYP_INT:
Expand Down Expand Up @@ -623,7 +675,11 @@ void CodeGenInterface::dumpSiVarLoc(const siVarLoc* varLoc) const
{
case VLT_REG:
case VLT_REG_BYREF:
#if defined(TARGET_WASM)
printf("$%u", (unsigned)varLoc->vlReg.vlrReg - ICorDebugInfo::WASM_LOCAL_REGNUM_BASE);
#else
printf("%s", getRegName(varLoc->vlReg.vlrReg));
#endif
if (varLoc->vlType == VLT_REG_BYREF)
{
printf(" byref");
Expand Down Expand Up @@ -1589,6 +1645,10 @@ void CodeGen::siInit()
siLastEndOffs = 0;

m_compiler->compResetScopeLists();

#if defined(TARGET_WASM)
siWasmScopesOpened = false;
#endif // defined(TARGET_WASM)
}

/*****************************************************************************
Expand Down Expand Up @@ -1674,11 +1734,35 @@ void CodeGen::siBeginBlock(BasicBlock* block)
void CodeGen::siOpenScopesForNonTrackedVars(const BasicBlock* block, unsigned int lastBlockILEndOffset)
{
#if defined(TARGET_WASM)
// TODO-WASM: Wasm structured control flow
// requirements are incompatible with debug codegen's
// desire to keep blocks in increasing IL offset
// order. Figure out the proper scope manipulations.
//
// Wasm is compiled only ahead of time, where getVars reports no explicit ranges, so every
// scope covers the whole method. Our elimination of irreducible flow does not emit blocks in
// increasing IL offset order, so open all scopes in the first emitted block.
if (m_compiler->opts.OptimizationDisabled() && !siWasmScopesOpened)
{
siWasmScopesOpened = true;

for (unsigned i = 0; i < m_compiler->info.compVarScopesCount; i++)
{
VarScopeDsc* varScope = &m_compiler->info.compVarScopes[i];
assert(varScope->vsdLifeBeg == 0);

LclVarDsc* lclVarDsc = m_compiler->lvaGetDesc(varScope->vsdVarNum);

// Only report locals that were referenced, if we're not doing debug codegen
if (m_compiler->opts.compDbgCode || (lclVarDsc->lvRefCnt() > 0))
{
JITDUMP("Scope info: opening scope, LVnum=%u [%03X..%03X)\n", varScope->vsdLVnum, varScope->vsdLifeBeg,
varScope->vsdLifeEnd);

varLiveKeeper->siStartVariableLiveRange(lclVarDsc, varScope->vsdVarNum);
}
else
{
JITDUMP("Skipping open scope for V%02u, unreferenced\n", varScope->vsdVarNum);
}
}
}

return;
#endif // defined(TARGET_WASM)

Expand Down
Loading
Loading