diff --git a/docs/design/datacontracts/Signature.md b/docs/design/datacontracts/Signature.md index e0eb96ab807a17..a2bdeed38bda3f 100644 --- a/docs/design/datacontracts/Signature.md +++ b/docs/design/datacontracts/Signature.md @@ -17,14 +17,26 @@ These tags are used in signatures generated internally by the runtime that are n ```csharp TypeHandle DecodeFieldSignature(BlobHandle blobHandle, ModuleHandle moduleHandle, TypeHandle ctx); + +// Returns the address of the first argument of a vararg call relative to the cookie pointer location. +TargetPointer GetVarArgArgsBase(TargetPointer vaSigCookieAddr); + +// Returns the address and length of the raw vararg signature blob held by the cookie. +void GetVarArgSignature(TargetPointer vaSigCookieAddr, out TargetPointer signatureAddress, out uint signatureLength); ``` +`vaSigCookieAddr` is the target address of the `VASigCookie*` slot pushed onto the stack by the +vararg call site (i.e. it is a pointer to a pointer to the cookie). `GetVarArgArgsBase` and +`GetVarArgSignature` throw if the address is null or the cookie pointer it references is null. + ## Version 1 Data descriptors used: | Data Descriptor Name | Field | Meaning | | --- | --- | --- | -| _none_ | | | +| `VASigCookie` | `SizeOfArgs` | Total size in bytes of the pushed argument list. Used on x86 to locate the args base. | +| `VASigCookie` | `SignaturePointer` | Target address of the raw vararg signature blob. | +| `VASigCookie` | `SignatureLength` | Length in bytes of the raw vararg signature blob. | Global variables used: | Global Name | Type | Purpose | @@ -37,6 +49,7 @@ Contracts used: | RuntimeTypeSystem | | Loader | | EcmaMetadata | +| RuntimeInfo | Constants: | Constant Name | Meaning | Value | @@ -69,3 +82,31 @@ TypeHandle ISignature.DecodeFieldSignature(BlobHandle blobHandle, ModuleHandle m ### Other consumers `RuntimeSignatureDecoder` is shared infrastructure within the cDAC. Other contracts construct their own decoder and provider directly when they need to decode method or local signatures rather than going through this contract. For example, the [StackWalk](./StackWalk.md) contract uses `RuntimeSignatureDecoder` with a GC-specific provider to classify method parameters during signature-based GC reference scanning. + +### Vararg call cookies + +`GetVarArgArgsBase` and `GetVarArgSignature` decode a `VASigCookie*` slot pushed by a vararg call site and are used by the DAC/DBI `GetVarArgSig` API to recover the location of the first argument and the raw vararg signature blob for a vararg call frame. + +```csharp +TargetPointer ISignature.GetVarArgArgsBase(TargetPointer vaSigCookieAddr) +{ + TargetPointer vaSigCookie = _target.ReadPointer(vaSigCookieAddr); + VASigCookie cookie = _target.ProcessedData.GetOrAdd(vaSigCookie); + + // On x86 the args are pushed below the cookie pointer (stack grows down on the args walk); + // on every other platform the first argument follows the cookie pointer in memory + // (stack grows up on the args walk). + if (RuntimeInfo.GetTargetArchitecture() == X86) + return vaSigCookieAddr + cookie.SizeOfArgs; + return vaSigCookieAddr + sizeof(TargetPointer); +} + +void ISignature.GetVarArgSignature(TargetPointer vaSigCookieAddr, out TargetPointer signatureAddress, out uint signatureLength) +{ + TargetPointer vaSigCookie = _target.ReadPointer(vaSigCookieAddr); + VASigCookie cookie = _target.ProcessedData.GetOrAdd(vaSigCookie); + + signatureAddress = cookie.SignaturePointer; + signatureLength = cookie.SignatureLength; +} +``` diff --git a/src/coreclr/vm/ceeload.h b/src/coreclr/vm/ceeload.h index 4462f788a61932..24737cb71c8576 100644 --- a/src/coreclr/vm/ceeload.h +++ b/src/coreclr/vm/ceeload.h @@ -351,6 +351,13 @@ struct VASigCookie Instantiation methodInst; }; +template<> +struct cdac_data +{ + static constexpr size_t SignaturePointer = offsetof(VASigCookie, signature) + offsetof(Signature, m_pSig); + static constexpr size_t SignatureLength = offsetof(VASigCookie, signature) + offsetof(Signature, m_cbSig); +}; + // // VASigCookies are allocated in VASigCookieBlocks to amortize // allocation cost and allow proper bookkeeping. diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index 307b2eef5bb9f7..88ec9e0a48cda9 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -416,6 +416,13 @@ CDAC_TYPE_FIELD(ArrayListBlock, T_UINT32, Size, cdac_data::Size) CDAC_TYPE_FIELD(ArrayListBlock, T_POINTER, ArrayStart, cdac_data::ArrayStart) CDAC_TYPE_END(ArrayListBlock) +CDAC_TYPE_BEGIN(VASigCookie) +CDAC_TYPE_INDETERMINATE(VASigCookie) +CDAC_TYPE_FIELD(VASigCookie, T_UINT32, SizeOfArgs, offsetof(VASigCookie, sizeOfArgs)) +CDAC_TYPE_FIELD(VASigCookie, T_POINTER, SignaturePointer, cdac_data::SignaturePointer) +CDAC_TYPE_FIELD(VASigCookie, T_UINT32, SignatureLength, cdac_data::SignatureLength) +CDAC_TYPE_END(VASigCookie) + // RuntimeTypeSystem CDAC_TYPE_BEGIN(MethodTable) diff --git a/src/coreclr/vm/siginfo.hpp b/src/coreclr/vm/siginfo.hpp index 69fefaecd43377..c19a75c5f8fefc 100644 --- a/src/coreclr/vm/siginfo.hpp +++ b/src/coreclr/vm/siginfo.hpp @@ -353,6 +353,8 @@ class Signature DWORD GetRawSigLen() const; private: + friend struct ::cdac_data; + PCCOR_SIGNATURE m_pSig; DWORD m_cbSig; }; // class Signature diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ISignature.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ISignature.cs index f53847ea4e3b55..e32b595f5f4b32 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ISignature.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ISignature.cs @@ -10,6 +10,8 @@ public interface ISignature : IContract { static string IContract.Name { get; } = nameof(Signature); TypeHandle DecodeFieldSignature(BlobHandle blobHandle, ModuleHandle moduleHandle, TypeHandle ctx) => throw new NotImplementedException(); + TargetPointer GetVarArgArgsBase(TargetPointer vaSigCookieAddr) => throw new NotImplementedException(); + void GetVarArgSignature(TargetPointer vaSigCookieAddr, out TargetPointer signatureAddress, out uint signatureLength) => throw new NotImplementedException(); } public readonly struct Signature : ISignature diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs index 27a8cdbc90d89a..192f211df22aeb 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs @@ -175,6 +175,7 @@ public enum DataType ComInterfaceEntry, InternalComInterfaceDispatch, AuxiliarySymbolInfo, + VASigCookie, CodeRangeMapRangeList, /* GC Data Types */ diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Signature/Signature_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Signature/Signature_1.cs index 8517cf674bccdb..7ac8ab4befde80 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Signature/Signature_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Signature/Signature_1.cs @@ -1,7 +1,9 @@ // 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 Microsoft.Diagnostics.DataContractReader.SignatureHelpers; @@ -49,4 +51,43 @@ TypeHandle ISignature.DecodeFieldSignature(BlobHandle blobHandle, ModuleHandle m RuntimeSignatureDecoder decoder = new(provider, _target, mdReader, ctx); return decoder.DecodeFieldSignature(ref blobReader); } + + TargetPointer ISignature.GetVarArgArgsBase(TargetPointer vaSigCookieAddr) + { + if (vaSigCookieAddr == TargetPointer.Null) + throw new ArgumentException("VASigCookie address must be non-null.", nameof(vaSigCookieAddr)); + // Compute the address of the first argument. On x86 the args are pushed below the cookie + // pointer (stack grows down on the args walk), so the first argument lies at + // vaSigCookieAddr + sizeOfArgs. + // On all other platforms the first argument follows the cookie pointer in memory + // (stack grows up on the args walk), so its address is at + // vaSigCookieAddr + sizeof(VASigCookie*). + if (_target.Contracts.RuntimeInfo.GetTargetArchitecture() == RuntimeInfoArchitecture.X86) + { + Data.VASigCookie cookie = GetCookie(vaSigCookieAddr); + return new TargetPointer(vaSigCookieAddr.Value + cookie.SizeOfArgs); + } + + return new TargetPointer(vaSigCookieAddr.Value + (ulong)_target.PointerSize); + } + + void ISignature.GetVarArgSignature(TargetPointer vaSigCookieAddr, out TargetPointer signatureAddress, out uint signatureLength) + { + if (vaSigCookieAddr == TargetPointer.Null) + throw new ArgumentException("VASigCookie address must be non-null.", nameof(vaSigCookieAddr)); + Data.VASigCookie cookie = GetCookie(vaSigCookieAddr); + + signatureAddress = cookie.SignaturePointer; + signatureLength = cookie.SignatureLength; + Debug.Assert(signatureAddress != TargetPointer.Null || signatureLength == 0, + "VASigCookie has a non-zero signature length but a null signature pointer."); + } + private Data.VASigCookie GetCookie(TargetPointer vaSigCookieAddr) + { + TargetPointer vaSigCookie = _target.ReadPointer(vaSigCookieAddr); + if (vaSigCookie == TargetPointer.Null) + throw new InvalidOperationException("VASigCookie pointer is null."); + + return _target.ProcessedData.GetOrAdd(vaSigCookie); + } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/VASigCookie.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/VASigCookie.cs new file mode 100644 index 00000000000000..666ebd65d84b23 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/VASigCookie.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.DataContractReader.Data; + +internal sealed class VASigCookie : IData +{ + static VASigCookie IData.Create(Target target, TargetPointer address) + => new VASigCookie(target, address); + + public VASigCookie(Target target, TargetPointer address) + { + Target.TypeInfo type = target.GetTypeInfo(DataType.VASigCookie); + + SizeOfArgs = target.ReadField(address, type, nameof(SizeOfArgs)); + SignaturePointer = target.ReadPointerField(address, type, nameof(SignaturePointer)); + SignatureLength = target.ReadField(address, type, nameof(SignatureLength)); + } + + public uint SizeOfArgs { get; init; } + public TargetPointer SignaturePointer { get; init; } + public uint SignatureLength { get; init; } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs index 4e46fc23d9e752..bffbf925fafa61 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs @@ -1167,7 +1167,40 @@ public int IsDiagnosticsHiddenOrLCGMethod(ulong vmMethodDesc, int* pRetVal) } public int GetVarArgSig(ulong VASigCookieAddr, ulong* pArgBase, DacDbiTargetBuffer* pRetVal) - => LegacyFallbackHelper.CanFallback() && _legacy is not null ? _legacy.GetVarArgSig(VASigCookieAddr, pArgBase, pRetVal) : HResults.E_NOTIMPL; + { + *pArgBase = 0; + *pRetVal = default; + int hr = HResults.S_OK; + try + { + Contracts.ISignature signature = _target.Contracts.Signature; + TargetPointer argBase = signature.GetVarArgArgsBase(new TargetPointer(VASigCookieAddr)); + signature.GetVarArgSignature(new TargetPointer(VASigCookieAddr), out TargetPointer sigAddr, out uint sigLen); + + *pArgBase = argBase.Value; + *pRetVal = new DacDbiTargetBuffer { pAddress = sigAddr.Value, cbSize = sigLen }; + } + catch (System.Exception ex) + { + hr = ex.HResult; + } +#if DEBUG + if (_legacy is not null) + { + ulong argBaseLocal; + DacDbiTargetBuffer retValLocal = default; + int hrLocal = _legacy.GetVarArgSig(VASigCookieAddr, &argBaseLocal, &retValLocal); + Debug.ValidateHResult(hr, hrLocal); + if (hr == HResults.S_OK) + { + Debug.Assert(*pArgBase == argBaseLocal, $"cDAC argBase: 0x{*pArgBase:X}, DAC argBase: 0x{argBaseLocal:X}"); + Debug.Assert(pRetVal->pAddress == retValLocal.pAddress, $"cDAC sigAddr: 0x{pRetVal->pAddress:X}, DAC sigAddr: 0x{retValLocal.pAddress:X}"); + Debug.Assert(pRetVal->cbSize == retValLocal.cbSize, $"cDAC sigLen: {pRetVal->cbSize}, DAC sigLen: {retValLocal.cbSize}"); + } + } +#endif + return hr; + } public int RequiresAlign8(ulong thExact, Interop.BOOL* pResult) { diff --git a/src/native/managed/cdac/tests/SignatureTests.cs b/src/native/managed/cdac/tests/SignatureTests.cs new file mode 100644 index 00000000000000..5bf02560583909 --- /dev/null +++ b/src/native/managed/cdac/tests/SignatureTests.cs @@ -0,0 +1,170 @@ +// 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 SignatureTests +{ + private static TargetTestHelpers.LayoutResult GetVASigCookieLayout(TargetTestHelpers helpers) + { + return helpers.LayoutFields( + [ + new(nameof(Data.VASigCookie.SizeOfArgs), DataType.uint32), + new(nameof(Data.VASigCookie.SignaturePointer), DataType.pointer), + new(nameof(Data.VASigCookie.SignatureLength), DataType.uint32), + ]); + } + + /// + /// Build a target with a single VASigCookie at a known address and a slot containing + /// a pointer to it (i.e., the "VASigCookieAddr" passed to the contract APIs). + /// + private static TestPlaceholderTarget BuildTarget( + MockTarget.Architecture arch, + string targetArchitecture, + uint sizeOfArgs, + ulong signaturePointer, + uint signatureLength, + out ulong vaSigCookieAddr, + out ulong vaSigCookiePtr, + bool nullCookie = false) + { + TargetTestHelpers helpers = new(arch); + var builder = new TestPlaceholderTarget.Builder(arch); + MockMemorySpace.Builder memBuilder = builder.MemoryBuilder; + MockMemorySpace.BumpAllocator allocator = memBuilder.CreateAllocator(0x1_0000, 0x2_0000); + + TargetTestHelpers.LayoutResult layout = GetVASigCookieLayout(helpers); + builder.AddTypes(new() + { + [DataType.VASigCookie] = new Target.TypeInfo() { Fields = layout.Fields, Size = layout.Stride }, + }); + + // Allocate and populate the VASigCookie struct. + MockMemorySpace.HeapFragment cookieFrag = allocator.Allocate(layout.Stride, "VASigCookie"); + helpers.Write(cookieFrag.Data.AsSpan(layout.Fields[nameof(Data.VASigCookie.SizeOfArgs)].Offset, sizeof(uint)), sizeOfArgs); + helpers.WritePointer(cookieFrag.Data.AsSpan(layout.Fields[nameof(Data.VASigCookie.SignaturePointer)].Offset, helpers.PointerSize), signaturePointer); + helpers.Write(cookieFrag.Data.AsSpan(layout.Fields[nameof(Data.VASigCookie.SignatureLength)].Offset, sizeof(uint)), signatureLength); + vaSigCookiePtr = cookieFrag.Address; + + // Allocate the slot that holds the pointer to the VASigCookie. This is the address + // passed to GetVarArgArgsBase / GetVarArgSignature. + MockMemorySpace.HeapFragment slotFrag = allocator.Allocate((ulong)helpers.PointerSize, "VASigCookieSlot"); + helpers.WritePointer(slotFrag.Data, nullCookie ? 0 : cookieFrag.Address); + vaSigCookieAddr = slotFrag.Address; + + // RuntimeInfo contract reads architecture from this global. + builder.AddGlobalStrings((Constants.Globals.Architecture, targetArchitecture)); + builder.AddContract(version: "c1"); + builder.AddContract(version: "c1"); + + return builder.Build(); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetVarArgSignature_ReturnsCookieSignature(MockTarget.Architecture arch) + { + const uint expectedSizeOfArgs = 0x40; + const ulong expectedSigPtr = 0x12_3400; + const uint expectedSigLen = 12; + + Target target = BuildTarget(arch, "x64", expectedSizeOfArgs, expectedSigPtr, expectedSigLen, + out ulong vaSigCookieAddr, out _); + ISignature signature = target.Contracts.Signature; + + signature.GetVarArgSignature(new TargetPointer(vaSigCookieAddr), out TargetPointer sigAddr, out uint sigLen); + + Assert.Equal(expectedSigPtr, sigAddr.Value); + Assert.Equal(expectedSigLen, sigLen); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetVarArgArgsBase_NonX86_ReturnsCookieAddrPlusPointerSize(MockTarget.Architecture arch) + { + Target target = BuildTarget(arch, "x64", sizeOfArgs: 0x80, signaturePointer: 0x1000, signatureLength: 4, + out ulong vaSigCookieAddr, out _); + ISignature signature = target.Contracts.Signature; + + TargetPointer argBase = signature.GetVarArgArgsBase(new TargetPointer(vaSigCookieAddr)); + + Assert.Equal(vaSigCookieAddr + (ulong)(arch.Is64Bit ? 8 : 4), argBase.Value); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetVarArgArgsBase_X86_ReturnsCookieAddrPlusSizeOfArgs(MockTarget.Architecture arch) + { + const uint sizeOfArgs = 0x18; + Target target = BuildTarget(arch, "x86", sizeOfArgs, signaturePointer: 0x1000, signatureLength: 4, + out ulong vaSigCookieAddr, out _); + ISignature signature = target.Contracts.Signature; + + TargetPointer argBase = signature.GetVarArgArgsBase(new TargetPointer(vaSigCookieAddr)); + + Assert.Equal(vaSigCookieAddr + sizeOfArgs, argBase.Value); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetVarArgSignature_NullCookieAddr_Throws(MockTarget.Architecture arch) + { + Target target = BuildTarget(arch, "x64", sizeOfArgs: 0, signaturePointer: 0, signatureLength: 0, + out _, out _); + ISignature signature = target.Contracts.Signature; + + Assert.Throws(() => signature.GetVarArgSignature(TargetPointer.Null, out _, out _)); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetVarArgArgsBase_NullCookieAddr_Throws(MockTarget.Architecture arch) + { + Target target = BuildTarget(arch, "x64", sizeOfArgs: 0, signaturePointer: 0, signatureLength: 0, + out _, out _); + ISignature signature = target.Contracts.Signature; + + Assert.Throws(() => signature.GetVarArgArgsBase(TargetPointer.Null)); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetVarArgSignature_NullCookiePointer_Throws(MockTarget.Architecture arch) + { + Target target = BuildTarget(arch, "x64", sizeOfArgs: 0, signaturePointer: 0, signatureLength: 0, + out ulong vaSigCookieAddr, out _, nullCookie: true); + ISignature signature = target.Contracts.Signature; + + Assert.Throws(() => signature.GetVarArgSignature(new TargetPointer(vaSigCookieAddr), out _, out _)); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetVarArgArgsBase_NullCookiePointer_Throws(MockTarget.Architecture arch) + { + Target target = BuildTarget(arch, "x64", sizeOfArgs: 0, signaturePointer: 0, signatureLength: 0, + out ulong vaSigCookieAddr, out _, nullCookie: true); + ISignature signature = target.Contracts.Signature; + + Assert.Throws(() => signature.GetVarArgArgsBase(new TargetPointer(vaSigCookieAddr))); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetVarArgSignature_ZeroLengthSignature_ReturnsZeroes(MockTarget.Architecture arch) + { + Target target = BuildTarget(arch, "x64", sizeOfArgs: 0x10, signaturePointer: 0, signatureLength: 0, + out ulong vaSigCookieAddr, out _); + ISignature signature = target.Contracts.Signature; + + signature.GetVarArgSignature(new TargetPointer(vaSigCookieAddr), out TargetPointer sigAddr, out uint sigLen); + + Assert.Equal(TargetPointer.Null, sigAddr); + Assert.Equal(0u, sigLen); + } +}