Back to Dotnet

Contract DebugInfo

docs/design/datacontracts/DebugInfo.md

11.0.10011.9 KB
Original Source

Contract DebugInfo

This contract is for fetching information related to DebugInfo associated with native code.

APIs of contract

csharp
[Flags]
public enum SourceTypes : uint
{
    Default = 0x00, // To indicate that nothing else applies
    StackEmpty = 0x01, // The stack is empty here
    CallInstruction = 0x02  // The actual instruction of a call
    Async = 0x04 // (Version 2+) Indicates suspension/resumption for an async call
}
csharp
public readonly struct OffsetMapping
{
    public uint NativeOffset { get; init; }
    public uint ILOffset { get; init; }
    public SourceTypes SourceType { get; init; }
}
csharp
// Returns true if the method at pCode has debug info associated with it.
// Methods such as ILStubs may be JIT-compiled but have no debug metadata.
bool HasDebugInfo(TargetCodePointer pCode);

// Given a code pointer, return the associated native/IL offset mapping and codeOffset.
// If preferUninstrumented, will always read the uninstrumented bounds.
// Otherwise will read the instrumented bounds and fallback to the uninstrumented bounds.
IEnumerable<OffsetMapping> GetMethodNativeMap(TargetCodePointer pCode, bool preferUninstrumented, out uint codeOffset);

Version 1

Data descriptors used:

Data Descriptor NameFieldMeaning
PatchpointInfoLocalCountNumber of locals in the method associated with the patchpoint.

Contracts used:

Contract Name
ExecutionManager

Constants:

Constant NameMeaningValue
IL_OFFSET_BIASIL offsets are encoded in the DebugInfo with this bias.0xfffffffd (-3)
DEBUG_INFO_BOUNDS_HAS_INSTRUMENTED_BOUNDSIndicates bounds data contains instrumented bounds0xFFFFFFFF
EXTRA_DEBUG_INFO_PATCHPOINTIndicates debug info contains patchpoint information0x1
EXTRA_DEBUG_INFO_RICHIndicates debug info contains rich information0x2
SOURCE_TYPE_BITSNumber of bits per bounds entry used to encode source type flags2

DebugInfo Stream Encoding

The DebugInfo stream is encoded using variable length 32-bit values with the following scheme:

A value can be stored using one or more nibbles (a nibble is a 4-bit value). 3 bits of a nibble are used to store 3 bits of the value, and the top bit indicates if the following nibble contains rest of the value. If the top bit is not set, then this nibble is the last part of the value. The higher bits of the value are written out first, and the lowest 3 bits are written out last.

In the encoded stream of bytes, the lower nibble of a byte is used before the high nibble.

A binary value ABCDEFGHI (where A is the highest bit) is encoded as the follow two bytes : 1DEF1ABC XXXX0GHI

Examples:

Decimal ValueHex ValueEncoded Result
00x0X0
10x1X1
70x7X7
80x809
90x919
630x3F7F
640x40F9 X0
650x41F9 X1
5110x1FFFF X7
5120x20089 08
5130x20189 18

Based on the encoding specification, we use a decoder defined originally for r2r dump NibbleReader.cs

Bounds Data Encoding (R2R Major Version 16+)

For R2R major version 16 and above, the bounds data uses a bit-packed encoding algorithm:

  1. The bounds entry count, bits needed for native deltas, and bits needed for IL offsets are encoded using the nibble scheme above
  2. Each bounds entry is then bit-packed with:
    • 2 bits for source type (SourceTypeInvalid=0, CallInstruction=1, StackEmpty=2, StackEmpty|CallInstruction=3)
    • Variable bits for native offset delta (accumulated from previous offset)
    • Variable bits for IL offset (with IL_OFFSET_BIAS applied)

The bit-packed data is read byte by byte, collecting bits until enough are available for each entry.

Implementation

csharp
bool IDebugInfo.HasDebugInfo(TargetCodePointer pCode)
{
    if (_eman.GetCodeBlockHandle(pCode) is not CodeBlockHandle cbh)
        return false;

    return _eman.GetDebugInfo(cbh, out _) != TargetPointer.Null;
}

IEnumerable<OffsetMapping> IDebugInfo.GetMethodNativeMap(TargetCodePointer pCode, bool preferUninstrumented, out uint codeOffset)
{
    // Get the method's DebugInfo
    if (_eman.GetCodeBlockHandle(pCode) is not CodeBlockHandle cbh)
        throw new InvalidOperationException($"No CodeBlockHandle found for native code {pCode}.");
    TargetPointer debugInfo = _eman.GetDebugInfo(cbh, out bool hasFlagByte);

    TargetCodePointer nativeCodeStart = _eman.GetStartAddress(cbh);
    codeOffset = (uint)(CodePointerUtils.AddressFromCodePointer(pCode, _target) - CodePointerUtils.AddressFromCodePointer(nativeCodeStart, _target));

    // No debug info exists (e.g. ILStubs). Return empty sequence.
    // Callers that need to distinguish this case should use HasDebugInfo first.
    if (debugInfo == TargetPointer.Null)
        return [];

    return RestoreBoundaries(debugInfo, hasFlagByte, preferUninstrumented);
}

private IEnumerable<OffsetMapping> RestoreBoundaries(TargetPointer debugInfo, bool hasFlagByte, bool preferUninstrumented)
{
    if (hasFlagByte)
    {
        // Check flag byte and skip over any patchpoint info
        byte flagByte = _target.Read<byte>(debugInfo++);

        if ((flagByte & EXTRA_DEBUG_INFO_PATCHPOINT) != 0)
        {
            uint localCount = _target.Read<uint>(debugInfo + /*PatchpointInfo::LocalCount offset*/)
            debugInfo += /*size of PatchpointInfo*/ + (localCount * 4);
        }

        if ((flagByte & EXTRA_DEBUG_INFO_RICH) != 0)
        {
            uint richDebugInfoSize = _target.Read<uint>(debugInfo);
            debugInfo += 4;
            debugInfo += richDebugInfoSize;
        }
    }

    NativeReader nibbleNativeReader = new(new TargetStream(_target, debugInfo, 24 /*maximum size of 4 32bit ints compressed*/), _target.IsLittleEndian);
    NibbleReader nibbleReader = new(nibbleNativeReader, 0);

    uint cbBounds = nibbleReader.ReadUInt();
    uint cbUninstrumentedBounds = 0;
    if (cbBounds == DEBUG_INFO_BOUNDS_HAS_INSTRUMENTED_BOUNDS)
    {
        // This means we have instrumented bounds.
        cbBounds = nibbleReader.ReadUInt();
        cbUninstrumentedBounds = nibbleReader.ReadUInt();
    }
    uint _ /*cbVars*/ = nibbleReader.ReadUInt();

    TargetPointer addrBounds = debugInfo + (uint)nibbleReader.GetNextByteOffset();
    // TargetPointer addrVars = addrBounds + cbBounds + cbUninstrumentedBounds;

    if (preferUninstrumented && cbUninstrumentedBounds != 0)
    {
        // If we have uninstrumented bounds, we will use them instead of the regular bounds.
        addrBounds += cbBounds;
        cbBounds = cbUninstrumentedBounds;
    }

    if (cbBounds > 0)
    {
        NativeReader boundsNativeReader = new(new TargetStream(_target, addrBounds, cbBounds), _target.IsLittleEndian);
        return DoBounds(boundsNativeReader);
    }

    return Enumerable.Empty<OffsetMapping>();
}

private static IEnumerable<OffsetMapping> DoBounds(NativeReader nativeReader)
{
    NibbleReader reader = new(nativeReader, 0);

    uint boundsEntryCount = reader.ReadUInt();

    uint bitsForNativeDelta = reader.ReadUInt() + 1; // Number of bits needed for native deltas
    uint bitsForILOffsets = reader.ReadUInt() + 1; // Number of bits needed for IL offsets

    uint bitsPerEntry = bitsForNativeDelta + bitsForILOffsets + SOURCE_TYPE_BITS; // 2 bits for source type
    ulong bitsMeaningfulMask = (1UL << ((int)bitsPerEntry)) - 1;
    int offsetOfActualBoundsData = reader.GetNextByteOffset();

    uint bitsCollected = 0;
    ulong bitTemp = 0;
    uint curBoundsProcessed = 0;

    uint previousNativeOffset = 0;

    while (curBoundsProcessed < boundsEntryCount)
    {
        bitTemp |= ((uint)nativeReader[offsetOfActualBoundsData++]) << (int)bitsCollected;
        bitsCollected += 8;
        while (bitsCollected >= bitsPerEntry)
        {
            ulong mappingDataEncoded = bitsMeaningfulMask & bitTemp;
            bitTemp >>= (int)bitsPerEntry;
            bitsCollected -= bitsPerEntry;

            SourceTypes sourceType = (mappingDataEncoded & 0x3) switch
            {
                0 => SourceTypes.SourceTypeInvalid,
                1 => SourceTypes.CallInstruction,
                2 => SourceTypes.StackEmpty,
                3 => SourceTypes.StackEmpty | SourceTypes.CallInstruction,
                _ => throw new InvalidOperationException($"Unknown source type encoding: {mappingDataEncoded & 0x3}")
            };

            mappingDataEncoded >>= (int)SOURCE_TYPE_BITS;
            uint nativeOffsetDelta = (uint)(mappingDataEncoded & ((1UL << (int)bitsForNativeDelta) - 1));
            previousNativeOffset += nativeOffsetDelta;
            uint nativeOffset = previousNativeOffset;

            mappingDataEncoded >>= (int)bitsForNativeDelta;
            uint ilOffset = (uint)mappingDataEncoded + IL_OFFSET_BIAS;

            yield return new OffsetMapping()
            {
                NativeOffset = nativeOffset,
                ILOffset = ilOffset,
                SourceType = sourceType
            };
            curBoundsProcessed++;
        }
    }
}

Version 2

Version 2 introduces two distinct changes:

  1. A unified header format ("fat" vs "slim") replacing the Version 1 flag byte and implicit layout.
  2. An additional SourceTypes.Async flag, expanding the per-entry source type encoding from 2 bits to a 3-bit bitfield.

The nibble-encoded variable-length integer mechanism is unchanged; only the header and bounds entry source-type packing differ.

Data descriptors used:

Data Descriptor NameFieldMeaning
(none)

Contracts used:

Contract Name
ExecutionManager

Constants:

Constant NameMeaningValue
IL_OFFSET_BIASIL offsets bias (unchanged from Version 1)0xfffffffd (-3)
DEBUG_INFO_FATMarker value in first nibble-coded integer indicating a fat header follows0x0
SOURCE_TYPE_BITSNumber of bits per bounds entry used for source type flags3

Header Encoding

The first nibble-decoded unsigned integer (countBoundsOrFatMarker):

  • If countBoundsOrFatMarker == DEBUG_INFO_FAT (0), the header is FAT and the next 6 nibble-decoded unsigned integers are, in order:
    1. BoundsSize
    2. VarsSize
    3. UninstrumentedBoundsSize
    4. PatchpointInfoSize
    5. RichDebugInfoSize
    6. AsyncInfoSize
  • Otherwise (SLIM header), the value is BoundsSize and the next nibble-decoded unsigned integer is VarsSize; all other sizes are implicitly 0.

After decoding sizes, chunk start addresses are computed by linear accumulation beginning at the first byte after the header stream:

BoundsStart = debugInfo + headerBytesConsumed
VarsStart = BoundsStart + BoundsSize
UninstrumentedBoundsStart = VarsStart + VarsSize
PatchpointInfoStart = UninstrumentedBoundsStart + UninstrumentedBoundsSize
RichDebugInfoStart = PatchpointInfoStart + PatchpointInfoSize
AsyncInfoStart = RichDebugInfoStart + RichDebugInfoSize
DebugInfoEnd = AsyncInfoStart + AsyncInfoSize

Bounds Entry Encoding Differences from Version 1

Version 1 packs each bounds entry using: [2 bits sourceType][nativeDeltaBits][ilOffsetBits].

Version 2 extends this to three independent flag bits for source type and so uses: [3 bits sourceFlags][nativeDeltaBits][ilOffsetBits].

Source type bits (low → high):

BitMaskMeaning
00x1CallInstruction
10x2StackEmpty
20x4Async (new in Version 2)

SourceTypeInvalid is represented by all three bits clear (0). Combinations are produced by OR-ing masks (e.g., StackEmpty | CallInstruction).

Pseudo-code for Version 2 source type extraction:

csharp
SourceTypes sourceType = 0;
if ((encoded & 0x1) != 0) sourceType |= SourceTypes.CallInstruction;
if ((encoded & 0x2) != 0) sourceType |= SourceTypes.StackEmpty;
if ((encoded & 0x4) != 0) sourceType |= SourceTypes.Async; // New bit

After masking the 3 bits, shift them out before reading native delta and IL offset fields as before.