diff --git a/docs/design/datacontracts/Signature.md b/docs/design/datacontracts/Signature.md index e0eb96ab807a17..6d91152e913c1d 100644 --- a/docs/design/datacontracts/Signature.md +++ b/docs/design/datacontracts/Signature.md @@ -17,6 +17,12 @@ 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); ``` ## Version 1 @@ -24,7 +30,9 @@ TypeHandle DecodeFieldSignature(BlobHandle blobHandle, ModuleHandle moduleHandle 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 +45,7 @@ Contracts used: | RuntimeTypeSystem | | Loader | | EcmaMetadata | +| RuntimeInfo | Constants: | Constant Name | Meaning | Value | @@ -69,3 +78,33 @@ 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. + +```csharp +TargetPointer ISignature.GetVarArgArgsBase(TargetPointer vaSigCookieAddr) +{ + // On x86 the args are pushed below the cookie pointer (stack grows down on the args walk), + // so the first argument lies at vaSigCookieAddr + cookie.SizeOfArgs. + // On every other platform the first argument follows the cookie pointer in memory + // (stack grows up on the args walk), so its address is vaSigCookieAddr + sizeof(VASigCookie*). + if (RuntimeInfo.GetTargetArchitecture() == X86) + { + TargetPointer vaSigCookie = _target.ReadPointer(vaSigCookieAddr); + VASigCookie cookie = _target.ProcessedData.GetOrAdd(vaSigCookie); + 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 514e6a79d08be9..fb94ca4218e96f 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -417,6 +417,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.Contracts/Contracts/Signature/Signature_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Signature/Signature_1.cs index 8517cf674bccdb..1c07cf9f8a47f8 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,36 @@ TypeHandle ISignature.DecodeFieldSignature(BlobHandle blobHandle, ModuleHandle m RuntimeSignatureDecoder decoder = new(provider, _target, mdReader, ctx); return decoder.DecodeFieldSignature(ref blobReader); } + + TargetPointer ISignature.GetVarArgArgsBase(TargetPointer 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) + { + 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); + 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.Contracts/DataType.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs index 4cf582abcdd40d..5399a9b7edba72 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs @@ -181,6 +181,7 @@ public enum DataType ComInterfaceEntry, InternalComInterfaceDispatch, AuxiliarySymbolInfo, + VASigCookie, CodeRangeMapRangeList, /* GC Data Types */ 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 cbb6949bcaf247..3bbdcc28f90eb7 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 @@ -1198,7 +1198,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..622108b494a4c3 --- /dev/null +++ b/src/native/managed/cdac/tests/SignatureTests.cs @@ -0,0 +1,178 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using 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 Dictionary + { + [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; + + // Reading the cookie pointer from address 0 fails the underlying memory read. + Assert.Throws(() => signature.GetVarArgSignature(TargetPointer.Null, out _, out _)); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetVarArgArgsBase_X86_NullCookieAddr_Throws(MockTarget.Architecture arch) + { + // On x86, GetVarArgArgsBase must dereference the cookie slot to read sizeOfArgs, so a + // null address fails the underlying memory read. On non-x86 the implementation only + // performs arithmetic and never reads from the target, so no exception is thrown there. + Target target = BuildTarget(arch, "x86", 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_X86_NullCookiePointer_Throws(MockTarget.Architecture arch) + { + // On x86, GetVarArgArgsBase reads the cookie pointer and then loads the cookie's + // sizeOfArgs field; a null cookie pointer causes that field read to fail. On non-x86 + // the implementation never dereferences the cookie, so the case does not apply. + Target target = BuildTarget(arch, "x86", 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); + } +}