Skip to content
Open
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
85 changes: 81 additions & 4 deletions docs/design/datacontracts/DebugInfo.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ public enum SourceTypes : uint
{
SourceTypeInvalid = 0x00, // To indicate that nothing else applies
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a pre-existing issue, but calling this constant SourceTypeInvalid doesn't sound right. I believe we do emit mappings that don't have any bits set. "Default" might be a better name.

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 |
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@davidwr @jakobbotsch - I noticed you were chatting about the extra bit on the other PR. These four source types are all mutually exclusive so they are encodable in only two bits if you'd like to update the runtime implementation. The interface defines them as a bit field implying they could be combined, but they can't in practice. (Technically the 'Invalid' value and the 'StackEmpty' value are combinable, buts its fine to encode that combination as 'StackEmpty')


### 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`).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although the encoding allows this combination to be represented, I'd never expect to see it. The bit patterns I'd expect to see are 0, 1, 2, and 4. We can either change the runtime implementation to make the combinations unrepresentable (a 4 value enumeration in 2 bits), or we could document it as-is.

Copy link
Member

@jakobbotsch jakobbotsch Nov 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

STACK_EMPTY | CALL_INSTRUCTION is produced by the JIT today -- in debug codegen when a call also happens to be the stack empty position.
What mappings would you expect to see in that case? It is odd to me that the source types are flags in the first place if this is not an expected possibility.


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;

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 @@ -658,11 +658,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 @@ -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
Expand Up @@ -12,6 +12,7 @@ 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 SOURCE_TYPE_BITS = 2;
private const uint IL_OFFSET_BIAS = unchecked((uint)-3);

[Flags]
Expand Down Expand Up @@ -110,7 +111,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 @@ -139,7 +140,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 @@ -156,6 +157,5 @@ private static IEnumerable<OffsetMapping> DoBounds(NativeReader nativeReader)
curBoundsProcessed++;
}
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
// 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 System.Linq;
using ILCompiler.Reflection.ReadyToRun;

namespace Microsoft.Diagnostics.DataContractReader.Contracts;

internal sealed class DebugInfo_2(Target target) : IDebugInfo
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than defining a new implementation that replicates most of the code from V1, what if we modify the existing DebugInfo_1 into DebugInfo_1_To_2(Target target, int version) and give it a little bit of conditional behavior in the right places? While I don't think this example is bad on its own, I'm hoping we can avoid having cDAC grow into something that has lots of duplicated code as contracts keep versioning. We always have the option to create a new type if we need it, but I hope we can reserve that for situations where the new version is substantially different.

{
private const uint DEBUG_INFO_FAT = 0;
private const uint SOURCE_TYPE_BITS = 3;
private const uint IL_OFFSET_BIAS = unchecked((uint)-3);

private record struct DebugInfoChunks
{
public TargetPointer BoundsStart;
public uint BoundsSize;
public TargetPointer VarsStart;
public uint VarsSize;
public TargetPointer UninstrumentedBoundsStart;
public uint UninstrumentedBoundsSize;
public TargetPointer PatchpointInfoStart;
public uint PatchpointInfoSize;
public TargetPointer RichDebugInfoStart;
public uint RichDebugInfoSize;
public TargetPointer AsyncInfoStart;
public uint AsyncInfoSize;
public TargetPointer DebugInfoEnd;
}

private readonly Target _target = target;
private readonly IExecutionManager _eman = target.Contracts.ExecutionManager;

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 _);

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

return RestoreBoundaries(debugInfo, preferUninstrumented);
}

private DebugInfoChunks DecodeChunks(TargetPointer debugInfo)
{
NativeReader nibbleNativeReader = new(new TargetStream(_target, debugInfo, 42 /*maximum size of 7 32bit ints compressed*/), _target.IsLittleEndian);
NibbleReader nibbleReader = new(nibbleNativeReader, 0);

uint countBoundsOrFatMarker = nibbleReader.ReadUInt();

DebugInfoChunks chunks = default;

if (countBoundsOrFatMarker == DEBUG_INFO_FAT)
{
// Fat header
chunks.BoundsSize = nibbleReader.ReadUInt();
chunks.VarsSize = nibbleReader.ReadUInt();
chunks.UninstrumentedBoundsSize = nibbleReader.ReadUInt();
chunks.PatchpointInfoSize = nibbleReader.ReadUInt();
chunks.RichDebugInfoSize = nibbleReader.ReadUInt();
chunks.AsyncInfoSize = nibbleReader.ReadUInt();
}
else
{
chunks.BoundsSize = countBoundsOrFatMarker;
chunks.VarsSize = nibbleReader.ReadUInt();
chunks.UninstrumentedBoundsSize = 0;
chunks.PatchpointInfoSize = 0;
chunks.RichDebugInfoSize = 0;
chunks.AsyncInfoSize = 0;
}

chunks.BoundsStart = debugInfo + (uint)nibbleReader.GetNextByteOffset();
chunks.VarsStart = chunks.BoundsStart + chunks.BoundsSize;
chunks.UninstrumentedBoundsStart = chunks.VarsStart + chunks.VarsSize;
chunks.PatchpointInfoStart = chunks.UninstrumentedBoundsStart + chunks.UninstrumentedBoundsSize;
chunks.RichDebugInfoStart = chunks.PatchpointInfoStart + chunks.PatchpointInfoSize;
chunks.AsyncInfoStart = chunks.RichDebugInfoStart + chunks.RichDebugInfoSize;
chunks.DebugInfoEnd = chunks.AsyncInfoStart + chunks.AsyncInfoSize;
return chunks;
}

private IEnumerable<OffsetMapping> RestoreBoundaries(TargetPointer debugInfo, bool preferUninstrumented)
{
DebugInfoChunks chunks = DecodeChunks(debugInfo);

TargetPointer addrBounds = chunks.BoundsStart;
uint cbBounds = chunks.BoundsSize;

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

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 + SOURCE_TYPE_BITS; // 3 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 = 0;
if ((mappingDataEncoded & 0x1) != 0) sourceType |= SourceTypes.CallInstruction;
if ((mappingDataEncoded & 0x2) != 0) sourceType |= SourceTypes.StackEmpty;
if ((mappingDataEncoded & 0x4) != 0) sourceType |= SourceTypes.Async;

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++;
}
}
}
}
Loading