diff --git a/docs/design/datacontracts/CallingConvention.md b/docs/design/datacontracts/CallingConvention.md new file mode 100644 index 00000000000000..8111fccdaea261 --- /dev/null +++ b/docs/design/datacontracts/CallingConvention.md @@ -0,0 +1,41 @@ +# Contract CallingConvention + +This contract walks a method's argument signature using the runtime's +calling-convention rules so consumers can locate each argument on the +caller's transition frame and reason about which slots hold GC references. + +The actual ABI (which registers hold which arguments, what alignment and +padding rules apply, how structs are promoted to registers vs spilled, how +varargs are passed, etc.) is documented in the CLR ABI specs and is not +re-described here: + +- [Common CLR ABI conventions](../coreclr/botr/clr-abi.md) + +This contract's responsibility is to surface the *result* of that walk in +a form the cDAC can use, byte-for-byte compatible with what the runtime +itself produces. + +## APIs of contract + +``` csharp +// Encode the argument GCRefMap blob for `methodDesc` byte-for-byte +// compatible with the runtime's ComputeCallRefMap (frames.cpp). +// Returns false when this contract declines to encode the method +// (e.g. an unported ABI path); callers should map false to E_NOTIMPL. +// When false, the value of `blob` is unspecified. +bool TryComputeArgGCRefMapBlob(MethodDescHandle methodDesc, out byte[] blob); +``` + +## Version 1 + +The single API is implemented by walking the shared `ArgIterator` +(`src/coreclr/tools/Common/CallingConvention/ArgIterator.cs`) and feeding +the per-argument result into a GCRefMap encoder that mirrors +`GCRefMapBuilder` (`src/coreclr/inc/gcrefmap.h`). + +`TryComputeArgGCRefMapBlob` returns `false` for any method whose +signature, ABI path, or generic context the encoder hasn't been taught +yet. The cdacstress harness (`src/coreclr/vm/cdacstress.cpp`, +`ARGITER` sub-check) uses byte-for-byte comparison of the returned blob +against the runtime's `ComputeCallRefMap` output as its correctness +oracle. diff --git a/docs/design/datacontracts/RuntimeTypeSystem.md b/docs/design/datacontracts/RuntimeTypeSystem.md index 204aa45b751eb7..f0ab93f4485b7a 100644 --- a/docs/design/datacontracts/RuntimeTypeSystem.md +++ b/docs/design/datacontracts/RuntimeTypeSystem.md @@ -73,6 +73,8 @@ partial interface IRuntimeTypeSystem : IContract public virtual bool IsObjRef(TypeHandle typeHandle); // True if the MethodTable represents a type that contains managed references public virtual bool ContainsGCPointers(TypeHandle typeHandle); + // True if the MethodTable represents a byref-like value type (Span, ReadOnlySpan, any ref struct). + public virtual bool IsByRefLike(TypeHandle typeHandle); // True if the type requires 8-byte alignment on platforms that don't 8-byte align by default (FEATURE_64BIT_ALIGNMENT) public virtual bool RequiresAlign8(TypeHandle typeHandle); // True if the MethodTable represents a continuation type used by the async continuation feature @@ -290,6 +292,10 @@ partial interface IRuntimeTypeSystem : IContract // Return true if the method is a wrapper stub (unboxing or instantiating). public virtual bool IsWrapperStub(MethodDescHandle methodDesc); + // Return true if the method is an unboxing stub (a wrapper around a + // value-type instance method that unboxes `this` before forwarding). + public virtual bool IsUnboxingStub(MethodDescHandle methodDesc); + } ``` @@ -302,6 +308,7 @@ bool IsFieldDescStatic(TargetPointer fieldDescPointer); bool IsFieldDescRVA(TargetPointer fieldDescPointer); uint GetFieldDescType(TargetPointer fieldDescPointer); uint GetFieldDescOffset(TargetPointer fieldDescPointer, FieldDefinition? fieldDef); +TypeHandle GetFieldDescApproxTypeHandle(TargetPointer fieldDescPointer); TargetPointer GetFieldDescStaticAddress(TargetPointer fieldDescPointer, bool unboxValueTypes = true); TargetPointer GetFieldDescThreadStaticAddress(TargetPointer fieldDescPointer, TargetPointer thread, bool unboxValueTypes = true); ``` @@ -330,6 +337,8 @@ internal partial struct RuntimeTypeSystem_1 GenericsMask_SharedInst = 0x00000020, // shared instantiation, e.g. List<__Canon> or List> GenericsMask_TypicalInstantiation = 0x00000030, // the type instantiated at its formal parameters, e.g. List + IsByRefLike = 0x00001000, // value type that may contain managed pointers (e.g. Span, ReadOnlySpan) + StringArrayValues = GenericsMask_NonGeneric, } @@ -404,6 +413,7 @@ internal partial struct RuntimeTypeSystem_1 public bool IsTrackedReferenceWithFinalizer => GetFlag(WFLAGS_HIGH.IsTrackedReferenceWithFinalizer) != 0; public bool IsGenericTypeDefinition => TestFlagWithMask(WFLAGS_LOW.GenericsMask, WFLAGS_LOW.GenericsMask_TypicalInstantiation); public bool IsSharedByGenericInstantiations => TestFlagWithMask(WFLAGS_LOW.GenericsMask, WFLAGS_LOW.GenericsMask_SharedInst); + public bool IsByRefLike => TestFlagWithMask(WFLAGS_LOW.IsByRefLike, WFLAGS_LOW.IsByRefLike); } [Flags] @@ -663,6 +673,8 @@ Contracts used: public bool ContainsGCPointers(TypeHandle TypeHandle) => !typeHandle.IsMethodTable() ? false : _methodTables[TypeHandle.Address].Flags.ContainsGCPointers; + public bool IsByRefLike(TypeHandle typeHandle) => typeHandle.IsMethodTable() && _methodTables[typeHandle.Address].Flags.IsByRefLike; + public bool RequiresAlign8(TypeHandle typeHandle) => !typeHandle.IsMethodTable() ? false : _methodTables[typeHandle.Address].Flags.RequiresAlign8; public bool IsCanonicalMethodTable(TypeHandle typeHandle) @@ -1867,6 +1879,17 @@ Determining if a method is a wrapper stub (unboxing or instantiating): } ``` +Determining if a method is an unboxing stub. An unboxing stub is a wrapper +around a value-type instance method whose `this` is a boxed object: the +stub unboxes `this` and forwards to the real instance method. The bit is +stored in `MethodDescFlags3` and surfaces as the `IsUnboxingStub` flag on +`MethodDesc`: + +```csharp + public bool IsUnboxingStub(MethodDescHandle methodDescHandle) + => _methodDescs[methodDescHandle.Address].IsUnboxingStub; +``` + Extracting a pointer to the `MethodDescVersioningState` data for a given method ```csharp @@ -2227,6 +2250,15 @@ TargetPointer GetFieldDescThreadStaticAddress(TargetPointer fieldDescPointer, Ta // Uses GetGCThreadStaticsBasePointer / GetNonGCThreadStaticsBasePointer. // The unboxValueTypes parameter behaves the same as in GetFieldDescStaticAddress. } + +TypeHandle GetFieldDescApproxTypeHandle(TargetPointer fieldDescPointer) +{ + // Resolve enclosing MT -> Module -> MetadataReader, decode the field's + // signature using the SignatureDecoder contract with a SignatureTypeProvider + // bound to the enclosing class as generic context, and return the resulting + // TypeHandle. Returns TypeHandle.Null if any link in the chain is unavailable + // (e.g. uncached constructed instantiation). +} ``` ### Other APIs diff --git a/eng/pipelines/runtime-diagnostics.yml b/eng/pipelines/runtime-diagnostics.yml index e5c4632258231c..975e9005c51a38 100644 --- a/eng/pipelines/runtime-diagnostics.yml +++ b/eng/pipelines/runtime-diagnostics.yml @@ -70,6 +70,7 @@ parameters: type: object default: - windows_x64 + - windows_x86 - linux_x64 - windows_arm64 - linux_arm64 diff --git a/src/coreclr/inc/clrconfigvalues.h b/src/coreclr/inc/clrconfigvalues.h index cf3d63e0b3757a..82f0f212713cf3 100644 --- a/src/coreclr/inc/clrconfigvalues.h +++ b/src/coreclr/inc/clrconfigvalues.h @@ -749,7 +749,7 @@ CONFIG_STRING_INFO(INTERNAL_PrestubHalt, W("PrestubHalt"), "") RETAIL_CONFIG_STRING_INFO(EXTERNAL_RestrictedGCStressExe, W("RestrictedGCStressExe"), "") RETAIL_CONFIG_DWORD_INFO(INTERNAL_CdacStressFailFast, W("CdacStressFailFast"), 0, "If nonzero, assert on cDAC/runtime GC ref mismatch during cDAC stress verification.") RETAIL_CONFIG_STRING_INFO(INTERNAL_CdacStressLogFile, W("CdacStressLogFile"), "Log file path for cDAC stress verification results.") -RETAIL_CONFIG_DWORD_INFO(INTERNAL_CdacStress, W("CdacStress"), 0, "Enable cDAC stress verification. Bit flags: 0x1=alloc points, 0x200=verbose per-ref diagnostics.") +RETAIL_CONFIG_DWORD_INFO(INTERNAL_CdacStress, W("CdacStress"), 0, "Enable cDAC stress verification.") CONFIG_DWORD_INFO(INTERNAL_ReturnSourceTypeForTesting, W("ReturnSourceTypeForTesting"), 0, "Allows returning the (internal only) source type of an IL to Native mapping for debugging purposes") RETAIL_CONFIG_DWORD_INFO(UNSUPPORTED_RSStressLog, W("RSStressLog"), 0, "Allows turning on logging for RS startup") CONFIG_DWORD_INFO(INTERNAL_SBDumpOnNewIndex, W("SBDumpOnNewIndex"), 0, "Used for Syncblock debugging. It's been a while since any of those have been used.") diff --git a/src/coreclr/inc/dacprivate.h b/src/coreclr/inc/dacprivate.h index 19453dc8608663..da35d7abaeb7ff 100644 --- a/src/coreclr/inc/dacprivate.h +++ b/src/coreclr/inc/dacprivate.h @@ -65,12 +65,33 @@ enum DACSTACKPRIV_REQUEST_FRAME_DATA = 0xf0000000 }; +#ifdef CDAC_STRESS // Private requests for the cDAC stress harness. enum { - DACSTRESSPRIV_REQUEST_FLUSH_TARGET_STATE = 0xf2000000 + DACSTRESSPRIV_REQUEST_FLUSH_TARGET_STATE = 0xf2000000, + DACSTRESSPRIV_REQUEST_COMPUTE_ARG_GCREFMAP = 0xf2000001 }; +// In/out request descriptor for DACSTRESSPRIV_REQUEST_COMPUTE_ARG_GCREFMAP. +// outBuffer is unused; the caller-allocated blob destination + size are +// carried by this struct, and the handler writes cbFilled and cbNeeded in +// place. +// S_OK blob fit; cbFilled bytes written to *BlobBuffer; cbNeeded == cbFilled. +// HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER) cbFilled = 0, cbNeeded = required size; *BlobBuffer untouched. +// E_NOTIMPL encoder declined this MD (bucketed as ARG_SKIP). +// E_FAIL encoder threw (bucketed as ARG_ERROR). +// E_INVALIDARG bad inBuffer. +struct DacStressArgGCRefMapRequest +{ + CLRDATA_ADDRESS MethodDesc; // [in] + CLRDATA_ADDRESS BlobBuffer; // [in] caller-allocated destination (in-proc pointer) + ULONG32 BlobBufferLen; // [in] capacity at BlobBuffer + ULONG32 cbFilled; // [out] bytes actually written to *BlobBuffer + ULONG32 cbNeeded; // [out] total bytes the blob requires +}; +#endif // CDAC_STRESS + enum DacpObjectType { OBJ_STRING=0,OBJ_FREE,OBJ_OBJECT,OBJ_ARRAY,OBJ_OTHER }; struct MSLAYOUT DacpObjectData { diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/ArgIterator.cs b/src/coreclr/tools/Common/CallingConvention/ArgIterator.cs similarity index 88% rename from src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/ArgIterator.cs rename to src/coreclr/tools/Common/CallingConvention/ArgIterator.cs index de2f97852565ea..76cf68a550c3a2 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/ArgIterator.cs +++ b/src/coreclr/tools/Common/CallingConvention/ArgIterator.cs @@ -2,21 +2,23 @@ // The .NET Foundation licenses this file to you under the MIT license. // Provides an abstraction over platform specific calling conventions (specifically, the calling convention -// utilized by the JIT on that platform). The caller enumerates each argument of a signature in turn, and is +// utilized by the JIT on that platform). The caller enumerates each argument of a signature in turn, and is // provided with information mapping that argument into registers and/or stack locations. +#nullable disable + using System; using System.Diagnostics; using Internal.JitInterface; -using Internal.NativeFormat; -using Internal.TypeSystem; using Internal.CorConstants; -using Internal; -using ILCompiler.DependencyAnalysis.Wasm; +using Internal.TypeSystem; +#if READYTORUN +using Internal.NativeFormat; +#endif -namespace ILCompiler.DependencyAnalysis.ReadyToRun +namespace Internal.CallingConvention { public enum CORCOMPILE_GCREFMAP_TOKENS : byte { @@ -36,175 +38,6 @@ public enum CallingConventions /*FastCall, CDecl */ } - internal struct TypeHandle - { - public TypeHandle(TypeDesc type) - { - _type = type; - _isByRef = _type.IsByRef; - if (_isByRef) - { - _type = ((ByRefType)_type).ParameterType; - } - } - - private readonly TypeDesc _type; - private readonly bool _isByRef; - - public bool Equals(TypeHandle other) - { - return _isByRef == other._isByRef && _type == other._type; - } - - public override int GetHashCode() { return (int)_type.GetHashCode(); } - - public bool IsNull() { return _type == null && !_isByRef; } - public bool IsValueType() { if (_isByRef) return false; return _type.IsValueType; } - public bool IsPointerType() { if (_isByRef) return false; return _type.IsPointer; } - - public bool HasIndeterminateSize() { return IsValueType() && ((DefType)_type).InstanceFieldSize.IsIndeterminate; } - - public int PointerSize => _type.Context.Target.PointerSize; - - public int GetSize() - { - if (IsValueType()) - return ((DefType)_type).InstanceFieldSize.AsInt; - else - return PointerSize; - } - - public bool RequiresAlign8() - { - if (_type.Context.Target.Architecture != TargetArchitecture.ARM) - { - return false; - } - if (_isByRef) - { - return false; - } - return _type.RequiresAlign8(); - } - - public bool IsHomogeneousAggregate() - { - TargetArchitecture targetArch = _type.Context.Target.Architecture; - if ((targetArch != TargetArchitecture.ARM) && (targetArch != TargetArchitecture.ARM64)) - { - return false; - } - if (_isByRef) - { - return false; - } - return _type is DefType defType && defType.IsHomogeneousAggregate; - } - - public int GetHomogeneousAggregateElementSize() - { - Debug.Assert(IsHomogeneousAggregate()); - switch (_type.Context.Target.Architecture) - { - case TargetArchitecture.ARM: - return RequiresAlign8() ? 8 : 4; - - case TargetArchitecture.ARM64: - return ((DefType)_type).GetHomogeneousAggregateElementSize(); - } - throw new InvalidOperationException(); - } - - public CorElementType GetCorElementType() - { - if (_isByRef) - { - return CorElementType.ELEMENT_TYPE_BYREF; - } - - Internal.TypeSystem.TypeFlags category = _type.UnderlyingType.Category; - // We use the UnderlyingType to handle Enums properly - return category switch - { - Internal.TypeSystem.TypeFlags.Boolean => CorElementType.ELEMENT_TYPE_BOOLEAN, - Internal.TypeSystem.TypeFlags.Char => CorElementType.ELEMENT_TYPE_CHAR, - Internal.TypeSystem.TypeFlags.SByte => CorElementType.ELEMENT_TYPE_I1, - Internal.TypeSystem.TypeFlags.Byte => CorElementType.ELEMENT_TYPE_U1, - Internal.TypeSystem.TypeFlags.Int16 => CorElementType.ELEMENT_TYPE_I2, - Internal.TypeSystem.TypeFlags.UInt16 => CorElementType.ELEMENT_TYPE_U2, - Internal.TypeSystem.TypeFlags.Int32 => CorElementType.ELEMENT_TYPE_I4, - Internal.TypeSystem.TypeFlags.UInt32 => CorElementType.ELEMENT_TYPE_U4, - Internal.TypeSystem.TypeFlags.Int64 => CorElementType.ELEMENT_TYPE_I8, - Internal.TypeSystem.TypeFlags.UInt64 => CorElementType.ELEMENT_TYPE_U8, - Internal.TypeSystem.TypeFlags.IntPtr => CorElementType.ELEMENT_TYPE_I, - Internal.TypeSystem.TypeFlags.UIntPtr => CorElementType.ELEMENT_TYPE_U, - Internal.TypeSystem.TypeFlags.Single => CorElementType.ELEMENT_TYPE_R4, - Internal.TypeSystem.TypeFlags.Double => CorElementType.ELEMENT_TYPE_R8, - Internal.TypeSystem.TypeFlags.ValueType => CorElementType.ELEMENT_TYPE_VALUETYPE, - Internal.TypeSystem.TypeFlags.Nullable => CorElementType.ELEMENT_TYPE_VALUETYPE, - Internal.TypeSystem.TypeFlags.Void => CorElementType.ELEMENT_TYPE_VOID, - Internal.TypeSystem.TypeFlags.Pointer => CorElementType.ELEMENT_TYPE_PTR, - Internal.TypeSystem.TypeFlags.FunctionPointer => CorElementType.ELEMENT_TYPE_FNPTR, - - _ => CorElementType.ELEMENT_TYPE_CLASS - }; - } - - private static int[] s_elemSizes = new int[] - { - 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 int GetElemSize(CorElementType t, TypeHandle thValueType) - { - if (((int)t) <= 0x1d) - { - int elemSize = s_elemSizes[(int)t]; - if (elemSize == -1) - { - return (int)thValueType.GetSize(); - } - if (elemSize == -2) - { - return thValueType.PointerSize; - } - return elemSize; - } - return 0; - } - - public TypeDesc GetRuntimeTypeHandle() { return _type; } - } - // Describes how a single argument is laid out in registers and/or stack locations when given as an input to a // managed method as part of a larger signature. // @@ -248,7 +81,7 @@ public void Init() m_byteStackIndex = -1; m_byteStackSize = 0; m_floatFlags = 0; - m_structFields = new FpStructInRegistersInfo(); + m_structFields = default; m_fRequires64BitAlignment = false; } @@ -263,7 +96,7 @@ internal readonly struct ArgDestination private readonly TransitionBlock _transitionBlock; // Offset of the argument relative to the base. On AMD64 on Unix, it can have a special - // value that represent a struct that contain both general purpose and floating point fields + // value that represent a struct that contain both general purpose and floating point fields // passed in registers. private readonly int _offset; @@ -285,7 +118,7 @@ public void GcMark(CORCOMPILE_GCREFMAP_TOKENS[] frame, int delta, bool interior) } // Returns true if the ArgDestination represents a homogeneous aggregate struct - bool IsHomogeneousAggregate() + private bool IsHomogeneousAggregate() { return _argLocDescForStructInRegs.HasValue; } @@ -314,14 +147,14 @@ private int GetStructGenRegDestinationAddress() // fn - promotion function to apply to each managed object pointer // sc - scan context to pass to the promotion function // fieldBytes - size of the structure - internal void ReportPointersFromStructInRegisters(TypeDesc type, int delta, CORCOMPILE_GCREFMAP_TOKENS[] frame) + internal void ReportPointersFromStructInRegisters(ITypeHandle type, int delta, CORCOMPILE_GCREFMAP_TOKENS[] frame) { Debug.Assert(IsStructPassedInRegs()); int genRegDest = GetStructGenRegDestinationAddress(); SYSTEMV_AMD64_CORINFO_STRUCT_REG_PASSING_DESCRIPTOR descriptor; - SystemVStructClassificator.GetSystemVAmd64PassStructInRegisterDescriptor(type, out descriptor); + type.GetSystemVAmd64PassStructInRegisterDescriptor(out descriptor); for (int i = 0; i < descriptor.eightByteCount; i++) { @@ -346,12 +179,12 @@ internal void ReportPointersFromStructInRegisters(TypeDesc type, int delta, CORC } } - internal class ArgIteratorData + internal class ArgIteratorData where TTypeHandle : ITypeHandle { public ArgIteratorData(bool hasThis, bool isVarArg, - TypeHandle[] parameterTypes, - TypeHandle returnType) + TTypeHandle[] parameterTypes, + TTypeHandle returnType) { _hasThis = hasThis; _isVarArg = isVarArg; @@ -361,15 +194,15 @@ public ArgIteratorData(bool hasThis, private bool _hasThis; private bool _isVarArg; - private TypeHandle[] _parameterTypes; - private TypeHandle _returnType; + private TTypeHandle[] _parameterTypes; + private TTypeHandle _returnType; public override bool Equals(object obj) { if (this == obj) return true; - ArgIteratorData other = obj as ArgIteratorData; + ArgIteratorData other = obj as ArgIteratorData; if (other == null) return false; @@ -391,9 +224,19 @@ public override bool Equals(object obj) public override int GetHashCode() { +#if READYTORUN return 37 + (_parameterTypes == null ? _returnType.GetHashCode() : VersionResilientHashCode.GenericInstanceHashCode(_returnType.GetHashCode(), _parameterTypes)); +#else + int hashcode = 37 + _returnType.GetHashCode(); + if (_parameterTypes != null) + { + for (int i = 0; i < _parameterTypes.Length; i++) + hashcode = hashcode * 31 + _parameterTypes[i].GetHashCode(); + } + return hashcode; +#endif } public bool HasThis() { return _hasThis; } @@ -401,21 +244,21 @@ public override int GetHashCode() public int NumFixedArgs() { return _parameterTypes != null ? _parameterTypes.Length : 0; } // Argument iteration. - public CorElementType GetArgumentType(int argNum, out TypeHandle thArgType) + public CorElementType GetArgumentType(int argNum, out TTypeHandle thArgType) { thArgType = _parameterTypes[argNum]; CorElementType returnValue = thArgType.GetCorElementType(); return returnValue; } - public TypeHandle GetByRefArgumentType(int argNum) + public TTypeHandle GetByRefArgumentType(int argNum) { return (argNum < _parameterTypes.Length && _parameterTypes[argNum].GetCorElementType() == CorElementType.ELEMENT_TYPE_BYREF) ? _parameterTypes[argNum] : - default(TypeHandle); + default; } - public CorElementType GetReturnType(out TypeHandle thRetType) + public CorElementType GetReturnType(out TTypeHandle thRetType) { thRetType = _returnType; return thRetType.GetCorElementType(); @@ -430,28 +273,29 @@ public CorElementType GetReturnType(out TypeHandle thRetType) // performance critical code. // // The ARGITERATOR_BASE argument of the template is provider of the parsed - // method signature. Typically, the arg iterator works on top of MetaSig. + // method signature. Typically, the arg iterator works on top of MetaSig. // Reflection invoke uses alternative implementation to save signature parsing // time because of it has the parsed signature available. //----------------------------------------------------------------------- //template - internal struct ArgIterator + internal struct ArgIterator where TTypeHandle : ITypeHandle { - private readonly TypeSystemContext _context; - private readonly TransitionBlock _transitionBlock; private bool _hasThis; private bool _hasParamType; private bool _hasAsyncContinuation; private bool _extraFunctionPointerArg; - private ArgIteratorData _argData; + private ArgIteratorData _argData; private bool[] _forcedByRefParams; private bool _skipFirstArg; private bool _extraObjectFirstArg; private CallingConventions _interpreterCallingConvention; private bool _hasArgLocDescForStructInRegs; private ArgLocDesc _argLocDescForStructInRegs; + private TTypeHandle _objectTypeHandle; + private TTypeHandle _intPtrTypeHandle; + private bool _isWindows; public bool HasThis => _hasThis; public bool IsVarArg => _argData.IsVarArg(); @@ -460,13 +304,13 @@ internal struct ArgIterator public int NumFixedArgs => _argData.NumFixedArgs() + (_extraFunctionPointerArg ? 1 : 0) + (_extraObjectFirstArg ? 1 : 0); // Argument iteration. - public CorElementType GetArgumentType(int argNum, out TypeHandle thArgType, out bool forceByRefReturn) + public CorElementType GetArgumentType(int argNum, out TTypeHandle thArgType, out bool forceByRefReturn) { forceByRefReturn = false; if (_extraObjectFirstArg && argNum == 0) { - thArgType = new TypeHandle(_context.GetWellKnownType(WellKnownType.Object)); + thArgType = _objectTypeHandle; return CorElementType.ELEMENT_TYPE_CLASS; } @@ -478,14 +322,14 @@ public CorElementType GetArgumentType(int argNum, out TypeHandle thArgType, out if (_extraFunctionPointerArg && argNum == _argData.NumFixedArgs()) { - thArgType = new TypeHandle(_context.GetWellKnownType(WellKnownType.IntPtr)); + thArgType = _intPtrTypeHandle; return CorElementType.ELEMENT_TYPE_I; } return _argData.GetArgumentType(argNum, out thArgType); } - public CorElementType GetReturnType(out TypeHandle thRetType, out bool forceByRefReturn) + public CorElementType GetReturnType(out TTypeHandle thRetType, out bool forceByRefReturn) { if (_forcedByRefParams != null && _forcedByRefParams.Length > 0) forceByRefReturn = _forcedByRefParams[0]; @@ -498,7 +342,7 @@ public CorElementType GetReturnType(out TypeHandle thRetType, out bool forceByRe public void Reset() { _argType = default(CorElementType); - _argTypeHandle = default(TypeHandle); + _argTypeHandle = default; _argSize = 0; _argNum = 0; _argForceByRef = false; @@ -510,18 +354,21 @@ public void Reset() // Constructor //------------------------------------------------------------ public ArgIterator( - TypeSystemContext context, - ArgIteratorData argData, - CallingConventions callConv, + TransitionBlock transitionBlock, + ArgIteratorData argData, + CallingConventions callConv, bool hasParamType, bool hasAsyncContinuation, - bool extraFunctionPointerArg, - bool[] forcedByRefParams, - bool skipFirstArg, - bool extraObjectFirstArg) + bool extraFunctionPointerArg, + bool[] forcedByRefParams, + bool skipFirstArg, + bool extraObjectFirstArg, + bool isWindows, + TTypeHandle objectTypeHandle, + TTypeHandle intPtrTypeHandle) { - this = default(ArgIterator); - _context = context; + this = default(ArgIterator); + _transitionBlock = transitionBlock; _argData = argData; _hasThis = callConv == CallingConventions.ManagedInstance; _hasParamType = hasParamType; @@ -531,7 +378,9 @@ public ArgIterator( _skipFirstArg = skipFirstArg; _extraObjectFirstArg = extraObjectFirstArg; _interpreterCallingConvention = callConv; - _transitionBlock = TransitionBlock.FromTarget(context.Target); + _isWindows = isWindows; + _objectTypeHandle = objectTypeHandle; + _intPtrTypeHandle = intPtrTypeHandle; } private uint SizeOfArgStack() @@ -583,7 +432,7 @@ public uint CbStackPop() } } - // Is there a hidden parameter for the return parameter? + // Is there a hidden parameter for the return parameter? // public bool HasRetBuffArg() { @@ -805,7 +654,7 @@ public int GetAsyncContinuationArgOffset() // Each time this is called, this returns a byte offset of the next // argument from the TransitionBlock* pointer. This offset can be positive *or* negative. // - // Returns TransitionBlock::InvalidOffset once you've hit the end + // Returns TransitionBlock::InvalidOffset once you've hit the end // of the list. //------------------------------------------------------------ public int GetNextOffset() @@ -908,11 +757,11 @@ public int GetNextOffset() CorElementType argType = GetArgumentType(_argNum, out _argTypeHandle, out _argForceByRef); - _argTypeHandleOfByRefParam = (argType == CorElementType.ELEMENT_TYPE_BYREF ? _argData.GetByRefArgumentType(_argNum) : default(TypeHandle)); + _argTypeHandleOfByRefParam = (argType == CorElementType.ELEMENT_TYPE_BYREF ? _argData.GetByRefArgumentType(_argNum) : default); _argNum++; - int argSize = TypeHandle.GetElemSize(argType, _argTypeHandle); + int argSize = ITypeHandle.GetElemSize(argType, _argTypeHandle); _argType = argType; _argSize = argSize; @@ -962,7 +811,7 @@ public int GetNextOffset() case CorElementType.ELEMENT_TYPE_VALUETYPE: { SYSTEMV_AMD64_CORINFO_STRUCT_REG_PASSING_DESCRIPTOR descriptor; - SystemVStructClassificator.GetSystemVAmd64PassStructInRegisterDescriptor(_argTypeHandle.GetRuntimeTypeHandle(), out descriptor); + _argTypeHandle.GetSystemVAmd64PassStructInRegisterDescriptor(out descriptor); if (descriptor.passedInRegisters) { @@ -988,7 +837,7 @@ public int GetNextOffset() // Check if we have enough registers available for the struct passing if ((cFPRegs + _x64UnixIdxFPReg <= TransitionBlock.X64UnixTransitionBlock.NUM_FLOAT_ARGUMENT_REGISTERS) && (cGenRegs + _x64UnixIdxGenReg) <= _transitionBlock.NumArgumentRegisters) { - _argLocDescForStructInRegs = new ArgLocDesc(); + _argLocDescForStructInRegs = default; _argLocDescForStructInRegs.m_cGenReg = (short)cGenRegs; _argLocDescForStructInRegs.m_cFloatReg = cFPRegs; _argLocDescForStructInRegs.m_idxGenReg = _x64UnixIdxGenReg; @@ -1080,7 +929,7 @@ public int GetNextOffset() int align; if (isValueType) { - align = Math.Clamp(((DefType)_argTypeHandle.GetRuntimeTypeHandle()).InstanceFieldAlignment.AsInt, 8, 16); + align = Math.Clamp(_argTypeHandle.GetFieldAlignment(), 8, 16); } else { @@ -1127,7 +976,7 @@ public int GetNextOffset() case CorElementType.ELEMENT_TYPE_VALUETYPE: { - // Value type case: extract the alignment requirement, note that this has to handle + // Value type case: extract the alignment requirement, note that this has to handle // the interop "native value types". fRequiresAlign64Bit = _argTypeHandle.RequiresAlign8(); @@ -1183,7 +1032,7 @@ public int GetNextOffset() { if ((_armWFPRegs & wAllocMask) == 0) { - // We found one, mark the register or registers as used. + // We found one, mark the register or registers as used. _armWFPRegs |= wAllocMask; // Indicate the registers used to the caller and return. @@ -1218,7 +1067,7 @@ public int GetNextOffset() if (fRequiresAlign64Bit) { // The argument requires 64-bit alignment. Align either the next general argument register if - // we have any left. See step C.3 in the algorithm in the ABI spec. + // we have any left. See step C.3 in the algorithm in the ABI spec. _armIdxGenReg = ALIGN_UP(_armIdxGenReg, 2); } @@ -1249,7 +1098,7 @@ public int GetNextOffset() if (fRequiresAlign64Bit) { // The argument requires 64-bit alignment. If it is going to be passed on the stack, align - // the next stack slot. See step C.6 in the algorithm in the ABI spec. + // the next stack slot. See step C.6 in the algorithm in the ABI spec. _armOfsStack = ALIGN_UP(_armOfsStack, _transitionBlock.PointerSize * 2); } @@ -1284,7 +1133,7 @@ public int GetNextOffset() // that are passed in FP argument registers if possible. if (_argTypeHandle.IsHomogeneousAggregate()) { - _argLocDescForStructInRegs = new ArgLocDesc(); + _argLocDescForStructInRegs = default; _argLocDescForStructInRegs.m_idxFloatReg = _arm64IdxFPReg; int haElementSize = _argTypeHandle.GetHomogeneousAggregateElementSize(); @@ -1347,7 +1196,7 @@ public int GetNextOffset() _arm64IdxGenReg += regSlots; return argOfsInner; } - else if (_context.Target.IsWindows && IsVarArg && (_arm64IdxGenReg < 8)) + else if (_isWindows && IsVarArg && (_arm64IdxGenReg < 8)) { // Address the Windows ARM64 varargs case where an arg is split between regs and stack. // This can happen in the varargs case because the first 64 bytes of the stack are loaded @@ -1421,8 +1270,7 @@ public int GetNextOffset() } else { - info = RiscVLoongArch64FpStruct.GetFpStructInRegistersInfo( - _argTypeHandle.GetRuntimeTypeHandle(), TargetArchitecture.RiscV64); + info = _argTypeHandle.GetFpStructInRegistersInfo(TargetArchitecture.RiscV64); if (info.flags != FpStruct.UseIntCallConv) { cFPRegs = ((info.flags & FpStruct.BothFloat) != 0) ? 2 : 1; @@ -1450,7 +1298,7 @@ public int GetNextOffset() if ((1 + _rvLa64IdxFPReg <= _transitionBlock.NumArgumentRegisters) && (1 + _rvLa64IdxGenReg <= _transitionBlock.NumArgumentRegisters)) { - _argLocDescForStructInRegs = new ArgLocDesc(); + _argLocDescForStructInRegs = default; _argLocDescForStructInRegs.m_idxFloatReg = _rvLa64IdxFPReg; _argLocDescForStructInRegs.m_cFloatReg = 1; @@ -1476,7 +1324,7 @@ public int GetNextOffset() if (info.flags != FpStruct.UseIntCallConv) { Debug.Assert((info.flags & (FpStruct.OnlyOne | FpStruct.BothFloat)) != 0); - _argLocDescForStructInRegs = new ArgLocDesc(); + _argLocDescForStructInRegs = default; _hasArgLocDescForStructInRegs = true; _argLocDescForStructInRegs.m_idxFloatReg = _rvLa64IdxFPReg; _argLocDescForStructInRegs.m_cFloatReg = cFPRegs; @@ -1525,14 +1373,14 @@ public int GetNextOffset() } } - public CorElementType GetArgType(out TypeHandle pTypeHandle) + public CorElementType GetArgType(out TTypeHandle pTypeHandle) { // LIMITED_METHOD_CONTRACT; pTypeHandle = _argTypeHandle; return _argType; } - public CorElementType GetByRefArgType(out TypeHandle pByRefArgTypeHandle) + public CorElementType GetByRefArgType(out TTypeHandle pByRefArgTypeHandle) { // LIMITED_METHOD_CONTRACT; pByRefArgTypeHandle = _argTypeHandleOfByRefParam; @@ -1591,7 +1439,7 @@ private void ForceSigWalk() int nArgs = NumFixedArgs; for (int i = (_skipFirstArg ? 1 : 0); i < nArgs; i++) { - TypeHandle thArgType; + TTypeHandle thArgType; bool argForcedToBeByref; CorElementType type = GetArgumentType(i, out thArgType, out argForcedToBeByref); if (argForcedToBeByref) @@ -1599,7 +1447,7 @@ private void ForceSigWalk() if (!_transitionBlock.IsArgumentInRegister(ref numRegistersUsed, type, thArgType)) { - int structSize = TypeHandle.GetElemSize(type, thArgType); + int structSize = ITypeHandle.GetElemSize(type, thArgType); nSizeOfArgStack += _transitionBlock.StackElemSize(structSize); @@ -1663,8 +1511,8 @@ private void ForceSigWalk() } else { - // All stack arguments take just one stack slot on AMD64 because of arguments bigger - // than a stack slot are passed by reference. + // All stack arguments take just one stack slot on AMD64 because of arguments bigger + // than a stack slot are passed by reference. stackElemSize = _transitionBlock.PointerSize; } } @@ -1722,7 +1570,7 @@ private void ForceSigWalk() { case TargetArchitecture.Wasm32: { - ArgLocDesc pLoc = new ArgLocDesc(); + ArgLocDesc pLoc = default; int byteArgSize = GetArgSize(); if (IsArgPassedByRef()) @@ -1735,7 +1583,7 @@ private void ForceSigWalk() { // LIMITED_METHOD_CONTRACT; - ArgLocDesc pLoc = new ArgLocDesc(); + ArgLocDesc pLoc = default; pLoc.m_fRequires64BitAlignment = _armRequires64BitAlignment; @@ -1778,7 +1626,7 @@ private void ForceSigWalk() { // LIMITED_METHOD_CONTRACT; - ArgLocDesc pLoc = new ArgLocDesc(); + ArgLocDesc pLoc = default; if (_transitionBlock.IsFloatArgumentRegisterOffset(argOffset)) { @@ -1829,7 +1677,7 @@ private void ForceSigWalk() // LIMITED_METHOD_CONTRACT; - ArgLocDesc pLoc = new ArgLocDesc(); + ArgLocDesc pLoc = default; if (_transitionBlock.IsFloatArgumentRegisterOffset(argOffset)) { @@ -1844,8 +1692,7 @@ private void ForceSigWalk() int byteArgSize = GetArgSize(); // Composites greater than 16bytes are passed by reference - TypeHandle dummy; - if (GetArgType(out dummy) == CorElementType.ELEMENT_TYPE_VALUETYPE && GetArgSize() > _transitionBlock.EnregisteredParamTypeMaxSize) + if (GetArgType(out _) == CorElementType.ELEMENT_TYPE_VALUETYPE && GetArgSize() > _transitionBlock.EnregisteredParamTypeMaxSize) { byteArgSize = _transitionBlock.PointerSize; } @@ -1880,13 +1727,13 @@ private void ForceSigWalk() if (argOffset == TransitionBlock.StructInRegsOffset) { - // We always already have argLocDesc for structs passed in registers, we + // We always already have argLocDesc for structs passed in registers, we // compute it in the GetNextOffset for those since it is always needed. Debug.Assert(false); return null; } - ArgLocDesc pLoc = new ArgLocDesc(); + ArgLocDesc pLoc = default; if (_transitionBlock.IsFloatArgumentRegisterOffset(argOffset)) { @@ -1932,8 +1779,8 @@ private void ForceSigWalk() // Cached information about last argument private CorElementType _argType; private int _argSize; - private TypeHandle _argTypeHandle; - private TypeHandle _argTypeHandleOfByRefParam; + private TTypeHandle _argTypeHandle; + private TTypeHandle _argTypeHandleOfByRefParam; private bool _argForceByRef; private int _x86OfsStack; // Current position of the stack iterator @@ -1974,7 +1821,7 @@ private void ForceSigWalk() private uint _returnedFpFieldOffset1st; private uint _returnedFpFieldOffset2nd; - /* ITERATION_STARTED = 0x0001, + /* ITERATION_STARTED = 0x0001, SIZE_OF_ARG_STACK_COMPUTED = 0x0002, RETURN_FLAGS_COMPUTED = 0x0004, RETURN_HAS_RET_BUFFER = 0x0008, // Cached value of HasRetBuffArg @@ -2011,7 +1858,7 @@ private enum AsyncContinuationLocation private void ComputeReturnFlags() { - TypeHandle thRetType; + TTypeHandle thRetType; CorElementType type = GetReturnType(out thRetType, out _RETURN_HAS_RET_BUFFER); if (!_RETURN_HAS_RET_BUFFER) diff --git a/src/coreclr/tools/Common/CallingConvention/FpStructInRegistersInfo.cs b/src/coreclr/tools/Common/CallingConvention/FpStructInRegistersInfo.cs new file mode 100644 index 00000000000000..7caaa939943e71 --- /dev/null +++ b/src/coreclr/tools/Common/CallingConvention/FpStructInRegistersInfo.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// RISC-V and LoongArch64 floating-point struct passing info. +// Extracted from Internal/Runtime/RiscVLoongArch64FpStruct.cs for standalone use. + +using System; + +namespace Internal.JitInterface +{ + [Flags] + public enum FpStruct + { + PosOnlyOne = 0, + PosBothFloat = 1, + PosFloatInt = 2, + PosIntFloat = 3, + PosSizeShift1st = 4, + PosSizeShift2nd = 6, + + UseIntCallConv = 0, + + OnlyOne = 1 << PosOnlyOne, + BothFloat = 1 << PosBothFloat, + FloatInt = 1 << PosFloatInt, + IntFloat = 1 << PosIntFloat, + SizeShift1stMask = 0b11 << PosSizeShift1st, + SizeShift2ndMask = 0b11 << PosSizeShift2nd, + } + + public struct FpStructInRegistersInfo + { + public FpStruct flags; + public uint offset1st; + public uint offset2nd; + + public uint SizeShift1st() { return (uint)((int)flags >> (int)FpStruct.PosSizeShift1st) & 0b11; } + public uint SizeShift2nd() { return (uint)((int)flags >> (int)FpStruct.PosSizeShift2nd) & 0b11; } + + public uint Size1st() { return 1u << (int)SizeShift1st(); } + public uint Size2nd() { return 1u << (int)SizeShift2nd(); } + } +} diff --git a/src/coreclr/tools/Common/CallingConvention/ITypeHandle.cs b/src/coreclr/tools/Common/CallingConvention/ITypeHandle.cs new file mode 100644 index 00000000000000..cb304e80f56484 --- /dev/null +++ b/src/coreclr/tools/Common/CallingConvention/ITypeHandle.cs @@ -0,0 +1,97 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Suppress analyzer warnings for crossgen2 code style when file-linked into cDAC +#pragma warning disable SA1001 // Commas should be followed by whitespace + +using Internal.CorConstants; +using Internal.JitInterface; +using Internal.TypeSystem; + +namespace Internal.CallingConvention +{ + /// + /// Abstraction over type information needed by ArgIterator and TransitionBlock + /// for calling convention computation. Implementations can be backed by crossgen2's + /// TypeDesc or by the cDAC's MethodTable reading. + /// + internal interface ITypeHandle + { + bool IsNull(); + bool IsValueType(); + bool IsPointerType(); + bool HasIndeterminateSize(); + int PointerSize { get; } + int GetSize(); + CorElementType GetCorElementType(); + bool RequiresAlign8(); + + // HFA - ARM/ARM64 + bool IsHomogeneousAggregate(); + int GetHomogeneousAggregateElementSize(); + + // SystemV AMD64 - x64 Unix struct classification + void GetSystemVAmd64PassStructInRegisterDescriptor(out SYSTEMV_AMD64_CORINFO_STRUCT_REG_PASSING_DESCRIPTOR descriptor); + + // RISC-V / LoongArch64 FP struct classification + FpStructInRegistersInfo GetFpStructInRegistersInfo(TargetArchitecture architecture); + + // x86 - trivial pointer-sized struct check for register passing + bool IsTrivialPointerSizedStruct(); + + // LoongArch64/Wasm alignment + int GetFieldAlignment(); + + private static readonly int[] s_elemSizes = new int[] + { + 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 + }; + + static int GetElemSize(CorElementType t, T thValueType) where T : ITypeHandle + { + if (((int)t) <= 0x1d) + { + int elemSize = s_elemSizes[(int)t]; + if (elemSize == -1) + { + return thValueType.GetSize(); + } + if (elemSize == -2) + { + return thValueType.PointerSize; + } + return elemSize; + } + return 0; + } + } +} diff --git a/src/coreclr/tools/Common/CallingConvention/SystemVAmd64PassingDescriptor.cs b/src/coreclr/tools/Common/CallingConvention/SystemVAmd64PassingDescriptor.cs new file mode 100644 index 00000000000000..951fc4a1825104 --- /dev/null +++ b/src/coreclr/tools/Common/CallingConvention/SystemVAmd64PassingDescriptor.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// System V AMD64 ABI struct passing classification types. +// Extracted from JitInterface/CorInfoTypes.cs for standalone use. +// See ABI spec: https://software.intel.com/sites/default/files/article/402129/mpx-linux64-abi.pdf + +namespace Internal.JitInterface +{ + public enum SystemVClassificationType : byte + { + SystemVClassificationTypeUnknown = 0, + SystemVClassificationTypeStruct = 1, + SystemVClassificationTypeNoClass = 2, + SystemVClassificationTypeMemory = 3, + SystemVClassificationTypeInteger = 4, + SystemVClassificationTypeIntegerReference = 5, + SystemVClassificationTypeIntegerByRef = 6, + SystemVClassificationTypeSSE = 7, + }; + + public struct SYSTEMV_AMD64_CORINFO_STRUCT_REG_PASSING_DESCRIPTOR + { + public const int CLR_SYSTEMV_MAX_EIGHTBYTES_COUNT_TO_PASS_IN_REGISTERS = 2; + public const int CLR_SYSTEMV_MAX_STRUCT_BYTES_TO_PASS_IN_REGISTERS = 16; + + public const int SYSTEMV_EIGHT_BYTE_SIZE_IN_BYTES = 8; + public const int SYSTEMV_MAX_NUM_FIELDS_IN_REGISTER_PASSED_STRUCT = 16; + + public byte _passedInRegisters; + public bool passedInRegisters { get { return _passedInRegisters != 0; } set { _passedInRegisters = value ? (byte)1 : (byte)0; } } + + public byte eightByteCount; + + public SystemVClassificationType eightByteClassifications0; + public SystemVClassificationType eightByteClassifications1; + + public byte eightByteSizes0; + public byte eightByteSizes1; + + public byte eightByteOffsets0; + public byte eightByteOffsets1; + }; +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/TransitionBlock.cs b/src/coreclr/tools/Common/CallingConvention/TransitionBlock.cs similarity index 89% rename from src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/TransitionBlock.cs rename to src/coreclr/tools/Common/CallingConvention/TransitionBlock.cs index 561b52a85d0534..bd2f6a39844017 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/TransitionBlock.cs +++ b/src/coreclr/tools/Common/CallingConvention/TransitionBlock.cs @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // Provides an abstraction over platform specific calling conventions (specifically, the calling convention -// utilized by the JIT on that platform). The caller enumerates each argument of a signature in turn, and is +// utilized by the JIT on that platform). The caller enumerates each argument of a signature in turn, and is // provided with information mapping that argument into registers and/or stack locations. using System; @@ -12,34 +12,29 @@ using Internal.CorConstants; using Internal.JitInterface; -namespace ILCompiler.DependencyAnalysis.ReadyToRun +namespace Internal.CallingConvention { internal abstract class TransitionBlock { - public static TransitionBlock FromTarget(TargetDetails target) + public static TransitionBlock FromTarget(TargetArchitecture arch, bool isWindows, bool isApplePlatform, bool isArmel) { - switch (target.Architecture) + switch (arch) { case TargetArchitecture.X86: return X86TransitionBlock.Instance; case TargetArchitecture.X64: - return target.OperatingSystem == TargetOS.Windows ? + return isWindows ? X64WindowsTransitionBlock.Instance : X64UnixTransitionBlock.Instance; case TargetArchitecture.ARM: - if (target.Abi == TargetAbi.NativeAotArmel) - { - return Arm32ElTransitionBlock.Instance; - } - else - { - return Arm32TransitionBlock.Instance; - } + return isArmel ? + Arm32ElTransitionBlock.Instance : + Arm32TransitionBlock.Instance; case TargetArchitecture.ARM64: - return target.IsApplePlatform ? + return isApplePlatform ? AppleArm64TransitionBlock.Instance : Arm64TransitionBlock.Instance; @@ -53,7 +48,7 @@ public static TransitionBlock FromTarget(TargetDetails target) return Wasm32TransitionBlock.Instance; default: - throw new NotImplementedException(target.Architecture.ToString()); + throw new NotImplementedException(arch.ToString()); } } @@ -107,7 +102,7 @@ public static TransitionBlock FromTarget(TargetDetails target) public abstract int OffsetOfFloatArgumentRegisters { get; } - public bool IsFloatArgumentRegisterOffset(int offset) => offset < 0; + public virtual bool IsFloatArgumentRegisterOffset(int offset) => offset < 0; public abstract int EnregisteredParamTypeMaxSize { get; } @@ -183,12 +178,12 @@ public int GetStackArgumentByteIndexFromOffset(int offset) /// to calling it for the "real" arguments. Pass in a typ of ELEMENT_TYPE_CLASS. /// /// - /// keeps track of the number of argument registers assigned previously. + /// keeps track of the number of argument registers assigned previously. /// The caller should initialize this variable to 0 - then each call will update it. /// /// parameter type /// Exact type info is used to check struct enregistration - public bool IsArgumentInRegister(ref int pNumRegistersUsed, CorElementType typ, TypeHandle thArgType) + public bool IsArgumentInRegister(ref int pNumRegistersUsed, CorElementType typ, ITypeHandle thArgType) { Debug.Assert(IsX86); @@ -230,44 +225,9 @@ public bool IsArgumentInRegister(ref int pNumRegistersUsed, CorElementType typ, return false; } - private bool IsTrivialPointerSizedStruct(TypeHandle thArgType) + private static bool IsTrivialPointerSizedStruct(ITypeHandle thArgType) { - Debug.Assert(IsX86); - Debug.Assert(thArgType.IsValueType()); - if (thArgType.GetSize() != 4) - { - // Type does not have trivial layout or has the wrong size. - return false; - } - TypeDesc typeOfEmbeddedField = null; - foreach (var field in thArgType.GetRuntimeTypeHandle().GetFields()) - { - if (field.IsStatic) - continue; - if (typeOfEmbeddedField != null) - { - // Type has more than one instance field - return false; - } - - typeOfEmbeddedField = field.FieldType; - } - - if ((typeOfEmbeddedField != null) && ((typeOfEmbeddedField.IsValueType) || (typeOfEmbeddedField.IsPointer))) - { - switch (typeOfEmbeddedField.UnderlyingType.Category) - { - case TypeFlags.IntPtr: - case TypeFlags.UIntPtr: - case TypeFlags.Int32: - case TypeFlags.UInt32: - case TypeFlags.Pointer: - return true; - case TypeFlags.ValueType: - return IsTrivialPointerSizedStruct(new TypeHandle(typeOfEmbeddedField)); - } - } - return false; + return thArgType.IsTrivialPointerSizedStruct(); } /// @@ -287,12 +247,12 @@ public bool IsArgPassedByRef(int size) /// /// Check whether an arg is automatically switched to passing by reference. - /// Note that this overload does not handle varargs. This method only works for + /// Note that this overload does not handle varargs. This method only works for /// valuetypes - true value types, primitives, enums and TypedReference. /// The method is only overridden to do something meaningful on X64, ARM64 and WASM. /// /// Type to analyze - public virtual bool IsArgPassedByRef(TypeHandle th) + public virtual bool IsArgPassedByRef(ITypeHandle th) { throw new NotImplementedException(Architecture.ToString()); } @@ -307,7 +267,7 @@ public virtual bool IsVarArgPassedByRef(int size) return size > EnregisteredParamTypeMaxSize; } - public void ComputeReturnValueTreatment(CorElementType type, TypeHandle thRetType, bool isVarArgMethod, out bool usesRetBuffer, out uint fpReturnSize, out uint returnedFpFieldOffset1st, out uint returnedFpFieldOffset2nd) + public void ComputeReturnValueTreatment(CorElementType type, ITypeHandle thRetType, bool isVarArgMethod, out bool usesRetBuffer, out uint fpReturnSize, out uint returnedFpFieldOffset1st, out uint returnedFpFieldOffset2nd) { usesRetBuffer = false; fpReturnSize = 0; @@ -348,7 +308,7 @@ public void ComputeReturnValueTreatment(CorElementType type, TypeHandle thRetTyp if ((Architecture == TargetArchitecture.X64) && IsX64UnixABI) { SYSTEMV_AMD64_CORINFO_STRUCT_REG_PASSING_DESCRIPTOR descriptor; - SystemVStructClassificator.GetSystemVAmd64PassStructInRegisterDescriptor(thRetType.GetRuntimeTypeHandle(), out descriptor); + thRetType.GetSystemVAmd64PassStructInRegisterDescriptor(out descriptor); if (descriptor.passedInRegisters) { @@ -410,8 +370,7 @@ public void ComputeReturnValueTreatment(CorElementType type, TypeHandle thRetTyp { if (IsLoongArch64 || IsRiscV64) { - FpStructInRegistersInfo info = RiscVLoongArch64FpStruct.GetFpStructInRegistersInfo( - thRetType.GetRuntimeTypeHandle(), Architecture); + FpStructInRegistersInfo info = thRetType.GetFpStructInRegistersInfo(Architecture); fpReturnSize = (uint)info.flags; returnedFpFieldOffset1st = info.offset1st; returnedFpFieldOffset2nd = info.offset2nd; @@ -477,7 +436,7 @@ public override int OffsetFromGCRefMapPos(int pos) } } - public override bool IsArgPassedByRef(TypeHandle th) => false; + public override bool IsArgPassedByRef(ITypeHandle th) => false; /// /// x86 is special as always @@ -505,7 +464,7 @@ internal abstract class X64TransitionBlock : TransitionBlock public override int PointerSize => 8; public override int FloatRegisterSize => 16; - public override bool IsArgPassedByRef(TypeHandle th) + public override bool IsArgPassedByRef(ITypeHandle th) { Debug.Assert(!th.IsNull()); Debug.Assert(th.IsValueType()); @@ -536,7 +495,7 @@ private sealed class X64WindowsTransitionBlock : X64TransitionBlock // Callee-saved registers, return address public override int SizeOfTransitionBlock => SizeOfCalleeSavedRegisters + PointerSize; public override int OffsetOfArgumentRegisters => SizeOfTransitionBlock; - // CALLDESCR_FPARGREGS is not set for Amd64 on + // CALLDESCR_FPARGREGS is not set for Amd64 on public override int OffsetOfFloatArgumentRegisters => 0; public override int EnregisteredParamTypeMaxSize => 8; public override int EnregisteredReturnTypeIntegerMaxSize => 8; @@ -560,7 +519,7 @@ internal sealed class X64UnixTransitionBlock : X64TransitionBlock public override int OffsetOfFloatArgumentRegisters => SizeOfM128A * NUM_FLOAT_ARGUMENT_REGISTERS; public override int EnregisteredParamTypeMaxSize => 16; public override int EnregisteredReturnTypeIntegerMaxSize => 16; - public override bool IsArgPassedByRef(TypeHandle th) => false; + public override bool IsArgPassedByRef(ITypeHandle th) => false; } private class Arm32TransitionBlock : TransitionBlock @@ -584,7 +543,7 @@ private class Arm32TransitionBlock : TransitionBlock public override bool IsArmhfABI => true; - public sealed override bool IsArgPassedByRef(TypeHandle th) => false; + public sealed override bool IsArgPassedByRef(ITypeHandle th) => false; public sealed override int GetRetBuffArgOffset(bool hasThis) => OffsetOfArgumentRegisters + (hasThis ? PointerSize : 0); @@ -597,7 +556,7 @@ public sealed override int StackElemSize(int parmSize, bool isValueType = false, private class Arm32ElTransitionBlock : Arm32TransitionBlock { - public new static TransitionBlock Instance = new Arm32ElTransitionBlock(); + public static new TransitionBlock Instance = new Arm32ElTransitionBlock(); public override bool IsArmhfABI => false; public override bool IsArmelABI => true; @@ -624,7 +583,7 @@ private class Arm64TransitionBlock : TransitionBlock public override int EnregisteredParamTypeMaxSize => 16; public override int EnregisteredReturnTypeIntegerMaxSize => 16; - public override bool IsArgPassedByRef(TypeHandle th) + public override bool IsArgPassedByRef(ITypeHandle th) { Debug.Assert(!th.IsNull()); Debug.Assert(th.IsValueType()); @@ -646,7 +605,7 @@ public override int StackElemSize(int parmSize, bool isValueType = false, bool i private sealed class AppleArm64TransitionBlock : Arm64TransitionBlock { - public new static TransitionBlock Instance = new AppleArm64TransitionBlock(); + public static new TransitionBlock Instance = new AppleArm64TransitionBlock(); public override bool IsAppleArm64ABI => true; public sealed override int StackElemSize(int parmSize, bool isValueType = false, bool isFloatHfa = false) @@ -689,7 +648,7 @@ private class LoongArch64TransitionBlock : TransitionBlock public override int EnregisteredParamTypeMaxSize => 16; public override int EnregisteredReturnTypeIntegerMaxSize => 16; - public override bool IsArgPassedByRef(TypeHandle th) + public override bool IsArgPassedByRef(ITypeHandle th) { Debug.Assert(!th.IsNull()); Debug.Assert(th.IsValueType()); @@ -733,7 +692,7 @@ private class RiscV64TransitionBlock : TransitionBlock public override int EnregisteredParamTypeMaxSize => 16; public override int EnregisteredReturnTypeIntegerMaxSize => 16; - public override bool IsArgPassedByRef(TypeHandle th) + public override bool IsArgPassedByRef(ITypeHandle th) { Debug.Assert(!th.IsNull()); Debug.Assert(th.IsValueType()); @@ -749,7 +708,6 @@ public override int StackElemSize(int parmSize, bool isValueType = false, bool i int stackSlotSize = 8; return ALIGN_UP(parmSize, stackSlotSize); } - } private class Wasm32TransitionBlock : TransitionBlock @@ -778,7 +736,7 @@ private class Wasm32TransitionBlock : TransitionBlock public override int GetRetBuffArgOffset(bool hasThis) => OffsetOfArgumentRegisters + (hasThis ? StackElemSize(PointerSize, false, false) : 0); - public override bool IsArgPassedByRef(TypeHandle th) + public override bool IsArgPassedByRef(ITypeHandle th) { return false; } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/GCRefMapBuilder.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/GCRefMapBuilder.cs index a97b2f2823a215..f2eac9476139e5 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/GCRefMapBuilder.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/GCRefMapBuilder.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Xml.Linq; using Internal.TypeSystem; +using Internal.CallingConvention; // The GCRef map is used to encode GC type of arguments for callsites. Logically, it is sequence where pos is // position of the reference in the stack frame and token is type of GC reference (one of GCREFMAP_XXX values). @@ -63,12 +64,18 @@ public GCRefMapBuilder(TargetDetails target, bool relocsOnly) _bits = 0; _pos = 0; Builder = new ObjectDataBuilder(target, relocsOnly); - _transitionBlock = TransitionBlock.FromTarget(target); + _transitionBlock = TransitionBlock.FromTarget(target.Architecture, + target.OperatingSystem == TargetOS.Windows, + target.IsApplePlatform, + target.Abi == TargetAbi.NativeAotArmel); } - internal static (ArgIterator, TransitionBlock) BuildArgIterator(MethodSignature signature, TypeSystemContext context, bool methodRequiresInstArg = false, bool isUnboxingStub = false, bool methodIsArrayAddressMethod = false, bool methodIsStringConstructor = false, bool methodIsAsyncCall = false) + internal static (ArgIterator, TransitionBlock) BuildArgIterator(MethodSignature signature, TypeSystemContext context, bool methodRequiresInstArg = false, bool isUnboxingStub = false, bool methodIsArrayAddressMethod = false, bool methodIsStringConstructor = false, bool methodIsAsyncCall = false) { - TransitionBlock transitionBlock = TransitionBlock.FromTarget(context.Target); + TransitionBlock transitionBlock = TransitionBlock.FromTarget(context.Target.Architecture, + context.Target.OperatingSystem == TargetOS.Windows, + context.Target.IsApplePlatform, + context.Target.Abi == TargetAbi.NativeAotArmel); bool hasThis = (signature.Flags & MethodSignatureFlags.Static) == 0; @@ -102,10 +109,10 @@ internal static (ArgIterator, TransitionBlock) BuildArgIterator(MethodSignature bool[] forcedByRefParams = new bool[parameterTypes.Length]; bool skipFirstArg = false; bool extraObjectFirstArg = false; - ArgIteratorData argIteratorData = new ArgIteratorData(hasThis, isVarArg, parameterTypes, returnType); + ArgIteratorData argIteratorData = new ArgIteratorData(hasThis, isVarArg, parameterTypes, returnType); - ArgIterator argit = new ArgIterator( - context, + ArgIterator argit = new ArgIterator( + transitionBlock, argIteratorData, callingConventions, hasParamType, @@ -113,14 +120,17 @@ internal static (ArgIterator, TransitionBlock) BuildArgIterator(MethodSignature extraFunctionPointerArg, forcedByRefParams, skipFirstArg, - extraObjectFirstArg); + extraObjectFirstArg, + isWindows: context.Target.IsWindows, + objectTypeHandle: new TypeHandle(context.GetWellKnownType(WellKnownType.Object)), + intPtrTypeHandle: new TypeHandle(context.GetWellKnownType(WellKnownType.IntPtr))); return (argit, transitionBlock); } public void GetCallRefMap(MethodDesc method, bool isUnboxingStub) { - (ArgIterator argit, TransitionBlock transitionBlock) = BuildArgIterator(method.Signature, method.Context, + (ArgIterator argit, TransitionBlock transitionBlock) = BuildArgIterator(method.Signature, method.Context, methodRequiresInstArg: method.RequiresInstArg(), isUnboxingStub: isUnboxingStub, methodIsArrayAddressMethod: method.IsArrayAddressMethod(), @@ -166,7 +176,7 @@ public void GetCallRefMap(MethodDesc method, bool isUnboxingStub) /// /// Fill in the GC-relevant stack frame locations. /// - private void FakeGcScanRoots(MethodDesc method, ArgIterator argit, CORCOMPILE_GCREFMAP_TOKENS[] frame, bool isUnboxingStub) + private void FakeGcScanRoots(MethodDesc method, ArgIterator argit, CORCOMPILE_GCREFMAP_TOKENS[] frame, bool isUnboxingStub) { // Encode generic instantiation arg if (argit.HasParamType) @@ -288,7 +298,7 @@ private void GcScanValueType(TypeDesc type, in ArgDestination argDest, int delta if (argDest.IsStructPassedInRegs()) { - argDest.ReportPointersFromStructInRegisters(type, delta, frame); + argDest.ReportPointersFromStructInRegisters(new TypeHandle(type), delta, frame); return; } } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/TypeHandle.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/TypeHandle.cs new file mode 100644 index 00000000000000..38d0f673a5de50 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/TypeHandle.cs @@ -0,0 +1,190 @@ +// 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.Diagnostics; + +using Internal.JitInterface; +using Internal.TypeSystem; +using Internal.CorConstants; +using Internal.Runtime; +using Internal.CallingConvention; + +namespace ILCompiler.DependencyAnalysis.ReadyToRun +{ + /// + /// Crossgen2's implementation of ITypeHandle, backed by Internal.TypeSystem.TypeDesc. + /// + internal struct TypeHandle : ITypeHandle + { + public TypeHandle(TypeDesc type) + { + _type = type; + _isByRef = _type.IsByRef; + if (_isByRef) + { + _type = ((ByRefType)_type).ParameterType; + } + } + + private readonly TypeDesc _type; + private readonly bool _isByRef; + + public bool Equals(TypeHandle other) + { + return _isByRef == other._isByRef && _type == other._type; + } + + public override int GetHashCode() { return (int)_type.GetHashCode(); } + + public bool IsNull() { return _type == null && !_isByRef; } + public bool IsValueType() { if (_isByRef) return false; return _type.IsValueType; } + public bool IsPointerType() { if (_isByRef) return false; return _type.IsPointer; } + + public bool HasIndeterminateSize() { return IsValueType() && ((DefType)_type).InstanceFieldSize.IsIndeterminate; } + + public int PointerSize => _type.Context.Target.PointerSize; + + public int GetSize() + { + if (IsValueType()) + return ((DefType)_type).InstanceFieldSize.AsInt; + else + return PointerSize; + } + + public bool RequiresAlign8() + { + if (_type.Context.Target.Architecture != TargetArchitecture.ARM) + { + return false; + } + if (_isByRef) + { + return false; + } + return _type.RequiresAlign8(); + } + + public bool IsHomogeneousAggregate() + { + TargetArchitecture targetArch = _type.Context.Target.Architecture; + if ((targetArch != TargetArchitecture.ARM) && (targetArch != TargetArchitecture.ARM64)) + { + return false; + } + if (_isByRef) + { + return false; + } + return _type is DefType defType && defType.IsHomogeneousAggregate; + } + + public int GetHomogeneousAggregateElementSize() + { + Debug.Assert(IsHomogeneousAggregate()); + switch (_type.Context.Target.Architecture) + { + case TargetArchitecture.ARM: + return RequiresAlign8() ? 8 : 4; + + case TargetArchitecture.ARM64: + return ((DefType)_type).GetHomogeneousAggregateElementSize(); + } + throw new InvalidOperationException(); + } + + public CorElementType GetCorElementType() + { + if (_isByRef) + { + return CorElementType.ELEMENT_TYPE_BYREF; + } + + Internal.TypeSystem.TypeFlags category = _type.UnderlyingType.Category; + // We use the UnderlyingType to handle Enums properly + return category switch + { + Internal.TypeSystem.TypeFlags.Boolean => CorElementType.ELEMENT_TYPE_BOOLEAN, + Internal.TypeSystem.TypeFlags.Char => CorElementType.ELEMENT_TYPE_CHAR, + Internal.TypeSystem.TypeFlags.SByte => CorElementType.ELEMENT_TYPE_I1, + Internal.TypeSystem.TypeFlags.Byte => CorElementType.ELEMENT_TYPE_U1, + Internal.TypeSystem.TypeFlags.Int16 => CorElementType.ELEMENT_TYPE_I2, + Internal.TypeSystem.TypeFlags.UInt16 => CorElementType.ELEMENT_TYPE_U2, + Internal.TypeSystem.TypeFlags.Int32 => CorElementType.ELEMENT_TYPE_I4, + Internal.TypeSystem.TypeFlags.UInt32 => CorElementType.ELEMENT_TYPE_U4, + Internal.TypeSystem.TypeFlags.Int64 => CorElementType.ELEMENT_TYPE_I8, + Internal.TypeSystem.TypeFlags.UInt64 => CorElementType.ELEMENT_TYPE_U8, + Internal.TypeSystem.TypeFlags.IntPtr => CorElementType.ELEMENT_TYPE_I, + Internal.TypeSystem.TypeFlags.UIntPtr => CorElementType.ELEMENT_TYPE_U, + Internal.TypeSystem.TypeFlags.Single => CorElementType.ELEMENT_TYPE_R4, + Internal.TypeSystem.TypeFlags.Double => CorElementType.ELEMENT_TYPE_R8, + Internal.TypeSystem.TypeFlags.ValueType => CorElementType.ELEMENT_TYPE_VALUETYPE, + Internal.TypeSystem.TypeFlags.Nullable => CorElementType.ELEMENT_TYPE_VALUETYPE, + Internal.TypeSystem.TypeFlags.Void => CorElementType.ELEMENT_TYPE_VOID, + Internal.TypeSystem.TypeFlags.Pointer => CorElementType.ELEMENT_TYPE_PTR, + Internal.TypeSystem.TypeFlags.FunctionPointer => CorElementType.ELEMENT_TYPE_FNPTR, + + _ => CorElementType.ELEMENT_TYPE_CLASS + }; + } + + public void GetSystemVAmd64PassStructInRegisterDescriptor(out SYSTEMV_AMD64_CORINFO_STRUCT_REG_PASSING_DESCRIPTOR descriptor) + { + SystemVStructClassificator.GetSystemVAmd64PassStructInRegisterDescriptor(_type, out descriptor); + } + + public FpStructInRegistersInfo GetFpStructInRegistersInfo(TargetArchitecture architecture) + { + return RiscVLoongArch64FpStruct.GetFpStructInRegistersInfo(_type, architecture); + } + + public bool IsTrivialPointerSizedStruct() + { + Debug.Assert(IsValueType()); + if (GetSize() != 4) + { + return false; + } + TypeDesc typeOfEmbeddedField = null; + foreach (var field in _type.GetFields()) + { + if (field.IsStatic) + continue; + if (typeOfEmbeddedField != null) + { + return false; + } + + typeOfEmbeddedField = field.FieldType; + } + + if ((typeOfEmbeddedField != null) && ((typeOfEmbeddedField.IsValueType) || (typeOfEmbeddedField.IsPointer))) + { + switch (typeOfEmbeddedField.UnderlyingType.Category) + { + case TypeFlags.IntPtr: + case TypeFlags.UIntPtr: + case TypeFlags.Int32: + case TypeFlags.UInt32: + case TypeFlags.Pointer: + return true; + case TypeFlags.ValueType: + return new TypeHandle(typeOfEmbeddedField).IsTrivialPointerSizedStruct(); + } + } + return false; + } + + public int GetFieldAlignment() + { + return ((DefType)_type).InstanceFieldAlignment.AsInt; + } + + /// + /// Escape hatch for crossgen2-specific code that needs the underlying TypeDesc. + /// Not part of the ITypeHandle interface. + /// + public TypeDesc GetRuntimeTypeHandle() { return _type; } + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmImportThunk.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmImportThunk.cs index 48f9f8f5f41cc3..a923cf86659334 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmImportThunk.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmImportThunk.cs @@ -5,6 +5,7 @@ using ILCompiler.ObjectWriter; using ILCompiler.ObjectWriter.WasmInstructions; using Internal.JitInterface; +using Internal.CallingConvention; using Internal.Text; using Internal.TypeSystem; using Internal.ReadyToRunConstants; @@ -124,7 +125,7 @@ protected override void EmitCode(NodeFactory factory, ref Wasm.WasmEmitter instr ISymbolNode helperTypeIndex = factory.WasmTypeNode(_helperTypeParams); MethodSignature methodSignature = WasmLowering.RaiseSignature(_wasmSignature, _context); - (ArgIterator argit, TransitionBlock transitionBlock) = GCRefMapBuilder.BuildArgIterator(methodSignature, _context); + (ArgIterator argit, TransitionBlock transitionBlock) = GCRefMapBuilder.BuildArgIterator(methodSignature, _context); int[] offsets = new int[methodSignature.Length]; bool[] isIndirectStructArg = new bool[methodSignature.Length]; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmInterpreterToR2RThunkNode.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmInterpreterToR2RThunkNode.cs index d3d6468a7ea213..0a88512554eebc 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmInterpreterToR2RThunkNode.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmInterpreterToR2RThunkNode.cs @@ -5,6 +5,7 @@ using ILCompiler.ObjectWriter; using ILCompiler.ObjectWriter.WasmInstructions; using Internal.JitInterface; +using Internal.CallingConvention; using Internal.Text; using Internal.TypeSystem; using System; @@ -84,7 +85,7 @@ protected override void EmitCode(NodeFactory factory, ref Wasm.WasmEmitter instr ISymbolNode targetTypeIndex = _targetTypeNode; MethodSignature methodSignature = WasmLowering.RaiseSignature(_wasmSignature, _context); - (ArgIterator argit, TransitionBlock transitionBlock) = GCRefMapBuilder.BuildArgIterator(methodSignature, _context); + (ArgIterator argit, TransitionBlock transitionBlock) = GCRefMapBuilder.BuildArgIterator(methodSignature, _context); bool hasRetBuffArg = _wasmSignature.SignatureString[0] == 'S'; bool hasThis = !methodSignature.IsStatic; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmR2RToInterpreterThunkNode.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmR2RToInterpreterThunkNode.cs index a519b0ea445c4f..909bb104602cce 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmR2RToInterpreterThunkNode.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmR2RToInterpreterThunkNode.cs @@ -5,6 +5,7 @@ using ILCompiler.ObjectWriter; using ILCompiler.ObjectWriter.WasmInstructions; using Internal.JitInterface; +using Internal.CallingConvention; using Internal.Text; using Internal.TypeSystem; using Internal.ReadyToRunConstants; @@ -93,7 +94,7 @@ protected override void EmitCode(NodeFactory factory, ref Wasm.WasmEmitter instr ISymbolNode helperTypeIndex = factory.WasmTypeNode(s_helperTypeParams); MethodSignature methodSignature = WasmLowering.RaiseSignature(_wasmSignature, _context); - (ArgIterator argit, TransitionBlock transitionBlock) = GCRefMapBuilder.BuildArgIterator(methodSignature, _context); + (ArgIterator argit, TransitionBlock transitionBlock) = GCRefMapBuilder.BuildArgIterator(methodSignature, _context); bool hasRetBuffArg = _wasmSignature.SignatureString[0] == 'S'; bool hasThis = !methodSignature.IsStatic; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj index 215a6bf805d571..23d157f599c620 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj @@ -163,6 +163,9 @@ + + + @@ -236,7 +239,6 @@ - @@ -305,8 +307,8 @@ - + diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/WasmLowering.ReadyToRun.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/WasmLowering.ReadyToRun.cs index d0d82721d001e3..d6bda24c13dec0 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/WasmLowering.ReadyToRun.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/WasmLowering.ReadyToRun.cs @@ -7,6 +7,7 @@ using ILCompiler; using ILCompiler.DependencyAnalysis.Wasm; using ILCompiler.DependencyAnalysis.ReadyToRun; +using Internal.CallingConvention; using Internal.TypeSystem; @@ -14,13 +15,12 @@ namespace Internal.JitInterface { public static partial class WasmLowering { - internal static bool CurrentArgLowersValueTypeToPassAsByref(ArgIterator argit) + internal static bool CurrentArgLowersValueTypeToPassAsByref(ArgIterator argit) { if (argit.IsValueType()) { // Check to see if this argument lowers to a byref on the wasm side - TypeHandle typeHandle; - argit.GetArgType(out typeHandle); + argit.GetArgType(out TypeHandle typeHandle); if (WasmLowering.LowerToAbiType(typeHandle.GetRuntimeTypeHandle()) == null) { return true; diff --git a/src/coreclr/vm/cdacstress.cpp b/src/coreclr/vm/cdacstress.cpp index af5f8362af37bd..810dc57672f262 100644 --- a/src/coreclr/vm/cdacstress.cpp +++ b/src/coreclr/vm/cdacstress.cpp @@ -26,6 +26,7 @@ #include "gccover.h" #include "sstring.h" #include "exinfo.h" +#include "gcrefmap.h" #ifdef TARGET_LINUX // process_vm_readv is the safe in-process read path on Linux. See @@ -47,15 +48,31 @@ static const unsigned int CDAC_DEFERRED_FRAME = 0x40000000; static const int MAX_DEFERRED_FRAMES = 64; // Bit flags for DOTNET_CdacStress configuration. +// +// Layout (little-endian DWORD): +// byte 0 (0x000000FF) -- WHERE: trigger points the stress harness fires at +// byte 1 (0x0000FF00) -- WHAT: which sub-checks run when a trigger fires +// byte 2 (0x00FF0000) -- MODIFIERS: output / behavior knobs +// +// A useful configuration combines at least one WHERE and at least one WHAT +// (e.g. 0x0101 = ALLOC + GCREFS, 0x0301 = ALLOC + GCREFS + ARGITER). enum CdacStressFlags : DWORD { - // Trigger points (where stress fires) - CDACSTRESS_ALLOC = 0x1, // Verify at allocation points + // WHERE -- trigger points + CDACSTRESS_ALLOC = 0x00000001, // Verify at allocation points (gchelpers.cpp) + + // WHAT -- sub-checks (require a WHERE bit to be set as well) + CDACSTRESS_GCREFS = 0x00000100, // Compare cDAC GetStackReferences vs runtime GC root oracle + CDACSTRESS_ARGITER = 0x00000200, // Compare CallingConvention.EnumerateArguments vs runtime ComputeCallRefMap - // Modifiers - CDACSTRESS_VERBOSE = 0x200, // Rich per-ref diagnostics in the log + // MODIFIERS + CDACSTRESS_VERBOSE = 0x00010000, // Rich per-ref diagnostics in the log }; +// Convenience masks. +static const DWORD CDACSTRESS_WHERE_MASK = 0x000000FF; +static const DWORD CDACSTRESS_WHAT_MASK = 0x0000FF00; + //----------------------------------------------------------------------------- // Types //----------------------------------------------------------------------------- @@ -193,6 +210,12 @@ extern void GcEnumObject(LPVOID pData, OBJECTREF *pObj, uint32_t flags); static bool IsDeferredFrame(CLRDATA_ADDRESS source, const CLRDATA_ADDRESS* deferred, int deferredCount); static void ResolveMethodName(CLRDATA_ADDRESS source, int sourceType, char* buf, int bufLen); +static void VerifyGcRefsAtStressPoint(Thread* pThread, PCONTEXT regs, DWORD osThreadId); +static void VerifyArgIteratorOnStack(Thread* pThread); +static void LogArgIteratorMismatch(MethodDesc* pMD, CLRDATA_ADDRESS mdAddr, + LPCSTR frameName, const char* methodName, + const BYTE* rtBlob, int rtLen, + const BYTE* cdacBlob, int cdacLen); //----------------------------------------------------------------------------- // Static state — cDAC reader @@ -231,6 +254,18 @@ static volatile LONG s_frameMatch = 0; static volatile LONG s_frameMismatch = 0; static volatile LONG s_frameKnownNie = 0; +// ArgIterator (sub-trigger CDACSTRESS_ARGITER) counters. Distinct MDs only; +// per-MD dedup means each MD contributes exactly once across the run. +static volatile LONG s_argIterPass = 0; +static volatile LONG s_argIterFail = 0; +static volatile LONG s_argIterSkip = 0; +static volatile LONG s_argIterError = 0; + +// Per-MD dedup for ArgIterator verification. Lazily allocated on first use, +// freed in Shutdown. Protected by s_cdacLock acquired in VerifyAtStressPoint. +class MethodDesc; +static SetSHash>* s_argIterVerifiedMDs = nullptr; + //----------------------------------------------------------------------------- // Thread-local state //----------------------------------------------------------------------------- @@ -318,6 +353,16 @@ static bool IsCdacStressVerboseEnabled() return (s_cdacStressLevel & CDACSTRESS_VERBOSE) != 0; } +static bool IsCdacStressGcRefsEnabled() +{ + return (s_cdacStressLevel & CDACSTRESS_GCREFS) != 0; +} + +static bool IsCdacStressArgIterEnabled() +{ + return (s_cdacStressLevel & CDACSTRESS_ARGITER) != 0; +} + // Single-line file logger. Self-guards on s_logFile, so callers don't need to. #define CDAC_LOG(...) \ do { \ @@ -532,6 +577,12 @@ void CdacStressPolicy::Shutdown() "CDAC GC Stress: %ld frames examined " "(%ld matched / %ld mismatched / %ld known-NIE)\n", (long)s_frameTotal, (long)s_frameMatch, (long)s_frameMismatch, (long)s_frameKnownNie); + if (IsCdacStressArgIterEnabled()) + { + fprintf(stderr, + "CDAC GC Stress: ArgIter: %ld pass / %ld fail / %ld skip / %ld error\n", + (long)s_argIterPass, (long)s_argIterFail, (long)s_argIterSkip, (long)s_argIterError); + } STRESS_LOG3(LF_GCROOTS, LL_ALWAYS, "CDAC GC Stress shutdown: %d verifications (%d pass / %d fail)\n", (int)totalVerifications, (int)s_passCount, (int)s_failCount); @@ -547,10 +598,34 @@ void CdacStressPolicy::Shutdown() fprintf(s_logFile, " Matched: %ld\n", (long)s_frameMatch); fprintf(s_logFile, " Mismatched: %ld\n", (long)s_frameMismatch); fprintf(s_logFile, " Known NIE: %ld\n", (long)s_frameKnownNie); + // Machine-readable sub-check markers. Mirrors the existing [ARG_STATS] + // line below: each is emitted only when its sub-check was enabled, so + // CdacStressResults can distinguish "GCREFS / ARGITER did not run" + // from "ran but produced zero results" (which the surrounding + // human-readable counters cannot, since they are always printed and + // always zero-initialized). + if (IsCdacStressGcRefsEnabled()) + { + fprintf(s_logFile, "[GC_STATS] verifications=%ld pass=%ld fail=%ld known_issue=%ld\n", + (long)totalVerifications, (long)s_passCount, + (long)s_failCount, (long)s_knownIssueCount); + } + if (IsCdacStressArgIterEnabled()) + { + fprintf(s_logFile, "[ARG_STATS] pass=%ld fail=%ld skip=%ld error=%ld\n", + (long)s_argIterPass, (long)s_argIterFail, + (long)s_argIterSkip, (long)s_argIterError); + } fclose(s_logFile); s_logFile = nullptr; } + if (s_argIterVerifiedMDs != nullptr) + { + delete s_argIterVerifiedMDs; + s_argIterVerifiedMDs = nullptr; + } + if (s_cdacSosDac != nullptr) { s_cdacSosDac->Release(); @@ -1248,6 +1323,451 @@ static bool IsDeferredFrame(CLRDATA_ADDRESS source, const CLRDATA_ADDRESS* defer return false; } +//----------------------------------------------------------------------------- +// ArgIterator sub-check: compare the cDAC's encoded GCRefMap blob against +// the runtime's ComputeCallRefMap output, byte-for-byte, for every MD on a +// transition Frame on the active thread. +//----------------------------------------------------------------------------- + +// Per-MD dedup. Protected by s_cdacLock (held by VerifyAtStressPoint). + +// Resolve a MethodDesc address to a human-readable name via the cDAC. +static void ResolveMethodNameFromMD(CLRDATA_ADDRESS mdAddr, char* buf, int bufLen) +{ + if (bufLen <= 0) + return; + + if (s_cdacSosDac != nullptr) + { + WCHAR wname[256] = {}; + unsigned int nameLen = 0; + if (SUCCEEDED(s_cdacSosDac->GetMethodDescName(mdAddr, ARRAY_SIZE(wname), wname, &nameLen)) && nameLen > 0) + { + WideCharToMultiByte(CP_UTF8, 0, wname, -1, buf, bufLen, NULL, NULL); + return; + } + } + snprintf(buf, bufLen, "", (unsigned long long)mdAddr); +} + +// Compute the runtime's authoritative GCRefMap blob for `pMD` and copy it +// into the caller's buffer (up to `bufSize` bytes). Returns the actual blob +// length on success, or a negative HRESULT-coded value on failure: +// -1 ComputeCallRefMap threw (signature couldn't be classified) +// -2 blob exceeded `bufSize` (caller should treat as oracle skip) +// A return >= 0 means `*pBufOut` has `return-value` valid bytes. +static int ComputeRuntimeArgGCRefMap(MethodDesc* pMD, BYTE* pBufOut, int bufSize) +{ + GCRefMapBuilder builder; + bool threw = false; + + // ComputeCallRefMap chains down to FakeGcScanRoots which declares + // STANDARD_VM_CONTRACT (MODE_PREEMPTIVE, GC_TRIGGERS, THROWS). The cdacstress + // hook fires from inside the allocator while the thread is in cooperative + // GC mode, so the strict mode/GC contract would assert. The work is + // signature-walking + metadata loads, both of which are safe to perform + // here (the runtime itself loads metadata in cooperative mode during JIT, + // and we hold s_cdacLock around the whole call). Acknowledge the contract + // violation explicitly so Checked builds don't false-fire. + CONTRACT_VIOLATION(ModeViolation | GCViolation); + + EX_TRY + { + ComputeCallRefMap(pMD, &builder, /*isDispatchCell*/ false); + } + EX_CATCH + { + threw = true; + } + EX_END_CATCH + + if (threw) + return -1; + + DWORD blobLen = 0; + PVOID blob = builder.GetBlob(&blobLen); + if ((int)blobLen > bufSize) + return -2; + + if (blobLen > 0) + memcpy(pBufOut, blob, blobLen); + return (int)blobLen; +} + +// Hex-dump a blob into `buf` ("aa bb cc ...") for diagnostic output. +// On overflow the dump is truncated with a trailing "..." marker. +static void FormatBlobHex(const BYTE* blob, int len, char* buf, size_t bufLen) +{ + if (bufLen == 0) + return; + buf[0] = '\0'; + size_t used = 0; + for (int i = 0; i < len; i++) + { + // Each byte needs 3 chars ("xx ") plus null and trailing "...". + if (used + 8 >= bufLen) + { + snprintf(buf + used, bufLen - used, "..."); + return; + } + int n = snprintf(buf + used, bufLen - used, "%02x ", blob[i]); + if (n <= 0) break; + used += (size_t)n; + } +} + +// Token name for log output. Matches CORCOMPILE_GCREFMAP_TOKENS in corcompile.h. +static const char* GCRefMapTokenName(int token) +{ + switch (token) + { + case GCREFMAP_SKIP: return "SKIP"; + case GCREFMAP_REF: return "REF"; + case GCREFMAP_INTERIOR: return "INTERIOR"; + case GCREFMAP_METHOD_PARAM: return "METHOD_PARAM"; + case GCREFMAP_TYPE_PARAM: return "TYPE_PARAM"; + case GCREFMAP_VASIG_COOKIE: return "VASIG_COOKIE"; + default: return "?"; + } +} + +// Per-slot location label for the ARG_FAIL table. On the architectures the +// runtime supports, the first NUM_ARGUMENT_REGISTERS positions cover the +// integer-arg registers and the rest are caller-stack slots. Naming the +// registers (vs printing raw offsets) is the difference between "I can read +// this" and "let me go grep the ABI doc". +static void FormatSlotLocation(int pos, int byteOffset, char* buf, size_t bufLen) +{ +#if defined(TARGET_AMD64) +# if defined(UNIX_AMD64_ABI) + static const char* regNames[] = { "RDI", "RSI", "RDX", "RCX", "R8", "R9" }; +# else + static const char* regNames[] = { "RCX", "RDX", "R8", "R9" }; +# endif +#elif defined(TARGET_ARM64) + static const char* regNames[] = { "X0", "X1", "X2", "X3", "X4", "X5", "X6", "X7" }; +#elif defined(TARGET_ARM) + static const char* regNames[] = { "R0", "R1", "R2", "R3" }; +#elif defined(TARGET_X86) + // x86 has 2 arg regs (ECX, EDX) and a non-monotonic pos->offset mapping; + // print pos+offset rather than guess the wrong register name. + static const char* regNames[] = { "ECX", "EDX" }; +#endif + +#if defined(TARGET_AMD64) || defined(TARGET_ARM64) || defined(TARGET_ARM) || defined(TARGET_X86) + const int numRegs = (int)(sizeof(regNames) / sizeof(regNames[0])); + if (pos >= 0 && pos < numRegs) + { + snprintf(buf, bufLen, "%-6s", regNames[pos]); + return; + } +#endif + + int stackByteOffset = byteOffset - (int)sizeof(TransitionBlock); + snprintf(buf, bufLen, "[sp+%d]", stackByteOffset); +} + +// Decode a GCRefMap blob into an offset->token map (sparse) plus the +// max pos seen. On x86 we consume the leading WriteStackPop prefix into +// `StackPop` so the remaining bitstream is the token stream proper, matching +// the runtime's GCInfoDecoder.ReadStackPop()-then-ReadToken() ordering. +struct DecodedBlob +{ + static const int MaxSlots = 64; + int Pos[MaxSlots]; + int Tok[MaxSlots]; + int Count; + int MaxPos; + int StackPop; // x86 only; 0 on other arches and on x86 VarArgs +}; + +static void DecodeBlob(const BYTE* blob, int len, DecodedBlob& out, bool isX86) +{ + out.Count = 0; + out.MaxPos = -1; + out.StackPop = 0; + if (blob == nullptr || len == 0) + return; + + GCRefMapDecoder decoder(const_cast(blob)); +#ifdef TARGET_X86 + if (isX86) + out.StackPop = (int)decoder.ReadStackPop(); +#else + (void)isX86; +#endif + while (!decoder.AtEnd() && out.Count < DecodedBlob::MaxSlots) + { + int token = decoder.ReadToken(); + int afterPos = decoder.CurrentPos(); + + if (token == GCREFMAP_SKIP) + { + // A skip token bumps pos but emits no entry. + if (afterPos - 1 > out.MaxPos) + out.MaxPos = afterPos - 1; + continue; + } + + // ReadToken stores the result at the position BEFORE the increment. + int slotPos = afterPos - 1; + out.Pos[out.Count] = slotPos; + out.Tok[out.Count] = token; + out.Count++; + if (slotPos > out.MaxPos) + out.MaxPos = slotPos; + } +} + +static int LookupTokenAtPos(const DecodedBlob& blob, int pos) +{ + for (int i = 0; i < blob.Count; i++) + { + if (blob.Pos[i] == pos) + return blob.Tok[i]; + } + return GCREFMAP_SKIP; +} + +// Compute the byte offset within the TransitionBlock for a given GCRefMap pos, +// mirroring ComputeCallRefMap (frames.cpp:2155-2163). +static int OffsetFromGCRefMapPos(int pos) +{ +#ifdef TARGET_X86 + if (pos < NUM_ARGUMENT_REGISTERS) + return TransitionBlock::GetOffsetOfArgumentRegisters() + ARGUMENTREGISTERS_SIZE - (pos + 1) * sizeof(TADDR); + return TransitionBlock::GetOffsetOfArgs() + (pos - NUM_ARGUMENT_REGISTERS) * sizeof(TADDR); +#else + return TransitionBlock::GetOffsetOfFirstGCRefMapSlot() + pos * TARGET_POINTER_SIZE; +#endif +} + +// Emit a per-slot comparison table when the runtime and cDAC GCRefMap blobs +// differ. Each row is one position; only positions with a non-skip token on +// at least one side are shown, and rows where the two tokens differ are +// flagged. Reads enormously better than two hex-strings when triaging a port +// bug ("oh, the cDAC missed the byref at stack[+0]" vs squinting at "85 04"). +static void LogArgIteratorMismatch(MethodDesc* pMD, CLRDATA_ADDRESS mdAddr, + LPCSTR frameName, const char* methodName, + const BYTE* rtBlob, int rtLen, + const BYTE* cdacBlob, int cdacLen) +{ +#ifdef TARGET_X86 + const bool isX86 = true; +#else + const bool isX86 = false; +#endif + + DecodedBlob rt, cdac; + DecodeBlob(rtBlob, rtLen, rt, isX86); + DecodeBlob(cdacBlob, cdacLen, cdac, isX86); + + int maxPos = rt.MaxPos > cdac.MaxPos ? rt.MaxPos : cdac.MaxPos; + if (maxPos < 0) maxPos = 0; + + char rtHex[256], cdacHex[256]; + FormatBlobHex(rtBlob, rtLen, rtHex, sizeof(rtHex)); + FormatBlobHex(cdacBlob, cdacLen, cdacHex, sizeof(cdacHex)); + + CDAC_LOG("[ARG_FAIL] MD=0x%llx frame=%s rtSize=%d cdacSize=%d %s\n", + (unsigned long long)mdAddr, frameName, rtLen, cdacLen, methodName); + CDAC_LOG(" RT: %s\n", rtHex); + CDAC_LOG(" cDAC: %s\n", cdacHex); + if (isX86) + { + const char* popDiff = (rt.StackPop != cdac.StackPop) ? " <-- DIFF" : ""; + CDAC_LOG(" stack_pop RT=%d cDAC=%d%s\n", rt.StackPop, cdac.StackPop, popDiff); + } + CDAC_LOG(" pos location RT token cDAC token diff\n"); + + for (int pos = 0; pos <= maxPos; pos++) + { + int rtTok = LookupTokenAtPos(rt, pos); + int cdacTok = LookupTokenAtPos(cdac, pos); + if (rtTok == GCREFMAP_SKIP && cdacTok == GCREFMAP_SKIP) + continue; + + char loc[24]; + FormatSlotLocation(pos, OffsetFromGCRefMapPos(pos), loc, sizeof(loc)); + + const char* diff = (rtTok != cdacTok) ? " <-- DIFF" : ""; + CDAC_LOG(" %3d %-8s %-13s %-15s%s\n", + pos, loc, GCRefMapTokenName(rtTok), GCRefMapTokenName(cdacTok), diff); + } +} + +// Verify ArgIterator output for a single MD. Computes the runtime oracle +// blob (via ComputeCallRefMap), asks the cDAC for the same blob via the +// private Request opcode, and compares byte-for-byte. +static void VerifyArgIteratorForMD(MethodDesc* pMD, FrameIdentifier frameId) +{ + char methodName[256]; + ResolveMethodNameFromMD((CLRDATA_ADDRESS)(LONG_PTR)pMD, methodName, sizeof(methodName)); + LPCSTR frameName = Frame::GetFrameTypeName(frameId); + if (frameName == nullptr) + frameName = ""; + + // Stack-allocated buffer for both the runtime oracle blob and the cDAC + // first-attempt response. Typical blobs are 1-4 bytes, so 64 covers + // nearly every signature in one call. The cDAC side falls back to a + // heap buffer via the ERROR_INSUFFICIENT_BUFFER two-call pattern below + // when an outlier exceeds it; for the runtime oracle, an overflow + // surfaces as an ARG_SKIP ("runtime-blob-too-large"). + const int kStackBufSize = 64; + + // 1. Runtime oracle. If the runtime itself can't classify this MD there's + // nothing for the cDAC to be wrong about, so silently skip -- + // counted as ARG_SKIP for visibility in stats. + BYTE rtBlob[kStackBufSize]; + int rtLen = ComputeRuntimeArgGCRefMap(pMD, rtBlob, (int)sizeof(rtBlob)); + if (rtLen < 0) + { + InterlockedIncrement(&s_argIterSkip); + const char* reason = (rtLen == -1) ? "runtime-threw" : "runtime-blob-too-large"; + CDAC_LOG("[ARG_SKIP] MD=0x%llx frame=%s reason=%s %s\n", + (unsigned long long)(LONG_PTR)pMD, frameName, reason, methodName); + return; + } + + // 2. cDAC side via the private Request opcode. outBuffer is unused; + // the request descriptor carries an [in,out] buffer descriptor that + // the handler writes through. Two-call shape: try the stack guess + // first; if it's too small, the handler returns + // ERROR_INSUFFICIENT_BUFFER with cbFilled = needed size, and we retry + // with a heap buffer. + BYTE stackBuf[kStackBufSize]; + + DacStressArgGCRefMapRequest req = {}; + req.MethodDesc = (CLRDATA_ADDRESS)(LONG_PTR)pMD; + req.BlobBuffer = (CLRDATA_ADDRESS)(LONG_PTR)stackBuf; + req.BlobBufferLen = sizeof(stackBuf); + + HRESULT cdacHr = s_cdacProcess->Request( + DACSTRESSPRIV_REQUEST_COMPUTE_ARG_GCREFMAP, + sizeof(req), (BYTE*)&req, + 0, nullptr); + + const BYTE* cdacBlob = stackBuf; + NewArrayHolder heapBuf; + if (cdacHr == HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER)) + { + ULONG32 need = req.cbNeeded; + heapBuf = new (nothrow) BYTE[need]; + if (heapBuf == nullptr) + { + InterlockedIncrement(&s_argIterSkip); + CDAC_LOG("[ARG_SKIP] MD=0x%llx frame=%s reason=oom-retry-buffer rtBlobSize=%d %s\n", + (unsigned long long)req.MethodDesc, frameName, rtLen, methodName); + return; + } + req.BlobBuffer = (CLRDATA_ADDRESS)(LONG_PTR)(BYTE*)heapBuf; + req.BlobBufferLen = need; + cdacHr = s_cdacProcess->Request( + DACSTRESSPRIV_REQUEST_COMPUTE_ARG_GCREFMAP, + sizeof(req), (BYTE*)&req, + 0, nullptr); + cdacBlob = heapBuf; + } + + if (cdacHr == E_NOTIMPL) + { + InterlockedIncrement(&s_argIterSkip); + CDAC_LOG("[ARG_SKIP] MD=0x%llx frame=%s reason=0x%08x rtBlobSize=%d %s\n", + (unsigned long long)req.MethodDesc, frameName, + (unsigned int)cdacHr, rtLen, methodName); + return; + } + if (FAILED(cdacHr)) + { + InterlockedIncrement(&s_argIterError); + CDAC_LOG("[ARG_ERROR] MD=0x%llx frame=%s cdacHr=0x%08x %s\n", + (unsigned long long)req.MethodDesc, frameName, + (unsigned int)cdacHr, methodName); + return; + } + + // 3. Byte-for-byte comparison. + if ((int)req.cbFilled == rtLen && memcmp(cdacBlob, rtBlob, rtLen) == 0) + { + InterlockedIncrement(&s_argIterPass); + CDAC_LOG("[ARG_PASS] MD=0x%llx frame=%s blobSize=%d %s\n", + (unsigned long long)req.MethodDesc, frameName, rtLen, methodName); + return; + } + + InterlockedIncrement(&s_argIterFail); + LogArgIteratorMismatch(pMD, req.MethodDesc, frameName, methodName, + rtBlob, rtLen, cdacBlob, (int)req.cbFilled); +} + +static void VerifyArgIteratorOnStack(Thread* pThread) +{ + _ASSERTE(s_cdacProcess != nullptr); + + // Lazily allocate the dedup set on first use. Bounded by the count of + // distinct MDs hitting frames during this run, so growing without bound is fine. + if (s_argIterVerifiedMDs == nullptr) + { + s_argIterVerifiedMDs = new (nothrow) SetSHash>(); + if (s_argIterVerifiedMDs == nullptr) + return; // OOM: skip ArgIter verification entirely this run. + } + + // Walk every stack frame (both frameless JIT frames and explicit "F" Frames). + // For each frame that resolves to a MethodDesc, verify it. The ArgIterator + // port produces a result for any MD regardless of which kind of frame surfaced + // it, so the only filter is "does this frame have an MD". Per-MD dedup keeps + // cost flat across long stress runs. + struct WalkCtx + { + FrameIdentifier lastFrameId; + }; + WalkCtx ctx; + ctx.lastFrameId = FrameIdentifier::None; + + auto callback = [](CrawlFrame* pCF, VOID* pData) -> StackWalkAction + { + WalkCtx* c = (WalkCtx*)pData; + + MethodDesc* pMD = pCF->GetFunction(); + if (pMD == nullptr) + return SWA_CONTINUE; + + // Frame identifier for logging context: explicit Frames carry their + // class id; frameless JIT frames have no Frame*, so report "None" + // (the cDAC walker treats it as just another managed frame). + FrameIdentifier id = FrameIdentifier::None; + if (!pCF->IsFrameless()) + { + Frame* pFrame = pCF->GetFrame(); + if (pFrame != nullptr) + id = pFrame->GetFrameIdentifier(); + } + + if (s_argIterVerifiedMDs->Lookup(pMD) != nullptr) + return SWA_CONTINUE; + + EX_TRY + { + s_argIterVerifiedMDs->Add(pMD); + } + EX_CATCH + { + // OOM adding to the dedup set: skip this MD and try again later. + return SWA_CONTINUE; + } + EX_END_CATCH + + VerifyArgIteratorForMD(pMD, id); + c->lastFrameId = id; + return SWA_CONTINUE; + }; + + GCForbidLoaderUseHolder forbidLoaderUse; + unsigned flags = ALLOW_ASYNC_STACK_WALK | ALLOW_INVALID_OBJECTS | GC_FUNCLET_REFERENCE_REPORTING; + pThread->StackWalkFrames(callback, &ctx, flags); +} + //----------------------------------------------------------------------------- // Stress verification implementation: shared by all trigger-point // specializations below. Compares cDAC vs runtime stack refs at the captured @@ -1266,6 +1786,34 @@ static void VerifyAtStressPoint(Thread* pThread, PCONTEXT regs) DWORD osThreadId = pThread->GetOSThreadId(); + // Each sub-check below is gated independently on its CDACSTRESS_* WHAT bit. + if (IsCdacStressGcRefsEnabled()) + { + VerifyGcRefsAtStressPoint(pThread, regs, osThreadId); + } + + if (IsCdacStressArgIterEnabled() && s_cdacProcess != nullptr) + { + s_currentContext = regs; + s_currentThreadId = osThreadId; + + // Flush target-state caches before walking. The GCREFS sub-check + // does this implicitly via its A.1 phase; if ARGITER runs without + // GCREFS, the cDAC's ProcessedData cache can be stale (or empty), + // which causes ValidateMethodDescPointer to fail for live MDs. + s_cdacProcess->Request(DACSTRESSPRIV_REQUEST_FLUSH_TARGET_STATE, 0, NULL, 0, NULL); + + VerifyArgIteratorOnStack(pThread); + s_currentContext = nullptr; + s_currentThreadId = 0; + } +} + +// GC-refs sub-check: compare cDAC GetStackReferences output against the +// runtime's own GC root enumeration at the captured CONTEXT. +static void VerifyGcRefsAtStressPoint(Thread* pThread, PCONTEXT regs, DWORD osThreadId) +{ + // Phase A: Collect raw refs from both sides (independent walks). // A.1: cDAC side. ReadThreadContext callback state is wired here so the diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index 77f13a829a1ae0..33e6bdbe6cd692 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -1723,6 +1723,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 d2b7888fdcc1d8..e25ca943d0f83c 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ContractRegistry.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ContractRegistry.cs @@ -97,6 +97,10 @@ public abstract class ContractRegistry /// public virtual INotifications Notifications => GetContract(); /// + /// Gets an instance of the CallingConvention contract for the target. + /// + public virtual ICallingConvention CallingConvention => GetContract(); + /// /// Gets an instance of the CodeNotifications contract for the target. /// public virtual ICodeNotifications CodeNotifications => GetContract(); 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..44e125fe0437f1 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ICallingConvention.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts; + +public interface ICallingConvention : IContract +{ + static string IContract.Name => nameof(CallingConvention); + + bool TryComputeArgGCRefMapBlob(MethodDescHandle methodDesc, out byte[] blob) + => 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 259a04ea087313..7753440a5841a9 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 @@ -149,6 +149,8 @@ public interface IRuntimeTypeSystem : IContract bool IsObjRef(TypeHandle typeHandle) => throw new NotImplementedException(); // True if the MethodTable represents a type that contains managed references bool ContainsGCPointers(TypeHandle typeHandle) => throw new NotImplementedException(); + // True if MethodTable represents a byreflike value (Span, ReadOnlySpan, etc.). + bool IsByRefLike(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 MethodTable represents a continuation subtype that has no metadata of its own @@ -285,6 +287,7 @@ public interface IRuntimeTypeSystem : IContract bool IsAsyncThunkMethod(MethodDescHandle methodDesc) => throw new NotImplementedException(); bool IsWrapperStub(MethodDescHandle methodDesc) => throw new NotImplementedException(); + bool IsUnboxingStub(MethodDescHandle methodDesc) => throw new NotImplementedException(); #endregion MethodDesc inspection APIs #region FieldDesc inspection APIs TargetPointer GetMTOfEnclosingClass(TargetPointer fieldDescPointer) => throw new NotImplementedException(); @@ -294,6 +297,7 @@ public interface IRuntimeTypeSystem : IContract bool IsFieldDescRVA(TargetPointer fieldDescPointer) => throw new NotImplementedException(); CorElementType GetFieldDescType(TargetPointer fieldDescPointer) => throw new NotImplementedException(); uint GetFieldDescOffset(TargetPointer fieldDescPointer, FieldDefinition? fieldDef) => throw new NotImplementedException(); + TypeHandle GetFieldDescApproxTypeHandle(TargetPointer fieldDescPointer) => throw new NotImplementedException(); 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(); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgumentLocation.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgumentLocation.cs new file mode 100644 index 00000000000000..2d1871d4840ede --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgumentLocation.cs @@ -0,0 +1,32 @@ +// 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; + +internal readonly struct ArgumentLocation +{ + public int Offset { get; init; } + public CorElementType ElementType { get; init; } + public TypeHandle TypeHandle { get; init; } + public bool IsThis { get; init; } + public bool IsValueTypeThis { get; init; } + public bool IsParamType { get; init; } + + // Implicit VASigCookie pointer for a vararg (__arglist) method. When set, + // the encoder emits a VASigCookie token here and stops reporting fixed + // arguments (the variadic tail is reported through the cookie at GC time). + public bool IsVASigCookie { get; init; } + + // Struct passed by reference (e.g. large struct on AMD64). + public bool IsPassedByRef { get; init; } + + // By-value ByRefLike struct (Span, ReadOnlySpan, ...). The encoder + // walks instance fields for these to emit INTERIOR tokens at each managed + // pointer slot. + public bool IsByRefLikeStruct { get; init; } + + // For generic-instantiation parameters with an uncached closed TypeHandle, + // the open generic MethodTable (e.g. Span for a Span arg) so + // encoders can inspect type structure as a fallback. + public TypeHandle OpenGenericType { get; init; } +} 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..969d2875f6217d --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CallingConvention_1.cs @@ -0,0 +1,1016 @@ +// 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.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using Internal.CallingConvention; +using Internal.CorConstants; +using Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; +using Microsoft.Diagnostics.DataContractReader.SignatureHelpers; + +using CallingConventions = Internal.CallingConvention.CallingConventions; +using CdacCorElementType = Microsoft.Diagnostics.DataContractReader.Contracts.CorElementType; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts; + +internal sealed class CallingConvention_1 : ICallingConvention +{ + private readonly Target _target; + + internal CallingConvention_1(Target target) + { + _target = target; + } + + public bool TryComputeArgGCRefMapBlob(MethodDescHandle methodDesc, out byte[] blob) + { + try + { + byte[]? result = ComputeArgGCRefMapBlobCore(methodDesc); + if (result is null) + { + blob = []; + return false; + } + blob = result; + return true; + } + catch (NotImplementedException) + { + // Any unported ABI path, including NIEs from GetArgumentLayout, + // maps to a clean decline (false). + blob = []; + return false; + } + } + + // Result of GetArgumentLayout: a single ArgIterator walk produces the + // per-argument locations the encoder iterates plus the x86 callee-pop + // stack-byte count it needs for the WriteStackPop prefix. Bundled so the + // implementation builds ArgIterator once per method instead of twice. + private readonly record struct ArgumentLayout( + IReadOnlyList Arguments, + uint CbStackPop); + + // Per-parameter metadata captured at signature-decode time. We track this + // out-of-band because the standard SignatureTypeProvider collapses + // ELEMENT_TYPE_BYREF, _PTR, _SZARRAY, and _ARRAY into the underlying type + // (or a null TypeHandle when the runtime hasn't cached the constructed + // form), making the top-level element type unrecoverable from + // methodSig.ParameterTypes alone. + private readonly struct ParamTypeInfo + { + // Set if the parameter is wrapped in ELEMENT_TYPE_BYREF. + public bool IsByRef { get; init; } + + // Outermost element type of the parameter signature, if known + // (Byref / Ptr / SzArray / Array). The enum's zero value (default) + // means "no constructed-type wrapper -- caller should fall back to + // GetSignatureCorElementType on the underlying TypeHandle". + public CdacCorElementType OutermostKind { get; init; } + + // For generic-instantiation parameters, the open generic type + // (e.g. Span for a Span arg). Used by the encoder when the + // constructed TypeHandle is null (uncached) to fall back to + // attributes of the open type (IsByRefLike, etc.). + public TypeHandle OpenGenericType { get; init; } + } + + private ArgumentLayout GetArgumentLayout(MethodDescHandle methodDesc) + { + IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem; + IRuntimeInfo runtimeInfo = _target.Contracts.RuntimeInfo; + + MethodSignature methodSig = DecodeMethodSignature(rts, methodDesc); + + // Re-decode the same signature with a wrapper provider to learn each + // parameter's outermost element type (Byref / Ptr / SzArray / Array) + // and whether it's wrapped in ELEMENT_TYPE_BYREF. The standard + // SignatureTypeProvider hides these wrappers (returning a null + // TypeHandle when GetConstructedType isn't cached), so without this + // out-of-band metadata the encoder would silently drop any arg whose + // outermost wrapper isn't in the loader's available-type-params list. + ParamTypeInfo[] paramInfo = DecodeParamTypeInfo(rts, methodDesc, methodSig.ParameterTypes.Length); + + bool isVarArg = methodSig.Header.CallingConvention is SignatureCallingConvention.VarArgs; + + bool hasThis = methodSig.Header.IsInstance; + bool requiresInstArg = false; + bool isAsync = false; + try + { + GenericContextLoc ctxLoc = rts.GetGenericContextLoc(methodDesc); + requiresInstArg = ctxLoc is GenericContextLoc.InstArgMethodDesc or GenericContextLoc.InstArgMethodTable; + isAsync = rts.IsAsyncMethod(methodDesc); + } + catch + { + } + + CdacTypeHandle[] parameterTypes = new CdacTypeHandle[methodSig.ParameterTypes.Length]; + for (int i = 0; i < parameterTypes.Length; i++) + { + parameterTypes[i] = new CdacTypeHandle(methodSig.ParameterTypes[i], _target, paramInfo[i].OutermostKind); + } + + CdacTypeHandle returnType = new CdacTypeHandle(methodSig.ReturnType, _target); + + TransitionBlock transitionBlock = BuildTransitionBlock(runtimeInfo); + + CallingConventions callingConventions = hasThis + ? CallingConventions.ManagedInstance + : CallingConventions.ManagedStatic; + + ArgIteratorData argIteratorData = new ArgIteratorData( + hasThis, isVarArg: isVarArg, parameterTypes, returnType); + + bool isWindows = runtimeInfo.GetTargetOperatingSystem() == RuntimeInfoOperatingSystem.Windows; + + ArgIterator argit = new ArgIterator( + transitionBlock, + argIteratorData, + callingConventions, + hasParamType: requiresInstArg, + hasAsyncContinuation: isAsync, + extraFunctionPointerArg: false, + forcedByRefParams: new bool[parameterTypes.Length], + skipFirstArg: false, + extraObjectFirstArg: false, + isWindows: isWindows, + objectTypeHandle: GetObjectTypeHandle(rts), + intPtrTypeHandle: GetIntPtrTypeHandle(rts)); + + List arguments = new(); + + if (hasThis) + { + TargetPointer methodTablePtr = rts.GetMethodTable(methodDesc); + TypeHandle owningType = rts.GetTypeHandle(methodTablePtr); + bool isValueTypeThis = rts.IsValueType(owningType) && !rts.IsUnboxingStub(methodDesc); + + arguments.Add(new ArgumentLocation + { + Offset = transitionBlock.ThisOffset, + ElementType = isValueTypeThis ? CdacCorElementType.ValueType : CdacCorElementType.Class, + TypeHandle = owningType, + IsThis = true, + IsValueTypeThis = isValueTypeThis, + }); + } + + if (argit.HasParamType) + { + arguments.Add(new ArgumentLocation + { + Offset = argit.GetParamTypeArgOffset(), + ElementType = CdacCorElementType.I, + IsParamType = true, + }); + } + + if (argit.HasAsyncContinuation) + { + arguments.Add(new ArgumentLocation + { + Offset = argit.GetAsyncContinuationArgOffset(), + ElementType = CdacCorElementType.Object, + }); + } + + // VarArgs: mirror the runtime's FakeGcScanRoots short-circuit -- emit + // the VASigCookie slot and stop. The variadic tail is reported via + // the cookie's signature at GC scan time, not via this contract. + // CbStackPop is 0 for VarArgs on x86 (caller cleans up), and + // argit.CbStackPop() is unsafe to call on the VarArgs-configured + // iterator -- short-circuit both here. + if (isVarArg) + { + arguments.Add(new ArgumentLocation + { + Offset = argit.GetVASigCookieOffset(), + ElementType = CdacCorElementType.I, + IsVASigCookie = true, + }); + return new ArgumentLayout(arguments, CbStackPop: 0); + } + + int argIndex = 0; + int argOffset; + while ((argOffset = argit.GetNextOffset()) != TransitionBlock.InvalidOffset) + { + if (argIndex < parameterTypes.Length) + { + CdacCorElementType elemType; + if (paramInfo[argIndex].IsByRef) + { + // ELEMENT_TYPE_BYREF wrapper: pass-by-reference (managed pointer). + elemType = CdacCorElementType.Byref; + } + else if (paramInfo[argIndex].OutermostKind != default(CdacCorElementType)) + { + // Outermost wrapper was something the standard signature + // provider may have dropped (SzArray / Array / Ptr). Use + // the kind we recorded during the wrapper-provider walk. + elemType = paramInfo[argIndex].OutermostKind; + } + else + { + elemType = rts.GetSignatureCorElementType(methodSig.ParameterTypes[argIndex]); + } + + if (argOffset == TransitionBlock.StructInRegsOffset) + throw new NotImplementedException("SystemV AMD64 struct-in-registers is not yet supported by the cDAC."); + + bool passedByRef = elemType == CdacCorElementType.ValueType + && transitionBlock.IsArgPassedByRef(parameterTypes[argIndex]); + + // Detect ByRefLike value types (Span, ReadOnlySpan, + // ref structs in general). The runtime emits one INTERIOR + // token per managed-pointer field inside the unboxed struct + // via ByRefPointerOffsetsReporter, in addition to any REF + // tokens from GCDesc. For constructed generic instantiations + // (Span) the closed TypeHandle may be uncached/null, so + // we fall back to the open generic type captured during + // signature decoding. + bool isByRefLikeStruct = false; + if (elemType == CdacCorElementType.ValueType && !passedByRef) + { + TypeHandle probe = methodSig.ParameterTypes[argIndex]; + if (probe.Address == TargetPointer.Null) + probe = paramInfo[argIndex].OpenGenericType; + if (probe.Address != TargetPointer.Null) + { + try { isByRefLikeStruct = rts.IsByRefLike(probe); } + catch { /* leave false on partial-state failures */ } + } + } + + arguments.Add(new ArgumentLocation + { + Offset = argOffset, + ElementType = elemType, + TypeHandle = methodSig.ParameterTypes[argIndex], + IsPassedByRef = passedByRef, + IsByRefLikeStruct = isByRefLikeStruct, + OpenGenericType = paramInfo[argIndex].OpenGenericType, + }); + } + argIndex++; + } + + // CbStackPop is only consumed on x86; skip the call elsewhere. + uint cbStackPop = runtimeInfo.GetTargetArchitecture() == RuntimeInfoArchitecture.X86 + ? argit.CbStackPop() + : 0; + + return new ArgumentLayout(arguments, cbStackPop); + } + + private MethodSignature DecodeMethodSignature( + IRuntimeTypeSystem rts, MethodDescHandle methodDesc) + { + TargetPointer methodTablePtr = rts.GetMethodTable(methodDesc); + 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) + throw new InvalidOperationException("Cannot read metadata for method"); + + // Carry both the method and its owning type as the generic context so + // ELEMENT_TYPE_VAR and _MVAR each resolve through the right + // instantiation. The standard one-handle SignatureTypeProvider throws + // NotSupportedException for whichever side it wasn't parameterized on. + MethodSigContext context = new(methodDesc, typeHandle); + MethodAndTypeContextProvider provider = new(_target, moduleHandle, rts); + RuntimeSignatureDecoder decoder = new( + provider, _target, mdReader, context); + + if (rts.IsStoredSigMethodDesc(methodDesc, out ReadOnlySpan storedSig)) + { + unsafe + { + fixed (byte* pStoredSig = storedSig) + { + BlobReader blobReader = new(pStoredSig, storedSig.Length); + return decoder.DecodeMethodSignature(ref blobReader); + } + } + } + + uint methodToken = rts.GetMethodToken(methodDesc); + if (methodToken == (uint)EcmaMetadataUtils.TokenType.mdtMethodDef) + throw new InvalidOperationException("Method has no token"); + + MethodDefinitionHandle methodDefHandle = MetadataTokens.MethodDefinitionHandle( + (int)EcmaMetadataUtils.GetRowId(methodToken)); + MethodDefinition methodDef = mdReader.GetMethodDefinition(methodDefHandle); + BlobReader sigReader = mdReader.GetBlobReader(methodDef.Signature); + return decoder.DecodeMethodSignature(ref sigReader); + } + + // Re-decode the method signature using a wrapper provider that records + // per-parameter metadata the standard signature provider would discard: + // - whether the parameter is wrapped in ELEMENT_TYPE_BYREF, and + // - the outermost element type (SzArray / Array / Ptr / Byref) so + // constructed-type wrappers the runtime hasn't cached don't get + // silently dropped via null TypeHandles. + private ParamTypeInfo[] DecodeParamTypeInfo(IRuntimeTypeSystem rts, MethodDescHandle methodDesc, int paramCount) + { + if (paramCount == 0) + return Array.Empty(); + + TargetPointer methodTablePtr = rts.GetMethodTable(methodDesc); + 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 new ParamTypeInfo[paramCount]; + + MethodSigContext context = new(methodDesc, typeHandle); + ParamMetadataProvider provider = new(new MethodAndTypeContextProvider(_target, moduleHandle, rts), rts); + RuntimeSignatureDecoder decoder = new( + provider, _target, mdReader, context); + + MethodSignature sig; + if (rts.IsStoredSigMethodDesc(methodDesc, out ReadOnlySpan storedSig)) + { + unsafe + { + fixed (byte* pStoredSig = storedSig) + { + BlobReader blobReader = new(pStoredSig, storedSig.Length); + sig = decoder.DecodeMethodSignature(ref blobReader); + } + } + } + else + { + uint methodToken = rts.GetMethodToken(methodDesc); + if (methodToken == (uint)EcmaMetadataUtils.TokenType.mdtMethodDef) + return new ParamTypeInfo[paramCount]; + + MethodDefinitionHandle methodDefHandle = MetadataTokens.MethodDefinitionHandle( + (int)EcmaMetadataUtils.GetRowId(methodToken)); + MethodDefinition methodDef = mdReader.GetMethodDefinition(methodDefHandle); + BlobReader sigReader = mdReader.GetBlobReader(methodDef.Signature); + sig = decoder.DecodeMethodSignature(ref sigReader); + } + + ParamTypeInfo[] result = new ParamTypeInfo[paramCount]; + int count = Math.Min(paramCount, sig.ParameterTypes.Length); + for (int i = 0; i < count; i++) + { + TrackedType t = sig.ParameterTypes[i]; + result[i] = new ParamTypeInfo + { + IsByRef = t.IsByRef, + OutermostKind = t.OutermostKind, + OpenGenericType = t.OpenGeneric, + }; + } + return result; + } + + private static TransitionBlock BuildTransitionBlock(IRuntimeInfo runtimeInfo) + { + RuntimeInfoArchitecture arch = runtimeInfo.GetTargetArchitecture(); + RuntimeInfoOperatingSystem os = runtimeInfo.GetTargetOperatingSystem(); + + Internal.TypeSystem.TargetArchitecture targetArch = arch switch + { + RuntimeInfoArchitecture.X86 => Internal.TypeSystem.TargetArchitecture.X86, + RuntimeInfoArchitecture.X64 => Internal.TypeSystem.TargetArchitecture.X64, + RuntimeInfoArchitecture.Arm => Internal.TypeSystem.TargetArchitecture.ARM, + RuntimeInfoArchitecture.Arm64 => Internal.TypeSystem.TargetArchitecture.ARM64, + RuntimeInfoArchitecture.LoongArch64 => Internal.TypeSystem.TargetArchitecture.LoongArch64, + RuntimeInfoArchitecture.RiscV64 => Internal.TypeSystem.TargetArchitecture.RiscV64, + RuntimeInfoArchitecture.Wasm => Internal.TypeSystem.TargetArchitecture.Wasm32, + _ => throw new NotSupportedException($"Unsupported architecture: {arch}"), + }; + + bool isWindows = os == RuntimeInfoOperatingSystem.Windows; + bool isApplePlatform = os == RuntimeInfoOperatingSystem.Apple; + + return TransitionBlock.FromTarget(targetArch, isWindows, isApplePlatform, isArmel: false); + } + + // Well-known type handles passed to ArgIterator. The shared iterator only + // dereferences them when extraObjectFirstArg / extraFunctionPointerArg are + // set; this contract never sets either, so the lookups are cheap insurance + // against a future cDAC change tripping a NullReferenceException deep in + // GetArgumentType. + private CdacTypeHandle GetObjectTypeHandle(IRuntimeTypeSystem rts) + { + TargetPointer objectMt = rts.GetWellKnownMethodTable(WellKnownMethodTable.Object); + return new CdacTypeHandle(rts.GetTypeHandle(objectMt), _target); + } + + private CdacTypeHandle GetIntPtrTypeHandle(IRuntimeTypeSystem rts) + { + return new CdacTypeHandle(rts.GetPrimitiveType(CdacCorElementType.I), _target); + } + + // Result type produced by ParamMetadataProvider. Carries the underlying + // TypeHandle (resolved by the inner provider when possible) plus the + // outermost element type and an IsByRef flag, both of which the standard + // SignatureTypeProvider would otherwise drop on the floor when the runtime + // hasn't cached the constructed-type instantiation. + private readonly struct TrackedType + { + public TypeHandle Underlying { get; init; } + public bool IsByRef { get; init; } + // The outermost ELEMENT_TYPE_* wrapper applied to this signature. + // The enum's zero value (default) means "no constructed-type wrapper; + // use GetSignatureCorElementType on Underlying instead". + public CdacCorElementType OutermostKind { get; init; } + // For generic instantiations: the open generic type before + // GetConstructedType collapsed it. Lets the encoder inspect + // attributes (IsByRefLike, etc.) even when the constructed + // TypeHandle isn't cached. + public TypeHandle OpenGeneric { get; init; } + } + + // ISignatureTypeProvider wrapper that records the outermost + // ELEMENT_TYPE_* wrapper (BYREF / PTR / SZARRAY / ARRAY) on each parameter + // so the caller can recover that information even when the standard + // SignatureTypeProvider would have returned a null TypeHandle from + // GetConstructedType. Used only by DecodeParamTypeInfo. The generic + // context is a MethodDescHandle so both ELEMENT_TYPE_VAR and _MVAR can be + // resolved by the inner MethodGenericContextProvider. + private sealed class ParamMetadataProvider : IRuntimeSignatureTypeProvider + { + private readonly MethodAndTypeContextProvider _inner; + private readonly IRuntimeTypeSystem _rts; + + public ParamMetadataProvider(MethodAndTypeContextProvider inner, IRuntimeTypeSystem rts) + { + _inner = inner; + _rts = rts; + } + + // Helpers: Wrap stamps Underlying but leaves OutermostKind at default + // (the enum's 0 value, which CdacCorElementType doesn't name) so callers + // know to fall back to GetSignatureCorElementType on Underlying. The + // constructed-type overrides (ByRef/Ptr/SzArray/Array) set + // OutermostKind explicitly. + private static TrackedType Wrap(TypeHandle th) + => new() { Underlying = th }; + + public TrackedType GetByReferenceType(TrackedType elementType) + => new() { Underlying = elementType.Underlying, IsByRef = true, + OutermostKind = CdacCorElementType.Byref }; + + public TrackedType GetPointerType(TrackedType elementType) + => new() { Underlying = elementType.Underlying, + OutermostKind = CdacCorElementType.Ptr }; + + public TrackedType GetArrayType(TrackedType elementType, ArrayShape shape) + => new() { Underlying = _inner.GetArrayType(elementType.Underlying, shape), + OutermostKind = CdacCorElementType.Array }; + + public TrackedType GetSZArrayType(TrackedType elementType) + => new() { Underlying = _inner.GetSZArrayType(elementType.Underlying), + OutermostKind = CdacCorElementType.SzArray }; + + public TrackedType GetFunctionPointerType(MethodSignature signature) + => Wrap(_inner.GetPrimitiveType(PrimitiveTypeCode.IntPtr)); + + public TrackedType GetGenericInstantiation(TrackedType genericType, ImmutableArray typeArguments) + { + ImmutableArray.Builder builder = ImmutableArray.CreateBuilder(typeArguments.Length); + for (int i = 0; i < typeArguments.Length; i++) + builder.Add(typeArguments[i].Underlying); + TypeHandle constructed = _inner.GetGenericInstantiation(genericType.Underlying, builder.ToImmutable()); + + // GetConstructedType returns null when the runtime hasn't cached + // this exact instantiation. Recover the would-be top-level kind + // (Class / ValueType / ...) from the open generic type so the + // encoder still sees the right token (REF for class, etc.). + CdacCorElementType kind = default; + if (constructed.Address == TargetPointer.Null && genericType.Underlying.Address != TargetPointer.Null) + { + try { kind = _rts.GetSignatureCorElementType(genericType.Underlying); } + catch { /* leave default */ } + } + return new TrackedType + { + Underlying = constructed, + OutermostKind = kind, + OpenGeneric = genericType.Underlying, + }; + } + + public TrackedType GetGenericMethodParameter(MethodSigContext context, int index) + => Wrap(_inner.GetGenericMethodParameter(context, index)); + + public TrackedType GetGenericTypeParameter(MethodSigContext context, int index) + => Wrap(_inner.GetGenericTypeParameter(context, index)); + + public TrackedType GetModifiedType(TrackedType modifier, TrackedType unmodifiedType, bool isRequired) + => unmodifiedType; + + public TrackedType GetPinnedType(TrackedType elementType) + => elementType; + + public TrackedType GetPrimitiveType(PrimitiveTypeCode typeCode) + => Wrap(_inner.GetPrimitiveType(typeCode)); + + public TrackedType GetTypeFromDefinition(MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) + => Wrap(_inner.GetTypeFromDefinition(reader, handle, rawTypeKind)); + + public TrackedType GetTypeFromReference(MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) + => Wrap(_inner.GetTypeFromReference(reader, handle, rawTypeKind)); + + public TrackedType GetTypeFromSpecification(MetadataReader reader, MethodSigContext context, TypeSpecificationHandle handle, byte rawTypeKind) + => Wrap(_inner.GetTypeFromSpecification(reader, context, handle, rawTypeKind)); + + public TrackedType GetInternalType(TargetPointer typeHandlePointer) + => Wrap(_inner.GetInternalType(typeHandlePointer)); + + public TrackedType GetInternalModifiedType(TargetPointer typeHandlePointer, TrackedType unmodifiedType, bool isRequired) + => unmodifiedType; + } + + // Generic context for signature decoding that carries both the method + // (for ELEMENT_TYPE_MVAR resolution) and its owning type (for + // ELEMENT_TYPE_VAR resolution). The existing SignatureTypeProvider + // only resolves one or the other depending on T -- since a method + // signature can reference both kinds of type parameters, we need both. + internal readonly record struct MethodSigContext(MethodDescHandle Method, TypeHandle OwningType); + + // SignatureTypeProvider variant that resolves both VAR (owning type's + // type parameters) and MVAR (method's type parameters) by pulling the + // appropriate field out of the MethodSigContext. Overrides the base + // implementations, which only handle one direction. + // Specialization that resolves generic parameters via the + // MethodSigContext (open generic MD + owning TypeHandle) instead of + // requiring the context to be exactly a MethodDescHandle or TypeHandle. + // + // The base SignatureTypeProvider deliberately keeps its + // GetGenericMethodParameter / GetGenericTypeParameter non-virtual to + // avoid breaking downstream derived types (an override would change + // the dispatch shape they shipped against). To still route the + // signature decoder through this class's specialized lookups, we + // re-implement the IRuntimeSignatureTypeProvider interface here: + // hiding the base's methods with `new` and explicitly re-declaring + // the interface in the type's base list causes the C# compiler to + // emit a MethodImpl that rewires the interface slots to the + // derived members. Result: through-interface dispatch (which is + // how RuntimeSignatureDecoder calls them) lands on this class's + // methods without making the base virtual. + internal sealed class MethodAndTypeContextProvider + : SignatureTypeProvider, + IRuntimeSignatureTypeProvider + { + private readonly IRuntimeTypeSystem _rts; + + public MethodAndTypeContextProvider(Target target, ModuleHandle moduleHandle, IRuntimeTypeSystem rts) + : base(target, moduleHandle) + { + _rts = rts; + } + + public new TypeHandle GetGenericMethodParameter(MethodSigContext context, int index) + => _rts.GetGenericMethodInstantiation(context.Method)[index]; + + public new TypeHandle GetGenericTypeParameter(MethodSigContext context, int index) + => _rts.GetInstantiation(context.OwningType)[index]; + } + + // ===================================================================== + // GCRefMap blob encoder. Produces byte-for-byte the same output as the + // runtime's ComputeCallRefMap (frames.cpp) via the shared ArgIterator + // walk above. Used by the cdacstress ArgIterator sub-check. + // ===================================================================== + + private const int MaxGCRefMapBlobLength = 252; + private const int MaxByRefLikeRecursionDepth = 16; + + private byte[]? ComputeArgGCRefMapBlobCore(MethodDescHandle methodDesc) + { + IRuntimeInfo runtimeInfo = _target.Contracts.RuntimeInfo; + IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem; + + RuntimeInfoArchitecture arch = runtimeInfo.GetTargetArchitecture(); + bool isX86 = arch is RuntimeInfoArchitecture.X86; + + int pointerSize = _target.PointerSize; + + SortedDictionary tokens = new(); + ArgumentLayout enumeration = GetArgumentLayout(methodDesc); + + GenericContextLoc ctxLoc = GenericContextLoc.None; + + foreach (ArgumentLocation arg in enumeration.Arguments) + { + GCRefMapToken token; + if (arg.IsThis) + { + token = arg.IsValueTypeThis ? GCRefMapToken.Interior : GCRefMapToken.Ref; + } + else if (arg.IsVASigCookie) + { + token = GCRefMapToken.VASigCookie; + } + else if (arg.IsParamType) + { + // Resolve InstArgMethodDesc vs InstArgMethodTable on demand + // (cheaper than caching when most methods aren't generic). + if (ctxLoc == GenericContextLoc.None) + ctxLoc = SafeGetGenericContextLoc(rts, methodDesc); + + token = ctxLoc switch + { + GenericContextLoc.InstArgMethodDesc => GCRefMapToken.MethodParam, + GenericContextLoc.InstArgMethodTable => GCRefMapToken.TypeParam, + _ => GCRefMapToken.Skip, + }; + if (token == GCRefMapToken.Skip) + continue; + } + else + { + switch ((CorElementType)arg.ElementType) + { + case CorElementType.Class: + case CorElementType.String: + case CorElementType.Object: + case CorElementType.Array: + case CorElementType.SzArray: + token = GCRefMapToken.Ref; + break; + + case CorElementType.Byref: + token = GCRefMapToken.Interior; + break; + + case CorElementType.ValueType: + if (arg.IsPassedByRef) + { + token = GCRefMapToken.Interior; + } + else + { + bool emitted = false; + + if (arg.IsByRefLikeStruct) + { + // ByRefLike value type (Span, ReadOnlySpan, + // ByteRef, any ref struct). Mirrors the runtime's + // ByRefPointerOffsetsReporter (siginfo.cpp): walk + // the type's instance fields and emit INTERIOR + // for each ELEMENT_TYPE_BYREF field at its + // in-struct offset. ELEMENT_TYPE_PTR / IntPtr / + // void* fields are explicitly NOT reported + // (so QCallTypeHandle, ObjectHandleOnStack, + // StringHandleOnStack contribute nothing). + // + // For uncached generic instantiations (Span + // whose closed MT isn't loaded), the field + // layout lives on the open generic (Span). + // The byref/ptr distinction is preserved at the + // FieldDesc level regardless of which T closes + // the type. + TypeHandle probe = arg.TypeHandle; + if (probe.Address == TargetPointer.Null) + probe = arg.OpenGenericType; + if (probe.Address != TargetPointer.Null) + { + EmitByRefLikeInterior(rts, probe, arg.Offset, tokens); + } + emitted = true; + } + + if (rts.ContainsGCPointers(arg.TypeHandle)) + { + // By-value struct with embedded GC pointers: emit one + // Ref token per pointer slot inside the struct. Mirrors + // the runtime's ReportPointersFromValueTypeArg + // (siginfo.cpp). The GCDesc series Offset is relative + // to a boxed object's start (including the leading MT + // pointer); subtract pointerSize to translate to the + // unboxed in-frame layout. + int structFieldStart = arg.Offset - pointerSize; + foreach ((uint seriesOffset, uint seriesSize) in rts.GetGCDescSeries(arg.TypeHandle)) + { + int seriesBase = structFieldStart + (int)seriesOffset; + for (int subOff = 0; subOff < (int)seriesSize; subOff += pointerSize) + { + tokens[seriesBase + subOff] = GCRefMapToken.Ref; + } + } + emitted = true; + } + + if (!emitted) + continue; + continue; + } + break; + + default: + continue; + } + } + + tokens[arg.Offset] = token; + } + + // No GC-significant arguments. On non-x86 the empty blob is just the + // pending byte flush. On x86 it still carries the WriteStackPop prefix, + // so emit that first. + if (tokens.Count == 0) + { + if (!isX86) + return EmptyGCRefMapBlob(); + GCRefMapEncoder enc0 = default; + enc0.WriteStackPop(enumeration.CbStackPop / (uint)pointerSize); + return enc0.Flush(); + } + + // Walk positions 0..maxPos and look up each one's offset in the token + // map. This is necessary on x86 because pos-order and offset-order + // diverge there (argument registers occupy the highest offsets but + // the lowest positions). On non-x86 the mapping is monotonic so we + // could iterate the offset map directly, but using OffsetFromGCRefMapPos + // for both keeps the code path uniform. + TransitionBlock tb = BuildTransitionBlock(runtimeInfo); + + // For x86 we need to know how many slot positions exist (we'd otherwise + // miss high-pos register slots when the offset map's max is on the + // stack). Walk every recorded offset and compute its position; for x86 + // OffsetFromGCRefMapPos is bijective so the inverse is well-defined. + int maxPos = -1; + foreach (int offset in tokens.Keys) + { + int pos = GCRefMapPosFromOffset(tb, offset, isX86, pointerSize); + if (pos < 0) + return null; // alignment / out-of-range -- conservative skip + if (pos > maxPos) maxPos = pos; + } + + GCRefMapEncoder enc = default; + if (isX86) + enc.WriteStackPop(enumeration.CbStackPop / (uint)pointerSize); + + for (int pos = 0; pos <= maxPos; pos++) + { + int offset = tb.OffsetFromGCRefMapPos(pos); + if (tokens.TryGetValue(offset, out GCRefMapToken token) && token != GCRefMapToken.Skip) + { + enc.WriteToken((uint)pos, (byte)token); + if (enc.Length > MaxGCRefMapBlobLength) + return null; + } + } + return enc.Flush(); + } + + // Inverse of TransitionBlock.OffsetFromGCRefMapPos. On non-x86 the mapping + // is offset = first + pos*ptr, so pos = (offset - first) / ptr. On x86 the + // first NumArgumentRegisters positions are argument registers laid out at + // OffsetOfArgumentRegisters + ARGUMENTREGISTERS_SIZE - (pos+1)*ptr; the + // remaining positions are stack args at OffsetOfArgs + (pos - n)*ptr. + // Returns -1 on misalignment. + private static int GCRefMapPosFromOffset(TransitionBlock tb, int offset, bool isX86, int pointerSize) + { + if (!isX86) + { + int delta = offset - tb.OffsetOfFirstGCRefMapSlot; + if (delta < 0 || delta % pointerSize != 0) return -1; + return delta / pointerSize; + } + + // x86: arg registers come first in pos order, then stack args. + int argRegBase = tb.OffsetOfArgumentRegisters; + int argRegEnd = argRegBase + tb.NumArgumentRegisters * pointerSize; + if (offset >= argRegBase && offset < argRegEnd) + { + int delta = offset - argRegBase; + if (delta % pointerSize != 0) return -1; + // Reverse: pos = NumArgumentRegisters - 1 - (delta / ptr) + return tb.NumArgumentRegisters - 1 - (delta / pointerSize); + } + if (offset >= tb.OffsetOfArgs) + { + int delta = offset - tb.OffsetOfArgs; + if (delta % pointerSize != 0) return -1; + return tb.NumArgumentRegisters + (delta / pointerSize); + } + return -1; + } + + private static GenericContextLoc SafeGetGenericContextLoc(IRuntimeTypeSystem rts, MethodDescHandle md) + { + try + { + return rts.GetGenericContextLoc(md); + } + catch + { + return GenericContextLoc.None; + } + } + + // Mirror of runtime ByRefPointerOffsetsReporter (siginfo.cpp): walk the + // instance fields of a ByRefLike value type and emit one INTERIOR token + // per ELEMENT_TYPE_BYREF field at its offset within the unboxed struct + // (so absolute offset is baseOffset + fieldOffset). Recurses into nested + // ByRefLike value-type fields. ELEMENT_TYPE_PTR / IntPtr / void* fields + // are deliberately skipped to match runtime behavior for QCall-style + // handle wrappers. + private static void EmitByRefLikeInterior( + IRuntimeTypeSystem rts, + TypeHandle byRefLikeType, + int baseOffset, + SortedDictionary tokens) + { + // Bound recursion just in case the data is corrupt / cycles in a dump. + EmitByRefLikeInteriorRecursive(rts, byRefLikeType, baseOffset, tokens, depth: 0); + } + + private static void EmitByRefLikeInteriorRecursive( + IRuntimeTypeSystem rts, + TypeHandle byRefLikeType, + int baseOffset, + SortedDictionary tokens, + int depth) + { + if (depth > MaxByRefLikeRecursionDepth) + return; + if (byRefLikeType.Address == TargetPointer.Null) + return; + + IEnumerable fieldDescs; + try + { + fieldDescs = rts.GetFieldDescList(byRefLikeType); + } + catch + { + return; + } + + foreach (TargetPointer fdPtr in fieldDescs) + { + bool isStatic; + CorElementType fieldType; + uint fieldOffset; + try + { + isStatic = rts.IsFieldDescStatic(fdPtr); + if (isStatic) + continue; + fieldType = rts.GetFieldDescType(fdPtr); + fieldOffset = rts.GetFieldDescOffset(fdPtr, fieldDef: null); + } + catch + { + continue; + } + + int absOffset = baseOffset + (int)fieldOffset; + + if (fieldType == CorElementType.Byref) + { + tokens[absOffset] = GCRefMapToken.Interior; + } + else if (fieldType == CorElementType.ValueType) + { + // Nested value-type field. Recurse only if the field's own + // MethodTable is ByRefLike (matches runtime Find(FieldDesc*) + // in ByRefPointerOffsetsReporter). + TypeHandle nested = rts.GetFieldDescApproxTypeHandle(fdPtr); + if (nested.Address == TargetPointer.Null) + continue; + bool nestedByRefLike; + try { nestedByRefLike = rts.IsByRefLike(nested); } + catch { continue; } + if (!nestedByRefLike) + continue; + EmitByRefLikeInteriorRecursive(rts, nested, absOffset, tokens, depth + 1); + } + } + } + + private static byte[] EmptyGCRefMapBlob() + { + GCRefMapEncoder enc = default; + return enc.Flush(); + } + + // Bit-stream encoder mirroring native GCRefMapBuilder (inc/gcrefmap.h). + // Every encoding rule -- AppendBit's 7-bit chunks with high-bit + // continuation, WriteToken's delta encoding, Flush's final byte -- + // matches byte-for-byte. + private struct GCRefMapEncoder + { + private int _pendingByte; + private int _bits; + private uint _pos; + private List _bytes; + + public int Length => _bytes?.Count ?? 0; + + private void AppendBit(uint bit) + { + _bytes ??= new List(8); + if (bit != 0) + { + while (_bits >= 7) + { + _bytes.Add((byte)(_pendingByte | 0x80)); + _pendingByte = 0; + _bits -= 7; + } + _pendingByte |= 1 << _bits; + } + _bits++; + } + + private void AppendTwoBit(uint bits) + { + AppendBit(bits & 1); + AppendBit(bits >> 1); + } + + private void AppendInt(uint val) + { + do + { + AppendBit(val & 1); + AppendBit((val >> 1) & 1); + AppendBit((val >> 2) & 1); + val >>= 3; + AppendBit(val != 0 ? 1u : 0u); + } + while (val != 0); + } + + // x86-only prefix: encode the callee-popped stack-byte count in + // pointer-size units before any tokens. Mirrors native + // GCRefMapBuilder::WriteStackPop (inc/gcrefmap.h). Must be called + // before the first WriteToken. + public void WriteStackPop(uint stackPop) + { + if (stackPop < 3) + { + AppendTwoBit(stackPop); + } + else + { + AppendTwoBit(3); + AppendInt(stackPop - 3); + } + } + + public void WriteToken(uint pos, uint token) + { + uint posDelta = pos - _pos; + _pos = pos + 1; + + if (posDelta != 0) + { + if (posDelta < 4) + { + while (posDelta > 0) + { + AppendTwoBit(0); + posDelta--; + } + } + else + { + AppendTwoBit(3); + AppendInt((posDelta - 4) << 1); + } + } + + if (token < 3) + { + AppendTwoBit(token); + } + else + { + AppendTwoBit(3); + AppendInt(((token - 3) << 1) | 1); + } + } + + public byte[] Flush() + { + _bytes ??= new List(1); + if ((_pendingByte & 0x7F) != 0 || _pos == 0) + _bytes.Add((byte)(_pendingByte & 0x7F)); + + return _bytes.ToArray(); + } + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CdacTypeHandle.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CdacTypeHandle.cs new file mode 100644 index 00000000000000..a5b1c8882badcc --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CdacTypeHandle.cs @@ -0,0 +1,216 @@ +// 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 Internal.CallingConvention; +using Internal.JitInterface; + +using CdacCorElementType = Microsoft.Diagnostics.DataContractReader.Contracts.CorElementType; +using SharedCorElementType = Internal.CorConstants.CorElementType; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; + +/// +/// Adapts cDAC's IRuntimeTypeSystem + TypeHandle to the shared +/// interface used by ArgIterator for calling-convention computation. +/// +internal readonly struct CdacTypeHandle : ITypeHandle +{ + private readonly TypeHandle _typeHandle; + private readonly Target _target; + + // Outermost ELEMENT_TYPE_* wrapper (PTR / BYREF / SZARRAY / ARRAY / etc.) + // recorded out-of-band by the signature wrapper provider in + // CallingConvention_1.ParamMetadataProvider. Used when the underlying + // TypeHandle would be null (the runtime hasn't cached the constructed + // form), in which case Rts.GetSignatureCorElementType would return 0 and + // ArgIterator would fail to classify the arg for stack-size accounting. + // `default` (the enum's 0 value, which CorElementType doesn't name) means + // "no override; ask Rts". + private readonly CdacCorElementType _kindOverride; + + public CdacTypeHandle(TypeHandle typeHandle, Target target) + : this(typeHandle, target, kindOverride: default) + { + } + + public CdacTypeHandle(TypeHandle typeHandle, Target target, CdacCorElementType kindOverride) + { + _typeHandle = typeHandle; + _target = target; + _kindOverride = kindOverride; + } + + private IRuntimeTypeSystem Rts => _target.Contracts.RuntimeTypeSystem; + + public int PointerSize => _target.PointerSize; + public RuntimeInfoArchitecture Arch => _target.Contracts.RuntimeInfo.GetTargetArchitecture(); + + public bool IsNull() => _typeHandle.IsNull && _kindOverride == default; + + public bool IsValueType() => !_typeHandle.IsNull && Rts.IsValueType(_typeHandle); + + public bool IsPointerType() + => _kindOverride == CdacCorElementType.Ptr + || (!_typeHandle.IsNull && Rts.IsPointer(_typeHandle)); + + public bool HasIndeterminateSize() => false; + + public int GetSize() + { + // Constructed pointer/array/byref args always occupy one TADDR slot + // in the transition block (the actual pointee is reached via the + // pointer value, not stored inline). When _kindOverride is set, the + // underlying TypeHandle may be null (uncached PTR), so GetBaseSize + // would fault. + if (_kindOverride is CdacCorElementType.Ptr + or CdacCorElementType.Byref + or CdacCorElementType.SzArray + or CdacCorElementType.Array) + { + return PointerSize; + } + + if (_typeHandle.IsNull) + return 0; + + // GetBaseSize returns the full object size including object header and padding. + // For value types used in calling convention, we need the unboxed size. + // BaseSize = ObjHeader + MethodTable* + unboxed fields, aligned to pointer size. + // Unboxed size = BaseSize - 2 * PointerSize (subtract ObjHeader + MT pointer). + uint baseSize = Rts.GetBaseSize(_typeHandle); + return (int)(baseSize - (uint)(2 * PointerSize)); + } + + public SharedCorElementType GetCorElementType() + { + if (_kindOverride != default) + return MapCorElementType(_kindOverride); + + if (_typeHandle.IsNull) + return (SharedCorElementType)0; + + // Mirror the runtime's MetaSig::PeekArgNormalized -- for value types + // it resolves the closed TypeHandle and returns + // MethodTable::GetInternalCorElementType, which collapses enums to + // their underlying primitive (byte enum -> U1, int enum -> I4, ...). + // The shared ArgIterator's x86 IsArgumentInRegister relies on this + // normalization to recognise sub-pointer-size enums as register- + // passable; returning ELEMENT_TYPE_VALUETYPE for a byte enum makes + // it fall into the IsTrivialPointerSizedStruct path which then + // (correctly) rejects it because GetSize() != PointerSize, and the + // arg gets mis-accounted as stack-passed. + CdacCorElementType cdacType = Rts.GetInternalCorElementType(_typeHandle); + return MapCorElementType(cdacType); + } + + public bool RequiresAlign8() + { + return !_typeHandle.IsNull && Rts.RequiresAlign8(_typeHandle); + } + + public bool IsHomogeneousAggregate() + { + if (Arch is not RuntimeInfoArchitecture.Arm and not RuntimeInfoArchitecture.Arm64) + return false; + + // TODO(hfa): Implement HFA detection for ARM/ARM64. + // See crossgen2 TypeHandle.IsHomogeneousAggregate(). + throw new NotImplementedException("HFA detection for ARM/ARM64 is not yet implemented."); + } + + public int GetHomogeneousAggregateElementSize() + { + if (Arch is not RuntimeInfoArchitecture.Arm and not RuntimeInfoArchitecture.Arm64) + return 0; + + // TODO(hfa): Return 4 for float HFA, 8 for double HFA, 16 for Vector128 HFA. + throw new NotImplementedException("HFA element size for ARM/ARM64 is not yet implemented."); + } + + public void GetSystemVAmd64PassStructInRegisterDescriptor(out SYSTEMV_AMD64_CORINFO_STRUCT_REG_PASSING_DESCRIPTOR descriptor) + { + throw new NotImplementedException("SystemV AMD64 struct-in-registers is not yet supported by the cDAC."); + } + + public FpStructInRegistersInfo GetFpStructInRegistersInfo(Internal.TypeSystem.TargetArchitecture architecture) + { + // TODO(riscv-loongarch): Implement RISC-V/LoongArch64 FP struct classification. + // Structs with 1-2 floating-point fields can be passed in FP registers. + throw new NotImplementedException("RISC-V/LoongArch64 FP struct classification is not yet implemented."); + } + + public bool IsTrivialPointerSizedStruct() + { + // Only meaningful on x86 -- this controls whether a value-type arg + // can be passed in a register. Outside x86 (where structs always go + // through other paths) we return false so callers ignore us. + if (Arch != RuntimeInfoArchitecture.X86 || _typeHandle.IsNull || !Rts.IsValueType(_typeHandle)) + return false; + + // Must be exactly pointer-size (4 bytes on x86). + if (GetSize() != PointerSize) + return false; + + // Walk instance fields: exactly one, and that field must itself be a + // pointer-sized primitive (IntPtr/UIntPtr/I/U/Ptr/FnPtr) or another + // trivial pointer-sized struct. Mirrors crossgen2's + // TypeHandle.IsTrivialPointerSizedStruct (ILCompiler.ReadyToRun). + TargetPointer? singleFieldType = null; + foreach (TargetPointer fieldDesc in Rts.GetFieldDescList(_typeHandle)) + { + if (Rts.IsFieldDescStatic(fieldDesc)) + continue; + + if (singleFieldType.HasValue) + return false; // more than one instance field + + singleFieldType = fieldDesc; + } + + if (!singleFieldType.HasValue) + return false; + + CdacCorElementType fieldType = Rts.GetFieldDescType(singleFieldType.Value); + switch (fieldType) + { + case CdacCorElementType.I: + case CdacCorElementType.U: + case CdacCorElementType.I4: + case CdacCorElementType.U4: + case CdacCorElementType.Ptr: + case CdacCorElementType.FnPtr: + // On x86 pointer-size == 4 bytes, so I4/U4 fit too. Covers + // enums whose underlying type is Int32/UInt32. + return true; + + case CdacCorElementType.ValueType: + // Recurse: if the wrapped struct is itself a trivial + // pointer-sized struct, we are too. Resolve the field's + // TypeHandle via the field's metadata signature and + // re-run IsTrivialPointerSizedStruct on it. + TypeHandle nested = Rts.GetFieldDescApproxTypeHandle(singleFieldType.Value); + if (nested.IsNull) + return false; + return new CdacTypeHandle(nested, _target).IsTrivialPointerSizedStruct(); + + default: + return false; + } + } + + // Only used by ArgIterator on WASM32 for stack alignment of value types. + public int GetFieldAlignment() + { + throw new NotImplementedException("Field alignment is not yet implemented."); + } + + /// + /// Maps cDAC CorElementType (short names like I4) to the shared CorElementType + /// (ELEMENT_TYPE_* names). The numeric values are identical, so we cast directly. + /// + private static SharedCorElementType MapCorElementType(CdacCorElementType cdacType) + { + return (SharedCorElementType)(int)cdacType; + } +} 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 807595cfb8ca67..b0d023ba5c972c 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 @@ -612,6 +612,7 @@ 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() && _methodTables[typeHandle.Address].Flags.IsByRefLike; public bool RequiresAlign8(TypeHandle typeHandle) => !typeHandle.IsMethodTable() ? false : _methodTables[typeHandle.Address].Flags.RequiresAlign8; public bool IsContinuationWithoutMetadata(TypeHandle typeHandle) => typeHandle.IsMethodTable() && ContinuationMethodTablePointer != TargetPointer.Null @@ -1989,6 +1990,12 @@ public bool IsWrapperStub(MethodDescHandle methodDescHandle) return IsWrapperStub(methodDesc); } + public bool IsUnboxingStub(MethodDescHandle methodDescHandle) + { + MethodDesc methodDesc = _methodDescs[methodDescHandle.Address]; + return methodDesc.IsUnboxingStub; + } + private sealed class NonValidatedMethodTableQueries : MethodValidation.IMethodTableQueries { private readonly RuntimeTypeSystem_1 _rts; @@ -2071,6 +2078,35 @@ uint IRuntimeTypeSystem.GetFieldDescOffset(TargetPointer fieldDescPointer, Field return fieldDesc.DWord2 & (uint)FieldDescFlags2.OffsetMask; } + TypeHandle IRuntimeTypeSystem.GetFieldDescApproxTypeHandle(TargetPointer fieldDescPointer) + { + try + { + TargetPointer enclosingMT = ((IRuntimeTypeSystem)this).GetMTOfEnclosingClass(fieldDescPointer); + if (enclosingMT == TargetPointer.Null) + return default; + TypeHandle enclosingType = GetTypeHandle(enclosingMT); + TargetPointer modulePtr = GetModule(enclosingType); + 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; + + uint memberDef = ((IRuntimeTypeSystem)this).GetFieldDescMemberDef(fieldDescPointer); + FieldDefinitionHandle fieldDefHandle = (FieldDefinitionHandle)MetadataTokens.Handle((int)memberDef); + FieldDefinition fieldDef = mdReader.GetFieldDefinition(fieldDefHandle); + + return _target.Contracts.Signature.DecodeFieldSignature(fieldDef.Signature, moduleHandle, enclosingType); + } + catch + { + return default; + } + } + TargetPointer IRuntimeTypeSystem.GetFieldDescByName(TypeHandle typeHandle, string fieldName) { if (!typeHandle.IsMethodTable()) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs index 855a6a6d233e8c..0fc3872f2ed963 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs @@ -341,9 +341,16 @@ IReadOnlyList IStackWalk.WalkStackReferences(ThreadData thre } } } + catch (NotImplementedException ex) + { + // The calling convention or frame type is not yet supported (e.g., VarArgs, + // SystemV struct-in-registers). Skip this frame -- the DSO will have partial + // results but won't fail the entire stack walk. + Debug.WriteLine($"Skipping frame at IP=0x{gcFrame.Frame.Context.InstructionPointer:X}: {ex.Message}"); + } catch (System.Exception ex) { - // Per-frame exceptions are intentionally swallowed to provide partial results + // Unexpected per-frame exceptions are swallowed to provide partial results // rather than failing the entire stack walk. This matches the resilience model // of the legacy DAC. Callers can detect incomplete results by comparing counts. Debug.WriteLine($"Exception during WalkStackReferences at IP=0x{gcFrame.Frame.Context.InstructionPointer:X}: {ex.GetType().Name}: {ex.Message}"); 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 8447404f05e556..be1e7e17e11228 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs @@ -26,6 +26,7 @@ public static void Register(ContractRegistry registry) registry.Register("c1", static t => new Notifications_1(t)); registry.Register("c1", static t => new CodeNotifications_1(t)); registry.Register("c1", static t => new Signature_1(t)); + registry.Register("c1", static t => new CallingConvention_1(t)); registry.Register("c1", static t => new BuiltInCOM_1(t)); registry.Register("c1", static t => new ObjectiveCMarshal_1(t)); registry.Register("c1", static t => new ConditionalWeakTable_1(t)); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Microsoft.Diagnostics.DataContractReader.Contracts.csproj b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Microsoft.Diagnostics.DataContractReader.Contracts.csproj index bf80c13bbf6212..74bc7458b2a5d2 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Microsoft.Diagnostics.DataContractReader.Contracts.csproj +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Microsoft.Diagnostics.DataContractReader.Contracts.csproj @@ -30,5 +30,12 @@ + + + + + + + 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 422f413eba57ac..e60f24bc90960e 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 @@ -26,6 +26,8 @@ internal enum WFLAGS_LOW : uint GenericsMask_SharedInst = 0x00000020, // shared instantiation, e.g. List<__Canon> or List> GenericsMask_TypicalInstantiation = 0x00000030, // the type instantiated at its formal parameters, e.g. List + IsByRefLike = 0x00001000, // value type that may contain managed pointers (e.g. Span, ReadOnlySpan) + StringArrayValues = GenericsMask_NonGeneric | 0, @@ -110,6 +112,7 @@ private bool TestFlagWithMask(WFLAGS2_ENUM mask, WFLAGS2_ENUM flag) public bool IsDynamicStatics => GetFlag(WFLAGS2_ENUM.DynamicStatics) != 0; public bool IsGenericTypeDefinition => TestFlagWithMask(WFLAGS_LOW.GenericsMask, WFLAGS_LOW.GenericsMask_TypicalInstantiation); public bool IsSharedByGenericInstantiations => TestFlagWithMask(WFLAGS_LOW.GenericsMask, WFLAGS_LOW.GenericsMask_SharedInst); + public bool IsByRefLike => TestFlagWithMask(WFLAGS_LOW.IsByRefLike, WFLAGS_LOW.IsByRefLike); public bool ContainsGenericVariables => GetFlag(WFLAGS_HIGH.ContainsGenericVariables) != 0; internal static EEClassOrCanonMTBits GetEEClassOrCanonMTBits(TargetPointer eeClassOrCanonMTPtr) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.IXCLRDataProcess.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.IXCLRDataProcess.cs index 2caee9ff9583b3..0b451de4d47b6f 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.IXCLRDataProcess.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.IXCLRDataProcess.cs @@ -19,8 +19,6 @@ namespace Microsoft.Diagnostics.DataContractReader.Legacy; /// public sealed unsafe partial class SOSDacImpl : IXCLRDataProcess, IXCLRDataProcess2 { - private const uint DacStressPrivRequestFlushTargetState = 0xf2000000; - int IXCLRDataProcess.Flush() { _target.Flush(FlushScope.All); @@ -754,22 +752,18 @@ int IXCLRDataProcess.Request(uint reqCode, uint inBufferSize, byte* inBuffer, ui hr = HResults.S_OK; } } - else if (reqCode == DacStressPrivRequestFlushTargetState) + else if (StressTestApi.CdacStressApi.IsStressRequest(reqCode)) { - if (inBufferSize == 0 && inBuffer is null && outBufferSize == 0 && outBuffer is null) - { - _target.Flush(FlushScope.ForwardExecution); - hr = HResults.S_OK; - } + hr = StressTestApi.CdacStressApi.HandleRequest(_target, reqCode, inBufferSize, inBuffer, outBufferSize, outBuffer); } else { return LegacyFallbackHelper.CanFallback() && _legacyProcess is not null ? _legacyProcess.Request(reqCode, inBufferSize, inBuffer, outBufferSize, outBuffer) : HResults.E_NOTIMPL; } #if DEBUG - // The private DACSTRESSPRIV_REQUEST_FLUSH_TARGET_STATE opcode is cDAC-only - // and must NOT be forwarded to the legacy DAC. - if (_legacyProcess is not null && reqCode != DacStressPrivRequestFlushTargetState) + // Private DACSTRESSPRIV_REQUEST_* opcodes are cDAC-only and must NOT be + // forwarded to the legacy DAC. + if (_legacyProcess is not null && !StressTestApi.CdacStressApi.IsStressRequest(reqCode)) { byte[] localBuffer = new byte[(int)outBufferSize]; fixed (byte* localOutBuffer = localBuffer) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/StressTestApi/CdacStressApi.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/StressTestApi/CdacStressApi.cs new file mode 100644 index 00000000000000..6a1a440ff07e93 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/StressTestApi/CdacStressApi.cs @@ -0,0 +1,113 @@ +// 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.CompilerServices; +using System.Runtime.InteropServices; +using Microsoft.Diagnostics.DataContractReader.Contracts; + +namespace Microsoft.Diagnostics.DataContractReader.Legacy.StressTestApi; + +// Handlers for the private DACSTRESSPRIV_REQUEST_* opcodes that the +// in-proc cDAC stress harness (src/coreclr/vm/cdacstress.cpp) issues +// through IXCLRDataProcess::Request. Kept out of SOSDacImpl so the +// stress-only surface is grouped in one place; SOSDacImpl just +// delegates when it sees one of these reqCodes. +internal static unsafe class CdacStressApi +{ + public const uint RequestFlushTargetState = 0xf2000000; + public const uint RequestComputeArgGCRefMap = 0xf2000001; + + // HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER). + private const int HResultErrorInsufficientBuffer = unchecked((int)0x8007007A); + + public static bool IsStressRequest(uint reqCode) + => reqCode == RequestFlushTargetState + || reqCode == RequestComputeArgGCRefMap; + + public static int HandleRequest(Target target, uint reqCode, uint inSize, byte* inBuffer, uint outSize, byte* outBuffer) + { + return reqCode switch + { + RequestFlushTargetState => HandleFlushTargetState(target, inSize, inBuffer, outSize, outBuffer), + RequestComputeArgGCRefMap => HandleComputeArgGCRefMap(target, inSize, inBuffer, outSize, outBuffer), + _ => HResults.E_INVALIDARG, + }; + } + + private static int HandleFlushTargetState(Target target, uint inSize, byte* inBuffer, uint outSize, byte* outBuffer) + { + if (inSize != 0 || inBuffer is not null || outSize != 0 || outBuffer is not null) + return HResults.E_INVALIDARG; + target.Flush(FlushScope.ForwardExecution); + return HResults.S_OK; + } + + // Mirrors DacStressArgGCRefMapRequest in src/coreclr/inc/dacprivate.h. + // The caller hands us an [in,out] descriptor with the MethodDesc plus a + // caller-allocated destination buffer; we write the blob there and + // populate cbFilled / cbNeeded. The COM `outBuffer` channel is unused. + [StructLayout(LayoutKind.Sequential)] + private struct DacStressArgGCRefMapRequest + { + public ulong MethodDesc; + public ulong BlobBuffer; + public uint BlobBufferLen; + public uint cbFilled; + public uint cbNeeded; + } + + private static int HandleComputeArgGCRefMap(Target target, uint inSize, byte* inBuffer, uint outSize, byte* outBuffer) + { + _ = outSize; + _ = outBuffer; + + if (inBuffer is null || inSize < (uint)sizeof(DacStressArgGCRefMapRequest)) + return HResults.E_INVALIDARG; + + // Alignment-safe view of the [in,out] descriptor. The cDAC ABI hands + // us a `byte*` from a COM marshaller with no guaranteed alignment. + DacStressArgGCRefMapRequest req = Unsafe.ReadUnaligned(inBuffer); + + byte[] blob; + bool encoded; + try + { + IRuntimeTypeSystem rts = target.Contracts.RuntimeTypeSystem; + MethodDescHandle mdh = rts.GetMethodDescHandle( + new ClrDataAddress(req.MethodDesc).ToTargetPointer(target)); + encoded = target.Contracts.CallingConvention.TryComputeArgGCRefMapBlob(mdh, out blob); + } + catch + { + req.cbFilled = 0; + req.cbNeeded = 0; + Unsafe.WriteUnaligned(inBuffer, req); + return HResults.E_FAIL; + } + + if (!encoded) + { + req.cbFilled = 0; + req.cbNeeded = 0; + Unsafe.WriteUnaligned(inBuffer, req); + return HResults.E_NOTIMPL; + } + + uint needed = (uint)blob.Length; + req.cbNeeded = needed; + + if (req.BlobBuffer == 0 || req.BlobBufferLen < needed) + { + req.cbFilled = 0; + Unsafe.WriteUnaligned(inBuffer, req); + return HResultErrorInsufficientBuffer; + } + + byte* dest = (byte*)(nuint)req.BlobBuffer; + blob.AsSpan().CopyTo(new Span(dest, (int)req.BlobBufferLen)); + req.cbFilled = needed; + Unsafe.WriteUnaligned(inBuffer, req); + return HResults.S_OK; + } +} diff --git a/src/native/managed/cdac/tests/StressTests/BasicCdacStressTests.cs b/src/native/managed/cdac/tests/StressTests/BasicCdacStressTests.cs deleted file mode 100644 index a5270ed023101b..00000000000000 --- a/src/native/managed/cdac/tests/StressTests/BasicCdacStressTests.cs +++ /dev/null @@ -1,66 +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.Collections.Generic; -using System.Runtime.InteropServices; -using System.Threading.Tasks; -using Microsoft.DotNet.XUnitExtensions; -using Xunit; -using Xunit.Abstractions; - -namespace Microsoft.Diagnostics.DataContractReader.Tests.GCStress; - -/// -/// Runs each debuggee app under corerun with DOTNET_CdacStress=0x001 (ALLOC) -/// and asserts that the cDAC stack reference verification produces no -/// `[FAIL]` results. `[KNOWN_ISSUE]` verifications (where the cDAC explicitly -/// marks a frame as deferred via `RecordDeferredFrame`) are tolerated. -/// -/// -/// Prerequisites: -/// - Build CoreCLR + cDAC (Checked): build.cmd -subset clr.runtime+tools.cdac -c Checked -/// - Generate core_root: src\tests\build.cmd Checked generatelayoutonly /p:LibrariesConfiguration=Release -/// - Build debuggees: dotnet build this test project -/// -/// The tests use CORE_ROOT env var if set, otherwise default to the standard artifacts path. -/// -public class BasicStressTests : CdacStressTestBase -{ - public BasicStressTests(ITestOutputHelper output) : base(output) { } - - public static IEnumerable Debuggees => - [ - ["BasicAlloc"], - ["DeepStack"], - ["Generics"], - ["MultiThread"], - ["Comprehensive"], - ["ExceptionHandling"], - ["StructScenarios"], - ["DynamicMethods"], - ]; - - public static IEnumerable WindowsOnlyDebuggees => - [ - ["PInvoke"], - ]; - - [Theory] - [MemberData(nameof(Debuggees))] - public async Task GCStress_AllVerificationsPass(string debuggeeName) - { - CdacStressResults results = await RunGCStressAsync(debuggeeName); - AssertAllPassed(results, debuggeeName); - } - - [ConditionalTheory] - [MemberData(nameof(WindowsOnlyDebuggees))] - public async Task GCStress_WindowsOnly_AllVerificationsPass(string debuggeeName) - { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - throw new SkipTestException("P/Invoke debuggee uses kernel32.dll (Windows only)"); - - CdacStressResults results = await RunGCStressAsync(debuggeeName); - AssertAllPassed(results, debuggeeName); - } -} diff --git a/src/native/managed/cdac/tests/StressTests/CdacStressResults.cs b/src/native/managed/cdac/tests/StressTests/CdacStressResults.cs index 05b55c0918ad96..c792aeefd2b627 100644 --- a/src/native/managed/cdac/tests/StressTests/CdacStressResults.cs +++ b/src/native/managed/cdac/tests/StressTests/CdacStressResults.cs @@ -26,14 +26,34 @@ namespace Microsoft.Diagnostics.DataContractReader.Tests.GCStress; /// internal sealed partial class CdacStressResults { + // GCREFS sub-check (DOTNET_CdacStress bit 0x100). The native harness + // emits one [GC_STATS] summary line at shutdown when GCREFS is enabled. + // AnyGcRefsRecorded distinguishes "GCREFS ran" from "no [GC_STATS] + // line in the log" so an ARGITER-only run (where Passed/Failed/etc are + // all zero by design) can be told apart from a GCREFS run where the + // debuggee crashed before any allocation fired. public int TotalVerifications { get; private set; } public int Passed { get; private set; } public int Failed { get; private set; } public int KnownIssues { get; private set; } + public bool AnyGcRefsRecorded { get; private set; } public string LogFilePath { get; private set; } = string.Empty; public List FailureDetails { get; } = []; public List FailedVerifications { get; } = []; + // ArgIter sub-check (DOTNET_CdacStress bit 0x200). The native harness + // emits one [ARG_STATS] summary line at shutdown when ARGITER is enabled. + // AnyArgIterRecorded distinguishes "ARGITER ran" from "no [ARG_STATS] + // line in the log" so callers can fail fast on a missing summary + // (typically meaning the runtime wasn't built with cdacstress support + // or ARGITER wasn't actually enabled this run). + public int ArgIterPassed { get; private set; } + public int ArgIterFailed { get; private set; } + public int ArgIterSkipped { get; private set; } + public int ArgIterErrors { get; private set; } + public bool AnyArgIterRecorded { get; private set; } + public List ArgIterFailureLines { get; } = []; + [GeneratedRegex(@"^\[PASS\]")] private static partial Regex PassPattern(); @@ -65,6 +85,22 @@ internal sealed partial class CdacStressResults [GeneratedRegex(@"^#\d+\s+.+?\s+\(cDAC=\d+\s+RT=\d+\)")] private static partial Regex StackTraceLinePattern(); + // ArgIter sub-check summary: "[ARG_STATS] pass=N fail=N skip=N error=N" + [GeneratedRegex(@"^\[ARG_STATS\]\s+pass=(\d+)\s+fail=(\d+)\s+skip=(\d+)\s+error=(\d+)")] + private static partial Regex ArgStatsPattern(); + + // GCREFS sub-check summary: "[GC_STATS] verifications=N pass=N fail=N known_issue=N". + // Like [ARG_STATS], emitted only when the sub-check ran -- presence is + // the authoritative signal that GCREFS was enabled this run. + [GeneratedRegex(@"^\[GC_STATS\]\s+verifications=(\d+)\s+pass=(\d+)\s+fail=(\d+)\s+known_issue=(\d+)")] + private static partial Regex GcStatsPattern(); + + // Per-method ArgIter failure / error markers; captured verbatim into + // ArgIterFailureLines so AssertAllArgIterPassed can include them in the + // failure message without re-parsing the structured fields. + [GeneratedRegex(@"^\[ARG_(FAIL|ERROR)\]")] + private static partial Regex ArgFailOrErrorPattern(); + public static CdacStressResults Parse(string logFilePath) { if (!File.Exists(logFilePath)) @@ -110,6 +146,38 @@ public static CdacStressResults Parse(string logFilePath) continue; } + Match argStatsMatch = ArgStatsPattern().Match(trimmed); + if (argStatsMatch.Success) + { + results.ArgIterPassed = int.Parse(argStatsMatch.Groups[1].Value, CultureInfo.InvariantCulture); + results.ArgIterFailed = int.Parse(argStatsMatch.Groups[2].Value, CultureInfo.InvariantCulture); + results.ArgIterSkipped = int.Parse(argStatsMatch.Groups[3].Value, CultureInfo.InvariantCulture); + results.ArgIterErrors = int.Parse(argStatsMatch.Groups[4].Value, CultureInfo.InvariantCulture); + results.AnyArgIterRecorded = true; + continue; + } + + Match gcStatsMatch = GcStatsPattern().Match(trimmed); + if (gcStatsMatch.Success) + { + // Authoritative GCREFS counts -- override anything inferred + // from the older "Total verifications:" line / per-frame + // [PASS]/[FAIL] increments so the two stay consistent. + results.TotalVerifications = int.Parse(gcStatsMatch.Groups[1].Value, CultureInfo.InvariantCulture); + results.Passed = int.Parse(gcStatsMatch.Groups[2].Value, CultureInfo.InvariantCulture); + results.Failed = int.Parse(gcStatsMatch.Groups[3].Value, CultureInfo.InvariantCulture); + results.KnownIssues = int.Parse(gcStatsMatch.Groups[4].Value, CultureInfo.InvariantCulture); + results.AnyGcRefsRecorded = true; + continue; + } + + if (ArgFailOrErrorPattern().IsMatch(trimmed)) + { + results.ArgIterFailureLines.Add(trimmed); + // Fall through: a stray [ARG_FAIL] without a preceding [ARG_STATS] + // still gets recorded for the failure analyzer below. + } + if (currentFailure is null) continue; @@ -177,8 +245,19 @@ public static CdacStressResults Parse(string logFilePath) _ => RefDisposition.Unknown, }; - public override string ToString() => - $"Total={TotalVerifications}, Passed={Passed}, Failed={Failed}, KnownIssues={KnownIssues}"; + public override string ToString() + { + // Format only the sub-checks that actually ran so the log clearly + // shows which mode produced the results. A mixed-mode run shows both. + var parts = new List(2); + if (AnyGcRefsRecorded) + parts.Add($"GCREFS Total={TotalVerifications} Passed={Passed} Failed={Failed} KnownIssues={KnownIssues}"); + if (AnyArgIterRecorded) + parts.Add($"ARGITER pass={ArgIterPassed} fail={ArgIterFailed} skip={ArgIterSkipped} error={ArgIterErrors}"); + if (parts.Count == 0) + return "(no sub-check ran -- neither [GC_STATS] nor [ARG_STATS] in log)"; + return string.Join("; ", parts); + } /// /// Formats the first N failed verifications using the structured per-frame data diff --git a/src/native/managed/cdac/tests/StressTests/CdacStressTestBase.cs b/src/native/managed/cdac/tests/StressTests/CdacStressTestBase.cs index 4e617f1c2d1589..f67ad3e5329ec7 100644 --- a/src/native/managed/cdac/tests/StressTests/CdacStressTestBase.cs +++ b/src/native/managed/cdac/tests/StressTests/CdacStressTestBase.cs @@ -15,21 +15,55 @@ namespace Microsoft.Diagnostics.DataContractReader.Tests.GCStress; /// /// Base class for cDAC stress tests. Runs a debuggee app under corerun -/// with DOTNET_CdacStress=0x001 (ALLOC) and parses the verification results. +/// with a configurable DOTNET_CdacStress value and parses the +/// verification results. /// public abstract class CdacStressTestBase { private readonly ITestOutputHelper _output; + /// + /// Stress sub-checks enabled together with the ALLOC (where) trigger. + /// Maps directly onto the WHAT byte of DOTNET_CdacStress. + /// + protected enum StressMode + { + /// + /// 0x101 = ALLOC + GCREFS -- compare cDAC GetStackReferences + /// vs the runtime's own GC root oracle at every allocation. + /// + GcRefs, + + /// + /// 0x201 = ALLOC + ARGITER -- compare cDAC EnumerateArguments- + /// derived GCRefMap blobs vs runtime ComputeCallRefMap at + /// every allocation. Independent of GCREFS so the two can be run + /// from separate test methods on the same build. + /// + ArgIter, + } + protected CdacStressTestBase(ITestOutputHelper output) { _output = output; } /// - /// Runs the named debuggee under GC stress and returns the parsed results. + /// Runs the named debuggee under the GCREFS sub-check + /// (DOTNET_CdacStress=0x101) and returns the parsed results. Convenience + /// shim around . /// - internal async Task RunGCStressAsync(string debuggeeName, int timeoutSeconds = 300) + internal Task RunGCRefStressAsync(string debuggeeName, int timeoutSeconds = 300) + => RunStressAsync(debuggeeName, StressMode.GcRefs, timeoutSeconds); + + /// + /// Runs the named debuggee under the ARGITER sub-check + /// (DOTNET_CdacStress=0x201) and returns the parsed results. + /// + internal Task RunArgIterStressAsync(string debuggeeName, int timeoutSeconds = 300) + => RunStressAsync(debuggeeName, StressMode.ArgIter, timeoutSeconds); + + private async Task RunStressAsync(string debuggeeName, StressMode mode, int timeoutSeconds) { string coreRoot = GetCoreRoot(); string corerun = Path.Combine(coreRoot, OperatingSystem.IsWindows() ? "corerun.exe" : "corerun"); @@ -41,9 +75,20 @@ internal async Task RunGCStressAsync(string debuggeeName, int // Locally, fall back to the system temp directory. string logDir = Environment.GetEnvironmentVariable("HELIX_WORKITEM_UPLOAD_ROOT") ?? Path.GetTempPath(); - string logFile = Path.Combine(logDir, $"cdac-gcstress-{debuggeeName}-{Guid.NewGuid():N}.txt"); + string modeTag = mode == StressMode.GcRefs ? "gcrefs" : "argiter"; + string logFile = Path.Combine(logDir, $"cdac-{modeTag}-{debuggeeName}-{Guid.NewGuid():N}.txt"); - _output.WriteLine($"Running GC stress: {debuggeeName}"); + // Mirrors the cdacstress.cpp flag layout: byte 0 = WHERE (0x01 = ALLOC), + // byte 1 = WHAT (0x100 = GCREFS, 0x200 = ARGITER). Verifies every stress + // hit; the debuggee's own iteration count keeps test time bounded. + string flags = mode switch + { + StressMode.GcRefs => "0x101", + StressMode.ArgIter => "0x201", + _ => throw new ArgumentOutOfRangeException(nameof(mode)), + }; + + _output.WriteLine($"Running {modeTag} stress: {debuggeeName} (DOTNET_CdacStress={flags})"); _output.WriteLine($" corerun: {corerun}"); _output.WriteLine($" debuggee: {debuggeeDll}"); _output.WriteLine($" log: {logFile}"); @@ -57,9 +102,7 @@ internal async Task RunGCStressAsync(string debuggeeName, int RedirectStandardError = true, }; psi.Environment["CORE_ROOT"] = coreRoot; - // Verifies every stress hit. We rely on the debuggee's own iteration - // count to keep test time bounded. - psi.Environment["DOTNET_CdacStress"] = "0x001"; + psi.Environment["DOTNET_CdacStress"] = flags; psi.Environment["DOTNET_CdacStressFailFast"] = "0"; psi.Environment["DOTNET_CdacStressLogFile"] = logFile; psi.Environment["DOTNET_ContinueOnAssert"] = "1"; @@ -82,7 +125,7 @@ internal async Task RunGCStressAsync(string debuggeeName, int catch (OperationCanceledException) { process.Kill(entireProcessTree: true); - Assert.Fail($"GC stress test '{debuggeeName}' timed out after {timeoutSeconds}s"); + Assert.Fail($"cDAC {modeTag} stress test '{debuggeeName}' timed out after {timeoutSeconds}s"); throw; } @@ -96,10 +139,10 @@ internal async Task RunGCStressAsync(string debuggeeName, int _output.WriteLine($" stderr: {stderr.TrimEnd()}"); Assert.True(process.ExitCode == 100, - $"GC stress test '{debuggeeName}' exited with {process.ExitCode} (expected 100).\nstdout: {stdout}\nstderr: {stderr}"); + $"cDAC {modeTag} stress test '{debuggeeName}' exited with {process.ExitCode} (expected 100).\nstdout: {stdout}\nstderr: {stderr}"); Assert.True(File.Exists(logFile), - $"GC stress results log not created: {logFile}\n" + + $"cDAC {modeTag} stress results log not created: {logFile}\n" + $" This usually means the cDAC stress framework failed to initialize\n" + $" (e.g. could not load mscordaccore_universal, log directory missing,\n" + $" or DOTNET_CdacStress not honored).\n" + @@ -113,8 +156,9 @@ internal async Task RunGCStressAsync(string debuggeeName, int } /// - /// Asserts the GC stress run produced at least one verification and had no - /// hard failures. is intentionally + /// Asserts the GCREFS stress run produced a [GC_STATS] summary + /// with at least one verification and no hard failures. + /// is intentionally /// tolerated (the native harness emits [KNOWN_ISSUE] for acknowledged /// divergences via s_knownIssueCount, separate from /// s_failCount) but is logged so regressions in the known-issue @@ -122,22 +166,121 @@ internal async Task RunGCStressAsync(string debuggeeName, int /// internal static void AssertAllPassed(CdacStressResults results, string debuggeeName) { + Assert.True(results.AnyGcRefsRecorded, + $"GCREFS stress test '{debuggeeName}' produced no [GC_STATS] line — " + + "GCREFS sub-check did not run (DOTNET_CdacStress missing the 0x100 bit, " + + "or the native harness was not built with cdacstress support).\n" + + $"Log: {results.LogFilePath}"); + Assert.True(results.TotalVerifications > 0, - $"GC stress test '{debuggeeName}' produced zero verifications — " + - "the cDAC stress framework may not be enabled (DOTNET_CdacStress unset, " + - "or coreclr built without CDAC_STRESS)."); + $"GCREFS stress test '{debuggeeName}' verified zero allocation sites — " + + "the debuggee may not have allocated, or the cdacstress framework " + + "did not initialize correctly.\n" + + $"Log: {results.LogFilePath}"); if (results.Failed > 0) { string analysis = results.AnalyzeFailures(maxFailures: 3); Assert.Fail( - $"GC stress test '{debuggeeName}' had {results.Failed} failure(s) " + + $"GCREFS stress test '{debuggeeName}' had {results.Failed} failure(s) " + $"out of {results.TotalVerifications} verifications " + $"({results.KnownIssues} known issue(s) tolerated).\n" + $"Log: {results.LogFilePath}\n\n{analysis}"); } } + /// + /// Asserts the ArgIter stress run produced an [ARG_STATS] summary + /// with non-zero verifications and zero hard failures. + /// is tolerated (and logged + /// so triage can see it), mirroring how + /// tolerates for GCREFS. + /// [ARG_SKIP] is emitted by the native harness when either side + /// returns E_NOTIMPL / S_FALSE -- an acknowledged gap, not a + /// divergence. [ARG_FAIL] (byte-for-byte mismatch) and + /// [ARG_ERROR] (unexpected failure HR from cDAC or runtime) still + /// fail the test. + /// + internal static void AssertAllArgIterPassed(CdacStressResults results, string debuggeeName) + { + Assert.True(results.AnyArgIterRecorded, + $"ArgIter stress test '{debuggeeName}' produced no [ARG_STATS] line — " + + "ARGITER sub-check did not run (DOTNET_CdacStress missing the 0x200 bit, " + + "or the native harness was not built with cdacstress support).\n" + + $"Log: {results.LogFilePath}"); + + int total = results.ArgIterPassed + results.ArgIterFailed + results.ArgIterSkipped + results.ArgIterErrors; + Assert.True(total > 0, + $"ArgIter stress test '{debuggeeName}' verified zero methods — " + + "the debuggee may have completed before any alloc trigger fired " + + "(typical fix: call AllocBurst() at the entry of each test method).\n" + + $"Log: {results.LogFilePath}"); + + if (results.ArgIterFailed > 0 || results.ArgIterErrors > 0) + { + // Surface up to a handful of [ARG_FAIL] / [ARG_ERROR] lines so the + // test failure message is actionable without opening the log. + const int MaxFailLines = 5; + string sample = results.ArgIterFailureLines.Count > 0 + ? string.Join('\n', results.ArgIterFailureLines.Take(MaxFailLines)) + : "(no [ARG_FAIL] / [ARG_ERROR] lines captured in log)"; + Assert.Fail( + $"ArgIter stress test '{debuggeeName}' had " + + $"{results.ArgIterFailed} fail / {results.ArgIterErrors} error out of " + + $"{total} verifications ({results.ArgIterSkipped} skip(s) tolerated).\n" + + $"Log: {results.LogFilePath}\n\n" + + $"First {Math.Min(MaxFailLines, results.ArgIterFailureLines.Count)} divergence line(s):\n{sample}"); + } + } + + /// + /// Resolve the OS + architecture of the corerun the harness will exec. + /// Both differ from the testhost process when CORE_ROOT points at a + /// different layout (typical local case: x64 dotnet driving an x86 or + /// cross-OS Core_Root). Parses both from the CORE_ROOT path's + /// <os>.<arch>.<config> segment when present; + /// falls back to the current process when not (Helix's path layout + /// doesn't encode arch/os, but matches the testhost there anyway). + /// + protected static void GetTargetPlatform(out OSPlatform os, out Architecture arch) + { + string coreRoot = GetCoreRoot(); + + // Standard layout: artifacts/tests/coreclr/../Tests/Core_Root + foreach (string segment in coreRoot.Split([Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar])) + { + string[] parts = segment.Split('.'); + if (parts.Length != 3) + continue; + OSPlatform? osMatch = parts[0].ToLowerInvariant() switch + { + "windows" => OSPlatform.Windows, + "linux" => OSPlatform.Linux, + "osx" => OSPlatform.OSX, + _ => null, + }; + Architecture? archMatch = parts[1].ToLowerInvariant() switch + { + "x86" => Architecture.X86, + "x64" => Architecture.X64, + "arm" => Architecture.Arm, + "arm64" => Architecture.Arm64, + _ => null, + }; + if (osMatch is not null && archMatch is not null) + { + os = osMatch.Value; + arch = archMatch.Value; + return; + } + } + + os = OperatingSystem.IsWindows() ? OSPlatform.Windows + : OperatingSystem.IsMacOS() ? OSPlatform.OSX + : OSPlatform.Linux; + arch = RuntimeInformation.ProcessArchitecture; + } + private static string GetCoreRoot() { // Explicit override wins (typical when running locally with a custom layout). diff --git a/src/native/managed/cdac/tests/StressTests/CdacStressTests.cs b/src/native/managed/cdac/tests/StressTests/CdacStressTests.cs new file mode 100644 index 00000000000000..4c6676b5e8b8a9 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/CdacStressTests.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 System.Runtime.InteropServices; +using System.Threading.Tasks; +using Microsoft.DotNet.XUnitExtensions; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.DataContractReader.Tests.GCStress; + +/// +/// Runs each debuggee under corerun with the cDAC stress framework enabled +/// and asserts the cross-checked verification produces no failures. The +/// GCRefStress_* theories run with DOTNET_CdacStress=0x101 (ALLOC + +/// GCREFS); the ArgIterStress_* theories run with 0x201 (ALLOC + +/// ARGITER). See StressTests/README.md for the flag layout and the +/// pass/fail semantics. +/// +public class CdacStressTests : CdacStressTestBase +{ + public CdacStressTests(ITestOutputHelper output) : base(output) { } + + public record Debuggee(string Name, bool WindowsOnly = false, bool SkipGCRefs = false); + + public static IEnumerable Debuggees => + [ + [new Debuggee("BasicAlloc")], + [new Debuggee("DeepStack")], + [new Debuggee("Generics")], + [new Debuggee("MultiThread")], + [new Debuggee("Comprehensive")], + [new Debuggee("ExceptionHandling")], + [new Debuggee("StructScenarios")], + [new Debuggee("DynamicMethods")], + [new Debuggee("CallSignatures")], + [new Debuggee("CrossModule")], + [new Debuggee("PInvoke", WindowsOnly: true)], + // VarArgs is intentionally excluded from GCREFS: the cDAC's + // GetStackReferences does not yet walk the VASigCookie signature + // blob to enumerate the variadic-tail GC refs, so GCREFS reports + // false failures on vararg frames. ARGITER has no such gap (the + // encoder emits GCRefMapToken.VASigCookie and stops, matching the + // runtime's FakeGcScanRoots short-circuit). + [new Debuggee("VarArgs", WindowsOnly: true, SkipGCRefs: true)], + ]; + + [ConditionalTheory] + [MemberData(nameof(Debuggees))] + public async Task GCRefStress_AllVerificationsPass(Debuggee debuggee) + { + GetTargetPlatform(out OSPlatform os, out Architecture arch); + + if (debuggee.WindowsOnly && os != OSPlatform.Windows) + throw new SkipTestException($"{debuggee.Name} debuggee is Windows-only."); + + if (debuggee.SkipGCRefs) + throw new SkipTestException($"{debuggee.Name} is excluded from GCREFS pending follow-up work."); + + // The GCREFS sub-check has only been validated on architectures where + // the cDAC GC root enumeration is at parity with the runtime. x86 has + // not been brought up yet (a separate effort); skip there until it is. + if (arch == Architecture.X86) + throw new SkipTestException("GCREFS stress is not yet validated on x86 (ARGITER stress runs there instead)"); + + CdacStressResults results = await RunGCRefStressAsync(debuggee.Name); + AssertAllPassed(results, debuggee.Name); + } + + [ConditionalTheory] + [MemberData(nameof(Debuggees))] + public async Task ArgIterStress_AllVerificationsPass(Debuggee debuggee) + { + GetTargetPlatform(out OSPlatform os, out Architecture arch); + + if (debuggee.WindowsOnly && os != OSPlatform.Windows) + throw new SkipTestException($"{debuggee.Name} debuggee is Windows-only."); + + // Scope of this PR: ARGITER is validated on Windows x86 / x64 + // only. Other architectures hit known gaps that need follow-up + // work (SystemV-AMD64 / ARM64 struct-in-register classification, + // arm32 ABI port). Skip there until those land. + if (os != OSPlatform.Windows || arch is not (Architecture.X86 or Architecture.X64)) + throw new SkipTestException( + "ARGITER stress is validated for windows-x86 / windows-x64 in this PR; " + + "other targets need follow-up work (SystemV / ARM64 struct-in-registers, ARM32 ABI port)."); + + CdacStressResults results = await RunArgIterStressAsync(debuggee.Name); + AssertAllArgIterPassed(results, debuggee.Name); + } +} diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/CallSignatures/CallSignatures.csproj b/src/native/managed/cdac/tests/StressTests/Debuggees/CallSignatures/CallSignatures.csproj new file mode 100644 index 00000000000000..1c979204990abb --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/CallSignatures/CallSignatures.csproj @@ -0,0 +1,8 @@ + + + + $(NoWarn);SA1136;CS0649 + + diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/CallSignatures/Program.cs b/src/native/managed/cdac/tests/StressTests/Debuggees/CallSignatures/Program.cs new file mode 100644 index 00000000000000..83b3c4e5b645ad --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/CallSignatures/Program.cs @@ -0,0 +1,493 @@ +// 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.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +/// +/// Exhaustive cdacstress ArgIterator debuggee. Covers a wide variety of +/// argument shapes that exercise different paths through the runtime +/// ComputeCallRefMap encoder and the cDAC CallingConventionGCRefMapBuilder: +/// - Register vs stack-passed parameters; long signatures that spill +/// - ByRef / in / out parameters (managed pointers -> INTERIOR) +/// - Native pointer / function pointer parameters (no token) +/// - Single- and multi-dimensional arrays (REF) +/// - Empty / tiny / pointer-sized / multi-field structs by value +/// - Structs containing object refs at the start, middle, end +/// - Nested structs (refs at deep offsets); deep nesting +/// - Value-type 'this' (interior pointer); ByRef return value +/// - ByRefLike value types: Span / ReadOnlySpan +/// - ByRefLike with only PTR fields (no INTERIOR expected) +/// - ByRefLike with multiple BYREF fields +/// - Nested ByRefLike (ref struct containing Span) +/// - Generic methods: T as ref / value type; multiple type params +/// - Generic value-type instance methods (interface dispatch) +/// - Enum arguments (Int32-, Int64-, byte-backed) +/// - Large-struct return (HasRetBuffArg) +/// - Mutually-recursive deep stack +/// +/// __arglist / vararg coverage lives in the dedicated VarArgs debuggee +/// because the native varargs calling convention is only supported on +/// Windows x86/x64/ARM64 (see src/coreclr/jit/target.h::compFeatureVarArg). +/// Building this debuggee with vararg methods would fail to JIT on +/// Linux/macOS, Windows ARM32, RISC-V, LoongArch64, and WASM. +/// Every test method begins with AllocBurst() so the cdacstress allocation +/// trigger fires while the frame is on the stack and per-MD dedup actually +/// produces an ARG_PASS / ARG_FAIL log line for it. +/// +internal static unsafe class Program +{ + // Static sink to keep allocations from being elided by the JIT. + private static object? s_sink; + + // Each test method calls this at entry. AllocBurst itself is also + // NoInlining so it shows up as a distinct frame, but the important + // thing is that the CALLER (the test method we want verified) is + // still on the stack at the moment of allocation. + // + // 32 allocations is intentional: the cdacstress allocation trigger + // serializes verifications on an internal lock, and other threads / + // helper allocations may swallow the trigger for a given alloc call. + // A bigger burst maximizes the chance that at least one fires while + // the caller's frame is live, which is what per-MD dedup needs to + // record an [ARG_PASS] / [ARG_FAIL] line for the caller. + [MethodImpl(MethodImplOptions.NoInlining)] + private static void AllocBurst() + { + for (int i = 0; i < 32; i++) + { + s_sink = new object(); + } + } + + private static int Main() + { + for (int iter = 0; iter < 50; iter++) + { + Drive(); + } + // Suppress unused warning for stack-allocated buffers in test methods. + GC.KeepAlive(s_sink); + return 100; + } + + // ---- Driver: invokes every test method. NoInlining so it stays its own + // frame; the test methods themselves are NoInlining (see attribute on + // each). Wrapped categories live in helpers below. + [MethodImpl(MethodImplOptions.NoInlining)] + private static void Drive() + { + ArgCountCategory(); + ByRefCategory(); + PointerCategory(); + ArrayCategory(); + StructByValueCategory(); + NestedStructCategory(); + ValueTypeThisCategory(); + ByRefLikeCategory(); + GenericCategory(); + EnumCategory(); + ReturnCategory(); + DeepStackCategory(); + } + + // ===== Category 1: argument count / register vs stack ===== + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ArgCountCategory() + { + OneRef("a"); + TwoRefs("a", "b"); + ThreeRefs("a", "b", "c"); + FourRefs("a", "b", "c", "d"); + EightRefs("a", "b", "c", "d", "e", "f", "g", "h"); + ManyPrimitives(1, 2, 3, 4, 5, 6, 7, 8); + ManyLongs(1, 2, 3, 4); + MixedSizes(1, 2L, "a", 3, "b", 4L); + MixedRefAndPrimitive("x", 1, "y", 2, "z", 3); + } + + [MethodImpl(MethodImplOptions.NoInlining)] private static void OneRef(string a) { AllocBurst(); GC.KeepAlive(a); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void TwoRefs(string a, string b) { AllocBurst(); GC.KeepAlive(a); GC.KeepAlive(b); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void ThreeRefs(string a, string b, string c) { AllocBurst(); GC.KeepAlive(a); GC.KeepAlive(b); GC.KeepAlive(c); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void FourRefs(string a, string b, string c, string d) { AllocBurst(); GC.KeepAlive(a); GC.KeepAlive(b); GC.KeepAlive(c); GC.KeepAlive(d); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void EightRefs(string a, string b, string c, string d, string e, string f, string g, string h) + { + AllocBurst(); + GC.KeepAlive(a); GC.KeepAlive(b); GC.KeepAlive(c); GC.KeepAlive(d); + GC.KeepAlive(e); GC.KeepAlive(f); GC.KeepAlive(g); GC.KeepAlive(h); + } + + [MethodImpl(MethodImplOptions.NoInlining)] private static int ManyPrimitives(int a, int b, int c, int d, int e, int f, int g, int h) { AllocBurst(); return a + b + c + d + e + f + g + h; } + [MethodImpl(MethodImplOptions.NoInlining)] private static long ManyLongs(long a, long b, long c, long d) { AllocBurst(); return a + b + c + d; } + [MethodImpl(MethodImplOptions.NoInlining)] private static void MixedSizes(int a, long b, string c, int d, string e, long f) { AllocBurst(); GC.KeepAlive(c); GC.KeepAlive(e); GC.KeepAlive((object)(a + b + d + f)); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void MixedRefAndPrimitive(string a, int b, string c, int d, string e, int f) { AllocBurst(); GC.KeepAlive(a); GC.KeepAlive(c); GC.KeepAlive(e); GC.KeepAlive((object)(b + d + f)); } + + // ===== Category 2: by-ref / in / out ===== + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ByRefCategory() + { + int x = 1; + ByRefInt(ref x); + InInt(in x); + OutInt(out _); + + string s = "a"; + ByRefRef(ref s); + + ByRefMixed(1, ref x, "lit", ref s); + + Holder h = default; + ByRefStruct(ref h); + } + + [MethodImpl(MethodImplOptions.NoInlining)] private static void ByRefInt(ref int x) { AllocBurst(); x++; } + [MethodImpl(MethodImplOptions.NoInlining)] private static void InInt(in int x) { AllocBurst(); GC.KeepAlive((object)x); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void OutInt(out int x) { AllocBurst(); x = 1; } + [MethodImpl(MethodImplOptions.NoInlining)] private static void ByRefRef(ref string s) { AllocBurst(); GC.KeepAlive(s); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void ByRefMixed(int a, ref int b, string c, ref string d) { AllocBurst(); b += a; GC.KeepAlive(c); GC.KeepAlive(d); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void ByRefStruct(ref Holder h) { AllocBurst(); GC.KeepAlive(h.Ref); } + + private struct Holder + { + public int Pad; + public object? Ref; + } + + // ===== Category 3: pointers (native, function) ===== + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void PointerCategory() + { + int v = 1; + PtrInt(&v); + Ptr2Int(&v, &v); + PtrMix("a", &v, "b"); + VoidPtr((void*)1); + FnPtrArg(&HelperForFnPtr); + } + + [MethodImpl(MethodImplOptions.NoInlining)] private static void PtrInt(int* p) { AllocBurst(); GC.KeepAlive((object)(*p)); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void Ptr2Int(int* a, int* b) { AllocBurst(); GC.KeepAlive((object)(*a + *b)); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void PtrMix(string a, int* b, string c) { AllocBurst(); GC.KeepAlive(a); GC.KeepAlive((object)(*b)); GC.KeepAlive(c); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void VoidPtr(void* p) { AllocBurst(); GC.KeepAlive((object)(nint)p); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void FnPtrArg(delegate* f) { AllocBurst(); GC.KeepAlive((object)f(1)); } + private static int HelperForFnPtr(int x) => x + 1; + + // ===== Category 4: arrays ===== + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ArrayCategory() + { + ArrayOne(new int[3]); + Array2D(new int[3, 3]); + ArrayJagged(new int[3][]); + ArrayObj(new object[3]); + ArrayMix(new int[3], "a", new object[3]); + } + + [MethodImpl(MethodImplOptions.NoInlining)] private static void ArrayOne(int[] a) { AllocBurst(); GC.KeepAlive(a); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void Array2D(int[,] a) { AllocBurst(); GC.KeepAlive(a); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void ArrayJagged(int[][] a) { AllocBurst(); GC.KeepAlive(a); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void ArrayObj(object[] a) { AllocBurst(); GC.KeepAlive(a); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void ArrayMix(int[] a, string b, object[] c) { AllocBurst(); GC.KeepAlive(a); GC.KeepAlive(b); GC.KeepAlive(c); } + + // ===== Category 5: structs by value ===== + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void StructByValueCategory() + { + Empty(); + Tiny(new TinyStruct { B = 1 }); + IntSized(new IntStruct { I = 1 }); + TwoInts(new TwoIntStruct { A = 1, B = 2 }); + DoubleSized(new DoubleStruct { D = 1.0 }); + Big(new BigStruct { A = 1, B = 2, C = 3, D = 4 }); + RefAtStart(new RefAtStartStruct { R = "a", Trailer = 1 }); + RefAtEnd(new RefAtEndStruct { Header = 1, R = "a" }); + RefInMiddle(new RefInMiddleStruct { Header = 1, R = "a", Trailer = 2 }); + TwoRefStructArg(new TwoRefStruct { A = "a", B = "b" }); + AlternatingRefs(new AlternatingRefsStruct { I1 = 1, R1 = "a", I2 = 2, R2 = "b" }); + RefAndArray(new RefAndArrayStruct { R = "a", Arr = new int[3] }); + } + + [MethodImpl(MethodImplOptions.NoInlining)] private static void Empty() { AllocBurst(); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void Tiny(TinyStruct s) { AllocBurst(); GC.KeepAlive((object)s.B); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void IntSized(IntStruct s) { AllocBurst(); GC.KeepAlive((object)s.I); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void TwoInts(TwoIntStruct s) { AllocBurst(); GC.KeepAlive((object)(s.A + s.B)); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void DoubleSized(DoubleStruct s) { AllocBurst(); GC.KeepAlive((object)s.D); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void Big(BigStruct s) { AllocBurst(); GC.KeepAlive((object)(s.A + s.B + s.C + s.D)); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void RefAtStart(RefAtStartStruct s) { AllocBurst(); GC.KeepAlive(s.R); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void RefAtEnd(RefAtEndStruct s) { AllocBurst(); GC.KeepAlive(s.R); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void RefInMiddle(RefInMiddleStruct s) { AllocBurst(); GC.KeepAlive(s.R); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void TwoRefStructArg(TwoRefStruct s) { AllocBurst(); GC.KeepAlive(s.A); GC.KeepAlive(s.B); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void AlternatingRefs(AlternatingRefsStruct s) { AllocBurst(); GC.KeepAlive(s.R1); GC.KeepAlive(s.R2); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void RefAndArray(RefAndArrayStruct s) { AllocBurst(); GC.KeepAlive(s.R); GC.KeepAlive(s.Arr); } + + private struct TinyStruct { public byte B; } + private struct IntStruct { public int I; } + private struct TwoIntStruct { public int A; public int B; } + private struct DoubleStruct { public double D; } + private struct BigStruct { public long A, B, C, D; } + private struct RefAtStartStruct { public object R; public int Trailer; } + private struct RefAtEndStruct { public int Header; public object R; } + private struct RefInMiddleStruct { public int Header; public object R; public int Trailer; } + private struct TwoRefStruct { public object A; public object B; } + private struct AlternatingRefsStruct { public int I1; public object R1; public int I2; public object R2; } + private struct RefAndArrayStruct { public object R; public int[] Arr; } + + // ===== Category 6: nested structs ===== + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void NestedStructCategory() + { + NestedPlain(new OuterPlain { I = new InnerPlain { A = 1 } }); + NestedRef(new OuterWithRef { H = 1, I = new InnerWithRef { Pad = 2, R = "a" }, T = "b" }); + DoublyNested(new Doubly { L0 = new L0 { L1 = new L1 { L2 = new L2 { R = "deep" } } } }); + NestedTwoLevelMixed(new MixedOuter + { + Pre = 1, + Mid = new MixedInner { A = "a", I = 2, B = "b" }, + Post = 3, + }); + } + + [MethodImpl(MethodImplOptions.NoInlining)] private static void NestedPlain(OuterPlain o) { AllocBurst(); GC.KeepAlive((object)o.I.A); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void NestedRef(OuterWithRef o) { AllocBurst(); GC.KeepAlive(o.I.R); GC.KeepAlive(o.T); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void DoublyNested(Doubly d) { AllocBurst(); GC.KeepAlive(d.L0.L1.L2.R); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void NestedTwoLevelMixed(MixedOuter m) { AllocBurst(); GC.KeepAlive(m.Mid.A); GC.KeepAlive(m.Mid.B); } + + private struct InnerPlain { public int A; } + private struct OuterPlain { public InnerPlain I; } + private struct InnerWithRef { public int Pad; public object R; } + private struct OuterWithRef { public int H; public InnerWithRef I; public string T; } + private struct L2 { public object R; } + private struct L1 { public L2 L2; } + private struct L0 { public L1 L1; } + private struct Doubly { public L0 L0; } + private struct MixedInner { public string A; public int I; public string B; } + private struct MixedOuter { public int Pre; public MixedInner Mid; public int Post; } + + // ===== Category 7: value-type 'this' (interior) ===== + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ValueTypeThisCategory() + { + // Instance method on a struct receives 'this' as a managed + // pointer interior to the struct -> INTERIOR. + var ws = new WithRefStructInstance { R = "a" }; + ws.Instance(); + + var gs = new GenericStructInstance { V = "a" }; + gs.Instance(); + + IDispatch d = new DispatchStruct { R = "b" }; + d.Method(); + } + + private struct WithRefStructInstance + { + public object R; + [MethodImpl(MethodImplOptions.NoInlining)] + public void Instance() { AllocBurst(); GC.KeepAlive(R); } + } + + private struct GenericStructInstance + { + public T V; + [MethodImpl(MethodImplOptions.NoInlining)] + public T Instance() { AllocBurst(); return V; } + } + + private interface IDispatch + { + void Method(); + } + + private struct DispatchStruct : IDispatch + { + public object R; + [MethodImpl(MethodImplOptions.NoInlining)] + public void Method() { AllocBurst(); GC.KeepAlive(R); } + } + + // ===== Category 8: ByRefLike (ref structs) ===== + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ByRefLikeCategory() + { + Span sp = stackalloc byte[16]; + ProcessSpan(sp); + ProcessReadOnlySpan(sp); + + // Two Span args next to each other. + Span sp2 = stackalloc byte[8]; + ProcessTwoSpans(sp, sp2); + + // Span + reference + Span mix. + ProcessSpanMix(sp, "x", sp2); + + // ByRefLike whose only field is a void* (no INTERIOR expected). + var ptrOnly = new PtrOnlyRefStruct { P = (void*)1 }; + ProcessPtrOnlyRefStruct(ptrOnly); + + // ByRefLike with two ref fields. + int a1 = 1, a2 = 2; + var multi = new TwoByRefStruct(ref a1, ref a2); + ProcessTwoByRefStruct(multi); + + // Ref struct containing a Span (nested ByRefLike). + Span nested = stackalloc byte[16]; + var nestedRef = new OuterRefWithSpan { Header = 1, Payload = nested, Trailer = 2 }; + ProcessOuterRefWithSpan(nestedRef); + } + + [MethodImpl(MethodImplOptions.NoInlining)] private static int ProcessSpan(Span s) { AllocBurst(); return s.Length; } + [MethodImpl(MethodImplOptions.NoInlining)] private static int ProcessReadOnlySpan(ReadOnlySpan s) { AllocBurst(); return s.Length; } + [MethodImpl(MethodImplOptions.NoInlining)] private static int ProcessTwoSpans(Span a, Span b) { AllocBurst(); return a.Length + b.Length; } + [MethodImpl(MethodImplOptions.NoInlining)] private static int ProcessSpanMix(Span a, string b, Span c) { AllocBurst(); GC.KeepAlive(b); return a.Length + c.Length; } + [MethodImpl(MethodImplOptions.NoInlining)] private static nint ProcessPtrOnlyRefStruct(PtrOnlyRefStruct s) { AllocBurst(); return (nint)s.P; } + [MethodImpl(MethodImplOptions.NoInlining)] private static int ProcessTwoByRefStruct(TwoByRefStruct s) { AllocBurst(); return s.A + s.B; } + [MethodImpl(MethodImplOptions.NoInlining)] private static int ProcessOuterRefWithSpan(OuterRefWithSpan o) { AllocBurst(); return o.Header + o.Payload.Length + o.Trailer; } + + private unsafe ref struct PtrOnlyRefStruct { public void* P; } + + private ref struct TwoByRefStruct + { + public ref int A; + public ref int B; + public TwoByRefStruct(ref int a, ref int b) { A = ref a; B = ref b; } + } + + private ref struct OuterRefWithSpan + { + public int Header; + public Span Payload; + public int Trailer; + } + + // ===== Category 9: generics ===== + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void GenericCategory() + { + GenericRef("a"); + GenericRef(new object()); + GenericVT(42); + GenericVT(3.14); + GenericMulti("a", new object()); + GenericConstrained(new MemoryStream()); + + // Generic instance method on a generic value type (shared + // canonical impl pulls in the param type via HasParamType). + var c1 = new Container { V = "v" }; + c1.Get(); + var c2 = new Container { V = new object() }; + c2.Get(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] private static T GenericRef(T v) where T : class { AllocBurst(); return v; } + [MethodImpl(MethodImplOptions.NoInlining)] private static T GenericVT(T v) where T : struct { AllocBurst(); return v; } + [MethodImpl(MethodImplOptions.NoInlining)] private static void GenericMulti(TA a, TB b) { AllocBurst(); GC.KeepAlive(a); GC.KeepAlive(b); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void GenericConstrained(T v) where T : class, IDisposable { AllocBurst(); GC.KeepAlive(v); } + + private struct Container + { + public T V; + [MethodImpl(MethodImplOptions.NoInlining)] public T Get() { AllocBurst(); return V; } + } + + // ===== Category 10: enums ===== + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void EnumCategory() + { + EnumInt(SomeEnum.A); + EnumLong(SomeLongEnum.A); + EnumByte(SomeByteEnum.A); + EnumInStruct(new EnumWrapper { E = SomeEnum.A, R = "a" }); + } + + [MethodImpl(MethodImplOptions.NoInlining)] private static SomeEnum EnumInt(SomeEnum e) { AllocBurst(); return e; } + [MethodImpl(MethodImplOptions.NoInlining)] private static SomeLongEnum EnumLong(SomeLongEnum e) { AllocBurst(); return e; } + [MethodImpl(MethodImplOptions.NoInlining)] private static SomeByteEnum EnumByte(SomeByteEnum e) { AllocBurst(); return e; } + [MethodImpl(MethodImplOptions.NoInlining)] private static void EnumInStruct(EnumWrapper w) { AllocBurst(); GC.KeepAlive(w.R); } + + private enum SomeEnum { A, B, C } + private enum SomeLongEnum : long { A, B, C } + private enum SomeByteEnum : byte { A, B, C } + private struct EnumWrapper { public SomeEnum E; public object R; } + + // ===== Category 11: returns ===== + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ReturnCategory() + { + _ = ReturnRef(); + _ = ReturnLarge(); + Span sp = stackalloc byte[16]; + _ = ReturnSpan(sp); + } + + [MethodImpl(MethodImplOptions.NoInlining)] private static string ReturnRef() { AllocBurst(); return "x"; } + // Large struct on Windows AMD64 (> 8 bytes, not power-of-2) -> HasRetBuffArg shifts arg offsets. + [MethodImpl(MethodImplOptions.NoInlining)] private static BigStruct ReturnLarge() { AllocBurst(); return new BigStruct { A = 1 }; } + [MethodImpl(MethodImplOptions.NoInlining)] private static Span ReturnSpan(Span s) { AllocBurst(); return s; } + + // ===== Category 12: deep stack ===== + // Mutually-recursive chains of methods with mixed signatures. At any + // given allocation trigger many frames are simultaneously live, so a + // single stack-walk verification run touches multiple MDs across + // diverse signature shapes. + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void DeepStackCategory() + { + DeepA("a", 1, 2L); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void DeepA(string a, int b, long c) + { + AllocBurst(); + DeepB(b, a, new RefAtStartStruct { R = a, Trailer = (int)c }); + GC.KeepAlive(a); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void DeepB(int b, string a, RefAtStartStruct s) + { + AllocBurst(); + DeepC(s, a, b); + GC.KeepAlive(s.R); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void DeepC(RefAtStartStruct s, string a, int b) + { + AllocBurst(); + Span sp = stackalloc byte[8]; + DeepD(sp, a, s, b); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void DeepD(Span sp, string a, RefAtStartStruct s, int b) + { + AllocBurst(); + DeepE("inner", a, s); + GC.KeepAlive(sp.Length); + GC.KeepAlive((object)b); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void DeepE(string label, string a, RefAtStartStruct s) + { + AllocBurst(); + GC.KeepAlive(label); + GC.KeepAlive(a); + GC.KeepAlive(s.R); + } +} diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/CrossModule/CrossModule.csproj b/src/native/managed/cdac/tests/StressTests/Debuggees/CrossModule/CrossModule.csproj new file mode 100644 index 00000000000000..617a3f02e13082 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/CrossModule/CrossModule.csproj @@ -0,0 +1,10 @@ + + + + + + + diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/CrossModule/Lib/CrossModuleLib.csproj b/src/native/managed/cdac/tests/StressTests/Debuggees/CrossModule/Lib/CrossModuleLib.csproj new file mode 100644 index 00000000000000..418b268dca0edd --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/CrossModule/Lib/CrossModuleLib.csproj @@ -0,0 +1,12 @@ + + + + Library + $(NetCoreAppToolCurrent) + true + enable + $(NoWarn);CS0649 + + diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/CrossModule/Lib/Types.cs b/src/native/managed/cdac/tests/StressTests/Debuggees/CrossModule/Lib/Types.cs new file mode 100644 index 00000000000000..a0d033148bf443 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/CrossModule/Lib/Types.cs @@ -0,0 +1,86 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace CrossModuleLib; + +/// +/// Reference type with embedded refs. Used as a method-arg type in the +/// CrossModule debuggee; the cDAC encoder's REF token emission for the +/// argument slot doesn't need cross-module metadata for the arg itself, +/// but the type's identity is resolved through this assembly's +/// MetadataReader. +/// +public class ManagedHolder +{ + public object? Ref1; + public string? Ref2; + public int Pad; +} + +/// +/// Value type with an embedded GC ref. Exercises the encoder's +/// GCDesc-driven REF emission across module boundaries: the +/// argument's TypeHandle resolves through the main module's +/// CrossModule.exe metadata, but the field-list walk (and offset +/// arithmetic) crosses into this library's MethodTable. +/// +public struct StructWithRef +{ + public int Header; + public object? Ref; + public int Trailer; +} + +/// +/// Nested value type whose Inner field is a value type defined in the +/// same library. Exercises GetFieldDescApproxTypeHandle's cross-module +/// resolution when the outer struct's enclosing module differs from +/// the inner field's referenced module. +/// +public struct OuterWithCrossModuleInner +{ + public int Pre; + public StructWithRef Inner; + public string? Tail; +} + +/// +/// ByRefLike struct defined in another module. Exercises the cDAC's +/// MethodTableFlags.IsByRefLike check after metadata resolution +/// crosses module boundaries. +/// +public ref struct CrossModuleRefStruct +{ + public int Header; + public Span Payload; + public int Trailer; +} + +/// +/// Generic class definition. The closed instantiation Generic<string> +/// is constructed at the use site in the main module, so the signature +/// TypeRef→TypeSpec resolution path walks both modules. +/// +public class Generic +{ + public T? Value; +} + +public struct GenericStruct +{ + public T? Value; + public int Tag; +} + +/// +/// Generic struct with an embedded GC ref. Encoder must walk this +/// type's GCDesc when an instantiation (e.g. GenericRefStruct<int>) +/// is used as a by-value arg in the main module. +/// +public struct GenericRefStruct +{ + public object? Ref; + public T? Value; +} diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/CrossModule/Program.cs b/src/native/managed/cdac/tests/StressTests/Debuggees/CrossModule/Program.cs new file mode 100644 index 00000000000000..51a1d0d7ad018f --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/CrossModule/Program.cs @@ -0,0 +1,144 @@ +// 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.CompilerServices; + +using CrossModuleLib; + +/// +/// Stresses the cDAC ArgIterator encoder across module boundaries. +/// Every test method's signature references a type defined in +/// CrossModuleLib.dll; the encoder must resolve those TypeRef tokens +/// against the lib's MetadataReader (not the main exe's) and walk +/// fields whose enclosing MethodTable lives in the lib module. +/// +/// Coverage: +/// - Class arg from other module (REF) +/// - By-value struct with embedded ref from other module (REF inside struct) +/// - Nested struct: outer + inner both in lib (cross-module GetFieldDescApproxTypeHandle) +/// - Mixed: outer in lib, contains string ref-field +/// - ByRefLike (ref struct) defined in other module +/// - Generic class instantiated with a main-module type +/// - Generic value type instantiated with a main-module type +/// - Generic struct with embedded ref, instantiated cross-module +/// +internal static class Program +{ + private static object? s_sink; + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void AllocBurst() + { + for (int i = 0; i < 32; i++) + s_sink = new object(); + } + + private static int Main() + { + for (int iter = 0; iter < 50; iter++) + { + Drive(); + } + GC.KeepAlive(s_sink); + return 100; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void Drive() + { + TakeClass(new ManagedHolder { Ref1 = new object(), Ref2 = "abc", Pad = 1 }); + + TakeStructWithRef(new StructWithRef { Header = 1, Ref = new object(), Trailer = 2 }); + + TakeOuter(new OuterWithCrossModuleInner + { + Pre = 1, + Inner = new StructWithRef { Header = 10, Ref = new object(), Trailer = 11 }, + Tail = "tail", + }); + + Span sp = stackalloc byte[16]; + TakeCrossModuleRefStruct(new CrossModuleRefStruct + { + Header = 1, + Payload = sp, + Trailer = 2, + }); + + TakeGenericClass(new Generic { Value = "g" }); + TakeGenericClassMainType(new Generic { Value = new MainModuleClass { R = "m" } }); + TakeGenericValue(new GenericStruct { Value = 42, Tag = 1 }); + TakeGenericValueWithRef(new GenericRefStruct { Ref = new object(), Value = 7 }); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void TakeClass(ManagedHolder h) + { + AllocBurst(); + GC.KeepAlive(h.Ref1); + GC.KeepAlive(h.Ref2); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void TakeStructWithRef(StructWithRef s) + { + AllocBurst(); + GC.KeepAlive(s.Ref); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void TakeOuter(OuterWithCrossModuleInner o) + { + AllocBurst(); + GC.KeepAlive(o.Inner.Ref); + GC.KeepAlive(o.Tail); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static int TakeCrossModuleRefStruct(CrossModuleRefStruct s) + { + AllocBurst(); + return s.Header + s.Payload.Length + s.Trailer; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void TakeGenericClass(Generic g) + { + AllocBurst(); + GC.KeepAlive(g); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void TakeGenericClassMainType(Generic g) + { + AllocBurst(); + GC.KeepAlive(g); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static int TakeGenericValue(GenericStruct g) + { + AllocBurst(); + return g.Value + g.Tag; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void TakeGenericValueWithRef(GenericRefStruct g) + { + AllocBurst(); + GC.KeepAlive(g.Ref); + } +} + +/// +/// Defined in the main module. Used as a generic type argument so the +/// closed instantiation Generic<MainModuleClass> combines a lib- +/// module open generic with a main-module type arg. The signature +/// TypeSpec for the parameter mixes TypeRef (Generic`1 from lib) with +/// TypeDef (MainModuleClass from main). +/// +internal class MainModuleClass +{ + public string? R; +} diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/StructScenarios/Program.cs b/src/native/managed/cdac/tests/StressTests/Debuggees/StructScenarios/Program.cs index 9067337495def2..561ec5123ec949 100644 --- a/src/native/managed/cdac/tests/StressTests/Debuggees/StructScenarios/Program.cs +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/StructScenarios/Program.cs @@ -20,6 +20,7 @@ static int Main() SmallStructReturnScenario(); StructWithRefsScenario(); InterfaceDispatchScenario(); + NestedStructScenario(); } return 100; } @@ -154,4 +155,157 @@ static void InterfaceDispatchScenario() GC.KeepAlive(s); GC.KeepAlive(gs); } + + // ===== Scenario 5: Nested structs ===== + // Argument GC scanning for by-value structs has to walk the GCDesc + // recursively when value-type fields contain (a) other value-type + // fields that in turn carry GC refs, or (b) ref-fields buried inside + // nested ByRefLike value types. The combinations below exercise the + // ArgIterator + GCDesc / ByRefPointerOffsetsReporter paths: + // - Plain nested value type with no refs (encoder should emit + // nothing, runtime should emit nothing). + // - Nested value type with GC refs at non-zero offsets (GCDesc + // series must aggregate inner ref offsets relative to the outer + // argument start). + // - Three levels of nesting with refs at the deepest level. + // - Nested ByRefLike struct (Span inside an outer ref struct): + // the encoder must walk the inner type's BYREF fields and emit + // INTERIOR at the correct offset within the outer struct. + + // Static sink so the JIT can't elide allocations / inline the + // NoInlining methods below by proving the result is dead. + static object? s_sink; + + struct InnerPlain + { + public int A; + } + + struct OuterPlain + { + public InnerPlain Inner; + } + + struct InnerWithRef + { + public int Pad; + public object Ref; + } + + struct OuterWithInnerRef + { + public int Header; + public InnerWithRef Inner; + public string Tail; + } + + struct DeepLevel0 + { + public object Ref; + } + + struct DeepLevel1 + { + public int Pad; + public DeepLevel0 Inner; + } + + struct DeepLevel2 + { + public DeepLevel1 Inner; + public int Trailer; + } + + ref struct OuterRefStructWithSpan + { + public int Header; + public Span Payload; + public int Trailer; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static int ProcessNestedPlain(OuterPlain p) + { + // Burn allocations in a loop so the cdacstress allocation trigger + // fires multiple times while this MD is live on the stack, and + // route the results through a static sink so the JIT can't elide + // them or inline this frame away. + for (int i = 0; i < 16; i++) + { + s_sink = new object(); + } + return p.Inner.A; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static object ProcessNestedRef(OuterWithInnerRef o) + { + for (int i = 0; i < 16; i++) + { + s_sink = new object(); + } + GC.KeepAlive(o.Tail); + return o.Inner.Ref; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static object ProcessDeeplyNested(DeepLevel2 d) + { + for (int i = 0; i < 16; i++) + { + s_sink = new object(); + } + return d.Inner.Inner.Ref; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static int ProcessNestedSpan(OuterRefStructWithSpan o) + { + for (int i = 0; i < 16; i++) + { + s_sink = new object(); + } + return o.Header + o.Payload.Length + o.Trailer; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void NestedStructScenario() + { + OuterPlain plain = new OuterPlain { Inner = new InnerPlain { A = 7 } }; + int v = ProcessNestedPlain(plain); + GC.KeepAlive(v); + + OuterWithInnerRef withRef = new OuterWithInnerRef + { + Header = 1, + Inner = new InnerWithRef { Pad = 2, Ref = new object() }, + Tail = "tail", + }; + object inner = ProcessNestedRef(withRef); + GC.KeepAlive(inner); + GC.KeepAlive(withRef.Tail); + + DeepLevel2 deep = new DeepLevel2 + { + Inner = new DeepLevel1 + { + Pad = 3, + Inner = new DeepLevel0 { Ref = new object() }, + }, + Trailer = 4, + }; + object deepRef = ProcessDeeplyNested(deep); + GC.KeepAlive(deepRef); + + byte[] buffer = new byte[16]; + OuterRefStructWithSpan refStruct = new OuterRefStructWithSpan + { + Header = 1, + Payload = buffer, + Trailer = 2, + }; + int sum = ProcessNestedSpan(refStruct); + GC.KeepAlive(sum); + GC.KeepAlive(buffer); + } } diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/VarArgs/Program.cs b/src/native/managed/cdac/tests/StressTests/Debuggees/VarArgs/Program.cs new file mode 100644 index 00000000000000..221755217422ab --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/VarArgs/Program.cs @@ -0,0 +1,106 @@ +// 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.CompilerServices; + +/// +/// Stresses the cDAC ArgIterator encoder's __arglist support +/// (.VASigCookie). +/// +/// +/// Lives in its own debuggee because the CLI's native varargs calling +/// convention is only supported on Windows x86 / x64 / ARM64. The JIT +/// gates the feature in src/coreclr/jit/target.h::compFeatureVarArg: +/// +/// return TargetOS::IsWindows && !TargetArchitecture::IsArm32; +/// +/// So this debuggee's methods will fail to JIT on Linux/macOS (all +/// architectures), Windows ARM32, RISC-V, LoongArch64, and WASM. The +/// xunit harness skips VarArgs on those targets via the +/// WindowsOnly flag on the Debuggee record. +/// +/// +/// +/// The VarArgs entry in CdacStressTests.Debuggees also sets +/// SkipGCRefs: true: the cDAC's GetStackReferences does not +/// yet walk the VASigCookie signature blob to enumerate variadic-tail GC +/// refs, so the GCREFS sub-check reports false failures on vararg frames. +/// ARGITER has no such gap (the encoder emits +/// GCRefMapToken.VASigCookie and stops, matching the runtime's +/// FakeGcScanRoots short-circuit), so we still exercise this +/// debuggee under the ArgIterStress_* theory. +/// +/// +internal static class Program +{ + private static object? s_sink; + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void AllocBurst() + { + for (int i = 0; i < 32; i++) + { + s_sink = new object(); + } + } + + private static int Main() + { + for (int iter = 0; iter < 50; iter++) + { + Drive(); + } + GC.KeepAlive(s_sink); + return 100; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void Drive() + { + VarargMixed(1, __arglist("a", 2, "b", 3.14)); + VarargAllRefs(1, __arglist("x", "y", "z")); + VarargFixedPrimitive(__arglist(1, 2L, 3.0)); + + var s = new InstanceVarargStruct { R = "this-ref" }; + s.Method(1, __arglist("inst-a", "inst-b")); + + DeepArglistOuter("outer", 1); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void VarargMixed(int first, __arglist) { AllocBurst(); GC.KeepAlive((object)first); } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void VarargAllRefs(int first, __arglist) { AllocBurst(); GC.KeepAlive((object)first); } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void VarargFixedPrimitive(__arglist) { AllocBurst(); } + + private struct InstanceVarargStruct + { + public object R; + + [MethodImpl(MethodImplOptions.NoInlining)] + public void Method(int first, __arglist) + { + AllocBurst(); + GC.KeepAlive(R); + GC.KeepAlive((object)first); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void DeepArglistOuter(string label, int n) + { + AllocBurst(); + DeepArglistInner(n, __arglist(label, n + 1, "tail")); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void DeepArglistInner(int n, __arglist) + { + AllocBurst(); + GC.KeepAlive((object)n); + } +} diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/VarArgs/VarArgs.csproj b/src/native/managed/cdac/tests/StressTests/Debuggees/VarArgs/VarArgs.csproj new file mode 100644 index 00000000000000..6b512ec9245ec3 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/VarArgs/VarArgs.csproj @@ -0,0 +1 @@ + diff --git a/src/native/managed/cdac/tests/StressTests/README.md b/src/native/managed/cdac/tests/StressTests/README.md index 50aae0aa6158ba..1ba33db244df0e 100644 --- a/src/native/managed/cdac/tests/StressTests/README.md +++ b/src/native/managed/cdac/tests/StressTests/README.md @@ -38,17 +38,42 @@ turns on hooks in `src/coreclr/vm/cdacstress.cpp`. The native hook: ### `DOTNET_CdacStress` flag layout -One trigger point is wired today: allocation (`gchelpers.cpp`). This is -unrelated to `DOTNET_GCStress` (the JIT instruction stress feature). +The DWORD is split into byte-wide regions: -| Bits | Name | Meaning | -|----------|-----------|-----------------------------------------------------------------| -| `0x001` | ALLOC | Verify at every managed allocation | -| `0x200` | VERBOSE | Rich per-ref diagnostics in the log | +| Byte | Region | Bits | Meaning | +|------|----------|-------------|-----------------------------------------------| +| 0 | WHERE | `0x000000FF`| Trigger points -- when the harness fires | +| 1 | WHAT | `0x0000FF00`| Sub-checks -- which comparison runs | +| 2 | MODIFIERS| `0x00FF0000`| Output / behavior knobs | + +A useful configuration sets at least one WHERE and at least one WHAT bit. + +| Bits | Region | Name | Meaning | +|--------------|----------|-----------|------------------------------------------------------------------------------| +| `0x00000001` | WHERE | ALLOC | Verify at every managed allocation (`gchelpers.cpp`) | +| `0x00000100` | WHAT | GCREFS | Compare cDAC `GetStackReferences` vs runtime GC root oracle | +| `0x00000200` | WHAT | ARGITER | Compare cDAC `CallingConvention.EnumerateArguments`-derived GCRefMap blobs vs runtime `ComputeCallRefMap` byte-for-byte (`[ARG_PASS]` / `[ARG_FAIL]` / `[ARG_SKIP]` / `[ARG_ERROR]` per MD, with a `[ARG_STATS]` summary at shutdown) | +| `0x00010000` | MODIFIER | VERBOSE | Rich per-ref diagnostics in the log | Common combinations: -- `0x001` — ALLOC (default for `RunStressTests.ps1` and the xUnit tests) -- `0x201` — ALLOC + VERBOSE (use when triaging a mismatch) +- `0x00101` -- ALLOC + GCREFS (default for `RunStressTests.ps1` and `GCRefStress_*` xunit theories) +- `0x00201` -- ALLOC + ARGITER (default for `ArgIterStress_*` xunit theories; independent run on the same Helix build so the two sub-checks don't share state) +- `0x00301` -- ALLOC + GCREFS + ARGITER (validates both sub-checks in one process) +- `0x10101` -- ALLOC + GCREFS + VERBOSE (use when triaging a GCREFS mismatch) + +### Per-sub-check summary markers + +The native harness emits one machine-readable line per enabled sub-check at +shutdown, parsed by `CdacStressResults`: + +- `[GC_STATS] verifications=N pass=N fail=N known_issue=N` -- emitted iff GCREFS ran +- `[ARG_STATS] pass=N fail=N skip=N error=N` -- emitted iff ARGITER ran + +Both lines are gated on their respective `IsCdacStress*Enabled()` helpers, so a +pure-ARGITER run does not produce `[GC_STATS]` and vice versa. The xunit +`AssertAll*Passed` helpers use the presence of the marker (`AnyGcRefsRecorded` +/ `AnyArgIterRecorded`) to distinguish "sub-check did not run" from "ran but +recorded zero verifications". ### Pass/fail semantics in the log @@ -72,7 +97,7 @@ See [known-issues.md § Log Format](known-issues.md#log-format) for the per-fram .\RunStressTests.ps1 -SkipBuild -Configuration Checked -Debuggee BasicAlloc # Run with verbose per-ref diagnostics (use when triaging a mismatch) -.\RunStressTests.ps1 -SkipBuild -Configuration Checked -CdacStress 0x201 +.\RunStressTests.ps1 -SkipBuild -Configuration Checked -CdacStress 0x10101 ``` Logs land under @@ -80,7 +105,7 @@ Logs land under ### Using `dotnet test` (xUnit harness — same path CI runs) -The xUnit harness defaults to `DOTNET_CdacStress=0x001` (ALLOC). +The xUnit harness defaults to `DOTNET_CdacStress=0x101` (ALLOC + GCREFS). ```powershell # Build and run all stress tests @@ -90,7 +115,7 @@ The xUnit harness defaults to `DOTNET_CdacStress=0x001` (ALLOC). .\.dotnet\dotnet.exe test src\native\managed\cdac\tests\StressTests --filter "FullyQualifiedName~BasicAlloc" # Override CdacStress flags for a single run (e.g. enable verbose diagnostics) -$env:DOTNET_CdacStress = "0x201" +$env:DOTNET_CdacStress = "0x10101" .\.dotnet\dotnet.exe test src\native\managed\cdac\tests\StressTests # Point at an existing Core_Root explicitly @@ -116,7 +141,7 @@ $env:CORE_ROOT = "path\to\Core_Root" 3. `Main()` must return `100` on success 4. Use `[MethodImpl(MethodImplOptions.NoInlining)]` on methods to prevent inlining 5. Use `GC.KeepAlive()` to ensure objects are live at GC stress points -6. Add the debuggee name to `BasicStressTests.Debuggees` +6. Add the debuggee name to `CdacStressTests.Debuggees` ## Debuggee Catalog @@ -126,22 +151,25 @@ $env:CORE_ROOT = "path\to\Core_Root" | **ExceptionHandling** | try/catch/finally funclets, nested exceptions, filter funclets, rethrow | | **DeepStack** | Deep recursion with live refs at each frame | | **Generics** | Generic method instantiations, interface dispatch, delegates | -| **PInvoke** | P/Invoke transitions, pinned GC handles, struct with object refs | +| **PInvoke** | P/Invoke transitions, pinned GC handles, struct with object refs (Windows-only) | | **MultiThread** | Concurrent threads with synchronized GC stress | | **Comprehensive** | All-in-one: every scenario in a single run | | **StructScenarios** | Struct returns, by-ref params | | **DynamicMethods** | DynamicMethod / IL emit | +| **CallSignatures** | Wide signature surface for the ARGITER sub-check (primitives, byref/ptr, structs, generics) | +| **CrossModule** | Calls across multiple assemblies exercising cross-module type references | +| **VarArgs** | `__arglist` / VASigCookie validation for ARGITER (Windows x86/x64/ARM64 only; excluded from GCREFS until GetStackReferences walks the cookie signature) | ## Architecture ``` -CdacStressTestBase.RunGCStressAsync(debuggeeName) +CdacStressTestBase.RunGCRefStressAsync(debuggeeName) │ ├── Locate core_root/corerun (CORE_ROOT env or default path) ├── Locate debuggee DLL (artifacts/bin/StressTests//...) ├── Start Process: corerun │ Environment: - │ DOTNET_CdacStress=0x001 + │ DOTNET_CdacStress=0x101 │ DOTNET_CdacStressLogFile= │ DOTNET_ContinueOnAssert=1 ├── Wait for exit (timeout: 300s) diff --git a/src/native/managed/cdac/tests/StressTests/RunStressTests.ps1 b/src/native/managed/cdac/tests/StressTests/RunStressTests.ps1 index dcaa176f20d65f..dbc80e7c1a46b5 100644 --- a/src/native/managed/cdac/tests/StressTests/RunStressTests.ps1 +++ b/src/native/managed/cdac/tests/StressTests/RunStressTests.ps1 @@ -11,14 +11,18 @@ Supports Windows, Linux, and macOS. - The DOTNET_CdacStress environment variable controls WHEN verification fires: - TRIGGERS: - 0x001 = ALLOC — verify at every managed allocation - MODIFIER: - 0x200 = VERBOSE — rich per-ref diagnostics in the log - - The runtime's own GC root enumeration is the single oracle. Any trigger - causes cDAC's GetStackReferences output to be compared against it. + The DOTNET_CdacStress environment variable is split into byte regions: + WHERE (byte 0): when verification fires + 0x00000001 = ALLOC — verify at every managed allocation + WHAT (byte 1): which sub-check runs at each fired trigger + 0x00000100 = GCREFS — compare cDAC GetStackReferences vs runtime GC roots + 0x00000200 = ARGITER — compare cDAC EnumerateArguments vs runtime ComputeCallRefMap + MODIFIER (byte 2): + 0x00010000 = VERBOSE — rich per-ref diagnostics in the log + + The runtime's own GC root enumeration is the single oracle for GCREFS. + A useful configuration combines at least one WHERE bit with at least one + WHAT bit (e.g. 0x101 = ALLOC + GCREFS). .PARAMETER Configuration Runtime configuration: Checked (default) or Debug. @@ -32,9 +36,10 @@ specific failure. .PARAMETER CdacStress - Hex value for DOTNET_CdacStress flags. Default: 0x001 (ALLOC). + Hex value for DOTNET_CdacStress flags. Default: 0x101 (ALLOC + GCREFS). Common values: - 0x001 = ALLOC (allocation points only, every hit verified) + 0x101 = ALLOC + GCREFS (allocation points, GC-refs comparison) + 0x301 = ALLOC + GCREFS + ARGITER (also runs the ArgIterator sub-check) .PARAMETER Debuggee Which debuggee(s) to run. Default: All. @@ -49,7 +54,7 @@ .EXAMPLE ./RunStressTests.ps1 -SkipBuild ./RunStressTests.ps1 -Debuggee BasicAlloc -SkipBuild - ./RunStressTests.ps1 -CdacStress 0x201 -SkipBuild # ALLOC + VERBOSE + ./RunStressTests.ps1 -CdacStress 0x10101 -SkipBuild # ALLOC + GCREFS + VERBOSE #> param( [ValidateSet("Checked", "Debug")] @@ -58,7 +63,7 @@ param( [ValidateSet("Release", "Checked", "Debug")] [string]$CdacConfiguration = "Release", - [string]$CdacStress = "0x001", + [string]$CdacStress = "0x101", [string[]]$Debuggee = @(), diff --git a/src/native/managed/cdac/tests/StressTests/cdac-stress-helix.proj b/src/native/managed/cdac/tests/StressTests/cdac-stress-helix.proj index bf00e68a2d5040..5ea1502e9fc778 100644 --- a/src/native/managed/cdac/tests/StressTests/cdac-stress-helix.proj +++ b/src/native/managed/cdac/tests/StressTests/cdac-stress-helix.proj @@ -43,6 +43,10 @@ to guarantee it's in the same shell as the test runner. --> + + diff --git a/src/native/managed/cdac/tests/StressTests/known-issues.md b/src/native/managed/cdac/tests/StressTests/known-issues.md index 838e4491d2dc48..b3741fae9eb1a2 100644 --- a/src/native/managed/cdac/tests/StressTests/known-issues.md +++ b/src/native/managed/cdac/tests/StressTests/known-issues.md @@ -6,8 +6,8 @@ enumeration and the runtime's own GC root scanning, exposed by the ## Verification verdicts -When running `RunStressTests.ps1` (Checked, `DOTNET_CdacStress=0x001` = -`ALLOC`), each verification is bucketed into one of: +When running `RunStressTests.ps1` (Checked, `DOTNET_CdacStress=0x101` = +`ALLOC + GCREFS`), each verification is bucketed into one of: | Verdict | Meaning | |---------|---------| @@ -92,6 +92,6 @@ Each verification emits a single header line followed by, on `[FAIL]` or ``` Frames whose counts match are omitted from the per-frame block in -concise mode; verbose mode (`DOTNET_CdacStress=0x201`) also emits the +concise mode; verbose mode (`DOTNET_CdacStress=0x10101`) also emits the matched refs.