From 02f7a3cbccc16e4e77ef61b2fcaaf2c3c128981b Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Mon, 23 Feb 2026 17:42:28 -0500 Subject: [PATCH 1/4] do not rely on method size array --- .../design/datacontracts/RuntimeTypeSystem.md | 56 +++++++++++++++---- .../vm/datadescriptor/datadescriptor.inc | 5 +- .../DataType.cs | 1 + .../Constants.cs | 2 - .../Contracts/RuntimeTypeSystem_1.cs | 55 ++++++++++++++---- .../TypeHierarchy/TypeHierarchy.csproj | 2 +- .../DumpTests/RuntimeTypeSystemDumpTests.cs | 1 - .../MockDescriptors.MethodDescriptors.cs | 6 +- 8 files changed, 95 insertions(+), 33 deletions(-) diff --git a/docs/design/datacontracts/RuntimeTypeSystem.md b/docs/design/datacontracts/RuntimeTypeSystem.md index 5d444662bb2bf4..f8b6a4a9c290a7 100644 --- a/docs/design/datacontracts/RuntimeTypeSystem.md +++ b/docs/design/datacontracts/RuntimeTypeSystem.md @@ -910,7 +910,6 @@ The version 1 `MethodDesc` APIs depend on the following globals: | --- | --- | | `MethodDescAlignment` | `MethodDescChunk` trailing data is allocated in multiples of this constant. The size (in bytes) of each `MethodDesc` (or subclass) instance is a multiple of this constant. | | `MethodDescTokenRemainderBitCount` | Number of bits in the token remainder in `MethodDesc` | -| `MethodDescSizeTable` | A pointer to the MethodDesc size table. The MethodDesc flags are used as an offset into this table to lookup the MethodDesc size. | In the runtime a `MethodDesc` implicitly belongs to a single `MethodDescChunk` and some common data is shared between method descriptors that belong to the same chunk. A single method table @@ -942,6 +941,24 @@ We depend on the following data descriptors: | `DynamicMethodDesc` | `MethodName` | Pointer to Null-terminated UTF8 string describing the Method desc | | `GCCoverageInfo` | `SavedCode` | Pointer to the GCCover saved code copy, if supported | +The following data descriptor types are used only for their sizes when computing the total size of a `MethodDesc` instance. +The size of a `MethodDesc` is the base size of its classification subtype plus the sizes of any optional trailing slots indicated by its flags: + +| Data Descriptor Name | Meaning | +| --- | --- | +| `MethodDesc` | Base size for `mcIL` classification | +| `FCallMethodDesc` | Base size for `mcFCall` classification | +| `PInvokeMethodDesc` | Base size for `mcPInvoke` classification | +| `EEImplMethodDesc` | Base size for `mcEEImpl` classification | +| `ArrayMethodDesc` | Base size for `mcArray` classification | +| `InstantiatedMethodDesc` | Base size for `mcInstantiated` classification | +| `CLRToCOMCallMethodDesc` | Base size for `mcComInterOp` classification | +| `DynamicMethodDesc` | Base size for `mcDynamic` classification | +| `NonVtableSlot` | Size of the non-vtable slot, added when `HasNonVtableSlot` flag is set | +| `MethodImpl` | Size of the MethodImpl data, added when `HasMethodImpl` flag is set | +| `NativeCodeSlot` | Size of the native code slot, added when `HasNativeCodeSlot` flag is set | +| `AsyncMethodData` | Size of the async method data, added when `HasAsyncMethodData` flag is set | + The contract depends on the following other contracts @@ -1181,17 +1198,32 @@ And the various apis are implemented with the following algorithms { MethodDesc methodDesc = _methodDescs[methodDescHandle.Address]; - // the runtime generates a table to lookup the size of a MethodDesc based on the flags - // read the location of the table and index into it using certain bits of MethodDesc.Flags - TargetPointer methodDescSizeTable = target.ReadGlobalPointer(Constants.Globals.MethodDescSizeTable); - - ushort arrayOffset = (ushort)(methodDesc.Flags & (ushort)( - MethodDescFlags.ClassificationMask | - MethodDescFlags.HasNonVtableSlot | - MethodDescFlags.HasMethodImpl | - MethodDescFlags.HasNativeCodeSlot | - MethodDescFlags.HasAsyncMethodData)); - return target.Read(methodDescSizeTable + arrayOffset); + // Compute the size from data descriptor type sizes. + // The base size comes from the classification subtype, and optional slot + // sizes are added based on the MethodDesc flags. + MethodClassification classification = (MethodClassification)(methodDesc.Flags & MethodDescFlags.ClassificationMask); + uint size = classification switch + { + MethodClassification.IL => target.GetTypeInfo(DataType.MethodDesc).Size, + MethodClassification.FCall => target.GetTypeInfo(DataType.FCallMethodDesc).Size, + MethodClassification.PInvoke => target.GetTypeInfo(DataType.PInvokeMethodDesc).Size, + MethodClassification.EEImpl => target.GetTypeInfo(DataType.EEImplMethodDesc).Size, + MethodClassification.Array => target.GetTypeInfo(DataType.ArrayMethodDesc).Size, + MethodClassification.Instantiated => target.GetTypeInfo(DataType.InstantiatedMethodDesc).Size, + MethodClassification.ComInterop => target.GetTypeInfo(DataType.CLRToCOMCallMethodDesc).Size, + MethodClassification.Dynamic => target.GetTypeInfo(DataType.DynamicMethodDesc).Size, + }; + + if (HasFlag(methodDesc, MethodDescFlags.HasNonVtableSlot)) + size += target.GetTypeInfo(DataType.NonVtableSlot).Size; + if (HasFlag(methodDesc, MethodDescFlags.HasMethodImpl)) + size += target.GetTypeInfo(DataType.MethodImpl).Size; + if (HasFlag(methodDesc, MethodDescFlags.HasNativeCodeSlot)) + size += target.GetTypeInfo(DataType.NativeCodeSlot).Size; + if (HasFlag(methodDesc, MethodDescFlags.HasAsyncMethodData)) + size += target.GetTypeInfo(DataType.AsyncMethodData).Size; + + return size; } public bool IsArrayMethod(MethodDescHandle methodDescHandle, out ArrayFunctionType functionType) diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index cc50f5dde51f01..4799a56f287703 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -464,6 +464,10 @@ CDAC_TYPE_BEGIN(NativeCodeSlot) CDAC_TYPE_SIZE(sizeof(MethodDesc::NativeCodeSlot)) CDAC_TYPE_END(NativeCodeSlot) +CDAC_TYPE_BEGIN(AsyncMethodData) +CDAC_TYPE_SIZE(sizeof(AsyncMethodData)) +CDAC_TYPE_END(AsyncMethodData) + CDAC_TYPE_BEGIN(InstantiatedMethodDesc) CDAC_TYPE_SIZE(sizeof(InstantiatedMethodDesc)) CDAC_TYPE_FIELD(InstantiatedMethodDesc, /*pointer*/, PerInstInfo, cdac_data::PerInstInfo) @@ -1151,7 +1155,6 @@ CDAC_GLOBAL(StressLogEnabled, uint8, 0) CDAC_GLOBAL_POINTER(ExecutionManagerCodeRangeMapAddress, cdac_data::CodeRangeMapAddress) CDAC_GLOBAL_POINTER(PlatformMetadata, &::g_cdacPlatformMetadata) CDAC_GLOBAL_POINTER(ProfilerControlBlock, &::g_profControlBlock) -CDAC_GLOBAL_POINTER(MethodDescSizeTable, &MethodDesc::s_ClassificationSizeTable) CDAC_GLOBAL_POINTER(GCLowestAddress, &g_lowest_address) CDAC_GLOBAL_POINTER(GCHighestAddress, &g_highest_address) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs index 6be64a591afb9d..feca2918689051 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs @@ -113,6 +113,7 @@ public enum DataType NonVtableSlot, MethodImpl, NativeCodeSlot, + AsyncMethodData, GCCoverageInfo, ArrayListBase, ArrayListBlock, diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs index 427ef634e7726a..68a4ac621aca3a 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs @@ -71,8 +71,6 @@ public static class Globals public const string PlatformMetadata = nameof(PlatformMetadata); public const string ProfilerControlBlock = nameof(ProfilerControlBlock); - public const string MethodDescSizeTable = nameof(MethodDescSizeTable); - public const string HashMapSlotsPerBucket = nameof(HashMapSlotsPerBucket); public const string HashMapValueMask = nameof(HashMapValueMask); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs index fc8e5628905669..27e24870c7a60f 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs @@ -226,17 +226,50 @@ private static uint ComputeToken(Target target, Data.MethodDesc desc, Data.Metho private static uint ComputeSize(Target target, Data.MethodDesc desc) { - // Size of the MethodDesc is variable, read it from the targets lookup table - // See MethodDesc::SizeOf in method.cpp for details - TargetPointer methodDescSizeTable = target.ReadGlobalPointer(Constants.Globals.MethodDescSizeTable); - - ushort arrayOffset = (ushort)(desc.Flags & (ushort)( - MethodDescFlags_1.MethodDescFlags.ClassificationMask | - MethodDescFlags_1.MethodDescFlags.HasNonVtableSlot | - MethodDescFlags_1.MethodDescFlags.HasMethodImpl | - MethodDescFlags_1.MethodDescFlags.HasNativeCodeSlot | - MethodDescFlags_1.MethodDescFlags.HasAsyncMethodData)); - return target.Read(methodDescSizeTable + arrayOffset); + // See s_ClassificationSizeTable in method.cpp in the runtime for how the size is determined based on the method classification and flags. + uint baseSize; + switch ((MethodClassification)(desc.Flags & (ushort)MethodDescFlags_1.MethodDescFlags.ClassificationMask)) + { + case MethodClassification.IL: + case MethodClassification.ComInterop: + baseSize = target.GetTypeInfo(DataType.MethodDesc).Size ?? throw new InvalidOperationException("MethodDesc type size must be known"); + break; + case MethodClassification.FCall: + baseSize = target.GetTypeInfo(DataType.FCallMethodDesc).Size ?? throw new InvalidOperationException("FCallMethodDesc type size must be known"); + break; + case MethodClassification.PInvoke: + baseSize = target.GetTypeInfo(DataType.PInvokeMethodDesc).Size ?? throw new InvalidOperationException("PInvokeMethodDesc type size must be known"); + break; + case MethodClassification.EEImpl: + baseSize = target.GetTypeInfo(DataType.EEImplMethodDesc).Size ?? throw new InvalidOperationException("EEImplMethodDesc type size must be known"); + break; + case MethodClassification.Array: + baseSize = target.GetTypeInfo(DataType.ArrayMethodDesc).Size ?? throw new InvalidOperationException("ArrayMethodDesc type size must be known"); + break; + case MethodClassification.Instantiated: + baseSize = target.GetTypeInfo(DataType.InstantiatedMethodDesc).Size ?? throw new InvalidOperationException("InstantiatedMethodDesc type size must be known"); + break; + case MethodClassification.Dynamic: + baseSize = target.GetTypeInfo(DataType.DynamicMethodDesc).Size ?? throw new InvalidOperationException("DynamicMethodDesc type size must be known"); + break; + default: + throw new InvalidOperationException("Invalid method classification"); + } + + MethodDescFlags_1.MethodDescFlags flags = (MethodDescFlags_1.MethodDescFlags)desc.Flags; + if (flags.HasFlag(MethodDescFlags_1.MethodDescFlags.HasNonVtableSlot)) + baseSize += (uint)target.PointerSize; + + if (flags.HasFlag(MethodDescFlags_1.MethodDescFlags.HasMethodImpl)) + baseSize += target.GetTypeInfo(DataType.MethodImpl).Size ?? throw new InvalidOperationException("MethodImpl type size must be known"); + + if (flags.HasFlag(MethodDescFlags_1.MethodDescFlags.HasNativeCodeSlot)) + baseSize += (uint)target.PointerSize; + + if (flags.HasFlag(MethodDescFlags_1.MethodDescFlags.HasAsyncMethodData)) + baseSize += target.GetTypeInfo(DataType.AsyncMethodData).Size ?? throw new InvalidOperationException("AsyncMethodDescData type size must be known"); + + return baseSize; } public MethodClassification Classification => (MethodClassification)((int)_desc.Flags & (int)MethodDescFlags_1.MethodDescFlags.ClassificationMask); diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/TypeHierarchy/TypeHierarchy.csproj b/src/native/managed/cdac/tests/DumpTests/Debuggees/TypeHierarchy/TypeHierarchy.csproj index bb776824769fe6..75ae87c9e7195f 100644 --- a/src/native/managed/cdac/tests/DumpTests/Debuggees/TypeHierarchy/TypeHierarchy.csproj +++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/TypeHierarchy/TypeHierarchy.csproj @@ -1,5 +1,5 @@ - Full + Heap diff --git a/src/native/managed/cdac/tests/DumpTests/RuntimeTypeSystemDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/RuntimeTypeSystemDumpTests.cs index b1914e48f3a6f7..dc71afdb62002d 100644 --- a/src/native/managed/cdac/tests/DumpTests/RuntimeTypeSystemDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/RuntimeTypeSystemDumpTests.cs @@ -17,7 +17,6 @@ namespace Microsoft.Diagnostics.DataContractReader.DumpTests; public class RuntimeTypeSystemDumpTests : DumpTestBase { protected override string DebuggeeName => "TypeHierarchy"; - protected override string DumpType => "full"; [ConditionalTheory] [MemberData(nameof(TestConfigurations))] diff --git a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.MethodDescriptors.cs b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.MethodDescriptors.cs index ea4e7ebd2e96b9..06ec85b8852758 100644 --- a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.MethodDescriptors.cs +++ b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.MethodDescriptors.cs @@ -101,14 +101,9 @@ internal MethodDescriptors(RuntimeTypeSystem rtsBuilder, Loader loaderBuilder, ( _allocator = Builder.CreateAllocator(allocationRange.Start, allocationRange.End); Types = GetTypes(); - // Add dummy MethodDescSizeTable. Sizes will be incorrect, but we don't use it in tests. - MockMemorySpace.HeapFragment methodDescSizeTable = _allocator.Allocate(0x100, "MethodDescSizeTable"); - Builder.AddHeapFragment(methodDescSizeTable); - Globals = rtsBuilder.Globals.Concat( [ new(nameof(Constants.Globals.MethodDescTokenRemainderBitCount), TokenRemainderBitCount), - new(nameof(Constants.Globals.MethodDescSizeTable), methodDescSizeTable.Address), ]).ToArray(); } @@ -126,6 +121,7 @@ internal MethodDescriptors(RuntimeTypeSystem rtsBuilder, Loader loaderBuilder, ( types[DataType.NonVtableSlot] = new Target.TypeInfo() { Size = (uint)TargetTestHelpers.PointerSize }; types[DataType.MethodImpl] = new Target.TypeInfo() { Size = (uint)TargetTestHelpers.PointerSize * 2 }; types[DataType.NativeCodeSlot] = new Target.TypeInfo() { Size = (uint)TargetTestHelpers.PointerSize }; + types[DataType.AsyncMethodData] = new Target.TypeInfo() { Size = (uint)TargetTestHelpers.PointerSize * 2 }; types[DataType.ArrayMethodDesc] = new Target.TypeInfo() { Size = types[DataType.StoredSigMethodDesc].Size.Value }; types[DataType.FCallMethodDesc] = new Target.TypeInfo() { Size = types[DataType.MethodDesc].Size.Value + (uint)TargetTestHelpers.PointerSize }; types[DataType.PInvokeMethodDesc] = new Target.TypeInfo() { Size = types[DataType.MethodDesc].Size.Value + (uint)TargetTestHelpers.PointerSize }; From ef3e018e7b0fbe30a428c210c7928069d175566b Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:17:42 -0500 Subject: [PATCH 2/4] update readme --- src/native/managed/cdac/tests/DumpTests/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/native/managed/cdac/tests/DumpTests/README.md b/src/native/managed/cdac/tests/DumpTests/README.md index d1dfeb3093a5cb..812c8ce4b8342c 100644 --- a/src/native/managed/cdac/tests/DumpTests/README.md +++ b/src/native/managed/cdac/tests/DumpTests/README.md @@ -31,7 +31,7 @@ features and then calls `Environment.FailFast()` to produce a crash dump. | ServerGC | Server GC mode heap structures | Heap | | StackWalk | Deterministic call stack (Main→A→B→C→FailFast) | Full | | MultiModule | Multi-assembly metadata resolution | Full | -| TypeHierarchy | Type inheritance, method tables | Full | +| TypeHierarchy | Type inheritance, method tables | Heap | The dump type is configured per-debuggee via the `DumpTypes` property in each debuggee's `.csproj` (default: `Heap`, set in `Debuggees/Directory.Build.props`). Debuggees that From 7eb024c90bb71608bb4ac2c3f86fa4b3ce3f1d1d Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:19:55 -0500 Subject: [PATCH 3/4] Heap dump is default type --- .../tests/DumpTests/Debuggees/TypeHierarchy/TypeHierarchy.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/TypeHierarchy/TypeHierarchy.csproj b/src/native/managed/cdac/tests/DumpTests/Debuggees/TypeHierarchy/TypeHierarchy.csproj index 75ae87c9e7195f..c0d42d7f25cde5 100644 --- a/src/native/managed/cdac/tests/DumpTests/Debuggees/TypeHierarchy/TypeHierarchy.csproj +++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/TypeHierarchy/TypeHierarchy.csproj @@ -1,5 +1,4 @@ - Heap From b12b46b17814901359497611bc77e0669235a604 Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:39:51 -0500 Subject: [PATCH 4/4] Address Copilot review feedback for MethodDesc size computation - Use DataType.CLRToCOMCallMethodDesc for ComInterop classification - Use DataType.NonVtableSlot instead of target.PointerSize - Use DataType.NativeCodeSlot instead of target.PointerSize - Fix error message typo: AsyncMethodDescData -> AsyncMethodData Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Contracts/RuntimeTypeSystem_1.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs index 27e24870c7a60f..da10f7f535cac0 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs @@ -231,7 +231,6 @@ private static uint ComputeSize(Target target, Data.MethodDesc desc) switch ((MethodClassification)(desc.Flags & (ushort)MethodDescFlags_1.MethodDescFlags.ClassificationMask)) { case MethodClassification.IL: - case MethodClassification.ComInterop: baseSize = target.GetTypeInfo(DataType.MethodDesc).Size ?? throw new InvalidOperationException("MethodDesc type size must be known"); break; case MethodClassification.FCall: @@ -249,6 +248,9 @@ private static uint ComputeSize(Target target, Data.MethodDesc desc) case MethodClassification.Instantiated: baseSize = target.GetTypeInfo(DataType.InstantiatedMethodDesc).Size ?? throw new InvalidOperationException("InstantiatedMethodDesc type size must be known"); break; + case MethodClassification.ComInterop: + baseSize = target.GetTypeInfo(DataType.CLRToCOMCallMethodDesc).Size ?? throw new InvalidOperationException("CLRToCOMCallMethodDesc type size must be known"); + break; case MethodClassification.Dynamic: baseSize = target.GetTypeInfo(DataType.DynamicMethodDesc).Size ?? throw new InvalidOperationException("DynamicMethodDesc type size must be known"); break; @@ -258,16 +260,16 @@ private static uint ComputeSize(Target target, Data.MethodDesc desc) MethodDescFlags_1.MethodDescFlags flags = (MethodDescFlags_1.MethodDescFlags)desc.Flags; if (flags.HasFlag(MethodDescFlags_1.MethodDescFlags.HasNonVtableSlot)) - baseSize += (uint)target.PointerSize; + baseSize += target.GetTypeInfo(DataType.NonVtableSlot).Size ?? throw new InvalidOperationException("NonVtableSlot type size must be known"); if (flags.HasFlag(MethodDescFlags_1.MethodDescFlags.HasMethodImpl)) baseSize += target.GetTypeInfo(DataType.MethodImpl).Size ?? throw new InvalidOperationException("MethodImpl type size must be known"); if (flags.HasFlag(MethodDescFlags_1.MethodDescFlags.HasNativeCodeSlot)) - baseSize += (uint)target.PointerSize; + baseSize += target.GetTypeInfo(DataType.NativeCodeSlot).Size ?? throw new InvalidOperationException("NativeCodeSlot type size must be known"); if (flags.HasFlag(MethodDescFlags_1.MethodDescFlags.HasAsyncMethodData)) - baseSize += target.GetTypeInfo(DataType.AsyncMethodData).Size ?? throw new InvalidOperationException("AsyncMethodDescData type size must be known"); + baseSize += target.GetTypeInfo(DataType.AsyncMethodData).Size ?? throw new InvalidOperationException("AsyncMethodData type size must be known"); return baseSize; }