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/GCInfo.md b/docs/design/datacontracts/GCInfo.md index 70964f409a93d1..dcbb949306fd45 100644 --- a/docs/design/datacontracts/GCInfo.md +++ b/docs/design/datacontracts/GCInfo.md @@ -40,12 +40,9 @@ uint GetSizeOfStackParameterArea(IGCInfoHandle handle); uint GetCalleePoppedArgumentsSize(IGCInfoHandle handle); // Returns the list of interruptible code offset ranges from the GCInfo -// (not implemented for x86 — x86 encodes per-offset transitions rather than explicit ranges). IReadOnlyList GetInterruptibleRanges(IGCInfoHandle handle); // Returns all live GC slots at the given instruction offset -// (not implemented for x86 — see X86GCInfo for the underlying transition data; the cDAC -// adapter is future work). IReadOnlyList EnumerateLiveSlots(IGCInfoHandle handle, uint instructionOffset, GcSlotEnumerationOptions options); ``` @@ -603,3 +600,51 @@ IReadOnlyList EnumerateLiveSlots(IGCInfoHandle handle, // Collect each live slot into a list and return it. } ``` + + +## x86 specifics + +x86 uses the legacy bit-packed `InfoHdr` byte-stream encoding (`src/coreclr/vm/gc_unwind_x86.inl`, `src/coreclr/inc/gcdecoder.cpp`) rather than the modern `GcInfoDecoder` shared by all other architectures. The cDAC decoder lives at `src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/` and is shared with the x86 stack walker. + +A few API behaviors are worth calling out: + +- `GetSizeOfStackParameterArea` always returns 0 -- x86 has no separate outgoing-argument scratch area; pushed args are reported directly through the per-offset transition stream. +- `GetInterruptibleRanges` reports one range covering the post-prolog body for fully-interruptible methods, or a single-byte range per call site for partially-interruptible methods. Consumed by `StackWalk_1.WalkStackReferences` for the catch-handler PC override (x86 uses the funclet EH model, see PR [#115957](https://github.com/dotnet/runtime/pull/115957)). +- `EnumerateLiveSlots` mirrors `EnumGcRefsX86`; see below. + +### `EnumerateLiveSlots` behavior + +Mirrors `EnumGcRefsX86` (gc_unwind_x86.inl), `scanArgRegTableI` (fully-interruptible) and `scanArgRegTable` (partially-interruptible). + +Early-return gates (no slots reported): +- `IsParentOfFuncletStackFrame` — the funclet sharing this parent's locals will report them itself. +- Code offset is in the prolog or any epilog (only reachable via `IsExecutionAborted`; GC info does not describe these regions). +- `IsExecutionAborted` and the method is not fully interruptible. + +Filter-funclet `SuppressUntrackedSlots` is honored within the untracked-locals step (the parent frame already reported them) but does not gate the rest of the walk. + +Sources of live slots: +- **Untracked frame locals** — always live for the entire method body. Encoded as signed delta-from-previous offsets in the untracked table. On EBP frames the encoded value resolves directly to `EBP + stkOffs` (`FRAMEREG_REL`). On ESP frames the encoded value is `argBase`-relative where `argBase = ESP + pushedSize` (mirrors `EnumGcRefsX86`); the decoder rebases to a true `SP_REL` offset by adding the pushed-arg size computed at the queried instruction offset. +- **VarPtr-tracked locals** — live when the lifetime-check offset is within `[BeginOffset, EndOffset)`. EBP-frame offsets are stored as their negated form (mirrors `if (info.ebpFrame) stkOffs = -stkOffs`); ESP-frame offsets receive the same `pushedSize` bias as untracked locals. Non-active frames evaluate the lifetime at `instructionOffset - 1` because a variable can be dead at the return address (e.g. when the call is the last instruction of a try and the return is the catch-block jump target). +- **Live registers** — accumulated from the LIVE/DEAD transition stream up to the queried offset. Callee-saved registers (EBX/EBP/ESI/EDI) are reported when execution will continue; callee-trashed scratch (EAX/ECX/EDX) is reported only on the active leaf frame. On non-leaf frames register liveness is evaluated at the instruction *before* the call (`regOffset = instructionOffset - 1`) since liveness can change across calls. +- **Pushed pointer args** — for fully-interruptible code, accumulated from the PUSH/POP transition stream. Non-pointer pushes (`IsPtr = false`) still bump the stack depth (so subsequent pushed-ptr indices stay aligned) but do not contribute a slot. At emit time, once `finalDepth` is known, each tracked push is reported as a positive SP-relative offset: `addr = ESP_call + (finalDepth - 1 - pushIndex) * sizeof(DWORD)` (mirrors `pPendingArgFirst - i * sizeof(DWORD)` in `EnumGcRefsX86`). The translation must be deferred because subsequent pushes/pops change `finalDepth`. For partially-interruptible call sites, slots come from the matching `GcTransitionCall` instead: explicit per-pointer offsets in the huge (0xFB) encoding, or a uint32 `ArgMask` / `IArgs` bitmap walked low-to-high with `addr = ESP + i * sizeof(DWORD)` for the tiny / small / medium / large encodings. +- **`IsParentOfFuncletStackFrame`** suppresses all reporting from the parent: the funclet itself reports the shared locals via `EnumerateLiveSlots` on the funclet frame. + +When `ReportFPBasedSlotsOnly` is set, the result list is filtered to drop register slots and any stack slot whose base is not the frame register (matching `GCInfoDecoder.ReportSlot`). + +### Encoding correctness notes (x86) + +A few subtleties of the legacy byte-stream encoding caught during stress validation, mirrored from native and worth remembering when modifying the decoder: + +- **Huge call-site (`0xFB`) code-delta is cumulative.** The uint32 code delta is `curOffs += delta`, not `curOffs = delta`. Assigning loses all preceding offset accumulation and corrupts every subsequent call site. +- **Partial-EBP this-pointer tag (`val & 0x80 == 0 && val & 0x0F == 0`).** This encodes the callee-saved register holding `this` at the next call site; native (`gc_unwind_x86.inl` ~line 970) sets `thisPtrReg` only and does *not* record a call entry. Emitting a `GcTransitionCall` at the current offset would overwrite the real call site's `CallRegisters` when both fall at the same `curOffs`. +- **Partial-interrupt EBP-less call sites emit a negative stack-depth delta** (callee-popped args reverse the prior pushes); the transition stream is generated accordingly. +- **`0xC0..0xCF` partial-interruptible byte range** in the huge encoding is a call entry that names only callee-saved registers (no pointer args), distinct from `0xFD..0xFF`. + +### Deferred edges + +These do not affect the GC root scan / `WalkStackReferences` path validated by the cDAC stress suite, but are noted for future work: + +- `info.thisPtrResult` reporting for synchronized methods on the `!willContinueExecution` path (the regular live-register report covers `willContinueExecution`, which is what stress exercises). +- VarPtr `0x2` legacy-encoder "this" bit (the modern x86 JIT uses `0x2` only for pinned, which we already pass through; the legacy "this" interpretation never appears in code from the current JIT). +- `IPtrMask` (`0xF0`) interior-pointer bitmaps for pushed args — accepted by the decoder as informational, but the bitmap is not yet applied to pushed-arg slots. Only relevant on the partial-interruptible ESP-frame path; in practice the current x86 JIT rarely emits these. diff --git a/docs/design/datacontracts/RuntimeTypeSystem.md b/docs/design/datacontracts/RuntimeTypeSystem.md index 176923e0ce6d0a..c9d32f319f8fc7 100644 --- a/docs/design/datacontracts/RuntimeTypeSystem.md +++ b/docs/design/datacontracts/RuntimeTypeSystem.md @@ -73,6 +73,8 @@ partial interface IRuntimeTypeSystem : IContract public virtual TargetPointer GetWellKnownMethodTable(WellKnownMethodTable kind); // 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] @@ -668,6 +678,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) @@ -1872,6 +1884,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 @@ -2232,6 +2255,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..89f854c7d07349 100644 --- a/src/coreclr/inc/dacprivate.h +++ b/src/coreclr/inc/dacprivate.h @@ -65,12 +65,32 @@ 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 in place. +// S_OK blob fit; cbFilled bytes written to *BlobBuffer. +// HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER) cbFilled = 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..970397400f2c99 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(FpStructInRegistersInfo); 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++) { @@ -350,8 +183,8 @@ internal class ArgIteratorData { public ArgIteratorData(bool hasThis, bool isVarArg, - TypeHandle[] parameterTypes, - TypeHandle returnType) + ITypeHandle[] parameterTypes, + ITypeHandle returnType) { _hasThis = hasThis; _isVarArg = isVarArg; @@ -361,8 +194,8 @@ public ArgIteratorData(bool hasThis, private bool _hasThis; private bool _isVarArg; - private TypeHandle[] _parameterTypes; - private TypeHandle _returnType; + private ITypeHandle[] _parameterTypes; + private ITypeHandle _returnType; public override bool Equals(object obj) { @@ -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 ITypeHandle thArgType) { thArgType = _parameterTypes[argNum]; CorElementType returnValue = thArgType.GetCorElementType(); return returnValue; } - public TypeHandle GetByRefArgumentType(int argNum) + public ITypeHandle GetByRefArgumentType(int argNum) { return (argNum < _parameterTypes.Length && _parameterTypes[argNum].GetCorElementType() == CorElementType.ELEMENT_TYPE_BYREF) ? _parameterTypes[argNum] : - default(TypeHandle); + null; } - public CorElementType GetReturnType(out TypeHandle thRetType) + public CorElementType GetReturnType(out ITypeHandle thRetType) { thRetType = _returnType; return thRetType.GetCorElementType(); @@ -430,15 +273,13 @@ 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 { - private readonly TypeSystemContext _context; - private readonly TransitionBlock _transitionBlock; private bool _hasThis; @@ -452,6 +293,9 @@ internal struct ArgIterator private CallingConventions _interpreterCallingConvention; private bool _hasArgLocDescForStructInRegs; private ArgLocDesc _argLocDescForStructInRegs; + private ITypeHandle _objectTypeHandle; + private ITypeHandle _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 ITypeHandle 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 ITypeHandle 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 = null; _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 = false, + ITypeHandle objectTypeHandle = null, + ITypeHandle intPtrTypeHandle = null) { this = default(ArgIterator); - _context = context; + _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) : null); _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(ArgLocDesc); _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(ArgLocDesc); _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(ArgLocDesc); _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(ArgLocDesc); _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 ITypeHandle pTypeHandle) { // LIMITED_METHOD_CONTRACT; pTypeHandle = _argTypeHandle; return _argType; } - public CorElementType GetByRefArgType(out TypeHandle pByRefArgTypeHandle) + public CorElementType GetByRefArgType(out ITypeHandle 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; + ITypeHandle 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(ArgLocDesc); int byteArgSize = GetArgSize(); if (IsArgPassedByRef()) @@ -1735,7 +1583,7 @@ private void ForceSigWalk() { // LIMITED_METHOD_CONTRACT; - ArgLocDesc pLoc = new ArgLocDesc(); + ArgLocDesc pLoc = default(ArgLocDesc); pLoc.m_fRequires64BitAlignment = _armRequires64BitAlignment; @@ -1778,7 +1626,7 @@ private void ForceSigWalk() { // LIMITED_METHOD_CONTRACT; - ArgLocDesc pLoc = new ArgLocDesc(); + ArgLocDesc pLoc = default(ArgLocDesc); if (_transitionBlock.IsFloatArgumentRegisterOffset(argOffset)) { @@ -1829,7 +1677,7 @@ private void ForceSigWalk() // LIMITED_METHOD_CONTRACT; - ArgLocDesc pLoc = new ArgLocDesc(); + ArgLocDesc pLoc = default(ArgLocDesc); 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(ArgLocDesc); 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 ITypeHandle _argTypeHandle; + private ITypeHandle _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; + ITypeHandle 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..335b3fdedb2d1b --- /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, ITypeHandle thValueType) + { + 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..5144f8cb999c74 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,8 @@ using System.Linq; using System.Xml.Linq; using Internal.TypeSystem; +using Internal.CallingConvention; +using ArgIterator = Internal.CallingConvention.ArgIterator; // 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 +65,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) { - 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; @@ -79,7 +87,7 @@ internal static (ArgIterator, TransitionBlock) BuildArgIterator(MethodSignature bool isVarArg = false; TypeHandle returnType = new TypeHandle(signature.ReturnType); - TypeHandle[] parameterTypes = new TypeHandle[signature.Length]; + ITypeHandle[] parameterTypes = new ITypeHandle[signature.Length]; for (int parameterIndex = 0; parameterIndex < parameterTypes.Length; parameterIndex++) { parameterTypes[parameterIndex] = new TypeHandle(signature[parameterIndex]); @@ -105,7 +113,7 @@ internal static (ArgIterator, TransitionBlock) BuildArgIterator(MethodSignature ArgIteratorData argIteratorData = new ArgIteratorData(hasThis, isVarArg, parameterTypes, returnType); ArgIterator argit = new ArgIterator( - context, + transitionBlock, argIteratorData, callingConventions, hasParamType, @@ -113,7 +121,10 @@ 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); } @@ -288,7 +299,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..a38b45da4df61e 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,12 +5,14 @@ using ILCompiler.ObjectWriter; using ILCompiler.ObjectWriter.WasmInstructions; using Internal.JitInterface; +using Internal.CallingConvention; using Internal.Text; using Internal.TypeSystem; using Internal.ReadyToRunConstants; using System; using System.Collections.Generic; using System.Diagnostics; +using ArgIterator = Internal.CallingConvention.ArgIterator; namespace ILCompiler.DependencyAnalysis.ReadyToRun { 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..54dfa50b4ff860 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,11 +5,13 @@ using ILCompiler.ObjectWriter; using ILCompiler.ObjectWriter.WasmInstructions; using Internal.JitInterface; +using Internal.CallingConvention; using Internal.Text; using Internal.TypeSystem; using System; using System.Collections.Generic; using System.Diagnostics; +using ArgIterator = Internal.CallingConvention.ArgIterator; using ILCompiler.DependencyAnalysisFramework; 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..c0543322a45b3c 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,12 +5,14 @@ using ILCompiler.ObjectWriter; using ILCompiler.ObjectWriter.WasmInstructions; using Internal.JitInterface; +using Internal.CallingConvention; using Internal.Text; using Internal.TypeSystem; using Internal.ReadyToRunConstants; using System; using System.Collections.Generic; using System.Diagnostics; +using ArgIterator = Internal.CallingConvention.ArgIterator; using ILCompiler.DependencyAnalysisFramework; 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..8497c653aae1e9 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,8 @@ using ILCompiler; using ILCompiler.DependencyAnalysis.Wasm; using ILCompiler.DependencyAnalysis.ReadyToRun; +using Internal.CallingConvention; +using ArgIterator = Internal.CallingConvention.ArgIterator; using Internal.TypeSystem; @@ -19,9 +21,9 @@ 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; + ITypeHandle typeHandle; argit.GetArgType(out typeHandle); - if (WasmLowering.LowerToAbiType(typeHandle.GetRuntimeTypeHandle()) == null) + if (WasmLowering.LowerToAbiType(((TypeHandle)typeHandle).GetRuntimeTypeHandle()) == null) { return true; } diff --git a/src/coreclr/vm/cdacstress.cpp b/src/coreclr/vm/cdacstress.cpp index af5f8362af37bd..24d5896c612f43 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,457 @@ 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" }; + const int numRegs = 6; +# else + static const char* regNames[] = { "RCX", "RDX", "R8", "R9" }; + const int numRegs = 4; +# endif +#elif defined(TARGET_ARM64) + static const char* regNames[] = { "X0", "X1", "X2", "X3", "X4", "X5", "X6", "X7" }; + const int numRegs = 8; +#elif defined(TARGET_ARM) + static const char* regNames[] = { "R0", "R1", "R2", "R3" }; + const int numRegs = 4; +#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" }; + const int numRegs = 2; +#else + static const char* const* regNames = nullptr; + const int numRegs = 0; +#endif + + if (regNames != nullptr && pos < numRegs) + { + snprintf(buf, bufLen, "%-6s", regNames[pos]); + } + else + { + 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[16]; + 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 +1792,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..511f835f2cef72 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -1203,6 +1203,11 @@ CDAC_TYPE_SIZE(sizeof(ExternalMethodFrame)) CDAC_TYPE_FIELD(ExternalMethodFrame, T_POINTER, Indirection, cdac_data::Indirection) CDAC_TYPE_END(ExternalMethodFrame) +CDAC_TYPE_BEGIN(PInvokeCalliFrame) +CDAC_TYPE_SIZE(sizeof(PInvokeCalliFrame)) +CDAC_TYPE_FIELD(PInvokeCalliFrame, T_POINTER, VASigCookiePtr, cdac_data::VASigCookiePtr) +CDAC_TYPE_END(PInvokeCalliFrame) + CDAC_TYPE_BEGIN(DynamicHelperFrame) CDAC_TYPE_SIZE(sizeof(DynamicHelperFrame)) CDAC_TYPE_FIELD(DynamicHelperFrame, T_INT32, DynamicHelperFrameFlags, cdac_data::DynamicHelperFrameFlags) @@ -1723,6 +1728,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/coreclr/vm/frames.h b/src/coreclr/vm/frames.h index feb81a0d9836cc..94496994ec88bb 100644 --- a/src/coreclr/vm/frames.h +++ b/src/coreclr/vm/frames.h @@ -1358,6 +1358,14 @@ class PInvokeCalliFrame : public FramedMethodFrame trace->InitForUnmanaged(GetPInvokeCalliTarget()); return TRUE; } + + friend struct ::cdac_data; +}; + +template <> +struct cdac_data +{ + static constexpr size_t VASigCookiePtr = offsetof(PInvokeCalliFrame, m_pVASigCookie); }; // Some context-related forwards. 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 14e6386a0ad644..e5c6750e1a5406 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 @@ -150,6 +150,8 @@ public interface IRuntimeTypeSystem : IContract TargetPointer GetWellKnownMethodTable(WellKnownMethodTable kind) => 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 @@ -286,6 +288,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(); @@ -295,6 +298,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..bc73954444c04f --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CallingConvention_1.cs @@ -0,0 +1,1052 @@ +// 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 ArgIterator = Internal.CallingConvention.ArgIterator; +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 lazy NIEs from + // EnumerateArguments, maps to a clean decline (false). + blob = []; + return false; + } + } + + internal uint GetCbStackPop(MethodDescHandle methodDesc) + { + IRuntimeInfo runtimeInfo = _target.Contracts.RuntimeInfo; + if (runtimeInfo.GetTargetArchitecture() != RuntimeInfoArchitecture.X86) + return 0; + + try + { + return GetCbStackPopCore(methodDesc, runtimeInfo); + } + catch + { + // Match the encoder's general behavior: any failure to compute + // produces a conservative zero, and the cdacstress framework + // reports the resulting mismatch as a [ARG_FAIL] rather than + // crashing the stress run. + return 0; + } + } + + private uint GetCbStackPopCore(MethodDescHandle methodDesc, IRuntimeInfo runtimeInfo) + { + IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem; + MethodSignature methodSig = DecodeMethodSignature(rts, methodDesc); + + // VarArgs methods don't pop arguments on x86 (caller cleans up). + // ArgIterator.CbStackPop already encodes this, but we never call it + // for VarArgs because EnumerateArguments throws first; mirror its + // 0 return here so the encoder writes the correct prefix. + if (methodSig.Header.CallingConvention is SignatureCallingConvention.VarArgs) + return 0; + + 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 + { + } + + ParamTypeInfo[] paramInfo = DecodeParamTypeInfo(rts, methodDesc, methodSig.ParameterTypes.Length); + ITypeHandle[] parameterTypes = new ITypeHandle[methodSig.ParameterTypes.Length]; + for (int i = 0; i < parameterTypes.Length; i++) + parameterTypes[i] = new CdacTypeHandle(methodSig.ParameterTypes[i], _target, paramInfo[i].OutermostKind); + ITypeHandle returnType = new CdacTypeHandle(methodSig.ReturnType, _target); + + TransitionBlock transitionBlock = BuildTransitionBlock(runtimeInfo); + CallingConventions callingConventions = hasThis + ? CallingConventions.ManagedInstance + : CallingConventions.ManagedStatic; + ArgIteratorData argIteratorData = new ArgIteratorData( + hasThis, isVarArg: false, 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); + + return argit.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; } + } + + internal IEnumerable EnumerateArguments(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 + { + } + + ITypeHandle[] parameterTypes = new ITypeHandle[methodSig.ParameterTypes.Length]; + for (int i = 0; i < parameterTypes.Length; i++) + { + parameterTypes[i] = new CdacTypeHandle(methodSig.ParameterTypes[i], _target, paramInfo[i].OutermostKind); + } + + ITypeHandle 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); + + if (hasThis) + { + TargetPointer methodTablePtr = rts.GetMethodTable(methodDesc); + TypeHandle owningType = rts.GetTypeHandle(methodTablePtr); + bool isValueTypeThis = rts.IsValueType(owningType) && !rts.IsUnboxingStub(methodDesc); + + yield return new ArgumentLocation + { + Offset = transitionBlock.ThisOffset, + ElementType = isValueTypeThis ? CdacCorElementType.ValueType : CdacCorElementType.Class, + TypeHandle = owningType, + IsThis = true, + IsValueTypeThis = isValueTypeThis, + }; + } + + if (argit.HasParamType) + { + yield return new ArgumentLocation + { + Offset = argit.GetParamTypeArgOffset(), + ElementType = CdacCorElementType.I, + IsParamType = true, + }; + } + + if (argit.HasAsyncContinuation) + { + yield return 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. + if (isVarArg) + { + yield return new ArgumentLocation + { + Offset = argit.GetVASigCookieOffset(), + ElementType = CdacCorElementType.I, + IsVASigCookie = true, + }; + yield break; + } + + 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 */ } + } + } + + yield return new ArgumentLocation + { + Offset = argOffset, + ElementType = elemType, + TypeHandle = methodSig.ParameterTypes[argIndex], + IsPassedByRef = passedByRef, + IsByRefLikeStruct = isByRefLikeStruct, + OpenGenericType = paramInfo[argIndex].OpenGenericType, + }; + } + argIndex++; + } + } + + 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); + } + + // 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 == End so + // callers know to fall back to GetSignatureCorElementType on Underlying. + // The constructed-type overrides (ByRef/Ptr/SzArray/Array) override + // 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(); + IEnumerable args = EnumerateArguments(methodDesc); + + GenericContextLoc ctxLoc = GenericContextLoc.None; + + foreach (ArgumentLocation arg in args) + { + 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(GetCbStackPop(methodDesc) / (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(GetCbStackPop(methodDesc) / (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..9b4b4a5b7e0b4b --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CdacTypeHandle.cs @@ -0,0 +1,219 @@ +// 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; + + private readonly RuntimeInfoArchitecture _arch; + private readonly RuntimeInfoOperatingSystem _os; + + // 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. + // CdacCorElementType.End (== default) 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; + _arch = _target.Contracts.RuntimeInfo.GetTargetArchitecture(); + _os = _target.Contracts.RuntimeInfo.GetTargetOperatingSystem(); + _kindOverride = kindOverride; + } + + private IRuntimeTypeSystem Rts => _target.Contracts.RuntimeTypeSystem; + + public int PointerSize => _target.PointerSize; + + 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/GCInfo/X86/GCArgTable.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/GCArgTable.cs index d0284332d230d6..01f27de174c7a9 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/GCArgTable.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/GCArgTable.cs @@ -197,6 +197,7 @@ private void GetTransitionsFullyInterruptible(ref TargetPointer offset) /// private void GetTransitionsEbpFrame(ref TargetPointer offset) { + uint curOffs = 0; while (true) { uint argMask = 0, byrefArgMask = 0; @@ -207,7 +208,6 @@ private void GetTransitionsEbpFrame(ref TargetPointer offset) uint argTabSize; uint val, nxt; - uint curOffs = 0; // Get the next byte and check for a 'special' entry uint encType = _target.Read(offset++); @@ -231,19 +231,14 @@ private void GetTransitionsEbpFrame(ref TargetPointer offset) } else { - RegMask reg; - if ((val & 0x10) != 0) - reg = RegMask.EDI; - else if ((val & 0x20) != 0) - reg = RegMask.ESI; - else if ((val & 0x40) != 0) - reg = RegMask.EBX; - else - throw new BadImageFormatException("Invalid register"); - transition = new GcTransitionCall((int)curOffs); - transition.CallRegisters.Add(new GcTransitionCall.CallRegister(reg, false)); - AddNewTransition(transition); - + // "This pointer liveness encoding" (val & 0x80 == 0 && val & 0x0F == 0): + // metadata for which callee-saved register holds the 'this' pointer + // at the next call site. Native (gc_unwind_x86.inl ~line 970) does NOT + // record a call entry here -- it only sets thisPtrReg. Adding a spurious + // GcTransitionCall at the current curOffs would overwrite the real + // call site's CallRegisters during EnumerateLiveSlots (since the + // partial-EBP decoder may emit the this-ptr tag at the same curOffs + // as a real call site), so we just consume the byte and continue. continue; } } @@ -296,7 +291,12 @@ private void GetTransitionsEbpFrame(ref TargetPointer offset) val = _target.Read(offset++); regMask = val & 0x7; byrefRegMask = val >> 4; - curOffs = _target.Read(offset); + // Code delta is 32-bit and is added to curOffs (mirrors `scanOffs +=` in + // native gc_unwind_x86.inl scanArgRegTable case 0xFB). The pre-PR cDAC port + // assigned `curOffs = ...` here, which silently truncated method-relative + // offsets for the first 0xFB call site and corrupted all subsequent calls + // in long methods (e.g. EventSource cctors). + curOffs += _target.Read(offset); offset += 4; argCnt = _target.Read(offset); offset += 4; @@ -345,7 +345,7 @@ argMask ... bitmask of pushed pointer arguments /// /// based on GCDump::DumpGCTable /// - private void SaveCallTransition(ref TargetPointer offset, uint val, uint curOffs, uint callRegMask, bool callPndTab, uint callPndTabCnt, uint callPndMask, uint lastSkip, ref uint imask) + private void SaveCallTransition(ref TargetPointer offset, uint curOffs, uint callRegMask, bool callPndTab, uint callPndTabCnt, uint callPndMask, ref uint imask) { uint iregMask, iargMask; iregMask = imask & 0xF; @@ -359,11 +359,6 @@ private void SaveCallTransition(ref TargetPointer offset, uint val, uint curOffs for (int i = 0; i < callPndTabCnt; i++) { uint pndOffs = _target.GCDecodeUnsigned(ref offset); - - uint stkOffs = val & ~byref_OFFSET_FLAG; - uint lowBit = val & byref_OFFSET_FLAG; - Console.WriteLine($"stkOffs: {stkOffs}, lowBit: {lowBit}"); - transition.PtrArgs.Add(new GcTransitionCall.PtrArg(pndOffs, 0)); } } @@ -375,15 +370,14 @@ private void SaveCallTransition(ref TargetPointer offset, uint val, uint curOffs transition.IArgs = iargMask; } - Console.WriteLine($"lastSkip: {lastSkip}"); imask /* = lastSkip */ = 0; } private void GetTransitionsNoEbp(ref TargetPointer offset) { uint curOffs = 0; - uint lastSkip = 0; uint imask = 0; + uint lastSkip; for (; ; ) { @@ -418,7 +412,6 @@ private void GetTransitionsNoEbp(ref TargetPointer offset) // skip = _target.GCDecodeUnsigned(ref offset); curOffs += skip; - lastSkip = skip; } else { @@ -431,18 +424,16 @@ private void GetTransitionsNoEbp(ref TargetPointer offset) { AddNewTransition(new GcTransitionRegister((int)curOffs, RegMask.ESP, Action.POP, false, false, (int)popSize)); } - else - lastSkip = skip; } } } else { - uint callArgCnt = 0; + uint callArgCnt; uint callRegMask; bool callPndTab = false; uint callPndMask = 0; - uint callPndTabCnt = 0, callPndTabSize = 0; + uint callPndTabCnt = 0; switch ((val & 0x70) >> 4) { @@ -452,8 +443,8 @@ private void GetTransitionsNoEbp(ref TargetPointer offset) // CallPattern.DecodeCallPattern((val & 0x7f), out callArgCnt, out callRegMask, out callPndMask, out lastSkip); curOffs += lastSkip; - SaveCallTransition(ref offset, val, curOffs, callRegMask, callPndTab, callPndTabCnt, callPndMask, lastSkip, ref imask); - AddNewTransition(new StackDepthTransition((int)curOffs, (int)callArgCnt)); + SaveCallTransition(ref offset, curOffs, callRegMask, callPndTab, callPndTabCnt, callPndMask, ref imask); + AddNewTransition(new StackDepthTransition((int)curOffs, -(int)callArgCnt)); break; case 5: @@ -467,8 +458,8 @@ private void GetTransitionsNoEbp(ref TargetPointer offset) callArgCnt = (val >> 3) & 0x7; lastSkip = CallPattern.CallCommonDelta[(int)(val >> 6)]; curOffs += lastSkip; - SaveCallTransition(ref offset, val, curOffs, callRegMask, callPndTab, callPndTabCnt, callPndMask, lastSkip, ref imask); - AddNewTransition(new StackDepthTransition((int)curOffs, (int)callArgCnt)); + SaveCallTransition(ref offset, curOffs, callRegMask, callPndTab, callPndTabCnt, callPndMask, ref imask); + AddNewTransition(new StackDepthTransition((int)curOffs, -(int)callArgCnt)); break; case 6: // @@ -478,8 +469,8 @@ private void GetTransitionsNoEbp(ref TargetPointer offset) callRegMask = val & 0xf; // EBP,EBX,ESI,EDI callArgCnt = _target.GCDecodeUnsigned(ref offset); callPndMask = _target.GCDecodeUnsigned(ref offset); - SaveCallTransition(ref offset, val, curOffs, callRegMask, callPndTab, callPndTabCnt, callPndMask, lastSkip, ref imask); - AddNewTransition(new StackDepthTransition((int)curOffs, (int)callArgCnt)); + SaveCallTransition(ref offset, curOffs, callRegMask, callPndTab, callPndTabCnt, callPndMask, ref imask); + AddNewTransition(new StackDepthTransition((int)curOffs, -(int)callArgCnt)); break; case 7: switch (val & 0x0C) @@ -505,11 +496,11 @@ private void GetTransitionsNoEbp(ref TargetPointer offset) offset += 4; callPndTabCnt = _target.Read(offset); offset += 4; - callPndTabSize = _target.Read(offset); + // Skip callPndTabSize - present in encoding but unused by the decoder. offset += 4; callPndTab = true; - SaveCallTransition(ref offset, val, curOffs, callRegMask, callPndTab, callPndTabCnt, callPndMask, lastSkip, ref imask); - AddNewTransition(new StackDepthTransition((int)curOffs, (int)callArgCnt)); + SaveCallTransition(ref offset, curOffs, callRegMask, callPndTab, callPndTabCnt, callPndMask, ref imask); + AddNewTransition(new StackDepthTransition((int)curOffs, -(int)callArgCnt)); break; case 0x0C: return; @@ -518,8 +509,6 @@ private void GetTransitionsNoEbp(ref TargetPointer offset) } break; } - Console.WriteLine($"CallArgCount: {callArgCnt}"); - Console.WriteLine($"CallPndTabCnt: {callPndTabSize}"); } } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/GCInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/GCInfo.cs index 9a8c44f7234f38..568469664ce059 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/GCInfo.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/GCInfo.cs @@ -69,6 +69,26 @@ public record X86GCInfo : IGCInfoDecoder public uint PushedArgSize => _pushedArgSize.Value; private readonly Lazy _pushedArgSize; + /// + /// The untracked frame variable table, always-live GC frame slots. + /// Decoded lazily on first access. + /// + internal ImmutableArray UntrackedSlots => _untrackedSlots.Value; + private readonly Lazy> _untrackedSlots; + + /// + /// The frame variable lifetime (VarPtr) table, per-offset-range tracked GC variables. + /// Decoded lazily on first access. + /// + internal ImmutableArray VarPtrLifetimes => _varPtrLifetimes.Value; + private readonly Lazy> _varPtrLifetimes; + + // Transition offsets sorted ascending. Cached so EnumerateLiveSlots / + // CalculatePushedArgSizeAt / GetInterruptibleRanges don't re-sort on every call + // (EnumerateLiveSlots fires once per managed frame during stack walking). + private ImmutableArray SortedTransitionOffsets => _sortedTransitionOffsets.Value; + private readonly Lazy> _sortedTransitionOffsets; + public X86GCInfo(Target target, TargetPointer gcInfoAddress, uint gcInfoVersion, uint relativeOffset = 0) { if (gcInfoVersion < MINIMUM_SUPPORTED_GCINFO_VERSION) @@ -151,6 +171,14 @@ public X86GCInfo(Target target, TargetPointer gcInfoAddress, uint gcInfoVersion, // Lazily calculate the pushed argument size. This forces the transitions to be decoded. _pushedArgSize = new(CalculatePushedArgSize); + + // Lazily decode the untracked-locals and VarPtr tables + _untrackedSlots = new(DecodeUntrackedSlots); + _varPtrLifetimes = new(DecodeVarPtrLifetimes); + + // Sorted offsets walked by EnumerateLiveSlots / CalculatePushedArgSizeAt / + // GetInterruptibleRanges. Cached once instead of re-sorting per call. + _sortedTransitionOffsets = new(() => [.. Transitions.Keys.OrderBy(o => o)]); } private ImmutableDictionary> DecodeTransitions() @@ -196,12 +224,19 @@ private ImmutableDictionary> DecodeTransitions() return argTable.Transitions.ToImmutableDictionary(); } - private uint CalculatePushedArgSize() + private uint CalculatePushedArgSize() => CalculatePushedArgSizeAt(RelativeOffset); + + /// + /// Number of bytes pushed for outgoing arguments at , + /// derived by walking the transition stream. Equivalent to native EnumGcRefsX86's + /// `pushedSize` from `scanArgRegTableI` / `scanArgRegTable`. + /// + private uint CalculatePushedArgSizeAt(uint codeOffset) { int depth = 0; - foreach (int offset in Transitions.Keys.OrderBy(i => i)) + foreach (int offset in SortedTransitionOffsets) { - if (offset > RelativeOffset) + if (offset > codeOffset) break; // calculate only to current offset foreach (BaseGcTransition gcTransition in Transitions[offset]) { @@ -233,6 +268,9 @@ private uint CalculatePushedArgSize() break; case IPtrMask: case GcTransitionCall: + case CalleeSavedRegister: + // Callee-saved register tags (e.g. partial-interrupt ESP-frame + // "Reg is saved" markers) don't affect outgoing-argument depth. break; default: throw new InvalidOperationException("Unsupported gc transition type"); @@ -240,9 +278,147 @@ private uint CalculatePushedArgSize() } } + // Clamp to >= 0: StackDepthTransition can carry negative deltas (call-site arg pops in + // partial-interrupt ESP-frame encoding) and a transient under-flow shouldn't wrap to a + // huge uint. + if (depth < 0) depth = 0; return (uint)(depth * _target.PointerSize); } + private ImmutableArray DecodeUntrackedSlots() + { + if (Header.UntrackedCount == 0) + return ImmutableArray.Empty; + + // The untracked-locals table follows the NoGCRegions table in the bitstream + // (see DecodeTransitions for the section layout). + TargetPointer offset = _gcInfoAddress + _infoHdrSize; + for (int i = 0; i < Header.NoGCRegionCount; i++) + { + _target.GCDecodeUnsigned(ref offset); + _target.GCDecodeUnsigned(ref offset); + } + + // Each entry is a signed varint, delta-encoded against the previous entry. + // Low 2 bits hold flags (byref=0x1, pinned=0x2); the remainder is the frame-relative + // stack offset. On EBP-frames the offset is EBP-relative; on ESP-frames it is + // ESP-relative. Double-aligned frames use a hybrid encoding: offsets that lie + // above the frame are EBP-relative even when the rest of the frame is ESP-based. + // Reference: gc_unwind_x86.inl (EnumGcRefsX86 untracked path) and + // ILCompiler.Reflection.ReadyToRun/x86/GcSlotTable.cs (DecodeUntracked). + uint calleeSavedRegsCount = 0; + if (Header.DoubleAlign) + { + if (Header.EdiSaved) calleeSavedRegsCount++; + if (Header.EsiSaved) calleeSavedRegsCount++; + if (Header.EbxSaved) calleeSavedRegsCount++; + } + + ImmutableArray.Builder builder = ImmutableArray.CreateBuilder((int)Header.UntrackedCount); + int lastStkOffs = 0; + for (uint i = 0; i < Header.UntrackedCount; i++) + { + int delta = _target.GCDecodeSigned(ref offset); + int stkOffs = lastStkOffs - delta; + lastStkOffs = stkOffs; + + uint lowBits = OFFSET_MASK & (uint)stkOffs; + stkOffs = (int)((uint)stkOffs & ~OFFSET_MASK); + + bool isEbpRelative = Header.EbpFrame; + if (Header.DoubleAlign && + (uint)stkOffs >= _target.PointerSize * (Header.FrameSize + calleeSavedRegsCount)) + { + // Double-aligned frame: offsets above the frame proper are EBP-relative. + isEbpRelative = true; + stkOffs -= (int)(_target.PointerSize * (Header.FrameSize + calleeSavedRegsCount)); + } + + builder.Add(new UntrackedSlot(stkOffs, isEbpRelative, lowBits)); + } + + return builder.MoveToImmutable(); + } + + private ImmutableArray DecodeVarPtrLifetimes() + { + if (Header.VarPtrTableSize == 0) + return ImmutableArray.Empty; + + // The VarPtr table follows the untracked-locals table in the bitstream. + TargetPointer offset = _gcInfoAddress + _infoHdrSize; + for (int i = 0; i < Header.NoGCRegionCount; i++) + { + _target.GCDecodeUnsigned(ref offset); + _target.GCDecodeUnsigned(ref offset); + } + for (int i = 0; i < Header.UntrackedCount; i++) + { + _target.GCDecodeSigned(ref offset); + } + + // Each entry is three unsigned varints: (varOffs, begOffs, endOffs). + // varOffs is absolute; begOffs is delta-from-previous-begOffs; endOffs is delta-from-begOffs. + // Low 2 bits of varOffs are flags matching LiveSlot.GcFlags (0x1 = byref/interior, 0x2 = pinned). + // Reference: gc_unwind_x86.inl varPtrTable processing and + // ILCompiler.Reflection.ReadyToRun/x86/GcSlotTable.cs (DecodeFrameVariableLifetimeTable). + ImmutableArray.Builder builder = ImmutableArray.CreateBuilder((int)Header.VarPtrTableSize); + uint curOffs = 0; + for (uint i = 0; i < Header.VarPtrTableSize; i++) + { + uint varOffsRaw = _target.GCDecodeUnsigned(ref offset); + uint begOffs = _target.GCDecodeUDelta(ref offset, curOffs); + uint endOffs = _target.GCDecodeUDelta(ref offset, begOffs); + + uint lowBits = varOffsRaw & OFFSET_MASK; + int stkOffs = (int)(varOffsRaw & ~OFFSET_MASK); + + // EBP-frames encode VarPtr offsets as positive values that mean EBP-relative-negative + // (locals live below EBP). Native EnumGcRefsX86 (gc_unwind_x86.inl) negates here. + if (Header.EbpFrame) + stkOffs = -stkOffs; + + curOffs = begOffs; + + builder.Add(new VarPtrLifetime(begOffs, endOffs, stkOffs, lowBits)); + } + + return builder.MoveToImmutable(); + } + + private const uint OFFSET_MASK = 0x3; + + /// + /// Returns true if falls within the method's prolog. + /// + private bool IsCodeOffsetInProlog(uint codeOffset) + => codeOffset < Header.PrologSize; + + /// + /// Returns true if falls within any epilog. + /// + private bool IsCodeOffsetInEpilog(uint codeOffset) + { + foreach (uint epilogStart in Header.Epilogs) + { + if (codeOffset > epilogStart && codeOffset < epilogStart + Header.EpilogSize) + return true; + } + return false; + } + + /// + /// Converts a single-bit value to the platform-agnostic + /// register number used by X86Context.TryReadRegister and by . + /// EAX=0, ECX=1, EDX=2, EBX=3, ESP=4, EBP=5, ESI=6, EDI=7 -- matches the x86 ModRM encoding. + /// + private static uint RegMaskToRegisterNumber(RegMask reg) + { + // RegMask is a flags enum where each register sits on its own bit + // (EAX=0x1, ECX=0x2, ..., EDI=0x80). Log2 yields the register number. + return (uint)System.Numerics.BitOperations.Log2((uint)reg); + } + uint IGCInfoDecoder.GetCodeLength() => MethodSize; uint IGCInfoDecoder.GetStackBaseRegister() @@ -255,10 +431,14 @@ uint IGCInfoDecoder.GetStackBaseRegister() } uint IGCInfoDecoder.GetSizeOfStackParameterArea() - => throw new NotSupportedException( - "x86 GC info does not encode a separate outgoing-argument scratch area; the cDAC " + - "GC scanner does not consume scratch-area sizing on x86 (the legacy x86 GC walker " + - "reasons over per-offset transitions instead)."); + { + // x86 GC info does not encode a separate outgoing-argument scratch area; the + // per-offset transitions report pushed argument pointers directly at each offset. + // Returning 0 disables the GcScanner's scratch-area filter on x86, which is the + // correct behaviour: the live state at a given offset (call site or fully-interruptible + // point) already excludes any args that have been popped by the time we resume there. + return 0; + } uint IGCInfoDecoder.GetCalleePoppedArgumentsSize() { @@ -269,8 +449,397 @@ uint IGCInfoDecoder.GetCalleePoppedArgumentsSize() } IReadOnlyList IGCInfoDecoder.GetInterruptibleRanges() - => throw new NotSupportedException("x86 GC info does not encode explicit interruptible ranges; per-offset transitions are used instead. Decoding for the cDAC IGCInfoDecoder consumers is not yet implemented."); + { + // The x86 GC info `interruptible` header bit divides methods into two encodings: + // + // * Fully interruptible (`Header.Interruptible == true`): every offset in the + // method body (post-prolog, pre-epilog) is GC-safe. The C++ walker + // (`EnumGcRefsX86` in gc_unwind_x86.inl) explicitly returns without + // reporting refs when the queried offset falls inside the prolog or any + // epilog, so we exclude those regions here too. + // * Partially interruptible (`Header.Interruptible == false`): only call sites + // are GC-safe. Each call site appears as a `GcTransitionCall` at its code + // offset. We surface each as a single-byte range so the only consumer + // (the catch-handler PC override in `StackWalk_1.WalkStackReferences`) can + // pick the first call-site offset at or after the clause start. + if (Header.Interruptible) + { + // Body minus prolog minus all epilogs. Epilogs are stored as code offsets + // (start of each epilog); each spans `EpilogSize` bytes. + uint cursor = Header.PrologSize; + uint methodSize = MethodSize; + List ranges = []; + foreach (int epilogStart in Header.Epilogs.OrderBy(e => e)) + { + uint eStart = (uint)epilogStart; + uint eEnd = eStart + Header.EpilogSize; + // IsCodeOffsetInEpilog treats `epilogStart` itself as NOT in the epilog + // (strict `>`), so the epilogStart byte is interruptible. End the preceding + // range at eStart+1 (clamped) to include that one byte. + uint rangeEnd = Math.Min(eStart + 1, methodSize); + if (rangeEnd > cursor) + ranges.Add(new InterruptibleRange(cursor, rangeEnd)); + cursor = Math.Max(cursor, eEnd); + } + if (cursor < methodSize) + ranges.Add(new InterruptibleRange(cursor, methodSize)); + return ranges; + } + + // Partially interruptible: emit each call-site offset as a (offset, offset+1) range. + List callRanges = []; + foreach (int offset in SortedTransitionOffsets) + { + if ((uint)offset < Header.PrologSize) + continue; + + foreach (BaseGcTransition transition in Transitions[offset]) + { + if (transition is GcTransitionCall) + { + callRanges.Add(new InterruptibleRange((uint)offset, (uint)offset + 1)); + break; + } + } + } + return callRanges; + } IReadOnlyList IGCInfoDecoder.EnumerateLiveSlots(uint instructionOffset, GcSlotEnumerationOptions options) - => throw new NotSupportedException("x86 GC info live-slot enumeration through IGCInfoDecoder is not yet implemented; the underlying InfoHdr/Transitions data is decoded but the IGCInfoDecoder.EnumerateLiveSlots adapter is future work."); + { + // LiveSlot.SpBase: 1 = SP-relative, 2 = FRAMEREG (EBP) relative. + // See IGCInfo.cs LiveSlot docs and GcScanner.EnumGcRefsForManagedFrame. + const uint SP_REL = 1; + const uint FRAMEREG_REL = 2; + + // The early-return gates below mirror EnumGcRefsX86 (gc_unwind_x86.inl). + + // Funclet (e.g. catch handler) sharing this parent's locals will report them itself. + if (options.IsParentOfFuncletStackFrame) + return Array.Empty(); + + // GC info doesn't describe live slots inside prolog/epilog. The runtime only reaches here + // in those regions on ExecutionAborted (thread abort, stack overflow); skip reporting. + if (IsCodeOffsetInProlog(instructionOffset) || IsCodeOffsetInEpilog(instructionOffset)) + return Array.Empty(); + + // Aborted execution at a non-safe-point in non-interruptible code yields no reliable info. + if (options.IsExecutionAborted && !Header.Interruptible) + return Array.Empty(); + + List result = []; + + // For ESP-based frames, untracked locals (and VarPtr locals when applicable) are + // argBase-relative where `argBase = ESP + pushedSize` (gc_unwind_x86.inl EnumGcRefsX86). + // Translate to a true SP-relative offset by adding the pushed size at the queried offset. + // EBP-frame offsets are FRAMEREG-relative and need no adjustment. + int espBias = Header.EbpFrame ? 0 : (int)CalculatePushedArgSizeAt(instructionOffset); + + // (1) Untracked frame locals -- always live for the entire method body. + // Filter funclets suppress untracked reporting because the parent frame already reports them + // (mirrors the isFilterFunclet path in EnumGcRefsX86). + if (!options.SuppressUntrackedSlots) + { + foreach (UntrackedSlot us in UntrackedSlots) + { + // LowBits encoding matches LiveSlot.GcFlags exactly: 0x1 = interior, 0x2 = pinned. + uint spBase = us.IsEbpRelative ? FRAMEREG_REL : SP_REL; + int spOffset = us.IsEbpRelative ? us.StackOffset : us.StackOffset + espBias; + result.Add(new LiveSlot(IsRegister: false, RegisterNumber: 0, SpOffset: spOffset, SpBase: spBase, GcFlags: us.LowBits)); + } + } + + // (2) VarPtr-tracked frame locals -- live when the lifetime-check offset is within [Begin, End). + // On non-active frames EnumGcRefsX86 evaluates lifetimes at curOffs-1: a variable can be dead + // at the return address (call was last instruction of a try, return jumps to a catch handler). + { + uint spBase = Header.EbpFrame ? FRAMEREG_REL : SP_REL; + uint varPtrOffset = (options.IsActiveFrame || instructionOffset == 0) + ? instructionOffset + : instructionOffset - 1; + foreach (VarPtrLifetime vp in VarPtrLifetimes) + { + if (varPtrOffset < vp.BeginOffset || varPtrOffset >= vp.EndOffset) + continue; + + // LowBits encoding matches LiveSlot.GcFlags exactly. + int spOffset = Header.EbpFrame ? vp.StackOffset : vp.StackOffset + espBias; + result.Add(new LiveSlot(IsRegister: false, RegisterNumber: 0, SpOffset: spOffset, SpBase: spBase, GcFlags: vp.LowBits)); + } + } + + // (3) Live registers and pushed pointer args from the transition stream. + EnumerateTransitionLiveSlots(instructionOffset, options, result, SP_REL); + + // ReportFPBasedSlotsOnly: drop register slots and any stack slot that isn't + // frame-register-relative. Mirrors GCInfoDecoder.ReportSlot. + if (options.ReportFPBasedSlotsOnly) + result.RemoveAll(s => s.IsRegister || s.SpBase != FRAMEREG_REL); + + return result; + } + + /// + /// Walks up to and including , + /// accumulating live register state and currently-pushed pointer arguments, and emits a + /// per live register / pushed pointer. + /// + /// + /// For fully-interruptible methods every transition strictly before + /// contributes to the current state. For + /// partially-interruptible methods the JIT only emits transitions at call sites; the live + /// state at the queried offset is whatever the most-recent call-site transition described. + /// Mirrors the byte-stream walks in scanArgRegTableI / scanArgRegTable (gc_unwind_x86.inl). + /// + private void EnumerateTransitionLiveSlots( + uint instructionOffset, + GcSlotEnumerationOptions options, + List result, + uint spRelBase) + { + // Live register state at the walked offset. + RegMask liveRegs = RegMask.NONE; + RegMask liveIptrRegs = RegMask.NONE; + + // Pushed pointer args, keyed by push-index (depth at PUSH time, 0-indexed). Bit 0 is the + // first push (highest stack address). The SP-relative byte offset is computed at emit + // time once finalDepth is known: addr = ESP_call + (finalDepth - 1 - pushIndex) * 4 + // (mirrors `pPendingArgFirst - i*sizeof(DWORD)` in EnumGcRefsX86). The translation must + // be deferred because subsequent pushes/pops change finalDepth. + SortedDictionary pushedPtrs = new(); + + // Total pushed pointer-size slots (incl. non-ptr args). Mirrors `argCnt` in scanArgRegTableI. + int depthSlots = 0; + + // Set when a partially-interruptible call site falls at instructionOffset; its embedded + // CallRegisters/PtrArgs/ArgMask describe the live state at the call site. + GcTransitionCall? activeCallSite = null; + + // On non-leaf frames register liveness is evaluated at the instruction *before* the call + // (a register holding a GC ref before a call may be dead afterwards). Active leaf uses + // the exact instructionOffset since execution is paused there. Mirrors curOffsRegs in + // EnumGcRefsX86. + uint regOffset = (options.IsActiveFrame || instructionOffset == 0) + ? instructionOffset + : instructionOffset - 1; + + foreach (int offset in SortedTransitionOffsets) + { + // Walk through instructionOffset (inclusive) so the call-site GcTransitionCall is + // captured for the partially-interruptible path; the regOffset adjustment above + // handles the register-state-before-call case for non-leaf fully-interruptible frames. + if (offset > instructionOffset) + break; + + foreach (BaseGcTransition transition in Transitions[offset]) + { + switch (transition) + { + case GcTransitionRegister regT: + // scanArgRegTableI gates only register-liveness bytes (00RRR DDD / 01RRR DDD) + // by curOffsRegs; arg-stream bytes (push/pop/non-ptr-push/kill, encoded as + // GcTransitionRegister with RegMask.ESP) always update depth and pushed-ptrs + // up to curOffsArgs. + if (regT.IsLive == Action.LIVE || regT.IsLive == Action.DEAD) + { + if ((uint)offset > regOffset) + continue; + } + ApplyRegisterTransition(regT, ref liveRegs, ref liveIptrRegs, ref depthSlots, pushedPtrs); + break; + case GcTransitionPointer ptrT: + ApplyPointerTransition(ptrT, ref depthSlots, pushedPtrs); + break; + case StackDepthTransition stackT: + depthSlots += stackT.StackDepthChange; + if (depthSlots < 0) depthSlots = 0; + break; + case GcTransitionCall callT when offset == (int)instructionOffset: + // Partially-interruptible call sites carry the only authoritative live + // state at the call instruction. For fully-interruptible code, + // GcTransitionCall is informational only -- the surrounding LIVE/DEAD/ + // PUSH/POP transitions already maintain the state. + activeCallSite = callT; + break; + case IPtrMask: + case CalleeSavedRegister: + case GcTransitionCall: + // CalleeSavedRegister is informational. IPtrMask is reserved for future + // interior-pointer-bitmap support. GcTransitionCall at offset != + // instructionOffset is also ignored. + break; + default: + throw new InvalidOperationException($"Unsupported x86 GC transition: {transition.GetType().Name}"); + } + } + } + + // Emit live registers. Callee-saved (EBX/EBP/ESI/EDI) are always reported when execution + // continues; callee-trashed (EAX/ECX/EDX) are valid only on the active leaf frame because + // any callee will have overwritten them. Mirrors CHK_AND_REPORT_REG in EnumGcRefsX86. + // (The !willContinueExecution case is short-circuited by the aborted+!interruptible gate.) + const RegMask CalleeTrashedScratch = RegMask.EAX | RegMask.ECX | RegMask.EDX; + foreach (RegMask r in EnumerateSingleRegs()) + { + if ((liveRegs & r) == 0) continue; + if (!options.IsActiveFrame && (r & CalleeTrashedScratch) != 0) continue; + + uint gcFlags = (liveIptrRegs & r) != 0 ? 0x1u : 0u; + result.Add(new LiveSlot(IsRegister: true, RegisterNumber: RegMaskToRegisterNumber(r), SpOffset: 0, SpBase: 0, GcFlags: gcFlags)); + } + + // Emit pushed pointer args as positive SP-relative offsets. Bit 0 (first push) ends up at + // the highest offset; the last push at offset 0. + foreach (KeyValuePair pushed in pushedPtrs) + { + int spOffset = (depthSlots - 1 - pushed.Key) * (int)_target.PointerSize; + result.Add(new LiveSlot(IsRegister: false, RegisterNumber: 0, SpOffset: spOffset, SpBase: spRelBase, GcFlags: pushed.Value)); + } + + // Partially-interruptible call site: emit its register set and pointer args directly. + if (activeCallSite is not null) + { + foreach (GcTransitionCall.CallRegister cr in activeCallSite.CallRegisters) + { + uint gcFlags = cr.IsByRef ? 0x1u : 0u; + result.Add(new LiveSlot(IsRegister: true, RegisterNumber: RegMaskToRegisterNumber(cr.Register), SpOffset: 0, SpBase: 0, GcFlags: gcFlags)); + } + if (activeCallSite.PtrArgs.Count > 0) + { + // Huge encoding (0xFB): explicit per-pointer stack offsets. + foreach (GcTransitionCall.PtrArg pa in activeCallSite.PtrArgs) + { + uint gcFlags = pa.LowBit != 0 ? 0x1u : 0u; + result.Add(new LiveSlot(IsRegister: false, RegisterNumber: 0, SpOffset: (int)pa.StackOffset, SpBase: spRelBase, GcFlags: gcFlags)); + } + } + else if (activeCallSite.ArgMask != 0) + { + // Tiny / small / medium / large encodings: argMask is a bitmap where bit i + // represents a live pointer at ESP + i*sizeof(DWORD). Mirrors the bitmap loop + // in scanArgRegTable (gc_unwind_x86.inl). + uint argMask = activeCallSite.ArgMask; + uint iargMask = activeCallSite.IArgs; + int i = 0; + while (argMask != 0) + { + if ((argMask & 1) != 0) + { + uint gcFlags = (iargMask & 1) != 0 ? 0x1u : 0u; + result.Add(new LiveSlot(IsRegister: false, RegisterNumber: 0, SpOffset: i * (int)_target.PointerSize, SpBase: spRelBase, GcFlags: gcFlags)); + } + argMask >>= 1; + iargMask >>= 1; + i++; + } + } + } + } + + private static void ApplyRegisterTransition( + GcTransitionRegister regT, + ref RegMask liveRegs, + ref RegMask liveIptrRegs, + ref int depthSlots, + SortedDictionary pushedPtrs) + { + switch (regT.IsLive) + { + case Action.LIVE: + liveRegs |= regT.Register; + if (regT.Iptr) liveIptrRegs |= regT.Register; + else liveIptrRegs &= ~regT.Register; + break; + case Action.DEAD: + liveRegs &= ~regT.Register; + liveIptrRegs &= ~regT.Register; + break; + case Action.PUSH: + // GcArgTable emits ESP push/pop as GcTransitionRegister with RegMask.ESP for + // non-ptr arg pushes (depth tracking only); real pointer pushes use other + // RegMasks. Mirror scanArgRegTableI: ESP-only pushes advance depth without + // recording a pointer. + bool isPtrPush = (regT.Register & ~RegMask.ESP) != 0; + for (int i = 0; i < regT.PushCountOrPopSize; i++) + { + if (isPtrPush) + pushedPtrs[depthSlots] = regT.Iptr ? 0x1u : 0u; + depthSlots++; + } + break; + case Action.POP: + for (int i = 0; i < regT.PushCountOrPopSize && depthSlots > 0; i++) + { + depthSlots--; + pushedPtrs.Remove(depthSlots); + } + break; + case Action.KILL: + // EBP-frame partial-interrupt 0xFD: invalidate all currently-tracked pushed args. + pushedPtrs.Clear(); + depthSlots = 0; + break; + } + } + + private static void ApplyPointerTransition( + GcTransitionPointer ptrT, + ref int depthSlots, + SortedDictionary pushedPtrs) + { + switch (ptrT.Act) + { + case Action.PUSH: + // Non-ptr arg pushes (GetTransitionsFullyInterruptible 0xB0..0xB7) advance depth + // only; pointer pushes also record into pushedPtrs. + if (ptrT.IsPtr) + pushedPtrs[depthSlots] = ptrT.Iptr ? 0x1u : 0u; + depthSlots++; + break; + case Action.POP: + for (uint i = 0; i < ptrT.ArgOffset && depthSlots > 0; i++) + { + depthSlots--; + pushedPtrs.Remove(depthSlots); + } + break; + case Action.KILL: + pushedPtrs.Clear(); + depthSlots = 0; + break; + } + } + + private static IEnumerable EnumerateSingleRegs() + { + yield return RegMask.EAX; + yield return RegMask.ECX; + yield return RegMask.EDX; + yield return RegMask.EBX; + yield return RegMask.EBP; + yield return RegMask.ESI; + yield return RegMask.EDI; + // ESP is intentionally excluded -- it's never a live GC ref holder. + } } + +/// +/// An always-live GC frame slot (entry of the untracked-locals table). +/// The slot is live for the entire method body (post-prolog, pre-epilog). +/// +/// Frame-relative byte offset of the slot. +/// True if is EBP-relative; false if ESP-relative. +/// Raw flag bits from the encoded offset (0x1 = byref/interior, 0x2 = pinned). +internal readonly record struct UntrackedSlot(int StackOffset, bool IsEbpRelative, uint LowBits); + +/// +/// A tracked GC frame variable with a per-offset lifetime range (entry of the +/// FrameVariableLifetime / VarPtr table). The slot is live while the executing +/// instruction offset lies in [BeginOffset, EndOffset). +/// VarPtr-tracked variables only exist on EBP-based frames. +/// +/// Inclusive code offset (relative to method start) at which the slot becomes live. +/// Exclusive code offset at which the slot becomes dead. +/// Frame-relative byte offset of the slot (EBP-relative on EBP frames, ESP-relative otherwise). +/// Raw flag bits from the encoded offset (0x1 = byref/interior, 0x2 = pinned). +internal readonly record struct VarPtrLifetime(uint BeginOffset, uint EndOffset, int StackOffset, uint LowBits); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/GCTransition.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/GCTransition.cs index d693702051410b..795ccdaf0a3448 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/GCTransition.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/GCTransition.cs @@ -79,6 +79,8 @@ public GcTransitionRegister(int codeOffset, RegMask reg, Action isLive, bool isT { Register = reg; IsLive = isLive; + IsThis = isThis; + Iptr = iptr; PushCountOrPopSize = pushCountOrPopSize; } @@ -130,6 +132,8 @@ public GcTransitionPointer(int codeOffset, uint argOffs, uint argCnt, Action act ArgOffset = argOffs; ArgCount = argCnt; Act = act; + IsThis = isThis; + Iptr = iptr; IsPtr = isPtr; } 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 41e2413a41df8b..cf079229ee4e22 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 @@ -613,6 +613,7 @@ public TargetPointer GetWellKnownMethodTable(WellKnownMethodTable kind) } 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 @@ -1990,6 +1991,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; @@ -2072,6 +2079,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/FrameHandling/X86FrameHandler.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/X86FrameHandler.cs index 8b06193322f99f..6af95413615273 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/X86FrameHandler.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/X86FrameHandler.cs @@ -22,6 +22,50 @@ public void HandleHijackFrame(HijackFrame frame) UpdateFromRegisterDict(args.Registers); } + public override void HandleTransitionFrame(FramedMethodFrame framedMethodFrame) + { + // Set IP, SP and callee-saved registers from the transition block (shared logic). + base.HandleTransitionFrame(framedMethodFrame); + + // x86: the base implementation skips the callee-popped argument byte count + // (cbStackPop) that the runtime's TransitionFrame::UpdateRegDisplay_Impl adds + // to CallerSP. Without it the missing offset propagates through subsequent + // EBP-chain unwinds. x64/arm64 use RtlVirtualUnwind which resyncs from PE + // unwind data, so they are unaffected. + // + // PInvokeCalliFrame has no MethodDesc -- pull cbStackPop from the VASigCookie. + // Every other transition Frame has a MethodDesc: ask the shared + // CallingConvention contract to produce its GCRefMap blob (which begins + // with the x86 stack-pop prefix written by GCRefMapBuilder::WriteStackPop) + // and decode just that prefix to recover cbStackPop in pointer-size units. + FrameHelpers frameHelpers = new(_target); + FrameType frameType = frameHelpers.GetFrameType( + _target.ProcessedData.GetOrAdd(framedMethodFrame.Address).Identifier); + + if (frameType == FrameType.PInvokeCalliFrame) + { + PInvokeCalliFrame frame = _target.ProcessedData.GetOrAdd(framedMethodFrame.Address); + if (frame.VASigCookiePtr != TargetPointer.Null) + { + VASigCookie cookie = _target.ProcessedData.GetOrAdd(frame.VASigCookiePtr); + _context.Context.Esp += cookie.SizeOfArgs; + } + return; + } + + if (framedMethodFrame.MethodDescPtr == TargetPointer.Null) + return; + + MethodDescHandle md = _target.Contracts.RuntimeTypeSystem.GetMethodDescHandle(framedMethodFrame.MethodDescPtr); + if (!_target.Contracts.CallingConvention.TryComputeArgGCRefMapBlob(md, out byte[] blob) || blob.Length == 0) + return; + + // ReadStackPop returns the count in pointer-size units (4 bytes on x86). + GCRefMapDecoder decoder = new(blob); + uint stackPopSlots = decoder.ReadStackPop(); + _context.Context.Esp += stackPopSlots * (uint)_target.PointerSize; + } + public override void HandleTailCallFrame(TailCallFrame frame) { _context.Context.Eip = (uint)frame.ReturnAddress; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GCRefMapDecoder.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GCRefMapDecoder.cs index 6815878ec65c86..e502e42988cc8f 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GCRefMapDecoder.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GCRefMapDecoder.cs @@ -34,8 +34,14 @@ internal enum GCRefMapToken /// internal ref struct GCRefMapDecoder { - private readonly Target _target; + // The decoder can source its bytes either from target memory (the R2R image, + // for ExternalMethodFrame / StubDispatchFrame) or from a host-side byte[] + // (for blobs we synthesize via ICallingConvention.TryComputeArgGCRefMapBlob). + // Exactly one of (_target + _currentByte) or (_blob) is non-null per instance. + private readonly Target? _target; private TargetPointer _currentByte; + private readonly byte[]? _blob; + private int _blobIndex; private int _pendingByte; private int _pos; @@ -43,6 +49,18 @@ public GCRefMapDecoder(Target target, TargetPointer blob) { _target = target; _currentByte = blob; + _blob = null; + _blobIndex = 0; + _pendingByte = 0x80; // Forces first byte read + _pos = 0; + } + + public GCRefMapDecoder(byte[] blob) + { + _target = null; + _currentByte = TargetPointer.Null; + _blob = blob; + _blobIndex = 0; _pendingByte = 0x80; // Forces first byte read _pos = 0; } @@ -56,8 +74,16 @@ private int GetBit() int x = _pendingByte; if ((x & 0x80) != 0) { - x = _target.Read(_currentByte); - _currentByte = new TargetPointer(_currentByte.Value + 1); + if (_blob is not null) + { + x = _blobIndex < _blob.Length ? _blob[_blobIndex] : 0; + _blobIndex++; + } + else + { + x = _target!.Read(_currentByte); + _currentByte = new TargetPointer(_currentByte.Value + 1); + } x |= (x & 0x80) << 7; } _pendingByte = x >> 1; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs index c94e7b66757a9d..4aeb9de2d66dd2 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs @@ -196,6 +196,20 @@ private void PromoteCallerStackUsingGCRefMap( if (_target.Contracts.RuntimeInfo.GetTargetArchitecture() is RuntimeInfoArchitecture.X86) decoder.ReadStackPop(); + EnumerateGCRefMapTokens(ref decoder, tb, scanContext); + } + + /// + /// Iterates the token stream of a GCRefMap and reports refs at each tagged + /// transition-block slot. Shared by both the R2R-blob path + /// () and the synthesized-blob + /// path (). + /// + private void EnumerateGCRefMapTokens( + ref GCRefMapDecoder decoder, + Data.TransitionBlock tb, + GcScanContext scanContext) + { while (!decoder.AtEnd) { int pos = decoder.CurrentPos; @@ -236,14 +250,23 @@ private void ScanDynamicHelperFrame( Data.TransitionBlock tb = _target.ProcessedData.GetOrAdd(transitionBlock); TargetPointer argRegStart = tb.ArgumentRegisters; + // x86 lays out ArgumentRegisters as { EDX, ECX } (see ENUM_ARGUMENT_REGISTERS_BACKWARD + // in src/coreclr/vm/i386/cgencpu.h). Native DynamicHelperFrame::GcScanRoots_Impl reports + // ObjectArg at offsetof(ArgumentRegisters, ECX) and ObjectArg2 at offsetof(EDX), which + // is the reverse of the layout-order indices used on other architectures. + bool isX86 = _target.Contracts.RuntimeInfo.GetTargetArchitecture() is RuntimeInfoArchitecture.X86; + int objectArgOffset = isX86 ? _target.PointerSize : 0; + int objectArg2Offset = isX86 ? 0 : _target.PointerSize; + if ((dynamicHelperFrameFlags & DynamicHelperFrameFlags_ObjectArg) != 0) { - scanContext.GCReportCallback(argRegStart, GcScanFlags.None); + TargetPointer argAddr = new(argRegStart.Value + (uint)objectArgOffset); + scanContext.GCReportCallback(argAddr, GcScanFlags.None); } if ((dynamicHelperFrameFlags & DynamicHelperFrameFlags_ObjectArg2) != 0) { - TargetPointer argAddr = new(argRegStart.Value + (uint)_target.PointerSize); + TargetPointer argAddr = new(argRegStart.Value + (uint)objectArg2Offset); scanContext.GCReportCallback(argAddr, GcScanFlags.None); } } @@ -331,18 +354,77 @@ private TargetPointer FindGCRefMap(TargetPointer indirection) /// Matches native TransitionFrame::PromoteCallerStack (frames.cpp:1494). /// /// - /// Not yet ported. Every call records a deferred frame so the stress harness - /// buckets the resulting cDAC-vs-runtime diff at this frame as a known issue - /// rather than a real cDAC bug. Will be replaced with a real port once the - /// signature- and ArgIterator-based ref enumeration lands. + /// On supported targets (Windows x86 / x64), synthesizes a GCRefMap from the + /// callee's MethodDesc via ICallingConvention.TryComputeArgGCRefMapBlob and + /// runs the same token-iteration loop as the R2R-backed path. On unsupported + /// targets (SystemV-AMD64 / ARM64 struct-in-registers, arm32, etc.) and on + /// any failure to synthesize a blob, records the frame as deferred so the + /// stress harness buckets the resulting diff as a known issue rather than a + /// real cDAC bug. /// - private static void PromoteCallerStack(TargetPointer frameAddress, GcScanContext scanContext) + private void PromoteCallerStack(TargetPointer frameAddress, GcScanContext scanContext) { - scanContext.RecordDeferredFrame(frameAddress); + IRuntimeInfo runtimeInfo = _target.Contracts.RuntimeInfo; + RuntimeInfoArchitecture arch = runtimeInfo.GetTargetArchitecture(); + bool supportedByCallingConvention = + runtimeInfo.GetTargetOperatingSystem() == RuntimeInfoOperatingSystem.Windows + && arch is RuntimeInfoArchitecture.X86 or RuntimeInfoArchitecture.X64; + + if (!supportedByCallingConvention) + { + scanContext.RecordDeferredFrame(frameAddress); + return; + } + + Data.FramedMethodFrame fmf = _target.ProcessedData.GetOrAdd(frameAddress); + if (fmf.MethodDescPtr == TargetPointer.Null) + { + scanContext.RecordDeferredFrame(frameAddress); + return; + } + + MethodDescHandle md = _target.Contracts.RuntimeTypeSystem.GetMethodDescHandle(fmf.MethodDescPtr); + if (!_target.Contracts.CallingConvention.TryComputeArgGCRefMapBlob(md, out byte[] blob) || blob.Length == 0) + { + scanContext.RecordDeferredFrame(frameAddress); + return; + } + + Data.TransitionBlock tb = _target.ProcessedData.GetOrAdd(fmf.TransitionBlockPtr); + GCRefMapDecoder decoder = new(blob); + + // x86 prefix: callee-popped stack-pop count in pointer-size units. The + // ref enumeration uses positions only, so consume and discard. + if (arch is RuntimeInfoArchitecture.X86) + decoder.ReadStackPop(); + + EnumerateGCRefMapTokens(ref decoder, tb, scanContext); } private TargetPointer AddressFromGCRefMapPos(Data.TransitionBlock tb, int pos) { + // x86 needs special handling because: + // - m_argumentRegisters is the FIRST field of TransitionBlock (offset 0) + // and is laid out in ENUM_ARGUMENT_REGISTERS_BACKWARD order, so the + // first two GCRefMap positions map to reverse offsets within ArgRegs. + // - Stack args begin at sizeof(TransitionBlock) (= OffsetOfArgs), after + // CalleeSavedRegisters + ReturnAddress. There is a 20-byte gap between + // the arg-regs area and OffsetOfArgs that is NOT walked by GCRefMap + // positions, so `FirstGCRefMapSlot + pos * PointerSize` (the default + // on other arches) is wrong for pos >= NUM_ARGUMENT_REGISTERS. + // Mirrors native OffsetFromGCRefMapPos (frames.cpp). + if (_target.Contracts.RuntimeInfo.GetTargetArchitecture() is RuntimeInfoArchitecture.X86) + { + const int x86NumArgRegs = 2; + int x86ArgRegsSize = x86NumArgRegs * _target.PointerSize; + if (pos < x86NumArgRegs) + { + int offset = x86ArgRegsSize - (pos + 1) * _target.PointerSize; + return new TargetPointer(tb.ArgumentRegisters.Value + (ulong)offset); + } + int stackOffset = (pos - x86NumArgRegs) * _target.PointerSize; + return new TargetPointer(tb.OffsetOfArgs.Value + (ulong)stackOffset); + } return new TargetPointer(tb.FirstGCRefMapSlot.Value + (ulong)(pos * _target.PointerSize)); } 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 c98c15249bfb11..95191a6dac2300 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/Data/Frames/PInvokeCalliFrame.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/PInvokeCalliFrame.cs new file mode 100644 index 00000000000000..b0d7353c158de2 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/PInvokeCalliFrame.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.DataContractReader.Data; + +[CdacType(nameof(DataType.PInvokeCalliFrame))] +internal partial class PInvokeCalliFrame : IData +{ + [Field] public TargetPointer VASigCookiePtr { get; } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/TransitionBlock.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/TransitionBlock.cs index 3daacea455fdbb..718e73492d0254 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/TransitionBlock.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/TransitionBlock.cs @@ -22,4 +22,12 @@ internal partial class TransitionBlock : IData /// [FieldAddress] public TargetPointer FirstGCRefMapSlot { get; } + + /// + /// Address just past the end of the TransitionBlock, where caller-pushed + /// stack arguments begin. On x86 this is where GCRefMap positions + /// >= NUM_ARGUMENT_REGISTERS map to (see native OffsetFromGCRefMapPos). + /// + [FieldAddress] + public TargetPointer OffsetOfArgs { get; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs index 4abbf4ce732740..e7d2e874b36bc4 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs @@ -178,6 +178,7 @@ public enum DataType ExternalMethodFrame, DynamicHelperFrame, InterpreterFrame, + PInvokeCalliFrame, ComCallWrapper, SimpleComCallWrapper, 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..d73fd88092ad20 --- /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)Unsafe.SizeOf()) + 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/DumpTests/StackReferenceDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/StackReferenceDumpTests.cs index d2a844e9de8d6e..b50da0526c7d4c 100644 --- a/src/native/managed/cdac/tests/DumpTests/StackReferenceDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/StackReferenceDumpTests.cs @@ -56,7 +56,6 @@ public void WalkStackReferences_RefsHaveValidSourceInfo(TestConfiguration config [ConditionalTheory] [MemberData(nameof(TestConfigurations))] [SkipOnVersion("net10.0", "InlinedCallFrame.Datum was added after net10.0")] - [SkipOnArch("x86", "GCInfo decoder does not support x86")] public void GCRoots_WalkStackReferences_FindsRefs(TestConfiguration config) { InitializeDumpTest(config, "GCRoots", "full"); @@ -73,7 +72,6 @@ public void GCRoots_WalkStackReferences_FindsRefs(TestConfiguration config) [ConditionalTheory] [MemberData(nameof(TestConfigurations))] [SkipOnVersion("net10.0", "InlinedCallFrame.Datum was added after net10.0")] - [SkipOnArch("x86", "GCInfo decoder does not support x86")] public void GCRoots_RefsPointToValidObjects(TestConfiguration config) { InitializeDumpTest(config, "GCRoots", "full"); 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..f3cd90148ac9ed 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"); + + // 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 GC stress: {debuggeeName}"); + _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,29 +156,151 @@ internal async Task RunGCStressAsync(string debuggeeName, int } /// - /// Asserts the GC stress run produced at least one verification and had 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 - /// count are visible during triage. + /// Asserts the GCREFS stress run produced a [GC_STATS] summary + /// with at least one verification and no hard failures. + /// + /// On Windows x86 / x64 the cDAC stack-ref enumeration is at parity with + /// the runtime and the framework rejects any : + /// a non-zero count signals a regression in the ICallingConvention port and + /// fails the test. On other targets we still tolerate KnownIssues because + /// PromoteCallerStack falls back to RecordDeferredFrame there + /// (see GcScanner.PromoteCallerStack). /// 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}"); } + + // On supported targets every Frame's caller-arg refs are enumerated via + // the GCRefMap blob synthesized by ICallingConvention -- there should be + // no deferred frames at all, so any KnownIssue count is a regression. + GetTargetPlatform(out OSPlatform os, out Architecture arch); + bool requiresZeroKnownIssues = + os == OSPlatform.Windows && arch is Architecture.X86 or Architecture.X64; + if (requiresZeroKnownIssues && results.KnownIssues > 0) + { + string analysis = results.AnalyzeFailures(maxFailures: 3); + Assert.Fail( + $"GCREFS stress test '{debuggeeName}' had {results.KnownIssues} known issue(s) " + + $"out of {results.TotalVerifications} verifications. " + + "Windows x86 / x64 do not accept any deferred frames in this PR's scope -- " + + "every transition Frame's caller-stack scan must succeed via the shared " + + "ICallingConvention.TryComputeArgGCRefMapBlob path. A non-zero count likely " + + "indicates the encoder declined a method it previously handled (regression " + + "in CallingConvention_1.ComputeArgGCRefMapBlobCore).\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() 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..57841006264962 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/CdacStressTests.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.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."); + + 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..8edf075463cfc1 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/CallSignatures/CallSignatures.csproj @@ -0,0 +1,9 @@ + + + latest + + $(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..a003eef81d04f2 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/CrossModule/CrossModule.csproj @@ -0,0 +1,13 @@ + + + latest + + + + + + + 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..ecab08f1f919fd --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/VarArgs/VarArgs.csproj @@ -0,0 +1,5 @@ + + + latest + + diff --git a/src/native/managed/cdac/tests/StressTests/README.md b/src/native/managed/cdac/tests/StressTests/README.md index 50aae0aa6158ba..4ec90b3917816a 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 `GCStress_*` 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,11 +151,14 @@ $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 @@ -141,7 +169,7 @@ CdacStressTestBase.RunGCStressAsync(debuggeeName) ├── 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/known-issues.md b/src/native/managed/cdac/tests/StressTests/known-issues.md index 838e4491d2dc48..37e8890fba70b6 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 | |---------|---------| @@ -68,6 +68,64 @@ need to be handled — that case currently isn't reachable because more structured mechanism (e.g., ETW events or StressLog) for better tooling integration and reduced I/O overhead during stress runs. +## Known intermittent failure: x86 stress flake (cDAC EnumGcRefs misses callee-saved register refs during EH-rich startup) + +Pattern (~20% of full x86 suite runs trigger this on at least one +debuggee): + +``` +[FAIL] Thread=0x... IP=0x... cDAC=7 RT=45 frames=12 (match=5 mismatch=7 known_nie=0) + Frame #4 System.RuntimeType.SplitName(...) [MISMATCH] cDAC=0 RT=1 SP_cDAC=0x0 SP_RT=0x0 + [ONLY(RT)] Addr=0x0 Obj=0x... Flags=0x0 Reg=6 Off=0 + Frame #5 System.RuntimeType.GetNestedType(...) [MISMATCH] cDAC=0 RT=3 ... + [ONLY(RT)] Addr=0x0 Obj=0x... Flags=0x0 Reg=6 Off=0 + ... + ... continues up through System.Diagnostics.Tracing.EventSource frames +``` + +Signature: the RT-only refs are concentrated in callee-saved registers +(`Reg=6` ESI, `Reg=7` EDI) on frames whose stack trace runs through +`NativeRuntimeEventSource..cctor()` -> `EventSource.Initialize` -> +`RuntimeType.GetNestedType` -> `RuntimeType.SplitName`. The frames +otherwise unwind cleanly and surrounding frames match. + +x86-only -- x64 and arm64 stress runs are consistently clean. This is +not the previously-tracked x64 GC-stress crashes #129545/#129546 +(those are completely different crashes in `MethodTable::Validate` +during managed exception unwind). + +Investigation so far: +- `HasFrameBeenUnwoundByAnyActiveException` is NOT the cause: across + ~77k invocations during a reproduced flake it returned `false` every + time, matching the runtime's behavior. +- `EnumerateLiveSlots` returns 0 slots for the affected frames at the + cDAC-computed `relativeOffset` (small values like 0x2d / 0x1f / 0x14). + The runtime sees ESI/EDI live at those frames -- so either cDAC's IP + differs from the runtime's view of the frame, or our partially- + interruptible call-site matching has an off-by-one when the trigger + fires between (rather than exactly at) call sites. + +Most likely root cause is one of: +1. For partially-interruptible methods, our `activeCallSite` match + requires `transition.Offset == instructionOffset` exactly. If the + IP cDAC reads for a frame mid-EH-dispatch is not the call-site + return-address offset (e.g., it's the call-instruction offset or + somewhere mid-instruction), the match fails and no register refs + are emitted. +2. The runtime tracks callee-saved register values across unwinds via + REGDISPLAY's `pCallerContext`; cDAC re-unwinds via + `Context.Clone().Unwind()` each call. A divergent context state + would produce a divergent IP and thus a divergent live-mask. +3. Some x86-specific frame-type handling difference between cDAC's + `StackWalk_1` iteration and the runtime's `StackWalk` during the + EH-dispatch-of-managed-exceptions path. + +Resolving this requires a follow-up investigation that compares the +IPs cDAC and the runtime see for these specific frames during a +reproduced flake (e.g., by adding per-frame instruction-pointer logging +to both sides of the stress harness and diffing them). It is x86-only, +flaky, and not gated on this PR. + ## Log Format Each verification emits a single header line followed by, on `[FAIL]` or @@ -92,6 +150,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.