From 36201f087eccdb0b59acbdde71da2c98afe99617 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Apr 2026 08:51:09 +0000 Subject: [PATCH 1/4] Add GetGCDescSeries API, OffsetOfContinuationData global, and continuation pretty-printing Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/6e988c50-2e06-4957-952d-9b624dd862ca Co-authored-by: rcj1 <77995559+rcj1@users.noreply.github.com> --- .../design/datacontracts/RuntimeTypeSystem.md | 3 + .../vm/datadescriptor/datadescriptor.inc | 5 + .../Contracts/IRuntimeTypeSystem.cs | 13 ++ .../Constants.cs | 4 + .../Contracts/RuntimeTypeSystem_1.cs | 40 ++++ .../TypeNameBuilder.cs | 60 +++++- .../managed/cdac/tests/MethodTableTests.cs | 197 ++++++++++++++++++ .../MockDescriptors.RuntimeTypeSystem.cs | 55 +++++ 8 files changed, 376 insertions(+), 1 deletion(-) diff --git a/docs/design/datacontracts/RuntimeTypeSystem.md b/docs/design/datacontracts/RuntimeTypeSystem.md index 83182979b22088..78b2d3add5203e 100644 --- a/docs/design/datacontracts/RuntimeTypeSystem.md +++ b/docs/design/datacontracts/RuntimeTypeSystem.md @@ -58,6 +58,8 @@ partial interface IRuntimeTypeSystem : IContract public virtual bool RequiresAlign8(TypeHandle typeHandle); // True if the MethodTable represents a continuation type used by the async continuation feature public virtual bool IsContinuation(TypeHandle typeHandle); + // Returns the raw CGCDescSeries entries for the method table, ordered highest to lowest (empty for non-MT handles, no GC pointers, or value-class series) + public virtual IEnumerable<(uint SeriesOffset, uint SeriesSize)> GetGCDescSeries(TypeHandle typeHandle); public virtual bool IsDynamicStatics(TypeHandle typeHandle); public virtual ushort GetNumInterfaces(TypeHandle typeHandle); @@ -415,6 +417,7 @@ The contract depends on the following globals | Global name | Meaning | | --- | --- | | `ContinuationMethodTable` | A pointer to the address of the base `Continuation` `MethodTable`, or null if no continuations have been created +| `OffsetOfContinuationData` | Byte offset from the start of an object (past the sync-block header) to the data payload of a `CORINFO_Continuation` struct; equals `3 * sizeof(void*) + 8` | `FreeObjectMethodTablePointer` | A pointer to the address of a `MethodTable` used by the GC to indicate reclaimed memory | `StaticsPointerMask` | For masking out a bit of DynamicStaticsInfo pointer fields | `ArrayBaseSize` | The base size of an array object; used to compute multidimensional array rank from `MethodTable::BaseSize` diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index 5fd9ac2e2a117a..a08d9604eb9536 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -1440,6 +1440,11 @@ CDAC_GLOBAL(FieldOffsetDynamicRVA, T_UINT32, FIELD_OFFSET_DYNAMIC_RVA) CDAC_GLOBAL_POINTER(ClrNotificationArguments, &::g_clrNotificationArguments) CDAC_GLOBAL_POINTER(ArrayBoundsZero, cdac_data::ArrayBoundsZero) CDAC_GLOBAL_POINTER(ContinuationMethodTable, &::g_pContinuationClassIfSubTypeCreated) +// Byte offset from the start of an object (method table pointer) to the data payload of +// CORINFO_Continuation. Equivalent to OFFSETOF__CORINFO_Continuation__data from corinfo.h: +// sizeof(MethodTable*) + sizeof(Next*) + sizeof(Resume*) + sizeof(Flags+State(uint64)) +// = 3 * TARGET_POINTER_SIZE + 8 +CDAC_GLOBAL(OffsetOfContinuationData, T_UINT32, 3 * TARGET_POINTER_SIZE + 8) CDAC_GLOBAL_POINTER(ExceptionMethodTable, &::g_pExceptionClass) CDAC_GLOBAL_POINTER(FreeObjectMethodTable, &::g_pFreeObjectMethodTable) CDAC_GLOBAL_POINTER(ObjectMethodTable, &::g_pObjectClass) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs index 8cc74fc5d1ee72..2674e6670d60b6 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs @@ -121,6 +121,19 @@ public interface IRuntimeTypeSystem : IContract bool RequiresAlign8(TypeHandle typeHandle) => throw new NotImplementedException(); // True if the MethodTable represents a continuation type used by the async continuation feature bool IsContinuation(TypeHandle typeHandle) => throw new NotImplementedException(); + /// + /// Returns the raw CGCDescSeries entries for the given method table, ordered from highest to lowest + /// (matching the iteration order of CGCDesc::GetHighestSeries down to GetLowestSeries). + /// Each entry is the raw (startoffset, seriessize) pair stored in the GC descriptor. + /// An empty sequence is returned when the method table has no GC pointers, is a value-class series + /// (repeating), or the handle does not represent a method table. + /// + /// + /// The raw seriessize field has the object base size subtracted during construction; callers + /// that need the true run length in bytes must add back, as in: + /// trueSize = (SeriesSize + baseSize) / pointerSize. + /// + IEnumerable<(uint SeriesOffset, uint SeriesSize)> GetGCDescSeries(TypeHandle typeHandle) => throw new NotImplementedException(); bool IsDynamicStatics(TypeHandle typeHandle) => throw new NotImplementedException(); ushort GetNumInterfaces(TypeHandle typeHandle) => throw new NotImplementedException(); 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 a742aee2e29e52..b741960ee70b2e 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs @@ -32,6 +32,10 @@ public static class Globals public const string RecommendedReaderVersion = nameof(RecommendedReaderVersion); public const string ContinuationMethodTable = nameof(ContinuationMethodTable); + // Byte offset from the start of an object to the data payload of a CORINFO_Continuation struct. + // Equals sizeof(MethodTable*) + sizeof(Next*) + sizeof(Resume*) + sizeof(Flags+State). + // See OFFSETOF__CORINFO_Continuation__data in corinfo.h. + public const string OffsetOfContinuationData = nameof(OffsetOfContinuationData); public const string ExceptionMethodTable = nameof(ExceptionMethodTable); public const string FreeObjectMethodTable = nameof(FreeObjectMethodTable); public const string ObjectMethodTable = nameof(ObjectMethodTable); 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 27e971fc0cd615..f2ae1f2e70017b 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 @@ -564,6 +564,46 @@ private Data.EEClass GetClassData(TypeHandle typeHandle) public bool IsContinuation(TypeHandle typeHandle) => typeHandle.IsMethodTable() && _continuationMethodTablePointer != TargetPointer.Null && _methodTables[typeHandle.Address].ParentMethodTable == _continuationMethodTablePointer; + + /// + IEnumerable<(uint SeriesOffset, uint SeriesSize)> IRuntimeTypeSystem.GetGCDescSeries(TypeHandle typeHandle) + { + if (!typeHandle.IsMethodTable()) + yield break; + + if (!ContainsGCPointers(typeHandle)) + yield break; + + ulong mtAddress = typeHandle.Address; + ulong pointerSize = (ulong)_target.PointerSize; + + // NumSeries is stored one pointer-width before the method table. When interpreted as a signed + // value, a negative count indicates a value-class (repeating) series, which uses a different + // layout and is not supported by this API. + // ReadPointer zero-extends to ulong; sign-extend to the native pointer width before checking sign. + long numSeries = _target.PointerSize == sizeof(uint) + ? (long)(int)_target.ReadPointer(mtAddress - pointerSize).Value // sign-extend 32-bit + : (long)_target.ReadPointer(mtAddress - pointerSize).Value; // 64-bit sign already correct + if (numSeries <= 0) + yield break; + + // The GC descriptor is laid out before the method table as follows (each cell is pointer-sized): + // + // [ series[n-1].seriessize ] [ series[n-1].startoffset ] <- lowest series in memory + // ... + // [ series[0].seriessize ] [ series[0].startoffset ] <- highest series (MT - 3*ptrSize) + // [ NumSeries ] <- MT - 1*ptrSize + // [ MethodTable ] <- MT address + // + // Iteration mirrors CGCDesc::GetHighestSeries() down to GetLowestSeries(). + for (ulong i = 0; i < (ulong)numSeries; i++) + { + ulong seriesBase = mtAddress - (3 + 2 * i) * pointerSize; + ulong seriesSize = _target.ReadPointer(seriesBase).Value; + ulong seriesOffset = _target.ReadPointer(seriesBase + pointerSize).Value; + yield return ((uint)seriesOffset, (uint)seriesSize); + } + } public bool IsDynamicStatics(TypeHandle typeHandle) => !typeHandle.IsMethodTable() ? false : _methodTables[typeHandle.Address].Flags.IsDynamicStatics; public ushort GetNumInterfaces(TypeHandle typeHandle) => !typeHandle.IsMethodTable() ? (ushort)0 : _methodTables[typeHandle.Address].NumInterfaces; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/TypeNameBuilder.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/TypeNameBuilder.cs index 201fdef999bab0..8b5e2d839620a0 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/TypeNameBuilder.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/TypeNameBuilder.cs @@ -6,6 +6,7 @@ using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; using System.Text; +using Microsoft.Diagnostics.DataContractReader; using Microsoft.Diagnostics.DataContractReader.Contracts; namespace Microsoft.Diagnostics.DataContractReader.Legacy; @@ -283,7 +284,14 @@ private static void AppendTypeCore(ref TypeNameBuilder tnb, Contracts.TypeHandle Contracts.ModuleHandle moduleHandle = tnb.Target.Contracts.Loader.GetModuleHandleFromModulePtr(typeSystemContract.GetModule(typeHandle)); if (MetadataTokens.EntityHandle((int)typeDefToken).IsNil) { - tnb.AddName("(dynamicClass)"); + if (typeSystemContract.IsContinuation(typeHandle)) + { + AppendContinuationName(ref tnb, typeSystemContract, typeHandle); + } + else + { + tnb.AddName("(dynamicClass)"); + } } else { @@ -480,6 +488,56 @@ private void AddAssemblySpec(string? assemblySpec) } } + /// + /// Builds the synthetic name for a dynamically-created continuation method table, mirroring the + /// native AsyncContinuationsManager::PrintContinuationName in asynccontinuations.h. + /// + /// + /// The name has the form: + /// Continuation_<dataSize>[_<gcOffset>_<gcCount>]* + /// where: + /// + /// + /// dataSize is the number of bytes of data payload (base size minus the fixed + /// object-header and continuation-header overhead). + /// + /// + /// Each _gcOffset_gcCount pair describes one GC-pointer run: gcOffset is the + /// offset in bytes from the start of the data payload to the run, and gcCount is the + /// number of pointer-sized GC references in that run. + /// + /// + /// Only GC descriptor series whose startoffset is at or above the continuation data + /// payload (i.e., after the fixed CORINFO_Continuation header fields) are included. + /// + private static void AppendContinuationName(ref TypeNameBuilder tnb, IRuntimeTypeSystem typeSystemContract, TypeHandle typeHandle) + { + uint baseSize = typeSystemContract.GetBaseSize(typeHandle); + uint continuationDataOffset = tnb.Target.ReadGlobal(Constants.Globals.OffsetOfContinuationData); + + // OBJHEADER_SIZE equals the pointer size on all supported platforms: + // 64-bit: 4 bytes pad + 4 bytes SyncBlockValue = 8 = pointer size + // 32-bit: 4 bytes SyncBlockValue = 4 = pointer size + uint objHeaderSize = (uint)tnb.Target.PointerSize; + uint dataSize = baseSize - (objHeaderSize + continuationDataOffset); + + var name = new StringBuilder("Continuation_"); + name.Append(dataSize); + + foreach ((uint seriesOffset, uint seriesSize) in typeSystemContract.GetGCDescSeries(typeHandle)) + { + if (seriesOffset < continuationDataOffset) + continue; + + name.Append('_'); + name.Append(seriesOffset - continuationDataOffset); + name.Append('_'); + name.Append((seriesSize + baseSize) / (uint)tnb.Target.PointerSize); + } + + tnb.AddNameNoEscaping(name); + } + private static void AppendNestedTypeDef(ref TypeNameBuilder tnb, MetadataReader reader, TypeDefinitionHandle typeDefToken, TypeNameFormat format) { TypeDefinition typeDef = reader.GetTypeDefinition(typeDefToken); diff --git a/src/native/managed/cdac/tests/MethodTableTests.cs b/src/native/managed/cdac/tests/MethodTableTests.cs index 89a87fd259cc4a..59bf95d34610ed 100644 --- a/src/native/managed/cdac/tests/MethodTableTests.cs +++ b/src/native/managed/cdac/tests/MethodTableTests.cs @@ -36,6 +36,7 @@ internal static (string Name, ulong Value)[] CreateContractGlobals(MockRTS rtsBu (nameof(Constants.Globals.ContinuationMethodTable), rtsBuilder.ContinuationMethodTableGlobalAddress), (nameof(Constants.Globals.MethodDescAlignment), rtsBuilder.MethodDescAlignment), (nameof(Constants.Globals.ArrayBaseSize), rtsBuilder.ArrayBaseSize), + (nameof(Constants.Globals.OffsetOfContinuationData), rtsBuilder.OffsetOfContinuationData), ]; public static IEnumerable StdArchBool() @@ -649,4 +650,200 @@ public void RequiresAlign8(MockTarget.Architecture arch, bool flagSet) Contracts.TypeHandle typeHandle = contract.GetTypeHandle(methodTablePtr); Assert.Equal(flagSet, contract.RequiresAlign8(typeHandle)); } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetGCDescSeriesReturnsEmptyForNonMethodTable(MockTarget.Architecture arch) + { + // TypeDesc handles should yield no series + TargetPointer typeDescAddress = default; + TestPlaceholderTarget target = CreateTarget( + arch, + rtsBuilder => + { + MockParamTypeDesc typeDesc = rtsBuilder.AddParamTypeDesc(); + typeDescAddress = typeDesc.Address | (ulong)RuntimeTypeSystem_1.TypeHandleBits.TypeDesc; + }); + + IRuntimeTypeSystem contract = target.Contracts.RuntimeTypeSystem; + Contracts.TypeHandle typeDescHandle = contract.GetTypeHandle(typeDescAddress); + Assert.Empty(contract.GetGCDescSeries(typeDescHandle)); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetGCDescSeriesReturnsEmptyWhenNoGCPointers(MockTarget.Architecture arch) + { + TargetPointer mtPtr = default; + TestPlaceholderTarget target = CreateTarget( + arch, + rtsBuilder => + { + MockEEClass eeClass = rtsBuilder.AddEEClass("NoGCPointers"); + MockMethodTable mt = rtsBuilder.AddMethodTable("NoGCPointers"); + uint baseSize = rtsBuilder.Builder.TargetTestHelpers.ObjectBaseSize; + mt.BaseSize = baseSize; + mt.ParentMethodTable = rtsBuilder.SystemObjectMethodTable.Address; + mt.NumVirtuals = 3; + eeClass.MethodTable = mt.Address; + mt.EEClassOrCanonMT = eeClass.Address; + // MTFlags does NOT have ContainsGCPointers (0x01000000) set + mtPtr = mt.Address; + }); + + IRuntimeTypeSystem contract = target.Contracts.RuntimeTypeSystem; + Contracts.TypeHandle typeHandle = contract.GetTypeHandle(mtPtr); + Assert.False(contract.ContainsGCPointers(typeHandle)); + Assert.Empty(contract.GetGCDescSeries(typeHandle)); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetGCDescSeriesReturnsSingleSeries(MockTarget.Architecture arch) + { + TargetPointer mtPtr = default; + uint expectedSeriesOffset = 0; + uint expectedSeriesSize = 0; + + TestPlaceholderTarget target = CreateTarget( + arch, + rtsBuilder => + { + TargetTestHelpers helpers = rtsBuilder.Builder.TargetTestHelpers; + uint pointerSize = (uint)helpers.PointerSize; + + // Object layout: [ObjHeader][MT*][ref1] + // BaseSize = ObjHeader + MT* + ref1 = 3 * pointerSize + uint baseSize = helpers.ObjHeaderSize + 2u * pointerSize; + + // One series covering the single reference field. + // seriessize is stored as (actualSize - baseSize); for one pointer: pointerSize - baseSize + ulong rawSeriesSize = pointerSize - baseSize; // stored as size_t (wraps to large value) + ulong rawSeriesOffset = helpers.ObjHeaderSize + pointerSize; // after ObjHeader+MT* + + MockEEClass eeClass = rtsBuilder.AddEEClass("SingleRef"); + MockMethodTable mt = rtsBuilder.AddMethodTableWithGCDesc( + "SingleRef", + baseSize, + [(rawSeriesSize, rawSeriesOffset)]); + mt.MTFlags |= 0x01000000u; // ContainsGCPointers + mt.ParentMethodTable = rtsBuilder.SystemObjectMethodTable.Address; + mt.NumVirtuals = 3; + eeClass.MethodTable = mt.Address; + mt.EEClassOrCanonMT = eeClass.Address; + + mtPtr = mt.Address; + expectedSeriesOffset = (uint)rawSeriesOffset; + expectedSeriesSize = (uint)rawSeriesSize; + }); + + IRuntimeTypeSystem contract = target.Contracts.RuntimeTypeSystem; + Contracts.TypeHandle typeHandle = contract.GetTypeHandle(mtPtr); + Assert.True(contract.ContainsGCPointers(typeHandle)); + + (uint SeriesOffset, uint SeriesSize)[] series = contract.GetGCDescSeries(typeHandle).ToArray(); + Assert.Single(series); + Assert.Equal(expectedSeriesOffset, series[0].SeriesOffset); + Assert.Equal(expectedSeriesSize, series[0].SeriesSize); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetGCDescSeriesReturnsMultipleSeriesInOrder(MockTarget.Architecture arch) + { + TargetPointer mtPtr = default; + (uint SeriesOffset, uint SeriesSize)[] expectedSeries = []; + + TestPlaceholderTarget target = CreateTarget( + arch, + rtsBuilder => + { + TargetTestHelpers helpers = rtsBuilder.Builder.TargetTestHelpers; + uint pointerSize = (uint)helpers.PointerSize; + + // Two separate GC reference runs in the object. + // Object layout: [ObjHeader][MT*][ref1][nonref][ref2] + uint baseSize = helpers.ObjHeaderSize + 4u * pointerSize; + + ulong series0Offset = helpers.ObjHeaderSize + pointerSize; // ref1 field + ulong series0Size = pointerSize - baseSize; // raw stored size + + ulong series1Offset = helpers.ObjHeaderSize + 3u * pointerSize; // ref2 field + ulong series1Size = pointerSize - baseSize; + + MockEEClass eeClass = rtsBuilder.AddEEClass("TwoRefs"); + MockMethodTable mt = rtsBuilder.AddMethodTableWithGCDesc( + "TwoRefs", + baseSize, + // Ordered highest (lowest index) to lowest as required by AddMethodTableWithGCDesc + [(series0Size, series0Offset), (series1Size, series1Offset)]); + mt.MTFlags |= 0x01000000u; // ContainsGCPointers + mt.ParentMethodTable = rtsBuilder.SystemObjectMethodTable.Address; + mt.NumVirtuals = 3; + eeClass.MethodTable = mt.Address; + mt.EEClassOrCanonMT = eeClass.Address; + + mtPtr = mt.Address; + expectedSeries = + [ + ((uint)series0Offset, (uint)series0Size), + ((uint)series1Offset, (uint)series1Size), + ]; + }); + + IRuntimeTypeSystem contract = target.Contracts.RuntimeTypeSystem; + Contracts.TypeHandle typeHandle = contract.GetTypeHandle(mtPtr); + + (uint SeriesOffset, uint SeriesSize)[] series = contract.GetGCDescSeries(typeHandle).ToArray(); + Assert.Equal(expectedSeries.Length, series.Length); + for (int i = 0; i < expectedSeries.Length; i++) + { + Assert.Equal(expectedSeries[i].SeriesOffset, series[i].SeriesOffset); + Assert.Equal(expectedSeries[i].SeriesSize, series[i].SeriesSize); + } + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetGCDescSeriesReturnsEmptyForValueClassSeries(MockTarget.Architecture arch) + { + // A negative NumSeries indicates a value-class (repeating) series layout. + // GetGCDescSeries should return empty for those. + TargetPointer mtPtr = default; + TestPlaceholderTarget target = CreateTarget( + arch, + rtsBuilder => + { + TargetTestHelpers helpers = rtsBuilder.Builder.TargetTestHelpers; + uint pointerSize = (uint)helpers.PointerSize; + uint baseSize = helpers.ObjHeaderSize + 2u * pointerSize; + + // Use a negative NumSeries to signal value-class series + ulong negativeCount = unchecked((ulong)(long)-1); // -1 as size_t + + MockEEClass eeClass = rtsBuilder.AddEEClass("ValueClassArray"); + MockMethodTable mt = rtsBuilder.AddMethodTableWithGCDesc( + "ValueClassArray", + baseSize, + // Pass one series entry but we'll override NumSeries to be negative below. + // Use a helper that sets negative count via the raw field. + [(pointerSize - baseSize, helpers.ObjHeaderSize + pointerSize)]); + mt.MTFlags |= 0x01000000u; // ContainsGCPointers + mt.ParentMethodTable = rtsBuilder.SystemObjectMethodTable.Address; + mt.NumVirtuals = 3; + eeClass.MethodTable = mt.Address; + mt.EEClassOrCanonMT = eeClass.Address; + mtPtr = mt.Address; + + // Overwrite NumSeries with a negative value (-1) to simulate value-class series + Span numSeriesSlot = rtsBuilder.Builder.BorrowAddressRange( + mt.Address - (ulong)pointerSize, (int)pointerSize); + helpers.WritePointer(numSeriesSlot, negativeCount); + }); + + IRuntimeTypeSystem contract = target.Contracts.RuntimeTypeSystem; + Contracts.TypeHandle typeHandle = contract.GetTypeHandle(mtPtr); + Assert.True(contract.ContainsGCPointers(typeHandle)); + Assert.Empty(contract.GetGCDescSeries(typeHandle)); + } } diff --git a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.RuntimeTypeSystem.cs b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.RuntimeTypeSystem.cs index 8510203e0e5ae0..a22b4184c0bfb2 100644 --- a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.RuntimeTypeSystem.cs +++ b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.RuntimeTypeSystem.cs @@ -316,6 +316,8 @@ public class RuntimeTypeSystem internal ulong ContinuationMethodTableGlobalAddress => TestContinuationMethodTableGlobalAddress; internal ulong MethodDescAlignment => GetMethodDescAlignment(Builder.TargetTestHelpers); internal ulong ArrayBaseSize => Builder.TargetTestHelpers.ArrayBaseBaseSize; + // OFFSETOF__CORINFO_Continuation__data = sizeof(MT*) + sizeof(Next*) + sizeof(Resume*) + 8 (Flags+State) + internal ulong OffsetOfContinuationData => 3ul * (ulong)Builder.TargetTestHelpers.PointerSize + 8; public RuntimeTypeSystem(MockMemorySpace.Builder builder) : this(builder, (DefaultAllocationRangeStart, DefaultAllocationRangeEnd)) @@ -447,5 +449,58 @@ private TView Add(Layout layout, ulong size, string name) MockMemorySpace.HeapFragment fragment = TypeSystemAllocator.Allocate(size, name); return layout.Create(fragment.Data.AsMemory(), fragment.Address); } + + /// + /// Allocates a method table together with a CGCDesc immediately before it in memory. + /// + /// Descriptive name for the allocation. + /// Value to store in the method table's BaseSize field. + /// + /// GC descriptor series ordered from highest to lowest + /// (matching CGCDesc::GetHighestSeries down to GetLowestSeries). + /// Each entry is (SeriesSize, SeriesOffset) – the raw field values stored in the + /// CGCDescSeries struct (i.e. seriessize already has BaseSize subtracted). + /// + /// + /// A whose address is after the GCDesc bytes. + /// The caller is responsible for setting additional method table flags (e.g. + /// ContainsGCPointers = 0x01000000) and linking to an EEClass. + /// + internal MockMethodTable AddMethodTableWithGCDesc(string name, uint baseSize, (ulong SeriesSize, ulong SeriesOffset)[] series) + { + int pointerSize = Builder.TargetTestHelpers.PointerSize; + + // GCDesc layout (each slot is pointer-sized): + // [ series[N-1].seriessize ] [ series[N-1].startoffset ] <- lowest series (fragment start) + // ... + // [ series[0].seriessize ] [ series[0].startoffset ] <- highest series (MT - 3*ptrSize) + // [ NumSeries ] <- MT - 1*ptrSize + // [ MethodTable data starts here ] + int gcDescSize = (1 + 2 * series.Length) * pointerSize; + int totalSize = gcDescSize + MethodTableLayout.Size; + + MockMemorySpace.HeapFragment fragment = TypeSystemAllocator.Allocate((ulong)totalSize, $"GCDesc+MethodTable '{name}'"); + + // Write series entries. The highest series (index 0) lives closest to the MT (highest address), + // and the lowest series (index N-1) lives farthest from the MT (lowest address). + // So series[i] goes at fragment offset (N-1-i)*2*pointerSize. + for (int i = 0; i < series.Length; i++) + { + int seriesBase = (series.Length - 1 - i) * 2 * pointerSize; + Builder.TargetTestHelpers.WritePointer(fragment.Data.AsSpan(seriesBase, pointerSize), series[i].SeriesSize); + Builder.TargetTestHelpers.WritePointer(fragment.Data.AsSpan(seriesBase + pointerSize, pointerSize), series[i].SeriesOffset); + } + + // Write NumSeries immediately before the MT + Builder.TargetTestHelpers.WritePointer( + fragment.Data.AsSpan(series.Length * 2 * pointerSize, pointerSize), + (ulong)series.Length); + + // The MockMethodTable lives at offset gcDescSize within the combined fragment + ulong mtAddress = fragment.Address + (ulong)gcDescSize; + MockMethodTable mt = MethodTableLayout.Create(fragment.Data.AsMemory(gcDescSize, MethodTableLayout.Size), mtAddress); + mt.BaseSize = baseSize; + return mt; + } } } From ecd3dd17706c9b4b098652cb20e877371ffbb390 Mon Sep 17 00:00:00 2001 From: rcj1 Date: Sat, 25 Apr 2026 11:43:35 -0700 Subject: [PATCH 2/4] cleanup and code review --- .../design/datacontracts/RuntimeTypeSystem.md | 52 +++++++- .../vm/datadescriptor/datadescriptor.inc | 9 +- .../Contracts/IRuntimeTypeSystem.cs | 15 +-- .../DataType.cs | 1 + .../Constants.cs | 4 - .../Contracts/RuntimeTypeSystem_1.cs | 67 +++++++---- .../TypeNameBuilder.cs | 12 +- .../managed/cdac/tests/MethodTableTests.cs | 112 ++++++++++++++---- .../MockDescriptors.RuntimeTypeSystem.cs | 79 +++++++++++- 9 files changed, 271 insertions(+), 80 deletions(-) diff --git a/docs/design/datacontracts/RuntimeTypeSystem.md b/docs/design/datacontracts/RuntimeTypeSystem.md index 78b2d3add5203e..e1a400a93e5633 100644 --- a/docs/design/datacontracts/RuntimeTypeSystem.md +++ b/docs/design/datacontracts/RuntimeTypeSystem.md @@ -58,8 +58,8 @@ partial interface IRuntimeTypeSystem : IContract public virtual bool RequiresAlign8(TypeHandle typeHandle); // True if the MethodTable represents a continuation type used by the async continuation feature public virtual bool IsContinuation(TypeHandle typeHandle); - // Returns the raw CGCDescSeries entries for the method table, ordered highest to lowest (empty for non-MT handles, no GC pointers, or value-class series) - public virtual IEnumerable<(uint SeriesOffset, uint SeriesSize)> GetGCDescSeries(TypeHandle typeHandle); + // Returns the GC pointer runs for the method table as (offset, size) pairs normalized to actual byte lengths. + public virtual IEnumerable<(uint SeriesOffset, uint SeriesSize)> GetGCDescSeries(TypeHandle typeHandle, uint objectSize); public virtual bool IsDynamicStatics(TypeHandle typeHandle); public virtual ushort GetNumInterfaces(TypeHandle typeHandle); @@ -417,7 +417,6 @@ The contract depends on the following globals | Global name | Meaning | | --- | --- | | `ContinuationMethodTable` | A pointer to the address of the base `Continuation` `MethodTable`, or null if no continuations have been created -| `OffsetOfContinuationData` | Byte offset from the start of an object (past the sync-block header) to the data payload of a `CORINFO_Continuation` struct; equals `3 * sizeof(void*) + 8` | `FreeObjectMethodTablePointer` | A pointer to the address of a `MethodTable` used by the GC to indicate reclaimed memory | `StaticsPointerMask` | For masking out a bit of DynamicStaticsInfo pointer fields | `ArrayBaseSize` | The base size of an array object; used to compute multidimensional array rank from `MethodTable::BaseSize` @@ -557,6 +556,53 @@ Contracts used: public bool RequiresAlign8(TypeHandle typeHandle) => !typeHandle.IsMethodTable() ? false : _methodTables[typeHandle.Address].Flags.RequiresAlign8; + public bool IsContinuation(TypeHandle typeHandle) => typeHandle.IsMethodTable() + && ContinuationMethodTablePointer != TargetPointer.Null + && _methodTables[typeHandle.Address].ParentMethodTable == ContinuationMethodTablePointer; + + IEnumerable<(uint SeriesOffset, uint SeriesSize)> GetGCDescSeries(TypeHandle typeHandle, uint objectSize) + { + // Returns empty if not a method table or has no GC pointers. + + // Read NumSeries from (mtAddress - pointerSize), sign-extended to native width. + // NumSeries == 0 → empty. + + // NumSeries > 0: Regular series. + // Used for normal objects and reference-type arrays (e.g. object[]). + // Memory layout (each slot is pointer-sized, growing away from MT): + // + // MT - (2*N+1)*ptrSize : series[N-1].seriessize + // MT - (2*N) *ptrSize : series[N-1].startoffset + // ... + // MT - 3*ptrSize : series[0].seriessize + // MT - 2*ptrSize : series[0].startoffset + // MT - 1*ptrSize : NumSeries (positive) + // MT : MethodTable + // + // The raw seriessize is stored with baseSize subtracted. + // Add objectSize back to get the true run length: + // trueRunLength = rawSeriesSize + objectSize + // For non-arrays objectSize == baseSize, recovering the actual run. + // For arrays objectSize > baseSize, extending the last series across all elements. + + // NumSeries < 0: Value-class (repeating) series. + // Used for arrays of value types containing GC references. + // |NumSeries| val_serie_items describe pointer runs within one array element. + // Memory layout: + // + // MT - (N+2)*ptrSize : val_serie[N-1] + // ... + // MT - 3*ptrSize : val_serie[0] + // MT - 2*ptrSize : startoffset + // MT - 1*ptrSize : NumSeries (-N) + // MT : MethodTable + // + // Each val_serie_item is { HALF_SIZE_T nptrs; HALF_SIZE_T skip; } packed into one pointer-width. + // HALF_SIZE_T is uint16 on 32-bit, uint32 on 64-bit. + // Read nptrs and skip as separate typed reads for endianness safety. + // runBytes = nptrs * pointerSize; advance currentOffset by runBytes + skip after each item. + } + public bool IsDynamicStatics(TypeHandle TypeHandle) => !typeHandle.IsMethodTable() ? false : _methodTables[TypeHandle.Address].Flags.IsDynamicStatics; public ushort GetNumInterfaces(TypeHandle TypeHandle) => !typeHandle.IsMethodTable() ? 0 : _methodTables[TypeHandle.Address].NumInterfaces; diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index a08d9604eb9536..fb98c9670a21fa 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -173,6 +173,10 @@ CDAC_TYPE_FIELD(String, T_POINTER, m_FirstChar, cdac_data::m_First CDAC_TYPE_FIELD(String, T_UINT32, m_StringLength, cdac_data::m_StringLength) CDAC_TYPE_END(String) +CDAC_TYPE_BEGIN(ContinuationObject) +CDAC_TYPE_SIZE(sizeof(ContinuationObject)) +CDAC_TYPE_END(ContinuationObject) + CDAC_TYPE_BEGIN(Array) CDAC_TYPE_SIZE(sizeof(ArrayBase)) CDAC_TYPE_FIELD(Array, T_UINT32, m_NumComponents, cdac_data::m_NumComponents) @@ -1440,11 +1444,6 @@ CDAC_GLOBAL(FieldOffsetDynamicRVA, T_UINT32, FIELD_OFFSET_DYNAMIC_RVA) CDAC_GLOBAL_POINTER(ClrNotificationArguments, &::g_clrNotificationArguments) CDAC_GLOBAL_POINTER(ArrayBoundsZero, cdac_data::ArrayBoundsZero) CDAC_GLOBAL_POINTER(ContinuationMethodTable, &::g_pContinuationClassIfSubTypeCreated) -// Byte offset from the start of an object (method table pointer) to the data payload of -// CORINFO_Continuation. Equivalent to OFFSETOF__CORINFO_Continuation__data from corinfo.h: -// sizeof(MethodTable*) + sizeof(Next*) + sizeof(Resume*) + sizeof(Flags+State(uint64)) -// = 3 * TARGET_POINTER_SIZE + 8 -CDAC_GLOBAL(OffsetOfContinuationData, T_UINT32, 3 * TARGET_POINTER_SIZE + 8) CDAC_GLOBAL_POINTER(ExceptionMethodTable, &::g_pExceptionClass) CDAC_GLOBAL_POINTER(FreeObjectMethodTable, &::g_pFreeObjectMethodTable) CDAC_GLOBAL_POINTER(ObjectMethodTable, &::g_pObjectClass) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs index 2674e6670d60b6..73a98b6a9d8c1e 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs @@ -122,18 +122,11 @@ public interface IRuntimeTypeSystem : IContract // True if the MethodTable represents a continuation type used by the async continuation feature bool IsContinuation(TypeHandle typeHandle) => throw new NotImplementedException(); /// - /// Returns the raw CGCDescSeries entries for the given method table, ordered from highest to lowest - /// (matching the iteration order of CGCDesc::GetHighestSeries down to GetLowestSeries). - /// Each entry is the raw (startoffset, seriessize) pair stored in the GC descriptor. - /// An empty sequence is returned when the method table has no GC pointers, is a value-class series - /// (repeating), or the handle does not represent a method table. + /// Enumerates GC pointer runs from the CGCDesc stored before the method table. + /// Returns (offset, size) pairs normalized to actual byte lengths. + /// See RuntimeTypeSystem.md for the full GCDesc format documentation. /// - /// - /// The raw seriessize field has the object base size subtracted during construction; callers - /// that need the true run length in bytes must add back, as in: - /// trueSize = (SeriesSize + baseSize) / pointerSize. - /// - IEnumerable<(uint SeriesOffset, uint SeriesSize)> GetGCDescSeries(TypeHandle typeHandle) => throw new NotImplementedException(); + IEnumerable<(uint SeriesOffset, uint SeriesSize)> GetGCDescSeries(TypeHandle typeHandle, uint objectSize) => throw new NotImplementedException(); bool IsDynamicStatics(TypeHandle typeHandle) => throw new NotImplementedException(); ushort GetNumInterfaces(TypeHandle typeHandle) => throw new NotImplementedException(); 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 989db1625b33f9..cc65732c7954f3 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs @@ -77,6 +77,7 @@ public enum DataType StressMsg, StressMsgHeader, Object, + ContinuationObject, NativeObjectWrapperObject, ManagedObjectWrapperHolderObject, ManagedObjectWrapperLayout, 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 b741960ee70b2e..a742aee2e29e52 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs @@ -32,10 +32,6 @@ public static class Globals public const string RecommendedReaderVersion = nameof(RecommendedReaderVersion); public const string ContinuationMethodTable = nameof(ContinuationMethodTable); - // Byte offset from the start of an object to the data payload of a CORINFO_Continuation struct. - // Equals sizeof(MethodTable*) + sizeof(Next*) + sizeof(Resume*) + sizeof(Flags+State). - // See OFFSETOF__CORINFO_Continuation__data in corinfo.h. - public const string OffsetOfContinuationData = nameof(OffsetOfContinuationData); public const string ExceptionMethodTable = nameof(ExceptionMethodTable); public const string FreeObjectMethodTable = nameof(FreeObjectMethodTable); public const string ObjectMethodTable = nameof(ObjectMethodTable); 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 f2ae1f2e70017b..cbab68915bcb70 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 @@ -565,8 +565,7 @@ public bool IsContinuation(TypeHandle typeHandle) => typeHandle.IsMethodTable() && _continuationMethodTablePointer != TargetPointer.Null && _methodTables[typeHandle.Address].ParentMethodTable == _continuationMethodTablePointer; - /// - IEnumerable<(uint SeriesOffset, uint SeriesSize)> IRuntimeTypeSystem.GetGCDescSeries(TypeHandle typeHandle) + IEnumerable<(uint SeriesOffset, uint SeriesSize)> IRuntimeTypeSystem.GetGCDescSeries(TypeHandle typeHandle, uint objectSize) { if (!typeHandle.IsMethodTable()) yield break; @@ -577,33 +576,55 @@ public bool IsContinuation(TypeHandle typeHandle) => typeHandle.IsMethodTable() ulong mtAddress = typeHandle.Address; ulong pointerSize = (ulong)_target.PointerSize; - // NumSeries is stored one pointer-width before the method table. When interpreted as a signed - // value, a negative count indicates a value-class (repeating) series, which uses a different - // layout and is not supported by this API. - // ReadPointer zero-extends to ulong; sign-extend to the native pointer width before checking sign. + // Sign-extend NumSeries from native pointer width. long numSeries = _target.PointerSize == sizeof(uint) - ? (long)(int)_target.ReadPointer(mtAddress - pointerSize).Value // sign-extend 32-bit - : (long)_target.ReadPointer(mtAddress - pointerSize).Value; // 64-bit sign already correct - if (numSeries <= 0) + ? (long)(int)_target.ReadPointer(mtAddress - pointerSize).Value + : (long)_target.ReadPointer(mtAddress - pointerSize).Value; + if (numSeries == 0) yield break; - // The GC descriptor is laid out before the method table as follows (each cell is pointer-sized): - // - // [ series[n-1].seriessize ] [ series[n-1].startoffset ] <- lowest series in memory - // ... - // [ series[0].seriessize ] [ series[0].startoffset ] <- highest series (MT - 3*ptrSize) - // [ NumSeries ] <- MT - 1*ptrSize - // [ MethodTable ] <- MT address - // - // Iteration mirrors CGCDesc::GetHighestSeries() down to GetLowestSeries(). - for (ulong i = 0; i < (ulong)numSeries; i++) + if (numSeries > 0) { - ulong seriesBase = mtAddress - (3 + 2 * i) * pointerSize; - ulong seriesSize = _target.ReadPointer(seriesBase).Value; - ulong seriesOffset = _target.ReadPointer(seriesBase + pointerSize).Value; - yield return ((uint)seriesOffset, (uint)seriesSize); + // Regular series: iterate from highest (closest to MT) to lowest. + for (ulong i = 0; i < (ulong)numSeries; i++) + { + ulong seriesBase = mtAddress - (3 + 2 * i) * pointerSize; + ulong rawSeriesSize = _target.ReadPointer(seriesBase).Value; + ulong seriesOffset = _target.ReadPointer(seriesBase + pointerSize).Value; + yield return ((uint)seriesOffset, (uint)(rawSeriesSize + objectSize)); + } + } + else + { + // Value-class (repeating) series. + long absNumSeries = -numSeries; + ulong startOffset = _target.ReadPointer(mtAddress - 2 * pointerSize).Value; + + ulong currentOffset = startOffset; + for (long i = 0; i < absNumSeries; i++) + { + ulong itemAddress = mtAddress - (3 + (ulong)i) * pointerSize; + + // Read val_serie_item fields individually for endianness safety. + uint nptrs, skip; + if (_target.PointerSize == sizeof(uint)) + { + nptrs = _target.Read(itemAddress); + skip = _target.Read(itemAddress + sizeof(ushort)); + } + else + { + nptrs = _target.Read(itemAddress); + skip = _target.Read(itemAddress + sizeof(uint)); + } + + uint runBytes = nptrs * (uint)pointerSize; + yield return ((uint)currentOffset, runBytes); + currentOffset += runBytes + skip; + } } } + public bool IsDynamicStatics(TypeHandle typeHandle) => !typeHandle.IsMethodTable() ? false : _methodTables[typeHandle.Address].Flags.IsDynamicStatics; public ushort GetNumInterfaces(TypeHandle typeHandle) => !typeHandle.IsMethodTable() ? (ushort)0 : _methodTables[typeHandle.Address].NumInterfaces; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/TypeNameBuilder.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/TypeNameBuilder.cs index 8b5e2d839620a0..5558e4aac72a39 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/TypeNameBuilder.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/TypeNameBuilder.cs @@ -513,18 +513,14 @@ private void AddAssemblySpec(string? assemblySpec) private static void AppendContinuationName(ref TypeNameBuilder tnb, IRuntimeTypeSystem typeSystemContract, TypeHandle typeHandle) { uint baseSize = typeSystemContract.GetBaseSize(typeHandle); - uint continuationDataOffset = tnb.Target.ReadGlobal(Constants.Globals.OffsetOfContinuationData); - - // OBJHEADER_SIZE equals the pointer size on all supported platforms: - // 64-bit: 4 bytes pad + 4 bytes SyncBlockValue = 8 = pointer size - // 32-bit: 4 bytes SyncBlockValue = 4 = pointer size - uint objHeaderSize = (uint)tnb.Target.PointerSize; + uint continuationDataOffset = tnb.Target.GetTypeInfo(DataType.ContinuationObject).Size!.Value; + uint objHeaderSize = tnb.Target.GetTypeInfo(DataType.ObjectHeader).Size!.Value; uint dataSize = baseSize - (objHeaderSize + continuationDataOffset); var name = new StringBuilder("Continuation_"); name.Append(dataSize); - foreach ((uint seriesOffset, uint seriesSize) in typeSystemContract.GetGCDescSeries(typeHandle)) + foreach ((uint seriesOffset, uint seriesSize) in typeSystemContract.GetGCDescSeries(typeHandle, baseSize)) { if (seriesOffset < continuationDataOffset) continue; @@ -532,7 +528,7 @@ private static void AppendContinuationName(ref TypeNameBuilder tnb, IRuntimeType name.Append('_'); name.Append(seriesOffset - continuationDataOffset); name.Append('_'); - name.Append((seriesSize + baseSize) / (uint)tnb.Target.PointerSize); + name.Append(seriesSize / (uint)tnb.Target.PointerSize); } tnb.AddNameNoEscaping(name); diff --git a/src/native/managed/cdac/tests/MethodTableTests.cs b/src/native/managed/cdac/tests/MethodTableTests.cs index 59bf95d34610ed..0089c08d2ba049 100644 --- a/src/native/managed/cdac/tests/MethodTableTests.cs +++ b/src/native/managed/cdac/tests/MethodTableTests.cs @@ -27,6 +27,7 @@ public class MethodTableTests [DataType.ParamTypeDesc] = TargetTestHelpers.CreateTypeInfo(rtsBuilder.ParamTypeDescLayout), [DataType.TypeVarTypeDesc] = TargetTestHelpers.CreateTypeInfo(rtsBuilder.TypeVarTypeDescLayout), [DataType.GCCoverageInfo] = TargetTestHelpers.CreateTypeInfo(rtsBuilder.GCCoverageInfoLayout), + [DataType.ContinuationObject] = new Target.TypeInfo { Size = rtsBuilder.ContinuationObjectSize }, }; internal static (string Name, ulong Value)[] CreateContractGlobals(MockRTS rtsBuilder) @@ -36,7 +37,6 @@ internal static (string Name, ulong Value)[] CreateContractGlobals(MockRTS rtsBu (nameof(Constants.Globals.ContinuationMethodTable), rtsBuilder.ContinuationMethodTableGlobalAddress), (nameof(Constants.Globals.MethodDescAlignment), rtsBuilder.MethodDescAlignment), (nameof(Constants.Globals.ArrayBaseSize), rtsBuilder.ArrayBaseSize), - (nameof(Constants.Globals.OffsetOfContinuationData), rtsBuilder.OffsetOfContinuationData), ]; public static IEnumerable StdArchBool() @@ -667,7 +667,7 @@ public void GetGCDescSeriesReturnsEmptyForNonMethodTable(MockTarget.Architecture IRuntimeTypeSystem contract = target.Contracts.RuntimeTypeSystem; Contracts.TypeHandle typeDescHandle = contract.GetTypeHandle(typeDescAddress); - Assert.Empty(contract.GetGCDescSeries(typeDescHandle)); + Assert.Empty(contract.GetGCDescSeries(typeDescHandle, 0)); } [Theory] @@ -694,7 +694,7 @@ public void GetGCDescSeriesReturnsEmptyWhenNoGCPointers(MockTarget.Architecture IRuntimeTypeSystem contract = target.Contracts.RuntimeTypeSystem; Contracts.TypeHandle typeHandle = contract.GetTypeHandle(mtPtr); Assert.False(contract.ContainsGCPointers(typeHandle)); - Assert.Empty(contract.GetGCDescSeries(typeHandle)); + Assert.Empty(contract.GetGCDescSeries(typeHandle, 0)); } [Theory] @@ -734,14 +734,16 @@ public void GetGCDescSeriesReturnsSingleSeries(MockTarget.Architecture arch) mtPtr = mt.Address; expectedSeriesOffset = (uint)rawSeriesOffset; - expectedSeriesSize = (uint)rawSeriesSize; + // After normalization: rawSeriesSize + baseSize = pointerSize (one pointer-sized run) + expectedSeriesSize = pointerSize; }); IRuntimeTypeSystem contract = target.Contracts.RuntimeTypeSystem; Contracts.TypeHandle typeHandle = contract.GetTypeHandle(mtPtr); Assert.True(contract.ContainsGCPointers(typeHandle)); - (uint SeriesOffset, uint SeriesSize)[] series = contract.GetGCDescSeries(typeHandle).ToArray(); + uint baseSize = contract.GetBaseSize(typeHandle); + (uint SeriesOffset, uint SeriesSize)[] series = contract.GetGCDescSeries(typeHandle, baseSize).ToArray(); Assert.Single(series); Assert.Equal(expectedSeriesOffset, series[0].SeriesOffset); Assert.Equal(expectedSeriesSize, series[0].SeriesSize); @@ -784,17 +786,19 @@ public void GetGCDescSeriesReturnsMultipleSeriesInOrder(MockTarget.Architecture mt.EEClassOrCanonMT = eeClass.Address; mtPtr = mt.Address; + // After normalization (rawSize + baseSize), each series covers one pointer expectedSeries = [ - ((uint)series0Offset, (uint)series0Size), - ((uint)series1Offset, (uint)series1Size), + ((uint)series0Offset, pointerSize), + ((uint)series1Offset, pointerSize), ]; }); IRuntimeTypeSystem contract = target.Contracts.RuntimeTypeSystem; Contracts.TypeHandle typeHandle = contract.GetTypeHandle(mtPtr); - (uint SeriesOffset, uint SeriesSize)[] series = contract.GetGCDescSeries(typeHandle).ToArray(); + uint baseSize = contract.GetBaseSize(typeHandle); + (uint SeriesOffset, uint SeriesSize)[] series = contract.GetGCDescSeries(typeHandle, baseSize).ToArray(); Assert.Equal(expectedSeries.Length, series.Length); for (int i = 0; i < expectedSeries.Length; i++) { @@ -805,29 +809,32 @@ public void GetGCDescSeriesReturnsMultipleSeriesInOrder(MockTarget.Architecture [Theory] [ClassData(typeof(MockTarget.StdArch))] - public void GetGCDescSeriesReturnsEmptyForValueClassSeries(MockTarget.Architecture arch) + public void GetGCDescSeriesReturnsSingleValueClassSeries(MockTarget.Architecture arch) { // A negative NumSeries indicates a value-class (repeating) series layout. - // GetGCDescSeries should return empty for those. + // For one val_serie_item with nptrs=1, skip=0, the API should return a single run. TargetPointer mtPtr = default; + uint expectedOffset = 0; + uint expectedSize = 0; + TestPlaceholderTarget target = CreateTarget( arch, rtsBuilder => { TargetTestHelpers helpers = rtsBuilder.Builder.TargetTestHelpers; uint pointerSize = (uint)helpers.PointerSize; - uint baseSize = helpers.ObjHeaderSize + 2u * pointerSize; - // Use a negative NumSeries to signal value-class series - ulong negativeCount = unchecked((ulong)(long)-1); // -1 as size_t + // Array of structs each containing one GC ref. + // startoffset points past the array header to the first element's ref field. + uint startOffset = helpers.ObjHeaderSize + 2u * pointerSize; // past ObjHeader + MT* + length + uint baseSize = helpers.ObjHeaderSize + 3u * pointerSize; - MockEEClass eeClass = rtsBuilder.AddEEClass("ValueClassArray"); - MockMethodTable mt = rtsBuilder.AddMethodTableWithGCDesc( - "ValueClassArray", + MockEEClass eeClass = rtsBuilder.AddEEClass("ValueClassArray_1ref"); + MockMethodTable mt = rtsBuilder.AddMethodTableWithValueClassGCDesc( + "ValueClassArray_1ref", baseSize, - // Pass one series entry but we'll override NumSeries to be negative below. - // Use a helper that sets negative count via the raw field. - [(pointerSize - baseSize, helpers.ObjHeaderSize + pointerSize)]); + startOffset, + [(1, 0)]); // nptrs=1, skip=0 mt.MTFlags |= 0x01000000u; // ContainsGCPointers mt.ParentMethodTable = rtsBuilder.SystemObjectMethodTable.Address; mt.NumVirtuals = 3; @@ -835,15 +842,72 @@ public void GetGCDescSeriesReturnsEmptyForValueClassSeries(MockTarget.Architectu mt.EEClassOrCanonMT = eeClass.Address; mtPtr = mt.Address; - // Overwrite NumSeries with a negative value (-1) to simulate value-class series - Span numSeriesSlot = rtsBuilder.Builder.BorrowAddressRange( - mt.Address - (ulong)pointerSize, (int)pointerSize); - helpers.WritePointer(numSeriesSlot, negativeCount); + expectedOffset = startOffset; + expectedSize = 1u * pointerSize; }); IRuntimeTypeSystem contract = target.Contracts.RuntimeTypeSystem; Contracts.TypeHandle typeHandle = contract.GetTypeHandle(mtPtr); Assert.True(contract.ContainsGCPointers(typeHandle)); - Assert.Empty(contract.GetGCDescSeries(typeHandle)); + + uint baseSize = contract.GetBaseSize(typeHandle); + (uint SeriesOffset, uint SeriesSize)[] series = contract.GetGCDescSeries(typeHandle, baseSize).ToArray(); + Assert.Single(series); + Assert.Equal(expectedOffset, series[0].SeriesOffset); + Assert.Equal(expectedSize, series[0].SeriesSize); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetGCDescSeriesReturnsMultipleValueClassSeries(MockTarget.Architecture arch) + { + // Two val_serie_items: [2 ptrs, skip 8 bytes] [1 ptr, skip 0 bytes] + // This models a value type like struct { ref a; ref b; int pad1; int pad2; ref c; } + TargetPointer mtPtr = default; + (uint SeriesOffset, uint SeriesSize)[] expectedSeries = []; + + TestPlaceholderTarget target = CreateTarget( + arch, + rtsBuilder => + { + TargetTestHelpers helpers = rtsBuilder.Builder.TargetTestHelpers; + uint pointerSize = (uint)helpers.PointerSize; + + uint startOffset = helpers.ObjHeaderSize + 2u * pointerSize; + uint baseSize = helpers.ObjHeaderSize + 3u * pointerSize; + + uint skip = 2u * pointerSize; // two pointer-sized non-ref fields between runs + + MockEEClass eeClass = rtsBuilder.AddEEClass("ValueClassArray_2runs"); + MockMethodTable mt = rtsBuilder.AddMethodTableWithValueClassGCDesc( + "ValueClassArray_2runs", + baseSize, + startOffset, + [(2, skip), (1, 0)]); // first: 2 ptrs then skip, second: 1 ptr no skip + mt.MTFlags |= 0x01000000u; // ContainsGCPointers + mt.ParentMethodTable = rtsBuilder.SystemObjectMethodTable.Address; + mt.NumVirtuals = 3; + eeClass.MethodTable = mt.Address; + mt.EEClassOrCanonMT = eeClass.Address; + mtPtr = mt.Address; + + expectedSeries = + [ + (startOffset, 2u * pointerSize), // first run: 2 ptrs + (startOffset + 2u * pointerSize + skip, 1u * pointerSize), // second run: 1 ptr after skip + ]; + }); + + IRuntimeTypeSystem contract = target.Contracts.RuntimeTypeSystem; + Contracts.TypeHandle typeHandle = contract.GetTypeHandle(mtPtr); + + uint baseSize = contract.GetBaseSize(typeHandle); + (uint SeriesOffset, uint SeriesSize)[] series = contract.GetGCDescSeries(typeHandle, baseSize).ToArray(); + Assert.Equal(expectedSeries.Length, series.Length); + for (int i = 0; i < expectedSeries.Length; i++) + { + Assert.Equal(expectedSeries[i].SeriesOffset, series[i].SeriesOffset); + Assert.Equal(expectedSeries[i].SeriesSize, series[i].SeriesSize); + } } } diff --git a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.RuntimeTypeSystem.cs b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.RuntimeTypeSystem.cs index a22b4184c0bfb2..6eb3faec8c3b9b 100644 --- a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.RuntimeTypeSystem.cs +++ b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.RuntimeTypeSystem.cs @@ -316,8 +316,8 @@ public class RuntimeTypeSystem internal ulong ContinuationMethodTableGlobalAddress => TestContinuationMethodTableGlobalAddress; internal ulong MethodDescAlignment => GetMethodDescAlignment(Builder.TargetTestHelpers); internal ulong ArrayBaseSize => Builder.TargetTestHelpers.ArrayBaseBaseSize; - // OFFSETOF__CORINFO_Continuation__data = sizeof(MT*) + sizeof(Next*) + sizeof(Resume*) + 8 (Flags+State) - internal ulong OffsetOfContinuationData => 3ul * (ulong)Builder.TargetTestHelpers.PointerSize + 8; + // sizeof(ContinuationObject) = sizeof(MT*) + sizeof(Next*) + sizeof(Resume*) + sizeof(Flags) + sizeof(State) + internal uint ContinuationObjectSize => 3u * (uint)Builder.TargetTestHelpers.PointerSize + 8; public RuntimeTypeSystem(MockMemorySpace.Builder builder) : this(builder, (DefaultAllocationRangeStart, DefaultAllocationRangeEnd)) @@ -502,5 +502,80 @@ internal MockMethodTable AddMethodTableWithGCDesc(string name, uint baseSize, (u mt.BaseSize = baseSize; return mt; } + + /// + /// Allocates a method table together with a value-class (repeating) CGCDesc immediately before it. + /// + /// Descriptive name for the allocation. + /// Value to store in the method table's BaseSize field. + /// The startoffset field of the CGCDescSeries (offset from object start). + /// + /// The val_serie_item entries, each as (nptrs, skip). + /// Index 0 corresponds to val_serie[0] (overlapping seriessize in the union). + /// + /// + /// A whose address is after the GCDesc bytes. + /// The caller is responsible for setting additional method table flags (e.g. + /// ContainsGCPointers = 0x01000000) and linking to an EEClass. + /// + internal MockMethodTable AddMethodTableWithValueClassGCDesc(string name, uint baseSize, ulong startOffset, (uint Nptrs, uint Skip)[] valSeries) + { + TargetTestHelpers helpers = Builder.TargetTestHelpers; + int pointerSize = helpers.PointerSize; + int halfSize = pointerSize / 2; + + // Value-class GCDesc layout (each slot is pointer-sized unless noted): + // [ val_serie[N-1] ] <- lowest address (fragment start) + // ... + // [ val_serie[0] ] <- overlaps seriessize in CGCDescSeries union + // [ startoffset ] <- one ptrSize slot + // [ NumSeries (-N) ] <- one ptrSize slot (negative) + // [ MethodTable data starts here ] + // + // ComputeSizeRepeating = sizeof(size_t) + sizeof(CGCDescSeries) + (N-1)*sizeof(val_serie_item) + // = ptrSize + 2*ptrSize + (N-1)*ptrSize = (N+2)*ptrSize + int gcDescSize = (valSeries.Length + 2) * pointerSize; + int totalSize = gcDescSize + MethodTableLayout.Size; + + MockMemorySpace.HeapFragment fragment = TypeSystemAllocator.Allocate((ulong)totalSize, $"ValueClassGCDesc+MethodTable '{name}'"); + + // Write val_serie items. val_serie[0] is closest to MT (highest address in the GCDesc region), + // val_serie[N-1] is farthest (lowest address). + // Each val_serie_item is { HALF_SIZE_T nptrs; HALF_SIZE_T skip; } + for (int i = 0; i < valSeries.Length; i++) + { + int itemOffset = (valSeries.Length - 1 - i) * pointerSize; + if (pointerSize == sizeof(uint)) + { + helpers.Write(fragment.Data.AsSpan(itemOffset, halfSize), (ushort)valSeries[i].Nptrs); + helpers.Write(fragment.Data.AsSpan(itemOffset + halfSize, halfSize), (ushort)valSeries[i].Skip); + } + else + { + helpers.Write(fragment.Data.AsSpan(itemOffset, halfSize), valSeries[i].Nptrs); + helpers.Write(fragment.Data.AsSpan(itemOffset + halfSize, halfSize), valSeries[i].Skip); + } + } + + // Write startoffset + helpers.WritePointer(fragment.Data.AsSpan(valSeries.Length * pointerSize, pointerSize), startOffset); + + // Write NumSeries as a negative value (-N) + long negativeCount = -valSeries.Length; + if (pointerSize == sizeof(uint)) + { + helpers.Write(fragment.Data.AsSpan((valSeries.Length + 1) * pointerSize, pointerSize), (int)negativeCount); + } + else + { + helpers.WritePointer(fragment.Data.AsSpan((valSeries.Length + 1) * pointerSize, pointerSize), unchecked((ulong)negativeCount)); + } + + // The MockMethodTable lives at offset gcDescSize within the combined fragment + ulong mtAddress = fragment.Address + (ulong)gcDescSize; + MockMethodTable mt = MethodTableLayout.Create(fragment.Data.AsMemory(gcDescSize, MethodTableLayout.Size), mtAddress); + mt.BaseSize = baseSize; + return mt; + } } } From c21229efb0ae1e85419b7e2bbf98006dcecd9c1b Mon Sep 17 00:00:00 2001 From: rcj1 Date: Mon, 27 Apr 2026 14:36:48 -0700 Subject: [PATCH 3/4] update outer loop --- .../design/datacontracts/RuntimeTypeSystem.md | 8 ++++ .../Contracts/RuntimeTypeSystem_1.cs | 37 ++++++++++--------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/docs/design/datacontracts/RuntimeTypeSystem.md b/docs/design/datacontracts/RuntimeTypeSystem.md index e1a400a93e5633..335e0007cc6105 100644 --- a/docs/design/datacontracts/RuntimeTypeSystem.md +++ b/docs/design/datacontracts/RuntimeTypeSystem.md @@ -601,6 +601,14 @@ Contracts used: // HALF_SIZE_T is uint16 on 32-bit, uint32 on 64-bit. // Read nptrs and skip as separate typed reads for endianness safety. // runBytes = nptrs * pointerSize; advance currentOffset by runBytes + skip after each item. + // + // The val_serie_items describe the GC pointer layout of a single array element. + // An outer loop repeats the pattern across all elements in the array: + // currentOffset = startoffset + // while currentOffset <= objectSize - pointerSize: + // for each val_serie_item: + // yield (currentOffset, nptrs * pointerSize) + // currentOffset += nptrs * pointerSize + skip } public bool IsDynamicStatics(TypeHandle TypeHandle) => !typeHandle.IsMethodTable() ? false : _methodTables[TypeHandle.Address].Flags.IsDynamicStatics; 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 cbab68915bcb70..4dc7f575794991 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 @@ -601,26 +601,29 @@ public bool IsContinuation(TypeHandle typeHandle) => typeHandle.IsMethodTable() ulong startOffset = _target.ReadPointer(mtAddress - 2 * pointerSize).Value; ulong currentOffset = startOffset; - for (long i = 0; i < absNumSeries; i++) + while (currentOffset <= objectSize - pointerSize) { - ulong itemAddress = mtAddress - (3 + (ulong)i) * pointerSize; - - // Read val_serie_item fields individually for endianness safety. - uint nptrs, skip; - if (_target.PointerSize == sizeof(uint)) - { - nptrs = _target.Read(itemAddress); - skip = _target.Read(itemAddress + sizeof(ushort)); - } - else + for (long i = 0; i < absNumSeries; i++) { - nptrs = _target.Read(itemAddress); - skip = _target.Read(itemAddress + sizeof(uint)); - } + ulong itemAddress = mtAddress - (3 + (ulong)i) * pointerSize; + + // Read val_serie_item fields individually for endianness safety. + uint nptrs, skip; + if (_target.PointerSize == sizeof(uint)) + { + nptrs = _target.Read(itemAddress); + skip = _target.Read(itemAddress + sizeof(ushort)); + } + else + { + nptrs = _target.Read(itemAddress); + skip = _target.Read(itemAddress + sizeof(uint)); + } - uint runBytes = nptrs * (uint)pointerSize; - yield return ((uint)currentOffset, runBytes); - currentOffset += runBytes + skip; + uint runBytes = nptrs * (uint)pointerSize; + yield return ((uint)currentOffset, runBytes); + currentOffset += runBytes + skip; + } } } } From 1cc0f46d3b964586db712b4d743f8845f1033fa7 Mon Sep 17 00:00:00 2001 From: rcj1 Date: Tue, 28 Apr 2026 16:19:10 -0700 Subject: [PATCH 4/4] adding component size param and adding test --- .../design/datacontracts/RuntimeTypeSystem.md | 6 +- .../Contracts/IRuntimeTypeSystem.cs | 2 +- .../Contracts/RuntimeTypeSystem_1.cs | 6 +- .../TypeNameBuilder.cs | 2 +- .../managed/cdac/tests/MethodTableTests.cs | 148 ++++++++++++++++-- 5 files changed, 149 insertions(+), 15 deletions(-) diff --git a/docs/design/datacontracts/RuntimeTypeSystem.md b/docs/design/datacontracts/RuntimeTypeSystem.md index 335e0007cc6105..641b259d50dac6 100644 --- a/docs/design/datacontracts/RuntimeTypeSystem.md +++ b/docs/design/datacontracts/RuntimeTypeSystem.md @@ -59,7 +59,7 @@ partial interface IRuntimeTypeSystem : IContract // True if the MethodTable represents a continuation type used by the async continuation feature public virtual bool IsContinuation(TypeHandle typeHandle); // Returns the GC pointer runs for the method table as (offset, size) pairs normalized to actual byte lengths. - public virtual IEnumerable<(uint SeriesOffset, uint SeriesSize)> GetGCDescSeries(TypeHandle typeHandle, uint objectSize); + public virtual IEnumerable<(uint SeriesOffset, uint SeriesSize)> GetGCDescSeries(TypeHandle typeHandle, uint numComponents = 0); public virtual bool IsDynamicStatics(TypeHandle typeHandle); public virtual ushort GetNumInterfaces(TypeHandle typeHandle); @@ -560,9 +560,11 @@ Contracts used: && ContinuationMethodTablePointer != TargetPointer.Null && _methodTables[typeHandle.Address].ParentMethodTable == ContinuationMethodTablePointer; - IEnumerable<(uint SeriesOffset, uint SeriesSize)> GetGCDescSeries(TypeHandle typeHandle, uint objectSize) + IEnumerable<(uint SeriesOffset, uint SeriesSize)> GetGCDescSeries(TypeHandle typeHandle, uint numComponents = 0) { // Returns empty if not a method table or has no GC pointers. + // Compute objectSize: baseSize + numComponents * componentSize. + // For non-array types, numComponents == 0 so objectSize == baseSize. // Read NumSeries from (mtAddress - pointerSize), sign-extended to native width. // NumSeries == 0 → empty. diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs index 73a98b6a9d8c1e..65acb76b7523e6 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs @@ -126,7 +126,7 @@ public interface IRuntimeTypeSystem : IContract /// Returns (offset, size) pairs normalized to actual byte lengths. /// See RuntimeTypeSystem.md for the full GCDesc format documentation. /// - IEnumerable<(uint SeriesOffset, uint SeriesSize)> GetGCDescSeries(TypeHandle typeHandle, uint objectSize) => throw new NotImplementedException(); + IEnumerable<(uint SeriesOffset, uint SeriesSize)> GetGCDescSeries(TypeHandle typeHandle, uint numComponents = 0) => throw new NotImplementedException(); bool IsDynamicStatics(TypeHandle typeHandle) => throw new NotImplementedException(); ushort GetNumInterfaces(TypeHandle typeHandle) => throw new NotImplementedException(); 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 4dc7f575794991..2351b8338f8ae7 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 @@ -565,7 +565,7 @@ public bool IsContinuation(TypeHandle typeHandle) => typeHandle.IsMethodTable() && _continuationMethodTablePointer != TargetPointer.Null && _methodTables[typeHandle.Address].ParentMethodTable == _continuationMethodTablePointer; - IEnumerable<(uint SeriesOffset, uint SeriesSize)> IRuntimeTypeSystem.GetGCDescSeries(TypeHandle typeHandle, uint objectSize) + IEnumerable<(uint SeriesOffset, uint SeriesSize)> IRuntimeTypeSystem.GetGCDescSeries(TypeHandle typeHandle, uint numComponents) { if (!typeHandle.IsMethodTable()) yield break; @@ -573,6 +573,10 @@ public bool IsContinuation(TypeHandle typeHandle) => typeHandle.IsMethodTable() if (!ContainsGCPointers(typeHandle)) yield break; + uint baseSize = GetBaseSize(typeHandle); + uint componentSize = GetComponentSize(typeHandle); + uint objectSize = baseSize + numComponents * componentSize; + ulong mtAddress = typeHandle.Address; ulong pointerSize = (ulong)_target.PointerSize; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/TypeNameBuilder.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/TypeNameBuilder.cs index 5558e4aac72a39..0bd9de10466510 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/TypeNameBuilder.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/TypeNameBuilder.cs @@ -520,7 +520,7 @@ private static void AppendContinuationName(ref TypeNameBuilder tnb, IRuntimeType var name = new StringBuilder("Continuation_"); name.Append(dataSize); - foreach ((uint seriesOffset, uint seriesSize) in typeSystemContract.GetGCDescSeries(typeHandle, baseSize)) + foreach ((uint seriesOffset, uint seriesSize) in typeSystemContract.GetGCDescSeries(typeHandle)) { if (seriesOffset < continuationDataOffset) continue; diff --git a/src/native/managed/cdac/tests/MethodTableTests.cs b/src/native/managed/cdac/tests/MethodTableTests.cs index 0089c08d2ba049..16453bd68399a9 100644 --- a/src/native/managed/cdac/tests/MethodTableTests.cs +++ b/src/native/managed/cdac/tests/MethodTableTests.cs @@ -667,7 +667,7 @@ public void GetGCDescSeriesReturnsEmptyForNonMethodTable(MockTarget.Architecture IRuntimeTypeSystem contract = target.Contracts.RuntimeTypeSystem; Contracts.TypeHandle typeDescHandle = contract.GetTypeHandle(typeDescAddress); - Assert.Empty(contract.GetGCDescSeries(typeDescHandle, 0)); + Assert.Empty(contract.GetGCDescSeries(typeDescHandle)); } [Theory] @@ -694,7 +694,7 @@ public void GetGCDescSeriesReturnsEmptyWhenNoGCPointers(MockTarget.Architecture IRuntimeTypeSystem contract = target.Contracts.RuntimeTypeSystem; Contracts.TypeHandle typeHandle = contract.GetTypeHandle(mtPtr); Assert.False(contract.ContainsGCPointers(typeHandle)); - Assert.Empty(contract.GetGCDescSeries(typeHandle, 0)); + Assert.Empty(contract.GetGCDescSeries(typeHandle)); } [Theory] @@ -742,8 +742,7 @@ public void GetGCDescSeriesReturnsSingleSeries(MockTarget.Architecture arch) Contracts.TypeHandle typeHandle = contract.GetTypeHandle(mtPtr); Assert.True(contract.ContainsGCPointers(typeHandle)); - uint baseSize = contract.GetBaseSize(typeHandle); - (uint SeriesOffset, uint SeriesSize)[] series = contract.GetGCDescSeries(typeHandle, baseSize).ToArray(); + (uint SeriesOffset, uint SeriesSize)[] series = contract.GetGCDescSeries(typeHandle).ToArray(); Assert.Single(series); Assert.Equal(expectedSeriesOffset, series[0].SeriesOffset); Assert.Equal(expectedSeriesSize, series[0].SeriesSize); @@ -797,8 +796,7 @@ public void GetGCDescSeriesReturnsMultipleSeriesInOrder(MockTarget.Architecture IRuntimeTypeSystem contract = target.Contracts.RuntimeTypeSystem; Contracts.TypeHandle typeHandle = contract.GetTypeHandle(mtPtr); - uint baseSize = contract.GetBaseSize(typeHandle); - (uint SeriesOffset, uint SeriesSize)[] series = contract.GetGCDescSeries(typeHandle, baseSize).ToArray(); + (uint SeriesOffset, uint SeriesSize)[] series = contract.GetGCDescSeries(typeHandle).ToArray(); Assert.Equal(expectedSeries.Length, series.Length); for (int i = 0; i < expectedSeries.Length; i++) { @@ -850,8 +848,7 @@ public void GetGCDescSeriesReturnsSingleValueClassSeries(MockTarget.Architecture Contracts.TypeHandle typeHandle = contract.GetTypeHandle(mtPtr); Assert.True(contract.ContainsGCPointers(typeHandle)); - uint baseSize = contract.GetBaseSize(typeHandle); - (uint SeriesOffset, uint SeriesSize)[] series = contract.GetGCDescSeries(typeHandle, baseSize).ToArray(); + (uint SeriesOffset, uint SeriesSize)[] series = contract.GetGCDescSeries(typeHandle).ToArray(); Assert.Single(series); Assert.Equal(expectedOffset, series[0].SeriesOffset); Assert.Equal(expectedSize, series[0].SeriesSize); @@ -901,8 +898,7 @@ public void GetGCDescSeriesReturnsMultipleValueClassSeries(MockTarget.Architectu IRuntimeTypeSystem contract = target.Contracts.RuntimeTypeSystem; Contracts.TypeHandle typeHandle = contract.GetTypeHandle(mtPtr); - uint baseSize = contract.GetBaseSize(typeHandle); - (uint SeriesOffset, uint SeriesSize)[] series = contract.GetGCDescSeries(typeHandle, baseSize).ToArray(); + (uint SeriesOffset, uint SeriesSize)[] series = contract.GetGCDescSeries(typeHandle).ToArray(); Assert.Equal(expectedSeries.Length, series.Length); for (int i = 0; i < expectedSeries.Length; i++) { @@ -910,4 +906,136 @@ public void GetGCDescSeriesReturnsMultipleValueClassSeries(MockTarget.Architectu Assert.Equal(expectedSeries[i].SeriesSize, series[i].SeriesSize); } } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetGCDescSeriesRegularSeriesWithArrayNumComponents(MockTarget.Architecture arch) + { + // object[] has a single regular series. When numComponents > 0, the series size + // should extend across all elements: rawSeriesSize + baseSize + numComponents * componentSize. + TargetPointer mtPtr = default; + uint expectedSeriesOffset = 0; + + TestPlaceholderTarget target = CreateTarget( + arch, + rtsBuilder => + { + TargetTestHelpers helpers = rtsBuilder.Builder.TargetTestHelpers; + uint pointerSize = (uint)helpers.PointerSize; + + // object[] layout: [ObjHeader][MT*][Length][elem0][elem1][elem2] + // baseSize covers the header: ObjHeader + MT* + Length = 3 * pointerSize + // componentSize = pointerSize (each element is a reference) + uint baseSize = helpers.ObjHeaderSize + 2u * pointerSize; // ObjHeader + MT* + Length field + uint componentSize = pointerSize; + + // One series starting after ObjHeader+MT*+Length, covering element slots. + // rawSeriesSize is stored as (actualRunForOneElement - baseSize). + // For object[], the series covers from first element to end: actualRun = pointerSize (per-element). + // But the raw value encodes (pointerSize - baseSize) so that rawSeriesSize + objectSize gives total span. + ulong rawSeriesSize = pointerSize - baseSize; // wraps unsigned + ulong seriesOffset = helpers.ObjHeaderSize + 2u * pointerSize; // after header + length + + MockEEClass eeClass = rtsBuilder.AddEEClass("ObjectArray"); + MockMethodTable mt = rtsBuilder.AddMethodTableWithGCDesc( + "ObjectArray", + baseSize, + [(rawSeriesSize, seriesOffset)]); + // Set array flags: HasComponentSize | Category_Array | componentSize in low bits + mt.MTFlags = (uint)(MethodTableFlags_1.WFLAGS_HIGH.HasComponentSize + | MethodTableFlags_1.WFLAGS_HIGH.Category_Array) + | componentSize + | 0x01000000u; // ContainsGCPointers + mt.ParentMethodTable = rtsBuilder.SystemObjectMethodTable.Address; + mt.NumVirtuals = 3; + eeClass.MethodTable = mt.Address; + mt.EEClassOrCanonMT = eeClass.Address; + + mtPtr = mt.Address; + expectedSeriesOffset = (uint)seriesOffset; + }); + + IRuntimeTypeSystem contract = target.Contracts.RuntimeTypeSystem; + Contracts.TypeHandle typeHandle = contract.GetTypeHandle(mtPtr); + Assert.True(contract.ContainsGCPointers(typeHandle)); + uint pointerSz = (uint)arch.PointerSize; + + // With 0 components, series size = rawSeriesSize + baseSize = pointerSize (one element worth) + (uint SeriesOffset, uint SeriesSize)[] series0 = contract.GetGCDescSeries(typeHandle, 0).ToArray(); + Assert.Single(series0); + Assert.Equal(expectedSeriesOffset, series0[0].SeriesOffset); + Assert.Equal(pointerSz, series0[0].SeriesSize); + + // With 3 components, objectSize = baseSize + 3*pointerSize, so series size = pointerSize - baseSize + objectSize = 4*pointerSize + uint numComponents = 3; + (uint SeriesOffset, uint SeriesSize)[] series3 = contract.GetGCDescSeries(typeHandle, numComponents).ToArray(); + Assert.Single(series3); + Assert.Equal(expectedSeriesOffset, series3[0].SeriesOffset); + Assert.Equal((numComponents + 1) * pointerSz, series3[0].SeriesSize); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetGCDescSeriesValueClassRepeatingWithArrayNumComponents(MockTarget.Architecture arch) + { + // Array of structs where each element has one GC ref (nptrs=1, skip=pointerSize for a non-ref field). + // With numComponents > 0, the repeating pattern should iterate across multiple elements. + TargetPointer mtPtr = default; + + TestPlaceholderTarget target = CreateTarget( + arch, + rtsBuilder => + { + TargetTestHelpers helpers = rtsBuilder.Builder.TargetTestHelpers; + uint pointerSize = (uint)helpers.PointerSize; + + // Array layout: [ObjHeader][MT*][Length][elem0.ref][elem0.int][elem1.ref][elem1.int]... + // Each element is { ref field, int field } = 2 * pointerSize. + uint baseSize = helpers.ObjHeaderSize + 2u * pointerSize; // header + length + uint componentSize = 2u * pointerSize; + uint startOffset = helpers.ObjHeaderSize + 2u * pointerSize; // first element starts after header + + MockEEClass eeClass = rtsBuilder.AddEEClass("StructArray"); + MockMethodTable mt = rtsBuilder.AddMethodTableWithValueClassGCDesc( + "StructArray", + baseSize, + startOffset, + [(1, pointerSize)]); // nptrs=1 ref, skip=pointerSize (non-ref field) + mt.MTFlags = (uint)(MethodTableFlags_1.WFLAGS_HIGH.HasComponentSize + | MethodTableFlags_1.WFLAGS_HIGH.Category_Array) + | componentSize + | 0x01000000u; // ContainsGCPointers + mt.ParentMethodTable = rtsBuilder.SystemObjectMethodTable.Address; + mt.NumVirtuals = 3; + eeClass.MethodTable = mt.Address; + mt.EEClassOrCanonMT = eeClass.Address; + + mtPtr = mt.Address; + }); + + IRuntimeTypeSystem contract = target.Contracts.RuntimeTypeSystem; + Contracts.TypeHandle typeHandle = contract.GetTypeHandle(mtPtr); + Assert.True(contract.ContainsGCPointers(typeHandle)); + uint pointerSz = (uint)arch.PointerSize; + uint elemSize = 2u * pointerSz; + uint startOff = (uint)arch.ObjectHeaderSize + 2u * pointerSz; + + // With 0 components, objectSize = baseSize. The while loop may execute 0 or 1 times + // depending on whether startOffset <= baseSize - pointerSize. + // baseSize = ObjHeaderSize + 2*ptr, startOffset = ObjHeaderSize + 2*ptr. + // objectSize - pointerSize = baseSize - pointerSize = ObjHeaderSize + pointerSize. + // startOffset (ObjHeaderSize + 2*ptr) > ObjHeaderSize + ptr, so: 0 iterations. + (uint SeriesOffset, uint SeriesSize)[] series0 = contract.GetGCDescSeries(typeHandle, 0).ToArray(); + Assert.Empty(series0); + + // With 2 components, objectSize = baseSize + 2 * elemSize = baseSize + 4*ptr. + // The loop should produce 2 runs (one per element), each at the ref field of that element. + uint numComponents = 2; + (uint SeriesOffset, uint SeriesSize)[] series2 = contract.GetGCDescSeries(typeHandle, numComponents).ToArray(); + Assert.Equal(2, series2.Length); + Assert.Equal(startOff, series2[0].SeriesOffset); + Assert.Equal(pointerSz, series2[0].SeriesSize); + Assert.Equal(startOff + elemSize, series2[1].SeriesOffset); + Assert.Equal(pointerSz, series2[1].SeriesSize); + } }