From 18602eae8b8495c66936af2e94094e0d2961501d Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Mon, 22 Apr 2024 14:57:14 -0700 Subject: [PATCH] [cdac] Read contract descriptor from target (#101208) - Get `DotNetRuntimeContractDescriptor` address from the target - Read contract descriptor to determine endianness and pointer size - Parse JSON descriptor and store contracts --- src/coreclr/debug/daccess/cdac.cpp | 20 +- src/coreclr/debug/daccess/cdac.h | 7 +- src/coreclr/debug/daccess/daccess.cpp | 10 +- .../src/ContractDescriptorParser.cs | 21 +- .../managed/cdacreader/src/Entrypoints.cs | 5 +- src/native/managed/cdacreader/src/Root.cs | 12 + src/native/managed/cdacreader/src/Target.cs | 208 ++++++++++++++++-- .../managed/cdacreader/src/cdacreader.csproj | 1 + .../tests/ContractDescriptorParserTests.cs | 2 + ...iagnostics.DataContractReader.Tests.csproj | 1 + 10 files changed, 241 insertions(+), 46 deletions(-) create mode 100644 src/native/managed/cdacreader/src/Root.cs diff --git a/src/coreclr/debug/daccess/cdac.cpp b/src/coreclr/debug/daccess/cdac.cpp index 23478592371a1..a4146709ec723 100644 --- a/src/coreclr/debug/daccess/cdac.cpp +++ b/src/coreclr/debug/daccess/cdac.cpp @@ -28,8 +28,12 @@ namespace int ReadFromTargetCallback(uint64_t addr, uint8_t* dest, uint32_t count, void* context) { - CDAC* cdac = reinterpret_cast(context); - return cdac->ReadFromTarget(addr, dest, count); + ICorDebugDataTarget* target = reinterpret_cast(context); + HRESULT hr = ReadFromDataTarget(target, addr, dest, count); + if (FAILED(hr)) + return hr; + + return S_OK; } } @@ -52,11 +56,12 @@ CDAC::CDAC(HMODULE module, uint64_t descriptorAddr, ICorDebugDataTarget* target) return; } + m_target->AddRef(); decltype(&cdac_reader_init) init = reinterpret_cast(::GetProcAddress(m_module, "cdac_reader_init")); decltype(&cdac_reader_get_sos_interface) getSosInterface = reinterpret_cast(::GetProcAddress(m_module, "cdac_reader_get_sos_interface")); _ASSERTE(init != nullptr && getSosInterface != nullptr); - init(descriptorAddr, &ReadFromTargetCallback, this, &m_cdac_handle); + init(descriptorAddr, &ReadFromTargetCallback, m_target, &m_cdac_handle); getSosInterface(m_cdac_handle, &m_sos); } @@ -77,12 +82,3 @@ IUnknown* CDAC::SosInterface() { return m_sos; } - -int CDAC::ReadFromTarget(uint64_t addr, uint8_t* dest, uint32_t count) -{ - HRESULT hr = ReadFromDataTarget(m_target, addr, dest, count); - if (FAILED(hr)) - return hr; - - return S_OK; -} diff --git a/src/coreclr/debug/daccess/cdac.h b/src/coreclr/debug/daccess/cdac.h index 54418dc549f1f..d5c0eecb0f737 100644 --- a/src/coreclr/debug/daccess/cdac.h +++ b/src/coreclr/debug/daccess/cdac.h @@ -21,7 +21,7 @@ class CDAC final CDAC(CDAC&& other) : m_module{ other.m_module } , m_cdac_handle{ other.m_cdac_handle } - , m_target{ other.m_target } + , m_target{ other.m_target.Extract() } , m_sos{ other.m_sos.Extract() } { other.m_module = NULL; @@ -34,7 +34,7 @@ class CDAC final { m_module = other.m_module; m_cdac_handle = other.m_cdac_handle; - m_target = other.m_target; + m_target = other.m_target.Extract(); m_sos = other.m_sos.Extract(); other.m_module = NULL; @@ -54,7 +54,6 @@ class CDAC final // This does not AddRef the returned interface IUnknown* SosInterface(); - int ReadFromTarget(uint64_t addr, uint8_t* dest, uint32_t count); private: CDAC(HMODULE module, uint64_t descriptorAddr, ICorDebugDataTarget* target); @@ -62,7 +61,7 @@ class CDAC final private: HMODULE m_module; intptr_t m_cdac_handle; - ICorDebugDataTarget* m_target; + NonVMComHolder m_target; NonVMComHolder m_sos; }; diff --git a/src/coreclr/debug/daccess/daccess.cpp b/src/coreclr/debug/daccess/daccess.cpp index 2f08750bc5c61..971b8b90980b3 100644 --- a/src/coreclr/debug/daccess/daccess.cpp +++ b/src/coreclr/debug/daccess/daccess.cpp @@ -30,6 +30,8 @@ #include #else extern "C" bool TryGetSymbol(ICorDebugDataTarget* dataTarget, uint64_t baseAddress, const char* symbolName, uint64_t* symbolAddress); +// cDAC depends on symbol lookup to find the contract descriptor +#define CAN_USE_CDAC #endif #include "dwbucketmanager.hpp" @@ -5493,15 +5495,16 @@ ClrDataAccess::Initialize(void) IfFailRet(GetDacGlobalValues()); IfFailRet(DacGetHostVtPtrs()); +// TODO: [cdac] TryGetSymbol is only implemented for Linux, OSX, and Windows. +#ifdef CAN_USE_CDAC CLRConfigNoCache enable = CLRConfigNoCache::Get("ENABLE_CDAC"); if (enable.IsSet()) { DWORD val; if (enable.TryAsInteger(10, val) && val == 1) { - // TODO: [cdac] Get contract descriptor from exported symbol uint64_t contractDescriptorAddr = 0; - //if (TryGetSymbol(m_pTarget, m_globalBase, "DotNetRuntimeContractDescriptor", &contractDescriptorAddr)) + if (TryGetSymbol(m_pTarget, m_globalBase, "DotNetRuntimeContractDescriptor", &contractDescriptorAddr)) { m_cdac = CDAC::Create(contractDescriptorAddr, m_pTarget); if (m_cdac.IsValid()) @@ -5514,6 +5517,7 @@ ClrDataAccess::Initialize(void) } } } +#endif // // DAC is now setup and ready to use @@ -6946,7 +6950,7 @@ GetDacTableAddress(ICorDebugDataTarget* dataTarget, ULONG64 baseAddress, PULONG6 return E_INVALIDARG; } #endif - // On MacOS, FreeBSD or NetBSD use the RVA include file + // On FreeBSD, NetBSD, or SunOS use the RVA include file *dacTableAddress = baseAddress + DAC_TABLE_RVA; #else // Otherwise, try to get the dac table address via the export symbol diff --git a/src/native/managed/cdacreader/src/ContractDescriptorParser.cs b/src/native/managed/cdacreader/src/ContractDescriptorParser.cs index fbf76cd4e8d43..a82235731e4a0 100644 --- a/src/native/managed/cdacreader/src/ContractDescriptorParser.cs +++ b/src/native/managed/cdacreader/src/ContractDescriptorParser.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Text.Json; using System.Text.Json.Serialization; @@ -23,13 +24,15 @@ public partial class ContractDescriptorParser /// /// Parses the "compact" representation of a contract descriptor. /// + // Workaround for https://github.com/dotnet/runtime/issues/101205 + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Root))] public static ContractDescriptor? ParseCompact(ReadOnlySpan json) { return JsonSerializer.Deserialize(json, ContractDescriptorContext.Default.ContractDescriptor); } [JsonSerializable(typeof(ContractDescriptor))] - [JsonSerializable(typeof(int))] + [JsonSerializable(typeof(int?))] [JsonSerializable(typeof(string))] [JsonSerializable(typeof(Dictionary))] [JsonSerializable(typeof(Dictionary))] @@ -38,11 +41,17 @@ public partial class ContractDescriptorParser [JsonSerializable(typeof(TypeDescriptor))] [JsonSerializable(typeof(FieldDescriptor))] [JsonSerializable(typeof(GlobalDescriptor))] + [JsonSerializable(typeof(Dictionary))] [JsonSourceGenerationOptions(AllowTrailingCommas = true, DictionaryKeyPolicy = JsonKnownNamingPolicy.Unspecified, // contracts, types and globals are case sensitive PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, NumberHandling = JsonNumberHandling.AllowReadingFromString, - ReadCommentHandling = JsonCommentHandling.Skip)] + ReadCommentHandling = JsonCommentHandling.Skip, + UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip, + UnknownTypeHandling = JsonUnknownTypeHandling.JsonElement, + Converters = [typeof(TypeDescriptorConverter), + typeof(FieldDescriptorConverter), + typeof(GlobalDescriptorConverter)])] internal sealed partial class ContractDescriptorContext : JsonSerializerContext { } @@ -58,7 +67,13 @@ public class ContractDescriptor public Dictionary? Globals { get; set; } [JsonExtensionData] - public Dictionary? Extras { get; set; } + public Dictionary? Extras { get; set; } + + public override string ToString() + { + return $"Version: {Version}, Baseline: {Baseline}, Contracts: {Contracts?.Count}, Types: {Types?.Count}, Globals: {Globals?.Count}"; + } + } [JsonConverter(typeof(TypeDescriptorConverter))] diff --git a/src/native/managed/cdacreader/src/Entrypoints.cs b/src/native/managed/cdacreader/src/Entrypoints.cs index 30cab9ec85188..908d534fe60c4 100644 --- a/src/native/managed/cdacreader/src/Entrypoints.cs +++ b/src/native/managed/cdacreader/src/Entrypoints.cs @@ -14,7 +14,10 @@ internal static class Entrypoints [UnmanagedCallersOnly(EntryPoint = $"{CDAC}init")] private static unsafe int Init(ulong descriptor, delegate* unmanaged readFromTarget, void* readContext, IntPtr* handle) { - Target target = new(descriptor, readFromTarget, readContext); + // TODO: [cdac] Better error code/details + if (!Target.TryCreate(descriptor, readFromTarget, readContext, out Target? target)) + return -1; + GCHandle gcHandle = GCHandle.Alloc(target); *handle = GCHandle.ToIntPtr(gcHandle); return 0; diff --git a/src/native/managed/cdacreader/src/Root.cs b/src/native/managed/cdacreader/src/Root.cs new file mode 100644 index 0000000000000..05aefea3dea2a --- /dev/null +++ b/src/native/managed/cdacreader/src/Root.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Serialization; + +namespace Microsoft.Diagnostics.DataContractReader; + +internal static class Root +{ + // https://github.com/dotnet/runtime/issues/101205 + public static JsonDerivedTypeAttribute[] R1 = new JsonDerivedTypeAttribute[] { null! }; +} diff --git a/src/native/managed/cdacreader/src/Target.cs b/src/native/managed/cdacreader/src/Target.cs index 898cab774bfb5..049a9f80ed226 100644 --- a/src/native/managed/cdacreader/src/Target.cs +++ b/src/native/managed/cdacreader/src/Target.cs @@ -3,6 +3,7 @@ using System; using System.Buffers.Binary; +using System.Collections.Generic; namespace Microsoft.Diagnostics.DataContractReader; @@ -16,49 +17,210 @@ public struct TargetPointer internal sealed unsafe class Target { - private readonly delegate* unmanaged _readFromTarget; - private readonly void* _readContext; + private const int StackAllocByteThreshold = 1024; - private bool _isLittleEndian; - private int _pointerSize; + private readonly struct Configuration + { + public bool IsLittleEndian { get; init; } + public int PointerSize { get; init; } + } + + private readonly Configuration _config; + private readonly Reader _reader; + + private readonly IReadOnlyDictionary _contracts = new Dictionary(); + private readonly TargetPointer[] _pointerData = []; + + public static bool TryCreate(ulong contractDescriptor, delegate* unmanaged readFromTarget, void* readContext, out Target? target) + { + Reader reader = new Reader(readFromTarget, readContext); + if (TryReadContractDescriptor(contractDescriptor, reader, out Configuration config, out ContractDescriptorParser.ContractDescriptor? descriptor, out TargetPointer[] pointerData)) + { + target = new Target(config, descriptor!, pointerData, reader); + return true; + } + + target = null; + return false; + } - public Target(ulong _, delegate* unmanaged readFromTarget, void* readContext) + private Target(Configuration config, ContractDescriptorParser.ContractDescriptor descriptor, TargetPointer[] pointerData, Reader reader) { - _readFromTarget = readFromTarget; - _readContext = readContext; + _config = config; + _reader = reader; + + // TODO: [cdac] Read globals and types + // note: we will probably want to store the globals and types into a more usable form + _contracts = descriptor.Contracts ?? []; - // TODO: [cdac] Populate from descriptor - _isLittleEndian = BitConverter.IsLittleEndian; - _pointerSize = IntPtr.Size; + _pointerData = pointerData; + } + + // See docs/design/datacontracts/contract-descriptor.md + private static bool TryReadContractDescriptor( + ulong address, + Reader reader, + out Configuration config, + out ContractDescriptorParser.ContractDescriptor? descriptor, + out TargetPointer[] pointerData) + { + config = default; + descriptor = null; + pointerData = []; + + // Magic - uint64_t + Span buffer = stackalloc byte[sizeof(ulong)]; + if (reader.ReadFromTarget(address, buffer) < 0) + return false; + + address += sizeof(ulong); + ReadOnlySpan magicLE = "DNCCDAC\0"u8; + ReadOnlySpan magicBE = "\0CADCCND"u8; + bool isLittleEndian = buffer.SequenceEqual(magicLE); + if (!isLittleEndian && !buffer.SequenceEqual(magicBE)) + return false; + + // Flags - uint32_t + if (!TryReadUInt32(address, isLittleEndian, reader, out uint flags)) + return false; + + address += sizeof(uint); + + // Bit 1 represents the pointer size. 0 = 64-bit, 1 = 32-bit. + int pointerSize = (int)(flags & 0x2) == 0 ? sizeof(ulong) : sizeof(uint); + + config = new Configuration { IsLittleEndian = isLittleEndian, PointerSize = pointerSize }; + + // Descriptor size - uint32_t + if (!TryReadUInt32(address, config.IsLittleEndian, reader, out uint descriptorSize)) + return false; + + address += sizeof(uint); + + // Descriptor - char* + if (!TryReadPointer(address, config, reader, out TargetPointer descriptorAddr)) + return false; + + address += (uint)pointerSize; + + // Pointer data count - uint32_t + if (!TryReadUInt32(address, config.IsLittleEndian, reader, out uint pointerDataCount)) + return false; + + address += sizeof(uint); + + // Padding - uint32_t + address += sizeof(uint); + + // Pointer data - uintptr_t* + if (!TryReadPointer(address, config, reader, out TargetPointer pointerDataAddr)) + return false; + + // Read descriptor + Span descriptorBuffer = descriptorSize <= StackAllocByteThreshold + ? stackalloc byte[(int)descriptorSize] + : new byte[(int)descriptorSize]; + if (reader.ReadFromTarget(descriptorAddr.Value, descriptorBuffer) < 0) + return false; + + descriptor = ContractDescriptorParser.ParseCompact(descriptorBuffer); + if (descriptor is null) + return false; + + // Read pointer data + pointerData = new TargetPointer[pointerDataCount]; + for (int i = 0; i < pointerDataCount; i++) + { + if (!TryReadPointer(pointerDataAddr.Value + (uint)(i * pointerSize), config, reader, out pointerData[i])) + return false; + } + + return true; + } + + public uint ReadUInt32(ulong address) + { + if (!TryReadUInt32(address, out uint value)) + throw new InvalidOperationException($"Failed to read uint32 at 0x{address:x8}."); + + return value; + } + + public bool TryReadUInt32(ulong address, out uint value) + => TryReadUInt32(address, _config.IsLittleEndian, _reader, out value); + + private static bool TryReadUInt32(ulong address, bool isLittleEndian, Reader reader, out uint value) + { + value = 0; + + Span buffer = stackalloc byte[sizeof(uint)]; + if (reader.ReadFromTarget(address, buffer) < 0) + return false; + + value = isLittleEndian + ? BinaryPrimitives.ReadUInt32LittleEndian(buffer) + : BinaryPrimitives.ReadUInt32BigEndian(buffer); + + return true; + } + + public TargetPointer ReadPointer(ulong address) + { + if (!TryReadPointer(address, out TargetPointer pointer)) + throw new InvalidOperationException($"Failed to read pointer at 0x{address:x8}."); + + return pointer; } public bool TryReadPointer(ulong address, out TargetPointer pointer) + => TryReadPointer(address, _config, _reader, out pointer); + + private static bool TryReadPointer(ulong address, Configuration config, Reader reader, out TargetPointer pointer) { pointer = TargetPointer.Null; - byte* buffer = stackalloc byte[_pointerSize]; - ReadOnlySpan span = new ReadOnlySpan(buffer, _pointerSize); - if (ReadFromTarget(address, buffer, (uint)_pointerSize) < 0) + Span buffer = stackalloc byte[config.PointerSize]; + if (reader.ReadFromTarget(address, buffer) < 0) return false; - if (_pointerSize == sizeof(uint)) + if (config.PointerSize == sizeof(uint)) { pointer = new TargetPointer( - _isLittleEndian - ? BinaryPrimitives.ReadUInt32LittleEndian(span) - : BinaryPrimitives.ReadUInt32BigEndian(span)); + config.IsLittleEndian + ? BinaryPrimitives.ReadUInt32LittleEndian(buffer) + : BinaryPrimitives.ReadUInt32BigEndian(buffer)); } - else if (_pointerSize == sizeof(ulong)) + else if (config.PointerSize == sizeof(ulong)) { pointer = new TargetPointer( - _isLittleEndian - ? BinaryPrimitives.ReadUInt64LittleEndian(span) - : BinaryPrimitives.ReadUInt64BigEndian(span)); + config.IsLittleEndian + ? BinaryPrimitives.ReadUInt64LittleEndian(buffer) + : BinaryPrimitives.ReadUInt64BigEndian(buffer)); } return true; } - private int ReadFromTarget(ulong address, byte* buffer, uint bytesToRead) - => _readFromTarget(address, buffer, bytesToRead, _readContext); + private sealed class Reader + { + private readonly delegate* unmanaged _readFromTarget; + private readonly void* _context; + + public Reader(delegate* unmanaged readFromTarget, void* context) + { + _readFromTarget = readFromTarget; + _context = context; + } + + public int ReadFromTarget(ulong address, Span buffer) + { + fixed (byte* bufferPtr = buffer) + { + return _readFromTarget(address, bufferPtr, (uint)buffer.Length, _context); + } + } + + public int ReadFromTarget(ulong address, byte* buffer, uint bytesToRead) + => _readFromTarget(address, buffer, bytesToRead, _context); + } } diff --git a/src/native/managed/cdacreader/src/cdacreader.csproj b/src/native/managed/cdacreader/src/cdacreader.csproj index 253bb3c6c27e0..20ecd197c7046 100644 --- a/src/native/managed/cdacreader/src/cdacreader.csproj +++ b/src/native/managed/cdacreader/src/cdacreader.csproj @@ -9,6 +9,7 @@ false true + false diff --git a/src/native/managed/cdacreader/tests/ContractDescriptorParserTests.cs b/src/native/managed/cdacreader/tests/ContractDescriptorParserTests.cs index eca740870727b..6f93b1225670e 100644 --- a/src/native/managed/cdacreader/tests/ContractDescriptorParserTests.cs +++ b/src/native/managed/cdacreader/tests/ContractDescriptorParserTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Text.Json; using System.Text.Unicode; using Xunit; @@ -12,6 +13,7 @@ public class ContractDescriptorParserTests [Fact] public void ParsesEmptyContract() { + Assert.False(JsonSerializer.IsReflectionEnabledByDefault); ReadOnlySpan json = "{}"u8; ContractDescriptorParser.ContractDescriptor descriptor = ContractDescriptorParser.ParseCompact(json); Assert.Null(descriptor.Version); diff --git a/src/native/managed/cdacreader/tests/Microsoft.Diagnostics.DataContractReader.Tests.csproj b/src/native/managed/cdacreader/tests/Microsoft.Diagnostics.DataContractReader.Tests.csproj index 22e8d256e01df..c12c45e6f1fe8 100644 --- a/src/native/managed/cdacreader/tests/Microsoft.Diagnostics.DataContractReader.Tests.csproj +++ b/src/native/managed/cdacreader/tests/Microsoft.Diagnostics.DataContractReader.Tests.csproj @@ -2,6 +2,7 @@ true $(NetCoreAppToolCurrent) + false