From e636a32b46088cd7edf38abf94ffe2bc697c50e6 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Thu, 21 May 2026 17:01:28 -0400 Subject: [PATCH 1/9] WIP: cDAC shared calling-convention port (recovered) Recovered from a dangling git stash that was previously lost. Includes: - Per-arch ArgIterator port under src/native/managed/cdac/.../Contracts/CallingConvention/ (X86, AMD64-Unix, AMD64-Windows, Arm32, Arm64, RiscV64+LoongArch64) atop ArgIteratorBase / ArgIteratorData / ArgIteratorFactory / TransitionBlockLayout. - New ICallingConvention contract and CallingConvention_1 implementation that drives the iterators from a decoded signature. - New data descriptor entries for TransitionBlock + ArgumentRegisters (src/coreclr/vm/datadescriptor/datadescriptor.inc, src/coreclr/vm/class.h). - GcScanner refactored to consume CallSiteLayout for caller-stack promotion; CallingConvention contract fetched lazily so stack-walk tests that do not register it still work. - Test suite under src/native/managed/cdac/tests/CallingConvention/ covering 214 per-arch cases, plus MockDescriptors.CallingConvention support and RuntimeTypeSystemGetVectorSizeTests. Post-recovery cleanup applied on top of the original stash: - Files relocated from Contracts/StackWalk/CallingConvention/ to Contracts/CallingConvention/; helper namespace renamed to Contracts.CallingConventionHelpers (matches Contracts/GCInfo/ pattern). - Collapsed the abstract AMD64UnixArgIterator + CdacAMD64UnixArgIterator pair into a single concrete AMD64UnixArgIterator. - Dropped vestigial extraObjectFirstArg / extraFunctionPointerArg ctor parameters that were never set to true in any caller. - Fixed API drift vs. current origin/main (GenericContextLoc-based check, non-generic ArgIterator types, Target.FieldInfo.TypeName). Builds cdac.slnx clean (0 errors, 0 warnings). Test result: 2469 passed, 0 failed, 16 skipped. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cdac-calling-conventions/README.md | 107 +++ cdac-calling-conventions/amd64-unix.md | 161 ++++ cdac-calling-conventions/amd64-windows.md | 156 ++++ cdac-calling-conventions/arm32.md | 135 +++ cdac-calling-conventions/arm64.md | 182 ++++ cdac-calling-conventions/x86.md | 103 +++ src/coreclr/vm/class.h | 1 + .../vm/datadescriptor/datadescriptor.inc | 24 + .../ContractRegistry.cs | 4 + .../Contracts/ICallingConvention.cs | 100 +++ .../Contracts/IRuntimeTypeSystem.cs | 11 + .../Constants.cs | 3 + .../CallingConvention/AMD64UnixArgIterator.cs | 213 +++++ .../AMD64WindowsArgIterator.cs | 98 +++ .../CallingConvention/ArgIteratorBase.cs | 347 ++++++++ .../CallingConvention/ArgIteratorData.cs | 104 +++ .../CallingConvention/ArgIteratorFactory.cs | 41 + .../CallingConvention/ArgTypeInfo.cs | 253 ++++++ .../ArgTypeInfoSignatureProvider.cs | 305 +++++++ .../CallingConvention/Arm32ArgIterator.cs | 213 +++++ .../CallingConvention/Arm64ArgIterator.cs | 213 +++++ .../CallingConvention/CallingConvention_1.cs | 167 ++++ .../RiscV64LoongArch64ArgIterator.cs | 98 +++ .../SystemVStructClassifier.cs | 459 +++++++++++ .../TransitionBlockLayout.cs | 42 + .../CallingConvention/X86ArgIterator.cs | 281 +++++++ .../Contracts/RuntimeTypeSystem_1.cs | 104 ++- .../Contracts/StackWalk/GC/GcScanner.cs | 157 +--- .../StackWalk/GC/GcSignatureTypeProvider.cs | 219 ----- .../Contracts/StackWalk/GC/GcTypeKind.cs | 42 + .../CoreCLRContracts.cs | 2 + .../Data/EEClass.cs | 2 + .../MethodTableFlags_1.cs | 4 + .../AMD64UnixCallingConventionTests.cs | 778 ++++++++++++++++++ .../AMD64WindowsCallingConventionTests.cs | 617 ++++++++++++++ .../Arm32CallingConventionTests.cs | 306 +++++++ .../Arm64CallingConventionTests.cs | 602 ++++++++++++++ .../tests/CallingConvention/CallConvCases.cs | 85 ++ .../CallingConventionTestHelpers.cs | 230 ++++++ .../CallingConventionTests.cs | 81 ++ .../CallingConvention/SignatureBlobBuilder.cs | 139 ++++ .../SyntheticVectorMetadata.cs | 140 ++++ .../tests/CallingConvention/TEST_INVENTORY.md | 284 +++++++ .../X86CallingConventionTests.cs | 255 ++++++ .../managed/cdac/tests/MethodTableTests.cs | 82 +- .../cdac/tests/MockDescriptors/Layout.cs | 1 - .../MockDescriptors.CallingConvention.cs | 252 ++++++ .../MockDescriptors.RuntimeTypeSystem.cs | 59 +- .../RuntimeTypeSystemGetVectorSizeTests.cs | 109 +++ 49 files changed, 8006 insertions(+), 365 deletions(-) create mode 100644 cdac-calling-conventions/README.md create mode 100644 cdac-calling-conventions/amd64-unix.md create mode 100644 cdac-calling-conventions/amd64-windows.md create mode 100644 cdac-calling-conventions/arm32.md create mode 100644 cdac-calling-conventions/arm64.md create mode 100644 cdac-calling-conventions/x86.md create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ICallingConvention.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/AMD64UnixArgIterator.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/AMD64WindowsArgIterator.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgIteratorBase.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgIteratorData.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgIteratorFactory.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgTypeInfo.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgTypeInfoSignatureProvider.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/Arm32ArgIterator.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/Arm64ArgIterator.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CallingConvention_1.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/RiscV64LoongArch64ArgIterator.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/SystemVStructClassifier.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/TransitionBlockLayout.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/X86ArgIterator.cs delete mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcSignatureTypeProvider.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcTypeKind.cs create mode 100644 src/native/managed/cdac/tests/CallingConvention/AMD64UnixCallingConventionTests.cs create mode 100644 src/native/managed/cdac/tests/CallingConvention/AMD64WindowsCallingConventionTests.cs create mode 100644 src/native/managed/cdac/tests/CallingConvention/Arm32CallingConventionTests.cs create mode 100644 src/native/managed/cdac/tests/CallingConvention/Arm64CallingConventionTests.cs create mode 100644 src/native/managed/cdac/tests/CallingConvention/CallConvCases.cs create mode 100644 src/native/managed/cdac/tests/CallingConvention/CallingConventionTestHelpers.cs create mode 100644 src/native/managed/cdac/tests/CallingConvention/CallingConventionTests.cs create mode 100644 src/native/managed/cdac/tests/CallingConvention/SignatureBlobBuilder.cs create mode 100644 src/native/managed/cdac/tests/CallingConvention/SyntheticVectorMetadata.cs create mode 100644 src/native/managed/cdac/tests/CallingConvention/TEST_INVENTORY.md create mode 100644 src/native/managed/cdac/tests/CallingConvention/X86CallingConventionTests.cs create mode 100644 src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.CallingConvention.cs create mode 100644 src/native/managed/cdac/tests/RuntimeTypeSystemGetVectorSizeTests.cs diff --git a/cdac-calling-conventions/README.md b/cdac-calling-conventions/README.md new file mode 100644 index 00000000000000..b6f148fdd25670 --- /dev/null +++ b/cdac-calling-conventions/README.md @@ -0,0 +1,107 @@ +# Managed Calling Conventions for cDAC Stack Walking + +This folder documents the calling conventions used by managed code on each +supported platform, as implemented by the per-architecture `ArgIterator` +subclasses in +[`src/native/managed/cdac/.../StackWalk/CallingConvention/`](../src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/). + +The cDAC's argument iterator is the managed reimplementation of the legacy DAC's +sig-walking layer. For each platform it answers the question: **"given a method +signature, where (register or stack offset) is each argument located when the +method is invoked?"** This is consumed by diagnostic tools (stack walks, locals +inspection, SOS, ClrMD, etc.) to inspect live frames in a target process. + +Each doc focuses on **what the managed CLR does**, with deltas vs. the native +platform ABI called out explicitly. They are not a substitute for the platform +ABI specs -- read those for the base rules, then read these for the managed +specials. + +## Platform docs + +| Platform | Doc | Iterator | +|---|---|---| +| x86 (Windows / Linux 32-bit) | [`x86.md`](./x86.md) | [`X86ArgIterator.cs`](../src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/X86ArgIterator.cs) | +| AMD64 Windows | [`amd64-windows.md`](./amd64-windows.md) | [`AMD64WindowsArgIterator.cs`](../src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/AMD64WindowsArgIterator.cs) | +| AMD64 Unix (Linux / macOS) | [`amd64-unix.md`](./amd64-unix.md) | [`AMD64UnixArgIterator.cs`](../src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/AMD64UnixArgIterator.cs) | +| ARM32 (AAPCS, Linux armhf / Windows ARM) | [`arm32.md`](./arm32.md) | [`Arm32ArgIterator.cs`](../src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/Arm32ArgIterator.cs) | +| ARM64 (AAPCS64, Linux / Windows / Apple) | [`arm64.md`](./arm64.md) | [`Arm64ArgIterator.cs`](../src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/Arm64ArgIterator.cs) | +| RISC-V 64 / LoongArch 64 | [`riscv64-loongarch64.md`](./riscv64-loongarch64.md) | [`RiscV64LoongArch64ArgIterator.cs`](../src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/RiscV64LoongArch64ArgIterator.cs) | + +## Cross-cutting concepts + +These apply to all platforms and are not repeated in each doc; consult the +upstream design doc [`docs/design/coreclr/botr/clr-abi.md`](../docs/design/coreclr/botr/clr-abi.md) +for full detail. + +### Argument prefix order + +Every managed method starts its argument list with zero or more **hidden +arguments**, in this fixed order, before any user arguments: + +``` +[this] [retBuf] [genericContext] [asyncContinuation] [varArgCookie] userArgs... +``` + +- **`this`**: Instance methods only. Always passed first (managed-specific -- + native C++ x64 reorders it after the ret buf on some platforms). +- **`retBuf`**: Hidden pointer for methods returning a value type that doesn't + fit in the return registers (rules vary per platform). Callee writes the + result through this pointer; on AMD64 the callee also returns the buffer + address in the integer return register. +- **`genericContext`**: For *shared generic* methods, a `MethodDesc*` (generic + methods) or `MethodTable*` (static methods on generic types) telling the + callee which instantiation it's serving. +- **`asyncContinuation`**: For methods participating in the async stack + protocol (new in the runtime-async work). +- **`varArgCookie`**: For managed varargs (`__arglist` / + `IMAGE_CEE_CS_CALLCONV_VARARG`), a pointer to a runtime-parseable signature + blob describing the variadic tail. + +The cDAC base class counts these in `ArgIteratorBase.ComputeInitialNumRegistersUsed` +(see [`ArgIteratorBase.cs`](../src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/ArgIteratorBase.cs)) +before user-arg iteration begins. x86 is an outlier -- it counts these +separately in its own `ComputeSizeOfArgStack` pass. + +### TypedReference + +`System.TypedReference` is `{ ref byte _value; IntPtr _type; }` = 16 bytes, +referenced in signatures by `ELEMENT_TYPE_TYPEDBYREF` (0x16) with no class +token. The runtime keeps a `g_TypedReferenceMT` global pointing at the +TypedReference MethodTable; the signature walker substitutes that MT whenever +it encounters `ELEMENT_TYPE_TYPEDBYREF`, then the iterator treats it as an +ordinary 16-byte value type. + +In cDAC, the substitution lives in `ArgTypeInfoSignatureProvider.GetTypedReferenceInfo()`. +Each platform doc summarizes where a `TypedReference` parameter and return +value land. + +### Implicit by-reference + +On most platforms, value types whose size exceeds a per-platform threshold are +passed via a hidden pointer (the *implicit byref*) instead of by value. The +JIT must: + +- Report the implicit-byref parameter as an interior pointer (GC `BYREF`) in + the GC info, because the caller may legitimately point at the GC heap (not + always a stack temp). +- Use checked write barriers for any stores through the pointer. + +The per-platform threshold is encoded as `EnregisteredParamTypeMaxSize` on each +iterator (see the abstract property on `ArgIteratorBase`). + +### Funclets, frame pointers, and other non-arg-iterator concerns + +The exception-handling funclet model, frame-pointer policy, GC-info layout, +profiler hooks, and other CLR-internal contracts are documented in +[`docs/design/coreclr/botr/clr-abi.md`](../docs/design/coreclr/botr/clr-abi.md). +These don't affect argument iteration directly; they affect codegen and +stack walking elsewhere. + +## Reference + +- [`docs/design/coreclr/botr/clr-abi.md`](../docs/design/coreclr/botr/clr-abi.md) -- the + authoritative CLR ABI design document. +- [`src/coreclr/vm/callingconvention.h`](../src/coreclr/vm/callingconvention.h) -- + the native VM's `ArgIteratorTemplate`, which each cDAC iterator mirrors. +- [`src/coreclr/tools/aot/ILCompiler.ReadyToRun/.../ArgIterator.cs`](../src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/ArgIterator.cs) -- + CrossGen2's managed port of the same logic. diff --git a/cdac-calling-conventions/amd64-unix.md b/cdac-calling-conventions/amd64-unix.md new file mode 100644 index 00000000000000..e7c12f2a859743 --- /dev/null +++ b/cdac-calling-conventions/amd64-unix.md @@ -0,0 +1,161 @@ +# AMD64 Unix (System V) Managed Calling Convention + +**Iterator:** [`AMD64UnixArgIterator.cs`](../src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/AMD64UnixArgIterator.cs) +**Classifier:** [`SystemVStructClassifier.cs`](../src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/SystemVStructClassifier.cs) +**Applies to:** Linux x64, macOS x64 (managed code on the System V AMD64 ABI). +**Base ABI:** System V AMD64 ABI -- see +[the spec](https://gitlab.com/x86-psABIs/x86-64-ABI) (§3.2.1 registers, +§3.2.3 parameter passing / classification, §3.3 vector types). + +## Register set + +| Use | Registers | +|---|---| +| Integer arg | `RDI, RSI, RDX, RCX, R8, R9` (6 slots) | +| Float arg | `XMM0`-`XMM7` (8 slots) | +| **Bank independence** | The integer and FP banks are tracked **independently** -- a float arg does **not** consume an int slot (unlike Windows x64) | +| Integer return | `RAX` (and `RDX` for the second eightbyte) | +| Float return | `XMM0` (and `XMM1` for the second eightbyte) | +| Volatile | `RAX, RCX, RDX, RSI, RDI, R8-R11, XMM0-XMM15` | +| Non-volatile | `RBX, RBP, R12-R15` | +| Stack slot size | 8 bytes | +| Stack alignment | 16 B at call site | +| Red zone | 128 bytes below `RSP` may be used by leaf functions without explicit allocation | + +## Argument placement rules: the eightbyte classifier + +For value types up to 16 bytes, the System V ABI defines a per-byte +**eightbyte classification** algorithm: + +1. Aggregates > 16 bytes -> passed in memory (on the stack, **by value -- no + hidden pointer**, unlike Windows x64). +2. Aggregates with a misaligned field -> in memory. +3. Otherwise the struct is partitioned into 1 or 2 eightbytes (bytes 0-7, + optionally 8-15), and each eightbyte gets a class: + - `INTEGER` (incl. CLR's `IntegerReference` for object refs and `IntegerByRef` + for managed pointers) + - `SSE` (float / double) + - `NO_CLASS` (padding / empty slot -- in the CLR currently promoted to + `INTEGER`; see TODO in `SystemVStructClassifier`) + - `MEMORY` (forces the whole struct to memory) +4. Merge rules (when two fields share a byte / eightbyte): + - Either side `INTEGER` -> `INTEGER` (INTEGER dominates SSE). + - Both SSE -> `SSE`. + - Either side `MEMORY` -> `MEMORY`. +5. Register assignment per eightbyte: + - `INTEGER`/`IntegerReference`/`IntegerByRef` -> next free `RDI..R9` slot. + - `SSE` -> next free `XMM0..XMM7` slot. +6. **All-or-nothing**: if even one eightbyte can't find its required register + (bank exhausted), the *entire struct* spills to the stack and no registers + are consumed by it. + +Examples: + +| Struct | Eightbytes | Classes | Placement | +|---|---|---|---| +| `{ int x; int y; }` (8 B) | 1 | `[INTEGER]` | 1 GP reg (`RDI`) | +| `{ int x; double d; }` (16 B) | 2 | `[INTEGER, SSE]` | 1 GP + 1 FP (`RDI, XMM0`) | +| `{ double a; double b; }` (16 B) | 2 | `[SSE, SSE]` | 2 FP (`XMM0, XMM1`) | +| `{ float a; float b; }` (8 B) | 1 | `[SSE]` (floats packed in low 64 bits of one XMM) | 1 FP (`XMM0`) | +| `{ int x; float f; }` (8 B, both in eightbyte 0) | 1 | `[INTEGER]` (INTEGER dominates) | 1 GP (`RDI`) | +| `{ long a; long b; long c; }` (24 B) | -- | `MEMORY` | Stack by value | + +See [the SysV struct passing research doc](../C:/Users/maxcharlamb/.copilot/session-state/52879186-8fcc-4ed2-9048-3fb6ef3bf6b3/research/can-you-explain-the-sysv-struct-passing-convetion-.md) +for a full deep-dive with code citations. + +## Return values + +| Return shape | Where | +|---|---| +| Integer / pointer / reference | `RAX` | +| `R4` / `R8` | `XMM0` | +| Value type <= 16 B that classifies in registers | Same banks as parameters, in order: eightbyte 0 -> `RAX`/`XMM0`, eightbyte 1 -> `RDX`/`XMM1` (with appropriate fallback if the two eightbytes use different banks) | +| Value type > 16 B or classified `MEMORY` | Caller-allocated return buffer; pointer passed as first hidden arg (`RDI`); callee returns the buffer address in `RAX` | + +## Managed-specific behavior + +### `this` is in `RDI` + +The first user-arg register is `RDI`, so `this` (always the first managed +arg) lands there. If there's a ret buf, ret buf -> `RDI` and `this` -> `RSI`. + +### Hidden argument prefix + +Standard CLR prefix applies. Each hidden arg consumes the next integer slot +(`RDI`, then `RSI`, ...): + +``` +[this:RDI] [retBuf:RSI] [genericContext:RDX] [asyncContinuation:RCX] [varArgCookie:R8] userArgs... +``` + +### Implicit by-reference is NOT used + +Unlike Windows x64, the System V ABI passes large structs **by value on the +stack**, not via a hidden pointer. The cDAC encodes this as: + +```csharp +public override bool IsArgPassedByRefBySize(int size) => false; +protected override bool IsArgPassedByRefArchSpecific() => false; +``` + +This means no GC-byref tracking is needed for value-type parameters on SysV. + +### CLR uses a subset of the ABI's classes + +The spec defines `NO_CLASS, INTEGER, SSE, SSEUP, X87, X87UP, COMPLEX_X87, MEMORY`. +The CLR uses only `NO_CLASS, INTEGER, SSE, MEMORY` plus two extensions +(`IntegerReference`, `IntegerByRef`) that carry GC liveness info. `SSEUP`, +`X87`, `X87UP`, `COMPLEX_X87` are not used: + +- `Vector128/256/512` and `Vector64` bypass the SysV classifier entirely + -- they are handled by the JIT as SIMD intrinsic types in single + XMM/YMM/ZMM registers. +- The CLR has no `long double` / `__float128` / x87 types. + +See the enum in [`SystemVStructDescriptor.cs`](../src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/SystemVStructDescriptor.cs). + +### Empty structs + +Managed structs with zero instance fields are passed by value on the stack +(matching the broader "explicit-layout / unusual struct -> stack" rule called +out in [`clr-abi.md:569`](../docs/design/coreclr/botr/clr-abi.md)). + +### Frame pointer + +System V x64 managed frames **always allocate a frame pointer** (RBP), since +CoreCLR PR [dotnet/coreclr#4019](https://github.com/dotnet/coreclr/pull/4019). +This makes stack walking via frame chains viable, unlike Windows x64 where +unwinding goes through PDATA/XDATA. + +### Funclets + +Same managed-EH model as other platforms. The catch funclet receives the +exception object in `RSI` (vs. `RCX` on Windows x64). + +### Known TODO: NoClass eightbytes + +The classifier currently promotes `NoClass` eightbytes (pure padding slots) +to `Integer` because the JIT mishandles `NoClass` (see TODO at +`SystemVStructClassifier.cs:439` and the mirrored TODO at +`src/coreclr/vm/methodtable.cpp:2660`). This is a known divergence from the +ABI spec; a small minority of structs may waste a GP register on a padding +slot as a result. + +## TypedReference + +`TypedReference = { ref byte _value; IntPtr _type; }` = 16 bytes. +Classification: `[IntegerByRef, Integer]` -> passed in **2 GP registers** +(typically `RDI, RSI`). Returned in `RAX, RDX`. + +The cDAC's `ArgTypeInfoSignatureProvider` substitutes the `g_TypedReferenceMT` +MethodTable when the signature contains `ELEMENT_TYPE_TYPEDBYREF`, so the +classifier walks its layout as if it were an ordinary 16-byte value type. + +## References + +- [System V AMD64 ABI spec](https://gitlab.com/x86-psABIs/x86-64-ABI) -- §3.2.3 classification & passing. +- [docs/design/coreclr/botr/clr-abi.md - "System V x86_64 support"](../docs/design/coreclr/botr/clr-abi.md) -- CLR deviations. +- [docs/design/coreclr/jit/struct-abi.md](../docs/design/coreclr/jit/struct-abi.md) -- struct-passing design notes. +- [src/coreclr/vm/callingconvention.h](../src/coreclr/vm/callingconvention.h) -- `UNIX_AMD64_ABI` branches. +- [src/coreclr/vm/methodtable.cpp](../src/coreclr/vm/methodtable.cpp) -- `ClassifyEightBytesWithManagedLayout` (the C++ classifier). +- [src/coreclr/tools/Common/JitInterface/SystemVStructClassificator.cs](../src/coreclr/tools/Common/JitInterface/SystemVStructClassificator.cs) -- CrossGen2's managed mirror. diff --git a/cdac-calling-conventions/amd64-windows.md b/cdac-calling-conventions/amd64-windows.md new file mode 100644 index 00000000000000..a1a3d4131c4114 --- /dev/null +++ b/cdac-calling-conventions/amd64-windows.md @@ -0,0 +1,156 @@ +# AMD64 Windows Managed Calling Convention + +**Iterator:** [`AMD64WindowsArgIterator.cs`](../src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/AMD64WindowsArgIterator.cs) +**Applies to:** Windows x64 (managed code on the Microsoft x64 ABI). +**Base ABI:** Microsoft x64 calling convention -- see +[x64 Software Conventions](https://learn.microsoft.com/cpp/build/x64-software-conventions). + +## Register set + +| Use | Registers | +|---|---| +| Integer arg | `RCX, RDX, R8, R9` (**4 slots, shared with FP slots by position**) | +| Float arg | `XMM0, XMM1, XMM2, XMM3` (**same 4 slot positions** -- each arg consumes either an int or an FP register at its slot index, not both) | +| Integer return | `RAX` | +| Float return | `XMM0` | +| Volatile | `RAX, RCX, RDX, R8-R11, XMM0-XMM5` | +| Non-volatile | `RBX, RBP, RDI, RSI, R12-R15, XMM6-XMM15` | +| Stack slot size | 8 bytes | +| Shadow space | 32 bytes immediately above the return address (caller reserves homes for the 4 register args) | +| Stack alignment | 16 B at call site | + +## Argument placement rules + +**Every argument consumes exactly one 8-byte slot** -- there is no splitting, +no multi-register passing, no eightbyte classification. + +- The first 4 slots (positions 0-3) are passed in registers; the choice + between `RCX..R9` and `XMM0..3` depends on the arg's type: + - `R4` / `R8` (float, double) at position N -> `XMM`. + - Anything else at position N -> integer register `RCX/RDX/R8/R9` for N=0..3. +- Slot 4+ goes on the stack at `[RSP + 32 + (N - 4) * 8]` (above shadow space). +- A value type whose size is **not in {1, 2, 4, 8} bytes** (i.e. 3, 5, 6, 7, + or >= 9) is passed by an **implicit hidden pointer** rather than by value. + The caller materializes the struct (usually on its own stack) and passes a + pointer. + +In cDAC this is encoded as: + +```csharp +EnregisteredParamTypeMaxSize = 8; +IsArgPassedByRefBySize(size) = size > 8 || !IsPow2(size); +``` + +## Return values + +| Return shape | Where | +|---|---| +| Integer / pointer / reference / value type with size in {1, 2, 4, 8} | `RAX` | +| `R4` / `R8` | `XMM0` | +| Value type not in {1, 2, 4, 8} (incl. `TypedByRef` = 16 B), or non-power-of-2 size | Caller-allocated return buffer; pointer passed as first hidden arg; callee returns the buffer address in `RAX` | + +## Managed-specific behavior + +### `this` is always in `RCX` + +Native C++ on Microsoft x64 pushes the ret buf into `RCX` and bumps `this` to +`RDX` when the function returns a large struct. **Managed code always uses +`RCX` for `this`** and `RDX` for the ret buf, regardless of whether a ret buf +is present. (This wasn't always the case -- up to .NET Framework 4.5 the +managed convention matched native; it changed for consistency with other +managed platforms.) + +The cDAC inherits this from `ArgIteratorBase.GetRetBuffArgOffset`, which +returns `argumentRegistersOffset + (hasThis ? PointerSize : 0)`. + +### Hidden argument prefix + +The CLR's standard prefix order applies: + +``` +[this:RCX] [retBuf:RDX] [genericContext] [asyncContinuation] [varArgCookie] userArgs... +``` + +Each takes the next available register slot in `RCX..R9`, then spills to the +stack. So a method with `this` + ret buf + generic context + 1 user arg lays +out as `RCX=this, RDX=retBuf, R8=genericContext, R9=userArg0`. + +The vararg cookie, async continuation, and generic context don't exist in +native; the cDAC counts them in `ArgIteratorBase.ComputeInitialNumRegistersUsed`. + +### Varargs + +Managed varargs (`IMAGE_CEE_CS_CALLCONV_VARARG`) follow the Microsoft +"duplicate-into-int-reg" rule for FP args: any `R4`/`R8` argument in the first +4 slots is duplicated into the matching `RCX..R9` slot as well as `XMM0..3`. +The cDAC iterator's per-arg logic is the same for fixed and variadic methods +-- the duplication is handled by the JIT, not represented in the iteration +output. + +### Implicit by-reference: GC-tracked pointers + +Unlike native, the implicit-byref pointer may legitimately point into the GC +heap (reflection/remoting paths), so the JIT: + +- Reports the implicit-byref parameter as a GC `BYREF` (interior pointer). +- Uses checked write barriers for stores through the pointer. + +### Empty structs go on the stack + +A managed struct with **zero instance fields** is passed by value on the stack +(never in a register), regardless of its declared size. Native C++ has no +equivalent since `sizeof(EmptyStruct) >= 1`. + +### Frame pointer + +Unlike System V x64 (which always uses RBP since CoreCLR PR +[dotnet/coreclr#4019](https://github.com/dotnet/coreclr/pull/4019)) and ARM/ARM64 +(which require a frame pointer), **Windows x64 typically omits the frame +pointer**. Unwinding uses PDATA/XDATA records, not frame chaining. The JIT +allocates RBP only when the function genuinely needs one (e.g., funclets, +`alloca`). + +### Funclets + +Catch / finally / filter handlers are emitted as separate functions +(*funclets*) with their own PDATA entries, looking to the OS like first-class +functions. The catch funclet receives the `System.Exception` reference in +`RCX`. This is a CLR construct; native SEH passes an `EXCEPTION_RECORD*`. + +### Secret VM-to-JIT register conventions + +Several "secret" registers carry runtime data for special call shapes: + +| Use | Register | +|---|---| +| Virtual stub dispatch (VSD) | `R11` (stub indirection cell) | +| `calli` P/Invoke target | `R10` | +| `calli` P/Invoke signature cookie | `R11` | +| Normal P/Invoke MethodDesc param | `R10` | + +`R10` and `R11` are volatile in the Microsoft x64 ABI and unused as argument +registers, which makes them safe choices. + +### Small primitives are zero/sign-extended + +Native Microsoft x64 leaves the upper bits of small return values +**undefined**. Managed code defines them: signed small types (`sbyte`, +`short`) are sign-extended to 32/64 bits; unsigned (`byte`, `ushort`, `bool`) +are zero-extended. The JIT relies on this when reading values back at call +sites. + +## TypedReference + +`TypedReference` is 16 bytes. Since 16 is not in {1, 2, 4, 8}, the +implicit-byref rule applies: + +- **As a parameter**: passed by hidden pointer; the slot in `RCX..R9` (or on + the stack) holds a pointer to a `TypedReference` value the caller has + materialized. +- **As a return value**: triggers a return buffer. + +## References + +- [docs/design/coreclr/botr/clr-abi.md - Special parameters](../docs/design/coreclr/botr/clr-abi.md) -- `this`, generics, varargs, async continuation. +- [src/coreclr/vm/callingconvention.h](../src/coreclr/vm/callingconvention.h) -- search for `TARGET_AMD64` and `UNIX_AMD64_ABI` to find the Windows-vs-Unix split (Windows is the `#else` arm). +- [Microsoft x64 calling convention](https://learn.microsoft.com/cpp/build/x64-calling-convention) -- base ABI documentation. diff --git a/cdac-calling-conventions/arm32.md b/cdac-calling-conventions/arm32.md new file mode 100644 index 00000000000000..5f64fcb4ba3276 --- /dev/null +++ b/cdac-calling-conventions/arm32.md @@ -0,0 +1,135 @@ +# ARM32 (AAPCS) Managed Calling Convention + +**Iterator:** [`Arm32ArgIterator.cs`](../src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/Arm32ArgIterator.cs) +**Applies to:** Linux armhf (hard-float), Windows on ARM (32-bit). Linux armel +(soft-float) is not yet implemented in cDAC -- see TODO on the iterator class. +**Base ABI:** ARM AAPCS / AAPCS-VFP -- see +[Procedure Call Standard for the ARM Architecture](https://github.com/ARM-software/abi-aa/blob/main/aapcs32/aapcs32.rst). + +## Register set + +| Use | Registers | +|---|---| +| Integer arg | `R0, R1, R2, R3` (4 slots) | +| Float arg (hard-float) | `S0`-`S15` (16 single-precision slots, or `D0`-`D7` paired as 8 double slots) | +| Integer return | `R0` (and `R1` for 64-bit) | +| Float return | `S0` (`R4`) / `D0` (`R8`) | +| Volatile | `R0-R3, R12, S0-S15`/`D0-D7` | +| Non-volatile | `R4-R11, LR, S16-S31`/`D8-D15` | +| Stack slot size | 4 bytes | +| Stack alignment | 8 B at call site (16 B at function entry on some targets) | + +## Argument placement rules + +### Integer / pointer / reference args + +- Scanned left-to-right. +- 32-bit args take the next free `R0..R3`, then spill to stack 4-byte slots. +- **64-bit args (`I8`, `U8`, `R8`) require 8-byte alignment** in both the + register file and the stack: + - If the next free register is odd-numbered (`R1` or `R3`), it's skipped + and the 64-bit value goes in the next aligned pair (`R2:R3` or `[stack + + 8]`). + - Stack offsets for 64-bit args are aligned up to 8 bytes. +- **Split between regs and stack**: a 64-bit arg that starts in `R3` is + passed half in `R3` and half on the stack (the "co-processor register + split" rule). This is the *only* case where a single arg spans the boundary + on ARM32. + +### Float / double / HFA args (hard-float path only) + +The hard-float (AAPCS-VFP) ABI uses a **bitmap allocator** over `S0..S15` so +that floats and doubles can interleave with gaps: + +- `R4` (float) takes one S-register slot; `R8` (double) takes one D-register + slot = 2 S-register slots. +- **HFAs** (Homogeneous Floating-point Aggregates -- structs of 1-4 identical + floats or doubles) are placed in consecutive S/D slots. +- If the bitmap can't fit the FP arg, all subsequent FP args go on the stack + (the FP bank is marked exhausted: `_wFPRegs = 0xffff`). + +The bitmap walk lives in `Arm32ArgIterator.GetNextOffsetForArg`, lines 79-103. + +### Varargs + +For variadic methods, **the FP register path is skipped entirely** -- all +args (including floats / doubles / HFAs) go through the integer/stack path. +This is checked via `!IsVarArg` in the FP allocation guard. + +## Return values + +| Return shape | Where | +|---|---| +| Integer / pointer / reference / 32-bit value type | `R0` | +| `I8` / `U8` | `R0:R1` (low in `R0`, high in `R1`) | +| `R4` / `R8` | `S0` / `D0` (or `R0` / `R0:R1` under softfp; not yet handled) | +| HFA (1-4 floats or doubles, hard-float) | `S0..S3` / `D0..D3` | +| Other value types | Caller-allocated return buffer; pointer passed as a hidden first arg in `R0` (or `R1` if `this` is present); callee uses the buffer | + +## Managed-specific behavior + +### Hidden argument prefix + +Standard CLR prefix applies. Each hidden arg consumes the next integer slot: + +``` +[this:R0] [retBuf:R1] [genericContext:R2] [asyncContinuation:R3] [varArgCookie] userArgs... +``` + +If there's no `this`, the ret buf takes `R0`. + +### Implicit by-reference: not used + +ARM32 sets `EnregisteredParamTypeMaxSize = 0`, meaning the iterator does not +apply an implicit-byref transformation. Value types are passed by value +according to the rules above. + +### HFA detection comes from `ArgTypeInfo` + +The iterator consults `_argTypeHandle.IsHomogeneousAggregate` and +`_argTypeHandle.RequiresAlign8` (computed by `ArgTypeInfo.FromTypeHandle` +based on `IRuntimeTypeSystem.IsHFA` and `RequiresAlign8`). On ARM32 the HFA +element size is determined entirely by alignment: 8-byte alignment -> double +HFA; 4-byte alignment -> float HFA (see `ArgTypeInfo.ComputeHfaElementSize`). + +### 64-bit alignment tracking + +The iterator records `_requires64BitAlignment` per arg so that downstream +consumers (e.g. SOS, ClrMD) can correctly compute frame offsets even when +register skipping occurs (e.g. an `I8` in `R2:R3` after a single-slot arg in +`R1`). + +### Frame pointer + +ARM/ARM64 always allocate a frame pointer (`R11` on ARM32) for both managed +frames and to support the InlinedCallFrame mechanism for P/Invokes +([`clr-abi.md:172`](../docs/design/coreclr/botr/clr-abi.md)). + +### Funclets + +Same managed-EH funclet model as other platforms. The catch funclet receives +the exception object in `R0`. + +### Softfp (armel) not yet supported + +The Linux armel calling convention uses integer registers for all args +including floats (no S/D registers used for argument passing). The cDAC +iterator currently hard-codes `IsArmhfABI = true`; a TODO on the class flags +the need to detect armel and disable the FP-register path. + +## TypedReference + +`TypedReference` is 16 bytes. On ARM32 it does not enregister (value types +generally don't get split across registers on ARM32; the iterator routes +them through the integer slots and stack). A `TypedReference` parameter +consumes 4 pointer-sized slots (16 bytes total) starting at the next 8-byte +alignment boundary. The current cDAC handling depends on the substitution +applied by `ArgTypeInfoSignatureProvider`; refer to the iterator's per-arg +logic for the concrete placement. + +## References + +- [AAPCS32](https://github.com/ARM-software/abi-aa/blob/main/aapcs32/aapcs32.rst) -- ARM 32-bit ABI. +- [Overview of ARM32 ABI Conventions (MSDN)](https://learn.microsoft.com/cpp/build/overview-of-arm-abi-conventions) -- Windows on ARM specifics. +- [src/coreclr/vm/callingconvention.h](../src/coreclr/vm/callingconvention.h) -- `TARGET_ARM` branches (search for `#elif defined(TARGET_ARM)`). +- [docs/design/coreclr/botr/clr-abi.md](../docs/design/coreclr/botr/clr-abi.md) -- ARM-specific notes throughout. diff --git a/cdac-calling-conventions/arm64.md b/cdac-calling-conventions/arm64.md new file mode 100644 index 00000000000000..43337927516667 --- /dev/null +++ b/cdac-calling-conventions/arm64.md @@ -0,0 +1,182 @@ +# ARM64 (AAPCS64) Managed Calling Convention + +**Iterator:** [`Arm64ArgIterator.cs`](../src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/Arm64ArgIterator.cs) +**Applies to:** Linux ARM64, Windows on ARM64, Apple (macOS/iOS) ARM64. +**Base ABI:** AAPCS64 -- see [Procedure Call Standard for the Arm 64-bit Architecture](https://github.com/ARM-software/abi-aa/blob/main/aapcs64/aapcs64.rst). + +## Register set + +| Use | Registers | +|---|---| +| Integer arg | `X0`-`X7` (8 slots) | +| Float / SIMD arg | `V0`-`V7` (8 slots, 16 bytes each) | +| Bank independence | The integer and FP banks are tracked **independently** (like SysV x64, unlike Windows x64) | +| Indirect result location | `X8` (return buffer pointer; **separate from arg regs**) | +| Integer return | `X0` (and `X1` for 16-byte structs) | +| Float return | `V0` (and `V1..V3` for HFAs) | +| Volatile | `X0-X17, V0-V7, V16-V31` | +| Non-volatile | `X19-X28, X29 (FP), X30 (LR), V8-V15` | +| Stack slot size | 8 bytes (4 bytes on Apple for natural-alignment packing) | +| Stack alignment | 16 B at call site | + +## Argument placement rules + +### Integer / pointer / reference args + +- An arg of size <= 8 takes the next free `X0..X7` slot. +- An arg of size 9-16 takes two consecutive integer registers (a "consecutive + pair"). +- If both halves don't fit in the remaining int registers: + - **Linux/Apple**: the entire arg goes on the stack; no registers are + consumed. + - **Windows**: special split rule -- the head goes into the remaining X + register(s) and the tail goes on the stack. This is **only** an issue for + variadic methods on Windows. (See "Windows varargs" below; not yet + implemented in cDAC.) +- Anything larger than 16 bytes is passed via **implicit by-reference**: the + caller materializes the value and passes a pointer in the next int slot. + +### Float / double / HFA args + +- `R4` (float) / `R8` (double) takes one V register slot at 16 bytes per slot + (low bits of `Vn`). +- **HFAs** (1-4 floats or doubles or vectors with all identical element + type) get spread across consecutive `V` registers, each in its own slot. + E.g. a `Vector4` (4 floats) takes `V0..V3`. +- If the HFA doesn't fit in remaining V slots, it goes on the stack (no V + registers consumed). + +The check is: + +```csharp +if (cFPRegs > 0 && !IsVarArg) { ... try V regs ... } +``` + +### Varargs + +Variadic methods diverge from the fixed-arg rules: + +- **All variadic args go through the X-register / stack path**, not V regs. + `R4`/`R8` arguments are widened to 64 bits and placed in `X0..X7` or on the + stack. The cDAC encodes this as the `!IsVarArg` guard at the FP branch. +- **HFAs lose their HFA-ness**: a homogeneous float aggregate is treated as + an ordinary composite for variadic calls. cDAC implements this by also + forcing the implicit-byref path for >16-byte HFAs under varargs: + ```csharp + protected override bool IsArgPassedByRefArchSpecific() + => _argType == CorElementType.ValueType + && _argSize > EnregisteredParamTypeMaxSize + && (!_argTypeHandle.IsHomogeneousAggregate || IsVarArg); + ``` +- **Apple ARM64**: variadic args go *entirely on the stack* (Linux/AAPCS64 + starts on stack only after `X7` is filled; Apple skips registers + altogether). Not yet specifically handled in cDAC -- the iterator only + applies Apple's natural-alignment stack-packing rule, not the + "all-on-stack" varargs rule. +- **Windows ARM64**: a variadic arg whose start fits in a remaining `X` + register but whose tail spills past `X7` is **split** between regs and + stack (the first 64 bytes of the stack are loaded into `X0..X7` and the + rest is contiguous). The CoreCLR VM has this code at + [`callingconvention.h:1740-1756`](../src/coreclr/vm/callingconvention.h); + the cDAC iterator does **not** yet implement it (known gap). + +### Apple ARM64 stack packing + +On Apple ARM64 (Darwin), stack arguments use **natural alignment** (smaller +than 8 bytes) rather than the AAPCS64 8-byte slot. The cDAC handles this in +`StackElemSize` and the per-arg alignment computation: + +```csharp +if (_isAppleArm64ABI) { + int alignment = isValueType ? (isFloatHFA ? 4 : 8) : cbArg; + _ofsStack = AlignUp(_ofsStack, alignment); +} +``` + +## Return values + +| Return shape | Where | +|---|---| +| Integer / pointer / reference / size <= 8 value type | `X0` | +| 16-byte value type | `X0, X1` | +| `R4` / `R8` | `V0` (full SIMD reg) | +| HFA (up to 4 floats/doubles) | `V0..V3` | +| Value type > 16 bytes (and not an HFA) | Caller passes an indirect result pointer in `X8`; callee writes through it; `X8` is **separate** from the regular arg registers | + +## Managed-specific behavior + +### Return buffer is in `X8` + +Unlike the other platforms where the ret buf consumes an argument register +slot, ARM64's AAPCS64 reserves `X8` (the "Indirect Result Location Register") +specifically for the return-buffer pointer. This means **the ret buf does +*not* consume an X0..X7 slot**, and `this` lands in `X0` even when a ret buf +is present. + +The cDAC reflects this with: + +```csharp +public override bool IsRetBuffPassedAsFirstArg => false; +public override int GetRetBuffArgOffset(bool hasThis) => (int)_layout.FirstGCRefMapSlot; +``` + +### Hidden argument prefix + +Standard CLR prefix applies, but the ret buf goes in `X8` rather than `X0`: + +``` +X8 = retBuf (separate reg) +[this:X0] [genericContext:X1] [asyncContinuation:X2] [varArgCookie:X3] userArgs... +``` + +### Implicit by-reference for large value types + +`EnregisteredParamTypeMaxSize = 16`. Value types larger than 16 bytes that +are *not* HFAs go via implicit by-reference; the caller may legitimately +point into the GC heap, so the JIT reports the pointer as a GC `BYREF` and +uses checked write barriers. + +HFAs that are also > 16 bytes (e.g. 4 doubles = 32 bytes) are passed in V +registers when *not* varargs, and by implicit-byref when varargs. See the +`IsArgPassedByRefArchSpecific` override above. + +### Frame pointer + +ARM64 always allocates a frame pointer (`X29`), partly for AAPCS64 frame +chaining and partly for the InlinedCallFrame P/Invoke mechanism. Funclets +share the parent function's `X29` to access its locals. + +### Funclets + +Same managed-EH funclet model. The catch funclet receives the exception +object in `X0`. + +## TypedReference + +`TypedReference` is 16 bytes. It is passed in **2 GP registers** -- typically +`X0, X1` -- since 16 <= `EnregisteredParamTypeMaxSize`. It is *not* an HFA so +the FP branch doesn't apply. Returned in `X0, X1`. + +The cDAC's `ArgTypeInfoSignatureProvider` substitutes the `g_TypedReferenceMT` +MethodTable when the signature contains `ELEMENT_TYPE_TYPEDBYREF`, so the +iterator treats it as an ordinary 16-byte value type. + +## Known gaps in cDAC + +The iterator's correctness is high for fixed-arg calls but has known holes: + +1. **Windows ARM64 varargs split** (the `X7 -> stack` boundary case) is not + implemented. CoreCLR has this at `callingconvention.h:1740-1756`. +2. **Apple ARM64 varargs** ("all variadic args on stack") is not specifically + handled; only the stack-packing portion is. +3. Tests for these gaps are present but marked `[Skip("audit gap")]`: + `Windows_VarArgs_StructSpansX7AndStack_AuditGap4`, + `HFA_FourFloats_ShouldReportFourFPSlots`. + +## References + +- [AAPCS64](https://github.com/ARM-software/abi-aa/blob/main/aapcs64/aapcs64.rst) -- §6.4 parameter passing, §6.8 variadic functions. +- [Apple ARM64 documentation](https://developer.apple.com/documentation/xcode/writing-arm64-code-for-apple-platforms) -- the deviations from AAPCS64. +- [Microsoft ARM64 ABI](https://learn.microsoft.com/cpp/build/arm64-windows-abi-conventions) -- Windows-specific varargs split. +- [src/coreclr/vm/callingconvention.h](../src/coreclr/vm/callingconvention.h) -- `TARGET_ARM64` branches (especially the varargs handling around lines 1700-1760). +- [docs/design/coreclr/botr/clr-abi.md](../docs/design/coreclr/botr/clr-abi.md) -- CLR-wide ABI notes. diff --git a/cdac-calling-conventions/x86.md b/cdac-calling-conventions/x86.md new file mode 100644 index 00000000000000..a61619732a2ebe --- /dev/null +++ b/cdac-calling-conventions/x86.md @@ -0,0 +1,103 @@ +# x86 (32-bit) Managed Calling Convention + +**Iterator:** [`X86ArgIterator.cs`](../src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/X86ArgIterator.cs) +**Applies to:** Windows x86, Linux x86 (32-bit). The same convention is used on +both OSes for managed code. +**Base ABI:** CLR-specific (loosely related to Microsoft `__fastcall`); not +documented as a platform standard. + +## Register set + +| Use | Registers | +|---|---| +| Integer arg | `ECX`, `EDX` (2 slots, **filled in declaration order, left-to-right**) | +| Float arg | -- (floats and doubles **never** go in registers; the FPU stack / XMM regs are not used for args) | +| Integer return | `EAX` (`EDX:EAX` for 64-bit) | +| Float return | `ST(0)` (x87 stack top) | +| Volatile (caller-saved) | `EAX, ECX, EDX` | +| Non-volatile (callee-saved) | `EBX, ESI, EDI, EBP` | +| Stack slot size | 4 bytes | + +## Argument placement rules + +The iterator scans arguments **left-to-right** and assigns each to either a +register slot or the stack: + +- An argument may be passed in a register when: + - There is still an unused register slot (`ECX` first, then `EDX`), **and** + - The argument is a pointer-sized primitive, GC reference, byref, or + array/pointer (always enregisters), **or** it is a value type of size + exactly **1, 2, or 4 bytes**. +- Otherwise the argument is pushed on the **stack at decreasing addresses** + (right-to-left in memory). Each stack arg is rounded up to a 4-byte slot. +- `R4` (float), `R8` (double), `I8`/`U8`, and `TypedByRef` **never** enregister + on x86 -- they always go on the stack. + +The eligibility check is in [`X86ArgIterator.IsArgumentInRegister`](../src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/X86ArgIterator.cs). + +## Return values + +| Return shape | Where | +|---|---| +| Integer / pointer / reference / `I4` and smaller value types | `EAX` | +| `I8` / `U8` | `EDX:EAX` (high in `EDX`, low in `EAX`) | +| `R4` / `R8` | `ST(0)` (x87 stack top) | +| Value type > 4 bytes (or 3 bytes), or `TypedByRef` | Caller-allocated return buffer; pointer to it is passed as a hidden first arg (after `this` if any); callee returns the buffer pointer in `EAX` | + +## Managed-specific behavior + +### Hidden argument placement is order-sensitive + +Unlike the 64-bit platforms where the hidden-arg slots are always counted up +front in `ComputeInitialNumRegistersUsed`, x86 places the generic context, the +async continuation, and the vararg cookie **after** the fixed args, and their +final location (`ECX`, `EDX`, or stack) depends on a full sig walk. The +iterator does that sig walk in `ComputeSizeOfArgStack` and records the +locations in `_paramTypeLoc` / `_asyncContinuationLoc`. + +``` +[this] [retBuf] userArgs... [asyncContinuation] [genericContext] +``` + +`this` and `retBuf` *do* take register slots first (see +`ComputeInitialNumRegistersUsed`). + +### `this` is in `ECX`; ret buf is in `ECX` or `EDX` + +If the method has a return buffer: + +- Without `this`: ret buf goes in `ECX` (first slot). +- With `this`: `this` -> `ECX`, ret buf -> `EDX`. + +See `GetRetBuffArgOffset`. + +### Varargs + +For `__arglist` (CLR managed varargs) methods, the cookie is **at the bottom +of the stack frame** (returned by `GetVASigCookieOffset` as +`SizeOfTransitionBlock`), and `_numRegistersUsed` is forced to +`NumArgumentRegisters` so all user args go on the stack. No register +allocation happens -- this matches the runtime's expectation that the callee +can walk varargs by pointer arithmetic. + +### Callee cleans the stack (stdcall-like) + +For non-vararg methods, the callee pops its arguments before returning +(`CbStackPop()` returns the stack arg size). Vararg methods follow `__cdecl` +and require the caller to clean up (`CbStackPop()` returns 0). + +### Implicit by-reference + +x86 does **not** use the implicit-byref mechanism (`EnregisteredParamTypeMaxSize = 0`). +Large value types are passed by value on the stack directly. + +## TypedReference + +`TypedByRef` never enregisters on x86 (see `IsArgumentInRegister` -- it falls +through the `default` case). It is passed by value on the stack as two +pointer-sized slots (`{ ref byte; IntPtr } = { void*; void* }`). + +## References + +- [docs/design/coreclr/botr/clr-abi.md - x86 ABI](../docs/design/coreclr/botr/clr-abi.md) -- search for "x86" sections. +- [src/coreclr/vm/callingconvention.h](../src/coreclr/vm/callingconvention.h) -- `TARGET_X86` branches (search for `#ifdef TARGET_X86`). diff --git a/src/coreclr/vm/class.h b/src/coreclr/vm/class.h index bc5f2df4753b5a..5d324262e4d61d 100644 --- a/src/coreclr/vm/class.h +++ b/src/coreclr/vm/class.h @@ -1784,6 +1784,7 @@ template<> struct cdac_data static constexpr size_t NumStaticFields = offsetof(EEClass, m_NumStaticFields); static constexpr size_t NumThreadStaticFields = offsetof(EEClass, m_NumThreadStaticFields); static constexpr size_t NumNonVirtualSlots = offsetof(EEClass, m_NumNonVirtualSlots); + static constexpr size_t BaseSizePadding = offsetof(EEClass, m_cbBaseSizePadding); }; // -------------------------------------------------------------------------------------------- diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index e5c8a863c4f5cf..415907525491f3 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -465,6 +465,7 @@ CDAC_TYPE_FIELD(EEClass, T_UINT16, NumInstanceFields, cdac_data::NumIns CDAC_TYPE_FIELD(EEClass, T_UINT16, NumStaticFields, cdac_data::NumStaticFields) CDAC_TYPE_FIELD(EEClass, T_UINT16, NumThreadStaticFields, cdac_data::NumThreadStaticFields) CDAC_TYPE_FIELD(EEClass, T_UINT16, NumNonVirtualSlots, cdac_data::NumNonVirtualSlots) +CDAC_TYPE_FIELD(EEClass, T_UINT8, BaseSizePadding, cdac_data::BaseSizePadding) CDAC_TYPE_END(EEClass) CDAC_TYPE_BEGIN(GenericsDictInfo) @@ -1047,14 +1048,27 @@ CDAC_TYPE_FIELD(TransitionBlock, T_UINT32, OffsetOfArgs, sizeof(TransitionBlock) // Offset to argument registers and first GCRefMap slot (platform-specific) #if (defined(TARGET_AMD64) && !defined(UNIX_AMD64_ABI)) || defined(TARGET_WASM) CDAC_TYPE_FIELD(TransitionBlock, T_UINT32, ArgumentRegisters, sizeof(TransitionBlock)) +CDAC_TYPE_FIELD(TransitionBlock, T_UINT32, ArgumentRegistersOffset, sizeof(TransitionBlock)) CDAC_TYPE_FIELD(TransitionBlock, T_UINT32, FirstGCRefMapSlot, sizeof(TransitionBlock)) #elif defined(TARGET_ARM64) CDAC_TYPE_FIELD(TransitionBlock, T_UINT32, ArgumentRegisters, offsetof(TransitionBlock, m_argumentRegisters)) +CDAC_TYPE_FIELD(TransitionBlock, T_UINT32, ArgumentRegistersOffset, offsetof(TransitionBlock, m_argumentRegisters)) CDAC_TYPE_FIELD(TransitionBlock, T_UINT32, FirstGCRefMapSlot, offsetof(TransitionBlock, m_x8RetBuffReg)) #else CDAC_TYPE_FIELD(TransitionBlock, T_UINT32, ArgumentRegisters, offsetof(TransitionBlock, m_argumentRegisters)) +CDAC_TYPE_FIELD(TransitionBlock, T_UINT32, ArgumentRegistersOffset, offsetof(TransitionBlock, m_argumentRegisters)) CDAC_TYPE_FIELD(TransitionBlock, T_UINT32, FirstGCRefMapSlot, offsetof(TransitionBlock, m_argumentRegisters)) #endif + +// Negative offset to where float argument registers are saved (relative to TransitionBlock pointer). +// This is -sizeof(FloatArgumentRegisters) (-padding) on platforms that have them, 0 otherwise. +#ifdef CALLDESCR_FPARGREGS +#ifdef TARGET_ARM +// ARM has 8-byte alignment padding after FloatArgumentRegisters +CDAC_TYPE_FIELD(TransitionBlock, T_INT32, OffsetOfFloatArgumentRegisters, -(int)(sizeof(FloatArgumentRegisters) + TARGET_POINTER_SIZE)) +#else +CDAC_TYPE_FIELD(TransitionBlock, T_INT32, OffsetOfFloatArgumentRegisters, -(int)sizeof(FloatArgumentRegisters)) +#endif CDAC_TYPE_END(TransitionBlock) #ifdef DEBUGGING_SUPPORTED @@ -1505,6 +1519,14 @@ CDAC_GLOBAL(FeatureWebcil, T_UINT8, 1) #else CDAC_GLOBAL(FeatureWebcil, T_UINT8, 0) #endif +#ifdef FEATURE_HFA +CDAC_GLOBAL(FeatureHFA, T_UINT8, 1) +#else +CDAC_GLOBAL(FeatureHFA, T_UINT8, 0) +#endif +#ifdef ARM_SOFTFP +CDAC_GLOBAL(FeatureArmSoftFP, T_UINT8, 1) +#endif // See Object::GetGCSafeMethodTable #ifdef TARGET_64BIT CDAC_GLOBAL(ObjectToMethodTableUnmask, T_UINT8, 1 | 1 << 1 | 1 << 2) @@ -1543,6 +1565,7 @@ CDAC_GLOBAL_POINTER(FreeObjectMethodTable, &::g_pFreeObjectMethodTable) CDAC_GLOBAL_POINTER(ObjectMethodTable, &::g_pObjectClass) CDAC_GLOBAL_POINTER(ObjectArrayMethodTable, &::g_pPredefinedArrayTypes[ELEMENT_TYPE_OBJECT]) CDAC_GLOBAL_POINTER(StringMethodTable, &::g_pStringClass) +CDAC_GLOBAL_POINTER(TypedReferenceMethodTable, &::g_TypedReferenceMT) CDAC_GLOBAL_POINTER(SyncTableEntries, &::g_pSyncTable) CDAC_GLOBAL_POINTER(MiniMetaDataBuffAddress, &::g_MiniMetaDataBuffAddress) CDAC_GLOBAL_POINTER(MiniMetaDataBuffMaxSize, &::g_MiniMetaDataBuffMaxSize) @@ -1612,6 +1635,7 @@ CDAC_GLOBAL_CONTRACT(AuxiliarySymbols, c1) #if FEATURE_COMINTEROP CDAC_GLOBAL_CONTRACT(BuiltInCOM, c1) #endif // FEATURE_COMINTEROP +CDAC_GLOBAL_CONTRACT(CallingConvention, c1) CDAC_GLOBAL_CONTRACT(CodeVersions, c1) CDAC_GLOBAL_CONTRACT(CodeNotifications, c1) #ifdef FEATURE_COMWRAPPERS diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ContractRegistry.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ContractRegistry.cs index 17cfcd1000ee19..0fd09c7d8f4120 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ContractRegistry.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ContractRegistry.cs @@ -124,6 +124,10 @@ public abstract class ContractRegistry /// Gets an instance of the Debugger contract for the target. /// public virtual IDebugger Debugger => GetContract(); + /// + /// Gets an instance of the CallingConvention contract for the target. + /// + public virtual ICallingConvention CallingConvention => GetContract(); /// /// Attempts to get an instance of the requested contract for the target. diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ICallingConvention.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ICallingConvention.cs new file mode 100644 index 00000000000000..54594258810f92 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ICallingConvention.cs @@ -0,0 +1,100 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts; + +/// +/// Describes a single register or stack slot of an argument at a call site, +/// relative to the start of the transition block. A simple argument has one +/// slot; a split SystemV struct (e.g. struct { object o; double d; }) has +/// multiple slots — one per eightbyte's register or stack location. +/// +/// Byte offset of the slot from the start of the transition block. +/// +/// The that describes the slot's contents (e.g. +/// for a GC ref slot, +/// for a floating-point slot). Callers (e.g. the GC scanner) classify the slot +/// from this. +/// +public readonly record struct ArgSlot( + int Offset, + CorElementType ElementType); + +/// +/// Describes the layout of a single argument at a call site, as imposed by the +/// target's managed calling convention. A simple argument has one +/// ; a split struct has multiple. +/// +/// +/// True if the argument is passed by implicit reference (e.g. a value type larger +/// than the ABI's enregister limit). When true, contains a +/// single slot holding an interior pointer to the value. +/// +/// +/// One or more register/stack slots that together carry the argument's value. +/// Always non-empty. +/// +public readonly record struct ArgLayout( + bool IsPassedByRef, + IReadOnlyList Slots); + +/// +/// Describes the layout of all arguments at a call site, as imposed by the +/// target's managed calling convention. Offsets are byte offsets from the +/// start of the transition block. +/// +/// +/// Byte offset of the this pointer slot if the method is an instance method; +/// for static methods. +/// +/// +/// True if this points at a value-type instance (i.e. the slot contains a +/// managed interior pointer). False for reference-type instance methods. +/// +/// +/// Byte offset of the implicit async-continuation argument slot for async methods; +/// if the method has no async-continuation argument. +/// +/// +/// Byte offset of the vararg-cookie slot for vararg methods; +/// for non-vararg methods. +/// +/// +/// Layout of each fixed argument in declaration order. Empty when the call site +/// cannot be described (e.g. missing signature, decode failure). +/// +public readonly record struct CallSiteLayout( + int? ThisOffset, + bool IsValueTypeThis, + int? AsyncContinuationOffset, + int? VarArgCookieOffset, + IReadOnlyList Arguments); + +/// +/// Computes call-site argument layouts according to the target runtime's +/// managed calling convention. +/// +public interface ICallingConvention : IContract +{ + static string IContract.Name { get; } = nameof(CallingConvention); + + /// + /// Computes the layout of arguments at a call site for the given method. + /// + /// The method whose call site should be described. + /// + /// The call-site layout. Returns a layout with an empty + /// list and null offsets if the method's call site cannot be described + /// (missing signature, decode failure, etc.). + /// + CallSiteLayout ComputeCallSiteLayout(MethodDescHandle method) + => throw new NotImplementedException(); +} + +public readonly struct CallingConvention : ICallingConvention +{ + // Everything throws NotImplementedException +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs index b2f109681dfc27..ef329fca2345bd 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs @@ -119,6 +119,9 @@ public interface IRuntimeTypeSystem : IContract // The component size is only available for strings and arrays. It is the size of the element type of the array, or the size of an ECMA 335 character (2 bytes) uint GetComponentSize(TypeHandle typeHandle) => throw new NotImplementedException(); + // Mirrors native MethodTable::GetNumInstanceFieldBytes: BaseSize - EEClass.BaseSizePadding. + int GetNumInstanceFieldBytes(TypeHandle typeHandle) => throw new NotImplementedException(); + // True if the MethodTable is the sentinel value associated with unallocated space in the managed heap bool IsFreeObjectMethodTable(TypeHandle typeHandle) => throw new NotImplementedException(); // True if the MethodTable is the System.Object MethodTable (g_pObjectClass) @@ -129,6 +132,14 @@ public interface IRuntimeTypeSystem : IContract bool ContainsGCPointers(TypeHandle typeHandle) => throw new NotImplementedException(); // True if the type requires 8-byte alignment on platforms that don't 8-byte align by default (FEATURE_64BIT_ALIGNMENT) bool RequiresAlign8(TypeHandle typeHandle) => throw new NotImplementedException(); + // True if the type is a Homogeneous Floating-point Aggregate (HFA), i.e. a value type whose + // fields are all of the same floating-point or short-vector type. Only meaningful on platforms with HFA (ARM/ARM64); + // returns false on other architectures. + bool IsHFA(TypeHandle typeHandle) => throw new NotImplementedException(); + // Returns the size in bytes of this type if it is a hardware vector type (System.Numerics.Vector`1, + // System.Runtime.Intrinsics.Vector64`1, or System.Runtime.Intrinsics.Vector128`1) with a primitive + // element type, or 0 otherwise. Used by HFA element-size resolution on platforms with HFA. + int GetVectorSize(TypeHandle typeHandle) => throw new NotImplementedException(); // True if the MethodTable represents a continuation type used by the async continuation feature bool IsContinuation(TypeHandle typeHandle) => throw new NotImplementedException(); /// diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs index 808b28a8b44de6..b34f6197d36acb 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs @@ -26,6 +26,8 @@ public static class Globals public const string FeatureOnStackReplacement = nameof(FeatureOnStackReplacement); public const string FeaturePortableEntrypoints = nameof(FeaturePortableEntrypoints); public const string FeatureWebcil = nameof(FeatureWebcil); + public const string FeatureHFA = nameof(FeatureHFA); + public const string FeatureArmSoftFP = nameof(FeatureArmSoftFP); public const string ObjectToMethodTableUnmask = nameof(ObjectToMethodTableUnmask); public const string SOSBreakingChangeVersion = nameof(SOSBreakingChangeVersion); @@ -37,6 +39,7 @@ public static class Globals public const string ObjectMethodTable = nameof(ObjectMethodTable); public const string ObjectArrayMethodTable = nameof(ObjectArrayMethodTable); public const string StringMethodTable = nameof(StringMethodTable); + public const string TypedReferenceMethodTable = nameof(TypedReferenceMethodTable); public const string MiniMetaDataBuffAddress = nameof(MiniMetaDataBuffAddress); public const string MiniMetaDataBuffMaxSize = nameof(MiniMetaDataBuffMaxSize); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/AMD64UnixArgIterator.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/AMD64UnixArgIterator.cs new file mode 100644 index 00000000000000..2ea580bc329bab --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/AMD64UnixArgIterator.cs @@ -0,0 +1,213 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Diagnostics.DataContractReader.RuntimeTypeSystemHelpers; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.CallingConventionHelpers; + +/// +/// Linux/macOS x64 (System V AMD64 ABI) argument iterator. GP args go in +/// RDI/RSI/RDX/RCX/R8/R9; FP args in XMM0-XMM7. Value-type structs <= 16 bytes +/// are classified per the SystemV "eightbyte" rules and may be split across +/// the GP and SSE register banks. +/// +internal sealed class AMD64UnixArgIterator : ArgIteratorBase +{ + private readonly Target _target; + + public override int NumArgumentRegisters => 6; + public override int NumFloatArgumentRegisters => 8; + public override int FloatRegisterSize => 16; + public override int EnregisteredParamTypeMaxSize => 16; + public override int EnregisteredReturnTypeIntegerMaxSize => 16; + public override int StackSlotSize => 8; + public override bool IsRetBuffPassedAsFirstArg => true; + + public AMD64UnixArgIterator( + TransitionBlockLayout layout, + ArgIteratorData argData, + bool hasParamType, + bool hasAsyncContinuation) + : base(layout, argData, hasParamType, hasAsyncContinuation) + { + _target = layout.Target; + } + + public override bool IsArgPassedByRefBySize(int size) => false; + + private static bool CanClassifyStruct(ArgTypeInfo typeInfo) + => typeInfo.RuntimeTypeHandle.IsMethodTable(); + + private SystemVStructDescriptor ClassifyStruct(ArgTypeInfo typeInfo, int structSize) + => SystemVStructClassifier.Classify(_target, typeInfo.RuntimeTypeHandle, structSize); + + protected override bool ValueTypeReturnNeedsRetBuf(ArgTypeInfo thRetType) + { + int size = thRetType.Size; + if (size > EnregisteredReturnTypeIntegerMaxSize) + return true; + if (!CanClassifyStruct(thRetType)) + return true; + SystemVStructDescriptor descriptor = ClassifyStruct(thRetType, size); + return !descriptor.PassedInRegisters; + } + + public override IEnumerable EnumerateArgs() + { + int idxGenReg = ComputeInitialNumRegistersUsed(); + int idxStack = 0; + int idxFPReg = 0; + + for (int argNum = 0; argNum < NumFixedArgs; argNum++) + { + CorElementType argType = GetArgumentType(argNum, out ArgTypeInfo argTypeInfo); + int argSize = GetElemSize(argType, argTypeInfo, _layout.PointerSize); + + IReadOnlyList locations; + + if (argType != CorElementType.ValueType && argType != CorElementType.TypedByRef) + { + locations = [PlaceScalar(argType, argSize, ref idxGenReg, ref idxFPReg, ref idxStack)]; + } + else + { + SystemVStructDescriptor descriptor = default; + if (argSize <= SystemVStructDescriptor.MaxStructBytesToPassInRegisters + && CanClassifyStruct(argTypeInfo)) + { + descriptor = ClassifyStruct(argTypeInfo, argSize); + } + + if (descriptor.PassedInRegisters + && TryClassifySysVLocations(descriptor, idxGenReg, idxFPReg, out List sysvLocations)) + { + locations = sysvLocations; + foreach (ArgLocation l in sysvLocations) + { + if (l.Kind == ArgLocationKind.GpRegister) idxGenReg++; + else if (l.Kind == ArgLocationKind.FpRegister) idxFPReg++; + } + } + else + { + locations = [PlaceStructOnStackLocal(argSize, argType, ref idxStack)]; + } + } + + bool isByRef = argType == CorElementType.Byref; + + yield return new ArgLocDesc + { + ArgType = argType, + ArgSize = argSize, + ArgTypeInfo = argTypeInfo, + IsByRef = isByRef, + Locations = locations, + }; + } + } + + private ArgLocation PlaceScalar( + CorElementType argType, int argSize, + ref int idxGenReg, ref int idxFPReg, ref int idxStack) + { + int cbArg = StackElemSize(argSize); + + if (argType is CorElementType.R4 or CorElementType.R8) + { + if (idxFPReg < NumFloatArgumentRegisters) + { + var loc = new ArgLocation + { + Kind = ArgLocationKind.FpRegister, + TransitionBlockOffset = _layout.OffsetOfFloatArgumentRegisters + idxFPReg * FloatRegisterSize, + Size = FloatRegisterSize, + ElementType = argType, + }; + idxFPReg++; + return loc; + } + } + else + { + int cGenRegs = cbArg / 8; + if (cGenRegs == 0) cGenRegs = 1; + if (idxGenReg + cGenRegs <= NumArgumentRegisters) + { + var loc = new ArgLocation + { + Kind = ArgLocationKind.GpRegister, + TransitionBlockOffset = _layout.ArgumentRegistersOffset + idxGenReg * _layout.PointerSize, + Size = cGenRegs * _layout.PointerSize, + ElementType = argType, + }; + idxGenReg += cGenRegs; + return loc; + } + } + + return PlaceOnStackLocal(cbArg, argType, ref idxStack); + } + + private ArgLocation PlaceStructOnStackLocal(int argSize, CorElementType argType, ref int idxStack) + => PlaceOnStackLocal(StackElemSize(argSize), argType, ref idxStack); + + private ArgLocation PlaceOnStackLocal(int cbArg, CorElementType argType, ref int idxStack) + { + int stackOfs = _layout.OffsetOfArgs + idxStack * _layout.PointerSize; + int slots = cbArg / _layout.PointerSize; + if (slots == 0) slots = 1; + idxStack += slots; + return new ArgLocation + { + Kind = ArgLocationKind.Stack, + TransitionBlockOffset = stackOfs, + Size = cbArg, + ElementType = argType, + }; + } + + private bool TryClassifySysVLocations( + SystemVStructDescriptor descriptor, + int startGenReg, int startFPReg, + out List locations) + { + int neededGen = 0, neededFP = 0; + for (int i = 0; i < descriptor.EightByteCount; i++) + { + SystemVClassification c = descriptor.Classification(i); + if (c is SystemVClassification.Integer or SystemVClassification.IntegerReference or SystemVClassification.IntegerByRef) + neededGen++; + else if (c == SystemVClassification.SSE) + neededFP++; + else { locations = null!; return false; } + } + + if (startGenReg + neededGen > NumArgumentRegisters || startFPReg + neededFP > NumFloatArgumentRegisters) + { locations = null!; return false; } + + locations = new List(descriptor.EightByteCount); + int genIdx = startGenReg, fpIdx = startFPReg; + for (int i = 0; i < descriptor.EightByteCount; i++) + { + SystemVClassification c = descriptor.Classification(i); + switch (c) + { + case SystemVClassification.Integer: + locations.Add(new ArgLocation { Kind = ArgLocationKind.GpRegister, TransitionBlockOffset = _layout.ArgumentRegistersOffset + genIdx * _layout.PointerSize, Size = _layout.PointerSize, ElementType = CorElementType.I8 }); + genIdx++; break; + case SystemVClassification.IntegerReference: + locations.Add(new ArgLocation { Kind = ArgLocationKind.GpRegister, TransitionBlockOffset = _layout.ArgumentRegistersOffset + genIdx * _layout.PointerSize, Size = _layout.PointerSize, ElementType = CorElementType.Class }); + genIdx++; break; + case SystemVClassification.IntegerByRef: + locations.Add(new ArgLocation { Kind = ArgLocationKind.GpRegister, TransitionBlockOffset = _layout.ArgumentRegistersOffset + genIdx * _layout.PointerSize, Size = _layout.PointerSize, ElementType = CorElementType.Byref }); + genIdx++; break; + case SystemVClassification.SSE: + locations.Add(new ArgLocation { Kind = ArgLocationKind.FpRegister, TransitionBlockOffset = _layout.OffsetOfFloatArgumentRegisters + fpIdx * FloatRegisterSize, Size = FloatRegisterSize, ElementType = CorElementType.R8 }); + fpIdx++; break; + } + } + return true; + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/AMD64WindowsArgIterator.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/AMD64WindowsArgIterator.cs new file mode 100644 index 00000000000000..6ab191290dcbc3 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/AMD64WindowsArgIterator.cs @@ -0,0 +1,98 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Numerics; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.CallingConventionHelpers; + +/// +/// Windows x64 (Microsoft AMD64) argument iterator. Each fixed arg occupies a +/// single pointer-sized slot; FP args (R4/R8) shadow into XMM0-XMM3. Implicit +/// byref applies to non-power-of-two structs or structs larger than 8 bytes. +/// +/// +/// See cdac-calling-conventions/amd64-windows.md at the repository root for the full +/// managed calling-convention write-up. +/// +internal sealed class AMD64WindowsArgIterator : ArgIteratorBase +{ + public override int NumArgumentRegisters => 4; // RCX, RDX, R8, R9 + public override int NumFloatArgumentRegisters => 0; // Shared with GP regs on Windows + public override int FloatRegisterSize => 16; + public override int EnregisteredParamTypeMaxSize => 8; + public override int EnregisteredReturnTypeIntegerMaxSize => 8; // RAX + public override int StackSlotSize => 8; + public override bool IsRetBuffPassedAsFirstArg => true; + + public AMD64WindowsArgIterator( + TransitionBlockLayout layout, + ArgIteratorData argData, + bool hasParamType, + bool hasAsyncContinuation) + : base(layout, argData, hasParamType, hasAsyncContinuation) + { + } + + public override int SizeOfFrameArgumentArray() + => (int)SizeOfArgStack() + SizeOfArgumentRegisters; + + public override bool IsArgPassedByRefBySize(int size) + => size > EnregisteredParamTypeMaxSize || !BitOperations.IsPow2(size); + + public override IEnumerable EnumerateArgs() + { + int curOfs = (int)_layout.OffsetOfArgs + ComputeInitialNumRegistersUsed() * _layout.PointerSize; + + for (int argNum = 0; argNum < NumFixedArgs; argNum++) + { + CorElementType argType = GetArgumentType(argNum, out ArgTypeInfo argTypeInfo); + int argSize = ArgTypeInfo.GetElemSize(argType, argTypeInfo, _layout.PointerSize); + + int cFPRegs = argType is CorElementType.R4 or CorElementType.R8 ? 1 : 0; + int argOfs = curOfs - (int)_layout.OffsetOfArgs; + curOfs += _layout.PointerSize; + + ArgLocation location; + if (cFPRegs == 0 || argOfs >= SizeOfArgumentRegisters) + { + ArgLocationKind kind = argOfs < SizeOfArgumentRegisters + ? ArgLocationKind.GpRegister + : ArgLocationKind.Stack; + int size = kind == ArgLocationKind.GpRegister + ? _layout.PointerSize + : StackElemSize(argSize); + location = new ArgLocation + { + Kind = kind, + TransitionBlockOffset = argOfs + (int)_layout.OffsetOfArgs, + Size = size, + ElementType = argType, + }; + } + else + { + int idxFpReg = argOfs / _layout.PointerSize; + location = new ArgLocation + { + Kind = ArgLocationKind.FpRegister, + TransitionBlockOffset = _layout.OffsetOfFloatArgumentRegisters + idxFpReg * FloatRegisterSize, + Size = FloatRegisterSize, + ElementType = argType, + }; + } + + bool isByRef = argType == CorElementType.Byref + || (argType == CorElementType.ValueType && IsArgPassedByRefBySize(argSize)); + + yield return new ArgLocDesc + { + ArgType = argType, + ArgSize = argSize, + ArgTypeInfo = argTypeInfo, + IsByRef = isByRef, + Locations = [location], + }; + } + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgIteratorBase.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgIteratorBase.cs new file mode 100644 index 00000000000000..a4bcc321be6b32 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgIteratorBase.cs @@ -0,0 +1,347 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Numerics; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.CallingConventionHelpers; + +/// +/// Shared ABI-independent logic for cDAC argument iterators. +/// +/// +/// Architecture-specific iterators provide register counts, stack-slot sizing, and +/// per-argument location enumeration, while this base class handles hidden argument +/// bookkeeping, return-buffer decisions, and lazy stack-size computation. +/// +internal abstract class ArgIteratorBase +{ + protected readonly TransitionBlockLayout _layout; + protected readonly ArgIteratorData _argData; + + private readonly bool _hasThis; + private readonly bool _hasParamType; + private readonly bool _hasAsyncContinuation; + + protected bool _SIZE_OF_ARG_STACK_COMPUTED; + protected int _nSizeOfArgStack; + + private bool _RETURN_FLAGS_COMPUTED; + private bool _RETURN_HAS_RET_BUFFER; + + #region Construction + + /// + /// Initializes a new iterator over a method signature using the supplied transition-block layout. + /// + protected ArgIteratorBase( + TransitionBlockLayout layout, + ArgIteratorData argData, + bool hasParamType, + bool hasAsyncContinuation) + { + _layout = layout; + _argData = argData; + _hasThis = argData.HasThis(); + _hasParamType = hasParamType; + _hasAsyncContinuation = hasAsyncContinuation; + } + + #endregion + + #region ABI characteristics + + public abstract int NumArgumentRegisters { get; } + public abstract int NumFloatArgumentRegisters { get; } + public abstract int FloatRegisterSize { get; } + public abstract int EnregisteredParamTypeMaxSize { get; } + public abstract int EnregisteredReturnTypeIntegerMaxSize { get; } + public abstract int StackSlotSize { get; } + public abstract bool IsRetBuffPassedAsFirstArg { get; } + + public bool HasThis => _hasThis; + public bool IsVarArg => _argData.IsVarArg(); + public bool HasParamType => _hasParamType; + public bool HasAsyncContinuation => _hasAsyncContinuation; + public int NumFixedArgs => _argData.NumFixedArgs(); + public int SizeOfArgumentRegisters => NumArgumentRegisters * _layout.PointerSize; + + #endregion + + #region Hidden arguments + + /// + /// Gets the transition-block offset of the hidden this argument. + /// + public virtual int GetThisOffset() + => _layout.ArgumentRegistersOffset; + + public virtual int OffsetFromGCRefMapPos(int pos) + => _layout.FirstGCRefMapSlot + pos * _layout.PointerSize; + + public virtual int GetRetBuffArgOffset(bool hasThis) + => _layout.ArgumentRegistersOffset + (hasThis ? StackElemSize(_layout.PointerSize) : 0); + + public virtual int GetVASigCookieOffset() + { + Debug.Assert(IsVarArg); + + int offset = _layout.ArgumentRegistersOffset; + int slotSize = StackElemSize(_layout.PointerSize); + + if (HasThis) + { + offset += slotSize; + } + + if (HasRetBuffArg() && IsRetBuffPassedAsFirstArg) + { + offset += slotSize; + } + + return offset; + } + + public virtual int GetParamTypeArgOffset() + { + Debug.Assert(HasParamType); + + int offset = _layout.ArgumentRegistersOffset; + int slotSize = StackElemSize(_layout.PointerSize); + + if (HasThis) + { + offset += slotSize; + } + + if (HasRetBuffArg() && IsRetBuffPassedAsFirstArg) + { + offset += slotSize; + } + + return offset; + } + + public virtual int GetAsyncContinuationArgOffset() + { + Debug.Assert(HasAsyncContinuation); + + int offset = _layout.ArgumentRegistersOffset; + int slotSize = StackElemSize(_layout.PointerSize); + + if (HasThis) + { + offset += slotSize; + } + + if (HasRetBuffArg() && IsRetBuffPassedAsFirstArg) + { + offset += slotSize; + } + + if (HasParamType) + { + offset += slotSize; + } + + return offset; + } + + public virtual int SizeOfFrameArgumentArray() + => checked((int)SizeOfArgStack()); + + public virtual uint CbStackPop() + => 0; + + #endregion + + #region Argument sizing and iteration + + public virtual int StackElemSize(int parmSize, bool isValueType = false, bool isFloatHfa = false) + => AlignUp(parmSize, StackSlotSize); + + public virtual bool IsArgPassedByRefBySize(int size) + => size > EnregisteredParamTypeMaxSize; + + /// + /// Computes the number of register slots consumed by hidden arguments before user arguments begin. + /// + protected virtual int ComputeInitialNumRegistersUsed() + { + int numRegistersUsed = 0; + + if (HasThis) + { + numRegistersUsed++; + } + + if (HasRetBuffArg() && IsRetBuffPassedAsFirstArg) + { + numRegistersUsed++; + } + + Debug.Assert(!IsVarArg || !HasParamType); + + if (HasParamType) + { + numRegistersUsed++; + } + + if (HasAsyncContinuation) + { + numRegistersUsed++; + } + + if (IsVarArg) + { + numRegistersUsed++; + } + + return numRegistersUsed; + } + + /// + /// Enumerates the fixed user-visible arguments and their locations. + /// + public abstract IEnumerable EnumerateArgs(); + + #endregion + + #region Signature inspection + + /// + /// Gets the argument type at the specified user-visible argument index. + /// + public CorElementType GetArgumentType(int argNum, out ArgTypeInfo thArgType) + { + return _argData.GetArgumentType(argNum, out thArgType); + } + + /// + /// Gets the method return type. + /// + public CorElementType GetReturnType(out ArgTypeInfo thRetType) + => _argData.GetReturnType(out thRetType); + + #endregion + + #region Return handling + + /// + /// Determines whether the signature uses a hidden return-buffer argument. + /// + public bool HasRetBuffArg() + { + if (!_RETURN_FLAGS_COMPUTED) + { + ComputeReturnFlags(); + } + + return _RETURN_HAS_RET_BUFFER; + } + + /// + /// Default return-buffer policy for value-type returns. + /// + protected virtual bool ValueTypeReturnNeedsRetBuf(ArgTypeInfo thRetType) + { + int size = thRetType.Size; + if (size > EnregisteredReturnTypeIntegerMaxSize) + { + return true; + } + + if (_layout.Architecture is RuntimeInfoArchitecture.X86 or RuntimeInfoArchitecture.X64) + { + return size <= 0 || !BitOperations.IsPow2((uint)size); + } + + return false; + } + + private void ComputeReturnFlags() + { + CorElementType type = GetReturnType(out ArgTypeInfo thRetType); + _RETURN_HAS_RET_BUFFER = type switch + { + CorElementType.TypedByRef => true, + CorElementType.ValueType => ValueTypeReturnNeedsRetBuf(thRetType), + _ => false, + }; + _RETURN_FLAGS_COMPUTED = true; + } + + #endregion + + #region Stack sizing + + /// + /// Gets the total stack space consumed by user arguments above the transition block. + /// + protected uint SizeOfArgStack() + { + if (!_SIZE_OF_ARG_STACK_COMPUTED) + { + ForceSigWalk(); + } + + Debug.Assert(_SIZE_OF_ARG_STACK_COMPUTED); + Debug.Assert((_nSizeOfArgStack % _layout.PointerSize) == 0); + return (uint)_nSizeOfArgStack; + } + + private void ForceSigWalk() + { + ComputeSizeOfArgStack(); + _SIZE_OF_ARG_STACK_COMPUTED = true; + } + + /// + /// Computes the stack footprint by walking the argument locations produced by . + /// + protected virtual void ComputeSizeOfArgStack() + { + int maxOffset = _layout.OffsetOfArgs; + foreach (ArgLocDesc arg in EnumerateArgs()) + { + foreach (ArgLocation location in arg.Locations) + { + if (location.Kind != ArgLocationKind.Stack) + { + continue; + } + + int endOffset = location.TransitionBlockOffset + location.Size; + if (endOffset > maxOffset) + { + maxOffset = endOffset; + } + } + } + + int sizeOfArgStack = maxOffset - _layout.OffsetOfArgs; + if (_layout.Architecture == RuntimeInfoArchitecture.X64 && + _layout.OperatingSystem == RuntimeInfoOperatingSystem.Windows) + { + sizeOfArgStack = sizeOfArgStack > SizeOfArgumentRegisters + ? sizeOfArgStack - SizeOfArgumentRegisters + : 0; + } + + _nSizeOfArgStack = AlignUp(sizeOfArgStack, StackElemSize(_layout.PointerSize)); + } + + #endregion + + #region Helpers + + public static int AlignUp(int input, int alignTo) + => (input + (alignTo - 1)) & ~(alignTo - 1); + + public static int GetElemSize(CorElementType elementType, ArgTypeInfo typeInfo, int pointerSize) + => ArgTypeInfo.GetElemSize(elementType, typeInfo, pointerSize); + + #endregion +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgIteratorData.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgIteratorData.cs new file mode 100644 index 00000000000000..2aacfae539306c --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgIteratorData.cs @@ -0,0 +1,104 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.CallingConventionHelpers; + +internal enum ArgLocationKind +{ + GpRegister, + FpRegister, + Stack, +} + +internal readonly struct ArgLocation +{ + public required ArgLocationKind Kind { get; init; } + public required int TransitionBlockOffset { get; init; } + public required int Size { get; init; } + public required CorElementType ElementType { get; init; } +} + +internal readonly struct ArgLocDesc +{ + public required CorElementType ArgType { get; init; } + public required int ArgSize { get; init; } + public required ArgTypeInfo ArgTypeInfo { get; init; } + public required bool IsByRef { get; init; } + public required IReadOnlyList Locations { get; init; } +} + +internal sealed class ArgIteratorData +{ + private readonly bool _hasThis; + private readonly bool _isVarArg; + private readonly ArgTypeInfo[] _parameterTypes; + private readonly ArgTypeInfo _returnType; + + public ArgIteratorData(bool hasThis, bool isVarArg, ArgTypeInfo[] parameterTypes, ArgTypeInfo returnType) + { + _hasThis = hasThis; + _isVarArg = isVarArg; + _parameterTypes = parameterTypes; + _returnType = returnType; + } + + public bool HasThis() => _hasThis; + public bool IsVarArg() => _isVarArg; + public int NumFixedArgs() => _parameterTypes.Length; + + public CorElementType GetArgumentType(int argNum, out ArgTypeInfo thArgType) + { + thArgType = _parameterTypes[argNum]; + return thArgType.CorElementType; + } + + public ArgTypeInfo GetByRefArgumentType(int argNum) + { + if (argNum < _parameterTypes.Length && _parameterTypes[argNum].CorElementType == CorElementType.Byref) + return _parameterTypes[argNum]; + return default; + } + + public CorElementType GetReturnType(out ArgTypeInfo thRetType) + { + thRetType = _returnType; + return thRetType.CorElementType; + } +} + +internal enum SystemVClassification : byte +{ + Unknown = 0, + Struct = 1, + NoClass = 2, + Memory = 3, + Integer = 4, + IntegerReference = 5, + IntegerByRef = 6, + SSE = 7, +} + +internal struct SystemVStructDescriptor +{ + public const int MaxEightBytes = 2; + public const int MaxStructBytesToPassInRegisters = 16; + public const int EightByteSizeInBytes = 8; + + public bool PassedInRegisters; + public byte EightByteCount; + public SystemVClassification EightByteClassification0; + public SystemVClassification EightByteClassification1; + public byte EightByteSize0; + public byte EightByteSize1; + public byte EightByteOffset0; + public byte EightByteOffset1; + + public SystemVClassification Classification(int index) => index switch + { + 0 => EightByteClassification0, + 1 => EightByteClassification1, + _ => throw new System.ArgumentOutOfRangeException(nameof(index)), + }; +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgIteratorFactory.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgIteratorFactory.cs new file mode 100644 index 00000000000000..3a2fc7ea4847cb --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgIteratorFactory.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Diagnostics.DataContractReader.Contracts; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.CallingConventionHelpers; + +/// +/// Factory to create the appropriate per-arch +/// subclass for the target architecture. +/// +internal static class ArgIteratorFactory +{ + public static ArgIteratorBase Create( + TransitionBlockLayout layout, + ArgIteratorData argData, + bool hasParamType, + bool hasAsyncContinuation) + { + return layout.Architecture switch + { + RuntimeInfoArchitecture.X86 => new X86ArgIterator( + layout, argData, hasParamType, hasAsyncContinuation), + RuntimeInfoArchitecture.X64 => layout.OperatingSystem != RuntimeInfoOperatingSystem.Windows + ? new AMD64UnixArgIterator( + layout, argData, hasParamType, hasAsyncContinuation) + : new AMD64WindowsArgIterator( + layout, argData, hasParamType, hasAsyncContinuation), + RuntimeInfoArchitecture.Arm => new Arm32ArgIterator( + layout, argData, hasParamType, hasAsyncContinuation, + isArmhfABI: !layout.Target.TryReadGlobal(Constants.Globals.FeatureArmSoftFP, out byte? _)), + RuntimeInfoArchitecture.Arm64 => new Arm64ArgIterator( + layout, argData, hasParamType, hasAsyncContinuation), + RuntimeInfoArchitecture.LoongArch64 or RuntimeInfoArchitecture.RiscV64 + => new RiscV64LoongArch64ArgIterator( + layout, argData, hasParamType, hasAsyncContinuation), + _ => throw new NotSupportedException(layout.Architecture.ToString()), + }; + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgTypeInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgTypeInfo.cs new file mode 100644 index 00000000000000..82f5001964216b --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgTypeInfo.cs @@ -0,0 +1,253 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Type information needed by ArgIterator for calling convention analysis. +// Ported from crossgen2's TypeHandle struct in ArgIterator.cs. + +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using Microsoft.Diagnostics.DataContractReader.RuntimeTypeSystemHelpers; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.CallingConventionHelpers; + +/// +/// Pre-computed type information needed by for +/// calling convention analysis. This is a value type to avoid allocations +/// during argument iteration. +/// +/// +/// Mirrors crossgen2's TypeHandle struct in ArgIterator.cs, but uses +/// data from the cDAC's rather than +/// crossgen2's TypeDesc. +/// +internal readonly struct ArgTypeInfo +{ + public CorElementType CorElementType { get; init; } + public int Size { get; init; } + public bool IsValueType { get; init; } + public bool RequiresAlign8 { get; init; } + public bool IsHomogeneousAggregate { get; init; } + public int HomogeneousAggregateElementSize { get; init; } + + /// + /// The TypeHandle from the target runtime, used for value type field enumeration + /// and SystemV struct classification. + /// + public TypeHandle RuntimeTypeHandle { get; init; } + + public bool IsNull => CorElementType == default && Size == 0; + + /// + /// Gets the element size for a given CorElementType, matching crossgen2's + /// TypeHandle.GetElemSize. Returns the type's actual size for value + /// types, or pointer size for reference types. + /// + public static int GetElemSize(CorElementType t, ArgTypeInfo thValueType, int pointerSize) + { + if ((int)t <= 0x1d) + { + int elemSize = s_elemSizes[(int)t]; + if (elemSize == -1) + return thValueType.Size; + if (elemSize == -2) + return pointerSize; + return elemSize; + } + return 0; + } + + private static readonly int[] s_elemSizes = + [ + 0, // ELEMENT_TYPE_END 0x0 + 0, // ELEMENT_TYPE_VOID 0x1 + 1, // ELEMENT_TYPE_BOOLEAN 0x2 + 2, // ELEMENT_TYPE_CHAR 0x3 + 1, // ELEMENT_TYPE_I1 0x4 + 1, // ELEMENT_TYPE_U1 0x5 + 2, // ELEMENT_TYPE_I2 0x6 + 2, // ELEMENT_TYPE_U2 0x7 + 4, // ELEMENT_TYPE_I4 0x8 + 4, // ELEMENT_TYPE_U4 0x9 + 8, // ELEMENT_TYPE_I8 0xa + 8, // ELEMENT_TYPE_U8 0xb + 4, // ELEMENT_TYPE_R4 0xc + 8, // ELEMENT_TYPE_R8 0xd + -2, // ELEMENT_TYPE_STRING 0xe + -2, // ELEMENT_TYPE_PTR 0xf + -2, // ELEMENT_TYPE_BYREF 0x10 + -1, // ELEMENT_TYPE_VALUETYPE 0x11 + -2, // ELEMENT_TYPE_CLASS 0x12 + 0, // ELEMENT_TYPE_VAR 0x13 + -2, // ELEMENT_TYPE_ARRAY 0x14 + 0, // ELEMENT_TYPE_GENERICINST 0x15 + 0, // ELEMENT_TYPE_TYPEDBYREF 0x16 + 0, // UNUSED 0x17 + -2, // ELEMENT_TYPE_I 0x18 + -2, // ELEMENT_TYPE_U 0x19 + 0, // UNUSED 0x1a + -2, // ELEMENT_TYPE_FPTR 0x1b + -2, // ELEMENT_TYPE_OBJECT 0x1c + -2, // ELEMENT_TYPE_SZARRAY 0x1d + ]; + + /// + /// Creates an from a target TypeHandle using the + /// runtime type system contract. + /// + public static ArgTypeInfo FromTypeHandle(Target target, TypeHandle th) + { + IRuntimeTypeSystem rts = target.Contracts.RuntimeTypeSystem; + CorElementType corType = rts.GetSignatureCorElementType(th); + + bool isValueType = corType is CorElementType.ValueType; + int size = isValueType + ? rts.GetNumInstanceFieldBytes(th) + : target.PointerSize; + + bool requiresAlign8 = false; + bool isHfa = false; + int hfaElemSize = 0; + + if (isValueType) + { + requiresAlign8 = rts.RequiresAlign8(th); + isHfa = rts.IsHFA(th); + if (isHfa) + { + hfaElemSize = ComputeHfaElementSize(target, rts, th, requiresAlign8); + } + } + + return new ArgTypeInfo + { + CorElementType = corType, + Size = size, + IsValueType = isValueType, + RequiresAlign8 = requiresAlign8, + IsHomogeneousAggregate = isHfa, + HomogeneousAggregateElementSize = hfaElemSize, + RuntimeTypeHandle = th, + }; + } + + /// + /// Computes the element size of a Homogeneous Floating-point Aggregate (HFA), + /// matching crossgen2's DefType.GetHomogeneousAggregateElementSize. + /// + /// + /// On ARM, the element size is fully determined by the alignment requirement: + /// HFAs of doubles have 8-byte alignment; HFAs of floats use 4-byte alignment. + /// + /// On ARM64, we walk the first field of the value type, recursing through nested + /// value types until we reach a primitive (R4/R8) or a Vector intrinsic + /// (Vector64`1, Vector128`1, or System.Numerics.Vector`1). This mirrors the + /// runtime's MethodTable::GetHFAType in src/coreclr/vm/class.cpp. + /// + private static int ComputeHfaElementSize(Target target, IRuntimeTypeSystem rts, TypeHandle th, bool requiresAlign8) + { + RuntimeInfoArchitecture arch = target.Contracts.RuntimeInfo.GetTargetArchitecture(); + if (arch == RuntimeInfoArchitecture.Arm) + { + return requiresAlign8 ? 8 : 4; + } + if (arch != RuntimeInfoArchitecture.Arm64) + { + // FEATURE_HFA is only enabled on ARM/ARM64; IsHFA should never be true elsewhere. + return 0; + } + + // ARM64: walk the first field, descending into nested value types. All HFA fields + // must be of the same primitive/vector type, so the first field determines the + // element size for the entire aggregate. + TypeHandle current = th; + // Bound the loop to defend against unexpected metadata cycles. + for (int depth = 0; depth < 16; depth++) + { + int vectorSize = rts.GetVectorSize(current); + if (vectorSize != 0) + return vectorSize; + + TargetPointer firstField = rts.GetFieldDescList(current); + if (firstField == TargetPointer.Null) + return 0; + CorElementType fieldType = rts.GetFieldDescType(firstField); + switch (fieldType) + { + case CorElementType.R4: + return 4; + case CorElementType.R8: + return 8; + case CorElementType.ValueType: + TypeHandle nested = LookupApproxFieldTypeHandle(target, rts, firstField); + if (nested.IsNull || !nested.IsMethodTable()) + return 0; + current = nested; + continue; + default: + // IsHFA should only be set on types that resolve to a valid HFA element; + // anything else here indicates a metadata mismatch we can't classify. + return 0; + } + } + return 0; + } + + /// + /// Resolves a field's declared type without triggering type loading. Mirrors native + /// FieldDesc::LookupApproxFieldTypeHandle (DAC variant): walks the field's + /// metadata signature and returns the resulting , or a null + /// handle when the type isn't already loaded. + /// + private static TypeHandle LookupApproxFieldTypeHandle(Target target, IRuntimeTypeSystem rts, TargetPointer fieldDescPointer) + { + if (fieldDescPointer == TargetPointer.Null) + return default; + + uint token = rts.GetFieldDescMemberDef(fieldDescPointer); + EntityHandle entityHandle = MetadataTokens.EntityHandle((int)token); + if (entityHandle.IsNil || entityHandle.Kind != HandleKind.FieldDefinition) + return default; + + TargetPointer enclosingMT = rts.GetMTOfEnclosingClass(fieldDescPointer); + TypeHandle ctx = rts.GetTypeHandle(enclosingMT); + if (!ctx.IsMethodTable()) + return default; + + TargetPointer modulePtr = rts.GetModule(ctx); + if (modulePtr == TargetPointer.Null) + return default; + + ModuleHandle moduleHandle = target.Contracts.Loader.GetModuleHandleFromModulePtr(modulePtr); + MetadataReader? mdReader = target.Contracts.EcmaMetadata.GetMetadata(moduleHandle); + if (mdReader is null) + return default; + + FieldDefinition fieldDef = mdReader.GetFieldDefinition((FieldDefinitionHandle)entityHandle); + try + { + return target.Contracts.Signature.DecodeFieldSignature(fieldDef.Signature, moduleHandle, ctx); + } + catch + { + return default; + } + } + + /// + /// Creates an for a primitive type that doesn't need + /// type handle resolution. + /// + public static ArgTypeInfo ForPrimitive(CorElementType corType, int pointerSize) + { + return new ArgTypeInfo + { + CorElementType = corType, + Size = GetElemSize(corType, default, pointerSize), + IsValueType = false, + RequiresAlign8 = false, + IsHomogeneousAggregate = false, + HomogeneousAggregateElementSize = 0, + RuntimeTypeHandle = default, + }; + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgTypeInfoSignatureProvider.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgTypeInfoSignatureProvider.cs new file mode 100644 index 00000000000000..8f789410bd106c --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgTypeInfoSignatureProvider.cs @@ -0,0 +1,305 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using Microsoft.Diagnostics.DataContractReader.SignatureHelpers; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.CallingConventionHelpers; + +/// +/// Generic context used to resolve ELEMENT_TYPE_VAR and ELEMENT_TYPE_MVAR +/// while decoding a method signature into values. +/// is the owning type's (used for VAR), +/// and is the owning method's +/// (used for MVAR). +/// +internal readonly record struct ArgTypeInfoSignatureContext(TypeHandle ClassContext, MethodDescHandle MethodContext); + +/// +/// Decodes signature elements directly into so that +/// can drive argument iteration without an intermediate +/// classification stage. +/// Implements , which +/// is a superset of SRM's +/// adding support for ELEMENT_TYPE_INTERNAL. +/// +/// +/// The provider is scoped to a single module: GetTypeFromDefinition and +/// GetTypeFromReference resolve TypeDef/TypeRef tokens via the module's lookup +/// tables so enums (and other runtime-normalized value types) are classified using their +/// actual , matching native +/// SigPointer::PeekElemTypeNormalized. For value-type elements the resolved +/// is surfaced in so +/// ArgIterator sees the correct size / HFA / alignment in a single signature walk +/// (mirroring native MetaSig::GetByValType). +/// +internal sealed class ArgTypeInfoSignatureProvider + : IRuntimeSignatureTypeProvider +{ + private readonly Target _target; + private readonly ModuleHandle _moduleHandle; + private ArgTypeInfo? _cachedTypedReferenceInfo; + + public ArgTypeInfoSignatureProvider(Target target, ModuleHandle moduleHandle) + { + _target = target; + _moduleHandle = moduleHandle; + } + + public ArgTypeInfo GetPrimitiveType(PrimitiveTypeCode typeCode) + => typeCode switch + { + PrimitiveTypeCode.String or PrimitiveTypeCode.Object + => ArgTypeInfo.ForPrimitive(CorElementType.Class, _target.PointerSize), + // TypedReference has no class token in the signature blob -- the runtime + // identifies its layout via the well-known g_TypedReferenceMT global. + // Mirroring native callingconvention.h:1351-1355, we substitute the + // TypedReference MethodTable here so the rest of ArgIterator (and the + // SystemV struct classifier) see it as an ordinary 16-byte value type. + PrimitiveTypeCode.TypedReference => GetTypedReferenceInfo(), + _ => ArgTypeInfo.ForPrimitive(PrimitiveToCorElementType(typeCode), _target.PointerSize), + }; + + private ArgTypeInfo GetTypedReferenceInfo() + { + if (_cachedTypedReferenceInfo is { } cached) + return cached; + + ArgTypeInfo info; + try + { + TargetPointer mtPtr = _target.ReadPointer( + _target.ReadGlobalPointer(Constants.Globals.TypedReferenceMethodTable)); + if (mtPtr == TargetPointer.Null) + { + info = UnresolvedValueType(); + } + else + { + TypeHandle th = _target.Contracts.RuntimeTypeSystem.GetTypeHandle(mtPtr); + info = ArgTypeInfo.FromTypeHandle(_target, th); + } + } + catch + { + // Older runtime images without the TypedReferenceMethodTable global, or + // any failure resolving the type, falls back to a conservative placeholder. + info = UnresolvedValueType(); + } + + _cachedTypedReferenceInfo = info; + return info; + } + + public ArgTypeInfo GetTypeFromDefinition(MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) + => FromTokenLookup(_target.Contracts.Loader.GetLookupTables(_moduleHandle).TypeDefToMethodTable, MetadataTokens.GetToken(handle), rawTypeKind); + + public ArgTypeInfo GetTypeFromReference(MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) + => FromTokenLookup(_target.Contracts.Loader.GetLookupTables(_moduleHandle).TypeRefToMethodTable, MetadataTokens.GetToken(handle), rawTypeKind); + + public ArgTypeInfo GetTypeFromSpecification(MetadataReader reader, ArgTypeInfoSignatureContext genericContext, TypeSpecificationHandle handle, byte rawTypeKind) + // TODO: Resolve the TypeSpec to a concrete (already-loaded) TypeHandle so that + // generic value-type instantiations get correct size / HFA classification. Native + // does this via SigPointer::GetTypeHandleThrowing + the instantiated-type + // hashtable; cDAC needs an equivalent lookup-only RTS API. Until then, fall back + // to a conservative pointer-sized placeholder for value types. + => rawTypeKind == (byte)SignatureTypeKind.ValueType + ? UnresolvedValueType() + : ArgTypeInfo.ForPrimitive(CorElementType.Class, _target.PointerSize); + + public ArgTypeInfo GetSZArrayType(ArgTypeInfo elementType) => ArgTypeInfo.ForPrimitive(CorElementType.SzArray, _target.PointerSize); + public ArgTypeInfo GetArrayType(ArgTypeInfo elementType, ArrayShape shape) => ArgTypeInfo.ForPrimitive(CorElementType.Array, _target.PointerSize); + public ArgTypeInfo GetByReferenceType(ArgTypeInfo elementType) => ArgTypeInfo.ForPrimitive(CorElementType.Byref, _target.PointerSize); + public ArgTypeInfo GetPointerType(ArgTypeInfo elementType) => ArgTypeInfo.ForPrimitive(CorElementType.Ptr, _target.PointerSize); + + public ArgTypeInfo GetGenericInstantiation(ArgTypeInfo genericType, ImmutableArray typeArguments) + { + // TODO: lookup the instantiated MethodTable so generic value-type args get correct + // size / HFA. For reference-type generic instantiations the open-generic's + // ArgTypeInfo (pointer-sized Class) is already correct; for value-type + // instantiations the open generic's size is not meaningful, so we downgrade to a + // conservative pointer-sized placeholder. + if (genericType.CorElementType == CorElementType.ValueType) + return UnresolvedValueType(); + return genericType; + } + + public ArgTypeInfo GetGenericMethodParameter(ArgTypeInfoSignatureContext genericContext, int index) + { + try + { + ReadOnlySpan instantiation = _target.Contracts.RuntimeTypeSystem.GetGenericMethodInstantiation(genericContext.MethodContext); + if ((uint)index >= (uint)instantiation.Length) + return ArgTypeInfo.ForPrimitive(CorElementType.Class, _target.PointerSize); + return BuildFromTypeHandle(instantiation[index]); + } + catch + { + return ArgTypeInfo.ForPrimitive(CorElementType.Class, _target.PointerSize); + } + } + + public ArgTypeInfo GetGenericTypeParameter(ArgTypeInfoSignatureContext genericContext, int index) + { + try + { + IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem; + TypeHandle classCtx = genericContext.ClassContext; + + if (rts.IsArray(classCtx, out _)) + { + // Match native SigTypeContext::InitTypeContext (typectxt.cpp): arrays use + // the element type as their class instantiation. RuntimeTypeSystem.GetInstantiation + // returns an empty span for arrays, so consult GetTypeParam directly (the + // managed equivalent of MethodTable::GetArrayInstantiation). + Debug.Assert(index == 0, "Array class context has a 1-element instantiation; index > 0 indicates a malformed signature."); + if (index != 0) + return ArgTypeInfo.ForPrimitive(CorElementType.Class, _target.PointerSize); + return BuildFromTypeHandle(rts.GetTypeParam(classCtx)); + } + + ReadOnlySpan instantiation = rts.GetInstantiation(classCtx); + if ((uint)index >= (uint)instantiation.Length) + return ArgTypeInfo.ForPrimitive(CorElementType.Class, _target.PointerSize); + return BuildFromTypeHandle(instantiation[index]); + } + catch + { + return ArgTypeInfo.ForPrimitive(CorElementType.Class, _target.PointerSize); + } + } + + public ArgTypeInfo GetFunctionPointerType(MethodSignature signature) + => ArgTypeInfo.ForPrimitive(CorElementType.FnPtr, _target.PointerSize); + public ArgTypeInfo GetModifiedType(ArgTypeInfo modifier, ArgTypeInfo unmodifiedType, bool isRequired) => unmodifiedType; + public ArgTypeInfo GetInternalModifiedType(TargetPointer typeHandlePointer, ArgTypeInfo unmodifiedType, bool isRequired) => unmodifiedType; + public ArgTypeInfo GetPinnedType(ArgTypeInfo elementType) => elementType; + + public ArgTypeInfo GetInternalType(TargetPointer typeHandlePointer) + { + if (typeHandlePointer == TargetPointer.Null) + return ArgTypeInfo.ForPrimitive(CorElementType.I, _target.PointerSize); + + try + { + return BuildFromTypeHandle(_target.Contracts.RuntimeTypeSystem.GetTypeHandle(typeHandlePointer)); + } + catch + { + return ArgTypeInfo.ForPrimitive(CorElementType.Class, _target.PointerSize); + } + } + + /// + /// Resolve a TypeDef/TypeRef token via the module's lookup tables and build an + /// from the resulting . Falls back + /// to a -driven conservative placeholder when the type + /// has not been loaded. + /// + private ArgTypeInfo FromTokenLookup(TargetPointer lookupTable, int token, byte rawTypeKind) + { + try + { + TargetPointer typeHandlePtr = _target.Contracts.Loader.GetModuleLookupMapElement(lookupTable, (uint)token, out _); + if (typeHandlePtr == TargetPointer.Null) + return FallbackForRawTypeKind(rawTypeKind); + + return BuildFromTypeHandle(_target.Contracts.RuntimeTypeSystem.GetTypeHandle(typeHandlePtr)); + } + catch + { + return FallbackForRawTypeKind(rawTypeKind); + } + } + + private ArgTypeInfo FallbackForRawTypeKind(byte rawTypeKind) + => rawTypeKind == (byte)SignatureTypeKind.ValueType + ? UnresolvedValueType() + : ArgTypeInfo.ForPrimitive(CorElementType.Class, _target.PointerSize); + + /// + /// Build an from a resolved . Mirrors + /// native SigPointer::PeekElemTypeNormalized + MetaSig::GetByValType: + /// enums collapse to their underlying primitive (via GetSignatureCorElementType) + /// so they classify as a non-GC scalar; value types surface the resolved + /// with full size / HFA / alignment for ArgIterator. + /// + private ArgTypeInfo BuildFromTypeHandle(TypeHandle typeHandle) + { + if (typeHandle.Address == TargetPointer.Null) + return ArgTypeInfo.ForPrimitive(CorElementType.Class, _target.PointerSize); + + IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem; + CorElementType corType = rts.GetSignatureCorElementType(typeHandle); + + switch (corType) + { + case CorElementType.Void: + case CorElementType.Boolean: + case CorElementType.Char: + case CorElementType.I1: + case CorElementType.U1: + case CorElementType.I2: + case CorElementType.U2: + case CorElementType.I4: + case CorElementType.U4: + case CorElementType.I8: + case CorElementType.U8: + case CorElementType.R4: + case CorElementType.R8: + case CorElementType.I: + case CorElementType.U: + case CorElementType.FnPtr: + case CorElementType.Ptr: + return ArgTypeInfo.ForPrimitive(corType, _target.PointerSize); + + case CorElementType.Byref: + return ArgTypeInfo.ForPrimitive(CorElementType.Byref, _target.PointerSize); + + case CorElementType.ValueType: + // GetSignatureCorElementType already collapses enums to their underlying + // primitive; anything still typed as ValueType is a real struct. + return ArgTypeInfo.FromTypeHandle(_target, typeHandle); + + default: + return ArgTypeInfo.ForPrimitive(CorElementType.Class, _target.PointerSize); + } + } + + /// + /// Conservative value-type placeholder used when a TypeSpec / TypedReference / + /// unloaded TypeDef/TypeRef can't be resolved to a concrete . + /// ArgIterator sees a pointer-sized value type with no HFA classification. + /// + private ArgTypeInfo UnresolvedValueType() + => new ArgTypeInfo + { + CorElementType = CorElementType.ValueType, + Size = _target.PointerSize, + IsValueType = true, + }; + + private static CorElementType PrimitiveToCorElementType(PrimitiveTypeCode typeCode) => typeCode switch + { + PrimitiveTypeCode.Void => CorElementType.Void, + PrimitiveTypeCode.Boolean => CorElementType.Boolean, + PrimitiveTypeCode.Char => CorElementType.Char, + PrimitiveTypeCode.SByte => CorElementType.I1, + PrimitiveTypeCode.Byte => CorElementType.U1, + PrimitiveTypeCode.Int16 => CorElementType.I2, + PrimitiveTypeCode.UInt16 => CorElementType.U2, + PrimitiveTypeCode.Int32 => CorElementType.I4, + PrimitiveTypeCode.UInt32 => CorElementType.U4, + PrimitiveTypeCode.Int64 => CorElementType.I8, + PrimitiveTypeCode.UInt64 => CorElementType.U8, + PrimitiveTypeCode.Single => CorElementType.R4, + PrimitiveTypeCode.Double => CorElementType.R8, + PrimitiveTypeCode.IntPtr => CorElementType.I, + PrimitiveTypeCode.UIntPtr => CorElementType.U, + _ => CorElementType.Void, + }; +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/Arm32ArgIterator.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/Arm32ArgIterator.cs new file mode 100644 index 00000000000000..39d76678154197 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/Arm32ArgIterator.cs @@ -0,0 +1,213 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.CallingConventionHelpers; + +/// +/// ARM32 (AAPCS) argument iterator. Integer arguments use R0-R3, hard-float +/// targets use the VFP argument register bank for floating-point values, and +/// overflow spills to the stack with the ABI's 64-bit-alignment rules. +/// +internal sealed class Arm32ArgIterator : ArgIteratorBase +{ + private readonly bool _isArmhfABI; + + public override int NumArgumentRegisters => 4; + public override int NumFloatArgumentRegisters => 16; + public override int FloatRegisterSize => 4; + public override int EnregisteredParamTypeMaxSize => 0; + public override int EnregisteredReturnTypeIntegerMaxSize => 4; + public override int StackSlotSize => 4; + public override bool IsRetBuffPassedAsFirstArg => true; + + public Arm32ArgIterator( + TransitionBlockLayout layout, + ArgIteratorData argData, + bool hasParamType, + bool hasAsyncContinuation, + bool isArmhfABI = true) + : base(layout, argData, hasParamType, hasAsyncContinuation) + { + _isArmhfABI = isArmhfABI; + } + + public override bool IsArgPassedByRefBySize(int size) => false; + + public override IEnumerable EnumerateArgs() + { + int idxGenReg = ComputeInitialNumRegistersUsed(); + int ofsStack = 0; + ushort wFPRegs = 0; + + for (int argNum = 0; argNum < NumFixedArgs; argNum++) + { + CorElementType argType = GetArgumentType(argNum, out ArgTypeInfo argTypeInfo); + int argSize = GetElemSize(argType, argTypeInfo, _layout.PointerSize); + int cbArg = StackElemSize(argSize); + + bool isFloatingPoint = false; + bool requiresAlign64Bit = false; + CorElementType fpElementType = argType; + + switch (argType) + { + case CorElementType.I8: + case CorElementType.U8: + requiresAlign64Bit = true; + break; + + case CorElementType.R4: + isFloatingPoint = true; + fpElementType = CorElementType.R4; + break; + + case CorElementType.R8: + isFloatingPoint = true; + requiresAlign64Bit = true; + fpElementType = CorElementType.R8; + break; + + case CorElementType.ValueType: + requiresAlign64Bit = argTypeInfo.RequiresAlign8; + if (argTypeInfo.IsHomogeneousAggregate) + { + isFloatingPoint = true; + fpElementType = argTypeInfo.HomogeneousAggregateElementSize == 4 + ? CorElementType.R4 + : CorElementType.R8; + } + break; + } + + IReadOnlyList locations; + if (isFloatingPoint && _isArmhfABI && !IsVarArg) + { + ushort wAllocMask = checked((ushort)((1 << (cbArg / 4)) - 1)); + ushort cSteps = (ushort)(requiresAlign64Bit ? 9 - (cbArg / 8) : 17 - (cbArg / 4)); + ushort cShift = requiresAlign64Bit ? (ushort)2 : (ushort)1; + + for (ushort i = 0; i < cSteps; i++) + { + if ((wFPRegs & wAllocMask) == 0) + { + wFPRegs |= wAllocMask; + locations = + [ + new ArgLocation + { + Kind = ArgLocationKind.FpRegister, + TransitionBlockOffset = _layout.OffsetOfFloatArgumentRegisters + (i * cShift * 4), + Size = cbArg, + ElementType = fpElementType, + } + ]; + goto Yield; + } + + wAllocMask <<= cShift; + } + + wFPRegs = 0xffff; + if (requiresAlign64Bit) + { + ofsStack = AlignUp(ofsStack, _layout.PointerSize * 2); + } + + locations = + [ + new ArgLocation + { + Kind = ArgLocationKind.Stack, + TransitionBlockOffset = _layout.OffsetOfArgs + ofsStack, + Size = cbArg, + ElementType = argType, + } + ]; + ofsStack += cbArg; + } + else + { + if (idxGenReg < NumArgumentRegisters) + { + if (requiresAlign64Bit) + { + idxGenReg = AlignUp(idxGenReg, 2); + } + + int argOffset = _layout.ArgumentRegistersOffset + idxGenReg * _layout.PointerSize; + int remainingRegs = NumArgumentRegisters - idxGenReg; + if (cbArg <= remainingRegs * _layout.PointerSize) + { + idxGenReg += AlignUp(cbArg, _layout.PointerSize) / _layout.PointerSize; + locations = + [ + new ArgLocation + { + Kind = ArgLocationKind.GpRegister, + TransitionBlockOffset = argOffset, + Size = cbArg, + ElementType = argType, + } + ]; + goto Yield; + } + + idxGenReg = NumArgumentRegisters; + if (ofsStack == 0 && remainingRegs > 0) + { + int regSize = remainingRegs * _layout.PointerSize; + int stackSize = cbArg - regSize; + locations = + [ + new ArgLocation + { + Kind = ArgLocationKind.GpRegister, + TransitionBlockOffset = argOffset, + Size = regSize, + ElementType = argType, + }, + new ArgLocation + { + Kind = ArgLocationKind.Stack, + TransitionBlockOffset = _layout.OffsetOfArgs, + Size = stackSize, + ElementType = argType, + } + ]; + ofsStack += stackSize; + goto Yield; + } + } + + if (requiresAlign64Bit) + { + ofsStack = AlignUp(ofsStack, _layout.PointerSize * 2); + } + + locations = + [ + new ArgLocation + { + Kind = ArgLocationKind.Stack, + TransitionBlockOffset = _layout.OffsetOfArgs + ofsStack, + Size = cbArg, + ElementType = argType, + } + ]; + ofsStack += cbArg; + } + + Yield: + yield return new ArgLocDesc + { + ArgType = argType, + ArgSize = argSize, + ArgTypeInfo = argTypeInfo, + IsByRef = argType == CorElementType.Byref, + Locations = locations, + }; + } + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/Arm64ArgIterator.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/Arm64ArgIterator.cs new file mode 100644 index 00000000000000..0e4c7fa86d904d --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/Arm64ArgIterator.cs @@ -0,0 +1,213 @@ +// 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.Diagnostics; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.CallingConventionHelpers; + +/// +/// ARM64 (AAPCS64 / Apple ARM64) argument iterator. Integer arguments use X0-X7, +/// floating-point arguments use V0-V7, Apple stack slots are tightly packed for +/// primitives, and HFAs are reported as one slot per FP register. +/// +internal sealed class Arm64ArgIterator : ArgIteratorBase +{ + private readonly bool _isAppleArm64ABI; + + public override int NumArgumentRegisters => 8; + public override int NumFloatArgumentRegisters => 8; + public override int FloatRegisterSize => 16; + public override int EnregisteredParamTypeMaxSize => 16; + public override int EnregisteredReturnTypeIntegerMaxSize => 16; + public override int StackSlotSize => 8; + public override bool IsRetBuffPassedAsFirstArg => false; + + public Arm64ArgIterator( + TransitionBlockLayout layout, + ArgIteratorData argData, + bool hasParamType, + bool hasAsyncContinuation) + : base(layout, argData, hasParamType, hasAsyncContinuation) + { + _isAppleArm64ABI = layout.OperatingSystem == RuntimeInfoOperatingSystem.Apple; + } + + public override int StackElemSize(int parmSize, bool isValueType = false, bool isFloatHfa = false) + { + if (_isAppleArm64ABI) + { + if (!isValueType) + { + Debug.Assert((parmSize & (parmSize - 1)) == 0); + return parmSize; + } + + if (isFloatHfa) + { + Debug.Assert((parmSize % 4) == 0); + return parmSize; + } + } + + return base.StackElemSize(parmSize, isValueType, isFloatHfa); + } + + public override bool IsArgPassedByRefBySize(int size) => size > EnregisteredParamTypeMaxSize; + + public override int GetRetBuffArgOffset(bool hasThis) + => _layout.FirstGCRefMapSlot; + + public override IEnumerable EnumerateArgs() + { + int idxGenReg = ComputeInitialNumRegistersUsed(); + int idxFPReg = 0; + int ofsStack = 0; + + for (int argNum = 0; argNum < NumFixedArgs; argNum++) + { + CorElementType argType = GetArgumentType(argNum, out ArgTypeInfo argTypeInfo); + int argSize = GetElemSize(argType, argTypeInfo, _layout.PointerSize); + bool isHomogeneousAggregate = argType == CorElementType.ValueType && argTypeInfo.IsHomogeneousAggregate; + bool isByRef = argType == CorElementType.Byref + || (argType == CorElementType.ValueType + && argSize > EnregisteredParamTypeMaxSize + && (!isHomogeneousAggregate || IsVarArg)); + + int effectiveArgSize = isByRef ? _layout.PointerSize : argSize; + int cFPRegs = 0; + bool isFloatHfa = false; + CorElementType fpElementType = argType; + + switch (argType) + { + case CorElementType.R4: + cFPRegs = 1; + fpElementType = CorElementType.R4; + break; + + case CorElementType.R8: + cFPRegs = 1; + fpElementType = CorElementType.R8; + break; + + case CorElementType.ValueType: + if (isHomogeneousAggregate) + { + int haElementSize = argTypeInfo.HomogeneousAggregateElementSize; + isFloatHfa = haElementSize == 4; + fpElementType = haElementSize == 4 ? CorElementType.R4 : CorElementType.R8; + cFPRegs = argSize / haElementSize; + } + break; + } + + int cbArg = StackElemSize(effectiveArgSize, argType == CorElementType.ValueType, isFloatHfa); + IReadOnlyList locations; + + if (cFPRegs > 0 && !IsVarArg) + { + if (idxFPReg + cFPRegs <= NumFloatArgumentRegisters) + { + List hfaLocations = new(cFPRegs); + for (int i = 0; i < cFPRegs; i++) + { + hfaLocations.Add(new ArgLocation + { + Kind = ArgLocationKind.FpRegister, + TransitionBlockOffset = _layout.OffsetOfFloatArgumentRegisters + ((idxFPReg + i) * FloatRegisterSize), + Size = FloatRegisterSize, + ElementType = fpElementType, + }); + } + + idxFPReg += cFPRegs; + locations = hfaLocations; + goto Yield; + } + + idxFPReg = NumFloatArgumentRegisters; + } + else + { + int regSlots = AlignUp(cbArg, _layout.PointerSize) / _layout.PointerSize; + if (idxGenReg + regSlots <= NumArgumentRegisters) + { + locations = + [ + new ArgLocation + { + Kind = ArgLocationKind.GpRegister, + TransitionBlockOffset = _layout.ArgumentRegistersOffset + (idxGenReg * _layout.PointerSize), + Size = regSlots * _layout.PointerSize, + ElementType = argType, + } + ]; + idxGenReg += regSlots; + goto Yield; + } + + bool allowVarArgSplit = _layout.OperatingSystem == RuntimeInfoOperatingSystem.Windows + && IsVarArg + && idxGenReg < NumArgumentRegisters + && !isHomogeneousAggregate; + if (allowVarArgSplit) + { + int headSize = (NumArgumentRegisters - idxGenReg) * _layout.PointerSize; + locations = + [ + new ArgLocation + { + Kind = ArgLocationKind.GpRegister, + TransitionBlockOffset = _layout.ArgumentRegistersOffset + (idxGenReg * _layout.PointerSize), + Size = headSize, + ElementType = argType, + }, + new ArgLocation + { + Kind = ArgLocationKind.Stack, + TransitionBlockOffset = _layout.OffsetOfArgs + ofsStack, + Size = cbArg - headSize, + ElementType = argType, + } + ]; + ofsStack += cbArg - headSize; + idxGenReg = NumArgumentRegisters; + goto Yield; + } + + idxGenReg = NumArgumentRegisters; + } + + if (_isAppleArm64ABI) + { + int alignment = !argTypeInfo.IsValueType + ? cbArg + : isFloatHfa ? 4 : 8; + ofsStack = AlignUp(ofsStack, alignment); + } + + locations = + [ + new ArgLocation + { + Kind = ArgLocationKind.Stack, + TransitionBlockOffset = _layout.OffsetOfArgs + ofsStack, + Size = cbArg, + ElementType = argType, + } + ]; + ofsStack += cbArg; + + Yield: + yield return new ArgLocDesc + { + ArgType = argType, + ArgSize = argSize, + ArgTypeInfo = argTypeInfo, + IsByRef = isByRef, + Locations = locations, + }; + } + } +} 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..be0b56bd3fdabd --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CallingConvention_1.cs @@ -0,0 +1,167 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using Microsoft.Diagnostics.DataContractReader.Contracts.CallingConventionHelpers; +using Microsoft.Diagnostics.DataContractReader.SignatureHelpers; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts; + +/// +/// CoreCLR implementation of . Decodes method +/// signatures and drives a per-arch subclass to +/// compute per-argument offsets and pass-style for a call site. +/// +internal sealed class CallingConvention_1 : ICallingConvention +{ + private static readonly IReadOnlyList EmptyArgs = Array.Empty(); + private static readonly CallSiteLayout EmptyLayout = new(null, false, null, null, EmptyArgs); + + private readonly Target _target; + private readonly TransitionBlockLayout _layout; + + public CallingConvention_1(Target target) + { + _target = target; + _layout = new TransitionBlockLayout(_target); + } + + CallSiteLayout ICallingConvention.ComputeCallSiteLayout(MethodDescHandle method) + { + if (!TryDecodeSignature(method, out MethodSignature methodSig)) + return EmptyLayout; + + bool isVarArg = methodSig.Header.CallingConvention is SignatureCallingConvention.VarArgs; + + bool hasThis = methodSig.Header.IsInstance; + bool requiresInstArg = false; + bool isAsync = false; + bool isValueTypeThis = false; + + try + { + IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem; + requiresInstArg = rts.GetGenericContextLoc(method) == GenericContextLoc.InstArg; + isAsync = rts.IsAsyncMethod(method); + if (hasThis) + { + TargetPointer methodTablePtr = rts.GetMethodTable(method); + TypeHandle enclosingType = rts.GetTypeHandle(methodTablePtr); + isValueTypeThis = rts.IsValueType(enclosingType); + } + } + catch (System.Exception ex) + { + Debug.Fail(ex.ToString()); + } + + ArgTypeInfo[] paramTypes = new ArgTypeInfo[methodSig.ParameterTypes.Length]; + for (int i = 0; i < paramTypes.Length; i++) + paramTypes[i] = methodSig.ParameterTypes[i]; + + ArgIteratorData argData = new( + hasThis, + isVarArg, + paramTypes, + methodSig.ReturnType); + + ArgIteratorBase argit = ArgIteratorFactory.Create( + _layout, + argData, + hasParamType: requiresInstArg, + hasAsyncContinuation: isAsync); + + int? thisOffset = argit.HasThis ? argit.GetThisOffset() : null; + int? asyncOffset = argit.HasAsyncContinuation ? argit.GetAsyncContinuationArgOffset() : null; + int? varArgCookieOffset = isVarArg ? argit.GetVASigCookieOffset() : null; + + List args = new(paramTypes.Length); + int argIndex = 0; + foreach (ArgLocDesc loc in argit.EnumerateArgs()) + { + if (argIndex >= paramTypes.Length) + break; + + var slots = new List(loc.Locations.Count); + foreach (ArgLocation l in loc.Locations) + slots.Add(new ArgSlot(l.TransitionBlockOffset, l.ElementType)); + + args.Add(new ArgLayout(loc.IsByRef, slots)); + argIndex++; + } + + return new CallSiteLayout(thisOffset, isValueTypeThis, asyncOffset, varArgCookieOffset, args); + } + + /// + /// Decodes the signature for into a + /// . Matches native + /// MethodDesc::GetSig: prefers a stored signature (dynamic, EEImpl, and + /// array method descs) before falling back to a metadata token lookup. + /// + private bool TryDecodeSignature(MethodDescHandle method, out MethodSignature methodSig) + { + methodSig = default; + try + { + IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem; + + TargetPointer methodTablePtr = rts.GetMethodTable(method); + TypeHandle typeHandle = rts.GetTypeHandle(methodTablePtr); + TargetPointer modulePtr = rts.GetModule(typeHandle); + + ModuleHandle moduleHandle = _target.Contracts.Loader.GetModuleHandleFromModulePtr(modulePtr); + MetadataReader? mdReader = _target.Contracts.EcmaMetadata.GetMetadata(moduleHandle); + + ArgTypeInfoSignatureProvider provider = new(_target, moduleHandle); + ArgTypeInfoSignatureContext genericContext = new(typeHandle, method); + + if (rts.IsStoredSigMethodDesc(method, out ReadOnlySpan storedSig)) + { + // Stored sigs (dynamic, EEImpl, array method descs) decode without needing + // a metadata reader for primitive or ELEMENT_TYPE_INTERNAL element types. + // A null reader is only a problem if the stored sig references a TypeDef / + // TypeRef / TypeSpec, which is unusual for these method-desc kinds; the + // outer catch handles that case gracefully. + RuntimeSignatureDecoder storedDecoder = new( + provider, _target, mdReader!, genericContext); + unsafe + { + fixed (byte* pStoredSig = storedSig) + { + BlobReader blobReader = new BlobReader(pStoredSig, storedSig.Length); + methodSig = storedDecoder.DecodeMethodSignature(ref blobReader); + } + } + return true; + } + + // Non-stored-sig path: needs a real metadata reader to look up the method def. + if (mdReader is null) + return false; + + RuntimeSignatureDecoder decoder = new( + provider, _target, mdReader, genericContext); + + uint methodToken = rts.GetMethodToken(method); + if (methodToken == (uint)EcmaMetadataUtils.TokenType.mdtMethodDef) + return false; + + MethodDefinitionHandle methodDefHandle = MetadataTokens.MethodDefinitionHandle( + (int)EcmaMetadataUtils.GetRowId(methodToken)); + MethodDefinition methodDef = mdReader.GetMethodDefinition(methodDefHandle); + + BlobReader bodyReader = mdReader.GetBlobReader(methodDef.Signature); + methodSig = decoder.DecodeMethodSignature(ref bodyReader); + return true; + } + catch + { + return false; + } + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/RiscV64LoongArch64ArgIterator.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/RiscV64LoongArch64ArgIterator.cs new file mode 100644 index 00000000000000..b04e7e3d35b19f --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/RiscV64LoongArch64ArgIterator.cs @@ -0,0 +1,98 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.CallingConventionHelpers; + +/// +/// Shared iterator for the current cDAC RISC-V64 / LoongArch64 implementation. +/// Floating-point scalars use the FP bank, integer-like values use the GP bank, +/// and overflow spills to the stack. Large value types are passed by implicit byref. +/// +internal sealed class RiscV64LoongArch64ArgIterator : ArgIteratorBase +{ + public override int NumArgumentRegisters => 8; + public override int NumFloatArgumentRegisters => 8; + public override int FloatRegisterSize => 8; + public override int EnregisteredParamTypeMaxSize => 16; + public override int EnregisteredReturnTypeIntegerMaxSize => 16; + public override int StackSlotSize => 8; + public override bool IsRetBuffPassedAsFirstArg => true; + + public RiscV64LoongArch64ArgIterator( + TransitionBlockLayout layout, + ArgIteratorData argData, + bool hasParamType, + bool hasAsyncContinuation) + : base(layout, argData, hasParamType, hasAsyncContinuation) + { + } + + public override bool IsArgPassedByRefBySize(int size) => size > EnregisteredParamTypeMaxSize; + + public override IEnumerable EnumerateArgs() + { + int idxGenReg = ComputeInitialNumRegistersUsed(); + int idxFPReg = 0; + int ofsStack = 0; + + for (int argNum = 0; argNum < NumFixedArgs; argNum++) + { + CorElementType argType = GetArgumentType(argNum, out ArgTypeInfo argTypeInfo); + int argSize = GetElemSize(argType, argTypeInfo, _layout.PointerSize); + bool isByRef = argType == CorElementType.Byref + || (argType == CorElementType.ValueType && argSize > EnregisteredParamTypeMaxSize); + int effectiveArgSize = isByRef ? _layout.PointerSize : argSize; + int cbArg = StackElemSize(effectiveArgSize, argType == CorElementType.ValueType, false); + + ArgLocation location; + if ((argType == CorElementType.R4 || argType == CorElementType.R8) && idxFPReg < NumFloatArgumentRegisters && !IsVarArg) + { + location = new ArgLocation + { + Kind = ArgLocationKind.FpRegister, + TransitionBlockOffset = _layout.OffsetOfFloatArgumentRegisters + (idxFPReg * FloatRegisterSize), + Size = FloatRegisterSize, + ElementType = argType, + }; + idxFPReg++; + } + else + { + int regSlots = AlignUp(cbArg, _layout.PointerSize) / _layout.PointerSize; + if (idxGenReg + regSlots <= NumArgumentRegisters) + { + location = new ArgLocation + { + Kind = ArgLocationKind.GpRegister, + TransitionBlockOffset = _layout.ArgumentRegistersOffset + (idxGenReg * _layout.PointerSize), + Size = regSlots * _layout.PointerSize, + ElementType = argType, + }; + idxGenReg += regSlots; + } + else + { + location = new ArgLocation + { + Kind = ArgLocationKind.Stack, + TransitionBlockOffset = _layout.OffsetOfArgs + ofsStack, + Size = cbArg, + ElementType = argType, + }; + ofsStack += cbArg; + } + } + + yield return new ArgLocDesc + { + ArgType = argType, + ArgSize = argSize, + ArgTypeInfo = argTypeInfo, + IsByRef = isByRef, + Locations = [location], + }; + } + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/SystemVStructClassifier.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/SystemVStructClassifier.cs new file mode 100644 index 00000000000000..ec92a61ade7cb4 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/SystemVStructClassifier.cs @@ -0,0 +1,459 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using Microsoft.Diagnostics.DataContractReader.RuntimeTypeSystemHelpers; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.CallingConventionHelpers; + +/// +/// SystemV AMD64 ABI struct classifier. Walks a value type's instance fields, +/// builds a per-byte map of field classifications, and assembles eightbyte +/// classifications per the SystemV spec (§3.2.3 "Passing"). Mirrors +/// SystemVStructClassificator.GetSystemVAmd64PassStructInRegisterDescriptor +/// in src/coreclr/tools/Common/JitInterface/SystemVStructClassificator.cs. +/// +/// +/// Used only by to decide between register +/// placement (with possible GP+SSE split), implicit by-reference, and stack +/// passing for value types on Linux/macOS x64. +/// +internal static class SystemVStructClassifier +{ + private const int MaxFields = SystemVStructDescriptor.MaxEightBytes * SystemVStructDescriptor.EightByteSizeInBytes; + + private struct Helper + { + public int StructSize; + public int EightByteCount; + public SystemVClassification[] EightByteClassifications; + public int[] EightByteSizes; + public int[] EightByteOffsets; + + public bool InEmbeddedStruct; + public int CurrentUniqueOffsetField; + public int LargestFieldOffset; + public SystemVClassification[] FieldClassifications; + public int[] FieldSizes; + public int[] FieldOffsets; + + public static Helper Create(int totalStructSize) => new() + { + StructSize = totalStructSize, + EightByteCount = 0, + InEmbeddedStruct = false, + CurrentUniqueOffsetField = 0, + LargestFieldOffset = -1, + EightByteClassifications = new SystemVClassification[SystemVStructDescriptor.MaxEightBytes], + EightByteSizes = new int[SystemVStructDescriptor.MaxEightBytes], + EightByteOffsets = new int[SystemVStructDescriptor.MaxEightBytes], + FieldClassifications = new SystemVClassification[MaxFields], + FieldSizes = new int[MaxFields], + FieldOffsets = new int[MaxFields], + }; + } + + /// + /// Attempts to classify the given value type per the SystemV AMD64 ABI. + /// Returns a descriptor with + /// set to true when the struct can be passed in registers, false otherwise. + /// + public static SystemVStructDescriptor Classify(Target target, TypeHandle typeHandle, int structSize) + { + if (!typeHandle.IsMethodTable() || structSize == 0 + || structSize > SystemVStructDescriptor.MaxStructBytesToPassInRegisters) + { + return default; + } + + IRuntimeTypeSystem rts = target.Contracts.RuntimeTypeSystem; + if (!rts.IsValueType(typeHandle)) + return default; + + // Intrinsic SIMD/Int128 types bypass the SysV struct path (they're handled by + // their own intrinsic register-passing rules at the JIT level). + if (IsSimdOrInt128Intrinsic(rts, typeHandle)) + return default; + + Helper helper = Helper.Create(structSize); + if (!ClassifyEightBytes(target, rts, typeHandle, ref helper, 0)) + return default; + + return new SystemVStructDescriptor + { + PassedInRegisters = true, + EightByteCount = (byte)helper.EightByteCount, + EightByteClassification0 = helper.EightByteClassifications[0], + EightByteClassification1 = helper.EightByteClassifications[1], + EightByteSize0 = (byte)helper.EightByteSizes[0], + EightByteSize1 = (byte)helper.EightByteSizes[1], + EightByteOffset0 = (byte)helper.EightByteOffsets[0], + EightByteOffset1 = (byte)helper.EightByteOffsets[1], + }; + } + + private static bool IsSimdOrInt128Intrinsic(IRuntimeTypeSystem rts, TypeHandle th) + { + // Vector64/128/256/512 of T and Int128/UInt128 carry a non-zero VectorSize on cDAC. + // If the type-system contract reports a vector size, treat it as an intrinsic that + // bypasses SysV struct classification. + try + { + return rts.GetVectorSize(th) != 0; + } + catch + { + return false; + } + } + + /// + /// Maps a primitive to its initial SystemV + /// classification. Mirrors TypeDef2SystemVClassification in the JIT + /// classifier. + /// + private static SystemVClassification CorElementTypeToClassification(CorElementType et) => et switch + { + CorElementType.Boolean or CorElementType.Char + or CorElementType.I1 or CorElementType.U1 + or CorElementType.I2 or CorElementType.U2 + or CorElementType.I4 or CorElementType.U4 + or CorElementType.I8 or CorElementType.U8 + or CorElementType.I or CorElementType.U + or CorElementType.Ptr or CorElementType.FnPtr + => SystemVClassification.Integer, + CorElementType.R4 or CorElementType.R8 + => SystemVClassification.SSE, + CorElementType.ValueType + => SystemVClassification.Struct, // recurse + CorElementType.Class or CorElementType.Object or CorElementType.String + or CorElementType.Array or CorElementType.SzArray + or CorElementType.Var or CorElementType.MVar + or CorElementType.GenericInst + => SystemVClassification.IntegerReference, + CorElementType.Byref + => SystemVClassification.IntegerByRef, + _ => SystemVClassification.Unknown, + }; + + /// + /// Merge lattice for overlapping/union fields. Matches + /// SystemVStructClassificator.ReClassifyField. + /// + private static SystemVClassification ReClassifyField(SystemVClassification original, SystemVClassification @new) + { + switch (@new) + { + case SystemVClassification.Integer: + // Integer overrides everything; the resulting class is Integer. + return SystemVClassification.Integer; + case SystemVClassification.SSE: + // If both old and new are SSE, the merge is SSE. Otherwise Integer wins. + return original == SystemVClassification.SSE + ? SystemVClassification.SSE + : SystemVClassification.Integer; + case SystemVClassification.IntegerReference: + // IntegerReference can only merge with itself. + return SystemVClassification.IntegerReference; + case SystemVClassification.IntegerByRef: + // IntegerByRef can only merge with itself. + return SystemVClassification.IntegerByRef; + default: + return SystemVClassification.Unknown; + } + } + + /// + /// Walks the instance fields of , classifying each. + /// Returns false if the struct cannot be enregistered (unaligned field, embedded + /// struct that can't enregister, etc.); true if the helper has been populated + /// successfully. + /// + private static bool ClassifyEightBytes( + Target target, + IRuntimeTypeSystem rts, + TypeHandle typeHandle, + ref Helper helper, + int startOffsetOfStruct) + { + ushort numInstanceFields = rts.GetNumInstanceFields(typeHandle); + TargetPointer firstFieldDesc = rts.GetFieldDescList(typeHandle); + + if (firstFieldDesc == TargetPointer.Null || numInstanceFields == 0) + { + // Empty struct: classify like padding. + helper.LargestFieldOffset = startOffsetOfStruct; + AssignClassifiedEightByteTypes(ref helper); + return true; + } + + uint fieldDescSize = (uint)target.GetTypeInfo(DataType.FieldDesc).Size!; + + // Walk the FieldDesc array. Each FieldDesc has a fixed size; statics are + // intermixed with instance fields in this array, so we filter by IsStatic. + ushort totalFields = (ushort)(numInstanceFields + rts.GetNumStaticFields(typeHandle) + + rts.GetNumThreadStaticFields(typeHandle)); + + for (ushort i = 0; i < totalFields; i++) + { + TargetPointer fdPtr = new(firstFieldDesc.Value + i * fieldDescSize); + if (rts.IsFieldDescStatic(fdPtr)) + continue; + + CorElementType fieldType = rts.GetFieldDescType(fdPtr); + uint memberDef = rts.GetFieldDescMemberDef(fdPtr); + EntityHandle entity = MetadataTokens.EntityHandle((int)memberDef); + if (entity.IsNil || entity.Kind != HandleKind.FieldDefinition) + return false; + + // Resolve the field's declared type (only needed for ValueType fields, but + // the offset comes from FieldDef in all cases). + FieldDefinition fieldDef = default; + ModuleHandle moduleHandle = default; + MetadataReader? mdReader = null; + try + { + TargetPointer enclosingMT = rts.GetMTOfEnclosingClass(fdPtr); + TypeHandle ctx = rts.GetTypeHandle(enclosingMT); + TargetPointer modulePtr = rts.GetModule(ctx); + if (modulePtr != TargetPointer.Null) + { + moduleHandle = target.Contracts.Loader.GetModuleHandleFromModulePtr(modulePtr); + mdReader = target.Contracts.EcmaMetadata.GetMetadata(moduleHandle); + if (mdReader is not null) + fieldDef = mdReader.GetFieldDefinition((FieldDefinitionHandle)entity); + } + } + catch + { + return false; + } + + uint fieldOffset = rts.GetFieldDescOffset(fdPtr, fieldDef); + int normalizedFieldOffset = (int)fieldOffset + startOffsetOfStruct; + + int fieldSize = ArgTypeInfo.GetElemSize(fieldType, default, target.PointerSize); + if (fieldType == CorElementType.ValueType) + { + // For nested value-type fields, resolve the field's TypeHandle to get its size. + TypeHandle fieldTH = ResolveFieldTypeHandle(target, rts, fdPtr, mdReader, fieldDef, moduleHandle); + if (fieldTH.IsMethodTable()) + fieldSize = rts.GetNumInstanceFieldBytes(fieldTH); + } + + if (normalizedFieldOffset + fieldSize > helper.StructSize) + return false; + + SystemVClassification fieldClass = CorElementTypeToClassification(fieldType); + if (fieldClass == SystemVClassification.Struct) + { + // Recurse into the nested value type's fields. + TypeHandle nested = ResolveFieldTypeHandle(target, rts, fdPtr, mdReader, fieldDef, moduleHandle); + if (!nested.IsMethodTable()) + return false; + + bool savedInEmbedded = helper.InEmbeddedStruct; + helper.InEmbeddedStruct = true; + bool nestedOk = ClassifyEightBytes(target, rts, nested, ref helper, normalizedFieldOffset); + helper.InEmbeddedStruct = savedInEmbedded; + if (!nestedOk) + return false; + continue; + } + + // Unaligned-field rule: a field that is not naturally aligned forces MEMORY. + if (fieldSize > 0 && (normalizedFieldOffset % fieldSize) != 0) + return false; + + // Overlapping-field (union) handling: if this offset has already been + // recorded, merge the new classification with the existing one. + if (normalizedFieldOffset <= helper.LargestFieldOffset) + { + int existing = -1; + for (int j = helper.CurrentUniqueOffsetField - 1; j >= 0; j--) + { + if (helper.FieldOffsets[j] == normalizedFieldOffset) + { + if (fieldSize > helper.FieldSizes[j]) + helper.FieldSizes[j] = fieldSize; + helper.FieldClassifications[j] = ReClassifyField(helper.FieldClassifications[j], fieldClass); + existing = j; + break; + } + } + if (existing >= 0) + continue; + } + else + { + helper.LargestFieldOffset = normalizedFieldOffset; + } + + if (helper.CurrentUniqueOffsetField >= MaxFields) + return false; + + helper.FieldClassifications[helper.CurrentUniqueOffsetField] = fieldClass; + helper.FieldSizes[helper.CurrentUniqueOffsetField] = fieldSize; + helper.FieldOffsets[helper.CurrentUniqueOffsetField] = normalizedFieldOffset; + helper.CurrentUniqueOffsetField++; + } + + AssignClassifiedEightByteTypes(ref helper); + return true; + } + + /// + /// Resolves a FieldDesc's declared type to its . Used for + /// nested-value-type recursion. Returns a null TypeHandle if resolution fails. + /// + private static TypeHandle ResolveFieldTypeHandle( + Target target, + IRuntimeTypeSystem rts, + TargetPointer fdPtr, + MetadataReader? mdReader, + FieldDefinition fieldDef, + ModuleHandle moduleHandle) + { + if (mdReader is null || moduleHandle.Address == TargetPointer.Null) + return default; + + try + { + TargetPointer enclosingMT = rts.GetMTOfEnclosingClass(fdPtr); + TypeHandle ctx = rts.GetTypeHandle(enclosingMT); + return target.Contracts.Signature.DecodeFieldSignature(fieldDef.Signature, moduleHandle, ctx); + } + catch + { + return default; + } + } + + /// + /// Byte-by-byte sweep that assembles eightbyte classifications from the + /// per-field classifications recorded in . Matches + /// SystemVStructClassificator.AssignClassifiedEightByteTypes. + /// + private static void AssignClassifiedEightByteTypes(ref Helper helper) + { + const int MaxBytes = SystemVStructDescriptor.MaxEightBytes * SystemVStructDescriptor.EightByteSizeInBytes; + if (helper.InEmbeddedStruct) + return; + + int largestFieldOffset = helper.LargestFieldOffset; + if (largestFieldOffset < 0) + largestFieldOffset = 0; + + Span sortedFieldOrder = stackalloc int[MaxBytes]; + for (int i = 0; i < MaxBytes; i++) sortedFieldOrder[i] = -1; + + int numFields = helper.CurrentUniqueOffsetField; + for (int i = 0; i < numFields; i++) + { + int off = helper.FieldOffsets[i]; + if (off < 0 || off >= MaxBytes) continue; + sortedFieldOrder[off] = i; + } + + int lastFieldOrdinal = largestFieldOffset < MaxBytes ? sortedFieldOrder[largestFieldOffset] : -1; + int lastFieldSize = lastFieldOrdinal >= 0 ? helper.FieldSizes[lastFieldOrdinal] : 0; + int offsetAfterLastFieldByte = largestFieldOffset + lastFieldSize; + SystemVClassification lastFieldClassification = lastFieldOrdinal >= 0 + ? helper.FieldClassifications[lastFieldOrdinal] + : SystemVClassification.NoClass; + + int usedEightBytes = 0; + int accumulatedSizeForEightBytes = 0; + bool foundFieldInEightByte = false; + + for (int offset = 0; offset < helper.StructSize; offset++) + { + SystemVClassification fieldClassificationType; + int fieldSize; + + int ordinal = offset < MaxBytes ? sortedFieldOrder[offset] : -1; + if (ordinal == -1) + { + if (offset < accumulatedSizeForEightBytes) + continue; // inside a previously-processed field + + fieldSize = 1; + // Padding before the last field's end -> NoClass; trailing padding + // inherits the last field's classification (per spec). + fieldClassificationType = offset < offsetAfterLastFieldByte + ? SystemVClassification.NoClass + : lastFieldClassification; + if (offset % SystemVStructDescriptor.EightByteSizeInBytes == 0) + foundFieldInEightByte = false; + } + else + { + foundFieldInEightByte = true; + fieldSize = helper.FieldSizes[ordinal]; + fieldClassificationType = helper.FieldClassifications[ordinal]; + accumulatedSizeForEightBytes = offset + fieldSize; + } + + int fieldStartEightByte = offset / SystemVStructDescriptor.EightByteSizeInBytes; + int fieldEndEightByte = (offset + fieldSize - 1) / SystemVStructDescriptor.EightByteSizeInBytes; + if (fieldEndEightByte >= SystemVStructDescriptor.MaxEightBytes) + return; // shouldn't happen for size <= 16, but guard anyway + + usedEightBytes = System.Math.Max(usedEightBytes, fieldEndEightByte + 1); + + for (int eb = fieldStartEightByte; eb <= fieldEndEightByte; eb++) + { + SystemVClassification existing = helper.EightByteClassifications[eb]; + if (existing == fieldClassificationType) + { + // Already this class + } + else if (existing == SystemVClassification.NoClass) + { + helper.EightByteClassifications[eb] = fieldClassificationType; + } + else if (existing == SystemVClassification.Memory + || fieldClassificationType == SystemVClassification.Memory + || existing == SystemVClassification.IntegerReference + || fieldClassificationType == SystemVClassification.IntegerReference + || existing == SystemVClassification.IntegerByRef + || fieldClassificationType == SystemVClassification.IntegerByRef) + { + helper.EightByteClassifications[eb] = ReClassifyField(existing, fieldClassificationType); + } + else if (existing == SystemVClassification.Integer + || fieldClassificationType == SystemVClassification.Integer) + { + helper.EightByteClassifications[eb] = SystemVClassification.Integer; + } + else + { + helper.EightByteClassifications[eb] = SystemVClassification.SSE; + } + } + + if ((offset + 1) % SystemVStructDescriptor.EightByteSizeInBytes == 0) + { + // Promote NoClass eightbyte to Integer when there's no field in it + // (matches the workaround in methodtable.cpp:2660). + int eb = offset / SystemVStructDescriptor.EightByteSizeInBytes; + if (!foundFieldInEightByte + && helper.EightByteClassifications[eb] == SystemVClassification.NoClass) + { + helper.EightByteClassifications[eb] = SystemVClassification.Integer; + } + foundFieldInEightByte = false; + } + } + + helper.EightByteCount = usedEightBytes; + for (int i = 0; i < usedEightBytes; i++) + { + helper.EightByteOffsets[i] = i * SystemVStructDescriptor.EightByteSizeInBytes; + int remaining = helper.StructSize - helper.EightByteOffsets[i]; + helper.EightByteSizes[i] = System.Math.Min(remaining, SystemVStructDescriptor.EightByteSizeInBytes); + } + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/TransitionBlockLayout.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/TransitionBlockLayout.cs new file mode 100644 index 00000000000000..3e30af11edec96 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/TransitionBlockLayout.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.CallingConventionHelpers; + +/// +/// Pure-data view over the target's TransitionBlock layout. Holds the +/// descriptor-read offsets plus the target's +/// and . Has no per-architecture logic — +/// per-arch ABI knowledge lives entirely in the +/// hierarchy. +/// +internal sealed class TransitionBlockLayout +{ + public Target Target { get; } + public int SizeOfTransitionBlock { get; } + public int ArgumentRegistersOffset { get; } + public int FirstGCRefMapSlot { get; } + public int OffsetOfArgs { get; } + public int OffsetOfFloatArgumentRegisters { get; } + public int PointerSize { get; } + public RuntimeInfoArchitecture Architecture { get; } + public RuntimeInfoOperatingSystem OperatingSystem { get; } + + public TransitionBlockLayout(Target target) + { + Target = target; + IRuntimeInfo runtimeInfo = target.Contracts.RuntimeInfo; + Architecture = runtimeInfo.GetTargetArchitecture(); + OperatingSystem = runtimeInfo.GetTargetOperatingSystem(); + PointerSize = target.PointerSize; + + Target.TypeInfo tbType = target.GetTypeInfo(DataType.TransitionBlock); + SizeOfTransitionBlock = (int)tbType.Size!; + ArgumentRegistersOffset = tbType.Fields["ArgumentRegistersOffset"].Offset; + FirstGCRefMapSlot = tbType.Fields["FirstGCRefMapSlot"].Offset; + OffsetOfArgs = tbType.Fields["OffsetOfArgs"].Offset; + OffsetOfFloatArgumentRegisters = tbType.Fields.ContainsKey("OffsetOfFloatArgumentRegisters") + ? tbType.Fields["OffsetOfFloatArgumentRegisters"].Offset + : 0; + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/X86ArgIterator.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/X86ArgIterator.cs new file mode 100644 index 00000000000000..0335eff14fbfff --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/X86ArgIterator.cs @@ -0,0 +1,281 @@ +// 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.Diagnostics; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.CallingConventionHelpers; + +/// +/// x86 managed calling-convention iterator. User arguments are enregistered into +/// ECX/EDX when eligible; remaining arguments are laid out on the stack in the +/// native x86 reverse-push order. +/// +internal sealed class X86ArgIterator : ArgIteratorBase +{ + private enum ParamTypeLocation + { + Stack, + Ecx, + Edx, + } + + private enum AsyncContinuationLocation + { + Stack, + Ecx, + Edx, + } + + private ParamTypeLocation _paramTypeLoc; + private AsyncContinuationLocation _asyncContinuationLoc; + + public override int NumArgumentRegisters => 2; + public override int NumFloatArgumentRegisters => 0; + public override int FloatRegisterSize => 0; + public override int EnregisteredParamTypeMaxSize => 0; + public override int EnregisteredReturnTypeIntegerMaxSize => 4; + public override int StackSlotSize => 4; + public override bool IsRetBuffPassedAsFirstArg => true; + + public X86ArgIterator( + TransitionBlockLayout layout, + ArgIteratorData argData, + bool hasParamType, + bool hasAsyncContinuation) + : base(layout, argData, hasParamType, hasAsyncContinuation) + { + } + + public override int GetThisOffset() + => _layout.ArgumentRegistersOffset + _layout.PointerSize; + + public override int OffsetFromGCRefMapPos(int pos) + { + if (pos < NumArgumentRegisters) + { + return _layout.FirstGCRefMapSlot + SizeOfArgumentRegisters - ((pos + 1) * _layout.PointerSize); + } + + return _layout.OffsetOfArgs + ((pos - NumArgumentRegisters) * _layout.PointerSize); + } + + public override int GetRetBuffArgOffset(bool hasThis) + => _layout.ArgumentRegistersOffset + (hasThis ? 0 : _layout.PointerSize); + + public override uint CbStackPop() + => IsVarArg ? 0u : SizeOfArgStack(); + + public override int GetVASigCookieOffset() + { + Debug.Assert(IsVarArg); + return _layout.SizeOfTransitionBlock; + } + + public override int GetParamTypeArgOffset() + { + Debug.Assert(HasParamType); + _ = SizeOfArgStack(); + + return _paramTypeLoc switch + { + ParamTypeLocation.Ecx => _layout.ArgumentRegistersOffset + _layout.PointerSize, + ParamTypeLocation.Edx => _layout.ArgumentRegistersOffset, + _ => _layout.SizeOfTransitionBlock, + }; + } + + public override int GetAsyncContinuationArgOffset() + { + Debug.Assert(HasAsyncContinuation); + _ = SizeOfArgStack(); + + return _asyncContinuationLoc switch + { + AsyncContinuationLocation.Ecx => _layout.ArgumentRegistersOffset + _layout.PointerSize, + AsyncContinuationLocation.Edx => _layout.ArgumentRegistersOffset, + _ => HasParamType && _paramTypeLoc == ParamTypeLocation.Stack + ? _layout.SizeOfTransitionBlock + _layout.PointerSize + : _layout.SizeOfTransitionBlock, + }; + } + + protected override int ComputeInitialNumRegistersUsed() + { + int numRegistersUsed = 0; + + if (HasThis) + { + numRegistersUsed++; + } + + if (HasRetBuffArg() && IsRetBuffPassedAsFirstArg) + { + numRegistersUsed++; + } + + return numRegistersUsed; + } + + protected override void ComputeSizeOfArgStack() + { + int numRegistersUsed = 0; + int sizeOfArgStack = 0; + + if (HasThis) + { + numRegistersUsed++; + } + + if (HasRetBuffArg() && IsRetBuffPassedAsFirstArg) + { + numRegistersUsed++; + } + + if (IsVarArg) + { + sizeOfArgStack += _layout.PointerSize; + numRegistersUsed = NumArgumentRegisters; + } + + for (int argNum = 0; argNum < NumFixedArgs; argNum++) + { + CorElementType argType = GetArgumentType(argNum, out ArgTypeInfo argTypeInfo); + int argSize = GetElemSize(argType, argTypeInfo, _layout.PointerSize); + if (!IsArgumentInRegister(ref numRegistersUsed, argType, argSize)) + { + sizeOfArgStack += StackElemSize(argSize); + } + } + + if (HasAsyncContinuation) + { + if (numRegistersUsed < NumArgumentRegisters) + { + numRegistersUsed++; + _asyncContinuationLoc = numRegistersUsed == 1 + ? AsyncContinuationLocation.Ecx + : AsyncContinuationLocation.Edx; + } + else + { + sizeOfArgStack += _layout.PointerSize; + _asyncContinuationLoc = AsyncContinuationLocation.Stack; + } + } + + if (HasParamType) + { + if (numRegistersUsed < NumArgumentRegisters) + { + numRegistersUsed++; + _paramTypeLoc = numRegistersUsed == 1 + ? ParamTypeLocation.Ecx + : ParamTypeLocation.Edx; + } + else + { + sizeOfArgStack += _layout.PointerSize; + _paramTypeLoc = ParamTypeLocation.Stack; + } + } + + _nSizeOfArgStack = AlignUp(sizeOfArgStack, StackElemSize(_layout.PointerSize)); + } + + public override IEnumerable EnumerateArgs() + { + int stackSize = (int)SizeOfArgStack(); + int numRegistersUsed = ComputeInitialNumRegistersUsed(); + int ofsStack = _layout.OffsetOfArgs + stackSize; + + if (IsVarArg) + { + numRegistersUsed = NumArgumentRegisters; + } + + for (int argNum = 0; argNum < NumFixedArgs; argNum++) + { + CorElementType argType = GetArgumentType(argNum, out ArgTypeInfo argTypeInfo); + int argSize = GetElemSize(argType, argTypeInfo, _layout.PointerSize); + + ArgLocation location; + if (IsArgumentInRegister(ref numRegistersUsed, argType, argSize)) + { + location = new ArgLocation + { + Kind = ArgLocationKind.GpRegister, + TransitionBlockOffset = _layout.ArgumentRegistersOffset + ((NumArgumentRegisters - numRegistersUsed) * _layout.PointerSize), + Size = _layout.PointerSize, + ElementType = argType, + }; + } + else + { + int stackElemSize = StackElemSize(argSize); + ofsStack -= stackElemSize; + location = new ArgLocation + { + Kind = ArgLocationKind.Stack, + TransitionBlockOffset = ofsStack, + Size = stackElemSize, + ElementType = argType, + }; + } + + yield return new ArgLocDesc + { + ArgType = argType, + ArgSize = argSize, + ArgTypeInfo = argTypeInfo, + IsByRef = argType == CorElementType.Byref, + Locations = [location], + }; + } + } + + private static bool IsArgumentInRegister(ref int numRegistersUsed, CorElementType elementType, int argSize) + { + if (numRegistersUsed >= 2) + { + return false; + } + + bool enregister = elementType switch + { + CorElementType.Boolean or + CorElementType.Char or + CorElementType.I1 or + CorElementType.U1 or + CorElementType.I2 or + CorElementType.U2 or + CorElementType.I4 or + CorElementType.U4 or + CorElementType.I or + CorElementType.U or + CorElementType.Ptr or + CorElementType.Byref or + CorElementType.Class or + CorElementType.Object or + CorElementType.String or + CorElementType.SzArray or + CorElementType.Array or + CorElementType.FnPtr => true, + CorElementType.ValueType => argSize is 1 or 2 or 4, + CorElementType.R4 or + CorElementType.R8 or + CorElementType.I8 or + CorElementType.U8 or + CorElementType.TypedByRef => false, + _ => false, + }; + + if (!enregister) + { + return false; + } + + numRegistersUsed++; + return true; + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs index e09129f2d1b38b..30b07003defa41 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Reflection.Metadata.Ecma335; using Microsoft.Diagnostics.DataContractReader.RuntimeTypeSystemHelpers; using Microsoft.Diagnostics.DataContractReader.Data; @@ -575,6 +576,63 @@ public bool IsObjRef(TypeHandle typeHandle) } public bool ContainsGCPointers(TypeHandle typeHandle) => !typeHandle.IsMethodTable() ? false : _methodTables[typeHandle.Address].Flags.ContainsGCPointers; public bool RequiresAlign8(TypeHandle typeHandle) => !typeHandle.IsMethodTable() ? false : _methodTables[typeHandle.Address].Flags.RequiresAlign8; + + public bool IsHFA(TypeHandle typeHandle) + { + if (!typeHandle.IsMethodTable()) + return false; + + // The IsHFA bit in the MethodTable flags is only meaningful when the target runtime + // was built with FEATURE_HFA. On other configurations the bit is either unused or + // reused for a different purpose (UNIX_AMD64_ABI uses it as IsRegStructPassed). + if (!_target.TryReadGlobal(Constants.Globals.FeatureHFA, out byte? featureHFA) || featureHFA == 0) + return false; + + return _methodTables[typeHandle.Address].Flags.IsHFA; + } + + public int GetVectorSize(TypeHandle typeHandle) + { + // Mirrors MethodTable::GetVectorSize in src/coreclr/vm/class.cpp. + + if (!typeHandle.IsMethodTable() || + _target.PointerSize != 8 || + !_methodTables[typeHandle.Address].Flags.IsIntrinsicType || + !TryGetMetadataReader(typeHandle, out _, out MetadataReader? mdReader)) + { + return 0; + } + + uint typeDefTok = GetTypeDefToken(typeHandle); + EntityHandle entityHandle = MetadataTokens.EntityHandle((int)typeDefTok); + if (entityHandle.IsNil || entityHandle.Kind != HandleKind.TypeDefinition) + return 0; + + TypeDefinition typeDef = mdReader.GetTypeDefinition((TypeDefinitionHandle)entityHandle); + string name = mdReader.GetString(typeDef.Name); + string ns = mdReader.GetString(typeDef.Namespace); + + // Mirrors MethodTable::GetVectorSize in src/coreclr/vm/class.cpp. + int vectorSize = (ns, name) switch + { + // System.Numerics.Vector`1's runtime size depends on the runtime-chosen vector width. + ("System.Numerics", "Vector`1") => GetNumInstanceFieldBytes(typeHandle), + ("System.Runtime.Intrinsics", "Vector128`1") => 16, + ("System.Runtime.Intrinsics", "Vector64`1") => 8, + _ => 0, + }; + if (vectorSize == 0) + return 0; + + // Validate that T (the element type) is a primitive type, matching the runtime check. + TypeHandle typeArg = GetInstantiation(typeHandle)[0]; + CorElementType elemType = GetSignatureCorElementType(typeArg); + bool isPrimitive = (elemType >= CorElementType.I1 && elemType <= CorElementType.R8) + || elemType == CorElementType.I + || elemType == CorElementType.U; + return isPrimitive ? vectorSize : 0; + } + public bool IsContinuation(TypeHandle typeHandle) => typeHandle.IsMethodTable() && _continuationMethodTablePointer != TargetPointer.Null && _methodTables[typeHandle.Address].ParentMethodTable == _continuationMethodTablePointer; @@ -2036,10 +2094,6 @@ TargetPointer IRuntimeTypeSystem.GetFieldDescByName(TypeHandle typeHandle, strin if (!typeHandle.IsMethodTable()) return TargetPointer.Null; - TargetPointer modulePtr = GetModule(typeHandle); - if (modulePtr == TargetPointer.Null) - return TargetPointer.Null; - uint typeDefToken = GetTypeDefToken(typeHandle); if (typeDefToken == 0) return TargetPointer.Null; @@ -2050,12 +2104,10 @@ TargetPointer IRuntimeTypeSystem.GetFieldDescByName(TypeHandle typeHandle, strin TypeDefinitionHandle typeDefHandle = (TypeDefinitionHandle)entityHandle; - ILoader loader = _target.Contracts.Loader; - ModuleHandle moduleHandle = loader.GetModuleHandleFromModulePtr(modulePtr); - MetadataReader? md = _target.Contracts.EcmaMetadata.GetMetadata(moduleHandle); - if (md is null) + if (!TryGetMetadataReader(typeHandle, out ModuleHandle moduleHandle, out MetadataReader? md)) return TargetPointer.Null; + ILoader loader = _target.Contracts.Loader; TargetPointer fieldDefToDescMap = loader.GetLookupTables(moduleHandle).FieldDefToDesc; foreach (FieldDefinitionHandle fieldDefHandle in md.GetTypeDefinition(typeDefHandle).GetFields()) { @@ -2090,9 +2142,6 @@ private TargetPointer GetFieldDescStaticOrThreadStaticAddress(TargetPointer fiel { TargetPointer enclosingMT = ((IRuntimeTypeSystem)this).GetMTOfEnclosingClass(fieldDescPointer); TypeHandle ctx = GetTypeHandle(enclosingMT); - TargetPointer modulePtr = GetModule(ctx); - ILoader loader = _target.Contracts.Loader; - ModuleHandle moduleHandle = loader.GetModuleHandleFromModulePtr(modulePtr); CorElementType type = ((IRuntimeTypeSystem)this).GetFieldDescType(fieldDescPointer); TargetPointer @base; if (type == CorElementType.Class || type == CorElementType.ValueType) @@ -2121,7 +2170,9 @@ private TargetPointer GetFieldDescStaticOrThreadStaticAddress(TargetPointer fiel if (@base == TargetPointer.Null) return TargetPointer.Null; - MetadataReader mdReader = _target.Contracts.EcmaMetadata.GetMetadata(moduleHandle)!; + if (!TryGetMetadataReader(ctx, out ModuleHandle moduleHandle, out MetadataReader? mdReader)) + return TargetPointer.Null; + uint token = ((IRuntimeTypeSystem)this).GetFieldDescMemberDef(fieldDescPointer); FieldDefinitionHandle fieldHandle = (FieldDefinitionHandle)MetadataTokens.Handle((int)token); FieldDefinition fieldDef = mdReader.GetFieldDefinition(fieldHandle); @@ -2155,4 +2206,33 @@ void IRuntimeTypeSystem.GetCoreLibFieldDescAndDef(string @namespace, string type MetadataReader mdReader = _target.Contracts.EcmaMetadata.GetMetadata(moduleHandle)!; fieldDef = mdReader.GetFieldDefinition(fieldHandle); } + + /// + /// Mirrors native MethodTable::GetNumInstanceFieldBytes: + /// returns BaseSize - EEClass.BaseSizePadding, the number of bytes occupied + /// by the instance fields of the type. + /// + public int GetNumInstanceFieldBytes(TypeHandle typeHandle) + => (int)GetBaseSize(typeHandle) - GetClassData(typeHandle).BaseSizePadding; + + /// + /// Gets the and for the module that + /// owns the given . Returns false if the TypeHandle does not + /// refer to a method table, has no owning module, or the module's metadata cannot be located. + /// + private bool TryGetMetadataReader(TypeHandle typeHandle, out ModuleHandle moduleHandle, [NotNullWhen(true)] out MetadataReader? mdReader) + { + moduleHandle = default; + mdReader = null; + if (!typeHandle.IsMethodTable()) + return false; + + TargetPointer modulePtr = GetModule(typeHandle); + if (modulePtr == TargetPointer.Null) + return false; + + moduleHandle = _target.Contracts.Loader.GetModuleHandleFromModulePtr(modulePtr); + mdReader = _target.Contracts.EcmaMetadata.GetMetadata(moduleHandle); + return mdReader is not null; + } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs index 71e38bd782faad..f3bd59ec4d8d49 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs @@ -3,9 +3,6 @@ using System; using System.Collections.Generic; -using System.Reflection.Metadata; -using System.Reflection.Metadata.Ecma335; -using Microsoft.Diagnostics.DataContractReader.SignatureHelpers; namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; @@ -19,6 +16,7 @@ internal class GcScanner private readonly Target _target; private readonly IExecutionManager _eman; private readonly IGCInfo _gcInfo; + private ICallingConvention? _callingConvention; private readonly FrameHelpers _frameHelpers; internal GcScanner(Target target) @@ -29,6 +27,8 @@ internal GcScanner(Target target) _frameHelpers = new FrameHelpers(target); } + private ICallingConvention CallingConvention => _callingConvention ??= _target.Contracts.CallingConvention; + /// /// Enumerates live GC slots for a managed (frameless) code frame. /// Port of native EECodeManager::EnumGcRefs (eetwain.cpp). @@ -329,133 +329,45 @@ private void PromoteCallerStack( IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem; MethodDescHandle mdh = rts.GetMethodDescHandle(methodDescPtr); - MethodSignature methodSig; - try - { - TargetPointer methodTablePtr = rts.GetMethodTable(mdh); - TypeHandle typeHandle = rts.GetTypeHandle(methodTablePtr); - TargetPointer modulePtr = rts.GetModule(typeHandle); - - ModuleHandle moduleHandle = _target.Contracts.Loader.GetModuleHandleFromModulePtr(modulePtr); - MetadataReader? mdReader = _target.Contracts.EcmaMetadata.GetMetadata(moduleHandle); - if (mdReader is null) - return; - - GcSignatureTypeProvider provider = new(_target, moduleHandle); - GcSignatureContext genericContext = new(typeHandle, mdh); - RuntimeSignatureDecoder decoder = new( - provider, _target, mdReader, genericContext); - - // Match native MethodDesc::GetSig: prefer stored signature (dynamic, EEImpl, - // and array method descs) before falling back to a metadata token lookup. - if (rts.IsStoredSigMethodDesc(mdh, out ReadOnlySpan storedSig)) - { - unsafe - { - fixed (byte* pStoredSig = storedSig) - { - BlobReader blobReader = new BlobReader(pStoredSig, storedSig.Length); - methodSig = decoder.DecodeMethodSignature(ref blobReader); - } - } - } - else - { - uint methodToken = rts.GetMethodToken(mdh); - if (methodToken == (uint)EcmaMetadataUtils.TokenType.mdtMethodDef) - return; - - MethodDefinitionHandle methodDefHandle = MetadataTokens.MethodDefinitionHandle((int)EcmaMetadataUtils.GetRowId(methodToken)); - MethodDefinition methodDef = mdReader.GetMethodDefinition(methodDefHandle); - - BlobReader blobReader = mdReader.GetBlobReader(methodDef.Signature); - methodSig = decoder.DecodeMethodSignature(ref blobReader); - } - } - catch (System.Exception) - { - return; - } - - if (methodSig.Header.CallingConvention is SignatureCallingConvention.VarArgs) - return; + CallSiteLayout layout = CallingConvention.ComputeCallSiteLayout(mdh); - bool hasThis = methodSig.Header.IsInstance; - bool hasRetBuf = methodSig.ReturnType is GcTypeKind.Other; - bool requiresInstArg = false; - bool isAsync = false; - bool isValueTypeThis = false; - - try - { - requiresInstArg = rts.GetGenericContextLoc(mdh) == GenericContextLoc.InstArg; - isAsync = rts.IsAsyncMethod(mdh); - } - catch + if (layout.ThisOffset is int thisOff) { + TargetPointer thisAddr = new(transitionBlock.Value + (ulong)thisOff); + GcScanFlags thisFlags = layout.IsValueTypeThis ? GcScanFlags.GC_CALL_INTERIOR : GcScanFlags.None; + scanContext.GCReportCallback(thisAddr, thisFlags); } - PromoteCallerStackHelper(transitionBlock, methodSig, hasThis, hasRetBuf, - requiresInstArg, isAsync, isValueTypeThis, scanContext); - } - - /// - /// Core logic for promoting caller stack GC references. - /// Matches native TransitionFrame::PromoteCallerStackHelper (frames.cpp:1560). - /// - private void PromoteCallerStackHelper( - TargetPointer transitionBlock, - MethodSignature methodSig, - bool hasThis, - bool hasRetBuf, - bool requiresInstArg, - bool isAsync, - bool isValueTypeThis, - GcScanContext scanContext) - { - Data.TransitionBlock tb = _target.ProcessedData.GetOrAdd(transitionBlock); - - int numRegistersUsed = 0; - if (hasThis) - numRegistersUsed++; - if (hasRetBuf) - numRegistersUsed++; - if (requiresInstArg) - numRegistersUsed++; - if (isAsync) - numRegistersUsed++; - - bool isArm64 = IsTargetArm64(); - if (isArm64) - numRegistersUsed++; - - if (hasThis) + if (layout.AsyncContinuationOffset is int asyncOff) { - int thisPos = isArm64 ? 1 : 0; - TargetPointer thisAddr = AddressFromGCRefMapPos(tb, thisPos); - GcScanFlags thisFlags = isValueTypeThis ? GcScanFlags.GC_CALL_INTERIOR : GcScanFlags.None; - scanContext.GCReportCallback(thisAddr, thisFlags); + TargetPointer asyncAddr = new(transitionBlock.Value + (ulong)asyncOff); + scanContext.GCReportCallback(asyncAddr, GcScanFlags.None); } - int pos = numRegistersUsed; - foreach (GcTypeKind kind in methodSig.ParameterTypes) + foreach (ArgLayout arg in layout.Arguments) { - TargetPointer slotAddress = AddressFromGCRefMapPos(tb, pos); - - switch (kind) + foreach (ArgSlot slot in arg.Slots) { - case GcTypeKind.Ref: - scanContext.GCReportCallback(slotAddress, GcScanFlags.None); - break; - case GcTypeKind.Interior: - scanContext.GCReportCallback(slotAddress, GcScanFlags.GC_CALL_INTERIOR); - break; - case GcTypeKind.Other: - break; - case GcTypeKind.None: - break; + TargetPointer slotAddress = new(transitionBlock.Value + (ulong)slot.Offset); + switch (GcTypeKindClassifier.GetGcKind(slot.ElementType)) + { + case GcTypeKind.Ref: + scanContext.GCReportCallback(slotAddress, GcScanFlags.None); + break; + case GcTypeKind.Interior: + scanContext.GCReportCallback(slotAddress, GcScanFlags.GC_CALL_INTERIOR); + break; + case GcTypeKind.Other: + if (arg.IsPassedByRef) + { + scanContext.GCReportCallback(slotAddress, GcScanFlags.GC_CALL_INTERIOR); + } + // TODO: For value types passed by value, enumerate fields for embedded GC refs + break; + case GcTypeKind.None: + break; + } } - pos++; } } @@ -464,11 +376,6 @@ private TargetPointer AddressFromGCRefMapPos(Data.TransitionBlock tb, int pos) return new TargetPointer(tb.FirstGCRefMapSlot.Value + (ulong)(pos * _target.PointerSize)); } - private bool IsTargetArm64() - { - return _target.Contracts.RuntimeInfo.GetTargetArchitecture() is RuntimeInfoArchitecture.Arm64; - } - private TargetPointer GetCallerSP(IPlatformAgnosticContext context, ref TargetPointer? cached) { if (cached is null) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcSignatureTypeProvider.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcSignatureTypeProvider.cs deleted file mode 100644 index 8852f733df6a97..00000000000000 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcSignatureTypeProvider.cs +++ /dev/null @@ -1,219 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Immutable; -using System.Diagnostics; -using System.Reflection.Metadata; -using System.Reflection.Metadata.Ecma335; -using Microsoft.Diagnostics.DataContractReader.SignatureHelpers; - -namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; - -/// -/// Classification of a signature type for GC scanning purposes. -/// -internal enum GcTypeKind -{ - /// Not a GC reference (primitives, pointers). - None, - /// Object reference (class, string, array). - Ref, - /// Interior pointer (byref). - Interior, - /// Value type that may contain embedded GC references. - Other, -} - -/// -/// Generic context used to resolve ELEMENT_TYPE_VAR and ELEMENT_TYPE_MVAR -/// while decoding a method signature for GC scanning. is the -/// owning type's (used for VAR), and -/// is the owning method's (used for MVAR). -/// -internal readonly record struct GcSignatureContext(TypeHandle ClassContext, MethodDescHandle MethodContext); - -/// -/// Classifies signature types for GC scanning purposes. -/// Implements which -/// is a superset of SRM's , -/// adding support for ELEMENT_TYPE_INTERNAL. -/// -/// -/// The provider is scoped to a single module: GetTypeFromDefinition and -/// GetTypeFromReference resolve TypeDef/TypeRef tokens via the module's -/// lookup tables so enums (and other runtime-normalized value types) are classified -/// using the actual , matching native -/// SigPointer::PeekElemTypeNormalized. -/// -internal sealed class GcSignatureTypeProvider - : IRuntimeSignatureTypeProvider -{ - private readonly Target _target; - private readonly ModuleHandle _moduleHandle; - - public GcSignatureTypeProvider(Target target, ModuleHandle moduleHandle) - { - _target = target; - _moduleHandle = moduleHandle; - } - - public GcTypeKind GetPrimitiveType(PrimitiveTypeCode typeCode) - => typeCode switch - { - PrimitiveTypeCode.String or PrimitiveTypeCode.Object => GcTypeKind.Ref, - PrimitiveTypeCode.TypedReference => GcTypeKind.Other, - _ => GcTypeKind.None, - }; - - public GcTypeKind GetTypeFromDefinition(MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) - => ClassifyTokenLookup(_target.Contracts.Loader.GetLookupTables(_moduleHandle).TypeDefToMethodTable, MetadataTokens.GetToken(handle), rawTypeKind); - - public GcTypeKind GetTypeFromReference(MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) - => ClassifyTokenLookup(_target.Contracts.Loader.GetLookupTables(_moduleHandle).TypeRefToMethodTable, MetadataTokens.GetToken(handle), rawTypeKind); - - public GcTypeKind GetTypeFromSpecification(MetadataReader reader, GcSignatureContext genericContext, TypeSpecificationHandle handle, byte rawTypeKind) - => rawTypeKind == (byte)SignatureTypeKind.ValueType ? GcTypeKind.Other : GcTypeKind.Ref; - - public GcTypeKind GetSZArrayType(GcTypeKind elementType) => GcTypeKind.Ref; - public GcTypeKind GetArrayType(GcTypeKind elementType, ArrayShape shape) => GcTypeKind.Ref; - public GcTypeKind GetByReferenceType(GcTypeKind elementType) => GcTypeKind.Interior; - public GcTypeKind GetPointerType(GcTypeKind elementType) => GcTypeKind.None; - - public GcTypeKind GetGenericInstantiation(GcTypeKind genericType, ImmutableArray typeArguments) - => genericType; - - public GcTypeKind GetGenericMethodParameter(GcSignatureContext genericContext, int index) - { - try - { - ReadOnlySpan instantiation = _target.Contracts.RuntimeTypeSystem.GetGenericMethodInstantiation(genericContext.MethodContext); - if ((uint)index >= (uint)instantiation.Length) - return GcTypeKind.Ref; - return ClassifyTypeHandle(instantiation[index]); - } - catch - { - return GcTypeKind.Ref; - } - } - - public GcTypeKind GetGenericTypeParameter(GcSignatureContext genericContext, int index) - { - try - { - IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem; - TypeHandle classCtx = genericContext.ClassContext; - - if (rts.IsArray(classCtx, out _)) - { - // Match native SigTypeContext::InitTypeContext (typectxt.cpp): arrays use - // the element type as their class instantiation. RuntimeTypeSystem.GetInstantiation - // returns an empty span for arrays, so consult GetTypeParam directly (the - // managed equivalent of MethodTable::GetArrayInstantiation). - Debug.Assert(index == 0, "Array class context has a 1-element instantiation; index > 0 indicates a malformed signature."); - if (index != 0) - return GcTypeKind.Ref; - return ClassifyTypeHandle(rts.GetTypeParam(classCtx)); - } - - ReadOnlySpan instantiation = rts.GetInstantiation(classCtx); - if ((uint)index >= (uint)instantiation.Length) - return GcTypeKind.Ref; - return ClassifyTypeHandle(instantiation[index]); - } - catch - { - return GcTypeKind.Ref; - } - } - - public GcTypeKind GetFunctionPointerType(MethodSignature signature) => GcTypeKind.None; - public GcTypeKind GetModifiedType(GcTypeKind modifier, GcTypeKind unmodifiedType, bool isRequired) => unmodifiedType; - public GcTypeKind GetInternalModifiedType(TargetPointer typeHandlePointer, GcTypeKind unmodifiedType, bool isRequired) => unmodifiedType; - public GcTypeKind GetPinnedType(GcTypeKind elementType) => elementType; - - public GcTypeKind GetInternalType(TargetPointer typeHandlePointer) - { - if (typeHandlePointer == TargetPointer.Null) - return GcTypeKind.None; - - try - { - return ClassifyTypeHandle(_target.Contracts.RuntimeTypeSystem.GetTypeHandle(typeHandlePointer)); - } - catch - { - return GcTypeKind.Ref; - } - } - - /// - /// Resolve a TypeDef/TypeRef token via the module's lookup tables and classify the - /// resulting . Falls back to a -based - /// classification when the type has not been loaded. - /// - private GcTypeKind ClassifyTokenLookup(TargetPointer lookupTable, int token, byte rawTypeKind) - { - try - { - TargetPointer typeHandlePtr = _target.Contracts.Loader.GetModuleLookupMapElement(lookupTable, (uint)token, out _); - if (typeHandlePtr == TargetPointer.Null) - return rawTypeKind == (byte)SignatureTypeKind.ValueType ? GcTypeKind.Other : GcTypeKind.Ref; - - return ClassifyTypeHandle(_target.Contracts.RuntimeTypeSystem.GetTypeHandle(typeHandlePtr)); - } - catch - { - return rawTypeKind == (byte)SignatureTypeKind.ValueType ? GcTypeKind.Other : GcTypeKind.Ref; - } - } - - /// - /// Classify a resolved . Mirrors native - /// SigPointer::PeekElemTypeNormalized + gElementTypeInfo[etype].m_gc: - /// enums collapse to their underlying primitive () so - /// they are skipped during stack scanning, matching native behavior. - /// - private GcTypeKind ClassifyTypeHandle(TypeHandle typeHandle) - { - if (typeHandle.Address == TargetPointer.Null) - return GcTypeKind.Ref; - - IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem; - CorElementType corType = rts.GetSignatureCorElementType(typeHandle); - - switch (corType) - { - case CorElementType.Void: - case CorElementType.Boolean: - case CorElementType.Char: - case CorElementType.I1: - case CorElementType.U1: - case CorElementType.I2: - case CorElementType.U2: - case CorElementType.I4: - case CorElementType.U4: - case CorElementType.I8: - case CorElementType.U8: - case CorElementType.R4: - case CorElementType.R8: - case CorElementType.I: - case CorElementType.U: - case CorElementType.FnPtr: - case CorElementType.Ptr: - return GcTypeKind.None; - - case CorElementType.Byref: - return GcTypeKind.Interior; - - case CorElementType.ValueType: - // Native PeekElemTypeNormalized resolves enums to their underlying primitive - // CorElementType, which classifies as TYPE_GC_NONE in gElementTypeInfo. - return rts.IsEnum(typeHandle) ? GcTypeKind.None : GcTypeKind.Other; - - default: - return GcTypeKind.Ref; - } - } -} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcTypeKind.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcTypeKind.cs new file mode 100644 index 00000000000000..de38ade64458c7 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcTypeKind.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; + +/// +/// GC classification of an argument for stack scanning. Mirrors the m_gc field of +/// native gElementTypeInfo in src/coreclr/vm/siginfo.cpp. +/// +internal enum GcTypeKind +{ + /// Not a GC reference (primitives, pointers). + None, + /// Object reference (class, string, array). + Ref, + /// Interior pointer (byref). + Interior, + /// Value type that may contain embedded GC references. + Other, +} + +/// +/// Maps a to its GC classification. Pure function of the +/// element type; not calling-convention or ABI dependent. +/// +internal static class GcTypeKindClassifier +{ + /// + /// Maps a (possibly normalized) to its GC classification, + /// matching the m_gc field of native gElementTypeInfo. + /// + public static GcTypeKind GetGcKind(CorElementType etype) => etype switch + { + CorElementType.Class or CorElementType.Object or CorElementType.String + or CorElementType.Array or CorElementType.SzArray + or CorElementType.Var or CorElementType.MVar + or CorElementType.GenericInst => GcTypeKind.Ref, + CorElementType.Byref => GcTypeKind.Interior, + CorElementType.ValueType or CorElementType.TypedByRef => GcTypeKind.Other, + _ => GcTypeKind.None, + }; +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs index 83996e38ef1d42..f367d7eb69bbda 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs @@ -71,5 +71,7 @@ public static void Register(ContractRegistry registry) registry.Register("c1", static t => new ExecutionManager_1(t)); registry.Register("c2", static t => new ExecutionManager_2(t)); + + registry.Register("c1", static t => new CallingConvention_1(t)); } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/EEClass.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/EEClass.cs index 0d0393f6e4e0df..4c4c8d4d689b74 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/EEClass.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/EEClass.cs @@ -20,6 +20,7 @@ public EEClass(Target target, TargetPointer address) NumThreadStaticFields = target.ReadField(address, type, nameof(NumThreadStaticFields)); FieldDescList = target.ReadPointerField(address, type, nameof(FieldDescList)); NumNonVirtualSlots = target.ReadField(address, type, nameof(NumNonVirtualSlots)); + BaseSizePadding = target.ReadField(address, type, nameof(BaseSizePadding)); } public TargetPointer MethodTable { get; init; } @@ -40,4 +41,5 @@ public EEClass(Target target, TargetPointer address) public ushort NumThreadStaticFields { get; init; } public TargetPointer FieldDescList { get; init; } public ushort NumNonVirtualSlots { get; init; } + public byte BaseSizePadding { get; init; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/MethodTableFlags_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/MethodTableFlags_1.cs index e82522b79c672e..dd65392056b1d3 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/MethodTableFlags_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/MethodTableFlags_1.cs @@ -24,6 +24,7 @@ internal enum WFLAGS_LOW : uint GenericsMask = 0x00000030, GenericsMask_NonGeneric = 0x00000000, // no instantiation GenericsMask_TypicalInstantiation = 0x00000030, // the type instantiated at its formal parameters, e.g. List + IsHFA = 0x00000800, StringArrayValues = GenericsMask_NonGeneric | @@ -59,6 +60,7 @@ internal enum WFLAGS_HIGH : uint internal enum WFLAGS2_ENUM : uint { DynamicStatics = 0x0002, + IsIntrinsicType = 0x0020, } public uint MTFlags { get; init; } @@ -104,9 +106,11 @@ private bool TestFlagWithMask(WFLAGS2_ENUM mask, WFLAGS2_ENUM flag) public bool HasInstantiation => !TestFlagWithMask(WFLAGS_LOW.GenericsMask, WFLAGS_LOW.GenericsMask_NonGeneric); public bool ContainsGCPointers => GetFlag(WFLAGS_HIGH.ContainsGCPointers) != 0; public bool RequiresAlign8 => GetFlag(WFLAGS_HIGH.RequiresAlign8) != 0; + public bool IsHFA => TestFlagWithMask(WFLAGS_LOW.IsHFA, WFLAGS_LOW.IsHFA); public bool IsCollectible => GetFlag(WFLAGS_HIGH.Collectible) != 0; public bool IsTrackedReferenceWithFinalizer => GetFlag(WFLAGS_HIGH.IsTrackedReferenceWithFinalizer) != 0; public bool IsDynamicStatics => GetFlag(WFLAGS2_ENUM.DynamicStatics) != 0; + public bool IsIntrinsicType => GetFlag(WFLAGS2_ENUM.IsIntrinsicType) != 0; public bool IsGenericTypeDefinition => TestFlagWithMask(WFLAGS_LOW.GenericsMask, WFLAGS_LOW.GenericsMask_TypicalInstantiation); public bool ContainsGenericVariables => GetFlag(WFLAGS_HIGH.ContainsGenericVariables) != 0; diff --git a/src/native/managed/cdac/tests/CallingConvention/AMD64UnixCallingConventionTests.cs b/src/native/managed/cdac/tests/CallingConvention/AMD64UnixCallingConventionTests.cs new file mode 100644 index 00000000000000..bd43f62ea4b0d3 --- /dev/null +++ b/src/native/managed/cdac/tests/CallingConvention/AMD64UnixCallingConventionTests.cs @@ -0,0 +1,778 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.Tests; + +/// +/// AMD64-Unix (System V AMD64 ABI) calling-convention tests. GP args go in +/// RDI/RSI/RDX/RCX/R8/R9 (6 slots); FP args in XMM0-XMM7 (8 slots). The two +/// banks are independent. +/// +/// The SysV struct classifier (SystemVStructClassifier) is exercised +/// end-to-end via the contract: each struct test allocates a value-type MT +/// in mock memory and references it via ELEMENT_TYPE_INTERNAL in the +/// stored sig blob. +/// +/// +public class AMD64UnixCallingConventionTests +{ + private static readonly Lazy s_syntheticVectorMetadata = new(SyntheticVectorMetadata.Create); + + private static CallConvTestCase Case => CallConvCases.AMD64Unix; + + private static int OffsetOfNthGPReg(int n) => Case.ArgumentRegistersOffset + n * Case.PointerSize; + private static int OffsetOfNthFPReg(int n) => Case.OffsetOfFloatArgumentRegisters!.Value + n * Case.FloatRegisterSize; + private static int OffsetOfNthStackSlot(int n) => Case.OffsetOfArgs + n * Case.PointerSize; + + [Fact] + public void SixInts_FillGPRegs() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => + { + sig.Return(CorElementType.Void); + for (int i = 0; i < Case.NumArgumentRegisters; i++) sig.Param(CorElementType.I8); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(Case.NumArgumentRegisters, layout.Arguments.Count); + for (int i = 0; i < Case.NumArgumentRegisters; i++) + { + int expected = Case.ArgumentRegistersOffset + i * Case.PointerSize; + Assert.Equal(expected, layout.Arguments[i].Slots[0].Offset); + } + } + + [Fact] + public void SeventhInt_GoesToStack() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => + { + sig.Return(CorElementType.Void); + for (int i = 0; i < Case.NumArgumentRegisters + 1; i++) sig.Param(CorElementType.I8); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(Case.NumArgumentRegisters + 1, layout.Arguments.Count); + Assert.Equal(Case.OffsetOfArgs, layout.Arguments[6].Slots[0].Offset); + } + + [Fact] + public void FourDoubles_FillFPRegs() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => + { + sig.Return(CorElementType.Void); + for (int i = 0; i < 4; i++) sig.Param(CorElementType.R8); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(4, layout.Arguments.Count); + for (int i = 0; i < 4; i++) + { + int expected = (Case.OffsetOfFloatArgumentRegisters ?? 0) + i * Case.FloatRegisterSize; + Assert.Equal(expected, layout.Arguments[i].Slots[0].Offset); + } + } + + [Fact] + public void MixedIntDouble_UseSeparateBanks() + { + // SysV bank-independent allocation: int goes to GP, double to FP regardless of position. + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => sig + .Return(CorElementType.Void) + .Param(CorElementType.I8) + .Param(CorElementType.R8) + .Param(CorElementType.I8) + .Param(CorElementType.R8)); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(4, layout.Arguments.Count); + Assert.Equal(Case.ArgumentRegistersOffset, layout.Arguments[0].Slots[0].Offset); + Assert.Equal((Case.OffsetOfFloatArgumentRegisters ?? 0), layout.Arguments[1].Slots[0].Offset); + Assert.Equal(Case.ArgumentRegistersOffset + Case.PointerSize, layout.Arguments[2].Slots[0].Offset); + Assert.Equal((Case.OffsetOfFloatArgumentRegisters ?? 0) + Case.FloatRegisterSize, layout.Arguments[3].Slots[0].Offset); + } + + // ----- SysV struct classification ----- + + [Fact] + public void Struct_TwoInts_PassedInOneGPReg() + { + // { int x; int y; } => 8 bytes => 1 eightbyte (Integer) => 1 GP reg. + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "TwoInts", structSize: 8, + fields: [new(0, CorElementType.I4), new(4, CorElementType.I4)]); + sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.False(arg.IsPassedByRef); + Assert.Single(arg.Slots); + Assert.Equal(CorElementType.I8, arg.Slots[0].ElementType); + Assert.Equal(Case.ArgumentRegistersOffset, arg.Slots[0].Offset); + } + + [Fact] + public void Struct_IntDouble_SplitAcrossGPAndFP() + { + // { int x; double d; } => 2 eightbytes (Integer, SSE) => 1 GP + 1 FP reg. + // This is the key SysV "split" case GcScanner needs to see per-register. + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "IntDouble", structSize: 16, + fields: [new(0, CorElementType.I4), new(8, CorElementType.R8)]); + sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.False(arg.IsPassedByRef); + Assert.Equal(2, arg.Slots.Count); + Assert.Equal(CorElementType.I8, arg.Slots[0].ElementType); + Assert.Equal(Case.ArgumentRegistersOffset, arg.Slots[0].Offset); + Assert.Equal(CorElementType.R8, arg.Slots[1].ElementType); + Assert.Equal((Case.OffsetOfFloatArgumentRegisters ?? 0), arg.Slots[1].Offset); + } + + [Fact] + public void Struct_TwoDoubles_PassedInTwoFPRegs() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "TwoDoubles", structSize: 16, + fields: [new(0, CorElementType.R8), new(8, CorElementType.R8)]); + sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.Equal(2, arg.Slots.Count); + Assert.Equal(CorElementType.R8, arg.Slots[0].ElementType); + Assert.Equal((Case.OffsetOfFloatArgumentRegisters ?? 0), arg.Slots[0].Offset); + Assert.Equal(CorElementType.R8, arg.Slots[1].ElementType); + Assert.Equal((Case.OffsetOfFloatArgumentRegisters ?? 0) + Case.FloatRegisterSize, arg.Slots[1].Offset); + } + + [Fact] + public void Struct_TwoFloats_PackInOneFPReg() + { + // { float a; float b; } => 8 bytes => 1 eightbyte (SSE) => 1 FP reg. + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "TwoFloats", structSize: 8, + fields: [new(0, CorElementType.R4), new(4, CorElementType.R4)]); + sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + Assert.Single(layout.Arguments[0].Slots); + Assert.Equal(CorElementType.R8, layout.Arguments[0].Slots[0].ElementType); + Assert.Equal((Case.OffsetOfFloatArgumentRegisters ?? 0), layout.Arguments[0].Slots[0].Offset); + } + + [Fact] + public void Struct_ObjectAndDouble_GCRefSplitWithFP() + { + // { object o; double d; } => 2 eightbytes (IntegerReference, SSE). + // The GP slot is reported as a managed reference (CorElementType.Class). + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "ObjectAndDouble", structSize: 16, + fields: [new(0, CorElementType.Object), new(8, CorElementType.R8)]); + sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.Equal(2, arg.Slots.Count); + Assert.Equal(CorElementType.Class, arg.Slots[0].ElementType); + Assert.Equal(Case.ArgumentRegistersOffset, arg.Slots[0].Offset); + Assert.Equal(CorElementType.R8, arg.Slots[1].ElementType); + Assert.Equal((Case.OffsetOfFloatArgumentRegisters ?? 0), arg.Slots[1].Offset); + } + + [Fact] + public void Struct_LargerThan16Bytes_StackByValue_NotByRef() + { + // { long a; long b; long c; } => 24 bytes > 16 => stack by value + // (NOT implicit by-ref like Windows x64). + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "ThreeLongs", structSize: 24, + fields: + [ + new(0, CorElementType.I8), + new(8, CorElementType.I8), + new(16, CorElementType.I8), + ]); + sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.False(arg.IsPassedByRef); // SysV passes large structs BY VALUE on stack. + Assert.Single(arg.Slots); + Assert.Equal(Case.OffsetOfArgs, arg.Slots[0].Offset); + } + + [Fact] + public void TypedReference_PassedInTwoGPRegs() + { + // System.TypedReference is { ref byte _value; IntPtr _type; } -> 16 bytes + // classified as [IntegerByRef, Integer] -> RDI + RSI on SysV AMD64. + // The signature uses ELEMENT_TYPE_TYPEDBYREF (0x16); the ArgIterator + // substitutes the well-known g_TypedReferenceMT MethodTable so the + // SysV struct classifier walks its layout normally. + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable typedRefMT = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "System.TypedReference", structSize: 16, + fields: [new(0, CorElementType.Byref), new(8, CorElementType.I)]); + rts.SetTypedReferenceMethodTable(typedRefMT.Address); + sig.Return(CorElementType.Void).Param(CorElementType.TypedByRef); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.False(arg.IsPassedByRef); + Assert.Equal(2, arg.Slots.Count); + Assert.Equal(Case.ArgumentRegistersOffset, arg.Slots[0].Offset); + Assert.Equal(Case.ArgumentRegistersOffset + Case.PointerSize, arg.Slots[1].Offset); + } + + [Fact] + public void TypedReference_GlobalNotSet_FallsBackToStack() + { + // When g_TypedReferenceMT isn't populated (older runtime / missing global), + // the signature provider falls back to a conservative pointer-sized + // value-type placeholder. SysV AMD64 still passes it on the stack rather + // than crashing on resolution. + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => sig.Return(CorElementType.Void).Param(CorElementType.TypedByRef)); + + // No SetTypedReferenceMethodTable call - global stays null. + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + // Should not throw; layout is best-effort. + } + + // ---- Section 1: Style-aligned coverage (mirrors AMD64-Windows tests) ---- + + // Integer-valued args consume the 6 GP arg-regs in order, then spill to + // stack slots that advance by one pointer-sized slot per overflow arg. + [Theory] + [InlineData(CorElementType.I1, 6)] + [InlineData(CorElementType.U1, 6)] + [InlineData(CorElementType.I2, 6)] + [InlineData(CorElementType.U2, 6)] + [InlineData(CorElementType.I4, 6)] + [InlineData(CorElementType.U4, 6)] + [InlineData(CorElementType.I8, 6)] + [InlineData(CorElementType.U8, 6)] + [InlineData(CorElementType.I4, 7)] + [InlineData(CorElementType.I8, 8)] + public void IntArgs_FillSixGPRegsAndSpillToStack(CorElementType elementType, int count) + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => + { + sig.Return(CorElementType.Void); + for (int i = 0; i < count; i++) sig.Param(elementType); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(count, layout.Arguments.Count); + + for (int i = 0; i < count; i++) + { + int expectedOffset = i < Case.NumArgumentRegisters + ? OffsetOfNthGPReg(i) + : OffsetOfNthStackSlot(i - Case.NumArgumentRegisters); + + Assert.Equal(expectedOffset, layout.Arguments[i].Slots[0].Offset); + } + } + + [Fact] + public void InstanceMethod_ThisOffsetIsFirstGPReg() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: true, + sig => sig.Return(CorElementType.Void)); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(OffsetOfNthGPReg(0), layout.ThisOffset); + } + + // Float args fill the 8 FP arg-regs (independent of the GP bank). + // Counts 1..8 exercise pure FP-bank allocation; this verifies the happy path. + [Theory] + [InlineData(CorElementType.R4, 1)] + [InlineData(CorElementType.R8, 1)] + [InlineData(CorElementType.R4, 4)] + [InlineData(CorElementType.R8, 4)] + [InlineData(CorElementType.R4, 8)] + [InlineData(CorElementType.R8, 8)] + public void FloatArgs_FillEightFPRegs(CorElementType elementType, int count) + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => + { + sig.Return(CorElementType.Void); + for (int i = 0; i < count; i++) sig.Param(elementType); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(count, layout.Arguments.Count); + + for (int i = 0; i < count; i++) + { + Assert.Equal(elementType, layout.Arguments[i].Slots[0].ElementType); + Assert.Equal(OffsetOfNthFPReg(i), layout.Arguments[i].Slots[0].Offset); + } + } + + // A lone float chooses the FP bank based on its FP-bank ordinal; surrounding + // ints continue to map to GP registers by their GP-bank ordinal. + // Verifies the SysV "independent banks" placement rule. + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + [InlineData(5)] + public void OneFloatAmongInts_LandsInFirstFPReg(int floatPosition) + { + const int totalArgs = 6; + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => + { + sig.Return(CorElementType.Void); + for (int i = 0; i < totalArgs; i++) + sig.Param(i == floatPosition ? CorElementType.R8 : CorElementType.I4); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(totalArgs, layout.Arguments.Count); + + int idxGen = 0; + int idxFP = 0; + for (int i = 0; i < totalArgs; i++) + { + if (i == floatPosition) + { + Assert.Equal(CorElementType.R8, layout.Arguments[i].Slots[0].ElementType); + Assert.Equal(OffsetOfNthFPReg(idxFP), layout.Arguments[i].Slots[0].Offset); + idxFP++; + } + else + { + Assert.Equal(CorElementType.I4, layout.Arguments[i].Slots[0].ElementType); + Assert.Equal(OffsetOfNthGPReg(idxGen), layout.Arguments[i].Slots[0].Offset); + idxGen++; + } + } + } + + [Fact] + public void ManyIntArgs_StackOffsetsProgress() + { + const int totalArgs = 10; + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => + { + sig.Return(CorElementType.Void); + for (int i = 0; i < totalArgs; i++) sig.Param(CorElementType.I8); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(totalArgs, layout.Arguments.Count); + + for (int i = 0; i < totalArgs; i++) + { + int expectedOffset = i < Case.NumArgumentRegisters + ? OffsetOfNthGPReg(i) + : OffsetOfNthStackSlot(i - Case.NumArgumentRegisters); + + Assert.Equal(expectedOffset, layout.Arguments[i].Slots[0].Offset); + } + } + + // ---- Section 2: Return-value classification baselines ---- + + // Baseline: 8-byte struct return classifies as 1 Integer eightbyte + // (reg-passable), so no ret buf is needed and the first user arg stays + // in the 1st GP reg. + [Fact] + public void Return_EightByteStruct_NoRetBuf_FirstArgStaysInFirstGPReg() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "EightBytes", structSize: 8, fields: [new(0, CorElementType.I8)]); + sig.ReturnValueType(new TargetPointer(mt.Address)).Param(CorElementType.I4); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + Assert.Equal(OffsetOfNthGPReg(0), layout.Arguments[0].Slots[0].Offset); + } + + // Baseline: 16-byte struct return classifies as 2 Integer eightbytes + // (reg-passable in RAX:RDX), so no ret buf. + [Fact] + public void Return_SixteenByteStruct_NoRetBuf_FirstArgStaysInFirstGPReg() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "SixteenBytes", structSize: 16, + fields: [new(0, CorElementType.I8), new(8, CorElementType.I8)]); + sig.ReturnValueType(new TargetPointer(mt.Address)).Param(CorElementType.I4); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + Assert.Equal(OffsetOfNthGPReg(0), layout.Arguments[0].Slots[0].Offset); + } + + // Baseline: >16-byte struct return cannot fit in two regs, so a hidden ret + // buf takes the 1st GP slot and the first user arg shifts to the 2nd GP reg. + [Fact] + public void Return_LargeStruct_HasRetBuf_FirstArgShiftsToSecondGPReg() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "TwentyFourBytes", structSize: 24, + fields: + [ + new(0, CorElementType.I8), + new(8, CorElementType.I8), + new(16, CorElementType.I8), + ]); + sig.ReturnValueType(new TargetPointer(mt.Address)).Param(CorElementType.I4); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + Assert.Equal(OffsetOfNthGPReg(1), layout.Arguments[0].Slots[0].Offset); + } + +#pragma warning disable xUnit1004 // Test methods should not be skipped — these track audit gaps. + + // ---- Section 3: Open audit gaps ---- + + // Audit gap (return classification): on AMD64-Unix, a 12-byte struct return + // {int, int, int} classifies as 2 Integer eightbytes -> reg-passable in + // RAX:RDX, so NO ret buf is needed. cDAC currently uses an X64-wide + // power-of-2 heuristic that wrongly forces a ret buf for non-power-of-2 + // sizes, shifting the first user arg from the 1st GP reg to the 2nd. + [Fact] + public void Return_TwelveByteStruct_NoRetBuf_FirstArgStaysInFirstGPReg() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "TwelveBytes", structSize: 12, + fields: + [ + new(0, CorElementType.I4), + new(4, CorElementType.I4), + new(8, CorElementType.I4), + ]); + sig.ReturnValueType(new TargetPointer(mt.Address)).Param(CorElementType.I4); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + Assert.Equal(OffsetOfNthGPReg(0), layout.Arguments[0].Slots[0].Offset); + } + + // Audit gap (return classification): {long, short} is 10 bytes, classifies + // as 2 Integer eightbytes -> reg-passable. cDAC's power-of-2 heuristic + // wrongly says ret buf. + [Fact] + public void Return_TenByteStruct_NoRetBuf_FirstArgStaysInFirstGPReg() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "TenBytes", structSize: 10, + fields: + [ + new(0, CorElementType.I8), + new(8, CorElementType.I2), + ]); + sig.ReturnValueType(new TargetPointer(mt.Address)).Param(CorElementType.I4); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + Assert.Equal(OffsetOfNthGPReg(0), layout.Arguments[0].Slots[0].Offset); + } + + // Audit gap (return classification): a 3-byte struct {byte, byte, byte} + // classifies as 1 Integer eightbyte -> reg-passable in RAX. cDAC's + // power-of-2 heuristic wrongly forces a ret buf. + [Fact] + public void Return_ThreeByteStruct_NoRetBuf_FirstArgStaysInFirstGPReg() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "ThreeBytes", structSize: 3, + fields: + [ + new(0, CorElementType.I1), + new(1, CorElementType.I1), + new(2, CorElementType.I1), + ]); + sig.ReturnValueType(new TargetPointer(mt.Address)).Param(CorElementType.I4); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + Assert.Equal(OffsetOfNthGPReg(0), layout.Arguments[0].Slots[0].Offset); + } + + // Regression: when the 8 FP arg-regs are exhausted, the 9th double must + // land on the stack. Verifies that the FP-overflow path in PlaceScalar + // does NOT fall through to the GP-arg path (it falls directly to the + // stack — matching native callingconvention.h:1457-1476). + [Fact] + public void NineDoubles_NinthGoesToFirstStackSlot() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => + { + sig.Return(CorElementType.Void); + for (int i = 0; i < 9; i++) sig.Param(CorElementType.R8); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(9, layout.Arguments.Count); + + // First 8 doubles in FP regs. + for (int i = 0; i < Case.NumFloatArgumentRegisters; i++) + Assert.Equal(OffsetOfNthFPReg(i), layout.Arguments[i].Slots[0].Offset); + + // 9th double on the stack (1st stack slot), NOT in a GP reg. + Assert.Equal(OffsetOfNthStackSlot(0), layout.Arguments[8].Slots[0].Offset); + } + + [Theory] + [InlineData(false, false, false, false)] + [InlineData(true, false, false, false)] + [InlineData(false, true, false, false)] + [InlineData(true, true, false, false)] + [InlineData(false, false, true, false)] + [InlineData(true, false, true, false)] + [InlineData(false, false, false, true)] + [InlineData(true, true, true, false)] + [InlineData(true, true, true, true)] + public void HiddenArgs_DoNotAffectFPPlacement_BankIndependence( + bool hasThis, + bool hasRetBuf, + bool hasParamType, + bool hasAsyncCont) + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, + hasThis, + (rts, sig) => + { + if (hasRetBuf) + { + MockMethodTable bigMT = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, + "BigReturn", + structSize: 24, + fields: + [ + new(0, CorElementType.I8), + new(8, CorElementType.I8), + new(16, CorElementType.I8), + ]); + sig.ReturnValueType(new TargetPointer(bigMT.Address)); + } + else + { + sig.Return(CorElementType.Void); + } + + sig.Param(CorElementType.R8); + }, + hasParamType, + hasAsyncCont); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + + ArgLayout arg = layout.Arguments[0]; + Assert.False(arg.IsPassedByRef); + Assert.Equal(CorElementType.R8, arg.Slots[0].ElementType); + Assert.Equal(OffsetOfNthFPReg(0), arg.Slots[0].Offset); + } + + [Theory] + [InlineData("Vector64`1", 8)] + [InlineData("Vector128`1", 16)] + public void VectorType_OnUnix_BypassesClassifier(string vectorTypeName, int vectorByteSize) + { + SyntheticVectorMetadata metadata = s_syntheticVectorMetadata.Value; + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, + hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddVectorMethodTable( + rts, + vectorTypeName, + vectorByteSize, + metadata.GetTypeDefToken(vectorTypeName)); + sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); + }, + syntheticMetadata: metadata); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + + ArgLayout arg = layout.Arguments[0]; + Assert.False(arg.IsPassedByRef); + Assert.Single(arg.Slots); + Assert.Equal(OffsetOfNthStackSlot(0), arg.Slots[0].Offset); + } + + [Fact] + public void EmptyStruct_PassedByValueOnStack() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, + hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, + "Empty", + structSize: 0, + fields: []); + sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + + ArgLayout arg = layout.Arguments[0]; + Assert.False(arg.IsPassedByRef); + Assert.Single(arg.Slots); + Assert.Equal(OffsetOfNthStackSlot(0), arg.Slots[0].Offset); + } + + [Theory] + [InlineData(CorElementType.Object)] + [InlineData(CorElementType.String)] + public void GCReferenceArgs_GoToGPRegs(CorElementType refType) + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => sig.Return(CorElementType.Void).Param(refType)); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + + ArgLayout arg = layout.Arguments[0]; + Assert.False(arg.IsPassedByRef); + Assert.Equal(OffsetOfNthGPReg(0), arg.Slots[0].Offset); + } + + [Fact] + public void GCReferenceArgs_GoToGPRegs_Class() + { + // ELEMENT_TYPE_CLASS with a dummy TypeDef token. The signature decoder + // falls back to a pointer-sized Class placeholder, which lands in a GP reg. + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => sig.Return(CorElementType.Void).ParamClass()); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.False(arg.IsPassedByRef); + Assert.Equal(OffsetOfNthGPReg(0), arg.Slots[0].Offset); + } + + [Fact] + public void VarArgs_ReturnsLayout_OnUnixTarget() + { + // Managed varargs are not supported on Unix, but the contract should + // handle the signature gracefully (it flows through the iterator). + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => sig.VarArg().Return(CorElementType.Void).Param(CorElementType.I4)); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + Assert.NotNull(layout.VarArgCookieOffset); + } + +#pragma warning restore xUnit1004 +} diff --git a/src/native/managed/cdac/tests/CallingConvention/AMD64WindowsCallingConventionTests.cs b/src/native/managed/cdac/tests/CallingConvention/AMD64WindowsCallingConventionTests.cs new file mode 100644 index 00000000000000..68a1f8d21bf174 --- /dev/null +++ b/src/native/managed/cdac/tests/CallingConvention/AMD64WindowsCallingConventionTests.cs @@ -0,0 +1,617 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.Tests; + +/// +/// AMD64-Windows (Microsoft x64) calling-convention tests. Each fixed arg +/// occupies a single pointer-sized slot; FP args shadow into XMM0-XMM3 at +/// the same index as their GP slot. +/// +public class AMD64WindowsCallingConventionTests +{ + private static readonly Lazy s_syntheticVectorMetadata = new(SyntheticVectorMetadata.Create); + + private static CallConvTestCase Case => CallConvCases.AMD64Windows; + + private static int OffsetOfFirstGPArg => Case.ArgumentRegistersOffset; + private static int OffsetOfStackArgs => Case.OffsetOfArgs + Case.NumArgumentRegisters * Case.PointerSize; + private static int OffsetOfFirstFPArg => Case.OffsetOfFloatArgumentRegisters.Value; + + // Integer-valued args consume RCX/RDX/R8/R9 in order, then spill to stack + // slots that advance by one pointer-sized home slot per argument. + [Theory] + [InlineData(CorElementType.I1, 4)] + [InlineData(CorElementType.U1, 4)] + [InlineData(CorElementType.I2, 4)] + [InlineData(CorElementType.U2, 4)] + [InlineData(CorElementType.I4, 4)] + [InlineData(CorElementType.U4, 4)] + [InlineData(CorElementType.I8, 4)] + [InlineData(CorElementType.U8, 4)] + [InlineData(CorElementType.I4, 5)] + [InlineData(CorElementType.I8, 5)] + public void IntArgs_FillRegsAndSpillToStack(CorElementType elementType, int count) + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => + { + sig.Return(CorElementType.Void); + for (int i = 0; i < count; i++) + { + sig.Param(elementType); + } + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(count, layout.Arguments.Count); + + for (int i = 0; i < count; i++) + { + int expectedOffset = i < 4 + ? OffsetOfFirstGPArg + i * Case.PointerSize + : OffsetOfStackArgs + (i - 4) * Case.PointerSize; + + Assert.Equal(expectedOffset, layout.Arguments[i].Slots[0].Offset); + } + } + + [Fact] + public void InstanceMethod_ThisOffsetIsFirst() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: true, + sig => sig.Return(CorElementType.Void)); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(OffsetOfFirstGPArg, layout.ThisOffset); + } + + // Floating-point args shadow into XMM0-XMM3 by position, then spill into + // pointer-sized stack home slots once the four register positions are used. + [Theory] + [InlineData(CorElementType.R4, 1)] + [InlineData(CorElementType.R8, 1)] + [InlineData(CorElementType.R4, 4)] + [InlineData(CorElementType.R8, 4)] + [InlineData(CorElementType.R4, 6)] + [InlineData(CorElementType.R8, 6)] + public void FloatArgs_FillFPRegsAndSpillToStack(CorElementType elementType, int count) + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => + { + sig.Return(CorElementType.Void); + for (int i = 0; i < count; i++) + { + sig.Param(elementType); + } + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(count, layout.Arguments.Count); + + for (int i = 0; i < count; i++) + { + int expectedOffset = i < 4 + ? OffsetOfFirstFPArg + i * Case.FloatRegisterSize + : OffsetOfStackArgs + (i - 4) * Case.PointerSize; + + Assert.Equal(elementType, layout.Arguments[i].Slots[0].ElementType); + Assert.Equal(expectedOffset, layout.Arguments[i].Slots[0].Offset); + } + } + + // A lone float still chooses the XMM bank based on its ordinal position, + // while the surrounding integer slots continue to map to GP registers. + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + public void OneFloatAmongInts_LandsInXMM(int floatPosition) + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => + { + sig.Return(CorElementType.Void); + for (int i = 0; i < 4; i++) + { + sig.Param(i == floatPosition ? CorElementType.R8 : CorElementType.I4); + } + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(4, layout.Arguments.Count); + + for (int i = 0; i < 4; i++) + { + int expectedOffset = i == floatPosition + ? OffsetOfFirstFPArg + i * Case.FloatRegisterSize + : OffsetOfFirstGPArg + i * Case.PointerSize; + CorElementType expectedType = i == floatPosition ? CorElementType.R8 : CorElementType.I4; + + Assert.Equal(expectedType, layout.Arguments[i].Slots[0].ElementType); + Assert.Equal(expectedOffset, layout.Arguments[i].Slots[0].Offset); + } + } + + [Fact] + public void InstanceMethod_FirstUserDoubleShiftsToXMM1() + { + // Hidden `this` consumes slot 0 (RCX). The first user-arg double therefore + // shadows into XMM1, NOT XMM0. This is a managed-specific consequence of + // `this` taking the first slot like any other argument. + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: true, + sig => sig.Return(CorElementType.Void).Param(CorElementType.R8)); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + Assert.Equal(OffsetOfFirstFPArg + 1 * Case.FloatRegisterSize, layout.Arguments[0].Slots[0].Offset); + } + + [Fact] + public void StaticMethod_RetBuf_FirstUserDoubleShiftsToXMM1() + { + // Method returns a 24-byte struct -> hidden retBuf in RCX. First user-arg + // double therefore shadows into XMM1, NOT XMM0. + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable bigMT = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "BigReturn", structSize: 24, + fields: + [ + new(0, CorElementType.I8), + new(8, CorElementType.I8), + new(16, CorElementType.I8), + ]); + sig.ReturnValueType(new TargetPointer(bigMT.Address)).Param(CorElementType.R8); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + Assert.Equal(OffsetOfFirstFPArg + 1 * Case.FloatRegisterSize, layout.Arguments[0].Slots[0].Offset); + } + + [Fact] + public void InstanceMethod_RetBuf_FirstUserDoubleShiftsToXMM2() + { + // `this` AND retBuf consume slots 0 and 1; first user-arg double -> XMM2. + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: true, + (rts, sig) => + { + MockMethodTable bigMT = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "BigReturn", structSize: 24, + fields: + [ + new(0, CorElementType.I8), + new(8, CorElementType.I8), + new(16, CorElementType.I8), + ]); + sig.ReturnValueType(new TargetPointer(bigMT.Address)).Param(CorElementType.R8); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + Assert.Equal(OffsetOfFirstFPArg + 2 * Case.FloatRegisterSize, layout.Arguments[0].Slots[0].Offset); + } + + /// + /// Verifies the CLR's hidden-arg prefix on AMD64 Windows. Each hidden arg (this, + /// retBuf, genericContext, asyncContinuation) consumes one of RCX/RDX/R8/R9. + /// The first user-arg double therefore lands at XMM<count-of-hidden-args>, + /// or on the stack when all 4 register slots are consumed. + /// + [Theory] + [InlineData(false, false, false, false, 0)] + [InlineData(true, false, false, false, 1)] + [InlineData(false, true, false, false, 1)] + [InlineData(true, true, false, false, 2)] + [InlineData(false, false, true, false, 1)] + [InlineData(true, false, true, false, 2)] + [InlineData(false, false, false, true, 1)] + [InlineData(true, true, true, false, 3)] + [InlineData(true, true, false, true, 3)] + [InlineData(true, true, true, true, -1)] + public void HiddenArgs_ShiftFirstUserDouble( + bool hasThis, bool hasRetBuf, bool hasParamType, bool hasAsyncCont, + int expectedFloatPosition) + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, + hasThis, + (rts, sig) => + { + if (hasRetBuf) + { + MockMethodTable bigMT = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, + "BigReturn", + structSize: 24, + fields: + [ + new(0, CorElementType.I8), + new(8, CorElementType.I8), + new(16, CorElementType.I8), + ]); + sig.ReturnValueType(new TargetPointer(bigMT.Address)); + } + else + { + sig.Return(CorElementType.Void); + } + + sig.Param(CorElementType.R8); + }, + hasParamType, + hasAsyncCont); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + Assert.Equal(CorElementType.R8, layout.Arguments[0].Slots[0].ElementType); + + int expectedOffset = expectedFloatPosition >= 0 + ? OffsetOfFirstFPArg + expectedFloatPosition * Case.FloatRegisterSize + : OffsetOfStackArgs; + + Assert.Equal(expectedOffset, layout.Arguments[0].Slots[0].Offset); + } + + /// + /// Verifies the CLR managed-vararg prefix order on AMD64 Windows. The cookie + /// occupies the slot after this/retBuf and before user args. The first user + /// arg therefore lands at slot index (hidden-arg count + 1 [for cookie]). + /// + [Theory] + [InlineData(false, false, 0, 1)] + [InlineData(true, false, 1, 2)] + [InlineData(false, true, 1, 2)] + [InlineData(true, true, 2, 3)] + public void VarArgs_CookieAndFirstUserArg_OnWindows( + bool hasThis, bool hasRetBuf, int expectedCookieSlot, int expectedFirstUserSlot) + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, + hasThis, + (rts, sig) => + { + sig.VarArg(); + if (hasRetBuf) + { + MockMethodTable bigMT = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, + "BigReturn", + structSize: 24, + fields: + [ + new(0, CorElementType.I8), + new(8, CorElementType.I8), + new(16, CorElementType.I8), + ]); + sig.ReturnValueType(new TargetPointer(bigMT.Address)); + } + else + { + sig.Return(CorElementType.Void); + } + + sig.Param(CorElementType.R8); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.NotNull(layout.VarArgCookieOffset); + Assert.Single(layout.Arguments); + Assert.Equal( + OffsetOfFirstGPArg + expectedCookieSlot * Case.PointerSize, + layout.VarArgCookieOffset.Value); + Assert.Equal( + OffsetOfFirstFPArg + expectedFirstUserSlot * Case.FloatRegisterSize, + layout.Arguments[0].Slots[0].Offset); + } + + [Fact] + public void NonVarArgs_HasNullVarArgCookieOffset() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => sig.Return(CorElementType.Void).Param(CorElementType.I4)); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Null(layout.VarArgCookieOffset); + } + + // Stack home slots continue to progress one pointer at a time once the four + // register-backed positions are exhausted by earlier integer arguments. + [Fact] + public void TenArgs_StackOffsetsProgress() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => + { + sig.Return(CorElementType.Void); + for (int i = 0; i < 10; i++) + { + sig.Param(CorElementType.I4); + } + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(10, layout.Arguments.Count); + + for (int i = 0; i < 10; i++) + { + int expectedOffset = i < 4 + ? OffsetOfFirstGPArg + i * Case.PointerSize + : OffsetOfStackArgs + (i - 4) * Case.PointerSize; + + Assert.Equal(expectedOffset, layout.Arguments[i].Slots[0].Offset); + } + } + + // Windows x64 does not classify HFA-shaped structs into XMM registers; small + // ones still use a GP slot, while larger ones take the existing byref path. + [Theory] + [InlineData(2, false)] + [InlineData(4, true)] + public void HFAShapedStruct_OnWindows_DoesNotEnregisterInFP(int floatCount, bool expectByref) + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockDescriptors.CallingConvention.ValueTypeField[] fields = new MockDescriptors.CallingConvention.ValueTypeField[floatCount]; + for (int i = 0; i < floatCount; i++) + { + fields[i] = new(i * sizeof(float), CorElementType.R4); + } + + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, + $"FloatStruct{floatCount}", + structSize: sizeof(float) * floatCount, + fields: fields); + sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + + ArgLayout arg = layout.Arguments[0]; + Assert.Equal(expectByref, arg.IsPassedByRef); + Assert.Single(arg.Slots); + Assert.Equal(OffsetOfFirstGPArg, arg.Slots[0].Offset); + } + + // ---- Implicit by-reference edge cases ---- + + [Fact] + public void NineByteStruct_ImplicitByref_OneSlot() + { + // 9-byte struct > 8 -> implicit byref. The arg slot holds an 8-byte + // pointer, not the struct contents (would otherwise take 2 slots). + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "NineBytes", structSize: 9, + fields: + [ + new(0, CorElementType.I8), + new(8, CorElementType.I1), + ]); + sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.True(arg.IsPassedByRef); + Assert.Single(arg.Slots); + Assert.Equal(OffsetOfFirstGPArg, arg.Slots[0].Offset); + } + + [Fact] + public void ThreeByteStruct_NonPowerOfTwo_ImplicitByref() + { + // 3-byte struct: size <= 8 but not in {1, 2, 4, 8}. Implicit byref applies. + // Verifies the `(size & (size - 1)) != 0` guard in IsArgPassedByRefBySize. + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "ThreeBytes", structSize: 3, + fields: + [ + new(0, CorElementType.I1), + new(1, CorElementType.I1), + new(2, CorElementType.I1), + ]); + sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + Assert.True(layout.Arguments[0].IsPassedByRef); + } + + [Fact] + public void EightByteStruct_Enregisters_NotByref() + { + // 8-byte struct is in the {1, 2, 4, 8} exception list -> NOT byref; + // enregistered as a value in RCX. + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "EightBytes", structSize: 8, + fields: [new(0, CorElementType.I8)]); + sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.False(arg.IsPassedByRef); + Assert.Equal(OffsetOfFirstGPArg, arg.Slots[0].Offset); + } + + [Theory] + [InlineData(CorElementType.I4)] + [InlineData(CorElementType.I8)] + [InlineData(CorElementType.R4)] + [InlineData(CorElementType.R8)] + public void Return_PrimitiveType_NoRetBuf(CorElementType retType) + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => sig.Return(retType).Param(CorElementType.I4)); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Null(layout.ThisOffset); + Assert.Single(layout.Arguments); + Assert.Equal(OffsetOfFirstGPArg, layout.Arguments[0].Slots[0].Offset); + } + + [Theory] + [InlineData(4, false)] + [InlineData(8, false)] + [InlineData(16, true)] + [InlineData(24, true)] + public void Return_ValueType_RetBufBySize(int structSize, bool expectRetBuf) + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockDescriptors.CallingConvention.ValueTypeField[] fields = new MockDescriptors.CallingConvention.ValueTypeField[structSize / sizeof(int)]; + for (int i = 0; i < fields.Length; i++) + { + fields[i] = new(i * sizeof(int), CorElementType.I4); + } + + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, $"ReturnStruct{structSize}", structSize, fields); + sig.ReturnValueType(new TargetPointer(mt.Address)).Param(CorElementType.I4); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + Assert.Equal( + expectRetBuf ? OffsetOfFirstGPArg + Case.PointerSize : OffsetOfFirstGPArg, + layout.Arguments[0].Slots[0].Offset); + } + + [Fact] + public void EmptyStruct_ImplicitByref() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "Empty", structSize: 0, fields: []); + sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + Assert.True(layout.Arguments[0].IsPassedByRef); + } + + [Theory] + [InlineData(CorElementType.Object)] + [InlineData(CorElementType.String)] + public void GCReferenceArgs_GoToGPRegs_NotByref(CorElementType refType) + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => sig.Return(CorElementType.Void).Param(refType)); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.False(arg.IsPassedByRef); + Assert.Equal(OffsetOfFirstGPArg, arg.Slots[0].Offset); + } + + /// + /// On AMD64 Windows, vector types are classified purely by size — the iterator + /// never consults GetVectorSize. Vector64 (8 B) enregisters as a GP slot; + /// Vector128 (16 B) is passed via implicit byref. This test confirms that + /// end-to-end vector detection (synthetic metadata -> GetVectorSize -> ArgTypeInfo) + /// produces the same placement the size rule would. + /// + [Theory] + [InlineData("Vector64`1", 8, false)] + [InlineData("Vector128`1", 16, true)] + public void VectorType_OnWindows_ClassifiedBySizeNotVectorness( + string vectorTypeName, + int vectorByteSize, + bool expectByref) + { + SyntheticVectorMetadata metadata = s_syntheticVectorMetadata.Value; + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, + hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddVectorMethodTable( + rts, + vectorTypeName, + vectorByteSize, + metadata.GetTypeDefToken(vectorTypeName)); + sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); + }, + syntheticMetadata: metadata); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + + ArgLayout arg = layout.Arguments[0]; + Assert.Equal(expectByref, arg.IsPassedByRef); + Assert.Single(arg.Slots); + Assert.Equal(OffsetOfFirstGPArg, arg.Slots[0].Offset); + } + + [Fact] + public void TypedReference_ImplicitByref_OneSlot() + { + // TypedReference is 16 bytes -> implicit byref on Win x64 (in contrast + // with SysV where it lands in 2 GP regs). Verifies that the substitution + // through g_TypedReferenceMT produces a 16-byte ArgTypeInfo that then + // takes the byref path. + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable typedRefMT = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "System.TypedReference", structSize: 16, + fields: [new(0, CorElementType.Byref), new(8, CorElementType.I)]); + rts.SetTypedReferenceMethodTable(typedRefMT.Address); + sig.Return(CorElementType.Void).Param(CorElementType.TypedByRef); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.True(arg.IsPassedByRef); + Assert.Single(arg.Slots); + Assert.Equal(OffsetOfFirstGPArg, arg.Slots[0].Offset); + } +} diff --git a/src/native/managed/cdac/tests/CallingConvention/Arm32CallingConventionTests.cs b/src/native/managed/cdac/tests/CallingConvention/Arm32CallingConventionTests.cs new file mode 100644 index 00000000000000..db23e00206c9a5 --- /dev/null +++ b/src/native/managed/cdac/tests/CallingConvention/Arm32CallingConventionTests.cs @@ -0,0 +1,306 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.Tests; + +/// +/// ARM32 (AAPCS, hard-float) calling-convention tests. Up to 4 GP args in +/// R0-R3, FP args in S0-S15 / D0-D7 via a bitmap allocator. +/// +public class Arm32CallingConventionTests +{ + private static CallConvTestCase Case => CallConvCases.Arm32; + + private static int OffsetOfNthGPReg(int n) => Case.ArgumentRegistersOffset + n * Case.PointerSize; + private static int OffsetOfNthStackSlot(int n) => Case.OffsetOfArgs + n * Case.PointerSize; + + [Fact] + public void FourInts_FillR0_R3() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => + { + sig.Return(CorElementType.Void); + for (int i = 0; i < Case.NumArgumentRegisters; i++) sig.Param(CorElementType.I4); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(Case.NumArgumentRegisters, layout.Arguments.Count); + for (int i = 0; i < Case.NumArgumentRegisters; i++) + { + Assert.Equal(OffsetOfNthGPReg(i), layout.Arguments[i].Slots[0].Offset); + } + } + + [Fact] + public void FifthInt_GoesToStack() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => + { + sig.Return(CorElementType.Void); + for (int i = 0; i < Case.NumArgumentRegisters + 1; i++) sig.Param(CorElementType.I4); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(Case.NumArgumentRegisters + 1, layout.Arguments.Count); + Assert.Equal(OffsetOfNthStackSlot(0), layout.Arguments[4].Slots[0].Offset); + } + + [Theory] + [InlineData(CorElementType.I1, 4)] + [InlineData(CorElementType.I2, 4)] + [InlineData(CorElementType.I4, 4)] + [InlineData(CorElementType.I4, 5)] + public void IntArgs_FillR0_R3_AndSpillToStack(CorElementType elementType, int count) + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => + { + sig.Return(CorElementType.Void); + for (int i = 0; i < count; i++) + { + sig.Param(elementType); + } + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(count, layout.Arguments.Count); + + for (int i = 0; i < count; i++) + { + int expectedOffset = i < Case.NumArgumentRegisters + ? OffsetOfNthGPReg(i) + : OffsetOfNthStackSlot(i - Case.NumArgumentRegisters); + Assert.Equal(expectedOffset, layout.Arguments[i].Slots[0].Offset); + } + } + + [Fact] + public void InstanceMethod_ThisOffsetIsR0() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: true, + sig => sig.Return(CorElementType.Void)); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(OffsetOfNthGPReg(0), layout.ThisOffset); + } + + [Theory] + [InlineData(CorElementType.R4)] + [InlineData(CorElementType.R8)] + public void FloatArg_GoesToFirstFPRegister(CorElementType elementType) + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => sig.Return(CorElementType.Void).Param(elementType)); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + Assert.Equal(Case.OffsetOfFloatArgumentRegisters, layout.Arguments[0].Slots[0].Offset); + } + + [Fact] + public void MixedIntAndFloat_UseSeparateBanks() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => sig.Return(CorElementType.Void).Param(CorElementType.I4).Param(CorElementType.R8)); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(2, layout.Arguments.Count); + Assert.Equal(OffsetOfNthGPReg(0), layout.Arguments[0].Slots[0].Offset); + Assert.Equal(Case.OffsetOfFloatArgumentRegisters, layout.Arguments[1].Slots[0].Offset); + } + + [Theory] + [InlineData(CorElementType.Object)] + [InlineData(CorElementType.String)] + public void GCReferenceArgs_GoToGPRegs_NotByref(CorElementType refType) + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => sig.Return(CorElementType.Void).Param(refType)); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + Assert.False(layout.Arguments[0].IsPassedByRef); + Assert.Equal(OffsetOfNthGPReg(0), layout.Arguments[0].Slots[0].Offset); + } + + [Fact] + public void I8_AfterI4_AlignsToEvenRegisterPair() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => sig + .Return(CorElementType.Void) + .Param(CorElementType.I4) + .Param(CorElementType.I8) + .Param(CorElementType.I4)); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(3, layout.Arguments.Count); + Assert.Equal(OffsetOfNthGPReg(0), layout.Arguments[0].Slots[0].Offset); + Assert.Equal(OffsetOfNthGPReg(2), layout.Arguments[1].Slots[0].Offset); + Assert.Equal(OffsetOfNthStackSlot(0), layout.Arguments[2].Slots[0].Offset); + } + + [Fact] + public void LargeStruct_PassedByValueOnStack() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, + "BigStruct", + structSize: 24, + fields: + [ + new(0, CorElementType.I4), + new(4, CorElementType.I4), + new(8, CorElementType.I4), + new(12, CorElementType.I4), + new(16, CorElementType.I4), + new(20, CorElementType.I4), + ]); + sig.Return(CorElementType.Void); + for (int i = 0; i < Case.NumArgumentRegisters; i++) + { + sig.Param(CorElementType.I4); + } + sig.ParamValueType(new TargetPointer(mt.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(Case.NumArgumentRegisters + 1, layout.Arguments.Count); + Assert.False(layout.Arguments[4].IsPassedByRef); + Assert.Equal(OffsetOfNthStackSlot(0), layout.Arguments[4].Slots[0].Offset); + } + + [Fact] + public void EightInts_FourInRegs_FourOnStack() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => + { + sig.Return(CorElementType.Void); + for (int i = 0; i < 8; i++) + { + sig.Param(CorElementType.I4); + } + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(8, layout.Arguments.Count); + + for (int i = 0; i < 8; i++) + { + int expectedOffset = i < Case.NumArgumentRegisters + ? OffsetOfNthGPReg(i) + : OffsetOfNthStackSlot(i - Case.NumArgumentRegisters); + Assert.Equal(expectedOffset, layout.Arguments[i].Slots[0].Offset); + } + } + + [Fact] + public void TypedReference_ConsumesR0AndR1() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable typedRefMT = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, + "System.TypedReference", + structSize: 8, + fields: [new(0, CorElementType.Byref), new(4, CorElementType.I)]); + rts.SetTypedReferenceMethodTable(typedRefMT.Address); + sig.Return(CorElementType.Void).Param(CorElementType.TypedByRef).Param(CorElementType.I4); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(2, layout.Arguments.Count); + Assert.False(layout.Arguments[0].IsPassedByRef); + Assert.Equal(OffsetOfNthGPReg(0), layout.Arguments[0].Slots[0].Offset); + Assert.Equal(OffsetOfNthGPReg(2), layout.Arguments[1].Slots[0].Offset); + } + + [Theory] + [InlineData(false, 1)] + [InlineData(true, 2)] + public void ReturnBuffer_ShiftsFirstUserArg(bool hasThis, int expectedUserArgRegister) + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, + hasThis, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, + "BigReturn", + structSize: 24, + fields: + [ + new(0, CorElementType.I4), + new(4, CorElementType.I4), + new(8, CorElementType.I4), + new(12, CorElementType.I4), + new(16, CorElementType.I4), + new(20, CorElementType.I4), + ]); + sig.ReturnValueType(new TargetPointer(mt.Address)).Param(CorElementType.I4); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + Assert.Equal(OffsetOfNthGPReg(expectedUserArgRegister), layout.Arguments[0].Slots[0].Offset); + } + + // ---- Soft-float (armel) ---- + + [Theory] + [InlineData(CorElementType.R4)] + [InlineData(CorElementType.R8)] + public void SoftFloat_FloatArg_GoesToGPReg_NotFPReg(CorElementType elementType) + { + // On armel (soft-float), FeatureArmSoftFP is present and all arguments + // -- including floats and doubles -- go through the GP register / stack + // path. This mirrors the native #ifndef ARM_SOFTFP gate in + // callingconvention.h:1546. + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (_, sig) => sig.Return(CorElementType.Void).Param(elementType), + isArmSoftFP: true); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + // On soft-float the arg should be in R0 (first GP reg), not S0/D0. + Assert.Equal(OffsetOfNthGPReg(0), layout.Arguments[0].Slots[0].Offset); + } + + [Fact] + public void SoftFloat_MixedIntAndFloat_BothInGPRegs() + { + // On armel, int and float args share the same GP register bank. + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (_, sig) => sig.Return(CorElementType.Void).Param(CorElementType.I4).Param(CorElementType.R4), + isArmSoftFP: true); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(2, layout.Arguments.Count); + Assert.Equal(OffsetOfNthGPReg(0), layout.Arguments[0].Slots[0].Offset); + Assert.Equal(OffsetOfNthGPReg(1), layout.Arguments[1].Slots[0].Offset); + } +} diff --git a/src/native/managed/cdac/tests/CallingConvention/Arm64CallingConventionTests.cs b/src/native/managed/cdac/tests/CallingConvention/Arm64CallingConventionTests.cs new file mode 100644 index 00000000000000..bf086f31de8ebf --- /dev/null +++ b/src/native/managed/cdac/tests/CallingConvention/Arm64CallingConventionTests.cs @@ -0,0 +1,602 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.Tests; + +/// +/// ARM64 (AAPCS64) calling-convention tests. Up to 8 GP args in X0-X7, FP +/// args in V0-V7. HFAs (homogeneous aggregates of 1-4 floats/doubles) are +/// passed in consecutive FP registers; large value types via implicit byref. +/// +public class Arm64CallingConventionTests +{ + private static CallConvTestCase Case => CallConvCases.Arm64Windows; + + private static int OffsetOfNthGPReg(int n) => Case.ArgumentRegistersOffset + n * Case.PointerSize; + private static int OffsetOfNthFPReg(int n) => Case.OffsetOfFloatArgumentRegisters!.Value + n * Case.FloatRegisterSize; + private static int OffsetOfNthStackSlot(int n) => Case.OffsetOfArgs + n * Case.PointerSize; + + [Fact] + public void EightInts_FillX0_X7() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => + { + sig.Return(CorElementType.Void); + for (int i = 0; i < Case.NumArgumentRegisters; i++) sig.Param(CorElementType.I8); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(Case.NumArgumentRegisters, layout.Arguments.Count); + for (int i = 0; i < Case.NumArgumentRegisters; i++) + Assert.Equal(OffsetOfNthGPReg(i), layout.Arguments[i].Slots[0].Offset); + } + + [Fact] + public void EightDoubles_FillV0_V7() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => + { + sig.Return(CorElementType.Void); + for (int i = 0; i < 8; i++) sig.Param(CorElementType.R8); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(8, layout.Arguments.Count); + for (int i = 0; i < 8; i++) + Assert.Equal(OffsetOfNthFPReg(i), layout.Arguments[i].Slots[0].Offset); + } + + [Fact] + public void InstanceMethod_ThisOffsetIsX0() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: true, + sig => sig.Return(CorElementType.Void)); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(OffsetOfNthGPReg(0), layout.ThisOffset); + } + + [Theory] + [InlineData(CorElementType.I1, 8)] + [InlineData(CorElementType.I2, 8)] + [InlineData(CorElementType.I4, 8)] + [InlineData(CorElementType.I8, 8)] + [InlineData(CorElementType.U1, 8)] + [InlineData(CorElementType.U2, 8)] + [InlineData(CorElementType.U4, 8)] + [InlineData(CorElementType.U8, 8)] + [InlineData(CorElementType.I4, 9)] + public void IntArgs_FillX0_X7_AndSpillToStack(CorElementType elementType, int count) + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => + { + sig.Return(CorElementType.Void); + for (int i = 0; i < count; i++) + { + sig.Param(elementType); + } + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(count, layout.Arguments.Count); + + for (int i = 0; i < count; i++) + { + int expectedOffset = i < Case.NumArgumentRegisters + ? OffsetOfNthGPReg(i) + : OffsetOfNthStackSlot(i - Case.NumArgumentRegisters); + Assert.Equal(expectedOffset, layout.Arguments[i].Slots[0].Offset); + } + } + + [Theory] + [InlineData(CorElementType.R4, 1)] + [InlineData(CorElementType.R8, 1)] + [InlineData(CorElementType.R4, 8)] + [InlineData(CorElementType.R8, 8)] + [InlineData(CorElementType.R4, 10)] + [InlineData(CorElementType.R8, 10)] + public void FloatArgs_FillV0_V7_AndSpillToStack(CorElementType elementType, int count) + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => + { + sig.Return(CorElementType.Void); + for (int i = 0; i < count; i++) + { + sig.Param(elementType); + } + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(count, layout.Arguments.Count); + + for (int i = 0; i < count; i++) + { + int expectedOffset = i < Case.NumFloatArgumentRegisters + ? OffsetOfNthFPReg(i) + : OffsetOfNthStackSlot(i - Case.NumFloatArgumentRegisters); + Assert.Equal(expectedOffset, layout.Arguments[i].Slots[0].Offset); + } + } + + [Fact] + public void MixedIntAndFloat_UseSeparateBanks() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => sig + .Return(CorElementType.Void) + .Param(CorElementType.I8) + .Param(CorElementType.R8) + .Param(CorElementType.I8) + .Param(CorElementType.R8)); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(4, layout.Arguments.Count); + Assert.Equal(OffsetOfNthGPReg(0), layout.Arguments[0].Slots[0].Offset); + Assert.Equal(OffsetOfNthFPReg(0), layout.Arguments[1].Slots[0].Offset); + Assert.Equal(OffsetOfNthGPReg(1), layout.Arguments[2].Slots[0].Offset); + Assert.Equal(OffsetOfNthFPReg(1), layout.Arguments[3].Slots[0].Offset); + } + + [Theory] + [InlineData(false, false, false, false)] + [InlineData(true, false, false, false)] + [InlineData(true, true, false, false)] + [InlineData(false, false, true, false)] + [InlineData(true, false, true, false)] + [InlineData(true, true, true, true)] + public void HiddenArgs_DoNotAffectFirstUserDouble( + bool hasThis, + bool hasRetBuf, + bool hasParamType, + bool hasAsyncCont) + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, + hasThis, + (rts, sig) => + { + if (hasRetBuf) + { + MockMethodTable bigMT = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, + "BigReturn", + structSize: 24, + fields: + [ + new(0, CorElementType.I8), + new(8, CorElementType.I8), + new(16, CorElementType.I8), + ]); + sig.ReturnValueType(new TargetPointer(bigMT.Address)); + } + else + { + sig.Return(CorElementType.Void); + } + + sig.Param(CorElementType.R8); + }, + hasParamType, + hasAsyncCont); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + Assert.Equal(OffsetOfNthFPReg(0), layout.Arguments[0].Slots[0].Offset); + } + + [Fact] + public void LargeStruct_ImplicitByRef_ConsumesOneGPReg() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, + "BigStruct", + structSize: 24, + fields: + [ + new(0, CorElementType.I8), + new(8, CorElementType.I8), + new(16, CorElementType.I8), + ]); + sig.Return(CorElementType.Void) + .ParamValueType(new TargetPointer(mt.Address)) + .Param(CorElementType.I8); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(2, layout.Arguments.Count); + Assert.True(layout.Arguments[0].IsPassedByRef); + Assert.Equal(OffsetOfNthGPReg(0), layout.Arguments[0].Slots[0].Offset); + Assert.Equal(OffsetOfNthGPReg(1), layout.Arguments[1].Slots[0].Offset); + } + + [Fact] + public void SixteenByteStruct_NotByRef_ConsumesTwoGPRegs() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, + "TwoLongs", + structSize: 16, + fields: [new(0, CorElementType.I8), new(8, CorElementType.I8)]); + sig.Return(CorElementType.Void) + .ParamValueType(new TargetPointer(mt.Address)) + .Param(CorElementType.I8); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(2, layout.Arguments.Count); + Assert.False(layout.Arguments[0].IsPassedByRef); + Assert.Equal(OffsetOfNthGPReg(0), layout.Arguments[0].Slots[0].Offset); + Assert.Equal(OffsetOfNthGPReg(2), layout.Arguments[1].Slots[0].Offset); + } + + [Fact] + public void TypedReference_ConsumesTwoGPRegs() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable typedRefMT = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, + "System.TypedReference", + structSize: 16, + fields: [new(0, CorElementType.Byref), new(8, CorElementType.I)]); + rts.SetTypedReferenceMethodTable(typedRefMT.Address); + sig.Return(CorElementType.Void) + .Param(CorElementType.TypedByRef) + .Param(CorElementType.I8); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(2, layout.Arguments.Count); + Assert.False(layout.Arguments[0].IsPassedByRef); + Assert.Equal(OffsetOfNthGPReg(0), layout.Arguments[0].Slots[0].Offset); + Assert.Equal(OffsetOfNthGPReg(2), layout.Arguments[1].Slots[0].Offset); + } + + [Theory] + [InlineData(CorElementType.Object)] + [InlineData(CorElementType.String)] + public void GCReferenceArgs_GoToGPRegs_NotByref(CorElementType refType) + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => sig.Return(CorElementType.Void).Param(refType)); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + Assert.False(layout.Arguments[0].IsPassedByRef); + Assert.Equal(OffsetOfNthGPReg(0), layout.Arguments[0].Slots[0].Offset); + } + + [Fact] + public void TenArgs_EightInRegs_TwoOnStack() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => + { + sig.Return(CorElementType.Void); + for (int i = 0; i < 10; i++) + { + sig.Param(CorElementType.I8); + } + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(10, layout.Arguments.Count); + + for (int i = 0; i < 10; i++) + { + int expectedOffset = i < Case.NumArgumentRegisters + ? OffsetOfNthGPReg(i) + : OffsetOfNthStackSlot(i - Case.NumArgumentRegisters); + Assert.Equal(expectedOffset, layout.Arguments[i].Slots[0].Offset); + } + } + + [Fact] + public void VarArgs_DoubleUsesGPReg_NotFPReg() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => sig.VarArg().Return(CorElementType.Void).Param(CorElementType.R8)); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.NotNull(layout.VarArgCookieOffset); + Assert.Single(layout.Arguments); + Assert.Equal(OffsetOfNthGPReg(0), layout.VarArgCookieOffset); + Assert.Equal(OffsetOfNthGPReg(1), layout.Arguments[0].Slots[0].Offset); + } + + [Fact] + public void Return_LargeStruct_RetBufInX8_FirstUserArgStaysInX0() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, + "BigReturn", + structSize: 24, + fields: + [ + new(0, CorElementType.I8), + new(8, CorElementType.I8), + new(16, CorElementType.I8), + ]); + sig.ReturnValueType(new TargetPointer(mt.Address)).Param(CorElementType.I8); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + Assert.Equal(OffsetOfNthGPReg(0), layout.Arguments[0].Slots[0].Offset); + } + + // ---- Homogeneous Floating-point Aggregate (HFA) helpers ---- + + private const uint WFlagsLow_IsHFA = 0x800; + + /// + /// Allocates a value-type MethodTable representing an HFA of + /// elements of (R4 or R8). Sets the IsHFA flag so the + /// signature provider reports IsHomogeneousAggregate=true with the right + /// element size. + /// + private static MockMethodTable AddHFA(MockDescriptors.RuntimeTypeSystem rts, CorElementType elemType, int count, string name) + { + int elemSize = elemType == CorElementType.R4 ? 4 : 8; + MockDescriptors.CallingConvention.ValueTypeField[] fields = new MockDescriptors.CallingConvention.ValueTypeField[count]; + for (int i = 0; i < count; i++) + fields[i] = new(i * elemSize, elemType); + + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, name, structSize: count * elemSize, fields: fields); + mt.MTFlags |= WFlagsLow_IsHFA; + return mt; + } + + // ----- Open audit gaps for ARM64 ----- +#pragma warning disable xUnit1004 // Test methods should not be skipped — these track audit gaps. + + [Fact] + public void Windows_VarArgs_StructSpansX7AndStack() + { + // Cookie consumes one GP slot (X0). 6 user longs fill X1-X6. + // The 16-byte struct's first 8 bytes fit in X7; second 8 bytes spill to stack. + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "TwoLongs", structSize: 16, + fields: [new(0, CorElementType.I8), new(8, CorElementType.I8)]); + sig.VarArg().Return(CorElementType.Void); + for (int i = 0; i < 6; i++) sig.Param(CorElementType.I8); + sig.ParamValueType(new TargetPointer(mt.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(7, layout.Arguments.Count); + ArgLayout structArg = layout.Arguments[6]; + Assert.Equal(2, structArg.Slots.Count); + Assert.Equal(OffsetOfNthGPReg(7), structArg.Slots[0].Offset); + Assert.Equal(OffsetOfNthStackSlot(0), structArg.Slots[1].Offset); + } + + // Under varargs, HFAs lose their HFA treatment and go through the GP path. + // They should NOT split across X7/stack -- they should either fit entirely + // in GP regs or go entirely to stack (no split for HFA-shaped composites). + [Fact] + public void Windows_VarArgs_HFA_DoesNotSplit_GoesToStack() + { + // Cookie consumes X0. 7 user longs fill X1-X7 (cookie + 7 = 8 total). + // The 4-float HFA (16 bytes, treated as composite under varargs) can't fit + // in GP regs (all exhausted). Under varargs, the FP path is skipped, so the + // HFA goes entirely to stack -- no split. + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable hfa = AddHFA(rts, CorElementType.R4, 4, "HFA_4F_VarArg"); + sig.VarArg().Return(CorElementType.Void); + for (int i = 0; i < 7; i++) sig.Param(CorElementType.I8); + sig.ParamValueType(new TargetPointer(hfa.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(8, layout.Arguments.Count); + ArgLayout hfaArg = layout.Arguments[7]; + // Under varargs the HFA is NOT passed in FP regs -- it goes through GP path. + // All GP regs are consumed, so the entire arg goes to stack (no split). + Assert.Single(hfaArg.Slots); + Assert.Equal(OffsetOfNthStackSlot(0), hfaArg.Slots[0].Offset); + } + + // ---- HFA per-slot reporting ---- + // + // ARM64 HFAs (homogeneous aggregates of 1-4 floats/doubles) are passed + // in consecutive FP registers. The iterator emits one ArgLocation per FP + // register with ElementType set to the HFA element type (R4 or R8) -- + // analogous to how SystemV per-eightbyte emission works on AMD64-Unix. + + [Theory] + [InlineData(CorElementType.R4, 2)] + [InlineData(CorElementType.R4, 3)] + [InlineData(CorElementType.R4, 4)] + [InlineData(CorElementType.R8, 2)] + [InlineData(CorElementType.R8, 3)] + [InlineData(CorElementType.R8, 4)] + public void HFA_OccupiesNConsecutiveFPRegs(CorElementType elemType, int count) + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = AddHFA(rts, elemType, count, $"HFA_{elemType}_{count}"); + sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.False(arg.IsPassedByRef); + Assert.Equal(count, arg.Slots.Count); + + for (int i = 0; i < count; i++) + { + Assert.Equal(elemType, arg.Slots[i].ElementType); + Assert.Equal(OffsetOfNthFPReg(i), arg.Slots[i].Offset); + } + } + + // HFA after some FP args: the HFA's first FP register is at the current + // FP-allocation index, not back at V0. Verifies the iterator correctly + // advances past the HFA so subsequent FP args don't overlap. + [Fact] + public void HFA_AfterTwoDoubles_StartsAtV2() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable hfa = AddHFA(rts, CorElementType.R8, 3, "HFA_3D"); + sig.Return(CorElementType.Void) + .Param(CorElementType.R8) + .Param(CorElementType.R8) + .ParamValueType(new TargetPointer(hfa.Address)) + .Param(CorElementType.R8); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(4, layout.Arguments.Count); + + // 2 doubles in V0, V1 + Assert.Equal(OffsetOfNthFPReg(0), layout.Arguments[0].Slots[0].Offset); + Assert.Equal(OffsetOfNthFPReg(1), layout.Arguments[1].Slots[0].Offset); + + // 3-double HFA in V2, V3, V4 + ArgLayout hfaArg = layout.Arguments[2]; + Assert.Equal(3, hfaArg.Slots.Count); + for (int i = 0; i < 3; i++) + Assert.Equal(OffsetOfNthFPReg(2 + i), hfaArg.Slots[i].Offset); + + // Trailing double picks up at V5 + Assert.Equal(OffsetOfNthFPReg(5), layout.Arguments[3].Slots[0].Offset); + } + + // HFA that exactly fills the remaining FP regs (boundary case): 6 doubles + // before a 2-double HFA → HFA fits in V6 + V7 with no overflow. + [Fact] + public void HFA_FitsExactlyInRemainingFPRegs() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable hfa = AddHFA(rts, CorElementType.R8, 2, "HFA_2D"); + sig.Return(CorElementType.Void); + for (int i = 0; i < 6; i++) sig.Param(CorElementType.R8); + sig.ParamValueType(new TargetPointer(hfa.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(7, layout.Arguments.Count); + + ArgLayout hfaArg = layout.Arguments[6]; + Assert.Equal(2, hfaArg.Slots.Count); + Assert.Equal(OffsetOfNthFPReg(6), hfaArg.Slots[0].Offset); + Assert.Equal(OffsetOfNthFPReg(7), hfaArg.Slots[1].Offset); + } + + // HFA that doesn't fit in the remaining FP regs: 5 doubles + 4-double HFA + // (needs 4 slots, only 3 remain) → ENTIRE HFA spills to stack, FP regs are + // marked exhausted, and no further FP enregistration happens. + [Fact] + public void HFA_DoesNotFit_EntireHFASpillsToStack() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable hfa = AddHFA(rts, CorElementType.R8, 4, "HFA_4D"); + sig.Return(CorElementType.Void); + for (int i = 0; i < 5; i++) sig.Param(CorElementType.R8); + sig.ParamValueType(new TargetPointer(hfa.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(6, layout.Arguments.Count); + + ArgLayout hfaArg = layout.Arguments[5]; + // Single stack slot covering the full 32-byte struct payload + Assert.Single(hfaArg.Slots); + Assert.Equal(OffsetOfNthStackSlot(0), hfaArg.Slots[0].Offset); + } + + // HFA placement is byref-free: a 4-double HFA (32 bytes > 16) is normally + // passed by implicit byref for ordinary value types, but the HFA path + // overrides that and keeps it as a multi-FP-reg pass-by-value. + [Fact] + public void HFA_FourDoubles_NotByRef() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable hfa = AddHFA(rts, CorElementType.R8, 4, "HFA_4D"); + sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(hfa.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + Assert.False(layout.Arguments[0].IsPassedByRef); + Assert.Equal(4, layout.Arguments[0].Slots.Count); + for (int i = 0; i < 4; i++) + Assert.Equal(OffsetOfNthFPReg(i), layout.Arguments[0].Slots[i].Offset); + } + + // Original 4-float HFA test (kept for continuity with the earlier audit-gap marker). + [Fact] + public void HFA_FourFloats_ShouldReportFourFPSlots() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = AddHFA(rts, CorElementType.R4, 4, "HFA_4F"); + sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + Assert.Equal(4, layout.Arguments[0].Slots.Count); + for (int i = 0; i < 4; i++) + Assert.Equal(OffsetOfNthFPReg(i), layout.Arguments[0].Slots[i].Offset); + } + +#pragma warning restore xUnit1004 +} diff --git a/src/native/managed/cdac/tests/CallingConvention/CallConvCases.cs b/src/native/managed/cdac/tests/CallingConvention/CallConvCases.cs new file mode 100644 index 00000000000000..682d0e7ce8c553 --- /dev/null +++ b/src/native/managed/cdac/tests/CallingConvention/CallConvCases.cs @@ -0,0 +1,85 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Diagnostics.DataContractReader.Contracts; + +namespace Microsoft.Diagnostics.DataContractReader.Tests; + +public sealed record CallConvTestCase( + string Name, + RuntimeInfoArchitecture Architecture, + RuntimeInfoOperatingSystem OperatingSystem, + bool Is64Bit, + int TransitionBlockSize, + int ArgumentRegistersOffset, + int FirstGCRefMapSlot, + int OffsetOfArgs, + int? OffsetOfFloatArgumentRegisters, + int NumArgumentRegisters, + int NumFloatArgumentRegisters, + int FloatRegisterSize) +{ + public int PointerSize => Is64Bit ? 8 : 4; + public MockTarget.Architecture MockArch => new() { IsLittleEndian = true, Is64Bit = Is64Bit }; + public override string ToString() => Name; +} + +public static class CallConvCases +{ + public static readonly CallConvTestCase X86 = new( + "x86", RuntimeInfoArchitecture.X86, RuntimeInfoOperatingSystem.Windows, Is64Bit: false, + TransitionBlockSize: 20, ArgumentRegistersOffset: 0, FirstGCRefMapSlot: 0, + OffsetOfArgs: 20, OffsetOfFloatArgumentRegisters: null, + NumArgumentRegisters: 2, NumFloatArgumentRegisters: 0, FloatRegisterSize: 0); + + public static readonly CallConvTestCase AMD64Windows = new( + "AMD64-Windows", RuntimeInfoArchitecture.X64, RuntimeInfoOperatingSystem.Windows, Is64Bit: true, + TransitionBlockSize: 40, ArgumentRegistersOffset: 40, FirstGCRefMapSlot: 40, + OffsetOfArgs: 40, OffsetOfFloatArgumentRegisters: 0, + NumArgumentRegisters: 4, NumFloatArgumentRegisters: 0, FloatRegisterSize: 16); + + public static readonly CallConvTestCase AMD64Unix = new( + "AMD64-Unix", RuntimeInfoArchitecture.X64, RuntimeInfoOperatingSystem.Unix, Is64Bit: true, + TransitionBlockSize: 48, ArgumentRegistersOffset: 0, FirstGCRefMapSlot: 0, + OffsetOfArgs: 48, OffsetOfFloatArgumentRegisters: -128, + NumArgumentRegisters: 6, NumFloatArgumentRegisters: 8, FloatRegisterSize: 16); + + public static readonly CallConvTestCase Arm32 = new( + "ARM32", RuntimeInfoArchitecture.Arm, RuntimeInfoOperatingSystem.Windows, Is64Bit: false, + TransitionBlockSize: 48, ArgumentRegistersOffset: 32, FirstGCRefMapSlot: 32, + OffsetOfArgs: 48, OffsetOfFloatArgumentRegisters: -68, + NumArgumentRegisters: 4, NumFloatArgumentRegisters: 16, FloatRegisterSize: 4); + + public static readonly CallConvTestCase Arm64Windows = new( + "ARM64-Windows", RuntimeInfoArchitecture.Arm64, RuntimeInfoOperatingSystem.Windows, Is64Bit: true, + TransitionBlockSize: 160, ArgumentRegistersOffset: 96, FirstGCRefMapSlot: 88, + OffsetOfArgs: 160, OffsetOfFloatArgumentRegisters: -128, + NumArgumentRegisters: 8, NumFloatArgumentRegisters: 8, FloatRegisterSize: 16); + + public static readonly CallConvTestCase Arm64Apple = new( + "ARM64-Apple", RuntimeInfoArchitecture.Arm64, RuntimeInfoOperatingSystem.Apple, Is64Bit: true, + TransitionBlockSize: 160, ArgumentRegistersOffset: 96, FirstGCRefMapSlot: 88, + OffsetOfArgs: 160, OffsetOfFloatArgumentRegisters: -128, + NumArgumentRegisters: 8, NumFloatArgumentRegisters: 8, FloatRegisterSize: 16); + + public static readonly CallConvTestCase LoongArch64 = new( + "LoongArch64", RuntimeInfoArchitecture.LoongArch64, RuntimeInfoOperatingSystem.Unix, Is64Bit: true, + TransitionBlockSize: 176, ArgumentRegistersOffset: 112, FirstGCRefMapSlot: 112, + OffsetOfArgs: 176, OffsetOfFloatArgumentRegisters: -64, + NumArgumentRegisters: 8, NumFloatArgumentRegisters: 8, FloatRegisterSize: 8); + + public static readonly CallConvTestCase RiscV64 = new( + "RiscV64", RuntimeInfoArchitecture.RiscV64, RuntimeInfoOperatingSystem.Unix, Is64Bit: true, + TransitionBlockSize: 192, ArgumentRegistersOffset: 128, FirstGCRefMapSlot: 128, + OffsetOfArgs: 192, OffsetOfFloatArgumentRegisters: -64, + NumArgumentRegisters: 8, NumFloatArgumentRegisters: 8, FloatRegisterSize: 8); + + public static IEnumerable AllCases => new[] + { + new object[] { X86 }, new object[] { AMD64Windows }, new object[] { AMD64Unix }, new object[] { Arm32 }, + new object[] { Arm64Windows }, new object[] { Arm64Apple }, new object[] { LoongArch64 }, new object[] { RiscV64 }, + }; + + public static IEnumerable AMD64UnixOnly => new[] { new object[] { AMD64Unix } }; +} diff --git a/src/native/managed/cdac/tests/CallingConvention/CallingConventionTestHelpers.cs b/src/native/managed/cdac/tests/CallingConvention/CallingConventionTestHelpers.cs new file mode 100644 index 00000000000000..0467a63bdf6b29 --- /dev/null +++ b/src/native/managed/cdac/tests/CallingConvention/CallingConventionTestHelpers.cs @@ -0,0 +1,230 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection.Metadata; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.DataContractReader.RuntimeTypeSystemHelpers; +using Moq; +using ModuleHandle = Microsoft.Diagnostics.DataContractReader.Contracts.ModuleHandle; +using TypeHandle = Microsoft.Diagnostics.DataContractReader.Contracts.TypeHandle; + +namespace Microsoft.Diagnostics.DataContractReader.Tests; + +/// +/// End-to-end test harness for the contract. +/// Builds a mock target containing a single stored-sig EEImpl method whose +/// signature is encoded as a raw blob — so the harness bypasses the +/// metadata reader entirely. +/// +internal static class CallingConventionTestHelpers +{ + public static (Target Target, MethodDescHandle Handle) CreateTargetWithStaticMethod( + CallConvTestCase testCase, + Action buildSignature, + SyntheticVectorMetadata? syntheticMetadata = null) + => CreateTargetWithMethod(testCase, hasThis: false, (_, sig) => buildSignature(sig), syntheticMetadata: syntheticMetadata); + + public static (Target Target, MethodDescHandle Handle) CreateTargetWithMethod( + CallConvTestCase testCase, + bool hasThis, + Action buildSignature, + bool hasParamType = false, + bool hasAsyncContinuation = false, + SyntheticVectorMetadata? syntheticMetadata = null) + => CreateTargetWithMethod(testCase, hasThis, (_, sig) => buildSignature(sig), hasParamType, hasAsyncContinuation, syntheticMetadata: syntheticMetadata); + + /// + /// Richer overload that gives the test callback access to the + /// builder so it can + /// allocate auxiliary mock types (e.g. value-type MTs for + /// ELEMENT_TYPE_INTERNAL sig references) before building the + /// method signature. + /// + /// + /// Optional callback to choose a different MethodTable as the enclosing + /// class of the test method (default is System.Object). The callback + /// runs after the configure callback so it can refer to MTs the + /// configure callback allocated. + /// + public static (Target Target, MethodDescHandle Handle) CreateTargetWithMethod( + CallConvTestCase testCase, + bool hasThis, + Action configure, + bool hasParamType = false, + bool hasAsyncContinuation = false, + Func? enclosingMTOverride = null, + SyntheticVectorMetadata? syntheticMetadata = null, + bool isArmSoftFP = false) + { + var targetBuilder = new TestPlaceholderTarget.Builder(testCase.MockArch); + MockDescriptors.RuntimeTypeSystem rtsBuilder = new(targetBuilder.MemoryBuilder); + MockLoaderBuilder loaderBuilder = new(targetBuilder.MemoryBuilder); + MockDescriptors.MockMethodDescriptorsBuilder mdsBuilder = new(rtsBuilder, loaderBuilder); + + MockLoaderModule mockModule = loaderBuilder.AddModule(simpleName: "TestModule"); + rtsBuilder.SystemObjectMethodTable.Module = mockModule.Address; + rtsBuilder.ContinuationMethodTable.Module = mockModule.Address; + + int pointerSize = targetBuilder.MemoryBuilder.TargetTestHelpers.PointerSize; + SignatureBlobBuilder sigBuilder = new(pointerSize, hasThis); + configure(rtsBuilder, sigBuilder); + byte[] sigBytes = sigBuilder.Build(); + + MockMethodTable enclosingMT = enclosingMTOverride?.Invoke(rtsBuilder) + ?? (hasParamType ? CreateSharedGenericMethodTable(rtsBuilder, hasThis) : rtsBuilder.SystemObjectMethodTable); + enclosingMT.Module = mockModule.Address; + MockMethodTableAuxiliaryData aux = rtsBuilder.AddMethodTableAuxiliaryData(); + aux.LoaderModule = mockModule.Address; + enclosingMT.AuxiliaryData = aux.Address; + + MockMemorySpace.HeapFragment sigFragment = rtsBuilder.TypeSystemAllocator.Allocate( + (ulong)sigBytes.Length, "TestMethodSig"); + Array.Copy(sigBytes, sigFragment.Data, sigBytes.Length); + + uint methodDescTotalSize = (uint)mdsBuilder.StoredSigMethodDescLayout.Size; + if (hasAsyncContinuation) + { + methodDescTotalSize += mdsBuilder.AsyncMethodDataSize; + } + + byte methodDescSize = (byte)(methodDescTotalSize / mdsBuilder.MethodDescAlignment); + byte chunkCount = 1; + byte chunkSize = (byte)(chunkCount * methodDescSize); + MockMethodDescChunk chunk = mdsBuilder.AddMethodDescChunk("TestMethod", chunkSize); + chunk.MethodTable = enclosingMT.Address; + chunk.Size = chunkSize; + chunk.Count = chunkCount; + + MockStoredSigMethodDesc md = chunk.GetMethodDescAtChunkIndex(0, mdsBuilder.StoredSigMethodDescLayout); + md.ChunkIndex = 0; + md.Slot = 0; + + ushort methodFlags = (ushort)MethodClassification.EEImpl; + if (!hasThis) + { + methodFlags |= (ushort)MethodDescFlags_1.MethodDescFlags.Static; + } + + if (hasAsyncContinuation) + { + methodFlags |= (ushort)MethodDescFlags_1.MethodDescFlags.HasAsyncMethodData; + } + + md.Flags = methodFlags; + md.Sig = sigFragment.Address; + md.CSig = (uint)sigBytes.Length; + + if (hasAsyncContinuation) + { + int asyncDataOffset = (int)(md.Address - chunk.Address) + mdsBuilder.StoredSigMethodDescLayout.Size; + targetBuilder.MemoryBuilder.TargetTestHelpers.Write( + chunk.Memory.Span.Slice(asyncDataOffset, sizeof(uint)), + (uint)RuntimeTypeSystem_1.AsyncMethodFlags.AsyncCall); + } + + Dictionary types = new Dictionary() + { + [DataType.TransitionBlock] = MockDescriptors.CallingConvention.CreateTransitionBlockTypeInfo(testCase), + [DataType.FieldDesc] = MockDescriptors.CallingConvention.CreateFieldDescTypeInfo(testCase.MockArch), + [DataType.MethodDesc] = TargetTestHelpers.CreateTypeInfo(mdsBuilder.MethodDescLayout), + [DataType.MethodDescChunk] = TargetTestHelpers.CreateTypeInfo(mdsBuilder.MethodDescChunkLayout), + [DataType.StoredSigMethodDesc] = TargetTestHelpers.CreateTypeInfo(mdsBuilder.StoredSigMethodDescLayout), + [DataType.EEImplMethodDesc] = new Target.TypeInfo { Size = mdsBuilder.EEImplMethodDescSize }, + [DataType.InstantiatedMethodDesc] = TargetTestHelpers.CreateTypeInfo(mdsBuilder.InstantiatedMethodDescLayout), + [DataType.DynamicMethodDesc] = TargetTestHelpers.CreateTypeInfo(mdsBuilder.DynamicMethodDescLayout), + [DataType.NonVtableSlot] = new Target.TypeInfo { Size = mdsBuilder.NonVtableSlotSize }, + [DataType.MethodImpl] = new Target.TypeInfo { Size = mdsBuilder.MethodImplSize }, + [DataType.NativeCodeSlot] = new Target.TypeInfo { Size = mdsBuilder.NativeCodeSlotSize }, + [DataType.AsyncMethodData] = new Target.TypeInfo + { + Size = mdsBuilder.AsyncMethodDataSize, + Fields = new Dictionary + { + [nameof(Data.AsyncMethodData.Flags)] = new Target.FieldInfo { Offset = 0 }, + }, + }, + [DataType.ArrayMethodDesc] = new Target.TypeInfo { Size = mdsBuilder.ArrayMethodDescSize }, + [DataType.FCallMethodDesc] = new Target.TypeInfo { Size = mdsBuilder.FCallMethodDescSize }, + [DataType.PInvokeMethodDesc] = new Target.TypeInfo { Size = mdsBuilder.PInvokeMethodDescSize }, + [DataType.CLRToCOMCallMethodDesc] = new Target.TypeInfo { Size = mdsBuilder.CLRToCOMCallMethodDescSize }, + } + .Concat(MethodTableTests.CreateContractTypes(rtsBuilder)) + .Concat(LoaderTests.CreateContractTypes(loaderBuilder)) + .ToDictionary(); + + var globals = MethodTableTests.CreateContractGlobals(rtsBuilder).Concat( + [ + (nameof(Constants.Globals.MethodDescTokenRemainderBitCount), + (ulong)MockDescriptors.MockMethodDescriptorsBuilder.TokenRemainderBitCount), + (nameof(Constants.Globals.FieldOffsetBigRVA), 0xFFFFFFFFUL), + ]); + + // ARM32 and ARM64 enable FEATURE_HFA (needed for HFA detection in IsHFA). + if (testCase.Architecture is RuntimeInfoArchitecture.Arm or RuntimeInfoArchitecture.Arm64) + globals = globals.Append((nameof(Constants.Globals.FeatureHFA), 1UL)); + + // ARM32 soft-float (armel): the presence of FeatureArmSoftFP tells the + // iterator to skip the FP register path. Only add it when explicitly requested. + if (isArmSoftFP) + globals = globals.Append((nameof(Constants.Globals.FeatureArmSoftFP), 1UL)); + + var globalsArray = globals.ToArray(); + + Mock execMgr = new(); + Mock precode = new(); + Mock platMetadata = new(); + + Mock ecmaMd = new(); + ecmaMd.Setup(e => e.GetMetadata(It.IsAny())).Returns( + (ModuleHandle moduleHandle) => syntheticMetadata is not null && moduleHandle.Address == mockModule.Address + ? syntheticMetadata.Reader + : (MetadataReader?)null); + + Mock sig = new(); + sig.Setup(s => s.DecodeFieldSignature(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(default(TypeHandle)); + + var target = targetBuilder + .AddTypes(types) + .AddGlobals(globalsArray) + .AddGlobalStrings( + (Constants.Globals.Architecture, testCase.Architecture.ToString().ToLowerInvariant()), + (Constants.Globals.OperatingSystem, testCase.OperatingSystem.ToString().ToLowerInvariant())) + .AddContract(version: "c1") + .AddContract(version: "c1") + .AddContract(version: "c1") + .AddContract(version: "c1") + .AddMockContract(ecmaMd) + .AddMockContract(sig) + .AddMockContract(execMgr) + .AddMockContract(precode) + .AddMockContract(platMetadata) + .Build(); + + MethodDescHandle mdh = target.Contracts.RuntimeTypeSystem.GetMethodDescHandle(new TargetPointer(md.Address)); + return (target, mdh); + } + + private static MockMethodTable CreateSharedGenericMethodTable(MockDescriptors.RuntimeTypeSystem rtsBuilder, bool hasThis) + { + MockEEClass eeClass = rtsBuilder.AddEEClass(hasThis ? "SharedGenericInterfaceEEClass" : "SharedGenericClassEEClass"); + MockMethodTable methodTable = rtsBuilder.AddMethodTable(hasThis ? "SharedGenericInterface" : "SharedGenericClass"); + + uint flags = (uint)MethodTableFlags_1.WFLAGS_LOW.GenericsMask_TypicalInstantiation; + if (hasThis) + { + flags |= (uint)MethodTableFlags_1.WFLAGS_HIGH.Category_Interface; + } + + methodTable.MTFlags = flags; + methodTable.BaseSize = rtsBuilder.Builder.TargetTestHelpers.ObjectBaseSize; + methodTable.ParentMethodTable = rtsBuilder.SystemObjectMethodTable.Address; + methodTable.NumVirtuals = 1; + eeClass.MethodTable = methodTable.Address; + methodTable.EEClassOrCanonMT = eeClass.Address; + return methodTable; + } +} diff --git a/src/native/managed/cdac/tests/CallingConvention/CallingConventionTests.cs b/src/native/managed/cdac/tests/CallingConvention/CallingConventionTests.cs new file mode 100644 index 00000000000000..176999ce5099b4 --- /dev/null +++ b/src/native/managed/cdac/tests/CallingConvention/CallingConventionTests.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.Tests; + +/// +/// Cross-architecture tests for . These +/// verify harness-level invariants (the contract decodes without throwing, +/// arg counts match) that should hold on every supported architecture. +/// Per-architecture offset assertions live in the platform-specific test +/// classes (e.g. X86CallingConventionTests). +/// +/// +/// Gaps NOT covered by Skip-tagged tests anywhere: +/// +/// #8 Base ComputeSizeOfArgStack byref adjustment — observable +/// via internal CbStackPop() / SizeOfFrameArgumentArray() +/// only, which are not exposed through . +/// #12 ARM64 / RV byref classification differences — the cDAC +/// heuristic agrees with native for all currently exercised struct shapes; +/// a divergent shape would need to be identified from native source. +/// Arm32 softfp detection — no detection mechanism in cDAC today. +/// SysV generic value-type TypeSpec resolution — needs generic +/// instantiation infrastructure in the mock RTS. +/// +/// +public class CallingConventionTests +{ + [Theory] + [MemberData(nameof(CallConvCases.AllCases), MemberType = typeof(CallConvCases))] + public void Harness_StaticMethod_OneInt_DecodesSuccessfully(CallConvTestCase testCase) + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + testCase, + sig => sig.Return(CorElementType.Void).Param(CorElementType.I4)); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + + Assert.Null(layout.ThisOffset); + Assert.Null(layout.AsyncContinuationOffset); + Assert.Single(layout.Arguments); + Assert.Single(layout.Arguments[0].Slots); + Assert.Equal(CorElementType.I4, layout.Arguments[0].Slots[0].ElementType); + } + + [Theory] + [MemberData(nameof(CallConvCases.AllCases), MemberType = typeof(CallConvCases))] + public void Harness_InstanceMethod_ReportsThisOffset(CallConvTestCase testCase) + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + testCase, hasThis: true, + sig => sig.Return(CorElementType.Void).Param(CorElementType.I4)); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.NotNull(layout.ThisOffset); + } + + /// + /// Verifies is true when the + /// instance method's enclosing class is a value type. Not arch-specific — + /// the bit is computed by CallingConvention_1 directly from the + /// enclosing MT's IsValueType flag. + /// + [Fact] + public void InstanceMethod_OnValueType_IsValueTypeThisShouldBeTrue() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + CallConvCases.AMD64Windows, hasThis: true, + (rts, sig) => sig.Return(CorElementType.Void), + enclosingMTOverride: rts => MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "ValueTypeWithMethod", structSize: 8, fields: [new(0, CorElementType.I4)])); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.NotNull(layout.ThisOffset); + Assert.True(layout.IsValueTypeThis, + "Instance method on a value type should report IsValueTypeThis == true so GcScanner emits GC_CALL_INTERIOR on the this slot"); + } +} diff --git a/src/native/managed/cdac/tests/CallingConvention/SignatureBlobBuilder.cs b/src/native/managed/cdac/tests/CallingConvention/SignatureBlobBuilder.cs new file mode 100644 index 00000000000000..ceeadff7864f91 --- /dev/null +++ b/src/native/managed/cdac/tests/CallingConvention/SignatureBlobBuilder.cs @@ -0,0 +1,139 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.Diagnostics.DataContractReader.Contracts; + +namespace Microsoft.Diagnostics.DataContractReader.Tests; + +/// +/// Builds a method signature blob (ECMA-335 §II.23.2.1) for tests. Supports +/// primitive element types and the cDAC-extension ELEMENT_TYPE_INTERNAL +/// (0x21) for referencing mock value-type method tables directly without a +/// metadata reader. +/// +internal sealed class SignatureBlobBuilder +{ + private const byte HASTHIS_FLAG = 0x20; + private const byte VARARG_CC = 0x05; // IMAGE_CEE_CS_CALLCONV_VARARG + private const byte ELEMENT_TYPE_INTERNAL = 0x21; + + private readonly bool _hasThis; + private readonly int _pointerSize; + private bool _isVarArg; + private readonly List _params = new(); + private ParamSpec _return = new(CorElementType.Void, default); + + public SignatureBlobBuilder(int pointerSize, bool hasThis = false) + { + _pointerSize = pointerSize; + _hasThis = hasThis; + } + + public SignatureBlobBuilder VarArg() + { + _isVarArg = true; + return this; + } + + public SignatureBlobBuilder Return(CorElementType primitive) + { + _return = new ParamSpec(primitive, default); + return this; + } + + public SignatureBlobBuilder ReturnValueType(TargetPointer methodTablePtr) + { + _return = new ParamSpec(CorElementType.ValueType, methodTablePtr); + return this; + } + + public SignatureBlobBuilder Param(CorElementType primitive) + { + _params.Add(new ParamSpec(primitive, default)); + return this; + } + + public SignatureBlobBuilder ParamValueType(TargetPointer methodTablePtr) + { + _params.Add(new ParamSpec(CorElementType.ValueType, methodTablePtr)); + return this; + } + + /// + /// Adds a parameter of ELEMENT_TYPE_CLASS with a dummy TypeDef token. + /// The token is encoded per ECMA-335 §II.23.2.8 (TypeDefOrRefOrSpecEncoded). + /// The cDAC signature decoder resolves this via the Loader's lookup tables; + /// when those aren't populated (as in most tests), it falls back to a + /// pointer-sized CorElementType.Class placeholder -- which is the + /// correct calling-convention shape for any managed reference type. + /// + public SignatureBlobBuilder ParamClass() + { + _params.Add(new ParamSpec(CorElementType.Class, default, true)); + return this; + } + + public byte[] Build() + { + using MemoryStream ms = new(); + using BinaryWriter bw = new(ms); + + byte callingConvention = (byte)((_hasThis ? HASTHIS_FLAG : 0) | (_isVarArg ? VARARG_CC : 0)); + bw.Write(callingConvention); + WriteCompressedUInt(bw, (uint)_params.Count); + WriteParam(bw, _return); + foreach (ParamSpec p in _params) + WriteParam(bw, p); + + return ms.ToArray(); + } + + private void WriteParam(BinaryWriter bw, ParamSpec p) + { + if (p.Type == CorElementType.ValueType && p.TypeHandle != TargetPointer.Null) + { + // Use ELEMENT_TYPE_INTERNAL + raw TypeHandle pointer to bypass metadata. + bw.Write(ELEMENT_TYPE_INTERNAL); + if (_pointerSize == 8) + bw.Write(p.TypeHandle.Value); + else + bw.Write((uint)p.TypeHandle.Value); + return; + } + if (p.IsDummyClassToken) + { + // ELEMENT_TYPE_CLASS followed by a TypeDefOrRefOrSpecEncoded token. + // We use TypeDef row 1 (the pseudo-type) encoded as (1 << 2 | 0) = 4. + bw.Write((byte)CorElementType.Class); + WriteCompressedUInt(bw, 4); // TypeDef row 1 + return; + } + bw.Write((byte)p.Type); + } + + private static void WriteCompressedUInt(BinaryWriter bw, uint value) + { + // ECMA-335 §II.23.2 compressed unsigned int + if (value < 0x80) + { + bw.Write((byte)value); + } + else if (value < 0x4000) + { + bw.Write((byte)((value >> 8) | 0x80)); + bw.Write((byte)(value & 0xFF)); + } + else + { + bw.Write((byte)((value >> 24) | 0xC0)); + bw.Write((byte)((value >> 16) & 0xFF)); + bw.Write((byte)((value >> 8) & 0xFF)); + bw.Write((byte)(value & 0xFF)); + } + } + + private readonly record struct ParamSpec(CorElementType Type, TargetPointer TypeHandle, bool IsDummyClassToken = false); +} diff --git a/src/native/managed/cdac/tests/CallingConvention/SyntheticVectorMetadata.cs b/src/native/managed/cdac/tests/CallingConvention/SyntheticVectorMetadata.cs new file mode 100644 index 00000000000000..f0ebbccde072f4 --- /dev/null +++ b/src/native/managed/cdac/tests/CallingConvention/SyntheticVectorMetadata.cs @@ -0,0 +1,140 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Reflection; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; + +namespace Microsoft.Diagnostics.DataContractReader.Tests; + +internal sealed class SyntheticVectorMetadata +{ + private readonly MetadataReaderProvider _provider; + private readonly Dictionary _typeDefTokens; + + private SyntheticVectorMetadata(MetadataReaderProvider provider, Dictionary typeDefTokens) + { + _provider = provider; + _typeDefTokens = typeDefTokens; + } + + public MetadataReader Reader => _provider.GetMetadataReader(); + + public uint GetTypeDefToken(string typeName) => _typeDefTokens[typeName]; + + public static SyntheticVectorMetadata Create() + { + MetadataBuilder builder = new(); + builder.AddAssembly( + builder.GetOrAddString("SyntheticAsm"), + new Version(1, 0, 0, 0), + default, + default, + 0, + AssemblyHashAlgorithm.None); + builder.AddModule( + 0, + builder.GetOrAddString("SyntheticModule"), + builder.GetOrAddGuid(Guid.NewGuid()), + default, + default); + + builder.AddTypeDefinition( + TypeAttributes.NotPublic, + default, + builder.GetOrAddString(""), + default, + MetadataTokens.FieldDefinitionHandle(1), + MetadataTokens.MethodDefinitionHandle(1)); + + Dictionary typeDefTokens = new(StringComparer.Ordinal); + foreach ((string ns, string name) in new[] + { + ("System.Runtime.Intrinsics", "Vector64`1"), + ("System.Runtime.Intrinsics", "Vector128`1"), + ("System.Numerics", "Vector`1"), + }) + { + TypeDefinitionHandle handle = builder.AddTypeDefinition( + TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.SequentialLayout, + builder.GetOrAddString(ns), + builder.GetOrAddString(name), + default, + MetadataTokens.FieldDefinitionHandle(1), + MetadataTokens.MethodDefinitionHandle(1)); + typeDefTokens.Add(name, (uint)MetadataTokens.GetToken(handle)); + } + + BlobBuilder output = new(); + new MetadataRootBuilder(builder).Serialize(output, methodBodyStreamRva: 0, mappedFieldDataStreamRva: 0); + + MetadataReaderProvider provider = MetadataReaderProvider.FromMetadataImage(ImmutableArray.Create(output.ToArray())); + MetadataReader reader = provider.GetMetadataReader(); + + Debug.Assert(typeDefTokens.Count == 3); + foreach (TypeDefinitionHandle handle in reader.TypeDefinitions) + { + TypeDefinition typeDef = reader.GetTypeDefinition(handle); + string name = reader.GetString(typeDef.Name); + if (typeDefTokens.TryGetValue(name, out uint expectedToken)) + { + Debug.Assert(expectedToken == (uint)MetadataTokens.GetToken(handle)); + } + } + + return new SyntheticVectorMetadata(provider, typeDefTokens); + } + + /// + /// Creates a metadata image containing the standard vector types plus one + /// additional type. Used by tests that need an intrinsic-flagged type whose + /// name the runtime does NOT recognize (e.g. to verify GetVectorSize returns 0). + /// + public static SyntheticVectorMetadata CreateWithExtraType(string extraNamespace, string extraName) + { + MetadataBuilder builder = new(); + builder.AddAssembly( + builder.GetOrAddString("SyntheticAsm"), + new Version(1, 0, 0, 0), + default, default, 0, AssemblyHashAlgorithm.None); + builder.AddModule( + 0, + builder.GetOrAddString("SyntheticModule"), + builder.GetOrAddGuid(Guid.NewGuid()), + default, default); + builder.AddTypeDefinition( + TypeAttributes.NotPublic, default, + builder.GetOrAddString(""), + default, + MetadataTokens.FieldDefinitionHandle(1), + MetadataTokens.MethodDefinitionHandle(1)); + + Dictionary typeDefTokens = new(StringComparer.Ordinal); + foreach ((string ns, string name) in new[] + { + ("System.Runtime.Intrinsics", "Vector64`1"), + ("System.Runtime.Intrinsics", "Vector128`1"), + ("System.Numerics", "Vector`1"), + (extraNamespace, extraName), + }) + { + TypeDefinitionHandle handle = builder.AddTypeDefinition( + TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.SequentialLayout, + builder.GetOrAddString(ns), + builder.GetOrAddString(name), + default, + MetadataTokens.FieldDefinitionHandle(1), + MetadataTokens.MethodDefinitionHandle(1)); + typeDefTokens.Add(name, (uint)MetadataTokens.GetToken(handle)); + } + + BlobBuilder output = new(); + new MetadataRootBuilder(builder).Serialize(output, methodBodyStreamRva: 0, mappedFieldDataStreamRva: 0); + MetadataReaderProvider provider = MetadataReaderProvider.FromMetadataImage(ImmutableArray.Create(output.ToArray())); + return new SyntheticVectorMetadata(provider, typeDefTokens); + } +} diff --git a/src/native/managed/cdac/tests/CallingConvention/TEST_INVENTORY.md b/src/native/managed/cdac/tests/CallingConvention/TEST_INVENTORY.md new file mode 100644 index 00000000000000..699c1bd301fb73 --- /dev/null +++ b/src/native/managed/cdac/tests/CallingConvention/TEST_INVENTORY.md @@ -0,0 +1,284 @@ +# Calling Convention Test Inventory + +This document tracks test coverage for each platform's managed calling +convention in the cDAC argument iterator. Each category represents a +behavioral aspect of the ABI that should have at least one test. + +Legend: +- **Covered** -- test(s) exist and pass +- **Gap** -- no test exists yet +- **Skipped** -- test exists but is marked `[Skip]` due to a known implementation gap +- N/A -- category doesn't apply to this platform + +## Test categories + +| # | Category | Description | +|---|---|---| +| 1 | **Integer arg register filling** | N integer args fill the GP arg registers in order | +| 2 | **Integer arg stack spill** | Args beyond the register count land on the stack at sequential offsets | +| 3 | **Float/double register filling** | FP args fill FP registers (XMM on x64, V on ARM64, S/D on ARM32) | +| 4 | **Float/double stack spill** | FP args beyond the register count land on the stack | +| 5 | **Mixed int/float bank independence** | Int and float args consume from separate banks (or shared on Win x64) | +| 6 | **`this` pointer placement** | Instance method `this` lands in the first GP register | +| 7 | **Return buffer (retBuf)** | Large-return methods get a hidden retBuf arg that shifts user args | +| 8 | **Hidden arg shifts (generic context, async cont)** | Each hidden arg consumes a register slot, shifting user args | +| 9 | **Vararg cookie placement** | Vararg methods have a cookie after this/retBuf, before user args | +| 10 | **Implicit by-reference** | Structs above a size threshold are passed via hidden pointer | +| 11 | **Non-byref struct enregistration** | Structs at or below the threshold enregister as a value | +| 12 | **SysV eightbyte classification** | Struct fields classified per eightbyte merge rules (AMD64 Unix only) | +| 13 | **SysV struct split (GP + FP)** | Struct with mixed int/float fields splits across GP and FP banks | +| 14 | **HFA detection** | Homogeneous float aggregates placed in consecutive FP registers (ARM only) | +| 15 | **HFA not honored on non-ARM** | HFA-shaped structs do NOT get FP treatment on x64 | +| 16 | **TypedReference** | `System.TypedReference` placed correctly via `g_TypedReferenceMT` substitution | +| 17 | **Vector types (real intrinsic detection)** | Vector64/128 via synthetic metadata and `GetVectorSize` | +| 18 | **Large struct on stack by value** | Structs > 16 B passed by value on stack (SysV), not by pointer | +| 19 | **Many args (10+)** | Stack offsets progress correctly at scale | +| 20 | **Return value placement** | Return types in correct register or via retBuf | +| 21 | **Empty struct** | Zero-field struct behavior | +| 22 | **64-bit alignment (ARM32)** | I8/R8 args skip odd-numbered registers on ARM32 | +| 23 | **Apple ARM64 stack packing** | Natural-alignment packing on Darwin ARM64 stack | +| 24 | **Vararg FP -> GP demotion** | Variadic FP args go through GP path, not FP registers | +| 25 | **GC reference args** | Object/String args placed in GP regs (not byref, not FP) | + +## Coverage matrix + +### AMD64 Windows + +| # | Category | Status | Test(s) | +|---|---|---|---| +| 1 | Int register filling | **Covered** | `IntArgs_FillRegsAndSpillToStack` (10 cases: I1-U8) | +| 2 | Int stack spill | **Covered** | `IntArgs_FillRegsAndSpillToStack` (I4x5, I8x5) | +| 3 | Float register filling | **Covered** | `FloatArgs_FillFPRegsAndSpillToStack` (R4x1, R8x1, R4x4, R8x4) | +| 4 | Float stack spill | **Covered** | `FloatArgs_FillFPRegsAndSpillToStack` (R4x6, R8x6) | +| 5 | Mixed int/float banks | **Covered** | `OneFloatAmongInts_LandsInXMM` (4 positions) | +| 6 | `this` placement | **Covered** | `InstanceMethod_ThisOffsetIsFirst` | +| 7 | Return buffer | **Covered** | `StaticMethod_RetBuf_*`, `InstanceMethod_RetBuf_*`, `HiddenArgs_ShiftFirstUserDouble` | +| 8 | Hidden arg shifts | **Covered** | `HiddenArgs_ShiftFirstUserDouble` (10 cases) | +| 9 | Vararg cookie | **Covered** | `VarArgs_CookieAndFirstUserArg_OnWindows` (4 cases), `NonVarArgs_HasNullVarArgCookieOffset` | +| 10 | Implicit byref | **Covered** | `NineByteStruct_*`, `ThreeByteStruct_*`, `TypedReference_ImplicitByref_*` | +| 11 | Non-byref enregistration | **Covered** | `EightByteStruct_Enregisters_NotByref` | +| 12 | SysV eightbyte | N/A | | +| 13 | SysV struct split | N/A | | +| 14 | HFA detection | N/A | | +| 15 | HFA not honored | **Covered** | `HFAShapedStruct_OnWindows_DoesNotEnregisterInFP` (2 cases) | +| 16 | TypedReference | **Covered** | `TypedReference_ImplicitByref_OneSlot` | +| 17 | Vector types | **Covered** | `VectorType_OnWindows_ClassifiedBySizeNotVectorness` (2 cases) | +| 18 | Large struct by value | N/A | (Windows uses byref, not by-value) | +| 19 | Many args (10+) | **Covered** | `TenArgs_StackOffsetsProgress` | +| 20 | Return value placement | **Gap** | No explicit return-register tests | +| 21 | Empty struct | **Gap** | No test (needs behavioral clarification) | +| 22 | 64-bit alignment | N/A | | +| 23 | Apple stack packing | N/A | | +| 24 | Vararg FP demotion | **Gap** | Not modeled in iterator; covered implicitly by position-based XMM | +| 25 | GC reference args | **Gap** | No explicit Object/String arg test | + +### AMD64 Unix (SysV) + +| # | Category | Status | Test(s) | +|---|---|---|---| +| 1 | Int register filling | **Covered** | `SixInts_FillGPRegs`, `IntArgs_FillSixGPRegsAndSpillToStack` (10 cases) | +| 2 | Int stack spill | **Covered** | `SeventhInt_GoesToStack`, `IntArgs_*` (I4x7, I8x7) | +| 3 | Float register filling | **Covered** | `FourDoubles_FillFPRegs`, `FloatArgs_FillEightFPRegs` (6 cases) | +| 4 | Float stack spill | **Covered** | `NineDoubles_NinthGoesToFirstStackSlot` | +| 5 | Mixed int/float banks | **Covered** | `MixedIntDouble_UseSeparateBanks`, `OneFloatAmongInts_*` (4 cases) | +| 6 | `this` placement | **Covered** | `InstanceMethod_ThisOffsetIsFirstGPReg` | +| 7 | Return buffer | **Covered** | `Return_LargeStruct_HasRetBuf_*` | +| 8 | Hidden arg shifts | **Gap** | No generic-context or async-continuation test | +| 9 | Vararg cookie | **Gap** | No vararg test (SysV varargs are Linux/macOS only and "not supported" per clr-abi.md:84) | +| 10 | Implicit byref | N/A | SysV doesn't use implicit byref | +| 11 | Non-byref enregistration | N/A | (all structs <= 16 B are classified, not byref'd) | +| 12 | SysV eightbyte classification | **Covered** | `Struct_TwoInts_*`, `Struct_TwoFloats_*`, `Struct_TwoDoubles_*` | +| 13 | SysV struct split (GP+FP) | **Covered** | `Struct_IntDouble_SplitAcrossGPAndFP`, `Struct_ObjectAndDouble_*` | +| 14 | HFA detection | N/A | | +| 15 | HFA not honored | N/A | (SysV has no HFA concept) | +| 16 | TypedReference | **Covered** | `TypedReference_PassedInTwoGPRegs`, `TypedReference_GlobalNotSet_FallsBackToStack` | +| 17 | Vector types | **Gap** | No Vector64/128 test (SysV classifier bypass not exercised) | +| 18 | Large struct by value | **Covered** | `Struct_LargerThan16Bytes_StackByValue_NotByRef` | +| 19 | Many args (10+) | **Covered** | `ManyIntArgs_StackOffsetsProgress` | +| 20 | Return value placement | **Covered** | `Return_EightByteStruct_*`, `Return_SixteenByteStruct_*`, `Return_ThreeByteStruct_*`, etc. (6 tests) | +| 21 | Empty struct | **Gap** | No test | +| 22 | 64-bit alignment | N/A | | +| 23 | Apple stack packing | N/A | (Apple is ARM64, not x64) | +| 24 | Vararg FP demotion | N/A | (managed varargs not supported on Unix) | +| 25 | GC reference args | **Gap** | No explicit Object/String arg test | + +### ARM32 + +| # | Category | Status | Test(s) | +|---|---|---|---| +| 1 | Int register filling | **Covered** | `FourInts_FillR0_R3` | +| 2 | Int stack spill | **Covered** | `FifthInt_GoesToStack` | +| 3 | Float register filling | **Gap** | No FP register test | +| 4 | Float stack spill | **Gap** | No FP spill test | +| 5 | Mixed int/float banks | **Gap** | No mixed test | +| 6 | `this` placement | **Gap** | No explicit test | +| 7 | Return buffer | **Gap** | No retBuf test | +| 8 | Hidden arg shifts | **Gap** | No test | +| 9 | Vararg cookie | **Gap** | No test | +| 10 | Implicit byref | N/A | (EnregisteredParamTypeMaxSize = 0) | +| 11 | Non-byref enregistration | N/A | | +| 12 | SysV eightbyte | N/A | | +| 13 | SysV struct split | N/A | | +| 14 | HFA detection | **Gap** | No HFA test | +| 15 | HFA not honored | N/A | | +| 16 | TypedReference | **Gap** | No test | +| 17 | Vector types | **Gap** | No test | +| 18 | Large struct by value | **Gap** | No test | +| 19 | Many args (10+) | **Gap** | No test | +| 20 | Return value placement | **Gap** | No test | +| 21 | Empty struct | **Gap** | No test | +| 22 | 64-bit alignment | **Gap** | No I8/R8 alignment-skip test | +| 23 | Apple stack packing | N/A | | +| 24 | Vararg FP demotion | **Gap** | No test | +| 25 | GC reference args | **Gap** | No test | + +### ARM64 + +| # | Category | Status | Test(s) | +|---|---|---|---| +| 1 | Int register filling | **Covered** | `EightInts_FillX0_X7` | +| 2 | Int stack spill | **Gap** | No explicit test | +| 3 | Float register filling | **Covered** | `EightDoubles_FillV0_V7` | +| 4 | Float stack spill | **Gap** | No FP spill test | +| 5 | Mixed int/float banks | **Gap** | No mixed test | +| 6 | `this` placement | **Covered** | `InstanceMethod_ThisOffsetIsX0` | +| 7 | Return buffer | **Gap** | No retBuf test (X8 behavior) | +| 8 | Hidden arg shifts | **Gap** | No test | +| 9 | Vararg cookie | **Gap** | No test | +| 10 | Implicit byref | **Gap** | No > 16 B struct test | +| 11 | Non-byref enregistration | **Gap** | No <= 16 B non-HFA struct test | +| 12 | SysV eightbyte | N/A | | +| 13 | SysV struct split | N/A | | +| 14 | HFA detection | **Covered** | `HfaFloat2/3/4_*`, `HfaDouble2_*` | +| 15 | HFA not honored | N/A | | +| 16 | TypedReference | **Gap** | No test | +| 17 | Vector types | **Gap** | No Vector64/128 in V-register test | +| 18 | Large struct by value | N/A | (ARM64 uses implicit byref) | +| 19 | Many args (10+) | **Gap** | No test | +| 20 | Return value placement | **Gap** | No test | +| 21 | Empty struct | **Gap** | No test | +| 22 | 64-bit alignment | N/A | | +| 23 | Apple stack packing | **Gap** | No Apple-specific test | +| 24 | Vararg FP demotion | **Skipped** | `Windows_VarArgs_StructSpansX7AndStack_AuditGap4` | +| 25 | GC reference args | **Gap** | No test | + +### RISC-V 64 / LoongArch 64 + +| # | Category | Status | Test(s) | +|---|---|---|---| +| 1 | Int register filling | **Covered** | `RiscV64_EightInts_FillA0_A7` | +| 2 | Int stack spill | **Gap** | No explicit test | +| 3 | Float register filling | **Covered** | `LoongArch64_OneFloat_GoesToFA0` | +| 4 | Float stack spill | **Gap** | No FP spill test | +| 5 | Mixed int/float banks | **Gap** | No mixed test | +| 6 | `this` placement | **Gap** | No test | +| 7 | Return buffer | **Gap** | No test | +| 8 | Hidden arg shifts | **Gap** | No test | +| 9 | Vararg cookie | **Gap** | No test | +| 10 | Implicit byref | **Gap** | No > 16 B struct test | +| 11 | Non-byref enregistration | **Gap** | No test | +| 12 | SysV eightbyte | N/A | | +| 13 | SysV struct split | N/A | | +| 14 | HFA detection | N/A | | +| 15 | HFA not honored | N/A | | +| 16 | TypedReference | **Gap** | No test | +| 17 | Vector types | **Gap** | No test | +| 18 | Large struct by value | N/A | | +| 19 | Many args (10+) | **Gap** | No test | +| 20 | Return value placement | **Gap** | No test | +| 21 | Empty struct | **Gap** | No test | +| 22 | 64-bit alignment | N/A | | +| 23 | Apple stack packing | N/A | | +| 24 | Vararg FP demotion | **Gap** | No test | +| 25 | GC reference args | **Gap** | No test | + +### x86 + +| # | Category | Status | Test(s) | +|---|---|---|---| +| 1 | Int register filling | **Covered** | `OneInt_*`, `TwoInts_*` | +| 2 | Int stack spill | **Skipped** | `ThirdInt_LandsAtOffsetOfArgs_AuditGap7` | +| 3 | Float register filling | N/A | (x86 has no FP arg registers) | +| 4 | Float stack spill | N/A | | +| 5 | Mixed int/float banks | N/A | | +| 6 | `this` placement | **Covered** | `InstanceMethod_ThisOffsetIsECX` | +| 7 | Return buffer | **Gap** | No retBuf test | +| 8 | Hidden arg shifts | **Gap** | No test | +| 9 | Vararg cookie | **Gap** | No test | +| 10 | Implicit byref | N/A | (EnregisteredParamTypeMaxSize = 0) | +| 11 | Non-byref enregistration | **Skipped** | `SmallValueType_Enregisters_AuditGap6` | +| 12 | SysV eightbyte | N/A | | +| 13 | SysV struct split | N/A | | +| 14 | HFA detection | N/A | | +| 15 | HFA not honored | N/A | | +| 16 | TypedReference | **Gap** | No test | +| 17 | Vector types | N/A | | +| 18 | Large struct by value | **Gap** | No test | +| 19 | Many args (10+) | **Gap** | No test | +| 20 | Return value placement | **Gap** | No test | +| 21 | Empty struct | **Gap** | No test | +| 22 | 64-bit alignment | N/A | | +| 23 | Apple stack packing | N/A | | +| 24 | Vararg FP demotion | N/A | | +| 25 | GC reference args | **Gap** | No test | + +### GetVectorSize (cross-platform, in `RuntimeTypeSystemGetVectorSizeTests.cs`) + +| Test | Status | +|---|---| +| Known intrinsic returns size (Vector64, Vector128) | **Covered** | +| Non-intrinsic type returns 0 | **Covered** | +| No metadata returns 0 | **Covered** | +| Unhandled intrinsic name returns 0 | **Covered** | +| System.Numerics.Vector returns field bytes | **Covered** | + +## AMD64 Unix: gap analysis and proposed tests + +The following categories are **gaps** for AMD64 Unix that should be filled: + +### Gap 8: Hidden arg shifts (generic context, async continuation) + +AMD64 Unix uses the same `ComputeInitialNumRegistersUsed` as Windows, counting +`this`, retBuf, paramType, asyncCont in RDI, RSI, RDX, ... before user args. +The Phase 2 `HiddenArgs_ShiftFirstUserDouble` theory was added for Windows only. + +**Proposed:** Port the same theory to `AMD64UnixCallingConventionTests.cs` with +Unix-specific offsets (RDI = slot 0, RSI = slot 1, etc.) and 6 GP regs instead +of 4. The same `hasParamType` / `hasAsyncContinuation` helper flags work. + +### Gap 17: Vector types (SysV classifier bypass) + +When `GetVectorSize` returns non-zero, `SystemVStructClassifier.ShouldClassify` +returns false, and the struct is not eightbyte-classified. The AMD64 Unix +iterator's behavior when classification is skipped should be verified: +- Vector64 (8 B) -> should go in a GP register (or a single XMM?) +- Vector128 (16 B) -> should go in a single XMM (not split into 2 eightbytes) + +**Proposed:** Add `VectorType_OnUnix_BypassesEightbyteClassification` theory +using the synthetic metadata infrastructure from Phase 4. May surface a real +gap if the iterator currently mis-places unclassifiable structs. + +### Gap 21: Empty struct + +SysV AMD64 should pass empty structs by value on the stack per `clr-abi.md:569`. +Needs behavioral validation first (does the mock produce size 0? does +`IsArgPassedByRefBySize(0)` return false on SysV? etc.). + +**Proposed:** Add `EmptyStruct_PassedByValue_OnStack` fact test. + +### Gap 25: GC reference args + +Object and String args should go in GP registers (RDI, RSI, ...) and not be +treated as implicit byref. Important for GC scanning correctness. + +**Proposed:** Add `ObjectAndStringArgs_GoToGPRegs_NotByref` fact test. + +### Gap 9: Vararg cookie + +Per `clr-abi.md:84`: "Managed varargs are supported on Windows only." Unix +managed varargs are explicitly not supported. So this is correctly N/A, but +worth a negative test confirming the contract doesn't crash on a vararg sig +for a Unix target. + +**Proposed:** Add `VarArgs_NotSupportedOnUnix_ReturnsEmptyOrThrows` fact test +(assert behavior matches the contract's current handling). diff --git a/src/native/managed/cdac/tests/CallingConvention/X86CallingConventionTests.cs b/src/native/managed/cdac/tests/CallingConvention/X86CallingConventionTests.cs new file mode 100644 index 00000000000000..7cf6ad6f436092 --- /dev/null +++ b/src/native/managed/cdac/tests/CallingConvention/X86CallingConventionTests.cs @@ -0,0 +1,255 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.Tests; + +/// +/// x86-specific calling-convention tests. ECX/EDX register placement (in +/// REVERSE — first arg in ECX which is the higher slot), stack overflow, and +/// the audit-gap regression markers for x86 register placement / sig-walk +/// accounting. +/// +public class X86CallingConventionTests +{ + private static CallConvTestCase Case => CallConvCases.X86; + + private static int OffsetOfECX => Case.ArgumentRegistersOffset + Case.PointerSize; + private static int OffsetOfEDX => Case.ArgumentRegistersOffset; + + [Fact] + public void OneInt_GoesToFirstArgRegSlot() + { + // x86 places args in registers in REVERSE: first int -> (NumArgRegs - 1) * PtrSize = 4 (ECX-style high slot). + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => sig.Return(CorElementType.Void).Param(CorElementType.I4)); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + Assert.Equal(Case.ArgumentRegistersOffset + (Case.NumArgumentRegisters - 1) * Case.PointerSize, layout.Arguments[0].Slots[0].Offset); + } + + [Fact] + public void TwoInts_FillBothArgRegs() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => sig.Return(CorElementType.Void).Param(CorElementType.I4).Param(CorElementType.I4)); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(2, layout.Arguments.Count); + // First arg @ ECX (high slot) + Assert.Equal(Case.ArgumentRegistersOffset + Case.PointerSize, layout.Arguments[0].Slots[0].Offset); + // Second arg @ EDX (offset 0) + Assert.Equal(Case.ArgumentRegistersOffset, layout.Arguments[1].Slots[0].Offset); + } + + [Fact] + public void InstanceMethod_ThisOffsetIsECX() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: true, + sig => sig.Return(CorElementType.Void)); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + // GetThisOffset = ArgRegsOffset + PointerSize (ECX slot, after EDX at 0) + Assert.Equal(Case.ArgumentRegistersOffset + Case.PointerSize, layout.ThisOffset); + } + + /// + /// Audit gap #6 (closed): x86 ArgIterator used to exclude ValueType + /// entirely from register placement. Native x86 enregisters value types + /// of size 1, 2, or 4 bytes. This test verifies the fixed behavior. + /// + [Fact] + public void SmallValueType_Enregisters_AuditGap6() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "OneInt", structSize: 4, + fields: [new(0, CorElementType.I4)]); + sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(Case.ArgumentRegistersOffset + (Case.NumArgumentRegisters - 1) * Case.PointerSize, layout.Arguments[0].Slots[0].Offset); + } + + /// + /// Audit gap #7 (closed): X86ArgIterator.ComputeSizeOfArgStack used to + /// assume ALL args go to stack, biasing the stack-arg offset upward. + /// After the fix, the first stack arg lands at exactly OffsetOfArgs. + /// + [Fact] + public void ThirdInt_LandsAtOffsetOfArgs_AuditGap7() + { + // Three ints: first two go in ECX/EDX, third spills to stack at OffsetOfArgs. + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => sig + .Return(CorElementType.Void) + .Param(CorElementType.I4) + .Param(CorElementType.I4) + .Param(CorElementType.I4)); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(3, layout.Arguments.Count); + Assert.Equal(OffsetOfECX, layout.Arguments[0].Slots[0].Offset); + Assert.Equal(OffsetOfEDX, layout.Arguments[1].Slots[0].Offset); + Assert.Equal(Case.OffsetOfArgs, layout.Arguments[2].Slots[0].Offset); + } + + [Fact] + public void StaticMethod_RetBuf_UserArgGoesToStack() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable bigMT = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "BigReturn", structSize: 12, + fields: [new(0, CorElementType.I4), new(4, CorElementType.I4), new(8, CorElementType.I4)]); + sig.ReturnValueType(new TargetPointer(bigMT.Address)).Param(CorElementType.I4); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + Assert.Equal(OffsetOfEDX, layout.Arguments[0].Slots[0].Offset); + } + + [Fact] + public void InstanceMethod_RetBuf_UserArgGoesToStack() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: true, + (rts, sig) => + { + MockMethodTable bigMT = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "BigReturn", structSize: 12, + fields: [new(0, CorElementType.I4), new(4, CorElementType.I4), new(8, CorElementType.I4)]); + sig.ReturnValueType(new TargetPointer(bigMT.Address)).Param(CorElementType.I4); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + Assert.Equal(Case.OffsetOfArgs, layout.Arguments[0].Slots[0].Offset); + } + + [Fact] + public void StaticMethod_WithParamType_UserArgGoesToECX_ParamTypeInEDX() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => sig.Return(CorElementType.Void).Param(CorElementType.I4), + hasParamType: true); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + Assert.Equal(OffsetOfECX, layout.Arguments[0].Slots[0].Offset); + } + +#pragma warning disable xUnit1004 // Test methods should not be skipped -- tracking an implementation gap. + [Fact] + public void VarArgs_CookieAtSizeOfTransitionBlock() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => sig.VarArg().Return(CorElementType.Void).Param(CorElementType.I4)); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.NotNull(layout.VarArgCookieOffset); + Assert.Equal(Case.TransitionBlockSize, layout.VarArgCookieOffset.Value); + Assert.Single(layout.Arguments); + // On x86 varargs, the cookie occupies the first stack slot (at OffsetOfArgs), + // so the first user arg is one slot above it. + Assert.Equal(Case.OffsetOfArgs + Case.PointerSize, layout.Arguments[0].Slots[0].Offset); + } +#pragma warning restore xUnit1004 + + [Fact] + public void TypedReference_GoesToStack_NotEnregistered() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable typedRefMT = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "System.TypedReference", structSize: 8, + fields: [new(0, CorElementType.Byref), new(4, CorElementType.I)]); + rts.SetTypedReferenceMethodTable(typedRefMT.Address); + sig.Return(CorElementType.Void).Param(CorElementType.TypedByRef); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + Assert.False(layout.Arguments[0].IsPassedByRef); + Assert.Equal(Case.OffsetOfArgs, layout.Arguments[0].Slots[0].Offset); + } + + [Fact] + public void LargeStruct_PassedByValueOnStack() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "BigStruct", structSize: 24, + fields: + [ + new(0, CorElementType.I4), new(4, CorElementType.I4), new(8, CorElementType.I4), + new(12, CorElementType.I4), new(16, CorElementType.I4), new(20, CorElementType.I4), + ]); + sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + Assert.False(layout.Arguments[0].IsPassedByRef); + Assert.Equal(Case.OffsetOfArgs, layout.Arguments[0].Slots[0].Offset); + } + + [Fact] + public void TenArgs_TwoInRegs_EightOnStack() + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => + { + sig.Return(CorElementType.Void); + for (int i = 0; i < 10; i++) sig.Param(CorElementType.I4); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(10, layout.Arguments.Count); + Assert.Equal(OffsetOfECX, layout.Arguments[0].Slots[0].Offset); + Assert.Equal(OffsetOfEDX, layout.Arguments[1].Slots[0].Offset); + + for (int i = 2; i < 10; i++) + { + int expectedStackOfs = Case.OffsetOfArgs + (10 - 1 - i) * Case.PointerSize; + Assert.Equal(expectedStackOfs, layout.Arguments[i].Slots[0].Offset); + } + } + + [Theory] + [InlineData(CorElementType.Object)] + [InlineData(CorElementType.String)] + public void GCReferenceArgs_Enregister(CorElementType refType) + { + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( + Case, + sig => sig.Return(CorElementType.Void).Param(refType)); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + Assert.False(layout.Arguments[0].IsPassedByRef); + Assert.Equal(OffsetOfECX, layout.Arguments[0].Slots[0].Offset); + } +} diff --git a/src/native/managed/cdac/tests/MethodTableTests.cs b/src/native/managed/cdac/tests/MethodTableTests.cs index 7bbe2743700689..f049f07465cd68 100644 --- a/src/native/managed/cdac/tests/MethodTableTests.cs +++ b/src/native/managed/cdac/tests/MethodTableTests.cs @@ -26,6 +26,15 @@ public class MethodTableTests [DataType.FnPtrTypeDesc] = TargetTestHelpers.CreateTypeInfo(rtsBuilder.FnPtrTypeDescLayout), [DataType.ParamTypeDesc] = TargetTestHelpers.CreateTypeInfo(rtsBuilder.ParamTypeDescLayout), [DataType.TypeVarTypeDesc] = TargetTestHelpers.CreateTypeInfo(rtsBuilder.TypeVarTypeDescLayout), + [DataType.GenericsDictInfo] = new Target.TypeInfo + { + Size = (uint)rtsBuilder.Builder.TargetTestHelpers.PointerSize, + Fields = new Dictionary + { + [nameof(Data.GenericsDictInfo.NumDicts)] = new Target.FieldInfo { Offset = 0 }, + [nameof(Data.GenericsDictInfo.NumTypeArgs)] = new Target.FieldInfo { Offset = sizeof(ushort) }, + }, + }, [DataType.GCCoverageInfo] = TargetTestHelpers.CreateTypeInfo(rtsBuilder.GCCoverageInfoLayout), [DataType.ContinuationObject] = new Target.TypeInfo { Size = rtsBuilder.ContinuationObjectSize }, }; @@ -34,8 +43,9 @@ internal static (string Name, ulong Value)[] CreateContractGlobals(MockRTS rtsBu => [ (nameof(Constants.Globals.FreeObjectMethodTable), rtsBuilder.FreeObjectMethodTableGlobalAddress), - (nameof(Constants.Globals.ContinuationMethodTable), rtsBuilder.ContinuationMethodTableGlobalAddress), (nameof(Constants.Globals.ObjectMethodTable), rtsBuilder.ObjectMethodTableGlobalAddress), + (nameof(Constants.Globals.ContinuationMethodTable), rtsBuilder.ContinuationMethodTableGlobalAddress), + (nameof(Constants.Globals.TypedReferenceMethodTable), rtsBuilder.TypedReferenceMethodTableGlobalAddress), (nameof(Constants.Globals.MethodDescAlignment), rtsBuilder.MethodDescAlignment), (nameof(Constants.Globals.ArrayBaseSize), rtsBuilder.ArrayBaseSize), ]; @@ -77,7 +87,6 @@ public void HasRuntimeTypeSystemContract(MockTarget.Architecture arch) Contracts.TypeHandle handle = contract.GetTypeHandle(freeObjectMethodTableAddress); Assert.NotEqual(TargetPointer.Null, handle.Address); Assert.True(contract.IsFreeObjectMethodTable(handle)); - Assert.False(contract.IsObject(handle)); } [Theory] @@ -93,7 +102,6 @@ public void ValidateSystemObjectMethodTable(MockTarget.Architecture arch) Contracts.TypeHandle systemObjectTypeHandle = contract.GetTypeHandle(systemObjectMethodTablePtr); Assert.Equal(systemObjectMethodTablePtr.Value, systemObjectTypeHandle.Address.Value); Assert.False(contract.IsFreeObjectMethodTable(systemObjectTypeHandle)); - Assert.True(contract.IsObject(systemObjectTypeHandle)); } [Theory] @@ -709,6 +717,74 @@ public void RequiresAlign8(MockTarget.Architecture arch, bool flagSet) Assert.Equal(flagSet, contract.RequiresAlign8(typeHandle)); } + public static IEnumerable IsHFAData() + { + yield return [(int)1, true, true]; + yield return [(int)1, false, false]; + yield return [(int)0, true, false]; + yield return [(int)0, false, false]; + } + + public static IEnumerable StdArchIsHFAData() + { + foreach (object[] arch in new MockTarget.StdArch()) + foreach (object[] hfaData in IsHFAData()) + yield return [.. arch, ..hfaData]; + } + + [Theory] + [MemberData(nameof(StdArchIsHFAData))] + public void IsHFA(MockTarget.Architecture arch, int featureHFA, bool flagSet, bool expected) + { + TargetPointer methodTablePtr = default; + var targetBuilder = new TestPlaceholderTarget.Builder(arch); + MockRTS rtsBuilder = new(targetBuilder.MemoryBuilder); + + MockEEClass eeClass = rtsBuilder.AddEEClass("HFAType"); + eeClass.CorTypeAttr = (uint)(System.Reflection.TypeAttributes.Public | System.Reflection.TypeAttributes.Class); + MockMethodTable methodTable = rtsBuilder.AddMethodTable("HFAType"); + uint mtFlags = (uint)MethodTableFlags_1.WFLAGS_HIGH.Category_ValueType; + if (flagSet) + mtFlags |= (uint)MethodTableFlags_1.WFLAGS_LOW.IsHFA; + methodTable.MTFlags = mtFlags; + methodTable.BaseSize = rtsBuilder.Builder.TargetTestHelpers.ObjectBaseSize; + methodTable.ParentMethodTable = rtsBuilder.SystemObjectMethodTable.Address; + methodTable.NumVirtuals = 3; + methodTablePtr = methodTable.Address; + eeClass.MethodTable = methodTable.Address; + methodTable.EEClassOrCanonMT = eeClass.Address; + + var builder = targetBuilder + .AddTypes(CreateContractTypes(rtsBuilder)) + .AddGlobals(CreateContractGlobals(rtsBuilder)) + .AddContract(version: "c1"); + if (featureHFA >= 0) + builder.AddGlobals((Constants.Globals.FeatureHFA, (ulong)featureHFA)); + TestPlaceholderTarget target = builder.Build(); + + IRuntimeTypeSystem contract = target.Contracts.RuntimeTypeSystem; + Contracts.TypeHandle typeHandle = contract.GetTypeHandle(methodTablePtr); + Assert.Equal(expected, contract.IsHFA(typeHandle)); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void IsHFAFalseForNullTypeHandle(MockTarget.Architecture arch) + { + // Null TypeHandle should never report HFA, regardless of FEATURE_HFA setting. + var targetBuilder = new TestPlaceholderTarget.Builder(arch); + MockRTS rtsBuilder = new(targetBuilder.MemoryBuilder); + TestPlaceholderTarget target = targetBuilder + .AddTypes(CreateContractTypes(rtsBuilder)) + .AddGlobals(CreateContractGlobals(rtsBuilder)) + .AddGlobals((Constants.Globals.FeatureHFA, 1UL)) + .AddContract(version: "c1") + .Build(); + + IRuntimeTypeSystem contract = target.Contracts.RuntimeTypeSystem; + Assert.False(contract.IsHFA(default)); + } + [Theory] [ClassData(typeof(MockTarget.StdArch))] public void GetGCDescSeriesReturnsEmptyForNonMethodTable(MockTarget.Architecture arch) diff --git a/src/native/managed/cdac/tests/MockDescriptors/Layout.cs b/src/native/managed/cdac/tests/MockDescriptors/Layout.cs index 57f1c667197fa9..d7f4638f13528b 100644 --- a/src/native/managed/cdac/tests/MockDescriptors/Layout.cs +++ b/src/native/managed/cdac/tests/MockDescriptors/Layout.cs @@ -86,7 +86,6 @@ public LayoutBuilder(string name, MockTarget.Architecture architecture) public LayoutBuilder AddField(string name, int offset, int size) { ArgumentException.ThrowIfNullOrEmpty(name); - ArgumentOutOfRangeException.ThrowIfNegative(offset); ArgumentOutOfRangeException.ThrowIfNegativeOrZero(size); _fields[name] = new LayoutField(name, offset, size); diff --git a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.CallingConvention.cs b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.CallingConvention.cs new file mode 100644 index 00000000000000..2a9be65cc43618 --- /dev/null +++ b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.CallingConvention.cs @@ -0,0 +1,252 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.DataContractReader.RuntimeTypeSystemHelpers; + +namespace Microsoft.Diagnostics.DataContractReader.Tests; + +/// +/// Per-architecture TransitionBlock descriptor TypeInfo for the +/// calling-convention test harness. cDAC's data-descriptor convention +/// encodes ABI constants in the "field offsets" (ArgumentRegistersOffset, +/// FirstGCRefMapSlot, OffsetOfArgs, optional +/// OffsetOfFloatArgumentRegisters), with Size carrying the +/// transition-block size. The values come directly from the +/// so tests can reference the same constants. +/// +internal partial class MockDescriptors +{ + public static class CallingConvention + { + public static Target.TypeInfo CreateTransitionBlockTypeInfo(CallConvTestCase testCase) + => TargetTestHelpers.CreateTypeInfo(CreateTransitionBlockLayout(testCase)); + + public static Layout CreateTransitionBlockLayout(CallConvTestCase testCase) + { + // The cDAC descriptor convention uses field offsets to carry ABI constant + // values; field size is unused at read-time, so 1 is just a placeholder. + LayoutBuilder builder = new("TransitionBlock", testCase.MockArch) + { + Size = testCase.TransitionBlockSize, + }; + builder.AddField("ArgumentRegistersOffset", testCase.ArgumentRegistersOffset, 1); + builder.AddField("FirstGCRefMapSlot", testCase.FirstGCRefMapSlot, 1); + builder.AddField("OffsetOfArgs", testCase.OffsetOfArgs, 1); + if (testCase.OffsetOfFloatArgumentRegisters is int f) + builder.AddField("OffsetOfFloatArgumentRegisters", f, 1); + return builder.Build(); + } + + // ----- FieldDesc layout / Value-type MT allocator ----- + + /// + /// Production FieldDesc layout: two DWORDs of flag bits packed with + /// the metadata token (DWord1) and the field offset + CorElementType (DWord2), + /// followed by a pointer to the enclosing MethodTable. + /// + public static Layout CreateFieldDescLayout(MockTarget.Architecture arch) + => MockFieldDesc.CreateLayout(arch); + + public static Target.TypeInfo CreateFieldDescTypeInfo(MockTarget.Architecture arch) + => TargetTestHelpers.CreateTypeInfo(CreateFieldDescLayout(arch)); + + /// + /// Describes a single instance field for . + /// Statics are excluded by definition — only instance fields are reported. + /// + public readonly record struct ValueTypeField(int Offset, CorElementType ElementType); + + /// + /// Allocates a value-type MethodTable + EEClass + FieldDesc array in mock + /// memory. Returns the MT address; tests embed that pointer into a + /// stored-sig blob via ELEMENT_TYPE_INTERNAL to reference the + /// value type without going through the metadata reader. + /// + public static MockMethodTable AddValueTypeMethodTable( + MockDescriptors.RuntimeTypeSystem rts, + string name, + int structSize, + IReadOnlyList fields) + { + MockTarget.Architecture arch = rts.Builder.TargetTestHelpers.Arch; + + MockEEClass eeClass = rts.AddEEClass(name); + eeClass.NumInstanceFields = (ushort)fields.Count; + eeClass.NumMethods = 0; + + // Allocate the FieldDesc array. + if (fields.Count > 0) + { + Layout fdLayout = CreateFieldDescLayout(arch); + MockMemorySpace.HeapFragment fdArray = rts.TypeSystemAllocator.Allocate( + (ulong)(fdLayout.Size * fields.Count), $"FieldDescs[{name}]"); + eeClass.FieldDescList = fdArray.Address; + + MockMethodTable enclosingMT = rts.AddMethodTable(name); + // Wire up MT <-> EEClass first since FieldDescs back-reference the MT. + eeClass.MethodTable = enclosingMT.Address; + enclosingMT.EEClassOrCanonMT = eeClass.Address; + + // ValueType category flag (low 16 bits of MTFlags Category_Mask) + IsValueType bit. + const uint Category_ValueType = 0x00040000; + enclosingMT.MTFlags = Category_ValueType; + + // BaseSize must be pointer-aligned for TypeValidation to accept this MT + // (real value-type MTs always satisfy this). Encode the actual struct size + // via BaseSizePadding so GetNumInstanceFieldBytes = BaseSize - BaseSizePadding + // returns the requested structSize. + int ptrSize = arch.Is64Bit ? 8 : 4; + uint alignedBaseSize = (uint)((structSize + (ptrSize - 1)) & ~(ptrSize - 1)); + if (alignedBaseSize == 0) alignedBaseSize = (uint)ptrSize; + enclosingMT.BaseSize = alignedBaseSize; + eeClass.BaseSizePadding = (byte)(alignedBaseSize - structSize); + + // Reserve a vtable slot so an instance method on this value type can validate. + enclosingMT.NumVirtuals = 1; + + for (int i = 0; i < fields.Count; i++) + { + ulong fdAddr = fdArray.Address + (ulong)(i * fdLayout.Size); + MockFieldDesc fd = fdLayout.Create( + fdArray.Data.AsMemory(i * fdLayout.Size, fdLayout.Size), + fdAddr); + + // DWord1: token (low 24 bits) + flags. Token field encodes RID; we + // just use the (1-based) field index since the classifier doesn't + // resolve tokens for ELEMENT_TYPE_INTERNAL sigs. + fd.DWord1 = (uint)(i + 1) & 0xFFFFFF; + + // DWord2: offset (low 27 bits) | (CorElementType << 27). + fd.DWord2 = ((uint)fields[i].Offset & 0x07FFFFFFu) + | (((uint)fields[i].ElementType & 0x1Fu) << 27); + + fd.MTOfEnclosingClass = enclosingMT.Address; + } + + return enclosingMT; + } + + // Empty-struct path: no FieldDesc array, just an MT. + MockMethodTable emptyMT = rts.AddMethodTable(name); + eeClass.MethodTable = emptyMT.Address; + emptyMT.EEClassOrCanonMT = eeClass.Address; + emptyMT.MTFlags = 0x00040000; // Category_ValueType + int emptyPtrSize = arch.Is64Bit ? 8 : 4; + uint emptyAlignedBaseSize = (uint)((structSize + (emptyPtrSize - 1)) & ~(emptyPtrSize - 1)); + if (emptyAlignedBaseSize == 0) emptyAlignedBaseSize = (uint)emptyPtrSize; + emptyMT.BaseSize = emptyAlignedBaseSize; + eeClass.BaseSizePadding = (byte)(emptyAlignedBaseSize - structSize); + return emptyMT; + } + + public static MockMethodTable AddVectorMethodTable( + MockDescriptors.RuntimeTypeSystem rts, + string vectorTypeName, + int vectorByteSize, + uint typeDefToken) + { + const uint Category_ValueType = 0x00040000; + const uint GenericsMask_GenericInst = 0x00000010; + const uint IsIntrinsicType = (uint)MethodTableFlags_1.WFLAGS2_ENUM.IsIntrinsicType; + const int MTFlags2TypeDefRidShift = 8; + + MockTarget.Architecture arch = rts.Builder.TargetTestHelpers.Arch; + MockEEClass eeClass = rts.AddEEClass(vectorTypeName); + eeClass.CorTypeAttr = (uint)(System.Reflection.TypeAttributes.Public | System.Reflection.TypeAttributes.Sealed | System.Reflection.TypeAttributes.SequentialLayout); + eeClass.NumMethods = 0; + + MockMethodTable methodTable = rts.AddMethodTable(vectorTypeName); + eeClass.MethodTable = methodTable.Address; + methodTable.EEClassOrCanonMT = eeClass.Address; + methodTable.MTFlags = Category_ValueType | GenericsMask_GenericInst; + methodTable.MTFlags2 = ((typeDefToken & 0x00FFFFFFu) << MTFlags2TypeDefRidShift) | IsIntrinsicType; + methodTable.Module = rts.SystemObjectMethodTable.Module; + methodTable.ParentMethodTable = rts.SystemObjectMethodTable.Address; + methodTable.NumVirtuals = 1; + + int ptrSize = arch.Is64Bit ? 8 : 4; + uint alignedBaseSize = (uint)((vectorByteSize + (ptrSize - 1)) & ~(ptrSize - 1)); + if (alignedBaseSize == 0) + alignedBaseSize = (uint)ptrSize; + + methodTable.BaseSize = alignedBaseSize; + eeClass.BaseSizePadding = (byte)(alignedBaseSize - vectorByteSize); + methodTable.PerInstInfo = AddSingleTypeInstantiation(rts, CreatePrimitiveTypeArg(rts, CorElementType.I4).Address); + return methodTable; + } + + private static MockMethodTable CreatePrimitiveTypeArg(MockDescriptors.RuntimeTypeSystem rts, CorElementType elementType) + { + MockEEClass eeClass = rts.AddEEClass($"{elementType}TypeArg"); + eeClass.InternalCorElementType = (byte)elementType; + + MockMethodTable methodTable = rts.AddMethodTable($"{elementType}TypeArg"); + methodTable.MTFlags = (uint)MethodTableFlags_1.WFLAGS_HIGH.Category_TruePrimitive; + methodTable.BaseSize = (uint)rts.Builder.TargetTestHelpers.PointerSize; + methodTable.Module = rts.SystemObjectMethodTable.Module; + methodTable.ParentMethodTable = rts.SystemObjectMethodTable.Address; + methodTable.NumVirtuals = 1; + eeClass.MethodTable = methodTable.Address; + methodTable.EEClassOrCanonMT = eeClass.Address; + return methodTable; + } + + private static ulong AddSingleTypeInstantiation(MockDescriptors.RuntimeTypeSystem rts, ulong typeArgAddress) + { + TargetTestHelpers helpers = rts.Builder.TargetTestHelpers; + int pointerSize = helpers.PointerSize; + + MockMemorySpace.HeapFragment perInstInfoFragment = rts.TypeSystemAllocator.Allocate( + (ulong)(pointerSize * 2), "PerInstInfo[1]"); + helpers.Write(perInstInfoFragment.Data.AsSpan(0, sizeof(ushort)), (ushort)1); + helpers.Write(perInstInfoFragment.Data.AsSpan(sizeof(ushort), sizeof(ushort)), (ushort)1); + + MockMemorySpace.HeapFragment dictionaryFragment = rts.TypeSystemAllocator.Allocate( + (ulong)pointerSize, "GenericDictionary[1]"); + helpers.WritePointer(dictionaryFragment.Data, typeArgAddress); + helpers.WritePointer(perInstInfoFragment.Data.AsSpan(pointerSize, pointerSize), dictionaryFragment.Address); + + return perInstInfoFragment.Address + (ulong)pointerSize; + } + } +} + +/// +/// Mock view of Data.FieldDesc for the calling-convention test harness. +/// Layout: two uint flag/offset words + a pointer to the enclosing MT. +/// +internal sealed class MockFieldDesc : TypedView +{ + private const string DWord1FieldName = nameof(Data.FieldDesc.DWord1); + private const string DWord2FieldName = nameof(Data.FieldDesc.DWord2); + private const string MTOfEnclosingClassFieldName = nameof(Data.FieldDesc.MTOfEnclosingClass); + + public static Layout CreateLayout(MockTarget.Architecture arch) + => new SequentialLayoutBuilder("FieldDesc", arch) + .AddUInt32Field(DWord1FieldName) + .AddUInt32Field(DWord2FieldName) + .AddPointerField(MTOfEnclosingClassFieldName) + .Build(); + + public uint DWord1 + { + get => ReadUInt32Field(DWord1FieldName); + set => WriteUInt32Field(DWord1FieldName, value); + } + + public uint DWord2 + { + get => ReadUInt32Field(DWord2FieldName); + set => WriteUInt32Field(DWord2FieldName, value); + } + + public ulong MTOfEnclosingClass + { + get => ReadPointerField(MTOfEnclosingClassFieldName); + set => WritePointerField(MTOfEnclosingClassFieldName, value); + } +} + diff --git a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.RuntimeTypeSystem.cs b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.RuntimeTypeSystem.cs index ca3b4198536a2b..4b7fbf70bd25b3 100644 --- a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.RuntimeTypeSystem.cs +++ b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.RuntimeTypeSystem.cs @@ -106,6 +106,7 @@ internal sealed class MockEEClass : TypedView private const string NumThreadStaticFieldsFieldName = nameof(Data.EEClass.NumThreadStaticFields); private const string FieldDescListFieldName = nameof(Data.EEClass.FieldDescList); private const string NumNonVirtualSlotsFieldName = nameof(Data.EEClass.NumNonVirtualSlots); + private const string BaseSizePaddingFieldName = nameof(Data.EEClass.BaseSizePadding); public static Layout CreateLayout(MockTarget.Architecture architecture) => new SequentialLayoutBuilder("EEClass", architecture) @@ -119,6 +120,7 @@ public static Layout CreateLayout(MockTarget.Architecture architect .AddUInt16Field(NumThreadStaticFieldsFieldName) .AddPointerField(FieldDescListFieldName) .AddUInt16Field(NumNonVirtualSlotsFieldName) + .AddByteField(BaseSizePaddingFieldName) .Build(); public ulong MethodTable @@ -151,11 +153,35 @@ public ushort NumInstanceFields set => WriteUInt16Field(NumInstanceFieldsFieldName, value); } + public ushort NumStaticFields + { + get => ReadUInt16Field(NumStaticFieldsFieldName); + set => WriteUInt16Field(NumStaticFieldsFieldName, value); + } + + public ushort NumThreadStaticFields + { + get => ReadUInt16Field(NumThreadStaticFieldsFieldName); + set => WriteUInt16Field(NumThreadStaticFieldsFieldName, value); + } + + public ulong FieldDescList + { + get => ReadPointerField(FieldDescListFieldName); + set => WritePointerField(FieldDescListFieldName, value); + } + public ushort NumNonVirtualSlots { get => ReadUInt16Field(NumNonVirtualSlotsFieldName); set => WriteUInt16Field(NumNonVirtualSlotsFieldName, value); } + + public byte BaseSizePadding + { + get => ReadByteField(BaseSizePaddingFieldName); + set => WriteByteField(BaseSizePaddingFieldName, value); + } } internal sealed class MockMethodTableAuxiliaryData : TypedView @@ -300,7 +326,8 @@ public class RuntimeTypeSystem { internal const ulong TestFreeObjectMethodTableGlobalAddress = 0x00000000_7a0000a0; internal const ulong TestContinuationMethodTableGlobalAddress = 0x00000000_7a0000b0; - internal const ulong TestObjectMethodTableGlobalAddress = 0x00000000_7a0000c0; + internal const ulong TestTypedReferenceMethodTableGlobalAddress = 0x00000000_7a0000c0; + internal const ulong TestObjectMethodTableGlobalAddress = 0x00000000_7a0000d0; private const ulong DefaultAllocationRangeStart = 0x00000000_4a000000; private const ulong DefaultAllocationRangeEnd = 0x00000000_4b000000; @@ -327,6 +354,7 @@ public class RuntimeTypeSystem internal ulong FreeObjectMethodTableAddress { get; private set; } internal ulong FreeObjectMethodTableGlobalAddress => TestFreeObjectMethodTableGlobalAddress; internal ulong ContinuationMethodTableGlobalAddress => TestContinuationMethodTableGlobalAddress; + internal ulong TypedReferenceMethodTableGlobalAddress => TestTypedReferenceMethodTableGlobalAddress; internal ulong ObjectMethodTableGlobalAddress => TestObjectMethodTableGlobalAddress; internal ulong MethodDescAlignment => GetMethodDescAlignment(Builder.TargetTestHelpers); internal ulong ArrayBaseSize => Builder.TargetTestHelpers.ArrayBaseBaseSize; @@ -360,6 +388,7 @@ private void AddGlobalPointers() { AddFreeObjectMethodTable(); AddContinuationMethodTableGlobal(); + AddTypedReferenceMethodTableGlobal(); AddObjectMethodTableGlobal(); } @@ -381,9 +410,15 @@ private void AddContinuationMethodTableGlobal() AddPointerGlobal("Address of Continuation Method Table", TestContinuationMethodTableGlobalAddress, 0); } + // Default to a null slot; tests that need a real System.TypedReference MT call + // SetTypedReferenceMethodTable to point the global at one. + private void AddTypedReferenceMethodTableGlobal() + { + AddPointerGlobal("Address of TypedReference Method Table", TestTypedReferenceMethodTableGlobalAddress, 0); + } + private void AddObjectMethodTableGlobal() { - // Initialized to 0; patched to point to SystemObjectMethodTable in AddSystemObjectType. AddPointerGlobal("Address of Object Method Table", TestObjectMethodTableGlobalAddress, 0); } @@ -402,9 +437,7 @@ private void AddSystemObjectType() SystemObjectEEClass.MethodTable = SystemObjectMethodTable.Address; SystemObjectMethodTable.EEClassOrCanonMT = SystemObjectEEClass.Address; - // Patch the ObjectMethodTable global to point to System.Object's MethodTable. - Span globalAddrBytes = Builder.BorrowAddressRange(TestObjectMethodTableGlobalAddress, Builder.TargetTestHelpers.PointerSize); - Builder.TargetTestHelpers.WritePointer(globalAddrBytes, SystemObjectMethodTable.Address); + SetObjectMethodTable(SystemObjectMethodTable.Address); } private void AddContinuationType() @@ -439,6 +472,22 @@ internal void SetContinuationMethodTable(ulong continuationMethodTable) Builder.TargetTestHelpers.WritePointer(globalAddrBytes, continuationMethodTable); } + internal void SetObjectMethodTable(ulong objectMethodTable) + { + Span globalAddrBytes = Builder.BorrowAddressRange(TestObjectMethodTableGlobalAddress, Builder.TargetTestHelpers.PointerSize); + Builder.TargetTestHelpers.WritePointer(globalAddrBytes, objectMethodTable); + } + + // Repoints the TypedReferenceMethodTable global at the given MT. Tests that + // exercise TypedByRef arg/return handling first allocate a value-type MT + // matching System.TypedReference's layout ({ ref byte, IntPtr }) and then + // call this to install it as the well-known target. + internal void SetTypedReferenceMethodTable(ulong typedReferenceMethodTable) + { + Span globalAddrBytes = Builder.BorrowAddressRange(TestTypedReferenceMethodTableGlobalAddress, Builder.TargetTestHelpers.PointerSize); + Builder.TargetTestHelpers.WritePointer(globalAddrBytes, typedReferenceMethodTable); + } + internal MockEEClass AddEEClass(string name) => Add(EEClassLayout, $"EEClass '{name}'"); diff --git a/src/native/managed/cdac/tests/RuntimeTypeSystemGetVectorSizeTests.cs b/src/native/managed/cdac/tests/RuntimeTypeSystemGetVectorSizeTests.cs new file mode 100644 index 00000000000000..605d36e1a24afb --- /dev/null +++ b/src/native/managed/cdac/tests/RuntimeTypeSystemGetVectorSizeTests.cs @@ -0,0 +1,109 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.Tests; + +public class RuntimeTypeSystemGetVectorSizeTests +{ + private static readonly Lazy s_syntheticMetadata = new(SyntheticVectorMetadata.Create); + + [Theory] + [InlineData("Vector64`1", 8)] + [InlineData("Vector128`1", 16)] + public void GetVectorSize_KnownIntrinsic_ReturnsSize(string typeName, int expectedSize) + { + SyntheticVectorMetadata metadata = s_syntheticMetadata.Value; + (Target target, TypeHandle handle) = CreateTarget( + metadata, + rts => MockDescriptors.CallingConvention.AddVectorMethodTable( + rts, + typeName, + expectedSize, + metadata.GetTypeDefToken(typeName))); + + Assert.Equal(expectedSize, target.Contracts.RuntimeTypeSystem.GetVectorSize(handle)); + } + + [Fact] + public void GetVectorSize_NotIntrinsicType_ReturnsZero() + { + (Target target, TypeHandle handle) = CreateTarget( + s_syntheticMetadata.Value, + rts => MockDescriptors.CallingConvention.AddValueTypeMethodTable(rts, "Vector128`1", structSize: 16, fields: [])); + + Assert.Equal(0, target.Contracts.RuntimeTypeSystem.GetVectorSize(handle)); + } + + [Fact] + public void GetVectorSize_NoMetadata_ReturnsZero() + { + SyntheticVectorMetadata metadata = s_syntheticMetadata.Value; + (Target target, TypeHandle handle) = CreateTarget( + syntheticMetadata: null, + rts => MockDescriptors.CallingConvention.AddVectorMethodTable( + rts, + "Vector128`1", + 16, + metadata.GetTypeDefToken("Vector128`1"))); + + Assert.Equal(0, target.Contracts.RuntimeTypeSystem.GetVectorSize(handle)); + } + + [Fact] + public void GetVectorSize_UnhandledIntrinsicName_ReturnsZero() + { + // Use Vector128 metadata/token but give it a name the runtime doesn't + // recognize. The real Vector128 token resolves to the correct TypeDef row + // whose name IS recognized, so we need a type name the runtime won't match. + // The simplest approach: build a one-off metadata image with a fake type. + SyntheticVectorMetadata fakeMetadata = SyntheticVectorMetadata.CreateWithExtraType( + "System.Runtime.Intrinsics", "FakeVector`1"); + (Target target, TypeHandle handle) = CreateTarget( + fakeMetadata, + rts => MockDescriptors.CallingConvention.AddVectorMethodTable( + rts, + "FakeVector`1", + 32, + fakeMetadata.GetTypeDefToken("FakeVector`1"))); + + Assert.Equal(0, target.Contracts.RuntimeTypeSystem.GetVectorSize(handle)); + } + + [Fact] + public void GetVectorSize_SystemNumericsVector_ReturnsFieldBytes() + { + SyntheticVectorMetadata metadata = s_syntheticMetadata.Value; + (Target target, TypeHandle handle) = CreateTarget( + metadata, + rts => MockDescriptors.CallingConvention.AddVectorMethodTable( + rts, + "Vector`1", + 32, + metadata.GetTypeDefToken("Vector`1"))); + + Assert.Equal(32, target.Contracts.RuntimeTypeSystem.GetVectorSize(handle)); + } + + private static (Target Target, TypeHandle Handle) CreateTarget( + SyntheticVectorMetadata? syntheticMetadata, + Func addMethodTable) + { + ulong methodTableAddress = 0; + (Target target, _) = CallingConventionTestHelpers.CreateTargetWithMethod( + CallConvCases.AMD64Windows, + hasThis: false, + (rts, sig) => + { + MockMethodTable methodTable = addMethodTable(rts); + methodTableAddress = methodTable.Address; + sig.Return(CorElementType.Void); + }, + syntheticMetadata: syntheticMetadata); + + return (target, target.Contracts.RuntimeTypeSystem.GetTypeHandle(new TargetPointer(methodTableAddress))); + } +} From 837a8b1ff3f5e364c8c4db3f3e725155a1d5cc81 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Thu, 21 May 2026 21:55:07 -0400 Subject: [PATCH 2/9] WIP: cDAC ByRefLike GC ref enumeration walker and tests - Add IRuntimeTypeSystem.EnumerateInstanceFieldDescs and LookupApproxFieldTypeHandle primitives - Implement GcRefEnumeration.EnumerateByRefLikeRoots walker (recursive field scan) - Update CallingConvention_1.ComputeValueTypeHandle to populate handle for ByRefLike args - GcScanner.ReportArgument dispatches to ByRefLike walker via rts.IsByRefLike - Consolidate duplicate LookupApproxFieldTypeHandle/ResolveFieldTypeHandle helpers - Add unit tests for ByRefLike dispatch in GcScannerReportArgumentTests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../vm/datadescriptor/datadescriptor.inc | 1 + .../Contracts/ICallingConvention.cs | 44 +- .../Contracts/IRuntimeTypeSystem.cs | 27 + .../CallingConvention/ArgTypeInfo.cs | 122 ++--- .../ArgTypeInfoSignatureProvider.cs | 144 +++-- .../CallingConvention/CallingConvention_1.cs | 59 +- .../SystemVStructClassifier.cs | 42 +- .../Contracts/RuntimeTypeSystem_1.cs | 70 +++ .../StackWalk/GC/GcRefEnumeration.cs | 158 ++++++ .../Contracts/StackWalk/GC/GcScanner.cs | 104 +++- .../MethodTableFlags_1.cs | 2 + .../AMD64UnixCallingConventionTests.cs | 71 +++ .../AMD64WindowsCallingConventionTests.cs | 109 ++++ .../Arm64CallingConventionTests.cs | 54 ++ .../DumpTests/CallSiteLayoutDumpTests.cs | 508 ++++++++++++++++++ .../CallSiteLayout/CallSiteLayout.csproj | 17 + .../Debuggees/CallSiteLayout/Program.cs | 276 ++++++++++ .../cdac/tests/DumpTests/DumpTestBase.cs | 12 +- .../cdac/tests/DumpTests/DumpTestHelpers.cs | 22 + .../cdac/tests/DumpTests/RunDumpTests.ps1 | 6 + .../tests/DumpTests/SkipOnArchAttribute.cs | 37 +- .../cdac/tests/GcRefEnumerationTests.cs | 151 ++++++ .../tests/GcScannerReportArgumentTests.cs | 483 +++++++++++++++++ 23 files changed, 2346 insertions(+), 173 deletions(-) create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcRefEnumeration.cs create mode 100644 src/native/managed/cdac/tests/DumpTests/CallSiteLayoutDumpTests.cs create mode 100644 src/native/managed/cdac/tests/DumpTests/Debuggees/CallSiteLayout/CallSiteLayout.csproj create mode 100644 src/native/managed/cdac/tests/DumpTests/Debuggees/CallSiteLayout/Program.cs create mode 100644 src/native/managed/cdac/tests/GcRefEnumerationTests.cs create mode 100644 src/native/managed/cdac/tests/GcScannerReportArgumentTests.cs diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index 415907525491f3..78e3ae3206494a 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -1069,6 +1069,7 @@ CDAC_TYPE_FIELD(TransitionBlock, T_INT32, OffsetOfFloatArgumentRegisters, -(int) #else CDAC_TYPE_FIELD(TransitionBlock, T_INT32, OffsetOfFloatArgumentRegisters, -(int)sizeof(FloatArgumentRegisters)) #endif +#endif // CALLDESCR_FPARGREGS CDAC_TYPE_END(TransitionBlock) #ifdef DEBUGGING_SUPPORTED 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 index 54594258810f92..3887991af70b96 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ICallingConvention.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ICallingConvention.cs @@ -37,9 +37,51 @@ public readonly record struct ArgSlot( /// One or more register/stack slots that together carry the argument's value. /// Always non-empty. /// +/// +/// Identity of the value type whose storage occupies as an +/// opaque, contiguous, undecomposed buffer, or +/// when the slots do not describe such a buffer. +/// +/// This is layout information about what the per-arch iterator chose to do, not +/// GC information per se: the iterator surfaces the type only when it left the +/// argument's storage in a single contiguous run that the iterator did not +/// crack open into individually-typed s. Consumers that want +/// to inspect the buffer's internals (e.g. the GC scanner walking its GC +/// descriptor, a future debugger arg-formatter rendering the buffer's fields) +/// can resolve the layout via . +/// +/// +/// Populated when all of the following hold: +/// +/// The argument is a value type passed by value +/// (not ). +/// The per-arch iterator left the storage opaque — i.e. +/// every entry in has +/// == , +/// and together they cover one contiguous byte range starting at +/// Slots[0].Offset. Examples: Windows-AMD64 enregistered struct, +/// ARM64 non-HFA struct in consecutive GPRs, any stack-passed struct. +/// +/// +/// +/// Both ordinary value types and ByRefLike types (ref-struct / Span-style) are +/// surfaced here. Consumers select the appropriate field-enumeration strategy +/// by querying : ordinary value types +/// walk the CGCDesc series; ByRefLike types require a field-by-field walk to +/// pick up managed byref fields that the CGCDesc series does not encode. +/// +/// +/// otherwise — i.e. for primitives, references, byrefs, +/// arguments passed by implicit reference (), HFAs +/// and other aggregates the iterator already decomposed into individually-typed +/// slots (e.g. SystemV split struct: one slot per eightbyte with each slot's +/// reflecting that eightbyte's classification). +/// +/// public readonly record struct ArgLayout( bool IsPassedByRef, - IReadOnlyList Slots); + IReadOnlyList Slots, + TypeHandle? ValueTypeHandle = null); /// /// Describes the layout of all arguments at a call site, as imposed by the 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 ef329fca2345bd..a50def1ce3534a 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 @@ -185,6 +185,12 @@ public interface IRuntimeTypeSystem : IContract CorElementType GetSignatureCorElementType(TypeHandle typeHandle) => throw new NotImplementedException(); bool IsValueType(TypeHandle typeHandle) => throw new NotImplementedException(); + // True if the type is a "ByRefLike" / ref struct (e.g. Span, ReadOnlySpan, + // TypedReference). ByRefLike types can have managed references *and* byref-typed + // fields embedded at arbitrary offsets, and require special handling in the GC + // scanner — the standard GCDesc walk only covers ordinary managed references. + bool IsByRefLike(TypeHandle typeHandle) => throw new NotImplementedException(); + // Internal element type of the type. Unlike GetSignatureCorElementType, this returns the underlying primitive // type for enums (e.g. I4 for an enum with int underlying type) and for PrimitiveValueType categories. // For arrays, reference types, and TypeDescs, behaves identically to GetSignatureCorElementType. @@ -281,6 +287,27 @@ public interface IRuntimeTypeSystem : IContract TargetPointer GetFieldDescByName(TypeHandle typeHandle, string fieldName) => throw new NotImplementedException(); TargetPointer GetFieldDescStaticAddress(TargetPointer fieldDescPointer, bool unboxValueTypes = true) => throw new NotImplementedException(); TargetPointer GetFieldDescThreadStaticAddress(TargetPointer fieldDescPointer, TargetPointer thread, bool unboxValueTypes = true) => throw new NotImplementedException(); + + /// + /// Enumerates the FieldDesc pointers for the instance (non-static) fields of + /// , in field-list order. Statics interleaved in + /// the underlying FieldDesc array are skipped. + /// + /// + /// Returns an empty sequence for type handles that do not refer to a MethodTable, + /// or for types with no FieldDescList. + /// + IEnumerable EnumerateInstanceFieldDescs(TypeHandle typeHandle) => throw new NotImplementedException(); + + /// + /// Resolves a field's declared type without triggering type loading. Mirrors + /// native FieldDesc::LookupApproxFieldTypeHandle (DAC variant): decodes + /// the field's metadata signature and returns the resulting . + /// Returns a default (null) TypeHandle when the field's type is not currently + /// resolvable (e.g., the enclosing module's metadata is unavailable, or the + /// referenced type is not loaded). + /// + TypeHandle LookupApproxFieldTypeHandle(TargetPointer fieldDescPointer) => throw new NotImplementedException(); #endregion FieldDesc inspection APIs #region Other APIs void GetCoreLibFieldDescAndDef(string typeNamespace, string typeName, string fieldName, out TargetPointer fieldDescAddr, out FieldDefinition fieldDef) => throw new NotImplementedException(); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgTypeInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgTypeInfo.cs index 82f5001964216b..1ac071c52cd2ce 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgTypeInfo.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgTypeInfo.cs @@ -92,37 +92,69 @@ public static int GetElemSize(CorElementType t, ArgTypeInfo thValueType, int poi /// /// Creates an from a target TypeHandle using the - /// runtime type system contract. + /// runtime type system contract. Handles primitives (using the static element-size + /// table), reference types (pointer-sized, projected to + /// to match downstream classifier expectations), and real value types (full MT layout + /// query for size / HFA / alignment). Mirrors native MetaSig::GetByValType + + /// SigPointer::PeekElemTypeNormalized. /// public static ArgTypeInfo FromTypeHandle(Target target, TypeHandle th) { IRuntimeTypeSystem rts = target.Contracts.RuntimeTypeSystem; CorElementType corType = rts.GetSignatureCorElementType(th); - bool isValueType = corType is CorElementType.ValueType; - int size = isValueType - ? rts.GetNumInstanceFieldBytes(th) - : target.PointerSize; - - bool requiresAlign8 = false; - bool isHfa = false; - int hfaElemSize = 0; - - if (isValueType) + switch (corType) { - requiresAlign8 = rts.RequiresAlign8(th); - isHfa = rts.IsHFA(th); - if (isHfa) - { - hfaElemSize = ComputeHfaElementSize(target, rts, th, requiresAlign8); - } + case CorElementType.ValueType: + return FromValueType(target, rts, th, corType); + + // Primitives have static sizes -- consult s_elemSizes rather than reading + // the MethodTable. CorElementType for primitives is already normalized by + // GetSignatureCorElementType (e.g. enums collapse to their underlying type). + case CorElementType.Void: + case CorElementType.Boolean: + case CorElementType.Char: + case CorElementType.I1: + case CorElementType.U1: + case CorElementType.I2: + case CorElementType.U2: + case CorElementType.I4: + case CorElementType.U4: + case CorElementType.I8: + case CorElementType.U8: + case CorElementType.R4: + case CorElementType.R8: + case CorElementType.I: + case CorElementType.U: + case CorElementType.FnPtr: + case CorElementType.Ptr: + return ForPrimitive(corType, target.PointerSize, th); + + case CorElementType.Byref: + return ForPrimitive(CorElementType.Byref, target.PointerSize, th); + + default: + // Reference types (Class, String, Object, Array, SzArray, etc.) are + // projected to CorElementType.Class so downstream classification + // (X86ArgIterator, SystemVStructClassifier) sees them as a single + // category. The TypeHandle is preserved for generic-instantiation + // matching. + return ForPrimitive(CorElementType.Class, target.PointerSize, th); } + } + + private static ArgTypeInfo FromValueType(Target target, IRuntimeTypeSystem rts, TypeHandle th, CorElementType corType) + { + int size = rts.GetNumInstanceFieldBytes(th); + bool requiresAlign8 = rts.RequiresAlign8(th); + bool isHfa = rts.IsHFA(th); + int hfaElemSize = isHfa ? ComputeHfaElementSize(target, rts, th, requiresAlign8) : 0; return new ArgTypeInfo { CorElementType = corType, Size = size, - IsValueType = isValueType, + IsValueType = true, RequiresAlign8 = requiresAlign8, IsHomogeneousAggregate = isHfa, HomogeneousAggregateElementSize = hfaElemSize, @@ -178,7 +210,7 @@ private static int ComputeHfaElementSize(Target target, IRuntimeTypeSystem rts, case CorElementType.R8: return 8; case CorElementType.ValueType: - TypeHandle nested = LookupApproxFieldTypeHandle(target, rts, firstField); + TypeHandle nested = rts.LookupApproxFieldTypeHandle(firstField); if (nested.IsNull || !nested.IsMethodTable()) return 0; current = nested; @@ -192,52 +224,20 @@ private static int ComputeHfaElementSize(Target target, IRuntimeTypeSystem rts, return 0; } - /// - /// Resolves a field's declared type without triggering type loading. Mirrors native - /// FieldDesc::LookupApproxFieldTypeHandle (DAC variant): walks the field's - /// metadata signature and returns the resulting , or a null - /// handle when the type isn't already loaded. - /// - private static TypeHandle LookupApproxFieldTypeHandle(Target target, IRuntimeTypeSystem rts, TargetPointer fieldDescPointer) - { - if (fieldDescPointer == TargetPointer.Null) - return default; - - uint token = rts.GetFieldDescMemberDef(fieldDescPointer); - EntityHandle entityHandle = MetadataTokens.EntityHandle((int)token); - if (entityHandle.IsNil || entityHandle.Kind != HandleKind.FieldDefinition) - return default; - - TargetPointer enclosingMT = rts.GetMTOfEnclosingClass(fieldDescPointer); - TypeHandle ctx = rts.GetTypeHandle(enclosingMT); - if (!ctx.IsMethodTable()) - return default; - - TargetPointer modulePtr = rts.GetModule(ctx); - if (modulePtr == TargetPointer.Null) - return default; - - ModuleHandle moduleHandle = target.Contracts.Loader.GetModuleHandleFromModulePtr(modulePtr); - MetadataReader? mdReader = target.Contracts.EcmaMetadata.GetMetadata(moduleHandle); - if (mdReader is null) - return default; - - FieldDefinition fieldDef = mdReader.GetFieldDefinition((FieldDefinitionHandle)entityHandle); - try - { - return target.Contracts.Signature.DecodeFieldSignature(fieldDef.Signature, moduleHandle, ctx); - } - catch - { - return default; - } - } - /// /// Creates an for a primitive type that doesn't need /// type handle resolution. /// public static ArgTypeInfo ForPrimitive(CorElementType corType, int pointerSize) + => ForPrimitive(corType, pointerSize, default); + + /// + /// Creates an for a primitive / reference type, optionally + /// carrying its resolved . The handle is used downstream by + /// generic-instantiation lookup so a type argument like string or int + /// can be matched against an instantiated type's PerInstInfo. + /// + public static ArgTypeInfo ForPrimitive(CorElementType corType, int pointerSize, TypeHandle runtimeTypeHandle) { return new ArgTypeInfo { @@ -247,7 +247,7 @@ public static ArgTypeInfo ForPrimitive(CorElementType corType, int pointerSize) RequiresAlign8 = false, IsHomogeneousAggregate = false, HomogeneousAggregateElementSize = 0, - RuntimeTypeHandle = default, + RuntimeTypeHandle = runtimeTypeHandle, }; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgTypeInfoSignatureProvider.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgTypeInfoSignatureProvider.cs index 8f789410bd106c..1a53e263de1597 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgTypeInfoSignatureProvider.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgTypeInfoSignatureProvider.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Reflection.Metadata; @@ -43,6 +44,7 @@ internal sealed class ArgTypeInfoSignatureProvider private readonly Target _target; private readonly ModuleHandle _moduleHandle; private ArgTypeInfo? _cachedTypedReferenceInfo; + private readonly Dictionary _primitiveTypeHandles = new(); public ArgTypeInfoSignatureProvider(Target target, ModuleHandle moduleHandle) { @@ -53,17 +55,50 @@ public ArgTypeInfoSignatureProvider(Target target, ModuleHandle moduleHandle) public ArgTypeInfo GetPrimitiveType(PrimitiveTypeCode typeCode) => typeCode switch { - PrimitiveTypeCode.String or PrimitiveTypeCode.Object - => ArgTypeInfo.ForPrimitive(CorElementType.Class, _target.PointerSize), + // Surface the resolved MethodTable for reference primitives so they can be + // matched as type arguments inside generic instantiations (see + // GetGenericInstantiation). The CorElementType.Class projection is preserved + // for backward compatibility with downstream Iterator/SystemV classification. + PrimitiveTypeCode.String + => ArgTypeInfo.ForPrimitive(CorElementType.Class, _target.PointerSize, ResolvePrimitiveTypeHandle(CorElementType.String)), + PrimitiveTypeCode.Object + => ArgTypeInfo.ForPrimitive(CorElementType.Class, _target.PointerSize, ResolvePrimitiveTypeHandle(CorElementType.Object)), // TypedReference has no class token in the signature blob -- the runtime // identifies its layout via the well-known g_TypedReferenceMT global. // Mirroring native callingconvention.h:1351-1355, we substitute the // TypedReference MethodTable here so the rest of ArgIterator (and the // SystemV struct classifier) see it as an ordinary 16-byte value type. PrimitiveTypeCode.TypedReference => GetTypedReferenceInfo(), - _ => ArgTypeInfo.ForPrimitive(PrimitiveToCorElementType(typeCode), _target.PointerSize), + _ => PrimitiveWithHandle(PrimitiveToCorElementType(typeCode)), }; + private ArgTypeInfo PrimitiveWithHandle(CorElementType corType) + => ArgTypeInfo.ForPrimitive(corType, _target.PointerSize, ResolvePrimitiveTypeHandle(corType)); + + /// + /// Resolves the canonical for a primitive + /// via the CoreLib binder. Returns default on failure (e.g. older runtime image, or a + /// CorElementType with no binder entry such as ). + /// + private TypeHandle ResolvePrimitiveTypeHandle(CorElementType corType) + { + if (_primitiveTypeHandles.TryGetValue(corType, out TypeHandle cached)) + return cached; + + TypeHandle th; + try + { + th = _target.Contracts.RuntimeTypeSystem.GetPrimitiveType(corType); + } + catch + { + th = default; + } + + _primitiveTypeHandles[corType] = th; + return th; + } + private ArgTypeInfo GetTypedReferenceInfo() { if (_cachedTypedReferenceInfo is { } cached) @@ -102,11 +137,13 @@ public ArgTypeInfo GetTypeFromReference(MetadataReader reader, TypeReferenceHand => FromTokenLookup(_target.Contracts.Loader.GetLookupTables(_moduleHandle).TypeRefToMethodTable, MetadataTokens.GetToken(handle), rawTypeKind); public ArgTypeInfo GetTypeFromSpecification(MetadataReader reader, ArgTypeInfoSignatureContext genericContext, TypeSpecificationHandle handle, byte rawTypeKind) - // TODO: Resolve the TypeSpec to a concrete (already-loaded) TypeHandle so that - // generic value-type instantiations get correct size / HFA classification. Native - // does this via SigPointer::GetTypeHandleThrowing + the instantiated-type - // hashtable; cDAC needs an equivalent lookup-only RTS API. Until then, fall back - // to a conservative pointer-sized placeholder for value types. + // Inline GENERICINST blobs in a method signature are dispatched to + // GetGenericInstantiation directly by SRM (recursing through nested levels), so + // common cases like KeyValuePair> never reach this + // path. This handler only fires for actual TypeSpec *tokens* referenced from a + // signature (e.g., certain custom-modifier encodings). For those, native + // resolves via SigPointer::GetTypeHandleThrowing; cDAC would need an equivalent + // lookup-only RTS API. Until then, fall back to a conservative placeholder. => rawTypeKind == (byte)SignatureTypeKind.ValueType ? UnresolvedValueType() : ArgTypeInfo.ForPrimitive(CorElementType.Class, _target.PointerSize); @@ -118,11 +155,49 @@ public ArgTypeInfo GetTypeFromSpecification(MetadataReader reader, ArgTypeInfoSi public ArgTypeInfo GetGenericInstantiation(ArgTypeInfo genericType, ImmutableArray typeArguments) { - // TODO: lookup the instantiated MethodTable so generic value-type args get correct - // size / HFA. For reference-type generic instantiations the open-generic's - // ArgTypeInfo (pointer-sized Class) is already correct; for value-type - // instantiations the open generic's size is not meaningful, so we downgrade to a - // conservative pointer-sized placeholder. + // Resolve the constructed instantiation to a concrete loaded TypeHandle so that + // value-type instantiations (KeyValuePair, Span, ValueTuple<...>, etc.) + // surface their actual size / HFA / alignment to ArgIterator -- matching native + // SigPointer::GetTypeHandleThrowing + the loader's available-instantiations + // lookup. Requires the open generic's TypeHandle and a TypeHandle for every + // type argument; if anything is missing we fall back to a conservative + // pointer-sized placeholder for value types (and pass-through for reference + // generics, whose pointer-sized representation is already correct). + TypeHandle openGeneric = genericType.RuntimeTypeHandle; + if (openGeneric.Address != TargetPointer.Null) + { + ImmutableArray.Builder argBuilder = ImmutableArray.CreateBuilder(typeArguments.Length); + bool haveAllArgHandles = true; + for (int i = 0; i < typeArguments.Length; i++) + { + TypeHandle argHandle = typeArguments[i].RuntimeTypeHandle; + if (argHandle.Address == TargetPointer.Null) + { + haveAllArgHandles = false; + break; + } + argBuilder.Add(argHandle); + } + + if (haveAllArgHandles) + { + try + { + TypeHandle constructed = _target.Contracts.RuntimeTypeSystem.GetConstructedType( + openGeneric, + CorElementType.GenericInst, + rank: 0, + argBuilder.MoveToImmutable()); + if (constructed.Address != TargetPointer.Null) + return BuildFromTypeHandle(constructed); + } + catch + { + // Fall through to the conservative placeholder below. + } + } + } + if (genericType.CorElementType == CorElementType.ValueType) return UnresolvedValueType(); return genericType; @@ -224,50 +299,17 @@ private ArgTypeInfo FallbackForRawTypeKind(byte rawTypeKind) /// /// Build an from a resolved . Mirrors /// native SigPointer::PeekElemTypeNormalized + MetaSig::GetByValType: - /// enums collapse to their underlying primitive (via GetSignatureCorElementType) - /// so they classify as a non-GC scalar; value types surface the resolved - /// with full size / HFA / alignment for ArgIterator. + /// enums collapse to their underlying primitive (via GetSignatureCorElementType); + /// value types surface the resolved with full size / HFA / + /// alignment for ArgIterator; reference types preserve the handle for generic + /// type-argument matching. /// private ArgTypeInfo BuildFromTypeHandle(TypeHandle typeHandle) { if (typeHandle.Address == TargetPointer.Null) return ArgTypeInfo.ForPrimitive(CorElementType.Class, _target.PointerSize); - IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem; - CorElementType corType = rts.GetSignatureCorElementType(typeHandle); - - switch (corType) - { - case CorElementType.Void: - case CorElementType.Boolean: - case CorElementType.Char: - case CorElementType.I1: - case CorElementType.U1: - case CorElementType.I2: - case CorElementType.U2: - case CorElementType.I4: - case CorElementType.U4: - case CorElementType.I8: - case CorElementType.U8: - case CorElementType.R4: - case CorElementType.R8: - case CorElementType.I: - case CorElementType.U: - case CorElementType.FnPtr: - case CorElementType.Ptr: - return ArgTypeInfo.ForPrimitive(corType, _target.PointerSize); - - case CorElementType.Byref: - return ArgTypeInfo.ForPrimitive(CorElementType.Byref, _target.PointerSize); - - case CorElementType.ValueType: - // GetSignatureCorElementType already collapses enums to their underlying - // primitive; anything still typed as ValueType is a real struct. - return ArgTypeInfo.FromTypeHandle(_target, typeHandle); - - default: - return ArgTypeInfo.ForPrimitive(CorElementType.Class, _target.PointerSize); - } + return ArgTypeInfo.FromTypeHandle(_target, typeHandle); } /// diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CallingConvention_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CallingConvention_1.cs index be0b56bd3fdabd..1f78afa149edc2 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CallingConvention_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CallingConvention_1.cs @@ -7,6 +7,7 @@ using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; using Microsoft.Diagnostics.DataContractReader.Contracts.CallingConventionHelpers; +using Microsoft.Diagnostics.DataContractReader.RuntimeTypeSystemHelpers; using Microsoft.Diagnostics.DataContractReader.SignatureHelpers; namespace Microsoft.Diagnostics.DataContractReader.Contracts; @@ -90,13 +91,69 @@ CallSiteLayout ICallingConvention.ComputeCallSiteLayout(MethodDescHandle method) foreach (ArgLocation l in loc.Locations) slots.Add(new ArgSlot(l.TransitionBlockOffset, l.ElementType)); - args.Add(new ArgLayout(loc.IsByRef, slots)); + args.Add(new ArgLayout(loc.IsByRef, slots, ComputeValueTypeHandle(loc, slots))); argIndex++; } return new CallSiteLayout(thisOffset, isValueTypeThis, asyncOffset, varArgCookieOffset, args); } + /// + /// Mirrors native MetaSig::GcScanRoots's value-type branch + /// (see src/coreclr/vm/siginfo.cpp): when an argument is a value type + /// passed by value in storage that the per-arch iterator did not + /// GC-decompose, the GC scanner walks the type's layout to report embedded + /// refs. Surface the value-type's on + /// so the scanner can do that walk. The scanner + /// dispatches on to choose + /// between a CGCDesc walk (ordinary value types) and a field walk + /// (ByRefLike types: Span<T>, ref structs). + /// + /// + /// The value type's when the layout describes a + /// contiguous by-value buffer that requires a layout-driven walk to report + /// refs (including ByRefLike types); otherwise + /// (primitives, references, byref-passed value types, iterator-decomposed + /// slots). + /// + private static TypeHandle? ComputeValueTypeHandle(ArgLocDesc loc, List slots) + { + // Pass-by-implicit-reference: the slot holds an interior pointer and the + // GC scanner reports it via the byref path; no value-type walk needed. + if (loc.IsByRef) + return null; + + // Native dispatches by gElementTypeInfo[etype].m_gc; only ValueType / + // TypedByRef route through the GCDesc walk. Other types are handled by + // the per-slot ElementType already. + if (loc.ArgType is not (CorElementType.ValueType or CorElementType.TypedByRef)) + return null; + + // If the per-arch iterator pre-decomposed the storage (e.g. SysV split + // structs on AMD64-Unix emit per-eightbyte ElementTypes like Class/Byref/R8; + // ARM64 HFAs emit per-FP-reg R4/R8 slots), the GC scanner already has + // sufficient information via ArgSlot.ElementType. Walking GCDesc would + // duplicate or contradict the iterator's classification. Discriminator: + // every slot has ElementType == ValueType iff the iterator did NOT + // decompose this argument. + foreach (ArgSlot s in slots) + { + if (s.ElementType != CorElementType.ValueType) + return null; + } + + TypeHandle th = loc.ArgTypeInfo.RuntimeTypeHandle; + if (!th.IsMethodTable()) + return null; + + // Both ordinary value types and ByRefLike types (Span, ref structs) are + // reported through ValueTypeHandle. The GC scanner dispatches on + // IRuntimeTypeSystem.IsByRefLike to choose the walk strategy: a CGCDesc + // series walk for ordinary value types, a field-by-field walk (parallel to + // native ByRefPointerOffsetsReporter in siginfo.cpp) for ByRefLike types. + return th; + } + /// /// Decodes the signature for into a /// . Matches native diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/SystemVStructClassifier.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/SystemVStructClassifier.cs index ec92a61ade7cb4..a64bcf8d109907 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/SystemVStructClassifier.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/SystemVStructClassifier.cs @@ -208,11 +208,10 @@ private static bool ClassifyEightBytes( if (entity.IsNil || entity.Kind != HandleKind.FieldDefinition) return false; - // Resolve the field's declared type (only needed for ValueType fields, but - // the offset comes from FieldDef in all cases). + // Resolve the field's FieldDefinition (used by GetFieldDescOffset for the + // BigRVA path on static RVA fields — instance walks never hit it, but we + // keep the lookup for safety/parity with the offset API contract). FieldDefinition fieldDef = default; - ModuleHandle moduleHandle = default; - MetadataReader? mdReader = null; try { TargetPointer enclosingMT = rts.GetMTOfEnclosingClass(fdPtr); @@ -220,8 +219,8 @@ private static bool ClassifyEightBytes( TargetPointer modulePtr = rts.GetModule(ctx); if (modulePtr != TargetPointer.Null) { - moduleHandle = target.Contracts.Loader.GetModuleHandleFromModulePtr(modulePtr); - mdReader = target.Contracts.EcmaMetadata.GetMetadata(moduleHandle); + ModuleHandle moduleHandle = target.Contracts.Loader.GetModuleHandleFromModulePtr(modulePtr); + MetadataReader? mdReader = target.Contracts.EcmaMetadata.GetMetadata(moduleHandle); if (mdReader is not null) fieldDef = mdReader.GetFieldDefinition((FieldDefinitionHandle)entity); } @@ -238,7 +237,7 @@ private static bool ClassifyEightBytes( if (fieldType == CorElementType.ValueType) { // For nested value-type fields, resolve the field's TypeHandle to get its size. - TypeHandle fieldTH = ResolveFieldTypeHandle(target, rts, fdPtr, mdReader, fieldDef, moduleHandle); + TypeHandle fieldTH = rts.LookupApproxFieldTypeHandle(fdPtr); if (fieldTH.IsMethodTable()) fieldSize = rts.GetNumInstanceFieldBytes(fieldTH); } @@ -250,7 +249,7 @@ private static bool ClassifyEightBytes( if (fieldClass == SystemVClassification.Struct) { // Recurse into the nested value type's fields. - TypeHandle nested = ResolveFieldTypeHandle(target, rts, fdPtr, mdReader, fieldDef, moduleHandle); + TypeHandle nested = rts.LookupApproxFieldTypeHandle(fdPtr); if (!nested.IsMethodTable()) return false; @@ -304,33 +303,6 @@ private static bool ClassifyEightBytes( return true; } - /// - /// Resolves a FieldDesc's declared type to its . Used for - /// nested-value-type recursion. Returns a null TypeHandle if resolution fails. - /// - private static TypeHandle ResolveFieldTypeHandle( - Target target, - IRuntimeTypeSystem rts, - TargetPointer fdPtr, - MetadataReader? mdReader, - FieldDefinition fieldDef, - ModuleHandle moduleHandle) - { - if (mdReader is null || moduleHandle.Address == TargetPointer.Null) - return default; - - try - { - TargetPointer enclosingMT = rts.GetMTOfEnclosingClass(fdPtr); - TypeHandle ctx = rts.GetTypeHandle(enclosingMT); - return target.Contracts.Signature.DecodeFieldSignature(fieldDef.Signature, moduleHandle, ctx); - } - catch - { - return default; - } - } - /// /// Byte-by-byte sweep that assembles eightbyte classifications from the /// per-field classifications recorded in . Matches 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 30b07003defa41..56518373aebc9a 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 @@ -575,6 +575,7 @@ public bool IsObjRef(TypeHandle typeHandle) return elementType is CorElementType.Class or CorElementType.Array or CorElementType.SzArray; } public bool ContainsGCPointers(TypeHandle typeHandle) => !typeHandle.IsMethodTable() ? false : _methodTables[typeHandle.Address].Flags.ContainsGCPointers; + public bool IsByRefLike(TypeHandle typeHandle) => !typeHandle.IsMethodTable() ? false : _methodTables[typeHandle.Address].Flags.IsByRefLike; public bool RequiresAlign8(TypeHandle typeHandle) => !typeHandle.IsMethodTable() ? false : _methodTables[typeHandle.Address].Flags.RequiresAlign8; public bool IsHFA(TypeHandle typeHandle) @@ -2193,6 +2194,75 @@ private TargetPointer GetFieldDescStaticOrThreadStaticAddress(TargetPointer fiel TargetPointer IRuntimeTypeSystem.GetFieldDescThreadStaticAddress(TargetPointer fieldDescPointer, TargetPointer thread, bool unboxValueTypes) => GetFieldDescStaticOrThreadStaticAddress(fieldDescPointer, thread, unboxValueTypes); + IEnumerable IRuntimeTypeSystem.EnumerateInstanceFieldDescs(TypeHandle typeHandle) + { + if (!typeHandle.IsMethodTable()) + yield break; + + TargetPointer firstFieldDesc = ((IRuntimeTypeSystem)this).GetFieldDescList(typeHandle); + if (firstFieldDesc == TargetPointer.Null) + yield break; + + IRuntimeTypeSystem rts = this; + ushort numInstanceFields = rts.GetNumInstanceFields(typeHandle); + if (numInstanceFields == 0) + yield break; + + // Statics are intermixed with instance fields in the FieldDesc array, so + // we iterate the whole array and filter. Total = instance + static + thread-static. + ushort totalFields = (ushort)(numInstanceFields + + rts.GetNumStaticFields(typeHandle) + + rts.GetNumThreadStaticFields(typeHandle)); + + uint fieldDescSize = (uint)_target.GetTypeInfo(DataType.FieldDesc).Size!; + + ushort seenInstance = 0; + for (ushort i = 0; i < totalFields && seenInstance < numInstanceFields; i++) + { + TargetPointer fdPtr = new(firstFieldDesc.Value + i * fieldDescSize); + if (rts.IsFieldDescStatic(fdPtr)) + continue; + seenInstance++; + yield return fdPtr; + } + } + + TypeHandle IRuntimeTypeSystem.LookupApproxFieldTypeHandle(TargetPointer fieldDescPointer) + { + if (fieldDescPointer == TargetPointer.Null) + return default; + + IRuntimeTypeSystem rts = this; + uint token = rts.GetFieldDescMemberDef(fieldDescPointer); + EntityHandle entityHandle = MetadataTokens.EntityHandle((int)token); + if (entityHandle.IsNil || entityHandle.Kind != HandleKind.FieldDefinition) + return default; + + TargetPointer enclosingMT = rts.GetMTOfEnclosingClass(fieldDescPointer); + TypeHandle ctx = rts.GetTypeHandle(enclosingMT); + if (!ctx.IsMethodTable()) + return default; + + TargetPointer modulePtr = rts.GetModule(ctx); + if (modulePtr == TargetPointer.Null) + return default; + + ModuleHandle moduleHandle = _target.Contracts.Loader.GetModuleHandleFromModulePtr(modulePtr); + MetadataReader? mdReader = _target.Contracts.EcmaMetadata.GetMetadata(moduleHandle); + if (mdReader is null) + return default; + + FieldDefinition fieldDef = mdReader.GetFieldDefinition((FieldDefinitionHandle)entityHandle); + try + { + return _target.Contracts.Signature.DecodeFieldSignature(fieldDef.Signature, moduleHandle, ctx); + } + catch + { + return default; + } + } + void IRuntimeTypeSystem.GetCoreLibFieldDescAndDef(string @namespace, string typeName, string fieldName, out TargetPointer fieldDescAddr, out FieldDefinition fieldDef) { ILoader loader = _target.Contracts.Loader; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcRefEnumeration.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcRefEnumeration.cs new file mode 100644 index 00000000000000..175ffe882d3b08 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcRefEnumeration.cs @@ -0,0 +1,158 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.DataContractReader.RuntimeTypeSystemHelpers; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; + +/// +/// Helpers for enumerating embedded managed references inside an unboxed value-type +/// instance. Mirrors native ReportPointersFromValueType +/// (src/coreclr/vm/siginfo.cpp): a CGCDesc series walk with the boxed-to-unboxed +/// offset adjustment subtracted out. +/// +/// +/// Callers are responsible for ensuring is an ordinary +/// (non-ByRefLike) value type when calling . +/// ByRefLike types (Span<T>, ref structs) can carry byref-typed fields at +/// arbitrary offsets which the CGCDesc series does not describe; use +/// for those. +/// +internal static class GcRefEnumeration +{ + /// + /// Yields the address of every managed reference embedded inside an unboxed + /// value-type instance located at . + /// + /// Runtime type system contract, used to read the CGCDesc series. + /// The value type whose layout describes the unboxed instance. + /// Address of the start of the unboxed instance (the field area). + /// Target pointer size in bytes (4 or 8). + /// + /// + /// returns offsets measured from the start + /// of a boxed object (i.e. including the MethodTable* prefix). For an + /// unboxed instance the same field sits bytes earlier, so + /// we subtract from each series offset. This matches the + /// native adjustment in ReportPointersFromValueType. + /// + /// + /// References are emitted in series order (i.e. the order the runtime stored them in + /// the GCDesc); no deduplication or sorting is performed. numComponents is fixed + /// at 0 because value-type arguments are never arrays. + /// + /// + public static IEnumerable EnumerateValueTypeRefs( + IRuntimeTypeSystem rts, + TypeHandle valueType, + TargetPointer baseAddress, + int pointerSize) + { + foreach ((uint seriesOffset, uint seriesSize) in rts.GetGCDescSeries(valueType, numComponents: 0)) + { + // Convert the boxed offset (relative to the MethodTable* slot) to an unboxed + // offset (relative to the field area). Equivalent to + // cur->GetSeriesOffset() - TARGET_POINTER_SIZE + // in native ReportPointersFromValueType. + ulong unboxedOffset = seriesOffset - (ulong)pointerSize; + ulong refCount = seriesSize / (ulong)pointerSize; + for (ulong i = 0; i < refCount; i++) + { + yield return new TargetPointer(baseAddress.Value + unboxedOffset + i * (ulong)pointerSize); + } + } + } + + /// + /// Yields the GC roots embedded inside an unboxed ByRefLike value-type instance + /// (a Span<T>, ReadOnlySpan<T>, or other ref struct) + /// located at . + /// + /// Runtime type system contract, used to walk the type's fields. + /// The ByRefLike value type whose layout describes the instance. + /// Address of the start of the unboxed instance (the field area). + /// Target pointer size in bytes (4 or 8). Used for nested non-ByRefLike value-type recursion. + /// + /// + /// Mirrors native MetaSig::ReportPointersFromValueTypeArg / + /// ByRefPointerOffsetsReporter in siginfo.cpp: ByRefLike types have + /// no usable CGCDesc series (interior byrefs are not encoded there), so we walk the + /// declared instance fields and emit one root per ref/byref field. Object refs are + /// emitted with ; managed byrefs are emitted with + /// . + /// + /// + /// Nested aggregate fields are handled compositionally: a nested ByRefLike field + /// recurses through this method; a nested non-ByRefLike value-type field delegates + /// to for the standard CGCDesc walk. + /// + /// + /// Primitives, raw pointers (), and function pointers + /// () are not GC-relevant and yield nothing. If a + /// nested value-type field's can't be resolved (e.g. the + /// enclosing module's metadata is unavailable), that field is skipped — matching the + /// native DAC's conservative behavior. + /// + /// + public static IEnumerable<(TargetPointer Address, GcScanFlags Flags)> EnumerateByRefLikeRoots( + IRuntimeTypeSystem rts, + TypeHandle byRefLikeType, + TargetPointer baseAddress, + int pointerSize) + { + foreach (TargetPointer fd in rts.EnumerateInstanceFieldDescs(byRefLikeType)) + { + // For instance fields the BigRVA sentinel path is never taken, so passing + // a default FieldDefinition is safe. + uint offset = rts.GetFieldDescOffset(fd, default); + TargetPointer fieldAddr = new(baseAddress.Value + offset); + CorElementType et = rts.GetFieldDescType(fd); + + switch (et) + { + case CorElementType.Class: + case CorElementType.String: + case CorElementType.Object: + case CorElementType.SzArray: + case CorElementType.Array: + yield return (fieldAddr, GcScanFlags.None); + break; + + case CorElementType.Byref: + yield return (fieldAddr, GcScanFlags.GC_CALL_INTERIOR); + break; + + case CorElementType.ValueType: + { + TypeHandle inner = rts.LookupApproxFieldTypeHandle(fd); + if (!inner.IsMethodTable()) + break; + + if (rts.IsByRefLike(inner)) + { + foreach ((TargetPointer addr, GcScanFlags flags) in + EnumerateByRefLikeRoots(rts, inner, fieldAddr, pointerSize)) + { + yield return (addr, flags); + } + } + else + { + foreach (TargetPointer refAddr in + EnumerateValueTypeRefs(rts, inner, fieldAddr, pointerSize)) + { + yield return (refAddr, GcScanFlags.None); + } + } + break; + } + + // Primitives, raw pointers, function pointers, etc. carry no GC refs. + default: + break; + } + } + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs index f3bd59ec4d8d49..3f73711d041340 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 @@ -346,28 +346,96 @@ private void PromoteCallerStack( foreach (ArgLayout arg in layout.Arguments) { - foreach (ArgSlot slot in arg.Slots) + ReportArgument(arg, transitionBlock, rts, _target.PointerSize, scanContext); + } + } + + /// + /// Reports GC references for a single argument from a transition frame's caller-stack + /// arguments. Mirrors the per-argument dispatch in native + /// MetaSig::GcScanRoots (src/coreclr/vm/siginfo.cpp): + /// + /// Reference slots are reported directly. + /// Byref slots are reported as interior pointers. + /// Value-type / TypedByRef slots passed by implicit reference are + /// reported as a single interior pointer (the caller-allocated buffer). + /// Value-type slots passed by value with a known + /// have their embedded refs enumerated: + /// ordinary value types via a CGCDesc walk (mirroring + /// ReportPointersFromValueType), ByRefLike types (Span<T>, + /// ref structs) via a field-by-field walk that also reports managed byref + /// fields as (mirroring + /// ByRefPointerOffsetsReporter). + /// + /// + /// + /// When the per-arch iterator GC-decomposed an argument (SysV split structs, ARM64 + /// HFAs), each slot already carries a GC-typed + /// (Class, Byref, etc.) and is + /// null; the per-slot loop handles those cases. The layout-driven walks only fire + /// when the storage is contiguous and undecomposed, indicated by a non-null + /// . + /// + internal static void ReportArgument( + ArgLayout arg, + TargetPointer transitionBlock, + IRuntimeTypeSystem rts, + int pointerSize, + GcScanContext scanContext) + { + // By-value, undecomposed value type with a resolved MethodTable: enumerate + // embedded GC refs. The Phase 1 producer (CallingConvention_1.ComputeValueTypeHandle) + // guarantees the storage is contiguous starting at arg.Slots[0].Offset. The walk + // strategy depends on the type: + // - Ordinary value type: CGCDesc series walk (covers object refs only). + // - ByRefLike (Span, ref structs): field-by-field walk that also reports + // managed byref fields as GC_CALL_INTERIOR (the CGCDesc series omits byrefs). + if (arg.ValueTypeHandle is { } valueTypeHandle && arg.Slots.Count > 0) + { + TargetPointer baseAddress = new(transitionBlock.Value + (ulong)arg.Slots[0].Offset); + if (rts.IsByRefLike(valueTypeHandle)) { - TargetPointer slotAddress = new(transitionBlock.Value + (ulong)slot.Offset); - switch (GcTypeKindClassifier.GetGcKind(slot.ElementType)) + foreach ((TargetPointer addr, GcScanFlags flags) in + GcRefEnumeration.EnumerateByRefLikeRoots(rts, valueTypeHandle, baseAddress, pointerSize)) { - case GcTypeKind.Ref: - scanContext.GCReportCallback(slotAddress, GcScanFlags.None); - break; - case GcTypeKind.Interior: - scanContext.GCReportCallback(slotAddress, GcScanFlags.GC_CALL_INTERIOR); - break; - case GcTypeKind.Other: - if (arg.IsPassedByRef) - { - scanContext.GCReportCallback(slotAddress, GcScanFlags.GC_CALL_INTERIOR); - } - // TODO: For value types passed by value, enumerate fields for embedded GC refs - break; - case GcTypeKind.None: - break; + scanContext.GCReportCallback(addr, flags); + } + } + else + { + foreach (TargetPointer refAddr in GcRefEnumeration.EnumerateValueTypeRefs( + rts, valueTypeHandle, baseAddress, pointerSize)) + { + scanContext.GCReportCallback(refAddr, GcScanFlags.None); } } + return; + } + + foreach (ArgSlot slot in arg.Slots) + { + TargetPointer slotAddress = new(transitionBlock.Value + (ulong)slot.Offset); + switch (GcTypeKindClassifier.GetGcKind(slot.ElementType)) + { + case GcTypeKind.Ref: + scanContext.GCReportCallback(slotAddress, GcScanFlags.None); + break; + case GcTypeKind.Interior: + scanContext.GCReportCallback(slotAddress, GcScanFlags.GC_CALL_INTERIOR); + break; + case GcTypeKind.Other: + if (arg.IsPassedByRef) + { + scanContext.GCReportCallback(slotAddress, GcScanFlags.GC_CALL_INTERIOR); + } + // else: the per-arch iterator GC-decomposed the storage so each + // ref-bearing slot already carries Class/Byref ElementType handled + // above, OR ValueTypeHandle would have been populated and handled + // by the CGCDesc-walk branch at the top of this method. + break; + case GcTypeKind.None: + break; + } } } 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 dd65392056b1d3..8b1bc378f01194 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/MethodTableFlags_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/MethodTableFlags_1.cs @@ -25,6 +25,7 @@ internal enum WFLAGS_LOW : uint GenericsMask_NonGeneric = 0x00000000, // no instantiation GenericsMask_TypicalInstantiation = 0x00000030, // the type instantiated at its formal parameters, e.g. List IsHFA = 0x00000800, + IsByRefLike = 0x00001000, StringArrayValues = GenericsMask_NonGeneric | @@ -107,6 +108,7 @@ private bool TestFlagWithMask(WFLAGS2_ENUM mask, WFLAGS2_ENUM flag) public bool ContainsGCPointers => GetFlag(WFLAGS_HIGH.ContainsGCPointers) != 0; public bool RequiresAlign8 => GetFlag(WFLAGS_HIGH.RequiresAlign8) != 0; public bool IsHFA => TestFlagWithMask(WFLAGS_LOW.IsHFA, WFLAGS_LOW.IsHFA); + public bool IsByRefLike => TestFlagWithMask(WFLAGS_LOW.IsByRefLike, WFLAGS_LOW.IsByRefLike); public bool IsCollectible => GetFlag(WFLAGS_HIGH.Collectible) != 0; public bool IsTrackedReferenceWithFinalizer => GetFlag(WFLAGS_HIGH.IsTrackedReferenceWithFinalizer) != 0; public bool IsDynamicStatics => GetFlag(WFLAGS2_ENUM.DynamicStatics) != 0; diff --git a/src/native/managed/cdac/tests/CallingConvention/AMD64UnixCallingConventionTests.cs b/src/native/managed/cdac/tests/CallingConvention/AMD64UnixCallingConventionTests.cs index bd43f62ea4b0d3..80417748c46139 100644 --- a/src/native/managed/cdac/tests/CallingConvention/AMD64UnixCallingConventionTests.cs +++ b/src/native/managed/cdac/tests/CallingConvention/AMD64UnixCallingConventionTests.cs @@ -775,4 +775,75 @@ public void VarArgs_ReturnsLayout_OnUnixTarget() } #pragma warning restore xUnit1004 + + // ----- ValueTypeHandle population for by-value value-type args ----- + // + // AMD64-Unix (SysV) pre-decomposes by-value structs into GC-typed eightbytes + // (I8/Class/Byref/R8) when they're passed in registers or split between + // regs+stack. For those cases ValueTypeHandle MUST be null (the existing + // per-slot ElementType already gives the GC scanner everything it needs; + // walking GCDesc would duplicate or contradict the eightbyte classification). + // + // ValueTypeHandle is only populated when the iterator hands back a single + // ValueType-typed slot, which happens when the struct is too large for + // enregistration and lands entirely on the stack as a contiguous buffer. + + [Fact] + public void ValueTypeByValue_LargeOnStack_PopulatesValueTypeHandle() + { + // 32-byte struct > 16 -> ineligible for enregistration -> passed by value + // on the stack as a contiguous buffer. The iterator emits a single + // ValueType-typed slot; ValueTypeHandle carries the MT for GCDesc walk. + ulong expectedMtAddress = 0; + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "FourLongs", structSize: 32, + fields: + [ + new(0, CorElementType.I8), + new(8, CorElementType.I8), + new(16, CorElementType.I8), + new(24, CorElementType.I8), + ]); + expectedMtAddress = mt.Address; + sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.False(arg.IsPassedByRef); + Assert.NotNull(arg.ValueTypeHandle); + Assert.Equal(expectedMtAddress, (ulong)arg.ValueTypeHandle.Value.Address); + } + + [Fact] + public void ValueTypeByValue_SysVDecomposed_DoesNotPopulateValueTypeHandle() + { + // { object o; double d; } -> SysV-classified into two eightbytes + // (IntegerReference, SSE). The iterator emits one ArgSlot with + // ElementType=Class and another with ElementType=R8. Because the per-slot + // ElementType already encodes GC refs precisely, ValueTypeHandle must be + // null -- the GCDesc walk would otherwise double-report or contradict. + // Pins the "all slots ValueType" discriminator in ComputeValueTypeHandle. + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "ObjectAndDouble2", structSize: 16, + fields: [new(0, CorElementType.Object), new(8, CorElementType.R8)]); + sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.False(arg.IsPassedByRef); + Assert.Equal(2, arg.Slots.Count); + Assert.Null(arg.ValueTypeHandle); + } } diff --git a/src/native/managed/cdac/tests/CallingConvention/AMD64WindowsCallingConventionTests.cs b/src/native/managed/cdac/tests/CallingConvention/AMD64WindowsCallingConventionTests.cs index 68a1f8d21bf174..3af13586462c96 100644 --- a/src/native/managed/cdac/tests/CallingConvention/AMD64WindowsCallingConventionTests.cs +++ b/src/native/managed/cdac/tests/CallingConvention/AMD64WindowsCallingConventionTests.cs @@ -614,4 +614,113 @@ public void TypedReference_ImplicitByref_OneSlot() Assert.Single(arg.Slots); Assert.Equal(OffsetOfFirstGPArg, arg.Slots[0].Offset); } + + // ----- ValueTypeHandle population for by-value value-type args ----- + // + // The GC scanner needs the value-type's TypeHandle to walk the GCDesc and + // report embedded managed references inside structs that the iterator did + // NOT pre-decompose into GC-typed ArgSlots. On AMD64-Windows, enregistered + // and stack-passed value-type args go through the GP path without per-slot + // GC typing, so ValueTypeHandle must be populated. + + [Fact] + public void ValueTypeByValue_Enregistered_PopulatesValueTypeHandle() + { + // 8-byte struct -> fits in RCX, NOT byref. ValueTypeHandle should point + // to the struct's MethodTable so GC scanner can walk its GCDesc. + ulong expectedMtAddress = 0; + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "ObjectAndPad", structSize: 8, + fields: [new(0, CorElementType.Object)]); + expectedMtAddress = mt.Address; + sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.False(arg.IsPassedByRef); + Assert.NotNull(arg.ValueTypeHandle); + Assert.Equal(expectedMtAddress, (ulong)arg.ValueTypeHandle.Value.Address); + } + + [Fact] + public void ValueTypeByValue_OnStack_PopulatesValueTypeHandle() + { + // 8-byte enregisterable struct as the 5th arg lands on the stack + // (RCX/RDX/R8/R9 consumed by 4 prior I8s) -- still by value, MT populated. + ulong expectedMtAddress = 0; + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "ObjectAndPad", structSize: 8, + fields: [new(0, CorElementType.Object)]); + expectedMtAddress = mt.Address; + sig.Return(CorElementType.Void); + for (int i = 0; i < 4; i++) sig.Param(CorElementType.I8); + sig.ParamValueType(new TargetPointer(mt.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Equal(5, layout.Arguments.Count); + ArgLayout structArg = layout.Arguments[4]; + Assert.False(structArg.IsPassedByRef); + Assert.NotNull(structArg.ValueTypeHandle); + Assert.Equal(expectedMtAddress, (ulong)structArg.ValueTypeHandle.Value.Address); + } + + [Fact] + public void ValueTypeByRef_DoesNotPopulateValueTypeHandle() + { + // 9-byte struct -> implicit byref. The slot carries a pointer, not the + // value bytes; GC scanner reports it via the byref path, not GCDesc walk. + // Regression guard: pins the IsByRef short-circuit in ComputeValueTypeHandle. + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "NineBytes", structSize: 9, + fields: [new(0, CorElementType.I8), new(8, CorElementType.I1)]); + sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.True(layout.Arguments[0].IsPassedByRef); + Assert.Null(layout.Arguments[0].ValueTypeHandle); + } + + [Fact] + public void ValueTypeByValue_ByRefLike_PopulatesValueTypeHandle() + { + // ByRefLike value-type args (Span, ref structs) are surfaced through + // ValueTypeHandle just like ordinary value types. The GC scanner queries + // IRuntimeTypeSystem.IsByRefLike to choose its walk strategy: a CGCDesc + // walk for ordinary types, or a field-by-field walk for ByRefLike types + // (the CGCDesc series doesn't encode managed byref fields). + const uint IsByRefLikeFlag = 0x00001000; + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "ByRefLikeStruct", structSize: 8, + fields: [new(0, CorElementType.I8)]); + mt.MTFlags |= IsByRefLikeFlag; + sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.False(arg.IsPassedByRef); + Assert.Single(arg.Slots); + Assert.NotNull(arg.ValueTypeHandle); + } } diff --git a/src/native/managed/cdac/tests/CallingConvention/Arm64CallingConventionTests.cs b/src/native/managed/cdac/tests/CallingConvention/Arm64CallingConventionTests.cs index bf086f31de8ebf..807bfa184789ac 100644 --- a/src/native/managed/cdac/tests/CallingConvention/Arm64CallingConventionTests.cs +++ b/src/native/managed/cdac/tests/CallingConvention/Arm64CallingConventionTests.cs @@ -599,4 +599,58 @@ public void HFA_FourFloats_ShouldReportFourFPSlots() } #pragma warning restore xUnit1004 + + // ----- ValueTypeHandle population for by-value value-type args ----- + // + // On ARM64, non-HFA value-type args of size <= 16 bytes go through the GP + // path as 1 or 2 ValueType-typed slots; > 16 bytes are passed by reference. + // HFAs go through the FP path and the iterator emits per-FP-reg slots with + // ElementType R4/R8 -- those are already GC-classified (no refs) and should + // NOT carry a ValueTypeHandle. + + [Fact] + public void ValueTypeByValue_NonHFA_EnregisteredInTwoGPRegs_PopulatesValueTypeHandle() + { + // 16-byte struct (two longs) -> X0 + X1 contiguously, two ValueType slots. + ulong expectedMtAddress = 0; + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( + rts, "TwoLongs16", structSize: 16, + fields: [new(0, CorElementType.I8), new(8, CorElementType.I8)]); + expectedMtAddress = mt.Address; + sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.False(arg.IsPassedByRef); + Assert.NotNull(arg.ValueTypeHandle); + Assert.Equal(expectedMtAddress, (ulong)arg.ValueTypeHandle.Value.Address); + } + + [Fact] + public void ValueTypeByValue_HFA_DoesNotPopulateValueTypeHandle() + { + // 4-double HFA -> V0-V3 with per-slot ElementType R8. HFAs cannot carry + // managed refs (they're pure FP), so ValueTypeHandle must be null. + // Pins the "all slots ValueType" discriminator in ComputeValueTypeHandle. + var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( + Case, hasThis: false, + (rts, sig) => + { + MockMethodTable hfa = AddHFA(rts, CorElementType.R8, 4, "HFA_4D_Handle"); + sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(hfa.Address)); + }); + + CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.False(arg.IsPassedByRef); + Assert.Equal(4, arg.Slots.Count); + Assert.Null(arg.ValueTypeHandle); + } } diff --git a/src/native/managed/cdac/tests/DumpTests/CallSiteLayoutDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/CallSiteLayoutDumpTests.cs new file mode 100644 index 00000000000000..bd7f9f32e5caa1 --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/CallSiteLayoutDumpTests.cs @@ -0,0 +1,508 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.DumpTests; + +/// +/// Dump-based integration tests for . +/// Validates the calling-convention layout produced for a range of signature +/// shapes (varargs, byref, by-value structs, ABI-byref structs, managed +/// objects, ByRefLike) against real method metadata in a dump. +/// +/// Scoped to Windows x64. Other platforms are skipped via attributes; the +/// debuggee is also marked WindowsOnly. +/// +public class CallSiteLayoutDumpTests : DumpTestBase +{ + protected override string DebuggeeName => "CallSiteLayout"; + protected override string DumpType => "full"; + + private static readonly string[] s_chainMethods = + [ + "M_Varargs", + "M_RefInt", + "M_OutObject", + "M_RefGuid", + "M_SmallStructWithRef", + "M_TwoInts", + "M_Guid", + "M_KvpStringString", + "M_String", + "M_Object", + "M_IntArray", + "M_SpanInt", + "M_TinyRefStruct", + "M_OneByteStruct", + "M_TwelveByteValueTuple", + "M_DecimalArg", + "M_ManyInts", + "M_MixedIntDouble", + "M_KvpStringInt", + "M_KvpIntKvpIntInt", + "M_EnumByte", + "M_InstanceIntInt", + ]; + + /// + /// Walks the FailFast thread once and resolves every chain method's MethodDescHandle. + /// + private Dictionary CollectChainMethods() + { + ThreadData thread = DumpTestHelpers.FindThreadWithMethod(Target, "M_TinyRefStruct"); + IStackWalk stackWalk = Target.Contracts.StackWalk; + IRuntimeTypeSystem rts = Target.Contracts.RuntimeTypeSystem; + + HashSet wanted = new(s_chainMethods); + Dictionary result = new(); + + foreach (IStackDataFrameHandle frame in stackWalk.CreateStackWalk(thread)) + { + TargetPointer mdPtr = stackWalk.GetMethodDescPtr(frame); + if (mdPtr == TargetPointer.Null) + continue; + MethodDescHandle md = rts.GetMethodDescHandle(mdPtr); + string? name = DumpTestHelpers.GetMethodName(Target, md); + if (name is not null && wanted.Contains(name) && !result.ContainsKey(name)) + result[name] = md; + } + + return result; + } + + private CallSiteLayout LayoutFor(string methodName) + { + Dictionary methods = CollectChainMethods(); + Assert.True(methods.TryGetValue(methodName, out MethodDescHandle md), + $"'{methodName}' frame not found on the FailFast thread"); + return Target.Contracts.CallingConvention.ComputeCallSiteLayout(md); + } + + // ===== varargs ===== + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + public void Varargs_HasVarArgCookieAndFixedArg(TestConfiguration config) + { + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_Varargs"); + + Assert.NotNull(layout.VarArgCookieOffset); + // Only the fixed `int fixedArg` is in Arguments; vararg slots are not enumerated here. + Assert.Single(layout.Arguments); + ArgLayout fixedArg = layout.Arguments[0]; + Assert.False(fixedArg.IsPassedByRef); + Assert.Null(fixedArg.ValueTypeHandle); + } + + // ===== byref ===== + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + public void RefInt_IsByRefNoValueTypeHandle(TestConfiguration config) + { + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_RefInt"); + + Assert.Null(layout.VarArgCookieOffset); + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.True(arg.IsPassedByRef); + Assert.Null(arg.ValueTypeHandle); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + public void OutObject_IsByRefNoValueTypeHandle(TestConfiguration config) + { + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_OutObject"); + + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.True(arg.IsPassedByRef); + Assert.Null(arg.ValueTypeHandle); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + public void RefGuid_IsByRefNoValueTypeHandle(TestConfiguration config) + { + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_RefGuid"); + + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.True(arg.IsPassedByRef); + Assert.Null(arg.ValueTypeHandle); + } + + // ===== structs (by-value) ===== + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + public void SmallStructWithRef_PopulatesValueTypeHandle(TestConfiguration config) + { + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_SmallStructWithRef"); + + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.False(arg.IsPassedByRef); + Assert.NotNull(arg.ValueTypeHandle); + Assert.Equal("StructWithRef", DumpTestHelpers.GetTypeName(Target, arg.ValueTypeHandle.Value)); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + public void TwoInts_PopulatesValueTypeHandle(TestConfiguration config) + { + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_TwoInts"); + + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.False(arg.IsPassedByRef); + Assert.NotNull(arg.ValueTypeHandle); + Assert.Equal("TwoInts", DumpTestHelpers.GetTypeName(Target, arg.ValueTypeHandle.Value)); + } + + // ===== struct > 8 bytes -> ABI byref ===== + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + public void Guid_AbiByRefNoValueTypeHandle(TestConfiguration config) + { + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_Guid"); + + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + // Guid is 16 bytes -> implicit-byref on Win-x64. + Assert.True(arg.IsPassedByRef); + Assert.Null(arg.ValueTypeHandle); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + public void KvpStringString_AbiByRefNoValueTypeHandle(TestConfiguration config) + { + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_KvpStringString"); + + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.True(arg.IsPassedByRef); + Assert.Null(arg.ValueTypeHandle); + } + + // ===== managed objects ===== + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + public void StringArg_NoValueTypeHandle(TestConfiguration config) + { + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_String"); + + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.False(arg.IsPassedByRef); + Assert.Null(arg.ValueTypeHandle); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + public void ObjectArg_NoValueTypeHandle(TestConfiguration config) + { + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_Object"); + + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.False(arg.IsPassedByRef); + Assert.Null(arg.ValueTypeHandle); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + public void IntArray_NoValueTypeHandle(TestConfiguration config) + { + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_IntArray"); + + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.False(arg.IsPassedByRef); + Assert.Null(arg.ValueTypeHandle); + } + + // ===== ByRefLike ===== + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + public void SpanInt_AbiByRefNoValueTypeHandle(TestConfiguration config) + { + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_SpanInt"); + + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + // Span is 16 bytes -> implicit-byref on Win-x64. Rule 1 (IsByRef) + // nulls ValueTypeHandle before the ByRefLike check has a chance to fire. + Assert.True(arg.IsPassedByRef); + Assert.Null(arg.ValueTypeHandle); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + public void TinyRefStruct_ByRefLikeGuardNullsValueTypeHandle(TestConfiguration config) + { + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_TinyRefStruct"); + + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + // 8-byte ref struct -> passed by value, but ByRefLike guard nulls + // ValueTypeHandle because GCDesc walk alone can't report its ref field. + Assert.False(arg.IsPassedByRef); + Assert.Null(arg.ValueTypeHandle); + } + + // ===== size-rule matrix ===== + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + public void OneByteStruct_EnregisteredByValue(TestConfiguration config) + { + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_OneByteStruct"); + + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + // 1-byte struct -> enregistered by value on Win-x64. + Assert.False(arg.IsPassedByRef); + Assert.NotNull(arg.ValueTypeHandle); + Assert.Equal("OneByteStruct", DumpTestHelpers.GetTypeName(Target, arg.ValueTypeHandle.Value)); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + public void TwelveByteValueTuple_AbiByRefNoValueTypeHandle(TestConfiguration config) + { + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_TwelveByteValueTuple"); + + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + // ValueTuple is 12 bytes -- not a power of two, + // so Win-x64 passes it by implicit reference. + Assert.True(arg.IsPassedByRef); + Assert.Null(arg.ValueTypeHandle); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + public void DecimalArg_AbiByRefNoValueTypeHandle(TestConfiguration config) + { + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_DecimalArg"); + + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + // decimal is 16 bytes -> ABI byref on Win-x64. + Assert.True(arg.IsPassedByRef); + Assert.Null(arg.ValueTypeHandle); + } + + // ===== register / stack slot mechanics ===== + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + public void ManyInts_SpillsLastArgsToStack(TestConfiguration config) + { + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_ManyInts"); + + Assert.Equal(6, layout.Arguments.Count); + // Win-x64 has 4 argument registers (RCX/RDX/R8/R9); the 5th and 6th args + // spill onto the stack. Slot offsets are monotonically increasing. + int prevOffset = int.MinValue; + for (int i = 0; i < layout.Arguments.Count; i++) + { + ArgLayout arg = layout.Arguments[i]; + Assert.False(arg.IsPassedByRef); + Assert.Null(arg.ValueTypeHandle); + Assert.Single(arg.Slots); + Assert.True(arg.Slots[0].Offset > prevOffset, + $"Arg {i} offset {arg.Slots[0].Offset} not greater than previous {prevOffset}"); + prevOffset = arg.Slots[0].Offset; + } + // The last two slot offsets must be at least 8 bytes apart from the + // 4th -- confirming they live past the 4-register window. + Assert.True(layout.Arguments[4].Slots[0].Offset >= layout.Arguments[3].Slots[0].Offset + 8); + Assert.True(layout.Arguments[5].Slots[0].Offset >= layout.Arguments[4].Slots[0].Offset + 8); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + public void MixedIntDouble_FloatArgsUseFpRegisterSlots(TestConfiguration config) + { + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_MixedIntDouble"); + + Assert.Equal(4, layout.Arguments.Count); + // (int, double, int, double): the doubles should surface as R8 slots + // (XMM register lanes), the ints as integer-typed slots. + Assert.Equal(CorElementType.I4, layout.Arguments[0].Slots[0].ElementType); + Assert.Equal(CorElementType.R8, layout.Arguments[1].Slots[0].ElementType); + Assert.Equal(CorElementType.I4, layout.Arguments[2].Slots[0].ElementType); + Assert.Equal(CorElementType.R8, layout.Arguments[3].Slots[0].ElementType); + foreach (ArgLayout arg in layout.Arguments) + { + Assert.False(arg.IsPassedByRef); + Assert.Null(arg.ValueTypeHandle); + } + } + + // ===== generics: heterogeneous + nested ===== + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + public void KvpStringInt_AbiByRefNoValueTypeHandle(TestConfiguration config) + { + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_KvpStringInt"); + + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + // KeyValuePair is 16 bytes on x64 (ref + 4 bytes padded to 8). + // The fix to GetGenericInstantiation must resolve both the ref-typed and + // primitive-typed type-args to TypeHandles so the instantiated MT is found. + Assert.True(arg.IsPassedByRef); + Assert.Null(arg.ValueTypeHandle); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + public void KvpIntKvpIntInt_NestedGenericResolvesViaRecursion(TestConfiguration config) + { + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_KvpIntKvpIntInt"); + + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + // KeyValuePair> is 12 bytes (int + 8-byte KVP). + // Both instantiations are inline GENERICINST blobs in the method signature, so + // SRM recurses through ArgTypeInfoSignatureProvider.GetGenericInstantiation at + // every level rather than dispatching to GetTypeFromSpecification. The inner + // KVP resolves to an 8-byte loaded MT, supplying a real TypeHandle as + // the second type arg of the outer instantiation; the outer then resolves to a + // loaded 12-byte MT. + // + // Discriminator: if inner resolution had failed (UnresolvedValueType, no + // TypeHandle), the outer would also fall back to UnresolvedValueType (size 8), + // and 8 is pow2 / <=8 so IsPassedByRef would be FALSE. Asserting IsPassedByRef + // == true proves the recursive GetGenericInstantiation chain succeeded all the + // way through the nested instantiation. + Assert.True(arg.IsPassedByRef); + Assert.Null(arg.ValueTypeHandle); + } + + // ===== enum collapse ===== + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + public void EnumByte_TreatedAsValueType(TestConfiguration config) + { + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_EnumByte"); + + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + // Native MetaSig::NextArgNormalized collapses enums to their underlying + // primitive (here U1). The cDAC iterator currently keeps the enum's + // MT-level classification (ValueType) and attaches its TypeHandle. + // That is *safe* for GC scanning (a byte enum has no embedded refs, + // so the GCDesc walk yields zero refs) but diverges from native. + // Tracking parity with NextArgNormalized's enum collapse is left as a + // TODO; this test pins the current behavior so the divergence is + // visible if/when the iterator is updated. + Assert.False(arg.IsPassedByRef); + Assert.NotNull(arg.ValueTypeHandle); + Assert.Equal("SmallByteEnum", DumpTestHelpers.GetTypeName(Target, arg.ValueTypeHandle.Value)); + } + + // ===== instance method ===== + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + public void InstanceIntInt_PopulatesThisOffset(TestConfiguration config) + { + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_InstanceIntInt"); + + // Instance method on a reference type: ThisOffset is populated and + // IsValueTypeThis is false. The two fixed args follow `this`. + Assert.NotNull(layout.ThisOffset); + Assert.False(layout.IsValueTypeThis); + Assert.Equal(2, layout.Arguments.Count); + foreach (ArgLayout arg in layout.Arguments) + { + Assert.False(arg.IsPassedByRef); + Assert.Null(arg.ValueTypeHandle); + Assert.Equal(CorElementType.I4, arg.Slots[0].ElementType); + } + // `this` occupies the first register slot; the two ints come after. + Assert.True(layout.Arguments[0].Slots[0].Offset > layout.ThisOffset.Value); + } +} + + diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/CallSiteLayout/CallSiteLayout.csproj b/src/native/managed/cdac/tests/DumpTests/Debuggees/CallSiteLayout/CallSiteLayout.csproj new file mode 100644 index 00000000000000..44695cda85ecc4 --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/CallSiteLayout/CallSiteLayout.csproj @@ -0,0 +1,17 @@ + + + + Full + + Jit + + true + + diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/CallSiteLayout/Program.cs b/src/native/managed/cdac/tests/DumpTests/Debuggees/CallSiteLayout/Program.cs new file mode 100644 index 00000000000000..96f35aa00348e3 --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/CallSiteLayout/Program.cs @@ -0,0 +1,276 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +/// +/// Debuggee for cDAC CallSiteLayoutDumpTests. Builds a deep call chain +/// where every frame remains live on the stack at . +/// Each method exercises a different signature shape so the test can assert +/// ICallingConvention.ComputeCallSiteLayout produces the expected +/// ArgLayout per category: +/// varargs, byref, structs (by-value & ABI-byref), managed objects, ByRefLike. +/// +/// All methods are so each frame is +/// independently discoverable by name in the dump. +/// +internal static class Program +{ + private static void Main() + { + M_Varargs(1, __arglist(42, 43)); + } + + // ----- varargs ----- + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_Varargs(int fixedArg, __arglist) + { + GC.KeepAlive(fixedArg); + int local = 7; + M_RefInt(ref local); + } + + // ----- byref ----- + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_RefInt(ref int x) + { + x++; + M_OutObject(out object o); + GC.KeepAlive(o); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_OutObject(out object o) + { + o = new object(); + Guid g = new Guid("11111111-2222-3333-4444-555555555555"); + M_RefGuid(ref g); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_RefGuid(ref Guid g) + { + GC.KeepAlive(g); + StructWithRef s = new StructWithRef { Ref = "cDAC-CallSiteLayout-StringRef" }; + M_SmallStructWithRef(s); + } + + // ----- structs (by-value) ----- + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_SmallStructWithRef(StructWithRef s) + { + GC.KeepAlive(s.Ref); + TwoInts p = new TwoInts { X = 1, Y = 2 }; + M_TwoInts(p); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_TwoInts(TwoInts p) + { + GC.KeepAlive(p.X + p.Y); + Guid g = new Guid("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"); + M_Guid(g); + } + + // ----- struct >8 bytes -> ABI byref ----- + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_Guid(Guid g) + { + GC.KeepAlive(g); + KeyValuePair kvp = new KeyValuePair("k", "v"); + M_KvpStringString(kvp); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_KvpStringString(KeyValuePair kvp) + { + GC.KeepAlive(kvp.Key); + GC.KeepAlive(kvp.Value); + M_String("cDAC-CallSiteLayout-StringArg"); + } + + // ----- managed objects ----- + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_String(string s) + { + GC.KeepAlive(s); + M_Object(new object()); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_Object(object o) + { + GC.KeepAlive(o); + M_IntArray(_intArrayArg); + } + + private static readonly int[] _intArrayArg = [1, 2, 3]; + private static readonly int[] _spanBuffer = [10, 20, 30]; + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_IntArray(int[] a) + { + GC.KeepAlive(a); + M_SpanInt(_spanBuffer.AsSpan()); + } + + // ----- ByRefLike ----- + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_SpanInt(Span s) + { + s[0]++; + int local = 99; + M_TinyRefStruct(new SmallRefStruct(ref local)); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_TinyRefStruct(SmallRefStruct t) + { + t.Bump(); + OneByteStruct ob = new OneByteStruct { X = 0xAB }; + M_OneByteStruct(ob); + } + + // ----- size-rule matrix: 1-byte struct enregistered by value ----- + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_OneByteStruct(OneByteStruct s) + { + GC.KeepAlive(s.X); + (int, int, int) v = (1, 2, 3); + M_TwelveByteValueTuple(v); + } + + // ----- 12-byte non-pow2 struct -> ABI byref ----- + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_TwelveByteValueTuple((int, int, int) v) + { + GC.KeepAlive(v.Item1 + v.Item2 + v.Item3); + M_DecimalArg(123.456m); + } + + // ----- 16-byte BCL struct -> ABI byref ----- + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_DecimalArg(decimal d) + { + GC.KeepAlive(d); + M_ManyInts(1, 2, 3, 4, 5, 6); + } + + // ----- 6 ints -> last two spill to the stack ----- + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_ManyInts(int a, int b, int c, int d, int e, int f) + { + GC.KeepAlive(a + b + c + d + e + f); + M_MixedIntDouble(7, 1.5, 8, 2.5); + } + + // ----- alternating int/double -> exercises FP register lanes (XMM1, XMM3) ----- + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_MixedIntDouble(int a, double b, int c, double d) + { + GC.KeepAlive(a + c); + GC.KeepAlive(b + d); + KeyValuePair kvp = new KeyValuePair("k2", 9); + M_KvpStringInt(kvp); + } + + // ----- heterogeneous generic type-args (ref + primitive) ----- + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_KvpStringInt(KeyValuePair kvp) + { + GC.KeepAlive(kvp.Key); + GC.KeepAlive(kvp.Value); + KeyValuePair> nested = + new KeyValuePair>(1, new KeyValuePair(2, 3)); + M_KvpIntKvpIntInt(nested); + } + + // ----- nested generic: both levels are inline GENERICINST blobs, resolved + // ----- by recursive GetGenericInstantiation (not via GetTypeFromSpecification). + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_KvpIntKvpIntInt(KeyValuePair> nested) + { + GC.KeepAlive(nested.Key); + GC.KeepAlive(nested.Value.Key + nested.Value.Value); + M_EnumByte(SmallByteEnum.B); + } + + // ----- enum collapses to its underlying primitive (U1) ----- + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_EnumByte(SmallByteEnum e) + { + GC.KeepAlive((int)e); + InstanceCallee callee = new InstanceCallee(); + callee.M_InstanceIntInt(11, 22); + } +} + +internal sealed class InstanceCallee +{ + private int _seed = 7; + + // ----- instance method: layout has a populated ThisOffset ----- + + [MethodImpl(MethodImplOptions.NoInlining)] + public void M_InstanceIntInt(int x, int y) + { + GC.KeepAlive(_seed + x + y); + Environment.FailFast("cDAC dump test: CallSiteLayout intentional crash"); + } +} + +internal struct OneByteStruct +{ + public byte X; +} + +internal enum SmallByteEnum : byte +{ + A = 1, + B = 2, +} + +internal struct StructWithRef +{ + public string Ref; +} + +internal struct TwoInts +{ + public int X; + public int Y; +} + +/// +/// 8-byte ref struct used to exercise the IsByRefLike guard in +/// ComputeValueTypeHandle. A single ref int field keeps the +/// struct pointer-sized so the Win-x64 ABI passes it by value (not as an +/// implicit byref). +/// +internal ref struct SmallRefStruct +{ + public ref int Field; + + public SmallRefStruct(ref int r) + { + Field = ref r; + } + + public void Bump() => Field++; +} diff --git a/src/native/managed/cdac/tests/DumpTests/DumpTestBase.cs b/src/native/managed/cdac/tests/DumpTests/DumpTestBase.cs index 4c0aaa34cb508c..e4284924d97a0a 100644 --- a/src/native/managed/cdac/tests/DumpTests/DumpTestBase.cs +++ b/src/native/managed/cdac/tests/DumpTests/DumpTestBase.cs @@ -190,8 +190,16 @@ private void EvaluateSkipAttributes(TestConfiguration config, string callerName, foreach (SkipOnArchAttribute attr in method.GetCustomAttributes()) { - if (string.Equals(attr.Arch, _dumpInfo.Arch, StringComparison.OrdinalIgnoreCase)) - throw new SkipTestException($"[{_dumpInfo.Arch}] {attr.Reason}"); + if (attr.IncludeOnly is not null) + { + if (!string.Equals(attr.IncludeOnly, _dumpInfo.Arch, StringComparison.OrdinalIgnoreCase)) + throw new SkipTestException($"[{_dumpInfo.Arch}] {attr.Reason}"); + } + else if (attr.Arch is not null) + { + if (string.Equals(attr.Arch, _dumpInfo.Arch, StringComparison.OrdinalIgnoreCase)) + throw new SkipTestException($"[{_dumpInfo.Arch}] {attr.Reason}"); + } } } } diff --git a/src/native/managed/cdac/tests/DumpTests/DumpTestHelpers.cs b/src/native/managed/cdac/tests/DumpTests/DumpTestHelpers.cs index e1d57de304c703..b851ce482d00d8 100644 --- a/src/native/managed/cdac/tests/DumpTests/DumpTestHelpers.cs +++ b/src/native/managed/cdac/tests/DumpTests/DumpTestHelpers.cs @@ -13,6 +13,28 @@ namespace Microsoft.Diagnostics.DataContractReader.DumpTests; /// internal static class DumpTestHelpers { + /// + /// Resolves a to its ECMA-335 type name (no namespace). + /// Returns null if the name cannot be resolved (e.g., missing metadata). + /// + public static string? GetTypeName(ContractDescriptorTarget target, TypeHandle typeHandle) + { + IRuntimeTypeSystem rts = target.Contracts.RuntimeTypeSystem; + uint token = rts.GetTypeDefToken(typeHandle); + TargetPointer modulePtr = rts.GetModule(typeHandle); + + ILoader loader = target.Contracts.Loader; + ModuleHandle moduleHandle = loader.GetModuleHandleFromModulePtr(modulePtr); + + IEcmaMetadata ecmaMetadata = target.Contracts.EcmaMetadata; + MetadataReader? reader = ecmaMetadata.GetMetadata(moduleHandle); + if (reader is null) + return null; + + TypeDefinitionHandle typeDef = MetadataTokens.TypeDefinitionHandle((int)(token & 0x00FFFFFF)); + return reader.GetString(reader.GetTypeDefinition(typeDef).Name); + } + /// /// Resolves the method name for a using the /// RuntimeTypeSystem, Loader, and EcmaMetadata contracts. Returns null diff --git a/src/native/managed/cdac/tests/DumpTests/RunDumpTests.ps1 b/src/native/managed/cdac/tests/DumpTests/RunDumpTests.ps1 index 05a22da4f03d0d..71fb6c993a8ea4 100644 --- a/src/native/managed/cdac/tests/DumpTests/RunDumpTests.ps1 +++ b/src/native/managed/cdac/tests/DumpTests/RunDumpTests.ps1 @@ -93,6 +93,8 @@ param( [string]$TestHostConfiguration = "Release", + [string]$RuntimeConfiguration = "", + [string]$Filter = "", [string]$DumpArchive = "", @@ -327,6 +329,10 @@ if ($Action -in @("dumps", "all")) { "/v:$msbuildVerbosity" ) + if ($RuntimeConfiguration) { + $msbuildArgs += "/p:RuntimeConfiguration=$RuntimeConfiguration" + } + if ($SetSignatureCheck) { $msbuildArgs += "/p:SetDisableAuxProviderSignatureCheck=true" } diff --git a/src/native/managed/cdac/tests/DumpTests/SkipOnArchAttribute.cs b/src/native/managed/cdac/tests/DumpTests/SkipOnArchAttribute.cs index 86a28c2fd27707..938c9230821829 100644 --- a/src/native/managed/cdac/tests/DumpTests/SkipOnArchAttribute.cs +++ b/src/native/managed/cdac/tests/DumpTests/SkipOnArchAttribute.cs @@ -6,20 +6,49 @@ namespace Microsoft.Diagnostics.DataContractReader.DumpTests; /// -/// When applied to a test method, causes the test to be skipped when -/// the dump architecture matches the specified value. Checked by +/// When applied to a test method, controls whether the test runs based on +/// the architecture where the dumps were generated. Checked by /// before each test runs. /// Multiple attributes can be stacked on a single method. +/// +/// There are two modes: +/// +/// Exclude (default): [SkipOnArch("x86", "reason")] — skip when +/// the dump arch matches. +/// Include-only: [SkipOnArch(IncludeOnly = "x64", Reason = "reason")] +/// — skip when the dump arch does not match. +/// /// [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] public sealed class SkipOnArchAttribute : Attribute { - public string Arch { get; } - public string Reason { get; } + /// + /// The arch to exclude. When set, the test is skipped if the dump arch matches. + /// + public string? Arch { get; } + /// + /// When set, the test is skipped if the dump arch does not match this value. + /// Mutually exclusive with . + /// + public string? IncludeOnly { get; set; } + + public string Reason { get; set; } = string.Empty; + + /// + /// Creates an exclude-mode attribute: skip when the dump arch equals . + /// public SkipOnArchAttribute(string arch, string reason) { Arch = arch; Reason = reason; } + + /// + /// Creates an attribute using named properties only (for include-only mode). + /// Usage: [SkipOnArch(IncludeOnly = "x64", Reason = "...")] + /// + public SkipOnArchAttribute() + { + } } diff --git a/src/native/managed/cdac/tests/GcRefEnumerationTests.cs b/src/native/managed/cdac/tests/GcRefEnumerationTests.cs new file mode 100644 index 00000000000000..00f547239bdc7e --- /dev/null +++ b/src/native/managed/cdac/tests/GcRefEnumerationTests.cs @@ -0,0 +1,151 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; +using Moq; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.Tests; + +public class GcRefEnumerationTests +{ + private static IRuntimeTypeSystem MockRtsWithSeries(params (uint Offset, uint Size)[] series) + { + Mock mock = new(MockBehavior.Strict); + mock.Setup(r => r.GetGCDescSeries(It.IsAny(), 0u)) + .Returns(series); + return mock.Object; + } + + private static TypeHandle MakeHandle(ulong address) + => new TypeHandle(new TargetPointer(address)); + + // ===== empty input ===== + + [Fact] + public void EmptySeries_YieldsNoRefs() + { + IRuntimeTypeSystem rts = MockRtsWithSeries(); + TargetPointer[] refs = GcRefEnumeration.EnumerateValueTypeRefs( + rts, MakeHandle(0x1000), new TargetPointer(0x2000), pointerSize: 8).ToArray(); + Assert.Empty(refs); + } + + // ===== single series, one ref ===== + + [Fact] + public void SingleSeriesOneRef_x64_EmitsOneAddressWithBoxedToUnboxedAdjustment() + { + // A struct laid out as { /* MT* at 0..7 */ ; objref at boxed offset 8 } has + // a single GCDesc series of (Offset=8, Size=8) on x64. Unboxed the same field + // sits at offset 0, so the emitted address is exactly baseAddress. + IRuntimeTypeSystem rts = MockRtsWithSeries((Offset: 8, Size: 8)); + TargetPointer[] refs = GcRefEnumeration.EnumerateValueTypeRefs( + rts, MakeHandle(0x1000), new TargetPointer(0x2000), pointerSize: 8).ToArray(); + Assert.Single(refs); + Assert.Equal(0x2000ul, refs[0].Value); + } + + [Fact] + public void SingleSeriesOneRef_x86_EmitsOneAddressWithBoxedToUnboxedAdjustment() + { + // Same shape on a 32-bit target: pointerSize=4 means MT* is 4 bytes, + // boxed offset 4 -> unboxed offset 0. + IRuntimeTypeSystem rts = MockRtsWithSeries((Offset: 4, Size: 4)); + TargetPointer[] refs = GcRefEnumeration.EnumerateValueTypeRefs( + rts, MakeHandle(0x1000), new TargetPointer(0x2000), pointerSize: 4).ToArray(); + Assert.Single(refs); + Assert.Equal(0x2000ul, refs[0].Value); + } + + // ===== single series, multiple adjacent refs ===== + + [Fact] + public void SingleSeriesTwoAdjacentRefs_x64_EmitsTwoConsecutiveAddresses() + { + // A struct with two adjacent objref fields at boxed offsets 8 and 16 produces + // a single series (Offset=8, Size=16). Unboxed offsets are 0 and 8. + IRuntimeTypeSystem rts = MockRtsWithSeries((Offset: 8, Size: 16)); + TargetPointer[] refs = GcRefEnumeration.EnumerateValueTypeRefs( + rts, MakeHandle(0x1000), new TargetPointer(0x2000), pointerSize: 8).ToArray(); + Assert.Equal(2, refs.Length); + Assert.Equal(0x2000ul, refs[0].Value); + Assert.Equal(0x2008ul, refs[1].Value); + } + + [Fact] + public void SingleSeriesFourAdjacentRefs_x86_EmitsFourConsecutiveAddresses() + { + IRuntimeTypeSystem rts = MockRtsWithSeries((Offset: 4, Size: 16)); + TargetPointer[] refs = GcRefEnumeration.EnumerateValueTypeRefs( + rts, MakeHandle(0x1000), new TargetPointer(0x2000), pointerSize: 4).ToArray(); + Assert.Equal(4, refs.Length); + Assert.Equal(0x2000ul, refs[0].Value); + Assert.Equal(0x2004ul, refs[1].Value); + Assert.Equal(0x2008ul, refs[2].Value); + Assert.Equal(0x200Cul, refs[3].Value); + } + + // ===== multiple disjoint series ===== + + [Fact] + public void MultipleDisjointSeries_x64_EmitsAllInSeriesOrder() + { + // Layout: { objref; long; objref } -> two single-ref series at boxed + // offsets 8 and 24 (gap of one 8-byte non-ref field between them). + IRuntimeTypeSystem rts = MockRtsWithSeries( + (Offset: 8, Size: 8), + (Offset: 24, Size: 8)); + TargetPointer[] refs = GcRefEnumeration.EnumerateValueTypeRefs( + rts, MakeHandle(0x1000), new TargetPointer(0x2000), pointerSize: 8).ToArray(); + Assert.Equal(2, refs.Length); + Assert.Equal(0x2000ul, refs[0].Value); // boxed 8 -> unboxed 0 + Assert.Equal(0x2010ul, refs[1].Value); // boxed 24 -> unboxed 16 + } + + [Fact] + public void MixedRunsAndGaps_x64_EmitsCorrectAddresses() + { + // Two series: a 2-ref run at boxed offset 8, then a 1-ref run at boxed offset 40. + IRuntimeTypeSystem rts = MockRtsWithSeries( + (Offset: 8, Size: 16), + (Offset: 40, Size: 8)); + TargetPointer[] refs = GcRefEnumeration.EnumerateValueTypeRefs( + rts, MakeHandle(0x1000), new TargetPointer(0x2000), pointerSize: 8).ToArray(); + Assert.Equal(3, refs.Length); + Assert.Equal(0x2000ul, refs[0].Value); // boxed 8 -> unboxed 0 + Assert.Equal(0x2008ul, refs[1].Value); // boxed 16 -> unboxed 8 + Assert.Equal(0x2020ul, refs[2].Value); // boxed 40 -> unboxed 32 + } + + // ===== base address arithmetic ===== + + [Fact] + public void BaseAddressIsOffsetCorrectly_NonZeroBase() + { + IRuntimeTypeSystem rts = MockRtsWithSeries((Offset: 8, Size: 16)); + TargetPointer[] refs = GcRefEnumeration.EnumerateValueTypeRefs( + rts, MakeHandle(0x1000), new TargetPointer(0xFFFF_FFFF_FFFF_0000ul), pointerSize: 8).ToArray(); + Assert.Equal(2, refs.Length); + Assert.Equal(0xFFFF_FFFF_FFFF_0000ul, refs[0].Value); + Assert.Equal(0xFFFF_FFFF_FFFF_0008ul, refs[1].Value); + } + + // ===== lazy enumeration ===== + + [Fact] + public void DoesNotCallGetGCDescSeriesUntilEnumerated() + { + Mock mock = new(MockBehavior.Strict); + IEnumerable result = GcRefEnumeration.EnumerateValueTypeRefs( + mock.Object, MakeHandle(0x1000), new TargetPointer(0x2000), pointerSize: 8); + // No setup on the mock -> if the helper ate the iterator eagerly the strict mock + // would throw. Materializing now must not throw because we set the call up below. + mock.Setup(r => r.GetGCDescSeries(It.IsAny(), 0u)) + .Returns(System.Array.Empty<(uint, uint)>()); + Assert.Empty(result.ToArray()); + } +} diff --git a/src/native/managed/cdac/tests/GcScannerReportArgumentTests.cs b/src/native/managed/cdac/tests/GcScannerReportArgumentTests.cs new file mode 100644 index 00000000000000..d55d7797fb2b2c --- /dev/null +++ b/src/native/managed/cdac/tests/GcScannerReportArgumentTests.cs @@ -0,0 +1,483 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; +using Moq; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.Tests; + +/// +/// Unit tests for , the per-argument GC reporting +/// dispatch used by GcScanner.PromoteCallerStack. Mirrors native +/// MetaSig::GcScanRoots behavior. +/// +public class GcScannerReportArgumentTests +{ + private const ulong TransitionBlockBase = 0x10_0000; + + private static GcScanContext NewContext(TestPlaceholderTarget target) + { + GcScanContext ctx = new GcScanContext(target, resolveInteriorPointers: false); + ctx.UpdateScanContext(new TargetPointer(0x1000), new TargetPointer(0x2000), new TargetPointer(0x3000)); + return ctx; + } + + private static List<(TargetPointer Address, GcScanFlags Flags)> Run( + ArgLayout arg, + IRuntimeTypeSystem rts, + MockTarget.Architecture arch) + { + // Stub reader: every read succeeds and returns zeros. The scanner reads the + // object pointer from each reported stack slot; we don't care about the value, + // we only verify the slot ADDRESSES the scanner reports. + TestPlaceholderTarget target = new TestPlaceholderTarget.Builder(arch) + .UseReader((ulong _, Span buffer) => + { + buffer.Clear(); + return 0; + }) + .Build(); + GcScanContext ctx = NewContext(target); + GcScanner.ReportArgument(arg, new TargetPointer(TransitionBlockBase), rts, target.PointerSize, ctx); + return ctx.StackRefs.Select(r => (r.Address, r.Flags)).ToList(); + } + + private static IRuntimeTypeSystem EmptyRts() => Mock.Of(); + + private static IRuntimeTypeSystem RtsWithSeries(params (uint Offset, uint Size)[] series) + { + Mock mock = new(MockBehavior.Strict); + mock.Setup(r => r.IsByRefLike(It.IsAny())).Returns(false); + mock.Setup(r => r.GetGCDescSeries(It.IsAny(), 0u)).Returns(series); + return mock.Object; + } + + // ===== reference / interior slots ===== + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void RefSlot_ReportedDirectly(MockTarget.Architecture arch) + { + ArgLayout arg = new(IsPassedByRef: false, Slots: new[] { new ArgSlot(0x20, CorElementType.Class) }); + List<(TargetPointer Address, GcScanFlags Flags)> reports = Run(arg, EmptyRts(), arch); + (TargetPointer addr, GcScanFlags flags) = Assert.Single(reports); + Assert.Equal(TransitionBlockBase + 0x20ul, addr.Value); + Assert.Equal(GcScanFlags.None, flags); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void ByrefSlot_ReportedAsInterior(MockTarget.Architecture arch) + { + ArgLayout arg = new(IsPassedByRef: false, Slots: new[] { new ArgSlot(0x40, CorElementType.Byref) }); + List<(TargetPointer Address, GcScanFlags Flags)> reports = Run(arg, EmptyRts(), arch); + (TargetPointer addr, GcScanFlags flags) = Assert.Single(reports); + Assert.Equal(TransitionBlockBase + 0x40ul, addr.Value); + Assert.Equal(GcScanFlags.GC_CALL_INTERIOR, flags); + } + + // ===== by-implicit-reference value type ===== + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void ValueTypeByRef_ReportedAsInterior(MockTarget.Architecture arch) + { + // Pass-by-implicit-reference structs (e.g. Win-x64 non-power-of-2 structs) carry an + // interior pointer in their single slot; ValueTypeHandle is null per the Phase 1 + // ComputeValueTypeHandle rule. + ArgLayout arg = new(IsPassedByRef: true, Slots: new[] { new ArgSlot(0x10, CorElementType.ValueType) }); + List<(TargetPointer Address, GcScanFlags Flags)> reports = Run(arg, EmptyRts(), arch); + (TargetPointer addr, GcScanFlags flags) = Assert.Single(reports); + Assert.Equal(TransitionBlockBase + 0x10ul, addr.Value); + Assert.Equal(GcScanFlags.GC_CALL_INTERIOR, flags); + } + + // ===== decomposed value type (SysV split / HFA) ===== + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void DecomposedValueType_ReportsPerSlotByElementType(MockTarget.Architecture arch) + { + // SysV-style split: eight-byte 0 is a ref, eight-byte 1 is a non-ref. + // ValueTypeHandle is null (Phase 1 rule: any non-ValueType slot ElementType + // disqualifies the CGCDesc walk because the iterator already did decomposition). + ArgLayout arg = new( + IsPassedByRef: false, + Slots: new[] + { + new ArgSlot(0x10, CorElementType.Class), + new ArgSlot(0x18, CorElementType.I8), + }); + List<(TargetPointer Address, GcScanFlags Flags)> reports = Run(arg, EmptyRts(), arch); + (TargetPointer addr, GcScanFlags flags) = Assert.Single(reports); + Assert.Equal(TransitionBlockBase + 0x10ul, addr.Value); + Assert.Equal(GcScanFlags.None, flags); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void UndecomposedValueTypeWithoutHandle_EmitsNothing(MockTarget.Architecture arch) + { + // A by-value value type that the iterator left undecomposed but for which the + // Phase 1 producer could not resolve a TypeHandle (e.g. an unresolved TypeSpec + // or a layout discontinuity). The scanner cannot safely walk an unknown layout + // so it reports nothing. + ArgLayout arg = new(IsPassedByRef: false, Slots: new[] { new ArgSlot(0x10, CorElementType.ValueType) }); + List<(TargetPointer Address, GcScanFlags Flags)> reports = Run(arg, EmptyRts(), arch); + Assert.Empty(reports); + } + + // ===== by-value value type with CGCDesc walk ===== + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void ByValueValueTypeWithHandle_WalksGcDescAndReportsRefs(MockTarget.Architecture arch) + { + int ps = arch.Is64Bit ? 8 : 4; + // Struct laid out (boxed) as { MT*; objref @ ps; objref @ 2*ps; nonref @ 3*ps }. + // GetGCDescSeries returns one series: (Offset=ps, Size=2*ps) -- two adjacent refs. + // Unboxed offsets are 0 and ps. With slots[0].Offset = 0x50, the expected report + // addresses are TransitionBlockBase + 0x50 and TransitionBlockBase + 0x50 + ps. + IRuntimeTypeSystem rts = RtsWithSeries(((uint)ps, (uint)(ps * 2))); + TypeHandle th = new TypeHandle(new TargetPointer(0xAA_BB_CC_00)); + ArgLayout arg = new( + IsPassedByRef: false, + Slots: new[] { new ArgSlot(0x50, CorElementType.ValueType) }, + ValueTypeHandle: th); + + List<(TargetPointer Address, GcScanFlags Flags)> reports = Run(arg, rts, arch); + + Assert.Equal(2, reports.Count); + Assert.Equal(TransitionBlockBase + 0x50ul, reports[0].Address.Value); + Assert.Equal(GcScanFlags.None, reports[0].Flags); + Assert.Equal(TransitionBlockBase + 0x50ul + (ulong)ps, reports[1].Address.Value); + Assert.Equal(GcScanFlags.None, reports[1].Flags); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void ByValueValueTypeWithHandle_MultiSlotStorage_UsesFirstSlotAsBase(MockTarget.Architecture arch) + { + // ARM64 16-byte non-HFA struct passed in X0+X1: two ValueType slots at + // contiguous offsets. Phase 1 populates ValueTypeHandle because all slots have + // ElementType=ValueType (the iterator did NOT decompose). The CGCDesc walk uses + // slots[0].Offset as the base of the contiguous storage. + int ps = arch.Is64Bit ? 8 : 4; + IRuntimeTypeSystem rts = RtsWithSeries(((uint)ps, (uint)ps)); + TypeHandle th = new TypeHandle(new TargetPointer(0x12_34_56_78)); + ArgLayout arg = new( + IsPassedByRef: false, + Slots: new[] + { + new ArgSlot(0x80, CorElementType.ValueType), + new ArgSlot(0x80 + ps, CorElementType.ValueType), + }, + ValueTypeHandle: th); + + List<(TargetPointer Address, GcScanFlags Flags)> reports = Run(arg, rts, arch); + + (TargetPointer addr, GcScanFlags flags) = Assert.Single(reports); + // unboxed offset = boxed offset - ps = ps - ps = 0, so the ref is at slots[0]. + Assert.Equal(TransitionBlockBase + 0x80ul, addr.Value); + Assert.Equal(GcScanFlags.None, flags); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void ByValueValueTypeWithHandle_NoRefs_EmitsNothing(MockTarget.Architecture arch) + { + // A struct with no managed refs (empty GCDesc series). The walk runs but yields + // zero callbacks. Verifies the helper doesn't emit spurious reports. + IRuntimeTypeSystem rts = RtsWithSeries(System.Array.Empty<(uint, uint)>()); + TypeHandle th = new TypeHandle(new TargetPointer(0xFF_FF_FF_FF)); + ArgLayout arg = new( + IsPassedByRef: false, + Slots: new[] { new ArgSlot(0x60, CorElementType.ValueType) }, + ValueTypeHandle: th); + + List<(TargetPointer Address, GcScanFlags Flags)> reports = Run(arg, rts, arch); + Assert.Empty(reports); + } + + // ===== primitive / other ===== + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void PrimitiveSlot_EmitsNothing(MockTarget.Architecture arch) + { + ArgLayout arg = new(IsPassedByRef: false, Slots: new[] { new ArgSlot(0x70, CorElementType.I4) }); + List<(TargetPointer Address, GcScanFlags Flags)> reports = Run(arg, EmptyRts(), arch); + Assert.Empty(reports); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void EmptySlots_EmitsNothing(MockTarget.Architecture arch) + { + ArgLayout arg = new(IsPassedByRef: false, Slots: System.Array.Empty()); + List<(TargetPointer Address, GcScanFlags Flags)> reports = Run(arg, EmptyRts(), arch); + Assert.Empty(reports); + } + + // ===== priority: ValueTypeHandle wins over per-slot dispatch ===== + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void ValueTypeHandleBranch_TakesPrecedenceOverPerSlotLoop(MockTarget.Architecture arch) + { + // Hypothetical: even if a slot's ElementType were Class, the presence of a + // ValueTypeHandle indicates the producer wants the GCDesc walk to drive reports. + // This documents the dispatch order so future producer/consumer changes preserve + // the invariant. + int ps = arch.Is64Bit ? 8 : 4; + IRuntimeTypeSystem rts = RtsWithSeries(((uint)ps, (uint)ps)); + TypeHandle th = new TypeHandle(new TargetPointer(0xAB_CD)); + ArgLayout arg = new( + IsPassedByRef: false, + Slots: new[] { new ArgSlot(0x90, CorElementType.ValueType) }, + ValueTypeHandle: th); + + List<(TargetPointer Address, GcScanFlags Flags)> reports = Run(arg, rts, arch); + (TargetPointer addr, GcScanFlags flags) = Assert.Single(reports); + Assert.Equal(TransitionBlockBase + 0x90ul, addr.Value); + Assert.Equal(GcScanFlags.None, flags); + } + + // ===== ByRefLike (Span, ref structs): field-walk branch ===== + + /// + /// Synthesizes an mock whose + /// returns true for the supplied + /// , and whose + /// yields synthetic + /// FieldDesc pointers (1, 2, 3, ...) for that type. Each FieldDesc's offset, element + /// type, and (for ValueType fields) inner-type-handle are wired from + /// . + /// + private static IRuntimeTypeSystem RtsForByRefLike( + TypeHandle byRefLikeType, + params (uint Offset, CorElementType Type, TypeHandle? Inner, bool InnerIsByRefLike, (uint Offset, uint Size)[]? InnerSeries)[] fields) + { + Mock mock = new(MockBehavior.Strict); + mock.Setup(r => r.IsByRefLike(byRefLikeType)).Returns(true); + + TargetPointer[] fdPtrs = Enumerable.Range(1, fields.Length) + .Select(i => new TargetPointer((ulong)i)).ToArray(); + mock.Setup(r => r.EnumerateInstanceFieldDescs(byRefLikeType)).Returns(fdPtrs); + + for (int i = 0; i < fields.Length; i++) + { + TargetPointer fdPtr = fdPtrs[i]; + (uint offset, CorElementType et, TypeHandle? inner, bool innerByRefLike, (uint, uint)[]? innerSeries) = fields[i]; + + mock.Setup(r => r.GetFieldDescOffset(fdPtr, It.IsAny())) + .Returns(offset); + mock.Setup(r => r.GetFieldDescType(fdPtr)).Returns(et); + + if (et == CorElementType.ValueType) + { + mock.Setup(r => r.LookupApproxFieldTypeHandle(fdPtr)) + .Returns(inner ?? default); + if (inner is { } innerTh) + { + mock.Setup(r => r.IsByRefLike(innerTh)).Returns(innerByRefLike); + if (!innerByRefLike && innerSeries is not null) + { + mock.Setup(r => r.GetGCDescSeries(innerTh, 0u)).Returns(innerSeries); + } + // For nested ByRefLike: the recursive walker will call + // EnumerateInstanceFieldDescs(innerTh) and resolve from there. + // Tests covering recursion add those setups explicitly. + } + } + } + + return mock.Object; + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void ByRefLike_SpanLike_EmitsObjectRefAtOffsetZero(MockTarget.Architecture arch) + { + // Shape of Span: { ref byref payload; int length }. The byref payload is + // GC_CALL_INTERIOR; the length is a primitive (skipped). + TypeHandle spanTh = new TypeHandle(new TargetPointer(0x1_0000)); + IRuntimeTypeSystem rts = RtsForByRefLike(spanTh, + (Offset: 0, Type: CorElementType.Byref, Inner: null, InnerIsByRefLike: false, InnerSeries: null), + (Offset: 8, Type: CorElementType.I4, Inner: null, InnerIsByRefLike: false, InnerSeries: null)); + + ArgLayout arg = new( + IsPassedByRef: false, + Slots: new[] { new ArgSlot(0x100, CorElementType.ValueType) }, + ValueTypeHandle: spanTh); + + List<(TargetPointer Address, GcScanFlags Flags)> reports = Run(arg, rts, arch); + + (TargetPointer addr, GcScanFlags flags) = Assert.Single(reports); + Assert.Equal(TransitionBlockBase + 0x100ul, addr.Value); + Assert.Equal(GcScanFlags.GC_CALL_INTERIOR, flags); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void ByRefLike_ObjectRefField_EmitsAsRoot(MockTarget.Architecture arch) + { + // ref struct { object x; } - object refs emit None (regular ref). + TypeHandle th = new TypeHandle(new TargetPointer(0x2_0000)); + IRuntimeTypeSystem rts = RtsForByRefLike(th, + (Offset: 16, Type: CorElementType.Class, Inner: null, InnerIsByRefLike: false, InnerSeries: null)); + + ArgLayout arg = new( + IsPassedByRef: false, + Slots: new[] { new ArgSlot(0x200, CorElementType.ValueType) }, + ValueTypeHandle: th); + + List<(TargetPointer Address, GcScanFlags Flags)> reports = Run(arg, rts, arch); + + (TargetPointer addr, GcScanFlags flags) = Assert.Single(reports); + Assert.Equal(TransitionBlockBase + 0x200ul + 16ul, addr.Value); + Assert.Equal(GcScanFlags.None, flags); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void ByRefLike_MixedFields_EmitsExpectedRootsInOrder(MockTarget.Architecture arch) + { + // ref struct { object obj; ref int rr; int prim; }. Expect 2 roots: + // - obj at +0 with None + // - rr at +ps with GC_CALL_INTERIOR + // prim is skipped. Order matches field-list order. + int ps = arch.Is64Bit ? 8 : 4; + TypeHandle th = new TypeHandle(new TargetPointer(0x3_0000)); + IRuntimeTypeSystem rts = RtsForByRefLike(th, + (Offset: 0, Type: CorElementType.Class, Inner: null, InnerIsByRefLike: false, InnerSeries: null), + (Offset: (uint)ps, Type: CorElementType.Byref, Inner: null, InnerIsByRefLike: false, InnerSeries: null), + (Offset: (uint)(ps * 2), Type: CorElementType.I4, Inner: null, InnerIsByRefLike: false, InnerSeries: null)); + + ArgLayout arg = new( + IsPassedByRef: false, + Slots: new[] { new ArgSlot(0x300, CorElementType.ValueType) }, + ValueTypeHandle: th); + + List<(TargetPointer Address, GcScanFlags Flags)> reports = Run(arg, rts, arch); + + Assert.Equal(2, reports.Count); + Assert.Equal(TransitionBlockBase + 0x300ul, reports[0].Address.Value); + Assert.Equal(GcScanFlags.None, reports[0].Flags); + Assert.Equal(TransitionBlockBase + 0x300ul + (ulong)ps, reports[1].Address.Value); + Assert.Equal(GcScanFlags.GC_CALL_INTERIOR, reports[1].Flags); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void ByRefLike_NestedOrdinaryStructField_DelegatesToCgcDescWalk(MockTarget.Architecture arch) + { + // ref struct { NormalStruct s; } where NormalStruct (non-ByRefLike) has one + // object ref at boxed offset = ps -> unboxed offset = 0. Slot offset for the + // nested field is 0x10 within the ByRefLike. Expect 1 ref report at + // base + 0x10 + 0. + int ps = arch.Is64Bit ? 8 : 4; + TypeHandle outerTh = new TypeHandle(new TargetPointer(0x4_0000)); + TypeHandle innerTh = new TypeHandle(new TargetPointer(0x4_1000)); + IRuntimeTypeSystem rts = RtsForByRefLike(outerTh, + (Offset: 0x10, Type: CorElementType.ValueType, Inner: innerTh, InnerIsByRefLike: false, + InnerSeries: new (uint, uint)[] { ((uint)ps, (uint)ps) })); + + ArgLayout arg = new( + IsPassedByRef: false, + Slots: new[] { new ArgSlot(0x400, CorElementType.ValueType) }, + ValueTypeHandle: outerTh); + + List<(TargetPointer Address, GcScanFlags Flags)> reports = Run(arg, rts, arch); + + (TargetPointer addr, GcScanFlags flags) = Assert.Single(reports); + Assert.Equal(TransitionBlockBase + 0x400ul + 0x10ul, addr.Value); + Assert.Equal(GcScanFlags.None, flags); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void ByRefLike_NestedByRefLikeField_RecursesAndPreservesOffset(MockTarget.Architecture arch) + { + // Outer ref struct { InnerRefStruct s; } where InnerRefStruct has one byref at + // offset 0. Outer places s at offset 0x20. Expect 1 interior-ref report at + // base + 0x20. + TypeHandle outerTh = new TypeHandle(new TargetPointer(0x5_0000)); + TypeHandle innerTh = new TypeHandle(new TargetPointer(0x5_1000)); + + Mock mock = new(MockBehavior.Strict); + mock.Setup(r => r.IsByRefLike(outerTh)).Returns(true); + mock.Setup(r => r.IsByRefLike(innerTh)).Returns(true); + + TargetPointer outerFd = new(1); + TargetPointer innerFd = new(2); + mock.Setup(r => r.EnumerateInstanceFieldDescs(outerTh)).Returns(new[] { outerFd }); + mock.Setup(r => r.EnumerateInstanceFieldDescs(innerTh)).Returns(new[] { innerFd }); + + mock.Setup(r => r.GetFieldDescOffset(outerFd, It.IsAny())).Returns(0x20u); + mock.Setup(r => r.GetFieldDescType(outerFd)).Returns(CorElementType.ValueType); + mock.Setup(r => r.LookupApproxFieldTypeHandle(outerFd)).Returns(innerTh); + + mock.Setup(r => r.GetFieldDescOffset(innerFd, It.IsAny())).Returns(0u); + mock.Setup(r => r.GetFieldDescType(innerFd)).Returns(CorElementType.Byref); + + ArgLayout arg = new( + IsPassedByRef: false, + Slots: new[] { new ArgSlot(0x500, CorElementType.ValueType) }, + ValueTypeHandle: outerTh); + + List<(TargetPointer Address, GcScanFlags Flags)> reports = Run(arg, mock.Object, arch); + + (TargetPointer addr, GcScanFlags flags) = Assert.Single(reports); + Assert.Equal(TransitionBlockBase + 0x500ul + 0x20ul, addr.Value); + Assert.Equal(GcScanFlags.GC_CALL_INTERIOR, flags); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void ByRefLike_NoFields_EmitsNothing(MockTarget.Architecture arch) + { + TypeHandle th = new TypeHandle(new TargetPointer(0x6_0000)); + Mock mock = new(MockBehavior.Strict); + mock.Setup(r => r.IsByRefLike(th)).Returns(true); + mock.Setup(r => r.EnumerateInstanceFieldDescs(th)).Returns(System.Array.Empty()); + + ArgLayout arg = new( + IsPassedByRef: false, + Slots: new[] { new ArgSlot(0x600, CorElementType.ValueType) }, + ValueTypeHandle: th); + + List<(TargetPointer Address, GcScanFlags Flags)> reports = Run(arg, mock.Object, arch); + Assert.Empty(reports); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void ByRefLike_NestedValueTypeWithUnresolvedInner_SkipsThatField(MockTarget.Architecture arch) + { + // ref struct { object obj; ValueType missing; } where the second field's inner + // TypeHandle can't be resolved (LookupApproxFieldTypeHandle returns default). + // The walker should emit the first field's root and skip the second, matching + // the native DAC's conservative behavior. + TypeHandle th = new TypeHandle(new TargetPointer(0x7_0000)); + IRuntimeTypeSystem rts = RtsForByRefLike(th, + (Offset: 0, Type: CorElementType.Class, Inner: null, InnerIsByRefLike: false, InnerSeries: null), + (Offset: 8, Type: CorElementType.ValueType, Inner: default(TypeHandle), InnerIsByRefLike: false, InnerSeries: null)); + + ArgLayout arg = new( + IsPassedByRef: false, + Slots: new[] { new ArgSlot(0x700, CorElementType.ValueType) }, + ValueTypeHandle: th); + + List<(TargetPointer Address, GcScanFlags Flags)> reports = Run(arg, rts, arch); + + (TargetPointer addr, GcScanFlags flags) = Assert.Single(reports); + Assert.Equal(TransitionBlockBase + 0x700ul, addr.Value); + Assert.Equal(GcScanFlags.None, flags); + } +} From 55a4b12d90c92cc25e9b92a1dc04c37d21a708cd Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Fri, 22 May 2026 10:51:19 -0400 Subject: [PATCH 3/9] WIP: cDAC CallSiteLayout comprehensive Win-x64 dump tests Rebuild the CallSiteLayout debuggee as a 54-frame deep chain that exercises every interesting AMD64 calling-convention shape (categories A-H: empty/int/double argbanks, mixed reg banks, managed ref args, small/large value types, ByRefLike + Span, instance/retbuf/generic/ vararg specials, Vector64/128/256/512, composite kitchen-sink frames). All 54 frames stay live to Environment.FailFast so a single dump captures the whole matrix on the FailFast thread. Replace CallSiteLayoutDumpTests with: - CallSiteLayoutDumpTestsBase: shared DumpTestBase subclass providing CollectChainMethods / LayoutFor / AssertSingle{ByRef,ByValueVT, ManagedRef} helpers. - CallSiteLayoutDumpTests_WinX64: 46 [ConditionalTheory] tests gated by [SkipOnOS=windows], [SkipOnArch=x64], [SkipOnVersion=net10.0] asserting the explicit Win-x64 layout (IsPassedByRef, slot offsets, ValueTypeHandle populated/null) for every frame. The tests are expectation-snapshots derived from the AMD64 Windows ABI. They pin cDAC ICallingConvention.ComputeCallSiteLayout output to the hand-derived spec. Independent ground-truth validation via SOS !clrstack -gc + transition frames is a tracked follow-up; the current SOS path exercises ComputeCallSiteLayout only on InlinedCallFrames. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DumpTests/CallSiteLayoutDumpTests.cs | 508 -------------- .../DumpTests/CallSiteLayoutDumpTestsBase.cs | 93 +++ .../CallSiteLayoutDumpTests_WinX64.cs | 647 ++++++++++++++++++ .../Debuggees/CallSiteLayout/Program.cs | 542 +++++++++++---- 4 files changed, 1167 insertions(+), 623 deletions(-) delete mode 100644 src/native/managed/cdac/tests/DumpTests/CallSiteLayoutDumpTests.cs create mode 100644 src/native/managed/cdac/tests/DumpTests/CallSiteLayoutDumpTestsBase.cs create mode 100644 src/native/managed/cdac/tests/DumpTests/CallSiteLayoutDumpTests_WinX64.cs diff --git a/src/native/managed/cdac/tests/DumpTests/CallSiteLayoutDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/CallSiteLayoutDumpTests.cs deleted file mode 100644 index bd7f9f32e5caa1..00000000000000 --- a/src/native/managed/cdac/tests/DumpTests/CallSiteLayoutDumpTests.cs +++ /dev/null @@ -1,508 +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 Microsoft.Diagnostics.DataContractReader.Contracts; -using Xunit; - -namespace Microsoft.Diagnostics.DataContractReader.DumpTests; - -/// -/// Dump-based integration tests for . -/// Validates the calling-convention layout produced for a range of signature -/// shapes (varargs, byref, by-value structs, ABI-byref structs, managed -/// objects, ByRefLike) against real method metadata in a dump. -/// -/// Scoped to Windows x64. Other platforms are skipped via attributes; the -/// debuggee is also marked WindowsOnly. -/// -public class CallSiteLayoutDumpTests : DumpTestBase -{ - protected override string DebuggeeName => "CallSiteLayout"; - protected override string DumpType => "full"; - - private static readonly string[] s_chainMethods = - [ - "M_Varargs", - "M_RefInt", - "M_OutObject", - "M_RefGuid", - "M_SmallStructWithRef", - "M_TwoInts", - "M_Guid", - "M_KvpStringString", - "M_String", - "M_Object", - "M_IntArray", - "M_SpanInt", - "M_TinyRefStruct", - "M_OneByteStruct", - "M_TwelveByteValueTuple", - "M_DecimalArg", - "M_ManyInts", - "M_MixedIntDouble", - "M_KvpStringInt", - "M_KvpIntKvpIntInt", - "M_EnumByte", - "M_InstanceIntInt", - ]; - - /// - /// Walks the FailFast thread once and resolves every chain method's MethodDescHandle. - /// - private Dictionary CollectChainMethods() - { - ThreadData thread = DumpTestHelpers.FindThreadWithMethod(Target, "M_TinyRefStruct"); - IStackWalk stackWalk = Target.Contracts.StackWalk; - IRuntimeTypeSystem rts = Target.Contracts.RuntimeTypeSystem; - - HashSet wanted = new(s_chainMethods); - Dictionary result = new(); - - foreach (IStackDataFrameHandle frame in stackWalk.CreateStackWalk(thread)) - { - TargetPointer mdPtr = stackWalk.GetMethodDescPtr(frame); - if (mdPtr == TargetPointer.Null) - continue; - MethodDescHandle md = rts.GetMethodDescHandle(mdPtr); - string? name = DumpTestHelpers.GetMethodName(Target, md); - if (name is not null && wanted.Contains(name) && !result.ContainsKey(name)) - result[name] = md; - } - - return result; - } - - private CallSiteLayout LayoutFor(string methodName) - { - Dictionary methods = CollectChainMethods(); - Assert.True(methods.TryGetValue(methodName, out MethodDescHandle md), - $"'{methodName}' frame not found on the FailFast thread"); - return Target.Contracts.CallingConvention.ComputeCallSiteLayout(md); - } - - // ===== varargs ===== - - [ConditionalTheory] - [MemberData(nameof(TestConfigurations))] - [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] - [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] - public void Varargs_HasVarArgCookieAndFixedArg(TestConfiguration config) - { - InitializeDumpTest(config); - CallSiteLayout layout = LayoutFor("M_Varargs"); - - Assert.NotNull(layout.VarArgCookieOffset); - // Only the fixed `int fixedArg` is in Arguments; vararg slots are not enumerated here. - Assert.Single(layout.Arguments); - ArgLayout fixedArg = layout.Arguments[0]; - Assert.False(fixedArg.IsPassedByRef); - Assert.Null(fixedArg.ValueTypeHandle); - } - - // ===== byref ===== - - [ConditionalTheory] - [MemberData(nameof(TestConfigurations))] - [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] - [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] - public void RefInt_IsByRefNoValueTypeHandle(TestConfiguration config) - { - InitializeDumpTest(config); - CallSiteLayout layout = LayoutFor("M_RefInt"); - - Assert.Null(layout.VarArgCookieOffset); - Assert.Single(layout.Arguments); - ArgLayout arg = layout.Arguments[0]; - Assert.True(arg.IsPassedByRef); - Assert.Null(arg.ValueTypeHandle); - } - - [ConditionalTheory] - [MemberData(nameof(TestConfigurations))] - [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] - [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] - public void OutObject_IsByRefNoValueTypeHandle(TestConfiguration config) - { - InitializeDumpTest(config); - CallSiteLayout layout = LayoutFor("M_OutObject"); - - Assert.Single(layout.Arguments); - ArgLayout arg = layout.Arguments[0]; - Assert.True(arg.IsPassedByRef); - Assert.Null(arg.ValueTypeHandle); - } - - [ConditionalTheory] - [MemberData(nameof(TestConfigurations))] - [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] - [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] - public void RefGuid_IsByRefNoValueTypeHandle(TestConfiguration config) - { - InitializeDumpTest(config); - CallSiteLayout layout = LayoutFor("M_RefGuid"); - - Assert.Single(layout.Arguments); - ArgLayout arg = layout.Arguments[0]; - Assert.True(arg.IsPassedByRef); - Assert.Null(arg.ValueTypeHandle); - } - - // ===== structs (by-value) ===== - - [ConditionalTheory] - [MemberData(nameof(TestConfigurations))] - [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] - [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] - public void SmallStructWithRef_PopulatesValueTypeHandle(TestConfiguration config) - { - InitializeDumpTest(config); - CallSiteLayout layout = LayoutFor("M_SmallStructWithRef"); - - Assert.Single(layout.Arguments); - ArgLayout arg = layout.Arguments[0]; - Assert.False(arg.IsPassedByRef); - Assert.NotNull(arg.ValueTypeHandle); - Assert.Equal("StructWithRef", DumpTestHelpers.GetTypeName(Target, arg.ValueTypeHandle.Value)); - } - - [ConditionalTheory] - [MemberData(nameof(TestConfigurations))] - [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] - [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] - public void TwoInts_PopulatesValueTypeHandle(TestConfiguration config) - { - InitializeDumpTest(config); - CallSiteLayout layout = LayoutFor("M_TwoInts"); - - Assert.Single(layout.Arguments); - ArgLayout arg = layout.Arguments[0]; - Assert.False(arg.IsPassedByRef); - Assert.NotNull(arg.ValueTypeHandle); - Assert.Equal("TwoInts", DumpTestHelpers.GetTypeName(Target, arg.ValueTypeHandle.Value)); - } - - // ===== struct > 8 bytes -> ABI byref ===== - - [ConditionalTheory] - [MemberData(nameof(TestConfigurations))] - [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] - [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] - public void Guid_AbiByRefNoValueTypeHandle(TestConfiguration config) - { - InitializeDumpTest(config); - CallSiteLayout layout = LayoutFor("M_Guid"); - - Assert.Single(layout.Arguments); - ArgLayout arg = layout.Arguments[0]; - // Guid is 16 bytes -> implicit-byref on Win-x64. - Assert.True(arg.IsPassedByRef); - Assert.Null(arg.ValueTypeHandle); - } - - [ConditionalTheory] - [MemberData(nameof(TestConfigurations))] - [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] - [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] - public void KvpStringString_AbiByRefNoValueTypeHandle(TestConfiguration config) - { - InitializeDumpTest(config); - CallSiteLayout layout = LayoutFor("M_KvpStringString"); - - Assert.Single(layout.Arguments); - ArgLayout arg = layout.Arguments[0]; - Assert.True(arg.IsPassedByRef); - Assert.Null(arg.ValueTypeHandle); - } - - // ===== managed objects ===== - - [ConditionalTheory] - [MemberData(nameof(TestConfigurations))] - [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] - [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] - public void StringArg_NoValueTypeHandle(TestConfiguration config) - { - InitializeDumpTest(config); - CallSiteLayout layout = LayoutFor("M_String"); - - Assert.Single(layout.Arguments); - ArgLayout arg = layout.Arguments[0]; - Assert.False(arg.IsPassedByRef); - Assert.Null(arg.ValueTypeHandle); - } - - [ConditionalTheory] - [MemberData(nameof(TestConfigurations))] - [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] - [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] - public void ObjectArg_NoValueTypeHandle(TestConfiguration config) - { - InitializeDumpTest(config); - CallSiteLayout layout = LayoutFor("M_Object"); - - Assert.Single(layout.Arguments); - ArgLayout arg = layout.Arguments[0]; - Assert.False(arg.IsPassedByRef); - Assert.Null(arg.ValueTypeHandle); - } - - [ConditionalTheory] - [MemberData(nameof(TestConfigurations))] - [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] - [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] - public void IntArray_NoValueTypeHandle(TestConfiguration config) - { - InitializeDumpTest(config); - CallSiteLayout layout = LayoutFor("M_IntArray"); - - Assert.Single(layout.Arguments); - ArgLayout arg = layout.Arguments[0]; - Assert.False(arg.IsPassedByRef); - Assert.Null(arg.ValueTypeHandle); - } - - // ===== ByRefLike ===== - - [ConditionalTheory] - [MemberData(nameof(TestConfigurations))] - [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] - [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] - public void SpanInt_AbiByRefNoValueTypeHandle(TestConfiguration config) - { - InitializeDumpTest(config); - CallSiteLayout layout = LayoutFor("M_SpanInt"); - - Assert.Single(layout.Arguments); - ArgLayout arg = layout.Arguments[0]; - // Span is 16 bytes -> implicit-byref on Win-x64. Rule 1 (IsByRef) - // nulls ValueTypeHandle before the ByRefLike check has a chance to fire. - Assert.True(arg.IsPassedByRef); - Assert.Null(arg.ValueTypeHandle); - } - - [ConditionalTheory] - [MemberData(nameof(TestConfigurations))] - [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] - [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] - public void TinyRefStruct_ByRefLikeGuardNullsValueTypeHandle(TestConfiguration config) - { - InitializeDumpTest(config); - CallSiteLayout layout = LayoutFor("M_TinyRefStruct"); - - Assert.Single(layout.Arguments); - ArgLayout arg = layout.Arguments[0]; - // 8-byte ref struct -> passed by value, but ByRefLike guard nulls - // ValueTypeHandle because GCDesc walk alone can't report its ref field. - Assert.False(arg.IsPassedByRef); - Assert.Null(arg.ValueTypeHandle); - } - - // ===== size-rule matrix ===== - - [ConditionalTheory] - [MemberData(nameof(TestConfigurations))] - [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] - [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] - public void OneByteStruct_EnregisteredByValue(TestConfiguration config) - { - InitializeDumpTest(config); - CallSiteLayout layout = LayoutFor("M_OneByteStruct"); - - Assert.Single(layout.Arguments); - ArgLayout arg = layout.Arguments[0]; - // 1-byte struct -> enregistered by value on Win-x64. - Assert.False(arg.IsPassedByRef); - Assert.NotNull(arg.ValueTypeHandle); - Assert.Equal("OneByteStruct", DumpTestHelpers.GetTypeName(Target, arg.ValueTypeHandle.Value)); - } - - [ConditionalTheory] - [MemberData(nameof(TestConfigurations))] - [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] - [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] - public void TwelveByteValueTuple_AbiByRefNoValueTypeHandle(TestConfiguration config) - { - InitializeDumpTest(config); - CallSiteLayout layout = LayoutFor("M_TwelveByteValueTuple"); - - Assert.Single(layout.Arguments); - ArgLayout arg = layout.Arguments[0]; - // ValueTuple is 12 bytes -- not a power of two, - // so Win-x64 passes it by implicit reference. - Assert.True(arg.IsPassedByRef); - Assert.Null(arg.ValueTypeHandle); - } - - [ConditionalTheory] - [MemberData(nameof(TestConfigurations))] - [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] - [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] - public void DecimalArg_AbiByRefNoValueTypeHandle(TestConfiguration config) - { - InitializeDumpTest(config); - CallSiteLayout layout = LayoutFor("M_DecimalArg"); - - Assert.Single(layout.Arguments); - ArgLayout arg = layout.Arguments[0]; - // decimal is 16 bytes -> ABI byref on Win-x64. - Assert.True(arg.IsPassedByRef); - Assert.Null(arg.ValueTypeHandle); - } - - // ===== register / stack slot mechanics ===== - - [ConditionalTheory] - [MemberData(nameof(TestConfigurations))] - [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] - [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] - public void ManyInts_SpillsLastArgsToStack(TestConfiguration config) - { - InitializeDumpTest(config); - CallSiteLayout layout = LayoutFor("M_ManyInts"); - - Assert.Equal(6, layout.Arguments.Count); - // Win-x64 has 4 argument registers (RCX/RDX/R8/R9); the 5th and 6th args - // spill onto the stack. Slot offsets are monotonically increasing. - int prevOffset = int.MinValue; - for (int i = 0; i < layout.Arguments.Count; i++) - { - ArgLayout arg = layout.Arguments[i]; - Assert.False(arg.IsPassedByRef); - Assert.Null(arg.ValueTypeHandle); - Assert.Single(arg.Slots); - Assert.True(arg.Slots[0].Offset > prevOffset, - $"Arg {i} offset {arg.Slots[0].Offset} not greater than previous {prevOffset}"); - prevOffset = arg.Slots[0].Offset; - } - // The last two slot offsets must be at least 8 bytes apart from the - // 4th -- confirming they live past the 4-register window. - Assert.True(layout.Arguments[4].Slots[0].Offset >= layout.Arguments[3].Slots[0].Offset + 8); - Assert.True(layout.Arguments[5].Slots[0].Offset >= layout.Arguments[4].Slots[0].Offset + 8); - } - - [ConditionalTheory] - [MemberData(nameof(TestConfigurations))] - [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] - [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] - public void MixedIntDouble_FloatArgsUseFpRegisterSlots(TestConfiguration config) - { - InitializeDumpTest(config); - CallSiteLayout layout = LayoutFor("M_MixedIntDouble"); - - Assert.Equal(4, layout.Arguments.Count); - // (int, double, int, double): the doubles should surface as R8 slots - // (XMM register lanes), the ints as integer-typed slots. - Assert.Equal(CorElementType.I4, layout.Arguments[0].Slots[0].ElementType); - Assert.Equal(CorElementType.R8, layout.Arguments[1].Slots[0].ElementType); - Assert.Equal(CorElementType.I4, layout.Arguments[2].Slots[0].ElementType); - Assert.Equal(CorElementType.R8, layout.Arguments[3].Slots[0].ElementType); - foreach (ArgLayout arg in layout.Arguments) - { - Assert.False(arg.IsPassedByRef); - Assert.Null(arg.ValueTypeHandle); - } - } - - // ===== generics: heterogeneous + nested ===== - - [ConditionalTheory] - [MemberData(nameof(TestConfigurations))] - [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] - [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] - public void KvpStringInt_AbiByRefNoValueTypeHandle(TestConfiguration config) - { - InitializeDumpTest(config); - CallSiteLayout layout = LayoutFor("M_KvpStringInt"); - - Assert.Single(layout.Arguments); - ArgLayout arg = layout.Arguments[0]; - // KeyValuePair is 16 bytes on x64 (ref + 4 bytes padded to 8). - // The fix to GetGenericInstantiation must resolve both the ref-typed and - // primitive-typed type-args to TypeHandles so the instantiated MT is found. - Assert.True(arg.IsPassedByRef); - Assert.Null(arg.ValueTypeHandle); - } - - [ConditionalTheory] - [MemberData(nameof(TestConfigurations))] - [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] - [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] - public void KvpIntKvpIntInt_NestedGenericResolvesViaRecursion(TestConfiguration config) - { - InitializeDumpTest(config); - CallSiteLayout layout = LayoutFor("M_KvpIntKvpIntInt"); - - Assert.Single(layout.Arguments); - ArgLayout arg = layout.Arguments[0]; - // KeyValuePair> is 12 bytes (int + 8-byte KVP). - // Both instantiations are inline GENERICINST blobs in the method signature, so - // SRM recurses through ArgTypeInfoSignatureProvider.GetGenericInstantiation at - // every level rather than dispatching to GetTypeFromSpecification. The inner - // KVP resolves to an 8-byte loaded MT, supplying a real TypeHandle as - // the second type arg of the outer instantiation; the outer then resolves to a - // loaded 12-byte MT. - // - // Discriminator: if inner resolution had failed (UnresolvedValueType, no - // TypeHandle), the outer would also fall back to UnresolvedValueType (size 8), - // and 8 is pow2 / <=8 so IsPassedByRef would be FALSE. Asserting IsPassedByRef - // == true proves the recursive GetGenericInstantiation chain succeeded all the - // way through the nested instantiation. - Assert.True(arg.IsPassedByRef); - Assert.Null(arg.ValueTypeHandle); - } - - // ===== enum collapse ===== - - [ConditionalTheory] - [MemberData(nameof(TestConfigurations))] - [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] - [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] - public void EnumByte_TreatedAsValueType(TestConfiguration config) - { - InitializeDumpTest(config); - CallSiteLayout layout = LayoutFor("M_EnumByte"); - - Assert.Single(layout.Arguments); - ArgLayout arg = layout.Arguments[0]; - // Native MetaSig::NextArgNormalized collapses enums to their underlying - // primitive (here U1). The cDAC iterator currently keeps the enum's - // MT-level classification (ValueType) and attaches its TypeHandle. - // That is *safe* for GC scanning (a byte enum has no embedded refs, - // so the GCDesc walk yields zero refs) but diverges from native. - // Tracking parity with NextArgNormalized's enum collapse is left as a - // TODO; this test pins the current behavior so the divergence is - // visible if/when the iterator is updated. - Assert.False(arg.IsPassedByRef); - Assert.NotNull(arg.ValueTypeHandle); - Assert.Equal("SmallByteEnum", DumpTestHelpers.GetTypeName(Target, arg.ValueTypeHandle.Value)); - } - - // ===== instance method ===== - - [ConditionalTheory] - [MemberData(nameof(TestConfigurations))] - [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] - [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] - public void InstanceIntInt_PopulatesThisOffset(TestConfiguration config) - { - InitializeDumpTest(config); - CallSiteLayout layout = LayoutFor("M_InstanceIntInt"); - - // Instance method on a reference type: ThisOffset is populated and - // IsValueTypeThis is false. The two fixed args follow `this`. - Assert.NotNull(layout.ThisOffset); - Assert.False(layout.IsValueTypeThis); - Assert.Equal(2, layout.Arguments.Count); - foreach (ArgLayout arg in layout.Arguments) - { - Assert.False(arg.IsPassedByRef); - Assert.Null(arg.ValueTypeHandle); - Assert.Equal(CorElementType.I4, arg.Slots[0].ElementType); - } - // `this` occupies the first register slot; the two ints come after. - Assert.True(layout.Arguments[0].Slots[0].Offset > layout.ThisOffset.Value); - } -} - - diff --git a/src/native/managed/cdac/tests/DumpTests/CallSiteLayoutDumpTestsBase.cs b/src/native/managed/cdac/tests/DumpTests/CallSiteLayoutDumpTestsBase.cs new file mode 100644 index 00000000000000..fdcb888b7b91ab --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/CallSiteLayoutDumpTestsBase.cs @@ -0,0 +1,93 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.DumpTests; + +/// +/// Shared infrastructure for the CallSiteLayout dump tests. Each ABI gets its +/// own subclass (, etc.) that +/// applies [SkipOnOS] / [SkipOnArch] attributes and encodes +/// the expected ABI-specific layout for every frame. +/// +public abstract class CallSiteLayoutDumpTestsBase : DumpTestBase +{ + protected override string DebuggeeName => "CallSiteLayout"; + protected override string DumpType => "full"; + + // Deepest non-FailFast frame on the chain. Used to discover the + // FailFast thread; resolution then visits every frame above it. + private const string LeafFrame = "M_Combo_RefStructWithMultipleRefs"; + + /// + /// Walks the FailFast thread once, recording the MethodDescHandle of + /// every named frame visited. Frame names beyond the chain are ignored. + /// + protected Dictionary CollectChainMethods() + { + ThreadData thread = DumpTestHelpers.FindThreadWithMethod(Target, LeafFrame); + IStackWalk stackWalk = Target.Contracts.StackWalk; + IRuntimeTypeSystem rts = Target.Contracts.RuntimeTypeSystem; + + Dictionary result = new(); + foreach (IStackDataFrameHandle frame in stackWalk.CreateStackWalk(thread)) + { + TargetPointer mdPtr = stackWalk.GetMethodDescPtr(frame); + if (mdPtr == TargetPointer.Null) + continue; + MethodDescHandle md = rts.GetMethodDescHandle(mdPtr); + string? name = DumpTestHelpers.GetMethodName(Target, md); + if (name is not null && !result.ContainsKey(name)) + result[name] = md; + } + + return result; + } + + /// + /// Looks up the named frame on the FailFast thread and computes its layout. + /// + protected CallSiteLayout LayoutFor(string methodName) + { + Dictionary methods = CollectChainMethods(); + Assert.True(methods.TryGetValue(methodName, out MethodDescHandle md), + $"'{methodName}' frame not found on the FailFast thread"); + return Target.Contracts.CallingConvention.ComputeCallSiteLayout(md); + } + + // ===== Common assertion helpers ===== + + /// Asserts a single-arg signature where the arg is passed by managed/implicit byref. + protected void AssertSingleByRef(string frame) + { + CallSiteLayout layout = LayoutFor(frame); + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.True(arg.IsPassedByRef, $"{frame}: expected IsPassedByRef=true"); + Assert.Null(arg.ValueTypeHandle); + } + + /// Asserts a single-arg signature passed by value with the given value-type name. + protected void AssertSingleByValueVT(string frame, string typeName) + { + CallSiteLayout layout = LayoutFor(frame); + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.False(arg.IsPassedByRef, $"{frame}: expected IsPassedByRef=false"); + Assert.NotNull(arg.ValueTypeHandle); + Assert.Equal(typeName, DumpTestHelpers.GetTypeName(Target, arg.ValueTypeHandle.Value)); + } + + /// Asserts a single-arg signature for a managed object reference (no VTH, not byref). + protected void AssertSingleManagedRef(string frame) + { + CallSiteLayout layout = LayoutFor(frame); + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.False(arg.IsPassedByRef, $"{frame}: expected IsPassedByRef=false"); + Assert.Null(arg.ValueTypeHandle); + } +} diff --git a/src/native/managed/cdac/tests/DumpTests/CallSiteLayoutDumpTests_WinX64.cs b/src/native/managed/cdac/tests/DumpTests/CallSiteLayoutDumpTests_WinX64.cs new file mode 100644 index 00000000000000..5b64d7cd50dcda --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/CallSiteLayoutDumpTests_WinX64.cs @@ -0,0 +1,647 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.DumpTests; + +/// +/// Windows x64 dump-based tests for +/// . The debuggee +/// (CallSiteLayout) builds a deep call chain that holds every relevant +/// calling-convention shape live to Environment.FailFast. Each test +/// asserts the Win-x64 ABI layout for one frame. +/// +/// Win-x64 rules in one paragraph: every argument occupies exactly one 8-byte +/// slot in the transition block. A value type is passed by value iff its size +/// is 1, 2, 4, or 8 bytes; otherwise it is copied to the caller's stack and the +/// slot holds a pointer to that copy (IsPassedByRef = true, set by +/// ArgIterator, not a user ref). HFAs are not enregistered on +/// Win-x64 (Microsoft ABI). 4 GP arg registers (RCX/RDX/R8/R9), 4 XMM lanes +/// mirrored; further args spill to the stack at +0x20. +/// +public class CallSiteLayoutDumpTests_WinX64 : CallSiteLayoutDumpTestsBase +{ + // ===== Category A: register-bank fill / spill (no GC refs) ===== + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void Empty_NoArguments(TestConfiguration config) + { + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_Empty"); + Assert.Null(layout.ThisOffset); + Assert.Null(layout.VarArgCookieOffset); + Assert.Empty(layout.Arguments); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void IntSix_AllFitsInRegisterSlots(TestConfiguration config) + { + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_Int_Six"); + Assert.Equal(6, layout.Arguments.Count); + int prev = int.MinValue; + foreach (ArgLayout a in layout.Arguments) + { + Assert.False(a.IsPassedByRef); + Assert.Null(a.ValueTypeHandle); + Assert.Single(a.Slots); + Assert.Equal(CorElementType.I4, a.Slots[0].ElementType); + Assert.True(a.Slots[0].Offset > prev); + prev = a.Slots[0].Offset; + } + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void IntNine_SpillsArgsBeyondRegisters(TestConfiguration config) + { + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_Int_Nine"); + Assert.Equal(9, layout.Arguments.Count); + // Win-x64 has 4 argument registers; args 5-9 spill to stack at +8 strides. + Assert.True(layout.Arguments[4].Slots[0].Offset >= layout.Arguments[3].Slots[0].Offset + 8); + Assert.True(layout.Arguments[8].Slots[0].Offset >= layout.Arguments[4].Slots[0].Offset + 32); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void DoubleFour_FloatsUseFpSlots(TestConfiguration config) + { + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_Double_Four"); + Assert.Equal(4, layout.Arguments.Count); + foreach (ArgLayout a in layout.Arguments) + { + Assert.False(a.IsPassedByRef); + Assert.Null(a.ValueTypeHandle); + Assert.Equal(CorElementType.R8, a.Slots[0].ElementType); + } + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void MixedID_AlternatingIntDouble(TestConfiguration config) + { + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_MixedID"); + Assert.Equal(6, layout.Arguments.Count); + Assert.Equal(CorElementType.I4, layout.Arguments[0].Slots[0].ElementType); + Assert.Equal(CorElementType.R8, layout.Arguments[1].Slots[0].ElementType); + Assert.Equal(CorElementType.I4, layout.Arguments[2].Slots[0].ElementType); + Assert.Equal(CorElementType.R8, layout.Arguments[3].Slots[0].ElementType); + } + + // ===== Category B: reference-typed args ===== + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void RefArgs_String_NoValueTypeHandle(TestConfiguration config) + { + InitializeDumpTest(config); + AssertSingleManagedRef("M_RefArgs_String"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void RefArgs_Object_NoValueTypeHandle(TestConfiguration config) + { + InitializeDumpTest(config); + AssertSingleManagedRef("M_RefArgs_Object"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void RefArgs_SzArray_NoValueTypeHandle(TestConfiguration config) + { + InitializeDumpTest(config); + AssertSingleManagedRef("M_RefArgs_SzArray"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void RefArgs_MdArray_NoValueTypeHandle(TestConfiguration config) + { + InitializeDumpTest(config); + AssertSingleManagedRef("M_RefArgs_MdArray"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void RefArgs_RefInt_IsByRef(TestConfiguration config) + { + InitializeDumpTest(config); + AssertSingleByRef("M_RefArgs_RefInt"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void RefArgs_OutObject_IsByRef(TestConfiguration config) + { + InitializeDumpTest(config); + AssertSingleByRef("M_RefArgs_OutObject"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void RefArgs_RefStruct_IsByRef(TestConfiguration config) + { + InitializeDumpTest(config); + AssertSingleByRef("M_RefArgs_RefStruct"); + } + + // ===== Category C: small by-value structs ===== + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void VT_Byte_EnregisteredByValue(TestConfiguration config) + { + InitializeDumpTest(config); + AssertSingleByValueVT("M_VT_Byte", "ByteStruct"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void VT_Short_EnregisteredByValue(TestConfiguration config) + { + InitializeDumpTest(config); + AssertSingleByValueVT("M_VT_Short", "ShortStruct"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void VT_Int_EnregisteredByValue(TestConfiguration config) + { + InitializeDumpTest(config); + AssertSingleByValueVT("M_VT_Int", "IntStruct"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void VT_TwoInts_EnregisteredByValue(TestConfiguration config) + { + InitializeDumpTest(config); + AssertSingleByValueVT("M_VT_TwoInts", "TwoInts"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void VT_ObjectOnly_PopulatesValueTypeHandle(TestConfiguration config) + { + // 8-byte struct containing a single object ref. This is the canonical + // Win-x64 case where the GCDesc walk is needed at scan time. + InitializeDumpTest(config); + AssertSingleByValueVT("M_VT_ObjectOnly", "ObjectStruct"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void VT_StringOnly_PopulatesValueTypeHandle(TestConfiguration config) + { + InitializeDumpTest(config); + AssertSingleByValueVT("M_VT_StringOnly", "StringStruct"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void VT_ThreeByte_AbiByRef(TestConfiguration config) + { + // 3 bytes -> non-power-of-two -> Win-x64 passes by implicit reference. + InitializeDumpTest(config); + AssertSingleByRef("M_VT_ThreeByte"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void VT_FiveByte_AbiByRef(TestConfiguration config) + { + InitializeDumpTest(config); + AssertSingleByRef("M_VT_FiveByte"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void VT_Twelve_AbiByRef(TestConfiguration config) + { + InitializeDumpTest(config); + AssertSingleByRef("M_VT_Twelve"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void VT_Guid_AbiByRef(TestConfiguration config) + { + // 16 bytes -> Win-x64 passes by implicit reference. + InitializeDumpTest(config); + AssertSingleByRef("M_VT_Guid"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void VT_Decimal_AbiByRef(TestConfiguration config) + { + InitializeDumpTest(config); + AssertSingleByRef("M_VT_Decimal"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void VT_KvpStrStr_AbiByRef(TestConfiguration config) + { + InitializeDumpTest(config); + AssertSingleByRef("M_VT_KvpStrStr"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void VT_KvpStrInt_AbiByRef(TestConfiguration config) + { + // 16 bytes (ref + 4 padded to 8). Exercises the nested + // GetGenericInstantiation path resolving both ref- and primitive- + // typed type args at one level of nesting. + InitializeDumpTest(config); + AssertSingleByRef("M_VT_KvpStrInt"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void VT_TwoFloats_EnregisteredByValue(TestConfiguration config) + { + // 8-byte HFA candidate. Win-x64 does NOT enregister HFAs as scalars; + // the whole struct travels in one slot by value. + InitializeDumpTest(config); + AssertSingleByValueVT("M_VT_TwoFloats", "TwoFloats"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void VT_TwoDoubles_AbiByRef(TestConfiguration config) + { + // 16-byte HFA candidate -> Win-x64 implicit-byref (no HFA enregistration). + InitializeDumpTest(config); + AssertSingleByRef("M_VT_TwoDoubles"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void VT_IntDouble_AbiByRef(TestConfiguration config) + { + // 16 bytes (4-byte int + 4 padding + 8-byte double) -> implicit-byref. + InitializeDumpTest(config); + AssertSingleByRef("M_VT_IntDouble"); + } + + // ===== Category D: large stack-passed structs ===== + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void VT_Big24_AbiByRef(TestConfiguration config) + { + InitializeDumpTest(config); + AssertSingleByRef("M_VT_Big24"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void VT_Big24WithRef_AbiByRef(TestConfiguration config) + { + // SysV would pass this on-stack by value and require a GCDesc walk; on + // Win-x64 it is implicit-byref so no in-arg GC scanning is needed -- + // the caller's stack temp holds the live ref and is covered by the + // caller's GC info. + InitializeDumpTest(config); + AssertSingleByRef("M_VT_Big24WithRef"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void VT_Big48WithTwoRefs_AbiByRef(TestConfiguration config) + { + InitializeDumpTest(config); + AssertSingleByRef("M_VT_Big48WithTwoRefs"); + } + + // ===== Category E: ByRefLike ===== + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void BRL_SpanInt_AbiByRef(TestConfiguration config) + { + // Span is 16 bytes -> Win-x64 implicit-byref. The IsByRef short + // circuit fires before the ByRefLike dispatch can populate + // ValueTypeHandle, so VTH must be null here. + InitializeDumpTest(config); + AssertSingleByRef("M_BRL_SpanInt"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void BRL_SpanObject_AbiByRef(TestConfiguration config) + { + InitializeDumpTest(config); + AssertSingleByRef("M_BRL_SpanObject"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void BRL_SmallRefStruct_ByValueRefersToByRefLike(TestConfiguration config) + { + // Pointer-sized ref struct holding a single managed byref. Passed by + // value on Win-x64 (1 slot, 8 bytes). The producer keeps the + // ValueTypeHandle so the consumer can dispatch to the ByRefLike walker + // and emit the inner byref field as an INTERIOR root. + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_BRL_SmallRefStruct"); + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.False(arg.IsPassedByRef); + Assert.NotNull(arg.ValueTypeHandle); + Assert.Equal("SmallRefStruct", DumpTestHelpers.GetTypeName(Target, arg.ValueTypeHandle.Value)); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void BRL_RefStructWithObject_AbiByRef(TestConfiguration config) + { + // RefStructWithObject is { object Obj; int Prim; } -> 16 bytes on x64. + // Implicit-byref on Win-x64; the caller's stack temp holds the live + // object so cross-frame GC scanning relies on the caller's reporting. + InitializeDumpTest(config); + AssertSingleByRef("M_BRL_RefStructWithObject"); + } + + // ===== Category F: special slots ===== + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void Special_Instance_PopulatesThisOffset(TestConfiguration config) + { + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_Special_Instance"); + Assert.NotNull(layout.ThisOffset); + Assert.False(layout.IsValueTypeThis); + Assert.Equal(2, layout.Arguments.Count); + Assert.True(layout.Arguments[0].Slots[0].Offset > layout.ThisOffset!.Value); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void Special_GenericRef_TypeArgIsManagedRef(TestConfiguration config) + { + // Generic method instantiated over `string`. The single fixed arg is a + // managed object reference -- no VTH, not byref. + InitializeDumpTest(config); + AssertSingleManagedRef("M_Special_GenericRef"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void Special_GenericVal_TypeArgIsPrimitive(TestConfiguration config) + { + // Generic method instantiated over `int` -- single primitive arg, no VTH. + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_Special_GenericVal"); + Assert.Single(layout.Arguments); + ArgLayout arg = layout.Arguments[0]; + Assert.False(arg.IsPassedByRef); + Assert.Null(arg.ValueTypeHandle); + Assert.Equal(CorElementType.I4, arg.Slots[0].ElementType); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void Special_Varargs_HasVarArgCookieAndFixedArg(TestConfiguration config) + { + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_Special_Varargs"); + Assert.NotNull(layout.VarArgCookieOffset); + // Only the fixed `int` is enumerated; vararg extras are not in Arguments. + Assert.Single(layout.Arguments); + ArgLayout fixedArg = layout.Arguments[0]; + Assert.False(fixedArg.IsPassedByRef); + Assert.Null(fixedArg.ValueTypeHandle); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void Special_NoVarargs_NoCookie(TestConfiguration config) + { + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_Special_NoVarargs"); + Assert.Null(layout.VarArgCookieOffset); + Assert.Equal(2, layout.Arguments.Count); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void Special_InstanceGenericClassGenericMethod_PopulatesThisOffset(TestConfiguration config) + { + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_Special_InstanceGenericClassGenericMethod"); + Assert.NotNull(layout.ThisOffset); + Assert.False(layout.IsValueTypeThis); + Assert.Equal(2, layout.Arguments.Count); + } + + // ===== Category G: vectors ===== + // Vector{256,512} are runtime-supported but the IsSupported gate in the + // debuggee may bypass the call; the frames are still on the chain because + // their parents pass Vector{N}.Zero unconditionally. + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void Vec_64_EnregisteredByValue(TestConfiguration config) + { + // Vector64 is 8 bytes. Win-x64 passes 8-byte structs by value. + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_Vec_64"); + Assert.Single(layout.Arguments); + Assert.False(layout.Arguments[0].IsPassedByRef); + Assert.NotNull(layout.Arguments[0].ValueTypeHandle); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void Vec_128_AbiByRef(TestConfiguration config) + { + // Vector128 is 16 bytes -> Win-x64 implicit-byref. + InitializeDumpTest(config); + AssertSingleByRef("M_Vec_128"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void Vec_256_AbiByRef(TestConfiguration config) + { + // Vector256 is 32 bytes -> Win-x64 implicit-byref. + InitializeDumpTest(config); + AssertSingleByRef("M_Vec_256"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void Vec_512_AbiByRef(TestConfiguration config) + { + // Vector512 is 64 bytes -> Win-x64 implicit-byref. + InitializeDumpTest(config); + AssertSingleByRef("M_Vec_512"); + } + + // ===== Category H: composite frames ===== + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "Debuggee is Windows-only")] + [SkipOnArch(IncludeOnly = "x64", Reason = "Layout asserts use Win-x64 ABI specifics")] + [SkipOnVersion("net10.0", "CodeVersions descriptor format incompatible with current cDAC reader on net10.0 dumps")] + public void Combo_RegBankExhaustion_AllArgsAccounted(TestConfiguration config) + { + InitializeDumpTest(config); + CallSiteLayout layout = LayoutFor("M_Combo_RegBankExhaustion"); + Assert.Equal(15, layout.Arguments.Count); + // First six pairs are int/double scalars - check the trailing 3 are object/string/array. + for (int i = 12; i < 15; i++) + { + Assert.False(layout.Arguments[i].IsPassedByRef); + Assert.Null(layout.Arguments[i].ValueTypeHandle); + } + } +} diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/CallSiteLayout/Program.cs b/src/native/managed/cdac/tests/DumpTests/Debuggees/CallSiteLayout/Program.cs index 96f35aa00348e3..c4e3da6f29df9c 100644 --- a/src/native/managed/cdac/tests/DumpTests/Debuggees/CallSiteLayout/Program.cs +++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/CallSiteLayout/Program.cs @@ -4,264 +4,539 @@ using System; using System.Collections.Generic; using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; /// -/// Debuggee for cDAC CallSiteLayoutDumpTests. Builds a deep call chain -/// where every frame remains live on the stack at . -/// Each method exercises a different signature shape so the test can assert -/// ICallingConvention.ComputeCallSiteLayout produces the expected -/// ArgLayout per category: -/// varargs, byref, structs (by-value & ABI-byref), managed objects, ByRefLike. +/// Debuggee for cDAC CallSiteLayoutDumpTests_*. Builds one deep call +/// chain where every frame is held live to . +/// Each frame exercises a single calling-convention axis (or, in the Combo +/// section, a deliberate combination). Test classes inspect each frame's +/// MethodDesc and assert the expected ArgLayout for the target ABI. /// -/// All methods are so each frame is -/// independently discoverable by name in the dump. +/// Categories (Program order = simple-to-complex, top-of-stack first): +/// A. Register-bank fill / spill (no GC refs) +/// B. Reference-typed args (Class/Byref dispatch) +/// C. Small by-value structs (size matrix, incl. structs holding refs) +/// D. Large stack-passed structs (>16 byte) +/// E. ByRefLike (Span<T>, ref structs) +/// F. Special arg slots (this, retbuf, generic context, varargs) +/// G. SIMD vectors (Vector64/128/256/512) +/// H. Composite kitchen-sink frames /// internal static class Program { private static void Main() { - M_Varargs(1, __arglist(42, 43)); + M_Empty(); } - // ----- varargs ----- + // ===== Category A: register-bank fill / spill, no GC refs ===== [MethodImpl(MethodImplOptions.NoInlining)] - private static void M_Varargs(int fixedArg, __arglist) + private static void M_Empty() { - GC.KeepAlive(fixedArg); - int local = 7; - M_RefInt(ref local); + M_Int_Six(1, 2, 3, 4, 5, 6); } - // ----- byref ----- + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_Int_Six(int a, int b, int c, int d, int e, int f) + { + GC.KeepAlive(a + b + c + d + e + f); + M_Int_Nine(1, 2, 3, 4, 5, 6, 7, 8, 9); + } [MethodImpl(MethodImplOptions.NoInlining)] - private static void M_RefInt(ref int x) + private static void M_Int_Nine(int a, int b, int c, int d, int e, int f, int g, int h, int i) + { + GC.KeepAlive(a + b + c + d + e + f + g + h + i); + M_Double_Four(1.5, 2.5, 3.5, 4.5); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_Double_Four(double a, double b, double c, double d) + { + GC.KeepAlive(a + b + c + d); + M_Double_Nine(1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_Double_Nine(double a, double b, double c, double d, double e, double f, double g, double h, double i) + { + GC.KeepAlive(a + b + c + d + e + f + g + h + i); + M_MixedID(1, 1.5, 2, 2.5, 3, 3.5); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_MixedID(int a, double b, int c, double d, int e, double f) + { + GC.KeepAlive(a + c + e); + GC.KeepAlive(b + d + f); + M_RefArgs_String("cDAC-CSL-String"); + } + + // ===== Category B: reference-typed args ===== + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_RefArgs_String(string s) + { + GC.KeepAlive(s); + M_RefArgs_Object(new object()); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_RefArgs_Object(object o) { - x++; - M_OutObject(out object o); GC.KeepAlive(o); + M_RefArgs_SzArray(_intArr); + } + + private static readonly int[] _intArr = [1, 2, 3]; + private static readonly int[,] _intMdArr = new int[2, 2] { { 1, 2 }, { 3, 4 } }; + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_RefArgs_SzArray(int[] a) + { + GC.KeepAlive(a); + M_RefArgs_MdArray(_intMdArr); } [MethodImpl(MethodImplOptions.NoInlining)] - private static void M_OutObject(out object o) + private static void M_RefArgs_MdArray(int[,] a) + { + GC.KeepAlive(a); + int local = 42; + M_RefArgs_RefInt(ref local); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_RefArgs_RefInt(ref int x) + { + x++; + M_RefArgs_OutObject(out object obj); + GC.KeepAlive(obj); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_RefArgs_OutObject(out object o) { o = new object(); Guid g = new Guid("11111111-2222-3333-4444-555555555555"); - M_RefGuid(ref g); + M_RefArgs_RefStruct(ref g); } [MethodImpl(MethodImplOptions.NoInlining)] - private static void M_RefGuid(ref Guid g) + private static void M_RefArgs_RefStruct(ref Guid g) { GC.KeepAlive(g); - StructWithRef s = new StructWithRef { Ref = "cDAC-CallSiteLayout-StringRef" }; - M_SmallStructWithRef(s); + M_VT_Byte(new ByteStruct { X = 0xAB }); + } + + // ===== Category C: small by-value structs ===== + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_VT_Byte(ByteStruct s) + { + GC.KeepAlive(s.X); + M_VT_Short(new ShortStruct { X = 0x1234 }); } - // ----- structs (by-value) ----- + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_VT_Short(ShortStruct s) + { + GC.KeepAlive(s.X); + M_VT_Int(new IntStruct { X = unchecked((int)0xDEADBEEF) }); + } [MethodImpl(MethodImplOptions.NoInlining)] - private static void M_SmallStructWithRef(StructWithRef s) + private static void M_VT_Int(IntStruct s) + { + GC.KeepAlive(s.X); + M_VT_TwoInts(new TwoInts { X = 1, Y = 2 }); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_VT_TwoInts(TwoInts s) + { + GC.KeepAlive(s.X + s.Y); + M_VT_ObjectOnly(new ObjectStruct { Ref = new object() }); + } + + // 8-byte by-value struct holding one object ref -- the canonical Win-x64 GCDesc-walk case. + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_VT_ObjectOnly(ObjectStruct s) { GC.KeepAlive(s.Ref); - TwoInts p = new TwoInts { X = 1, Y = 2 }; - M_TwoInts(p); + M_VT_StringOnly(new StringStruct { Ref = "cDAC-CSL-StringInStruct" }); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_VT_StringOnly(StringStruct s) + { + GC.KeepAlive(s.Ref); + M_VT_ThreeByte(new ThreeByteStruct { A = 1, B = 2, C = 3 }); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_VT_ThreeByte(ThreeByteStruct s) + { + GC.KeepAlive(s.A + s.B + s.C); + M_VT_FiveByte(new FiveByteStruct { I = 0x01020304, B = 5 }); } [MethodImpl(MethodImplOptions.NoInlining)] - private static void M_TwoInts(TwoInts p) + private static void M_VT_FiveByte(FiveByteStruct s) { - GC.KeepAlive(p.X + p.Y); - Guid g = new Guid("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"); - M_Guid(g); + GC.KeepAlive(s.I + s.B); + M_VT_Twelve((1, 2, 3)); } - // ----- struct >8 bytes -> ABI byref ----- + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_VT_Twelve((int X, int Y, int Z) v) + { + GC.KeepAlive(v.X + v.Y + v.Z); + M_VT_Guid(new Guid("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee")); + } [MethodImpl(MethodImplOptions.NoInlining)] - private static void M_Guid(Guid g) + private static void M_VT_Guid(Guid g) { GC.KeepAlive(g); - KeyValuePair kvp = new KeyValuePair("k", "v"); - M_KvpStringString(kvp); + M_VT_Decimal(123.456m); } [MethodImpl(MethodImplOptions.NoInlining)] - private static void M_KvpStringString(KeyValuePair kvp) + private static void M_VT_Decimal(decimal d) + { + GC.KeepAlive(d); + M_VT_KvpStrStr(new KeyValuePair("k", "v")); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_VT_KvpStrStr(KeyValuePair kvp) { GC.KeepAlive(kvp.Key); GC.KeepAlive(kvp.Value); - M_String("cDAC-CallSiteLayout-StringArg"); + M_VT_KvpStrInt(new KeyValuePair("k2", 99)); } - // ----- managed objects ----- + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_VT_KvpStrInt(KeyValuePair kvp) + { + GC.KeepAlive(kvp.Key); + GC.KeepAlive(kvp.Value); + M_VT_TwoFloats(new TwoFloats { X = 1.5f, Y = 2.5f }); + } [MethodImpl(MethodImplOptions.NoInlining)] - private static void M_String(string s) + private static void M_VT_TwoFloats(TwoFloats s) { - GC.KeepAlive(s); - M_Object(new object()); + GC.KeepAlive(s.X + s.Y); + M_VT_TwoDoubles(new TwoDoubles { X = 1.5, Y = 2.5 }); } [MethodImpl(MethodImplOptions.NoInlining)] - private static void M_Object(object o) + private static void M_VT_TwoDoubles(TwoDoubles s) { - GC.KeepAlive(o); - M_IntArray(_intArrayArg); + GC.KeepAlive(s.X + s.Y); + M_VT_IntDouble(new IntDoubleStruct { I = 7, D = 8.5 }); } - private static readonly int[] _intArrayArg = [1, 2, 3]; - private static readonly int[] _spanBuffer = [10, 20, 30]; + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_VT_IntDouble(IntDoubleStruct s) + { + GC.KeepAlive(s.I); + GC.KeepAlive(s.D); + M_VT_Empty(default); + } [MethodImpl(MethodImplOptions.NoInlining)] - private static void M_IntArray(int[] a) + private static void M_VT_Empty(EmptyStruct _) { - GC.KeepAlive(a); - M_SpanInt(_spanBuffer.AsSpan()); + M_VT_Big24(new Big24NoRef { A = 1, B = 2, C = 3 }); } - // ----- ByRefLike ----- + // ===== Category D: large stack-passed structs (>16) ===== [MethodImpl(MethodImplOptions.NoInlining)] - private static void M_SpanInt(Span s) + private static void M_VT_Big24(Big24NoRef s) + { + GC.KeepAlive(s.A + s.B + s.C); + M_VT_Big24WithRef(new Big24WithRef { Prefix = 0x11, Ref = new object(), Suffix = 0x22 }); + } + + // 24-byte struct with object ref at offset 8 -- the canonical SysV GCDesc-walk case. + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_VT_Big24WithRef(Big24WithRef s) + { + GC.KeepAlive(s.Prefix); + GC.KeepAlive(s.Ref); + GC.KeepAlive(s.Suffix); + M_VT_Big48WithTwoRefs(new Big48TwoRefs { A = 1, R1 = "r1", B = 2, R2 = "r2", C = 3 }); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_VT_Big48WithTwoRefs(Big48TwoRefs s) + { + GC.KeepAlive(s.A + s.B + s.C); + GC.KeepAlive(s.R1); + GC.KeepAlive(s.R2); + M_BRL_SpanInt(_spanIntBuffer.AsSpan()); + } + + // ===== Category E: ByRefLike ===== + + private static readonly int[] _spanIntBuffer = [10, 20, 30]; + private static readonly object[] _spanObjBuffer = [new object(), new object()]; + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_BRL_SpanInt(Span s) { s[0]++; - int local = 99; - M_TinyRefStruct(new SmallRefStruct(ref local)); + M_BRL_SpanObject(_spanObjBuffer.AsSpan()); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_BRL_SpanObject(Span s) + { + GC.KeepAlive(s[0]); + int local = 100; + M_BRL_SmallRefStruct(new SmallRefStruct(ref local)); } [MethodImpl(MethodImplOptions.NoInlining)] - private static void M_TinyRefStruct(SmallRefStruct t) + private static void M_BRL_SmallRefStruct(SmallRefStruct t) { t.Bump(); - OneByteStruct ob = new OneByteStruct { X = 0xAB }; - M_OneByteStruct(ob); + M_BRL_RefStructWithObject(new RefStructWithObject { Obj = new object(), Prim = 7 }); } - // ----- size-rule matrix: 1-byte struct enregistered by value ----- + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_BRL_RefStructWithObject(RefStructWithObject r) + { + GC.KeepAlive(r.Obj); + GC.KeepAlive(r.Prim); + new InstanceCallee().M_Special_Instance(11, 22); + } + + // ===== Category F: special arg slots ===== [MethodImpl(MethodImplOptions.NoInlining)] - private static void M_OneByteStruct(OneByteStruct s) + internal static Big32Struct M_Special_RetBufSmall(int seed) { - GC.KeepAlive(s.X); - (int, int, int) v = (1, 2, 3); - M_TwelveByteValueTuple(v); + Big32Struct ret = default; + ret.A = seed; + ret.B = seed + 1; + ret.C = seed + 2; + ret.D = seed + 3; + // Continue chain inside the retbuf method so the frame is live. + M_Special_RetBufLargeWithDouble(1.5); + return ret; } - // ----- 12-byte non-pow2 struct -> ABI byref ----- + [MethodImpl(MethodImplOptions.NoInlining)] + internal static Big64Struct M_Special_RetBufLargeWithDouble(double firstUserArg) + { + Big64Struct ret = default; + ret.A = (long)firstUserArg; + ret.B = ret.C = ret.D = ret.E = ret.F = ret.G = ret.H = 0; + M_Special_GenericRef("cDAC-generic-ref"); + return ret; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_Special_GenericRef(T x) where T : class + { + GC.KeepAlive(x); + M_Special_GenericVal(42); + } [MethodImpl(MethodImplOptions.NoInlining)] - private static void M_TwelveByteValueTuple((int, int, int) v) + private static void M_Special_GenericVal(T x) where T : struct { - GC.KeepAlive(v.Item1 + v.Item2 + v.Item3); - M_DecimalArg(123.456m); + GC.KeepAlive(x); + M_Special_Varargs(1, __arglist(42, 43)); } - // ----- 16-byte BCL struct -> ABI byref ----- + [MethodImpl(MethodImplOptions.NoInlining)] + private static void M_Special_Varargs(int fixedArg, __arglist) + { + GC.KeepAlive(fixedArg); + M_Special_NoVarargs(1, 2); + } [MethodImpl(MethodImplOptions.NoInlining)] - private static void M_DecimalArg(decimal d) + private static void M_Special_NoVarargs(int a, int b) { - GC.KeepAlive(d); - M_ManyInts(1, 2, 3, 4, 5, 6); + GC.KeepAlive(a + b); + new GenericContainer().M_Special_InstanceGenericClassGenericMethod("a", 7); } - // ----- 6 ints -> last two spill to the stack ----- + // ===== Category G: vectors ===== [MethodImpl(MethodImplOptions.NoInlining)] - private static void M_ManyInts(int a, int b, int c, int d, int e, int f) + internal static void M_Vec_64(Vector64 v) { - GC.KeepAlive(a + b + c + d + e + f); - M_MixedIntDouble(7, 1.5, 8, 2.5); + GC.KeepAlive(v); + M_Vec_128(Vector128.Zero); } - // ----- alternating int/double -> exercises FP register lanes (XMM1, XMM3) ----- + [MethodImpl(MethodImplOptions.NoInlining)] + internal static void M_Vec_128(Vector128 v) + { + GC.KeepAlive(v); + M_Vec_256(Vector256.Zero); + } [MethodImpl(MethodImplOptions.NoInlining)] - private static void M_MixedIntDouble(int a, double b, int c, double d) + internal static void M_Vec_256(Vector256 v) { - GC.KeepAlive(a + c); - GC.KeepAlive(b + d); - KeyValuePair kvp = new KeyValuePair("k2", 9); - M_KvpStringInt(kvp); + GC.KeepAlive(v); + M_Vec_512(Vector512.Zero); } - // ----- heterogeneous generic type-args (ref + primitive) ----- + [MethodImpl(MethodImplOptions.NoInlining)] + internal static void M_Vec_512(Vector512 v) + { + GC.KeepAlive(v); + M_Combo_InstanceManyArgs(new ComboReceiver(), 1, new object(), + new KeyValuePair("c", 2), _spanObjBuffer.AsSpan(), 5); + } + + // ===== Category H: composites ===== [MethodImpl(MethodImplOptions.NoInlining)] - private static void M_KvpStringInt(KeyValuePair kvp) + internal static void M_Combo_InstanceManyArgs(ComboReceiver r, int ix, object o, + KeyValuePair kvp, Span span, int tail) { - GC.KeepAlive(kvp.Key); - GC.KeepAlive(kvp.Value); - KeyValuePair> nested = - new KeyValuePair>(1, new KeyValuePair(2, 3)); - M_KvpIntKvpIntInt(nested); + r.M_Combo_InstanceMethodRun(ix, o, kvp, span, tail); } - // ----- nested generic: both levels are inline GENERICINST blobs, resolved - // ----- by recursive GetGenericInstantiation (not via GetTypeFromSpecification). + [MethodImpl(MethodImplOptions.NoInlining)] + internal static Big32Struct M_Combo_VarargsRetbuf(int seed, __arglist) + { + Big32Struct ret = default; + ret.A = seed; + M_Combo_RegBankExhaustion( + 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5, 5.5, 6, 6.5, + new object(), "tail-string", _comboTailArr); + return ret; + } [MethodImpl(MethodImplOptions.NoInlining)] - private static void M_KvpIntKvpIntInt(KeyValuePair> nested) + internal static void M_Combo_RegBankExhaustion( + int i1, double d1, int i2, double d2, int i3, double d3, + int i4, double d4, int i5, double d5, int i6, double d6, + object o, string s, int[] a) { - GC.KeepAlive(nested.Key); - GC.KeepAlive(nested.Value.Key + nested.Value.Value); - M_EnumByte(SmallByteEnum.B); + GC.KeepAlive(i1 + i2 + i3 + i4 + i5 + i6); + GC.KeepAlive(d1 + d2 + d3 + d4 + d5 + d6); + GC.KeepAlive(o); + GC.KeepAlive(s); + GC.KeepAlive(a); + M_Combo_DeepNestedGeneric(new KeyValuePair, int>( + new KeyValuePair("nested", Guid.Empty), 99)); } - // ----- enum collapses to its underlying primitive (U1) ----- + private static readonly int[] _comboTailArr = [1]; + + [MethodImpl(MethodImplOptions.NoInlining)] + internal static void M_Combo_DeepNestedGeneric(KeyValuePair, int> v) + { + GC.KeepAlive(v.Key.Key); + GC.KeepAlive(v.Key.Value); + GC.KeepAlive(v.Value); + int local = 0; + M_Combo_RefStructWithMultipleRefs(new MultiRefStruct(ref local, new object(), "s")); + } [MethodImpl(MethodImplOptions.NoInlining)] - private static void M_EnumByte(SmallByteEnum e) + internal static void M_Combo_RefStructWithMultipleRefs(MultiRefStruct mrs) { - GC.KeepAlive((int)e); - InstanceCallee callee = new InstanceCallee(); - callee.M_InstanceIntInt(11, 22); + mrs.Touch(); + Environment.FailFast("cDAC dump test: CallSiteLayout intentional crash"); } } +// ===== Instance / generic receiver classes ===== + internal sealed class InstanceCallee { private int _seed = 7; - // ----- instance method: layout has a populated ThisOffset ----- - [MethodImpl(MethodImplOptions.NoInlining)] - public void M_InstanceIntInt(int x, int y) + public void M_Special_Instance(int x, int y) { GC.KeepAlive(_seed + x + y); - Environment.FailFast("cDAC dump test: CallSiteLayout intentional crash"); + Program.M_Special_RetBufSmall(3); } } -internal struct OneByteStruct +internal sealed class GenericContainer { - public byte X; + private int _seed = 13; + + [MethodImpl(MethodImplOptions.NoInlining)] + public void M_Special_InstanceGenericClassGenericMethod(T classT, U methodU) + { + GC.KeepAlive(_seed); + GC.KeepAlive(classT); + GC.KeepAlive(methodU); + Program.M_Vec_64(Vector64.Zero); + } } -internal enum SmallByteEnum : byte +internal sealed class ComboReceiver { - A = 1, - B = 2, + private long _bias = 0x10000; + + [MethodImpl(MethodImplOptions.NoInlining)] + public void M_Combo_InstanceMethodRun(int ix, object o, KeyValuePair kvp, Span span, int tail) + { + GC.KeepAlive(_bias + ix + tail); + GC.KeepAlive(o); + GC.KeepAlive(kvp.Key); + GC.KeepAlive(span[0]); + Program.M_Combo_VarargsRetbuf(123, __arglist("a", 1.5)); + } } -internal struct StructWithRef +// ===== Type definitions ===== + +internal struct EmptyStruct { } + +internal struct ByteStruct { public byte X; } +internal struct ShortStruct { public short X; } +internal struct IntStruct { public int X; } +internal struct TwoInts { public int X; public int Y; } +internal struct ObjectStruct { public object Ref; } +internal struct StringStruct { public string Ref; } + +internal struct ThreeByteStruct { public byte A; public byte B; public byte C; } + +[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential, Pack = 1)] +internal struct FiveByteStruct { public int I; public byte B; } + +internal struct TwoFloats { public float X; public float Y; } +internal struct TwoDoubles { public double X; public double Y; } +internal struct IntDoubleStruct { public int I; public double D; } + +internal struct Big24NoRef { public long A; public long B; public long C; } +internal struct Big24WithRef { public long Prefix; public object Ref; public long Suffix; } +internal struct Big48TwoRefs { - public string Ref; + public long A; public string R1; public long B; public string R2; public long C; } -internal struct TwoInts +internal struct Big32Struct { public long A; public long B; public long C; public long D; } +internal struct Big64Struct { - public int X; - public int Y; + public long A; public long B; public long C; public long D; + public long E; public long F; public long G; public long H; } /// -/// 8-byte ref struct used to exercise the IsByRefLike guard in -/// ComputeValueTypeHandle. A single ref int field keeps the -/// struct pointer-sized so the Win-x64 ABI passes it by value (not as an -/// implicit byref). +/// Pointer-sized ref struct containing a single managed byref. Sized to be passed +/// by-value on Win-x64 (1-slot), exercising the ByRefLike walker dispatch. /// internal ref struct SmallRefStruct { @@ -274,3 +549,40 @@ public SmallRefStruct(ref int r) public void Bump() => Field++; } + +/// +/// Ref struct with mixed contents: an object field and a primitive. Exercises +/// the ByRefLike walker emitting an object root (None) without any byref. +/// +internal ref struct RefStructWithObject +{ + public object Obj; + public int Prim; +} + +/// +/// Ref struct with three GC-trackable members: a byref, an object, and a +/// string. Used by the kitchen-sink combo to exercise multi-field ByRefLike +/// walker emission ordering and recursion (none here, but recursion-ready +/// shape). +/// +internal ref struct MultiRefStruct +{ + public ref int Local; + public object Obj; + public string Str; + + public MultiRefStruct(ref int local, object obj, string str) + { + Local = ref local; + Obj = obj; + Str = str; + } + + public void Touch() + { + Local++; + GC.KeepAlive(Obj); + GC.KeepAlive(Str); + } +} From cc6395c9d4bd7fd3c409bb1e48db7bb7c64bbb3f Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Fri, 22 May 2026 10:57:56 -0400 Subject: [PATCH 4/9] WIP: cDAC restrict calling-convention port to AMD64 only Trim the v3 calling-convention port down to AMD64 (Windows + SysV/Unix) to keep the PR surface area small while we iterate on the design and SOS-parity validation. Removed: - Per-arch iterators: X86ArgIterator, Arm32ArgIterator, Arm64ArgIterator, RiscV64LoongArch64ArgIterator. - Per-arch unit tests: X86CallingConventionTests, Arm32CallingConventionTests, Arm64CallingConventionTests. - Non-AMD64 entries in CallConvCases (X86, Arm32, Arm64Windows, Arm64Apple, LoongArch64, RiscV64). - Reference to X86CallingConventionTests from the harness comment. Kept: - AMD64WindowsArgIterator + AMD64UnixArgIterator + SystemVStructClassifier. - Shared infrastructure (ArgIteratorBase, ArgIteratorData, ArgIteratorFactory, TransitionBlockLayout, ArgTypeInfo, ArgTypeInfoSignatureProvider, CallingConvention_1). - AMD64WindowsCallingConventionTests + AMD64UnixCallingConventionTests + the cross-arch CallingConventionTests harness. - ByRefLike walker + 46 Win-x64 CallSiteLayout dump tests (unchanged). ArgIteratorFactory.Create now throws NotSupportedException for any architecture other than X64. Other arches will be reintroduced incrementally on follow-up branches once the AMD64 surface is reviewed and merged. Tests: 2463 passed, 0 failed, 16 skipped (down from 2558 = removed 95 non-AMD64 unit tests). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../CallingConvention/ArgIteratorFactory.cs | 10 - .../CallingConvention/Arm32ArgIterator.cs | 213 ------ .../CallingConvention/Arm64ArgIterator.cs | 213 ------ .../RiscV64LoongArch64ArgIterator.cs | 98 --- .../CallingConvention/X86ArgIterator.cs | 281 -------- .../Arm32CallingConventionTests.cs | 306 -------- .../Arm64CallingConventionTests.cs | 656 ------------------ .../tests/CallingConvention/CallConvCases.cs | 39 +- .../CallingConventionTests.cs | 2 +- .../X86CallingConventionTests.cs | 255 ------- 10 files changed, 2 insertions(+), 2071 deletions(-) delete mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/Arm32ArgIterator.cs delete mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/Arm64ArgIterator.cs delete mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/RiscV64LoongArch64ArgIterator.cs delete mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/X86ArgIterator.cs delete mode 100644 src/native/managed/cdac/tests/CallingConvention/Arm32CallingConventionTests.cs delete mode 100644 src/native/managed/cdac/tests/CallingConvention/Arm64CallingConventionTests.cs delete mode 100644 src/native/managed/cdac/tests/CallingConvention/X86CallingConventionTests.cs diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgIteratorFactory.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgIteratorFactory.cs index 3a2fc7ea4847cb..a9e26add517313 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgIteratorFactory.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgIteratorFactory.cs @@ -20,21 +20,11 @@ public static ArgIteratorBase Create( { return layout.Architecture switch { - RuntimeInfoArchitecture.X86 => new X86ArgIterator( - layout, argData, hasParamType, hasAsyncContinuation), RuntimeInfoArchitecture.X64 => layout.OperatingSystem != RuntimeInfoOperatingSystem.Windows ? new AMD64UnixArgIterator( layout, argData, hasParamType, hasAsyncContinuation) : new AMD64WindowsArgIterator( layout, argData, hasParamType, hasAsyncContinuation), - RuntimeInfoArchitecture.Arm => new Arm32ArgIterator( - layout, argData, hasParamType, hasAsyncContinuation, - isArmhfABI: !layout.Target.TryReadGlobal(Constants.Globals.FeatureArmSoftFP, out byte? _)), - RuntimeInfoArchitecture.Arm64 => new Arm64ArgIterator( - layout, argData, hasParamType, hasAsyncContinuation), - RuntimeInfoArchitecture.LoongArch64 or RuntimeInfoArchitecture.RiscV64 - => new RiscV64LoongArch64ArgIterator( - layout, argData, hasParamType, hasAsyncContinuation), _ => throw new NotSupportedException(layout.Architecture.ToString()), }; } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/Arm32ArgIterator.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/Arm32ArgIterator.cs deleted file mode 100644 index 39d76678154197..00000000000000 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/Arm32ArgIterator.cs +++ /dev/null @@ -1,213 +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; - -namespace Microsoft.Diagnostics.DataContractReader.Contracts.CallingConventionHelpers; - -/// -/// ARM32 (AAPCS) argument iterator. Integer arguments use R0-R3, hard-float -/// targets use the VFP argument register bank for floating-point values, and -/// overflow spills to the stack with the ABI's 64-bit-alignment rules. -/// -internal sealed class Arm32ArgIterator : ArgIteratorBase -{ - private readonly bool _isArmhfABI; - - public override int NumArgumentRegisters => 4; - public override int NumFloatArgumentRegisters => 16; - public override int FloatRegisterSize => 4; - public override int EnregisteredParamTypeMaxSize => 0; - public override int EnregisteredReturnTypeIntegerMaxSize => 4; - public override int StackSlotSize => 4; - public override bool IsRetBuffPassedAsFirstArg => true; - - public Arm32ArgIterator( - TransitionBlockLayout layout, - ArgIteratorData argData, - bool hasParamType, - bool hasAsyncContinuation, - bool isArmhfABI = true) - : base(layout, argData, hasParamType, hasAsyncContinuation) - { - _isArmhfABI = isArmhfABI; - } - - public override bool IsArgPassedByRefBySize(int size) => false; - - public override IEnumerable EnumerateArgs() - { - int idxGenReg = ComputeInitialNumRegistersUsed(); - int ofsStack = 0; - ushort wFPRegs = 0; - - for (int argNum = 0; argNum < NumFixedArgs; argNum++) - { - CorElementType argType = GetArgumentType(argNum, out ArgTypeInfo argTypeInfo); - int argSize = GetElemSize(argType, argTypeInfo, _layout.PointerSize); - int cbArg = StackElemSize(argSize); - - bool isFloatingPoint = false; - bool requiresAlign64Bit = false; - CorElementType fpElementType = argType; - - switch (argType) - { - case CorElementType.I8: - case CorElementType.U8: - requiresAlign64Bit = true; - break; - - case CorElementType.R4: - isFloatingPoint = true; - fpElementType = CorElementType.R4; - break; - - case CorElementType.R8: - isFloatingPoint = true; - requiresAlign64Bit = true; - fpElementType = CorElementType.R8; - break; - - case CorElementType.ValueType: - requiresAlign64Bit = argTypeInfo.RequiresAlign8; - if (argTypeInfo.IsHomogeneousAggregate) - { - isFloatingPoint = true; - fpElementType = argTypeInfo.HomogeneousAggregateElementSize == 4 - ? CorElementType.R4 - : CorElementType.R8; - } - break; - } - - IReadOnlyList locations; - if (isFloatingPoint && _isArmhfABI && !IsVarArg) - { - ushort wAllocMask = checked((ushort)((1 << (cbArg / 4)) - 1)); - ushort cSteps = (ushort)(requiresAlign64Bit ? 9 - (cbArg / 8) : 17 - (cbArg / 4)); - ushort cShift = requiresAlign64Bit ? (ushort)2 : (ushort)1; - - for (ushort i = 0; i < cSteps; i++) - { - if ((wFPRegs & wAllocMask) == 0) - { - wFPRegs |= wAllocMask; - locations = - [ - new ArgLocation - { - Kind = ArgLocationKind.FpRegister, - TransitionBlockOffset = _layout.OffsetOfFloatArgumentRegisters + (i * cShift * 4), - Size = cbArg, - ElementType = fpElementType, - } - ]; - goto Yield; - } - - wAllocMask <<= cShift; - } - - wFPRegs = 0xffff; - if (requiresAlign64Bit) - { - ofsStack = AlignUp(ofsStack, _layout.PointerSize * 2); - } - - locations = - [ - new ArgLocation - { - Kind = ArgLocationKind.Stack, - TransitionBlockOffset = _layout.OffsetOfArgs + ofsStack, - Size = cbArg, - ElementType = argType, - } - ]; - ofsStack += cbArg; - } - else - { - if (idxGenReg < NumArgumentRegisters) - { - if (requiresAlign64Bit) - { - idxGenReg = AlignUp(idxGenReg, 2); - } - - int argOffset = _layout.ArgumentRegistersOffset + idxGenReg * _layout.PointerSize; - int remainingRegs = NumArgumentRegisters - idxGenReg; - if (cbArg <= remainingRegs * _layout.PointerSize) - { - idxGenReg += AlignUp(cbArg, _layout.PointerSize) / _layout.PointerSize; - locations = - [ - new ArgLocation - { - Kind = ArgLocationKind.GpRegister, - TransitionBlockOffset = argOffset, - Size = cbArg, - ElementType = argType, - } - ]; - goto Yield; - } - - idxGenReg = NumArgumentRegisters; - if (ofsStack == 0 && remainingRegs > 0) - { - int regSize = remainingRegs * _layout.PointerSize; - int stackSize = cbArg - regSize; - locations = - [ - new ArgLocation - { - Kind = ArgLocationKind.GpRegister, - TransitionBlockOffset = argOffset, - Size = regSize, - ElementType = argType, - }, - new ArgLocation - { - Kind = ArgLocationKind.Stack, - TransitionBlockOffset = _layout.OffsetOfArgs, - Size = stackSize, - ElementType = argType, - } - ]; - ofsStack += stackSize; - goto Yield; - } - } - - if (requiresAlign64Bit) - { - ofsStack = AlignUp(ofsStack, _layout.PointerSize * 2); - } - - locations = - [ - new ArgLocation - { - Kind = ArgLocationKind.Stack, - TransitionBlockOffset = _layout.OffsetOfArgs + ofsStack, - Size = cbArg, - ElementType = argType, - } - ]; - ofsStack += cbArg; - } - - Yield: - yield return new ArgLocDesc - { - ArgType = argType, - ArgSize = argSize, - ArgTypeInfo = argTypeInfo, - IsByRef = argType == CorElementType.Byref, - Locations = locations, - }; - } - } -} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/Arm64ArgIterator.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/Arm64ArgIterator.cs deleted file mode 100644 index 0e4c7fa86d904d..00000000000000 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/Arm64ArgIterator.cs +++ /dev/null @@ -1,213 +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.Diagnostics; - -namespace Microsoft.Diagnostics.DataContractReader.Contracts.CallingConventionHelpers; - -/// -/// ARM64 (AAPCS64 / Apple ARM64) argument iterator. Integer arguments use X0-X7, -/// floating-point arguments use V0-V7, Apple stack slots are tightly packed for -/// primitives, and HFAs are reported as one slot per FP register. -/// -internal sealed class Arm64ArgIterator : ArgIteratorBase -{ - private readonly bool _isAppleArm64ABI; - - public override int NumArgumentRegisters => 8; - public override int NumFloatArgumentRegisters => 8; - public override int FloatRegisterSize => 16; - public override int EnregisteredParamTypeMaxSize => 16; - public override int EnregisteredReturnTypeIntegerMaxSize => 16; - public override int StackSlotSize => 8; - public override bool IsRetBuffPassedAsFirstArg => false; - - public Arm64ArgIterator( - TransitionBlockLayout layout, - ArgIteratorData argData, - bool hasParamType, - bool hasAsyncContinuation) - : base(layout, argData, hasParamType, hasAsyncContinuation) - { - _isAppleArm64ABI = layout.OperatingSystem == RuntimeInfoOperatingSystem.Apple; - } - - public override int StackElemSize(int parmSize, bool isValueType = false, bool isFloatHfa = false) - { - if (_isAppleArm64ABI) - { - if (!isValueType) - { - Debug.Assert((parmSize & (parmSize - 1)) == 0); - return parmSize; - } - - if (isFloatHfa) - { - Debug.Assert((parmSize % 4) == 0); - return parmSize; - } - } - - return base.StackElemSize(parmSize, isValueType, isFloatHfa); - } - - public override bool IsArgPassedByRefBySize(int size) => size > EnregisteredParamTypeMaxSize; - - public override int GetRetBuffArgOffset(bool hasThis) - => _layout.FirstGCRefMapSlot; - - public override IEnumerable EnumerateArgs() - { - int idxGenReg = ComputeInitialNumRegistersUsed(); - int idxFPReg = 0; - int ofsStack = 0; - - for (int argNum = 0; argNum < NumFixedArgs; argNum++) - { - CorElementType argType = GetArgumentType(argNum, out ArgTypeInfo argTypeInfo); - int argSize = GetElemSize(argType, argTypeInfo, _layout.PointerSize); - bool isHomogeneousAggregate = argType == CorElementType.ValueType && argTypeInfo.IsHomogeneousAggregate; - bool isByRef = argType == CorElementType.Byref - || (argType == CorElementType.ValueType - && argSize > EnregisteredParamTypeMaxSize - && (!isHomogeneousAggregate || IsVarArg)); - - int effectiveArgSize = isByRef ? _layout.PointerSize : argSize; - int cFPRegs = 0; - bool isFloatHfa = false; - CorElementType fpElementType = argType; - - switch (argType) - { - case CorElementType.R4: - cFPRegs = 1; - fpElementType = CorElementType.R4; - break; - - case CorElementType.R8: - cFPRegs = 1; - fpElementType = CorElementType.R8; - break; - - case CorElementType.ValueType: - if (isHomogeneousAggregate) - { - int haElementSize = argTypeInfo.HomogeneousAggregateElementSize; - isFloatHfa = haElementSize == 4; - fpElementType = haElementSize == 4 ? CorElementType.R4 : CorElementType.R8; - cFPRegs = argSize / haElementSize; - } - break; - } - - int cbArg = StackElemSize(effectiveArgSize, argType == CorElementType.ValueType, isFloatHfa); - IReadOnlyList locations; - - if (cFPRegs > 0 && !IsVarArg) - { - if (idxFPReg + cFPRegs <= NumFloatArgumentRegisters) - { - List hfaLocations = new(cFPRegs); - for (int i = 0; i < cFPRegs; i++) - { - hfaLocations.Add(new ArgLocation - { - Kind = ArgLocationKind.FpRegister, - TransitionBlockOffset = _layout.OffsetOfFloatArgumentRegisters + ((idxFPReg + i) * FloatRegisterSize), - Size = FloatRegisterSize, - ElementType = fpElementType, - }); - } - - idxFPReg += cFPRegs; - locations = hfaLocations; - goto Yield; - } - - idxFPReg = NumFloatArgumentRegisters; - } - else - { - int regSlots = AlignUp(cbArg, _layout.PointerSize) / _layout.PointerSize; - if (idxGenReg + regSlots <= NumArgumentRegisters) - { - locations = - [ - new ArgLocation - { - Kind = ArgLocationKind.GpRegister, - TransitionBlockOffset = _layout.ArgumentRegistersOffset + (idxGenReg * _layout.PointerSize), - Size = regSlots * _layout.PointerSize, - ElementType = argType, - } - ]; - idxGenReg += regSlots; - goto Yield; - } - - bool allowVarArgSplit = _layout.OperatingSystem == RuntimeInfoOperatingSystem.Windows - && IsVarArg - && idxGenReg < NumArgumentRegisters - && !isHomogeneousAggregate; - if (allowVarArgSplit) - { - int headSize = (NumArgumentRegisters - idxGenReg) * _layout.PointerSize; - locations = - [ - new ArgLocation - { - Kind = ArgLocationKind.GpRegister, - TransitionBlockOffset = _layout.ArgumentRegistersOffset + (idxGenReg * _layout.PointerSize), - Size = headSize, - ElementType = argType, - }, - new ArgLocation - { - Kind = ArgLocationKind.Stack, - TransitionBlockOffset = _layout.OffsetOfArgs + ofsStack, - Size = cbArg - headSize, - ElementType = argType, - } - ]; - ofsStack += cbArg - headSize; - idxGenReg = NumArgumentRegisters; - goto Yield; - } - - idxGenReg = NumArgumentRegisters; - } - - if (_isAppleArm64ABI) - { - int alignment = !argTypeInfo.IsValueType - ? cbArg - : isFloatHfa ? 4 : 8; - ofsStack = AlignUp(ofsStack, alignment); - } - - locations = - [ - new ArgLocation - { - Kind = ArgLocationKind.Stack, - TransitionBlockOffset = _layout.OffsetOfArgs + ofsStack, - Size = cbArg, - ElementType = argType, - } - ]; - ofsStack += cbArg; - - Yield: - yield return new ArgLocDesc - { - ArgType = argType, - ArgSize = argSize, - ArgTypeInfo = argTypeInfo, - IsByRef = isByRef, - Locations = locations, - }; - } - } -} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/RiscV64LoongArch64ArgIterator.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/RiscV64LoongArch64ArgIterator.cs deleted file mode 100644 index b04e7e3d35b19f..00000000000000 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/RiscV64LoongArch64ArgIterator.cs +++ /dev/null @@ -1,98 +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; - -namespace Microsoft.Diagnostics.DataContractReader.Contracts.CallingConventionHelpers; - -/// -/// Shared iterator for the current cDAC RISC-V64 / LoongArch64 implementation. -/// Floating-point scalars use the FP bank, integer-like values use the GP bank, -/// and overflow spills to the stack. Large value types are passed by implicit byref. -/// -internal sealed class RiscV64LoongArch64ArgIterator : ArgIteratorBase -{ - public override int NumArgumentRegisters => 8; - public override int NumFloatArgumentRegisters => 8; - public override int FloatRegisterSize => 8; - public override int EnregisteredParamTypeMaxSize => 16; - public override int EnregisteredReturnTypeIntegerMaxSize => 16; - public override int StackSlotSize => 8; - public override bool IsRetBuffPassedAsFirstArg => true; - - public RiscV64LoongArch64ArgIterator( - TransitionBlockLayout layout, - ArgIteratorData argData, - bool hasParamType, - bool hasAsyncContinuation) - : base(layout, argData, hasParamType, hasAsyncContinuation) - { - } - - public override bool IsArgPassedByRefBySize(int size) => size > EnregisteredParamTypeMaxSize; - - public override IEnumerable EnumerateArgs() - { - int idxGenReg = ComputeInitialNumRegistersUsed(); - int idxFPReg = 0; - int ofsStack = 0; - - for (int argNum = 0; argNum < NumFixedArgs; argNum++) - { - CorElementType argType = GetArgumentType(argNum, out ArgTypeInfo argTypeInfo); - int argSize = GetElemSize(argType, argTypeInfo, _layout.PointerSize); - bool isByRef = argType == CorElementType.Byref - || (argType == CorElementType.ValueType && argSize > EnregisteredParamTypeMaxSize); - int effectiveArgSize = isByRef ? _layout.PointerSize : argSize; - int cbArg = StackElemSize(effectiveArgSize, argType == CorElementType.ValueType, false); - - ArgLocation location; - if ((argType == CorElementType.R4 || argType == CorElementType.R8) && idxFPReg < NumFloatArgumentRegisters && !IsVarArg) - { - location = new ArgLocation - { - Kind = ArgLocationKind.FpRegister, - TransitionBlockOffset = _layout.OffsetOfFloatArgumentRegisters + (idxFPReg * FloatRegisterSize), - Size = FloatRegisterSize, - ElementType = argType, - }; - idxFPReg++; - } - else - { - int regSlots = AlignUp(cbArg, _layout.PointerSize) / _layout.PointerSize; - if (idxGenReg + regSlots <= NumArgumentRegisters) - { - location = new ArgLocation - { - Kind = ArgLocationKind.GpRegister, - TransitionBlockOffset = _layout.ArgumentRegistersOffset + (idxGenReg * _layout.PointerSize), - Size = regSlots * _layout.PointerSize, - ElementType = argType, - }; - idxGenReg += regSlots; - } - else - { - location = new ArgLocation - { - Kind = ArgLocationKind.Stack, - TransitionBlockOffset = _layout.OffsetOfArgs + ofsStack, - Size = cbArg, - ElementType = argType, - }; - ofsStack += cbArg; - } - } - - yield return new ArgLocDesc - { - ArgType = argType, - ArgSize = argSize, - ArgTypeInfo = argTypeInfo, - IsByRef = isByRef, - Locations = [location], - }; - } - } -} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/X86ArgIterator.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/X86ArgIterator.cs deleted file mode 100644 index 0335eff14fbfff..00000000000000 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/X86ArgIterator.cs +++ /dev/null @@ -1,281 +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.Diagnostics; - -namespace Microsoft.Diagnostics.DataContractReader.Contracts.CallingConventionHelpers; - -/// -/// x86 managed calling-convention iterator. User arguments are enregistered into -/// ECX/EDX when eligible; remaining arguments are laid out on the stack in the -/// native x86 reverse-push order. -/// -internal sealed class X86ArgIterator : ArgIteratorBase -{ - private enum ParamTypeLocation - { - Stack, - Ecx, - Edx, - } - - private enum AsyncContinuationLocation - { - Stack, - Ecx, - Edx, - } - - private ParamTypeLocation _paramTypeLoc; - private AsyncContinuationLocation _asyncContinuationLoc; - - public override int NumArgumentRegisters => 2; - public override int NumFloatArgumentRegisters => 0; - public override int FloatRegisterSize => 0; - public override int EnregisteredParamTypeMaxSize => 0; - public override int EnregisteredReturnTypeIntegerMaxSize => 4; - public override int StackSlotSize => 4; - public override bool IsRetBuffPassedAsFirstArg => true; - - public X86ArgIterator( - TransitionBlockLayout layout, - ArgIteratorData argData, - bool hasParamType, - bool hasAsyncContinuation) - : base(layout, argData, hasParamType, hasAsyncContinuation) - { - } - - public override int GetThisOffset() - => _layout.ArgumentRegistersOffset + _layout.PointerSize; - - public override int OffsetFromGCRefMapPos(int pos) - { - if (pos < NumArgumentRegisters) - { - return _layout.FirstGCRefMapSlot + SizeOfArgumentRegisters - ((pos + 1) * _layout.PointerSize); - } - - return _layout.OffsetOfArgs + ((pos - NumArgumentRegisters) * _layout.PointerSize); - } - - public override int GetRetBuffArgOffset(bool hasThis) - => _layout.ArgumentRegistersOffset + (hasThis ? 0 : _layout.PointerSize); - - public override uint CbStackPop() - => IsVarArg ? 0u : SizeOfArgStack(); - - public override int GetVASigCookieOffset() - { - Debug.Assert(IsVarArg); - return _layout.SizeOfTransitionBlock; - } - - public override int GetParamTypeArgOffset() - { - Debug.Assert(HasParamType); - _ = SizeOfArgStack(); - - return _paramTypeLoc switch - { - ParamTypeLocation.Ecx => _layout.ArgumentRegistersOffset + _layout.PointerSize, - ParamTypeLocation.Edx => _layout.ArgumentRegistersOffset, - _ => _layout.SizeOfTransitionBlock, - }; - } - - public override int GetAsyncContinuationArgOffset() - { - Debug.Assert(HasAsyncContinuation); - _ = SizeOfArgStack(); - - return _asyncContinuationLoc switch - { - AsyncContinuationLocation.Ecx => _layout.ArgumentRegistersOffset + _layout.PointerSize, - AsyncContinuationLocation.Edx => _layout.ArgumentRegistersOffset, - _ => HasParamType && _paramTypeLoc == ParamTypeLocation.Stack - ? _layout.SizeOfTransitionBlock + _layout.PointerSize - : _layout.SizeOfTransitionBlock, - }; - } - - protected override int ComputeInitialNumRegistersUsed() - { - int numRegistersUsed = 0; - - if (HasThis) - { - numRegistersUsed++; - } - - if (HasRetBuffArg() && IsRetBuffPassedAsFirstArg) - { - numRegistersUsed++; - } - - return numRegistersUsed; - } - - protected override void ComputeSizeOfArgStack() - { - int numRegistersUsed = 0; - int sizeOfArgStack = 0; - - if (HasThis) - { - numRegistersUsed++; - } - - if (HasRetBuffArg() && IsRetBuffPassedAsFirstArg) - { - numRegistersUsed++; - } - - if (IsVarArg) - { - sizeOfArgStack += _layout.PointerSize; - numRegistersUsed = NumArgumentRegisters; - } - - for (int argNum = 0; argNum < NumFixedArgs; argNum++) - { - CorElementType argType = GetArgumentType(argNum, out ArgTypeInfo argTypeInfo); - int argSize = GetElemSize(argType, argTypeInfo, _layout.PointerSize); - if (!IsArgumentInRegister(ref numRegistersUsed, argType, argSize)) - { - sizeOfArgStack += StackElemSize(argSize); - } - } - - if (HasAsyncContinuation) - { - if (numRegistersUsed < NumArgumentRegisters) - { - numRegistersUsed++; - _asyncContinuationLoc = numRegistersUsed == 1 - ? AsyncContinuationLocation.Ecx - : AsyncContinuationLocation.Edx; - } - else - { - sizeOfArgStack += _layout.PointerSize; - _asyncContinuationLoc = AsyncContinuationLocation.Stack; - } - } - - if (HasParamType) - { - if (numRegistersUsed < NumArgumentRegisters) - { - numRegistersUsed++; - _paramTypeLoc = numRegistersUsed == 1 - ? ParamTypeLocation.Ecx - : ParamTypeLocation.Edx; - } - else - { - sizeOfArgStack += _layout.PointerSize; - _paramTypeLoc = ParamTypeLocation.Stack; - } - } - - _nSizeOfArgStack = AlignUp(sizeOfArgStack, StackElemSize(_layout.PointerSize)); - } - - public override IEnumerable EnumerateArgs() - { - int stackSize = (int)SizeOfArgStack(); - int numRegistersUsed = ComputeInitialNumRegistersUsed(); - int ofsStack = _layout.OffsetOfArgs + stackSize; - - if (IsVarArg) - { - numRegistersUsed = NumArgumentRegisters; - } - - for (int argNum = 0; argNum < NumFixedArgs; argNum++) - { - CorElementType argType = GetArgumentType(argNum, out ArgTypeInfo argTypeInfo); - int argSize = GetElemSize(argType, argTypeInfo, _layout.PointerSize); - - ArgLocation location; - if (IsArgumentInRegister(ref numRegistersUsed, argType, argSize)) - { - location = new ArgLocation - { - Kind = ArgLocationKind.GpRegister, - TransitionBlockOffset = _layout.ArgumentRegistersOffset + ((NumArgumentRegisters - numRegistersUsed) * _layout.PointerSize), - Size = _layout.PointerSize, - ElementType = argType, - }; - } - else - { - int stackElemSize = StackElemSize(argSize); - ofsStack -= stackElemSize; - location = new ArgLocation - { - Kind = ArgLocationKind.Stack, - TransitionBlockOffset = ofsStack, - Size = stackElemSize, - ElementType = argType, - }; - } - - yield return new ArgLocDesc - { - ArgType = argType, - ArgSize = argSize, - ArgTypeInfo = argTypeInfo, - IsByRef = argType == CorElementType.Byref, - Locations = [location], - }; - } - } - - private static bool IsArgumentInRegister(ref int numRegistersUsed, CorElementType elementType, int argSize) - { - if (numRegistersUsed >= 2) - { - return false; - } - - bool enregister = elementType switch - { - CorElementType.Boolean or - CorElementType.Char or - CorElementType.I1 or - CorElementType.U1 or - CorElementType.I2 or - CorElementType.U2 or - CorElementType.I4 or - CorElementType.U4 or - CorElementType.I or - CorElementType.U or - CorElementType.Ptr or - CorElementType.Byref or - CorElementType.Class or - CorElementType.Object or - CorElementType.String or - CorElementType.SzArray or - CorElementType.Array or - CorElementType.FnPtr => true, - CorElementType.ValueType => argSize is 1 or 2 or 4, - CorElementType.R4 or - CorElementType.R8 or - CorElementType.I8 or - CorElementType.U8 or - CorElementType.TypedByRef => false, - _ => false, - }; - - if (!enregister) - { - return false; - } - - numRegistersUsed++; - return true; - } -} diff --git a/src/native/managed/cdac/tests/CallingConvention/Arm32CallingConventionTests.cs b/src/native/managed/cdac/tests/CallingConvention/Arm32CallingConventionTests.cs deleted file mode 100644 index db23e00206c9a5..00000000000000 --- a/src/native/managed/cdac/tests/CallingConvention/Arm32CallingConventionTests.cs +++ /dev/null @@ -1,306 +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 Microsoft.Diagnostics.DataContractReader.Contracts; -using Xunit; - -namespace Microsoft.Diagnostics.DataContractReader.Tests; - -/// -/// ARM32 (AAPCS, hard-float) calling-convention tests. Up to 4 GP args in -/// R0-R3, FP args in S0-S15 / D0-D7 via a bitmap allocator. -/// -public class Arm32CallingConventionTests -{ - private static CallConvTestCase Case => CallConvCases.Arm32; - - private static int OffsetOfNthGPReg(int n) => Case.ArgumentRegistersOffset + n * Case.PointerSize; - private static int OffsetOfNthStackSlot(int n) => Case.OffsetOfArgs + n * Case.PointerSize; - - [Fact] - public void FourInts_FillR0_R3() - { - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( - Case, - sig => - { - sig.Return(CorElementType.Void); - for (int i = 0; i < Case.NumArgumentRegisters; i++) sig.Param(CorElementType.I4); - }); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Equal(Case.NumArgumentRegisters, layout.Arguments.Count); - for (int i = 0; i < Case.NumArgumentRegisters; i++) - { - Assert.Equal(OffsetOfNthGPReg(i), layout.Arguments[i].Slots[0].Offset); - } - } - - [Fact] - public void FifthInt_GoesToStack() - { - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( - Case, - sig => - { - sig.Return(CorElementType.Void); - for (int i = 0; i < Case.NumArgumentRegisters + 1; i++) sig.Param(CorElementType.I4); - }); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Equal(Case.NumArgumentRegisters + 1, layout.Arguments.Count); - Assert.Equal(OffsetOfNthStackSlot(0), layout.Arguments[4].Slots[0].Offset); - } - - [Theory] - [InlineData(CorElementType.I1, 4)] - [InlineData(CorElementType.I2, 4)] - [InlineData(CorElementType.I4, 4)] - [InlineData(CorElementType.I4, 5)] - public void IntArgs_FillR0_R3_AndSpillToStack(CorElementType elementType, int count) - { - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( - Case, - sig => - { - sig.Return(CorElementType.Void); - for (int i = 0; i < count; i++) - { - sig.Param(elementType); - } - }); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Equal(count, layout.Arguments.Count); - - for (int i = 0; i < count; i++) - { - int expectedOffset = i < Case.NumArgumentRegisters - ? OffsetOfNthGPReg(i) - : OffsetOfNthStackSlot(i - Case.NumArgumentRegisters); - Assert.Equal(expectedOffset, layout.Arguments[i].Slots[0].Offset); - } - } - - [Fact] - public void InstanceMethod_ThisOffsetIsR0() - { - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( - Case, hasThis: true, - sig => sig.Return(CorElementType.Void)); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Equal(OffsetOfNthGPReg(0), layout.ThisOffset); - } - - [Theory] - [InlineData(CorElementType.R4)] - [InlineData(CorElementType.R8)] - public void FloatArg_GoesToFirstFPRegister(CorElementType elementType) - { - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( - Case, - sig => sig.Return(CorElementType.Void).Param(elementType)); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Single(layout.Arguments); - Assert.Equal(Case.OffsetOfFloatArgumentRegisters, layout.Arguments[0].Slots[0].Offset); - } - - [Fact] - public void MixedIntAndFloat_UseSeparateBanks() - { - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( - Case, - sig => sig.Return(CorElementType.Void).Param(CorElementType.I4).Param(CorElementType.R8)); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Equal(2, layout.Arguments.Count); - Assert.Equal(OffsetOfNthGPReg(0), layout.Arguments[0].Slots[0].Offset); - Assert.Equal(Case.OffsetOfFloatArgumentRegisters, layout.Arguments[1].Slots[0].Offset); - } - - [Theory] - [InlineData(CorElementType.Object)] - [InlineData(CorElementType.String)] - public void GCReferenceArgs_GoToGPRegs_NotByref(CorElementType refType) - { - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( - Case, - sig => sig.Return(CorElementType.Void).Param(refType)); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Single(layout.Arguments); - Assert.False(layout.Arguments[0].IsPassedByRef); - Assert.Equal(OffsetOfNthGPReg(0), layout.Arguments[0].Slots[0].Offset); - } - - [Fact] - public void I8_AfterI4_AlignsToEvenRegisterPair() - { - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( - Case, - sig => sig - .Return(CorElementType.Void) - .Param(CorElementType.I4) - .Param(CorElementType.I8) - .Param(CorElementType.I4)); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Equal(3, layout.Arguments.Count); - Assert.Equal(OffsetOfNthGPReg(0), layout.Arguments[0].Slots[0].Offset); - Assert.Equal(OffsetOfNthGPReg(2), layout.Arguments[1].Slots[0].Offset); - Assert.Equal(OffsetOfNthStackSlot(0), layout.Arguments[2].Slots[0].Offset); - } - - [Fact] - public void LargeStruct_PassedByValueOnStack() - { - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( - Case, hasThis: false, - (rts, sig) => - { - MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( - rts, - "BigStruct", - structSize: 24, - fields: - [ - new(0, CorElementType.I4), - new(4, CorElementType.I4), - new(8, CorElementType.I4), - new(12, CorElementType.I4), - new(16, CorElementType.I4), - new(20, CorElementType.I4), - ]); - sig.Return(CorElementType.Void); - for (int i = 0; i < Case.NumArgumentRegisters; i++) - { - sig.Param(CorElementType.I4); - } - sig.ParamValueType(new TargetPointer(mt.Address)); - }); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Equal(Case.NumArgumentRegisters + 1, layout.Arguments.Count); - Assert.False(layout.Arguments[4].IsPassedByRef); - Assert.Equal(OffsetOfNthStackSlot(0), layout.Arguments[4].Slots[0].Offset); - } - - [Fact] - public void EightInts_FourInRegs_FourOnStack() - { - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( - Case, - sig => - { - sig.Return(CorElementType.Void); - for (int i = 0; i < 8; i++) - { - sig.Param(CorElementType.I4); - } - }); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Equal(8, layout.Arguments.Count); - - for (int i = 0; i < 8; i++) - { - int expectedOffset = i < Case.NumArgumentRegisters - ? OffsetOfNthGPReg(i) - : OffsetOfNthStackSlot(i - Case.NumArgumentRegisters); - Assert.Equal(expectedOffset, layout.Arguments[i].Slots[0].Offset); - } - } - - [Fact] - public void TypedReference_ConsumesR0AndR1() - { - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( - Case, hasThis: false, - (rts, sig) => - { - MockMethodTable typedRefMT = MockDescriptors.CallingConvention.AddValueTypeMethodTable( - rts, - "System.TypedReference", - structSize: 8, - fields: [new(0, CorElementType.Byref), new(4, CorElementType.I)]); - rts.SetTypedReferenceMethodTable(typedRefMT.Address); - sig.Return(CorElementType.Void).Param(CorElementType.TypedByRef).Param(CorElementType.I4); - }); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Equal(2, layout.Arguments.Count); - Assert.False(layout.Arguments[0].IsPassedByRef); - Assert.Equal(OffsetOfNthGPReg(0), layout.Arguments[0].Slots[0].Offset); - Assert.Equal(OffsetOfNthGPReg(2), layout.Arguments[1].Slots[0].Offset); - } - - [Theory] - [InlineData(false, 1)] - [InlineData(true, 2)] - public void ReturnBuffer_ShiftsFirstUserArg(bool hasThis, int expectedUserArgRegister) - { - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( - Case, - hasThis, - (rts, sig) => - { - MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( - rts, - "BigReturn", - structSize: 24, - fields: - [ - new(0, CorElementType.I4), - new(4, CorElementType.I4), - new(8, CorElementType.I4), - new(12, CorElementType.I4), - new(16, CorElementType.I4), - new(20, CorElementType.I4), - ]); - sig.ReturnValueType(new TargetPointer(mt.Address)).Param(CorElementType.I4); - }); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Single(layout.Arguments); - Assert.Equal(OffsetOfNthGPReg(expectedUserArgRegister), layout.Arguments[0].Slots[0].Offset); - } - - // ---- Soft-float (armel) ---- - - [Theory] - [InlineData(CorElementType.R4)] - [InlineData(CorElementType.R8)] - public void SoftFloat_FloatArg_GoesToGPReg_NotFPReg(CorElementType elementType) - { - // On armel (soft-float), FeatureArmSoftFP is present and all arguments - // -- including floats and doubles -- go through the GP register / stack - // path. This mirrors the native #ifndef ARM_SOFTFP gate in - // callingconvention.h:1546. - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( - Case, hasThis: false, - (_, sig) => sig.Return(CorElementType.Void).Param(elementType), - isArmSoftFP: true); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Single(layout.Arguments); - // On soft-float the arg should be in R0 (first GP reg), not S0/D0. - Assert.Equal(OffsetOfNthGPReg(0), layout.Arguments[0].Slots[0].Offset); - } - - [Fact] - public void SoftFloat_MixedIntAndFloat_BothInGPRegs() - { - // On armel, int and float args share the same GP register bank. - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( - Case, hasThis: false, - (_, sig) => sig.Return(CorElementType.Void).Param(CorElementType.I4).Param(CorElementType.R4), - isArmSoftFP: true); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Equal(2, layout.Arguments.Count); - Assert.Equal(OffsetOfNthGPReg(0), layout.Arguments[0].Slots[0].Offset); - Assert.Equal(OffsetOfNthGPReg(1), layout.Arguments[1].Slots[0].Offset); - } -} diff --git a/src/native/managed/cdac/tests/CallingConvention/Arm64CallingConventionTests.cs b/src/native/managed/cdac/tests/CallingConvention/Arm64CallingConventionTests.cs deleted file mode 100644 index 807bfa184789ac..00000000000000 --- a/src/native/managed/cdac/tests/CallingConvention/Arm64CallingConventionTests.cs +++ /dev/null @@ -1,656 +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 Microsoft.Diagnostics.DataContractReader.Contracts; -using Xunit; - -namespace Microsoft.Diagnostics.DataContractReader.Tests; - -/// -/// ARM64 (AAPCS64) calling-convention tests. Up to 8 GP args in X0-X7, FP -/// args in V0-V7. HFAs (homogeneous aggregates of 1-4 floats/doubles) are -/// passed in consecutive FP registers; large value types via implicit byref. -/// -public class Arm64CallingConventionTests -{ - private static CallConvTestCase Case => CallConvCases.Arm64Windows; - - private static int OffsetOfNthGPReg(int n) => Case.ArgumentRegistersOffset + n * Case.PointerSize; - private static int OffsetOfNthFPReg(int n) => Case.OffsetOfFloatArgumentRegisters!.Value + n * Case.FloatRegisterSize; - private static int OffsetOfNthStackSlot(int n) => Case.OffsetOfArgs + n * Case.PointerSize; - - [Fact] - public void EightInts_FillX0_X7() - { - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( - Case, - sig => - { - sig.Return(CorElementType.Void); - for (int i = 0; i < Case.NumArgumentRegisters; i++) sig.Param(CorElementType.I8); - }); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Equal(Case.NumArgumentRegisters, layout.Arguments.Count); - for (int i = 0; i < Case.NumArgumentRegisters; i++) - Assert.Equal(OffsetOfNthGPReg(i), layout.Arguments[i].Slots[0].Offset); - } - - [Fact] - public void EightDoubles_FillV0_V7() - { - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( - Case, - sig => - { - sig.Return(CorElementType.Void); - for (int i = 0; i < 8; i++) sig.Param(CorElementType.R8); - }); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Equal(8, layout.Arguments.Count); - for (int i = 0; i < 8; i++) - Assert.Equal(OffsetOfNthFPReg(i), layout.Arguments[i].Slots[0].Offset); - } - - [Fact] - public void InstanceMethod_ThisOffsetIsX0() - { - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( - Case, hasThis: true, - sig => sig.Return(CorElementType.Void)); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Equal(OffsetOfNthGPReg(0), layout.ThisOffset); - } - - [Theory] - [InlineData(CorElementType.I1, 8)] - [InlineData(CorElementType.I2, 8)] - [InlineData(CorElementType.I4, 8)] - [InlineData(CorElementType.I8, 8)] - [InlineData(CorElementType.U1, 8)] - [InlineData(CorElementType.U2, 8)] - [InlineData(CorElementType.U4, 8)] - [InlineData(CorElementType.U8, 8)] - [InlineData(CorElementType.I4, 9)] - public void IntArgs_FillX0_X7_AndSpillToStack(CorElementType elementType, int count) - { - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( - Case, - sig => - { - sig.Return(CorElementType.Void); - for (int i = 0; i < count; i++) - { - sig.Param(elementType); - } - }); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Equal(count, layout.Arguments.Count); - - for (int i = 0; i < count; i++) - { - int expectedOffset = i < Case.NumArgumentRegisters - ? OffsetOfNthGPReg(i) - : OffsetOfNthStackSlot(i - Case.NumArgumentRegisters); - Assert.Equal(expectedOffset, layout.Arguments[i].Slots[0].Offset); - } - } - - [Theory] - [InlineData(CorElementType.R4, 1)] - [InlineData(CorElementType.R8, 1)] - [InlineData(CorElementType.R4, 8)] - [InlineData(CorElementType.R8, 8)] - [InlineData(CorElementType.R4, 10)] - [InlineData(CorElementType.R8, 10)] - public void FloatArgs_FillV0_V7_AndSpillToStack(CorElementType elementType, int count) - { - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( - Case, - sig => - { - sig.Return(CorElementType.Void); - for (int i = 0; i < count; i++) - { - sig.Param(elementType); - } - }); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Equal(count, layout.Arguments.Count); - - for (int i = 0; i < count; i++) - { - int expectedOffset = i < Case.NumFloatArgumentRegisters - ? OffsetOfNthFPReg(i) - : OffsetOfNthStackSlot(i - Case.NumFloatArgumentRegisters); - Assert.Equal(expectedOffset, layout.Arguments[i].Slots[0].Offset); - } - } - - [Fact] - public void MixedIntAndFloat_UseSeparateBanks() - { - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( - Case, - sig => sig - .Return(CorElementType.Void) - .Param(CorElementType.I8) - .Param(CorElementType.R8) - .Param(CorElementType.I8) - .Param(CorElementType.R8)); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Equal(4, layout.Arguments.Count); - Assert.Equal(OffsetOfNthGPReg(0), layout.Arguments[0].Slots[0].Offset); - Assert.Equal(OffsetOfNthFPReg(0), layout.Arguments[1].Slots[0].Offset); - Assert.Equal(OffsetOfNthGPReg(1), layout.Arguments[2].Slots[0].Offset); - Assert.Equal(OffsetOfNthFPReg(1), layout.Arguments[3].Slots[0].Offset); - } - - [Theory] - [InlineData(false, false, false, false)] - [InlineData(true, false, false, false)] - [InlineData(true, true, false, false)] - [InlineData(false, false, true, false)] - [InlineData(true, false, true, false)] - [InlineData(true, true, true, true)] - public void HiddenArgs_DoNotAffectFirstUserDouble( - bool hasThis, - bool hasRetBuf, - bool hasParamType, - bool hasAsyncCont) - { - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( - Case, - hasThis, - (rts, sig) => - { - if (hasRetBuf) - { - MockMethodTable bigMT = MockDescriptors.CallingConvention.AddValueTypeMethodTable( - rts, - "BigReturn", - structSize: 24, - fields: - [ - new(0, CorElementType.I8), - new(8, CorElementType.I8), - new(16, CorElementType.I8), - ]); - sig.ReturnValueType(new TargetPointer(bigMT.Address)); - } - else - { - sig.Return(CorElementType.Void); - } - - sig.Param(CorElementType.R8); - }, - hasParamType, - hasAsyncCont); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Single(layout.Arguments); - Assert.Equal(OffsetOfNthFPReg(0), layout.Arguments[0].Slots[0].Offset); - } - - [Fact] - public void LargeStruct_ImplicitByRef_ConsumesOneGPReg() - { - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( - Case, hasThis: false, - (rts, sig) => - { - MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( - rts, - "BigStruct", - structSize: 24, - fields: - [ - new(0, CorElementType.I8), - new(8, CorElementType.I8), - new(16, CorElementType.I8), - ]); - sig.Return(CorElementType.Void) - .ParamValueType(new TargetPointer(mt.Address)) - .Param(CorElementType.I8); - }); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Equal(2, layout.Arguments.Count); - Assert.True(layout.Arguments[0].IsPassedByRef); - Assert.Equal(OffsetOfNthGPReg(0), layout.Arguments[0].Slots[0].Offset); - Assert.Equal(OffsetOfNthGPReg(1), layout.Arguments[1].Slots[0].Offset); - } - - [Fact] - public void SixteenByteStruct_NotByRef_ConsumesTwoGPRegs() - { - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( - Case, hasThis: false, - (rts, sig) => - { - MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( - rts, - "TwoLongs", - structSize: 16, - fields: [new(0, CorElementType.I8), new(8, CorElementType.I8)]); - sig.Return(CorElementType.Void) - .ParamValueType(new TargetPointer(mt.Address)) - .Param(CorElementType.I8); - }); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Equal(2, layout.Arguments.Count); - Assert.False(layout.Arguments[0].IsPassedByRef); - Assert.Equal(OffsetOfNthGPReg(0), layout.Arguments[0].Slots[0].Offset); - Assert.Equal(OffsetOfNthGPReg(2), layout.Arguments[1].Slots[0].Offset); - } - - [Fact] - public void TypedReference_ConsumesTwoGPRegs() - { - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( - Case, hasThis: false, - (rts, sig) => - { - MockMethodTable typedRefMT = MockDescriptors.CallingConvention.AddValueTypeMethodTable( - rts, - "System.TypedReference", - structSize: 16, - fields: [new(0, CorElementType.Byref), new(8, CorElementType.I)]); - rts.SetTypedReferenceMethodTable(typedRefMT.Address); - sig.Return(CorElementType.Void) - .Param(CorElementType.TypedByRef) - .Param(CorElementType.I8); - }); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Equal(2, layout.Arguments.Count); - Assert.False(layout.Arguments[0].IsPassedByRef); - Assert.Equal(OffsetOfNthGPReg(0), layout.Arguments[0].Slots[0].Offset); - Assert.Equal(OffsetOfNthGPReg(2), layout.Arguments[1].Slots[0].Offset); - } - - [Theory] - [InlineData(CorElementType.Object)] - [InlineData(CorElementType.String)] - public void GCReferenceArgs_GoToGPRegs_NotByref(CorElementType refType) - { - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( - Case, - sig => sig.Return(CorElementType.Void).Param(refType)); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Single(layout.Arguments); - Assert.False(layout.Arguments[0].IsPassedByRef); - Assert.Equal(OffsetOfNthGPReg(0), layout.Arguments[0].Slots[0].Offset); - } - - [Fact] - public void TenArgs_EightInRegs_TwoOnStack() - { - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( - Case, - sig => - { - sig.Return(CorElementType.Void); - for (int i = 0; i < 10; i++) - { - sig.Param(CorElementType.I8); - } - }); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Equal(10, layout.Arguments.Count); - - for (int i = 0; i < 10; i++) - { - int expectedOffset = i < Case.NumArgumentRegisters - ? OffsetOfNthGPReg(i) - : OffsetOfNthStackSlot(i - Case.NumArgumentRegisters); - Assert.Equal(expectedOffset, layout.Arguments[i].Slots[0].Offset); - } - } - - [Fact] - public void VarArgs_DoubleUsesGPReg_NotFPReg() - { - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( - Case, - sig => sig.VarArg().Return(CorElementType.Void).Param(CorElementType.R8)); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.NotNull(layout.VarArgCookieOffset); - Assert.Single(layout.Arguments); - Assert.Equal(OffsetOfNthGPReg(0), layout.VarArgCookieOffset); - Assert.Equal(OffsetOfNthGPReg(1), layout.Arguments[0].Slots[0].Offset); - } - - [Fact] - public void Return_LargeStruct_RetBufInX8_FirstUserArgStaysInX0() - { - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( - Case, hasThis: false, - (rts, sig) => - { - MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( - rts, - "BigReturn", - structSize: 24, - fields: - [ - new(0, CorElementType.I8), - new(8, CorElementType.I8), - new(16, CorElementType.I8), - ]); - sig.ReturnValueType(new TargetPointer(mt.Address)).Param(CorElementType.I8); - }); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Single(layout.Arguments); - Assert.Equal(OffsetOfNthGPReg(0), layout.Arguments[0].Slots[0].Offset); - } - - // ---- Homogeneous Floating-point Aggregate (HFA) helpers ---- - - private const uint WFlagsLow_IsHFA = 0x800; - - /// - /// Allocates a value-type MethodTable representing an HFA of - /// elements of (R4 or R8). Sets the IsHFA flag so the - /// signature provider reports IsHomogeneousAggregate=true with the right - /// element size. - /// - private static MockMethodTable AddHFA(MockDescriptors.RuntimeTypeSystem rts, CorElementType elemType, int count, string name) - { - int elemSize = elemType == CorElementType.R4 ? 4 : 8; - MockDescriptors.CallingConvention.ValueTypeField[] fields = new MockDescriptors.CallingConvention.ValueTypeField[count]; - for (int i = 0; i < count; i++) - fields[i] = new(i * elemSize, elemType); - - MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( - rts, name, structSize: count * elemSize, fields: fields); - mt.MTFlags |= WFlagsLow_IsHFA; - return mt; - } - - // ----- Open audit gaps for ARM64 ----- -#pragma warning disable xUnit1004 // Test methods should not be skipped — these track audit gaps. - - [Fact] - public void Windows_VarArgs_StructSpansX7AndStack() - { - // Cookie consumes one GP slot (X0). 6 user longs fill X1-X6. - // The 16-byte struct's first 8 bytes fit in X7; second 8 bytes spill to stack. - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( - Case, hasThis: false, - (rts, sig) => - { - MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( - rts, "TwoLongs", structSize: 16, - fields: [new(0, CorElementType.I8), new(8, CorElementType.I8)]); - sig.VarArg().Return(CorElementType.Void); - for (int i = 0; i < 6; i++) sig.Param(CorElementType.I8); - sig.ParamValueType(new TargetPointer(mt.Address)); - }); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Equal(7, layout.Arguments.Count); - ArgLayout structArg = layout.Arguments[6]; - Assert.Equal(2, structArg.Slots.Count); - Assert.Equal(OffsetOfNthGPReg(7), structArg.Slots[0].Offset); - Assert.Equal(OffsetOfNthStackSlot(0), structArg.Slots[1].Offset); - } - - // Under varargs, HFAs lose their HFA treatment and go through the GP path. - // They should NOT split across X7/stack -- they should either fit entirely - // in GP regs or go entirely to stack (no split for HFA-shaped composites). - [Fact] - public void Windows_VarArgs_HFA_DoesNotSplit_GoesToStack() - { - // Cookie consumes X0. 7 user longs fill X1-X7 (cookie + 7 = 8 total). - // The 4-float HFA (16 bytes, treated as composite under varargs) can't fit - // in GP regs (all exhausted). Under varargs, the FP path is skipped, so the - // HFA goes entirely to stack -- no split. - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( - Case, hasThis: false, - (rts, sig) => - { - MockMethodTable hfa = AddHFA(rts, CorElementType.R4, 4, "HFA_4F_VarArg"); - sig.VarArg().Return(CorElementType.Void); - for (int i = 0; i < 7; i++) sig.Param(CorElementType.I8); - sig.ParamValueType(new TargetPointer(hfa.Address)); - }); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Equal(8, layout.Arguments.Count); - ArgLayout hfaArg = layout.Arguments[7]; - // Under varargs the HFA is NOT passed in FP regs -- it goes through GP path. - // All GP regs are consumed, so the entire arg goes to stack (no split). - Assert.Single(hfaArg.Slots); - Assert.Equal(OffsetOfNthStackSlot(0), hfaArg.Slots[0].Offset); - } - - // ---- HFA per-slot reporting ---- - // - // ARM64 HFAs (homogeneous aggregates of 1-4 floats/doubles) are passed - // in consecutive FP registers. The iterator emits one ArgLocation per FP - // register with ElementType set to the HFA element type (R4 or R8) -- - // analogous to how SystemV per-eightbyte emission works on AMD64-Unix. - - [Theory] - [InlineData(CorElementType.R4, 2)] - [InlineData(CorElementType.R4, 3)] - [InlineData(CorElementType.R4, 4)] - [InlineData(CorElementType.R8, 2)] - [InlineData(CorElementType.R8, 3)] - [InlineData(CorElementType.R8, 4)] - public void HFA_OccupiesNConsecutiveFPRegs(CorElementType elemType, int count) - { - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( - Case, hasThis: false, - (rts, sig) => - { - MockMethodTable mt = AddHFA(rts, elemType, count, $"HFA_{elemType}_{count}"); - sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); - }); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Single(layout.Arguments); - ArgLayout arg = layout.Arguments[0]; - Assert.False(arg.IsPassedByRef); - Assert.Equal(count, arg.Slots.Count); - - for (int i = 0; i < count; i++) - { - Assert.Equal(elemType, arg.Slots[i].ElementType); - Assert.Equal(OffsetOfNthFPReg(i), arg.Slots[i].Offset); - } - } - - // HFA after some FP args: the HFA's first FP register is at the current - // FP-allocation index, not back at V0. Verifies the iterator correctly - // advances past the HFA so subsequent FP args don't overlap. - [Fact] - public void HFA_AfterTwoDoubles_StartsAtV2() - { - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( - Case, hasThis: false, - (rts, sig) => - { - MockMethodTable hfa = AddHFA(rts, CorElementType.R8, 3, "HFA_3D"); - sig.Return(CorElementType.Void) - .Param(CorElementType.R8) - .Param(CorElementType.R8) - .ParamValueType(new TargetPointer(hfa.Address)) - .Param(CorElementType.R8); - }); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Equal(4, layout.Arguments.Count); - - // 2 doubles in V0, V1 - Assert.Equal(OffsetOfNthFPReg(0), layout.Arguments[0].Slots[0].Offset); - Assert.Equal(OffsetOfNthFPReg(1), layout.Arguments[1].Slots[0].Offset); - - // 3-double HFA in V2, V3, V4 - ArgLayout hfaArg = layout.Arguments[2]; - Assert.Equal(3, hfaArg.Slots.Count); - for (int i = 0; i < 3; i++) - Assert.Equal(OffsetOfNthFPReg(2 + i), hfaArg.Slots[i].Offset); - - // Trailing double picks up at V5 - Assert.Equal(OffsetOfNthFPReg(5), layout.Arguments[3].Slots[0].Offset); - } - - // HFA that exactly fills the remaining FP regs (boundary case): 6 doubles - // before a 2-double HFA → HFA fits in V6 + V7 with no overflow. - [Fact] - public void HFA_FitsExactlyInRemainingFPRegs() - { - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( - Case, hasThis: false, - (rts, sig) => - { - MockMethodTable hfa = AddHFA(rts, CorElementType.R8, 2, "HFA_2D"); - sig.Return(CorElementType.Void); - for (int i = 0; i < 6; i++) sig.Param(CorElementType.R8); - sig.ParamValueType(new TargetPointer(hfa.Address)); - }); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Equal(7, layout.Arguments.Count); - - ArgLayout hfaArg = layout.Arguments[6]; - Assert.Equal(2, hfaArg.Slots.Count); - Assert.Equal(OffsetOfNthFPReg(6), hfaArg.Slots[0].Offset); - Assert.Equal(OffsetOfNthFPReg(7), hfaArg.Slots[1].Offset); - } - - // HFA that doesn't fit in the remaining FP regs: 5 doubles + 4-double HFA - // (needs 4 slots, only 3 remain) → ENTIRE HFA spills to stack, FP regs are - // marked exhausted, and no further FP enregistration happens. - [Fact] - public void HFA_DoesNotFit_EntireHFASpillsToStack() - { - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( - Case, hasThis: false, - (rts, sig) => - { - MockMethodTable hfa = AddHFA(rts, CorElementType.R8, 4, "HFA_4D"); - sig.Return(CorElementType.Void); - for (int i = 0; i < 5; i++) sig.Param(CorElementType.R8); - sig.ParamValueType(new TargetPointer(hfa.Address)); - }); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Equal(6, layout.Arguments.Count); - - ArgLayout hfaArg = layout.Arguments[5]; - // Single stack slot covering the full 32-byte struct payload - Assert.Single(hfaArg.Slots); - Assert.Equal(OffsetOfNthStackSlot(0), hfaArg.Slots[0].Offset); - } - - // HFA placement is byref-free: a 4-double HFA (32 bytes > 16) is normally - // passed by implicit byref for ordinary value types, but the HFA path - // overrides that and keeps it as a multi-FP-reg pass-by-value. - [Fact] - public void HFA_FourDoubles_NotByRef() - { - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( - Case, hasThis: false, - (rts, sig) => - { - MockMethodTable hfa = AddHFA(rts, CorElementType.R8, 4, "HFA_4D"); - sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(hfa.Address)); - }); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Single(layout.Arguments); - Assert.False(layout.Arguments[0].IsPassedByRef); - Assert.Equal(4, layout.Arguments[0].Slots.Count); - for (int i = 0; i < 4; i++) - Assert.Equal(OffsetOfNthFPReg(i), layout.Arguments[0].Slots[i].Offset); - } - - // Original 4-float HFA test (kept for continuity with the earlier audit-gap marker). - [Fact] - public void HFA_FourFloats_ShouldReportFourFPSlots() - { - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( - Case, hasThis: false, - (rts, sig) => - { - MockMethodTable mt = AddHFA(rts, CorElementType.R4, 4, "HFA_4F"); - sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); - }); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Single(layout.Arguments); - Assert.Equal(4, layout.Arguments[0].Slots.Count); - for (int i = 0; i < 4; i++) - Assert.Equal(OffsetOfNthFPReg(i), layout.Arguments[0].Slots[i].Offset); - } - -#pragma warning restore xUnit1004 - - // ----- ValueTypeHandle population for by-value value-type args ----- - // - // On ARM64, non-HFA value-type args of size <= 16 bytes go through the GP - // path as 1 or 2 ValueType-typed slots; > 16 bytes are passed by reference. - // HFAs go through the FP path and the iterator emits per-FP-reg slots with - // ElementType R4/R8 -- those are already GC-classified (no refs) and should - // NOT carry a ValueTypeHandle. - - [Fact] - public void ValueTypeByValue_NonHFA_EnregisteredInTwoGPRegs_PopulatesValueTypeHandle() - { - // 16-byte struct (two longs) -> X0 + X1 contiguously, two ValueType slots. - ulong expectedMtAddress = 0; - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( - Case, hasThis: false, - (rts, sig) => - { - MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( - rts, "TwoLongs16", structSize: 16, - fields: [new(0, CorElementType.I8), new(8, CorElementType.I8)]); - expectedMtAddress = mt.Address; - sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); - }); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Single(layout.Arguments); - ArgLayout arg = layout.Arguments[0]; - Assert.False(arg.IsPassedByRef); - Assert.NotNull(arg.ValueTypeHandle); - Assert.Equal(expectedMtAddress, (ulong)arg.ValueTypeHandle.Value.Address); - } - - [Fact] - public void ValueTypeByValue_HFA_DoesNotPopulateValueTypeHandle() - { - // 4-double HFA -> V0-V3 with per-slot ElementType R8. HFAs cannot carry - // managed refs (they're pure FP), so ValueTypeHandle must be null. - // Pins the "all slots ValueType" discriminator in ComputeValueTypeHandle. - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( - Case, hasThis: false, - (rts, sig) => - { - MockMethodTable hfa = AddHFA(rts, CorElementType.R8, 4, "HFA_4D_Handle"); - sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(hfa.Address)); - }); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Single(layout.Arguments); - ArgLayout arg = layout.Arguments[0]; - Assert.False(arg.IsPassedByRef); - Assert.Equal(4, arg.Slots.Count); - Assert.Null(arg.ValueTypeHandle); - } -} diff --git a/src/native/managed/cdac/tests/CallingConvention/CallConvCases.cs b/src/native/managed/cdac/tests/CallingConvention/CallConvCases.cs index 682d0e7ce8c553..26d40c237c07e5 100644 --- a/src/native/managed/cdac/tests/CallingConvention/CallConvCases.cs +++ b/src/native/managed/cdac/tests/CallingConvention/CallConvCases.cs @@ -27,12 +27,6 @@ public sealed record CallConvTestCase( public static class CallConvCases { - public static readonly CallConvTestCase X86 = new( - "x86", RuntimeInfoArchitecture.X86, RuntimeInfoOperatingSystem.Windows, Is64Bit: false, - TransitionBlockSize: 20, ArgumentRegistersOffset: 0, FirstGCRefMapSlot: 0, - OffsetOfArgs: 20, OffsetOfFloatArgumentRegisters: null, - NumArgumentRegisters: 2, NumFloatArgumentRegisters: 0, FloatRegisterSize: 0); - public static readonly CallConvTestCase AMD64Windows = new( "AMD64-Windows", RuntimeInfoArchitecture.X64, RuntimeInfoOperatingSystem.Windows, Is64Bit: true, TransitionBlockSize: 40, ArgumentRegistersOffset: 40, FirstGCRefMapSlot: 40, @@ -45,40 +39,9 @@ public static class CallConvCases OffsetOfArgs: 48, OffsetOfFloatArgumentRegisters: -128, NumArgumentRegisters: 6, NumFloatArgumentRegisters: 8, FloatRegisterSize: 16); - public static readonly CallConvTestCase Arm32 = new( - "ARM32", RuntimeInfoArchitecture.Arm, RuntimeInfoOperatingSystem.Windows, Is64Bit: false, - TransitionBlockSize: 48, ArgumentRegistersOffset: 32, FirstGCRefMapSlot: 32, - OffsetOfArgs: 48, OffsetOfFloatArgumentRegisters: -68, - NumArgumentRegisters: 4, NumFloatArgumentRegisters: 16, FloatRegisterSize: 4); - - public static readonly CallConvTestCase Arm64Windows = new( - "ARM64-Windows", RuntimeInfoArchitecture.Arm64, RuntimeInfoOperatingSystem.Windows, Is64Bit: true, - TransitionBlockSize: 160, ArgumentRegistersOffset: 96, FirstGCRefMapSlot: 88, - OffsetOfArgs: 160, OffsetOfFloatArgumentRegisters: -128, - NumArgumentRegisters: 8, NumFloatArgumentRegisters: 8, FloatRegisterSize: 16); - - public static readonly CallConvTestCase Arm64Apple = new( - "ARM64-Apple", RuntimeInfoArchitecture.Arm64, RuntimeInfoOperatingSystem.Apple, Is64Bit: true, - TransitionBlockSize: 160, ArgumentRegistersOffset: 96, FirstGCRefMapSlot: 88, - OffsetOfArgs: 160, OffsetOfFloatArgumentRegisters: -128, - NumArgumentRegisters: 8, NumFloatArgumentRegisters: 8, FloatRegisterSize: 16); - - public static readonly CallConvTestCase LoongArch64 = new( - "LoongArch64", RuntimeInfoArchitecture.LoongArch64, RuntimeInfoOperatingSystem.Unix, Is64Bit: true, - TransitionBlockSize: 176, ArgumentRegistersOffset: 112, FirstGCRefMapSlot: 112, - OffsetOfArgs: 176, OffsetOfFloatArgumentRegisters: -64, - NumArgumentRegisters: 8, NumFloatArgumentRegisters: 8, FloatRegisterSize: 8); - - public static readonly CallConvTestCase RiscV64 = new( - "RiscV64", RuntimeInfoArchitecture.RiscV64, RuntimeInfoOperatingSystem.Unix, Is64Bit: true, - TransitionBlockSize: 192, ArgumentRegistersOffset: 128, FirstGCRefMapSlot: 128, - OffsetOfArgs: 192, OffsetOfFloatArgumentRegisters: -64, - NumArgumentRegisters: 8, NumFloatArgumentRegisters: 8, FloatRegisterSize: 8); - public static IEnumerable AllCases => new[] { - new object[] { X86 }, new object[] { AMD64Windows }, new object[] { AMD64Unix }, new object[] { Arm32 }, - new object[] { Arm64Windows }, new object[] { Arm64Apple }, new object[] { LoongArch64 }, new object[] { RiscV64 }, + new object[] { AMD64Windows }, new object[] { AMD64Unix }, }; public static IEnumerable AMD64UnixOnly => new[] { new object[] { AMD64Unix } }; diff --git a/src/native/managed/cdac/tests/CallingConvention/CallingConventionTests.cs b/src/native/managed/cdac/tests/CallingConvention/CallingConventionTests.cs index 176999ce5099b4..0018f1a51bdafe 100644 --- a/src/native/managed/cdac/tests/CallingConvention/CallingConventionTests.cs +++ b/src/native/managed/cdac/tests/CallingConvention/CallingConventionTests.cs @@ -11,7 +11,7 @@ namespace Microsoft.Diagnostics.DataContractReader.Tests; /// verify harness-level invariants (the contract decodes without throwing, /// arg counts match) that should hold on every supported architecture. /// Per-architecture offset assertions live in the platform-specific test -/// classes (e.g. X86CallingConventionTests). +/// classes (e.g. AMD64WindowsCallingConventionTests). /// /// /// Gaps NOT covered by Skip-tagged tests anywhere: diff --git a/src/native/managed/cdac/tests/CallingConvention/X86CallingConventionTests.cs b/src/native/managed/cdac/tests/CallingConvention/X86CallingConventionTests.cs deleted file mode 100644 index 7cf6ad6f436092..00000000000000 --- a/src/native/managed/cdac/tests/CallingConvention/X86CallingConventionTests.cs +++ /dev/null @@ -1,255 +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 Microsoft.Diagnostics.DataContractReader.Contracts; -using Xunit; - -namespace Microsoft.Diagnostics.DataContractReader.Tests; - -/// -/// x86-specific calling-convention tests. ECX/EDX register placement (in -/// REVERSE — first arg in ECX which is the higher slot), stack overflow, and -/// the audit-gap regression markers for x86 register placement / sig-walk -/// accounting. -/// -public class X86CallingConventionTests -{ - private static CallConvTestCase Case => CallConvCases.X86; - - private static int OffsetOfECX => Case.ArgumentRegistersOffset + Case.PointerSize; - private static int OffsetOfEDX => Case.ArgumentRegistersOffset; - - [Fact] - public void OneInt_GoesToFirstArgRegSlot() - { - // x86 places args in registers in REVERSE: first int -> (NumArgRegs - 1) * PtrSize = 4 (ECX-style high slot). - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( - Case, - sig => sig.Return(CorElementType.Void).Param(CorElementType.I4)); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Single(layout.Arguments); - Assert.Equal(Case.ArgumentRegistersOffset + (Case.NumArgumentRegisters - 1) * Case.PointerSize, layout.Arguments[0].Slots[0].Offset); - } - - [Fact] - public void TwoInts_FillBothArgRegs() - { - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( - Case, - sig => sig.Return(CorElementType.Void).Param(CorElementType.I4).Param(CorElementType.I4)); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Equal(2, layout.Arguments.Count); - // First arg @ ECX (high slot) - Assert.Equal(Case.ArgumentRegistersOffset + Case.PointerSize, layout.Arguments[0].Slots[0].Offset); - // Second arg @ EDX (offset 0) - Assert.Equal(Case.ArgumentRegistersOffset, layout.Arguments[1].Slots[0].Offset); - } - - [Fact] - public void InstanceMethod_ThisOffsetIsECX() - { - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( - Case, hasThis: true, - sig => sig.Return(CorElementType.Void)); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - // GetThisOffset = ArgRegsOffset + PointerSize (ECX slot, after EDX at 0) - Assert.Equal(Case.ArgumentRegistersOffset + Case.PointerSize, layout.ThisOffset); - } - - /// - /// Audit gap #6 (closed): x86 ArgIterator used to exclude ValueType - /// entirely from register placement. Native x86 enregisters value types - /// of size 1, 2, or 4 bytes. This test verifies the fixed behavior. - /// - [Fact] - public void SmallValueType_Enregisters_AuditGap6() - { - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( - Case, hasThis: false, - (rts, sig) => - { - MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( - rts, "OneInt", structSize: 4, - fields: [new(0, CorElementType.I4)]); - sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); - }); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Equal(Case.ArgumentRegistersOffset + (Case.NumArgumentRegisters - 1) * Case.PointerSize, layout.Arguments[0].Slots[0].Offset); - } - - /// - /// Audit gap #7 (closed): X86ArgIterator.ComputeSizeOfArgStack used to - /// assume ALL args go to stack, biasing the stack-arg offset upward. - /// After the fix, the first stack arg lands at exactly OffsetOfArgs. - /// - [Fact] - public void ThirdInt_LandsAtOffsetOfArgs_AuditGap7() - { - // Three ints: first two go in ECX/EDX, third spills to stack at OffsetOfArgs. - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( - Case, - sig => sig - .Return(CorElementType.Void) - .Param(CorElementType.I4) - .Param(CorElementType.I4) - .Param(CorElementType.I4)); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Equal(3, layout.Arguments.Count); - Assert.Equal(OffsetOfECX, layout.Arguments[0].Slots[0].Offset); - Assert.Equal(OffsetOfEDX, layout.Arguments[1].Slots[0].Offset); - Assert.Equal(Case.OffsetOfArgs, layout.Arguments[2].Slots[0].Offset); - } - - [Fact] - public void StaticMethod_RetBuf_UserArgGoesToStack() - { - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( - Case, hasThis: false, - (rts, sig) => - { - MockMethodTable bigMT = MockDescriptors.CallingConvention.AddValueTypeMethodTable( - rts, "BigReturn", structSize: 12, - fields: [new(0, CorElementType.I4), new(4, CorElementType.I4), new(8, CorElementType.I4)]); - sig.ReturnValueType(new TargetPointer(bigMT.Address)).Param(CorElementType.I4); - }); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Single(layout.Arguments); - Assert.Equal(OffsetOfEDX, layout.Arguments[0].Slots[0].Offset); - } - - [Fact] - public void InstanceMethod_RetBuf_UserArgGoesToStack() - { - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( - Case, hasThis: true, - (rts, sig) => - { - MockMethodTable bigMT = MockDescriptors.CallingConvention.AddValueTypeMethodTable( - rts, "BigReturn", structSize: 12, - fields: [new(0, CorElementType.I4), new(4, CorElementType.I4), new(8, CorElementType.I4)]); - sig.ReturnValueType(new TargetPointer(bigMT.Address)).Param(CorElementType.I4); - }); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Single(layout.Arguments); - Assert.Equal(Case.OffsetOfArgs, layout.Arguments[0].Slots[0].Offset); - } - - [Fact] - public void StaticMethod_WithParamType_UserArgGoesToECX_ParamTypeInEDX() - { - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( - Case, hasThis: false, - (rts, sig) => sig.Return(CorElementType.Void).Param(CorElementType.I4), - hasParamType: true); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Single(layout.Arguments); - Assert.Equal(OffsetOfECX, layout.Arguments[0].Slots[0].Offset); - } - -#pragma warning disable xUnit1004 // Test methods should not be skipped -- tracking an implementation gap. - [Fact] - public void VarArgs_CookieAtSizeOfTransitionBlock() - { - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( - Case, - sig => sig.VarArg().Return(CorElementType.Void).Param(CorElementType.I4)); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.NotNull(layout.VarArgCookieOffset); - Assert.Equal(Case.TransitionBlockSize, layout.VarArgCookieOffset.Value); - Assert.Single(layout.Arguments); - // On x86 varargs, the cookie occupies the first stack slot (at OffsetOfArgs), - // so the first user arg is one slot above it. - Assert.Equal(Case.OffsetOfArgs + Case.PointerSize, layout.Arguments[0].Slots[0].Offset); - } -#pragma warning restore xUnit1004 - - [Fact] - public void TypedReference_GoesToStack_NotEnregistered() - { - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( - Case, hasThis: false, - (rts, sig) => - { - MockMethodTable typedRefMT = MockDescriptors.CallingConvention.AddValueTypeMethodTable( - rts, "System.TypedReference", structSize: 8, - fields: [new(0, CorElementType.Byref), new(4, CorElementType.I)]); - rts.SetTypedReferenceMethodTable(typedRefMT.Address); - sig.Return(CorElementType.Void).Param(CorElementType.TypedByRef); - }); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Single(layout.Arguments); - Assert.False(layout.Arguments[0].IsPassedByRef); - Assert.Equal(Case.OffsetOfArgs, layout.Arguments[0].Slots[0].Offset); - } - - [Fact] - public void LargeStruct_PassedByValueOnStack() - { - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithMethod( - Case, hasThis: false, - (rts, sig) => - { - MockMethodTable mt = MockDescriptors.CallingConvention.AddValueTypeMethodTable( - rts, "BigStruct", structSize: 24, - fields: - [ - new(0, CorElementType.I4), new(4, CorElementType.I4), new(8, CorElementType.I4), - new(12, CorElementType.I4), new(16, CorElementType.I4), new(20, CorElementType.I4), - ]); - sig.Return(CorElementType.Void).ParamValueType(new TargetPointer(mt.Address)); - }); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Single(layout.Arguments); - Assert.False(layout.Arguments[0].IsPassedByRef); - Assert.Equal(Case.OffsetOfArgs, layout.Arguments[0].Slots[0].Offset); - } - - [Fact] - public void TenArgs_TwoInRegs_EightOnStack() - { - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( - Case, - sig => - { - sig.Return(CorElementType.Void); - for (int i = 0; i < 10; i++) sig.Param(CorElementType.I4); - }); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Equal(10, layout.Arguments.Count); - Assert.Equal(OffsetOfECX, layout.Arguments[0].Slots[0].Offset); - Assert.Equal(OffsetOfEDX, layout.Arguments[1].Slots[0].Offset); - - for (int i = 2; i < 10; i++) - { - int expectedStackOfs = Case.OffsetOfArgs + (10 - 1 - i) * Case.PointerSize; - Assert.Equal(expectedStackOfs, layout.Arguments[i].Slots[0].Offset); - } - } - - [Theory] - [InlineData(CorElementType.Object)] - [InlineData(CorElementType.String)] - public void GCReferenceArgs_Enregister(CorElementType refType) - { - var (target, mdh) = CallingConventionTestHelpers.CreateTargetWithStaticMethod( - Case, - sig => sig.Return(CorElementType.Void).Param(refType)); - - CallSiteLayout layout = target.Contracts.CallingConvention.ComputeCallSiteLayout(mdh); - Assert.Single(layout.Arguments); - Assert.False(layout.Arguments[0].IsPassedByRef); - Assert.Equal(OffsetOfECX, layout.Arguments[0].Slots[0].Offset); - } -} From bc1d599dc1cb81ac5093c04d7b1d85ce80fea14a Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Fri, 22 May 2026 11:00:03 -0400 Subject: [PATCH 5/9] Remove cdac-calling-conventions notes folder These were per-arch ABI notes used while drafting the calling-convention port. Now that v4 is AMD64-only and the relevant content lives in the AMD64 unit tests and dump-test expectations, the folder is no longer needed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cdac-calling-conventions/README.md | 107 ------------- cdac-calling-conventions/amd64-unix.md | 161 ------------------- cdac-calling-conventions/amd64-windows.md | 156 ------------------- cdac-calling-conventions/arm32.md | 135 ---------------- cdac-calling-conventions/arm64.md | 182 ---------------------- cdac-calling-conventions/x86.md | 103 ------------ 6 files changed, 844 deletions(-) delete mode 100644 cdac-calling-conventions/README.md delete mode 100644 cdac-calling-conventions/amd64-unix.md delete mode 100644 cdac-calling-conventions/amd64-windows.md delete mode 100644 cdac-calling-conventions/arm32.md delete mode 100644 cdac-calling-conventions/arm64.md delete mode 100644 cdac-calling-conventions/x86.md diff --git a/cdac-calling-conventions/README.md b/cdac-calling-conventions/README.md deleted file mode 100644 index b6f148fdd25670..00000000000000 --- a/cdac-calling-conventions/README.md +++ /dev/null @@ -1,107 +0,0 @@ -# Managed Calling Conventions for cDAC Stack Walking - -This folder documents the calling conventions used by managed code on each -supported platform, as implemented by the per-architecture `ArgIterator` -subclasses in -[`src/native/managed/cdac/.../StackWalk/CallingConvention/`](../src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/). - -The cDAC's argument iterator is the managed reimplementation of the legacy DAC's -sig-walking layer. For each platform it answers the question: **"given a method -signature, where (register or stack offset) is each argument located when the -method is invoked?"** This is consumed by diagnostic tools (stack walks, locals -inspection, SOS, ClrMD, etc.) to inspect live frames in a target process. - -Each doc focuses on **what the managed CLR does**, with deltas vs. the native -platform ABI called out explicitly. They are not a substitute for the platform -ABI specs -- read those for the base rules, then read these for the managed -specials. - -## Platform docs - -| Platform | Doc | Iterator | -|---|---|---| -| x86 (Windows / Linux 32-bit) | [`x86.md`](./x86.md) | [`X86ArgIterator.cs`](../src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/X86ArgIterator.cs) | -| AMD64 Windows | [`amd64-windows.md`](./amd64-windows.md) | [`AMD64WindowsArgIterator.cs`](../src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/AMD64WindowsArgIterator.cs) | -| AMD64 Unix (Linux / macOS) | [`amd64-unix.md`](./amd64-unix.md) | [`AMD64UnixArgIterator.cs`](../src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/AMD64UnixArgIterator.cs) | -| ARM32 (AAPCS, Linux armhf / Windows ARM) | [`arm32.md`](./arm32.md) | [`Arm32ArgIterator.cs`](../src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/Arm32ArgIterator.cs) | -| ARM64 (AAPCS64, Linux / Windows / Apple) | [`arm64.md`](./arm64.md) | [`Arm64ArgIterator.cs`](../src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/Arm64ArgIterator.cs) | -| RISC-V 64 / LoongArch 64 | [`riscv64-loongarch64.md`](./riscv64-loongarch64.md) | [`RiscV64LoongArch64ArgIterator.cs`](../src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/RiscV64LoongArch64ArgIterator.cs) | - -## Cross-cutting concepts - -These apply to all platforms and are not repeated in each doc; consult the -upstream design doc [`docs/design/coreclr/botr/clr-abi.md`](../docs/design/coreclr/botr/clr-abi.md) -for full detail. - -### Argument prefix order - -Every managed method starts its argument list with zero or more **hidden -arguments**, in this fixed order, before any user arguments: - -``` -[this] [retBuf] [genericContext] [asyncContinuation] [varArgCookie] userArgs... -``` - -- **`this`**: Instance methods only. Always passed first (managed-specific -- - native C++ x64 reorders it after the ret buf on some platforms). -- **`retBuf`**: Hidden pointer for methods returning a value type that doesn't - fit in the return registers (rules vary per platform). Callee writes the - result through this pointer; on AMD64 the callee also returns the buffer - address in the integer return register. -- **`genericContext`**: For *shared generic* methods, a `MethodDesc*` (generic - methods) or `MethodTable*` (static methods on generic types) telling the - callee which instantiation it's serving. -- **`asyncContinuation`**: For methods participating in the async stack - protocol (new in the runtime-async work). -- **`varArgCookie`**: For managed varargs (`__arglist` / - `IMAGE_CEE_CS_CALLCONV_VARARG`), a pointer to a runtime-parseable signature - blob describing the variadic tail. - -The cDAC base class counts these in `ArgIteratorBase.ComputeInitialNumRegistersUsed` -(see [`ArgIteratorBase.cs`](../src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/ArgIteratorBase.cs)) -before user-arg iteration begins. x86 is an outlier -- it counts these -separately in its own `ComputeSizeOfArgStack` pass. - -### TypedReference - -`System.TypedReference` is `{ ref byte _value; IntPtr _type; }` = 16 bytes, -referenced in signatures by `ELEMENT_TYPE_TYPEDBYREF` (0x16) with no class -token. The runtime keeps a `g_TypedReferenceMT` global pointing at the -TypedReference MethodTable; the signature walker substitutes that MT whenever -it encounters `ELEMENT_TYPE_TYPEDBYREF`, then the iterator treats it as an -ordinary 16-byte value type. - -In cDAC, the substitution lives in `ArgTypeInfoSignatureProvider.GetTypedReferenceInfo()`. -Each platform doc summarizes where a `TypedReference` parameter and return -value land. - -### Implicit by-reference - -On most platforms, value types whose size exceeds a per-platform threshold are -passed via a hidden pointer (the *implicit byref*) instead of by value. The -JIT must: - -- Report the implicit-byref parameter as an interior pointer (GC `BYREF`) in - the GC info, because the caller may legitimately point at the GC heap (not - always a stack temp). -- Use checked write barriers for any stores through the pointer. - -The per-platform threshold is encoded as `EnregisteredParamTypeMaxSize` on each -iterator (see the abstract property on `ArgIteratorBase`). - -### Funclets, frame pointers, and other non-arg-iterator concerns - -The exception-handling funclet model, frame-pointer policy, GC-info layout, -profiler hooks, and other CLR-internal contracts are documented in -[`docs/design/coreclr/botr/clr-abi.md`](../docs/design/coreclr/botr/clr-abi.md). -These don't affect argument iteration directly; they affect codegen and -stack walking elsewhere. - -## Reference - -- [`docs/design/coreclr/botr/clr-abi.md`](../docs/design/coreclr/botr/clr-abi.md) -- the - authoritative CLR ABI design document. -- [`src/coreclr/vm/callingconvention.h`](../src/coreclr/vm/callingconvention.h) -- - the native VM's `ArgIteratorTemplate`, which each cDAC iterator mirrors. -- [`src/coreclr/tools/aot/ILCompiler.ReadyToRun/.../ArgIterator.cs`](../src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/ArgIterator.cs) -- - CrossGen2's managed port of the same logic. diff --git a/cdac-calling-conventions/amd64-unix.md b/cdac-calling-conventions/amd64-unix.md deleted file mode 100644 index e7c12f2a859743..00000000000000 --- a/cdac-calling-conventions/amd64-unix.md +++ /dev/null @@ -1,161 +0,0 @@ -# AMD64 Unix (System V) Managed Calling Convention - -**Iterator:** [`AMD64UnixArgIterator.cs`](../src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/AMD64UnixArgIterator.cs) -**Classifier:** [`SystemVStructClassifier.cs`](../src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/SystemVStructClassifier.cs) -**Applies to:** Linux x64, macOS x64 (managed code on the System V AMD64 ABI). -**Base ABI:** System V AMD64 ABI -- see -[the spec](https://gitlab.com/x86-psABIs/x86-64-ABI) (§3.2.1 registers, -§3.2.3 parameter passing / classification, §3.3 vector types). - -## Register set - -| Use | Registers | -|---|---| -| Integer arg | `RDI, RSI, RDX, RCX, R8, R9` (6 slots) | -| Float arg | `XMM0`-`XMM7` (8 slots) | -| **Bank independence** | The integer and FP banks are tracked **independently** -- a float arg does **not** consume an int slot (unlike Windows x64) | -| Integer return | `RAX` (and `RDX` for the second eightbyte) | -| Float return | `XMM0` (and `XMM1` for the second eightbyte) | -| Volatile | `RAX, RCX, RDX, RSI, RDI, R8-R11, XMM0-XMM15` | -| Non-volatile | `RBX, RBP, R12-R15` | -| Stack slot size | 8 bytes | -| Stack alignment | 16 B at call site | -| Red zone | 128 bytes below `RSP` may be used by leaf functions without explicit allocation | - -## Argument placement rules: the eightbyte classifier - -For value types up to 16 bytes, the System V ABI defines a per-byte -**eightbyte classification** algorithm: - -1. Aggregates > 16 bytes -> passed in memory (on the stack, **by value -- no - hidden pointer**, unlike Windows x64). -2. Aggregates with a misaligned field -> in memory. -3. Otherwise the struct is partitioned into 1 or 2 eightbytes (bytes 0-7, - optionally 8-15), and each eightbyte gets a class: - - `INTEGER` (incl. CLR's `IntegerReference` for object refs and `IntegerByRef` - for managed pointers) - - `SSE` (float / double) - - `NO_CLASS` (padding / empty slot -- in the CLR currently promoted to - `INTEGER`; see TODO in `SystemVStructClassifier`) - - `MEMORY` (forces the whole struct to memory) -4. Merge rules (when two fields share a byte / eightbyte): - - Either side `INTEGER` -> `INTEGER` (INTEGER dominates SSE). - - Both SSE -> `SSE`. - - Either side `MEMORY` -> `MEMORY`. -5. Register assignment per eightbyte: - - `INTEGER`/`IntegerReference`/`IntegerByRef` -> next free `RDI..R9` slot. - - `SSE` -> next free `XMM0..XMM7` slot. -6. **All-or-nothing**: if even one eightbyte can't find its required register - (bank exhausted), the *entire struct* spills to the stack and no registers - are consumed by it. - -Examples: - -| Struct | Eightbytes | Classes | Placement | -|---|---|---|---| -| `{ int x; int y; }` (8 B) | 1 | `[INTEGER]` | 1 GP reg (`RDI`) | -| `{ int x; double d; }` (16 B) | 2 | `[INTEGER, SSE]` | 1 GP + 1 FP (`RDI, XMM0`) | -| `{ double a; double b; }` (16 B) | 2 | `[SSE, SSE]` | 2 FP (`XMM0, XMM1`) | -| `{ float a; float b; }` (8 B) | 1 | `[SSE]` (floats packed in low 64 bits of one XMM) | 1 FP (`XMM0`) | -| `{ int x; float f; }` (8 B, both in eightbyte 0) | 1 | `[INTEGER]` (INTEGER dominates) | 1 GP (`RDI`) | -| `{ long a; long b; long c; }` (24 B) | -- | `MEMORY` | Stack by value | - -See [the SysV struct passing research doc](../C:/Users/maxcharlamb/.copilot/session-state/52879186-8fcc-4ed2-9048-3fb6ef3bf6b3/research/can-you-explain-the-sysv-struct-passing-convetion-.md) -for a full deep-dive with code citations. - -## Return values - -| Return shape | Where | -|---|---| -| Integer / pointer / reference | `RAX` | -| `R4` / `R8` | `XMM0` | -| Value type <= 16 B that classifies in registers | Same banks as parameters, in order: eightbyte 0 -> `RAX`/`XMM0`, eightbyte 1 -> `RDX`/`XMM1` (with appropriate fallback if the two eightbytes use different banks) | -| Value type > 16 B or classified `MEMORY` | Caller-allocated return buffer; pointer passed as first hidden arg (`RDI`); callee returns the buffer address in `RAX` | - -## Managed-specific behavior - -### `this` is in `RDI` - -The first user-arg register is `RDI`, so `this` (always the first managed -arg) lands there. If there's a ret buf, ret buf -> `RDI` and `this` -> `RSI`. - -### Hidden argument prefix - -Standard CLR prefix applies. Each hidden arg consumes the next integer slot -(`RDI`, then `RSI`, ...): - -``` -[this:RDI] [retBuf:RSI] [genericContext:RDX] [asyncContinuation:RCX] [varArgCookie:R8] userArgs... -``` - -### Implicit by-reference is NOT used - -Unlike Windows x64, the System V ABI passes large structs **by value on the -stack**, not via a hidden pointer. The cDAC encodes this as: - -```csharp -public override bool IsArgPassedByRefBySize(int size) => false; -protected override bool IsArgPassedByRefArchSpecific() => false; -``` - -This means no GC-byref tracking is needed for value-type parameters on SysV. - -### CLR uses a subset of the ABI's classes - -The spec defines `NO_CLASS, INTEGER, SSE, SSEUP, X87, X87UP, COMPLEX_X87, MEMORY`. -The CLR uses only `NO_CLASS, INTEGER, SSE, MEMORY` plus two extensions -(`IntegerReference`, `IntegerByRef`) that carry GC liveness info. `SSEUP`, -`X87`, `X87UP`, `COMPLEX_X87` are not used: - -- `Vector128/256/512` and `Vector64` bypass the SysV classifier entirely - -- they are handled by the JIT as SIMD intrinsic types in single - XMM/YMM/ZMM registers. -- The CLR has no `long double` / `__float128` / x87 types. - -See the enum in [`SystemVStructDescriptor.cs`](../src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/SystemVStructDescriptor.cs). - -### Empty structs - -Managed structs with zero instance fields are passed by value on the stack -(matching the broader "explicit-layout / unusual struct -> stack" rule called -out in [`clr-abi.md:569`](../docs/design/coreclr/botr/clr-abi.md)). - -### Frame pointer - -System V x64 managed frames **always allocate a frame pointer** (RBP), since -CoreCLR PR [dotnet/coreclr#4019](https://github.com/dotnet/coreclr/pull/4019). -This makes stack walking via frame chains viable, unlike Windows x64 where -unwinding goes through PDATA/XDATA. - -### Funclets - -Same managed-EH model as other platforms. The catch funclet receives the -exception object in `RSI` (vs. `RCX` on Windows x64). - -### Known TODO: NoClass eightbytes - -The classifier currently promotes `NoClass` eightbytes (pure padding slots) -to `Integer` because the JIT mishandles `NoClass` (see TODO at -`SystemVStructClassifier.cs:439` and the mirrored TODO at -`src/coreclr/vm/methodtable.cpp:2660`). This is a known divergence from the -ABI spec; a small minority of structs may waste a GP register on a padding -slot as a result. - -## TypedReference - -`TypedReference = { ref byte _value; IntPtr _type; }` = 16 bytes. -Classification: `[IntegerByRef, Integer]` -> passed in **2 GP registers** -(typically `RDI, RSI`). Returned in `RAX, RDX`. - -The cDAC's `ArgTypeInfoSignatureProvider` substitutes the `g_TypedReferenceMT` -MethodTable when the signature contains `ELEMENT_TYPE_TYPEDBYREF`, so the -classifier walks its layout as if it were an ordinary 16-byte value type. - -## References - -- [System V AMD64 ABI spec](https://gitlab.com/x86-psABIs/x86-64-ABI) -- §3.2.3 classification & passing. -- [docs/design/coreclr/botr/clr-abi.md - "System V x86_64 support"](../docs/design/coreclr/botr/clr-abi.md) -- CLR deviations. -- [docs/design/coreclr/jit/struct-abi.md](../docs/design/coreclr/jit/struct-abi.md) -- struct-passing design notes. -- [src/coreclr/vm/callingconvention.h](../src/coreclr/vm/callingconvention.h) -- `UNIX_AMD64_ABI` branches. -- [src/coreclr/vm/methodtable.cpp](../src/coreclr/vm/methodtable.cpp) -- `ClassifyEightBytesWithManagedLayout` (the C++ classifier). -- [src/coreclr/tools/Common/JitInterface/SystemVStructClassificator.cs](../src/coreclr/tools/Common/JitInterface/SystemVStructClassificator.cs) -- CrossGen2's managed mirror. diff --git a/cdac-calling-conventions/amd64-windows.md b/cdac-calling-conventions/amd64-windows.md deleted file mode 100644 index a1a3d4131c4114..00000000000000 --- a/cdac-calling-conventions/amd64-windows.md +++ /dev/null @@ -1,156 +0,0 @@ -# AMD64 Windows Managed Calling Convention - -**Iterator:** [`AMD64WindowsArgIterator.cs`](../src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/AMD64WindowsArgIterator.cs) -**Applies to:** Windows x64 (managed code on the Microsoft x64 ABI). -**Base ABI:** Microsoft x64 calling convention -- see -[x64 Software Conventions](https://learn.microsoft.com/cpp/build/x64-software-conventions). - -## Register set - -| Use | Registers | -|---|---| -| Integer arg | `RCX, RDX, R8, R9` (**4 slots, shared with FP slots by position**) | -| Float arg | `XMM0, XMM1, XMM2, XMM3` (**same 4 slot positions** -- each arg consumes either an int or an FP register at its slot index, not both) | -| Integer return | `RAX` | -| Float return | `XMM0` | -| Volatile | `RAX, RCX, RDX, R8-R11, XMM0-XMM5` | -| Non-volatile | `RBX, RBP, RDI, RSI, R12-R15, XMM6-XMM15` | -| Stack slot size | 8 bytes | -| Shadow space | 32 bytes immediately above the return address (caller reserves homes for the 4 register args) | -| Stack alignment | 16 B at call site | - -## Argument placement rules - -**Every argument consumes exactly one 8-byte slot** -- there is no splitting, -no multi-register passing, no eightbyte classification. - -- The first 4 slots (positions 0-3) are passed in registers; the choice - between `RCX..R9` and `XMM0..3` depends on the arg's type: - - `R4` / `R8` (float, double) at position N -> `XMM`. - - Anything else at position N -> integer register `RCX/RDX/R8/R9` for N=0..3. -- Slot 4+ goes on the stack at `[RSP + 32 + (N - 4) * 8]` (above shadow space). -- A value type whose size is **not in {1, 2, 4, 8} bytes** (i.e. 3, 5, 6, 7, - or >= 9) is passed by an **implicit hidden pointer** rather than by value. - The caller materializes the struct (usually on its own stack) and passes a - pointer. - -In cDAC this is encoded as: - -```csharp -EnregisteredParamTypeMaxSize = 8; -IsArgPassedByRefBySize(size) = size > 8 || !IsPow2(size); -``` - -## Return values - -| Return shape | Where | -|---|---| -| Integer / pointer / reference / value type with size in {1, 2, 4, 8} | `RAX` | -| `R4` / `R8` | `XMM0` | -| Value type not in {1, 2, 4, 8} (incl. `TypedByRef` = 16 B), or non-power-of-2 size | Caller-allocated return buffer; pointer passed as first hidden arg; callee returns the buffer address in `RAX` | - -## Managed-specific behavior - -### `this` is always in `RCX` - -Native C++ on Microsoft x64 pushes the ret buf into `RCX` and bumps `this` to -`RDX` when the function returns a large struct. **Managed code always uses -`RCX` for `this`** and `RDX` for the ret buf, regardless of whether a ret buf -is present. (This wasn't always the case -- up to .NET Framework 4.5 the -managed convention matched native; it changed for consistency with other -managed platforms.) - -The cDAC inherits this from `ArgIteratorBase.GetRetBuffArgOffset`, which -returns `argumentRegistersOffset + (hasThis ? PointerSize : 0)`. - -### Hidden argument prefix - -The CLR's standard prefix order applies: - -``` -[this:RCX] [retBuf:RDX] [genericContext] [asyncContinuation] [varArgCookie] userArgs... -``` - -Each takes the next available register slot in `RCX..R9`, then spills to the -stack. So a method with `this` + ret buf + generic context + 1 user arg lays -out as `RCX=this, RDX=retBuf, R8=genericContext, R9=userArg0`. - -The vararg cookie, async continuation, and generic context don't exist in -native; the cDAC counts them in `ArgIteratorBase.ComputeInitialNumRegistersUsed`. - -### Varargs - -Managed varargs (`IMAGE_CEE_CS_CALLCONV_VARARG`) follow the Microsoft -"duplicate-into-int-reg" rule for FP args: any `R4`/`R8` argument in the first -4 slots is duplicated into the matching `RCX..R9` slot as well as `XMM0..3`. -The cDAC iterator's per-arg logic is the same for fixed and variadic methods --- the duplication is handled by the JIT, not represented in the iteration -output. - -### Implicit by-reference: GC-tracked pointers - -Unlike native, the implicit-byref pointer may legitimately point into the GC -heap (reflection/remoting paths), so the JIT: - -- Reports the implicit-byref parameter as a GC `BYREF` (interior pointer). -- Uses checked write barriers for stores through the pointer. - -### Empty structs go on the stack - -A managed struct with **zero instance fields** is passed by value on the stack -(never in a register), regardless of its declared size. Native C++ has no -equivalent since `sizeof(EmptyStruct) >= 1`. - -### Frame pointer - -Unlike System V x64 (which always uses RBP since CoreCLR PR -[dotnet/coreclr#4019](https://github.com/dotnet/coreclr/pull/4019)) and ARM/ARM64 -(which require a frame pointer), **Windows x64 typically omits the frame -pointer**. Unwinding uses PDATA/XDATA records, not frame chaining. The JIT -allocates RBP only when the function genuinely needs one (e.g., funclets, -`alloca`). - -### Funclets - -Catch / finally / filter handlers are emitted as separate functions -(*funclets*) with their own PDATA entries, looking to the OS like first-class -functions. The catch funclet receives the `System.Exception` reference in -`RCX`. This is a CLR construct; native SEH passes an `EXCEPTION_RECORD*`. - -### Secret VM-to-JIT register conventions - -Several "secret" registers carry runtime data for special call shapes: - -| Use | Register | -|---|---| -| Virtual stub dispatch (VSD) | `R11` (stub indirection cell) | -| `calli` P/Invoke target | `R10` | -| `calli` P/Invoke signature cookie | `R11` | -| Normal P/Invoke MethodDesc param | `R10` | - -`R10` and `R11` are volatile in the Microsoft x64 ABI and unused as argument -registers, which makes them safe choices. - -### Small primitives are zero/sign-extended - -Native Microsoft x64 leaves the upper bits of small return values -**undefined**. Managed code defines them: signed small types (`sbyte`, -`short`) are sign-extended to 32/64 bits; unsigned (`byte`, `ushort`, `bool`) -are zero-extended. The JIT relies on this when reading values back at call -sites. - -## TypedReference - -`TypedReference` is 16 bytes. Since 16 is not in {1, 2, 4, 8}, the -implicit-byref rule applies: - -- **As a parameter**: passed by hidden pointer; the slot in `RCX..R9` (or on - the stack) holds a pointer to a `TypedReference` value the caller has - materialized. -- **As a return value**: triggers a return buffer. - -## References - -- [docs/design/coreclr/botr/clr-abi.md - Special parameters](../docs/design/coreclr/botr/clr-abi.md) -- `this`, generics, varargs, async continuation. -- [src/coreclr/vm/callingconvention.h](../src/coreclr/vm/callingconvention.h) -- search for `TARGET_AMD64` and `UNIX_AMD64_ABI` to find the Windows-vs-Unix split (Windows is the `#else` arm). -- [Microsoft x64 calling convention](https://learn.microsoft.com/cpp/build/x64-calling-convention) -- base ABI documentation. diff --git a/cdac-calling-conventions/arm32.md b/cdac-calling-conventions/arm32.md deleted file mode 100644 index 5f64fcb4ba3276..00000000000000 --- a/cdac-calling-conventions/arm32.md +++ /dev/null @@ -1,135 +0,0 @@ -# ARM32 (AAPCS) Managed Calling Convention - -**Iterator:** [`Arm32ArgIterator.cs`](../src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/Arm32ArgIterator.cs) -**Applies to:** Linux armhf (hard-float), Windows on ARM (32-bit). Linux armel -(soft-float) is not yet implemented in cDAC -- see TODO on the iterator class. -**Base ABI:** ARM AAPCS / AAPCS-VFP -- see -[Procedure Call Standard for the ARM Architecture](https://github.com/ARM-software/abi-aa/blob/main/aapcs32/aapcs32.rst). - -## Register set - -| Use | Registers | -|---|---| -| Integer arg | `R0, R1, R2, R3` (4 slots) | -| Float arg (hard-float) | `S0`-`S15` (16 single-precision slots, or `D0`-`D7` paired as 8 double slots) | -| Integer return | `R0` (and `R1` for 64-bit) | -| Float return | `S0` (`R4`) / `D0` (`R8`) | -| Volatile | `R0-R3, R12, S0-S15`/`D0-D7` | -| Non-volatile | `R4-R11, LR, S16-S31`/`D8-D15` | -| Stack slot size | 4 bytes | -| Stack alignment | 8 B at call site (16 B at function entry on some targets) | - -## Argument placement rules - -### Integer / pointer / reference args - -- Scanned left-to-right. -- 32-bit args take the next free `R0..R3`, then spill to stack 4-byte slots. -- **64-bit args (`I8`, `U8`, `R8`) require 8-byte alignment** in both the - register file and the stack: - - If the next free register is odd-numbered (`R1` or `R3`), it's skipped - and the 64-bit value goes in the next aligned pair (`R2:R3` or `[stack + - 8]`). - - Stack offsets for 64-bit args are aligned up to 8 bytes. -- **Split between regs and stack**: a 64-bit arg that starts in `R3` is - passed half in `R3` and half on the stack (the "co-processor register - split" rule). This is the *only* case where a single arg spans the boundary - on ARM32. - -### Float / double / HFA args (hard-float path only) - -The hard-float (AAPCS-VFP) ABI uses a **bitmap allocator** over `S0..S15` so -that floats and doubles can interleave with gaps: - -- `R4` (float) takes one S-register slot; `R8` (double) takes one D-register - slot = 2 S-register slots. -- **HFAs** (Homogeneous Floating-point Aggregates -- structs of 1-4 identical - floats or doubles) are placed in consecutive S/D slots. -- If the bitmap can't fit the FP arg, all subsequent FP args go on the stack - (the FP bank is marked exhausted: `_wFPRegs = 0xffff`). - -The bitmap walk lives in `Arm32ArgIterator.GetNextOffsetForArg`, lines 79-103. - -### Varargs - -For variadic methods, **the FP register path is skipped entirely** -- all -args (including floats / doubles / HFAs) go through the integer/stack path. -This is checked via `!IsVarArg` in the FP allocation guard. - -## Return values - -| Return shape | Where | -|---|---| -| Integer / pointer / reference / 32-bit value type | `R0` | -| `I8` / `U8` | `R0:R1` (low in `R0`, high in `R1`) | -| `R4` / `R8` | `S0` / `D0` (or `R0` / `R0:R1` under softfp; not yet handled) | -| HFA (1-4 floats or doubles, hard-float) | `S0..S3` / `D0..D3` | -| Other value types | Caller-allocated return buffer; pointer passed as a hidden first arg in `R0` (or `R1` if `this` is present); callee uses the buffer | - -## Managed-specific behavior - -### Hidden argument prefix - -Standard CLR prefix applies. Each hidden arg consumes the next integer slot: - -``` -[this:R0] [retBuf:R1] [genericContext:R2] [asyncContinuation:R3] [varArgCookie] userArgs... -``` - -If there's no `this`, the ret buf takes `R0`. - -### Implicit by-reference: not used - -ARM32 sets `EnregisteredParamTypeMaxSize = 0`, meaning the iterator does not -apply an implicit-byref transformation. Value types are passed by value -according to the rules above. - -### HFA detection comes from `ArgTypeInfo` - -The iterator consults `_argTypeHandle.IsHomogeneousAggregate` and -`_argTypeHandle.RequiresAlign8` (computed by `ArgTypeInfo.FromTypeHandle` -based on `IRuntimeTypeSystem.IsHFA` and `RequiresAlign8`). On ARM32 the HFA -element size is determined entirely by alignment: 8-byte alignment -> double -HFA; 4-byte alignment -> float HFA (see `ArgTypeInfo.ComputeHfaElementSize`). - -### 64-bit alignment tracking - -The iterator records `_requires64BitAlignment` per arg so that downstream -consumers (e.g. SOS, ClrMD) can correctly compute frame offsets even when -register skipping occurs (e.g. an `I8` in `R2:R3` after a single-slot arg in -`R1`). - -### Frame pointer - -ARM/ARM64 always allocate a frame pointer (`R11` on ARM32) for both managed -frames and to support the InlinedCallFrame mechanism for P/Invokes -([`clr-abi.md:172`](../docs/design/coreclr/botr/clr-abi.md)). - -### Funclets - -Same managed-EH funclet model as other platforms. The catch funclet receives -the exception object in `R0`. - -### Softfp (armel) not yet supported - -The Linux armel calling convention uses integer registers for all args -including floats (no S/D registers used for argument passing). The cDAC -iterator currently hard-codes `IsArmhfABI = true`; a TODO on the class flags -the need to detect armel and disable the FP-register path. - -## TypedReference - -`TypedReference` is 16 bytes. On ARM32 it does not enregister (value types -generally don't get split across registers on ARM32; the iterator routes -them through the integer slots and stack). A `TypedReference` parameter -consumes 4 pointer-sized slots (16 bytes total) starting at the next 8-byte -alignment boundary. The current cDAC handling depends on the substitution -applied by `ArgTypeInfoSignatureProvider`; refer to the iterator's per-arg -logic for the concrete placement. - -## References - -- [AAPCS32](https://github.com/ARM-software/abi-aa/blob/main/aapcs32/aapcs32.rst) -- ARM 32-bit ABI. -- [Overview of ARM32 ABI Conventions (MSDN)](https://learn.microsoft.com/cpp/build/overview-of-arm-abi-conventions) -- Windows on ARM specifics. -- [src/coreclr/vm/callingconvention.h](../src/coreclr/vm/callingconvention.h) -- `TARGET_ARM` branches (search for `#elif defined(TARGET_ARM)`). -- [docs/design/coreclr/botr/clr-abi.md](../docs/design/coreclr/botr/clr-abi.md) -- ARM-specific notes throughout. diff --git a/cdac-calling-conventions/arm64.md b/cdac-calling-conventions/arm64.md deleted file mode 100644 index 43337927516667..00000000000000 --- a/cdac-calling-conventions/arm64.md +++ /dev/null @@ -1,182 +0,0 @@ -# ARM64 (AAPCS64) Managed Calling Convention - -**Iterator:** [`Arm64ArgIterator.cs`](../src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/Arm64ArgIterator.cs) -**Applies to:** Linux ARM64, Windows on ARM64, Apple (macOS/iOS) ARM64. -**Base ABI:** AAPCS64 -- see [Procedure Call Standard for the Arm 64-bit Architecture](https://github.com/ARM-software/abi-aa/blob/main/aapcs64/aapcs64.rst). - -## Register set - -| Use | Registers | -|---|---| -| Integer arg | `X0`-`X7` (8 slots) | -| Float / SIMD arg | `V0`-`V7` (8 slots, 16 bytes each) | -| Bank independence | The integer and FP banks are tracked **independently** (like SysV x64, unlike Windows x64) | -| Indirect result location | `X8` (return buffer pointer; **separate from arg regs**) | -| Integer return | `X0` (and `X1` for 16-byte structs) | -| Float return | `V0` (and `V1..V3` for HFAs) | -| Volatile | `X0-X17, V0-V7, V16-V31` | -| Non-volatile | `X19-X28, X29 (FP), X30 (LR), V8-V15` | -| Stack slot size | 8 bytes (4 bytes on Apple for natural-alignment packing) | -| Stack alignment | 16 B at call site | - -## Argument placement rules - -### Integer / pointer / reference args - -- An arg of size <= 8 takes the next free `X0..X7` slot. -- An arg of size 9-16 takes two consecutive integer registers (a "consecutive - pair"). -- If both halves don't fit in the remaining int registers: - - **Linux/Apple**: the entire arg goes on the stack; no registers are - consumed. - - **Windows**: special split rule -- the head goes into the remaining X - register(s) and the tail goes on the stack. This is **only** an issue for - variadic methods on Windows. (See "Windows varargs" below; not yet - implemented in cDAC.) -- Anything larger than 16 bytes is passed via **implicit by-reference**: the - caller materializes the value and passes a pointer in the next int slot. - -### Float / double / HFA args - -- `R4` (float) / `R8` (double) takes one V register slot at 16 bytes per slot - (low bits of `Vn`). -- **HFAs** (1-4 floats or doubles or vectors with all identical element - type) get spread across consecutive `V` registers, each in its own slot. - E.g. a `Vector4` (4 floats) takes `V0..V3`. -- If the HFA doesn't fit in remaining V slots, it goes on the stack (no V - registers consumed). - -The check is: - -```csharp -if (cFPRegs > 0 && !IsVarArg) { ... try V regs ... } -``` - -### Varargs - -Variadic methods diverge from the fixed-arg rules: - -- **All variadic args go through the X-register / stack path**, not V regs. - `R4`/`R8` arguments are widened to 64 bits and placed in `X0..X7` or on the - stack. The cDAC encodes this as the `!IsVarArg` guard at the FP branch. -- **HFAs lose their HFA-ness**: a homogeneous float aggregate is treated as - an ordinary composite for variadic calls. cDAC implements this by also - forcing the implicit-byref path for >16-byte HFAs under varargs: - ```csharp - protected override bool IsArgPassedByRefArchSpecific() - => _argType == CorElementType.ValueType - && _argSize > EnregisteredParamTypeMaxSize - && (!_argTypeHandle.IsHomogeneousAggregate || IsVarArg); - ``` -- **Apple ARM64**: variadic args go *entirely on the stack* (Linux/AAPCS64 - starts on stack only after `X7` is filled; Apple skips registers - altogether). Not yet specifically handled in cDAC -- the iterator only - applies Apple's natural-alignment stack-packing rule, not the - "all-on-stack" varargs rule. -- **Windows ARM64**: a variadic arg whose start fits in a remaining `X` - register but whose tail spills past `X7` is **split** between regs and - stack (the first 64 bytes of the stack are loaded into `X0..X7` and the - rest is contiguous). The CoreCLR VM has this code at - [`callingconvention.h:1740-1756`](../src/coreclr/vm/callingconvention.h); - the cDAC iterator does **not** yet implement it (known gap). - -### Apple ARM64 stack packing - -On Apple ARM64 (Darwin), stack arguments use **natural alignment** (smaller -than 8 bytes) rather than the AAPCS64 8-byte slot. The cDAC handles this in -`StackElemSize` and the per-arg alignment computation: - -```csharp -if (_isAppleArm64ABI) { - int alignment = isValueType ? (isFloatHFA ? 4 : 8) : cbArg; - _ofsStack = AlignUp(_ofsStack, alignment); -} -``` - -## Return values - -| Return shape | Where | -|---|---| -| Integer / pointer / reference / size <= 8 value type | `X0` | -| 16-byte value type | `X0, X1` | -| `R4` / `R8` | `V0` (full SIMD reg) | -| HFA (up to 4 floats/doubles) | `V0..V3` | -| Value type > 16 bytes (and not an HFA) | Caller passes an indirect result pointer in `X8`; callee writes through it; `X8` is **separate** from the regular arg registers | - -## Managed-specific behavior - -### Return buffer is in `X8` - -Unlike the other platforms where the ret buf consumes an argument register -slot, ARM64's AAPCS64 reserves `X8` (the "Indirect Result Location Register") -specifically for the return-buffer pointer. This means **the ret buf does -*not* consume an X0..X7 slot**, and `this` lands in `X0` even when a ret buf -is present. - -The cDAC reflects this with: - -```csharp -public override bool IsRetBuffPassedAsFirstArg => false; -public override int GetRetBuffArgOffset(bool hasThis) => (int)_layout.FirstGCRefMapSlot; -``` - -### Hidden argument prefix - -Standard CLR prefix applies, but the ret buf goes in `X8` rather than `X0`: - -``` -X8 = retBuf (separate reg) -[this:X0] [genericContext:X1] [asyncContinuation:X2] [varArgCookie:X3] userArgs... -``` - -### Implicit by-reference for large value types - -`EnregisteredParamTypeMaxSize = 16`. Value types larger than 16 bytes that -are *not* HFAs go via implicit by-reference; the caller may legitimately -point into the GC heap, so the JIT reports the pointer as a GC `BYREF` and -uses checked write barriers. - -HFAs that are also > 16 bytes (e.g. 4 doubles = 32 bytes) are passed in V -registers when *not* varargs, and by implicit-byref when varargs. See the -`IsArgPassedByRefArchSpecific` override above. - -### Frame pointer - -ARM64 always allocates a frame pointer (`X29`), partly for AAPCS64 frame -chaining and partly for the InlinedCallFrame P/Invoke mechanism. Funclets -share the parent function's `X29` to access its locals. - -### Funclets - -Same managed-EH funclet model. The catch funclet receives the exception -object in `X0`. - -## TypedReference - -`TypedReference` is 16 bytes. It is passed in **2 GP registers** -- typically -`X0, X1` -- since 16 <= `EnregisteredParamTypeMaxSize`. It is *not* an HFA so -the FP branch doesn't apply. Returned in `X0, X1`. - -The cDAC's `ArgTypeInfoSignatureProvider` substitutes the `g_TypedReferenceMT` -MethodTable when the signature contains `ELEMENT_TYPE_TYPEDBYREF`, so the -iterator treats it as an ordinary 16-byte value type. - -## Known gaps in cDAC - -The iterator's correctness is high for fixed-arg calls but has known holes: - -1. **Windows ARM64 varargs split** (the `X7 -> stack` boundary case) is not - implemented. CoreCLR has this at `callingconvention.h:1740-1756`. -2. **Apple ARM64 varargs** ("all variadic args on stack") is not specifically - handled; only the stack-packing portion is. -3. Tests for these gaps are present but marked `[Skip("audit gap")]`: - `Windows_VarArgs_StructSpansX7AndStack_AuditGap4`, - `HFA_FourFloats_ShouldReportFourFPSlots`. - -## References - -- [AAPCS64](https://github.com/ARM-software/abi-aa/blob/main/aapcs64/aapcs64.rst) -- §6.4 parameter passing, §6.8 variadic functions. -- [Apple ARM64 documentation](https://developer.apple.com/documentation/xcode/writing-arm64-code-for-apple-platforms) -- the deviations from AAPCS64. -- [Microsoft ARM64 ABI](https://learn.microsoft.com/cpp/build/arm64-windows-abi-conventions) -- Windows-specific varargs split. -- [src/coreclr/vm/callingconvention.h](../src/coreclr/vm/callingconvention.h) -- `TARGET_ARM64` branches (especially the varargs handling around lines 1700-1760). -- [docs/design/coreclr/botr/clr-abi.md](../docs/design/coreclr/botr/clr-abi.md) -- CLR-wide ABI notes. diff --git a/cdac-calling-conventions/x86.md b/cdac-calling-conventions/x86.md deleted file mode 100644 index a61619732a2ebe..00000000000000 --- a/cdac-calling-conventions/x86.md +++ /dev/null @@ -1,103 +0,0 @@ -# x86 (32-bit) Managed Calling Convention - -**Iterator:** [`X86ArgIterator.cs`](../src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/X86ArgIterator.cs) -**Applies to:** Windows x86, Linux x86 (32-bit). The same convention is used on -both OSes for managed code. -**Base ABI:** CLR-specific (loosely related to Microsoft `__fastcall`); not -documented as a platform standard. - -## Register set - -| Use | Registers | -|---|---| -| Integer arg | `ECX`, `EDX` (2 slots, **filled in declaration order, left-to-right**) | -| Float arg | -- (floats and doubles **never** go in registers; the FPU stack / XMM regs are not used for args) | -| Integer return | `EAX` (`EDX:EAX` for 64-bit) | -| Float return | `ST(0)` (x87 stack top) | -| Volatile (caller-saved) | `EAX, ECX, EDX` | -| Non-volatile (callee-saved) | `EBX, ESI, EDI, EBP` | -| Stack slot size | 4 bytes | - -## Argument placement rules - -The iterator scans arguments **left-to-right** and assigns each to either a -register slot or the stack: - -- An argument may be passed in a register when: - - There is still an unused register slot (`ECX` first, then `EDX`), **and** - - The argument is a pointer-sized primitive, GC reference, byref, or - array/pointer (always enregisters), **or** it is a value type of size - exactly **1, 2, or 4 bytes**. -- Otherwise the argument is pushed on the **stack at decreasing addresses** - (right-to-left in memory). Each stack arg is rounded up to a 4-byte slot. -- `R4` (float), `R8` (double), `I8`/`U8`, and `TypedByRef` **never** enregister - on x86 -- they always go on the stack. - -The eligibility check is in [`X86ArgIterator.IsArgumentInRegister`](../src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/X86ArgIterator.cs). - -## Return values - -| Return shape | Where | -|---|---| -| Integer / pointer / reference / `I4` and smaller value types | `EAX` | -| `I8` / `U8` | `EDX:EAX` (high in `EDX`, low in `EAX`) | -| `R4` / `R8` | `ST(0)` (x87 stack top) | -| Value type > 4 bytes (or 3 bytes), or `TypedByRef` | Caller-allocated return buffer; pointer to it is passed as a hidden first arg (after `this` if any); callee returns the buffer pointer in `EAX` | - -## Managed-specific behavior - -### Hidden argument placement is order-sensitive - -Unlike the 64-bit platforms where the hidden-arg slots are always counted up -front in `ComputeInitialNumRegistersUsed`, x86 places the generic context, the -async continuation, and the vararg cookie **after** the fixed args, and their -final location (`ECX`, `EDX`, or stack) depends on a full sig walk. The -iterator does that sig walk in `ComputeSizeOfArgStack` and records the -locations in `_paramTypeLoc` / `_asyncContinuationLoc`. - -``` -[this] [retBuf] userArgs... [asyncContinuation] [genericContext] -``` - -`this` and `retBuf` *do* take register slots first (see -`ComputeInitialNumRegistersUsed`). - -### `this` is in `ECX`; ret buf is in `ECX` or `EDX` - -If the method has a return buffer: - -- Without `this`: ret buf goes in `ECX` (first slot). -- With `this`: `this` -> `ECX`, ret buf -> `EDX`. - -See `GetRetBuffArgOffset`. - -### Varargs - -For `__arglist` (CLR managed varargs) methods, the cookie is **at the bottom -of the stack frame** (returned by `GetVASigCookieOffset` as -`SizeOfTransitionBlock`), and `_numRegistersUsed` is forced to -`NumArgumentRegisters` so all user args go on the stack. No register -allocation happens -- this matches the runtime's expectation that the callee -can walk varargs by pointer arithmetic. - -### Callee cleans the stack (stdcall-like) - -For non-vararg methods, the callee pops its arguments before returning -(`CbStackPop()` returns the stack arg size). Vararg methods follow `__cdecl` -and require the caller to clean up (`CbStackPop()` returns 0). - -### Implicit by-reference - -x86 does **not** use the implicit-byref mechanism (`EnregisteredParamTypeMaxSize = 0`). -Large value types are passed by value on the stack directly. - -## TypedReference - -`TypedByRef` never enregisters on x86 (see `IsArgumentInRegister` -- it falls -through the `default` case). It is passed by value on the stack as two -pointer-sized slots (`{ ref byte; IntPtr } = { void*; void* }`). - -## References - -- [docs/design/coreclr/botr/clr-abi.md - x86 ABI](../docs/design/coreclr/botr/clr-abi.md) -- search for "x86" sections. -- [src/coreclr/vm/callingconvention.h](../src/coreclr/vm/callingconvention.h) -- `TARGET_X86` branches (search for `#ifdef TARGET_X86`). From bf673d8c6fed79f06b6c2b6620e566043c6506a6 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Fri, 22 May 2026 11:02:12 -0400 Subject: [PATCH 6/9] Strip XML doc comments from cDAC calling-convention port Severely cut down on the doc comments added in this branch. The code is WIP and the comments will be reintroduced (much more focused) once the shape stabilizes. This removes ~700 lines of /// summary/remarks/list blocks from new calling-convention iterators, contracts, tests, and the dump-test debuggee, plus the doc comments added on top of existing modified files (ContractRegistry, IRuntimeTypeSystem, RuntimeTypeSystem_1, GcScanner, DumpTestHelpers, SkipOnArchAttribute). Pre-existing doc comments on those files are preserved. Tests: 2463 passed, 0 failed, 16 skipped. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ContractRegistry.cs | 3 - .../Contracts/ICallingConvention.cs | 106 ------------------ .../Contracts/IRuntimeTypeSystem.cs | 17 --- .../CallingConvention/AMD64UnixArgIterator.cs | 6 - .../AMD64WindowsArgIterator.cs | 9 -- .../CallingConvention/ArgIteratorBase.cs | 38 ------- .../CallingConvention/ArgIteratorFactory.cs | 4 - .../CallingConvention/ArgTypeInfo.cs | 50 --------- .../ArgTypeInfoSignatureProvider.cs | 49 -------- .../CallingConvention/CallingConvention_1.cs | 29 ----- .../SystemVStructClassifier.cs | 37 ------ .../TransitionBlockLayout.cs | 7 -- .../Contracts/RuntimeTypeSystem_1.cs | 10 -- .../StackWalk/GC/GcRefEnumeration.cs | 66 ----------- .../Contracts/StackWalk/GC/GcScanner.cs | 24 ---- .../Contracts/StackWalk/GC/GcTypeKind.cs | 16 --- .../AMD64UnixCallingConventionTests.cs | 11 -- .../AMD64WindowsCallingConventionTests.cs | 23 ---- .../CallingConventionTestHelpers.cs | 19 ---- .../CallingConventionTests.cs | 27 ----- .../CallingConvention/SignatureBlobBuilder.cs | 14 --- .../SyntheticVectorMetadata.cs | 5 - .../DumpTests/CallSiteLayoutDumpTestsBase.cs | 16 --- .../CallSiteLayoutDumpTests_WinX64.cs | 15 --- .../Debuggees/CallSiteLayout/Program.cs | 31 ----- .../cdac/tests/DumpTests/DumpTestHelpers.cs | 4 - .../tests/DumpTests/SkipOnArchAttribute.cs | 24 ---- .../tests/GcScannerReportArgumentTests.cs | 14 --- .../MockDescriptors.CallingConvention.cs | 28 ----- 29 files changed, 702 deletions(-) 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 0fd09c7d8f4120..f26679d77bdd2b 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ContractRegistry.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ContractRegistry.cs @@ -124,9 +124,6 @@ public abstract class ContractRegistry /// Gets an instance of the Debugger contract for the target. /// public virtual IDebugger Debugger => GetContract(); - /// - /// Gets an instance of the CallingConvention contract for the target. - /// public virtual ICallingConvention CallingConvention => GetContract(); /// 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 index 3887991af70b96..a4deacc348b9d5 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ICallingConvention.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ICallingConvention.cs @@ -6,108 +6,15 @@ namespace Microsoft.Diagnostics.DataContractReader.Contracts; -/// -/// Describes a single register or stack slot of an argument at a call site, -/// relative to the start of the transition block. A simple argument has one -/// slot; a split SystemV struct (e.g. struct { object o; double d; }) has -/// multiple slots — one per eightbyte's register or stack location. -/// -/// Byte offset of the slot from the start of the transition block. -/// -/// The that describes the slot's contents (e.g. -/// for a GC ref slot, -/// for a floating-point slot). Callers (e.g. the GC scanner) classify the slot -/// from this. -/// public readonly record struct ArgSlot( int Offset, CorElementType ElementType); -/// -/// Describes the layout of a single argument at a call site, as imposed by the -/// target's managed calling convention. A simple argument has one -/// ; a split struct has multiple. -/// -/// -/// True if the argument is passed by implicit reference (e.g. a value type larger -/// than the ABI's enregister limit). When true, contains a -/// single slot holding an interior pointer to the value. -/// -/// -/// One or more register/stack slots that together carry the argument's value. -/// Always non-empty. -/// -/// -/// Identity of the value type whose storage occupies as an -/// opaque, contiguous, undecomposed buffer, or -/// when the slots do not describe such a buffer. -/// -/// This is layout information about what the per-arch iterator chose to do, not -/// GC information per se: the iterator surfaces the type only when it left the -/// argument's storage in a single contiguous run that the iterator did not -/// crack open into individually-typed s. Consumers that want -/// to inspect the buffer's internals (e.g. the GC scanner walking its GC -/// descriptor, a future debugger arg-formatter rendering the buffer's fields) -/// can resolve the layout via . -/// -/// -/// Populated when all of the following hold: -/// -/// The argument is a value type passed by value -/// (not ). -/// The per-arch iterator left the storage opaque — i.e. -/// every entry in has -/// == , -/// and together they cover one contiguous byte range starting at -/// Slots[0].Offset. Examples: Windows-AMD64 enregistered struct, -/// ARM64 non-HFA struct in consecutive GPRs, any stack-passed struct. -/// -/// -/// -/// Both ordinary value types and ByRefLike types (ref-struct / Span-style) are -/// surfaced here. Consumers select the appropriate field-enumeration strategy -/// by querying : ordinary value types -/// walk the CGCDesc series; ByRefLike types require a field-by-field walk to -/// pick up managed byref fields that the CGCDesc series does not encode. -/// -/// -/// otherwise — i.e. for primitives, references, byrefs, -/// arguments passed by implicit reference (), HFAs -/// and other aggregates the iterator already decomposed into individually-typed -/// slots (e.g. SystemV split struct: one slot per eightbyte with each slot's -/// reflecting that eightbyte's classification). -/// -/// public readonly record struct ArgLayout( bool IsPassedByRef, IReadOnlyList Slots, TypeHandle? ValueTypeHandle = null); -/// -/// Describes the layout of all arguments at a call site, as imposed by the -/// target's managed calling convention. Offsets are byte offsets from the -/// start of the transition block. -/// -/// -/// Byte offset of the this pointer slot if the method is an instance method; -/// for static methods. -/// -/// -/// True if this points at a value-type instance (i.e. the slot contains a -/// managed interior pointer). False for reference-type instance methods. -/// -/// -/// Byte offset of the implicit async-continuation argument slot for async methods; -/// if the method has no async-continuation argument. -/// -/// -/// Byte offset of the vararg-cookie slot for vararg methods; -/// for non-vararg methods. -/// -/// -/// Layout of each fixed argument in declaration order. Empty when the call site -/// cannot be described (e.g. missing signature, decode failure). -/// public readonly record struct CallSiteLayout( int? ThisOffset, bool IsValueTypeThis, @@ -115,23 +22,10 @@ public readonly record struct CallSiteLayout( int? VarArgCookieOffset, IReadOnlyList Arguments); -/// -/// Computes call-site argument layouts according to the target runtime's -/// managed calling convention. -/// public interface ICallingConvention : IContract { static string IContract.Name { get; } = nameof(CallingConvention); - /// - /// Computes the layout of arguments at a call site for the given method. - /// - /// The method whose call site should be described. - /// - /// The call-site layout. Returns a layout with an empty - /// list and null offsets if the method's call site cannot be described - /// (missing signature, decode failure, etc.). - /// CallSiteLayout ComputeCallSiteLayout(MethodDescHandle method) => throw new 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 a50def1ce3534a..d7bcd5228e7f33 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs @@ -288,25 +288,8 @@ public interface IRuntimeTypeSystem : IContract TargetPointer GetFieldDescStaticAddress(TargetPointer fieldDescPointer, bool unboxValueTypes = true) => throw new NotImplementedException(); TargetPointer GetFieldDescThreadStaticAddress(TargetPointer fieldDescPointer, TargetPointer thread, bool unboxValueTypes = true) => throw new NotImplementedException(); - /// - /// Enumerates the FieldDesc pointers for the instance (non-static) fields of - /// , in field-list order. Statics interleaved in - /// the underlying FieldDesc array are skipped. - /// - /// - /// Returns an empty sequence for type handles that do not refer to a MethodTable, - /// or for types with no FieldDescList. - /// IEnumerable EnumerateInstanceFieldDescs(TypeHandle typeHandle) => throw new NotImplementedException(); - /// - /// Resolves a field's declared type without triggering type loading. Mirrors - /// native FieldDesc::LookupApproxFieldTypeHandle (DAC variant): decodes - /// the field's metadata signature and returns the resulting . - /// Returns a default (null) TypeHandle when the field's type is not currently - /// resolvable (e.g., the enclosing module's metadata is unavailable, or the - /// referenced type is not loaded). - /// TypeHandle LookupApproxFieldTypeHandle(TargetPointer fieldDescPointer) => throw new NotImplementedException(); #endregion FieldDesc inspection APIs #region Other APIs diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/AMD64UnixArgIterator.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/AMD64UnixArgIterator.cs index 2ea580bc329bab..6a0f3b10fc8012 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/AMD64UnixArgIterator.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/AMD64UnixArgIterator.cs @@ -6,12 +6,6 @@ namespace Microsoft.Diagnostics.DataContractReader.Contracts.CallingConventionHelpers; -/// -/// Linux/macOS x64 (System V AMD64 ABI) argument iterator. GP args go in -/// RDI/RSI/RDX/RCX/R8/R9; FP args in XMM0-XMM7. Value-type structs <= 16 bytes -/// are classified per the SystemV "eightbyte" rules and may be split across -/// the GP and SSE register banks. -/// internal sealed class AMD64UnixArgIterator : ArgIteratorBase { private readonly Target _target; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/AMD64WindowsArgIterator.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/AMD64WindowsArgIterator.cs index 6ab191290dcbc3..d9eee4dc5142d9 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/AMD64WindowsArgIterator.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/AMD64WindowsArgIterator.cs @@ -6,15 +6,6 @@ namespace Microsoft.Diagnostics.DataContractReader.Contracts.CallingConventionHelpers; -/// -/// Windows x64 (Microsoft AMD64) argument iterator. Each fixed arg occupies a -/// single pointer-sized slot; FP args (R4/R8) shadow into XMM0-XMM3. Implicit -/// byref applies to non-power-of-two structs or structs larger than 8 bytes. -/// -/// -/// See cdac-calling-conventions/amd64-windows.md at the repository root for the full -/// managed calling-convention write-up. -/// internal sealed class AMD64WindowsArgIterator : ArgIteratorBase { public override int NumArgumentRegisters => 4; // RCX, RDX, R8, R9 diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgIteratorBase.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgIteratorBase.cs index a4bcc321be6b32..05974ca6b1613d 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgIteratorBase.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgIteratorBase.cs @@ -8,14 +8,6 @@ namespace Microsoft.Diagnostics.DataContractReader.Contracts.CallingConventionHelpers; -/// -/// Shared ABI-independent logic for cDAC argument iterators. -/// -/// -/// Architecture-specific iterators provide register counts, stack-slot sizing, and -/// per-argument location enumeration, while this base class handles hidden argument -/// bookkeeping, return-buffer decisions, and lazy stack-size computation. -/// internal abstract class ArgIteratorBase { protected readonly TransitionBlockLayout _layout; @@ -33,9 +25,6 @@ internal abstract class ArgIteratorBase #region Construction - /// - /// Initializes a new iterator over a method signature using the supplied transition-block layout. - /// protected ArgIteratorBase( TransitionBlockLayout layout, ArgIteratorData argData, @@ -72,9 +61,6 @@ protected ArgIteratorBase( #region Hidden arguments - /// - /// Gets the transition-block offset of the hidden this argument. - /// public virtual int GetThisOffset() => _layout.ArgumentRegistersOffset; @@ -165,9 +151,6 @@ public virtual int StackElemSize(int parmSize, bool isValueType = false, bool is public virtual bool IsArgPassedByRefBySize(int size) => size > EnregisteredParamTypeMaxSize; - /// - /// Computes the number of register slots consumed by hidden arguments before user arguments begin. - /// protected virtual int ComputeInitialNumRegistersUsed() { int numRegistersUsed = 0; @@ -202,26 +185,17 @@ protected virtual int ComputeInitialNumRegistersUsed() return numRegistersUsed; } - /// - /// Enumerates the fixed user-visible arguments and their locations. - /// public abstract IEnumerable EnumerateArgs(); #endregion #region Signature inspection - /// - /// Gets the argument type at the specified user-visible argument index. - /// public CorElementType GetArgumentType(int argNum, out ArgTypeInfo thArgType) { return _argData.GetArgumentType(argNum, out thArgType); } - /// - /// Gets the method return type. - /// public CorElementType GetReturnType(out ArgTypeInfo thRetType) => _argData.GetReturnType(out thRetType); @@ -229,9 +203,6 @@ public CorElementType GetReturnType(out ArgTypeInfo thRetType) #region Return handling - /// - /// Determines whether the signature uses a hidden return-buffer argument. - /// public bool HasRetBuffArg() { if (!_RETURN_FLAGS_COMPUTED) @@ -242,9 +213,6 @@ public bool HasRetBuffArg() return _RETURN_HAS_RET_BUFFER; } - /// - /// Default return-buffer policy for value-type returns. - /// protected virtual bool ValueTypeReturnNeedsRetBuf(ArgTypeInfo thRetType) { int size = thRetType.Size; @@ -277,9 +245,6 @@ private void ComputeReturnFlags() #region Stack sizing - /// - /// Gets the total stack space consumed by user arguments above the transition block. - /// protected uint SizeOfArgStack() { if (!_SIZE_OF_ARG_STACK_COMPUTED) @@ -298,9 +263,6 @@ private void ForceSigWalk() _SIZE_OF_ARG_STACK_COMPUTED = true; } - /// - /// Computes the stack footprint by walking the argument locations produced by . - /// protected virtual void ComputeSizeOfArgStack() { int maxOffset = _layout.OffsetOfArgs; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgIteratorFactory.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgIteratorFactory.cs index a9e26add517313..9e65113767a303 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgIteratorFactory.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgIteratorFactory.cs @@ -6,10 +6,6 @@ namespace Microsoft.Diagnostics.DataContractReader.Contracts.CallingConventionHelpers; -/// -/// Factory to create the appropriate per-arch -/// subclass for the target architecture. -/// internal static class ArgIteratorFactory { public static ArgIteratorBase Create( diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgTypeInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgTypeInfo.cs index 1ac071c52cd2ce..7d1bb1414018b0 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgTypeInfo.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgTypeInfo.cs @@ -10,16 +10,6 @@ namespace Microsoft.Diagnostics.DataContractReader.Contracts.CallingConventionHelpers; -/// -/// Pre-computed type information needed by for -/// calling convention analysis. This is a value type to avoid allocations -/// during argument iteration. -/// -/// -/// Mirrors crossgen2's TypeHandle struct in ArgIterator.cs, but uses -/// data from the cDAC's rather than -/// crossgen2's TypeDesc. -/// internal readonly struct ArgTypeInfo { public CorElementType CorElementType { get; init; } @@ -29,19 +19,10 @@ internal readonly struct ArgTypeInfo public bool IsHomogeneousAggregate { get; init; } public int HomogeneousAggregateElementSize { get; init; } - /// - /// The TypeHandle from the target runtime, used for value type field enumeration - /// and SystemV struct classification. - /// public TypeHandle RuntimeTypeHandle { get; init; } public bool IsNull => CorElementType == default && Size == 0; - /// - /// Gets the element size for a given CorElementType, matching crossgen2's - /// TypeHandle.GetElemSize. Returns the type's actual size for value - /// types, or pointer size for reference types. - /// public static int GetElemSize(CorElementType t, ArgTypeInfo thValueType, int pointerSize) { if ((int)t <= 0x1d) @@ -90,14 +71,6 @@ public static int GetElemSize(CorElementType t, ArgTypeInfo thValueType, int poi -2, // ELEMENT_TYPE_SZARRAY 0x1d ]; - /// - /// Creates an from a target TypeHandle using the - /// runtime type system contract. Handles primitives (using the static element-size - /// table), reference types (pointer-sized, projected to - /// to match downstream classifier expectations), and real value types (full MT layout - /// query for size / HFA / alignment). Mirrors native MetaSig::GetByValType + - /// SigPointer::PeekElemTypeNormalized. - /// public static ArgTypeInfo FromTypeHandle(Target target, TypeHandle th) { IRuntimeTypeSystem rts = target.Contracts.RuntimeTypeSystem; @@ -162,19 +135,6 @@ private static ArgTypeInfo FromValueType(Target target, IRuntimeTypeSystem rts, }; } - /// - /// Computes the element size of a Homogeneous Floating-point Aggregate (HFA), - /// matching crossgen2's DefType.GetHomogeneousAggregateElementSize. - /// - /// - /// On ARM, the element size is fully determined by the alignment requirement: - /// HFAs of doubles have 8-byte alignment; HFAs of floats use 4-byte alignment. - /// - /// On ARM64, we walk the first field of the value type, recursing through nested - /// value types until we reach a primitive (R4/R8) or a Vector intrinsic - /// (Vector64`1, Vector128`1, or System.Numerics.Vector`1). This mirrors the - /// runtime's MethodTable::GetHFAType in src/coreclr/vm/class.cpp. - /// private static int ComputeHfaElementSize(Target target, IRuntimeTypeSystem rts, TypeHandle th, bool requiresAlign8) { RuntimeInfoArchitecture arch = target.Contracts.RuntimeInfo.GetTargetArchitecture(); @@ -224,19 +184,9 @@ private static int ComputeHfaElementSize(Target target, IRuntimeTypeSystem rts, return 0; } - /// - /// Creates an for a primitive type that doesn't need - /// type handle resolution. - /// public static ArgTypeInfo ForPrimitive(CorElementType corType, int pointerSize) => ForPrimitive(corType, pointerSize, default); - /// - /// Creates an for a primitive / reference type, optionally - /// carrying its resolved . The handle is used downstream by - /// generic-instantiation lookup so a type argument like string or int - /// can be matched against an instantiated type's PerInstInfo. - /// public static ArgTypeInfo ForPrimitive(CorElementType corType, int pointerSize, TypeHandle runtimeTypeHandle) { return new ArgTypeInfo diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgTypeInfoSignatureProvider.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgTypeInfoSignatureProvider.cs index 1a53e263de1597..c83e34c5e03972 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgTypeInfoSignatureProvider.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/ArgTypeInfoSignatureProvider.cs @@ -11,33 +11,8 @@ namespace Microsoft.Diagnostics.DataContractReader.Contracts.CallingConventionHelpers; -/// -/// Generic context used to resolve ELEMENT_TYPE_VAR and ELEMENT_TYPE_MVAR -/// while decoding a method signature into values. -/// is the owning type's (used for VAR), -/// and is the owning method's -/// (used for MVAR). -/// internal readonly record struct ArgTypeInfoSignatureContext(TypeHandle ClassContext, MethodDescHandle MethodContext); -/// -/// Decodes signature elements directly into so that -/// can drive argument iteration without an intermediate -/// classification stage. -/// Implements , which -/// is a superset of SRM's -/// adding support for ELEMENT_TYPE_INTERNAL. -/// -/// -/// The provider is scoped to a single module: GetTypeFromDefinition and -/// GetTypeFromReference resolve TypeDef/TypeRef tokens via the module's lookup -/// tables so enums (and other runtime-normalized value types) are classified using their -/// actual , matching native -/// SigPointer::PeekElemTypeNormalized. For value-type elements the resolved -/// is surfaced in so -/// ArgIterator sees the correct size / HFA / alignment in a single signature walk -/// (mirroring native MetaSig::GetByValType). -/// internal sealed class ArgTypeInfoSignatureProvider : IRuntimeSignatureTypeProvider { @@ -75,11 +50,6 @@ public ArgTypeInfo GetPrimitiveType(PrimitiveTypeCode typeCode) private ArgTypeInfo PrimitiveWithHandle(CorElementType corType) => ArgTypeInfo.ForPrimitive(corType, _target.PointerSize, ResolvePrimitiveTypeHandle(corType)); - /// - /// Resolves the canonical for a primitive - /// via the CoreLib binder. Returns default on failure (e.g. older runtime image, or a - /// CorElementType with no binder entry such as ). - /// private TypeHandle ResolvePrimitiveTypeHandle(CorElementType corType) { if (_primitiveTypeHandles.TryGetValue(corType, out TypeHandle cached)) @@ -269,12 +239,6 @@ public ArgTypeInfo GetInternalType(TargetPointer typeHandlePointer) } } - /// - /// Resolve a TypeDef/TypeRef token via the module's lookup tables and build an - /// from the resulting . Falls back - /// to a -driven conservative placeholder when the type - /// has not been loaded. - /// private ArgTypeInfo FromTokenLookup(TargetPointer lookupTable, int token, byte rawTypeKind) { try @@ -296,14 +260,6 @@ private ArgTypeInfo FallbackForRawTypeKind(byte rawTypeKind) ? UnresolvedValueType() : ArgTypeInfo.ForPrimitive(CorElementType.Class, _target.PointerSize); - /// - /// Build an from a resolved . Mirrors - /// native SigPointer::PeekElemTypeNormalized + MetaSig::GetByValType: - /// enums collapse to their underlying primitive (via GetSignatureCorElementType); - /// value types surface the resolved with full size / HFA / - /// alignment for ArgIterator; reference types preserve the handle for generic - /// type-argument matching. - /// private ArgTypeInfo BuildFromTypeHandle(TypeHandle typeHandle) { if (typeHandle.Address == TargetPointer.Null) @@ -312,11 +268,6 @@ private ArgTypeInfo BuildFromTypeHandle(TypeHandle typeHandle) return ArgTypeInfo.FromTypeHandle(_target, typeHandle); } - /// - /// Conservative value-type placeholder used when a TypeSpec / TypedReference / - /// unloaded TypeDef/TypeRef can't be resolved to a concrete . - /// ArgIterator sees a pointer-sized value type with no HFA classification. - /// private ArgTypeInfo UnresolvedValueType() => new ArgTypeInfo { diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CallingConvention_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CallingConvention_1.cs index 1f78afa149edc2..6e8da67366974d 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CallingConvention_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CallingConvention_1.cs @@ -12,11 +12,6 @@ namespace Microsoft.Diagnostics.DataContractReader.Contracts; -/// -/// CoreCLR implementation of . Decodes method -/// signatures and drives a per-arch subclass to -/// compute per-argument offsets and pass-style for a call site. -/// internal sealed class CallingConvention_1 : ICallingConvention { private static readonly IReadOnlyList EmptyArgs = Array.Empty(); @@ -98,24 +93,6 @@ CallSiteLayout ICallingConvention.ComputeCallSiteLayout(MethodDescHandle method) return new CallSiteLayout(thisOffset, isValueTypeThis, asyncOffset, varArgCookieOffset, args); } - /// - /// Mirrors native MetaSig::GcScanRoots's value-type branch - /// (see src/coreclr/vm/siginfo.cpp): when an argument is a value type - /// passed by value in storage that the per-arch iterator did not - /// GC-decompose, the GC scanner walks the type's layout to report embedded - /// refs. Surface the value-type's on - /// so the scanner can do that walk. The scanner - /// dispatches on to choose - /// between a CGCDesc walk (ordinary value types) and a field walk - /// (ByRefLike types: Span<T>, ref structs). - /// - /// - /// The value type's when the layout describes a - /// contiguous by-value buffer that requires a layout-driven walk to report - /// refs (including ByRefLike types); otherwise - /// (primitives, references, byref-passed value types, iterator-decomposed - /// slots). - /// private static TypeHandle? ComputeValueTypeHandle(ArgLocDesc loc, List slots) { // Pass-by-implicit-reference: the slot holds an interior pointer and the @@ -154,12 +131,6 @@ CallSiteLayout ICallingConvention.ComputeCallSiteLayout(MethodDescHandle method) return th; } - /// - /// Decodes the signature for into a - /// . Matches native - /// MethodDesc::GetSig: prefers a stored signature (dynamic, EEImpl, and - /// array method descs) before falling back to a metadata token lookup. - /// private bool TryDecodeSignature(MethodDescHandle method, out MethodSignature methodSig) { methodSig = default; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/SystemVStructClassifier.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/SystemVStructClassifier.cs index a64bcf8d109907..87c44f7e8caa8a 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/SystemVStructClassifier.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/SystemVStructClassifier.cs @@ -8,18 +8,6 @@ namespace Microsoft.Diagnostics.DataContractReader.Contracts.CallingConventionHelpers; -/// -/// SystemV AMD64 ABI struct classifier. Walks a value type's instance fields, -/// builds a per-byte map of field classifications, and assembles eightbyte -/// classifications per the SystemV spec (§3.2.3 "Passing"). Mirrors -/// SystemVStructClassificator.GetSystemVAmd64PassStructInRegisterDescriptor -/// in src/coreclr/tools/Common/JitInterface/SystemVStructClassificator.cs. -/// -/// -/// Used only by to decide between register -/// placement (with possible GP+SSE split), implicit by-reference, and stack -/// passing for value types on Linux/macOS x64. -/// internal static class SystemVStructClassifier { private const int MaxFields = SystemVStructDescriptor.MaxEightBytes * SystemVStructDescriptor.EightByteSizeInBytes; @@ -55,11 +43,6 @@ private struct Helper }; } - /// - /// Attempts to classify the given value type per the SystemV AMD64 ABI. - /// Returns a descriptor with - /// set to true when the struct can be passed in registers, false otherwise. - /// public static SystemVStructDescriptor Classify(Target target, TypeHandle typeHandle, int structSize) { if (!typeHandle.IsMethodTable() || structSize == 0 @@ -109,11 +92,6 @@ private static bool IsSimdOrInt128Intrinsic(IRuntimeTypeSystem rts, TypeHandle t } } - /// - /// Maps a primitive to its initial SystemV - /// classification. Mirrors TypeDef2SystemVClassification in the JIT - /// classifier. - /// private static SystemVClassification CorElementTypeToClassification(CorElementType et) => et switch { CorElementType.Boolean or CorElementType.Char @@ -138,10 +116,6 @@ or CorElementType.GenericInst _ => SystemVClassification.Unknown, }; - /// - /// Merge lattice for overlapping/union fields. Matches - /// SystemVStructClassificator.ReClassifyField. - /// private static SystemVClassification ReClassifyField(SystemVClassification original, SystemVClassification @new) { switch (@new) @@ -165,12 +139,6 @@ private static SystemVClassification ReClassifyField(SystemVClassification origi } } - /// - /// Walks the instance fields of , classifying each. - /// Returns false if the struct cannot be enregistered (unaligned field, embedded - /// struct that can't enregister, etc.); true if the helper has been populated - /// successfully. - /// private static bool ClassifyEightBytes( Target target, IRuntimeTypeSystem rts, @@ -303,11 +271,6 @@ private static bool ClassifyEightBytes( return true; } - /// - /// Byte-by-byte sweep that assembles eightbyte classifications from the - /// per-field classifications recorded in . Matches - /// SystemVStructClassificator.AssignClassifiedEightByteTypes. - /// private static void AssignClassifiedEightByteTypes(ref Helper helper) { const int MaxBytes = SystemVStructDescriptor.MaxEightBytes * SystemVStructDescriptor.EightByteSizeInBytes; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/TransitionBlockLayout.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/TransitionBlockLayout.cs index 3e30af11edec96..3284996ca8033b 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/TransitionBlockLayout.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/TransitionBlockLayout.cs @@ -3,13 +3,6 @@ namespace Microsoft.Diagnostics.DataContractReader.Contracts.CallingConventionHelpers; -/// -/// Pure-data view over the target's TransitionBlock layout. Holds the -/// descriptor-read offsets plus the target's -/// and . Has no per-architecture logic — -/// per-arch ABI knowledge lives entirely in the -/// hierarchy. -/// internal sealed class TransitionBlockLayout { public Target Target { get; } 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 56518373aebc9a..4c90916bba866d 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs @@ -2277,19 +2277,9 @@ void IRuntimeTypeSystem.GetCoreLibFieldDescAndDef(string @namespace, string type fieldDef = mdReader.GetFieldDefinition(fieldHandle); } - /// - /// Mirrors native MethodTable::GetNumInstanceFieldBytes: - /// returns BaseSize - EEClass.BaseSizePadding, the number of bytes occupied - /// by the instance fields of the type. - /// public int GetNumInstanceFieldBytes(TypeHandle typeHandle) => (int)GetBaseSize(typeHandle) - GetClassData(typeHandle).BaseSizePadding; - /// - /// Gets the and for the module that - /// owns the given . Returns false if the TypeHandle does not - /// refer to a method table, has no owning module, or the module's metadata cannot be located. - /// private bool TryGetMetadataReader(TypeHandle typeHandle, out ModuleHandle moduleHandle, [NotNullWhen(true)] out MetadataReader? mdReader) { moduleHandle = default; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcRefEnumeration.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcRefEnumeration.cs index 175ffe882d3b08..c101369e8b50ba 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcRefEnumeration.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcRefEnumeration.cs @@ -7,43 +7,8 @@ namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; -/// -/// Helpers for enumerating embedded managed references inside an unboxed value-type -/// instance. Mirrors native ReportPointersFromValueType -/// (src/coreclr/vm/siginfo.cpp): a CGCDesc series walk with the boxed-to-unboxed -/// offset adjustment subtracted out. -/// -/// -/// Callers are responsible for ensuring is an ordinary -/// (non-ByRefLike) value type when calling . -/// ByRefLike types (Span<T>, ref structs) can carry byref-typed fields at -/// arbitrary offsets which the CGCDesc series does not describe; use -/// for those. -/// internal static class GcRefEnumeration { - /// - /// Yields the address of every managed reference embedded inside an unboxed - /// value-type instance located at . - /// - /// Runtime type system contract, used to read the CGCDesc series. - /// The value type whose layout describes the unboxed instance. - /// Address of the start of the unboxed instance (the field area). - /// Target pointer size in bytes (4 or 8). - /// - /// - /// returns offsets measured from the start - /// of a boxed object (i.e. including the MethodTable* prefix). For an - /// unboxed instance the same field sits bytes earlier, so - /// we subtract from each series offset. This matches the - /// native adjustment in ReportPointersFromValueType. - /// - /// - /// References are emitted in series order (i.e. the order the runtime stored them in - /// the GCDesc); no deduplication or sorting is performed. numComponents is fixed - /// at 0 because value-type arguments are never arrays. - /// - /// public static IEnumerable EnumerateValueTypeRefs( IRuntimeTypeSystem rts, TypeHandle valueType, @@ -65,37 +30,6 @@ public static IEnumerable EnumerateValueTypeRefs( } } - /// - /// Yields the GC roots embedded inside an unboxed ByRefLike value-type instance - /// (a Span<T>, ReadOnlySpan<T>, or other ref struct) - /// located at . - /// - /// Runtime type system contract, used to walk the type's fields. - /// The ByRefLike value type whose layout describes the instance. - /// Address of the start of the unboxed instance (the field area). - /// Target pointer size in bytes (4 or 8). Used for nested non-ByRefLike value-type recursion. - /// - /// - /// Mirrors native MetaSig::ReportPointersFromValueTypeArg / - /// ByRefPointerOffsetsReporter in siginfo.cpp: ByRefLike types have - /// no usable CGCDesc series (interior byrefs are not encoded there), so we walk the - /// declared instance fields and emit one root per ref/byref field. Object refs are - /// emitted with ; managed byrefs are emitted with - /// . - /// - /// - /// Nested aggregate fields are handled compositionally: a nested ByRefLike field - /// recurses through this method; a nested non-ByRefLike value-type field delegates - /// to for the standard CGCDesc walk. - /// - /// - /// Primitives, raw pointers (), and function pointers - /// () are not GC-relevant and yield nothing. If a - /// nested value-type field's can't be resolved (e.g. the - /// enclosing module's metadata is unavailable), that field is skipped — matching the - /// native DAC's conservative behavior. - /// - /// public static IEnumerable<(TargetPointer Address, GcScanFlags Flags)> EnumerateByRefLikeRoots( IRuntimeTypeSystem rts, TypeHandle byRefLikeType, 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 3f73711d041340..bcd43aeb399592 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 @@ -351,31 +351,7 @@ private void PromoteCallerStack( } /// - /// Reports GC references for a single argument from a transition frame's caller-stack - /// arguments. Mirrors the per-argument dispatch in native - /// MetaSig::GcScanRoots (src/coreclr/vm/siginfo.cpp): - /// - /// Reference slots are reported directly. - /// Byref slots are reported as interior pointers. - /// Value-type / TypedByRef slots passed by implicit reference are - /// reported as a single interior pointer (the caller-allocated buffer). - /// Value-type slots passed by value with a known - /// have their embedded refs enumerated: - /// ordinary value types via a CGCDesc walk (mirroring - /// ReportPointersFromValueType), ByRefLike types (Span<T>, - /// ref structs) via a field-by-field walk that also reports managed byref - /// fields as (mirroring - /// ByRefPointerOffsetsReporter). - /// /// - /// - /// When the per-arch iterator GC-decomposed an argument (SysV split structs, ARM64 - /// HFAs), each slot already carries a GC-typed - /// (Class, Byref, etc.) and is - /// null; the per-slot loop handles those cases. The layout-driven walks only fire - /// when the storage is contiguous and undecomposed, indicated by a non-null - /// . - /// internal static void ReportArgument( ArgLayout arg, TargetPointer transitionBlock, diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcTypeKind.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcTypeKind.cs index de38ade64458c7..7e1c9c3f0a039a 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcTypeKind.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcTypeKind.cs @@ -3,32 +3,16 @@ namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; -/// -/// GC classification of an argument for stack scanning. Mirrors the m_gc field of -/// native gElementTypeInfo in src/coreclr/vm/siginfo.cpp. -/// internal enum GcTypeKind { - /// Not a GC reference (primitives, pointers). None, - /// Object reference (class, string, array). Ref, - /// Interior pointer (byref). Interior, - /// Value type that may contain embedded GC references. Other, } -/// -/// Maps a to its GC classification. Pure function of the -/// element type; not calling-convention or ABI dependent. -/// internal static class GcTypeKindClassifier { - /// - /// Maps a (possibly normalized) to its GC classification, - /// matching the m_gc field of native gElementTypeInfo. - /// public static GcTypeKind GetGcKind(CorElementType etype) => etype switch { CorElementType.Class or CorElementType.Object or CorElementType.String diff --git a/src/native/managed/cdac/tests/CallingConvention/AMD64UnixCallingConventionTests.cs b/src/native/managed/cdac/tests/CallingConvention/AMD64UnixCallingConventionTests.cs index 80417748c46139..6610da162b228f 100644 --- a/src/native/managed/cdac/tests/CallingConvention/AMD64UnixCallingConventionTests.cs +++ b/src/native/managed/cdac/tests/CallingConvention/AMD64UnixCallingConventionTests.cs @@ -7,17 +7,6 @@ namespace Microsoft.Diagnostics.DataContractReader.Tests; -/// -/// AMD64-Unix (System V AMD64 ABI) calling-convention tests. GP args go in -/// RDI/RSI/RDX/RCX/R8/R9 (6 slots); FP args in XMM0-XMM7 (8 slots). The two -/// banks are independent. -/// -/// The SysV struct classifier (SystemVStructClassifier) is exercised -/// end-to-end via the contract: each struct test allocates a value-type MT -/// in mock memory and references it via ELEMENT_TYPE_INTERNAL in the -/// stored sig blob. -/// -/// public class AMD64UnixCallingConventionTests { private static readonly Lazy s_syntheticVectorMetadata = new(SyntheticVectorMetadata.Create); diff --git a/src/native/managed/cdac/tests/CallingConvention/AMD64WindowsCallingConventionTests.cs b/src/native/managed/cdac/tests/CallingConvention/AMD64WindowsCallingConventionTests.cs index 3af13586462c96..c0ce14a52ac85d 100644 --- a/src/native/managed/cdac/tests/CallingConvention/AMD64WindowsCallingConventionTests.cs +++ b/src/native/managed/cdac/tests/CallingConvention/AMD64WindowsCallingConventionTests.cs @@ -8,11 +8,6 @@ namespace Microsoft.Diagnostics.DataContractReader.Tests; -/// -/// AMD64-Windows (Microsoft x64) calling-convention tests. Each fixed arg -/// occupies a single pointer-sized slot; FP args shadow into XMM0-XMM3 at -/// the same index as their GP slot. -/// public class AMD64WindowsCallingConventionTests { private static readonly Lazy s_syntheticVectorMetadata = new(SyntheticVectorMetadata.Create); @@ -208,12 +203,6 @@ public void InstanceMethod_RetBuf_FirstUserDoubleShiftsToXMM2() Assert.Equal(OffsetOfFirstFPArg + 2 * Case.FloatRegisterSize, layout.Arguments[0].Slots[0].Offset); } - /// - /// Verifies the CLR's hidden-arg prefix on AMD64 Windows. Each hidden arg (this, - /// retBuf, genericContext, asyncContinuation) consumes one of RCX/RDX/R8/R9. - /// The first user-arg double therefore lands at XMM<count-of-hidden-args>, - /// or on the stack when all 4 register slots are consumed. - /// [Theory] [InlineData(false, false, false, false, 0)] [InlineData(true, false, false, false, 1)] @@ -269,11 +258,6 @@ public void HiddenArgs_ShiftFirstUserDouble( Assert.Equal(expectedOffset, layout.Arguments[0].Slots[0].Offset); } - /// - /// Verifies the CLR managed-vararg prefix order on AMD64 Windows. The cookie - /// occupies the slot after this/retBuf and before user args. The first user - /// arg therefore lands at slot index (hidden-arg count + 1 [for cookie]). - /// [Theory] [InlineData(false, false, 0, 1)] [InlineData(true, false, 1, 2)] @@ -550,13 +534,6 @@ public void GCReferenceArgs_GoToGPRegs_NotByref(CorElementType refType) Assert.Equal(OffsetOfFirstGPArg, arg.Slots[0].Offset); } - /// - /// On AMD64 Windows, vector types are classified purely by size — the iterator - /// never consults GetVectorSize. Vector64 (8 B) enregisters as a GP slot; - /// Vector128 (16 B) is passed via implicit byref. This test confirms that - /// end-to-end vector detection (synthetic metadata -> GetVectorSize -> ArgTypeInfo) - /// produces the same placement the size rule would. - /// [Theory] [InlineData("Vector64`1", 8, false)] [InlineData("Vector128`1", 16, true)] diff --git a/src/native/managed/cdac/tests/CallingConvention/CallingConventionTestHelpers.cs b/src/native/managed/cdac/tests/CallingConvention/CallingConventionTestHelpers.cs index 0467a63bdf6b29..5c4156f0866ea9 100644 --- a/src/native/managed/cdac/tests/CallingConvention/CallingConventionTestHelpers.cs +++ b/src/native/managed/cdac/tests/CallingConvention/CallingConventionTestHelpers.cs @@ -13,12 +13,6 @@ namespace Microsoft.Diagnostics.DataContractReader.Tests; -/// -/// End-to-end test harness for the contract. -/// Builds a mock target containing a single stored-sig EEImpl method whose -/// signature is encoded as a raw blob — so the harness bypasses the -/// metadata reader entirely. -/// internal static class CallingConventionTestHelpers { public static (Target Target, MethodDescHandle Handle) CreateTargetWithStaticMethod( @@ -36,19 +30,6 @@ public static (Target Target, MethodDescHandle Handle) CreateTargetWithMethod( SyntheticVectorMetadata? syntheticMetadata = null) => CreateTargetWithMethod(testCase, hasThis, (_, sig) => buildSignature(sig), hasParamType, hasAsyncContinuation, syntheticMetadata: syntheticMetadata); - /// - /// Richer overload that gives the test callback access to the - /// builder so it can - /// allocate auxiliary mock types (e.g. value-type MTs for - /// ELEMENT_TYPE_INTERNAL sig references) before building the - /// method signature. - /// - /// - /// Optional callback to choose a different MethodTable as the enclosing - /// class of the test method (default is System.Object). The callback - /// runs after the configure callback so it can refer to MTs the - /// configure callback allocated. - /// public static (Target Target, MethodDescHandle Handle) CreateTargetWithMethod( CallConvTestCase testCase, bool hasThis, diff --git a/src/native/managed/cdac/tests/CallingConvention/CallingConventionTests.cs b/src/native/managed/cdac/tests/CallingConvention/CallingConventionTests.cs index 0018f1a51bdafe..03428d5f8b5a6a 100644 --- a/src/native/managed/cdac/tests/CallingConvention/CallingConventionTests.cs +++ b/src/native/managed/cdac/tests/CallingConvention/CallingConventionTests.cs @@ -6,27 +6,6 @@ namespace Microsoft.Diagnostics.DataContractReader.Tests; -/// -/// Cross-architecture tests for . These -/// verify harness-level invariants (the contract decodes without throwing, -/// arg counts match) that should hold on every supported architecture. -/// Per-architecture offset assertions live in the platform-specific test -/// classes (e.g. AMD64WindowsCallingConventionTests). -/// -/// -/// Gaps NOT covered by Skip-tagged tests anywhere: -/// -/// #8 Base ComputeSizeOfArgStack byref adjustment — observable -/// via internal CbStackPop() / SizeOfFrameArgumentArray() -/// only, which are not exposed through . -/// #12 ARM64 / RV byref classification differences — the cDAC -/// heuristic agrees with native for all currently exercised struct shapes; -/// a divergent shape would need to be identified from native source. -/// Arm32 softfp detection — no detection mechanism in cDAC today. -/// SysV generic value-type TypeSpec resolution — needs generic -/// instantiation infrastructure in the mock RTS. -/// -/// public class CallingConventionTests { [Theory] @@ -58,12 +37,6 @@ public void Harness_InstanceMethod_ReportsThisOffset(CallConvTestCase testCase) Assert.NotNull(layout.ThisOffset); } - /// - /// Verifies is true when the - /// instance method's enclosing class is a value type. Not arch-specific — - /// the bit is computed by CallingConvention_1 directly from the - /// enclosing MT's IsValueType flag. - /// [Fact] public void InstanceMethod_OnValueType_IsValueTypeThisShouldBeTrue() { diff --git a/src/native/managed/cdac/tests/CallingConvention/SignatureBlobBuilder.cs b/src/native/managed/cdac/tests/CallingConvention/SignatureBlobBuilder.cs index ceeadff7864f91..74261931db5243 100644 --- a/src/native/managed/cdac/tests/CallingConvention/SignatureBlobBuilder.cs +++ b/src/native/managed/cdac/tests/CallingConvention/SignatureBlobBuilder.cs @@ -8,12 +8,6 @@ namespace Microsoft.Diagnostics.DataContractReader.Tests; -/// -/// Builds a method signature blob (ECMA-335 §II.23.2.1) for tests. Supports -/// primitive element types and the cDAC-extension ELEMENT_TYPE_INTERNAL -/// (0x21) for referencing mock value-type method tables directly without a -/// metadata reader. -/// internal sealed class SignatureBlobBuilder { private const byte HASTHIS_FLAG = 0x20; @@ -62,14 +56,6 @@ public SignatureBlobBuilder ParamValueType(TargetPointer methodTablePtr) return this; } - /// - /// Adds a parameter of ELEMENT_TYPE_CLASS with a dummy TypeDef token. - /// The token is encoded per ECMA-335 §II.23.2.8 (TypeDefOrRefOrSpecEncoded). - /// The cDAC signature decoder resolves this via the Loader's lookup tables; - /// when those aren't populated (as in most tests), it falls back to a - /// pointer-sized CorElementType.Class placeholder -- which is the - /// correct calling-convention shape for any managed reference type. - /// public SignatureBlobBuilder ParamClass() { _params.Add(new ParamSpec(CorElementType.Class, default, true)); diff --git a/src/native/managed/cdac/tests/CallingConvention/SyntheticVectorMetadata.cs b/src/native/managed/cdac/tests/CallingConvention/SyntheticVectorMetadata.cs index f0ebbccde072f4..10ac3b6e32293b 100644 --- a/src/native/managed/cdac/tests/CallingConvention/SyntheticVectorMetadata.cs +++ b/src/native/managed/cdac/tests/CallingConvention/SyntheticVectorMetadata.cs @@ -89,11 +89,6 @@ public static SyntheticVectorMetadata Create() return new SyntheticVectorMetadata(provider, typeDefTokens); } - /// - /// Creates a metadata image containing the standard vector types plus one - /// additional type. Used by tests that need an intrinsic-flagged type whose - /// name the runtime does NOT recognize (e.g. to verify GetVectorSize returns 0). - /// public static SyntheticVectorMetadata CreateWithExtraType(string extraNamespace, string extraName) { MetadataBuilder builder = new(); diff --git a/src/native/managed/cdac/tests/DumpTests/CallSiteLayoutDumpTestsBase.cs b/src/native/managed/cdac/tests/DumpTests/CallSiteLayoutDumpTestsBase.cs index fdcb888b7b91ab..489006c2ff4130 100644 --- a/src/native/managed/cdac/tests/DumpTests/CallSiteLayoutDumpTestsBase.cs +++ b/src/native/managed/cdac/tests/DumpTests/CallSiteLayoutDumpTestsBase.cs @@ -7,12 +7,6 @@ namespace Microsoft.Diagnostics.DataContractReader.DumpTests; -/// -/// Shared infrastructure for the CallSiteLayout dump tests. Each ABI gets its -/// own subclass (, etc.) that -/// applies [SkipOnOS] / [SkipOnArch] attributes and encodes -/// the expected ABI-specific layout for every frame. -/// public abstract class CallSiteLayoutDumpTestsBase : DumpTestBase { protected override string DebuggeeName => "CallSiteLayout"; @@ -22,10 +16,6 @@ public abstract class CallSiteLayoutDumpTestsBase : DumpTestBase // FailFast thread; resolution then visits every frame above it. private const string LeafFrame = "M_Combo_RefStructWithMultipleRefs"; - /// - /// Walks the FailFast thread once, recording the MethodDescHandle of - /// every named frame visited. Frame names beyond the chain are ignored. - /// protected Dictionary CollectChainMethods() { ThreadData thread = DumpTestHelpers.FindThreadWithMethod(Target, LeafFrame); @@ -47,9 +37,6 @@ protected Dictionary CollectChainMethods() return result; } - /// - /// Looks up the named frame on the FailFast thread and computes its layout. - /// protected CallSiteLayout LayoutFor(string methodName) { Dictionary methods = CollectChainMethods(); @@ -60,7 +47,6 @@ protected CallSiteLayout LayoutFor(string methodName) // ===== Common assertion helpers ===== - /// Asserts a single-arg signature where the arg is passed by managed/implicit byref. protected void AssertSingleByRef(string frame) { CallSiteLayout layout = LayoutFor(frame); @@ -70,7 +56,6 @@ protected void AssertSingleByRef(string frame) Assert.Null(arg.ValueTypeHandle); } - /// Asserts a single-arg signature passed by value with the given value-type name. protected void AssertSingleByValueVT(string frame, string typeName) { CallSiteLayout layout = LayoutFor(frame); @@ -81,7 +66,6 @@ protected void AssertSingleByValueVT(string frame, string typeName) Assert.Equal(typeName, DumpTestHelpers.GetTypeName(Target, arg.ValueTypeHandle.Value)); } - /// Asserts a single-arg signature for a managed object reference (no VTH, not byref). protected void AssertSingleManagedRef(string frame) { CallSiteLayout layout = LayoutFor(frame); diff --git a/src/native/managed/cdac/tests/DumpTests/CallSiteLayoutDumpTests_WinX64.cs b/src/native/managed/cdac/tests/DumpTests/CallSiteLayoutDumpTests_WinX64.cs index 5b64d7cd50dcda..e5b26b38858edf 100644 --- a/src/native/managed/cdac/tests/DumpTests/CallSiteLayoutDumpTests_WinX64.cs +++ b/src/native/managed/cdac/tests/DumpTests/CallSiteLayoutDumpTests_WinX64.cs @@ -6,21 +6,6 @@ namespace Microsoft.Diagnostics.DataContractReader.DumpTests; -/// -/// Windows x64 dump-based tests for -/// . The debuggee -/// (CallSiteLayout) builds a deep call chain that holds every relevant -/// calling-convention shape live to Environment.FailFast. Each test -/// asserts the Win-x64 ABI layout for one frame. -/// -/// Win-x64 rules in one paragraph: every argument occupies exactly one 8-byte -/// slot in the transition block. A value type is passed by value iff its size -/// is 1, 2, 4, or 8 bytes; otherwise it is copied to the caller's stack and the -/// slot holds a pointer to that copy (IsPassedByRef = true, set by -/// ArgIterator, not a user ref). HFAs are not enregistered on -/// Win-x64 (Microsoft ABI). 4 GP arg registers (RCX/RDX/R8/R9), 4 XMM lanes -/// mirrored; further args spill to the stack at +0x20. -/// public class CallSiteLayoutDumpTests_WinX64 : CallSiteLayoutDumpTestsBase { // ===== Category A: register-bank fill / spill (no GC refs) ===== diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/CallSiteLayout/Program.cs b/src/native/managed/cdac/tests/DumpTests/Debuggees/CallSiteLayout/Program.cs index c4e3da6f29df9c..380acf93896dbb 100644 --- a/src/native/managed/cdac/tests/DumpTests/Debuggees/CallSiteLayout/Program.cs +++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/CallSiteLayout/Program.cs @@ -6,23 +6,6 @@ using System.Runtime.CompilerServices; using System.Runtime.Intrinsics; -/// -/// Debuggee for cDAC CallSiteLayoutDumpTests_*. Builds one deep call -/// chain where every frame is held live to . -/// Each frame exercises a single calling-convention axis (or, in the Combo -/// section, a deliberate combination). Test classes inspect each frame's -/// MethodDesc and assert the expected ArgLayout for the target ABI. -/// -/// Categories (Program order = simple-to-complex, top-of-stack first): -/// A. Register-bank fill / spill (no GC refs) -/// B. Reference-typed args (Class/Byref dispatch) -/// C. Small by-value structs (size matrix, incl. structs holding refs) -/// D. Large stack-passed structs (>16 byte) -/// E. ByRefLike (Span<T>, ref structs) -/// F. Special arg slots (this, retbuf, generic context, varargs) -/// G. SIMD vectors (Vector64/128/256/512) -/// H. Composite kitchen-sink frames -/// internal static class Program { private static void Main() @@ -534,10 +517,6 @@ internal struct Big64Struct public long E; public long F; public long G; public long H; } -/// -/// Pointer-sized ref struct containing a single managed byref. Sized to be passed -/// by-value on Win-x64 (1-slot), exercising the ByRefLike walker dispatch. -/// internal ref struct SmallRefStruct { public ref int Field; @@ -550,22 +529,12 @@ public SmallRefStruct(ref int r) public void Bump() => Field++; } -/// -/// Ref struct with mixed contents: an object field and a primitive. Exercises -/// the ByRefLike walker emitting an object root (None) without any byref. -/// internal ref struct RefStructWithObject { public object Obj; public int Prim; } -/// -/// Ref struct with three GC-trackable members: a byref, an object, and a -/// string. Used by the kitchen-sink combo to exercise multi-field ByRefLike -/// walker emission ordering and recursion (none here, but recursion-ready -/// shape). -/// internal ref struct MultiRefStruct { public ref int Local; diff --git a/src/native/managed/cdac/tests/DumpTests/DumpTestHelpers.cs b/src/native/managed/cdac/tests/DumpTests/DumpTestHelpers.cs index b851ce482d00d8..9f4a89b8931727 100644 --- a/src/native/managed/cdac/tests/DumpTests/DumpTestHelpers.cs +++ b/src/native/managed/cdac/tests/DumpTests/DumpTestHelpers.cs @@ -13,10 +13,6 @@ namespace Microsoft.Diagnostics.DataContractReader.DumpTests; /// internal static class DumpTestHelpers { - /// - /// Resolves a to its ECMA-335 type name (no namespace). - /// Returns null if the name cannot be resolved (e.g., missing metadata). - /// public static string? GetTypeName(ContractDescriptorTarget target, TypeHandle typeHandle) { IRuntimeTypeSystem rts = target.Contracts.RuntimeTypeSystem; diff --git a/src/native/managed/cdac/tests/DumpTests/SkipOnArchAttribute.cs b/src/native/managed/cdac/tests/DumpTests/SkipOnArchAttribute.cs index 938c9230821829..52d098152b969c 100644 --- a/src/native/managed/cdac/tests/DumpTests/SkipOnArchAttribute.cs +++ b/src/native/managed/cdac/tests/DumpTests/SkipOnArchAttribute.cs @@ -6,48 +6,24 @@ namespace Microsoft.Diagnostics.DataContractReader.DumpTests; /// -/// When applied to a test method, controls whether the test runs based on -/// the architecture where the dumps were generated. Checked by /// before each test runs. /// Multiple attributes can be stacked on a single method. -/// -/// There are two modes: -/// -/// Exclude (default): [SkipOnArch("x86", "reason")] — skip when -/// the dump arch matches. -/// Include-only: [SkipOnArch(IncludeOnly = "x64", Reason = "reason")] -/// — skip when the dump arch does not match. -/// /// [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] public sealed class SkipOnArchAttribute : Attribute { - /// - /// The arch to exclude. When set, the test is skipped if the dump arch matches. - /// public string? Arch { get; } - /// - /// When set, the test is skipped if the dump arch does not match this value. - /// Mutually exclusive with . - /// public string? IncludeOnly { get; set; } public string Reason { get; set; } = string.Empty; - /// - /// Creates an exclude-mode attribute: skip when the dump arch equals . - /// public SkipOnArchAttribute(string arch, string reason) { Arch = arch; Reason = reason; } - /// - /// Creates an attribute using named properties only (for include-only mode). - /// Usage: [SkipOnArch(IncludeOnly = "x64", Reason = "...")] - /// public SkipOnArchAttribute() { } diff --git a/src/native/managed/cdac/tests/GcScannerReportArgumentTests.cs b/src/native/managed/cdac/tests/GcScannerReportArgumentTests.cs index d55d7797fb2b2c..44695219355a16 100644 --- a/src/native/managed/cdac/tests/GcScannerReportArgumentTests.cs +++ b/src/native/managed/cdac/tests/GcScannerReportArgumentTests.cs @@ -11,11 +11,6 @@ namespace Microsoft.Diagnostics.DataContractReader.Tests; -/// -/// Unit tests for , the per-argument GC reporting -/// dispatch used by GcScanner.PromoteCallerStack. Mirrors native -/// MetaSig::GcScanRoots behavior. -/// public class GcScannerReportArgumentTests { private const ulong TransitionBlockBase = 0x10_0000; @@ -250,15 +245,6 @@ public void ValueTypeHandleBranch_TakesPrecedenceOverPerSlotLoop(MockTarget.Arch // ===== ByRefLike (Span, ref structs): field-walk branch ===== - /// - /// Synthesizes an mock whose - /// returns true for the supplied - /// , and whose - /// yields synthetic - /// FieldDesc pointers (1, 2, 3, ...) for that type. Each FieldDesc's offset, element - /// type, and (for ValueType fields) inner-type-handle are wired from - /// . - /// private static IRuntimeTypeSystem RtsForByRefLike( TypeHandle byRefLikeType, params (uint Offset, CorElementType Type, TypeHandle? Inner, bool InnerIsByRefLike, (uint Offset, uint Size)[]? InnerSeries)[] fields) diff --git a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.CallingConvention.cs b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.CallingConvention.cs index 2a9be65cc43618..8914cf478b627c 100644 --- a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.CallingConvention.cs +++ b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.CallingConvention.cs @@ -8,15 +8,6 @@ namespace Microsoft.Diagnostics.DataContractReader.Tests; -/// -/// Per-architecture TransitionBlock descriptor TypeInfo for the -/// calling-convention test harness. cDAC's data-descriptor convention -/// encodes ABI constants in the "field offsets" (ArgumentRegistersOffset, -/// FirstGCRefMapSlot, OffsetOfArgs, optional -/// OffsetOfFloatArgumentRegisters), with Size carrying the -/// transition-block size. The values come directly from the -/// so tests can reference the same constants. -/// internal partial class MockDescriptors { public static class CallingConvention @@ -42,29 +33,14 @@ public static Layout CreateTransitionBlockLayout(CallConvTestCase testCase) // ----- FieldDesc layout / Value-type MT allocator ----- - /// - /// Production FieldDesc layout: two DWORDs of flag bits packed with - /// the metadata token (DWord1) and the field offset + CorElementType (DWord2), - /// followed by a pointer to the enclosing MethodTable. - /// public static Layout CreateFieldDescLayout(MockTarget.Architecture arch) => MockFieldDesc.CreateLayout(arch); public static Target.TypeInfo CreateFieldDescTypeInfo(MockTarget.Architecture arch) => TargetTestHelpers.CreateTypeInfo(CreateFieldDescLayout(arch)); - /// - /// Describes a single instance field for . - /// Statics are excluded by definition — only instance fields are reported. - /// public readonly record struct ValueTypeField(int Offset, CorElementType ElementType); - /// - /// Allocates a value-type MethodTable + EEClass + FieldDesc array in mock - /// memory. Returns the MT address; tests embed that pointer into a - /// stored-sig blob via ELEMENT_TYPE_INTERNAL to reference the - /// value type without going through the metadata reader. - /// public static MockMethodTable AddValueTypeMethodTable( MockDescriptors.RuntimeTypeSystem rts, string name, @@ -214,10 +190,6 @@ private static ulong AddSingleTypeInstantiation(MockDescriptors.RuntimeTypeSyste } } -/// -/// Mock view of Data.FieldDesc for the calling-convention test harness. -/// Layout: two uint flag/offset words + a pointer to the enclosing MT. -/// internal sealed class MockFieldDesc : TypedView { private const string DWord1FieldName = nameof(Data.FieldDesc.DWord1); From f48471682e297b3167594f89e01f8845d57c03cb Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Fri, 22 May 2026 11:04:16 -0400 Subject: [PATCH 7/9] Restore CallingConvention contract registry doc-comment Match the surrounding one-line `/// Gets an instance of the X contract for the target.` convention on `ContractRegistry.CallingConvention`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ContractRegistry.cs | 3 +++ 1 file changed, 3 insertions(+) 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 f26679d77bdd2b..0fd09c7d8f4120 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ContractRegistry.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ContractRegistry.cs @@ -124,6 +124,9 @@ public abstract class ContractRegistry /// Gets an instance of the Debugger contract for the target. /// public virtual IDebugger Debugger => GetContract(); + /// + /// Gets an instance of the CallingConvention contract for the target. + /// public virtual ICallingConvention CallingConvention => GetContract(); /// From 69cdae386c87dac09839b0fc1aeff32c0f66c18c Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Fri, 22 May 2026 11:04:27 -0400 Subject: [PATCH 8/9] Remove cDAC calling-convention TEST_INVENTORY.md WIP planning artifact; not needed in the repo. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tests/CallingConvention/TEST_INVENTORY.md | 284 ------------------ 1 file changed, 284 deletions(-) delete mode 100644 src/native/managed/cdac/tests/CallingConvention/TEST_INVENTORY.md diff --git a/src/native/managed/cdac/tests/CallingConvention/TEST_INVENTORY.md b/src/native/managed/cdac/tests/CallingConvention/TEST_INVENTORY.md deleted file mode 100644 index 699c1bd301fb73..00000000000000 --- a/src/native/managed/cdac/tests/CallingConvention/TEST_INVENTORY.md +++ /dev/null @@ -1,284 +0,0 @@ -# Calling Convention Test Inventory - -This document tracks test coverage for each platform's managed calling -convention in the cDAC argument iterator. Each category represents a -behavioral aspect of the ABI that should have at least one test. - -Legend: -- **Covered** -- test(s) exist and pass -- **Gap** -- no test exists yet -- **Skipped** -- test exists but is marked `[Skip]` due to a known implementation gap -- N/A -- category doesn't apply to this platform - -## Test categories - -| # | Category | Description | -|---|---|---| -| 1 | **Integer arg register filling** | N integer args fill the GP arg registers in order | -| 2 | **Integer arg stack spill** | Args beyond the register count land on the stack at sequential offsets | -| 3 | **Float/double register filling** | FP args fill FP registers (XMM on x64, V on ARM64, S/D on ARM32) | -| 4 | **Float/double stack spill** | FP args beyond the register count land on the stack | -| 5 | **Mixed int/float bank independence** | Int and float args consume from separate banks (or shared on Win x64) | -| 6 | **`this` pointer placement** | Instance method `this` lands in the first GP register | -| 7 | **Return buffer (retBuf)** | Large-return methods get a hidden retBuf arg that shifts user args | -| 8 | **Hidden arg shifts (generic context, async cont)** | Each hidden arg consumes a register slot, shifting user args | -| 9 | **Vararg cookie placement** | Vararg methods have a cookie after this/retBuf, before user args | -| 10 | **Implicit by-reference** | Structs above a size threshold are passed via hidden pointer | -| 11 | **Non-byref struct enregistration** | Structs at or below the threshold enregister as a value | -| 12 | **SysV eightbyte classification** | Struct fields classified per eightbyte merge rules (AMD64 Unix only) | -| 13 | **SysV struct split (GP + FP)** | Struct with mixed int/float fields splits across GP and FP banks | -| 14 | **HFA detection** | Homogeneous float aggregates placed in consecutive FP registers (ARM only) | -| 15 | **HFA not honored on non-ARM** | HFA-shaped structs do NOT get FP treatment on x64 | -| 16 | **TypedReference** | `System.TypedReference` placed correctly via `g_TypedReferenceMT` substitution | -| 17 | **Vector types (real intrinsic detection)** | Vector64/128 via synthetic metadata and `GetVectorSize` | -| 18 | **Large struct on stack by value** | Structs > 16 B passed by value on stack (SysV), not by pointer | -| 19 | **Many args (10+)** | Stack offsets progress correctly at scale | -| 20 | **Return value placement** | Return types in correct register or via retBuf | -| 21 | **Empty struct** | Zero-field struct behavior | -| 22 | **64-bit alignment (ARM32)** | I8/R8 args skip odd-numbered registers on ARM32 | -| 23 | **Apple ARM64 stack packing** | Natural-alignment packing on Darwin ARM64 stack | -| 24 | **Vararg FP -> GP demotion** | Variadic FP args go through GP path, not FP registers | -| 25 | **GC reference args** | Object/String args placed in GP regs (not byref, not FP) | - -## Coverage matrix - -### AMD64 Windows - -| # | Category | Status | Test(s) | -|---|---|---|---| -| 1 | Int register filling | **Covered** | `IntArgs_FillRegsAndSpillToStack` (10 cases: I1-U8) | -| 2 | Int stack spill | **Covered** | `IntArgs_FillRegsAndSpillToStack` (I4x5, I8x5) | -| 3 | Float register filling | **Covered** | `FloatArgs_FillFPRegsAndSpillToStack` (R4x1, R8x1, R4x4, R8x4) | -| 4 | Float stack spill | **Covered** | `FloatArgs_FillFPRegsAndSpillToStack` (R4x6, R8x6) | -| 5 | Mixed int/float banks | **Covered** | `OneFloatAmongInts_LandsInXMM` (4 positions) | -| 6 | `this` placement | **Covered** | `InstanceMethod_ThisOffsetIsFirst` | -| 7 | Return buffer | **Covered** | `StaticMethod_RetBuf_*`, `InstanceMethod_RetBuf_*`, `HiddenArgs_ShiftFirstUserDouble` | -| 8 | Hidden arg shifts | **Covered** | `HiddenArgs_ShiftFirstUserDouble` (10 cases) | -| 9 | Vararg cookie | **Covered** | `VarArgs_CookieAndFirstUserArg_OnWindows` (4 cases), `NonVarArgs_HasNullVarArgCookieOffset` | -| 10 | Implicit byref | **Covered** | `NineByteStruct_*`, `ThreeByteStruct_*`, `TypedReference_ImplicitByref_*` | -| 11 | Non-byref enregistration | **Covered** | `EightByteStruct_Enregisters_NotByref` | -| 12 | SysV eightbyte | N/A | | -| 13 | SysV struct split | N/A | | -| 14 | HFA detection | N/A | | -| 15 | HFA not honored | **Covered** | `HFAShapedStruct_OnWindows_DoesNotEnregisterInFP` (2 cases) | -| 16 | TypedReference | **Covered** | `TypedReference_ImplicitByref_OneSlot` | -| 17 | Vector types | **Covered** | `VectorType_OnWindows_ClassifiedBySizeNotVectorness` (2 cases) | -| 18 | Large struct by value | N/A | (Windows uses byref, not by-value) | -| 19 | Many args (10+) | **Covered** | `TenArgs_StackOffsetsProgress` | -| 20 | Return value placement | **Gap** | No explicit return-register tests | -| 21 | Empty struct | **Gap** | No test (needs behavioral clarification) | -| 22 | 64-bit alignment | N/A | | -| 23 | Apple stack packing | N/A | | -| 24 | Vararg FP demotion | **Gap** | Not modeled in iterator; covered implicitly by position-based XMM | -| 25 | GC reference args | **Gap** | No explicit Object/String arg test | - -### AMD64 Unix (SysV) - -| # | Category | Status | Test(s) | -|---|---|---|---| -| 1 | Int register filling | **Covered** | `SixInts_FillGPRegs`, `IntArgs_FillSixGPRegsAndSpillToStack` (10 cases) | -| 2 | Int stack spill | **Covered** | `SeventhInt_GoesToStack`, `IntArgs_*` (I4x7, I8x7) | -| 3 | Float register filling | **Covered** | `FourDoubles_FillFPRegs`, `FloatArgs_FillEightFPRegs` (6 cases) | -| 4 | Float stack spill | **Covered** | `NineDoubles_NinthGoesToFirstStackSlot` | -| 5 | Mixed int/float banks | **Covered** | `MixedIntDouble_UseSeparateBanks`, `OneFloatAmongInts_*` (4 cases) | -| 6 | `this` placement | **Covered** | `InstanceMethod_ThisOffsetIsFirstGPReg` | -| 7 | Return buffer | **Covered** | `Return_LargeStruct_HasRetBuf_*` | -| 8 | Hidden arg shifts | **Gap** | No generic-context or async-continuation test | -| 9 | Vararg cookie | **Gap** | No vararg test (SysV varargs are Linux/macOS only and "not supported" per clr-abi.md:84) | -| 10 | Implicit byref | N/A | SysV doesn't use implicit byref | -| 11 | Non-byref enregistration | N/A | (all structs <= 16 B are classified, not byref'd) | -| 12 | SysV eightbyte classification | **Covered** | `Struct_TwoInts_*`, `Struct_TwoFloats_*`, `Struct_TwoDoubles_*` | -| 13 | SysV struct split (GP+FP) | **Covered** | `Struct_IntDouble_SplitAcrossGPAndFP`, `Struct_ObjectAndDouble_*` | -| 14 | HFA detection | N/A | | -| 15 | HFA not honored | N/A | (SysV has no HFA concept) | -| 16 | TypedReference | **Covered** | `TypedReference_PassedInTwoGPRegs`, `TypedReference_GlobalNotSet_FallsBackToStack` | -| 17 | Vector types | **Gap** | No Vector64/128 test (SysV classifier bypass not exercised) | -| 18 | Large struct by value | **Covered** | `Struct_LargerThan16Bytes_StackByValue_NotByRef` | -| 19 | Many args (10+) | **Covered** | `ManyIntArgs_StackOffsetsProgress` | -| 20 | Return value placement | **Covered** | `Return_EightByteStruct_*`, `Return_SixteenByteStruct_*`, `Return_ThreeByteStruct_*`, etc. (6 tests) | -| 21 | Empty struct | **Gap** | No test | -| 22 | 64-bit alignment | N/A | | -| 23 | Apple stack packing | N/A | (Apple is ARM64, not x64) | -| 24 | Vararg FP demotion | N/A | (managed varargs not supported on Unix) | -| 25 | GC reference args | **Gap** | No explicit Object/String arg test | - -### ARM32 - -| # | Category | Status | Test(s) | -|---|---|---|---| -| 1 | Int register filling | **Covered** | `FourInts_FillR0_R3` | -| 2 | Int stack spill | **Covered** | `FifthInt_GoesToStack` | -| 3 | Float register filling | **Gap** | No FP register test | -| 4 | Float stack spill | **Gap** | No FP spill test | -| 5 | Mixed int/float banks | **Gap** | No mixed test | -| 6 | `this` placement | **Gap** | No explicit test | -| 7 | Return buffer | **Gap** | No retBuf test | -| 8 | Hidden arg shifts | **Gap** | No test | -| 9 | Vararg cookie | **Gap** | No test | -| 10 | Implicit byref | N/A | (EnregisteredParamTypeMaxSize = 0) | -| 11 | Non-byref enregistration | N/A | | -| 12 | SysV eightbyte | N/A | | -| 13 | SysV struct split | N/A | | -| 14 | HFA detection | **Gap** | No HFA test | -| 15 | HFA not honored | N/A | | -| 16 | TypedReference | **Gap** | No test | -| 17 | Vector types | **Gap** | No test | -| 18 | Large struct by value | **Gap** | No test | -| 19 | Many args (10+) | **Gap** | No test | -| 20 | Return value placement | **Gap** | No test | -| 21 | Empty struct | **Gap** | No test | -| 22 | 64-bit alignment | **Gap** | No I8/R8 alignment-skip test | -| 23 | Apple stack packing | N/A | | -| 24 | Vararg FP demotion | **Gap** | No test | -| 25 | GC reference args | **Gap** | No test | - -### ARM64 - -| # | Category | Status | Test(s) | -|---|---|---|---| -| 1 | Int register filling | **Covered** | `EightInts_FillX0_X7` | -| 2 | Int stack spill | **Gap** | No explicit test | -| 3 | Float register filling | **Covered** | `EightDoubles_FillV0_V7` | -| 4 | Float stack spill | **Gap** | No FP spill test | -| 5 | Mixed int/float banks | **Gap** | No mixed test | -| 6 | `this` placement | **Covered** | `InstanceMethod_ThisOffsetIsX0` | -| 7 | Return buffer | **Gap** | No retBuf test (X8 behavior) | -| 8 | Hidden arg shifts | **Gap** | No test | -| 9 | Vararg cookie | **Gap** | No test | -| 10 | Implicit byref | **Gap** | No > 16 B struct test | -| 11 | Non-byref enregistration | **Gap** | No <= 16 B non-HFA struct test | -| 12 | SysV eightbyte | N/A | | -| 13 | SysV struct split | N/A | | -| 14 | HFA detection | **Covered** | `HfaFloat2/3/4_*`, `HfaDouble2_*` | -| 15 | HFA not honored | N/A | | -| 16 | TypedReference | **Gap** | No test | -| 17 | Vector types | **Gap** | No Vector64/128 in V-register test | -| 18 | Large struct by value | N/A | (ARM64 uses implicit byref) | -| 19 | Many args (10+) | **Gap** | No test | -| 20 | Return value placement | **Gap** | No test | -| 21 | Empty struct | **Gap** | No test | -| 22 | 64-bit alignment | N/A | | -| 23 | Apple stack packing | **Gap** | No Apple-specific test | -| 24 | Vararg FP demotion | **Skipped** | `Windows_VarArgs_StructSpansX7AndStack_AuditGap4` | -| 25 | GC reference args | **Gap** | No test | - -### RISC-V 64 / LoongArch 64 - -| # | Category | Status | Test(s) | -|---|---|---|---| -| 1 | Int register filling | **Covered** | `RiscV64_EightInts_FillA0_A7` | -| 2 | Int stack spill | **Gap** | No explicit test | -| 3 | Float register filling | **Covered** | `LoongArch64_OneFloat_GoesToFA0` | -| 4 | Float stack spill | **Gap** | No FP spill test | -| 5 | Mixed int/float banks | **Gap** | No mixed test | -| 6 | `this` placement | **Gap** | No test | -| 7 | Return buffer | **Gap** | No test | -| 8 | Hidden arg shifts | **Gap** | No test | -| 9 | Vararg cookie | **Gap** | No test | -| 10 | Implicit byref | **Gap** | No > 16 B struct test | -| 11 | Non-byref enregistration | **Gap** | No test | -| 12 | SysV eightbyte | N/A | | -| 13 | SysV struct split | N/A | | -| 14 | HFA detection | N/A | | -| 15 | HFA not honored | N/A | | -| 16 | TypedReference | **Gap** | No test | -| 17 | Vector types | **Gap** | No test | -| 18 | Large struct by value | N/A | | -| 19 | Many args (10+) | **Gap** | No test | -| 20 | Return value placement | **Gap** | No test | -| 21 | Empty struct | **Gap** | No test | -| 22 | 64-bit alignment | N/A | | -| 23 | Apple stack packing | N/A | | -| 24 | Vararg FP demotion | **Gap** | No test | -| 25 | GC reference args | **Gap** | No test | - -### x86 - -| # | Category | Status | Test(s) | -|---|---|---|---| -| 1 | Int register filling | **Covered** | `OneInt_*`, `TwoInts_*` | -| 2 | Int stack spill | **Skipped** | `ThirdInt_LandsAtOffsetOfArgs_AuditGap7` | -| 3 | Float register filling | N/A | (x86 has no FP arg registers) | -| 4 | Float stack spill | N/A | | -| 5 | Mixed int/float banks | N/A | | -| 6 | `this` placement | **Covered** | `InstanceMethod_ThisOffsetIsECX` | -| 7 | Return buffer | **Gap** | No retBuf test | -| 8 | Hidden arg shifts | **Gap** | No test | -| 9 | Vararg cookie | **Gap** | No test | -| 10 | Implicit byref | N/A | (EnregisteredParamTypeMaxSize = 0) | -| 11 | Non-byref enregistration | **Skipped** | `SmallValueType_Enregisters_AuditGap6` | -| 12 | SysV eightbyte | N/A | | -| 13 | SysV struct split | N/A | | -| 14 | HFA detection | N/A | | -| 15 | HFA not honored | N/A | | -| 16 | TypedReference | **Gap** | No test | -| 17 | Vector types | N/A | | -| 18 | Large struct by value | **Gap** | No test | -| 19 | Many args (10+) | **Gap** | No test | -| 20 | Return value placement | **Gap** | No test | -| 21 | Empty struct | **Gap** | No test | -| 22 | 64-bit alignment | N/A | | -| 23 | Apple stack packing | N/A | | -| 24 | Vararg FP demotion | N/A | | -| 25 | GC reference args | **Gap** | No test | - -### GetVectorSize (cross-platform, in `RuntimeTypeSystemGetVectorSizeTests.cs`) - -| Test | Status | -|---|---| -| Known intrinsic returns size (Vector64, Vector128) | **Covered** | -| Non-intrinsic type returns 0 | **Covered** | -| No metadata returns 0 | **Covered** | -| Unhandled intrinsic name returns 0 | **Covered** | -| System.Numerics.Vector returns field bytes | **Covered** | - -## AMD64 Unix: gap analysis and proposed tests - -The following categories are **gaps** for AMD64 Unix that should be filled: - -### Gap 8: Hidden arg shifts (generic context, async continuation) - -AMD64 Unix uses the same `ComputeInitialNumRegistersUsed` as Windows, counting -`this`, retBuf, paramType, asyncCont in RDI, RSI, RDX, ... before user args. -The Phase 2 `HiddenArgs_ShiftFirstUserDouble` theory was added for Windows only. - -**Proposed:** Port the same theory to `AMD64UnixCallingConventionTests.cs` with -Unix-specific offsets (RDI = slot 0, RSI = slot 1, etc.) and 6 GP regs instead -of 4. The same `hasParamType` / `hasAsyncContinuation` helper flags work. - -### Gap 17: Vector types (SysV classifier bypass) - -When `GetVectorSize` returns non-zero, `SystemVStructClassifier.ShouldClassify` -returns false, and the struct is not eightbyte-classified. The AMD64 Unix -iterator's behavior when classification is skipped should be verified: -- Vector64 (8 B) -> should go in a GP register (or a single XMM?) -- Vector128 (16 B) -> should go in a single XMM (not split into 2 eightbytes) - -**Proposed:** Add `VectorType_OnUnix_BypassesEightbyteClassification` theory -using the synthetic metadata infrastructure from Phase 4. May surface a real -gap if the iterator currently mis-places unclassifiable structs. - -### Gap 21: Empty struct - -SysV AMD64 should pass empty structs by value on the stack per `clr-abi.md:569`. -Needs behavioral validation first (does the mock produce size 0? does -`IsArgPassedByRefBySize(0)` return false on SysV? etc.). - -**Proposed:** Add `EmptyStruct_PassedByValue_OnStack` fact test. - -### Gap 25: GC reference args - -Object and String args should go in GP registers (RDI, RSI, ...) and not be -treated as implicit byref. Important for GC scanning correctness. - -**Proposed:** Add `ObjectAndStringArgs_GoToGPRegs_NotByref` fact test. - -### Gap 9: Vararg cookie - -Per `clr-abi.md:84`: "Managed varargs are supported on Windows only." Unix -managed varargs are explicitly not supported. So this is correctly N/A, but -worth a negative test confirming the contract doesn't crash on a vararg sig -for a Unix target. - -**Proposed:** Add `VarArgs_NotSupportedOnUnix_ReturnsEmptyOrThrows` fact test -(assert behavior matches the contract's current handling). From e63fee627e4f938f8bf30113a898ed9aea3c6a6b Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Fri, 22 May 2026 11:09:23 -0400 Subject: [PATCH 9/9] Restore pre-existing doc comments mangled by the strip The previous strip removed lines on doc-comment blocks where my edits had expanded a pre-existing summary, leaving orphaned text. Restore: - SkipOnArchAttribute class summary back to origin/main verbatim (the strip removed the first two lines of the original summary). - GcScanner.ReportArgument: drop the empty / shell on this newly-added helper. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Contracts/StackWalk/GC/GcScanner.cs | 2 -- src/native/managed/cdac/tests/DumpTests/SkipOnArchAttribute.cs | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) 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 bcd43aeb399592..c3ebff7dfe4f82 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs @@ -350,8 +350,6 @@ private void PromoteCallerStack( } } - /// - /// internal static void ReportArgument( ArgLayout arg, TargetPointer transitionBlock, diff --git a/src/native/managed/cdac/tests/DumpTests/SkipOnArchAttribute.cs b/src/native/managed/cdac/tests/DumpTests/SkipOnArchAttribute.cs index 52d098152b969c..1d393a4b6183c8 100644 --- a/src/native/managed/cdac/tests/DumpTests/SkipOnArchAttribute.cs +++ b/src/native/managed/cdac/tests/DumpTests/SkipOnArchAttribute.cs @@ -6,6 +6,8 @@ namespace Microsoft.Diagnostics.DataContractReader.DumpTests; /// +/// When applied to a test method, causes the test to be skipped when +/// the dump architecture matches the specified value. Checked by /// before each test runs. /// Multiple attributes can be stacked on a single method. ///