Skip to content

[cdac] SystemV-AMD64 struct-in-register classifier: enable ARGITER + zero-known-issues on Linux x64#130257

Merged
max-charlamb merged 7 commits into
dotnet:mainfrom
max-charlamb:cdac-sysv-classifier
Jul 8, 2026
Merged

[cdac] SystemV-AMD64 struct-in-register classifier: enable ARGITER + zero-known-issues on Linux x64#130257
max-charlamb merged 7 commits into
dotnet:mainfrom
max-charlamb:cdac-sysv-classifier

Conversation

@max-charlamb

@max-charlamb max-charlamb commented Jul 6, 2026

Copy link
Copy Markdown
Member

Fills in the SystemV-AMD64 struct-in-register classification path in the cDAC's ArgIterator port so ICallingConvention.TryComputeArgGCRefMapBlob produces correct output on Unix x64. Lights up Linux x64 for both the ARGITER stress sub-check and the zero-known-issues GCREFS assertion.

Follow-up to #130090. Advances the platform matrix tracked in #130008.

Approach

The runtime already pre-computes the eightbyte classification for every enregisterable managed value type at MethodTable construction time and caches it in EEClass::m_eightByteRegistersInfo (inside EEClassOptionalFields). Runtime callers -- ArgIterator, getSystemVAmd64PassStructInRegisterDescriptor -- read that cached info via SystemVRegDescriptorFromSystemVEightByteRegistersInfo rather than re-running ClassifyEightBytes. The cDAC now does the same.

Non-enregisterable structs (too large, or classified as SSEUP/X87/Memory) never reach StoreEightByteClassification, so their descriptor reads back with NumEightBytes == 0. That's the natural passedInRegisters = false signal the cDAC surfaces. Native P/Invoke layouts are not cached here (the runtime classifies them on demand) -- the cDAC never walks native transition frames for GC scanning, so this is fine.

Because the stress harness compares against ComputeCallRefMap -- which uses the runtime path -- reading the same cached info gives byte-for-byte parity by construction. No merge-rule / intrinsic-rejection / Int128 divergence to reason about.

macOS x64 uses the same SystemV path and works the same way if run locally, but we don't add osx_x64 to the Helix stress matrix in this PR.

Descriptor plumbing

C++:

  • class.h: cdac_data<EEClass>::OptionalFields; cdac_data<EEClassOptionalFields> (UNIX_AMD64_ABI-guarded) exposing the EightByteRegistersInfo offset. Friend declaration on EEClassOptionalFields.
  • methodtable.h: cdac_data<SystemVEightByteRegistersInfo> for NumEightBytes plus the two byte[2] eightbyte tables. Friend struct.
  • datadescriptor.inc: EEClass.OptionalFields field + new EEClassOptionalFields and SystemVEightByteRegistersInfo types, both under #ifdef UNIX_AMD64_ABI.

Managed:

  • DataType: new EEClassOptionalFields and SystemVEightByteRegistersInfo entries.
  • Data.EEClass: adds OptionalFields.
  • Data.EEClassOptionalFields: [Field] SystemVEightByteRegistersInfo inline sub-IData.
  • Data.SystemVEightByteRegistersInfo: [Field] NumEightBytes, OnInit-populated arrays.
  • CdacTypeHandle.GetSystemVAmd64PassStructInRegisterDescriptor: ~40 LOC inline. Reads EEClass -> OptionalFields -> EightByteRegistersInfo, then projects into SYSTEMV_AMD64_CORINFO_STRUCT_REG_PASSING_DESCRIPTOR with eightByteOffsets synthesized as i * 8 (matches SystemVRegDescriptorFromSystemVEightByteRegistersInfo).

Test coverage

CallSignatures debuggee gains a SysVCategory exercising:

  • Single-eightbyte shapes: IntPair (Integer), FloatPair (SSE), IntFloat (merged Integer), SingleRef (IntegerReference)
  • Two-eightbyte shapes: LongLong, LongDouble, DoubleDouble, RefInt
  • Nested value type, ByRefLike (Span<byte>)
  • Rejection / stack-passed: 24-byte 3-ref struct, Vector128<int>, empty struct
  • Int128Wrapper (2 Integer eightbytes -- register-passed; runtime does NOT reject by name)
  • Unions (LayoutKind.Explicit), sub-eightbyte trailer, single-field wrapper

Same debuggee runs everywhere; only exercises the new descriptor read on Unix x64 (Windows targets don't emit the descriptor entries, so the classifier returns passedInRegisters = false immediately via a TryGetTypeInfo probe).

AssertAllPassed now folds KnownIssues into the Failed check unconditionally -- no more platform gate on the assertion side; any surviving known-issue count is a regression.

Local validation

  • dotnet build src/native/managed/cdac/cdac.slnx -c Release: clean
  • Unit tests: 2646 pass / 0 fail / 16 skipped
  • Windows x64 local stress smoke (RunStressTests.ps1):
    • GCREFS CallSignatures: 6150 / 6150 pass, 0 known-issue
    • ARGITER CallSignatures (with the SysVCategory): 383 pass / 0 fail / 0 skip / 0 error

The descriptor entries are UNIX_AMD64_ABI-only, so the descriptor-read path only activates on Linux/macOS x64 targets; no regression on any pre-existing platform. CI validates linux-x64 end-to-end via the byte-for-byte ARGITER comparison against runtime ComputeCallRefMap.

Note

This change was authored with assistance from GitHub Copilot.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot couldn't run its full agentic review because no GitHub Actions runner was available. Make sure your repository has a runner available to run Copilot's review, or add a copilot-setup-steps.yml file specifying one with the runs-on attribute. See the docs for more details.

Enables SystemV-AMD64 struct-in-register classification for cDAC ArgIterator so GCRefMap generation matches runtime behavior on Unix x64, and flips related stress/GCREFS gates to an inverse “unsupported platforms” list.

Changes:

  • Add Unix x64 SystemV-AMD64 eightbyte struct classifier and wire it into CdacTypeHandle.
  • Expand CallSignatures stress debuggee with SysV-focused signature shapes.
  • Simplify platform gating logic by skipping only remaining unsupported architectures (RiscV64/LoongArch64/Wasm).

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/native/managed/cdac/tests/StressTests/Debuggees/CallSignatures/Program.cs Adds SysV category signatures to exercise Unix x64 struct-in-register classification.
src/native/managed/cdac/tests/StressTests/CdacStressTests.cs Switches ARGITER stress gate to an inverse unsupported-arch list.
src/native/managed/cdac/tests/StressTests/CdacStressTestBase.cs Switches zero-known-issues assertion gate to an inverse unsupported-arch list.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs Aligns deferred-frame gating with new inverse unsupported-arch list.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs Adds IsIntrinsicType contract impl, refactors intrinsic vector detection, bumps HFA recursion cap.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/SystemVStructClassifier.cs Introduces SystemV-AMD64 struct-in-register classifier implementation.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CdacTypeHandle.cs Plumbs the new classifier into the calling-convention surface.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs Adds IsIntrinsicType(TypeHandle) to the contract.
docs/design/datacontracts/RuntimeTypeSystem.md Documents the new IsIntrinsicType API and its implementation.

@max-charlamb
max-charlamb force-pushed the cdac-sysv-classifier branch from fa16cf5 to 7460a2c Compare July 6, 2026 22:16
Copilot AI review requested due to automatic review settings July 6, 2026 22:36
@max-charlamb
max-charlamb force-pushed the cdac-sysv-classifier branch from 7460a2c to 00dc8d2 Compare July 6, 2026 22:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.

@max-charlamb
max-charlamb force-pushed the cdac-sysv-classifier branch from 00dc8d2 to b9a2860 Compare July 6, 2026 22:49
Copilot AI review requested due to automatic review settings July 6, 2026 22:54
@max-charlamb
max-charlamb force-pushed the cdac-sysv-classifier branch from b9a2860 to aa33f63 Compare July 6, 2026 22:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Comment thread src/native/managed/cdac/tests/StressTests/CdacStressTestBase.cs Outdated
Comment thread src/native/managed/cdac/tests/StressTests/Debuggees/CallSignatures/Program.cs Outdated
Copilot AI review requested due to automatic review settings July 6, 2026 23:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Comment thread src/native/managed/cdac/tests/StressTests/Debuggees/CallSignatures/Program.cs Outdated
@max-charlamb
max-charlamb marked this pull request as ready for review July 7, 2026 02:36
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag
See info in area-owners.md if you want to be subscribed.

max-charlamb pushed a commit to max-charlamb/runtime that referenced this pull request Jul 7, 2026
Per David Wrighton's suggestion on PR dotnet#130257: instead of porting the
~430 LOC SystemV-AMD64 struct-in-register classifier from
crossgen2/methodtable.cpp, read the pre-classified data that the runtime
already caches on EEClass::m_eightByteRegistersInfo.

The runtime's MethodTableBuilder::StoreEightByteClassification (called
from SystemVAmd64CheckForPassStructInRegister at MethodTable build time)
populates m_eightByteRegistersInfo inside EEClassOptionalFields with the
enregisterable-struct classification: NumEightBytes, per-eightbyte
classification tags, per-eightbyte sizes. Callers (arg iterator, JIT-EE
interface) read that cached info via
SystemVRegDescriptorFromSystemVEightByteRegistersInfo instead of
re-running ClassifyEightBytes. The cDAC does the same.

Non-enregisterable structs (too large, or classified as SSEUP/X87/Memory)
never reach StoreEightByteClassification, so their m_eightByteRegistersInfo
reads back with NumEightBytes == 0. That's the natural
"passedInRegisters = false" signal the cDAC surfaces.

Descriptor plumbing:
* class.h: cdac_data<EEClass>::OptionalFields; cdac_data<EEClassOptionalFields>
  (UNIX_AMD64_ABI-guarded, includes the EightByteRegistersInfo offset).
* methodtable.h: cdac_data<SystemVEightByteRegistersInfo> for NumEightBytes
  plus the two byte[2] arrays; friend struct on the class so offsetof can
  reach the private fields.
* datadescriptor.inc: EEClass.OptionalFields field + new
  EEClassOptionalFields and SystemVEightByteRegistersInfo types, both
  under #ifdef UNIX_AMD64_ABI.

Managed side:
* DataType: new EEClassOptionalFields and SystemVEightByteRegistersInfo entries.
* Data.EEClass: OptionalFields TargetPointer field.
* Data.EEClassOptionalFields: [Field] SystemVEightByteRegistersInfo inline sub-IData.
* Data.SystemVEightByteRegistersInfo: [Field] NumEightBytes; OnInit-populated
  arrays for the two eightbyte tables.
* CdacTypeHandle.GetSystemVAmd64PassStructInRegisterDescriptor: reads
  EEClass -> OptionalFields -> EightByteRegistersInfo directly (~40 LOC inline;
  the standalone SystemVStructClassifier.cs file is deleted).
* MockDescriptors.RuntimeTypeSystem: mock EEClass layout gains OptionalFields
  so unit tests build the same shape as the runtime.

Advantages vs the classifier port:
* Byte-for-byte matches ComputeCallRefMap by construction (the stress
  harness compares against the runtime path).
* No more crossgen2-vs-runtime Int128/UInt128 divergence to reason about --
  the classifier just doesn't run.
* ~371 LOC net removed.
* No maintenance burden as the runtime classifier evolves.

Only activates when the target runtime is UNIX_AMD64_ABI; on other
runtimes the descriptor entries are ifdef'd out and the classifier
returns "not passed in registers" immediately via the TryGetTypeInfo
probe.

Windows unit tests: all 2646 cDAC tests still pass.
Linux x64 stress: TBD (pushed for CI validation).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 7, 2026 19:25
@max-charlamb
max-charlamb force-pushed the cdac-sysv-classifier branch from 252c8b1 to 3423ebc Compare July 7, 2026 19:25
@max-charlamb
max-charlamb force-pushed the cdac-sysv-classifier branch from 3423ebc to 2b4cd5f Compare July 7, 2026 19:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.

Comment thread src/native/managed/cdac/tests/StressTests/Debuggees/CallSignatures/Program.cs Outdated
Rather than reading the two eightbyte tables as byte[] arrays via
ReadBuffer (indexed only by NumEightBytes, which is read from target
memory without validation), split each table into two explicitly-named
[Field] properties: EightByteClassification0/1 and EightByteSize0/1.

Two benefits:

1. The Data class is fully declarative -- no OnInit, no arrays. The
   source generator emits an ordinary two-uint8 read per slot, so
   there's no code path that could index past slot 1 regardless of
   what NumEightBytes says.

2. Any consumer keyed off descriptor.eightByteCount is now safe against
   a corrupted / mis-read NumEightBytes: the previously Debug.Assert-only
   check in CdacTypeHandle is now a runtime guard that treats
   NumEightBytes > 2 as "not passed in registers", so downstream loops
   (e.g. ArgIterator, TransitionBlock) can't overrun a two-slot descriptor.

Also adds a static_assert in methodtable.h that
CLR_SYSTEMV_MAX_EIGHTBYTES_COUNT_TO_PASS_IN_REGISTERS is still 2, so if
the runtime ever grows the descriptor the cDAC-side split gets a build
break rather than silently truncating.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.

Copilot AI review requested due to automatic review settings July 8, 2026 01:40
* GcScanner.PromoteCallerStack: reinstate a cheap arch/OS support gate
  (now including Unix x64) so unsupported targets go straight to
  RecordDeferredFrame instead of hitting the NotImplementedException
  fallback inside CallingConvention_1.TryComputeArgGCRefMapBlob. Avoids
  exception-driven control flow (and first-chance exceptions under
  debuggers) on RiscV64 / LoongArch64 / Wasm targets. Tracks the same
  supported-platform list as the runtime stress gate; TODO points at
  dotnet#130008 for extending coverage.

* CdacTypeHandle: reuse
  SYSTEMV_AMD64_CORINFO_STRUCT_REG_PASSING_DESCRIPTOR.SYSTEMV_EIGHT_BYTE_SIZE_IN_BYTES
  instead of a locally-defined 8 constant.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 3 comments.

Comment thread src/coreclr/vm/methodtable.h
Copilot AI review requested due to automatic review settings July 8, 2026 01:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.

* DataType: move EEClassOptionalFields / SystemVEightByteRegistersInfo
  back next to EEClass so type-system entries stay grouped together.
* CallingConvention_1.ComputeArgGCRefMapBlobCore: hoist the shared
  TransitionBlock to the top of the method so the SysV struct-in-regs
  branch and the pos/offset conversion loop reuse the same instance.
* CallingConvention_1.GetArgumentLayout: unwrap the ArgLocDesc? at the
  call site with a throw-on-null instead of dereferencing Value later;
  makes the invariant explicit at the point it's required.
* GcScanner.PromoteCallerStack: drop the arch/OS supported-target gate
  again -- TryComputeArgGCRefMapBlob's own NotImplementedException path
  is the sole decline point.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 8, 2026 14:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.

The Debug.Assert this using existed for was replaced with a throw-on-null
in a prior commit; drop the now-unused import.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 8, 2026 14:52

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.

@max-charlamb
max-charlamb enabled auto-merge (squash) July 8, 2026 22:22
@max-charlamb
max-charlamb merged commit 2269602 into dotnet:main Jul 8, 2026
132 of 136 checks passed
@max-charlamb
max-charlamb deleted the cdac-sysv-classifier branch July 8, 2026 23:36
@dotnet-milestone-bot dotnet-milestone-bot Bot added this to the 11.0-preview7 milestone Jul 10, 2026
eiriktsarpalis pushed a commit that referenced this pull request Jul 15, 2026
…zero-known-issues on Linux x64 (#130257)

Fills in the SystemV-AMD64 struct-in-register classification path in the
cDAC's ArgIterator port so
`ICallingConvention.TryComputeArgGCRefMapBlob` produces correct output
on Unix x64. Lights up **Linux x64** for both the ARGITER stress
sub-check and the zero-known-issues GCREFS assertion.

Follow-up to #130090. Advances the platform matrix tracked in #130008.

## Approach

The runtime already pre-computes the eightbyte classification for every
enregisterable managed value type at `MethodTable` construction time and
caches it in `EEClass::m_eightByteRegistersInfo` (inside
`EEClassOptionalFields`). Runtime callers -- `ArgIterator`,
`getSystemVAmd64PassStructInRegisterDescriptor` -- read that cached info
via `SystemVRegDescriptorFromSystemVEightByteRegistersInfo` rather than
re-running `ClassifyEightBytes`. The cDAC now does the same.

Non-enregisterable structs (too large, or classified as
`SSEUP`/`X87`/`Memory`) never reach `StoreEightByteClassification`, so
their descriptor reads back with `NumEightBytes == 0`. That's the
natural `passedInRegisters = false` signal the cDAC surfaces. Native
P/Invoke layouts are not cached here (the runtime classifies them on
demand) -- the cDAC never walks native transition frames for GC
scanning, so this is fine.

Because the stress harness compares against `ComputeCallRefMap` -- which
uses the runtime path -- reading the same cached info gives
byte-for-byte parity by construction. No merge-rule /
intrinsic-rejection / `Int128` divergence to reason about.

macOS x64 uses the same SystemV path and works the same way if run
locally, but we don't add `osx_x64` to the Helix stress matrix in this
PR.

## Descriptor plumbing

**C++:**
- `class.h`: `cdac_data<EEClass>::OptionalFields`;
`cdac_data<EEClassOptionalFields>` (UNIX_AMD64_ABI-guarded) exposing the
`EightByteRegistersInfo` offset. Friend declaration on
`EEClassOptionalFields`.
- `methodtable.h`: `cdac_data<SystemVEightByteRegistersInfo>` for
`NumEightBytes` plus the two `byte[2]` eightbyte tables. Friend struct.
- `datadescriptor.inc`: `EEClass.OptionalFields` field + new
`EEClassOptionalFields` and `SystemVEightByteRegistersInfo` types, both
under `#ifdef UNIX_AMD64_ABI`.

**Managed:**
- `DataType`: new `EEClassOptionalFields` and
`SystemVEightByteRegistersInfo` entries.
- `Data.EEClass`: adds `OptionalFields`.
- `Data.EEClassOptionalFields`: `[Field] SystemVEightByteRegistersInfo`
inline sub-IData.
- `Data.SystemVEightByteRegistersInfo`: `[Field] NumEightBytes`,
`OnInit`-populated arrays.
- `CdacTypeHandle.GetSystemVAmd64PassStructInRegisterDescriptor`: ~40
LOC inline. Reads `EEClass -> OptionalFields -> EightByteRegistersInfo`,
then projects into `SYSTEMV_AMD64_CORINFO_STRUCT_REG_PASSING_DESCRIPTOR`
with `eightByteOffsets` synthesized as `i * 8` (matches
`SystemVRegDescriptorFromSystemVEightByteRegistersInfo`).

## Test coverage

`CallSignatures` debuggee gains a `SysVCategory` exercising:
- Single-eightbyte shapes: `IntPair` (Integer), `FloatPair` (SSE),
`IntFloat` (merged Integer), `SingleRef` (IntegerReference)
- Two-eightbyte shapes: `LongLong`, `LongDouble`, `DoubleDouble`,
`RefInt`
- Nested value type, ByRefLike (`Span<byte>`)
- Rejection / stack-passed: 24-byte 3-ref struct, `Vector128<int>`,
empty struct
- `Int128Wrapper` (2 Integer eightbytes -- register-passed; runtime does
NOT reject by name)
- Unions (`LayoutKind.Explicit`), sub-eightbyte trailer, single-field
wrapper

Same debuggee runs everywhere; only exercises the new descriptor read on
Unix x64 (Windows targets don't emit the descriptor entries, so the
classifier returns `passedInRegisters = false` immediately via a
`TryGetTypeInfo` probe).

`AssertAllPassed` now folds `KnownIssues` into the `Failed` check
unconditionally -- no more platform gate on the assertion side; any
surviving known-issue count is a regression.

## Local validation

- `dotnet build src/native/managed/cdac/cdac.slnx -c Release`: clean
- Unit tests: 2646 pass / 0 fail / 16 skipped
- **Windows x64 local stress smoke** (`RunStressTests.ps1`):
  - GCREFS `CallSignatures`: 6150 / 6150 pass, 0 known-issue
- ARGITER `CallSignatures` (with the `SysVCategory`): 383 pass / 0 fail
/ 0 skip / 0 error

The descriptor entries are `UNIX_AMD64_ABI`-only, so the descriptor-read
path only activates on Linux/macOS x64 targets; no regression on any
pre-existing platform. CI validates `linux-x64` end-to-end via the
byte-for-byte ARGITER comparison against runtime `ComputeCallRefMap`.

> [!NOTE]
> This change was authored with assistance from GitHub Copilot.

---------

Co-authored-by: Max Charlamb <maxcharlamb@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants