|
| 1 | +# Contract DebugInfo |
| 2 | + |
| 3 | +This contract is for fetching information related to DebugInfo associated with native code. |
| 4 | + |
| 5 | +## APIs of contract |
| 6 | + |
| 7 | +```csharp |
| 8 | +[Flags] |
| 9 | +public enum SourceTypes : uint |
| 10 | +{ |
| 11 | + SourceTypeInvalid = 0x00, // To indicate that nothing else applies |
| 12 | + StackEmpty = 0x01, // The stack is empty here |
| 13 | + CallInstruction = 0x02 // The actual instruction of a call. |
| 14 | +} |
| 15 | +``` |
| 16 | + |
| 17 | +```csharp |
| 18 | +public readonly struct OffsetMapping |
| 19 | +{ |
| 20 | + public uint NativeOffset { get; init; } |
| 21 | + public uint ILOffset { get; init; } |
| 22 | + public SourceTypes SourceType { get; init; } |
| 23 | +} |
| 24 | +``` |
| 25 | + |
| 26 | +```csharp |
| 27 | +// Given a code pointer, return the associated native/IL offset mapping and codeOffset. |
| 28 | +// If preferUninstrumented, will always read the uninstrumented bounds. |
| 29 | +// Otherwise will read the instrumented bounds and fallback to the uninstrumented bounds. |
| 30 | +IEnumerable<OffsetMapping> GetMethodNativeMap(TargetCodePointer pCode, bool preferUninstrumented, out uint codeOffset); |
| 31 | +``` |
| 32 | + |
| 33 | +## Version 1 |
| 34 | + |
| 35 | +Data descriptors used: |
| 36 | +| Data Descriptor Name | Field | Meaning | |
| 37 | +| --- | --- | --- | |
| 38 | +| `PatchpointInfo` | `LocalCount` | Number of locals in the method associated with the patchpoint. | |
| 39 | + |
| 40 | +Contracts used: |
| 41 | +| Contract Name | |
| 42 | +| --- | |
| 43 | +| `CodeVersions` | |
| 44 | +| `ExecutionManager` | |
| 45 | + |
| 46 | +Constants: |
| 47 | +| Constant Name | Meaning | Value | |
| 48 | +| --- | --- | --- | |
| 49 | +| IL_OFFSET_BIAS | IL offsets are encoded in the DebugInfo with this bias. | `0xfffffffd` (-3) | |
| 50 | +| DEBUG_INFO_BOUNDS_HAS_INSTRUMENTED_BOUNDS | Indicates bounds data contains instrumented bounds | `0xFFFFFFFF` | |
| 51 | +| EXTRA_DEBUG_INFO_PATCHPOINT | Indicates debug info contains patchpoint information | 0x1 | |
| 52 | +| EXTRA_DEBUG_INFO_RICH | Indicates debug info contains rich information | 0x2 | |
| 53 | + |
| 54 | +### DebugInfo Stream Encoding |
| 55 | + |
| 56 | +The DebugInfo stream is encoded using variable length 32-bit values with the following scheme: |
| 57 | + |
| 58 | +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. |
| 59 | + |
| 60 | +In the encoded stream of bytes, the lower nibble of a byte is used before the high nibble. |
| 61 | + |
| 62 | +A binary value ABCDEFGHI (where A is the highest bit) is encoded as |
| 63 | +the follow two bytes : 1DEF1ABC XXXX0GHI |
| 64 | + |
| 65 | +Examples: |
| 66 | +| Decimal Value | Hex Value | Encoded Result | |
| 67 | +| --- | --- | --- | |
| 68 | +| 0 | 0x0 | X0 | |
| 69 | +| 1 | 0x1 | X1 | |
| 70 | +| 7 | 0x7 | X7 | |
| 71 | +| 8 | 0x8 | 09 | |
| 72 | +| 9 | 0x9 | 19 | |
| 73 | +| 63 | 0x3F | 7F | |
| 74 | +| 64 | 0x40 | F9 X0 | |
| 75 | +| 65 | 0x41 | F9 X1 | |
| 76 | +| 511 | 0x1FF | FF X7 | |
| 77 | +| 512 | 0x200 | 89 08 | |
| 78 | +| 513 | 0x201 | 89 18 | |
| 79 | + |
| 80 | +Based on the encoding specification, we use a decoder defined originally for r2r dump `NibbleReader.cs` |
| 81 | + |
| 82 | +### Bounds Data Encoding (R2R Major Version 16+) |
| 83 | + |
| 84 | +For R2R major version 16 and above, the bounds data uses a bit-packed encoding algorithm: |
| 85 | + |
| 86 | +1. The bounds entry count, bits needed for native deltas, and bits needed for IL offsets are encoded using the nibble scheme above |
| 87 | +2. Each bounds entry is then bit-packed with: |
| 88 | + - 2 bits for source type (SourceTypeInvalid=0, CallInstruction=1, StackEmpty=2, StackEmpty|CallInstruction=3) |
| 89 | + - Variable bits for native offset delta (accumulated from previous offset) |
| 90 | + - Variable bits for IL offset (with IL_OFFSET_BIAS applied) |
| 91 | + |
| 92 | +The bit-packed data is read byte by byte, collecting bits until enough are available for each entry. |
| 93 | + |
| 94 | +### Implementation |
| 95 | + |
| 96 | +``` csharp |
| 97 | +IEnumerable<OffsetMapping> IDebugInfo.GetMethodNativeMap(TargetCodePointer pCode, bool preferUninstrumented, out uint codeOffset) |
| 98 | +{ |
| 99 | + // Get the method's DebugInfo |
| 100 | + if (_eman.GetCodeBlockHandle(pCode) is not CodeBlockHandle cbh) |
| 101 | + throw new InvalidOperationException($"No CodeBlockHandle found for native code {pCode}."); |
| 102 | + TargetPointer debugInfo = _eman.GetDebugInfo(cbh, out bool hasFlagByte); |
| 103 | + |
| 104 | + TargetCodePointer nativeCodeStart = _eman.GetStartAddress(cbh); |
| 105 | + codeOffset = (uint)(CodePointerUtils.AddressFromCodePointer(pCode, _target) - CodePointerUtils.AddressFromCodePointer(nativeCodeStart, _target)); |
| 106 | + |
| 107 | + return RestoreBoundaries(debugInfo, hasFlagByte, preferUninstrumented); |
| 108 | +} |
| 109 | + |
| 110 | +private IEnumerable<OffsetMapping> RestoreBoundaries(TargetPointer debugInfo, bool hasFlagByte, bool preferUninstrumented) |
| 111 | +{ |
| 112 | + if (hasFlagByte) |
| 113 | + { |
| 114 | + // Check flag byte and skip over any patchpoint info |
| 115 | + byte flagByte = _target.Read<byte>(debugInfo++); |
| 116 | + |
| 117 | + if ((flagByte & EXTRA_DEBUG_INFO_PATCHPOINT) != 0) |
| 118 | + { |
| 119 | + uint localCount = _target.Read<uint>(debugInfo + /*PatchpointInfo::LocalCount offset*/) |
| 120 | + debugInfo += /*size of PatchpointInfo*/ + (localCount * 4); |
| 121 | + } |
| 122 | + |
| 123 | + if ((flagByte & EXTRA_DEBUG_INFO_RICH) != 0) |
| 124 | + { |
| 125 | + uint richDebugInfoSize = _target.Read<uint>(debugInfo); |
| 126 | + debugInfo += 4; |
| 127 | + debugInfo += richDebugInfoSize; |
| 128 | + } |
| 129 | + } |
| 130 | + |
| 131 | + NativeReader nibbleNativeReader = new(new TargetStream(_target, debugInfo, 24 /*maximum size of 4 32bit ints compressed*/), _target.IsLittleEndian); |
| 132 | + NibbleReader nibbleReader = new(nibbleNativeReader, 0); |
| 133 | + |
| 134 | + uint cbBounds = nibbleReader.ReadUInt(); |
| 135 | + uint cbUninstrumentedBounds = 0; |
| 136 | + if (cbBounds == DEBUG_INFO_BOUNDS_HAS_INSTRUMENTED_BOUNDS) |
| 137 | + { |
| 138 | + // This means we have instrumented bounds. |
| 139 | + cbBounds = nibbleReader.ReadUInt(); |
| 140 | + cbUninstrumentedBounds = nibbleReader.ReadUInt(); |
| 141 | + } |
| 142 | + uint _ /*cbVars*/ = nibbleReader.ReadUInt(); |
| 143 | + |
| 144 | + TargetPointer addrBounds = debugInfo + (uint)nibbleReader.GetNextByteOffset(); |
| 145 | + // TargetPointer addrVars = addrBounds + cbBounds + cbUninstrumentedBounds; |
| 146 | +
|
| 147 | + if (preferUninstrumented && cbUninstrumentedBounds != 0) |
| 148 | + { |
| 149 | + // If we have uninstrumented bounds, we will use them instead of the regular bounds. |
| 150 | + addrBounds += cbBounds; |
| 151 | + cbBounds = cbUninstrumentedBounds; |
| 152 | + } |
| 153 | + |
| 154 | + if (cbBounds > 0) |
| 155 | + { |
| 156 | + NativeReader boundsNativeReader = new(new TargetStream(_target, addrBounds, cbBounds), _target.IsLittleEndian); |
| 157 | + return DoBounds(boundsNativeReader); |
| 158 | + } |
| 159 | + |
| 160 | + return Enumerable.Empty<OffsetMapping>(); |
| 161 | +} |
| 162 | + |
| 163 | +private static IEnumerable<OffsetMapping> DoBounds(NativeReader nativeReader) |
| 164 | +{ |
| 165 | + NibbleReader reader = new(nativeReader, 0); |
| 166 | + |
| 167 | + uint boundsEntryCount = reader.ReadUInt(); |
| 168 | + |
| 169 | + uint bitsForNativeDelta = reader.ReadUInt() + 1; // Number of bits needed for native deltas |
| 170 | + uint bitsForILOffsets = reader.ReadUInt() + 1; // Number of bits needed for IL offsets |
| 171 | +
|
| 172 | + uint bitsPerEntry = bitsForNativeDelta + bitsForILOffsets + 2; // 2 bits for source type |
| 173 | + ulong bitsMeaningfulMask = (1UL << ((int)bitsPerEntry)) - 1; |
| 174 | + int offsetOfActualBoundsData = reader.GetNextByteOffset(); |
| 175 | + |
| 176 | + uint bitsCollected = 0; |
| 177 | + ulong bitTemp = 0; |
| 178 | + uint curBoundsProcessed = 0; |
| 179 | + |
| 180 | + uint previousNativeOffset = 0; |
| 181 | + |
| 182 | + while (curBoundsProcessed < boundsEntryCount) |
| 183 | + { |
| 184 | + bitTemp |= ((uint)nativeReader[offsetOfActualBoundsData++]) << (int)bitsCollected; |
| 185 | + bitsCollected += 8; |
| 186 | + while (bitsCollected >= bitsPerEntry) |
| 187 | + { |
| 188 | + ulong mappingDataEncoded = bitsMeaningfulMask & bitTemp; |
| 189 | + bitTemp >>= (int)bitsPerEntry; |
| 190 | + bitsCollected -= bitsPerEntry; |
| 191 | + |
| 192 | + SourceTypes sourceType = (mappingDataEncoded & 0x3) switch |
| 193 | + { |
| 194 | + 0 => SourceTypes.SourceTypeInvalid, |
| 195 | + 1 => SourceTypes.CallInstruction, |
| 196 | + 2 => SourceTypes.StackEmpty, |
| 197 | + 3 => SourceTypes.StackEmpty | SourceTypes.CallInstruction, |
| 198 | + _ => throw new InvalidOperationException($"Unknown source type encoding: {mappingDataEncoded & 0x3}") |
| 199 | + }; |
| 200 | + |
| 201 | + mappingDataEncoded >>= 2; |
| 202 | + uint nativeOffsetDelta = (uint)(mappingDataEncoded & ((1UL << (int)bitsForNativeDelta) - 1)); |
| 203 | + previousNativeOffset += nativeOffsetDelta; |
| 204 | + uint nativeOffset = previousNativeOffset; |
| 205 | + |
| 206 | + mappingDataEncoded >>= (int)bitsForNativeDelta; |
| 207 | + uint ilOffset = (uint)mappingDataEncoded + IL_OFFSET_BIAS; |
| 208 | + |
| 209 | + yield return new OffsetMapping() |
| 210 | + { |
| 211 | + NativeOffset = nativeOffset, |
| 212 | + ILOffset = ilOffset, |
| 213 | + SourceType = sourceType |
| 214 | + }; |
| 215 | + curBoundsProcessed++; |
| 216 | + } |
| 217 | + } |
| 218 | +} |
| 219 | +``` |
0 commit comments