From f02998f2c01284fe22cf0b17a1ad6ff7c9f52648 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Fri, 24 Apr 2026 14:17:51 -0400 Subject: [PATCH] [cDAC] Port ArgIterator from crossgen2 for calling convention analysis Port crossgen2's ArgIterator and TransitionBlock into the cDAC for correct per-architecture argument placement in PromoteCallerStack. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../vm/datadescriptor/datadescriptor.inc | 12 + .../CallingConvention/ArgIterator.cs | 797 ++++++++++++++++++ .../CallingConvention/ArgIteratorData.cs | 82 ++ .../CallingConvention/ArgTypeInfo.cs | 142 ++++ .../CallingConventionInfo.cs | 274 ++++++ .../StackWalk/FrameHandling/FrameIterator.cs | 118 ++- .../Data/Frames/TransitionBlock.cs | 8 + 7 files changed, 1395 insertions(+), 38 deletions(-) create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/ArgIterator.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/ArgIteratorData.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/ArgTypeInfo.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/CallingConventionInfo.cs diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index 9b9d2d21f191eb..62714b3266a672 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -986,6 +986,18 @@ CDAC_TYPE_FIELD(TransitionBlock, T_UINT32, FirstGCRefMapSlot, offsetof(Transitio 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 +#else +CDAC_TYPE_FIELD(TransitionBlock, T_INT32, OffsetOfFloatArgumentRegisters, 0) +#endif CDAC_TYPE_END(TransitionBlock) #ifdef DEBUGGING_SUPPORTED diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/ArgIterator.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/ArgIterator.cs new file mode 100644 index 00000000000000..cbb42062124f0a --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/ArgIterator.cs @@ -0,0 +1,797 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Provides an abstraction over platform specific calling conventions. +// Ported from crossgen2's ArgIterator.cs. + +using System; +using System.Diagnostics; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers.CallingConvention; + +/// +/// Enumerates method arguments and maps each to a register or stack offset +/// within a TransitionBlock. +/// Ported from crossgen2's ArgIterator. +/// +internal struct ArgIterator +{ + private readonly CallingConventionInfo _ccInfo; + private readonly ArgIteratorData _argData; + private readonly bool _hasThis; + private readonly bool _hasParamType; + private readonly bool _hasAsyncContinuation; + private readonly bool _extraFunctionPointerArg; + private readonly bool[] _forcedByRefParams; + private readonly bool _skipFirstArg; + private readonly bool _extraObjectFirstArg; + + // Iteration state + private bool _ITERATION_STARTED; + private bool _SIZE_OF_ARG_STACK_COMPUTED; + private bool _RETURN_FLAGS_COMPUTED; + private bool _RETURN_HAS_RET_BUFFER; + + private CorElementType _argType; + private ArgTypeInfo _argTypeHandle; + private ArgTypeInfo _argTypeHandleOfByRefParam; + private int _argSize; + private int _argNum; + private bool _argForceByRef; + private int _nSizeOfArgStack; + + // Per-architecture register allocation state + // x86 + private int _x86NumRegistersUsed; + private int _x86OfsStack; + + // x64 Windows + private int _x64WindowsCurOfs; + + // x64 Unix + private int _x64UnixIdxGenReg; + private int _x64UnixIdxStack; + private int _x64UnixIdxFPReg; + + // ARM32 + private int _armIdxGenReg; + private int _armOfsStack; + private ushort _armWFPRegs; + private bool _armRequires64BitAlignment; + + // ARM64 + private int _arm64IdxGenReg; + private int _arm64OfsStack; + private int _arm64IdxFPReg; + + // LoongArch64 / RISC-V64 + private int _rvLa64IdxGenReg; + private int _rvLa64OfsStack; + private int _rvLa64IdxFPReg; + + // Struct-in-registers tracking + private bool _hasArgLocDescForStructInRegs; +#pragma warning disable CS0649 // Assigned in platform-specific paths (ARM64 HFA, Unix AMD64 struct-in-regs) + private ArgLocDesc _argLocDescForStructInRegs; +#pragma warning restore CS0649 + + // x86 param type location + private enum ParamTypeLocation + { + Stack, + Ecx, + Edx, + } + private ParamTypeLocation _paramTypeLoc; + + private enum AsyncContinuationLocation + { + Stack, + Ecx, + Edx, + } + private AsyncContinuationLocation _asyncContinuationLoc; + + public bool HasThis => _hasThis; + public bool IsVarArg => _argData.IsVarArg(); + public bool HasParamType => _hasParamType; + public bool HasAsyncContinuation => _hasAsyncContinuation; + public int NumFixedArgs => _argData.NumFixedArgs() + (_extraFunctionPointerArg ? 1 : 0) + (_extraObjectFirstArg ? 1 : 0); + + public ArgIterator( + CallingConventionInfo ccInfo, + ArgIteratorData argData, + bool hasParamType, + bool hasAsyncContinuation, + bool[] forcedByRefParams, + bool skipFirstArg = false, + bool extraObjectFirstArg = false, + bool extraFunctionPointerArg = false) + { + this = default; + _ccInfo = ccInfo; + _argData = argData; + _hasThis = argData.HasThis(); + _hasParamType = hasParamType; + _hasAsyncContinuation = hasAsyncContinuation; + _extraFunctionPointerArg = extraFunctionPointerArg; + _forcedByRefParams = forcedByRefParams; + _skipFirstArg = skipFirstArg; + _extraObjectFirstArg = extraObjectFirstArg; + } + + public CorElementType GetArgumentType(int argNum, out ArgTypeInfo thArgType, out bool forceByRefReturn) + { + forceByRefReturn = false; + + if (_extraObjectFirstArg && argNum == 0) + { + thArgType = ArgTypeInfo.ForPrimitive(CorElementType.Class, _ccInfo.PointerSize); + return CorElementType.Class; + } + + argNum = _extraObjectFirstArg ? argNum - 1 : argNum; + + if (_forcedByRefParams is not null && (argNum + 1) < _forcedByRefParams.Length) + forceByRefReturn = _forcedByRefParams[argNum + 1]; + + if (_extraFunctionPointerArg && argNum == _argData.NumFixedArgs()) + { + thArgType = ArgTypeInfo.ForPrimitive(CorElementType.I, _ccInfo.PointerSize); + return CorElementType.I; + } + + return _argData.GetArgumentType(argNum, out thArgType); + } + + public CorElementType GetReturnType(out ArgTypeInfo thRetType, out bool forceByRefReturn) + { + forceByRefReturn = _forcedByRefParams is not null && _forcedByRefParams.Length > 0 && _forcedByRefParams[0]; + return _argData.GetReturnType(out thRetType); + } + + public void Reset() + { + _argType = default; + _argTypeHandle = default; + _argSize = 0; + _argNum = 0; + _argForceByRef = false; + _ITERATION_STARTED = false; + } + + private uint SizeOfArgStack() + { + if (!_SIZE_OF_ARG_STACK_COMPUTED) + ForceSigWalk(); + Debug.Assert(_SIZE_OF_ARG_STACK_COMPUTED); + return (uint)_nSizeOfArgStack; + } + + public int SizeOfFrameArgumentArray() + { + uint size = SizeOfArgStack(); + + if (_ccInfo.Architecture == RuntimeInfoArchitecture.X64 && !_ccInfo.IsX64UnixABI) + { + size += (uint)_ccInfo.SizeOfArgumentRegisters; + } + + return (int)size; + } + + public uint CbStackPop() + { + if (_ccInfo.Architecture == RuntimeInfoArchitecture.X86) + { + return IsVarArg ? 0 : SizeOfArgStack(); + } + throw new NotImplementedException(); + } + + public bool HasRetBuffArg() + { + if (!_RETURN_FLAGS_COMPUTED) + ComputeReturnFlags(); + return _RETURN_HAS_RET_BUFFER; + } + + public int GetThisOffset() => _ccInfo.ThisOffset; + + public int GetVASigCookieOffset() + { + Debug.Assert(IsVarArg); + + if (_ccInfo.Architecture == RuntimeInfoArchitecture.X86) + { + return (int)_ccInfo.SizeOfTransitionBlock; + } + + int ret = (int)_ccInfo.ArgumentRegistersOffset; + if (HasThis) + ret += _ccInfo.PointerSize; + if (HasRetBuffArg() && _ccInfo.IsRetBuffPassedAsFirstArg) + ret += _ccInfo.PointerSize; + return ret; + } + + public int GetParamTypeArgOffset() + { + Debug.Assert(HasParamType); + + if (_ccInfo.Architecture == RuntimeInfoArchitecture.X86) + { + if (!_SIZE_OF_ARG_STACK_COMPUTED) + ForceSigWalk(); + + return _paramTypeLoc switch + { + ParamTypeLocation.Ecx => (int)_ccInfo.ArgumentRegistersOffset + _ccInfo.PointerSize, // ECX offset + ParamTypeLocation.Edx => (int)_ccInfo.ArgumentRegistersOffset, // EDX offset + _ => (int)_ccInfo.SizeOfTransitionBlock, + }; + } + + int ret = (int)_ccInfo.ArgumentRegistersOffset; + if (HasThis) ret += _ccInfo.PointerSize; + if (HasRetBuffArg() && _ccInfo.IsRetBuffPassedAsFirstArg) ret += _ccInfo.PointerSize; + return ret; + } + + public int GetAsyncContinuationArgOffset() + { + Debug.Assert(HasAsyncContinuation); + + if (_ccInfo.Architecture == RuntimeInfoArchitecture.X86) + { + if (!_SIZE_OF_ARG_STACK_COMPUTED) + ForceSigWalk(); + + return _asyncContinuationLoc switch + { + AsyncContinuationLocation.Ecx => (int)_ccInfo.ArgumentRegistersOffset + _ccInfo.PointerSize, + AsyncContinuationLocation.Edx => (int)_ccInfo.ArgumentRegistersOffset, + _ => HasParamType && _paramTypeLoc == ParamTypeLocation.Stack + ? (int)_ccInfo.SizeOfTransitionBlock + _ccInfo.PointerSize + : (int)_ccInfo.SizeOfTransitionBlock, + }; + } + + int ret = (int)_ccInfo.ArgumentRegistersOffset; + if (HasThis) ret += _ccInfo.PointerSize; + if (HasRetBuffArg() && _ccInfo.IsRetBuffPassedAsFirstArg) ret += _ccInfo.PointerSize; + if (HasParamType) ret += _ccInfo.PointerSize; + return ret; + } + + public bool IsArgPassedByRef() + { + if (_argForceByRef) + return true; + if (_argType == CorElementType.Byref) + return true; + + if (_ccInfo.EnregisteredParamTypeMaxSize != 0) + { + return _ccInfo.Architecture switch + { + RuntimeInfoArchitecture.X64 => _ccInfo.IsArgPassedByRef(_argSize), + RuntimeInfoArchitecture.Arm64 => _argType == CorElementType.ValueType + && (_argSize > _ccInfo.EnregisteredParamTypeMaxSize) && (!_argTypeHandle.IsHomogeneousAggregate || IsVarArg), + RuntimeInfoArchitecture.LoongArch64 or RuntimeInfoArchitecture.RiscV64 => _argType == CorElementType.ValueType + && _argSize > _ccInfo.EnregisteredParamTypeMaxSize, + _ => false, + }; + } + return false; + } + + public ArgLocDesc? GetArgLoc(int _) + { + return _hasArgLocDescForStructInRegs ? _argLocDescForStructInRegs : null; + } + + public int GetArgSize() => _argSize; + + /// + /// Returns the next argument's offset in the transition block, or + /// when all arguments + /// have been enumerated. + /// + public int GetNextOffset() + { + if (!_ITERATION_STARTED) + { + int numRegistersUsed = 0; + + if (HasThis) numRegistersUsed++; + if (HasRetBuffArg() && _ccInfo.IsRetBuffPassedAsFirstArg) numRegistersUsed++; + + Debug.Assert(!IsVarArg || !HasParamType); + + if (_ccInfo.Architecture != RuntimeInfoArchitecture.X86) + { + if (HasParamType) numRegistersUsed++; + if (HasAsyncContinuation) numRegistersUsed++; + } + + if (_ccInfo.Architecture != RuntimeInfoArchitecture.X86 && IsVarArg) + numRegistersUsed++; + + switch (_ccInfo.Architecture) + { + case RuntimeInfoArchitecture.X86: + if (IsVarArg) numRegistersUsed = _ccInfo.NumArgumentRegisters; + _x86NumRegistersUsed = numRegistersUsed; + _x86OfsStack = (int)(_ccInfo.OffsetOfArgs + SizeOfArgStack()); + break; + + case RuntimeInfoArchitecture.X64: + if (_ccInfo.IsX64UnixABI) + { + _x64UnixIdxGenReg = numRegistersUsed; + _x64UnixIdxStack = 0; + _x64UnixIdxFPReg = 0; + } + else + { + _x64WindowsCurOfs = (int)_ccInfo.OffsetOfArgs + numRegistersUsed * _ccInfo.PointerSize; + } + break; + + case RuntimeInfoArchitecture.Arm: + _armIdxGenReg = numRegistersUsed; + _armOfsStack = 0; + _armWFPRegs = 0; + break; + + case RuntimeInfoArchitecture.Arm64: + _arm64IdxGenReg = numRegistersUsed; + _arm64OfsStack = 0; + _arm64IdxFPReg = 0; + break; + + case RuntimeInfoArchitecture.LoongArch64: + case RuntimeInfoArchitecture.RiscV64: + _rvLa64IdxGenReg = numRegistersUsed; + _rvLa64OfsStack = 0; + _rvLa64IdxFPReg = 0; + break; + + default: + throw new NotSupportedException(_ccInfo.Architecture.ToString()); + } + + _argNum = _skipFirstArg ? 1 : 0; + _ITERATION_STARTED = true; + } + + if (_argNum >= NumFixedArgs) + return CallingConventionInfo.InvalidOffset; + + CorElementType argType = GetArgumentType(_argNum, out _argTypeHandle, out _argForceByRef); + _argTypeHandleOfByRefParam = argType == CorElementType.Byref ? _argData.GetByRefArgumentType(_argNum) : default; + _argNum++; + + int argSize = ArgTypeInfo.GetElemSize(argType, _argTypeHandle, _ccInfo.PointerSize); + _argType = argType; + _argSize = argSize; + + argType = _argForceByRef ? CorElementType.Byref : argType; + argSize = _argForceByRef ? _ccInfo.PointerSize : argSize; + + _hasArgLocDescForStructInRegs = false; + + switch (_ccInfo.Architecture) + { + case RuntimeInfoArchitecture.X64: + return GetNextOffsetX64(argType, argSize); + + case RuntimeInfoArchitecture.Arm64: + return GetNextOffsetArm64(argType, argSize); + + case RuntimeInfoArchitecture.X86: + return GetNextOffsetX86(argType, argSize); + + case RuntimeInfoArchitecture.Arm: + return GetNextOffsetArm32(argType, argSize); + + case RuntimeInfoArchitecture.LoongArch64: + case RuntimeInfoArchitecture.RiscV64: + return GetNextOffsetRiscVLoongArch(argType, argSize); + + default: + throw new NotSupportedException(_ccInfo.Architecture.ToString()); + } + } + + // ---- Per-architecture GetNextOffset implementations ---- + // These match crossgen2's ArgIterator.GetNextOffset() switch cases. + + private int GetNextOffsetX64(CorElementType argType, int argSize) + { + if (_ccInfo.IsX64UnixABI) + { + // TODO: Full Unix AMD64 implementation with SystemV struct classification + // For now, simplified: all args go through GP regs then stack + int cbArg = _ccInfo.StackElemSize(argSize); + int cGenRegs = cbArg / 8; + + if (argType is CorElementType.R4 or CorElementType.R8) + { + if (_x64UnixIdxFPReg < _ccInfo.NumFloatArgumentRegisters) + { + int argOfs = _ccInfo.OffsetOfFloatArgumentRegisters + _x64UnixIdxFPReg * _ccInfo.FloatRegisterSize; + _x64UnixIdxFPReg++; + return argOfs; + } + } + else if (cGenRegs > 0 && _x64UnixIdxGenReg + cGenRegs <= _ccInfo.NumArgumentRegisters) + { + int argOfs = (int)_ccInfo.ArgumentRegistersOffset + _x64UnixIdxGenReg * _ccInfo.PointerSize; + _x64UnixIdxGenReg += cGenRegs; + return argOfs; + } + + int stackOfs = (int)_ccInfo.OffsetOfArgs + _x64UnixIdxStack * _ccInfo.PointerSize; + _x64UnixIdxStack += _ccInfo.StackElemSize(argSize) / _ccInfo.PointerSize; + return stackOfs; + } + else + { + // Windows x64: each arg takes exactly one slot + int cFPRegs = argType is CorElementType.R4 or CorElementType.R8 ? 1 : 0; + + int argOfs = _x64WindowsCurOfs - (int)_ccInfo.OffsetOfArgs; + _x64WindowsCurOfs += _ccInfo.PointerSize; + + if (cFPRegs == 0 || argOfs >= _ccInfo.SizeOfArgumentRegisters) + { + return argOfs + (int)_ccInfo.OffsetOfArgs; + } + else + { + int idxFpReg = argOfs / _ccInfo.PointerSize; + return _ccInfo.OffsetOfFloatArgumentRegisters + idxFpReg * 16; // SizeOfM128A + } + } + } + + private int GetNextOffsetArm64(CorElementType argType, int argSize) + { + int cFPRegs = 0; + bool isFloatHFA = false; + + switch (argType) + { + case CorElementType.R4: + case CorElementType.R8: + cFPRegs = 1; + break; + + case CorElementType.ValueType: + if (_argTypeHandle.IsHomogeneousAggregate) + { + int haElementSize = _argTypeHandle.HomogeneousAggregateElementSize; + if (haElementSize == 4) isFloatHFA = true; + cFPRegs = argSize / haElementSize; + } + else if (argSize > _ccInfo.EnregisteredParamTypeMaxSize) + { + argSize = _ccInfo.PointerSize; + } + break; + } + + bool isValueType = argType == CorElementType.ValueType; + int cbArg = _ccInfo.StackElemSize(argSize, isValueType, isFloatHFA); + + if (cFPRegs > 0 && !IsVarArg) + { + if (cFPRegs + _arm64IdxFPReg <= 8) + { + int argOfs = _ccInfo.OffsetOfFloatArgumentRegisters + _arm64IdxFPReg * 16; + _arm64IdxFPReg += cFPRegs; + return argOfs; + } + else + { + _arm64IdxFPReg = 8; + } + } + else + { + int regSlots = CallingConventionInfo.AlignUp(cbArg, _ccInfo.PointerSize) / _ccInfo.PointerSize; + if (_arm64IdxGenReg + regSlots <= 8) + { + int argOfs = (int)_ccInfo.ArgumentRegistersOffset + _arm64IdxGenReg * 8; + _arm64IdxGenReg += regSlots; + return argOfs; + } + else + { + _arm64IdxGenReg = 8; + } + } + + if (_ccInfo.IsAppleArm64ABI) + { + int alignment = isValueType ? (isFloatHFA ? 4 : 8) : cbArg; + _arm64OfsStack = CallingConventionInfo.AlignUp(_arm64OfsStack, alignment); + } + + int result = (int)_ccInfo.OffsetOfArgs + _arm64OfsStack; + _arm64OfsStack += cbArg; + return result; + } + + private int GetNextOffsetX86(CorElementType argType, int argSize) + { + if (_x86NumRegistersUsed < _ccInfo.NumArgumentRegisters + && argType is not CorElementType.ValueType + and not CorElementType.R4 + and not CorElementType.R8 + and not CorElementType.I8 + and not CorElementType.U8) + { + _x86NumRegistersUsed++; + return (int)_ccInfo.ArgumentRegistersOffset + + (_ccInfo.NumArgumentRegisters - _x86NumRegistersUsed) * _ccInfo.PointerSize; + } + + int cbArg = _ccInfo.StackElemSize(argSize); + _x86OfsStack -= cbArg; + return _x86OfsStack; + } + + private int GetNextOffsetArm32(CorElementType argType, int argSize) + { + bool fFloatingPoint = false; + bool fRequiresAlign64Bit = false; + + switch (argType) + { + case CorElementType.I8: + case CorElementType.U8: + fRequiresAlign64Bit = true; + break; + case CorElementType.R4: + fFloatingPoint = true; + break; + case CorElementType.R8: + fFloatingPoint = true; + fRequiresAlign64Bit = true; + break; + case CorElementType.ValueType: + fRequiresAlign64Bit = _argTypeHandle.RequiresAlign8; + if (_argTypeHandle.IsHomogeneousAggregate) fFloatingPoint = true; + break; + } + + _armRequires64BitAlignment = fRequiresAlign64Bit; + int cbArg = _ccInfo.StackElemSize(argSize); + + if (fFloatingPoint && _ccInfo.IsArmhfABI && !IsVarArg) + { + ushort wAllocMask = checked((ushort)((1 << (cbArg / 4)) - 1)); + ushort cSteps = (ushort)(fRequiresAlign64Bit ? 9 - (cbArg / 8) : 17 - (cbArg / 4)); + ushort cShift = fRequiresAlign64Bit ? (ushort)2 : (ushort)1; + + for (ushort i = 0; i < cSteps; i++) + { + if ((_armWFPRegs & wAllocMask) == 0) + { + _armWFPRegs |= wAllocMask; + return _ccInfo.OffsetOfFloatArgumentRegisters + (i * cShift * 4); + } + wAllocMask <<= cShift; + } + + _armWFPRegs = 0xffff; + + if (fRequiresAlign64Bit) + _armOfsStack = CallingConventionInfo.AlignUp(_armOfsStack, _ccInfo.PointerSize * 2); + + int argOfs = (int)_ccInfo.OffsetOfArgs + _armOfsStack; + _armOfsStack += cbArg; + return argOfs; + } + + if (_armIdxGenReg < 4) + { + if (fRequiresAlign64Bit) + _armIdxGenReg = CallingConventionInfo.AlignUp(_armIdxGenReg, 2); + + int argOfs = (int)_ccInfo.ArgumentRegistersOffset + _armIdxGenReg * 4; + int cRemainingRegs = 4 - _armIdxGenReg; + + if (cbArg <= cRemainingRegs * _ccInfo.PointerSize) + { + _armIdxGenReg += CallingConventionInfo.AlignUp(cbArg, _ccInfo.PointerSize) / _ccInfo.PointerSize; + return argOfs; + } + + _armIdxGenReg = 4; + + if (_armOfsStack == 0) + { + _armOfsStack += cbArg - cRemainingRegs * _ccInfo.PointerSize; + return argOfs; + } + } + + if (fRequiresAlign64Bit) + _armOfsStack = CallingConventionInfo.AlignUp(_armOfsStack, _ccInfo.PointerSize * 2); + + int result = (int)_ccInfo.OffsetOfArgs + _armOfsStack; + _armOfsStack += cbArg; + return result; + } + + private int GetNextOffsetRiscVLoongArch(CorElementType argType, int argSize) + { + // Simplified: no FP struct detection, just use integer calling convention + int cFPRegs = argType is CorElementType.R4 or CorElementType.R8 ? 1 : 0; + + if (argType == CorElementType.ValueType && argSize > _ccInfo.EnregisteredParamTypeMaxSize) + argSize = _ccInfo.PointerSize; + + int cbArg = _ccInfo.StackElemSize(argSize); + + if (cFPRegs > 0 && !IsVarArg && cFPRegs + _rvLa64IdxFPReg <= _ccInfo.NumFloatArgumentRegisters) + { + int argOfs = _ccInfo.OffsetOfFloatArgumentRegisters + _rvLa64IdxFPReg * _ccInfo.FloatRegisterSize; + _rvLa64IdxFPReg += cFPRegs; + return argOfs; + } + + int regSlots = CallingConventionInfo.AlignUp(cbArg, _ccInfo.PointerSize) / _ccInfo.PointerSize; + if (_rvLa64IdxGenReg + regSlots <= _ccInfo.NumArgumentRegisters) + { + int argOfs = (int)_ccInfo.ArgumentRegistersOffset + _rvLa64IdxGenReg * _ccInfo.PointerSize; + _rvLa64IdxGenReg += regSlots; + return argOfs; + } + else + { + _rvLa64IdxGenReg = _ccInfo.NumArgumentRegisters; + } + + int result = (int)_ccInfo.OffsetOfArgs + _rvLa64OfsStack; + _rvLa64OfsStack += cbArg; + return result; + } + + // ---- Return type computation ---- + + private void ComputeReturnFlags() + { + _RETURN_FLAGS_COMPUTED = true; + CorElementType retType = GetReturnType(out ArgTypeInfo thRetType, out bool forceByRef); + + if (forceByRef) + { + _RETURN_HAS_RET_BUFFER = true; + return; + } + + switch (retType) + { + case CorElementType.TypedByRef: + _RETURN_HAS_RET_BUFFER = true; + break; + + case CorElementType.ValueType: + if (thRetType.Size > _ccInfo.EnregisteredParamTypeMaxSize && _ccInfo.EnregisteredParamTypeMaxSize > 0) + { + _RETURN_HAS_RET_BUFFER = true; + } + else if (_ccInfo.Architecture is RuntimeInfoArchitecture.X86 or RuntimeInfoArchitecture.X64) + { + int size = thRetType.Size; + if ((size & (size - 1)) != 0) // not power of 2 + _RETURN_HAS_RET_BUFFER = true; + } + break; + } + } + + private void ForceSigWalk() + { + Debug.Assert(!_ITERATION_STARTED); + + int numRegistersUsed = 0; + int nSizeOfArgStack = 0; + + if (_ccInfo.Architecture == RuntimeInfoArchitecture.X86) + { + if (HasThis) numRegistersUsed++; + if (HasRetBuffArg() && _ccInfo.IsRetBuffPassedAsFirstArg) numRegistersUsed++; + if (IsVarArg) + { + nSizeOfArgStack += _ccInfo.PointerSize; + numRegistersUsed = _ccInfo.NumArgumentRegisters; + } + + int nArgs = NumFixedArgs; + for (int i = _skipFirstArg ? 1 : 0; i < nArgs; i++) + { + CorElementType type = GetArgumentType(i, out ArgTypeInfo thArgType, out bool argForced); + if (argForced) type = CorElementType.Byref; + + // Simplified: assume all non-trivial types go to stack + int structSize = ArgTypeInfo.GetElemSize(type, thArgType, _ccInfo.PointerSize); + nSizeOfArgStack += _ccInfo.StackElemSize(structSize); + } + + if (HasAsyncContinuation) + { + if (numRegistersUsed < _ccInfo.NumArgumentRegisters) + { + numRegistersUsed++; + _asyncContinuationLoc = numRegistersUsed == 1 ? AsyncContinuationLocation.Ecx : AsyncContinuationLocation.Edx; + } + else + { + nSizeOfArgStack += _ccInfo.PointerSize; + _asyncContinuationLoc = AsyncContinuationLocation.Stack; + } + } + + if (HasParamType) + { + if (numRegistersUsed < _ccInfo.NumArgumentRegisters) + { + numRegistersUsed++; + _paramTypeLoc = numRegistersUsed == 1 ? ParamTypeLocation.Ecx : ParamTypeLocation.Edx; + } + else + { + nSizeOfArgStack += _ccInfo.PointerSize; + _paramTypeLoc = ParamTypeLocation.Stack; + } + } + } + else + { + // Non-x86: iterate through GetNextOffset to compute stack size + int maxOffset = (int)_ccInfo.OffsetOfArgs; + int ofs; + while (CallingConventionInfo.InvalidOffset != (ofs = GetNextOffset())) + { + int stackElemSize; + if (_ccInfo.Architecture == RuntimeInfoArchitecture.X64) + { + stackElemSize = _ccInfo.IsX64UnixABI + ? _ccInfo.StackElemSize(GetArgSize()) + : _ccInfo.PointerSize; + } + else + { + stackElemSize = _ccInfo.StackElemSize(GetArgSize()); + } + + int endOfs = ofs + stackElemSize; + if (IsArgumentRegisterOffset(ofs)) + continue; + if (CallingConventionInfo.IsFloatArgumentRegisterOffset(ofs)) + continue; + if (ofs == CallingConventionInfo.StructInRegsOffset) + continue; + if (endOfs > maxOffset) + maxOffset = endOfs; + } + + nSizeOfArgStack = maxOffset - (int)_ccInfo.OffsetOfArgs; + Reset(); + } + + _nSizeOfArgStack = nSizeOfArgStack; + _SIZE_OF_ARG_STACK_COMPUTED = true; + } + + private bool IsArgumentRegisterOffset(int offset) + { + return _ccInfo.IsArgumentRegisterOffset(offset); + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/ArgIteratorData.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/ArgIteratorData.cs new file mode 100644 index 00000000000000..b48a86f5b2e0ee --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/ArgIteratorData.cs @@ -0,0 +1,82 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Ported from crossgen2's ArgIterator.cs — data holder types. + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers.CallingConvention; + +/// +/// Describes how a single argument is laid out in registers and/or stack locations. +/// Ported from crossgen2's ArgLocDesc. +/// +internal struct ArgLocDesc +{ + public int m_idxFloatReg; + public int m_cFloatReg; + + public int m_idxGenReg; + public short m_cGenReg; + + public bool m_fRequires64BitAlignment; + + public int m_byteStackIndex; + public int m_byteStackSize; + + public void Init() + { + m_idxFloatReg = -1; + m_cFloatReg = 0; + m_idxGenReg = -1; + m_cGenReg = 0; + m_byteStackIndex = -1; + m_byteStackSize = 0; + m_fRequires64BitAlignment = false; + } +} + +/// +/// Holds parsed method signature data for . +/// Ported from crossgen2's ArgIteratorData. +/// +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; + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/ArgTypeInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/ArgTypeInfo.cs new file mode 100644 index 00000000000000..bd3ca6ecf632d5 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/ArgTypeInfo.cs @@ -0,0 +1,142 @@ +// 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. + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers.CallingConvention; + +/// +/// 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 + ? (int)rts.GetBaseSize(th) - 2 * target.PointerSize // InstanceFieldSize = BaseSize - ObjHeader - MethodTable ptr + : target.PointerSize; + + bool requiresAlign8 = false; + bool isHfa = false; + int hfaElemSize = 0; + + if (isValueType) + { + // TODO: Implement RequiresAlign8 via IRuntimeTypeSystem + // TODO: Implement IsHomogeneousAggregate via IRuntimeTypeSystem + } + + return new ArgTypeInfo + { + CorElementType = corType, + Size = size, + IsValueType = isValueType, + RequiresAlign8 = requiresAlign8, + IsHomogeneousAggregate = isHfa, + HomogeneousAggregateElementSize = hfaElemSize, + RuntimeTypeHandle = th, + }; + } + + /// + /// 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/StackWalk/CallingConvention/CallingConventionInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/CallingConventionInfo.cs new file mode 100644 index 00000000000000..ee89c064c24a27 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/CallingConventionInfo.cs @@ -0,0 +1,274 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Provides an abstraction over platform-specific calling conventions +// (specifically, the managed calling convention utilized by the JIT). +// Ported from crossgen2's TransitionBlock.cs. + +using System; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers.CallingConvention; + +/// +/// Architecture-specific calling convention constants and methods for +/// mapping method arguments to register and stack locations. +/// +/// +/// Layout-dependent values (offsets, sizes) come from the data descriptor +/// (). ABI-invariant values (register counts, +/// alignment rules) are hardcoded per architecture since they are defined by +/// the hardware/OS ABI and never change. +/// +internal sealed class CallingConventionInfo +{ + // Layout values from the data descriptor + private readonly uint _sizeOfTransitionBlock; + private readonly uint _argumentRegistersOffset; + private readonly uint _firstGCRefMapSlot; + private readonly uint _offsetOfArgs; + private readonly int _offsetOfFloatArgumentRegisters; + + // ABI invariants + public int PointerSize { get; } + public int NumArgumentRegisters { get; } + public int NumFloatArgumentRegisters { get; } + public int FloatRegisterSize { get; } + public int EnregisteredParamTypeMaxSize { get; } + public int StackSlotSize { get; } + public bool IsRetBuffPassedAsFirstArg { get; } + public bool IsX64UnixABI { get; } + public bool IsAppleArm64ABI { get; } + public bool IsArmhfABI { get; } + + public RuntimeInfoArchitecture Architecture { get; } + + // Convenience accessors + public uint SizeOfTransitionBlock => _sizeOfTransitionBlock; + public uint ArgumentRegistersOffset => _argumentRegistersOffset; + public uint FirstGCRefMapSlot => _firstGCRefMapSlot; + public uint OffsetOfArgs => _offsetOfArgs; + public int OffsetOfFloatArgumentRegisters => _offsetOfFloatArgumentRegisters; + public int SizeOfArgumentRegisters => NumArgumentRegisters * PointerSize; + + public const int InvalidOffset = -1; + public const int StructInRegsOffset = -2; + + /// + /// Creates a for the given target, reading + /// layout data from the data descriptor and filling in ABI constants from + /// the target's architecture and OS. + /// + public CallingConventionInfo(Target target) + { + IRuntimeInfo runtimeInfo = target.Contracts.RuntimeInfo; + Architecture = runtimeInfo.GetTargetArchitecture(); + RuntimeInfoOperatingSystem os = runtimeInfo.GetTargetOperatingSystem(); + PointerSize = target.PointerSize; + + // Read layout values from the data descriptor + Target.TypeInfo tbType = target.GetTypeInfo(DataType.TransitionBlock); + _sizeOfTransitionBlock = (uint)tbType.Size!; + _argumentRegistersOffset = (uint)tbType.Fields["ArgumentRegistersOffset"].Offset; + _firstGCRefMapSlot = (uint)tbType.Fields["FirstGCRefMapSlot"].Offset; + _offsetOfArgs = (uint)tbType.Fields["OffsetOfArgs"].Offset; + _offsetOfFloatArgumentRegisters = tbType.Fields["OffsetOfFloatArgumentRegisters"].Offset; + + // Fill in ABI invariants based on architecture + switch (Architecture) + { + case RuntimeInfoArchitecture.X86: + NumArgumentRegisters = 2; // ECX, EDX + NumFloatArgumentRegisters = 0; + FloatRegisterSize = 0; + EnregisteredParamTypeMaxSize = 0; + StackSlotSize = 4; + IsRetBuffPassedAsFirstArg = true; + break; + + case RuntimeInfoArchitecture.X64: + if (os is RuntimeInfoOperatingSystem.Unix or RuntimeInfoOperatingSystem.Apple) + { + // Unix/Apple AMD64 ABI (SysV) + NumArgumentRegisters = 6; // RDI, RSI, RDX, RCX, R8, R9 + NumFloatArgumentRegisters = 8; // XMM0-XMM7 + FloatRegisterSize = 16; // M128A + EnregisteredParamTypeMaxSize = 16; + IsX64UnixABI = true; + } + else + { + // Windows AMD64 ABI + NumArgumentRegisters = 4; // RCX, RDX, R8, R9 + NumFloatArgumentRegisters = 0; // Shared with GP regs on Windows + FloatRegisterSize = 16; + EnregisteredParamTypeMaxSize = 8; + } + StackSlotSize = 8; + IsRetBuffPassedAsFirstArg = true; + break; + + case RuntimeInfoArchitecture.Arm: + NumArgumentRegisters = 4; // R0-R3 + NumFloatArgumentRegisters = 16; // 16 single-precision slots (D0-D7 / S0-S15) + FloatRegisterSize = 4; + EnregisteredParamTypeMaxSize = 0; + StackSlotSize = 4; + IsRetBuffPassedAsFirstArg = true; + IsArmhfABI = true; // TODO: detect armel + break; + + case RuntimeInfoArchitecture.Arm64: + NumArgumentRegisters = 8; // X0-X7 + NumFloatArgumentRegisters = 8; // V0-V7 + FloatRegisterSize = 16; + EnregisteredParamTypeMaxSize = 16; + StackSlotSize = 8; + IsRetBuffPassedAsFirstArg = false; // ARM64 uses X8 for retbuf + IsAppleArm64ABI = os == RuntimeInfoOperatingSystem.Apple; + break; + + case RuntimeInfoArchitecture.LoongArch64: + NumArgumentRegisters = 8; // A0-A7 + NumFloatArgumentRegisters = 8; // FA0-FA7 + FloatRegisterSize = 8; + EnregisteredParamTypeMaxSize = 16; + StackSlotSize = 8; + IsRetBuffPassedAsFirstArg = true; + break; + + case RuntimeInfoArchitecture.RiscV64: + NumArgumentRegisters = 8; // a0-a7 + NumFloatArgumentRegisters = 8; // fa0-fa7 + FloatRegisterSize = 8; + EnregisteredParamTypeMaxSize = 16; + StackSlotSize = 8; + IsRetBuffPassedAsFirstArg = true; + break; + + default: + throw new NotSupportedException($"Architecture {Architecture} is not supported for calling convention analysis."); + } + } + + // ---- Derived methods ---- + + /// + /// Returns the byte offset of the 'this' pointer in the transition block. + /// + public int ThisOffset + { + get + { + if (Architecture == RuntimeInfoArchitecture.X86) + { + // ECX offset within ArgumentRegisters: ECX is at offset PointerSize (after EDX at 0) + return (int)ArgumentRegistersOffset + PointerSize; + } + return (int)ArgumentRegistersOffset; + } + } + + /// + /// Rounds up a parameter size to the stack slot size for the platform. + /// + public int StackElemSize(int parmSize, bool isValueType = false, bool isFloatHfa = false) + { + if (IsAppleArm64ABI) + { + if (!isValueType) + { + // Primitives use their natural size, no padding + return parmSize; + } + if (isFloatHfa) + { + // Float HFA: 4-byte alignment + return parmSize; + } + } + return AlignUp(parmSize, StackSlotSize); + } + + /// + /// Maps a GCRefMap position index to the byte offset in the transition block. + /// + public int OffsetFromGCRefMapPos(int pos) + { + if (Architecture == RuntimeInfoArchitecture.X86) + { + if (pos < NumArgumentRegisters) + { + return (int)ArgumentRegistersOffset + SizeOfArgumentRegisters - (pos + 1) * PointerSize; + } + return (int)OffsetOfArgs + (pos - NumArgumentRegisters) * PointerSize; + } + return (int)FirstGCRefMapSlot + pos * PointerSize; + } + + /// + /// Returns true if the argument at the given offset is a float register. + /// Float register offsets are negative. + /// + public static bool IsFloatArgumentRegisterOffset(int offset) => offset < 0; + + /// + /// Returns true if the argument at the given offset is in a general-purpose register. + /// + public bool IsArgumentRegisterOffset(int offset) + { + return offset >= (int)ArgumentRegistersOffset + && offset < (int)ArgumentRegistersOffset + SizeOfArgumentRegisters; + } + + /// + /// Returns true if the argument at the given offset is on the stack. + /// + public bool IsStackArgumentOffset(int offset) + { + return offset >= (int)ArgumentRegistersOffset + SizeOfArgumentRegisters; + } + + /// + /// Checks if a value type of the given size should be passed by reference + /// (applies to x64 and ARM64). + /// + public bool IsArgPassedByRef(int size) + { + if (EnregisteredParamTypeMaxSize == 0) + return false; + + if (Architecture == RuntimeInfoArchitecture.X64) + { + // On x64, also check power-of-2 rule + return size > EnregisteredParamTypeMaxSize || (size & (size - 1)) != 0; + } + + return size > EnregisteredParamTypeMaxSize; + } + + /// + /// Returns the byte offset of the return buffer argument. + /// + public int GetRetBuffArgOffset(bool hasThis) + { + if (Architecture == RuntimeInfoArchitecture.X86) + { + // x86: retbuf goes in EDX if hasThis (this in ECX), else ECX + return hasThis + ? (int)ArgumentRegistersOffset // EDX offset = 0 + : (int)ArgumentRegistersOffset + PointerSize; // ECX offset + } + if (Architecture == RuntimeInfoArchitecture.Arm64) + { + // ARM64: retbuf is in X8, which is at FirstGCRefMapSlot + return (int)FirstGCRefMapSlot; + } + // Default: retbuf is after 'this' in the argument registers + return (int)ArgumentRegistersOffset + (hasThis ? PointerSize : 0); + } + + internal static int AlignUp(int value, int alignment) + { + return (value + (alignment - 1)) & ~(alignment - 1); + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameIterator.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameIterator.cs index 0152698f6bc65c..88bfcaece90d1d 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameIterator.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameIterator.cs @@ -4,6 +4,7 @@ using System; using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; +using Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers.CallingConvention; using Microsoft.Diagnostics.DataContractReader.Data; namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; @@ -50,11 +51,15 @@ internal enum FrameType private readonly Target target; private readonly TargetPointer terminator; private TargetPointer currentFramePointer; + private CallingConventionInfo? _callingConventionInfo; internal Data.Frame CurrentFrame => target.ProcessedData.GetOrAdd(currentFramePointer); public TargetPointer CurrentFrameAddress => currentFramePointer; + private CallingConventionInfo GetCallingConventionInfo() + => _callingConventionInfo ??= new CallingConventionInfo(target); + public FrameIterator(Target target, ThreadData threadData) { this.target = target; @@ -413,8 +418,8 @@ private void PromoteCallerStackUsingGCRefMap( { int pos = decoder.CurrentPos; GCRefMapToken token = decoder.ReadToken(); - uint offset = OffsetFromGCRefMapPos(pos); - TargetPointer slotAddress = new(transitionBlock.Value + offset); + int offset = GetCallingConventionInfo().OffsetFromGCRefMapPos(pos); + TargetPointer slotAddress = new(transitionBlock.Value + (ulong)offset); switch (token) { @@ -618,7 +623,6 @@ private void PromoteCallerStack( MethodDescHandle mdh = rts.GetMethodDescHandle(methodDescPtr); bool hasThis = methodSig.Header.IsInstance; - bool hasRetBuf = methodSig.ReturnType is GcTypeKind.Other; bool requiresInstArg = false; bool isAsync = false; bool isValueTypeThis = false; @@ -635,54 +639,73 @@ private void PromoteCallerStack( { } - PromoteCallerStackHelper(transitionBlock, methodSig, hasThis, hasRetBuf, + PromoteCallerStackHelper(transitionBlock, methodSig, hasThis, requiresInstArg, isAsync, isValueTypeThis, scanContext); } /// /// Core logic for promoting caller stack GC references. - /// Matches native TransitionFrame::PromoteCallerStackHelper (frames.cpp:1560). + /// Uses to correctly map arguments to their + /// register/stack locations per the target's calling convention. + /// Matches native TransitionFrame::PromoteCallerStackHelper (frames.cpp:1546). /// private void PromoteCallerStackHelper( TargetPointer transitionBlock, MethodSignature methodSig, bool hasThis, - bool hasRetBuf, bool requiresInstArg, bool isAsync, bool isValueTypeThis, GcScanContext scanContext) { - int numRegistersUsed = 0; - if (hasThis) - numRegistersUsed++; - if (hasRetBuf) - numRegistersUsed++; - if (requiresInstArg) - numRegistersUsed++; - if (isAsync) - numRegistersUsed++; - - bool isArm64 = IsTargetArm64(); - if (isArm64) - numRegistersUsed++; - - if (hasThis) + CallingConventionInfo ccInfo; + try + { + ccInfo = GetCallingConventionInfo(); + } + catch { - int thisPos = isArm64 ? 1 : 0; - uint thisOffset = OffsetFromGCRefMapPos(thisPos); - TargetPointer thisAddr = new(transitionBlock.Value + thisOffset); + return; + } + + // Build ArgTypeInfo array from decoded signature + ArgTypeInfo[] paramTypes = new ArgTypeInfo[methodSig.ParameterTypes.Length]; + for (int i = 0; i < paramTypes.Length; i++) + { + paramTypes[i] = GcTypeKindToArgTypeInfo(methodSig.ParameterTypes[i], ccInfo.PointerSize); + } + ArgTypeInfo returnTypeInfo = GcTypeKindToArgTypeInfo(methodSig.ReturnType, ccInfo.PointerSize); + + ArgIteratorData argData = new(hasThis, methodSig.Header.CallingConvention is SignatureCallingConvention.VarArgs, paramTypes, returnTypeInfo); + CallingConvention.ArgIterator argit = new(ccInfo, argData, hasParamType: requiresInstArg, hasAsyncContinuation: isAsync, forcedByRefParams: System.Array.Empty()); + + // Promote 'this' for non-static methods + if (argit.HasThis) + { + int thisOffset = argit.GetThisOffset(); + TargetPointer thisAddr = new(transitionBlock.Value + (ulong)thisOffset); GcScanFlags thisFlags = isValueTypeThis ? GcScanFlags.GC_CALL_INTERIOR : GcScanFlags.None; scanContext.GCReportCallback(thisAddr, thisFlags); } - // TODO(stackref): Promote async continuation pointer at its specific offset + // Promote async continuation + if (argit.HasAsyncContinuation) + { + int asyncOffset = argit.GetAsyncContinuationArgOffset(); + TargetPointer asyncAddr = new(transitionBlock.Value + (ulong)asyncOffset); + scanContext.GCReportCallback(asyncAddr, GcScanFlags.None); + } - int pos = numRegistersUsed; - foreach (GcTypeKind kind in methodSig.ParameterTypes) + // Walk each argument using ArgIterator for correct offsets + int argIndex = 0; + int argOffset; + while ((argOffset = argit.GetNextOffset()) != CallingConventionInfo.InvalidOffset) { - uint offset = OffsetFromGCRefMapPos(pos); - TargetPointer slotAddress = new(transitionBlock.Value + offset); + if (argIndex >= methodSig.ParameterTypes.Length) + break; + + GcTypeKind kind = methodSig.ParameterTypes[argIndex]; + TargetPointer slotAddress = new(transitionBlock.Value + (ulong)argOffset); switch (kind) { @@ -693,15 +716,41 @@ private void PromoteCallerStackHelper( scanContext.GCReportCallback(slotAddress, GcScanFlags.GC_CALL_INTERIOR); break; case GcTypeKind.Other: - // TODO(stackref): Value type GCDesc scanning + // Value type: if passed by ref, report as interior pointer + if (argit.IsArgPassedByRef()) + { + 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++; + argIndex++; } } + /// + /// Converts a to a minimal for + /// ArgIterator consumption. This is a bridge until we have a full type-aware provider. + /// + private static ArgTypeInfo GcTypeKindToArgTypeInfo(GcTypeKind kind, int pointerSize) + { + return kind switch + { + GcTypeKind.None => ArgTypeInfo.ForPrimitive(CorElementType.I, pointerSize), + GcTypeKind.Ref => ArgTypeInfo.ForPrimitive(CorElementType.Class, pointerSize), + GcTypeKind.Interior => ArgTypeInfo.ForPrimitive(CorElementType.Byref, pointerSize), + GcTypeKind.Other => new ArgTypeInfo + { + CorElementType = CorElementType.ValueType, + Size = pointerSize, // Conservative: assume pointer-sized for now + IsValueType = true, + }, + _ => ArgTypeInfo.ForPrimitive(CorElementType.I, pointerSize), + }; + } + private ReadOnlySpan GetMethodSignatureBytes(TargetPointer methodDescPtr, out MetadataReader? metadataReader) { metadataReader = null; @@ -733,13 +782,6 @@ private ReadOnlySpan GetMethodSignatureBytes(TargetPointer methodDescPtr, return blobReader.ReadBytes(blobReader.Length); } - private uint OffsetFromGCRefMapPos(int pos) - { - Target.TypeInfo tbType = target.GetTypeInfo(DataType.TransitionBlock); - uint firstSlotOffset = (uint)tbType.Fields[nameof(Data.TransitionBlock.FirstGCRefMapSlot)].Offset; - return firstSlotOffset + (uint)(pos * target.PointerSize); - } - private bool IsTargetArm64() { return target.Contracts.RuntimeInfo.GetTargetArchitecture() is RuntimeInfoArchitecture.Arm64; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/TransitionBlock.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/TransitionBlock.cs index 97443b11f43645..e727286cc7de06 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/TransitionBlock.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/TransitionBlock.cs @@ -23,6 +23,7 @@ public TransitionBlock(Target target, TargetPointer address) // in the data descriptor. They represent computed layout positions, not actual memory reads. FirstGCRefMapSlot = (uint)type.Fields[nameof(FirstGCRefMapSlot)].Offset; ArgumentRegistersOffset = (uint)type.Fields[nameof(ArgumentRegistersOffset)].Offset; + OffsetOfFloatArgumentRegisters = type.Fields[nameof(OffsetOfFloatArgumentRegisters)].Offset; } public TargetPointer ReturnAddress { get; } @@ -42,4 +43,11 @@ public TransitionBlock(Target target, TargetPointer address) /// Offset to the argument registers area, relative to the TransitionBlock pointer. /// public uint ArgumentRegistersOffset { get; } + + /// + /// Offset to the float argument registers area, relative to the TransitionBlock pointer. + /// Negative on most platforms (float regs are stored before the TransitionBlock). + /// Zero on platforms without float argument registers (x86, Windows x64). + /// + public int OffsetOfFloatArgumentRegisters { get; } }