Skip to content
Merged
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
267 changes: 35 additions & 232 deletions docs/design/datacontracts/DebugInfo.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,7 @@ public enum SourceTypes : uint
Default = 0x00, // To indicate that nothing else applies
StackEmpty = 0x01, // The stack is empty here
CallInstruction = 0x02 // The actual instruction of a call
Async = 0x04 // (Version 2+) Indicates suspension/resumption for an async call
Async = 0x04 // Indicates suspension/resumption for an async call
}
```

Expand All@@ -35,15 +35,12 @@ bool HasDebugInfo(TargetCodePointer pCode);
IEnumerable<OffsetMapping> GetMethodNativeMap(TargetCodePointer pCode, bool preferUninstrumented, out uint codeOffset);
```

## Version 1
## Version 2

<!-- BEGIN GENERATED: usage contract=DebugInfo version=c1 -->
<!-- BEGIN GENERATED: usage contract=DebugInfo version=c2 -->
### Data descriptors used

| Data Descriptor | Field | Type | Meaning |
| --- | --- | --- | --- |
| `PatchpointInfo` | *(type size)* | `uint32` | Size in bytes of the fixed patchpoint header before its variable local-offset data |
| `PatchpointInfo` | `LocalCount` | `uint32` | Number of locals in the method associated with the patchpoint. |
_None._

### Global variables used

Expand All@@ -53,19 +50,34 @@ _None._

| Contract Name |
| --- |
| `CodeVersions` |
| `ExecutionManager` |
| `PlatformMetadata` |
<!-- END GENERATED: usage contract=DebugInfo version=c1 -->
| `RuntimeInfo` |
<!-- END GENERATED: usage contract=DebugInfo version=c2 -->

### Constants

Constants:
| Constant Name | Meaning | Value |
| --- | --- | --- |
| IL_OFFSET_BIAS | IL offsets are encoded in the DebugInfo with this bias. | `0xfffffffd` (-3) |
| 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 |
| `IL_OFFSET_BIAS` | Bias used to encode IL offsets | `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 to encode source type flags | `3` |
| `MAX_ILNUM` | Bias for adjusted encoding of variable numbers | `0xfffffffa` (-6) |
| `CALL_RETURN_ILNUM` | Special variable number identifying a call-return-value entry | `0xfffffffb` (-5) |
| `VLT_REG` | Variable is in a register | `0` |
| `VLT_REG_BYREF` | Address of the variable is in a register | `1` |
| `VLT_REG_FP` | Variable is in an FP register | `2` |
| `VLT_STK` | Variable is on the stack | `3` |
| `VLT_STK_BYREF` | Address of the variable is on the stack | `4` |
| `VLT_REG_REG` | Variable lives in two registers | `5` |
| `VLT_REG_STK` | Variable lives partly in a register and partly on the stack | `6` |
| `VLT_STK_REG` | Reverse of `VLT_REG_STK` | `7` |
| `VLT_STK2` | Variable lives in two stack slots | `8` |
| `VLT_FPSTK` | Variable is on the floating-point stack | `9` |
| `VLT_FIXED_VA` | Fixed argument in a varargs function | `10` |
| `VLT_COUNT` | Number of valid `VarLocType` values | `11` |
| `VLT_INVALID` | Sentinel for invalid locations | `12` |

### DebugInfo Stream Encoding

Expand DownExpand Up@@ -95,195 +107,6 @@ Examples:

Based on the encoding specification, we use a decoder defined originally for r2r dump `NibbleReader.cs`

### Bounds Data Encoding (R2R Major Version 16+)

For R2R major version 16 and above, the bounds data uses a bit-packed encoding algorithm:

1. The bounds entry count, bits needed for native deltas, and bits needed for IL offsets are encoded using the nibble scheme above
2. Each bounds entry is then bit-packed with:
- 2 bits for source type (SourceTypeInvalid=0, CallInstruction=1, StackEmpty=2, StackEmpty|CallInstruction=3)
- Variable bits for native offset delta (accumulated from previous offset)
- Variable bits for IL offset (with IL_OFFSET_BIAS applied)

The bit-packed data is read byte by byte, collecting bits until enough are available for each entry.

### Implementation

``` csharp
bool IDebugInfo.HasDebugInfo(TargetCodePointer pCode)
Comment thread
noahfalk marked this conversation as resolved.
{
if (_eman.GetCodeBlockHandle(pCode) is not CodeBlockHandle cbh)
return false;

return _eman.GetDebugInfo(cbh, out _) != TargetPointer.Null;
}

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

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

// No debug info exists (e.g. ILStubs). Return empty sequence.
// Callers that need to distinguish this case should use HasDebugInfo first.
if (debugInfo == TargetPointer.Null)
return [];

return RestoreBoundaries(debugInfo, hasFlagByte, preferUninstrumented);
}

private IEnumerable<OffsetMapping> RestoreBoundaries(TargetPointer debugInfo, bool hasFlagByte, bool preferUninstrumented)
{
if (hasFlagByte)
{
// Check flag byte and skip over any patchpoint info
byte flagByte = _target.Read<byte>(debugInfo++);

if ((flagByte & EXTRA_DEBUG_INFO_PATCHPOINT) != 0)
{
uint localCount = _target.Read<uint>(debugInfo + /*PatchpointInfo::LocalCount offset*/)
debugInfo += /*size of PatchpointInfo*/ + (localCount * 4);
}

if ((flagByte & EXTRA_DEBUG_INFO_RICH) != 0)
{
uint richDebugInfoSize = _target.Read<uint>(debugInfo);
debugInfo += 4;
debugInfo += richDebugInfoSize;
}
}

NativeReader nibbleNativeReader = new(new TargetStream(_target, debugInfo, 24 /*maximum size of 4 32bit ints compressed*/), _target.IsLittleEndian);
NibbleReader nibbleReader = new(nibbleNativeReader, 0);

uint cbBounds = nibbleReader.ReadUInt();
uint cbUninstrumentedBounds = 0;
if (cbBounds == DEBUG_INFO_BOUNDS_HAS_INSTRUMENTED_BOUNDS)
{
// This means we have instrumented bounds.
cbBounds = nibbleReader.ReadUInt();
cbUninstrumentedBounds = nibbleReader.ReadUInt();
}
uint _ /*cbVars*/ = nibbleReader.ReadUInt();

TargetPointer addrBounds = debugInfo + (uint)nibbleReader.GetNextByteOffset();
// TargetPointer addrVars = addrBounds + cbBounds + cbUninstrumentedBounds;

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

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

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; // 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 >>= (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++;
}
}
}
```

## 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.

<!-- BEGIN GENERATED: usage contract=DebugInfo version=c2 diff-from=c1 -->
### Data descriptor changes from `c1`

| Change | Data Descriptor | Field | Type | Meaning |
| --- | --- | --- | --- | --- |
| Removed | `PatchpointInfo` | *(type size)* | `uint32` | Size in bytes of the fixed patchpoint header before its variable local-offset data |
| Removed | `PatchpointInfo` | `LocalCount` | `uint32` | Number of locals in the method associated with the patchpoint. |

### Global variable changes from `c1`

_No changes._

### Contract dependency changes from `c1`

| Change | Contract Name |
| --- | --- |
| Added | `CodeVersions` |
| Added | `RuntimeInfo` |
<!-- END GENERATED: usage contract=DebugInfo version=c2 diff-from=c1 -->


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`):
Expand All@@ -309,34 +132,33 @@ AsyncInfoStart = RichDebugInfoStart + RichDebugInfoSize
DebugInfoEnd = AsyncInfoStart + AsyncInfoSize
```

### Bounds Entry Encoding Differences from Version 1
### Bounds Entry Encoding

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]`.
Each bounds entry uses three independent flag bits for source type:
`[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) |
| 2 | 0x4 | `Async` |

`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:
Pseudo-code for 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
if ((encoded & 0x4) != 0) sourceType |= SourceTypes.Async;
```

After masking the 3 bits, shift them out before reading native delta and IL offset fields as before.

### Variable Location APIs (Version 2+)
### Variable Location APIs

Version 2 adds support for decoding native variable location info from the Vars section of the debug info blob.
The contract decodes native variable location information from the Vars section of the debug info blob.

Additional APIs:
```csharp
Expand DownExpand Up@@ -376,25 +198,6 @@ public readonly struct DebugVarInfo
IEnumerable<DebugVarInfo> GetMethodVarInfo(TargetCodePointer pCode, out uint codeOffset);
```

Additional constants (Version 2):
| Constant Name | Meaning | Value |
| --- | --- | --- |
| `MAX_ILNUM` | Bias for adjusted encoding of variable numbers | `0xfffffffa` (-6) |
| `CALL_RETURN_ILNUM` | Special variable number identifying a call-return-value entry | `0xfffffffb` (-5) |
| `VLT_REG` | Variable is in a register | `0` |
| `VLT_REG_BYREF` | Address of the variable is in a register | `1` |
| `VLT_REG_FP` | Variable is in an FP register | `2` |
| `VLT_STK` | Variable is on the stack | `3` |
| `VLT_STK_BYREF` | Address of the variable is on the stack | `4` |
| `VLT_REG_REG` | Variable lives in two registers | `5` |
| `VLT_REG_STK` | Variable lives partly in a register and partly on the stack | `6` |
| `VLT_STK_REG` | Reverse of VLT_REG_STK | `7` |
| `VLT_STK2` | Variable lives in two stack slots | `8` |
| `VLT_FPSTK` | Variable is on the floating-point stack | `9` |
| `VLT_FIXED_VA` | Fixed argument in a varargs function | `10` |
| `VLT_COUNT` | Number of valid VarLocType values | `11` |
| `VLT_INVALID` | Sentinel for invalid locations | `12` |

### Vars Data Encoding

Each variable entry in the Vars section is nibble-encoded as follows:
Expand All@@ -420,7 +223,7 @@ Each variable entry in the Vars section is nibble-encoded as follows:

Signed integers are encoded using the same unsigned scheme, with the sign bit stored in bit 0 (`value = unsigned >> 1`, negate if `unsigned & 1`). On x86, stack offsets are DWORD-aligned and stored divided by `sizeof(DWORD)`.

### Async Suspension Point APIs (Version 2+)
### Async Suspension Point APIs

We also support decoding async suspension points (and their captured continuation-object locals) from the `AsyncInfo` chunk of the debug info blob. The chunk is present only for methods that the JIT compiled with runtime-async suspension points; for all other methods, `AsyncInfoSize` is `0` in the FAT header and the API returns an empty list.

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Remove development-only contract versions by noahfalk · Pull Request #131790 · dotnet/runtime · GitHub
Skip to content
Merged
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
267 changes: 35 additions & 232 deletions docs/design/datacontracts/DebugInfo.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,7 @@ public enum SourceTypes : uint
Default = 0x00, // To indicate that nothing else applies
StackEmpty = 0x01, // The stack is empty here
CallInstruction = 0x02 // The actual instruction of a call
Async = 0x04 // (Version 2+) Indicates suspension/resumption for an async call
Async = 0x04 // Indicates suspension/resumption for an async call
}
```

Expand All@@ -35,15 +35,12 @@ bool HasDebugInfo(TargetCodePointer pCode);
IEnumerable<OffsetMapping> GetMethodNativeMap(TargetCodePointer pCode, bool preferUninstrumented, out uint codeOffset);
```

## Version 1
## Version 2

<!-- BEGIN GENERATED: usage contract=DebugInfo version=c1 -->
<!-- BEGIN GENERATED: usage contract=DebugInfo version=c2 -->
### Data descriptors used

| Data Descriptor | Field | Type | Meaning |
| --- | --- | --- | --- |
| `PatchpointInfo` | *(type size)* | `uint32` | Size in bytes of the fixed patchpoint header before its variable local-offset data |
| `PatchpointInfo` | `LocalCount` | `uint32` | Number of locals in the method associated with the patchpoint. |
_None._

### Global variables used

Expand All@@ -53,19 +50,34 @@ _None._

| Contract Name |
| --- |
| `CodeVersions` |
| `ExecutionManager` |
| `PlatformMetadata` |
<!-- END GENERATED: usage contract=DebugInfo version=c1 -->
| `RuntimeInfo` |
<!-- END GENERATED: usage contract=DebugInfo version=c2 -->

### Constants

Constants:
| Constant Name | Meaning | Value |
| --- | --- | --- |
| IL_OFFSET_BIAS | IL offsets are encoded in the DebugInfo with this bias. | `0xfffffffd` (-3) |
| 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 |
| `IL_OFFSET_BIAS` | Bias used to encode IL offsets | `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 to encode source type flags | `3` |
| `MAX_ILNUM` | Bias for adjusted encoding of variable numbers | `0xfffffffa` (-6) |
| `CALL_RETURN_ILNUM` | Special variable number identifying a call-return-value entry | `0xfffffffb` (-5) |
| `VLT_REG` | Variable is in a register | `0` |
| `VLT_REG_BYREF` | Address of the variable is in a register | `1` |
| `VLT_REG_FP` | Variable is in an FP register | `2` |
| `VLT_STK` | Variable is on the stack | `3` |
| `VLT_STK_BYREF` | Address of the variable is on the stack | `4` |
| `VLT_REG_REG` | Variable lives in two registers | `5` |
| `VLT_REG_STK` | Variable lives partly in a register and partly on the stack | `6` |
| `VLT_STK_REG` | Reverse of `VLT_REG_STK` | `7` |
| `VLT_STK2` | Variable lives in two stack slots | `8` |
| `VLT_FPSTK` | Variable is on the floating-point stack | `9` |
| `VLT_FIXED_VA` | Fixed argument in a varargs function | `10` |
| `VLT_COUNT` | Number of valid `VarLocType` values | `11` |
| `VLT_INVALID` | Sentinel for invalid locations | `12` |

### DebugInfo Stream Encoding

Expand DownExpand Up@@ -95,195 +107,6 @@ Examples:

Based on the encoding specification, we use a decoder defined originally for r2r dump `NibbleReader.cs`

### Bounds Data Encoding (R2R Major Version 16+)

For R2R major version 16 and above, the bounds data uses a bit-packed encoding algorithm:

1. The bounds entry count, bits needed for native deltas, and bits needed for IL offsets are encoded using the nibble scheme above
2. Each bounds entry is then bit-packed with:
- 2 bits for source type (SourceTypeInvalid=0, CallInstruction=1, StackEmpty=2, StackEmpty|CallInstruction=3)
- Variable bits for native offset delta (accumulated from previous offset)
- Variable bits for IL offset (with IL_OFFSET_BIAS applied)

The bit-packed data is read byte by byte, collecting bits until enough are available for each entry.

### Implementation

``` csharp
bool IDebugInfo.HasDebugInfo(TargetCodePointer pCode)
Comment thread
noahfalk marked this conversation as resolved.
{
if (_eman.GetCodeBlockHandle(pCode) is not CodeBlockHandle cbh)
return false;

return _eman.GetDebugInfo(cbh, out _) != TargetPointer.Null;
}

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

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

// No debug info exists (e.g. ILStubs). Return empty sequence.
// Callers that need to distinguish this case should use HasDebugInfo first.
if (debugInfo == TargetPointer.Null)
return [];

return RestoreBoundaries(debugInfo, hasFlagByte, preferUninstrumented);
}

private IEnumerable<OffsetMapping> RestoreBoundaries(TargetPointer debugInfo, bool hasFlagByte, bool preferUninstrumented)
{
if (hasFlagByte)
{
// Check flag byte and skip over any patchpoint info
byte flagByte = _target.Read<byte>(debugInfo++);

if ((flagByte & EXTRA_DEBUG_INFO_PATCHPOINT) != 0)
{
uint localCount = _target.Read<uint>(debugInfo + /*PatchpointInfo::LocalCount offset*/)
debugInfo += /*size of PatchpointInfo*/ + (localCount * 4);
}

if ((flagByte & EXTRA_DEBUG_INFO_RICH) != 0)
{
uint richDebugInfoSize = _target.Read<uint>(debugInfo);
debugInfo += 4;
debugInfo += richDebugInfoSize;
}
}

NativeReader nibbleNativeReader = new(new TargetStream(_target, debugInfo, 24 /*maximum size of 4 32bit ints compressed*/), _target.IsLittleEndian);
NibbleReader nibbleReader = new(nibbleNativeReader, 0);

uint cbBounds = nibbleReader.ReadUInt();
uint cbUninstrumentedBounds = 0;
if (cbBounds == DEBUG_INFO_BOUNDS_HAS_INSTRUMENTED_BOUNDS)
{
// This means we have instrumented bounds.
cbBounds = nibbleReader.ReadUInt();
cbUninstrumentedBounds = nibbleReader.ReadUInt();
}
uint _ /*cbVars*/ = nibbleReader.ReadUInt();

TargetPointer addrBounds = debugInfo + (uint)nibbleReader.GetNextByteOffset();
// TargetPointer addrVars = addrBounds + cbBounds + cbUninstrumentedBounds;

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

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

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; // 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 >>= (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++;
}
}
}
```

## 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.

<!-- BEGIN GENERATED: usage contract=DebugInfo version=c2 diff-from=c1 -->
### Data descriptor changes from `c1`

| Change | Data Descriptor | Field | Type | Meaning |
| --- | --- | --- | --- | --- |
| Removed | `PatchpointInfo` | *(type size)* | `uint32` | Size in bytes of the fixed patchpoint header before its variable local-offset data |
| Removed | `PatchpointInfo` | `LocalCount` | `uint32` | Number of locals in the method associated with the patchpoint. |

### Global variable changes from `c1`

_No changes._

### Contract dependency changes from `c1`

| Change | Contract Name |
| --- | --- |
| Added | `CodeVersions` |
| Added | `RuntimeInfo` |
<!-- END GENERATED: usage contract=DebugInfo version=c2 diff-from=c1 -->


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`):
Expand All@@ -309,34 +132,33 @@ AsyncInfoStart = RichDebugInfoStart + RichDebugInfoSize
DebugInfoEnd = AsyncInfoStart + AsyncInfoSize
```

### Bounds Entry Encoding Differences from Version 1
### Bounds Entry Encoding

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]`.
Each bounds entry uses three independent flag bits for source type:
`[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) |
| 2 | 0x4 | `Async` |

`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:
Pseudo-code for 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
if ((encoded & 0x4) != 0) sourceType |= SourceTypes.Async;
```

After masking the 3 bits, shift them out before reading native delta and IL offset fields as before.

### Variable Location APIs (Version 2+)
### Variable Location APIs

Version 2 adds support for decoding native variable location info from the Vars section of the debug info blob.
The contract decodes native variable location information from the Vars section of the debug info blob.

Additional APIs:
```csharp
Expand DownExpand Up@@ -376,25 +198,6 @@ public readonly struct DebugVarInfo
IEnumerable<DebugVarInfo> GetMethodVarInfo(TargetCodePointer pCode, out uint codeOffset);
```

Additional constants (Version 2):
| Constant Name | Meaning | Value |
| --- | --- | --- |
| `MAX_ILNUM` | Bias for adjusted encoding of variable numbers | `0xfffffffa` (-6) |
| `CALL_RETURN_ILNUM` | Special variable number identifying a call-return-value entry | `0xfffffffb` (-5) |
| `VLT_REG` | Variable is in a register | `0` |
| `VLT_REG_BYREF` | Address of the variable is in a register | `1` |
| `VLT_REG_FP` | Variable is in an FP register | `2` |
| `VLT_STK` | Variable is on the stack | `3` |
| `VLT_STK_BYREF` | Address of the variable is on the stack | `4` |
| `VLT_REG_REG` | Variable lives in two registers | `5` |
| `VLT_REG_STK` | Variable lives partly in a register and partly on the stack | `6` |
| `VLT_STK_REG` | Reverse of VLT_REG_STK | `7` |
| `VLT_STK2` | Variable lives in two stack slots | `8` |
| `VLT_FPSTK` | Variable is on the floating-point stack | `9` |
| `VLT_FIXED_VA` | Fixed argument in a varargs function | `10` |
| `VLT_COUNT` | Number of valid VarLocType values | `11` |
| `VLT_INVALID` | Sentinel for invalid locations | `12` |

### Vars Data Encoding

Each variable entry in the Vars section is nibble-encoded as follows:
Expand All@@ -420,7 +223,7 @@ Each variable entry in the Vars section is nibble-encoded as follows:

Signed integers are encoded using the same unsigned scheme, with the sign bit stored in bit 0 (`value = unsigned >> 1`, negate if `unsigned & 1`). On x86, stack offsets are DWORD-aligned and stored divided by `sizeof(DWORD)`.

### Async Suspension Point APIs (Version 2+)
### Async Suspension Point APIs

We also support decoding async suspension points (and their captured continuation-object locals) from the `AsyncInfo` chunk of the debug info blob. The chunk is present only for methods that the JIT compiled with runtime-async suspension points; for all other methods, `AsyncInfoSize` is `0` in the FAT header and the API returns an empty list.

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Remove development-only contract versions by noahfalk · Pull Request #131790 · dotnet/runtime · GitHub
Skip to content
Merged
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
267 changes: 35 additions & 232 deletions docs/design/datacontracts/DebugInfo.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,7 @@ public enum SourceTypes : uint
Default = 0x00, // To indicate that nothing else applies
StackEmpty = 0x01, // The stack is empty here
CallInstruction = 0x02 // The actual instruction of a call
Async = 0x04 // (Version 2+) Indicates suspension/resumption for an async call
Async = 0x04 // Indicates suspension/resumption for an async call
}
```

Expand All@@ -35,15 +35,12 @@ bool HasDebugInfo(TargetCodePointer pCode);
IEnumerable<OffsetMapping> GetMethodNativeMap(TargetCodePointer pCode, bool preferUninstrumented, out uint codeOffset);
```

## Version 1
## Version 2

<!-- BEGIN GENERATED: usage contract=DebugInfo version=c1 -->
<!-- BEGIN GENERATED: usage contract=DebugInfo version=c2 -->
### Data descriptors used

| Data Descriptor | Field | Type | Meaning |
| --- | --- | --- | --- |
| `PatchpointInfo` | *(type size)* | `uint32` | Size in bytes of the fixed patchpoint header before its variable local-offset data |
| `PatchpointInfo` | `LocalCount` | `uint32` | Number of locals in the method associated with the patchpoint. |
_None._

### Global variables used

Expand All@@ -53,19 +50,34 @@ _None._

| Contract Name |
| --- |
| `CodeVersions` |
| `ExecutionManager` |
| `PlatformMetadata` |
<!-- END GENERATED: usage contract=DebugInfo version=c1 -->
| `RuntimeInfo` |
<!-- END GENERATED: usage contract=DebugInfo version=c2 -->

### Constants

Constants:
| Constant Name | Meaning | Value |
| --- | --- | --- |
| IL_OFFSET_BIAS | IL offsets are encoded in the DebugInfo with this bias. | `0xfffffffd` (-3) |
| 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 |
| `IL_OFFSET_BIAS` | Bias used to encode IL offsets | `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 to encode source type flags | `3` |
| `MAX_ILNUM` | Bias for adjusted encoding of variable numbers | `0xfffffffa` (-6) |
| `CALL_RETURN_ILNUM` | Special variable number identifying a call-return-value entry | `0xfffffffb` (-5) |
| `VLT_REG` | Variable is in a register | `0` |
| `VLT_REG_BYREF` | Address of the variable is in a register | `1` |
| `VLT_REG_FP` | Variable is in an FP register | `2` |
| `VLT_STK` | Variable is on the stack | `3` |
| `VLT_STK_BYREF` | Address of the variable is on the stack | `4` |
| `VLT_REG_REG` | Variable lives in two registers | `5` |
| `VLT_REG_STK` | Variable lives partly in a register and partly on the stack | `6` |
| `VLT_STK_REG` | Reverse of `VLT_REG_STK` | `7` |
| `VLT_STK2` | Variable lives in two stack slots | `8` |
| `VLT_FPSTK` | Variable is on the floating-point stack | `9` |
| `VLT_FIXED_VA` | Fixed argument in a varargs function | `10` |
| `VLT_COUNT` | Number of valid `VarLocType` values | `11` |
| `VLT_INVALID` | Sentinel for invalid locations | `12` |

### DebugInfo Stream Encoding

Expand DownExpand Up@@ -95,195 +107,6 @@ Examples:

Based on the encoding specification, we use a decoder defined originally for r2r dump `NibbleReader.cs`

### Bounds Data Encoding (R2R Major Version 16+)

For R2R major version 16 and above, the bounds data uses a bit-packed encoding algorithm:

1. The bounds entry count, bits needed for native deltas, and bits needed for IL offsets are encoded using the nibble scheme above
2. Each bounds entry is then bit-packed with:
- 2 bits for source type (SourceTypeInvalid=0, CallInstruction=1, StackEmpty=2, StackEmpty|CallInstruction=3)
- Variable bits for native offset delta (accumulated from previous offset)
- Variable bits for IL offset (with IL_OFFSET_BIAS applied)

The bit-packed data is read byte by byte, collecting bits until enough are available for each entry.

### Implementation

``` csharp
bool IDebugInfo.HasDebugInfo(TargetCodePointer pCode)
Comment thread
noahfalk marked this conversation as resolved.
{
if (_eman.GetCodeBlockHandle(pCode) is not CodeBlockHandle cbh)
return false;

return _eman.GetDebugInfo(cbh, out _) != TargetPointer.Null;
}

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

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

// No debug info exists (e.g. ILStubs). Return empty sequence.
// Callers that need to distinguish this case should use HasDebugInfo first.
if (debugInfo == TargetPointer.Null)
return [];

return RestoreBoundaries(debugInfo, hasFlagByte, preferUninstrumented);
}

private IEnumerable<OffsetMapping> RestoreBoundaries(TargetPointer debugInfo, bool hasFlagByte, bool preferUninstrumented)
{
if (hasFlagByte)
{
// Check flag byte and skip over any patchpoint info
byte flagByte = _target.Read<byte>(debugInfo++);

if ((flagByte & EXTRA_DEBUG_INFO_PATCHPOINT) != 0)
{
uint localCount = _target.Read<uint>(debugInfo + /*PatchpointInfo::LocalCount offset*/)
debugInfo += /*size of PatchpointInfo*/ + (localCount * 4);
}

if ((flagByte & EXTRA_DEBUG_INFO_RICH) != 0)
{
uint richDebugInfoSize = _target.Read<uint>(debugInfo);
debugInfo += 4;
debugInfo += richDebugInfoSize;
}
}

NativeReader nibbleNativeReader = new(new TargetStream(_target, debugInfo, 24 /*maximum size of 4 32bit ints compressed*/), _target.IsLittleEndian);
NibbleReader nibbleReader = new(nibbleNativeReader, 0);

uint cbBounds = nibbleReader.ReadUInt();
uint cbUninstrumentedBounds = 0;
if (cbBounds == DEBUG_INFO_BOUNDS_HAS_INSTRUMENTED_BOUNDS)
{
// This means we have instrumented bounds.
cbBounds = nibbleReader.ReadUInt();
cbUninstrumentedBounds = nibbleReader.ReadUInt();
}
uint _ /*cbVars*/ = nibbleReader.ReadUInt();

TargetPointer addrBounds = debugInfo + (uint)nibbleReader.GetNextByteOffset();
// TargetPointer addrVars = addrBounds + cbBounds + cbUninstrumentedBounds;

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

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

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; // 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 >>= (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++;
}
}
}
```

## 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.

<!-- BEGIN GENERATED: usage contract=DebugInfo version=c2 diff-from=c1 -->
### Data descriptor changes from `c1`

| Change | Data Descriptor | Field | Type | Meaning |
| --- | --- | --- | --- | --- |
| Removed | `PatchpointInfo` | *(type size)* | `uint32` | Size in bytes of the fixed patchpoint header before its variable local-offset data |
| Removed | `PatchpointInfo` | `LocalCount` | `uint32` | Number of locals in the method associated with the patchpoint. |

### Global variable changes from `c1`

_No changes._

### Contract dependency changes from `c1`

| Change | Contract Name |
| --- | --- |
| Added | `CodeVersions` |
| Added | `RuntimeInfo` |
<!-- END GENERATED: usage contract=DebugInfo version=c2 diff-from=c1 -->


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`):
Expand All@@ -309,34 +132,33 @@ AsyncInfoStart = RichDebugInfoStart + RichDebugInfoSize
DebugInfoEnd = AsyncInfoStart + AsyncInfoSize
```

### Bounds Entry Encoding Differences from Version 1
### Bounds Entry Encoding

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]`.
Each bounds entry uses three independent flag bits for source type:
`[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) |
| 2 | 0x4 | `Async` |

`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:
Pseudo-code for 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
if ((encoded & 0x4) != 0) sourceType |= SourceTypes.Async;
```

After masking the 3 bits, shift them out before reading native delta and IL offset fields as before.

### Variable Location APIs (Version 2+)
### Variable Location APIs

Version 2 adds support for decoding native variable location info from the Vars section of the debug info blob.
The contract decodes native variable location information from the Vars section of the debug info blob.

Additional APIs:
```csharp
Expand DownExpand Up@@ -376,25 +198,6 @@ public readonly struct DebugVarInfo
IEnumerable<DebugVarInfo> GetMethodVarInfo(TargetCodePointer pCode, out uint codeOffset);
```

Additional constants (Version 2):
| Constant Name | Meaning | Value |
| --- | --- | --- |
| `MAX_ILNUM` | Bias for adjusted encoding of variable numbers | `0xfffffffa` (-6) |
| `CALL_RETURN_ILNUM` | Special variable number identifying a call-return-value entry | `0xfffffffb` (-5) |
| `VLT_REG` | Variable is in a register | `0` |
| `VLT_REG_BYREF` | Address of the variable is in a register | `1` |
| `VLT_REG_FP` | Variable is in an FP register | `2` |
| `VLT_STK` | Variable is on the stack | `3` |
| `VLT_STK_BYREF` | Address of the variable is on the stack | `4` |
| `VLT_REG_REG` | Variable lives in two registers | `5` |
| `VLT_REG_STK` | Variable lives partly in a register and partly on the stack | `6` |
| `VLT_STK_REG` | Reverse of VLT_REG_STK | `7` |
| `VLT_STK2` | Variable lives in two stack slots | `8` |
| `VLT_FPSTK` | Variable is on the floating-point stack | `9` |
| `VLT_FIXED_VA` | Fixed argument in a varargs function | `10` |
| `VLT_COUNT` | Number of valid VarLocType values | `11` |
| `VLT_INVALID` | Sentinel for invalid locations | `12` |

### Vars Data Encoding

Each variable entry in the Vars section is nibble-encoded as follows:
Expand All@@ -420,7 +223,7 @@ Each variable entry in the Vars section is nibble-encoded as follows:

Signed integers are encoded using the same unsigned scheme, with the sign bit stored in bit 0 (`value = unsigned >> 1`, negate if `unsigned & 1`). On x86, stack offsets are DWORD-aligned and stored divided by `sizeof(DWORD)`.

### Async Suspension Point APIs (Version 2+)
### Async Suspension Point APIs

We also support decoding async suspension points (and their captured continuation-object locals) from the `AsyncInfo` chunk of the debug info blob. The chunk is present only for methods that the JIT compiled with runtime-async suspension points; for all other methods, `AsyncInfoSize` is `0` in the FAT header and the API returns an empty list.

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Remove development-only contract versions by noahfalk · Pull Request #131790 · dotnet/runtime · GitHub
Skip to content
Merged
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
267 changes: 35 additions & 232 deletions docs/design/datacontracts/DebugInfo.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,7 @@ public enum SourceTypes : uint
Default = 0x00, // To indicate that nothing else applies
StackEmpty = 0x01, // The stack is empty here
CallInstruction = 0x02 // The actual instruction of a call
Async = 0x04 // (Version 2+) Indicates suspension/resumption for an async call
Async = 0x04 // Indicates suspension/resumption for an async call
}
```

Expand All@@ -35,15 +35,12 @@ bool HasDebugInfo(TargetCodePointer pCode);
IEnumerable<OffsetMapping> GetMethodNativeMap(TargetCodePointer pCode, bool preferUninstrumented, out uint codeOffset);
```

## Version 1
## Version 2

<!-- BEGIN GENERATED: usage contract=DebugInfo version=c1 -->
<!-- BEGIN GENERATED: usage contract=DebugInfo version=c2 -->
### Data descriptors used

| Data Descriptor | Field | Type | Meaning |
| --- | --- | --- | --- |
| `PatchpointInfo` | *(type size)* | `uint32` | Size in bytes of the fixed patchpoint header before its variable local-offset data |
| `PatchpointInfo` | `LocalCount` | `uint32` | Number of locals in the method associated with the patchpoint. |
_None._

### Global variables used

Expand All@@ -53,19 +50,34 @@ _None._

| Contract Name |
| --- |
| `CodeVersions` |
| `ExecutionManager` |
| `PlatformMetadata` |
<!-- END GENERATED: usage contract=DebugInfo version=c1 -->
| `RuntimeInfo` |
<!-- END GENERATED: usage contract=DebugInfo version=c2 -->

### Constants

Constants:
| Constant Name | Meaning | Value |
| --- | --- | --- |
| IL_OFFSET_BIAS | IL offsets are encoded in the DebugInfo with this bias. | `0xfffffffd` (-3) |
| 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 |
| `IL_OFFSET_BIAS` | Bias used to encode IL offsets | `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 to encode source type flags | `3` |
| `MAX_ILNUM` | Bias for adjusted encoding of variable numbers | `0xfffffffa` (-6) |
| `CALL_RETURN_ILNUM` | Special variable number identifying a call-return-value entry | `0xfffffffb` (-5) |
| `VLT_REG` | Variable is in a register | `0` |
| `VLT_REG_BYREF` | Address of the variable is in a register | `1` |
| `VLT_REG_FP` | Variable is in an FP register | `2` |
| `VLT_STK` | Variable is on the stack | `3` |
| `VLT_STK_BYREF` | Address of the variable is on the stack | `4` |
| `VLT_REG_REG` | Variable lives in two registers | `5` |
| `VLT_REG_STK` | Variable lives partly in a register and partly on the stack | `6` |
| `VLT_STK_REG` | Reverse of `VLT_REG_STK` | `7` |
| `VLT_STK2` | Variable lives in two stack slots | `8` |
| `VLT_FPSTK` | Variable is on the floating-point stack | `9` |
| `VLT_FIXED_VA` | Fixed argument in a varargs function | `10` |
| `VLT_COUNT` | Number of valid `VarLocType` values | `11` |
| `VLT_INVALID` | Sentinel for invalid locations | `12` |

### DebugInfo Stream Encoding

Expand DownExpand Up@@ -95,195 +107,6 @@ Examples:

Based on the encoding specification, we use a decoder defined originally for r2r dump `NibbleReader.cs`

### Bounds Data Encoding (R2R Major Version 16+)

For R2R major version 16 and above, the bounds data uses a bit-packed encoding algorithm:

1. The bounds entry count, bits needed for native deltas, and bits needed for IL offsets are encoded using the nibble scheme above
2. Each bounds entry is then bit-packed with:
- 2 bits for source type (SourceTypeInvalid=0, CallInstruction=1, StackEmpty=2, StackEmpty|CallInstruction=3)
- Variable bits for native offset delta (accumulated from previous offset)
- Variable bits for IL offset (with IL_OFFSET_BIAS applied)

The bit-packed data is read byte by byte, collecting bits until enough are available for each entry.

### Implementation

``` csharp
bool IDebugInfo.HasDebugInfo(TargetCodePointer pCode)
Comment thread
noahfalk marked this conversation as resolved.
{
if (_eman.GetCodeBlockHandle(pCode) is not CodeBlockHandle cbh)
return false;

return _eman.GetDebugInfo(cbh, out _) != TargetPointer.Null;
}

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

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

// No debug info exists (e.g. ILStubs). Return empty sequence.
// Callers that need to distinguish this case should use HasDebugInfo first.
if (debugInfo == TargetPointer.Null)
return [];

return RestoreBoundaries(debugInfo, hasFlagByte, preferUninstrumented);
}

private IEnumerable<OffsetMapping> RestoreBoundaries(TargetPointer debugInfo, bool hasFlagByte, bool preferUninstrumented)
{
if (hasFlagByte)
{
// Check flag byte and skip over any patchpoint info
byte flagByte = _target.Read<byte>(debugInfo++);

if ((flagByte & EXTRA_DEBUG_INFO_PATCHPOINT) != 0)
{
uint localCount = _target.Read<uint>(debugInfo + /*PatchpointInfo::LocalCount offset*/)
debugInfo += /*size of PatchpointInfo*/ + (localCount * 4);
}

if ((flagByte & EXTRA_DEBUG_INFO_RICH) != 0)
{
uint richDebugInfoSize = _target.Read<uint>(debugInfo);
debugInfo += 4;
debugInfo += richDebugInfoSize;
}
}

NativeReader nibbleNativeReader = new(new TargetStream(_target, debugInfo, 24 /*maximum size of 4 32bit ints compressed*/), _target.IsLittleEndian);
NibbleReader nibbleReader = new(nibbleNativeReader, 0);

uint cbBounds = nibbleReader.ReadUInt();
uint cbUninstrumentedBounds = 0;
if (cbBounds == DEBUG_INFO_BOUNDS_HAS_INSTRUMENTED_BOUNDS)
{
// This means we have instrumented bounds.
cbBounds = nibbleReader.ReadUInt();
cbUninstrumentedBounds = nibbleReader.ReadUInt();
}
uint _ /*cbVars*/ = nibbleReader.ReadUInt();

TargetPointer addrBounds = debugInfo + (uint)nibbleReader.GetNextByteOffset();
// TargetPointer addrVars = addrBounds + cbBounds + cbUninstrumentedBounds;

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

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

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; // 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 >>= (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++;
}
}
}
```

## 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.

<!-- BEGIN GENERATED: usage contract=DebugInfo version=c2 diff-from=c1 -->
### Data descriptor changes from `c1`

| Change | Data Descriptor | Field | Type | Meaning |
| --- | --- | --- | --- | --- |
| Removed | `PatchpointInfo` | *(type size)* | `uint32` | Size in bytes of the fixed patchpoint header before its variable local-offset data |
| Removed | `PatchpointInfo` | `LocalCount` | `uint32` | Number of locals in the method associated with the patchpoint. |

### Global variable changes from `c1`

_No changes._

### Contract dependency changes from `c1`

| Change | Contract Name |
| --- | --- |
| Added | `CodeVersions` |
| Added | `RuntimeInfo` |
<!-- END GENERATED: usage contract=DebugInfo version=c2 diff-from=c1 -->


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`):
Expand All@@ -309,34 +132,33 @@ AsyncInfoStart = RichDebugInfoStart + RichDebugInfoSize
DebugInfoEnd = AsyncInfoStart + AsyncInfoSize
```

### Bounds Entry Encoding Differences from Version 1
### Bounds Entry Encoding

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]`.
Each bounds entry uses three independent flag bits for source type:
`[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) |
| 2 | 0x4 | `Async` |

`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:
Pseudo-code for 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
if ((encoded & 0x4) != 0) sourceType |= SourceTypes.Async;
```

After masking the 3 bits, shift them out before reading native delta and IL offset fields as before.

### Variable Location APIs (Version 2+)
### Variable Location APIs

Version 2 adds support for decoding native variable location info from the Vars section of the debug info blob.
The contract decodes native variable location information from the Vars section of the debug info blob.

Additional APIs:
```csharp
Expand DownExpand Up@@ -376,25 +198,6 @@ public readonly struct DebugVarInfo
IEnumerable<DebugVarInfo> GetMethodVarInfo(TargetCodePointer pCode, out uint codeOffset);
```

Additional constants (Version 2):
| Constant Name | Meaning | Value |
| --- | --- | --- |
| `MAX_ILNUM` | Bias for adjusted encoding of variable numbers | `0xfffffffa` (-6) |
| `CALL_RETURN_ILNUM` | Special variable number identifying a call-return-value entry | `0xfffffffb` (-5) |
| `VLT_REG` | Variable is in a register | `0` |
| `VLT_REG_BYREF` | Address of the variable is in a register | `1` |
| `VLT_REG_FP` | Variable is in an FP register | `2` |
| `VLT_STK` | Variable is on the stack | `3` |
| `VLT_STK_BYREF` | Address of the variable is on the stack | `4` |
| `VLT_REG_REG` | Variable lives in two registers | `5` |
| `VLT_REG_STK` | Variable lives partly in a register and partly on the stack | `6` |
| `VLT_STK_REG` | Reverse of VLT_REG_STK | `7` |
| `VLT_STK2` | Variable lives in two stack slots | `8` |
| `VLT_FPSTK` | Variable is on the floating-point stack | `9` |
| `VLT_FIXED_VA` | Fixed argument in a varargs function | `10` |
| `VLT_COUNT` | Number of valid VarLocType values | `11` |
| `VLT_INVALID` | Sentinel for invalid locations | `12` |

### Vars Data Encoding

Each variable entry in the Vars section is nibble-encoded as follows:
Expand All@@ -420,7 +223,7 @@ Each variable entry in the Vars section is nibble-encoded as follows:

Signed integers are encoded using the same unsigned scheme, with the sign bit stored in bit 0 (`value = unsigned >> 1`, negate if `unsigned & 1`). On x86, stack offsets are DWORD-aligned and stored divided by `sizeof(DWORD)`.

### Async Suspension Point APIs (Version 2+)
### Async Suspension Point APIs

We also support decoding async suspension points (and their captured continuation-object locals) from the `AsyncInfo` chunk of the debug info blob. The chunk is present only for methods that the JIT compiled with runtime-async suspension points; for all other methods, `AsyncInfoSize` is `0` in the FAT header and the API returns an empty list.

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Remove development-only contract versions by noahfalk · Pull Request #131790 · dotnet/runtime · GitHub
Skip to content
Merged
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
267 changes: 35 additions & 232 deletions docs/design/datacontracts/DebugInfo.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,7 @@ public enum SourceTypes : uint
Default = 0x00, // To indicate that nothing else applies
StackEmpty = 0x01, // The stack is empty here
CallInstruction = 0x02 // The actual instruction of a call
Async = 0x04 // (Version 2+) Indicates suspension/resumption for an async call
Async = 0x04 // Indicates suspension/resumption for an async call
}
```

Expand All@@ -35,15 +35,12 @@ bool HasDebugInfo(TargetCodePointer pCode);
IEnumerable<OffsetMapping> GetMethodNativeMap(TargetCodePointer pCode, bool preferUninstrumented, out uint codeOffset);
```

## Version 1
## Version 2

<!-- BEGIN GENERATED: usage contract=DebugInfo version=c1 -->
<!-- BEGIN GENERATED: usage contract=DebugInfo version=c2 -->
### Data descriptors used

| Data Descriptor | Field | Type | Meaning |
| --- | --- | --- | --- |
| `PatchpointInfo` | *(type size)* | `uint32` | Size in bytes of the fixed patchpoint header before its variable local-offset data |
| `PatchpointInfo` | `LocalCount` | `uint32` | Number of locals in the method associated with the patchpoint. |
_None._

### Global variables used

Expand All@@ -53,19 +50,34 @@ _None._

| Contract Name |
| --- |
| `CodeVersions` |
| `ExecutionManager` |
| `PlatformMetadata` |
<!-- END GENERATED: usage contract=DebugInfo version=c1 -->
| `RuntimeInfo` |
<!-- END GENERATED: usage contract=DebugInfo version=c2 -->

### Constants

Constants:
| Constant Name | Meaning | Value |
| --- | --- | --- |
| IL_OFFSET_BIAS | IL offsets are encoded in the DebugInfo with this bias. | `0xfffffffd` (-3) |
| 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 |
| `IL_OFFSET_BIAS` | Bias used to encode IL offsets | `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 to encode source type flags | `3` |
| `MAX_ILNUM` | Bias for adjusted encoding of variable numbers | `0xfffffffa` (-6) |
| `CALL_RETURN_ILNUM` | Special variable number identifying a call-return-value entry | `0xfffffffb` (-5) |
| `VLT_REG` | Variable is in a register | `0` |
| `VLT_REG_BYREF` | Address of the variable is in a register | `1` |
| `VLT_REG_FP` | Variable is in an FP register | `2` |
| `VLT_STK` | Variable is on the stack | `3` |
| `VLT_STK_BYREF` | Address of the variable is on the stack | `4` |
| `VLT_REG_REG` | Variable lives in two registers | `5` |
| `VLT_REG_STK` | Variable lives partly in a register and partly on the stack | `6` |
| `VLT_STK_REG` | Reverse of `VLT_REG_STK` | `7` |
| `VLT_STK2` | Variable lives in two stack slots | `8` |
| `VLT_FPSTK` | Variable is on the floating-point stack | `9` |
| `VLT_FIXED_VA` | Fixed argument in a varargs function | `10` |
| `VLT_COUNT` | Number of valid `VarLocType` values | `11` |
| `VLT_INVALID` | Sentinel for invalid locations | `12` |

### DebugInfo Stream Encoding

Expand DownExpand Up@@ -95,195 +107,6 @@ Examples:

Based on the encoding specification, we use a decoder defined originally for r2r dump `NibbleReader.cs`

### Bounds Data Encoding (R2R Major Version 16+)

For R2R major version 16 and above, the bounds data uses a bit-packed encoding algorithm:

1. The bounds entry count, bits needed for native deltas, and bits needed for IL offsets are encoded using the nibble scheme above
2. Each bounds entry is then bit-packed with:
- 2 bits for source type (SourceTypeInvalid=0, CallInstruction=1, StackEmpty=2, StackEmpty|CallInstruction=3)
- Variable bits for native offset delta (accumulated from previous offset)
- Variable bits for IL offset (with IL_OFFSET_BIAS applied)

The bit-packed data is read byte by byte, collecting bits until enough are available for each entry.

### Implementation

``` csharp
bool IDebugInfo.HasDebugInfo(TargetCodePointer pCode)
Comment thread
noahfalk marked this conversation as resolved.
{
if (_eman.GetCodeBlockHandle(pCode) is not CodeBlockHandle cbh)
return false;

return _eman.GetDebugInfo(cbh, out _) != TargetPointer.Null;
}

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

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

// No debug info exists (e.g. ILStubs). Return empty sequence.
// Callers that need to distinguish this case should use HasDebugInfo first.
if (debugInfo == TargetPointer.Null)
return [];

return RestoreBoundaries(debugInfo, hasFlagByte, preferUninstrumented);
}

private IEnumerable<OffsetMapping> RestoreBoundaries(TargetPointer debugInfo, bool hasFlagByte, bool preferUninstrumented)
{
if (hasFlagByte)
{
// Check flag byte and skip over any patchpoint info
byte flagByte = _target.Read<byte>(debugInfo++);

if ((flagByte & EXTRA_DEBUG_INFO_PATCHPOINT) != 0)
{
uint localCount = _target.Read<uint>(debugInfo + /*PatchpointInfo::LocalCount offset*/)
debugInfo += /*size of PatchpointInfo*/ + (localCount * 4);
}

if ((flagByte & EXTRA_DEBUG_INFO_RICH) != 0)
{
uint richDebugInfoSize = _target.Read<uint>(debugInfo);
debugInfo += 4;
debugInfo += richDebugInfoSize;
}
}

NativeReader nibbleNativeReader = new(new TargetStream(_target, debugInfo, 24 /*maximum size of 4 32bit ints compressed*/), _target.IsLittleEndian);
NibbleReader nibbleReader = new(nibbleNativeReader, 0);

uint cbBounds = nibbleReader.ReadUInt();
uint cbUninstrumentedBounds = 0;
if (cbBounds == DEBUG_INFO_BOUNDS_HAS_INSTRUMENTED_BOUNDS)
{
// This means we have instrumented bounds.
cbBounds = nibbleReader.ReadUInt();
cbUninstrumentedBounds = nibbleReader.ReadUInt();
}
uint _ /*cbVars*/ = nibbleReader.ReadUInt();

TargetPointer addrBounds = debugInfo + (uint)nibbleReader.GetNextByteOffset();
// TargetPointer addrVars = addrBounds + cbBounds + cbUninstrumentedBounds;

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

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

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; // 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 >>= (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++;
}
}
}
```

## 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.

<!-- BEGIN GENERATED: usage contract=DebugInfo version=c2 diff-from=c1 -->
### Data descriptor changes from `c1`

| Change | Data Descriptor | Field | Type | Meaning |
| --- | --- | --- | --- | --- |
| Removed | `PatchpointInfo` | *(type size)* | `uint32` | Size in bytes of the fixed patchpoint header before its variable local-offset data |
| Removed | `PatchpointInfo` | `LocalCount` | `uint32` | Number of locals in the method associated with the patchpoint. |

### Global variable changes from `c1`

_No changes._

### Contract dependency changes from `c1`

| Change | Contract Name |
| --- | --- |
| Added | `CodeVersions` |
| Added | `RuntimeInfo` |
<!-- END GENERATED: usage contract=DebugInfo version=c2 diff-from=c1 -->


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`):
Expand All@@ -309,34 +132,33 @@ AsyncInfoStart = RichDebugInfoStart + RichDebugInfoSize
DebugInfoEnd = AsyncInfoStart + AsyncInfoSize
```

### Bounds Entry Encoding Differences from Version 1
### Bounds Entry Encoding

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]`.
Each bounds entry uses three independent flag bits for source type:
`[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) |
| 2 | 0x4 | `Async` |

`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:
Pseudo-code for 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
if ((encoded & 0x4) != 0) sourceType |= SourceTypes.Async;
```

After masking the 3 bits, shift them out before reading native delta and IL offset fields as before.

### Variable Location APIs (Version 2+)
### Variable Location APIs

Version 2 adds support for decoding native variable location info from the Vars section of the debug info blob.
The contract decodes native variable location information from the Vars section of the debug info blob.

Additional APIs:
```csharp
Expand DownExpand Up@@ -376,25 +198,6 @@ public readonly struct DebugVarInfo
IEnumerable<DebugVarInfo> GetMethodVarInfo(TargetCodePointer pCode, out uint codeOffset);
```

Additional constants (Version 2):
| Constant Name | Meaning | Value |
| --- | --- | --- |
| `MAX_ILNUM` | Bias for adjusted encoding of variable numbers | `0xfffffffa` (-6) |
| `CALL_RETURN_ILNUM` | Special variable number identifying a call-return-value entry | `0xfffffffb` (-5) |
| `VLT_REG` | Variable is in a register | `0` |
| `VLT_REG_BYREF` | Address of the variable is in a register | `1` |
| `VLT_REG_FP` | Variable is in an FP register | `2` |
| `VLT_STK` | Variable is on the stack | `3` |
| `VLT_STK_BYREF` | Address of the variable is on the stack | `4` |
| `VLT_REG_REG` | Variable lives in two registers | `5` |
| `VLT_REG_STK` | Variable lives partly in a register and partly on the stack | `6` |
| `VLT_STK_REG` | Reverse of VLT_REG_STK | `7` |
| `VLT_STK2` | Variable lives in two stack slots | `8` |
| `VLT_FPSTK` | Variable is on the floating-point stack | `9` |
| `VLT_FIXED_VA` | Fixed argument in a varargs function | `10` |
| `VLT_COUNT` | Number of valid VarLocType values | `11` |
| `VLT_INVALID` | Sentinel for invalid locations | `12` |

### Vars Data Encoding

Each variable entry in the Vars section is nibble-encoded as follows:
Expand All@@ -420,7 +223,7 @@ Each variable entry in the Vars section is nibble-encoded as follows:

Signed integers are encoded using the same unsigned scheme, with the sign bit stored in bit 0 (`value = unsigned >> 1`, negate if `unsigned & 1`). On x86, stack offsets are DWORD-aligned and stored divided by `sizeof(DWORD)`.

### Async Suspension Point APIs (Version 2+)
### Async Suspension Point APIs

We also support decoding async suspension points (and their captured continuation-object locals) from the `AsyncInfo` chunk of the debug info blob. The chunk is present only for methods that the JIT compiled with runtime-async suspension points; for all other methods, `AsyncInfoSize` is `0` in the FAT header and the API returns an empty list.

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Remove development-only contract versions by noahfalk · Pull Request #131790 · dotnet/runtime · GitHub
Skip to content
Merged
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
267 changes: 35 additions & 232 deletions docs/design/datacontracts/DebugInfo.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,7 @@ public enum SourceTypes : uint
Default = 0x00, // To indicate that nothing else applies
StackEmpty = 0x01, // The stack is empty here
CallInstruction = 0x02 // The actual instruction of a call
Async = 0x04 // (Version 2+) Indicates suspension/resumption for an async call
Async = 0x04 // Indicates suspension/resumption for an async call
}
```

Expand All@@ -35,15 +35,12 @@ bool HasDebugInfo(TargetCodePointer pCode);
IEnumerable<OffsetMapping> GetMethodNativeMap(TargetCodePointer pCode, bool preferUninstrumented, out uint codeOffset);
```

## Version 1
## Version 2

<!-- BEGIN GENERATED: usage contract=DebugInfo version=c1 -->
<!-- BEGIN GENERATED: usage contract=DebugInfo version=c2 -->
### Data descriptors used

| Data Descriptor | Field | Type | Meaning |
| --- | --- | --- | --- |
| `PatchpointInfo` | *(type size)* | `uint32` | Size in bytes of the fixed patchpoint header before its variable local-offset data |
| `PatchpointInfo` | `LocalCount` | `uint32` | Number of locals in the method associated with the patchpoint. |
_None._

### Global variables used

Expand All@@ -53,19 +50,34 @@ _None._

| Contract Name |
| --- |
| `CodeVersions` |
| `ExecutionManager` |
| `PlatformMetadata` |
<!-- END GENERATED: usage contract=DebugInfo version=c1 -->
| `RuntimeInfo` |
<!-- END GENERATED: usage contract=DebugInfo version=c2 -->

### Constants

Constants:
| Constant Name | Meaning | Value |
| --- | --- | --- |
| IL_OFFSET_BIAS | IL offsets are encoded in the DebugInfo with this bias. | `0xfffffffd` (-3) |
| 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 |
| `IL_OFFSET_BIAS` | Bias used to encode IL offsets | `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 to encode source type flags | `3` |
| `MAX_ILNUM` | Bias for adjusted encoding of variable numbers | `0xfffffffa` (-6) |
| `CALL_RETURN_ILNUM` | Special variable number identifying a call-return-value entry | `0xfffffffb` (-5) |
| `VLT_REG` | Variable is in a register | `0` |
| `VLT_REG_BYREF` | Address of the variable is in a register | `1` |
| `VLT_REG_FP` | Variable is in an FP register | `2` |
| `VLT_STK` | Variable is on the stack | `3` |
| `VLT_STK_BYREF` | Address of the variable is on the stack | `4` |
| `VLT_REG_REG` | Variable lives in two registers | `5` |
| `VLT_REG_STK` | Variable lives partly in a register and partly on the stack | `6` |
| `VLT_STK_REG` | Reverse of `VLT_REG_STK` | `7` |
| `VLT_STK2` | Variable lives in two stack slots | `8` |
| `VLT_FPSTK` | Variable is on the floating-point stack | `9` |
| `VLT_FIXED_VA` | Fixed argument in a varargs function | `10` |
| `VLT_COUNT` | Number of valid `VarLocType` values | `11` |
| `VLT_INVALID` | Sentinel for invalid locations | `12` |

### DebugInfo Stream Encoding

Expand DownExpand Up@@ -95,195 +107,6 @@ Examples:

Based on the encoding specification, we use a decoder defined originally for r2r dump `NibbleReader.cs`

### Bounds Data Encoding (R2R Major Version 16+)

For R2R major version 16 and above, the bounds data uses a bit-packed encoding algorithm:

1. The bounds entry count, bits needed for native deltas, and bits needed for IL offsets are encoded using the nibble scheme above
2. Each bounds entry is then bit-packed with:
- 2 bits for source type (SourceTypeInvalid=0, CallInstruction=1, StackEmpty=2, StackEmpty|CallInstruction=3)
- Variable bits for native offset delta (accumulated from previous offset)
- Variable bits for IL offset (with IL_OFFSET_BIAS applied)

The bit-packed data is read byte by byte, collecting bits until enough are available for each entry.

### Implementation

``` csharp
bool IDebugInfo.HasDebugInfo(TargetCodePointer pCode)
Comment thread
noahfalk marked this conversation as resolved.
{
if (_eman.GetCodeBlockHandle(pCode) is not CodeBlockHandle cbh)
return false;

return _eman.GetDebugInfo(cbh, out _) != TargetPointer.Null;
}

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

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

// No debug info exists (e.g. ILStubs). Return empty sequence.
// Callers that need to distinguish this case should use HasDebugInfo first.
if (debugInfo == TargetPointer.Null)
return [];

return RestoreBoundaries(debugInfo, hasFlagByte, preferUninstrumented);
}

private IEnumerable<OffsetMapping> RestoreBoundaries(TargetPointer debugInfo, bool hasFlagByte, bool preferUninstrumented)
{
if (hasFlagByte)
{
// Check flag byte and skip over any patchpoint info
byte flagByte = _target.Read<byte>(debugInfo++);

if ((flagByte & EXTRA_DEBUG_INFO_PATCHPOINT) != 0)
{
uint localCount = _target.Read<uint>(debugInfo + /*PatchpointInfo::LocalCount offset*/)
debugInfo += /*size of PatchpointInfo*/ + (localCount * 4);
}

if ((flagByte & EXTRA_DEBUG_INFO_RICH) != 0)
{
uint richDebugInfoSize = _target.Read<uint>(debugInfo);
debugInfo += 4;
debugInfo += richDebugInfoSize;
}
}

NativeReader nibbleNativeReader = new(new TargetStream(_target, debugInfo, 24 /*maximum size of 4 32bit ints compressed*/), _target.IsLittleEndian);
NibbleReader nibbleReader = new(nibbleNativeReader, 0);

uint cbBounds = nibbleReader.ReadUInt();
uint cbUninstrumentedBounds = 0;
if (cbBounds == DEBUG_INFO_BOUNDS_HAS_INSTRUMENTED_BOUNDS)
{
// This means we have instrumented bounds.
cbBounds = nibbleReader.ReadUInt();
cbUninstrumentedBounds = nibbleReader.ReadUInt();
}
uint _ /*cbVars*/ = nibbleReader.ReadUInt();

TargetPointer addrBounds = debugInfo + (uint)nibbleReader.GetNextByteOffset();
// TargetPointer addrVars = addrBounds + cbBounds + cbUninstrumentedBounds;

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

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

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; // 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 >>= (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++;
}
}
}
```

## 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.

<!-- BEGIN GENERATED: usage contract=DebugInfo version=c2 diff-from=c1 -->
### Data descriptor changes from `c1`

| Change | Data Descriptor | Field | Type | Meaning |
| --- | --- | --- | --- | --- |
| Removed | `PatchpointInfo` | *(type size)* | `uint32` | Size in bytes of the fixed patchpoint header before its variable local-offset data |
| Removed | `PatchpointInfo` | `LocalCount` | `uint32` | Number of locals in the method associated with the patchpoint. |

### Global variable changes from `c1`

_No changes._

### Contract dependency changes from `c1`

| Change | Contract Name |
| --- | --- |
| Added | `CodeVersions` |
| Added | `RuntimeInfo` |
<!-- END GENERATED: usage contract=DebugInfo version=c2 diff-from=c1 -->


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`):
Expand All@@ -309,34 +132,33 @@ AsyncInfoStart = RichDebugInfoStart + RichDebugInfoSize
DebugInfoEnd = AsyncInfoStart + AsyncInfoSize
```

### Bounds Entry Encoding Differences from Version 1
### Bounds Entry Encoding

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]`.
Each bounds entry uses three independent flag bits for source type:
`[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) |
| 2 | 0x4 | `Async` |

`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:
Pseudo-code for 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
if ((encoded & 0x4) != 0) sourceType |= SourceTypes.Async;
```

After masking the 3 bits, shift them out before reading native delta and IL offset fields as before.

### Variable Location APIs (Version 2+)
### Variable Location APIs

Version 2 adds support for decoding native variable location info from the Vars section of the debug info blob.
The contract decodes native variable location information from the Vars section of the debug info blob.

Additional APIs:
```csharp
Expand DownExpand Up@@ -376,25 +198,6 @@ public readonly struct DebugVarInfo
IEnumerable<DebugVarInfo> GetMethodVarInfo(TargetCodePointer pCode, out uint codeOffset);
```

Additional constants (Version 2):
| Constant Name | Meaning | Value |
| --- | --- | --- |
| `MAX_ILNUM` | Bias for adjusted encoding of variable numbers | `0xfffffffa` (-6) |
| `CALL_RETURN_ILNUM` | Special variable number identifying a call-return-value entry | `0xfffffffb` (-5) |
| `VLT_REG` | Variable is in a register | `0` |
| `VLT_REG_BYREF` | Address of the variable is in a register | `1` |
| `VLT_REG_FP` | Variable is in an FP register | `2` |
| `VLT_STK` | Variable is on the stack | `3` |
| `VLT_STK_BYREF` | Address of the variable is on the stack | `4` |
| `VLT_REG_REG` | Variable lives in two registers | `5` |
| `VLT_REG_STK` | Variable lives partly in a register and partly on the stack | `6` |
| `VLT_STK_REG` | Reverse of VLT_REG_STK | `7` |
| `VLT_STK2` | Variable lives in two stack slots | `8` |
| `VLT_FPSTK` | Variable is on the floating-point stack | `9` |
| `VLT_FIXED_VA` | Fixed argument in a varargs function | `10` |
| `VLT_COUNT` | Number of valid VarLocType values | `11` |
| `VLT_INVALID` | Sentinel for invalid locations | `12` |

### Vars Data Encoding

Each variable entry in the Vars section is nibble-encoded as follows:
Expand All@@ -420,7 +223,7 @@ Each variable entry in the Vars section is nibble-encoded as follows:

Signed integers are encoded using the same unsigned scheme, with the sign bit stored in bit 0 (`value = unsigned >> 1`, negate if `unsigned & 1`). On x86, stack offsets are DWORD-aligned and stored divided by `sizeof(DWORD)`.

### Async Suspension Point APIs (Version 2+)
### Async Suspension Point APIs

We also support decoding async suspension points (and their captured continuation-object locals) from the `AsyncInfo` chunk of the debug info blob. The chunk is present only for methods that the JIT compiled with runtime-async suspension points; for all other methods, `AsyncInfoSize` is `0` in the FAT header and the API returns an empty list.

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Remove development-only contract versions by noahfalk · Pull Request #131790 · dotnet/runtime · GitHub
Skip to content
Merged
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
267 changes: 35 additions & 232 deletions docs/design/datacontracts/DebugInfo.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,7 @@ public enum SourceTypes : uint
Default = 0x00, // To indicate that nothing else applies
StackEmpty = 0x01, // The stack is empty here
CallInstruction = 0x02 // The actual instruction of a call
Async = 0x04 // (Version 2+) Indicates suspension/resumption for an async call
Async = 0x04 // Indicates suspension/resumption for an async call
}
```

Expand All@@ -35,15 +35,12 @@ bool HasDebugInfo(TargetCodePointer pCode);
IEnumerable<OffsetMapping> GetMethodNativeMap(TargetCodePointer pCode, bool preferUninstrumented, out uint codeOffset);
```

## Version 1
## Version 2

<!-- BEGIN GENERATED: usage contract=DebugInfo version=c1 -->
<!-- BEGIN GENERATED: usage contract=DebugInfo version=c2 -->
### Data descriptors used

| Data Descriptor | Field | Type | Meaning |
| --- | --- | --- | --- |
| `PatchpointInfo` | *(type size)* | `uint32` | Size in bytes of the fixed patchpoint header before its variable local-offset data |
| `PatchpointInfo` | `LocalCount` | `uint32` | Number of locals in the method associated with the patchpoint. |
_None._

### Global variables used

Expand All@@ -53,19 +50,34 @@ _None._

| Contract Name |
| --- |
| `CodeVersions` |
| `ExecutionManager` |
| `PlatformMetadata` |
<!-- END GENERATED: usage contract=DebugInfo version=c1 -->
| `RuntimeInfo` |
<!-- END GENERATED: usage contract=DebugInfo version=c2 -->

### Constants

Constants:
| Constant Name | Meaning | Value |
| --- | --- | --- |
| IL_OFFSET_BIAS | IL offsets are encoded in the DebugInfo with this bias. | `0xfffffffd` (-3) |
| 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 |
| `IL_OFFSET_BIAS` | Bias used to encode IL offsets | `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 to encode source type flags | `3` |
| `MAX_ILNUM` | Bias for adjusted encoding of variable numbers | `0xfffffffa` (-6) |
| `CALL_RETURN_ILNUM` | Special variable number identifying a call-return-value entry | `0xfffffffb` (-5) |
| `VLT_REG` | Variable is in a register | `0` |
| `VLT_REG_BYREF` | Address of the variable is in a register | `1` |
| `VLT_REG_FP` | Variable is in an FP register | `2` |
| `VLT_STK` | Variable is on the stack | `3` |
| `VLT_STK_BYREF` | Address of the variable is on the stack | `4` |
| `VLT_REG_REG` | Variable lives in two registers | `5` |
| `VLT_REG_STK` | Variable lives partly in a register and partly on the stack | `6` |
| `VLT_STK_REG` | Reverse of `VLT_REG_STK` | `7` |
| `VLT_STK2` | Variable lives in two stack slots | `8` |
| `VLT_FPSTK` | Variable is on the floating-point stack | `9` |
| `VLT_FIXED_VA` | Fixed argument in a varargs function | `10` |
| `VLT_COUNT` | Number of valid `VarLocType` values | `11` |
| `VLT_INVALID` | Sentinel for invalid locations | `12` |

### DebugInfo Stream Encoding

Expand DownExpand Up@@ -95,195 +107,6 @@ Examples:

Based on the encoding specification, we use a decoder defined originally for r2r dump `NibbleReader.cs`

### Bounds Data Encoding (R2R Major Version 16+)

For R2R major version 16 and above, the bounds data uses a bit-packed encoding algorithm:

1. The bounds entry count, bits needed for native deltas, and bits needed for IL offsets are encoded using the nibble scheme above
2. Each bounds entry is then bit-packed with:
- 2 bits for source type (SourceTypeInvalid=0, CallInstruction=1, StackEmpty=2, StackEmpty|CallInstruction=3)
- Variable bits for native offset delta (accumulated from previous offset)
- Variable bits for IL offset (with IL_OFFSET_BIAS applied)

The bit-packed data is read byte by byte, collecting bits until enough are available for each entry.

### Implementation

``` csharp
bool IDebugInfo.HasDebugInfo(TargetCodePointer pCode)
Comment thread
noahfalk marked this conversation as resolved.
{
if (_eman.GetCodeBlockHandle(pCode) is not CodeBlockHandle cbh)
return false;

return _eman.GetDebugInfo(cbh, out _) != TargetPointer.Null;
}

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

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

// No debug info exists (e.g. ILStubs). Return empty sequence.
// Callers that need to distinguish this case should use HasDebugInfo first.
if (debugInfo == TargetPointer.Null)
return [];

return RestoreBoundaries(debugInfo, hasFlagByte, preferUninstrumented);
}

private IEnumerable<OffsetMapping> RestoreBoundaries(TargetPointer debugInfo, bool hasFlagByte, bool preferUninstrumented)
{
if (hasFlagByte)
{
// Check flag byte and skip over any patchpoint info
byte flagByte = _target.Read<byte>(debugInfo++);

if ((flagByte & EXTRA_DEBUG_INFO_PATCHPOINT) != 0)
{
uint localCount = _target.Read<uint>(debugInfo + /*PatchpointInfo::LocalCount offset*/)
debugInfo += /*size of PatchpointInfo*/ + (localCount * 4);
}

if ((flagByte & EXTRA_DEBUG_INFO_RICH) != 0)
{
uint richDebugInfoSize = _target.Read<uint>(debugInfo);
debugInfo += 4;
debugInfo += richDebugInfoSize;
}
}

NativeReader nibbleNativeReader = new(new TargetStream(_target, debugInfo, 24 /*maximum size of 4 32bit ints compressed*/), _target.IsLittleEndian);
NibbleReader nibbleReader = new(nibbleNativeReader, 0);

uint cbBounds = nibbleReader.ReadUInt();
uint cbUninstrumentedBounds = 0;
if (cbBounds == DEBUG_INFO_BOUNDS_HAS_INSTRUMENTED_BOUNDS)
{
// This means we have instrumented bounds.
cbBounds = nibbleReader.ReadUInt();
cbUninstrumentedBounds = nibbleReader.ReadUInt();
}
uint _ /*cbVars*/ = nibbleReader.ReadUInt();

TargetPointer addrBounds = debugInfo + (uint)nibbleReader.GetNextByteOffset();
// TargetPointer addrVars = addrBounds + cbBounds + cbUninstrumentedBounds;

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

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

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; // 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 >>= (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++;
}
}
}
```

## 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.

<!-- BEGIN GENERATED: usage contract=DebugInfo version=c2 diff-from=c1 -->
### Data descriptor changes from `c1`

| Change | Data Descriptor | Field | Type | Meaning |
| --- | --- | --- | --- | --- |
| Removed | `PatchpointInfo` | *(type size)* | `uint32` | Size in bytes of the fixed patchpoint header before its variable local-offset data |
| Removed | `PatchpointInfo` | `LocalCount` | `uint32` | Number of locals in the method associated with the patchpoint. |

### Global variable changes from `c1`

_No changes._

### Contract dependency changes from `c1`

| Change | Contract Name |
| --- | --- |
| Added | `CodeVersions` |
| Added | `RuntimeInfo` |
<!-- END GENERATED: usage contract=DebugInfo version=c2 diff-from=c1 -->


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`):
Expand All@@ -309,34 +132,33 @@ AsyncInfoStart = RichDebugInfoStart + RichDebugInfoSize
DebugInfoEnd = AsyncInfoStart + AsyncInfoSize
```

### Bounds Entry Encoding Differences from Version 1
### Bounds Entry Encoding

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]`.
Each bounds entry uses three independent flag bits for source type:
`[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) |
| 2 | 0x4 | `Async` |

`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:
Pseudo-code for 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
if ((encoded & 0x4) != 0) sourceType |= SourceTypes.Async;
```

After masking the 3 bits, shift them out before reading native delta and IL offset fields as before.

### Variable Location APIs (Version 2+)
### Variable Location APIs

Version 2 adds support for decoding native variable location info from the Vars section of the debug info blob.
The contract decodes native variable location information from the Vars section of the debug info blob.

Additional APIs:
```csharp
Expand DownExpand Up@@ -376,25 +198,6 @@ public readonly struct DebugVarInfo
IEnumerable<DebugVarInfo> GetMethodVarInfo(TargetCodePointer pCode, out uint codeOffset);
```

Additional constants (Version 2):
| Constant Name | Meaning | Value |
| --- | --- | --- |
| `MAX_ILNUM` | Bias for adjusted encoding of variable numbers | `0xfffffffa` (-6) |
| `CALL_RETURN_ILNUM` | Special variable number identifying a call-return-value entry | `0xfffffffb` (-5) |
| `VLT_REG` | Variable is in a register | `0` |
| `VLT_REG_BYREF` | Address of the variable is in a register | `1` |
| `VLT_REG_FP` | Variable is in an FP register | `2` |
| `VLT_STK` | Variable is on the stack | `3` |
| `VLT_STK_BYREF` | Address of the variable is on the stack | `4` |
| `VLT_REG_REG` | Variable lives in two registers | `5` |
| `VLT_REG_STK` | Variable lives partly in a register and partly on the stack | `6` |
| `VLT_STK_REG` | Reverse of VLT_REG_STK | `7` |
| `VLT_STK2` | Variable lives in two stack slots | `8` |
| `VLT_FPSTK` | Variable is on the floating-point stack | `9` |
| `VLT_FIXED_VA` | Fixed argument in a varargs function | `10` |
| `VLT_COUNT` | Number of valid VarLocType values | `11` |
| `VLT_INVALID` | Sentinel for invalid locations | `12` |

### Vars Data Encoding

Each variable entry in the Vars section is nibble-encoded as follows:
Expand All@@ -420,7 +223,7 @@ Each variable entry in the Vars section is nibble-encoded as follows:

Signed integers are encoded using the same unsigned scheme, with the sign bit stored in bit 0 (`value = unsigned >> 1`, negate if `unsigned & 1`). On x86, stack offsets are DWORD-aligned and stored divided by `sizeof(DWORD)`.

### Async Suspension Point APIs (Version 2+)
### Async Suspension Point APIs

We also support decoding async suspension points (and their captured continuation-object locals) from the `AsyncInfo` chunk of the debug info blob. The chunk is present only for methods that the JIT compiled with runtime-async suspension points; for all other methods, `AsyncInfoSize` is `0` in the FAT header and the API returns an empty list.

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Remove development-only contract versions by noahfalk · Pull Request #131790 · dotnet/runtime · GitHub
Skip to content
Merged
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
267 changes: 35 additions & 232 deletions docs/design/datacontracts/DebugInfo.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,7 @@ public enum SourceTypes : uint
Default = 0x00, // To indicate that nothing else applies
StackEmpty = 0x01, // The stack is empty here
CallInstruction = 0x02 // The actual instruction of a call
Async = 0x04 // (Version 2+) Indicates suspension/resumption for an async call
Async = 0x04 // Indicates suspension/resumption for an async call
}
```

Expand All@@ -35,15 +35,12 @@ bool HasDebugInfo(TargetCodePointer pCode);
IEnumerable<OffsetMapping> GetMethodNativeMap(TargetCodePointer pCode, bool preferUninstrumented, out uint codeOffset);
```

## Version 1
## Version 2

<!-- BEGIN GENERATED: usage contract=DebugInfo version=c1 -->
<!-- BEGIN GENERATED: usage contract=DebugInfo version=c2 -->
### Data descriptors used

| Data Descriptor | Field | Type | Meaning |
| --- | --- | --- | --- |
| `PatchpointInfo` | *(type size)* | `uint32` | Size in bytes of the fixed patchpoint header before its variable local-offset data |
| `PatchpointInfo` | `LocalCount` | `uint32` | Number of locals in the method associated with the patchpoint. |
_None._

### Global variables used

Expand All@@ -53,19 +50,34 @@ _None._

| Contract Name |
| --- |
| `CodeVersions` |
| `ExecutionManager` |
| `PlatformMetadata` |
<!-- END GENERATED: usage contract=DebugInfo version=c1 -->
| `RuntimeInfo` |
<!-- END GENERATED: usage contract=DebugInfo version=c2 -->

### Constants

Constants:
| Constant Name | Meaning | Value |
| --- | --- | --- |
| IL_OFFSET_BIAS | IL offsets are encoded in the DebugInfo with this bias. | `0xfffffffd` (-3) |
| 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 |
| `IL_OFFSET_BIAS` | Bias used to encode IL offsets | `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 to encode source type flags | `3` |
| `MAX_ILNUM` | Bias for adjusted encoding of variable numbers | `0xfffffffa` (-6) |
| `CALL_RETURN_ILNUM` | Special variable number identifying a call-return-value entry | `0xfffffffb` (-5) |
| `VLT_REG` | Variable is in a register | `0` |
| `VLT_REG_BYREF` | Address of the variable is in a register | `1` |
| `VLT_REG_FP` | Variable is in an FP register | `2` |
| `VLT_STK` | Variable is on the stack | `3` |
| `VLT_STK_BYREF` | Address of the variable is on the stack | `4` |
| `VLT_REG_REG` | Variable lives in two registers | `5` |
| `VLT_REG_STK` | Variable lives partly in a register and partly on the stack | `6` |
| `VLT_STK_REG` | Reverse of `VLT_REG_STK` | `7` |
| `VLT_STK2` | Variable lives in two stack slots | `8` |
| `VLT_FPSTK` | Variable is on the floating-point stack | `9` |
| `VLT_FIXED_VA` | Fixed argument in a varargs function | `10` |
| `VLT_COUNT` | Number of valid `VarLocType` values | `11` |
| `VLT_INVALID` | Sentinel for invalid locations | `12` |

### DebugInfo Stream Encoding

Expand DownExpand Up@@ -95,195 +107,6 @@ Examples:

Based on the encoding specification, we use a decoder defined originally for r2r dump `NibbleReader.cs`

### Bounds Data Encoding (R2R Major Version 16+)

For R2R major version 16 and above, the bounds data uses a bit-packed encoding algorithm:

1. The bounds entry count, bits needed for native deltas, and bits needed for IL offsets are encoded using the nibble scheme above
2. Each bounds entry is then bit-packed with:
- 2 bits for source type (SourceTypeInvalid=0, CallInstruction=1, StackEmpty=2, StackEmpty|CallInstruction=3)
- Variable bits for native offset delta (accumulated from previous offset)
- Variable bits for IL offset (with IL_OFFSET_BIAS applied)

The bit-packed data is read byte by byte, collecting bits until enough are available for each entry.

### Implementation

``` csharp
bool IDebugInfo.HasDebugInfo(TargetCodePointer pCode)
Comment thread
noahfalk marked this conversation as resolved.
{
if (_eman.GetCodeBlockHandle(pCode) is not CodeBlockHandle cbh)
return false;

return _eman.GetDebugInfo(cbh, out _) != TargetPointer.Null;
}

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

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

// No debug info exists (e.g. ILStubs). Return empty sequence.
// Callers that need to distinguish this case should use HasDebugInfo first.
if (debugInfo == TargetPointer.Null)
return [];

return RestoreBoundaries(debugInfo, hasFlagByte, preferUninstrumented);
}

private IEnumerable<OffsetMapping> RestoreBoundaries(TargetPointer debugInfo, bool hasFlagByte, bool preferUninstrumented)
{
if (hasFlagByte)
{
// Check flag byte and skip over any patchpoint info
byte flagByte = _target.Read<byte>(debugInfo++);

if ((flagByte & EXTRA_DEBUG_INFO_PATCHPOINT) != 0)
{
uint localCount = _target.Read<uint>(debugInfo + /*PatchpointInfo::LocalCount offset*/)
debugInfo += /*size of PatchpointInfo*/ + (localCount * 4);
}

if ((flagByte & EXTRA_DEBUG_INFO_RICH) != 0)
{
uint richDebugInfoSize = _target.Read<uint>(debugInfo);
debugInfo += 4;
debugInfo += richDebugInfoSize;
}
}

NativeReader nibbleNativeReader = new(new TargetStream(_target, debugInfo, 24 /*maximum size of 4 32bit ints compressed*/), _target.IsLittleEndian);
NibbleReader nibbleReader = new(nibbleNativeReader, 0);

uint cbBounds = nibbleReader.ReadUInt();
uint cbUninstrumentedBounds = 0;
if (cbBounds == DEBUG_INFO_BOUNDS_HAS_INSTRUMENTED_BOUNDS)
{
// This means we have instrumented bounds.
cbBounds = nibbleReader.ReadUInt();
cbUninstrumentedBounds = nibbleReader.ReadUInt();
}
uint _ /*cbVars*/ = nibbleReader.ReadUInt();

TargetPointer addrBounds = debugInfo + (uint)nibbleReader.GetNextByteOffset();
// TargetPointer addrVars = addrBounds + cbBounds + cbUninstrumentedBounds;

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

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

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; // 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 >>= (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++;
}
}
}
```

## 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.

<!-- BEGIN GENERATED: usage contract=DebugInfo version=c2 diff-from=c1 -->
### Data descriptor changes from `c1`

| Change | Data Descriptor | Field | Type | Meaning |
| --- | --- | --- | --- | --- |
| Removed | `PatchpointInfo` | *(type size)* | `uint32` | Size in bytes of the fixed patchpoint header before its variable local-offset data |
| Removed | `PatchpointInfo` | `LocalCount` | `uint32` | Number of locals in the method associated with the patchpoint. |

### Global variable changes from `c1`

_No changes._

### Contract dependency changes from `c1`

| Change | Contract Name |
| --- | --- |
| Added | `CodeVersions` |
| Added | `RuntimeInfo` |
<!-- END GENERATED: usage contract=DebugInfo version=c2 diff-from=c1 -->


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`):
Expand All@@ -309,34 +132,33 @@ AsyncInfoStart = RichDebugInfoStart + RichDebugInfoSize
DebugInfoEnd = AsyncInfoStart + AsyncInfoSize
```

### Bounds Entry Encoding Differences from Version 1
### Bounds Entry Encoding

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]`.
Each bounds entry uses three independent flag bits for source type:
`[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) |
| 2 | 0x4 | `Async` |

`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:
Pseudo-code for 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
if ((encoded & 0x4) != 0) sourceType |= SourceTypes.Async;
```

After masking the 3 bits, shift them out before reading native delta and IL offset fields as before.

### Variable Location APIs (Version 2+)
### Variable Location APIs

Version 2 adds support for decoding native variable location info from the Vars section of the debug info blob.
The contract decodes native variable location information from the Vars section of the debug info blob.

Additional APIs:
```csharp
Expand DownExpand Up@@ -376,25 +198,6 @@ public readonly struct DebugVarInfo
IEnumerable<DebugVarInfo> GetMethodVarInfo(TargetCodePointer pCode, out uint codeOffset);
```

Additional constants (Version 2):
| Constant Name | Meaning | Value |
| --- | --- | --- |
| `MAX_ILNUM` | Bias for adjusted encoding of variable numbers | `0xfffffffa` (-6) |
| `CALL_RETURN_ILNUM` | Special variable number identifying a call-return-value entry | `0xfffffffb` (-5) |
| `VLT_REG` | Variable is in a register | `0` |
| `VLT_REG_BYREF` | Address of the variable is in a register | `1` |
| `VLT_REG_FP` | Variable is in an FP register | `2` |
| `VLT_STK` | Variable is on the stack | `3` |
| `VLT_STK_BYREF` | Address of the variable is on the stack | `4` |
| `VLT_REG_REG` | Variable lives in two registers | `5` |
| `VLT_REG_STK` | Variable lives partly in a register and partly on the stack | `6` |
| `VLT_STK_REG` | Reverse of VLT_REG_STK | `7` |
| `VLT_STK2` | Variable lives in two stack slots | `8` |
| `VLT_FPSTK` | Variable is on the floating-point stack | `9` |
| `VLT_FIXED_VA` | Fixed argument in a varargs function | `10` |
| `VLT_COUNT` | Number of valid VarLocType values | `11` |
| `VLT_INVALID` | Sentinel for invalid locations | `12` |

### Vars Data Encoding

Each variable entry in the Vars section is nibble-encoded as follows:
Expand All@@ -420,7 +223,7 @@ Each variable entry in the Vars section is nibble-encoded as follows:

Signed integers are encoded using the same unsigned scheme, with the sign bit stored in bit 0 (`value = unsigned >> 1`, negate if `unsigned & 1`). On x86, stack offsets are DWORD-aligned and stored divided by `sizeof(DWORD)`.

### Async Suspension Point APIs (Version 2+)
### Async Suspension Point APIs

We also support decoding async suspension points (and their captured continuation-object locals) from the `AsyncInfo` chunk of the debug info blob. The chunk is present only for methods that the JIT compiled with runtime-async suspension points; for all other methods, `AsyncInfoSize` is `0` in the FAT header and the API returns an empty list.

Expand Down
Loading
Loading