diff --git a/docs/design/datacontracts/DebugInfo.md b/docs/design/datacontracts/DebugInfo.md index 034eb3609900ed..26581a75d308d8 100644 --- a/docs/design/datacontracts/DebugInfo.md +++ b/docs/design/datacontracts/DebugInfo.md @@ -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 } ``` @@ -40,7 +41,6 @@ Data descriptors used: Contracts used: | Contract Name | | --- | -| `CodeVersions` | | `ExecutionManager` | Constants: @@ -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 @@ -169,7 +170,7 @@ private static IEnumerable 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(); @@ -198,7 +199,7 @@ private static IEnumerable 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; @@ -217,3 +218,79 @@ private static IEnumerable 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 | + +### 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`). + +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. \ No newline at end of file diff --git a/src/coreclr/inc/patchpointinfo.h b/src/coreclr/inc/patchpointinfo.h index f403d268d20a7c..f2c01d351c67c2 100644 --- a/src/coreclr/inc/patchpointinfo.h +++ b/src/coreclr/inc/patchpointinfo.h @@ -7,10 +7,6 @@ #include -#ifndef JIT_BUILD -#include "cdacdata.h" -#endif // JIT_BUILD - #ifndef _PATCHPOINTINFO_H_ #define _PATCHPOINTINFO_H_ @@ -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; -#endif // JIT_BUILD -}; - -#ifndef JIT_BUILD -template<> -struct cdac_data -{ - static constexpr size_t LocalCount = offsetof(PatchpointInfo, m_numberOfLocals); }; -#endif // JIT_BUILD typedef DPTR(struct PatchpointInfo) PTR_PatchpointInfo; diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index cc578f17a00b19..9a9aa614e4e82a 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -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::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)) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IDebugInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IDebugInfo.cs index 80a34bb8aef754..f373ef10c8d248 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IDebugInfo.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IDebugInfo.cs @@ -12,7 +12,7 @@ public enum SourceTypes : uint /// /// Indicates that no other options apply /// - SourceTypeInvalid = 0x00, + Default = 0x00, /// /// The stack is empty here /// @@ -21,6 +21,10 @@ public enum SourceTypes : uint /// The actual instruction of a call /// CallInstruction = 0x02, + /// + /// Indicates suspension/resumption for an async call + /// + Async = 0x04, } public readonly struct OffsetMapping diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/DebugInfo/DebugInfoFactory.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/DebugInfo/DebugInfoFactory.cs index 40072342b05892..faf6028a641da8 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/DebugInfo/DebugInfoFactory.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/DebugInfo/DebugInfoFactory.cs @@ -10,6 +10,7 @@ IDebugInfo IContractFactory.CreateContract(Target target, int versio return version switch { 1 => new DebugInfo_1(target), + 2 => new DebugInfo_2(target), _ => default(DebugInfo), }; } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/DebugInfo/DebugInfoHelpers.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/DebugInfo/DebugInfoHelpers.cs new file mode 100644 index 00000000000000..c5196af5f20663 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/DebugInfo/DebugInfoHelpers.cs @@ -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; + +/// +/// Shared bounds decoding helpers for DebugInfo contract versions. +/// V1: 2-bit enumerated source type. +/// V2: 3-bit flags (CallInstruction, StackEmpty, Async). +/// +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 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++; + } + } + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/DebugInfo/DebugInfo_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/DebugInfo/DebugInfo_1.cs index a5ac95ca95847e..cfb85043415a3f 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/DebugInfo/DebugInfo_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/DebugInfo/DebugInfo_1.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using ILCompiler.Reflection.ReadyToRun; namespace Microsoft.Diagnostics.DataContractReader.Contracts; @@ -12,7 +11,6 @@ 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 @@ -94,68 +92,9 @@ private IEnumerable RestoreBoundaries(TargetPointer debugInfo, bo if (cbBounds > 0) { NativeReader boundsNativeReader = new(new TargetStream(_target, addrBounds, cbBounds), _target.IsLittleEndian); - return DoBounds(boundsNativeReader); - } - - return Enumerable.Empty(); - } - - private static IEnumerable 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 []; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/DebugInfo/DebugInfo_2.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/DebugInfo/DebugInfo_2.cs new file mode 100644 index 00000000000000..632d9ac2c4e923 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/DebugInfo/DebugInfo_2.cs @@ -0,0 +1,108 @@ +// 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 ILCompiler.Reflection.ReadyToRun; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts; + +internal sealed class DebugInfo_2(Target target) : IDebugInfo +{ + private const uint DEBUG_INFO_FAT = 0; + + 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 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 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 DebugInfoHelpers.DoBounds(boundsNativeReader, 2); + } + + return []; + } +}