From 5e5874ea0a1a2890cbf4354c1d3060d825c4a58d Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Tue, 7 Jul 2026 15:24:57 -0400 Subject: [PATCH 1/7] [cdac] Read runtime-cached SystemV-AMD64 struct classification; enable ARGITER + zero-known-issues GCREFS on Linux x64 Enables ARGITER stress verification and zero-known-issue GCREFS on Linux x64 by reading the SystemV-AMD64 struct-in-register classification that the runtime already caches on EEClass, plus supporting stress harness plumbing so ComputeCallRefMap can serve as the byte-for-byte oracle for cDAC's TryComputeArgGCRefMapBlob. Approach: instead of porting the classifier from crossgen2, read the data the runtime already computes. The runtime's MethodTableBuilder::StoreEightByteClassification (called from SystemVAmd64CheckForPassStructInRegister during MethodTable construction) populates EEClass::m_eightByteRegistersInfo (inside EEClassOptionalFields) with the enregisterable-struct classification: NumEightBytes, per-eightbyte classification tags, per-eightbyte sizes. Runtime callers (arg iterator, JIT-EE interface) read that cached info via SystemVRegDescriptorFromSystemVEightByteRegistersInfo instead of re-running ClassifyEightBytes. The cDAC does the same. Non-enregisterable structs (too large, or classified as SSEUP/X87/Memory) never reach StoreEightByteClassification, so their descriptor reads back with NumEightBytes == 0. That's the natural "passedInRegisters = false" signal the cDAC surfaces. Native P/Invoke layouts are not cached here (the runtime classifies them on demand); the cDAC never walks native transition frames for GC scanning so this is fine. Because the classifier compares against ComputeCallRefMap (which uses the runtime path), a descriptor-read implementation matches byte-for- byte by construction. No merge-rule / intrinsic-rejection / Int128 divergence to reason about. C++ descriptor plumbing: * class.h: cdac_data::OptionalFields, plus cdac_data (UNIX_AMD64_ABI-guarded) with the EightByteRegistersInfo offset. Friend declaration on EEClassOptionalFields so offsetof can reach the private field. * methodtable.h: cdac_data exposes NumEightBytes plus the two byte[2] eightbyte tables. Friend struct on the class. * datadescriptor.inc: EEClass.OptionalFields field + new EEClassOptionalFields and SystemVEightByteRegistersInfo types, both ifdef'd on UNIX_AMD64_ABI. Managed side: * DataType: new EEClassOptionalFields and SystemVEightByteRegistersInfo enum entries. * Data.EEClass: OptionalFields TargetPointer field. * Data.EEClassOptionalFields: [Field] SystemVEightByteRegistersInfo inline sub-IData. * Data.SystemVEightByteRegistersInfo: [Field] NumEightBytes; OnInit- populated arrays for the two eightbyte tables. * CdacTypeHandle.GetSystemVAmd64PassStructInRegisterDescriptor: ~40 LOC inline read of EEClass -> OptionalFields -> EightByteRegistersInfo, then projects into SYSTEMV_AMD64_CORINFO_STRUCT_REG_PASSING_DESCRIPTOR with eightByteOffsets synthesized as i * 8 (matches the runtime conversion). * MockDescriptors.RuntimeTypeSystem: mock EEClass layout gains OptionalFields so unit tests build the same shape. Stress harness plumbing (unchanged from earlier iterations): * CdacStressTestBase.AssertAllPassed folds KnownIssues into the Failed check, so any surviving known-issue count is a regression. * CdacStressTests.cs no longer gates ARGITER by platform. * GcScanner.PromoteCallerStack: no platform gate; always tries TryComputeArgGCRefMapBlob first. * Debuggees/CallSignatures: SysVCategory covers the interesting enregisterable and stack-passed shapes (Int/Float pairs, single/ multi-ref, nested, Span, three-refs stack, Int128, empty, unions, small unaligned, single-field wrapper), plus intrinsic Vector rejection. Windows unit tests: all 2646 cDAC unit tests pass. Linux x64 stress: TBD (this branch push triggers PR CI). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/class.h | 9 ++ .../vm/datadescriptor/datadescriptor.inc | 17 +++ src/coreclr/vm/methodtable.h | 9 ++ .../CallingConvention/CdacTypeHandle.cs | 42 ++++++- .../Contracts/StackWalk/GC/GcScanner.cs | 21 ---- .../Data/EEClass.cs | 1 + .../Data/EEClassOptionalFields.cs | 10 ++ .../Data/SystemVEightByteRegistersInfo.cs | 22 ++++ .../DataType.cs | 2 + src/native/managed/cdac/gen/Parser.cs | 9 ++ .../tests/StressTests/CdacStressTestBase.cs | 33 +----- .../cdac/tests/StressTests/CdacStressTests.cs | 24 +--- .../Debuggees/CallSignatures/Program.cs | 108 +++++++++++++++++- .../MockDescriptors.RuntimeTypeSystem.cs | 2 + 14 files changed, 235 insertions(+), 74 deletions(-) create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/EEClassOptionalFields.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/SystemVEightByteRegistersInfo.cs diff --git a/src/coreclr/vm/class.h b/src/coreclr/vm/class.h index cb0aa94f1650ac..131dd6ebba0c37 100644 --- a/src/coreclr/vm/class.h +++ b/src/coreclr/vm/class.h @@ -613,6 +613,7 @@ class EEClassOptionalFields // for MethodTableBuilder and NativeImageDumper, which need raw field-level access. friend class EEClass; friend class MethodTableBuilder; + friend struct ::cdac_data; // // GENERICS RELATED FIELDS. @@ -1785,6 +1786,14 @@ template<> struct cdac_data static constexpr size_t NumThreadStaticFields = offsetof(EEClass, m_NumThreadStaticFields); static constexpr size_t NumNonVirtualSlots = offsetof(EEClass, m_NumNonVirtualSlots); static constexpr size_t BaseSizePadding = offsetof(EEClass, m_cbBaseSizePadding); + static constexpr size_t OptionalFields = offsetof(EEClass, m_rpOptionalFields); +}; + +template<> struct cdac_data +{ +#if defined(UNIX_AMD64_ABI) + static constexpr size_t EightByteRegistersInfo = offsetof(EEClassOptionalFields, m_eightByteRegistersInfo); +#endif // UNIX_AMD64_ABI }; // -------------------------------------------------------------------------------------------- diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index 8481dd43b88885..6baf938e0911e8 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -572,8 +572,25 @@ CDAC_TYPE_FIELD(EEClass, T_UINT16, NumStaticFields, cdac_data::NumStati CDAC_TYPE_FIELD(EEClass, T_UINT16, NumThreadStaticFields, cdac_data::NumThreadStaticFields) CDAC_TYPE_FIELD(EEClass, T_UINT16, NumNonVirtualSlots, cdac_data::NumNonVirtualSlots) CDAC_TYPE_FIELD(EEClass, T_UINT8, BaseSizePadding, cdac_data::BaseSizePadding) +CDAC_TYPE_FIELD(EEClass, T_POINTER, OptionalFields, cdac_data::OptionalFields) CDAC_TYPE_END(EEClass) +CDAC_TYPE_BEGIN(EEClassOptionalFields) +CDAC_TYPE_INDETERMINATE(EEClassOptionalFields) +#ifdef UNIX_AMD64_ABI +CDAC_TYPE_FIELD(EEClassOptionalFields, TYPE(SystemVEightByteRegistersInfo), EightByteRegistersInfo, cdac_data::EightByteRegistersInfo) +#endif // UNIX_AMD64_ABI +CDAC_TYPE_END(EEClassOptionalFields) + +#ifdef UNIX_AMD64_ABI +CDAC_TYPE_BEGIN(SystemVEightByteRegistersInfo) +CDAC_TYPE_INDETERMINATE(SystemVEightByteRegistersInfo) +CDAC_TYPE_FIELD(SystemVEightByteRegistersInfo, T_UINT8, NumEightBytes, cdac_data::NumEightBytes) +CDAC_TYPE_FIELD(SystemVEightByteRegistersInfo, T_UINT8, EightByteClassifications, cdac_data::EightByteClassifications) +CDAC_TYPE_FIELD(SystemVEightByteRegistersInfo, T_UINT8, EightByteSizes, cdac_data::EightByteSizes) +CDAC_TYPE_END(SystemVEightByteRegistersInfo) +#endif // UNIX_AMD64_ABI + CDAC_TYPE_BEGIN(GenericsDictInfo) CDAC_TYPE_INDETERMINATE(GenericsDictInfo) CDAC_TYPE_FIELD(GenericsDictInfo, T_UINT16, NumDicts, offsetof(GenericsDictInfo, m_wNumDicts)) diff --git a/src/coreclr/vm/methodtable.h b/src/coreclr/vm/methodtable.h index 563cc497ec4d94..7776bc6e12b085 100644 --- a/src/coreclr/vm/methodtable.h +++ b/src/coreclr/vm/methodtable.h @@ -921,6 +921,15 @@ struct SystemVEightByteRegistersInfo _ASSERTE(index < NumEightBytes); return EightByteSizes[index]; } + + friend struct ::cdac_data; +}; + +template<> struct cdac_data +{ + static constexpr size_t NumEightBytes = offsetof(SystemVEightByteRegistersInfo, NumEightBytes); + static constexpr size_t EightByteClassifications = offsetof(SystemVEightByteRegistersInfo, EightByteClassifications); + static constexpr size_t EightByteSizes = offsetof(SystemVEightByteRegistersInfo, EightByteSizes); }; #endif diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CdacTypeHandle.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CdacTypeHandle.cs index 41988ead99c5a7..90226837a9bb7b 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CdacTypeHandle.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CdacTypeHandle.cs @@ -121,9 +121,49 @@ public int GetHomogeneousAggregateElementSize() public void GetSystemVAmd64PassStructInRegisterDescriptor(out SYSTEMV_AMD64_CORINFO_STRUCT_REG_PASSING_DESCRIPTOR descriptor) { - throw new NotImplementedException("SystemV AMD64 struct-in-registers is not yet supported by the cDAC."); + descriptor = default; + descriptor.passedInRegisters = false; + + if (_typeHandle.IsNull) + return; + + // The runtime pre-computes the eightbyte classification for every managed + // value type at MethodTable construction and caches it in + // EEClass::m_eightByteRegistersInfo (inside EEClassOptionalFields). We + // read that cached descriptor instead of re-classifying, mirroring + // jitinterface.cpp::SystemVRegDescriptorFromSystemVEightByteRegistersInfo. + // + // The runtime only allocates m_eightByteRegistersInfo on UNIX_AMD64_ABI + // builds; on other targets the field is absent from the + // EEClassOptionalFields descriptor and EightByteRegistersInfo below is + // null. + TargetPointer eeClassPtr = Rts.GetClassPointer(_typeHandle); + if (eeClassPtr == TargetPointer.Null) + return; + + Data.EEClass eeClass = _target.ProcessedData.GetOrAdd(eeClassPtr); + if (eeClass.OptionalFields == TargetPointer.Null) + return; + + Data.EEClassOptionalFields optFields = _target.ProcessedData.GetOrAdd(eeClass.OptionalFields); + if (optFields.EightByteRegistersInfo is not Data.SystemVEightByteRegistersInfo info) + return; // Non-Unix-x64 target: descriptor doesn't include this field. + + if (info.NumEightBytes == 0) + return; // Runtime marked this struct as not-enregisterable. + + descriptor.passedInRegisters = true; + descriptor.eightByteCount = info.NumEightBytes; + descriptor.eightByteClassifications0 = (SystemVClassificationType)info.EightByteClassifications[0]; + descriptor.eightByteSizes0 = info.EightByteSizes[0]; + descriptor.eightByteOffsets0 = 0; + descriptor.eightByteClassifications1 = (SystemVClassificationType)info.EightByteClassifications[1]; + descriptor.eightByteSizes1 = info.EightByteSizes[1]; + descriptor.eightByteOffsets1 = SystemVEightByteSizeInBytes; } + private const int SystemVEightByteSizeInBytes = 8; + public FpStructInRegistersInfo GetFpStructInRegistersInfo(Internal.TypeSystem.TargetArchitecture architecture) { // TODO(riscv-loongarch): Implement RISC-V/LoongArch64 FP struct classification. diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs index d62d01e68bc1d4..99adf052797a1c 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs @@ -337,27 +337,6 @@ private TargetPointer FindGCRefMap(TargetPointer indirection) /// private void PromoteCallerStack(TargetPointer frameAddress, GcScanContext scanContext) { - IRuntimeInfo runtimeInfo = _target.Contracts.RuntimeInfo; - RuntimeInfoArchitecture arch = runtimeInfo.GetTargetArchitecture(); - RuntimeInfoOperatingSystem os = runtimeInfo.GetTargetOperatingSystem(); - // Matches the ARGITER stress-test gate in CdacStressTests.cs. Platforms - // still missing calling-convention coverage (SystemV-AMD64 / RISC-V / - // LoongArch / WASM) fall through to RecordDeferredFrame. - // TODO(https://github.com/dotnet/runtime/issues/130008): extend - // ICallingConvention.TryComputeArgGCRefMapBlob coverage to the - // remaining targets so this path fires everywhere. - bool supportedByCallingConvention = - (os is RuntimeInfoOperatingSystem.Windows - && arch is RuntimeInfoArchitecture.X86 or RuntimeInfoArchitecture.X64 or RuntimeInfoArchitecture.Arm64) - || (os is RuntimeInfoOperatingSystem.Unix - && arch is RuntimeInfoArchitecture.Arm or RuntimeInfoArchitecture.Arm64); - - if (!supportedByCallingConvention) - { - scanContext.RecordDeferredFrame(frameAddress); - return; - } - Data.FramedMethodFrame fmf = _target.ProcessedData.GetOrAdd(frameAddress); if (fmf.MethodDescPtr == TargetPointer.Null) { diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/EEClass.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/EEClass.cs index 2fcbd027729c4b..2b8f495c507d73 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/EEClass.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/EEClass.cs @@ -25,4 +25,5 @@ internal sealed partial class EEClass : IData [Field] public TargetPointer FieldDescList { get; } [Field] public ushort NumNonVirtualSlots { get; } [Field] public byte BaseSizePadding { get; } + [Field] public TargetPointer OptionalFields { get; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/EEClassOptionalFields.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/EEClassOptionalFields.cs new file mode 100644 index 00000000000000..a35baa6c0d3f7a --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/EEClassOptionalFields.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.DataContractReader.Data; + +[CdacType(nameof(DataType.EEClassOptionalFields))] +internal sealed partial class EEClassOptionalFields : IData +{ + [Field] public SystemVEightByteRegistersInfo? EightByteRegistersInfo { get; } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/SystemVEightByteRegistersInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/SystemVEightByteRegistersInfo.cs new file mode 100644 index 00000000000000..82c2f867e84b5d --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/SystemVEightByteRegistersInfo.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.DataContractReader.Data; + +[CdacType(nameof(DataType.SystemVEightByteRegistersInfo))] +internal sealed partial class SystemVEightByteRegistersInfo : IData +{ + [Field] public byte NumEightBytes { get; } + + // CLR_SYSTEMV_MAX_EIGHTBYTES_COUNT_TO_PASS_IN_REGISTERS: fixed at 2 in + // src/coreclr/inc/corinfo.h. Slots beyond NumEightBytes are undefined. + public byte[] EightByteClassifications { get; private set; } = new byte[2]; + public byte[] EightByteSizes { get; private set; } = new byte[2]; + + partial void OnInit(Target target, TargetPointer address) + { + Target.TypeInfo type = target.GetTypeInfo(DataType.SystemVEightByteRegistersInfo); + target.ReadBuffer(address + (ulong)type.Fields[nameof(EightByteClassifications)].Offset, EightByteClassifications); + target.ReadBuffer(address + (ulong)type.Fields[nameof(EightByteSizes)].Offset, EightByteSizes); + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs index e7d2e874b36bc4..365c5c142e86bc 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs @@ -62,6 +62,8 @@ public enum DataType MethodTable, DynamicStaticsInfo, EEClass, + EEClassOptionalFields, + SystemVEightByteRegistersInfo, CoreLibBinder, MethodTableAuxiliaryData, GenericsDictInfo, diff --git a/src/native/managed/cdac/gen/Parser.cs b/src/native/managed/cdac/gen/Parser.cs index f6fc1739596760..8f149ff137969e 100644 --- a/src/native/managed/cdac/gen/Parser.cs +++ b/src/native/managed/cdac/gen/Parser.cs @@ -357,6 +357,15 @@ private static (FieldReadKind, string?, bool) ClassifyFieldRead(IPropertySymbol isNullable = true; type = named.TypeArguments[0]; } + else if (type.IsReferenceType && prop.NullableAnnotation == NullableAnnotation.Annotated) + { + // Nullable reference type annotation (e.g. `IDataT? Foo`). Only + // meaningful for IData sub-typed fields; primitive fields use + // Nullable above. Treated the same as optional: emitter guards + // the descriptor lookup with ContainsKey and assigns default (null) + // when absent. + isNullable = true; + } string fqn = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); diff --git a/src/native/managed/cdac/tests/StressTests/CdacStressTestBase.cs b/src/native/managed/cdac/tests/StressTests/CdacStressTestBase.cs index b490417c0bef2b..5caa35b130d11d 100644 --- a/src/native/managed/cdac/tests/StressTests/CdacStressTestBase.cs +++ b/src/native/managed/cdac/tests/StressTests/CdacStressTestBase.cs @@ -157,7 +157,8 @@ private async Task RunStressAsync(string debuggeeName, Stress /// /// Asserts the GCREFS stress run produced a [GC_STATS] summary - /// with at least one verification and no hard failures. + /// with at least one verification, no hard failures, and no deferred + /// (known-issue) frames. /// internal static void AssertAllPassed(CdacStressResults results, string debuggeeName) { @@ -173,37 +174,13 @@ internal static void AssertAllPassed(CdacStressResults results, string debuggeeN "did not initialize correctly.\n" + $"Log: {results.LogFilePath}"); - if (results.Failed > 0) + if (results.Failed > 0 || results.KnownIssues > 0) { string analysis = results.AnalyzeFailures(maxFailures: 3); Assert.Fail( $"GCREFS stress test '{debuggeeName}' had {results.Failed} failure(s) " + - $"out of {results.TotalVerifications} verifications " + - $"({results.KnownIssues} known issue(s) tolerated).\n" + - $"Log: {results.LogFilePath}\n\n{analysis}"); - } - - // On supported targets every Frame's caller-arg refs are enumerated via - // the GCRefMap blob synthesized by ICallingConvention -- there should be - // no deferred frames at all, so any KnownIssue count is a regression. - // Keep this gate in sync with the ARGITER-support gate in - // ArgIterStress_AllVerificationsPass below and with the - // supportedByCallingConvention gate in GcScanner.PromoteCallerStack. - GetTargetPlatform(out OSPlatform os, out Architecture arch); - bool requiresZeroKnownIssues = - (os == OSPlatform.Windows && arch is Architecture.X86 or Architecture.X64 or Architecture.Arm64) - || (os == OSPlatform.Linux && arch is Architecture.Arm or Architecture.Arm64); - if (requiresZeroKnownIssues && results.KnownIssues > 0) - { - string analysis = results.AnalyzeFailures(maxFailures: 3); - Assert.Fail( - $"GCREFS stress test '{debuggeeName}' had {results.KnownIssues} known issue(s) " + - $"out of {results.TotalVerifications} verifications. " + - "This platform is expected to enumerate every transition Frame's " + - "caller-stack refs via ICallingConvention.TryComputeArgGCRefMapBlob with no " + - "deferred frames. A non-zero KnownIssues count indicates the encoder declined " + - "a method it should support (e.g. a regression in ComputeArgGCRefMapBlobCore " + - "or a new code path returning E_NOTIMPL).\n" + + $"and {results.KnownIssues} known issue(s) out of " + + $"{results.TotalVerifications} verifications.\n" + $"Log: {results.LogFilePath}\n\n{analysis}"); } } diff --git a/src/native/managed/cdac/tests/StressTests/CdacStressTests.cs b/src/native/managed/cdac/tests/StressTests/CdacStressTests.cs index 47d3132c39640e..eeaa46877e0002 100644 --- a/src/native/managed/cdac/tests/StressTests/CdacStressTests.cs +++ b/src/native/managed/cdac/tests/StressTests/CdacStressTests.cs @@ -52,9 +52,6 @@ public async Task GCRefStress_AllVerificationsPass(Debuggee debuggee) { GetTargetPlatform(out OSPlatform os, out _); - // TODO(https://github.com/dotnet/runtime/issues/130008): extend GCREFS stress coverage to non-Windows / ARM - // targets once ICallingConvention.TryComputeArgGCRefMapBlob supports them (SystemV-AMD64 / ARM64 - // struct-in-register classification, ARM32 ABI port). if (debuggee.WindowsOnly && os != OSPlatform.Windows) throw new SkipTestException($"{debuggee.Name} debuggee is Windows-only."); @@ -69,30 +66,11 @@ public async Task GCRefStress_AllVerificationsPass(Debuggee debuggee) [MemberData(nameof(Debuggees))] public async Task ArgIterStress_AllVerificationsPass(Debuggee debuggee) { - GetTargetPlatform(out OSPlatform os, out Architecture arch); + GetTargetPlatform(out OSPlatform os, out _); if (debuggee.WindowsOnly && os != OSPlatform.Windows) throw new SkipTestException($"{debuggee.Name} debuggee is Windows-only."); - // ARGITER stress requires a CdacTypeHandle whose calling-convention - // helpers are filled in for the target ABI. Currently validated: - // - Windows x86 / x64 (TransitionBlock + IsTrivialPointerSizedStruct) - // - Windows ARM64 (HFA via MethodTable enum_flag_IsHFA) - // - Linux ARM / ARM64 (HFA, same path) - // Still skipped: - // - Linux / macOS x64 (SystemV-AMD64 eightbyte classifier not ported) - // - RISC-V / LoongArch64 (FP struct classifier not ported) - // - WASM32 (GetFieldAlignment not ported) - bool argIterSupported = - (os == OSPlatform.Windows && arch is Architecture.X86 or Architecture.X64 or Architecture.Arm64) - || (os == OSPlatform.Linux && arch is Architecture.Arm or Architecture.Arm64); - if (!argIterSupported) - throw new SkipTestException( - "ARGITER stress: needs follow-up work for this platform " + - "(SystemV-AMD64 struct classifier for linux/macOS x64, " + - "Windows ARM32 ABI port, RISC-V / LoongArch FP struct classifier, " + - "or WASM GetFieldAlignment)."); - CdacStressResults results = await RunArgIterStressAsync(debuggee.Name); AssertAllArgIterPassed(results, debuggee.Name); } diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/CallSignatures/Program.cs b/src/native/managed/cdac/tests/StressTests/Debuggees/CallSignatures/Program.cs index 82b40fbb26d034..56b56f7e90441c 100644 --- a/src/native/managed/cdac/tests/StressTests/Debuggees/CallSignatures/Program.cs +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/CallSignatures/Program.cs @@ -95,6 +95,7 @@ private static void Drive() EnumCategory(); ReturnCategory(); HfaCategory(); + SysVCategory(); DeepStackCategory(); } @@ -537,7 +538,112 @@ private struct NestedDouble4 { public Double2 First; public Double2 Second; } [MethodImpl(MethodImplOptions.NoInlining)] private static void Vec128FloatArg(Vector128 v) { AllocBurst(); GC.KeepAlive((object)v.GetElement(0)); } [MethodImpl(MethodImplOptions.NoInlining)] private static void VecNumericsFloatArg(Vector v) { AllocBurst(); GC.KeepAlive((object)v[0]); } - // ===== Category 13: deep stack ===== + // ===== Category 13: SystemV-AMD64 struct-in-register shapes ===== + // Exercises SystemV-AMD64 struct classification. On non-Unix-x64 targets + // these methods still run but the cDAC has no cached eightbyte descriptor + // to read (it lives in EEClassOptionalFields only on UNIX_AMD64_ABI + // builds), so GetSystemVAmd64PassStructInRegisterDescriptor returns "not + // in registers" -- the runtime and cDAC agree via other paths and no + // divergence surfaces. On Linux/macOS x64 the runtime pre-computes the + // eightbyte classification at MethodTable-build time and the cDAC reads + // that cached SystemVEightByteRegistersInfo to decide FP-reg vs GP-reg + // vs stack placement, so ArgIterator produces different GCRefMap tokens + // depending on which eightbytes end up in which register class. + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void SysVCategory() + { + // Single-eightbyte shapes. + SysVIntPair(default); // 8 bytes, Integer eightbyte + SysVFloatPair(default); // 8 bytes, SSE eightbyte + SysVIntFloat(default); // 8 bytes, mixed -> Integer eightbyte (Integer wins) + SysVSingleRef(default); // 8 bytes, IntegerReference eightbyte + + // Two-eightbyte shapes. + SysVLongLong(default); // 16 bytes, Integer + Integer + SysVLongDouble(default); // 16 bytes, Integer + SSE + SysVDoubleDouble(default); // 16 bytes, SSE + SSE + SysVRefInt(default); // 16 bytes, IntegerReference + Integer + + // Nested value type. + SysVNested(default); // outer wraps inner-struct + int + + // ByRefLike passed as struct on SystemV. + Span sp = stackalloc byte[8]; + SysVSpanByte(sp); // Span: IntegerByRef + Integer + + SysVThreeRefs(default); // 24 bytes -> stack + SysVVector128Arg(default); // intrinsic Vector -> stack + SysVInt128Arg(default); // 2 Integer eightbytes (runtime does NOT reject Int128 by name) + SysVEmpty(default); // 1-byte empty struct -> 1 Integer eightbyte (padding is normalized to Integer) + + // Unions (LayoutKind.Explicit with overlapping fields). Exercises + // the ReClassifyField merge path in the classifier. C# rules forbid + // mixing GC refs with non-refs at the same offset, so the only + // legal overlaps are Integer/Integer, SSE/SSE, or Integer/SSE (both + // primitive kinds). + SysVIntFloatUnion(default); // int + float at offset 0 -> Integer eightbyte + SysVIntIntUnion(default); // int + int at offset 0 -> Integer eightbyte + + // Unaligned sub-eightbyte and single-field wrappers. + SysVSmall(default); // { int a; byte b; } -> 1 Integer eightbyte (unaligned tail) + SysVWrapInt(default); // { int a; } single-field wrapper -> 1 Integer eightbyte + } + + // ---- SystemV struct shapes ---- + private struct IntPair { public int A, B; } + private struct FloatPair { public float A, B; } + private struct IntFloat { public int A; public float B; } + private struct SingleRef { public object? R; } + private struct LongLong { public long A, B; } + private struct LongDouble { public long A; public double B; } + private struct DoubleDouble { public double A, B; } + private struct RefInt { public object? R; public long Padding; } + private struct SysVNestedStruct { public IntPair Inner; public int Trailing; } + private struct SysVThreeRefsStruct { public object? R1, R2, R3; } + private struct Int128Wrapper { public System.Int128 V; } + private struct EmptyStruct { } + private struct SmallUnaligned { public int A; public byte B; } + private struct WrapInt { public int A; } + + // Union: int and float at the same offset. Classifier sees both as + // unique-offset fields and merges via ReClassifyField (Integer+SSE = Integer). + [StructLayout(LayoutKind.Explicit)] + private struct IntFloatUnion + { + [FieldOffset(0)] public int I; + [FieldOffset(0)] public float F; + } + + // Union: two ints at the same offset. Merge collapses to Integer. + [StructLayout(LayoutKind.Explicit)] + private struct IntIntUnion + { + [FieldOffset(0)] public int A; + [FieldOffset(0)] public int B; + } + + // ---- SystemV arg helpers ---- + [MethodImpl(MethodImplOptions.NoInlining)] private static void SysVIntPair(IntPair s) { AllocBurst(); GC.KeepAlive((object)(s.A + s.B)); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void SysVFloatPair(FloatPair s) { AllocBurst(); GC.KeepAlive((object)(s.A + s.B)); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void SysVIntFloat(IntFloat s) { AllocBurst(); GC.KeepAlive((object)(s.A + s.B)); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void SysVSingleRef(SingleRef s) { AllocBurst(); GC.KeepAlive(s.R); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void SysVLongLong(LongLong s) { AllocBurst(); GC.KeepAlive((object)(s.A + s.B)); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void SysVLongDouble(LongDouble s) { AllocBurst(); GC.KeepAlive((object)(s.A + s.B)); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void SysVDoubleDouble(DoubleDouble s) { AllocBurst(); GC.KeepAlive((object)(s.A + s.B)); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void SysVRefInt(RefInt s) { AllocBurst(); GC.KeepAlive(s.R); GC.KeepAlive((object)s.Padding); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void SysVNested(SysVNestedStruct s) { AllocBurst(); GC.KeepAlive((object)(s.Inner.A + s.Trailing)); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void SysVSpanByte(Span s) { AllocBurst(); GC.KeepAlive((object)s.Length); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void SysVThreeRefs(SysVThreeRefsStruct s) { AllocBurst(); GC.KeepAlive(s.R1); GC.KeepAlive(s.R2); GC.KeepAlive(s.R3); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void SysVVector128Arg(Vector128 v) { AllocBurst(); GC.KeepAlive((object)v.GetElement(0)); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void SysVInt128Arg(Int128Wrapper s) { AllocBurst(); GC.KeepAlive((object)s.V.ToString()); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void SysVEmpty(EmptyStruct s) { AllocBurst(); GC.KeepAlive((object)s); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void SysVSmall(SmallUnaligned s) { AllocBurst(); GC.KeepAlive((object)(s.A + s.B)); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void SysVWrapInt(WrapInt s) { AllocBurst(); GC.KeepAlive((object)s.A); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void SysVIntFloatUnion(IntFloatUnion s) { AllocBurst(); GC.KeepAlive((object)s.I); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void SysVIntIntUnion(IntIntUnion s) { AllocBurst(); GC.KeepAlive((object)s.A); } + + // ===== Category 14: deep stack ===== // Mutually-recursive chains of methods with mixed signatures. At any // given allocation trigger many frames are simultaneously live, so a // single stack-walk verification run touches multiple MDs across diff --git a/src/native/managed/cdac/tests/UnitTests/MockDescriptors/MockDescriptors.RuntimeTypeSystem.cs b/src/native/managed/cdac/tests/UnitTests/MockDescriptors/MockDescriptors.RuntimeTypeSystem.cs index 182f5935b60ed8..9e58b5b2d2bfae 100644 --- a/src/native/managed/cdac/tests/UnitTests/MockDescriptors/MockDescriptors.RuntimeTypeSystem.cs +++ b/src/native/managed/cdac/tests/UnitTests/MockDescriptors/MockDescriptors.RuntimeTypeSystem.cs @@ -108,6 +108,7 @@ internal sealed class MockEEClass : TypedView private const string FieldDescListFieldName = nameof(Data.EEClass.FieldDescList); private const string NumNonVirtualSlotsFieldName = nameof(Data.EEClass.NumNonVirtualSlots); private const string BaseSizePaddingFieldName = nameof(Data.EEClass.BaseSizePadding); + private const string OptionalFieldsFieldName = nameof(Data.EEClass.OptionalFields); public static Layout CreateLayout(MockTarget.Architecture architecture) => new SequentialLayoutBuilder("EEClass", architecture) @@ -122,6 +123,7 @@ public static Layout CreateLayout(MockTarget.Architecture architect .AddPointerField(FieldDescListFieldName) .AddUInt16Field(NumNonVirtualSlotsFieldName) .AddByteField(BaseSizePaddingFieldName) + .AddPointerField(OptionalFieldsFieldName) .Build(); public ulong MethodTable From e59c7472372ab2628b05100c683dd28701116ad8 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Tue, 7 Jul 2026 16:11:23 -0400 Subject: [PATCH 2/7] Address Copilot review feedback: bounds, enum order, parser scope * CdacTypeHandle.GetSystemVAmd64PassStructInRegisterDescriptor: only populate the second eightbyte slot when NumEightBytes > 1. Slots beyond NumEightBytes are undefined in the runtime's cached SystemVEightByteRegistersInfo. Mirrors jitinterface.cpp::SystemVRegDescriptorFromSystemVEightByteRegistersInfo. Added a Debug.Assert(NumEightBytes <= 2) so a corrupted descriptor trips the assert instead of silently producing an out-of-bounds descriptor. * DataType: move new EEClassOptionalFields / SystemVEightByteRegistersInfo entries to the end of the enum so we append rather than renumber intermediate values. * gen/Parser.cs: narrow the nullable-reference-type check to IData-typed properties. Non-IData nullable reference [Field]s aren't a real scenario today, and staying narrow avoids silently making future ones optional. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../CallingConvention/CdacTypeHandle.cs | 16 +++++++++++++--- .../DataType.cs | 4 ++-- src/native/managed/cdac/gen/Parser.cs | 14 ++++++++------ 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CdacTypeHandle.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CdacTypeHandle.cs index 90226837a9bb7b..6977eccd4f9012 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CdacTypeHandle.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CdacTypeHandle.cs @@ -152,14 +152,24 @@ public void GetSystemVAmd64PassStructInRegisterDescriptor(out SYSTEMV_AMD64_CORI if (info.NumEightBytes == 0) return; // Runtime marked this struct as not-enregisterable. + Debug.Assert(info.NumEightBytes <= 2, "SystemV descriptor never encodes more than 2 eightbytes"); + descriptor.passedInRegisters = true; descriptor.eightByteCount = info.NumEightBytes; descriptor.eightByteClassifications0 = (SystemVClassificationType)info.EightByteClassifications[0]; descriptor.eightByteSizes0 = info.EightByteSizes[0]; descriptor.eightByteOffsets0 = 0; - descriptor.eightByteClassifications1 = (SystemVClassificationType)info.EightByteClassifications[1]; - descriptor.eightByteSizes1 = info.EightByteSizes[1]; - descriptor.eightByteOffsets1 = SystemVEightByteSizeInBytes; + + // Slots beyond NumEightBytes are undefined in the runtime's cached + // SystemVEightByteRegistersInfo. Only populate the second slot when + // it's actually in use; mirrors + // jitinterface.cpp::SystemVRegDescriptorFromSystemVEightByteRegistersInfo. + if (info.NumEightBytes > 1) + { + descriptor.eightByteClassifications1 = (SystemVClassificationType)info.EightByteClassifications[1]; + descriptor.eightByteSizes1 = info.EightByteSizes[1]; + descriptor.eightByteOffsets1 = SystemVEightByteSizeInBytes; + } } private const int SystemVEightByteSizeInBytes = 8; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs index 365c5c142e86bc..c573cd7841551d 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs @@ -62,8 +62,6 @@ public enum DataType MethodTable, DynamicStaticsInfo, EEClass, - EEClassOptionalFields, - SystemVEightByteRegistersInfo, CoreLibBinder, MethodTableAuxiliaryData, GenericsDictInfo, @@ -216,6 +214,8 @@ public enum DataType EnCAddedStaticField, EnCSyncBlockInfo, UnorderedArrayBase, + EEClassOptionalFields, + SystemVEightByteRegistersInfo, } public static class DataTypeTargetExtensions diff --git a/src/native/managed/cdac/gen/Parser.cs b/src/native/managed/cdac/gen/Parser.cs index 8f149ff137969e..9250fec6c74a73 100644 --- a/src/native/managed/cdac/gen/Parser.cs +++ b/src/native/managed/cdac/gen/Parser.cs @@ -357,13 +357,15 @@ private static (FieldReadKind, string?, bool) ClassifyFieldRead(IPropertySymbol isNullable = true; type = named.TypeArguments[0]; } - else if (type.IsReferenceType && prop.NullableAnnotation == NullableAnnotation.Annotated) + else if (ImplementsIData(type) && prop.NullableAnnotation == NullableAnnotation.Annotated) { - // Nullable reference type annotation (e.g. `IDataT? Foo`). Only - // meaningful for IData sub-typed fields; primitive fields use - // Nullable above. Treated the same as optional: emitter guards - // the descriptor lookup with ContainsKey and assigns default (null) - // when absent. + // Nullable reference-type annotation on an IData sub-typed + // field (e.g. `IDataT? Foo`). Treated the same as optional: + // emitter guards the descriptor lookup with ContainsKey and + // assigns default (null) when absent. Only IData sub-types + // participate; non-IData reference-typed [Field]s aren't a + // real scenario today and staying narrow here avoids silently + // making them optional. isNullable = true; } From 70e02851478d0e4d70943df962ea794c23833a9f Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Tue, 7 Jul 2026 16:29:33 -0400 Subject: [PATCH 3/7] cDAC: encode SystemV struct-in-registers args in GCRefMap Previously ComputeArgGCRefMapBlobCore threw NotImplementedException when the ArgIterator returned TransitionBlock.StructInRegsOffset, so the whole method's blob was declined and the frame was deferred to the legacy scan path -- reintroducing KnownIssues on Linux/macOS x64 for every method with a SystemV register-passed struct arg. Now that the classifier correctly identifies register-passed structs (via runtime-cached SystemVEightByteRegistersInfo), plug in the encoding side. Mirrors ArgDestination::ReportPointersFromStructInRegisters in src/coreclr/vm/argdestination.h: int genRegOffset = OffsetOfArgumentRegisters + m_idxGenReg * PointerSize; for each eightbyte i: if classification == SSE: skip -- XMM register, no GC ref, DO NOT advance genRegOffset (SSE eightbytes are float-reg-only) else: if IntegerReference: token[genRegOffset] = REF if IntegerByRef: token[genRegOffset] = INTERIOR genRegOffset += eightByteSize Plumbing: * ArgumentLocation: adds IsStructPassedInRegs, SysVEightByteDescriptor, and SysVIdxGenReg. The descriptor comes from CdacTypeHandle (which reads EEClass::m_eightByteRegistersInfo); m_idxGenReg comes from argit.GetArgLoc(argOffset). * CallingConvention_1.GetArgumentLayout: replaces the throw at StructInRegsOffset with population of the new ArgumentLocation fields. * ComputeArgGCRefMapBlobCore: adds an IsStructPassedInRegs branch that runs the eightbyte-to-register-slot loop above. Test coverage: CallSignatures.Program adds a SysVDoubleRef test case ({ double D; object R; }) which exercises the "SSE eightbyte doesn't advance the general-register cursor" edge case -- the ref must end up in RDI (idxGenReg + 0), NOT RSI (idxGenReg + 1). Windows unit tests: all cDAC tests pass. Windows targets never hit this path (descriptor absent, classifier returns not-in-registers); Linux/macOS x64 CI validates end-to-end via the byte-identical comparison against runtime ComputeCallRefMap. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../CallingConvention/ArgumentLocation.cs | 10 +++ .../CallingConvention/CallingConvention_1.cs | 51 +++++++++++++++- .../CallingConvention/CdacTypeHandle.cs | 23 ++----- .../Data/SystemVEightByteRegistersInfo.cs | 3 +- src/native/managed/cdac/gen/Parser.cs | 9 +-- .../Debuggees/CallSignatures/Program.cs | 61 ++++++++----------- 6 files changed, 96 insertions(+), 61 deletions(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgumentLocation.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgumentLocation.cs index 2d1871d4840ede..c00ac4284a9ec9 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgumentLocation.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgumentLocation.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Internal.JitInterface; + namespace Microsoft.Diagnostics.DataContractReader.Contracts; internal readonly struct ArgumentLocation @@ -29,4 +31,12 @@ internal readonly struct ArgumentLocation // the open generic MethodTable (e.g. Span for a Span arg) so // encoders can inspect type structure as a fallback. public TypeHandle OpenGenericType { get; init; } + + // SystemV-AMD64 struct passed in registers. Offset is the StructInRegsOffset + // sentinel; the encoder consumes SysVEightByteDescriptor + SysVIdxGenReg. + public bool IsStructPassedInRegs { get; init; } + public SYSTEMV_AMD64_CORINFO_STRUCT_REG_PASSING_DESCRIPTOR SysVEightByteDescriptor { get; init; } + // Index of the first general-purpose argument register assigned to this + // struct (0 = RDI, 1 = RSI, ...). + public int SysVIdxGenReg { get; init; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CallingConvention_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CallingConvention_1.cs index 969d2875f6217d..88fe17c903356f 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CallingConvention_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CallingConvention_1.cs @@ -4,10 +4,12 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics; using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; using Internal.CallingConvention; using Internal.CorConstants; +using Internal.JitInterface; using Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; using Microsoft.Diagnostics.DataContractReader.SignatureHelpers; @@ -222,7 +224,26 @@ private ArgumentLayout GetArgumentLayout(MethodDescHandle methodDesc) } if (argOffset == TransitionBlock.StructInRegsOffset) - throw new NotImplementedException("SystemV AMD64 struct-in-registers is not yet supported by the cDAC."); + { + // SystemV-AMD64 struct-in-registers. + SYSTEMV_AMD64_CORINFO_STRUCT_REG_PASSING_DESCRIPTOR sysvDesc; + parameterTypes[argIndex].GetSystemVAmd64PassStructInRegisterDescriptor(out sysvDesc); + Internal.CallingConvention.ArgLocDesc? loc = argit.GetArgLoc(argOffset); + Debug.Assert(loc.HasValue); + + arguments.Add(new ArgumentLocation + { + Offset = argOffset, + ElementType = elemType, + TypeHandle = methodSig.ParameterTypes[argIndex], + IsStructPassedInRegs = true, + SysVEightByteDescriptor = sysvDesc, + SysVIdxGenReg = loc.Value.m_idxGenReg, + OpenGenericType = paramInfo[argIndex].OpenGenericType, + }); + argIndex++; + continue; + } bool passedByRef = elemType == CdacCorElementType.ValueType && transitionBlock.IsArgPassedByRef(parameterTypes[argIndex]); @@ -619,6 +640,34 @@ public MethodAndTypeContextProvider(Target target, ModuleHandle moduleHandle, IR { token = GCRefMapToken.VASigCookie; } + else if (arg.IsStructPassedInRegs) + { + // Mirrors ArgDestination::ReportPointersFromStructInRegisters + // in src/coreclr/vm/argdestination.h. + TransitionBlock tbForStruct = BuildTransitionBlock(runtimeInfo); + int genRegOffset = tbForStruct.OffsetOfArgumentRegisters + arg.SysVIdxGenReg * pointerSize; + for (int i = 0; i < arg.SysVEightByteDescriptor.eightByteCount; i++) + { + SystemVClassificationType cls = (i == 0) + ? arg.SysVEightByteDescriptor.eightByteClassifications0 + : arg.SysVEightByteDescriptor.eightByteClassifications1; + int size = (i == 0) + ? arg.SysVEightByteDescriptor.eightByteSizes0 + : arg.SysVEightByteDescriptor.eightByteSizes1; + + // SSE eightbytes go to XMM regs; don't advance genRegOffset. + if (cls == SystemVClassificationType.SystemVClassificationTypeSSE) + continue; + + if (cls == SystemVClassificationType.SystemVClassificationTypeIntegerReference) + tokens[genRegOffset] = GCRefMapToken.Ref; + else if (cls == SystemVClassificationType.SystemVClassificationTypeIntegerByRef) + tokens[genRegOffset] = GCRefMapToken.Interior; + + genRegOffset += size; + } + continue; + } else if (arg.IsParamType) { // Resolve InstArgMethodDesc vs InstArgMethodTable on demand diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CdacTypeHandle.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CdacTypeHandle.cs index 6977eccd4f9012..9cef50afef0988 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CdacTypeHandle.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CdacTypeHandle.cs @@ -127,16 +127,9 @@ public void GetSystemVAmd64PassStructInRegisterDescriptor(out SYSTEMV_AMD64_CORI if (_typeHandle.IsNull) return; - // The runtime pre-computes the eightbyte classification for every managed - // value type at MethodTable construction and caches it in - // EEClass::m_eightByteRegistersInfo (inside EEClassOptionalFields). We - // read that cached descriptor instead of re-classifying, mirroring - // jitinterface.cpp::SystemVRegDescriptorFromSystemVEightByteRegistersInfo. - // - // The runtime only allocates m_eightByteRegistersInfo on UNIX_AMD64_ABI - // builds; on other targets the field is absent from the - // EEClassOptionalFields descriptor and EightByteRegistersInfo below is - // null. + // Read the runtime-cached classification from EEClassOptionalFields; + // mirrors SystemVRegDescriptorFromSystemVEightByteRegistersInfo in + // jitinterface.cpp. Field only populated on UNIX_AMD64_ABI builds. TargetPointer eeClassPtr = Rts.GetClassPointer(_typeHandle); if (eeClassPtr == TargetPointer.Null) return; @@ -147,12 +140,12 @@ public void GetSystemVAmd64PassStructInRegisterDescriptor(out SYSTEMV_AMD64_CORI Data.EEClassOptionalFields optFields = _target.ProcessedData.GetOrAdd(eeClass.OptionalFields); if (optFields.EightByteRegistersInfo is not Data.SystemVEightByteRegistersInfo info) - return; // Non-Unix-x64 target: descriptor doesn't include this field. + return; if (info.NumEightBytes == 0) - return; // Runtime marked this struct as not-enregisterable. + return; - Debug.Assert(info.NumEightBytes <= 2, "SystemV descriptor never encodes more than 2 eightbytes"); + Debug.Assert(info.NumEightBytes <= 2); descriptor.passedInRegisters = true; descriptor.eightByteCount = info.NumEightBytes; @@ -160,10 +153,6 @@ public void GetSystemVAmd64PassStructInRegisterDescriptor(out SYSTEMV_AMD64_CORI descriptor.eightByteSizes0 = info.EightByteSizes[0]; descriptor.eightByteOffsets0 = 0; - // Slots beyond NumEightBytes are undefined in the runtime's cached - // SystemVEightByteRegistersInfo. Only populate the second slot when - // it's actually in use; mirrors - // jitinterface.cpp::SystemVRegDescriptorFromSystemVEightByteRegistersInfo. if (info.NumEightBytes > 1) { descriptor.eightByteClassifications1 = (SystemVClassificationType)info.EightByteClassifications[1]; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/SystemVEightByteRegistersInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/SystemVEightByteRegistersInfo.cs index 82c2f867e84b5d..27b141d38816b2 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/SystemVEightByteRegistersInfo.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/SystemVEightByteRegistersInfo.cs @@ -8,8 +8,7 @@ internal sealed partial class SystemVEightByteRegistersInfo : IData sub-typed - // field (e.g. `IDataT? Foo`). Treated the same as optional: - // emitter guards the descriptor lookup with ContainsKey and - // assigns default (null) when absent. Only IData sub-types - // participate; non-IData reference-typed [Field]s aren't a - // real scenario today and staying narrow here avoids silently - // making them optional. + // IData? field: emitter treats it as optional (ContainsKey + // guard + default(null) when the descriptor omits the field). isNullable = true; } diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/CallSignatures/Program.cs b/src/native/managed/cdac/tests/StressTests/Debuggees/CallSignatures/Program.cs index 56b56f7e90441c..95521b24c47cfd 100644 --- a/src/native/managed/cdac/tests/StressTests/Debuggees/CallSignatures/Program.cs +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/CallSignatures/Program.cs @@ -539,55 +539,46 @@ private struct NestedDouble4 { public Double2 First; public Double2 Second; } [MethodImpl(MethodImplOptions.NoInlining)] private static void VecNumericsFloatArg(Vector v) { AllocBurst(); GC.KeepAlive((object)v[0]); } // ===== Category 13: SystemV-AMD64 struct-in-register shapes ===== - // Exercises SystemV-AMD64 struct classification. On non-Unix-x64 targets - // these methods still run but the cDAC has no cached eightbyte descriptor - // to read (it lives in EEClassOptionalFields only on UNIX_AMD64_ABI - // builds), so GetSystemVAmd64PassStructInRegisterDescriptor returns "not - // in registers" -- the runtime and cDAC agree via other paths and no - // divergence surfaces. On Linux/macOS x64 the runtime pre-computes the - // eightbyte classification at MethodTable-build time and the cDAC reads - // that cached SystemVEightByteRegistersInfo to decide FP-reg vs GP-reg - // vs stack placement, so ArgIterator produces different GCRefMap tokens - // depending on which eightbytes end up in which register class. + // On non-Unix-x64 targets the cDAC's descriptor is absent so classification + // returns "not in registers" and the runtime path agrees trivially. Linux/ + // macOS x64 read the cached SystemVEightByteRegistersInfo and produce + // different GCRefMap tokens per eightbyte classification. [MethodImpl(MethodImplOptions.NoInlining)] private static void SysVCategory() { // Single-eightbyte shapes. - SysVIntPair(default); // 8 bytes, Integer eightbyte - SysVFloatPair(default); // 8 bytes, SSE eightbyte - SysVIntFloat(default); // 8 bytes, mixed -> Integer eightbyte (Integer wins) - SysVSingleRef(default); // 8 bytes, IntegerReference eightbyte + SysVIntPair(default); // Integer + SysVFloatPair(default); // SSE + SysVIntFloat(default); // mixed -> Integer (Integer wins) + SysVSingleRef(default); // IntegerReference // Two-eightbyte shapes. - SysVLongLong(default); // 16 bytes, Integer + Integer - SysVLongDouble(default); // 16 bytes, Integer + SSE - SysVDoubleDouble(default); // 16 bytes, SSE + SSE - SysVRefInt(default); // 16 bytes, IntegerReference + Integer + SysVLongLong(default); // Integer + Integer + SysVLongDouble(default); // Integer + SSE + SysVDoubleDouble(default); // SSE + SSE + SysVRefInt(default); // IntegerReference + Integer - // Nested value type. - SysVNested(default); // outer wraps inner-struct + int + SysVNested(default); // nested value type - // ByRefLike passed as struct on SystemV. + // Span: IntegerByRef + Integer. Span sp = stackalloc byte[8]; - SysVSpanByte(sp); // Span: IntegerByRef + Integer + SysVSpanByte(sp); SysVThreeRefs(default); // 24 bytes -> stack SysVVector128Arg(default); // intrinsic Vector -> stack - SysVInt128Arg(default); // 2 Integer eightbytes (runtime does NOT reject Int128 by name) - SysVEmpty(default); // 1-byte empty struct -> 1 Integer eightbyte (padding is normalized to Integer) + SysVInt128Arg(default); // 2 Integer eightbytes (runtime doesn't reject Int128 by name) + SysVEmpty(default); // 1-byte empty -> 1 Integer eightbyte (NoClass normalized to Integer) + // SSE + IntegerReference: ref goes in RDI, not RSI -- SSE eightbytes + // don't advance the general-register cursor. + SysVDoubleRef(default); - // Unions (LayoutKind.Explicit with overlapping fields). Exercises - // the ReClassifyField merge path in the classifier. C# rules forbid - // mixing GC refs with non-refs at the same offset, so the only - // legal overlaps are Integer/Integer, SSE/SSE, or Integer/SSE (both - // primitive kinds). - SysVIntFloatUnion(default); // int + float at offset 0 -> Integer eightbyte - SysVIntIntUnion(default); // int + int at offset 0 -> Integer eightbyte + // Unions (LayoutKind.Explicit) -- exercises the classifier's merge path. + SysVIntFloatUnion(default); // int + float @ 0 -> Integer + SysVIntIntUnion(default); // int + int @ 0 -> Integer - // Unaligned sub-eightbyte and single-field wrappers. - SysVSmall(default); // { int a; byte b; } -> 1 Integer eightbyte (unaligned tail) - SysVWrapInt(default); // { int a; } single-field wrapper -> 1 Integer eightbyte + SysVSmall(default); // { int a; byte b; } -> Integer (unaligned tail) + SysVWrapInt(default); // { int a; } single-field wrapper } // ---- SystemV struct shapes ---- @@ -599,6 +590,7 @@ private struct LongLong { public long A, B; } private struct LongDouble { public long A; public double B; } private struct DoubleDouble { public double A, B; } private struct RefInt { public object? R; public long Padding; } + private struct DoubleRef { public double D; public object? R; } private struct SysVNestedStruct { public IntPair Inner; public int Trailing; } private struct SysVThreeRefsStruct { public object? R1, R2, R3; } private struct Int128Wrapper { public System.Int128 V; } @@ -632,6 +624,7 @@ private struct IntIntUnion [MethodImpl(MethodImplOptions.NoInlining)] private static void SysVLongDouble(LongDouble s) { AllocBurst(); GC.KeepAlive((object)(s.A + s.B)); } [MethodImpl(MethodImplOptions.NoInlining)] private static void SysVDoubleDouble(DoubleDouble s) { AllocBurst(); GC.KeepAlive((object)(s.A + s.B)); } [MethodImpl(MethodImplOptions.NoInlining)] private static void SysVRefInt(RefInt s) { AllocBurst(); GC.KeepAlive(s.R); GC.KeepAlive((object)s.Padding); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void SysVDoubleRef(DoubleRef s) { AllocBurst(); GC.KeepAlive(s.R); GC.KeepAlive((object)s.D); } [MethodImpl(MethodImplOptions.NoInlining)] private static void SysVNested(SysVNestedStruct s) { AllocBurst(); GC.KeepAlive((object)(s.Inner.A + s.Trailing)); } [MethodImpl(MethodImplOptions.NoInlining)] private static void SysVSpanByte(Span s) { AllocBurst(); GC.KeepAlive((object)s.Length); } [MethodImpl(MethodImplOptions.NoInlining)] private static void SysVThreeRefs(SysVThreeRefsStruct s) { AllocBurst(); GC.KeepAlive(s.R1); GC.KeepAlive(s.R2); GC.KeepAlive(s.R3); } From 511d49ee0aa04edab7b788d3c0ec18e7156d6573 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Tue, 7 Jul 2026 21:39:32 -0400 Subject: [PATCH 4/7] cDAC: expose SystemVEightByteRegistersInfo as two typed slot pairs Rather than reading the two eightbyte tables as byte[] arrays via ReadBuffer (indexed only by NumEightBytes, which is read from target memory without validation), split each table into two explicitly-named [Field] properties: EightByteClassification0/1 and EightByteSize0/1. Two benefits: 1. The Data class is fully declarative -- no OnInit, no arrays. The source generator emits an ordinary two-uint8 read per slot, so there's no code path that could index past slot 1 regardless of what NumEightBytes says. 2. Any consumer keyed off descriptor.eightByteCount is now safe against a corrupted / mis-read NumEightBytes: the previously Debug.Assert-only check in CdacTypeHandle is now a runtime guard that treats NumEightBytes > 2 as "not passed in registers", so downstream loops (e.g. ArgIterator, TransitionBlock) can't overrun a two-slot descriptor. Also adds a static_assert in methodtable.h that CLR_SYSTEMV_MAX_EIGHTBYTES_COUNT_TO_PASS_IN_REGISTERS is still 2, so if the runtime ever grows the descriptor the cDAC-side split gets a build break rather than silently truncating. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/datadescriptor/datadescriptor.inc | 6 ++++-- src/coreclr/vm/methodtable.h | 7 +++++-- .../CallingConvention/CdacTypeHandle.cs | 14 +++++++++----- .../Data/SystemVEightByteRegistersInfo.cs | 16 +++++----------- 4 files changed, 23 insertions(+), 20 deletions(-) diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index 6baf938e0911e8..10d8174282d240 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -586,8 +586,10 @@ CDAC_TYPE_END(EEClassOptionalFields) CDAC_TYPE_BEGIN(SystemVEightByteRegistersInfo) CDAC_TYPE_INDETERMINATE(SystemVEightByteRegistersInfo) CDAC_TYPE_FIELD(SystemVEightByteRegistersInfo, T_UINT8, NumEightBytes, cdac_data::NumEightBytes) -CDAC_TYPE_FIELD(SystemVEightByteRegistersInfo, T_UINT8, EightByteClassifications, cdac_data::EightByteClassifications) -CDAC_TYPE_FIELD(SystemVEightByteRegistersInfo, T_UINT8, EightByteSizes, cdac_data::EightByteSizes) +CDAC_TYPE_FIELD(SystemVEightByteRegistersInfo, T_UINT8, EightByteClassification0, cdac_data::EightByteClassification0) +CDAC_TYPE_FIELD(SystemVEightByteRegistersInfo, T_UINT8, EightByteClassification1, cdac_data::EightByteClassification1) +CDAC_TYPE_FIELD(SystemVEightByteRegistersInfo, T_UINT8, EightByteSize0, cdac_data::EightByteSize0) +CDAC_TYPE_FIELD(SystemVEightByteRegistersInfo, T_UINT8, EightByteSize1, cdac_data::EightByteSize1) CDAC_TYPE_END(SystemVEightByteRegistersInfo) #endif // UNIX_AMD64_ABI diff --git a/src/coreclr/vm/methodtable.h b/src/coreclr/vm/methodtable.h index 7776bc6e12b085..10706c80144bf3 100644 --- a/src/coreclr/vm/methodtable.h +++ b/src/coreclr/vm/methodtable.h @@ -928,8 +928,11 @@ struct SystemVEightByteRegistersInfo template<> struct cdac_data { static constexpr size_t NumEightBytes = offsetof(SystemVEightByteRegistersInfo, NumEightBytes); - static constexpr size_t EightByteClassifications = offsetof(SystemVEightByteRegistersInfo, EightByteClassifications); - static constexpr size_t EightByteSizes = offsetof(SystemVEightByteRegistersInfo, EightByteSizes); + static constexpr size_t EightByteClassification0 = offsetof(SystemVEightByteRegistersInfo, EightByteClassifications) + 0 * sizeof(SystemVClassificationType); + static constexpr size_t EightByteClassification1 = offsetof(SystemVEightByteRegistersInfo, EightByteClassifications) + 1 * sizeof(SystemVClassificationType); + static constexpr size_t EightByteSize0 = offsetof(SystemVEightByteRegistersInfo, EightByteSizes) + 0; + static constexpr size_t EightByteSize1 = offsetof(SystemVEightByteRegistersInfo, EightByteSizes) + 1; + static_assert(CLR_SYSTEMV_MAX_EIGHTBYTES_COUNT_TO_PASS_IN_REGISTERS == 2, "cdac descriptor exposes exactly two eightbyte slots"); }; #endif diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CdacTypeHandle.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CdacTypeHandle.cs index 9cef50afef0988..c485bee9f181cb 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CdacTypeHandle.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CdacTypeHandle.cs @@ -145,18 +145,22 @@ public void GetSystemVAmd64PassStructInRegisterDescriptor(out SYSTEMV_AMD64_CORI if (info.NumEightBytes == 0) return; - Debug.Assert(info.NumEightBytes <= 2); + // Runtime contract: at most CLR_SYSTEMV_MAX_EIGHTBYTES_COUNT_TO_PASS_IN_REGISTERS (2) + // eightbytes. Treat a corrupted / out-of-range count as "not passed in + // registers" so downstream loops keyed off eightByteCount can't overrun. + if (info.NumEightBytes > 2) + return; descriptor.passedInRegisters = true; descriptor.eightByteCount = info.NumEightBytes; - descriptor.eightByteClassifications0 = (SystemVClassificationType)info.EightByteClassifications[0]; - descriptor.eightByteSizes0 = info.EightByteSizes[0]; + descriptor.eightByteClassifications0 = (SystemVClassificationType)info.EightByteClassification0; + descriptor.eightByteSizes0 = info.EightByteSize0; descriptor.eightByteOffsets0 = 0; if (info.NumEightBytes > 1) { - descriptor.eightByteClassifications1 = (SystemVClassificationType)info.EightByteClassifications[1]; - descriptor.eightByteSizes1 = info.EightByteSizes[1]; + descriptor.eightByteClassifications1 = (SystemVClassificationType)info.EightByteClassification1; + descriptor.eightByteSizes1 = info.EightByteSize1; descriptor.eightByteOffsets1 = SystemVEightByteSizeInBytes; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/SystemVEightByteRegistersInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/SystemVEightByteRegistersInfo.cs index 27b141d38816b2..f11fd63462d198 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/SystemVEightByteRegistersInfo.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/SystemVEightByteRegistersInfo.cs @@ -6,16 +6,10 @@ namespace Microsoft.Diagnostics.DataContractReader.Data; [CdacType(nameof(DataType.SystemVEightByteRegistersInfo))] internal sealed partial class SystemVEightByteRegistersInfo : IData { - [Field] public byte NumEightBytes { get; } - // Slots beyond NumEightBytes are undefined. - public byte[] EightByteClassifications { get; private set; } = new byte[2]; - public byte[] EightByteSizes { get; private set; } = new byte[2]; - - partial void OnInit(Target target, TargetPointer address) - { - Target.TypeInfo type = target.GetTypeInfo(DataType.SystemVEightByteRegistersInfo); - target.ReadBuffer(address + (ulong)type.Fields[nameof(EightByteClassifications)].Offset, EightByteClassifications); - target.ReadBuffer(address + (ulong)type.Fields[nameof(EightByteSizes)].Offset, EightByteSizes); - } + [Field] public byte NumEightBytes { get; } + [Field] public byte EightByteClassification0 { get; } + [Field] public byte EightByteClassification1 { get; } + [Field] public byte EightByteSize0 { get; } + [Field] public byte EightByteSize1 { get; } } From a26feda8d9cf0bc82bcc6a079dba6df1af4a5cda Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Tue, 7 Jul 2026 21:43:00 -0400 Subject: [PATCH 5/7] Address remaining Copilot review feedback: arch gate + shared constant * GcScanner.PromoteCallerStack: reinstate a cheap arch/OS support gate (now including Unix x64) so unsupported targets go straight to RecordDeferredFrame instead of hitting the NotImplementedException fallback inside CallingConvention_1.TryComputeArgGCRefMapBlob. Avoids exception-driven control flow (and first-chance exceptions under debuggers) on RiscV64 / LoongArch64 / Wasm targets. Tracks the same supported-platform list as the runtime stress gate; TODO points at #130008 for extending coverage. * CdacTypeHandle: reuse SYSTEMV_AMD64_CORINFO_STRUCT_REG_PASSING_DESCRIPTOR.SYSTEMV_EIGHT_BYTE_SIZE_IN_BYTES instead of a locally-defined 8 constant. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../CallingConvention/CdacTypeHandle.cs | 4 +--- .../Contracts/StackWalk/GC/GcScanner.cs | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CdacTypeHandle.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CdacTypeHandle.cs index c485bee9f181cb..750b6012365365 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CdacTypeHandle.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CdacTypeHandle.cs @@ -161,12 +161,10 @@ public void GetSystemVAmd64PassStructInRegisterDescriptor(out SYSTEMV_AMD64_CORI { descriptor.eightByteClassifications1 = (SystemVClassificationType)info.EightByteClassification1; descriptor.eightByteSizes1 = info.EightByteSize1; - descriptor.eightByteOffsets1 = SystemVEightByteSizeInBytes; + descriptor.eightByteOffsets1 = SYSTEMV_AMD64_CORINFO_STRUCT_REG_PASSING_DESCRIPTOR.SYSTEMV_EIGHT_BYTE_SIZE_IN_BYTES; } } - private const int SystemVEightByteSizeInBytes = 8; - public FpStructInRegistersInfo GetFpStructInRegistersInfo(Internal.TypeSystem.TargetArchitecture architecture) { // TODO(riscv-loongarch): Implement RISC-V/LoongArch64 FP struct classification. diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs index 99adf052797a1c..1753a3ed743a0b 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs @@ -337,6 +337,27 @@ private TargetPointer FindGCRefMap(TargetPointer indirection) /// private void PromoteCallerStack(TargetPointer frameAddress, GcScanContext scanContext) { + // Fast-reject targets whose ArgIterator port isn't yet complete. Without + // this gate the eventual NotImplementedException inside + // TryComputeArgGCRefMapBlob would drive routine stack scans through + // exception-based control flow (expensive, and surfaces as first-chance + // exceptions under debuggers). + // TODO(https://github.com/dotnet/runtime/issues/130008): extend coverage + // and remove this gate. + IRuntimeInfo runtimeInfo = _target.Contracts.RuntimeInfo; + RuntimeInfoArchitecture arch = runtimeInfo.GetTargetArchitecture(); + RuntimeInfoOperatingSystem os = runtimeInfo.GetTargetOperatingSystem(); + bool supported = + (os is RuntimeInfoOperatingSystem.Windows + && arch is RuntimeInfoArchitecture.X86 or RuntimeInfoArchitecture.X64 or RuntimeInfoArchitecture.Arm64) + || (os is RuntimeInfoOperatingSystem.Unix or RuntimeInfoOperatingSystem.Apple + && arch is RuntimeInfoArchitecture.X64 or RuntimeInfoArchitecture.Arm or RuntimeInfoArchitecture.Arm64); + if (!supported) + { + scanContext.RecordDeferredFrame(frameAddress); + return; + } + Data.FramedMethodFrame fmf = _target.ProcessedData.GetOrAdd(frameAddress); if (fmf.MethodDescPtr == TargetPointer.Null) { From a6c77e59401aad03ca95ce9a9516c4b345894586 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Wed, 8 Jul 2026 10:25:37 -0400 Subject: [PATCH 6/7] cDAC SysV: hoist TransitionBlock; group Data enum entries; misc cleanup * DataType: move EEClassOptionalFields / SystemVEightByteRegistersInfo back next to EEClass so type-system entries stay grouped together. * CallingConvention_1.ComputeArgGCRefMapBlobCore: hoist the shared TransitionBlock to the top of the method so the SysV struct-in-regs branch and the pos/offset conversion loop reuse the same instance. * CallingConvention_1.GetArgumentLayout: unwrap the ArgLocDesc? at the call site with a throw-on-null instead of dereferencing Value later; makes the invariant explicit at the point it's required. * GcScanner.PromoteCallerStack: drop the arch/OS supported-target gate again -- TryComputeArgGCRefMapBlob's own NotImplementedException path is the sole decline point. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../CallingConvention/CallingConvention_1.cs | 10 ++++----- .../Contracts/StackWalk/GC/GcScanner.cs | 21 ------------------- .../DataType.cs | 4 ++-- 3 files changed, 6 insertions(+), 29 deletions(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CallingConvention_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CallingConvention_1.cs index 88fe17c903356f..a4fdb36903368c 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CallingConvention_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CallingConvention_1.cs @@ -228,8 +228,7 @@ private ArgumentLayout GetArgumentLayout(MethodDescHandle methodDesc) // SystemV-AMD64 struct-in-registers. SYSTEMV_AMD64_CORINFO_STRUCT_REG_PASSING_DESCRIPTOR sysvDesc; parameterTypes[argIndex].GetSystemVAmd64PassStructInRegisterDescriptor(out sysvDesc); - Internal.CallingConvention.ArgLocDesc? loc = argit.GetArgLoc(argOffset); - Debug.Assert(loc.HasValue); + ArgLocDesc loc = argit.GetArgLoc(argOffset) ?? throw new InvalidOperationException("ArgIterator returned null ArgLocDesc for struct-in-registers argument"); arguments.Add(new ArgumentLocation { @@ -238,7 +237,7 @@ private ArgumentLayout GetArgumentLayout(MethodDescHandle methodDesc) TypeHandle = methodSig.ParameterTypes[argIndex], IsStructPassedInRegs = true, SysVEightByteDescriptor = sysvDesc, - SysVIdxGenReg = loc.Value.m_idxGenReg, + SysVIdxGenReg = loc.m_idxGenReg, OpenGenericType = paramInfo[argIndex].OpenGenericType, }); argIndex++; @@ -623,6 +622,7 @@ public MethodAndTypeContextProvider(Target target, ModuleHandle moduleHandle, IR bool isX86 = arch is RuntimeInfoArchitecture.X86; int pointerSize = _target.PointerSize; + TransitionBlock tb = BuildTransitionBlock(runtimeInfo); SortedDictionary tokens = new(); ArgumentLayout enumeration = GetArgumentLayout(methodDesc); @@ -644,8 +644,7 @@ public MethodAndTypeContextProvider(Target target, ModuleHandle moduleHandle, IR { // Mirrors ArgDestination::ReportPointersFromStructInRegisters // in src/coreclr/vm/argdestination.h. - TransitionBlock tbForStruct = BuildTransitionBlock(runtimeInfo); - int genRegOffset = tbForStruct.OffsetOfArgumentRegisters + arg.SysVIdxGenReg * pointerSize; + int genRegOffset = tb.OffsetOfArgumentRegisters + arg.SysVIdxGenReg * pointerSize; for (int i = 0; i < arg.SysVEightByteDescriptor.eightByteCount; i++) { SystemVClassificationType cls = (i == 0) @@ -790,7 +789,6 @@ public MethodAndTypeContextProvider(Target target, ModuleHandle moduleHandle, IR // the lowest positions). On non-x86 the mapping is monotonic so we // could iterate the offset map directly, but using OffsetFromGCRefMapPos // for both keeps the code path uniform. - TransitionBlock tb = BuildTransitionBlock(runtimeInfo); // For x86 we need to know how many slot positions exist (we'd otherwise // miss high-pos register slots when the offset map's max is on the diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs index 1753a3ed743a0b..99adf052797a1c 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs @@ -337,27 +337,6 @@ private TargetPointer FindGCRefMap(TargetPointer indirection) /// private void PromoteCallerStack(TargetPointer frameAddress, GcScanContext scanContext) { - // Fast-reject targets whose ArgIterator port isn't yet complete. Without - // this gate the eventual NotImplementedException inside - // TryComputeArgGCRefMapBlob would drive routine stack scans through - // exception-based control flow (expensive, and surfaces as first-chance - // exceptions under debuggers). - // TODO(https://github.com/dotnet/runtime/issues/130008): extend coverage - // and remove this gate. - IRuntimeInfo runtimeInfo = _target.Contracts.RuntimeInfo; - RuntimeInfoArchitecture arch = runtimeInfo.GetTargetArchitecture(); - RuntimeInfoOperatingSystem os = runtimeInfo.GetTargetOperatingSystem(); - bool supported = - (os is RuntimeInfoOperatingSystem.Windows - && arch is RuntimeInfoArchitecture.X86 or RuntimeInfoArchitecture.X64 or RuntimeInfoArchitecture.Arm64) - || (os is RuntimeInfoOperatingSystem.Unix or RuntimeInfoOperatingSystem.Apple - && arch is RuntimeInfoArchitecture.X64 or RuntimeInfoArchitecture.Arm or RuntimeInfoArchitecture.Arm64); - if (!supported) - { - scanContext.RecordDeferredFrame(frameAddress); - return; - } - Data.FramedMethodFrame fmf = _target.ProcessedData.GetOrAdd(frameAddress); if (fmf.MethodDescPtr == TargetPointer.Null) { diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs index c573cd7841551d..365c5c142e86bc 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs @@ -62,6 +62,8 @@ public enum DataType MethodTable, DynamicStaticsInfo, EEClass, + EEClassOptionalFields, + SystemVEightByteRegistersInfo, CoreLibBinder, MethodTableAuxiliaryData, GenericsDictInfo, @@ -214,8 +216,6 @@ public enum DataType EnCAddedStaticField, EnCSyncBlockInfo, UnorderedArrayBase, - EEClassOptionalFields, - SystemVEightByteRegistersInfo, } public static class DataTypeTargetExtensions From 122dabe84d41ee6e3a562c22e9eab76bd12bd1a1 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Wed, 8 Jul 2026 10:52:14 -0400 Subject: [PATCH 7/7] cDAC SysV: remove unused System.Diagnostics using The Debug.Assert this using existed for was replaced with a throw-on-null in a prior commit; drop the now-unused import. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Contracts/CallingConvention/CallingConvention_1.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CallingConvention_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CallingConvention_1.cs index a4fdb36903368c..86be2e82a66fb4 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CallingConvention_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CallingConvention_1.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Diagnostics; using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; using Internal.CallingConvention;