-
Notifications
You must be signed in to change notification settings - Fork 5.2k
[cDAC] Update DebugInfo contract for new format #121418
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
f4c1566
897a891
e632409
1b06041
97ec3ae
a85baf7
35a3c6e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,7 +10,8 @@ public enum SourceTypes : uint | |
| { | ||
| SourceTypeInvalid = 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<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(); | ||
|
|
||
|
|
@@ -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; | ||
|
|
@@ -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 | | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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`). | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| 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. | ||
| 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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| { | ||
| 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++; | ||
| } | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
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.