diff --git a/src/coreclr/vm/class.h b/src/coreclr/vm/class.h index bc5f2df4753b5a..5d324262e4d61d 100644 --- a/src/coreclr/vm/class.h +++ b/src/coreclr/vm/class.h @@ -1784,6 +1784,7 @@ template<> struct cdac_data static constexpr size_t NumStaticFields = offsetof(EEClass, m_NumStaticFields); 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); }; // -------------------------------------------------------------------------------------------- diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index e5c8a863c4f5cf..78e3ae3206494a 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -465,6 +465,7 @@ CDAC_TYPE_FIELD(EEClass, T_UINT16, NumInstanceFields, cdac_data::NumIns CDAC_TYPE_FIELD(EEClass, T_UINT16, NumStaticFields, cdac_data::NumStaticFields) 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_END(EEClass) CDAC_TYPE_BEGIN(GenericsDictInfo) @@ -1047,14 +1048,28 @@ CDAC_TYPE_FIELD(TransitionBlock, T_UINT32, OffsetOfArgs, sizeof(TransitionBlock) // Offset to argument registers and first GCRefMap slot (platform-specific) #if (defined(TARGET_AMD64) && !defined(UNIX_AMD64_ABI)) || defined(TARGET_WASM) CDAC_TYPE_FIELD(TransitionBlock, T_UINT32, ArgumentRegisters, sizeof(TransitionBlock)) +CDAC_TYPE_FIELD(TransitionBlock, T_UINT32, ArgumentRegistersOffset, sizeof(TransitionBlock)) CDAC_TYPE_FIELD(TransitionBlock, T_UINT32, FirstGCRefMapSlot, sizeof(TransitionBlock)) #elif defined(TARGET_ARM64) CDAC_TYPE_FIELD(TransitionBlock, T_UINT32, ArgumentRegisters, offsetof(TransitionBlock, m_argumentRegisters)) +CDAC_TYPE_FIELD(TransitionBlock, T_UINT32, ArgumentRegistersOffset, offsetof(TransitionBlock, m_argumentRegisters)) CDAC_TYPE_FIELD(TransitionBlock, T_UINT32, FirstGCRefMapSlot, offsetof(TransitionBlock, m_x8RetBuffReg)) #else CDAC_TYPE_FIELD(TransitionBlock, T_UINT32, ArgumentRegisters, offsetof(TransitionBlock, m_argumentRegisters)) +CDAC_TYPE_FIELD(TransitionBlock, T_UINT32, ArgumentRegistersOffset, offsetof(TransitionBlock, m_argumentRegisters)) CDAC_TYPE_FIELD(TransitionBlock, T_UINT32, FirstGCRefMapSlot, offsetof(TransitionBlock, m_argumentRegisters)) #endif + +// Negative offset to where float argument registers are saved (relative to TransitionBlock pointer). +// This is -sizeof(FloatArgumentRegisters) (-padding) on platforms that have them, 0 otherwise. +#ifdef CALLDESCR_FPARGREGS +#ifdef TARGET_ARM +// ARM has 8-byte alignment padding after FloatArgumentRegisters +CDAC_TYPE_FIELD(TransitionBlock, T_INT32, OffsetOfFloatArgumentRegisters, -(int)(sizeof(FloatArgumentRegisters) + TARGET_POINTER_SIZE)) +#else +CDAC_TYPE_FIELD(TransitionBlock, T_INT32, OffsetOfFloatArgumentRegisters, -(int)sizeof(FloatArgumentRegisters)) +#endif +#endif // CALLDESCR_FPARGREGS CDAC_TYPE_END(TransitionBlock) #ifdef DEBUGGING_SUPPORTED @@ -1505,6 +1520,14 @@ CDAC_GLOBAL(FeatureWebcil, T_UINT8, 1) #else CDAC_GLOBAL(FeatureWebcil, T_UINT8, 0) #endif +#ifdef FEATURE_HFA +CDAC_GLOBAL(FeatureHFA, T_UINT8, 1) +#else +CDAC_GLOBAL(FeatureHFA, T_UINT8, 0) +#endif +#ifdef ARM_SOFTFP +CDAC_GLOBAL(FeatureArmSoftFP, T_UINT8, 1) +#endif // See Object::GetGCSafeMethodTable #ifdef TARGET_64BIT CDAC_GLOBAL(ObjectToMethodTableUnmask, T_UINT8, 1 | 1 << 1 | 1 << 2) @@ -1543,6 +1566,7 @@ CDAC_GLOBAL_POINTER(FreeObjectMethodTable, &::g_pFreeObjectMethodTable) CDAC_GLOBAL_POINTER(ObjectMethodTable, &::g_pObjectClass) CDAC_GLOBAL_POINTER(ObjectArrayMethodTable, &::g_pPredefinedArrayTypes[ELEMENT_TYPE_OBJECT]) CDAC_GLOBAL_POINTER(StringMethodTable, &::g_pStringClass) +CDAC_GLOBAL_POINTER(TypedReferenceMethodTable, &::g_TypedReferenceMT) CDAC_GLOBAL_POINTER(SyncTableEntries, &::g_pSyncTable) CDAC_GLOBAL_POINTER(MiniMetaDataBuffAddress, &::g_MiniMetaDataBuffAddress) CDAC_GLOBAL_POINTER(MiniMetaDataBuffMaxSize, &::g_MiniMetaDataBuffMaxSize) @@ -1612,6 +1636,7 @@ CDAC_GLOBAL_CONTRACT(AuxiliarySymbols, c1) #if FEATURE_COMINTEROP CDAC_GLOBAL_CONTRACT(BuiltInCOM, c1) #endif // FEATURE_COMINTEROP +CDAC_GLOBAL_CONTRACT(CallingConvention, c1) CDAC_GLOBAL_CONTRACT(CodeVersions, c1) CDAC_GLOBAL_CONTRACT(CodeNotifications, c1) #ifdef FEATURE_COMWRAPPERS diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ContractRegistry.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ContractRegistry.cs index 17cfcd1000ee19..0fd09c7d8f4120 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ContractRegistry.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ContractRegistry.cs @@ -124,6 +124,10 @@ public abstract class ContractRegistry /// Gets an instance of the Debugger contract for the target. /// public virtual IDebugger Debugger => GetContract(); + /// + /// Gets an instance of the CallingConvention contract for the target. + /// + public virtual ICallingConvention CallingConvention => GetContract(); /// /// Attempts to get an instance of the requested contract for the target. diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ICallingConvention.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ICallingConvention.cs new file mode 100644 index 00000000000000..a4deacc348b9d5 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ICallingConvention.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts; + +public readonly record struct ArgSlot( + int Offset, + CorElementType ElementType); + +public readonly record struct ArgLayout( + bool IsPassedByRef, + IReadOnlyList Slots, + TypeHandle? ValueTypeHandle = null); + +public readonly record struct CallSiteLayout( + int? ThisOffset, + bool IsValueTypeThis, + int? AsyncContinuationOffset, + int? VarArgCookieOffset, + IReadOnlyList Arguments); + +public interface ICallingConvention : IContract +{ + static string IContract.Name { get; } = nameof(CallingConvention); + + CallSiteLayout ComputeCallSiteLayout(MethodDescHandle method) + => throw new NotImplementedException(); +} + +public readonly struct CallingConvention : ICallingConvention +{ + // Everything throws NotImplementedException +} 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 b2f109681dfc27..d7bcd5228e7f33 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 @@ -119,6 +119,9 @@ public interface IRuntimeTypeSystem : IContract // The component size is only available for strings and arrays. It is the size of the element type of the array, or the size of an ECMA 335 character (2 bytes) uint GetComponentSize(TypeHandle typeHandle) => throw new NotImplementedException(); + // Mirrors native MethodTable::GetNumInstanceFieldBytes: BaseSize - EEClass.BaseSizePadding. + int GetNumInstanceFieldBytes(TypeHandle typeHandle) => throw new NotImplementedException(); + // True if the MethodTable is the sentinel value associated with unallocated space in the managed heap bool IsFreeObjectMethodTable(TypeHandle typeHandle) => throw new NotImplementedException(); // True if the MethodTable is the System.Object MethodTable (g_pObjectClass) @@ -129,6 +132,14 @@ public interface IRuntimeTypeSystem : IContract bool ContainsGCPointers(TypeHandle typeHandle) => throw new NotImplementedException(); // True if the type requires 8-byte alignment on platforms that don't 8-byte align by default (FEATURE_64BIT_ALIGNMENT) bool RequiresAlign8(TypeHandle typeHandle) => throw new NotImplementedException(); + // True if the type is a Homogeneous Floating-point Aggregate (HFA), i.e. a value type whose + // fields are all of the same floating-point or short-vector type. Only meaningful on platforms with HFA (ARM/ARM64); + // returns false on other architectures. + bool IsHFA(TypeHandle typeHandle) => throw new NotImplementedException(); + // Returns the size in bytes of this type if it is a hardware vector type (System.Numerics.Vector`1, + // System.Runtime.Intrinsics.Vector64`1, or System.Runtime.Intrinsics.Vector128`1) with a primitive + // element type, or 0 otherwise. Used by HFA element-size resolution on platforms with HFA. + int GetVectorSize(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(); /// @@ -174,6 +185,12 @@ public interface IRuntimeTypeSystem : IContract CorElementType GetSignatureCorElementType(TypeHandle typeHandle) => throw new NotImplementedException(); bool IsValueType(TypeHandle typeHandle) => throw new NotImplementedException(); + // True if the type is a "ByRefLike" / ref struct (e.g. Span, ReadOnlySpan, + // TypedReference). ByRefLike types can have managed references *and* byref-typed + // fields embedded at arbitrary offsets, and require special handling in the GC + // scanner — the standard GCDesc walk only covers ordinary managed references. + bool IsByRefLike(TypeHandle typeHandle) => throw new NotImplementedException(); + // Internal element type of the type. Unlike GetSignatureCorElementType, this returns the underlying primitive // type for enums (e.g. I4 for an enum with int underlying type) and for PrimitiveValueType categories. // For arrays, reference types, and TypeDescs, behaves identically to GetSignatureCorElementType. @@ -270,6 +287,10 @@ public interface IRuntimeTypeSystem : IContract TargetPointer GetFieldDescByName(TypeHandle typeHandle, string fieldName) => throw new NotImplementedException(); TargetPointer GetFieldDescStaticAddress(TargetPointer fieldDescPointer, bool unboxValueTypes = true) => throw new NotImplementedException(); TargetPointer GetFieldDescThreadStaticAddress(TargetPointer fieldDescPointer, TargetPointer thread, bool unboxValueTypes = true) => throw new NotImplementedException(); + + IEnumerable EnumerateInstanceFieldDescs(TypeHandle typeHandle) => throw new NotImplementedException(); + + TypeHandle LookupApproxFieldTypeHandle(TargetPointer fieldDescPointer) => throw new NotImplementedException(); #endregion FieldDesc inspection APIs #region Other APIs void GetCoreLibFieldDescAndDef(string typeNamespace, string typeName, string fieldName, out TargetPointer fieldDescAddr, out FieldDefinition fieldDef) => 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 808b28a8b44de6..b34f6197d36acb 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs @@ -26,6 +26,8 @@ public static class Globals public const string FeatureOnStackReplacement = nameof(FeatureOnStackReplacement); public const string FeaturePortableEntrypoints = nameof(FeaturePortableEntrypoints); public const string FeatureWebcil = nameof(FeatureWebcil); + public const string FeatureHFA = nameof(FeatureHFA); + public const string FeatureArmSoftFP = nameof(FeatureArmSoftFP); public const string ObjectToMethodTableUnmask = nameof(ObjectToMethodTableUnmask); public const string SOSBreakingChangeVersion = nameof(SOSBreakingChangeVersion); @@ -37,6 +39,7 @@ public static class Globals public const string ObjectMethodTable = nameof(ObjectMethodTable); public const string ObjectArrayMethodTable = nameof(ObjectArrayMethodTable); public const string StringMethodTable = nameof(StringMethodTable); + public const string TypedReferenceMethodTable = nameof(TypedReferenceMethodTable); public const string MiniMetaDataBuffAddress = nameof(MiniMetaDataBuffAddress); public const string MiniMetaDataBuffMaxSize = nameof(MiniMetaDataBuffMaxSize); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/AMD64UnixArgIterator.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/AMD64UnixArgIterator.cs new file mode 100644 index 00000000000000..6a0f3b10fc8012 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/AMD64UnixArgIterator.cs @@ -0,0 +1,207 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Diagnostics.DataContractReader.RuntimeTypeSystemHelpers; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.CallingConventionHelpers; + +internal sealed class AMD64UnixArgIterator : ArgIteratorBase +{ + private readonly Target _target; + + public override int NumArgumentRegisters => 6; + public override int NumFloatArgumentRegisters => 8; + public override int FloatRegisterSize => 16; + public override int EnregisteredParamTypeMaxSize => 16; + public override int EnregisteredReturnTypeIntegerMaxSize => 16; + public override int StackSlotSize => 8; + public override bool IsRetBuffPassedAsFirstArg => true; + + public AMD64UnixArgIterator( + TransitionBlockLayout layout, + ArgIteratorData argData, + bool hasParamType, + bool hasAsyncContinuation) + : base(layout, argData, hasParamType, hasAsyncContinuation) + { + _target = layout.Target; + } + + public override bool IsArgPassedByRefBySize(int size) => false; + + private static bool CanClassifyStruct(ArgTypeInfo typeInfo) + => typeInfo.RuntimeTypeHandle.IsMethodTable(); + + private SystemVStructDescriptor ClassifyStruct(ArgTypeInfo typeInfo, int structSize) + => SystemVStructClassifier.Classify(_target, typeInfo.RuntimeTypeHandle, structSize); + + protected override bool ValueTypeReturnNeedsRetBuf(ArgTypeInfo thRetType) + { + int size = thRetType.Size; + if (size > EnregisteredReturnTypeIntegerMaxSize) + return true; + if (!CanClassifyStruct(thRetType)) + return true; + SystemVStructDescriptor descriptor = ClassifyStruct(thRetType, size); + return !descriptor.PassedInRegisters; + } + + public override IEnumerable EnumerateArgs() + { + int idxGenReg = ComputeInitialNumRegistersUsed(); + int idxStack = 0; + int idxFPReg = 0; + + for (int argNum = 0; argNum < NumFixedArgs; argNum++) + { + CorElementType argType = GetArgumentType(argNum, out ArgTypeInfo argTypeInfo); + int argSize = GetElemSize(argType, argTypeInfo, _layout.PointerSize); + + IReadOnlyList locations; + + if (argType != CorElementType.ValueType && argType != CorElementType.TypedByRef) + { + locations = [PlaceScalar(argType, argSize, ref idxGenReg, ref idxFPReg, ref idxStack)]; + } + else + { + SystemVStructDescriptor descriptor = default; + if (argSize <= SystemVStructDescriptor.MaxStructBytesToPassInRegisters + && CanClassifyStruct(argTypeInfo)) + { + descriptor = ClassifyStruct(argTypeInfo, argSize); + } + + if (descriptor.PassedInRegisters + && TryClassifySysVLocations(descriptor, idxGenReg, idxFPReg, out List sysvLocations)) + { + locations = sysvLocations; + foreach (ArgLocation l in sysvLocations) + { + if (l.Kind == ArgLocationKind.GpRegister) idxGenReg++; + else if (l.Kind == ArgLocationKind.FpRegister) idxFPReg++; + } + } + else + { + locations = [PlaceStructOnStackLocal(argSize, argType, ref idxStack)]; + } + } + + bool isByRef = argType == CorElementType.Byref; + + yield return new ArgLocDesc + { + ArgType = argType, + ArgSize = argSize, + ArgTypeInfo = argTypeInfo, + IsByRef = isByRef, + Locations = locations, + }; + } + } + + private ArgLocation PlaceScalar( + CorElementType argType, int argSize, + ref int idxGenReg, ref int idxFPReg, ref int idxStack) + { + int cbArg = StackElemSize(argSize); + + if (argType is CorElementType.R4 or CorElementType.R8) + { + if (idxFPReg < NumFloatArgumentRegisters) + { + var loc = new ArgLocation + { + Kind = ArgLocationKind.FpRegister, + TransitionBlockOffset = _layout.OffsetOfFloatArgumentRegisters + idxFPReg * FloatRegisterSize, + Size = FloatRegisterSize, + ElementType = argType, + }; + idxFPReg++; + return loc; + } + } + else + { + int cGenRegs = cbArg / 8; + if (cGenRegs == 0) cGenRegs = 1; + if (idxGenReg + cGenRegs <= NumArgumentRegisters) + { + var loc = new ArgLocation + { + Kind = ArgLocationKind.GpRegister, + TransitionBlockOffset = _layout.ArgumentRegistersOffset + idxGenReg * _layout.PointerSize, + Size = cGenRegs * _layout.PointerSize, + ElementType = argType, + }; + idxGenReg += cGenRegs; + return loc; + } + } + + return PlaceOnStackLocal(cbArg, argType, ref idxStack); + } + + private ArgLocation PlaceStructOnStackLocal(int argSize, CorElementType argType, ref int idxStack) + => PlaceOnStackLocal(StackElemSize(argSize), argType, ref idxStack); + + private ArgLocation PlaceOnStackLocal(int cbArg, CorElementType argType, ref int idxStack) + { + int stackOfs = _layout.OffsetOfArgs + idxStack * _layout.PointerSize; + int slots = cbArg / _layout.PointerSize; + if (slots == 0) slots = 1; + idxStack += slots; + return new ArgLocation + { + Kind = ArgLocationKind.Stack, + TransitionBlockOffset = stackOfs, + Size = cbArg, + ElementType = argType, + }; + } + + private bool TryClassifySysVLocations( + SystemVStructDescriptor descriptor, + int startGenReg, int startFPReg, + out List locations) + { + int neededGen = 0, neededFP = 0; + for (int i = 0; i < descriptor.EightByteCount; i++) + { + SystemVClassification c = descriptor.Classification(i); + if (c is SystemVClassification.Integer or SystemVClassification.IntegerReference or SystemVClassification.IntegerByRef) + neededGen++; + else if (c == SystemVClassification.SSE) + neededFP++; + else { locations = null!; return false; } + } + + if (startGenReg + neededGen > NumArgumentRegisters || startFPReg + neededFP > NumFloatArgumentRegisters) + { locations = null!; return false; } + + locations = new List(descriptor.EightByteCount); + int genIdx = startGenReg, fpIdx = startFPReg; + for (int i = 0; i < descriptor.EightByteCount; i++) + { + SystemVClassification c = descriptor.Classification(i); + switch (c) + { + case SystemVClassification.Integer: + locations.Add(new ArgLocation { Kind = ArgLocationKind.GpRegister, TransitionBlockOffset = _layout.ArgumentRegistersOffset + genIdx * _layout.PointerSize, Size = _layout.PointerSize, ElementType = CorElementType.I8 }); + genIdx++; break; + case SystemVClassification.IntegerReference: + locations.Add(new ArgLocation { Kind = ArgLocationKind.GpRegister, TransitionBlockOffset = _layout.ArgumentRegistersOffset + genIdx * _layout.PointerSize, Size = _layout.PointerSize, ElementType = CorElementType.Class }); + genIdx++; break; + case SystemVClassification.IntegerByRef: + locations.Add(new ArgLocation { Kind = ArgLocationKind.GpRegister, TransitionBlockOffset = _layout.ArgumentRegistersOffset + genIdx * _layout.PointerSize, Size = _layout.PointerSize, ElementType = CorElementType.Byref }); + genIdx++; break; + case SystemVClassification.SSE: + locations.Add(new ArgLocation { Kind = ArgLocationKind.FpRegister, TransitionBlockOffset = _layout.OffsetOfFloatArgumentRegisters + fpIdx * FloatRegisterSize, Size = FloatRegisterSize, ElementType = CorElementType.R8 }); + fpIdx++; break; + } + } + return true; + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/AMD64WindowsArgIterator.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/AMD64WindowsArgIterator.cs new file mode 100644 index 00000000000000..d9eee4dc5142d9 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/AMD64WindowsArgIterator.cs @@ -0,0 +1,89 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Numerics; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.CallingConventionHelpers; + +internal sealed class AMD64WindowsArgIterator : ArgIteratorBase +{ + public override int NumArgumentRegisters => 4; // RCX, RDX, R8, R9 + public override int NumFloatArgumentRegisters => 0; // Shared with GP regs on Windows + public override int FloatRegisterSize => 16; + public override int EnregisteredParamTypeMaxSize => 8; + public override int EnregisteredReturnTypeIntegerMaxSize => 8; // RAX + public override int StackSlotSize => 8; + public override bool IsRetBuffPassedAsFirstArg => true; + + public AMD64WindowsArgIterator( + TransitionBlockLayout layout, + ArgIteratorData argData, + bool hasParamType, + bool hasAsyncContinuation) + : base(layout, argData, hasParamType, hasAsyncContinuation) + { + } + + public override int SizeOfFrameArgumentArray() + => (int)SizeOfArgStack() + SizeOfArgumentRegisters; + + public override bool IsArgPassedByRefBySize(int size) + => size > EnregisteredParamTypeMaxSize || !BitOperations.IsPow2(size); + + public override IEnumerable EnumerateArgs() + { + int curOfs = (int)_layout.OffsetOfArgs + ComputeInitialNumRegistersUsed() * _layout.PointerSize; + + for (int argNum = 0; argNum < NumFixedArgs; argNum++) + { + CorElementType argType = GetArgumentType(argNum, out ArgTypeInfo argTypeInfo); + int argSize = ArgTypeInfo.GetElemSize(argType, argTypeInfo, _layout.PointerSize); + + int cFPRegs = argType is CorElementType.R4 or CorElementType.R8 ? 1 : 0; + int argOfs = curOfs - (int)_layout.OffsetOfArgs; + curOfs += _layout.PointerSize; + + ArgLocation location; + if (cFPRegs == 0 || argOfs >= SizeOfArgumentRegisters) + { + ArgLocationKind kind = argOfs < SizeOfArgumentRegisters + ? ArgLocationKind.GpRegister + : ArgLocationKind.Stack; + int size = kind == ArgLocationKind.GpRegister + ? _layout.PointerSize + : StackElemSize(argSize); + location = new ArgLocation + { + Kind = kind, + TransitionBlockOffset = argOfs + (int)_layout.OffsetOfArgs, + Size = size, + ElementType = argType, + }; + } + else + { + int idxFpReg = argOfs / _layout.PointerSize; + location = new ArgLocation + { + Kind = ArgLocationKind.FpRegister, + TransitionBlockOffset = _layout.OffsetOfFloatArgumentRegisters + idxFpReg * FloatRegisterSize, + Size = FloatRegisterSize, + ElementType = argType, + }; + } + + bool isByRef = argType == CorElementType.Byref + || (argType == CorElementType.ValueType && IsArgPassedByRefBySize(argSize)); + + yield return new ArgLocDesc + { + ArgType = argType, + ArgSize = argSize, + ArgTypeInfo = argTypeInfo, + IsByRef = isByRef, + Locations = [location], + }; + } + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgIteratorBase.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgIteratorBase.cs new file mode 100644 index 00000000000000..05974ca6b1613d --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgIteratorBase.cs @@ -0,0 +1,309 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Numerics; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.CallingConventionHelpers; + +internal abstract class ArgIteratorBase +{ + protected readonly TransitionBlockLayout _layout; + protected readonly ArgIteratorData _argData; + + private readonly bool _hasThis; + private readonly bool _hasParamType; + private readonly bool _hasAsyncContinuation; + + protected bool _SIZE_OF_ARG_STACK_COMPUTED; + protected int _nSizeOfArgStack; + + private bool _RETURN_FLAGS_COMPUTED; + private bool _RETURN_HAS_RET_BUFFER; + + #region Construction + + protected ArgIteratorBase( + TransitionBlockLayout layout, + ArgIteratorData argData, + bool hasParamType, + bool hasAsyncContinuation) + { + _layout = layout; + _argData = argData; + _hasThis = argData.HasThis(); + _hasParamType = hasParamType; + _hasAsyncContinuation = hasAsyncContinuation; + } + + #endregion + + #region ABI characteristics + + public abstract int NumArgumentRegisters { get; } + public abstract int NumFloatArgumentRegisters { get; } + public abstract int FloatRegisterSize { get; } + public abstract int EnregisteredParamTypeMaxSize { get; } + public abstract int EnregisteredReturnTypeIntegerMaxSize { get; } + public abstract int StackSlotSize { get; } + public abstract bool IsRetBuffPassedAsFirstArg { get; } + + public bool HasThis => _hasThis; + public bool IsVarArg => _argData.IsVarArg(); + public bool HasParamType => _hasParamType; + public bool HasAsyncContinuation => _hasAsyncContinuation; + public int NumFixedArgs => _argData.NumFixedArgs(); + public int SizeOfArgumentRegisters => NumArgumentRegisters * _layout.PointerSize; + + #endregion + + #region Hidden arguments + + public virtual int GetThisOffset() + => _layout.ArgumentRegistersOffset; + + public virtual int OffsetFromGCRefMapPos(int pos) + => _layout.FirstGCRefMapSlot + pos * _layout.PointerSize; + + public virtual int GetRetBuffArgOffset(bool hasThis) + => _layout.ArgumentRegistersOffset + (hasThis ? StackElemSize(_layout.PointerSize) : 0); + + public virtual int GetVASigCookieOffset() + { + Debug.Assert(IsVarArg); + + int offset = _layout.ArgumentRegistersOffset; + int slotSize = StackElemSize(_layout.PointerSize); + + if (HasThis) + { + offset += slotSize; + } + + if (HasRetBuffArg() && IsRetBuffPassedAsFirstArg) + { + offset += slotSize; + } + + return offset; + } + + public virtual int GetParamTypeArgOffset() + { + Debug.Assert(HasParamType); + + int offset = _layout.ArgumentRegistersOffset; + int slotSize = StackElemSize(_layout.PointerSize); + + if (HasThis) + { + offset += slotSize; + } + + if (HasRetBuffArg() && IsRetBuffPassedAsFirstArg) + { + offset += slotSize; + } + + return offset; + } + + public virtual int GetAsyncContinuationArgOffset() + { + Debug.Assert(HasAsyncContinuation); + + int offset = _layout.ArgumentRegistersOffset; + int slotSize = StackElemSize(_layout.PointerSize); + + if (HasThis) + { + offset += slotSize; + } + + if (HasRetBuffArg() && IsRetBuffPassedAsFirstArg) + { + offset += slotSize; + } + + if (HasParamType) + { + offset += slotSize; + } + + return offset; + } + + public virtual int SizeOfFrameArgumentArray() + => checked((int)SizeOfArgStack()); + + public virtual uint CbStackPop() + => 0; + + #endregion + + #region Argument sizing and iteration + + public virtual int StackElemSize(int parmSize, bool isValueType = false, bool isFloatHfa = false) + => AlignUp(parmSize, StackSlotSize); + + public virtual bool IsArgPassedByRefBySize(int size) + => size > EnregisteredParamTypeMaxSize; + + protected virtual int ComputeInitialNumRegistersUsed() + { + int numRegistersUsed = 0; + + if (HasThis) + { + numRegistersUsed++; + } + + if (HasRetBuffArg() && IsRetBuffPassedAsFirstArg) + { + numRegistersUsed++; + } + + Debug.Assert(!IsVarArg || !HasParamType); + + if (HasParamType) + { + numRegistersUsed++; + } + + if (HasAsyncContinuation) + { + numRegistersUsed++; + } + + if (IsVarArg) + { + numRegistersUsed++; + } + + return numRegistersUsed; + } + + public abstract IEnumerable EnumerateArgs(); + + #endregion + + #region Signature inspection + + public CorElementType GetArgumentType(int argNum, out ArgTypeInfo thArgType) + { + return _argData.GetArgumentType(argNum, out thArgType); + } + + public CorElementType GetReturnType(out ArgTypeInfo thRetType) + => _argData.GetReturnType(out thRetType); + + #endregion + + #region Return handling + + public bool HasRetBuffArg() + { + if (!_RETURN_FLAGS_COMPUTED) + { + ComputeReturnFlags(); + } + + return _RETURN_HAS_RET_BUFFER; + } + + protected virtual bool ValueTypeReturnNeedsRetBuf(ArgTypeInfo thRetType) + { + int size = thRetType.Size; + if (size > EnregisteredReturnTypeIntegerMaxSize) + { + return true; + } + + if (_layout.Architecture is RuntimeInfoArchitecture.X86 or RuntimeInfoArchitecture.X64) + { + return size <= 0 || !BitOperations.IsPow2((uint)size); + } + + return false; + } + + private void ComputeReturnFlags() + { + CorElementType type = GetReturnType(out ArgTypeInfo thRetType); + _RETURN_HAS_RET_BUFFER = type switch + { + CorElementType.TypedByRef => true, + CorElementType.ValueType => ValueTypeReturnNeedsRetBuf(thRetType), + _ => false, + }; + _RETURN_FLAGS_COMPUTED = true; + } + + #endregion + + #region Stack sizing + + protected uint SizeOfArgStack() + { + if (!_SIZE_OF_ARG_STACK_COMPUTED) + { + ForceSigWalk(); + } + + Debug.Assert(_SIZE_OF_ARG_STACK_COMPUTED); + Debug.Assert((_nSizeOfArgStack % _layout.PointerSize) == 0); + return (uint)_nSizeOfArgStack; + } + + private void ForceSigWalk() + { + ComputeSizeOfArgStack(); + _SIZE_OF_ARG_STACK_COMPUTED = true; + } + + protected virtual void ComputeSizeOfArgStack() + { + int maxOffset = _layout.OffsetOfArgs; + foreach (ArgLocDesc arg in EnumerateArgs()) + { + foreach (ArgLocation location in arg.Locations) + { + if (location.Kind != ArgLocationKind.Stack) + { + continue; + } + + int endOffset = location.TransitionBlockOffset + location.Size; + if (endOffset > maxOffset) + { + maxOffset = endOffset; + } + } + } + + int sizeOfArgStack = maxOffset - _layout.OffsetOfArgs; + if (_layout.Architecture == RuntimeInfoArchitecture.X64 && + _layout.OperatingSystem == RuntimeInfoOperatingSystem.Windows) + { + sizeOfArgStack = sizeOfArgStack > SizeOfArgumentRegisters + ? sizeOfArgStack - SizeOfArgumentRegisters + : 0; + } + + _nSizeOfArgStack = AlignUp(sizeOfArgStack, StackElemSize(_layout.PointerSize)); + } + + #endregion + + #region Helpers + + public static int AlignUp(int input, int alignTo) + => (input + (alignTo - 1)) & ~(alignTo - 1); + + public static int GetElemSize(CorElementType elementType, ArgTypeInfo typeInfo, int pointerSize) + => ArgTypeInfo.GetElemSize(elementType, typeInfo, pointerSize); + + #endregion +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgIteratorData.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgIteratorData.cs new file mode 100644 index 00000000000000..2aacfae539306c --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgIteratorData.cs @@ -0,0 +1,104 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.CallingConventionHelpers; + +internal enum ArgLocationKind +{ + GpRegister, + FpRegister, + Stack, +} + +internal readonly struct ArgLocation +{ + public required ArgLocationKind Kind { get; init; } + public required int TransitionBlockOffset { get; init; } + public required int Size { get; init; } + public required CorElementType ElementType { get; init; } +} + +internal readonly struct ArgLocDesc +{ + public required CorElementType ArgType { get; init; } + public required int ArgSize { get; init; } + public required ArgTypeInfo ArgTypeInfo { get; init; } + public required bool IsByRef { get; init; } + public required IReadOnlyList Locations { get; init; } +} + +internal sealed class ArgIteratorData +{ + private readonly bool _hasThis; + private readonly bool _isVarArg; + private readonly ArgTypeInfo[] _parameterTypes; + private readonly ArgTypeInfo _returnType; + + public ArgIteratorData(bool hasThis, bool isVarArg, ArgTypeInfo[] parameterTypes, ArgTypeInfo returnType) + { + _hasThis = hasThis; + _isVarArg = isVarArg; + _parameterTypes = parameterTypes; + _returnType = returnType; + } + + public bool HasThis() => _hasThis; + public bool IsVarArg() => _isVarArg; + public int NumFixedArgs() => _parameterTypes.Length; + + public CorElementType GetArgumentType(int argNum, out ArgTypeInfo thArgType) + { + thArgType = _parameterTypes[argNum]; + return thArgType.CorElementType; + } + + public ArgTypeInfo GetByRefArgumentType(int argNum) + { + if (argNum < _parameterTypes.Length && _parameterTypes[argNum].CorElementType == CorElementType.Byref) + return _parameterTypes[argNum]; + return default; + } + + public CorElementType GetReturnType(out ArgTypeInfo thRetType) + { + thRetType = _returnType; + return thRetType.CorElementType; + } +} + +internal enum SystemVClassification : byte +{ + Unknown = 0, + Struct = 1, + NoClass = 2, + Memory = 3, + Integer = 4, + IntegerReference = 5, + IntegerByRef = 6, + SSE = 7, +} + +internal struct SystemVStructDescriptor +{ + public const int MaxEightBytes = 2; + public const int MaxStructBytesToPassInRegisters = 16; + public const int EightByteSizeInBytes = 8; + + public bool PassedInRegisters; + public byte EightByteCount; + public SystemVClassification EightByteClassification0; + public SystemVClassification EightByteClassification1; + public byte EightByteSize0; + public byte EightByteSize1; + public byte EightByteOffset0; + public byte EightByteOffset1; + + public SystemVClassification Classification(int index) => index switch + { + 0 => EightByteClassification0, + 1 => EightByteClassification1, + _ => throw new System.ArgumentOutOfRangeException(nameof(index)), + }; +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgIteratorFactory.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgIteratorFactory.cs new file mode 100644 index 00000000000000..9e65113767a303 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgIteratorFactory.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Diagnostics.DataContractReader.Contracts; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.CallingConventionHelpers; + +internal static class ArgIteratorFactory +{ + public static ArgIteratorBase Create( + TransitionBlockLayout layout, + ArgIteratorData argData, + bool hasParamType, + bool hasAsyncContinuation) + { + return layout.Architecture switch + { + RuntimeInfoArchitecture.X64 => layout.OperatingSystem != RuntimeInfoOperatingSystem.Windows + ? new AMD64UnixArgIterator( + layout, argData, hasParamType, hasAsyncContinuation) + : new AMD64WindowsArgIterator( + layout, argData, hasParamType, hasAsyncContinuation), + _ => throw new NotSupportedException(layout.Architecture.ToString()), + }; + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgTypeInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgTypeInfo.cs new file mode 100644 index 00000000000000..7d1bb1414018b0 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgTypeInfo.cs @@ -0,0 +1,203 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Type information needed by ArgIterator for calling convention analysis. +// Ported from crossgen2's TypeHandle struct in ArgIterator.cs. + +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using Microsoft.Diagnostics.DataContractReader.RuntimeTypeSystemHelpers; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.CallingConventionHelpers; + +internal readonly struct ArgTypeInfo +{ + public CorElementType CorElementType { get; init; } + public int Size { get; init; } + public bool IsValueType { get; init; } + public bool RequiresAlign8 { get; init; } + public bool IsHomogeneousAggregate { get; init; } + public int HomogeneousAggregateElementSize { get; init; } + + public TypeHandle RuntimeTypeHandle { get; init; } + + public bool IsNull => CorElementType == default && Size == 0; + + public static int GetElemSize(CorElementType t, ArgTypeInfo thValueType, int pointerSize) + { + if ((int)t <= 0x1d) + { + int elemSize = s_elemSizes[(int)t]; + if (elemSize == -1) + return thValueType.Size; + if (elemSize == -2) + return pointerSize; + return elemSize; + } + return 0; + } + + private static readonly int[] s_elemSizes = + [ + 0, // ELEMENT_TYPE_END 0x0 + 0, // ELEMENT_TYPE_VOID 0x1 + 1, // ELEMENT_TYPE_BOOLEAN 0x2 + 2, // ELEMENT_TYPE_CHAR 0x3 + 1, // ELEMENT_TYPE_I1 0x4 + 1, // ELEMENT_TYPE_U1 0x5 + 2, // ELEMENT_TYPE_I2 0x6 + 2, // ELEMENT_TYPE_U2 0x7 + 4, // ELEMENT_TYPE_I4 0x8 + 4, // ELEMENT_TYPE_U4 0x9 + 8, // ELEMENT_TYPE_I8 0xa + 8, // ELEMENT_TYPE_U8 0xb + 4, // ELEMENT_TYPE_R4 0xc + 8, // ELEMENT_TYPE_R8 0xd + -2, // ELEMENT_TYPE_STRING 0xe + -2, // ELEMENT_TYPE_PTR 0xf + -2, // ELEMENT_TYPE_BYREF 0x10 + -1, // ELEMENT_TYPE_VALUETYPE 0x11 + -2, // ELEMENT_TYPE_CLASS 0x12 + 0, // ELEMENT_TYPE_VAR 0x13 + -2, // ELEMENT_TYPE_ARRAY 0x14 + 0, // ELEMENT_TYPE_GENERICINST 0x15 + 0, // ELEMENT_TYPE_TYPEDBYREF 0x16 + 0, // UNUSED 0x17 + -2, // ELEMENT_TYPE_I 0x18 + -2, // ELEMENT_TYPE_U 0x19 + 0, // UNUSED 0x1a + -2, // ELEMENT_TYPE_FPTR 0x1b + -2, // ELEMENT_TYPE_OBJECT 0x1c + -2, // ELEMENT_TYPE_SZARRAY 0x1d + ]; + + public static ArgTypeInfo FromTypeHandle(Target target, TypeHandle th) + { + IRuntimeTypeSystem rts = target.Contracts.RuntimeTypeSystem; + CorElementType corType = rts.GetSignatureCorElementType(th); + + switch (corType) + { + case CorElementType.ValueType: + return FromValueType(target, rts, th, corType); + + // Primitives have static sizes -- consult s_elemSizes rather than reading + // the MethodTable. CorElementType for primitives is already normalized by + // GetSignatureCorElementType (e.g. enums collapse to their underlying type). + case CorElementType.Void: + case CorElementType.Boolean: + case CorElementType.Char: + case CorElementType.I1: + case CorElementType.U1: + case CorElementType.I2: + case CorElementType.U2: + case CorElementType.I4: + case CorElementType.U4: + case CorElementType.I8: + case CorElementType.U8: + case CorElementType.R4: + case CorElementType.R8: + case CorElementType.I: + case CorElementType.U: + case CorElementType.FnPtr: + case CorElementType.Ptr: + return ForPrimitive(corType, target.PointerSize, th); + + case CorElementType.Byref: + return ForPrimitive(CorElementType.Byref, target.PointerSize, th); + + default: + // Reference types (Class, String, Object, Array, SzArray, etc.) are + // projected to CorElementType.Class so downstream classification + // (X86ArgIterator, SystemVStructClassifier) sees them as a single + // category. The TypeHandle is preserved for generic-instantiation + // matching. + return ForPrimitive(CorElementType.Class, target.PointerSize, th); + } + } + + private static ArgTypeInfo FromValueType(Target target, IRuntimeTypeSystem rts, TypeHandle th, CorElementType corType) + { + int size = rts.GetNumInstanceFieldBytes(th); + bool requiresAlign8 = rts.RequiresAlign8(th); + bool isHfa = rts.IsHFA(th); + int hfaElemSize = isHfa ? ComputeHfaElementSize(target, rts, th, requiresAlign8) : 0; + + return new ArgTypeInfo + { + CorElementType = corType, + Size = size, + IsValueType = true, + RequiresAlign8 = requiresAlign8, + IsHomogeneousAggregate = isHfa, + HomogeneousAggregateElementSize = hfaElemSize, + RuntimeTypeHandle = th, + }; + } + + private static int ComputeHfaElementSize(Target target, IRuntimeTypeSystem rts, TypeHandle th, bool requiresAlign8) + { + RuntimeInfoArchitecture arch = target.Contracts.RuntimeInfo.GetTargetArchitecture(); + if (arch == RuntimeInfoArchitecture.Arm) + { + return requiresAlign8 ? 8 : 4; + } + if (arch != RuntimeInfoArchitecture.Arm64) + { + // FEATURE_HFA is only enabled on ARM/ARM64; IsHFA should never be true elsewhere. + return 0; + } + + // ARM64: walk the first field, descending into nested value types. All HFA fields + // must be of the same primitive/vector type, so the first field determines the + // element size for the entire aggregate. + TypeHandle current = th; + // Bound the loop to defend against unexpected metadata cycles. + for (int depth = 0; depth < 16; depth++) + { + int vectorSize = rts.GetVectorSize(current); + if (vectorSize != 0) + return vectorSize; + + TargetPointer firstField = rts.GetFieldDescList(current); + if (firstField == TargetPointer.Null) + return 0; + CorElementType fieldType = rts.GetFieldDescType(firstField); + switch (fieldType) + { + case CorElementType.R4: + return 4; + case CorElementType.R8: + return 8; + case CorElementType.ValueType: + TypeHandle nested = rts.LookupApproxFieldTypeHandle(firstField); + if (nested.IsNull || !nested.IsMethodTable()) + return 0; + current = nested; + continue; + default: + // IsHFA should only be set on types that resolve to a valid HFA element; + // anything else here indicates a metadata mismatch we can't classify. + return 0; + } + } + return 0; + } + + public static ArgTypeInfo ForPrimitive(CorElementType corType, int pointerSize) + => ForPrimitive(corType, pointerSize, default); + + public static ArgTypeInfo ForPrimitive(CorElementType corType, int pointerSize, TypeHandle runtimeTypeHandle) + { + return new ArgTypeInfo + { + CorElementType = corType, + Size = GetElemSize(corType, default, pointerSize), + IsValueType = false, + RequiresAlign8 = false, + IsHomogeneousAggregate = false, + HomogeneousAggregateElementSize = 0, + RuntimeTypeHandle = runtimeTypeHandle, + }; + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgTypeInfoSignatureProvider.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgTypeInfoSignatureProvider.cs new file mode 100644 index 00000000000000..c83e34c5e03972 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgTypeInfoSignatureProvider.cs @@ -0,0 +1,298 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using Microsoft.Diagnostics.DataContractReader.SignatureHelpers; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.CallingConventionHelpers; + +internal readonly record struct ArgTypeInfoSignatureContext(TypeHandle ClassContext, MethodDescHandle MethodContext); + +internal sealed class ArgTypeInfoSignatureProvider + : IRuntimeSignatureTypeProvider +{ + private readonly Target _target; + private readonly ModuleHandle _moduleHandle; + private ArgTypeInfo? _cachedTypedReferenceInfo; + private readonly Dictionary _primitiveTypeHandles = new(); + + public ArgTypeInfoSignatureProvider(Target target, ModuleHandle moduleHandle) + { + _target = target; + _moduleHandle = moduleHandle; + } + + public ArgTypeInfo GetPrimitiveType(PrimitiveTypeCode typeCode) + => typeCode switch + { + // Surface the resolved MethodTable for reference primitives so they can be + // matched as type arguments inside generic instantiations (see + // GetGenericInstantiation). The CorElementType.Class projection is preserved + // for backward compatibility with downstream Iterator/SystemV classification. + PrimitiveTypeCode.String + => ArgTypeInfo.ForPrimitive(CorElementType.Class, _target.PointerSize, ResolvePrimitiveTypeHandle(CorElementType.String)), + PrimitiveTypeCode.Object + => ArgTypeInfo.ForPrimitive(CorElementType.Class, _target.PointerSize, ResolvePrimitiveTypeHandle(CorElementType.Object)), + // TypedReference has no class token in the signature blob -- the runtime + // identifies its layout via the well-known g_TypedReferenceMT global. + // Mirroring native callingconvention.h:1351-1355, we substitute the + // TypedReference MethodTable here so the rest of ArgIterator (and the + // SystemV struct classifier) see it as an ordinary 16-byte value type. + PrimitiveTypeCode.TypedReference => GetTypedReferenceInfo(), + _ => PrimitiveWithHandle(PrimitiveToCorElementType(typeCode)), + }; + + private ArgTypeInfo PrimitiveWithHandle(CorElementType corType) + => ArgTypeInfo.ForPrimitive(corType, _target.PointerSize, ResolvePrimitiveTypeHandle(corType)); + + private TypeHandle ResolvePrimitiveTypeHandle(CorElementType corType) + { + if (_primitiveTypeHandles.TryGetValue(corType, out TypeHandle cached)) + return cached; + + TypeHandle th; + try + { + th = _target.Contracts.RuntimeTypeSystem.GetPrimitiveType(corType); + } + catch + { + th = default; + } + + _primitiveTypeHandles[corType] = th; + return th; + } + + private ArgTypeInfo GetTypedReferenceInfo() + { + if (_cachedTypedReferenceInfo is { } cached) + return cached; + + ArgTypeInfo info; + try + { + TargetPointer mtPtr = _target.ReadPointer( + _target.ReadGlobalPointer(Constants.Globals.TypedReferenceMethodTable)); + if (mtPtr == TargetPointer.Null) + { + info = UnresolvedValueType(); + } + else + { + TypeHandle th = _target.Contracts.RuntimeTypeSystem.GetTypeHandle(mtPtr); + info = ArgTypeInfo.FromTypeHandle(_target, th); + } + } + catch + { + // Older runtime images without the TypedReferenceMethodTable global, or + // any failure resolving the type, falls back to a conservative placeholder. + info = UnresolvedValueType(); + } + + _cachedTypedReferenceInfo = info; + return info; + } + + public ArgTypeInfo GetTypeFromDefinition(MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) + => FromTokenLookup(_target.Contracts.Loader.GetLookupTables(_moduleHandle).TypeDefToMethodTable, MetadataTokens.GetToken(handle), rawTypeKind); + + public ArgTypeInfo GetTypeFromReference(MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) + => FromTokenLookup(_target.Contracts.Loader.GetLookupTables(_moduleHandle).TypeRefToMethodTable, MetadataTokens.GetToken(handle), rawTypeKind); + + public ArgTypeInfo GetTypeFromSpecification(MetadataReader reader, ArgTypeInfoSignatureContext genericContext, TypeSpecificationHandle handle, byte rawTypeKind) + // Inline GENERICINST blobs in a method signature are dispatched to + // GetGenericInstantiation directly by SRM (recursing through nested levels), so + // common cases like KeyValuePair> never reach this + // path. This handler only fires for actual TypeSpec *tokens* referenced from a + // signature (e.g., certain custom-modifier encodings). For those, native + // resolves via SigPointer::GetTypeHandleThrowing; cDAC would need an equivalent + // lookup-only RTS API. Until then, fall back to a conservative placeholder. + => rawTypeKind == (byte)SignatureTypeKind.ValueType + ? UnresolvedValueType() + : ArgTypeInfo.ForPrimitive(CorElementType.Class, _target.PointerSize); + + public ArgTypeInfo GetSZArrayType(ArgTypeInfo elementType) => ArgTypeInfo.ForPrimitive(CorElementType.SzArray, _target.PointerSize); + public ArgTypeInfo GetArrayType(ArgTypeInfo elementType, ArrayShape shape) => ArgTypeInfo.ForPrimitive(CorElementType.Array, _target.PointerSize); + public ArgTypeInfo GetByReferenceType(ArgTypeInfo elementType) => ArgTypeInfo.ForPrimitive(CorElementType.Byref, _target.PointerSize); + public ArgTypeInfo GetPointerType(ArgTypeInfo elementType) => ArgTypeInfo.ForPrimitive(CorElementType.Ptr, _target.PointerSize); + + public ArgTypeInfo GetGenericInstantiation(ArgTypeInfo genericType, ImmutableArray typeArguments) + { + // Resolve the constructed instantiation to a concrete loaded TypeHandle so that + // value-type instantiations (KeyValuePair, Span, ValueTuple<...>, etc.) + // surface their actual size / HFA / alignment to ArgIterator -- matching native + // SigPointer::GetTypeHandleThrowing + the loader's available-instantiations + // lookup. Requires the open generic's TypeHandle and a TypeHandle for every + // type argument; if anything is missing we fall back to a conservative + // pointer-sized placeholder for value types (and pass-through for reference + // generics, whose pointer-sized representation is already correct). + TypeHandle openGeneric = genericType.RuntimeTypeHandle; + if (openGeneric.Address != TargetPointer.Null) + { + ImmutableArray.Builder argBuilder = ImmutableArray.CreateBuilder(typeArguments.Length); + bool haveAllArgHandles = true; + for (int i = 0; i < typeArguments.Length; i++) + { + TypeHandle argHandle = typeArguments[i].RuntimeTypeHandle; + if (argHandle.Address == TargetPointer.Null) + { + haveAllArgHandles = false; + break; + } + argBuilder.Add(argHandle); + } + + if (haveAllArgHandles) + { + try + { + TypeHandle constructed = _target.Contracts.RuntimeTypeSystem.GetConstructedType( + openGeneric, + CorElementType.GenericInst, + rank: 0, + argBuilder.MoveToImmutable()); + if (constructed.Address != TargetPointer.Null) + return BuildFromTypeHandle(constructed); + } + catch + { + // Fall through to the conservative placeholder below. + } + } + } + + if (genericType.CorElementType == CorElementType.ValueType) + return UnresolvedValueType(); + return genericType; + } + + public ArgTypeInfo GetGenericMethodParameter(ArgTypeInfoSignatureContext genericContext, int index) + { + try + { + ReadOnlySpan instantiation = _target.Contracts.RuntimeTypeSystem.GetGenericMethodInstantiation(genericContext.MethodContext); + if ((uint)index >= (uint)instantiation.Length) + return ArgTypeInfo.ForPrimitive(CorElementType.Class, _target.PointerSize); + return BuildFromTypeHandle(instantiation[index]); + } + catch + { + return ArgTypeInfo.ForPrimitive(CorElementType.Class, _target.PointerSize); + } + } + + public ArgTypeInfo GetGenericTypeParameter(ArgTypeInfoSignatureContext genericContext, int index) + { + try + { + IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem; + TypeHandle classCtx = genericContext.ClassContext; + + if (rts.IsArray(classCtx, out _)) + { + // Match native SigTypeContext::InitTypeContext (typectxt.cpp): arrays use + // the element type as their class instantiation. RuntimeTypeSystem.GetInstantiation + // returns an empty span for arrays, so consult GetTypeParam directly (the + // managed equivalent of MethodTable::GetArrayInstantiation). + Debug.Assert(index == 0, "Array class context has a 1-element instantiation; index > 0 indicates a malformed signature."); + if (index != 0) + return ArgTypeInfo.ForPrimitive(CorElementType.Class, _target.PointerSize); + return BuildFromTypeHandle(rts.GetTypeParam(classCtx)); + } + + ReadOnlySpan instantiation = rts.GetInstantiation(classCtx); + if ((uint)index >= (uint)instantiation.Length) + return ArgTypeInfo.ForPrimitive(CorElementType.Class, _target.PointerSize); + return BuildFromTypeHandle(instantiation[index]); + } + catch + { + return ArgTypeInfo.ForPrimitive(CorElementType.Class, _target.PointerSize); + } + } + + public ArgTypeInfo GetFunctionPointerType(MethodSignature signature) + => ArgTypeInfo.ForPrimitive(CorElementType.FnPtr, _target.PointerSize); + public ArgTypeInfo GetModifiedType(ArgTypeInfo modifier, ArgTypeInfo unmodifiedType, bool isRequired) => unmodifiedType; + public ArgTypeInfo GetInternalModifiedType(TargetPointer typeHandlePointer, ArgTypeInfo unmodifiedType, bool isRequired) => unmodifiedType; + public ArgTypeInfo GetPinnedType(ArgTypeInfo elementType) => elementType; + + public ArgTypeInfo GetInternalType(TargetPointer typeHandlePointer) + { + if (typeHandlePointer == TargetPointer.Null) + return ArgTypeInfo.ForPrimitive(CorElementType.I, _target.PointerSize); + + try + { + return BuildFromTypeHandle(_target.Contracts.RuntimeTypeSystem.GetTypeHandle(typeHandlePointer)); + } + catch + { + return ArgTypeInfo.ForPrimitive(CorElementType.Class, _target.PointerSize); + } + } + + private ArgTypeInfo FromTokenLookup(TargetPointer lookupTable, int token, byte rawTypeKind) + { + try + { + TargetPointer typeHandlePtr = _target.Contracts.Loader.GetModuleLookupMapElement(lookupTable, (uint)token, out _); + if (typeHandlePtr == TargetPointer.Null) + return FallbackForRawTypeKind(rawTypeKind); + + return BuildFromTypeHandle(_target.Contracts.RuntimeTypeSystem.GetTypeHandle(typeHandlePtr)); + } + catch + { + return FallbackForRawTypeKind(rawTypeKind); + } + } + + private ArgTypeInfo FallbackForRawTypeKind(byte rawTypeKind) + => rawTypeKind == (byte)SignatureTypeKind.ValueType + ? UnresolvedValueType() + : ArgTypeInfo.ForPrimitive(CorElementType.Class, _target.PointerSize); + + private ArgTypeInfo BuildFromTypeHandle(TypeHandle typeHandle) + { + if (typeHandle.Address == TargetPointer.Null) + return ArgTypeInfo.ForPrimitive(CorElementType.Class, _target.PointerSize); + + return ArgTypeInfo.FromTypeHandle(_target, typeHandle); + } + + private ArgTypeInfo UnresolvedValueType() + => new ArgTypeInfo + { + CorElementType = CorElementType.ValueType, + Size = _target.PointerSize, + IsValueType = true, + }; + + private static CorElementType PrimitiveToCorElementType(PrimitiveTypeCode typeCode) => typeCode switch + { + PrimitiveTypeCode.Void => CorElementType.Void, + PrimitiveTypeCode.Boolean => CorElementType.Boolean, + PrimitiveTypeCode.Char => CorElementType.Char, + PrimitiveTypeCode.SByte => CorElementType.I1, + PrimitiveTypeCode.Byte => CorElementType.U1, + PrimitiveTypeCode.Int16 => CorElementType.I2, + PrimitiveTypeCode.UInt16 => CorElementType.U2, + PrimitiveTypeCode.Int32 => CorElementType.I4, + PrimitiveTypeCode.UInt32 => CorElementType.U4, + PrimitiveTypeCode.Int64 => CorElementType.I8, + PrimitiveTypeCode.UInt64 => CorElementType.U8, + PrimitiveTypeCode.Single => CorElementType.R4, + PrimitiveTypeCode.Double => CorElementType.R8, + PrimitiveTypeCode.IntPtr => CorElementType.I, + PrimitiveTypeCode.UIntPtr => CorElementType.U, + _ => CorElementType.Void, + }; +} 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 new file mode 100644 index 00000000000000..6e8da67366974d --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CallingConvention_1.cs @@ -0,0 +1,195 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using Microsoft.Diagnostics.DataContractReader.Contracts.CallingConventionHelpers; +using Microsoft.Diagnostics.DataContractReader.RuntimeTypeSystemHelpers; +using Microsoft.Diagnostics.DataContractReader.SignatureHelpers; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts; + +internal sealed class CallingConvention_1 : ICallingConvention +{ + private static readonly IReadOnlyList EmptyArgs = Array.Empty(); + private static readonly CallSiteLayout EmptyLayout = new(null, false, null, null, EmptyArgs); + + private readonly Target _target; + private readonly TransitionBlockLayout _layout; + + public CallingConvention_1(Target target) + { + _target = target; + _layout = new TransitionBlockLayout(_target); + } + + CallSiteLayout ICallingConvention.ComputeCallSiteLayout(MethodDescHandle method) + { + if (!TryDecodeSignature(method, out MethodSignature methodSig)) + return EmptyLayout; + + bool isVarArg = methodSig.Header.CallingConvention is SignatureCallingConvention.VarArgs; + + bool hasThis = methodSig.Header.IsInstance; + bool requiresInstArg = false; + bool isAsync = false; + bool isValueTypeThis = false; + + try + { + IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem; + requiresInstArg = rts.GetGenericContextLoc(method) == GenericContextLoc.InstArg; + isAsync = rts.IsAsyncMethod(method); + if (hasThis) + { + TargetPointer methodTablePtr = rts.GetMethodTable(method); + TypeHandle enclosingType = rts.GetTypeHandle(methodTablePtr); + isValueTypeThis = rts.IsValueType(enclosingType); + } + } + catch (System.Exception ex) + { + Debug.Fail(ex.ToString()); + } + + ArgTypeInfo[] paramTypes = new ArgTypeInfo[methodSig.ParameterTypes.Length]; + for (int i = 0; i < paramTypes.Length; i++) + paramTypes[i] = methodSig.ParameterTypes[i]; + + ArgIteratorData argData = new( + hasThis, + isVarArg, + paramTypes, + methodSig.ReturnType); + + ArgIteratorBase argit = ArgIteratorFactory.Create( + _layout, + argData, + hasParamType: requiresInstArg, + hasAsyncContinuation: isAsync); + + int? thisOffset = argit.HasThis ? argit.GetThisOffset() : null; + int? asyncOffset = argit.HasAsyncContinuation ? argit.GetAsyncContinuationArgOffset() : null; + int? varArgCookieOffset = isVarArg ? argit.GetVASigCookieOffset() : null; + + List args = new(paramTypes.Length); + int argIndex = 0; + foreach (ArgLocDesc loc in argit.EnumerateArgs()) + { + if (argIndex >= paramTypes.Length) + break; + + var slots = new List(loc.Locations.Count); + foreach (ArgLocation l in loc.Locations) + slots.Add(new ArgSlot(l.TransitionBlockOffset, l.ElementType)); + + args.Add(new ArgLayout(loc.IsByRef, slots, ComputeValueTypeHandle(loc, slots))); + argIndex++; + } + + return new CallSiteLayout(thisOffset, isValueTypeThis, asyncOffset, varArgCookieOffset, args); + } + + private static TypeHandle? ComputeValueTypeHandle(ArgLocDesc loc, List slots) + { + // Pass-by-implicit-reference: the slot holds an interior pointer and the + // GC scanner reports it via the byref path; no value-type walk needed. + if (loc.IsByRef) + return null; + + // Native dispatches by gElementTypeInfo[etype].m_gc; only ValueType / + // TypedByRef route through the GCDesc walk. Other types are handled by + // the per-slot ElementType already. + if (loc.ArgType is not (CorElementType.ValueType or CorElementType.TypedByRef)) + return null; + + // If the per-arch iterator pre-decomposed the storage (e.g. SysV split + // structs on AMD64-Unix emit per-eightbyte ElementTypes like Class/Byref/R8; + // ARM64 HFAs emit per-FP-reg R4/R8 slots), the GC scanner already has + // sufficient information via ArgSlot.ElementType. Walking GCDesc would + // duplicate or contradict the iterator's classification. Discriminator: + // every slot has ElementType == ValueType iff the iterator did NOT + // decompose this argument. + foreach (ArgSlot s in slots) + { + if (s.ElementType != CorElementType.ValueType) + return null; + } + + TypeHandle th = loc.ArgTypeInfo.RuntimeTypeHandle; + if (!th.IsMethodTable()) + return null; + + // Both ordinary value types and ByRefLike types (Span, ref structs) are + // reported through ValueTypeHandle. The GC scanner dispatches on + // IRuntimeTypeSystem.IsByRefLike to choose the walk strategy: a CGCDesc + // series walk for ordinary value types, a field-by-field walk (parallel to + // native ByRefPointerOffsetsReporter in siginfo.cpp) for ByRefLike types. + return th; + } + + private bool TryDecodeSignature(MethodDescHandle method, out MethodSignature methodSig) + { + methodSig = default; + try + { + IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem; + + TargetPointer methodTablePtr = rts.GetMethodTable(method); + TypeHandle typeHandle = rts.GetTypeHandle(methodTablePtr); + TargetPointer modulePtr = rts.GetModule(typeHandle); + + ModuleHandle moduleHandle = _target.Contracts.Loader.GetModuleHandleFromModulePtr(modulePtr); + MetadataReader? mdReader = _target.Contracts.EcmaMetadata.GetMetadata(moduleHandle); + + ArgTypeInfoSignatureProvider provider = new(_target, moduleHandle); + ArgTypeInfoSignatureContext genericContext = new(typeHandle, method); + + if (rts.IsStoredSigMethodDesc(method, out ReadOnlySpan storedSig)) + { + // Stored sigs (dynamic, EEImpl, array method descs) decode without needing + // a metadata reader for primitive or ELEMENT_TYPE_INTERNAL element types. + // A null reader is only a problem if the stored sig references a TypeDef / + // TypeRef / TypeSpec, which is unusual for these method-desc kinds; the + // outer catch handles that case gracefully. + RuntimeSignatureDecoder storedDecoder = new( + provider, _target, mdReader!, genericContext); + unsafe + { + fixed (byte* pStoredSig = storedSig) + { + BlobReader blobReader = new BlobReader(pStoredSig, storedSig.Length); + methodSig = storedDecoder.DecodeMethodSignature(ref blobReader); + } + } + return true; + } + + // Non-stored-sig path: needs a real metadata reader to look up the method def. + if (mdReader is null) + return false; + + RuntimeSignatureDecoder decoder = new( + provider, _target, mdReader, genericContext); + + uint methodToken = rts.GetMethodToken(method); + if (methodToken == (uint)EcmaMetadataUtils.TokenType.mdtMethodDef) + return false; + + MethodDefinitionHandle methodDefHandle = MetadataTokens.MethodDefinitionHandle( + (int)EcmaMetadataUtils.GetRowId(methodToken)); + MethodDefinition methodDef = mdReader.GetMethodDefinition(methodDefHandle); + + BlobReader bodyReader = mdReader.GetBlobReader(methodDef.Signature); + methodSig = decoder.DecodeMethodSignature(ref bodyReader); + return true; + } + catch + { + return false; + } + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/SystemVStructClassifier.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/SystemVStructClassifier.cs new file mode 100644 index 00000000000000..87c44f7e8caa8a --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/SystemVStructClassifier.cs @@ -0,0 +1,394 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using Microsoft.Diagnostics.DataContractReader.RuntimeTypeSystemHelpers; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.CallingConventionHelpers; + +internal static class SystemVStructClassifier +{ + private const int MaxFields = SystemVStructDescriptor.MaxEightBytes * SystemVStructDescriptor.EightByteSizeInBytes; + + private struct Helper + { + public int StructSize; + public int EightByteCount; + public SystemVClassification[] EightByteClassifications; + public int[] EightByteSizes; + public int[] EightByteOffsets; + + public bool InEmbeddedStruct; + public int CurrentUniqueOffsetField; + public int LargestFieldOffset; + public SystemVClassification[] FieldClassifications; + public int[] FieldSizes; + public int[] FieldOffsets; + + public static Helper Create(int totalStructSize) => new() + { + StructSize = totalStructSize, + EightByteCount = 0, + InEmbeddedStruct = false, + CurrentUniqueOffsetField = 0, + LargestFieldOffset = -1, + EightByteClassifications = new SystemVClassification[SystemVStructDescriptor.MaxEightBytes], + EightByteSizes = new int[SystemVStructDescriptor.MaxEightBytes], + EightByteOffsets = new int[SystemVStructDescriptor.MaxEightBytes], + FieldClassifications = new SystemVClassification[MaxFields], + FieldSizes = new int[MaxFields], + FieldOffsets = new int[MaxFields], + }; + } + + public static SystemVStructDescriptor Classify(Target target, TypeHandle typeHandle, int structSize) + { + if (!typeHandle.IsMethodTable() || structSize == 0 + || structSize > SystemVStructDescriptor.MaxStructBytesToPassInRegisters) + { + return default; + } + + IRuntimeTypeSystem rts = target.Contracts.RuntimeTypeSystem; + if (!rts.IsValueType(typeHandle)) + return default; + + // Intrinsic SIMD/Int128 types bypass the SysV struct path (they're handled by + // their own intrinsic register-passing rules at the JIT level). + if (IsSimdOrInt128Intrinsic(rts, typeHandle)) + return default; + + Helper helper = Helper.Create(structSize); + if (!ClassifyEightBytes(target, rts, typeHandle, ref helper, 0)) + return default; + + return new SystemVStructDescriptor + { + PassedInRegisters = true, + EightByteCount = (byte)helper.EightByteCount, + EightByteClassification0 = helper.EightByteClassifications[0], + EightByteClassification1 = helper.EightByteClassifications[1], + EightByteSize0 = (byte)helper.EightByteSizes[0], + EightByteSize1 = (byte)helper.EightByteSizes[1], + EightByteOffset0 = (byte)helper.EightByteOffsets[0], + EightByteOffset1 = (byte)helper.EightByteOffsets[1], + }; + } + + private static bool IsSimdOrInt128Intrinsic(IRuntimeTypeSystem rts, TypeHandle th) + { + // Vector64/128/256/512 of T and Int128/UInt128 carry a non-zero VectorSize on cDAC. + // If the type-system contract reports a vector size, treat it as an intrinsic that + // bypasses SysV struct classification. + try + { + return rts.GetVectorSize(th) != 0; + } + catch + { + return false; + } + } + + private static SystemVClassification CorElementTypeToClassification(CorElementType et) => et switch + { + CorElementType.Boolean or CorElementType.Char + or CorElementType.I1 or CorElementType.U1 + or CorElementType.I2 or CorElementType.U2 + or CorElementType.I4 or CorElementType.U4 + or CorElementType.I8 or CorElementType.U8 + or CorElementType.I or CorElementType.U + or CorElementType.Ptr or CorElementType.FnPtr + => SystemVClassification.Integer, + CorElementType.R4 or CorElementType.R8 + => SystemVClassification.SSE, + CorElementType.ValueType + => SystemVClassification.Struct, // recurse + CorElementType.Class or CorElementType.Object or CorElementType.String + or CorElementType.Array or CorElementType.SzArray + or CorElementType.Var or CorElementType.MVar + or CorElementType.GenericInst + => SystemVClassification.IntegerReference, + CorElementType.Byref + => SystemVClassification.IntegerByRef, + _ => SystemVClassification.Unknown, + }; + + private static SystemVClassification ReClassifyField(SystemVClassification original, SystemVClassification @new) + { + switch (@new) + { + case SystemVClassification.Integer: + // Integer overrides everything; the resulting class is Integer. + return SystemVClassification.Integer; + case SystemVClassification.SSE: + // If both old and new are SSE, the merge is SSE. Otherwise Integer wins. + return original == SystemVClassification.SSE + ? SystemVClassification.SSE + : SystemVClassification.Integer; + case SystemVClassification.IntegerReference: + // IntegerReference can only merge with itself. + return SystemVClassification.IntegerReference; + case SystemVClassification.IntegerByRef: + // IntegerByRef can only merge with itself. + return SystemVClassification.IntegerByRef; + default: + return SystemVClassification.Unknown; + } + } + + private static bool ClassifyEightBytes( + Target target, + IRuntimeTypeSystem rts, + TypeHandle typeHandle, + ref Helper helper, + int startOffsetOfStruct) + { + ushort numInstanceFields = rts.GetNumInstanceFields(typeHandle); + TargetPointer firstFieldDesc = rts.GetFieldDescList(typeHandle); + + if (firstFieldDesc == TargetPointer.Null || numInstanceFields == 0) + { + // Empty struct: classify like padding. + helper.LargestFieldOffset = startOffsetOfStruct; + AssignClassifiedEightByteTypes(ref helper); + return true; + } + + uint fieldDescSize = (uint)target.GetTypeInfo(DataType.FieldDesc).Size!; + + // Walk the FieldDesc array. Each FieldDesc has a fixed size; statics are + // intermixed with instance fields in this array, so we filter by IsStatic. + ushort totalFields = (ushort)(numInstanceFields + rts.GetNumStaticFields(typeHandle) + + rts.GetNumThreadStaticFields(typeHandle)); + + for (ushort i = 0; i < totalFields; i++) + { + TargetPointer fdPtr = new(firstFieldDesc.Value + i * fieldDescSize); + if (rts.IsFieldDescStatic(fdPtr)) + continue; + + CorElementType fieldType = rts.GetFieldDescType(fdPtr); + uint memberDef = rts.GetFieldDescMemberDef(fdPtr); + EntityHandle entity = MetadataTokens.EntityHandle((int)memberDef); + if (entity.IsNil || entity.Kind != HandleKind.FieldDefinition) + return false; + + // Resolve the field's FieldDefinition (used by GetFieldDescOffset for the + // BigRVA path on static RVA fields — instance walks never hit it, but we + // keep the lookup for safety/parity with the offset API contract). + FieldDefinition fieldDef = default; + try + { + TargetPointer enclosingMT = rts.GetMTOfEnclosingClass(fdPtr); + TypeHandle ctx = rts.GetTypeHandle(enclosingMT); + TargetPointer modulePtr = rts.GetModule(ctx); + if (modulePtr != TargetPointer.Null) + { + ModuleHandle moduleHandle = target.Contracts.Loader.GetModuleHandleFromModulePtr(modulePtr); + MetadataReader? mdReader = target.Contracts.EcmaMetadata.GetMetadata(moduleHandle); + if (mdReader is not null) + fieldDef = mdReader.GetFieldDefinition((FieldDefinitionHandle)entity); + } + } + catch + { + return false; + } + + uint fieldOffset = rts.GetFieldDescOffset(fdPtr, fieldDef); + int normalizedFieldOffset = (int)fieldOffset + startOffsetOfStruct; + + int fieldSize = ArgTypeInfo.GetElemSize(fieldType, default, target.PointerSize); + if (fieldType == CorElementType.ValueType) + { + // For nested value-type fields, resolve the field's TypeHandle to get its size. + TypeHandle fieldTH = rts.LookupApproxFieldTypeHandle(fdPtr); + if (fieldTH.IsMethodTable()) + fieldSize = rts.GetNumInstanceFieldBytes(fieldTH); + } + + if (normalizedFieldOffset + fieldSize > helper.StructSize) + return false; + + SystemVClassification fieldClass = CorElementTypeToClassification(fieldType); + if (fieldClass == SystemVClassification.Struct) + { + // Recurse into the nested value type's fields. + TypeHandle nested = rts.LookupApproxFieldTypeHandle(fdPtr); + if (!nested.IsMethodTable()) + return false; + + bool savedInEmbedded = helper.InEmbeddedStruct; + helper.InEmbeddedStruct = true; + bool nestedOk = ClassifyEightBytes(target, rts, nested, ref helper, normalizedFieldOffset); + helper.InEmbeddedStruct = savedInEmbedded; + if (!nestedOk) + return false; + continue; + } + + // Unaligned-field rule: a field that is not naturally aligned forces MEMORY. + if (fieldSize > 0 && (normalizedFieldOffset % fieldSize) != 0) + return false; + + // Overlapping-field (union) handling: if this offset has already been + // recorded, merge the new classification with the existing one. + if (normalizedFieldOffset <= helper.LargestFieldOffset) + { + int existing = -1; + for (int j = helper.CurrentUniqueOffsetField - 1; j >= 0; j--) + { + if (helper.FieldOffsets[j] == normalizedFieldOffset) + { + if (fieldSize > helper.FieldSizes[j]) + helper.FieldSizes[j] = fieldSize; + helper.FieldClassifications[j] = ReClassifyField(helper.FieldClassifications[j], fieldClass); + existing = j; + break; + } + } + if (existing >= 0) + continue; + } + else + { + helper.LargestFieldOffset = normalizedFieldOffset; + } + + if (helper.CurrentUniqueOffsetField >= MaxFields) + return false; + + helper.FieldClassifications[helper.CurrentUniqueOffsetField] = fieldClass; + helper.FieldSizes[helper.CurrentUniqueOffsetField] = fieldSize; + helper.FieldOffsets[helper.CurrentUniqueOffsetField] = normalizedFieldOffset; + helper.CurrentUniqueOffsetField++; + } + + AssignClassifiedEightByteTypes(ref helper); + return true; + } + + private static void AssignClassifiedEightByteTypes(ref Helper helper) + { + const int MaxBytes = SystemVStructDescriptor.MaxEightBytes * SystemVStructDescriptor.EightByteSizeInBytes; + if (helper.InEmbeddedStruct) + return; + + int largestFieldOffset = helper.LargestFieldOffset; + if (largestFieldOffset < 0) + largestFieldOffset = 0; + + Span sortedFieldOrder = stackalloc int[MaxBytes]; + for (int i = 0; i < MaxBytes; i++) sortedFieldOrder[i] = -1; + + int numFields = helper.CurrentUniqueOffsetField; + for (int i = 0; i < numFields; i++) + { + int off = helper.FieldOffsets[i]; + if (off < 0 || off >= MaxBytes) continue; + sortedFieldOrder[off] = i; + } + + int lastFieldOrdinal = largestFieldOffset < MaxBytes ? sortedFieldOrder[largestFieldOffset] : -1; + int lastFieldSize = lastFieldOrdinal >= 0 ? helper.FieldSizes[lastFieldOrdinal] : 0; + int offsetAfterLastFieldByte = largestFieldOffset + lastFieldSize; + SystemVClassification lastFieldClassification = lastFieldOrdinal >= 0 + ? helper.FieldClassifications[lastFieldOrdinal] + : SystemVClassification.NoClass; + + int usedEightBytes = 0; + int accumulatedSizeForEightBytes = 0; + bool foundFieldInEightByte = false; + + for (int offset = 0; offset < helper.StructSize; offset++) + { + SystemVClassification fieldClassificationType; + int fieldSize; + + int ordinal = offset < MaxBytes ? sortedFieldOrder[offset] : -1; + if (ordinal == -1) + { + if (offset < accumulatedSizeForEightBytes) + continue; // inside a previously-processed field + + fieldSize = 1; + // Padding before the last field's end -> NoClass; trailing padding + // inherits the last field's classification (per spec). + fieldClassificationType = offset < offsetAfterLastFieldByte + ? SystemVClassification.NoClass + : lastFieldClassification; + if (offset % SystemVStructDescriptor.EightByteSizeInBytes == 0) + foundFieldInEightByte = false; + } + else + { + foundFieldInEightByte = true; + fieldSize = helper.FieldSizes[ordinal]; + fieldClassificationType = helper.FieldClassifications[ordinal]; + accumulatedSizeForEightBytes = offset + fieldSize; + } + + int fieldStartEightByte = offset / SystemVStructDescriptor.EightByteSizeInBytes; + int fieldEndEightByte = (offset + fieldSize - 1) / SystemVStructDescriptor.EightByteSizeInBytes; + if (fieldEndEightByte >= SystemVStructDescriptor.MaxEightBytes) + return; // shouldn't happen for size <= 16, but guard anyway + + usedEightBytes = System.Math.Max(usedEightBytes, fieldEndEightByte + 1); + + for (int eb = fieldStartEightByte; eb <= fieldEndEightByte; eb++) + { + SystemVClassification existing = helper.EightByteClassifications[eb]; + if (existing == fieldClassificationType) + { + // Already this class + } + else if (existing == SystemVClassification.NoClass) + { + helper.EightByteClassifications[eb] = fieldClassificationType; + } + else if (existing == SystemVClassification.Memory + || fieldClassificationType == SystemVClassification.Memory + || existing == SystemVClassification.IntegerReference + || fieldClassificationType == SystemVClassification.IntegerReference + || existing == SystemVClassification.IntegerByRef + || fieldClassificationType == SystemVClassification.IntegerByRef) + { + helper.EightByteClassifications[eb] = ReClassifyField(existing, fieldClassificationType); + } + else if (existing == SystemVClassification.Integer + || fieldClassificationType == SystemVClassification.Integer) + { + helper.EightByteClassifications[eb] = SystemVClassification.Integer; + } + else + { + helper.EightByteClassifications[eb] = SystemVClassification.SSE; + } + } + + if ((offset + 1) % SystemVStructDescriptor.EightByteSizeInBytes == 0) + { + // Promote NoClass eightbyte to Integer when there's no field in it + // (matches the workaround in methodtable.cpp:2660). + int eb = offset / SystemVStructDescriptor.EightByteSizeInBytes; + if (!foundFieldInEightByte + && helper.EightByteClassifications[eb] == SystemVClassification.NoClass) + { + helper.EightByteClassifications[eb] = SystemVClassification.Integer; + } + foundFieldInEightByte = false; + } + } + + helper.EightByteCount = usedEightBytes; + for (int i = 0; i < usedEightBytes; i++) + { + helper.EightByteOffsets[i] = i * SystemVStructDescriptor.EightByteSizeInBytes; + int remaining = helper.StructSize - helper.EightByteOffsets[i]; + helper.EightByteSizes[i] = System.Math.Min(remaining, SystemVStructDescriptor.EightByteSizeInBytes); + } + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/TransitionBlockLayout.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/TransitionBlockLayout.cs new file mode 100644 index 00000000000000..3284996ca8033b --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/TransitionBlockLayout.cs @@ -0,0 +1,35 @@ +// 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.Contracts.CallingConventionHelpers; + +internal sealed class TransitionBlockLayout +{ + public Target Target { get; } + public int SizeOfTransitionBlock { get; } + public int ArgumentRegistersOffset { get; } + public int FirstGCRefMapSlot { get; } + public int OffsetOfArgs { get; } + public int OffsetOfFloatArgumentRegisters { get; } + public int PointerSize { get; } + public RuntimeInfoArchitecture Architecture { get; } + public RuntimeInfoOperatingSystem OperatingSystem { get; } + + public TransitionBlockLayout(Target target) + { + Target = target; + IRuntimeInfo runtimeInfo = target.Contracts.RuntimeInfo; + Architecture = runtimeInfo.GetTargetArchitecture(); + OperatingSystem = runtimeInfo.GetTargetOperatingSystem(); + PointerSize = target.PointerSize; + + Target.TypeInfo tbType = target.GetTypeInfo(DataType.TransitionBlock); + SizeOfTransitionBlock = (int)tbType.Size!; + ArgumentRegistersOffset = tbType.Fields["ArgumentRegistersOffset"].Offset; + FirstGCRefMapSlot = tbType.Fields["FirstGCRefMapSlot"].Offset; + OffsetOfArgs = tbType.Fields["OffsetOfArgs"].Offset; + OffsetOfFloatArgumentRegisters = tbType.Fields.ContainsKey("OffsetOfFloatArgumentRegisters") + ? tbType.Fields["OffsetOfFloatArgumentRegisters"].Offset + : 0; + } +} 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 e09129f2d1b38b..4c90916bba866d 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 @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Reflection.Metadata.Ecma335; using Microsoft.Diagnostics.DataContractReader.RuntimeTypeSystemHelpers; using Microsoft.Diagnostics.DataContractReader.Data; @@ -574,7 +575,65 @@ public bool IsObjRef(TypeHandle typeHandle) return elementType is CorElementType.Class or CorElementType.Array or CorElementType.SzArray; } public bool ContainsGCPointers(TypeHandle typeHandle) => !typeHandle.IsMethodTable() ? false : _methodTables[typeHandle.Address].Flags.ContainsGCPointers; + public bool IsByRefLike(TypeHandle typeHandle) => !typeHandle.IsMethodTable() ? false : _methodTables[typeHandle.Address].Flags.IsByRefLike; public bool RequiresAlign8(TypeHandle typeHandle) => !typeHandle.IsMethodTable() ? false : _methodTables[typeHandle.Address].Flags.RequiresAlign8; + + public bool IsHFA(TypeHandle typeHandle) + { + if (!typeHandle.IsMethodTable()) + return false; + + // The IsHFA bit in the MethodTable flags is only meaningful when the target runtime + // was built with FEATURE_HFA. On other configurations the bit is either unused or + // reused for a different purpose (UNIX_AMD64_ABI uses it as IsRegStructPassed). + if (!_target.TryReadGlobal(Constants.Globals.FeatureHFA, out byte? featureHFA) || featureHFA == 0) + return false; + + return _methodTables[typeHandle.Address].Flags.IsHFA; + } + + public int GetVectorSize(TypeHandle typeHandle) + { + // Mirrors MethodTable::GetVectorSize in src/coreclr/vm/class.cpp. + + if (!typeHandle.IsMethodTable() || + _target.PointerSize != 8 || + !_methodTables[typeHandle.Address].Flags.IsIntrinsicType || + !TryGetMetadataReader(typeHandle, out _, out MetadataReader? mdReader)) + { + return 0; + } + + uint typeDefTok = GetTypeDefToken(typeHandle); + EntityHandle entityHandle = MetadataTokens.EntityHandle((int)typeDefTok); + if (entityHandle.IsNil || entityHandle.Kind != HandleKind.TypeDefinition) + return 0; + + TypeDefinition typeDef = mdReader.GetTypeDefinition((TypeDefinitionHandle)entityHandle); + string name = mdReader.GetString(typeDef.Name); + string ns = mdReader.GetString(typeDef.Namespace); + + // Mirrors MethodTable::GetVectorSize in src/coreclr/vm/class.cpp. + int vectorSize = (ns, name) switch + { + // System.Numerics.Vector`1's runtime size depends on the runtime-chosen vector width. + ("System.Numerics", "Vector`1") => GetNumInstanceFieldBytes(typeHandle), + ("System.Runtime.Intrinsics", "Vector128`1") => 16, + ("System.Runtime.Intrinsics", "Vector64`1") => 8, + _ => 0, + }; + if (vectorSize == 0) + return 0; + + // Validate that T (the element type) is a primitive type, matching the runtime check. + TypeHandle typeArg = GetInstantiation(typeHandle)[0]; + CorElementType elemType = GetSignatureCorElementType(typeArg); + bool isPrimitive = (elemType >= CorElementType.I1 && elemType <= CorElementType.R8) + || elemType == CorElementType.I + || elemType == CorElementType.U; + return isPrimitive ? vectorSize : 0; + } + public bool IsContinuation(TypeHandle typeHandle) => typeHandle.IsMethodTable() && _continuationMethodTablePointer != TargetPointer.Null && _methodTables[typeHandle.Address].ParentMethodTable == _continuationMethodTablePointer; @@ -2036,10 +2095,6 @@ TargetPointer IRuntimeTypeSystem.GetFieldDescByName(TypeHandle typeHandle, strin if (!typeHandle.IsMethodTable()) return TargetPointer.Null; - TargetPointer modulePtr = GetModule(typeHandle); - if (modulePtr == TargetPointer.Null) - return TargetPointer.Null; - uint typeDefToken = GetTypeDefToken(typeHandle); if (typeDefToken == 0) return TargetPointer.Null; @@ -2050,12 +2105,10 @@ TargetPointer IRuntimeTypeSystem.GetFieldDescByName(TypeHandle typeHandle, strin TypeDefinitionHandle typeDefHandle = (TypeDefinitionHandle)entityHandle; - ILoader loader = _target.Contracts.Loader; - ModuleHandle moduleHandle = loader.GetModuleHandleFromModulePtr(modulePtr); - MetadataReader? md = _target.Contracts.EcmaMetadata.GetMetadata(moduleHandle); - if (md is null) + if (!TryGetMetadataReader(typeHandle, out ModuleHandle moduleHandle, out MetadataReader? md)) return TargetPointer.Null; + ILoader loader = _target.Contracts.Loader; TargetPointer fieldDefToDescMap = loader.GetLookupTables(moduleHandle).FieldDefToDesc; foreach (FieldDefinitionHandle fieldDefHandle in md.GetTypeDefinition(typeDefHandle).GetFields()) { @@ -2090,9 +2143,6 @@ private TargetPointer GetFieldDescStaticOrThreadStaticAddress(TargetPointer fiel { TargetPointer enclosingMT = ((IRuntimeTypeSystem)this).GetMTOfEnclosingClass(fieldDescPointer); TypeHandle ctx = GetTypeHandle(enclosingMT); - TargetPointer modulePtr = GetModule(ctx); - ILoader loader = _target.Contracts.Loader; - ModuleHandle moduleHandle = loader.GetModuleHandleFromModulePtr(modulePtr); CorElementType type = ((IRuntimeTypeSystem)this).GetFieldDescType(fieldDescPointer); TargetPointer @base; if (type == CorElementType.Class || type == CorElementType.ValueType) @@ -2121,7 +2171,9 @@ private TargetPointer GetFieldDescStaticOrThreadStaticAddress(TargetPointer fiel if (@base == TargetPointer.Null) return TargetPointer.Null; - MetadataReader mdReader = _target.Contracts.EcmaMetadata.GetMetadata(moduleHandle)!; + if (!TryGetMetadataReader(ctx, out ModuleHandle moduleHandle, out MetadataReader? mdReader)) + return TargetPointer.Null; + uint token = ((IRuntimeTypeSystem)this).GetFieldDescMemberDef(fieldDescPointer); FieldDefinitionHandle fieldHandle = (FieldDefinitionHandle)MetadataTokens.Handle((int)token); FieldDefinition fieldDef = mdReader.GetFieldDefinition(fieldHandle); @@ -2142,6 +2194,75 @@ private TargetPointer GetFieldDescStaticOrThreadStaticAddress(TargetPointer fiel TargetPointer IRuntimeTypeSystem.GetFieldDescThreadStaticAddress(TargetPointer fieldDescPointer, TargetPointer thread, bool unboxValueTypes) => GetFieldDescStaticOrThreadStaticAddress(fieldDescPointer, thread, unboxValueTypes); + IEnumerable IRuntimeTypeSystem.EnumerateInstanceFieldDescs(TypeHandle typeHandle) + { + if (!typeHandle.IsMethodTable()) + yield break; + + TargetPointer firstFieldDesc = ((IRuntimeTypeSystem)this).GetFieldDescList(typeHandle); + if (firstFieldDesc == TargetPointer.Null) + yield break; + + IRuntimeTypeSystem rts = this; + ushort numInstanceFields = rts.GetNumInstanceFields(typeHandle); + if (numInstanceFields == 0) + yield break; + + // Statics are intermixed with instance fields in the FieldDesc array, so + // we iterate the whole array and filter. Total = instance + static + thread-static. + ushort totalFields = (ushort)(numInstanceFields + + rts.GetNumStaticFields(typeHandle) + + rts.GetNumThreadStaticFields(typeHandle)); + + uint fieldDescSize = (uint)_target.GetTypeInfo(DataType.FieldDesc).Size!; + + ushort seenInstance = 0; + for (ushort i = 0; i < totalFields && seenInstance < numInstanceFields; i++) + { + TargetPointer fdPtr = new(firstFieldDesc.Value + i * fieldDescSize); + if (rts.IsFieldDescStatic(fdPtr)) + continue; + seenInstance++; + yield return fdPtr; + } + } + + TypeHandle IRuntimeTypeSystem.LookupApproxFieldTypeHandle(TargetPointer fieldDescPointer) + { + if (fieldDescPointer == TargetPointer.Null) + return default; + + IRuntimeTypeSystem rts = this; + uint token = rts.GetFieldDescMemberDef(fieldDescPointer); + EntityHandle entityHandle = MetadataTokens.EntityHandle((int)token); + if (entityHandle.IsNil || entityHandle.Kind != HandleKind.FieldDefinition) + return default; + + TargetPointer enclosingMT = rts.GetMTOfEnclosingClass(fieldDescPointer); + TypeHandle ctx = rts.GetTypeHandle(enclosingMT); + if (!ctx.IsMethodTable()) + return default; + + TargetPointer modulePtr = rts.GetModule(ctx); + if (modulePtr == TargetPointer.Null) + return default; + + ModuleHandle moduleHandle = _target.Contracts.Loader.GetModuleHandleFromModulePtr(modulePtr); + MetadataReader? mdReader = _target.Contracts.EcmaMetadata.GetMetadata(moduleHandle); + if (mdReader is null) + return default; + + FieldDefinition fieldDef = mdReader.GetFieldDefinition((FieldDefinitionHandle)entityHandle); + try + { + return _target.Contracts.Signature.DecodeFieldSignature(fieldDef.Signature, moduleHandle, ctx); + } + catch + { + return default; + } + } + void IRuntimeTypeSystem.GetCoreLibFieldDescAndDef(string @namespace, string typeName, string fieldName, out TargetPointer fieldDescAddr, out FieldDefinition fieldDef) { ILoader loader = _target.Contracts.Loader; @@ -2155,4 +2276,23 @@ void IRuntimeTypeSystem.GetCoreLibFieldDescAndDef(string @namespace, string type MetadataReader mdReader = _target.Contracts.EcmaMetadata.GetMetadata(moduleHandle)!; fieldDef = mdReader.GetFieldDefinition(fieldHandle); } + + public int GetNumInstanceFieldBytes(TypeHandle typeHandle) + => (int)GetBaseSize(typeHandle) - GetClassData(typeHandle).BaseSizePadding; + + private bool TryGetMetadataReader(TypeHandle typeHandle, out ModuleHandle moduleHandle, [NotNullWhen(true)] out MetadataReader? mdReader) + { + moduleHandle = default; + mdReader = null; + if (!typeHandle.IsMethodTable()) + return false; + + TargetPointer modulePtr = GetModule(typeHandle); + if (modulePtr == TargetPointer.Null) + return false; + + moduleHandle = _target.Contracts.Loader.GetModuleHandleFromModulePtr(modulePtr); + mdReader = _target.Contracts.EcmaMetadata.GetMetadata(moduleHandle); + return mdReader is not null; + } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcRefEnumeration.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcRefEnumeration.cs new file mode 100644 index 00000000000000..c101369e8b50ba --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcRefEnumeration.cs @@ -0,0 +1,92 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.DataContractReader.RuntimeTypeSystemHelpers; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; + +internal static class GcRefEnumeration +{ + public static IEnumerable EnumerateValueTypeRefs( + IRuntimeTypeSystem rts, + TypeHandle valueType, + TargetPointer baseAddress, + int pointerSize) + { + foreach ((uint seriesOffset, uint seriesSize) in rts.GetGCDescSeries(valueType, numComponents: 0)) + { + // Convert the boxed offset (relative to the MethodTable* slot) to an unboxed + // offset (relative to the field area). Equivalent to + // cur->GetSeriesOffset() - TARGET_POINTER_SIZE + // in native ReportPointersFromValueType. + ulong unboxedOffset = seriesOffset - (ulong)pointerSize; + ulong refCount = seriesSize / (ulong)pointerSize; + for (ulong i = 0; i < refCount; i++) + { + yield return new TargetPointer(baseAddress.Value + unboxedOffset + i * (ulong)pointerSize); + } + } + } + + public static IEnumerable<(TargetPointer Address, GcScanFlags Flags)> EnumerateByRefLikeRoots( + IRuntimeTypeSystem rts, + TypeHandle byRefLikeType, + TargetPointer baseAddress, + int pointerSize) + { + foreach (TargetPointer fd in rts.EnumerateInstanceFieldDescs(byRefLikeType)) + { + // For instance fields the BigRVA sentinel path is never taken, so passing + // a default FieldDefinition is safe. + uint offset = rts.GetFieldDescOffset(fd, default); + TargetPointer fieldAddr = new(baseAddress.Value + offset); + CorElementType et = rts.GetFieldDescType(fd); + + switch (et) + { + case CorElementType.Class: + case CorElementType.String: + case CorElementType.Object: + case CorElementType.SzArray: + case CorElementType.Array: + yield return (fieldAddr, GcScanFlags.None); + break; + + case CorElementType.Byref: + yield return (fieldAddr, GcScanFlags.GC_CALL_INTERIOR); + break; + + case CorElementType.ValueType: + { + TypeHandle inner = rts.LookupApproxFieldTypeHandle(fd); + if (!inner.IsMethodTable()) + break; + + if (rts.IsByRefLike(inner)) + { + foreach ((TargetPointer addr, GcScanFlags flags) in + EnumerateByRefLikeRoots(rts, inner, fieldAddr, pointerSize)) + { + yield return (addr, flags); + } + } + else + { + foreach (TargetPointer refAddr in + EnumerateValueTypeRefs(rts, inner, fieldAddr, pointerSize)) + { + yield return (refAddr, GcScanFlags.None); + } + } + break; + } + + // Primitives, raw pointers, function pointers, etc. carry no GC refs. + default: + break; + } + } + } +} 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 71e38bd782faad..c3ebff7dfe4f82 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 @@ -3,9 +3,6 @@ using System; using System.Collections.Generic; -using System.Reflection.Metadata; -using System.Reflection.Metadata.Ecma335; -using Microsoft.Diagnostics.DataContractReader.SignatureHelpers; namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; @@ -19,6 +16,7 @@ internal class GcScanner private readonly Target _target; private readonly IExecutionManager _eman; private readonly IGCInfo _gcInfo; + private ICallingConvention? _callingConvention; private readonly FrameHelpers _frameHelpers; internal GcScanner(Target target) @@ -29,6 +27,8 @@ internal GcScanner(Target target) _frameHelpers = new FrameHelpers(target); } + private ICallingConvention CallingConvention => _callingConvention ??= _target.Contracts.CallingConvention; + /// /// Enumerates live GC slots for a managed (frameless) code frame. /// Port of native EECodeManager::EnumGcRefs (eetwain.cpp). @@ -329,120 +329,67 @@ private void PromoteCallerStack( IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem; MethodDescHandle mdh = rts.GetMethodDescHandle(methodDescPtr); - MethodSignature methodSig; - try - { - TargetPointer methodTablePtr = rts.GetMethodTable(mdh); - TypeHandle typeHandle = rts.GetTypeHandle(methodTablePtr); - TargetPointer modulePtr = rts.GetModule(typeHandle); - - ModuleHandle moduleHandle = _target.Contracts.Loader.GetModuleHandleFromModulePtr(modulePtr); - MetadataReader? mdReader = _target.Contracts.EcmaMetadata.GetMetadata(moduleHandle); - if (mdReader is null) - return; - - GcSignatureTypeProvider provider = new(_target, moduleHandle); - GcSignatureContext genericContext = new(typeHandle, mdh); - RuntimeSignatureDecoder decoder = new( - provider, _target, mdReader, genericContext); - - // Match native MethodDesc::GetSig: prefer stored signature (dynamic, EEImpl, - // and array method descs) before falling back to a metadata token lookup. - if (rts.IsStoredSigMethodDesc(mdh, out ReadOnlySpan storedSig)) - { - unsafe - { - fixed (byte* pStoredSig = storedSig) - { - BlobReader blobReader = new BlobReader(pStoredSig, storedSig.Length); - methodSig = decoder.DecodeMethodSignature(ref blobReader); - } - } - } - else - { - uint methodToken = rts.GetMethodToken(mdh); - if (methodToken == (uint)EcmaMetadataUtils.TokenType.mdtMethodDef) - return; - - MethodDefinitionHandle methodDefHandle = MetadataTokens.MethodDefinitionHandle((int)EcmaMetadataUtils.GetRowId(methodToken)); - MethodDefinition methodDef = mdReader.GetMethodDefinition(methodDefHandle); + CallSiteLayout layout = CallingConvention.ComputeCallSiteLayout(mdh); - BlobReader blobReader = mdReader.GetBlobReader(methodDef.Signature); - methodSig = decoder.DecodeMethodSignature(ref blobReader); - } - } - catch (System.Exception) + if (layout.ThisOffset is int thisOff) { - return; + TargetPointer thisAddr = new(transitionBlock.Value + (ulong)thisOff); + GcScanFlags thisFlags = layout.IsValueTypeThis ? GcScanFlags.GC_CALL_INTERIOR : GcScanFlags.None; + scanContext.GCReportCallback(thisAddr, thisFlags); } - if (methodSig.Header.CallingConvention is SignatureCallingConvention.VarArgs) - return; - - bool hasThis = methodSig.Header.IsInstance; - bool hasRetBuf = methodSig.ReturnType is GcTypeKind.Other; - bool requiresInstArg = false; - bool isAsync = false; - bool isValueTypeThis = false; - - try + if (layout.AsyncContinuationOffset is int asyncOff) { - requiresInstArg = rts.GetGenericContextLoc(mdh) == GenericContextLoc.InstArg; - isAsync = rts.IsAsyncMethod(mdh); + TargetPointer asyncAddr = new(transitionBlock.Value + (ulong)asyncOff); + scanContext.GCReportCallback(asyncAddr, GcScanFlags.None); } - catch + + foreach (ArgLayout arg in layout.Arguments) { + ReportArgument(arg, transitionBlock, rts, _target.PointerSize, scanContext); } - - PromoteCallerStackHelper(transitionBlock, methodSig, hasThis, hasRetBuf, - requiresInstArg, isAsync, isValueTypeThis, scanContext); } - /// - /// Core logic for promoting caller stack GC references. - /// Matches native TransitionFrame::PromoteCallerStackHelper (frames.cpp:1560). - /// - private void PromoteCallerStackHelper( + internal static void ReportArgument( + ArgLayout arg, TargetPointer transitionBlock, - MethodSignature methodSig, - bool hasThis, - bool hasRetBuf, - bool requiresInstArg, - bool isAsync, - bool isValueTypeThis, + IRuntimeTypeSystem rts, + int pointerSize, GcScanContext scanContext) { - Data.TransitionBlock tb = _target.ProcessedData.GetOrAdd(transitionBlock); - - int numRegistersUsed = 0; - if (hasThis) - numRegistersUsed++; - if (hasRetBuf) - numRegistersUsed++; - if (requiresInstArg) - numRegistersUsed++; - if (isAsync) - numRegistersUsed++; - - bool isArm64 = IsTargetArm64(); - if (isArm64) - numRegistersUsed++; - - if (hasThis) + // By-value, undecomposed value type with a resolved MethodTable: enumerate + // embedded GC refs. The Phase 1 producer (CallingConvention_1.ComputeValueTypeHandle) + // guarantees the storage is contiguous starting at arg.Slots[0].Offset. The walk + // strategy depends on the type: + // - Ordinary value type: CGCDesc series walk (covers object refs only). + // - ByRefLike (Span, ref structs): field-by-field walk that also reports + // managed byref fields as GC_CALL_INTERIOR (the CGCDesc series omits byrefs). + if (arg.ValueTypeHandle is { } valueTypeHandle && arg.Slots.Count > 0) { - int thisPos = isArm64 ? 1 : 0; - TargetPointer thisAddr = AddressFromGCRefMapPos(tb, thisPos); - GcScanFlags thisFlags = isValueTypeThis ? GcScanFlags.GC_CALL_INTERIOR : GcScanFlags.None; - scanContext.GCReportCallback(thisAddr, thisFlags); + TargetPointer baseAddress = new(transitionBlock.Value + (ulong)arg.Slots[0].Offset); + if (rts.IsByRefLike(valueTypeHandle)) + { + foreach ((TargetPointer addr, GcScanFlags flags) in + GcRefEnumeration.EnumerateByRefLikeRoots(rts, valueTypeHandle, baseAddress, pointerSize)) + { + scanContext.GCReportCallback(addr, flags); + } + } + else + { + foreach (TargetPointer refAddr in GcRefEnumeration.EnumerateValueTypeRefs( + rts, valueTypeHandle, baseAddress, pointerSize)) + { + scanContext.GCReportCallback(refAddr, GcScanFlags.None); + } + } + return; } - int pos = numRegistersUsed; - foreach (GcTypeKind kind in methodSig.ParameterTypes) + foreach (ArgSlot slot in arg.Slots) { - TargetPointer slotAddress = AddressFromGCRefMapPos(tb, pos); - - switch (kind) + TargetPointer slotAddress = new(transitionBlock.Value + (ulong)slot.Offset); + switch (GcTypeKindClassifier.GetGcKind(slot.ElementType)) { case GcTypeKind.Ref: scanContext.GCReportCallback(slotAddress, GcScanFlags.None); @@ -451,11 +398,18 @@ private void PromoteCallerStackHelper( scanContext.GCReportCallback(slotAddress, GcScanFlags.GC_CALL_INTERIOR); break; case GcTypeKind.Other: + if (arg.IsPassedByRef) + { + scanContext.GCReportCallback(slotAddress, GcScanFlags.GC_CALL_INTERIOR); + } + // else: the per-arch iterator GC-decomposed the storage so each + // ref-bearing slot already carries Class/Byref ElementType handled + // above, OR ValueTypeHandle would have been populated and handled + // by the CGCDesc-walk branch at the top of this method. break; case GcTypeKind.None: break; } - pos++; } } @@ -464,11 +418,6 @@ private TargetPointer AddressFromGCRefMapPos(Data.TransitionBlock tb, int pos) return new TargetPointer(tb.FirstGCRefMapSlot.Value + (ulong)(pos * _target.PointerSize)); } - private bool IsTargetArm64() - { - return _target.Contracts.RuntimeInfo.GetTargetArchitecture() is RuntimeInfoArchitecture.Arm64; - } - private TargetPointer GetCallerSP(IPlatformAgnosticContext context, ref TargetPointer? cached) { if (cached is null) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcSignatureTypeProvider.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcSignatureTypeProvider.cs deleted file mode 100644 index 8852f733df6a97..00000000000000 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcSignatureTypeProvider.cs +++ /dev/null @@ -1,219 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Immutable; -using System.Diagnostics; -using System.Reflection.Metadata; -using System.Reflection.Metadata.Ecma335; -using Microsoft.Diagnostics.DataContractReader.SignatureHelpers; - -namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; - -/// -/// Classification of a signature type for GC scanning purposes. -/// -internal enum GcTypeKind -{ - /// Not a GC reference (primitives, pointers). - None, - /// Object reference (class, string, array). - Ref, - /// Interior pointer (byref). - Interior, - /// Value type that may contain embedded GC references. - Other, -} - -/// -/// Generic context used to resolve ELEMENT_TYPE_VAR and ELEMENT_TYPE_MVAR -/// while decoding a method signature for GC scanning. is the -/// owning type's (used for VAR), and -/// is the owning method's (used for MVAR). -/// -internal readonly record struct GcSignatureContext(TypeHandle ClassContext, MethodDescHandle MethodContext); - -/// -/// Classifies signature types for GC scanning purposes. -/// Implements which -/// is a superset of SRM's , -/// adding support for ELEMENT_TYPE_INTERNAL. -/// -/// -/// The provider is scoped to a single module: GetTypeFromDefinition and -/// GetTypeFromReference resolve TypeDef/TypeRef tokens via the module's -/// lookup tables so enums (and other runtime-normalized value types) are classified -/// using the actual , matching native -/// SigPointer::PeekElemTypeNormalized. -/// -internal sealed class GcSignatureTypeProvider - : IRuntimeSignatureTypeProvider -{ - private readonly Target _target; - private readonly ModuleHandle _moduleHandle; - - public GcSignatureTypeProvider(Target target, ModuleHandle moduleHandle) - { - _target = target; - _moduleHandle = moduleHandle; - } - - public GcTypeKind GetPrimitiveType(PrimitiveTypeCode typeCode) - => typeCode switch - { - PrimitiveTypeCode.String or PrimitiveTypeCode.Object => GcTypeKind.Ref, - PrimitiveTypeCode.TypedReference => GcTypeKind.Other, - _ => GcTypeKind.None, - }; - - public GcTypeKind GetTypeFromDefinition(MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) - => ClassifyTokenLookup(_target.Contracts.Loader.GetLookupTables(_moduleHandle).TypeDefToMethodTable, MetadataTokens.GetToken(handle), rawTypeKind); - - public GcTypeKind GetTypeFromReference(MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) - => ClassifyTokenLookup(_target.Contracts.Loader.GetLookupTables(_moduleHandle).TypeRefToMethodTable, MetadataTokens.GetToken(handle), rawTypeKind); - - public GcTypeKind GetTypeFromSpecification(MetadataReader reader, GcSignatureContext genericContext, TypeSpecificationHandle handle, byte rawTypeKind) - => rawTypeKind == (byte)SignatureTypeKind.ValueType ? GcTypeKind.Other : GcTypeKind.Ref; - - public GcTypeKind GetSZArrayType(GcTypeKind elementType) => GcTypeKind.Ref; - public GcTypeKind GetArrayType(GcTypeKind elementType, ArrayShape shape) => GcTypeKind.Ref; - public GcTypeKind GetByReferenceType(GcTypeKind elementType) => GcTypeKind.Interior; - public GcTypeKind GetPointerType(GcTypeKind elementType) => GcTypeKind.None; - - public GcTypeKind GetGenericInstantiation(GcTypeKind genericType, ImmutableArray typeArguments) - => genericType; - - public GcTypeKind GetGenericMethodParameter(GcSignatureContext genericContext, int index) - { - try - { - ReadOnlySpan instantiation = _target.Contracts.RuntimeTypeSystem.GetGenericMethodInstantiation(genericContext.MethodContext); - if ((uint)index >= (uint)instantiation.Length) - return GcTypeKind.Ref; - return ClassifyTypeHandle(instantiation[index]); - } - catch - { - return GcTypeKind.Ref; - } - } - - public GcTypeKind GetGenericTypeParameter(GcSignatureContext genericContext, int index) - { - try - { - IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem; - TypeHandle classCtx = genericContext.ClassContext; - - if (rts.IsArray(classCtx, out _)) - { - // Match native SigTypeContext::InitTypeContext (typectxt.cpp): arrays use - // the element type as their class instantiation. RuntimeTypeSystem.GetInstantiation - // returns an empty span for arrays, so consult GetTypeParam directly (the - // managed equivalent of MethodTable::GetArrayInstantiation). - Debug.Assert(index == 0, "Array class context has a 1-element instantiation; index > 0 indicates a malformed signature."); - if (index != 0) - return GcTypeKind.Ref; - return ClassifyTypeHandle(rts.GetTypeParam(classCtx)); - } - - ReadOnlySpan instantiation = rts.GetInstantiation(classCtx); - if ((uint)index >= (uint)instantiation.Length) - return GcTypeKind.Ref; - return ClassifyTypeHandle(instantiation[index]); - } - catch - { - return GcTypeKind.Ref; - } - } - - public GcTypeKind GetFunctionPointerType(MethodSignature signature) => GcTypeKind.None; - public GcTypeKind GetModifiedType(GcTypeKind modifier, GcTypeKind unmodifiedType, bool isRequired) => unmodifiedType; - public GcTypeKind GetInternalModifiedType(TargetPointer typeHandlePointer, GcTypeKind unmodifiedType, bool isRequired) => unmodifiedType; - public GcTypeKind GetPinnedType(GcTypeKind elementType) => elementType; - - public GcTypeKind GetInternalType(TargetPointer typeHandlePointer) - { - if (typeHandlePointer == TargetPointer.Null) - return GcTypeKind.None; - - try - { - return ClassifyTypeHandle(_target.Contracts.RuntimeTypeSystem.GetTypeHandle(typeHandlePointer)); - } - catch - { - return GcTypeKind.Ref; - } - } - - /// - /// Resolve a TypeDef/TypeRef token via the module's lookup tables and classify the - /// resulting . Falls back to a -based - /// classification when the type has not been loaded. - /// - private GcTypeKind ClassifyTokenLookup(TargetPointer lookupTable, int token, byte rawTypeKind) - { - try - { - TargetPointer typeHandlePtr = _target.Contracts.Loader.GetModuleLookupMapElement(lookupTable, (uint)token, out _); - if (typeHandlePtr == TargetPointer.Null) - return rawTypeKind == (byte)SignatureTypeKind.ValueType ? GcTypeKind.Other : GcTypeKind.Ref; - - return ClassifyTypeHandle(_target.Contracts.RuntimeTypeSystem.GetTypeHandle(typeHandlePtr)); - } - catch - { - return rawTypeKind == (byte)SignatureTypeKind.ValueType ? GcTypeKind.Other : GcTypeKind.Ref; - } - } - - /// - /// Classify a resolved . Mirrors native - /// SigPointer::PeekElemTypeNormalized + gElementTypeInfo[etype].m_gc: - /// enums collapse to their underlying primitive () so - /// they are skipped during stack scanning, matching native behavior. - /// - private GcTypeKind ClassifyTypeHandle(TypeHandle typeHandle) - { - if (typeHandle.Address == TargetPointer.Null) - return GcTypeKind.Ref; - - IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem; - CorElementType corType = rts.GetSignatureCorElementType(typeHandle); - - switch (corType) - { - case CorElementType.Void: - case CorElementType.Boolean: - case CorElementType.Char: - case CorElementType.I1: - case CorElementType.U1: - case CorElementType.I2: - case CorElementType.U2: - case CorElementType.I4: - case CorElementType.U4: - case CorElementType.I8: - case CorElementType.U8: - case CorElementType.R4: - case CorElementType.R8: - case CorElementType.I: - case CorElementType.U: - case CorElementType.FnPtr: - case CorElementType.Ptr: - return GcTypeKind.None; - - case CorElementType.Byref: - return GcTypeKind.Interior; - - case CorElementType.ValueType: - // Native PeekElemTypeNormalized resolves enums to their underlying primitive - // CorElementType, which classifies as TYPE_GC_NONE in gElementTypeInfo. - return rts.IsEnum(typeHandle) ? GcTypeKind.None : GcTypeKind.Other; - - default: - return GcTypeKind.Ref; - } - } -} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcTypeKind.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcTypeKind.cs new file mode 100644 index 00000000000000..7e1c9c3f0a039a --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcTypeKind.cs @@ -0,0 +1,26 @@ +// 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.Contracts.StackWalkHelpers; + +internal enum GcTypeKind +{ + None, + Ref, + Interior, + Other, +} + +internal static class GcTypeKindClassifier +{ + public static GcTypeKind GetGcKind(CorElementType etype) => etype switch + { + CorElementType.Class or CorElementType.Object or CorElementType.String + or CorElementType.Array or CorElementType.SzArray + or CorElementType.Var or CorElementType.MVar + or CorElementType.GenericInst => GcTypeKind.Ref, + CorElementType.Byref => GcTypeKind.Interior, + CorElementType.ValueType or CorElementType.TypedByRef => GcTypeKind.Other, + _ => GcTypeKind.None, + }; +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs index 83996e38ef1d42..f367d7eb69bbda 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs @@ -71,5 +71,7 @@ public static void Register(ContractRegistry registry) registry.Register("c1", static t => new ExecutionManager_1(t)); registry.Register("c2", static t => new ExecutionManager_2(t)); + + registry.Register("c1", static t => new CallingConvention_1(t)); } } 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 0d0393f6e4e0df..4c4c8d4d689b74 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 @@ -20,6 +20,7 @@ public EEClass(Target target, TargetPointer address) NumThreadStaticFields = target.ReadField(address, type, nameof(NumThreadStaticFields)); FieldDescList = target.ReadPointerField(address, type, nameof(FieldDescList)); NumNonVirtualSlots = target.ReadField(address, type, nameof(NumNonVirtualSlots)); + BaseSizePadding = target.ReadField(address, type, nameof(BaseSizePadding)); } public TargetPointer MethodTable { get; init; } @@ -40,4 +41,5 @@ public EEClass(Target target, TargetPointer address) public ushort NumThreadStaticFields { get; init; } public TargetPointer FieldDescList { get; init; } public ushort NumNonVirtualSlots { get; init; } + public byte BaseSizePadding { get; init; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/MethodTableFlags_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/MethodTableFlags_1.cs index e82522b79c672e..8b1bc378f01194 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/MethodTableFlags_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/MethodTableFlags_1.cs @@ -24,6 +24,8 @@ internal enum WFLAGS_LOW : uint GenericsMask = 0x00000030, GenericsMask_NonGeneric = 0x00000000, // no instantiation GenericsMask_TypicalInstantiation = 0x00000030, // the type instantiated at its formal parameters, e.g. List + IsHFA = 0x00000800, + IsByRefLike = 0x00001000, StringArrayValues = GenericsMask_NonGeneric | @@ -59,6 +61,7 @@ internal enum WFLAGS_HIGH : uint internal enum WFLAGS2_ENUM : uint { DynamicStatics = 0x0002, + IsIntrinsicType = 0x0020, } public uint MTFlags { get; init; } @@ -104,9 +107,12 @@ private bool TestFlagWithMask(WFLAGS2_ENUM mask, WFLAGS2_ENUM flag) public bool HasInstantiation => !TestFlagWithMask(WFLAGS_LOW.GenericsMask, WFLAGS_LOW.GenericsMask_NonGeneric); public bool ContainsGCPointers => GetFlag(WFLAGS_HIGH.ContainsGCPointers) != 0; public bool RequiresAlign8 => GetFlag(WFLAGS_HIGH.RequiresAlign8) != 0; + public bool IsHFA => TestFlagWithMask(WFLAGS_LOW.IsHFA, WFLAGS_LOW.IsHFA); + public bool IsByRefLike => TestFlagWithMask(WFLAGS_LOW.IsByRefLike, WFLAGS_LOW.IsByRefLike); public bool IsCollectible => GetFlag(WFLAGS_HIGH.Collectible) != 0; public bool IsTrackedReferenceWithFinalizer => GetFlag(WFLAGS_HIGH.IsTrackedReferenceWithFinalizer) != 0; public bool IsDynamicStatics => GetFlag(WFLAGS2_ENUM.DynamicStatics) != 0; + public bool IsIntrinsicType => GetFlag(WFLAGS2_ENUM.IsIntrinsicType) != 0; public bool IsGenericTypeDefinition => TestFlagWithMask(WFLAGS_LOW.GenericsMask, WFLAGS_LOW.GenericsMask_TypicalInstantiation); public bool ContainsGenericVariables => GetFlag(WFLAGS_HIGH.ContainsGenericVariables) != 0; diff --git a/src/native/managed/cdac/tests/CallingConvention/AMD64UnixCallingConventionTests.cs b/src/native/managed/cdac/tests/CallingConvention/AMD64UnixCallingConventionTests.cs new file mode 100644 index 00000000000000..6610da162b228f --- /dev/null +++ b/src/native/managed/cdac/tests/CallingConvention/AMD64UnixCallingConventionTests.cs @@ -0,0 +1,838 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.Tests; + +public class AMD64UnixCallingConventionTests +{ + private static readonly Lazy s_syntheticVectorMetadata = new(SyntheticVectorMetadata.Create); + + private static CallConvTestCase Case => CallConvCases.AMD64Unix; + + private static int OffsetOfNthGPReg(int n) => Case.ArgumentRegistersOffset + n * Case.PointerSize; + private static int OffsetOfNthFPReg(int n) => Case.OffsetOfFloatArgumentRegisters!.Value + n * Case.FloatRegisterSize; + private static int OffsetOfNthStackSlot(int n) => Case.OffsetOfArgs + n * Case.PointerSize; + + [Fact] + public void SixInts_FillGPRegs() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => + { + sig.Return(CorElementType.Void); + for (int i = 0; i < Case.NumArgumentRegisters; i++) sig.Param(CorElementType.I8); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(Case.NumArgumentRegisters, layout.Arguments.Count); + for (int i = 0; i < Case.NumArgumentRegisters; i++) + { + int expected = Case.ArgumentRegistersOffset + i * Case.PointerSize; + Assert.Equal(expected, layout.Arguments[i].Slots[0].Offset); + } + } + + [Fact] + public void SeventhInt_GoesToStack() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => + { + sig.Return(CorElementType.Void); + for (int i = 0; i < Case.NumArgumentRegisters + 1; i++) sig.Param(CorElementType.I8); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(Case.NumArgumentRegisters + 1, layout.Arguments.Count); + Assert.Equal(Case.OffsetOfArgs, layout.Arguments[6].Slots[0].Offset); + } + + [Fact] + public void FourDoubles_FillFPRegs() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => + { + sig.Return(CorElementType.Void); + for (int i = 0; i < 4; i++) sig.Param(CorElementType.R8); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(4, layout.Arguments.Count); + for (int i = 0; i < 4; i++) + { + int expected = (Case.OffsetOfFloatArgumentRegisters ?? 0) + i * Case.FloatRegisterSize; + Assert.Equal(expected, layout.Arguments[i].Slots[0].Offset); + } + } + + [Fact] + public void MixedIntDouble_UseSeparateBanks() + { + // SysV bank-independent allocation: int goes to GP, double to FP regardless of position. + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => sig + .Return(CorElementType.Void) + .Param(CorElementType.I8) + .Param(CorElementType.R8) + .Param(CorElementType.I8) + .Param(CorElementType.R8)); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(4, layout.Arguments.Count); + Assert.Equal(Case.ArgumentRegistersOffset, layout.Arguments[0].Slots[0].Offset); + Assert.Equal((Case.OffsetOfFloatArgumentRegisters ?? 0), layout.Arguments[1].Slots[0].Offset); + Assert.Equal(Case.ArgumentRegistersOffset + Case.PointerSize, layout.Arguments[2].Slots[0].Offset); + Assert.Equal((Case.OffsetOfFloatArgumentRegisters ?? 0) + Case.FloatRegisterSize, layout.Arguments[3].Slots[0].Offset); + } + + // ----- SysV struct classification ----- + + [Fact] + public void Struct_TwoInts_PassedInOneGPReg() + { + // { int x; int y; } => 8 bytes => 1 eightbyte (Integer) => 1 GP reg. + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "TwoInts", structSize: 8, + fields: [new(0, CorElementType.I4), new(4, CorElementType.I4)]); + sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.False(arg.IsPassedByRef); + Assert.Single(arg.Slots); + Assert.Equal(CorElementType.I8, arg.Slots[0].ElementType); + Assert.Equal(Case.ArgumentRegistersOffset, arg.Slots[0].Offset); + } + + [Fact] + public void Struct_IntDouble_SplitAcrossGPAndFP() + { + // { int x; double d; } => 2 eightbytes (Integer, SSE) => 1 GP + 1 FP reg. + // This is the key SysV "split" case GcScanner needs to see per-register. + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "IntDouble", structSize: 16, + fields: [new(0, CorElementType.I4), new(8, CorElementType.R8)]); + sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.False(arg.IsPassedByRef); + Assert.Equal(2, arg.Slots.Count); + Assert.Equal(CorElementType.I8, arg.Slots[0].ElementType); + Assert.Equal(Case.ArgumentRegistersOffset, arg.Slots[0].Offset); + Assert.Equal(CorElementType.R8, arg.Slots[1].ElementType); + Assert.Equal((Case.OffsetOfFloatArgumentRegisters ?? 0), arg.Slots[1].Offset); + } + + [Fact] + public void Struct_TwoDoubles_PassedInTwoFPRegs() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "TwoDoubles", structSize: 16, + fields: [new(0, CorElementType.R8), new(8, CorElementType.R8)]); + sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.Equal(2, arg.Slots.Count); + Assert.Equal(CorElementType.R8, arg.Slots[0].ElementType); + Assert.Equal((Case.OffsetOfFloatArgumentRegisters ?? 0), arg.Slots[0].Offset); + Assert.Equal(CorElementType.R8, arg.Slots[1].ElementType); + Assert.Equal((Case.OffsetOfFloatArgumentRegisters ?? 0) + Case.FloatRegisterSize, arg.Slots[1].Offset); + } + + [Fact] + public void Struct_TwoFloats_PackInOneFPReg() + { + // { float a; float b; } => 8 bytes => 1 eightbyte (SSE) => 1 FP reg. + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "TwoFloats", structSize: 8, + fields: [new(0, CorElementType.R4), new(4, CorElementType.R4)]); + sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + Assert.Single(layout.Arguments[0].Slots); + Assert.Equal(CorElementType.R8, layout.Arguments[0].Slots[0].ElementType); + Assert.Equal((Case.OffsetOfFloatArgumentRegisters ?? 0), layout.Arguments[0].Slots[0].Offset); + } + + [Fact] + public void Struct_ObjectAndDouble_GCRefSplitWithFP() + { + // { object o; double d; } => 2 eightbytes (IntegerReference, SSE). + // The GP slot is reported as a managed reference (CorElementType.Class). + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "ObjectAndDouble", structSize: 16, + fields: [new(0, CorElementType.Object), new(8, CorElementType.R8)]); + sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.Equal(2, arg.Slots.Count); + Assert.Equal(CorElementType.Class, arg.Slots[0].ElementType); + Assert.Equal(Case.ArgumentRegistersOffset, arg.Slots[0].Offset); + Assert.Equal(CorElementType.R8, arg.Slots[1].ElementType); + Assert.Equal((Case.OffsetOfFloatArgumentRegisters ?? 0), arg.Slots[1].Offset); + } + + [Fact] + public void Struct_LargerThan16Bytes_StackByValue_NotByRef() + { + // { long a; long b; long c; } => 24 bytes > 16 => stack by value + // (NOT implicit by-ref like Windows x64). + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "ThreeLongs", structSize: 24, + fields: + [ + new(0, CorElementType.I8), + new(8, CorElementType.I8), + new(16, CorElementType.I8), + ]); + sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.False(arg.IsPassedByRef); // SysV passes large structs BY VALUE on stack. + Assert.Single(arg.Slots); + Assert.Equal(Case.OffsetOfArgs, arg.Slots[0].Offset); + } + + [Fact] + public void TypedReference_PassedInTwoGPRegs() + { + // System.TypedReference is { ref byte _value; IntPtr _type; } -> 16 bytes + // classified as [IntegerByRef, Integer] -> RDI + RSI on SysV AMD64. + // The signature uses ELEMENT_TYPE_TYPEDBYREF (0x16); the ArgIterator + // substitutes the well-known g_TypedReferenceMT MethodTable so the + // SysV struct classifier walks its layout normally. + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable typedRefMT = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "System.TypedReference", structSize: 16, + fields: [new(0, CorElementType.Byref), new(8, CorElementType.I)]); + rts.SetTypedReferenceMethodTable(typedRefMT.Address); + sig.Return(CorElementType.Void).Param(CorElementType.TypedByRef); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.False(arg.IsPassedByRef); + Assert.Equal(2, arg.Slots.Count); + Assert.Equal(Case.ArgumentRegistersOffset, arg.Slots[0].Offset); + Assert.Equal(Case.ArgumentRegistersOffset + Case.PointerSize, arg.Slots[1].Offset); + } + + [Fact] + public void TypedReference_GlobalNotSet_FallsBackToStack() + { + // When g_TypedReferenceMT isn't populated (older runtime / missing global), + // the signature provider falls back to a conservative pointer-sized + // value-type placeholder. SysV AMD64 still passes it on the stack rather + // than crashing on resolution. + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => sig.Return(CorElementType.Void).Param(CorElementType.TypedByRef)); + + // No SetTypedReferenceMethodTable call - global stays null. + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + // Should not throw; layout is best-effort. + } + + // ---- Section 1: Style-aligned coverage (mirrors AMD64-Windows tests) ---- + + // Integer-valued args consume the 6 GP arg-regs in order, then spill to + // stack slots that advance by one pointer-sized slot per overflow arg. + [Theory] + [InlineData(CorElementType.I1, 6)] + [InlineData(CorElementType.U1, 6)] + [InlineData(CorElementType.I2, 6)] + [InlineData(CorElementType.U2, 6)] + [InlineData(CorElementType.I4, 6)] + [InlineData(CorElementType.U4, 6)] + [InlineData(CorElementType.I8, 6)] + [InlineData(CorElementType.U8, 6)] + [InlineData(CorElementType.I4, 7)] + [InlineData(CorElementType.I8, 8)] + public void IntArgs_FillSixGPRegsAndSpillToStack(CorElementType elementType, int count) + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => + { + sig.Return(CorElementType.Void); + for (int i = 0; i < count; i++) sig.Param(elementType); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(count, layout.Arguments.Count); + + for (int i = 0; i < count; i++) + { + int expectedOffset = i < Case.NumArgumentRegisters + ? OffsetOfNthGPReg(i) + : OffsetOfNthStackSlot(i - Case.NumArgumentRegisters); + + Assert.Equal(expectedOffset, layout.Arguments[i].Slots[0].Offset); + } + } + + [Fact] + public void InstanceMethod_ThisOffsetIsFirstGPReg() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: true, + sig => sig.Return(CorElementType.Void)); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(OffsetOfNthGPReg(0), layout.ThisOffset); + } + + // Float args fill the 8 FP arg-regs (independent of the GP bank). + // Counts 1..8 exercise pure FP-bank allocation; this verifies the happy path. + [Theory] + [InlineData(CorElementType.R4, 1)] + [InlineData(CorElementType.R8, 1)] + [InlineData(CorElementType.R4, 4)] + [InlineData(CorElementType.R8, 4)] + [InlineData(CorElementType.R4, 8)] + [InlineData(CorElementType.R8, 8)] + public void FloatArgs_FillEightFPRegs(CorElementType elementType, int count) + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => + { + sig.Return(CorElementType.Void); + for (int i = 0; i < count; i++) sig.Param(elementType); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(count, layout.Arguments.Count); + + for (int i = 0; i < count; i++) + { + Assert.Equal(elementType, layout.Arguments[i].Slots[0].ElementType); + Assert.Equal(OffsetOfNthFPReg(i), layout.Arguments[i].Slots[0].Offset); + } + } + + // A lone float chooses the FP bank based on its FP-bank ordinal; surrounding + // ints continue to map to GP registers by their GP-bank ordinal. + // Verifies the SysV "independent banks" placement rule. + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + [InlineData(5)] + public void OneFloatAmongInts_LandsInFirstFPReg(int floatPosition) + { + const int totalArgs = 6; + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => + { + sig.Return(CorElementType.Void); + for (int i = 0; i < totalArgs; i++) + sig.Param(i == floatPosition ? CorElementType.R8 : CorElementType.I4); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(totalArgs, layout.Arguments.Count); + + int idxGen = 0; + int idxFP = 0; + for (int i = 0; i < totalArgs; i++) + { + if (i == floatPosition) + { + Assert.Equal(CorElementType.R8, layout.Arguments[i].Slots[0].ElementType); + Assert.Equal(OffsetOfNthFPReg(idxFP), layout.Arguments[i].Slots[0].Offset); + idxFP++; + } + else + { + Assert.Equal(CorElementType.I4, layout.Arguments[i].Slots[0].ElementType); + Assert.Equal(OffsetOfNthGPReg(idxGen), layout.Arguments[i].Slots[0].Offset); + idxGen++; + } + } + } + + [Fact] + public void ManyIntArgs_StackOffsetsProgress() + { + const int totalArgs = 10; + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => + { + sig.Return(CorElementType.Void); + for (int i = 0; i < totalArgs; i++) sig.Param(CorElementType.I8); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(totalArgs, layout.Arguments.Count); + + for (int i = 0; i < totalArgs; i++) + { + int expectedOffset = i < Case.NumArgumentRegisters + ? OffsetOfNthGPReg(i) + : OffsetOfNthStackSlot(i - Case.NumArgumentRegisters); + + Assert.Equal(expectedOffset, layout.Arguments[i].Slots[0].Offset); + } + } + + // ---- Section 2: Return-value classification baselines ---- + + // Baseline: 8-byte struct return classifies as 1 Integer eightbyte + // (reg-passable), so no ret buf is needed and the first user arg stays + // in the 1st GP reg. + [Fact] + public void Return_EightByteStruct_NoRetBuf_FirstArgStaysInFirstGPReg() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "EightBytes", structSize: 8, fields: [new(0, CorElementType.I8)]); + sig.ReturnValueType(new TargetPointer(mt.Address)).Param(CorElementType.I4); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + Assert.Equal(OffsetOfNthGPReg(0), layout.Arguments[0].Slots[0].Offset); + } + + // Baseline: 16-byte struct return classifies as 2 Integer eightbytes + // (reg-passable in RAX:RDX), so no ret buf. + [Fact] + public void Return_SixteenByteStruct_NoRetBuf_FirstArgStaysInFirstGPReg() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "SixteenBytes", structSize: 16, + fields: [new(0, CorElementType.I8), new(8, CorElementType.I8)]); + sig.ReturnValueType(new TargetPointer(mt.Address)).Param(CorElementType.I4); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + Assert.Equal(OffsetOfNthGPReg(0), layout.Arguments[0].Slots[0].Offset); + } + + // Baseline: >16-byte struct return cannot fit in two regs, so a hidden ret + // buf takes the 1st GP slot and the first user arg shifts to the 2nd GP reg. + [Fact] + public void Return_LargeStruct_HasRetBuf_FirstArgShiftsToSecondGPReg() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "TwentyFourBytes", structSize: 24, + fields: + [ + new(0, CorElementType.I8), + new(8, CorElementType.I8), + new(16, CorElementType.I8), + ]); + sig.ReturnValueType(new TargetPointer(mt.Address)).Param(CorElementType.I4); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + Assert.Equal(OffsetOfNthGPReg(1), layout.Arguments[0].Slots[0].Offset); + } + +#pragma warning disable xUnit1004 // Test methods should not be skipped — these track audit gaps. + + // ---- Section 3: Open audit gaps ---- + + // Audit gap (return classification): on AMD64-Unix, a 12-byte struct return + // {int, int, int} classifies as 2 Integer eightbytes -> reg-passable in + // RAX:RDX, so NO ret buf is needed. cDAC currently uses an X64-wide + // power-of-2 heuristic that wrongly forces a ret buf for non-power-of-2 + // sizes, shifting the first user arg from the 1st GP reg to the 2nd. + [Fact] + public void Return_TwelveByteStruct_NoRetBuf_FirstArgStaysInFirstGPReg() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "TwelveBytes", structSize: 12, + fields: + [ + new(0, CorElementType.I4), + new(4, CorElementType.I4), + new(8, CorElementType.I4), + ]); + sig.ReturnValueType(new TargetPointer(mt.Address)).Param(CorElementType.I4); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + Assert.Equal(OffsetOfNthGPReg(0), layout.Arguments[0].Slots[0].Offset); + } + + // Audit gap (return classification): {long, short} is 10 bytes, classifies + // as 2 Integer eightbytes -> reg-passable. cDAC's power-of-2 heuristic + // wrongly says ret buf. + [Fact] + public void Return_TenByteStruct_NoRetBuf_FirstArgStaysInFirstGPReg() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "TenBytes", structSize: 10, + fields: + [ + new(0, CorElementType.I8), + new(8, CorElementType.I2), + ]); + sig.ReturnValueType(new TargetPointer(mt.Address)).Param(CorElementType.I4); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + Assert.Equal(OffsetOfNthGPReg(0), layout.Arguments[0].Slots[0].Offset); + } + + // Audit gap (return classification): a 3-byte struct {byte, byte, byte} + // classifies as 1 Integer eightbyte -> reg-passable in RAX. cDAC's + // power-of-2 heuristic wrongly forces a ret buf. + [Fact] + public void Return_ThreeByteStruct_NoRetBuf_FirstArgStaysInFirstGPReg() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "ThreeBytes", structSize: 3, + fields: + [ + new(0, CorElementType.I1), + new(1, CorElementType.I1), + new(2, CorElementType.I1), + ]); + sig.ReturnValueType(new TargetPointer(mt.Address)).Param(CorElementType.I4); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + Assert.Equal(OffsetOfNthGPReg(0), layout.Arguments[0].Slots[0].Offset); + } + + // Regression: when the 8 FP arg-regs are exhausted, the 9th double must + // land on the stack. Verifies that the FP-overflow path in PlaceScalar + // does NOT fall through to the GP-arg path (it falls directly to the + // stack — matching native callingconvention.h:1457-1476). + [Fact] + public void NineDoubles_NinthGoesToFirstStackSlot() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => + { + sig.Return(CorElementType.Void); + for (int i = 0; i < 9; i++) sig.Param(CorElementType.R8); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(9, layout.Arguments.Count); + + // First 8 doubles in FP regs. + for (int i = 0; i < Case.NumFloatArgumentRegisters; i++) + Assert.Equal(OffsetOfNthFPReg(i), layout.Arguments[i].Slots[0].Offset); + + // 9th double on the stack (1st stack slot), NOT in a GP reg. + Assert.Equal(OffsetOfNthStackSlot(0), layout.Arguments[8].Slots[0].Offset); + } + + [Theory] + [InlineData(false, false, false, false)] + [InlineData(true, false, false, false)] + [InlineData(false, true, false, false)] + [InlineData(true, true, false, false)] + [InlineData(false, false, true, false)] + [InlineData(true, false, true, false)] + [InlineData(false, false, false, true)] + [InlineData(true, true, true, false)] + [InlineData(true, true, true, true)] + public void HiddenArgs_DoNotAffectFPPlacement_BankIndependence( + bool hasThis, + bool hasRetBuf, + bool hasParamType, + bool hasAsyncCont) + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, + hasThis, + (rts, sig) => + { + if (hasRetBuf) + { + MockMethodTable bigMT = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, + "BigReturn", + structSize: 24, + fields: + [ + new(0, CorElementType.I8), + new(8, CorElementType.I8), + new(16, CorElementType.I8), + ]); + sig.ReturnValueType(new TargetPointer(bigMT.Address)); + } + else + { + sig.Return(CorElementType.Void); + } + + sig.Param(CorElementType.R8); + }, + hasParamType, + hasAsyncCont); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + + ArgLayout arg = layout.Arguments[0]; + Assert.False(arg.IsPassedByRef); + Assert.Equal(CorElementType.R8, arg.Slots[0].ElementType); + Assert.Equal(OffsetOfNthFPReg(0), arg.Slots[0].Offset); + } + + [Theory] + [InlineData("Vector64`1", 8)] + [InlineData("Vector128`1", 16)] + public void VectorType_OnUnix_BypassesClassifier(string vectorTypeName, int vectorByteSize) + { + SyntheticVectorMetadata metadata = s_syntheticVectorMetadata.Value; + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, + hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddVectorMethodTable( + rts, + vectorTypeName, + vectorByteSize, + metadata.GetTypeDefToken(vectorTypeName)); + sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); + }, + syntheticMetadata: metadata); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + + ArgLayout arg = layout.Arguments[0]; + Assert.False(arg.IsPassedByRef); + Assert.Single(arg.Slots); + Assert.Equal(OffsetOfNthStackSlot(0), arg.Slots[0].Offset); + } + + [Fact] + public void EmptyStruct_PassedByValueOnStack() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, + hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, + "Empty", + structSize: 0, + fields: []); + sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + + ArgLayout arg = layout.Arguments[0]; + Assert.False(arg.IsPassedByRef); + Assert.Single(arg.Slots); + Assert.Equal(OffsetOfNthStackSlot(0), arg.Slots[0].Offset); + } + + [Theory] + [InlineData(CorElementType.Object)] + [InlineData(CorElementType.String)] + public void GCReferenceArgs_GoToGPRegs(CorElementType refType) + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => sig.Return(CorElementType.Void).Param(refType)); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + + ArgLayout arg = layout.Arguments[0]; + Assert.False(arg.IsPassedByRef); + Assert.Equal(OffsetOfNthGPReg(0), arg.Slots[0].Offset); + } + + [Fact] + public void GCReferenceArgs_GoToGPRegs_Class() + { + // ELEMENT_TYPE_CLASS with a dummy TypeDef token. The signature decoder + // falls back to a pointer-sized Class placeholder, which lands in a GP reg. + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => sig.Return(CorElementType.Void).ParamClass()); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.False(arg.IsPassedByRef); + Assert.Equal(OffsetOfNthGPReg(0), arg.Slots[0].Offset); + } + + [Fact] + public void VarArgs_ReturnsLayout_OnUnixTarget() + { + // Managed varargs are not supported on Unix, but the contract should + // handle the signature gracefully (it flows through the iterator). + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => sig.VarArg().Return(CorElementType.Void).Param(CorElementType.I4)); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + Assert.NotNull(layout.VarArgCookieOffset); + } + +#pragma warning restore xUnit1004 + + // ----- ValueTypeHandle population for by-value value-type args ----- + // + // AMD64-Unix (SysV) pre-decomposes by-value structs into GC-typed eightbytes + // (I8/Class/Byref/R8) when they're passed in registers or split between + // regs+stack. For those cases ValueTypeHandle MUST be null (the existing + // per-slot ElementType already gives the GC scanner everything it needs; + // walking GCDesc would duplicate or contradict the eightbyte classification). + // + // ValueTypeHandle is only populated when the iterator hands back a single + // ValueType-typed slot, which happens when the struct is too large for + // enregistration and lands entirely on the stack as a contiguous buffer. + + [Fact] + public void ValueTypeByValue_LargeOnStack_PopulatesValueTypeHandle() + { + // 32-byte struct > 16 -> ineligible for enregistration -> passed by value + // on the stack as a contiguous buffer. The iterator emits a single + // ValueType-typed slot; ValueTypeHandle carries the MT for GCDesc walk. + ulong expectedMtAddress = 0; + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "FourLongs", structSize: 32, + fields: + [ + new(0, CorElementType.I8), + new(8, CorElementType.I8), + new(16, CorElementType.I8), + new(24, CorElementType.I8), + ]); + expectedMtAddress = mt.Address; + sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.False(arg.IsPassedByRef); + Assert.NotNull(arg.ValueTypeHandle); + Assert.Equal(expectedMtAddress, (ulong)arg.ValueTypeHandle.Value.Address); + } + + [Fact] + public void ValueTypeByValue_SysVDecomposed_DoesNotPopulateValueTypeHandle() + { + // { object o; double d; } -> SysV-classified into two eightbytes + // (IntegerReference, SSE). The iterator emits one ArgSlot with + // ElementType=Class and another with ElementType=R8. Because the per-slot + // ElementType already encodes GC refs precisely, ValueTypeHandle must be + // null -- the GCDesc walk would otherwise double-report or contradict. + // Pins the "all slots ValueType" discriminator in ComputeValueTypeHandle. + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "ObjectAndDouble2", structSize: 16, + fields: [new(0, CorElementType.Object), new(8, CorElementType.R8)]); + sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.False(arg.IsPassedByRef); + Assert.Equal(2, arg.Slots.Count); + Assert.Null(arg.ValueTypeHandle); + } +} diff --git a/src/native/managed/cdac/tests/CallingConvention/AMD64WindowsCallingConventionTests.cs b/src/native/managed/cdac/tests/CallingConvention/AMD64WindowsCallingConventionTests.cs new file mode 100644 index 00000000000000..c0ce14a52ac85d --- /dev/null +++ b/src/native/managed/cdac/tests/CallingConvention/AMD64WindowsCallingConventionTests.cs @@ -0,0 +1,703 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.Tests; + +public class AMD64WindowsCallingConventionTests +{ + private static readonly Lazy s_syntheticVectorMetadata = new(SyntheticVectorMetadata.Create); + + private static CallConvTestCase Case => CallConvCases.AMD64Windows; + + private static int OffsetOfFirstGPArg => Case.ArgumentRegistersOffset; + private static int OffsetOfStackArgs => Case.OffsetOfArgs + Case.NumArgumentRegisters * Case.PointerSize; + private static int OffsetOfFirstFPArg => Case.OffsetOfFloatArgumentRegisters.Value; + + // Integer-valued args consume RCX/RDX/R8/R9 in order, then spill to stack + // slots that advance by one pointer-sized home slot per argument. + [Theory] + [InlineData(CorElementType.I1, 4)] + [InlineData(CorElementType.U1, 4)] + [InlineData(CorElementType.I2, 4)] + [InlineData(CorElementType.U2, 4)] + [InlineData(CorElementType.I4, 4)] + [InlineData(CorElementType.U4, 4)] + [InlineData(CorElementType.I8, 4)] + [InlineData(CorElementType.U8, 4)] + [InlineData(CorElementType.I4, 5)] + [InlineData(CorElementType.I8, 5)] + public void IntArgs_FillRegsAndSpillToStack(CorElementType elementType, int count) + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => + { + sig.Return(CorElementType.Void); + for (int i = 0; i < count; i++) + { + sig.Param(elementType); + } + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(count, layout.Arguments.Count); + + for (int i = 0; i < count; i++) + { + int expectedOffset = i < 4 + ? OffsetOfFirstGPArg + i * Case.PointerSize + : OffsetOfStackArgs + (i - 4) * Case.PointerSize; + + Assert.Equal(expectedOffset, layout.Arguments[i].Slots[0].Offset); + } + } + + [Fact] + public void InstanceMethod_ThisOffsetIsFirst() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: true, + sig => sig.Return(CorElementType.Void)); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(OffsetOfFirstGPArg, layout.ThisOffset); + } + + // Floating-point args shadow into XMM0-XMM3 by position, then spill into + // pointer-sized stack home slots once the four register positions are used. + [Theory] + [InlineData(CorElementType.R4, 1)] + [InlineData(CorElementType.R8, 1)] + [InlineData(CorElementType.R4, 4)] + [InlineData(CorElementType.R8, 4)] + [InlineData(CorElementType.R4, 6)] + [InlineData(CorElementType.R8, 6)] + public void FloatArgs_FillFPRegsAndSpillToStack(CorElementType elementType, int count) + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => + { + sig.Return(CorElementType.Void); + for (int i = 0; i < count; i++) + { + sig.Param(elementType); + } + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(count, layout.Arguments.Count); + + for (int i = 0; i < count; i++) + { + int expectedOffset = i < 4 + ? OffsetOfFirstFPArg + i * Case.FloatRegisterSize + : OffsetOfStackArgs + (i - 4) * Case.PointerSize; + + Assert.Equal(elementType, layout.Arguments[i].Slots[0].ElementType); + Assert.Equal(expectedOffset, layout.Arguments[i].Slots[0].Offset); + } + } + + // A lone float still chooses the XMM bank based on its ordinal position, + // while the surrounding integer slots continue to map to GP registers. + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + public void OneFloatAmongInts_LandsInXMM(int floatPosition) + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => + { + sig.Return(CorElementType.Void); + for (int i = 0; i < 4; i++) + { + sig.Param(i == floatPosition ? CorElementType.R8 : CorElementType.I4); + } + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(4, layout.Arguments.Count); + + for (int i = 0; i < 4; i++) + { + int expectedOffset = i == floatPosition + ? OffsetOfFirstFPArg + i * Case.FloatRegisterSize + : OffsetOfFirstGPArg + i * Case.PointerSize; + CorElementType expectedType = i == floatPosition ? CorElementType.R8 : CorElementType.I4; + + Assert.Equal(expectedType, layout.Arguments[i].Slots[0].ElementType); + Assert.Equal(expectedOffset, layout.Arguments[i].Slots[0].Offset); + } + } + + [Fact] + public void InstanceMethod_FirstUserDoubleShiftsToXMM1() + { + // Hidden `this` consumes slot 0 (RCX). The first user-arg double therefore + // shadows into XMM1, NOT XMM0. This is a managed-specific consequence of + // `this` taking the first slot like any other argument. + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: true, + sig => sig.Return(CorElementType.Void).Param(CorElementType.R8)); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + Assert.Equal(OffsetOfFirstFPArg + 1 * Case.FloatRegisterSize, layout.Arguments[0].Slots[0].Offset); + } + + [Fact] + public void StaticMethod_RetBuf_FirstUserDoubleShiftsToXMM1() + { + // Method returns a 24-byte struct -> hidden retBuf in RCX. First user-arg + // double therefore shadows into XMM1, NOT XMM0. + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable bigMT = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "BigReturn", structSize: 24, + fields: + [ + new(0, CorElementType.I8), + new(8, CorElementType.I8), + new(16, CorElementType.I8), + ]); + sig.ReturnValueType(new TargetPointer(bigMT.Address)).Param(CorElementType.R8); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + Assert.Equal(OffsetOfFirstFPArg + 1 * Case.FloatRegisterSize, layout.Arguments[0].Slots[0].Offset); + } + + [Fact] + public void InstanceMethod_RetBuf_FirstUserDoubleShiftsToXMM2() + { + // `this` AND retBuf consume slots 0 and 1; first user-arg double -> XMM2. + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: true, + (rts, sig) => + { + MockMethodTable bigMT = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "BigReturn", structSize: 24, + fields: + [ + new(0, CorElementType.I8), + new(8, CorElementType.I8), + new(16, CorElementType.I8), + ]); + sig.ReturnValueType(new TargetPointer(bigMT.Address)).Param(CorElementType.R8); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + Assert.Equal(OffsetOfFirstFPArg + 2 * Case.FloatRegisterSize, layout.Arguments[0].Slots[0].Offset); + } + + [Theory] + [InlineData(false, false, false, false, 0)] + [InlineData(true, false, false, false, 1)] + [InlineData(false, true, false, false, 1)] + [InlineData(true, true, false, false, 2)] + [InlineData(false, false, true, false, 1)] + [InlineData(true, false, true, false, 2)] + [InlineData(false, false, false, true, 1)] + [InlineData(true, true, true, false, 3)] + [InlineData(true, true, false, true, 3)] + [InlineData(true, true, true, true, -1)] + public void HiddenArgs_ShiftFirstUserDouble( + bool hasThis, bool hasRetBuf, bool hasParamType, bool hasAsyncCont, + int expectedFloatPosition) + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, + hasThis, + (rts, sig) => + { + if (hasRetBuf) + { + MockMethodTable bigMT = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, + "BigReturn", + structSize: 24, + fields: + [ + new(0, CorElementType.I8), + new(8, CorElementType.I8), + new(16, CorElementType.I8), + ]); + sig.ReturnValueType(new TargetPointer(bigMT.Address)); + } + else + { + sig.Return(CorElementType.Void); + } + + sig.Param(CorElementType.R8); + }, + hasParamType, + hasAsyncCont); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + Assert.Equal(CorElementType.R8, layout.Arguments[0].Slots[0].ElementType); + + int expectedOffset = expectedFloatPosition >= 0 + ? OffsetOfFirstFPArg + expectedFloatPosition * Case.FloatRegisterSize + : OffsetOfStackArgs; + + Assert.Equal(expectedOffset, layout.Arguments[0].Slots[0].Offset); + } + + [Theory] + [InlineData(false, false, 0, 1)] + [InlineData(true, false, 1, 2)] + [InlineData(false, true, 1, 2)] + [InlineData(true, true, 2, 3)] + public void VarArgs_CookieAndFirstUserArg_OnWindows( + bool hasThis, bool hasRetBuf, int expectedCookieSlot, int expectedFirstUserSlot) + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, + hasThis, + (rts, sig) => + { + sig.VarArg(); + if (hasRetBuf) + { + MockMethodTable bigMT = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, + "BigReturn", + structSize: 24, + fields: + [ + new(0, CorElementType.I8), + new(8, CorElementType.I8), + new(16, CorElementType.I8), + ]); + sig.ReturnValueType(new TargetPointer(bigMT.Address)); + } + else + { + sig.Return(CorElementType.Void); + } + + sig.Param(CorElementType.R8); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.NotNull(layout.VarArgCookieOffset); + Assert.Single(layout.Arguments); + Assert.Equal( + OffsetOfFirstGPArg + expectedCookieSlot * Case.PointerSize, + layout.VarArgCookieOffset.Value); + Assert.Equal( + OffsetOfFirstFPArg + expectedFirstUserSlot * Case.FloatRegisterSize, + layout.Arguments[0].Slots[0].Offset); + } + + [Fact] + public void NonVarArgs_HasNullVarArgCookieOffset() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => sig.Return(CorElementType.Void).Param(CorElementType.I4)); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Null(layout.VarArgCookieOffset); + } + + // Stack home slots continue to progress one pointer at a time once the four + // register-backed positions are exhausted by earlier integer arguments. + [Fact] + public void TenArgs_StackOffsetsProgress() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => + { + sig.Return(CorElementType.Void); + for (int i = 0; i < 10; i++) + { + sig.Param(CorElementType.I4); + } + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(10, layout.Arguments.Count); + + for (int i = 0; i < 10; i++) + { + int expectedOffset = i < 4 + ? OffsetOfFirstGPArg + i * Case.PointerSize + : OffsetOfStackArgs + (i - 4) * Case.PointerSize; + + Assert.Equal(expectedOffset, layout.Arguments[i].Slots[0].Offset); + } + } + + // Windows x64 does not classify HFA-shaped structs into XMM registers; small + // ones still use a GP slot, while larger ones take the existing byref path. + [Theory] + [InlineData(2, false)] + [InlineData(4, true)] + public void HFAShapedStruct_OnWindows_DoesNotEnregisterInFP(int floatCount, bool expectByref) + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockDescriptors.CallingConvention.ValueTypeField[] fields = new MockDescriptors.CallingConvention.ValueTypeField[floatCount]; + for (int i = 0; i < floatCount; i++) + { + fields[i] = new(i * sizeof(float), CorElementType.R4); + } + + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, + $"FloatStruct{floatCount}", + structSize: sizeof(float) * floatCount, + fields: fields); + sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + + ArgLayout arg = layout.Arguments[0]; + Assert.Equal(expectByref, arg.IsPassedByRef); + Assert.Single(arg.Slots); + Assert.Equal(OffsetOfFirstGPArg, arg.Slots[0].Offset); + } + + // ---- Implicit by-reference edge cases ---- + + [Fact] + public void NineByteStruct_ImplicitByref_OneSlot() + { + // 9-byte struct > 8 -> implicit byref. The arg slot holds an 8-byte + // pointer, not the struct contents (would otherwise take 2 slots). + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "NineBytes", structSize: 9, + fields: + [ + new(0, CorElementType.I8), + new(8, CorElementType.I1), + ]); + sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.True(arg.IsPassedByRef); + Assert.Single(arg.Slots); + Assert.Equal(OffsetOfFirstGPArg, arg.Slots[0].Offset); + } + + [Fact] + public void ThreeByteStruct_NonPowerOfTwo_ImplicitByref() + { + // 3-byte struct: size <= 8 but not in {1, 2, 4, 8}. Implicit byref applies. + // Verifies the `(size & (size - 1)) != 0` guard in IsArgPassedByRefBySize. + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "ThreeBytes", structSize: 3, + fields: + [ + new(0, CorElementType.I1), + new(1, CorElementType.I1), + new(2, CorElementType.I1), + ]); + sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + Assert.True(layout.Arguments[0].IsPassedByRef); + } + + [Fact] + public void EightByteStruct_Enregisters_NotByref() + { + // 8-byte struct is in the {1, 2, 4, 8} exception list -> NOT byref; + // enregistered as a value in RCX. + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "EightBytes", structSize: 8, + fields: [new(0, CorElementType.I8)]); + sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.False(arg.IsPassedByRef); + Assert.Equal(OffsetOfFirstGPArg, arg.Slots[0].Offset); + } + + [Theory] + [InlineData(CorElementType.I4)] + [InlineData(CorElementType.I8)] + [InlineData(CorElementType.R4)] + [InlineData(CorElementType.R8)] + public void Return_PrimitiveType_NoRetBuf(CorElementType retType) + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => sig.Return(retType).Param(CorElementType.I4)); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Null(layout.ThisOffset); + Assert.Single(layout.Arguments); + Assert.Equal(OffsetOfFirstGPArg, layout.Arguments[0].Slots[0].Offset); + } + + [Theory] + [InlineData(4, false)] + [InlineData(8, false)] + [InlineData(16, true)] + [InlineData(24, true)] + public void Return_ValueType_RetBufBySize(int structSize, bool expectRetBuf) + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockDescriptors.CallingConvention.ValueTypeField[] fields = new MockDescriptors.CallingConvention.ValueTypeField[structSize / sizeof(int)]; + for (int i = 0; i < fields.Length; i++) + { + fields[i] = new(i * sizeof(int), CorElementType.I4); + } + + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, $"ReturnStruct{structSize}", structSize, fields); + sig.ReturnValueType(new TargetPointer(mt.Address)).Param(CorElementType.I4); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + Assert.Equal( + expectRetBuf ? OffsetOfFirstGPArg + Case.PointerSize : OffsetOfFirstGPArg, + layout.Arguments[0].Slots[0].Offset); + } + + [Fact] + public void EmptyStruct_ImplicitByref() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "Empty", structSize: 0, fields: []); + sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + Assert.True(layout.Arguments[0].IsPassedByRef); + } + + [Theory] + [InlineData(CorElementType.Object)] + [InlineData(CorElementType.String)] + public void GCReferenceArgs_GoToGPRegs_NotByref(CorElementType refType) + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => sig.Return(CorElementType.Void).Param(refType)); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.False(arg.IsPassedByRef); + Assert.Equal(OffsetOfFirstGPArg, arg.Slots[0].Offset); + } + + [Theory] + [InlineData("Vector64`1", 8, false)] + [InlineData("Vector128`1", 16, true)] + public void VectorType_OnWindows_ClassifiedBySizeNotVectorness( + string vectorTypeName, + int vectorByteSize, + bool expectByref) + { + SyntheticVectorMetadata metadata = s_syntheticVectorMetadata.Value; + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, + hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddVectorMethodTable( + rts, + vectorTypeName, + vectorByteSize, + metadata.GetTypeDefToken(vectorTypeName)); + sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); + }, + syntheticMetadata: metadata); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + + ArgLayout arg = layout.Arguments[0]; + Assert.Equal(expectByref, arg.IsPassedByRef); + Assert.Single(arg.Slots); + Assert.Equal(OffsetOfFirstGPArg, arg.Slots[0].Offset); + } + + [Fact] + public void TypedReference_ImplicitByref_OneSlot() + { + // TypedReference is 16 bytes -> implicit byref on Win x64 (in contrast + // with SysV where it lands in 2 GP regs). Verifies that the substitution + // through g_TypedReferenceMT produces a 16-byte ArgTypeInfo that then + // takes the byref path. + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable typedRefMT = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "System.TypedReference", structSize: 16, + fields: [new(0, CorElementType.Byref), new(8, CorElementType.I)]); + rts.SetTypedReferenceMethodTable(typedRefMT.Address); + sig.Return(CorElementType.Void).Param(CorElementType.TypedByRef); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.True(arg.IsPassedByRef); + Assert.Single(arg.Slots); + Assert.Equal(OffsetOfFirstGPArg, arg.Slots[0].Offset); + } + + // ----- ValueTypeHandle population for by-value value-type args ----- + // + // The GC scanner needs the value-type's TypeHandle to walk the GCDesc and + // report embedded managed references inside structs that the iterator did + // NOT pre-decompose into GC-typed ArgSlots. On AMD64-Windows, enregistered + // and stack-passed value-type args go through the GP path without per-slot + // GC typing, so ValueTypeHandle must be populated. + + [Fact] + public void ValueTypeByValue_Enregistered_PopulatesValueTypeHandle() + { + // 8-byte struct -> fits in RCX, NOT byref. ValueTypeHandle should point + // to the struct's MethodTable so GC scanner can walk its GCDesc. + ulong expectedMtAddress = 0; + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "ObjectAndPad", structSize: 8, + fields: [new(0, CorElementType.Object)]); + expectedMtAddress = mt.Address; + sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.False(arg.IsPassedByRef); + Assert.NotNull(arg.ValueTypeHandle); + Assert.Equal(expectedMtAddress, (ulong)arg.ValueTypeHandle.Value.Address); + } + + [Fact] + public void ValueTypeByValue_OnStack_PopulatesValueTypeHandle() + { + // 8-byte enregisterable struct as the 5th arg lands on the stack + // (RCX/RDX/R8/R9 consumed by 4 prior I8s) -- still by value, MT populated. + ulong expectedMtAddress = 0; + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "ObjectAndPad", structSize: 8, + fields: [new(0, CorElementType.Object)]); + expectedMtAddress = mt.Address; + sig.Return(CorElementType.Void); + for (int i = 0; i < 4; i++) sig.Param(CorElementType.I8); + sig.ParamValueType(new TargetPointer(mt.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(5, layout.Arguments.Count); + ArgLayout structArg = layout.Arguments[4]; + Assert.False(structArg.IsPassedByRef); + Assert.NotNull(structArg.ValueTypeHandle); + Assert.Equal(expectedMtAddress, (ulong)structArg.ValueTypeHandle.Value.Address); + } + + [Fact] + public void ValueTypeByRef_DoesNotPopulateValueTypeHandle() + { + // 9-byte struct -> implicit byref. The slot carries a pointer, not the + // value bytes; GC scanner reports it via the byref path, not GCDesc walk. + // Regression guard: pins the IsByRef short-circuit in ComputeValueTypeHandle. + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "NineBytes", structSize: 9, + fields: [new(0, CorElementType.I8), new(8, CorElementType.I1)]); + sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.True(layout.Arguments[0].IsPassedByRef); + Assert.Null(layout.Arguments[0].ValueTypeHandle); + } + + [Fact] + public void ValueTypeByValue_ByRefLike_PopulatesValueTypeHandle() + { + // ByRefLike value-type args (Span, ref structs) are surfaced through + // ValueTypeHandle just like ordinary value types. The GC scanner queries + // IRuntimeTypeSystem.IsByRefLike to choose its walk strategy: a CGCDesc + // walk for ordinary types, or a field-by-field walk for ByRefLike types + // (the CGCDesc series doesn't encode managed byref fields). + const uint IsByRefLikeFlag = 0x00001000; + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "ByRefLikeStruct", structSize: 8, + fields: [new(0, CorElementType.I8)]); + mt.MTFlags |= IsByRefLikeFlag; + sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.False(arg.IsPassedByRef); + Assert.Single(arg.Slots); + Assert.NotNull(arg.ValueTypeHandle); + } +} diff --git a/src/native/managed/cdac/tests/CallingConvention/CallConvCases.cs b/src/native/managed/cdac/tests/CallingConvention/CallConvCases.cs new file mode 100644 index 00000000000000..26d40c237c07e5 --- /dev/null +++ b/src/native/managed/cdac/tests/CallingConvention/CallConvCases.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Diagnostics.DataContractReader.Contracts; + +namespace Microsoft.Diagnostics.DataContractReader.Tests; + +public sealed record CallConvTestCase( + string Name, + RuntimeInfoArchitecture Architecture, + RuntimeInfoOperatingSystem OperatingSystem, + bool Is64Bit, + int TransitionBlockSize, + int ArgumentRegistersOffset, + int FirstGCRefMapSlot, + int OffsetOfArgs, + int? OffsetOfFloatArgumentRegisters, + int NumArgumentRegisters, + int NumFloatArgumentRegisters, + int FloatRegisterSize) +{ + public int PointerSize => Is64Bit ? 8 : 4; + public MockTarget.Architecture MockArch => new() { IsLittleEndian = true, Is64Bit = Is64Bit }; + public override string ToString() => Name; +} + +public static class CallConvCases +{ + public static readonly CallConvTestCase AMD64Windows = new( + "AMD64-Windows", RuntimeInfoArchitecture.X64, RuntimeInfoOperatingSystem.Windows, Is64Bit: true, + TransitionBlockSize: 40, ArgumentRegistersOffset: 40, FirstGCRefMapSlot: 40, + OffsetOfArgs: 40, OffsetOfFloatArgumentRegisters: 0, + NumArgumentRegisters: 4, NumFloatArgumentRegisters: 0, FloatRegisterSize: 16); + + public static readonly CallConvTestCase AMD64Unix = new( + "AMD64-Unix", RuntimeInfoArchitecture.X64, RuntimeInfoOperatingSystem.Unix, Is64Bit: true, + TransitionBlockSize: 48, ArgumentRegistersOffset: 0, FirstGCRefMapSlot: 0, + OffsetOfArgs: 48, OffsetOfFloatArgumentRegisters: -128, + NumArgumentRegisters: 6, NumFloatArgumentRegisters: 8, FloatRegisterSize: 16); + + public static IEnumerable AllCases => new[] + { + new object[] { AMD64Windows }, new object[] { AMD64Unix }, + }; + + public static IEnumerable AMD64UnixOnly => new[] { new object[] { AMD64Unix } }; +} diff --git a/src/native/managed/cdac/tests/CallingConvention/CallingConventionTestHelpers.cs b/src/native/managed/cdac/tests/CallingConvention/CallingConventionTestHelpers.cs new file mode 100644 index 00000000000000..5c4156f0866ea9 --- /dev/null +++ b/src/native/managed/cdac/tests/CallingConvention/CallingConventionTestHelpers.cs @@ -0,0 +1,211 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection.Metadata; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.DataContractReader.RuntimeTypeSystemHelpers; +using Moq; +using ModuleHandle = Microsoft.Diagnostics.DataContractReader.Contracts.ModuleHandle; +using TypeHandle = Microsoft.Diagnostics.DataContractReader.Contracts.TypeHandle; + +namespace Microsoft.Diagnostics.DataContractReader.Tests; + +internal static class CallingConventionTestHelpers +{ + public static (Target Target, MethodDescHandle Handle) CreateTargetWithStaticMethod( + CallConvTestCase testCase, + Action buildSignature, + SyntheticVectorMetadata? syntheticMetadata = null) + => CreateTargetWithMethod(testCase, hasThis: false, (_, sig) => buildSignature(sig), syntheticMetadata: syntheticMetadata); + + public static (Target Target, MethodDescHandle Handle) CreateTargetWithMethod( + CallConvTestCase testCase, + bool hasThis, + Action buildSignature, + bool hasParamType = false, + bool hasAsyncContinuation = false, + SyntheticVectorMetadata? syntheticMetadata = null) + => CreateTargetWithMethod(testCase, hasThis, (_, sig) => buildSignature(sig), hasParamType, hasAsyncContinuation, syntheticMetadata: syntheticMetadata); + + public static (Target Target, MethodDescHandle Handle) CreateTargetWithMethod( + CallConvTestCase testCase, + bool hasThis, + Action configure, + bool hasParamType = false, + bool hasAsyncContinuation = false, + Func? enclosingMTOverride = null, + SyntheticVectorMetadata? syntheticMetadata = null, + bool isArmSoftFP = false) + { + var targetBuilder = new TestPlaceholderTarget.Builder(testCase.MockArch); + MockDescriptors.RuntimeTypeSystem rtsBuilder = new(targetBuilder.MemoryBuilder); + MockLoaderBuilder loaderBuilder = new(targetBuilder.MemoryBuilder); + MockDescriptors.MockMethodDescriptorsBuilder mdsBuilder = new(rtsBuilder, loaderBuilder); + + MockLoaderModule mockModule = loaderBuilder.AddModule(simpleName: "TestModule"); + rtsBuilder.SystemObjectMethodTable.Module = mockModule.Address; + rtsBuilder.ContinuationMethodTable.Module = mockModule.Address; + + int pointerSize = targetBuilder.MemoryBuilder.TargetTestHelpers.PointerSize; + SignatureBlobBuilder sigBuilder = new(pointerSize, hasThis); + configure(rtsBuilder, sigBuilder); + byte[] sigBytes = sigBuilder.Build(); + + MockMethodTable enclosingMT = enclosingMTOverride?.Invoke(rtsBuilder) + ?? (hasParamType ? CreateSharedGenericMethodTable(rtsBuilder, hasThis) : rtsBuilder.SystemObjectMethodTable); + enclosingMT.Module = mockModule.Address; + MockMethodTableAuxiliaryData aux = rtsBuilder.AddMethodTableAuxiliaryData(); + aux.LoaderModule = mockModule.Address; + enclosingMT.AuxiliaryData = aux.Address; + + MockMemorySpace.HeapFragment sigFragment = rtsBuilder.TypeSystemAllocator.Allocate( + (ulong)sigBytes.Length, "TestMethodSig"); + Array.Copy(sigBytes, sigFragment.Data, sigBytes.Length); + + uint methodDescTotalSize = (uint)mdsBuilder.StoredSigMethodDescLayout.Size; + if (hasAsyncContinuation) + { + methodDescTotalSize += mdsBuilder.AsyncMethodDataSize; + } + + byte methodDescSize = (byte)(methodDescTotalSize / mdsBuilder.MethodDescAlignment); + byte chunkCount = 1; + byte chunkSize = (byte)(chunkCount * methodDescSize); + MockMethodDescChunk chunk = mdsBuilder.AddMethodDescChunk("TestMethod", chunkSize); + chunk.MethodTable = enclosingMT.Address; + chunk.Size = chunkSize; + chunk.Count = chunkCount; + + MockStoredSigMethodDesc md = chunk.GetMethodDescAtChunkIndex(0, mdsBuilder.StoredSigMethodDescLayout); + md.ChunkIndex = 0; + md.Slot = 0; + + ushort methodFlags = (ushort)MethodClassification.EEImpl; + if (!hasThis) + { + methodFlags |= (ushort)MethodDescFlags_1.MethodDescFlags.Static; + } + + if (hasAsyncContinuation) + { + methodFlags |= (ushort)MethodDescFlags_1.MethodDescFlags.HasAsyncMethodData; + } + + md.Flags = methodFlags; + md.Sig = sigFragment.Address; + md.CSig = (uint)sigBytes.Length; + + if (hasAsyncContinuation) + { + int asyncDataOffset = (int)(md.Address - chunk.Address) + mdsBuilder.StoredSigMethodDescLayout.Size; + targetBuilder.MemoryBuilder.TargetTestHelpers.Write( + chunk.Memory.Span.Slice(asyncDataOffset, sizeof(uint)), + (uint)RuntimeTypeSystem_1.AsyncMethodFlags.AsyncCall); + } + + Dictionary types = new Dictionary() + { + [DataType.TransitionBlock] = MockDescriptors.CallingConvention.CreateTransitionBlockTypeInfo(testCase), + [DataType.FieldDesc] = MockDescriptors.CallingConvention.CreateFieldDescTypeInfo(testCase.MockArch), + [DataType.MethodDesc] = TargetTestHelpers.CreateTypeInfo(mdsBuilder.MethodDescLayout), + [DataType.MethodDescChunk] = TargetTestHelpers.CreateTypeInfo(mdsBuilder.MethodDescChunkLayout), + [DataType.StoredSigMethodDesc] = TargetTestHelpers.CreateTypeInfo(mdsBuilder.StoredSigMethodDescLayout), + [DataType.EEImplMethodDesc] = new Target.TypeInfo { Size = mdsBuilder.EEImplMethodDescSize }, + [DataType.InstantiatedMethodDesc] = TargetTestHelpers.CreateTypeInfo(mdsBuilder.InstantiatedMethodDescLayout), + [DataType.DynamicMethodDesc] = TargetTestHelpers.CreateTypeInfo(mdsBuilder.DynamicMethodDescLayout), + [DataType.NonVtableSlot] = new Target.TypeInfo { Size = mdsBuilder.NonVtableSlotSize }, + [DataType.MethodImpl] = new Target.TypeInfo { Size = mdsBuilder.MethodImplSize }, + [DataType.NativeCodeSlot] = new Target.TypeInfo { Size = mdsBuilder.NativeCodeSlotSize }, + [DataType.AsyncMethodData] = new Target.TypeInfo + { + Size = mdsBuilder.AsyncMethodDataSize, + Fields = new Dictionary + { + [nameof(Data.AsyncMethodData.Flags)] = new Target.FieldInfo { Offset = 0 }, + }, + }, + [DataType.ArrayMethodDesc] = new Target.TypeInfo { Size = mdsBuilder.ArrayMethodDescSize }, + [DataType.FCallMethodDesc] = new Target.TypeInfo { Size = mdsBuilder.FCallMethodDescSize }, + [DataType.PInvokeMethodDesc] = new Target.TypeInfo { Size = mdsBuilder.PInvokeMethodDescSize }, + [DataType.CLRToCOMCallMethodDesc] = new Target.TypeInfo { Size = mdsBuilder.CLRToCOMCallMethodDescSize }, + } + .Concat(MethodTableTests.CreateContractTypes(rtsBuilder)) + .Concat(LoaderTests.CreateContractTypes(loaderBuilder)) + .ToDictionary(); + + var globals = MethodTableTests.CreateContractGlobals(rtsBuilder).Concat( + [ + (nameof(Constants.Globals.MethodDescTokenRemainderBitCount), + (ulong)MockDescriptors.MockMethodDescriptorsBuilder.TokenRemainderBitCount), + (nameof(Constants.Globals.FieldOffsetBigRVA), 0xFFFFFFFFUL), + ]); + + // ARM32 and ARM64 enable FEATURE_HFA (needed for HFA detection in IsHFA). + if (testCase.Architecture is RuntimeInfoArchitecture.Arm or RuntimeInfoArchitecture.Arm64) + globals = globals.Append((nameof(Constants.Globals.FeatureHFA), 1UL)); + + // ARM32 soft-float (armel): the presence of FeatureArmSoftFP tells the + // iterator to skip the FP register path. Only add it when explicitly requested. + if (isArmSoftFP) + globals = globals.Append((nameof(Constants.Globals.FeatureArmSoftFP), 1UL)); + + var globalsArray = globals.ToArray(); + + Mock execMgr = new(); + Mock precode = new(); + Mock platMetadata = new(); + + Mock ecmaMd = new(); + ecmaMd.Setup(e => e.GetMetadata(It.IsAny())).Returns( + (ModuleHandle moduleHandle) => syntheticMetadata is not null && moduleHandle.Address == mockModule.Address + ? syntheticMetadata.Reader + : (MetadataReader?)null); + + Mock sig = new(); + sig.Setup(s => s.DecodeFieldSignature(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(default(TypeHandle)); + + var target = targetBuilder + .AddTypes(types) + .AddGlobals(globalsArray) + .AddGlobalStrings( + (Constants.Globals.Architecture, testCase.Architecture.ToString().ToLowerInvariant()), + (Constants.Globals.OperatingSystem, testCase.OperatingSystem.ToString().ToLowerInvariant())) + .AddContract(version: "c1") + .AddContract(version: "c1") + .AddContract(version: "c1") + .AddContract(version: "c1") + .AddMockContract(ecmaMd) + .AddMockContract(sig) + .AddMockContract(execMgr) + .AddMockContract(precode) + .AddMockContract(platMetadata) + .Build(); + + MethodDescHandle mdh = target.Contracts.RuntimeTypeSystem.GetMethodDescHandle(new TargetPointer(md.Address)); + return (target, mdh); + } + + private static MockMethodTable CreateSharedGenericMethodTable(MockDescriptors.RuntimeTypeSystem rtsBuilder, bool hasThis) + { + MockEEClass eeClass = rtsBuilder.AddEEClass(hasThis ? "SharedGenericInterfaceEEClass" : "SharedGenericClassEEClass"); + MockMethodTable methodTable = rtsBuilder.AddMethodTable(hasThis ? "SharedGenericInterface" : "SharedGenericClass"); + + uint flags = (uint)MethodTableFlags_1.WFLAGS_LOW.GenericsMask_TypicalInstantiation; + if (hasThis) + { + flags |= (uint)MethodTableFlags_1.WFLAGS_HIGH.Category_Interface; + } + + methodTable.MTFlags = flags; + methodTable.BaseSize = rtsBuilder.Builder.TargetTestHelpers.ObjectBaseSize; + methodTable.ParentMethodTable = rtsBuilder.SystemObjectMethodTable.Address; + methodTable.NumVirtuals = 1; + eeClass.MethodTable = methodTable.Address; + methodTable.EEClassOrCanonMT = eeClass.Address; + return methodTable; + } +} diff --git a/src/native/managed/cdac/tests/CallingConvention/CallingConventionTests.cs b/src/native/managed/cdac/tests/CallingConvention/CallingConventionTests.cs new file mode 100644 index 00000000000000..03428d5f8b5a6a --- /dev/null +++ b/src/native/managed/cdac/tests/CallingConvention/CallingConventionTests.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.Tests; + +public class CallingConventionTests +{ + [Theory] + [MemberData(nameof(CallConvCases.AllCases), MemberType = typeof(CallConvCases))] + public void Harness_StaticMethod_OneInt_DecodesSuccessfully(CallConvTestCase testCase) + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + testCase, + sig => sig.Return(CorElementType.Void).Param(CorElementType.I4)); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + + Assert.Null(layout.ThisOffset); + Assert.Null(layout.AsyncContinuationOffset); + Assert.Single(layout.Arguments); + Assert.Single(layout.Arguments[0].Slots); + Assert.Equal(CorElementType.I4, layout.Arguments[0].Slots[0].ElementType); + } + + [Theory] + [MemberData(nameof(CallConvCases.AllCases), MemberType = typeof(CallConvCases))] + public void Harness_InstanceMethod_ReportsThisOffset(CallConvTestCase testCase) + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + testCase, hasThis: true, + sig => sig.Return(CorElementType.Void).Param(CorElementType.I4)); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.NotNull(layout.ThisOffset); + } + + [Fact] + public void InstanceMethod_OnValueType_IsValueTypeThisShouldBeTrue() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + CallConvCases.AMD64Windows, hasThis: true, + (rts, sig) => sig.Return(CorElementType.Void), + enclosingMTOverride: rts => MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "ValueTypeWithMethod", structSize: 8, fields: [new(0, CorElementType.I4)])); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.NotNull(layout.ThisOffset); + Assert.True(layout.IsValueTypeThis, + "Instance method on a value type should report IsValueTypeThis == true so GcScanner emits GC_CALL_INTERIOR on the this slot"); + } +} diff --git a/src/native/managed/cdac/tests/CallingConvention/SignatureBlobBuilder.cs b/src/native/managed/cdac/tests/CallingConvention/SignatureBlobBuilder.cs new file mode 100644 index 00000000000000..74261931db5243 --- /dev/null +++ b/src/native/managed/cdac/tests/CallingConvention/SignatureBlobBuilder.cs @@ -0,0 +1,125 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.Diagnostics.DataContractReader.Contracts; + +namespace Microsoft.Diagnostics.DataContractReader.Tests; + +internal sealed class SignatureBlobBuilder +{ + private const byte HASTHIS_FLAG = 0x20; + private const byte VARARG_CC = 0x05; // IMAGE_CEE_CS_CALLCONV_VARARG + private const byte ELEMENT_TYPE_INTERNAL = 0x21; + + private readonly bool _hasThis; + private readonly int _pointerSize; + private bool _isVarArg; + private readonly List _params = new(); + private ParamSpec _return = new(CorElementType.Void, default); + + public SignatureBlobBuilder(int pointerSize, bool hasThis = false) + { + _pointerSize = pointerSize; + _hasThis = hasThis; + } + + public SignatureBlobBuilder VarArg() + { + _isVarArg = true; + return this; + } + + public SignatureBlobBuilder Return(CorElementType primitive) + { + _return = new ParamSpec(primitive, default); + return this; + } + + public SignatureBlobBuilder ReturnValueType(TargetPointer methodTablePtr) + { + _return = new ParamSpec(CorElementType.ValueType, methodTablePtr); + return this; + } + + public SignatureBlobBuilder Param(CorElementType primitive) + { + _params.Add(new ParamSpec(primitive, default)); + return this; + } + + public SignatureBlobBuilder ParamValueType(TargetPointer methodTablePtr) + { + _params.Add(new ParamSpec(CorElementType.ValueType, methodTablePtr)); + return this; + } + + public SignatureBlobBuilder ParamClass() + { + _params.Add(new ParamSpec(CorElementType.Class, default, true)); + return this; + } + + public byte[] Build() + { + using MemoryStream ms = new(); + using BinaryWriter bw = new(ms); + + byte callingConvention = (byte)((_hasThis ? HASTHIS_FLAG : 0) | (_isVarArg ? VARARG_CC : 0)); + bw.Write(callingConvention); + WriteCompressedUInt(bw, (uint)_params.Count); + WriteParam(bw, _return); + foreach (ParamSpec p in _params) + WriteParam(bw, p); + + return ms.ToArray(); + } + + private void WriteParam(BinaryWriter bw, ParamSpec p) + { + if (p.Type == CorElementType.ValueType && p.TypeHandle != TargetPointer.Null) + { + // Use ELEMENT_TYPE_INTERNAL + raw TypeHandle pointer to bypass metadata. + bw.Write(ELEMENT_TYPE_INTERNAL); + if (_pointerSize == 8) + bw.Write(p.TypeHandle.Value); + else + bw.Write((uint)p.TypeHandle.Value); + return; + } + if (p.IsDummyClassToken) + { + // ELEMENT_TYPE_CLASS followed by a TypeDefOrRefOrSpecEncoded token. + // We use TypeDef row 1 (the pseudo-type) encoded as (1 << 2 | 0) = 4. + bw.Write((byte)CorElementType.Class); + WriteCompressedUInt(bw, 4); // TypeDef row 1 + return; + } + bw.Write((byte)p.Type); + } + + private static void WriteCompressedUInt(BinaryWriter bw, uint value) + { + // ECMA-335 §II.23.2 compressed unsigned int + if (value < 0x80) + { + bw.Write((byte)value); + } + else if (value < 0x4000) + { + bw.Write((byte)((value >> 8) | 0x80)); + bw.Write((byte)(value & 0xFF)); + } + else + { + bw.Write((byte)((value >> 24) | 0xC0)); + bw.Write((byte)((value >> 16) & 0xFF)); + bw.Write((byte)((value >> 8) & 0xFF)); + bw.Write((byte)(value & 0xFF)); + } + } + + private readonly record struct ParamSpec(CorElementType Type, TargetPointer TypeHandle, bool IsDummyClassToken = false); +} diff --git a/src/native/managed/cdac/tests/CallingConvention/SyntheticVectorMetadata.cs b/src/native/managed/cdac/tests/CallingConvention/SyntheticVectorMetadata.cs new file mode 100644 index 00000000000000..10ac3b6e32293b --- /dev/null +++ b/src/native/managed/cdac/tests/CallingConvention/SyntheticVectorMetadata.cs @@ -0,0 +1,135 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Reflection; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; + +namespace Microsoft.Diagnostics.DataContractReader.Tests; + +internal sealed class SyntheticVectorMetadata +{ + private readonly MetadataReaderProvider _provider; + private readonly Dictionary _typeDefTokens; + + private SyntheticVectorMetadata(MetadataReaderProvider provider, Dictionary typeDefTokens) + { + _provider = provider; + _typeDefTokens = typeDefTokens; + } + + public MetadataReader Reader => _provider.GetMetadataReader(); + + public uint GetTypeDefToken(string typeName) => _typeDefTokens[typeName]; + + public static SyntheticVectorMetadata Create() + { + MetadataBuilder builder = new(); + builder.AddAssembly( + builder.GetOrAddString("SyntheticAsm"), + new Version(1, 0, 0, 0), + default, + default, + 0, + AssemblyHashAlgorithm.None); + builder.AddModule( + 0, + builder.GetOrAddString("SyntheticModule"), + builder.GetOrAddGuid(Guid.NewGuid()), + default, + default); + + builder.AddTypeDefinition( + TypeAttributes.NotPublic, + default, + builder.GetOrAddString(""), + default, + MetadataTokens.FieldDefinitionHandle(1), + MetadataTokens.MethodDefinitionHandle(1)); + + Dictionary typeDefTokens = new(StringComparer.Ordinal); + foreach ((string ns, string name) in new[] + { + ("System.Runtime.Intrinsics", "Vector64`1"), + ("System.Runtime.Intrinsics", "Vector128`1"), + ("System.Numerics", "Vector`1"), + }) + { + TypeDefinitionHandle handle = builder.AddTypeDefinition( + TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.SequentialLayout, + builder.GetOrAddString(ns), + builder.GetOrAddString(name), + default, + MetadataTokens.FieldDefinitionHandle(1), + MetadataTokens.MethodDefinitionHandle(1)); + typeDefTokens.Add(name, (uint)MetadataTokens.GetToken(handle)); + } + + BlobBuilder output = new(); + new MetadataRootBuilder(builder).Serialize(output, methodBodyStreamRva: 0, mappedFieldDataStreamRva: 0); + + MetadataReaderProvider provider = MetadataReaderProvider.FromMetadataImage(ImmutableArray.Create(output.ToArray())); + MetadataReader reader = provider.GetMetadataReader(); + + Debug.Assert(typeDefTokens.Count == 3); + foreach (TypeDefinitionHandle handle in reader.TypeDefinitions) + { + TypeDefinition typeDef = reader.GetTypeDefinition(handle); + string name = reader.GetString(typeDef.Name); + if (typeDefTokens.TryGetValue(name, out uint expectedToken)) + { + Debug.Assert(expectedToken == (uint)MetadataTokens.GetToken(handle)); + } + } + + return new SyntheticVectorMetadata(provider, typeDefTokens); + } + + public static SyntheticVectorMetadata CreateWithExtraType(string extraNamespace, string extraName) + { + MetadataBuilder builder = new(); + builder.AddAssembly( + builder.GetOrAddString("SyntheticAsm"), + new Version(1, 0, 0, 0), + default, default, 0, AssemblyHashAlgorithm.None); + builder.AddModule( + 0, + builder.GetOrAddString("SyntheticModule"), + builder.GetOrAddGuid(Guid.NewGuid()), + default, default); + builder.AddTypeDefinition( + TypeAttributes.NotPublic, default, + builder.GetOrAddString(""), + default, + MetadataTokens.FieldDefinitionHandle(1), + MetadataTokens.MethodDefinitionHandle(1)); + + Dictionary typeDefTokens = new(StringComparer.Ordinal); + foreach ((string ns, string name) in new[] + { + ("System.Runtime.Intrinsics", "Vector64`1"), + ("System.Runtime.Intrinsics", "Vector128`1"), + ("System.Numerics", "Vector`1"), + (extraNamespace, extraName), + }) + { + TypeDefinitionHandle handle = builder.AddTypeDefinition( + TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.SequentialLayout, + builder.GetOrAddString(ns), + builder.GetOrAddString(name), + default, + MetadataTokens.FieldDefinitionHandle(1), + MetadataTokens.MethodDefinitionHandle(1)); + typeDefTokens.Add(name, (uint)MetadataTokens.GetToken(handle)); + } + + BlobBuilder output = new(); + new MetadataRootBuilder(builder).Serialize(output, methodBodyStreamRva: 0, mappedFieldDataStreamRva: 0); + MetadataReaderProvider provider = MetadataReaderProvider.FromMetadataImage(ImmutableArray.Create(output.ToArray())); + return new SyntheticVectorMetadata(provider, typeDefTokens); + } +} diff --git a/src/native/managed/cdac/tests/DumpTests/CallSiteLayoutDumpTestsBase.cs b/src/native/managed/cdac/tests/DumpTests/CallSiteLayoutDumpTestsBase.cs new file mode 100644 index 00000000000000..489006c2ff4130 --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/CallSiteLayoutDumpTestsBase.cs @@ -0,0 +1,77 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.DumpTests; + +public abstract class CallSiteLayoutDumpTestsBase : DumpTestBase +{ + protected override string DebuggeeName => "CallSiteLayout"; + protected override string DumpType => "full"; + + // Deepest non-FailFast frame on the chain. Used to discover the + // FailFast thread; resolution then visits every frame above it. + private const string LeafFrame = "M_Combo_RefStructWithMultipleRefs"; + + protected Dictionary CollectChainMethods() + { + ThreadData thread = DumpTestHelpers.FindThreadWithMethod(Target, LeafFrame); + IStackWalk stackWalk = Target.Contracts.StackWalk; + IRuntimeTypeSystem rts = Target.Contracts.RuntimeTypeSystem; + + Dictionary result = new(); + foreach (IStackDataFrameHandle frame in stackWalk.CreateStackWalk(thread)) + { + TargetPointer mdPtr = stackWalk.GetMethodDescPtr(frame); + if (mdPtr == TargetPointer.Null) + continue; + MethodDescHandle md = rts.GetMethodDescHandle(mdPtr); + string? name = DumpTestHelpers.GetMethodName(Target, md); + if (name is not null && !result.ContainsKey(name)) + result[name] = md; + } + + return result; + } + + protected CallSiteLayout LayoutFor(string methodName) + { + Dictionary methods = CollectChainMethods(); + Assert.True(methods.TryGetValue(methodName, out MethodDescHandle md), + $"'{methodName}' frame not found on the FailFast thread"); + return Target.Contracts.CallingConvention.ComputeCallSiteLayout(md); + } + + // ===== Common assertion helpers ===== + + protected void AssertSingleByRef(string frame) + { + CallSiteLayout layout = LayoutFor(frame); + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.True(arg.IsPassedByRef, $"{frame}: expected IsPassedByRef=true"); + Assert.Null(arg.ValueTypeHandle); + } + + protected void AssertSingleByValueVT(string frame, string typeName) + { + CallSiteLayout layout = LayoutFor(frame); + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.False(arg.IsPassedByRef, $"{frame}: expected IsPassedByRef=false"); + Assert.NotNull(arg.ValueTypeHandle); + Assert.Equal(typeName, DumpTestHelpers.GetTypeName(Target, arg.ValueTypeHandle.Value)); + } + + protected void AssertSingleManagedRef(string frame) + { + CallSiteLayout layout = LayoutFor(frame); + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.False(arg.IsPassedByRef, $"{frame}: expected IsPassedByRef=false"); + Assert.Null(arg.ValueTypeHandle); + } +} diff --git a/src/native/managed/cdac/tests/DumpTests/CallSiteLayoutDumpTests_WinX64.cs b/src/native/managed/cdac/tests/DumpTests/CallSiteLayoutDumpTests_WinX64.cs new file mode 100644 index 00000000000000..e5b26b38858edf --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/CallSiteLayoutDumpTests_WinX64.cs @@ -0,0 +1,632 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.DumpTests; + +public class CallSiteLayoutDumpTests_WinX64 : CallSiteLayoutDumpTestsBase +{ + // ===== Category A: register-bank fill / spill (no GC refs) ===== + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void Empty_NoArguments(TestConfiguration config) + { + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_Empty"); + Assert.Null(layout.ThisOffset); + Assert.Null(layout.VarArgCookieOffset); + Assert.Empty(layout.Arguments); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void IntSix_AllFitsInRegisterSlots(TestConfiguration config) + { + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_Int_Six"); + Assert.Equal(6, layout.Arguments.Count); + int prev = int.MinValue; + foreach (ArgLayout a in layout.Arguments) + { + Assert.False(a.IsPassedByRef); + Assert.Null(a.ValueTypeHandle); + Assert.Single(a.Slots); + Assert.Equal(CorElementType.I4, a.Slots[0].ElementType); + Assert.True(a.Slots[0].Offset > prev); + prev = a.Slots[0].Offset; + } + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void IntNine_SpillsArgsBeyondRegisters(TestConfiguration config) + { + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_Int_Nine"); + Assert.Equal(9, layout.Arguments.Count); + // Win-x64 has 4 argument registers; args 5-9 spill to stack at +8 strides. + Assert.True(layout.Arguments[4].Slots[0].Offset >= layout.Arguments[3].Slots[0].Offset + 8); + Assert.True(layout.Arguments[8].Slots[0].Offset >= layout.Arguments[4].Slots[0].Offset + 32); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void DoubleFour_FloatsUseFpSlots(TestConfiguration config) + { + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_Double_Four"); + Assert.Equal(4, layout.Arguments.Count); + foreach (ArgLayout a in layout.Arguments) + { + Assert.False(a.IsPassedByRef); + Assert.Null(a.ValueTypeHandle); + Assert.Equal(CorElementType.R8, a.Slots[0].ElementType); + } + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void MixedID_AlternatingIntDouble(TestConfiguration config) + { + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_MixedID"); + Assert.Equal(6, layout.Arguments.Count); + Assert.Equal(CorElementType.I4, layout.Arguments[0].Slots[0].ElementType); + Assert.Equal(CorElementType.R8, layout.Arguments[1].Slots[0].ElementType); + Assert.Equal(CorElementType.I4, layout.Arguments[2].Slots[0].ElementType); + Assert.Equal(CorElementType.R8, layout.Arguments[3].Slots[0].ElementType); + } + + // ===== Category B: reference-typed args ===== + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void RefArgs_String_NoValueTypeHandle(TestConfiguration config) + { + InitializeDumpTest(config); + AssertSingleManagedRef("M_RefArgs_String"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void RefArgs_Object_NoValueTypeHandle(TestConfiguration config) + { + InitializeDumpTest(config); + AssertSingleManagedRef("M_RefArgs_Object"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void RefArgs_SzArray_NoValueTypeHandle(TestConfiguration config) + { + InitializeDumpTest(config); + AssertSingleManagedRef("M_RefArgs_SzArray"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void RefArgs_MdArray_NoValueTypeHandle(TestConfiguration config) + { + InitializeDumpTest(config); + AssertSingleManagedRef("M_RefArgs_MdArray"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void RefArgs_RefInt_IsByRef(TestConfiguration config) + { + InitializeDumpTest(config); + AssertSingleByRef("M_RefArgs_RefInt"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void RefArgs_OutObject_IsByRef(TestConfiguration config) + { + InitializeDumpTest(config); + AssertSingleByRef("M_RefArgs_OutObject"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void RefArgs_RefStruct_IsByRef(TestConfiguration config) + { + InitializeDumpTest(config); + AssertSingleByRef("M_RefArgs_RefStruct"); + } + + // ===== Category C: small by-value structs ===== + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void VT_Byte_EnregisteredByValue(TestConfiguration config) + { + InitializeDumpTest(config); + AssertSingleByValueVT("M_VT_Byte", "ByteStruct"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void VT_Short_EnregisteredByValue(TestConfiguration config) + { + InitializeDumpTest(config); + AssertSingleByValueVT("M_VT_Short", "ShortStruct"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void VT_Int_EnregisteredByValue(TestConfiguration config) + { + InitializeDumpTest(config); + AssertSingleByValueVT("M_VT_Int", "IntStruct"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void VT_TwoInts_EnregisteredByValue(TestConfiguration config) + { + InitializeDumpTest(config); + AssertSingleByValueVT("M_VT_TwoInts", "TwoInts"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void VT_ObjectOnly_PopulatesValueTypeHandle(TestConfiguration config) + { + // 8-byte struct containing a single object ref. This is the canonical + // Win-x64 case where the GCDesc walk is needed at scan time. + InitializeDumpTest(config); + AssertSingleByValueVT("M_VT_ObjectOnly", "ObjectStruct"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void VT_StringOnly_PopulatesValueTypeHandle(TestConfiguration config) + { + InitializeDumpTest(config); + AssertSingleByValueVT("M_VT_StringOnly", "StringStruct"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void VT_ThreeByte_AbiByRef(TestConfiguration config) + { + // 3 bytes -> non-power-of-two -> Win-x64 passes by implicit reference. + InitializeDumpTest(config); + AssertSingleByRef("M_VT_ThreeByte"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void VT_FiveByte_AbiByRef(TestConfiguration config) + { + InitializeDumpTest(config); + AssertSingleByRef("M_VT_FiveByte"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void VT_Twelve_AbiByRef(TestConfiguration config) + { + InitializeDumpTest(config); + AssertSingleByRef("M_VT_Twelve"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void VT_Guid_AbiByRef(TestConfiguration config) + { + // 16 bytes -> Win-x64 passes by implicit reference. + InitializeDumpTest(config); + AssertSingleByRef("M_VT_Guid"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void VT_Decimal_AbiByRef(TestConfiguration config) + { + InitializeDumpTest(config); + AssertSingleByRef("M_VT_Decimal"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void VT_KvpStrStr_AbiByRef(TestConfiguration config) + { + InitializeDumpTest(config); + AssertSingleByRef("M_VT_KvpStrStr"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void VT_KvpStrInt_AbiByRef(TestConfiguration config) + { + // 16 bytes (ref + 4 padded to 8). Exercises the nested + // GetGenericInstantiation path resolving both ref- and primitive- + // typed type args at one level of nesting. + InitializeDumpTest(config); + AssertSingleByRef("M_VT_KvpStrInt"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void VT_TwoFloats_EnregisteredByValue(TestConfiguration config) + { + // 8-byte HFA candidate. Win-x64 does NOT enregister HFAs as scalars; + // the whole struct travels in one slot by value. + InitializeDumpTest(config); + AssertSingleByValueVT("M_VT_TwoFloats", "TwoFloats"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void VT_TwoDoubles_AbiByRef(TestConfiguration config) + { + // 16-byte HFA candidate -> Win-x64 implicit-byref (no HFA enregistration). + InitializeDumpTest(config); + AssertSingleByRef("M_VT_TwoDoubles"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void VT_IntDouble_AbiByRef(TestConfiguration config) + { + // 16 bytes (4-byte int + 4 padding + 8-byte double) -> implicit-byref. + InitializeDumpTest(config); + AssertSingleByRef("M_VT_IntDouble"); + } + + // ===== Category D: large stack-passed structs ===== + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void VT_Big24_AbiByRef(TestConfiguration config) + { + InitializeDumpTest(config); + AssertSingleByRef("M_VT_Big24"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void VT_Big24WithRef_AbiByRef(TestConfiguration config) + { + // SysV would pass this on-stack by value and require a GCDesc walk; on + // Win-x64 it is implicit-byref so no in-arg GC scanning is needed -- + // the caller's stack temp holds the live ref and is covered by the + // caller's GC info. + InitializeDumpTest(config); + AssertSingleByRef("M_VT_Big24WithRef"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void VT_Big48WithTwoRefs_AbiByRef(TestConfiguration config) + { + InitializeDumpTest(config); + AssertSingleByRef("M_VT_Big48WithTwoRefs"); + } + + // ===== Category E: ByRefLike ===== + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void BRL_SpanInt_AbiByRef(TestConfiguration config) + { + // Span is 16 bytes -> Win-x64 implicit-byref. The IsByRef short + // circuit fires before the ByRefLike dispatch can populate + // ValueTypeHandle, so VTH must be null here. + InitializeDumpTest(config); + AssertSingleByRef("M_BRL_SpanInt"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void BRL_SpanObject_AbiByRef(TestConfiguration config) + { + InitializeDumpTest(config); + AssertSingleByRef("M_BRL_SpanObject"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void BRL_SmallRefStruct_ByValueRefersToByRefLike(TestConfiguration config) + { + // Pointer-sized ref struct holding a single managed byref. Passed by + // value on Win-x64 (1 slot, 8 bytes). The producer keeps the + // ValueTypeHandle so the consumer can dispatch to the ByRefLike walker + // and emit the inner byref field as an INTERIOR root. + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_BRL_SmallRefStruct"); + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.False(arg.IsPassedByRef); + Assert.NotNull(arg.ValueTypeHandle); + Assert.Equal("SmallRefStruct", DumpTestHelpers.GetTypeName(Target, arg.ValueTypeHandle.Value)); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void BRL_RefStructWithObject_AbiByRef(TestConfiguration config) + { + // RefStructWithObject is { object Obj; int Prim; } -> 16 bytes on x64. + // Implicit-byref on Win-x64; the caller's stack temp holds the live + // object so cross-frame GC scanning relies on the caller's reporting. + InitializeDumpTest(config); + AssertSingleByRef("M_BRL_RefStructWithObject"); + } + + // ===== Category F: special slots ===== + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void Special_Instance_PopulatesThisOffset(TestConfiguration config) + { + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_Special_Instance"); + Assert.NotNull(layout.ThisOffset); + Assert.False(layout.IsValueTypeThis); + Assert.Equal(2, layout.Arguments.Count); + Assert.True(layout.Arguments[0].Slots[0].Offset > layout.ThisOffset!.Value); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void Special_GenericRef_TypeArgIsManagedRef(TestConfiguration config) + { + // Generic method instantiated over `string`. The single fixed arg is a + // managed object reference -- no VTH, not byref. + InitializeDumpTest(config); + AssertSingleManagedRef("M_Special_GenericRef"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void Special_GenericVal_TypeArgIsPrimitive(TestConfiguration config) + { + // Generic method instantiated over `int` -- single primitive arg, no VTH. + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_Special_GenericVal"); + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.False(arg.IsPassedByRef); + Assert.Null(arg.ValueTypeHandle); + Assert.Equal(CorElementType.I4, arg.Slots[0].ElementType); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void Special_Varargs_HasVarArgCookieAndFixedArg(TestConfiguration config) + { + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_Special_Varargs"); + Assert.NotNull(layout.VarArgCookieOffset); + // Only the fixed `int` is enumerated; vararg extras are not in Arguments. + Assert.Single(layout.Arguments); + ArgLayout fixedArg = layout.Arguments[0]; + Assert.False(fixedArg.IsPassedByRef); + Assert.Null(fixedArg.ValueTypeHandle); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void Special_NoVarargs_NoCookie(TestConfiguration config) + { + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_Special_NoVarargs"); + Assert.Null(layout.VarArgCookieOffset); + Assert.Equal(2, layout.Arguments.Count); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void Special_InstanceGenericClassGenericMethod_PopulatesThisOffset(TestConfiguration config) + { + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_Special_InstanceGenericClassGenericMethod"); + Assert.NotNull(layout.ThisOffset); + Assert.False(layout.IsValueTypeThis); + Assert.Equal(2, layout.Arguments.Count); + } + + // ===== Category G: vectors ===== + // Vector{256,512} are runtime-supported but the IsSupported gate in the + // debuggee may bypass the call; the frames are still on the chain because + // their parents pass Vector{N}.Zero unconditionally. + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void Vec_64_EnregisteredByValue(TestConfiguration config) + { + // Vector64 is 8 bytes. Win-x64 passes 8-byte structs by value. + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_Vec_64"); + Assert.Single(layout.Arguments); + Assert.False(layout.Arguments[0].IsPassedByRef); + Assert.NotNull(layout.Arguments[0].ValueTypeHandle); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void Vec_128_AbiByRef(TestConfiguration config) + { + // Vector128 is 16 bytes -> Win-x64 implicit-byref. + InitializeDumpTest(config); + AssertSingleByRef("M_Vec_128"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void Vec_256_AbiByRef(TestConfiguration config) + { + // Vector256 is 32 bytes -> Win-x64 implicit-byref. + InitializeDumpTest(config); + AssertSingleByRef("M_Vec_256"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void Vec_512_AbiByRef(TestConfiguration config) + { + // Vector512 is 64 bytes -> Win-x64 implicit-byref. + InitializeDumpTest(config); + AssertSingleByRef("M_Vec_512"); + } + + // ===== Category H: composite frames ===== + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void Combo_RegBankExhaustion_AllArgsAccounted(TestConfiguration config) + { + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_Combo_RegBankExhaustion"); + Assert.Equal(15, layout.Arguments.Count); + // First six pairs are int/double scalars - check the trailing 3 are object/string/array. + for (int i = 12; i < 15; i++) + { + Assert.False(layout.Arguments[i].IsPassedByRef); + Assert.Null(layout.Arguments[i].ValueTypeHandle); + } + } +} diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/CallSiteLayout/CallSiteLayout.csproj b/src/native/managed/cdac/tests/DumpTests/Debuggees/CallSiteLayout/CallSiteLayout.csproj new file mode 100644 index 00000000000000..44695cda85ecc4 --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/CallSiteLayout/CallSiteLayout.csproj @@ -0,0 +1,17 @@ + + + + Full + + Jit + + true + + diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/CallSiteLayout/Program.cs b/src/native/managed/cdac/tests/DumpTests/Debuggees/CallSiteLayout/Program.cs new file mode 100644 index 00000000000000..380acf93896dbb --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/CallSiteLayout/Program.cs @@ -0,0 +1,557 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +internal static class Program +{ + private static void Main() + { + M_Empty(); + } + + // ===== Category A: register-bank fill / spill, no GC refs ===== + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_Empty() + { + M_Int_Six(1, 2, 3, 4, 5, 6); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_Int_Six(int a, int b, int c, int d, int e, int f) + { + GC.KeepAlive(a + b + c + d + e + f); + M_Int_Nine(1, 2, 3, 4, 5, 6, 7, 8, 9); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_Int_Nine(int a, int b, int c, int d, int e, int f, int g, int h, int i) + { + GC.KeepAlive(a + b + c + d + e + f + g + h + i); + M_Double_Four(1.5, 2.5, 3.5, 4.5); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_Double_Four(double a, double b, double c, double d) + { + GC.KeepAlive(a + b + c + d); + M_Double_Nine(1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_Double_Nine(double a, double b, double c, double d, double e, double f, double g, double h, double i) + { + GC.KeepAlive(a + b + c + d + e + f + g + h + i); + M_MixedID(1, 1.5, 2, 2.5, 3, 3.5); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_MixedID(int a, double b, int c, double d, int e, double f) + { + GC.KeepAlive(a + c + e); + GC.KeepAlive(b + d + f); + M_RefArgs_String("cDAC-CSL-String"); + } + + // ===== Category B: reference-typed args ===== + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_RefArgs_String(string s) + { + GC.KeepAlive(s); + M_RefArgs_Object(new object()); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_RefArgs_Object(object o) + { + GC.KeepAlive(o); + M_RefArgs_SzArray(_intArr); + } + + private static readonly int[] _intArr = [1, 2, 3]; + private static readonly int[,] _intMdArr = new int[2, 2] { { 1, 2 }, { 3, 4 } }; + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_RefArgs_SzArray(int[] a) + { + GC.KeepAlive(a); + M_RefArgs_MdArray(_intMdArr); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_RefArgs_MdArray(int[,] a) + { + GC.KeepAlive(a); + int local = 42; + M_RefArgs_RefInt(ref local); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_RefArgs_RefInt(ref int x) + { + x++; + M_RefArgs_OutObject(out object obj); + GC.KeepAlive(obj); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_RefArgs_OutObject(out object o) + { + o = new object(); + Guid g = new Guid("11111111-2222-3333-4444-555555555555"); + M_RefArgs_RefStruct(ref g); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_RefArgs_RefStruct(ref Guid g) + { + GC.KeepAlive(g); + M_VT_Byte(new ByteStruct { X = 0xAB }); + } + + // ===== Category C: small by-value structs ===== + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_VT_Byte(ByteStruct s) + { + GC.KeepAlive(s.X); + M_VT_Short(new ShortStruct { X = 0x1234 }); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_VT_Short(ShortStruct s) + { + GC.KeepAlive(s.X); + M_VT_Int(new IntStruct { X = unchecked((int)0xDEADBEEF) }); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_VT_Int(IntStruct s) + { + GC.KeepAlive(s.X); + M_VT_TwoInts(new TwoInts { X = 1, Y = 2 }); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_VT_TwoInts(TwoInts s) + { + GC.KeepAlive(s.X + s.Y); + M_VT_ObjectOnly(new ObjectStruct { Ref = new object() }); + } + + // 8-byte by-value struct holding one object ref -- the canonical Win-x64 GCDesc-walk case. + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_VT_ObjectOnly(ObjectStruct s) + { + GC.KeepAlive(s.Ref); + M_VT_StringOnly(new StringStruct { Ref = "cDAC-CSL-StringInStruct" }); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_VT_StringOnly(StringStruct s) + { + GC.KeepAlive(s.Ref); + M_VT_ThreeByte(new ThreeByteStruct { A = 1, B = 2, C = 3 }); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_VT_ThreeByte(ThreeByteStruct s) + { + GC.KeepAlive(s.A + s.B + s.C); + M_VT_FiveByte(new FiveByteStruct { I = 0x01020304, B = 5 }); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_VT_FiveByte(FiveByteStruct s) + { + GC.KeepAlive(s.I + s.B); + M_VT_Twelve((1, 2, 3)); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_VT_Twelve((int X, int Y, int Z) v) + { + GC.KeepAlive(v.X + v.Y + v.Z); + M_VT_Guid(new Guid("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee")); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_VT_Guid(Guid g) + { + GC.KeepAlive(g); + M_VT_Decimal(123.456m); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_VT_Decimal(decimal d) + { + GC.KeepAlive(d); + M_VT_KvpStrStr(new KeyValuePair("k", "v")); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_VT_KvpStrStr(KeyValuePair kvp) + { + GC.KeepAlive(kvp.Key); + GC.KeepAlive(kvp.Value); + M_VT_KvpStrInt(new KeyValuePair("k2", 99)); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_VT_KvpStrInt(KeyValuePair kvp) + { + GC.KeepAlive(kvp.Key); + GC.KeepAlive(kvp.Value); + M_VT_TwoFloats(new TwoFloats { X = 1.5f, Y = 2.5f }); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_VT_TwoFloats(TwoFloats s) + { + GC.KeepAlive(s.X + s.Y); + M_VT_TwoDoubles(new TwoDoubles { X = 1.5, Y = 2.5 }); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_VT_TwoDoubles(TwoDoubles s) + { + GC.KeepAlive(s.X + s.Y); + M_VT_IntDouble(new IntDoubleStruct { I = 7, D = 8.5 }); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_VT_IntDouble(IntDoubleStruct s) + { + GC.KeepAlive(s.I); + GC.KeepAlive(s.D); + M_VT_Empty(default); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_VT_Empty(EmptyStruct _) + { + M_VT_Big24(new Big24NoRef { A = 1, B = 2, C = 3 }); + } + + // ===== Category D: large stack-passed structs (>16) ===== + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_VT_Big24(Big24NoRef s) + { + GC.KeepAlive(s.A + s.B + s.C); + M_VT_Big24WithRef(new Big24WithRef { Prefix = 0x11, Ref = new object(), Suffix = 0x22 }); + } + + // 24-byte struct with object ref at offset 8 -- the canonical SysV GCDesc-walk case. + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_VT_Big24WithRef(Big24WithRef s) + { + GC.KeepAlive(s.Prefix); + GC.KeepAlive(s.Ref); + GC.KeepAlive(s.Suffix); + M_VT_Big48WithTwoRefs(new Big48TwoRefs { A = 1, R1 = "r1", B = 2, R2 = "r2", C = 3 }); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_VT_Big48WithTwoRefs(Big48TwoRefs s) + { + GC.KeepAlive(s.A + s.B + s.C); + GC.KeepAlive(s.R1); + GC.KeepAlive(s.R2); + M_BRL_SpanInt(_spanIntBuffer.AsSpan()); + } + + // ===== Category E: ByRefLike ===== + + private static readonly int[] _spanIntBuffer = [10, 20, 30]; + private static readonly object[] _spanObjBuffer = [new object(), new object()]; + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_BRL_SpanInt(Span s) + { + s[0]++; + M_BRL_SpanObject(_spanObjBuffer.AsSpan()); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_BRL_SpanObject(Span s) + { + GC.KeepAlive(s[0]); + int local = 100; + M_BRL_SmallRefStruct(new SmallRefStruct(ref local)); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_BRL_SmallRefStruct(SmallRefStruct t) + { + t.Bump(); + M_BRL_RefStructWithObject(new RefStructWithObject { Obj = new object(), Prim = 7 }); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_BRL_RefStructWithObject(RefStructWithObject r) + { + GC.KeepAlive(r.Obj); + GC.KeepAlive(r.Prim); + new InstanceCallee().M_Special_Instance(11, 22); + } + + // ===== Category F: special arg slots ===== + + [MethodImpl(MethodImplOptions.NoInlining)] + internal static Big32Struct M_Special_RetBufSmall(int seed) + { + Big32Struct ret = default; + ret.A = seed; + ret.B = seed + 1; + ret.C = seed + 2; + ret.D = seed + 3; + // Continue chain inside the retbuf method so the frame is live. + M_Special_RetBufLargeWithDouble(1.5); + return ret; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + internal static Big64Struct M_Special_RetBufLargeWithDouble(double firstUserArg) + { + Big64Struct ret = default; + ret.A = (long)firstUserArg; + ret.B = ret.C = ret.D = ret.E = ret.F = ret.G = ret.H = 0; + M_Special_GenericRef("cDAC-generic-ref"); + return ret; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_Special_GenericRef(T x) where T : class + { + GC.KeepAlive(x); + M_Special_GenericVal(42); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_Special_GenericVal(T x) where T : struct + { + GC.KeepAlive(x); + M_Special_Varargs(1, __arglist(42, 43)); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_Special_Varargs(int fixedArg, __arglist) + { + GC.KeepAlive(fixedArg); + M_Special_NoVarargs(1, 2); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_Special_NoVarargs(int a, int b) + { + GC.KeepAlive(a + b); + new GenericContainer().M_Special_InstanceGenericClassGenericMethod("a", 7); + } + + // ===== Category G: vectors ===== + + [MethodImpl(MethodImplOptions.NoInlining)] + internal static void M_Vec_64(Vector64 v) + { + GC.KeepAlive(v); + M_Vec_128(Vector128.Zero); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + internal static void M_Vec_128(Vector128 v) + { + GC.KeepAlive(v); + M_Vec_256(Vector256.Zero); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + internal static void M_Vec_256(Vector256 v) + { + GC.KeepAlive(v); + M_Vec_512(Vector512.Zero); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + internal static void M_Vec_512(Vector512 v) + { + GC.KeepAlive(v); + M_Combo_InstanceManyArgs(new ComboReceiver(), 1, new object(), + new KeyValuePair("c", 2), _spanObjBuffer.AsSpan(), 5); + } + + // ===== Category H: composites ===== + + [MethodImpl(MethodImplOptions.NoInlining)] + internal static void M_Combo_InstanceManyArgs(ComboReceiver r, int ix, object o, + KeyValuePair kvp, Span span, int tail) + { + r.M_Combo_InstanceMethodRun(ix, o, kvp, span, tail); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + internal static Big32Struct M_Combo_VarargsRetbuf(int seed, __arglist) + { + Big32Struct ret = default; + ret.A = seed; + M_Combo_RegBankExhaustion( + 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5, 5.5, 6, 6.5, + new object(), "tail-string", _comboTailArr); + return ret; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + internal static void M_Combo_RegBankExhaustion( + int i1, double d1, int i2, double d2, int i3, double d3, + int i4, double d4, int i5, double d5, int i6, double d6, + object o, string s, int[] a) + { + GC.KeepAlive(i1 + i2 + i3 + i4 + i5 + i6); + GC.KeepAlive(d1 + d2 + d3 + d4 + d5 + d6); + GC.KeepAlive(o); + GC.KeepAlive(s); + GC.KeepAlive(a); + M_Combo_DeepNestedGeneric(new KeyValuePair, int>( + new KeyValuePair("nested", Guid.Empty), 99)); + } + + private static readonly int[] _comboTailArr = [1]; + + [MethodImpl(MethodImplOptions.NoInlining)] + internal static void M_Combo_DeepNestedGeneric(KeyValuePair, int> v) + { + GC.KeepAlive(v.Key.Key); + GC.KeepAlive(v.Key.Value); + GC.KeepAlive(v.Value); + int local = 0; + M_Combo_RefStructWithMultipleRefs(new MultiRefStruct(ref local, new object(), "s")); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + internal static void M_Combo_RefStructWithMultipleRefs(MultiRefStruct mrs) + { + mrs.Touch(); + Environment.FailFast("cDAC dump test: CallSiteLayout intentional crash"); + } +} + +// ===== Instance / generic receiver classes ===== + +internal sealed class InstanceCallee +{ + private int _seed = 7; + + [MethodImpl(MethodImplOptions.NoInlining)] + public void M_Special_Instance(int x, int y) + { + GC.KeepAlive(_seed + x + y); + Program.M_Special_RetBufSmall(3); + } +} + +internal sealed class GenericContainer +{ + private int _seed = 13; + + [MethodImpl(MethodImplOptions.NoInlining)] + public void M_Special_InstanceGenericClassGenericMethod(T classT, U methodU) + { + GC.KeepAlive(_seed); + GC.KeepAlive(classT); + GC.KeepAlive(methodU); + Program.M_Vec_64(Vector64.Zero); + } +} + +internal sealed class ComboReceiver +{ + private long _bias = 0x10000; + + [MethodImpl(MethodImplOptions.NoInlining)] + public void M_Combo_InstanceMethodRun(int ix, object o, KeyValuePair kvp, Span span, int tail) + { + GC.KeepAlive(_bias + ix + tail); + GC.KeepAlive(o); + GC.KeepAlive(kvp.Key); + GC.KeepAlive(span[0]); + Program.M_Combo_VarargsRetbuf(123, __arglist("a", 1.5)); + } +} + +// ===== Type definitions ===== + +internal struct EmptyStruct { } + +internal struct ByteStruct { public byte X; } +internal struct ShortStruct { public short X; } +internal struct IntStruct { public int X; } +internal struct TwoInts { public int X; public int Y; } +internal struct ObjectStruct { public object Ref; } +internal struct StringStruct { public string Ref; } + +internal struct ThreeByteStruct { public byte A; public byte B; public byte C; } + +[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential, Pack = 1)] +internal struct FiveByteStruct { public int I; public byte B; } + +internal struct TwoFloats { public float X; public float Y; } +internal struct TwoDoubles { public double X; public double Y; } +internal struct IntDoubleStruct { public int I; public double D; } + +internal struct Big24NoRef { public long A; public long B; public long C; } +internal struct Big24WithRef { public long Prefix; public object Ref; public long Suffix; } +internal struct Big48TwoRefs +{ + public long A; public string R1; public long B; public string R2; public long C; +} + +internal struct Big32Struct { public long A; public long B; public long C; public long D; } +internal struct Big64Struct +{ + public long A; public long B; public long C; public long D; + public long E; public long F; public long G; public long H; +} + +internal ref struct SmallRefStruct +{ + public ref int Field; + + public SmallRefStruct(ref int r) + { + Field = ref r; + } + + public void Bump() => Field++; +} + +internal ref struct RefStructWithObject +{ + public object Obj; + public int Prim; +} + +internal ref struct MultiRefStruct +{ + public ref int Local; + public object Obj; + public string Str; + + public MultiRefStruct(ref int local, object obj, string str) + { + Local = ref local; + Obj = obj; + Str = str; + } + + public void Touch() + { + Local++; + GC.KeepAlive(Obj); + GC.KeepAlive(Str); + } +} diff --git a/src/native/managed/cdac/tests/DumpTests/DumpTestBase.cs b/src/native/managed/cdac/tests/DumpTests/DumpTestBase.cs index 4c0aaa34cb508c..e4284924d97a0a 100644 --- a/src/native/managed/cdac/tests/DumpTests/DumpTestBase.cs +++ b/src/native/managed/cdac/tests/DumpTests/DumpTestBase.cs @@ -190,8 +190,16 @@ private void EvaluateSkipAttributes(TestConfiguration config, string callerName, foreach (SkipOnArchAttribute attr in method.GetCustomAttributes()) { - if (string.Equals(attr.Arch, _dumpInfo.Arch, StringComparison.OrdinalIgnoreCase)) - throw new SkipTestException($"[{_dumpInfo.Arch}] {attr.Reason}"); + if (attr.IncludeOnly is not null) + { + if (!string.Equals(attr.IncludeOnly, _dumpInfo.Arch, StringComparison.OrdinalIgnoreCase)) + throw new SkipTestException($"[{_dumpInfo.Arch}] {attr.Reason}"); + } + else if (attr.Arch is not null) + { + if (string.Equals(attr.Arch, _dumpInfo.Arch, StringComparison.OrdinalIgnoreCase)) + throw new SkipTestException($"[{_dumpInfo.Arch}] {attr.Reason}"); + } } } } diff --git a/src/native/managed/cdac/tests/DumpTests/DumpTestHelpers.cs b/src/native/managed/cdac/tests/DumpTests/DumpTestHelpers.cs index e1d57de304c703..9f4a89b8931727 100644 --- a/src/native/managed/cdac/tests/DumpTests/DumpTestHelpers.cs +++ b/src/native/managed/cdac/tests/DumpTests/DumpTestHelpers.cs @@ -13,6 +13,24 @@ namespace Microsoft.Diagnostics.DataContractReader.DumpTests; /// internal static class DumpTestHelpers { + public static string? GetTypeName(ContractDescriptorTarget target, TypeHandle typeHandle) + { + IRuntimeTypeSystem rts = target.Contracts.RuntimeTypeSystem; + uint token = rts.GetTypeDefToken(typeHandle); + TargetPointer modulePtr = rts.GetModule(typeHandle); + + ILoader loader = target.Contracts.Loader; + ModuleHandle moduleHandle = loader.GetModuleHandleFromModulePtr(modulePtr); + + IEcmaMetadata ecmaMetadata = target.Contracts.EcmaMetadata; + MetadataReader? reader = ecmaMetadata.GetMetadata(moduleHandle); + if (reader is null) + return null; + + TypeDefinitionHandle typeDef = MetadataTokens.TypeDefinitionHandle((int)(token & 0x00FFFFFF)); + return reader.GetString(reader.GetTypeDefinition(typeDef).Name); + } + /// /// Resolves the method name for a using the /// RuntimeTypeSystem, Loader, and EcmaMetadata contracts. Returns null diff --git a/src/native/managed/cdac/tests/DumpTests/RunDumpTests.ps1 b/src/native/managed/cdac/tests/DumpTests/RunDumpTests.ps1 index 05a22da4f03d0d..71fb6c993a8ea4 100644 --- a/src/native/managed/cdac/tests/DumpTests/RunDumpTests.ps1 +++ b/src/native/managed/cdac/tests/DumpTests/RunDumpTests.ps1 @@ -93,6 +93,8 @@ param( [string]$TestHostConfiguration = "Release", + [string]$RuntimeConfiguration = "", + [string]$Filter = "", [string]$DumpArchive = "", @@ -327,6 +329,10 @@ if ($Action -in @("dumps", "all")) { "/v:$msbuildVerbosity" ) + if ($RuntimeConfiguration) { + $msbuildArgs += "/p:RuntimeConfiguration=$RuntimeConfiguration" + } + if ($SetSignatureCheck) { $msbuildArgs += "/p:SetDisableAuxProviderSignatureCheck=true" } diff --git a/src/native/managed/cdac/tests/DumpTests/SkipOnArchAttribute.cs b/src/native/managed/cdac/tests/DumpTests/SkipOnArchAttribute.cs index 86a28c2fd27707..1d393a4b6183c8 100644 --- a/src/native/managed/cdac/tests/DumpTests/SkipOnArchAttribute.cs +++ b/src/native/managed/cdac/tests/DumpTests/SkipOnArchAttribute.cs @@ -14,12 +14,19 @@ namespace Microsoft.Diagnostics.DataContractReader.DumpTests; [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] public sealed class SkipOnArchAttribute : Attribute { - public string Arch { get; } - public string Reason { get; } + public string? Arch { get; } + + public string? IncludeOnly { get; set; } + + public string Reason { get; set; } = string.Empty; public SkipOnArchAttribute(string arch, string reason) { Arch = arch; Reason = reason; } + + public SkipOnArchAttribute() + { + } } diff --git a/src/native/managed/cdac/tests/GcRefEnumerationTests.cs b/src/native/managed/cdac/tests/GcRefEnumerationTests.cs new file mode 100644 index 00000000000000..00f547239bdc7e --- /dev/null +++ b/src/native/managed/cdac/tests/GcRefEnumerationTests.cs @@ -0,0 +1,151 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; +using Moq; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.Tests; + +public class GcRefEnumerationTests +{ + private static IRuntimeTypeSystem MockRtsWithSeries(params (uint Offset, uint Size)[] series) + { + Mock mock = new(MockBehavior.Strict); + mock.Setup(r => r.GetGCDescSeries(It.IsAny(), 0u)) + .Returns(series); + return mock.Object; + } + + private static TypeHandle MakeHandle(ulong address) + => new TypeHandle(new TargetPointer(address)); + + // ===== empty input ===== + + [Fact] + public void EmptySeries_YieldsNoRefs() + { + IRuntimeTypeSystem rts = MockRtsWithSeries(); + TargetPointer[] refs = GcRefEnumeration.EnumerateValueTypeRefs( + rts, MakeHandle(0x1000), new TargetPointer(0x2000), pointerSize: 8).ToArray(); + Assert.Empty(refs); + } + + // ===== single series, one ref ===== + + [Fact] + public void SingleSeriesOneRef_x64_EmitsOneAddressWithBoxedToUnboxedAdjustment() + { + // A struct laid out as { /* MT* at 0..7 */ ; objref at boxed offset 8 } has + // a single GCDesc series of (Offset=8, Size=8) on x64. Unboxed the same field + // sits at offset 0, so the emitted address is exactly baseAddress. + IRuntimeTypeSystem rts = MockRtsWithSeries((Offset: 8, Size: 8)); + TargetPointer[] refs = GcRefEnumeration.EnumerateValueTypeRefs( + rts, MakeHandle(0x1000), new TargetPointer(0x2000), pointerSize: 8).ToArray(); + Assert.Single(refs); + Assert.Equal(0x2000ul, refs[0].Value); + } + + [Fact] + public void SingleSeriesOneRef_x86_EmitsOneAddressWithBoxedToUnboxedAdjustment() + { + // Same shape on a 32-bit target: pointerSize=4 means MT* is 4 bytes, + // boxed offset 4 -> unboxed offset 0. + IRuntimeTypeSystem rts = MockRtsWithSeries((Offset: 4, Size: 4)); + TargetPointer[] refs = GcRefEnumeration.EnumerateValueTypeRefs( + rts, MakeHandle(0x1000), new TargetPointer(0x2000), pointerSize: 4).ToArray(); + Assert.Single(refs); + Assert.Equal(0x2000ul, refs[0].Value); + } + + // ===== single series, multiple adjacent refs ===== + + [Fact] + public void SingleSeriesTwoAdjacentRefs_x64_EmitsTwoConsecutiveAddresses() + { + // A struct with two adjacent objref fields at boxed offsets 8 and 16 produces + // a single series (Offset=8, Size=16). Unboxed offsets are 0 and 8. + IRuntimeTypeSystem rts = MockRtsWithSeries((Offset: 8, Size: 16)); + TargetPointer[] refs = GcRefEnumeration.EnumerateValueTypeRefs( + rts, MakeHandle(0x1000), new TargetPointer(0x2000), pointerSize: 8).ToArray(); + Assert.Equal(2, refs.Length); + Assert.Equal(0x2000ul, refs[0].Value); + Assert.Equal(0x2008ul, refs[1].Value); + } + + [Fact] + public void SingleSeriesFourAdjacentRefs_x86_EmitsFourConsecutiveAddresses() + { + IRuntimeTypeSystem rts = MockRtsWithSeries((Offset: 4, Size: 16)); + TargetPointer[] refs = GcRefEnumeration.EnumerateValueTypeRefs( + rts, MakeHandle(0x1000), new TargetPointer(0x2000), pointerSize: 4).ToArray(); + Assert.Equal(4, refs.Length); + Assert.Equal(0x2000ul, refs[0].Value); + Assert.Equal(0x2004ul, refs[1].Value); + Assert.Equal(0x2008ul, refs[2].Value); + Assert.Equal(0x200Cul, refs[3].Value); + } + + // ===== multiple disjoint series ===== + + [Fact] + public void MultipleDisjointSeries_x64_EmitsAllInSeriesOrder() + { + // Layout: { objref; long; objref } -> two single-ref series at boxed + // offsets 8 and 24 (gap of one 8-byte non-ref field between them). + IRuntimeTypeSystem rts = MockRtsWithSeries( + (Offset: 8, Size: 8), + (Offset: 24, Size: 8)); + TargetPointer[] refs = GcRefEnumeration.EnumerateValueTypeRefs( + rts, MakeHandle(0x1000), new TargetPointer(0x2000), pointerSize: 8).ToArray(); + Assert.Equal(2, refs.Length); + Assert.Equal(0x2000ul, refs[0].Value); // boxed 8 -> unboxed 0 + Assert.Equal(0x2010ul, refs[1].Value); // boxed 24 -> unboxed 16 + } + + [Fact] + public void MixedRunsAndGaps_x64_EmitsCorrectAddresses() + { + // Two series: a 2-ref run at boxed offset 8, then a 1-ref run at boxed offset 40. + IRuntimeTypeSystem rts = MockRtsWithSeries( + (Offset: 8, Size: 16), + (Offset: 40, Size: 8)); + TargetPointer[] refs = GcRefEnumeration.EnumerateValueTypeRefs( + rts, MakeHandle(0x1000), new TargetPointer(0x2000), pointerSize: 8).ToArray(); + Assert.Equal(3, refs.Length); + Assert.Equal(0x2000ul, refs[0].Value); // boxed 8 -> unboxed 0 + Assert.Equal(0x2008ul, refs[1].Value); // boxed 16 -> unboxed 8 + Assert.Equal(0x2020ul, refs[2].Value); // boxed 40 -> unboxed 32 + } + + // ===== base address arithmetic ===== + + [Fact] + public void BaseAddressIsOffsetCorrectly_NonZeroBase() + { + IRuntimeTypeSystem rts = MockRtsWithSeries((Offset: 8, Size: 16)); + TargetPointer[] refs = GcRefEnumeration.EnumerateValueTypeRefs( + rts, MakeHandle(0x1000), new TargetPointer(0xFFFF_FFFF_FFFF_0000ul), pointerSize: 8).ToArray(); + Assert.Equal(2, refs.Length); + Assert.Equal(0xFFFF_FFFF_FFFF_0000ul, refs[0].Value); + Assert.Equal(0xFFFF_FFFF_FFFF_0008ul, refs[1].Value); + } + + // ===== lazy enumeration ===== + + [Fact] + public void DoesNotCallGetGCDescSeriesUntilEnumerated() + { + Mock mock = new(MockBehavior.Strict); + IEnumerable result = GcRefEnumeration.EnumerateValueTypeRefs( + mock.Object, MakeHandle(0x1000), new TargetPointer(0x2000), pointerSize: 8); + // No setup on the mock -> if the helper ate the iterator eagerly the strict mock + // would throw. Materializing now must not throw because we set the call up below. + mock.Setup(r => r.GetGCDescSeries(It.IsAny(), 0u)) + .Returns(System.Array.Empty<(uint, uint)>()); + Assert.Empty(result.ToArray()); + } +} diff --git a/src/native/managed/cdac/tests/GcScannerReportArgumentTests.cs b/src/native/managed/cdac/tests/GcScannerReportArgumentTests.cs new file mode 100644 index 00000000000000..44695219355a16 --- /dev/null +++ b/src/native/managed/cdac/tests/GcScannerReportArgumentTests.cs @@ -0,0 +1,469 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; +using Moq; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.Tests; + +public class GcScannerReportArgumentTests +{ + private const ulong TransitionBlockBase = 0x10_0000; + + private static GcScanContext NewContext(TestPlaceholderTarget target) + { + GcScanContext ctx = new GcScanContext(target, resolveInteriorPointers: false); + ctx.UpdateScanContext(new TargetPointer(0x1000), new TargetPointer(0x2000), new TargetPointer(0x3000)); + return ctx; + } + + private static List<(TargetPointer Address, GcScanFlags Flags)> Run( + ArgLayout arg, + IRuntimeTypeSystem rts, + MockTarget.Architecture arch) + { + // Stub reader: every read succeeds and returns zeros. The scanner reads the + // object pointer from each reported stack slot; we don't care about the value, + // we only verify the slot ADDRESSES the scanner reports. + TestPlaceholderTarget target = new TestPlaceholderTarget.Builder(arch) + .UseReader((ulong _, Span buffer) => + { + buffer.Clear(); + return 0; + }) + .Build(); + GcScanContext ctx = NewContext(target); + GcScanner.ReportArgument(arg, new TargetPointer(TransitionBlockBase), rts, target.PointerSize, ctx); + return ctx.StackRefs.Select(r => (r.Address, r.Flags)).ToList(); + } + + private static IRuntimeTypeSystem EmptyRts() => Mock.Of(); + + private static IRuntimeTypeSystem RtsWithSeries(params (uint Offset, uint Size)[] series) + { + Mock mock = new(MockBehavior.Strict); + mock.Setup(r => r.IsByRefLike(It.IsAny())).Returns(false); + mock.Setup(r => r.GetGCDescSeries(It.IsAny(), 0u)).Returns(series); + return mock.Object; + } + + // ===== reference / interior slots ===== + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void RefSlot_ReportedDirectly(MockTarget.Architecture arch) + { + ArgLayout arg = new(IsPassedByRef: false, Slots: new[] { new ArgSlot(0x20, CorElementType.Class) }); + List<(TargetPointer Address, GcScanFlags Flags)> reports = Run(arg, EmptyRts(), arch); + (TargetPointer addr, GcScanFlags flags) = Assert.Single(reports); + Assert.Equal(TransitionBlockBase + 0x20ul, addr.Value); + Assert.Equal(GcScanFlags.None, flags); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void ByrefSlot_ReportedAsInterior(MockTarget.Architecture arch) + { + ArgLayout arg = new(IsPassedByRef: false, Slots: new[] { new ArgSlot(0x40, CorElementType.Byref) }); + List<(TargetPointer Address, GcScanFlags Flags)> reports = Run(arg, EmptyRts(), arch); + (TargetPointer addr, GcScanFlags flags) = Assert.Single(reports); + Assert.Equal(TransitionBlockBase + 0x40ul, addr.Value); + Assert.Equal(GcScanFlags.GC_CALL_INTERIOR, flags); + } + + // ===== by-implicit-reference value type ===== + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void ValueTypeByRef_ReportedAsInterior(MockTarget.Architecture arch) + { + // Pass-by-implicit-reference structs (e.g. Win-x64 non-power-of-2 structs) carry an + // interior pointer in their single slot; ValueTypeHandle is null per the Phase 1 + // ComputeValueTypeHandle rule. + ArgLayout arg = new(IsPassedByRef: true, Slots: new[] { new ArgSlot(0x10, CorElementType.ValueType) }); + List<(TargetPointer Address, GcScanFlags Flags)> reports = Run(arg, EmptyRts(), arch); + (TargetPointer addr, GcScanFlags flags) = Assert.Single(reports); + Assert.Equal(TransitionBlockBase + 0x10ul, addr.Value); + Assert.Equal(GcScanFlags.GC_CALL_INTERIOR, flags); + } + + // ===== decomposed value type (SysV split / HFA) ===== + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void DecomposedValueType_ReportsPerSlotByElementType(MockTarget.Architecture arch) + { + // SysV-style split: eight-byte 0 is a ref, eight-byte 1 is a non-ref. + // ValueTypeHandle is null (Phase 1 rule: any non-ValueType slot ElementType + // disqualifies the CGCDesc walk because the iterator already did decomposition). + ArgLayout arg = new( + IsPassedByRef: false, + Slots: new[] + { + new ArgSlot(0x10, CorElementType.Class), + new ArgSlot(0x18, CorElementType.I8), + }); + List<(TargetPointer Address, GcScanFlags Flags)> reports = Run(arg, EmptyRts(), arch); + (TargetPointer addr, GcScanFlags flags) = Assert.Single(reports); + Assert.Equal(TransitionBlockBase + 0x10ul, addr.Value); + Assert.Equal(GcScanFlags.None, flags); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void UndecomposedValueTypeWithoutHandle_EmitsNothing(MockTarget.Architecture arch) + { + // A by-value value type that the iterator left undecomposed but for which the + // Phase 1 producer could not resolve a TypeHandle (e.g. an unresolved TypeSpec + // or a layout discontinuity). The scanner cannot safely walk an unknown layout + // so it reports nothing. + ArgLayout arg = new(IsPassedByRef: false, Slots: new[] { new ArgSlot(0x10, CorElementType.ValueType) }); + List<(TargetPointer Address, GcScanFlags Flags)> reports = Run(arg, EmptyRts(), arch); + Assert.Empty(reports); + } + + // ===== by-value value type with CGCDesc walk ===== + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void ByValueValueTypeWithHandle_WalksGcDescAndReportsRefs(MockTarget.Architecture arch) + { + int ps = arch.Is64Bit ? 8 : 4; + // Struct laid out (boxed) as { MT*; objref @ ps; objref @ 2*ps; nonref @ 3*ps }. + // GetGCDescSeries returns one series: (Offset=ps, Size=2*ps) -- two adjacent refs. + // Unboxed offsets are 0 and ps. With slots[0].Offset = 0x50, the expected report + // addresses are TransitionBlockBase + 0x50 and TransitionBlockBase + 0x50 + ps. + IRuntimeTypeSystem rts = RtsWithSeries(((uint)ps, (uint)(ps * 2))); + TypeHandle th = new TypeHandle(new TargetPointer(0xAA_BB_CC_00)); + ArgLayout arg = new( + IsPassedByRef: false, + Slots: new[] { new ArgSlot(0x50, CorElementType.ValueType) }, + ValueTypeHandle: th); + + List<(TargetPointer Address, GcScanFlags Flags)> reports = Run(arg, rts, arch); + + Assert.Equal(2, reports.Count); + Assert.Equal(TransitionBlockBase + 0x50ul, reports[0].Address.Value); + Assert.Equal(GcScanFlags.None, reports[0].Flags); + Assert.Equal(TransitionBlockBase + 0x50ul + (ulong)ps, reports[1].Address.Value); + Assert.Equal(GcScanFlags.None, reports[1].Flags); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void ByValueValueTypeWithHandle_MultiSlotStorage_UsesFirstSlotAsBase(MockTarget.Architecture arch) + { + // ARM64 16-byte non-HFA struct passed in X0+X1: two ValueType slots at + // contiguous offsets. Phase 1 populates ValueTypeHandle because all slots have + // ElementType=ValueType (the iterator did NOT decompose). The CGCDesc walk uses + // slots[0].Offset as the base of the contiguous storage. + int ps = arch.Is64Bit ? 8 : 4; + IRuntimeTypeSystem rts = RtsWithSeries(((uint)ps, (uint)ps)); + TypeHandle th = new TypeHandle(new TargetPointer(0x12_34_56_78)); + ArgLayout arg = new( + IsPassedByRef: false, + Slots: new[] + { + new ArgSlot(0x80, CorElementType.ValueType), + new ArgSlot(0x80 + ps, CorElementType.ValueType), + }, + ValueTypeHandle: th); + + List<(TargetPointer Address, GcScanFlags Flags)> reports = Run(arg, rts, arch); + + (TargetPointer addr, GcScanFlags flags) = Assert.Single(reports); + // unboxed offset = boxed offset - ps = ps - ps = 0, so the ref is at slots[0]. + Assert.Equal(TransitionBlockBase + 0x80ul, addr.Value); + Assert.Equal(GcScanFlags.None, flags); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void ByValueValueTypeWithHandle_NoRefs_EmitsNothing(MockTarget.Architecture arch) + { + // A struct with no managed refs (empty GCDesc series). The walk runs but yields + // zero callbacks. Verifies the helper doesn't emit spurious reports. + IRuntimeTypeSystem rts = RtsWithSeries(System.Array.Empty<(uint, uint)>()); + TypeHandle th = new TypeHandle(new TargetPointer(0xFF_FF_FF_FF)); + ArgLayout arg = new( + IsPassedByRef: false, + Slots: new[] { new ArgSlot(0x60, CorElementType.ValueType) }, + ValueTypeHandle: th); + + List<(TargetPointer Address, GcScanFlags Flags)> reports = Run(arg, rts, arch); + Assert.Empty(reports); + } + + // ===== primitive / other ===== + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void PrimitiveSlot_EmitsNothing(MockTarget.Architecture arch) + { + ArgLayout arg = new(IsPassedByRef: false, Slots: new[] { new ArgSlot(0x70, CorElementType.I4) }); + List<(TargetPointer Address, GcScanFlags Flags)> reports = Run(arg, EmptyRts(), arch); + Assert.Empty(reports); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void EmptySlots_EmitsNothing(MockTarget.Architecture arch) + { + ArgLayout arg = new(IsPassedByRef: false, Slots: System.Array.Empty()); + List<(TargetPointer Address, GcScanFlags Flags)> reports = Run(arg, EmptyRts(), arch); + Assert.Empty(reports); + } + + // ===== priority: ValueTypeHandle wins over per-slot dispatch ===== + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void ValueTypeHandleBranch_TakesPrecedenceOverPerSlotLoop(MockTarget.Architecture arch) + { + // Hypothetical: even if a slot's ElementType were Class, the presence of a + // ValueTypeHandle indicates the producer wants the GCDesc walk to drive reports. + // This documents the dispatch order so future producer/consumer changes preserve + // the invariant. + int ps = arch.Is64Bit ? 8 : 4; + IRuntimeTypeSystem rts = RtsWithSeries(((uint)ps, (uint)ps)); + TypeHandle th = new TypeHandle(new TargetPointer(0xAB_CD)); + ArgLayout arg = new( + IsPassedByRef: false, + Slots: new[] { new ArgSlot(0x90, CorElementType.ValueType) }, + ValueTypeHandle: th); + + List<(TargetPointer Address, GcScanFlags Flags)> reports = Run(arg, rts, arch); + (TargetPointer addr, GcScanFlags flags) = Assert.Single(reports); + Assert.Equal(TransitionBlockBase + 0x90ul, addr.Value); + Assert.Equal(GcScanFlags.None, flags); + } + + // ===== ByRefLike (Span, ref structs): field-walk branch ===== + + private static IRuntimeTypeSystem RtsForByRefLike( + TypeHandle byRefLikeType, + params (uint Offset, CorElementType Type, TypeHandle? Inner, bool InnerIsByRefLike, (uint Offset, uint Size)[]? InnerSeries)[] fields) + { + Mock mock = new(MockBehavior.Strict); + mock.Setup(r => r.IsByRefLike(byRefLikeType)).Returns(true); + + TargetPointer[] fdPtrs = Enumerable.Range(1, fields.Length) + .Select(i => new TargetPointer((ulong)i)).ToArray(); + mock.Setup(r => r.EnumerateInstanceFieldDescs(byRefLikeType)).Returns(fdPtrs); + + for (int i = 0; i < fields.Length; i++) + { + TargetPointer fdPtr = fdPtrs[i]; + (uint offset, CorElementType et, TypeHandle? inner, bool innerByRefLike, (uint, uint)[]? innerSeries) = fields[i]; + + mock.Setup(r => r.GetFieldDescOffset(fdPtr, It.IsAny())) + .Returns(offset); + mock.Setup(r => r.GetFieldDescType(fdPtr)).Returns(et); + + if (et == CorElementType.ValueType) + { + mock.Setup(r => r.LookupApproxFieldTypeHandle(fdPtr)) + .Returns(inner ?? default); + if (inner is { } innerTh) + { + mock.Setup(r => r.IsByRefLike(innerTh)).Returns(innerByRefLike); + if (!innerByRefLike && innerSeries is not null) + { + mock.Setup(r => r.GetGCDescSeries(innerTh, 0u)).Returns(innerSeries); + } + // For nested ByRefLike: the recursive walker will call + // EnumerateInstanceFieldDescs(innerTh) and resolve from there. + // Tests covering recursion add those setups explicitly. + } + } + } + + return mock.Object; + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void ByRefLike_SpanLike_EmitsObjectRefAtOffsetZero(MockTarget.Architecture arch) + { + // Shape of Span: { ref byref payload; int length }. The byref payload is + // GC_CALL_INTERIOR; the length is a primitive (skipped). + TypeHandle spanTh = new TypeHandle(new TargetPointer(0x1_0000)); + IRuntimeTypeSystem rts = RtsForByRefLike(spanTh, + (Offset: 0, Type: CorElementType.Byref, Inner: null, InnerIsByRefLike: false, InnerSeries: null), + (Offset: 8, Type: CorElementType.I4, Inner: null, InnerIsByRefLike: false, InnerSeries: null)); + + ArgLayout arg = new( + IsPassedByRef: false, + Slots: new[] { new ArgSlot(0x100, CorElementType.ValueType) }, + ValueTypeHandle: spanTh); + + List<(TargetPointer Address, GcScanFlags Flags)> reports = Run(arg, rts, arch); + + (TargetPointer addr, GcScanFlags flags) = Assert.Single(reports); + Assert.Equal(TransitionBlockBase + 0x100ul, addr.Value); + Assert.Equal(GcScanFlags.GC_CALL_INTERIOR, flags); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void ByRefLike_ObjectRefField_EmitsAsRoot(MockTarget.Architecture arch) + { + // ref struct { object x; } - object refs emit None (regular ref). + TypeHandle th = new TypeHandle(new TargetPointer(0x2_0000)); + IRuntimeTypeSystem rts = RtsForByRefLike(th, + (Offset: 16, Type: CorElementType.Class, Inner: null, InnerIsByRefLike: false, InnerSeries: null)); + + ArgLayout arg = new( + IsPassedByRef: false, + Slots: new[] { new ArgSlot(0x200, CorElementType.ValueType) }, + ValueTypeHandle: th); + + List<(TargetPointer Address, GcScanFlags Flags)> reports = Run(arg, rts, arch); + + (TargetPointer addr, GcScanFlags flags) = Assert.Single(reports); + Assert.Equal(TransitionBlockBase + 0x200ul + 16ul, addr.Value); + Assert.Equal(GcScanFlags.None, flags); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void ByRefLike_MixedFields_EmitsExpectedRootsInOrder(MockTarget.Architecture arch) + { + // ref struct { object obj; ref int rr; int prim; }. Expect 2 roots: + // - obj at +0 with None + // - rr at +ps with GC_CALL_INTERIOR + // prim is skipped. Order matches field-list order. + int ps = arch.Is64Bit ? 8 : 4; + TypeHandle th = new TypeHandle(new TargetPointer(0x3_0000)); + IRuntimeTypeSystem rts = RtsForByRefLike(th, + (Offset: 0, Type: CorElementType.Class, Inner: null, InnerIsByRefLike: false, InnerSeries: null), + (Offset: (uint)ps, Type: CorElementType.Byref, Inner: null, InnerIsByRefLike: false, InnerSeries: null), + (Offset: (uint)(ps * 2), Type: CorElementType.I4, Inner: null, InnerIsByRefLike: false, InnerSeries: null)); + + ArgLayout arg = new( + IsPassedByRef: false, + Slots: new[] { new ArgSlot(0x300, CorElementType.ValueType) }, + ValueTypeHandle: th); + + List<(TargetPointer Address, GcScanFlags Flags)> reports = Run(arg, rts, arch); + + Assert.Equal(2, reports.Count); + Assert.Equal(TransitionBlockBase + 0x300ul, reports[0].Address.Value); + Assert.Equal(GcScanFlags.None, reports[0].Flags); + Assert.Equal(TransitionBlockBase + 0x300ul + (ulong)ps, reports[1].Address.Value); + Assert.Equal(GcScanFlags.GC_CALL_INTERIOR, reports[1].Flags); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void ByRefLike_NestedOrdinaryStructField_DelegatesToCgcDescWalk(MockTarget.Architecture arch) + { + // ref struct { NormalStruct s; } where NormalStruct (non-ByRefLike) has one + // object ref at boxed offset = ps -> unboxed offset = 0. Slot offset for the + // nested field is 0x10 within the ByRefLike. Expect 1 ref report at + // base + 0x10 + 0. + int ps = arch.Is64Bit ? 8 : 4; + TypeHandle outerTh = new TypeHandle(new TargetPointer(0x4_0000)); + TypeHandle innerTh = new TypeHandle(new TargetPointer(0x4_1000)); + IRuntimeTypeSystem rts = RtsForByRefLike(outerTh, + (Offset: 0x10, Type: CorElementType.ValueType, Inner: innerTh, InnerIsByRefLike: false, + InnerSeries: new (uint, uint)[] { ((uint)ps, (uint)ps) })); + + ArgLayout arg = new( + IsPassedByRef: false, + Slots: new[] { new ArgSlot(0x400, CorElementType.ValueType) }, + ValueTypeHandle: outerTh); + + List<(TargetPointer Address, GcScanFlags Flags)> reports = Run(arg, rts, arch); + + (TargetPointer addr, GcScanFlags flags) = Assert.Single(reports); + Assert.Equal(TransitionBlockBase + 0x400ul + 0x10ul, addr.Value); + Assert.Equal(GcScanFlags.None, flags); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void ByRefLike_NestedByRefLikeField_RecursesAndPreservesOffset(MockTarget.Architecture arch) + { + // Outer ref struct { InnerRefStruct s; } where InnerRefStruct has one byref at + // offset 0. Outer places s at offset 0x20. Expect 1 interior-ref report at + // base + 0x20. + TypeHandle outerTh = new TypeHandle(new TargetPointer(0x5_0000)); + TypeHandle innerTh = new TypeHandle(new TargetPointer(0x5_1000)); + + Mock mock = new(MockBehavior.Strict); + mock.Setup(r => r.IsByRefLike(outerTh)).Returns(true); + mock.Setup(r => r.IsByRefLike(innerTh)).Returns(true); + + TargetPointer outerFd = new(1); + TargetPointer innerFd = new(2); + mock.Setup(r => r.EnumerateInstanceFieldDescs(outerTh)).Returns(new[] { outerFd }); + mock.Setup(r => r.EnumerateInstanceFieldDescs(innerTh)).Returns(new[] { innerFd }); + + mock.Setup(r => r.GetFieldDescOffset(outerFd, It.IsAny())).Returns(0x20u); + mock.Setup(r => r.GetFieldDescType(outerFd)).Returns(CorElementType.ValueType); + mock.Setup(r => r.LookupApproxFieldTypeHandle(outerFd)).Returns(innerTh); + + mock.Setup(r => r.GetFieldDescOffset(innerFd, It.IsAny())).Returns(0u); + mock.Setup(r => r.GetFieldDescType(innerFd)).Returns(CorElementType.Byref); + + ArgLayout arg = new( + IsPassedByRef: false, + Slots: new[] { new ArgSlot(0x500, CorElementType.ValueType) }, + ValueTypeHandle: outerTh); + + List<(TargetPointer Address, GcScanFlags Flags)> reports = Run(arg, mock.Object, arch); + + (TargetPointer addr, GcScanFlags flags) = Assert.Single(reports); + Assert.Equal(TransitionBlockBase + 0x500ul + 0x20ul, addr.Value); + Assert.Equal(GcScanFlags.GC_CALL_INTERIOR, flags); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void ByRefLike_NoFields_EmitsNothing(MockTarget.Architecture arch) + { + TypeHandle th = new TypeHandle(new TargetPointer(0x6_0000)); + Mock mock = new(MockBehavior.Strict); + mock.Setup(r => r.IsByRefLike(th)).Returns(true); + mock.Setup(r => r.EnumerateInstanceFieldDescs(th)).Returns(System.Array.Empty()); + + ArgLayout arg = new( + IsPassedByRef: false, + Slots: new[] { new ArgSlot(0x600, CorElementType.ValueType) }, + ValueTypeHandle: th); + + List<(TargetPointer Address, GcScanFlags Flags)> reports = Run(arg, mock.Object, arch); + Assert.Empty(reports); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void ByRefLike_NestedValueTypeWithUnresolvedInner_SkipsThatField(MockTarget.Architecture arch) + { + // ref struct { object obj; ValueType missing; } where the second field's inner + // TypeHandle can't be resolved (LookupApproxFieldTypeHandle returns default). + // The walker should emit the first field's root and skip the second, matching + // the native DAC's conservative behavior. + TypeHandle th = new TypeHandle(new TargetPointer(0x7_0000)); + IRuntimeTypeSystem rts = RtsForByRefLike(th, + (Offset: 0, Type: CorElementType.Class, Inner: null, InnerIsByRefLike: false, InnerSeries: null), + (Offset: 8, Type: CorElementType.ValueType, Inner: default(TypeHandle), InnerIsByRefLike: false, InnerSeries: null)); + + ArgLayout arg = new( + IsPassedByRef: false, + Slots: new[] { new ArgSlot(0x700, CorElementType.ValueType) }, + ValueTypeHandle: th); + + List<(TargetPointer Address, GcScanFlags Flags)> reports = Run(arg, rts, arch); + + (TargetPointer addr, GcScanFlags flags) = Assert.Single(reports); + Assert.Equal(TransitionBlockBase + 0x700ul, addr.Value); + Assert.Equal(GcScanFlags.None, flags); + } +} diff --git a/src/native/managed/cdac/tests/MethodTableTests.cs b/src/native/managed/cdac/tests/MethodTableTests.cs index 7bbe2743700689..f049f07465cd68 100644 --- a/src/native/managed/cdac/tests/MethodTableTests.cs +++ b/src/native/managed/cdac/tests/MethodTableTests.cs @@ -26,6 +26,15 @@ public class MethodTableTests [DataType.FnPtrTypeDesc] = TargetTestHelpers.CreateTypeInfo(rtsBuilder.FnPtrTypeDescLayout), [DataType.ParamTypeDesc] = TargetTestHelpers.CreateTypeInfo(rtsBuilder.ParamTypeDescLayout), [DataType.TypeVarTypeDesc] = TargetTestHelpers.CreateTypeInfo(rtsBuilder.TypeVarTypeDescLayout), + [DataType.GenericsDictInfo] = new Target.TypeInfo + { + Size = (uint)rtsBuilder.Builder.TargetTestHelpers.PointerSize, + Fields = new Dictionary + { + [nameof(Data.GenericsDictInfo.NumDicts)] = new Target.FieldInfo { Offset = 0 }, + [nameof(Data.GenericsDictInfo.NumTypeArgs)] = new Target.FieldInfo { Offset = sizeof(ushort) }, + }, + }, [DataType.GCCoverageInfo] = TargetTestHelpers.CreateTypeInfo(rtsBuilder.GCCoverageInfoLayout), [DataType.ContinuationObject] = new Target.TypeInfo { Size = rtsBuilder.ContinuationObjectSize }, }; @@ -34,8 +43,9 @@ internal static (string Name, ulong Value)[] CreateContractGlobals(MockRTS rtsBu => [ (nameof(Constants.Globals.FreeObjectMethodTable), rtsBuilder.FreeObjectMethodTableGlobalAddress), - (nameof(Constants.Globals.ContinuationMethodTable), rtsBuilder.ContinuationMethodTableGlobalAddress), (nameof(Constants.Globals.ObjectMethodTable), rtsBuilder.ObjectMethodTableGlobalAddress), + (nameof(Constants.Globals.ContinuationMethodTable), rtsBuilder.ContinuationMethodTableGlobalAddress), + (nameof(Constants.Globals.TypedReferenceMethodTable), rtsBuilder.TypedReferenceMethodTableGlobalAddress), (nameof(Constants.Globals.MethodDescAlignment), rtsBuilder.MethodDescAlignment), (nameof(Constants.Globals.ArrayBaseSize), rtsBuilder.ArrayBaseSize), ]; @@ -77,7 +87,6 @@ public void HasRuntimeTypeSystemContract(MockTarget.Architecture arch) Contracts.TypeHandle handle = contract.GetTypeHandle(freeObjectMethodTableAddress); Assert.NotEqual(TargetPointer.Null, handle.Address); Assert.True(contract.IsFreeObjectMethodTable(handle)); - Assert.False(contract.IsObject(handle)); } [Theory] @@ -93,7 +102,6 @@ public void ValidateSystemObjectMethodTable(MockTarget.Architecture arch) Contracts.TypeHandle systemObjectTypeHandle = contract.GetTypeHandle(systemObjectMethodTablePtr); Assert.Equal(systemObjectMethodTablePtr.Value, systemObjectTypeHandle.Address.Value); Assert.False(contract.IsFreeObjectMethodTable(systemObjectTypeHandle)); - Assert.True(contract.IsObject(systemObjectTypeHandle)); } [Theory] @@ -709,6 +717,74 @@ public void RequiresAlign8(MockTarget.Architecture arch, bool flagSet) Assert.Equal(flagSet, contract.RequiresAlign8(typeHandle)); } + public static IEnumerable IsHFAData() + { + yield return [(int)1, true, true]; + yield return [(int)1, false, false]; + yield return [(int)0, true, false]; + yield return [(int)0, false, false]; + } + + public static IEnumerable StdArchIsHFAData() + { + foreach (object[] arch in new MockTarget.StdArch()) + foreach (object[] hfaData in IsHFAData()) + yield return [.. arch, ..hfaData]; + } + + [Theory] + [MemberData(nameof(StdArchIsHFAData))] + public void IsHFA(MockTarget.Architecture arch, int featureHFA, bool flagSet, bool expected) + { + TargetPointer methodTablePtr = default; + var targetBuilder = new TestPlaceholderTarget.Builder(arch); + MockRTS rtsBuilder = new(targetBuilder.MemoryBuilder); + + MockEEClass eeClass = rtsBuilder.AddEEClass("HFAType"); + eeClass.CorTypeAttr = (uint)(System.Reflection.TypeAttributes.Public | System.Reflection.TypeAttributes.Class); + MockMethodTable methodTable = rtsBuilder.AddMethodTable("HFAType"); + uint mtFlags = (uint)MethodTableFlags_1.WFLAGS_HIGH.Category_ValueType; + if (flagSet) + mtFlags |= (uint)MethodTableFlags_1.WFLAGS_LOW.IsHFA; + methodTable.MTFlags = mtFlags; + methodTable.BaseSize = rtsBuilder.Builder.TargetTestHelpers.ObjectBaseSize; + methodTable.ParentMethodTable = rtsBuilder.SystemObjectMethodTable.Address; + methodTable.NumVirtuals = 3; + methodTablePtr = methodTable.Address; + eeClass.MethodTable = methodTable.Address; + methodTable.EEClassOrCanonMT = eeClass.Address; + + var builder = targetBuilder + .AddTypes(CreateContractTypes(rtsBuilder)) + .AddGlobals(CreateContractGlobals(rtsBuilder)) + .AddContract(version: "c1"); + if (featureHFA >= 0) + builder.AddGlobals((Constants.Globals.FeatureHFA, (ulong)featureHFA)); + TestPlaceholderTarget target = builder.Build(); + + IRuntimeTypeSystem contract = target.Contracts.RuntimeTypeSystem; + Contracts.TypeHandle typeHandle = contract.GetTypeHandle(methodTablePtr); + Assert.Equal(expected, contract.IsHFA(typeHandle)); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void IsHFAFalseForNullTypeHandle(MockTarget.Architecture arch) + { + // Null TypeHandle should never report HFA, regardless of FEATURE_HFA setting. + var targetBuilder = new TestPlaceholderTarget.Builder(arch); + MockRTS rtsBuilder = new(targetBuilder.MemoryBuilder); + TestPlaceholderTarget target = targetBuilder + .AddTypes(CreateContractTypes(rtsBuilder)) + .AddGlobals(CreateContractGlobals(rtsBuilder)) + .AddGlobals((Constants.Globals.FeatureHFA, 1UL)) + .AddContract(version: "c1") + .Build(); + + IRuntimeTypeSystem contract = target.Contracts.RuntimeTypeSystem; + Assert.False(contract.IsHFA(default)); + } + [Theory] [ClassData(typeof(MockTarget.StdArch))] public void GetGCDescSeriesReturnsEmptyForNonMethodTable(MockTarget.Architecture arch) diff --git a/src/native/managed/cdac/tests/MockDescriptors/Layout.cs b/src/native/managed/cdac/tests/MockDescriptors/Layout.cs index 57f1c667197fa9..d7f4638f13528b 100644 --- a/src/native/managed/cdac/tests/MockDescriptors/Layout.cs +++ b/src/native/managed/cdac/tests/MockDescriptors/Layout.cs @@ -86,7 +86,6 @@ public LayoutBuilder(string name, MockTarget.Architecture architecture) public LayoutBuilder AddField(string name, int offset, int size) { ArgumentException.ThrowIfNullOrEmpty(name); - ArgumentOutOfRangeException.ThrowIfNegative(offset); ArgumentOutOfRangeException.ThrowIfNegativeOrZero(size); _fields[name] = new LayoutField(name, offset, size); diff --git a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.CallingConvention.cs b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.CallingConvention.cs new file mode 100644 index 00000000000000..8914cf478b627c --- /dev/null +++ b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.CallingConvention.cs @@ -0,0 +1,224 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.DataContractReader.RuntimeTypeSystemHelpers; + +namespace Microsoft.Diagnostics.DataContractReader.Tests; + +internal partial class MockDescriptors +{ + public static class CallingConvention + { + public static Target.TypeInfo CreateTransitionBlockTypeInfo(CallConvTestCase testCase) + => TargetTestHelpers.CreateTypeInfo(CreateTransitionBlockLayout(testCase)); + + public static Layout CreateTransitionBlockLayout(CallConvTestCase testCase) + { + // The cDAC descriptor convention uses field offsets to carry ABI constant + // values; field size is unused at read-time, so 1 is just a placeholder. + LayoutBuilder builder = new("TransitionBlock", testCase.MockArch) + { + Size = testCase.TransitionBlockSize, + }; + builder.AddField("ArgumentRegistersOffset", testCase.ArgumentRegistersOffset, 1); + builder.AddField("FirstGCRefMapSlot", testCase.FirstGCRefMapSlot, 1); + builder.AddField("OffsetOfArgs", testCase.OffsetOfArgs, 1); + if (testCase.OffsetOfFloatArgumentRegisters is int f) + builder.AddField("OffsetOfFloatArgumentRegisters", f, 1); + return builder.Build(); + } + + // ----- FieldDesc layout / Value-type MT allocator ----- + + public static Layout CreateFieldDescLayout(MockTarget.Architecture arch) + => MockFieldDesc.CreateLayout(arch); + + public static Target.TypeInfo CreateFieldDescTypeInfo(MockTarget.Architecture arch) + => TargetTestHelpers.CreateTypeInfo(CreateFieldDescLayout(arch)); + + public readonly record struct ValueTypeField(int Offset, CorElementType ElementType); + + public static MockMethodTable AddValueTypeMethodTable( + MockDescriptors.RuntimeTypeSystem rts, + string name, + int structSize, + IReadOnlyList fields) + { + MockTarget.Architecture arch = rts.Builder.TargetTestHelpers.Arch; + + MockEEClass eeClass = rts.AddEEClass(name); + eeClass.NumInstanceFields = (ushort)fields.Count; + eeClass.NumMethods = 0; + + // Allocate the FieldDesc array. + if (fields.Count > 0) + { + Layout fdLayout = CreateFieldDescLayout(arch); + MockMemorySpace.HeapFragment fdArray = rts.TypeSystemAllocator.Allocate( + (ulong)(fdLayout.Size * fields.Count), $"FieldDescs[{name}]"); + eeClass.FieldDescList = fdArray.Address; + + MockMethodTable enclosingMT = rts.AddMethodTable(name); + // Wire up MT <-> EEClass first since FieldDescs back-reference the MT. + eeClass.MethodTable = enclosingMT.Address; + enclosingMT.EEClassOrCanonMT = eeClass.Address; + + // ValueType category flag (low 16 bits of MTFlags Category_Mask) + IsValueType bit. + const uint Category_ValueType = 0x00040000; + enclosingMT.MTFlags = Category_ValueType; + + // BaseSize must be pointer-aligned for TypeValidation to accept this MT + // (real value-type MTs always satisfy this). Encode the actual struct size + // via BaseSizePadding so GetNumInstanceFieldBytes = BaseSize - BaseSizePadding + // returns the requested structSize. + int ptrSize = arch.Is64Bit ? 8 : 4; + uint alignedBaseSize = (uint)((structSize + (ptrSize - 1)) & ~(ptrSize - 1)); + if (alignedBaseSize == 0) alignedBaseSize = (uint)ptrSize; + enclosingMT.BaseSize = alignedBaseSize; + eeClass.BaseSizePadding = (byte)(alignedBaseSize - structSize); + + // Reserve a vtable slot so an instance method on this value type can validate. + enclosingMT.NumVirtuals = 1; + + for (int i = 0; i < fields.Count; i++) + { + ulong fdAddr = fdArray.Address + (ulong)(i * fdLayout.Size); + MockFieldDesc fd = fdLayout.Create( + fdArray.Data.AsMemory(i * fdLayout.Size, fdLayout.Size), + fdAddr); + + // DWord1: token (low 24 bits) + flags. Token field encodes RID; we + // just use the (1-based) field index since the classifier doesn't + // resolve tokens for ELEMENT_TYPE_INTERNAL sigs. + fd.DWord1 = (uint)(i + 1) & 0xFFFFFF; + + // DWord2: offset (low 27 bits) | (CorElementType << 27). + fd.DWord2 = ((uint)fields[i].Offset & 0x07FFFFFFu) + | (((uint)fields[i].ElementType & 0x1Fu) << 27); + + fd.MTOfEnclosingClass = enclosingMT.Address; + } + + return enclosingMT; + } + + // Empty-struct path: no FieldDesc array, just an MT. + MockMethodTable emptyMT = rts.AddMethodTable(name); + eeClass.MethodTable = emptyMT.Address; + emptyMT.EEClassOrCanonMT = eeClass.Address; + emptyMT.MTFlags = 0x00040000; // Category_ValueType + int emptyPtrSize = arch.Is64Bit ? 8 : 4; + uint emptyAlignedBaseSize = (uint)((structSize + (emptyPtrSize - 1)) & ~(emptyPtrSize - 1)); + if (emptyAlignedBaseSize == 0) emptyAlignedBaseSize = (uint)emptyPtrSize; + emptyMT.BaseSize = emptyAlignedBaseSize; + eeClass.BaseSizePadding = (byte)(emptyAlignedBaseSize - structSize); + return emptyMT; + } + + public static MockMethodTable AddVectorMethodTable( + MockDescriptors.RuntimeTypeSystem rts, + string vectorTypeName, + int vectorByteSize, + uint typeDefToken) + { + const uint Category_ValueType = 0x00040000; + const uint GenericsMask_GenericInst = 0x00000010; + const uint IsIntrinsicType = (uint)MethodTableFlags_1.WFLAGS2_ENUM.IsIntrinsicType; + const int MTFlags2TypeDefRidShift = 8; + + MockTarget.Architecture arch = rts.Builder.TargetTestHelpers.Arch; + MockEEClass eeClass = rts.AddEEClass(vectorTypeName); + eeClass.CorTypeAttr = (uint)(System.Reflection.TypeAttributes.Public | System.Reflection.TypeAttributes.Sealed | System.Reflection.TypeAttributes.SequentialLayout); + eeClass.NumMethods = 0; + + MockMethodTable methodTable = rts.AddMethodTable(vectorTypeName); + eeClass.MethodTable = methodTable.Address; + methodTable.EEClassOrCanonMT = eeClass.Address; + methodTable.MTFlags = Category_ValueType | GenericsMask_GenericInst; + methodTable.MTFlags2 = ((typeDefToken & 0x00FFFFFFu) << MTFlags2TypeDefRidShift) | IsIntrinsicType; + methodTable.Module = rts.SystemObjectMethodTable.Module; + methodTable.ParentMethodTable = rts.SystemObjectMethodTable.Address; + methodTable.NumVirtuals = 1; + + int ptrSize = arch.Is64Bit ? 8 : 4; + uint alignedBaseSize = (uint)((vectorByteSize + (ptrSize - 1)) & ~(ptrSize - 1)); + if (alignedBaseSize == 0) + alignedBaseSize = (uint)ptrSize; + + methodTable.BaseSize = alignedBaseSize; + eeClass.BaseSizePadding = (byte)(alignedBaseSize - vectorByteSize); + methodTable.PerInstInfo = AddSingleTypeInstantiation(rts, CreatePrimitiveTypeArg(rts, CorElementType.I4).Address); + return methodTable; + } + + private static MockMethodTable CreatePrimitiveTypeArg(MockDescriptors.RuntimeTypeSystem rts, CorElementType elementType) + { + MockEEClass eeClass = rts.AddEEClass($"{elementType}TypeArg"); + eeClass.InternalCorElementType = (byte)elementType; + + MockMethodTable methodTable = rts.AddMethodTable($"{elementType}TypeArg"); + methodTable.MTFlags = (uint)MethodTableFlags_1.WFLAGS_HIGH.Category_TruePrimitive; + methodTable.BaseSize = (uint)rts.Builder.TargetTestHelpers.PointerSize; + methodTable.Module = rts.SystemObjectMethodTable.Module; + methodTable.ParentMethodTable = rts.SystemObjectMethodTable.Address; + methodTable.NumVirtuals = 1; + eeClass.MethodTable = methodTable.Address; + methodTable.EEClassOrCanonMT = eeClass.Address; + return methodTable; + } + + private static ulong AddSingleTypeInstantiation(MockDescriptors.RuntimeTypeSystem rts, ulong typeArgAddress) + { + TargetTestHelpers helpers = rts.Builder.TargetTestHelpers; + int pointerSize = helpers.PointerSize; + + MockMemorySpace.HeapFragment perInstInfoFragment = rts.TypeSystemAllocator.Allocate( + (ulong)(pointerSize * 2), "PerInstInfo[1]"); + helpers.Write(perInstInfoFragment.Data.AsSpan(0, sizeof(ushort)), (ushort)1); + helpers.Write(perInstInfoFragment.Data.AsSpan(sizeof(ushort), sizeof(ushort)), (ushort)1); + + MockMemorySpace.HeapFragment dictionaryFragment = rts.TypeSystemAllocator.Allocate( + (ulong)pointerSize, "GenericDictionary[1]"); + helpers.WritePointer(dictionaryFragment.Data, typeArgAddress); + helpers.WritePointer(perInstInfoFragment.Data.AsSpan(pointerSize, pointerSize), dictionaryFragment.Address); + + return perInstInfoFragment.Address + (ulong)pointerSize; + } + } +} + +internal sealed class MockFieldDesc : TypedView +{ + private const string DWord1FieldName = nameof(Data.FieldDesc.DWord1); + private const string DWord2FieldName = nameof(Data.FieldDesc.DWord2); + private const string MTOfEnclosingClassFieldName = nameof(Data.FieldDesc.MTOfEnclosingClass); + + public static Layout CreateLayout(MockTarget.Architecture arch) + => new SequentialLayoutBuilder("FieldDesc", arch) + .AddUInt32Field(DWord1FieldName) + .AddUInt32Field(DWord2FieldName) + .AddPointerField(MTOfEnclosingClassFieldName) + .Build(); + + public uint DWord1 + { + get => ReadUInt32Field(DWord1FieldName); + set => WriteUInt32Field(DWord1FieldName, value); + } + + public uint DWord2 + { + get => ReadUInt32Field(DWord2FieldName); + set => WriteUInt32Field(DWord2FieldName, value); + } + + public ulong MTOfEnclosingClass + { + get => ReadPointerField(MTOfEnclosingClassFieldName); + set => WritePointerField(MTOfEnclosingClassFieldName, value); + } +} + diff --git a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.RuntimeTypeSystem.cs b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.RuntimeTypeSystem.cs index ca3b4198536a2b..4b7fbf70bd25b3 100644 --- a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.RuntimeTypeSystem.cs +++ b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.RuntimeTypeSystem.cs @@ -106,6 +106,7 @@ internal sealed class MockEEClass : TypedView private const string NumThreadStaticFieldsFieldName = nameof(Data.EEClass.NumThreadStaticFields); private const string FieldDescListFieldName = nameof(Data.EEClass.FieldDescList); private const string NumNonVirtualSlotsFieldName = nameof(Data.EEClass.NumNonVirtualSlots); + private const string BaseSizePaddingFieldName = nameof(Data.EEClass.BaseSizePadding); public static Layout CreateLayout(MockTarget.Architecture architecture) => new SequentialLayoutBuilder("EEClass", architecture) @@ -119,6 +120,7 @@ public static Layout CreateLayout(MockTarget.Architecture architect .AddUInt16Field(NumThreadStaticFieldsFieldName) .AddPointerField(FieldDescListFieldName) .AddUInt16Field(NumNonVirtualSlotsFieldName) + .AddByteField(BaseSizePaddingFieldName) .Build(); public ulong MethodTable @@ -151,11 +153,35 @@ public ushort NumInstanceFields set => WriteUInt16Field(NumInstanceFieldsFieldName, value); } + public ushort NumStaticFields + { + get => ReadUInt16Field(NumStaticFieldsFieldName); + set => WriteUInt16Field(NumStaticFieldsFieldName, value); + } + + public ushort NumThreadStaticFields + { + get => ReadUInt16Field(NumThreadStaticFieldsFieldName); + set => WriteUInt16Field(NumThreadStaticFieldsFieldName, value); + } + + public ulong FieldDescList + { + get => ReadPointerField(FieldDescListFieldName); + set => WritePointerField(FieldDescListFieldName, value); + } + public ushort NumNonVirtualSlots { get => ReadUInt16Field(NumNonVirtualSlotsFieldName); set => WriteUInt16Field(NumNonVirtualSlotsFieldName, value); } + + public byte BaseSizePadding + { + get => ReadByteField(BaseSizePaddingFieldName); + set => WriteByteField(BaseSizePaddingFieldName, value); + } } internal sealed class MockMethodTableAuxiliaryData : TypedView @@ -300,7 +326,8 @@ public class RuntimeTypeSystem { internal const ulong TestFreeObjectMethodTableGlobalAddress = 0x00000000_7a0000a0; internal const ulong TestContinuationMethodTableGlobalAddress = 0x00000000_7a0000b0; - internal const ulong TestObjectMethodTableGlobalAddress = 0x00000000_7a0000c0; + internal const ulong TestTypedReferenceMethodTableGlobalAddress = 0x00000000_7a0000c0; + internal const ulong TestObjectMethodTableGlobalAddress = 0x00000000_7a0000d0; private const ulong DefaultAllocationRangeStart = 0x00000000_4a000000; private const ulong DefaultAllocationRangeEnd = 0x00000000_4b000000; @@ -327,6 +354,7 @@ public class RuntimeTypeSystem internal ulong FreeObjectMethodTableAddress { get; private set; } internal ulong FreeObjectMethodTableGlobalAddress => TestFreeObjectMethodTableGlobalAddress; internal ulong ContinuationMethodTableGlobalAddress => TestContinuationMethodTableGlobalAddress; + internal ulong TypedReferenceMethodTableGlobalAddress => TestTypedReferenceMethodTableGlobalAddress; internal ulong ObjectMethodTableGlobalAddress => TestObjectMethodTableGlobalAddress; internal ulong MethodDescAlignment => GetMethodDescAlignment(Builder.TargetTestHelpers); internal ulong ArrayBaseSize => Builder.TargetTestHelpers.ArrayBaseBaseSize; @@ -360,6 +388,7 @@ private void AddGlobalPointers() { AddFreeObjectMethodTable(); AddContinuationMethodTableGlobal(); + AddTypedReferenceMethodTableGlobal(); AddObjectMethodTableGlobal(); } @@ -381,9 +410,15 @@ private void AddContinuationMethodTableGlobal() AddPointerGlobal("Address of Continuation Method Table", TestContinuationMethodTableGlobalAddress, 0); } + // Default to a null slot; tests that need a real System.TypedReference MT call + // SetTypedReferenceMethodTable to point the global at one. + private void AddTypedReferenceMethodTableGlobal() + { + AddPointerGlobal("Address of TypedReference Method Table", TestTypedReferenceMethodTableGlobalAddress, 0); + } + private void AddObjectMethodTableGlobal() { - // Initialized to 0; patched to point to SystemObjectMethodTable in AddSystemObjectType. AddPointerGlobal("Address of Object Method Table", TestObjectMethodTableGlobalAddress, 0); } @@ -402,9 +437,7 @@ private void AddSystemObjectType() SystemObjectEEClass.MethodTable = SystemObjectMethodTable.Address; SystemObjectMethodTable.EEClassOrCanonMT = SystemObjectEEClass.Address; - // Patch the ObjectMethodTable global to point to System.Object's MethodTable. - Span globalAddrBytes = Builder.BorrowAddressRange(TestObjectMethodTableGlobalAddress, Builder.TargetTestHelpers.PointerSize); - Builder.TargetTestHelpers.WritePointer(globalAddrBytes, SystemObjectMethodTable.Address); + SetObjectMethodTable(SystemObjectMethodTable.Address); } private void AddContinuationType() @@ -439,6 +472,22 @@ internal void SetContinuationMethodTable(ulong continuationMethodTable) Builder.TargetTestHelpers.WritePointer(globalAddrBytes, continuationMethodTable); } + internal void SetObjectMethodTable(ulong objectMethodTable) + { + Span globalAddrBytes = Builder.BorrowAddressRange(TestObjectMethodTableGlobalAddress, Builder.TargetTestHelpers.PointerSize); + Builder.TargetTestHelpers.WritePointer(globalAddrBytes, objectMethodTable); + } + + // Repoints the TypedReferenceMethodTable global at the given MT. Tests that + // exercise TypedByRef arg/return handling first allocate a value-type MT + // matching System.TypedReference's layout ({ ref byte, IntPtr }) and then + // call this to install it as the well-known target. + internal void SetTypedReferenceMethodTable(ulong typedReferenceMethodTable) + { + Span globalAddrBytes = Builder.BorrowAddressRange(TestTypedReferenceMethodTableGlobalAddress, Builder.TargetTestHelpers.PointerSize); + Builder.TargetTestHelpers.WritePointer(globalAddrBytes, typedReferenceMethodTable); + } + internal MockEEClass AddEEClass(string name) => Add(EEClassLayout, $"EEClass '{name}'"); diff --git a/src/native/managed/cdac/tests/RuntimeTypeSystemGetVectorSizeTests.cs b/src/native/managed/cdac/tests/RuntimeTypeSystemGetVectorSizeTests.cs new file mode 100644 index 00000000000000..605d36e1a24afb --- /dev/null +++ b/src/native/managed/cdac/tests/RuntimeTypeSystemGetVectorSizeTests.cs @@ -0,0 +1,109 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.Tests; + +public class RuntimeTypeSystemGetVectorSizeTests +{ + private static readonly Lazy s_syntheticMetadata = new(SyntheticVectorMetadata.Create); + + [Theory] + [InlineData("Vector64`1", 8)] + [InlineData("Vector128`1", 16)] + public void GetVectorSize_KnownIntrinsic_ReturnsSize(string typeName, int expectedSize) + { + SyntheticVectorMetadata metadata = s_syntheticMetadata.Value; + (Target target, TypeHandle handle) = CreateTarget( + metadata, + rts => MockDescriptors.CallingConvention.AddVectorMethodTable( + rts, + typeName, + expectedSize, + metadata.GetTypeDefToken(typeName))); + + Assert.Equal(expectedSize, target.Contracts.RuntimeTypeSystem.GetVectorSize(handle)); + } + + [Fact] + public void GetVectorSize_NotIntrinsicType_ReturnsZero() + { + (Target target, TypeHandle handle) = CreateTarget( + s_syntheticMetadata.Value, + rts => MockDescriptors.CallingConvention.AddValueTypeMethodTable(rts, "Vector128`1", structSize: 16, fields: [])); + + Assert.Equal(0, target.Contracts.RuntimeTypeSystem.GetVectorSize(handle)); + } + + [Fact] + public void GetVectorSize_NoMetadata_ReturnsZero() + { + SyntheticVectorMetadata metadata = s_syntheticMetadata.Value; + (Target target, TypeHandle handle) = CreateTarget( + syntheticMetadata: null, + rts => MockDescriptors.CallingConvention.AddVectorMethodTable( + rts, + "Vector128`1", + 16, + metadata.GetTypeDefToken("Vector128`1"))); + + Assert.Equal(0, target.Contracts.RuntimeTypeSystem.GetVectorSize(handle)); + } + + [Fact] + public void GetVectorSize_UnhandledIntrinsicName_ReturnsZero() + { + // Use Vector128 metadata/token but give it a name the runtime doesn't + // recognize. The real Vector128 token resolves to the correct TypeDef row + // whose name IS recognized, so we need a type name the runtime won't match. + // The simplest approach: build a one-off metadata image with a fake type. + SyntheticVectorMetadata fakeMetadata = SyntheticVectorMetadata.CreateWithExtraType( + "System.Runtime.Intrinsics", "FakeVector`1"); + (Target target, TypeHandle handle) = CreateTarget( + fakeMetadata, + rts => MockDescriptors.CallingConvention.AddVectorMethodTable( + rts, + "FakeVector`1", + 32, + fakeMetadata.GetTypeDefToken("FakeVector`1"))); + + Assert.Equal(0, target.Contracts.RuntimeTypeSystem.GetVectorSize(handle)); + } + + [Fact] + public void GetVectorSize_SystemNumericsVector_ReturnsFieldBytes() + { + SyntheticVectorMetadata metadata = s_syntheticMetadata.Value; + (Target target, TypeHandle handle) = CreateTarget( + metadata, + rts => MockDescriptors.CallingConvention.AddVectorMethodTable( + rts, + "Vector`1", + 32, + metadata.GetTypeDefToken("Vector`1"))); + + Assert.Equal(32, target.Contracts.RuntimeTypeSystem.GetVectorSize(handle)); + } + + private static (Target Target, TypeHandle Handle) CreateTarget( + SyntheticVectorMetadata? syntheticMetadata, + Func addMethodTable) + { + ulong methodTableAddress = 0; + (Target target, _) = CallingConventionTestHelpers.CreateTargetWithMethod( + CallConvCases.AMD64Windows, + hasThis: false, + (rts, sig) => + { + MockMethodTable methodTable = addMethodTable(rts); + methodTableAddress = methodTable.Address; + sig.Return(CorElementType.Void); + }, + syntheticMetadata: syntheticMetadata); + + return (target, target.Contracts.RuntimeTypeSystem.GetTypeHandle(new TargetPointer(methodTableAddress))); + } +}