Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions docs/design/datacontracts/ExecutionManager.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ struct CodeBlockHandle
// Get the exception clause info for the code block
List<ExceptionClauseInfo> GetExceptionClauses(CodeBlockHandle codeInfoHandle);

// Classify a code address as a known stub kind (precode, jump stub, VSD stub, etc.)
// or as managed code. Returns CodeBlockUnknown if the address is not recognized.
StubKind GetStubKind(TargetCodePointer jittedCodeAddress);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm worried that this classification would make the interface easily broken if we added/removed/merged/split/redefined stubs in the future. How about instead of returning an explicit enumeration value we return a string with a looser guarantee? Something like this:

// If the code refers to a dynamically generated runtime stub, return a non-null name
// describing what kind of stub it is. The exact kinds of stubs and their functionality
// might shift across different runtime versions.
string? GetStubSymbol(CodeBlockHandle codeInfoHandle)

Hopefully this together with GetRelativeOffset() could implement the GetRuntimeNameByAddress API. I am using a CodeBlockHandle here thinking we might benefit from caching some CodeBlock data about stubs too (RangeSection, StartAddress, CodeBlockStubKind) even though it currently appears we don't. Even if we continue to not cache anything it still makes the API a little more consistent.


// Extension Methods (implemented in terms of other APIs)
// Returns true if the code block is a funclet (exception handler, filter, or finally)
bool IsFunclet(CodeBlockHandle codeInfoHandle);
Expand Down Expand Up @@ -143,6 +147,8 @@ Data descriptors used:
| `RangeSection` | `Flags` | Flags for the range section |
| `RangeSection` | `HeapList` | Pointer to the heap list |
| `RangeSection` | `R2RModule` | ReadyToRun module |
| `RangeSection` | `RangeList` | Pointer to the `CodeRangeMapRangeList` associated with this range section |
| `CodeRangeMapRangeList` | `RangeListType` | Integer identifying the stub code block kind for this range list |
| `CodeHeapListNode` | `Next` | Next node |
| `CodeHeapListNode` | `StartAddress` | Start address of the used portion of the code heap |
| `CodeHeapListNode` | `EndAddress` | End address of the used portion of the code heap |
Expand Down Expand Up @@ -219,6 +225,11 @@ Global variables used:
| `GCInfoVersion` | uint32 | JITted code GCInfo version |
| `FeatureOnStackReplacement` | uint8 | 1 if FEATURE_ON_STACK_REPLACEMENT is enabled, 0 otherwise |
| `FeaturePortableEntrypoints` | uint8 | 1 if FEATURE_PORTABLE_ENTRYPOINTS is enabled, 0 otherwise |
| `ThePreStub` | TargetPointer | Address of the pre-stub entry point (only present when `FeaturePortableEntrypoints` is disabled) |
| `GenericPInvokeCalliHelper` | TargetPointer | Address of the generic PInvoke CALLI helper stub |
| `VarargPInvokeStub` | TargetPointer | Address of the vararg PInvoke stub |
| `VarargPInvokeStub_RetBuffArg` | TargetPointer | Address of the vararg PInvoke stub with return buffer argument (not present on x86, ARM64, LoongArch64, RISC-V) |
| `TailCallJitHelper` | TargetPointer | Address of the JIT tail call helper (only present on x86 Windows) |
| `ObjectMethodTable` | TargetPointer | Pointer to the `System.Object` MethodTable, used for catch-all handler detection |

Contract constants used:
Expand Down Expand Up @@ -501,6 +512,36 @@ After obtaining the clause array bounds, the common iteration logic classifies e

`IsFilterFunclet` first checks `IsFunclet`. If the code block is a funclet, it retrieves the EH clauses for the method and checks whether any filter clause's handler offset matches the funclet's relative offset. If a match is found, the funclet is a filter funclet.

### Stub Kind Classification

`GetStubKind` classifies a code address as a known stub type or managed code. It first checks the address against well-known global stub pointers (`ThePreStub`, `VarargPInvokeStub`, `VarargPInvokeStub_RetBuffArg`, `GenericPInvokeCalliHelper`, `TailCallJitHelper`). If the address matches one of these, it returns the corresponding `StubKind` immediately.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Are we doing this classification just to match the name returned by the legacy DAC?

I think we should depend on coreclr.dll symbols for the names of these assembly helpers (similar to what we have done for JIT helpers).

Copy link
Copy Markdown
Contributor Author

@rcj1 rcj1 Apr 20, 2026

Choose a reason for hiding this comment

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

Yes, we are matching the DAC. The DAC uses FindStubManager, which does find these helpers. We can modify the DAC to exclude those, and rely on the debugger to fall back to coreclr symbols.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I assume that this is only about symbols displayed by disassembly windbg. We do not need to match 1:1 what windbg displays in disassembly. We just need to make sure that it displays it is reasonable.

If we do nothing (ie stop recognizing these addresses in GetRuntimeNameByAddress), what is going to break?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

BTW: windbg disassembly experience that involves this API is very poor today. When I run !u on a method that just calls Console.ReadLine, I see this:

00007ff9`c0cb4780 55              push    rbp
00007ff9`c0cb4781 4883ec20        sub     rsp,20h
00007ff9`c0cb4785 488d6c2420      lea     rbp,[rsp+20h]
00007ff9`c0cb478a 48894d10        mov     qword ptr [rbp+10h],rcx
>>> 00007ff9`c0cb478e ff155c051d00    call    qword ptr [CLRStub[MethodDescPrestub]@00007FF9C0E84CF0 (00007ff9`c0e84cf0)]
00007ff9`c0cb4794 90              nop
00007ff9`c0cb4795 4883c420        add     rsp,20h
00007ff9`c0cb4799 5d              pop     rbp
00007ff9`c0cb479a c3              ret

I assume that MethodDescPrestub in the disassembly comes from this API. It is close to useless information. Raw address with no symbols name would be about as useful.

I would like to see something like call qword ptr [System_Console!System.Console.ReadLine (00007ff9c0e84cf0)]` in the disassembly, similar to what it was the case in .NET Framework. The contract of these DAC APIs is poorly defined and windbg makes a lot of assumptions about what they do. These details changed as the runtime evolved and ended up breaking the experience. Instead of trying to match 1:1 what these APIs do currently, we should focus on what they need to do to produce human friendly disassembly in windbg in common cases.


If no global match is found, the method looks up the address in the `RangeSectionMap`. If a `RangeSection` is found, the JIT manager for that section classifies the code:

- **EEJitManager**: If the range section is a range list, reads the `CodeRangeMapRangeList.RangeListType` to determine the stub code block kind. Otherwise, it uses the nibble map to find the method code start, reads the code header indirect pointer, and checks whether it is a stub code block (value ≤ `StubCodeBlockLast`). If so, the value identifies the specific stub kind.
- **ReadyToRunJitManager**: Checks whether the address falls within a delay-load method call thunk region.

```csharp
StubKind GetStubKind(TargetCodePointer jittedCodeAddress)
{
TargetPointer address = CodePointerUtils.AddressFromCodePointer(jittedCodeAddress);

// Check well-known global stubs
if (address == ThePreStub) return StubKind.PreStub;
if (address == VarargPInvokeStub || address == VarargPInvokeStub_RetBuffArg
|| address == GenericPInvokeCalliHelper)
return StubKind.InteropDispatchStub;
if (address == TailCallJitHelper) return StubKind.TailCallStub;

// Look up in range section map
RangeSection range = FindRangeSection(jittedCodeAddress);
if (range == null) return StubKind.CodeBlockUnknown;

JitManager jitManager = GetJitManager(range);
return jitManager.GetStubCodeBlockKind(range, jittedCodeAddress);
}
```

### EE JIT Manager and Code Heap Info

```csharp
Expand Down
18 changes: 18 additions & 0 deletions docs/design/datacontracts/PrecodeStubs.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ This contract provides support for examining [precode](../coreclr/botr/method-de
```csharp
// Gets a pointer to the MethodDesc for a given stub entrypoint
TargetPointer GetMethodDescFromStubAddress(TargetCodePointer entryPoint);

// Enumerates candidate precode entry points near a given code address.
// This is used to resolve a code address that falls within a
// precode stub back to its entry point.
IEnumerable<TargetCodePointer> GetCandidateEntryPoints(TargetCodePointer address);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We should be able to find the precode start address using some address math rather than having to do guess-and-check repeatedly. I think the algorithm would look something like this:

TADDR GetStubStartFromInterior(RangeSection* pRS, TADDR interiorPrecodeAddress)
 {
     if (pRS == nullptr || !(pRS->_flags & RangeSection::RANGE_SECTION_RANGELIST))
         return 0;
     size_t stubSize = 0;
     switch (pRS->_pRangeList->GetCodeBlockKind())
     {
     case STUB_CODE_BLOCK_FIXUPPRECODE:
         stubSize = FixupPrecode::CodeSize;
         break;
     case STUB_CODE_BLOCK_STUBPRECODE:
         stubSize = StubPrecode::CodeSize;
         break;
     default:
         return 0;
     }
     const size_t pageMask = GetStubCodePageSize() - 1;
     const TADDR  pageBase = interiorPrecodeAddress & ~static_cast<TADDR>(pageMask);
     const size_t offset   = static_cast<size_t>(interiorPrecodeAddress - pageBase);
     return pageBase + (offset / stubSize) * stubSize;
 }

```

## Version 1, 2, and 3
Expand Down Expand Up @@ -295,4 +300,17 @@ After the initial precode type is determined, for stub precodes a refined precod

return precode.GetMethodDesc(_target, MachineDescriptor);
}

IEnumerable<TargetCodePointer> IPrecodeStubs.GetCandidateEntryPoints(TargetCodePointer address)
{
TargetPointer instrPointer = CodePointerReadableInstrPointer(address);
ulong aligned = instrPointer.Value & ~(ulong)(PointerSize - 1);
uint count = StubPrecodeSize / PointerSize;

for (uint i = 0; i < count; i++)
{
TargetPointer candidateAddr = new TargetPointer(aligned - (i * PointerSize));
yield return CodePointerFromAddress(candidateAddr);
}
}
```
2 changes: 1 addition & 1 deletion src/coreclr/debug/daccess/daccess.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5327,7 +5327,7 @@ static int FormatCLRStubName(

// Compute the address as a string safely.
WCHAR addrString[Max64BitHexString + 1];
FormatInteger(addrString, ARRAY_SIZE(addrString), "%p", stubAddr);
FormatInteger(addrString, ARRAY_SIZE(addrString), sizeof(void*) == 8 ? "%016llX" : "%08X", stubAddr);
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

FormatInteger is passed stubAddr (a TADDR/uintptr_t) with the format string %016llX on 64-bit. On non-Windows 64-bit platforms, uintptr_t is typically unsigned long, which does not match %llX and can cause undefined behavior in sprintf_s. Use a format specifier that matches uintptr_t (e.g., PRIxPTR) or cast stubAddr to an explicit unsigned long long (and similarly to unsigned int for the 32-bit branch) so the varargs type always matches the format string.

Suggested change
FormatInteger(addrString, ARRAY_SIZE(addrString), sizeof(void*) == 8 ? "%016llX" : "%08X", stubAddr);
#ifdef HOST_64BIT
FormatInteger(addrString, ARRAY_SIZE(addrString), "%016llX", static_cast<unsigned long long>(stubAddr));
#else
FormatInteger(addrString, ARRAY_SIZE(addrString), "%08X", static_cast<unsigned int>(stubAddr));
#endif

Copilot uses AI. Check for mistakes.
size_t addStringLen = u16_strlen(addrString);

// Compute maximum length, include the null terminator.
Expand Down
8 changes: 8 additions & 0 deletions src/coreclr/inc/gfunc_list.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@

DEFINE_DACGFN(DACNotifyCompilationFinished)
DEFINE_DACGFN(ThePreStub)
DEFINE_DACGFN(GenericPInvokeCalliHelper)
DEFINE_DACGFN(VarargPInvokeStub)
#if !defined(TARGET_X86) && !defined(TARGET_ARM64) && !defined(TARGET_LOONGARCH64) && !defined(TARGET_RISCV64)
DEFINE_DACGFN(VarargPInvokeStub_RetBuffArg)
#endif
#if defined(TARGET_X86) && !defined(UNIX_X86_ABI)
DEFINE_DACGFN(JIT_TailCall)
#endif

DEFINE_DACGFN(ThePreStubPatchLabel)
#ifdef FEATURE_COMINTEROP
Expand Down
7 changes: 7 additions & 0 deletions src/coreclr/vm/cgensys.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ extern "C" void STDCALL GenericPInvokeCalliHelper(void);

extern "C" PCODE STDCALL ExternalMethodFixupWorker(TransitionBlock * pTransitionBlock, TADDR pIndirection, DWORD sectionIndex, Module * pModule);

// cDAC-visible TADDR wrappers for function entrypoints.
inline const TADDR g_cdacGenericPInvokeCalliHelper = (TADDR)(void*)GenericPInvokeCalliHelper;
inline const TADDR g_cdacVarargPInvokeStub = (TADDR)(void*)VarargPInvokeStub;
#if !defined(TARGET_X86) && !defined(TARGET_ARM64) && !defined(TARGET_LOONGARCH64) && !defined(TARGET_RISCV64)
inline const TADDR g_cdacVarargPInvokeStub_RetBuffArg = (TADDR)(void*)VarargPInvokeStub_RetBuffArg;
#endif

extern "C" void STDCALL VirtualMethodFixupStub(void);
extern "C" void STDCALL VirtualMethodFixupPatchLabel(void);

Expand Down
3 changes: 3 additions & 0 deletions src/coreclr/vm/class.h
Original file line number Diff line number Diff line change
Expand Up @@ -1955,6 +1955,9 @@ inline BOOL EEClass::IsInt128OrHasInt128Fields()
VOID InitPreStubManager();

EXTERN_C void STDCALL ThePreStub();
#ifndef FEATURE_PORTABLE_ENTRYPOINTS
inline const TADDR g_cdacThePreStub = (TADDR)(void*)ThePreStub;
#endif // !FEATURE_PORTABLE_ENTRYPOINTS

inline PCODE GetPreStubEntryPoint()
{
Expand Down
1 change: 1 addition & 0 deletions src/coreclr/vm/codeman.h
Original file line number Diff line number Diff line change
Expand Up @@ -793,6 +793,7 @@ template<> struct cdac_data<RangeSection>
static constexpr size_t Flags = offsetof(RangeSection, _flags);
static constexpr size_t HeapList = offsetof(RangeSection, _pHeapList);
static constexpr size_t R2RModule = offsetof(RangeSection, _pR2RModule);
static constexpr size_t RangeList = offsetof(RangeSection, _pRangeList);
};

enum class RangeSectionLockState
Expand Down
17 changes: 17 additions & 0 deletions src/coreclr/vm/datadescriptor/datadescriptor.inc
Original file line number Diff line number Diff line change
Expand Up @@ -805,8 +805,14 @@ CDAC_TYPE_FIELD(RangeSection, T_POINTER, JitManager, cdac_data<RangeSection>::Ji
CDAC_TYPE_FIELD(RangeSection, T_INT32, Flags, cdac_data<RangeSection>::Flags)
CDAC_TYPE_FIELD(RangeSection, T_POINTER, HeapList, cdac_data<RangeSection>::HeapList)
CDAC_TYPE_FIELD(RangeSection, T_POINTER, R2RModule, cdac_data<RangeSection>::R2RModule)
CDAC_TYPE_FIELD(RangeSection, T_POINTER, RangeList, cdac_data<RangeSection>::RangeList)
CDAC_TYPE_END(RangeSection)

CDAC_TYPE_BEGIN(CodeRangeMapRangeList)
CDAC_TYPE_INDETERMINATE(CodeRangeMapRangeList)
CDAC_TYPE_FIELD(CodeRangeMapRangeList, T_INT32, RangeListType, cdac_data<CodeRangeMapRangeList>::RangeListType)
CDAC_TYPE_END(CodeRangeMapRangeList)

CDAC_TYPE_BEGIN(EEJitManager)
CDAC_TYPE_INDETERMINATE(EEJitManager)
CDAC_TYPE_FIELD(EEJitManager, T_BOOL, StoreRichDebugInfo, cdac_data<EEJitManager>::StoreRichDebugInfo)
Expand Down Expand Up @@ -1412,6 +1418,17 @@ CDAC_GLOBAL(ArrayBaseSize, T_UINT32, ARRAYBASE_BASESIZE)
CDAC_GLOBAL(SyncBlockValueToObjectOffset, T_UINT16, OBJHEADER_SIZE - cdac_data<ObjHeader>::SyncBlockValue)
CDAC_GLOBAL(StubCodeBlockLast, T_UINT8, STUB_CODE_BLOCK_LAST)
CDAC_GLOBAL(DefaultADID, T_UINT32, DefaultADID)
#ifndef FEATURE_PORTABLE_ENTRYPOINTS
CDAC_GLOBAL_POINTER(ThePreStub, &g_cdacThePreStub)
#endif // !FEATURE_PORTABLE_ENTRYPOINTS
CDAC_GLOBAL_POINTER(GenericPInvokeCalliHelper, &g_cdacGenericPInvokeCalliHelper)
CDAC_GLOBAL_POINTER(VarargPInvokeStub, &g_cdacVarargPInvokeStub)
#if !defined(TARGET_X86) && !defined(TARGET_ARM64) && !defined(TARGET_LOONGARCH64) && !defined(TARGET_RISCV64)
CDAC_GLOBAL_POINTER(VarargPInvokeStub_RetBuffArg, &g_cdacVarargPInvokeStub_RetBuffArg)
#endif
#if defined(TARGET_X86) && !defined(UNIX_X86_ABI)
CDAC_GLOBAL_POINTER(TailCallJitHelper, &g_cdacTailCallJitHelper)
#endif // TARGET_X86 && !UNIX_X86_ABI
#ifndef TARGET_UNIX
CDAC_GLOBAL(SizeOfGenericModeBlock, T_UINT32, sizeof(GenericModeBlock))
#endif
Expand Down
4 changes: 4 additions & 0 deletions src/coreclr/vm/jitinterface.h
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,10 @@ extern "C"
#endif // !defined(TARGET_ARM64) && !defined(TARGET_LOONGARCH64) && !defined(TARGET_RISCV64)
};

#if defined(TARGET_X86) && !defined(UNIX_X86_ABI)
inline const TADDR g_cdacTailCallJitHelper = (TADDR)(void*)JIT_TailCall;
#endif // TARGET_X86 && !UNIX_X86_ABI

/*********************************************************************/
/*********************************************************************/

Expand Down
7 changes: 7 additions & 0 deletions src/coreclr/vm/loaderallocator.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,13 @@ class CodeRangeMapRangeList : public RangeList
SArray<TADDR> _starts;
void* _id;
bool _collectible;
friend struct ::cdac_data<CodeRangeMapRangeList>;
};

template<>
struct cdac_data<CodeRangeMapRangeList>
{
static constexpr size_t RangeListType = offsetof(CodeRangeMapRangeList, _rangeListType);
};

// Iterator over Assemblies in the same ALC
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,26 @@ public interface ICodeHeapInfo
{
}

public enum StubKind : uint
{
CodeBlockUnknown = 0,
CodeBlockJumpStub = 1,
CodeBlockDynamicHelper = 3,
CodeBlockPrecode = 4,
CodeBlockVSDDispatchStub = 5,
CodeBlockVSDResolveStub = 6,
CodeBlockVSDLookupStub = 7,
CodeBlockVSDVTableStub = 8,
CodeBlockCallCounting = 9,
CodeBlockStubLinkStub = 10,
CodeBlockMethodCallThunk = 11,
CodeBlockNoCode = 12,
CodeBlockManaged = 13,
PreStub = 14,
InteropDispatchStub = 15,
TailCallStub = 16,
}

public sealed class LoaderCodeHeapInfo : ICodeHeapInfo
{
public TargetPointer HeapAddress { get; }
Expand Down Expand Up @@ -101,6 +121,7 @@ public interface IExecutionManager : IContract
List<ExceptionClauseInfo> GetExceptionClauses(CodeBlockHandle codeInfoHandle) => throw new NotImplementedException();
JitManagerInfo GetEEJitManagerInfo() => throw new NotImplementedException();
IEnumerable<ICodeHeapInfo> GetCodeHeapInfos() => throw new NotImplementedException();
StubKind GetStubKind(TargetCodePointer jittedCodeAddress) => throw new NotImplementedException();
}

public readonly struct ExecutionManager : IExecutionManager
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;

namespace Microsoft.Diagnostics.DataContractReader.Contracts;

public interface IPrecodeStubs : IContract
{
static string IContract.Name { get; } = nameof(PrecodeStubs);
TargetPointer GetMethodDescFromStubAddress(TargetCodePointer entryPoint) => throw new NotImplementedException();
IEnumerable<TargetCodePointer> GetCandidateEntryPoints(TargetCodePointer address) => throw new NotImplementedException();
}

public readonly struct PrecodeStubs : IPrecodeStubs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ public enum DataType
ComInterfaceEntry,
InternalComInterfaceDispatch,
AuxiliarySymbolInfo,
CodeRangeMapRangeList,

/* GC Data Types */

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ public static class Globals
public const string ExecutionManagerCodeRangeMapAddress = nameof(ExecutionManagerCodeRangeMapAddress);
public const string EEJitManagerAddress = nameof(EEJitManagerAddress);
public const string StubCodeBlockLast = nameof(StubCodeBlockLast);
public const string ThePreStub = nameof(ThePreStub);
public const string VarargPInvokeStub = nameof(VarargPInvokeStub);
public const string VarargPInvokeStub_RetBuffArg = nameof(VarargPInvokeStub_RetBuffArg);
public const string GenericPInvokeCalliHelper = nameof(GenericPInvokeCalliHelper);
public const string TailCallJitHelper = nameof(TailCallJitHelper);
public const string DefaultADID = nameof(DefaultADID);
public const string StaticsPointerMask = nameof(StaticsPointerMask);
public const string PtrArrayOffsetToDataArray = nameof(PtrArrayOffsetToDataArray);
Expand Down
Loading
Loading