Skip to content
87 changes: 82 additions & 5 deletions docs/design/datacontracts/DebugInfo.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ This contract is for fetching information related to DebugInfo associated with n
[Flags]
public enum SourceTypes : uint
{
SourceTypeInvalid = 0x00, // To indicate that nothing else applies
Default = 0x00, // To indicate that nothing else applies
StackEmpty = 0x01, // The stack is empty here
CallInstruction = 0x02 // The actual instruction of a call.
CallInstruction = 0x02 // The actual instruction of a call
Async = 0x04 // (Version 2+) Indicates suspension/resumption for an async call
}
```

Expand Down Expand Up @@ -40,7 +41,6 @@ Data descriptors used:
Contracts used:
| Contract Name |
| --- |
| `CodeVersions` |
| `ExecutionManager` |

Constants:
Expand All @@ -50,6 +50,7 @@ Constants:
| DEBUG_INFO_BOUNDS_HAS_INSTRUMENTED_BOUNDS | Indicates bounds data contains instrumented bounds | `0xFFFFFFFF` |
| EXTRA_DEBUG_INFO_PATCHPOINT | Indicates debug info contains patchpoint information | 0x1 |
| EXTRA_DEBUG_INFO_RICH | Indicates debug info contains rich information | 0x2 |
| SOURCE_TYPE_BITS | Number of bits per bounds entry used to encode source type flags | 2 |

### DebugInfo Stream Encoding

Expand Down Expand Up @@ -169,7 +170,7 @@ private static IEnumerable<OffsetMapping> DoBounds(NativeReader nativeReader)
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 + 2; // 2 bits for source type
uint bitsPerEntry = bitsForNativeDelta + bitsForILOffsets + SOURCE_TYPE_BITS; // 2 bits for source type
ulong bitsMeaningfulMask = (1UL << ((int)bitsPerEntry)) - 1;
int offsetOfActualBoundsData = reader.GetNextByteOffset();

Expand Down Expand Up @@ -198,7 +199,7 @@ private static IEnumerable<OffsetMapping> DoBounds(NativeReader nativeReader)
_ => throw new InvalidOperationException($"Unknown source type encoding: {mappingDataEncoded & 0x3}")
};

mappingDataEncoded >>= 2;
mappingDataEncoded >>= (int)SOURCE_TYPE_BITS;
uint nativeOffsetDelta = (uint)(mappingDataEncoded & ((1UL << (int)bitsForNativeDelta) - 1));
previousNativeOffset += nativeOffsetDelta;
uint nativeOffset = previousNativeOffset;
Expand All @@ -217,3 +218,79 @@ private static IEnumerable<OffsetMapping> DoBounds(NativeReader nativeReader)
}
}
```

## 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 Name | Field | Meaning |
| --- | --- | --- |
| _(none)_ | | |

Contracts used:
| Contract Name |
| --- |
| `ExecutionManager` |

Constants:
| Constant Name | Meaning | Value |
| --- | --- | --- |
| IL_OFFSET_BIAS | IL offsets bias (unchanged from Version 1) | `0xfffffffd` (-3) |
| DEBUG_INFO_FAT | Marker value in first nibble-coded integer indicating a fat header follows | `0x0` |
| SOURCE_TYPE_BITS | Number of bits per bounds entry used for source type flags | 3 |
Comment thread
max-charlamb marked this conversation as resolved.

### 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):
| Bit | Mask | Meaning |
| --- | --- | --- |
| 0 | 0x1 | `CallInstruction` |
| 1 | 0x2 | `StackEmpty` |
| 2 | 0x4 | `Async` (new in Version 2) |

`SourceTypeInvalid` is represented by all three bits clear (0). Combinations are produced by OR-ing masks (e.g., `StackEmpty | CallInstruction`).
Comment thread
max-charlamb marked this conversation as resolved.

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.
16 changes: 0 additions & 16 deletions src/coreclr/inc/patchpointinfo.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@

#include <clrtypes.h>

#ifndef JIT_BUILD
#include "cdacdata.h"
#endif // JIT_BUILD

#ifndef _PATCHPOINTINFO_H_
#define _PATCHPOINTINFO_H_

Expand Down Expand Up @@ -205,19 +201,7 @@ struct PatchpointInfo
int32_t m_securityCookieOffset;
int32_t m_monitorAcquiredOffset;
int32_t m_offsetAndExposureData[];

#ifndef JIT_BUILD
friend struct ::cdac_data<PatchpointInfo>;
#endif // JIT_BUILD
};

#ifndef JIT_BUILD
template<>
struct cdac_data<PatchpointInfo>
{
static constexpr size_t LocalCount = offsetof(PatchpointInfo, m_numberOfLocals);
};
#endif // JIT_BUILD

typedef DPTR(struct PatchpointInfo) PTR_PatchpointInfo;

Comment thread
max-charlamb marked this conversation as resolved.
Expand Down
5 changes: 0 additions & 5 deletions src/coreclr/vm/datadescriptor/datadescriptor.inc
Original file line number Diff line number Diff line change
Expand Up @@ -670,11 +670,6 @@ CDAC_TYPE_FIELD(RealCodeHeader, /* T_RUNTIME_FUNCTION */, UnwindInfos, offsetof(
#endif // FEATURE_EH_FUNCLETS
CDAC_TYPE_END(RealCodeHeader)

CDAC_TYPE_BEGIN(PatchpointInfo)
CDAC_TYPE_SIZE(sizeof(PatchpointInfo))
CDAC_TYPE_FIELD(PatchpointInfo, /*uint32*/, LocalCount, cdac_data<PatchpointInfo>::LocalCount)
CDAC_TYPE_END(PatchpointInfo)

CDAC_TYPE_BEGIN(CodeHeapListNode)
CDAC_TYPE_FIELD(CodeHeapListNode, /*pointer*/, Next, offsetof(HeapList, hpNext))
CDAC_TYPE_FIELD(CodeHeapListNode, /*pointer*/, StartAddress, offsetof(HeapList, startAddress))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public enum SourceTypes : uint
/// <summary>
/// Indicates that no other options apply
/// </summary>
SourceTypeInvalid = 0x00,
Default = 0x00,
/// <summary>
/// The stack is empty here
/// </summary>
Expand All @@ -21,6 +21,10 @@ public enum SourceTypes : uint
/// The actual instruction of a call
/// </summary>
CallInstruction = 0x02,
/// <summary>
/// Indicates suspension/resumption for an async call
/// </summary>
Async = 0x04,
}

public readonly struct OffsetMapping
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ IDebugInfo IContractFactory<IDebugInfo>.CreateContract(Target target, int versio
return version switch
{
1 => new DebugInfo_1(target),
2 => new DebugInfo_2(target),
_ => default(DebugInfo),
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using ILCompiler.Reflection.ReadyToRun;

namespace Microsoft.Diagnostics.DataContractReader.Contracts;

/// <summary>
/// Shared bounds decoding helpers for DebugInfo contract versions.
/// V1: 2-bit enumerated source type.
/// V2: 3-bit flags (CallInstruction, StackEmpty, Async).
/// </summary>
internal static class DebugInfoHelpers
{
private const uint IL_OFFSET_BIAS = unchecked((uint)-3);
private const uint SOURCE_TYPE_BITS_V1 = 2;
private const uint SOURCE_TYPE_BITS_V2 = 3;


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

uint boundsEntryCount = reader.ReadUInt();
Debug.Assert(boundsEntryCount > 0, "Expected at least one entry in bounds.");

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 sourceTypeBitCount = (version == 1) ? SOURCE_TYPE_BITS_V1 : SOURCE_TYPE_BITS_V2;
uint bitsPerEntry = bitsForNativeDelta + bitsForILOffsets + sourceTypeBitCount;
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;

ulong sourceTypeBitsMask = (1UL << ((int)sourceTypeBitCount)) - 1;
ulong sourceTypeBits = mappingDataEncoded & sourceTypeBitsMask;
SourceTypes sourceType = 0;
if ((sourceTypeBits & 0x1) != 0) sourceType |= SourceTypes.CallInstruction;
if ((sourceTypeBits & 0x2) != 0) sourceType |= SourceTypes.StackEmpty;
if ((sourceTypeBits & 0x4) != 0) sourceType |= SourceTypes.Async;

mappingDataEncoded >>= (int)sourceTypeBitCount;
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++;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,13 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using ILCompiler.Reflection.ReadyToRun;

namespace Microsoft.Diagnostics.DataContractReader.Contracts;

internal sealed class DebugInfo_1(Target target) : IDebugInfo
{
private const uint DEBUG_INFO_BOUNDS_HAS_INSTRUMENTED_BOUNDS = 0xFFFFFFFF;
private const uint IL_OFFSET_BIAS = unchecked((uint)-3);

[Flags]
internal enum ExtraDebugInfoFlags_1 : byte
Expand Down Expand Up @@ -94,68 +92,9 @@ private IEnumerable<OffsetMapping> RestoreBoundaries(TargetPointer debugInfo, bo
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();
Debug.Assert(boundsEntryCount > 0, "Expected at least one entry in bounds.");

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 + 2; // 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 >>= 2;
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++;
}
return DebugInfoHelpers.DoBounds(boundsNativeReader, 1);
}

return [];
}
}
Loading
Loading