From eb540a47ed036ecaa4db396f903630305b4088ae Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Thu, 2 Jul 2026 17:02:56 -0400 Subject: [PATCH 01/13] [prototype] cdac-lite: native crash-dump memory enumeration Prototype of a small standalone native component (cdac-lite) that reproduces the DAC's crash-dump memory enumeration by reading the runtime's contract/data descriptors directly, without a version-matched mscordaccore. - src/coreclr/debug/cdaclite: exports CLRDataCreateInstance + ICLRDataEnumMemoryRegions; contract walks (gc, thread, loader, handles, jit, statics, syncblock, stresslog, interop) + conservative stack scan for the Normal tier; size-optimized build (~114 KB on windows-x64). - createdump integration (env-gated DOTNET_DbgUseCdacLite), Normal + Heap tiers. - cdac DumpTests: add Mini (MiniDumpNormal) tier + MiniDumpTests covering the clrstack/clrthreads scenarios supported by a normal minidump. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/debug/CMakeLists.txt | 3 + src/coreclr/debug/cdaclite/CMakeLists.txt | 93 +++ src/coreclr/debug/cdaclite/cdaclite.cpp | 154 ++++ src/coreclr/debug/cdaclite/cdaclite.h | 66 ++ src/coreclr/debug/cdaclite/cdaclite.src | 5 + .../debug/cdaclite/cdaclite_unixexports.src | 1 + src/coreclr/debug/cdaclite/cdaclitetest.cpp | 660 ++++++++++++++++++ .../debug/cdaclite/contracts/CMakeLists.txt | 18 + .../debug/cdaclite/contracts/contracts.h | 26 + src/coreclr/debug/cdaclite/contracts/gc.cpp | 444 ++++++++++++ src/coreclr/debug/cdaclite/contracts/gc.h | 32 + .../debug/cdaclite/contracts/handles.cpp | 148 ++++ .../debug/cdaclite/contracts/handles.h | 31 + .../debug/cdaclite/contracts/interop.cpp | 92 +++ .../debug/cdaclite/contracts/interop.h | 30 + src/coreclr/debug/cdaclite/contracts/jit.cpp | 83 +++ src/coreclr/debug/cdaclite/contracts/jit.h | 31 + .../debug/cdaclite/contracts/loader.cpp | 149 ++++ src/coreclr/debug/cdaclite/contracts/loader.h | 40 ++ .../debug/cdaclite/contracts/loaderheaps.cpp | 148 ++++ .../debug/cdaclite/contracts/loaderheaps.h | 33 + .../debug/cdaclite/contracts/stackscan.cpp | 369 ++++++++++ .../debug/cdaclite/contracts/stackscan.h | 33 + .../debug/cdaclite/contracts/statics.cpp | 114 +++ .../debug/cdaclite/contracts/statics.h | 37 + .../debug/cdaclite/contracts/stresslog.cpp | 116 +++ .../debug/cdaclite/contracts/stresslog.h | 31 + .../debug/cdaclite/contracts/syncblock.cpp | 91 +++ .../debug/cdaclite/contracts/syncblock.h | 31 + .../debug/cdaclite/contracts/thread.cpp | 73 ++ src/coreclr/debug/cdaclite/contracts/thread.h | 30 + .../debug/cdaclite/data/CMakeLists.txt | 9 + .../debug/cdaclite/data/datadescriptor.cpp | 464 ++++++++++++ .../debug/cdaclite/data/datadescriptor.h | 124 ++++ src/coreclr/debug/cdaclite/data/datatype.h | 168 +++++ .../debug/cdaclite/data/runtimetypes.h | 370 ++++++++++ src/coreclr/debug/cdaclite/data/target.cpp | 190 +++++ src/coreclr/debug/cdaclite/data/target.h | 164 +++++ src/coreclr/debug/cdaclite/datatarget.cpp | 20 + src/coreclr/debug/cdaclite/datatarget.h | 92 +++ src/coreclr/debug/cdaclite/enumerate.cpp | 227 ++++++ .../debug/cdaclite/json/CMakeLists.txt | 5 + src/coreclr/debug/cdaclite/json/json.cpp | 441 ++++++++++++ src/coreclr/debug/cdaclite/json/json.h | 71 ++ .../debug/createdump/createdumpwindows.cpp | 359 +++++++++- .../ContractDescriptorTarget.cs | 31 +- .../managed/cdac/scripts/DumpHelpers.cs | 3 +- .../managed/cdac/scripts/StacksCommand.cs | 2 +- .../managed/cdac/scripts/ThreadsCommand.cs | 2 +- .../DumpTests/Debuggees/Directory.Build.props | 2 +- .../Debuggees/StackWalk/StackWalk.csproj | 2 +- .../cdac/tests/DumpTests/DumpTests.targets | 16 +- .../cdac/tests/DumpTests/MiniDumpTests.cs | 206 ++++++ .../managed/cdac/tests/DumpTests/README.md | 27 +- .../tests/TestInfrastructure/DumpTestBase.cs | 4 +- 55 files changed, 6176 insertions(+), 35 deletions(-) create mode 100644 src/coreclr/debug/cdaclite/CMakeLists.txt create mode 100644 src/coreclr/debug/cdaclite/cdaclite.cpp create mode 100644 src/coreclr/debug/cdaclite/cdaclite.h create mode 100644 src/coreclr/debug/cdaclite/cdaclite.src create mode 100644 src/coreclr/debug/cdaclite/cdaclite_unixexports.src create mode 100644 src/coreclr/debug/cdaclite/cdaclitetest.cpp create mode 100644 src/coreclr/debug/cdaclite/contracts/CMakeLists.txt create mode 100644 src/coreclr/debug/cdaclite/contracts/contracts.h create mode 100644 src/coreclr/debug/cdaclite/contracts/gc.cpp create mode 100644 src/coreclr/debug/cdaclite/contracts/gc.h create mode 100644 src/coreclr/debug/cdaclite/contracts/handles.cpp create mode 100644 src/coreclr/debug/cdaclite/contracts/handles.h create mode 100644 src/coreclr/debug/cdaclite/contracts/interop.cpp create mode 100644 src/coreclr/debug/cdaclite/contracts/interop.h create mode 100644 src/coreclr/debug/cdaclite/contracts/jit.cpp create mode 100644 src/coreclr/debug/cdaclite/contracts/jit.h create mode 100644 src/coreclr/debug/cdaclite/contracts/loader.cpp create mode 100644 src/coreclr/debug/cdaclite/contracts/loader.h create mode 100644 src/coreclr/debug/cdaclite/contracts/loaderheaps.cpp create mode 100644 src/coreclr/debug/cdaclite/contracts/loaderheaps.h create mode 100644 src/coreclr/debug/cdaclite/contracts/stackscan.cpp create mode 100644 src/coreclr/debug/cdaclite/contracts/stackscan.h create mode 100644 src/coreclr/debug/cdaclite/contracts/statics.cpp create mode 100644 src/coreclr/debug/cdaclite/contracts/statics.h create mode 100644 src/coreclr/debug/cdaclite/contracts/stresslog.cpp create mode 100644 src/coreclr/debug/cdaclite/contracts/stresslog.h create mode 100644 src/coreclr/debug/cdaclite/contracts/syncblock.cpp create mode 100644 src/coreclr/debug/cdaclite/contracts/syncblock.h create mode 100644 src/coreclr/debug/cdaclite/contracts/thread.cpp create mode 100644 src/coreclr/debug/cdaclite/contracts/thread.h create mode 100644 src/coreclr/debug/cdaclite/data/CMakeLists.txt create mode 100644 src/coreclr/debug/cdaclite/data/datadescriptor.cpp create mode 100644 src/coreclr/debug/cdaclite/data/datadescriptor.h create mode 100644 src/coreclr/debug/cdaclite/data/datatype.h create mode 100644 src/coreclr/debug/cdaclite/data/runtimetypes.h create mode 100644 src/coreclr/debug/cdaclite/data/target.cpp create mode 100644 src/coreclr/debug/cdaclite/data/target.h create mode 100644 src/coreclr/debug/cdaclite/datatarget.cpp create mode 100644 src/coreclr/debug/cdaclite/datatarget.h create mode 100644 src/coreclr/debug/cdaclite/enumerate.cpp create mode 100644 src/coreclr/debug/cdaclite/json/CMakeLists.txt create mode 100644 src/coreclr/debug/cdaclite/json/json.cpp create mode 100644 src/coreclr/debug/cdaclite/json/json.h create mode 100644 src/native/managed/cdac/tests/DumpTests/MiniDumpTests.cs diff --git a/src/coreclr/debug/CMakeLists.txt b/src/coreclr/debug/CMakeLists.txt index 4771c8bfe34022..0f3f0f98be1a68 100644 --- a/src/coreclr/debug/CMakeLists.txt +++ b/src/coreclr/debug/CMakeLists.txt @@ -13,6 +13,9 @@ endif() if(CLR_CMAKE_HOST_WIN32) add_subdirectory(createdump) endif(CLR_CMAKE_HOST_WIN32) +if(NOT CLR_CROSS_COMPONENTS_BUILD) + add_subdirectory(cdaclite) +endif(NOT CLR_CROSS_COMPONENTS_BUILD) if(FEATURE_SINGLE_FILE_DIAGNOSTICS) add_subdirectory(runtimeinfo) endif(FEATURE_SINGLE_FILE_DIAGNOSTICS) diff --git a/src/coreclr/debug/cdaclite/CMakeLists.txt b/src/coreclr/debug/cdaclite/CMakeLists.txt new file mode 100644 index 00000000000000..64d1f153cffcab --- /dev/null +++ b/src/coreclr/debug/cdaclite/CMakeLists.txt @@ -0,0 +1,93 @@ +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +include_directories(BEFORE ${VM_DIR}) +include_directories(${CLR_DIR}/debug/datadescriptor-shared/inc) + +add_subdirectory(json) +add_subdirectory(data) +add_subdirectory(contracts) + +set(CDACLITE_SOURCES + cdaclite.cpp + datatarget.cpp + enumerate.cpp +) + +if(CLR_CMAKE_HOST_WIN32) + set(DEF_SOURCES cdaclite.src) + set(CURRENT_BINARY_DIR_FOR_CONFIG ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}) + + # Preprocess exports definition file + preprocess_file(${CMAKE_CURRENT_SOURCE_DIR}/${DEF_SOURCES} ${CURRENT_BINARY_DIR_FOR_CONFIG}/cdaclite.def) + add_custom_target(cdaclite_def DEPENDS ${CURRENT_BINARY_DIR_FOR_CONFIG}/cdaclite.def) +else(CLR_CMAKE_HOST_WIN32) + set(DEF_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/cdaclite_unixexports.src) + set(EXPORTS_FILE ${CMAKE_CURRENT_BINARY_DIR}/cdaclite.exports) + generate_exports_file(${DEF_SOURCES} ${EXPORTS_FILE}) + add_custom_target(cdaclite_exports DEPENDS ${EXPORTS_FILE}) + set_exports_linker_option(${EXPORTS_FILE}) +endif(CLR_CMAKE_HOST_WIN32) + +add_library_clr(cdaclite SHARED ${CDACLITE_SOURCES}) + +if(CLR_CMAKE_HOST_WIN32) + add_dependencies(cdaclite cdaclite_def) + set_property(TARGET cdaclite APPEND_STRING PROPERTY LINK_FLAGS " /DEF:\"${CURRENT_BINARY_DIR_FOR_CONFIG}/cdaclite.def\"") +else(CLR_CMAKE_HOST_WIN32) + add_dependencies(cdaclite cdaclite_exports) + set_property(TARGET cdaclite APPEND_STRING PROPERTY LINK_FLAGS ${EXPORTS_LINKER_OPTION}) + set_property(TARGET cdaclite APPEND_STRING PROPERTY LINK_DEPENDS ${EXPORTS_FILE}) +endif(CLR_CMAKE_HOST_WIN32) + +set(CDACLITE_LIBRARIES + cdacgc + cdacdata + corguids + dbgutil + coreclrminipal +) + +if(CLR_CMAKE_HOST_WIN32) + list(APPEND CDACLITE_LIBRARIES + kernel32.lib + advapi32.lib + ${STATIC_MT_CRT_LIB} + ) +else(CLR_CMAKE_HOST_WIN32) + list(APPEND CDACLITE_LIBRARIES + coreclrpal + ) +endif(CLR_CMAKE_HOST_WIN32) + +target_link_libraries(cdaclite PRIVATE ${CDACLITE_LIBRARIES}) + +# Optimize cdac-lite for binary size: the whole point of cdac-lite is to be a small, +# standalone native package. Favor size over speed for the cdac-lite-specific targets +# (the shared coreclr static libs it links keep their default optimization). +if(CLR_CMAKE_HOST_WIN32) + set(CDACLITE_SIZE_COMPILE_OPTS /O1 /Gw /Gy) + set(CDACLITE_SIZE_LINK_OPTS "/OPT:REF /OPT:ICF") +else() + set(CDACLITE_SIZE_COMPILE_OPTS -Os -ffunction-sections -fdata-sections) + set(CDACLITE_SIZE_LINK_OPTS "-Wl,--gc-sections") +endif() + +foreach(_cdaclite_target cdaclite cdacgc cdacdata cdacjson) + target_compile_options(${_cdaclite_target} PRIVATE ${CDACLITE_SIZE_COMPILE_OPTS}) +endforeach() +set_property(TARGET cdaclite APPEND_STRING PROPERTY LINK_FLAGS " ${CDACLITE_SIZE_LINK_OPTS}") + +install_clr(TARGETS cdaclite DESTINATIONS . sharedFramework COMPONENT debug) + +# Standalone test harness (Windows-only): drives cdaclite against a live process. +if(CLR_CMAKE_HOST_WIN32) + add_executable_clr(cdaclitetest cdaclitetest.cpp) + target_link_libraries(cdaclitetest + PRIVATE + corguids + kernel32.lib + advapi32.lib + dbghelp.lib + ${STATIC_MT_CRT_LIB} + ) +endif(CLR_CMAKE_HOST_WIN32) diff --git a/src/coreclr/debug/cdaclite/cdaclite.cpp b/src/coreclr/debug/cdaclite/cdaclite.cpp new file mode 100644 index 00000000000000..cb68d1f0e14f6c --- /dev/null +++ b/src/coreclr/debug/cdaclite/cdaclite.cpp @@ -0,0 +1,154 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +//***************************************************************************** +// cdaclite.cpp +// +// Creation: the classic DAC factory entry point (CLRDataCreateInstance) and the +// CDacLite COM object lifetime (construction, IUnknown, logging). The data-target +// source lives in datatarget.{h,cpp} and the memory enumeration in enumerate.cpp. +//***************************************************************************** + +#include "cdaclite.h" +#include "datatarget.h" + +#include +#include "corerror.h" +#include +#include +#include + +#include +#include + +// Implemented per-platform in dbgutil (dbgutil.cpp / elfreader.cpp / machoreader.cpp). +// Resolves an exported symbol in the target image using only the data target. +extern "C" bool TryGetSymbol(ICorDebugDataTarget* dataTarget, uint64_t baseAddress, const char* symbolName, uint64_t* symbolAddress); + +namespace cdac +{ + CDacLite::CDacLite(ICLRDataTarget* target, uint64_t contractDescriptorAddr) + : m_ref(1), m_target(target), m_contractDescriptorAddr(contractDescriptorAddr) + { + m_target->AddRef(); + } + + CDacLite::~CDacLite() + { + m_target->Release(); + } + + HRESULT STDMETHODCALLTYPE CDacLite::QueryInterface(REFIID riid, void** ppvObject) + { + if (ppvObject == nullptr) + { + return E_POINTER; + } + if (riid == IID_IUnknown || riid == __uuidof(ICLRDataEnumMemoryRegions)) + { + *ppvObject = static_cast(this); + AddRef(); + return S_OK; + } + *ppvObject = nullptr; + return E_NOINTERFACE; + } + + ULONG STDMETHODCALLTYPE CDacLite::AddRef() + { + return InterlockedIncrement(&m_ref); + } + + ULONG STDMETHODCALLTYPE CDacLite::Release() + { + LONG ref = InterlockedDecrement(&m_ref); + if (ref == 0) + { + delete this; + } + return ref; + } + + void CDacLite::Log(ICLRDataEnumMemoryRegionsCallback* callback, const char* format, ...) + { + char buffer[1024]; + va_list args; + va_start(args, format); + vsnprintf(buffer, sizeof(buffer), format, args); + va_end(args); + buffer[sizeof(buffer) - 1] = '\0'; + + ICLRDataLoggingCallback* logger = nullptr; + if (callback != nullptr && + SUCCEEDED(callback->QueryInterface(__uuidof(ICLRDataLoggingCallback), (void**)&logger)) && + logger != nullptr) + { + logger->LogMessage(buffer); + logger->Release(); + } + else + { + fprintf(stderr, "cdaclite: %s\n", buffer); +#ifdef HOST_WINDOWS + OutputDebugStringA("cdaclite: "); + OutputDebugStringA(buffer); + OutputDebugStringA("\n"); +#endif + } + } +} + +// +// The classic DAC factory entry point. dbghelp (Windows) and createdump +// (Unix/macOS) call this to obtain an ICLRDataEnumMemoryRegions for a target. +// +STDAPI CLRDataCreateInstance(REFIID iid, ICLRDataTarget* pLegacyTarget, void** iface) +{ + if (pLegacyTarget == nullptr || iface == nullptr) + { + return E_INVALIDARG; + } + + *iface = nullptr; + + // Determine the runtime module base, preferring ICLRRuntimeLocator and + // falling back to the well-known CLR module name. + CLRDATA_ADDRESS base = 0; + ICLRRuntimeLocator* locator = nullptr; + if (SUCCEEDED(pLegacyTarget->QueryInterface(__uuidof(ICLRRuntimeLocator), (void**)&locator)) && + locator != nullptr && + locator->GetRuntimeBase(&base) == S_OK) + { + locator->Release(); + } + else + { + if (locator != nullptr) + { + locator->Release(); + } + HRESULT hr = pLegacyTarget->GetImageBase(TARGET_MAIN_CLR_DLL_NAME_W, &base); + if (FAILED(hr)) + { + return hr; + } + } + + // Locate the exported contract descriptor in the target. + cdac::DataTargetAdapter adapter(pLegacyTarget); + uint64_t contractDescriptorAddr = 0; + if (!TryGetSymbol(&adapter, (uint64_t)base, "DotNetRuntimeContractDescriptor", &contractDescriptorAddr) || + contractDescriptorAddr == 0) + { + return E_FAIL; + } + + cdac::CDacLite* instance = new (std::nothrow) cdac::CDacLite(pLegacyTarget, contractDescriptorAddr); + if (instance == nullptr) + { + return E_OUTOFMEMORY; + } + + HRESULT hr = instance->QueryInterface(iid, iface); + instance->Release(); + return hr; +} diff --git a/src/coreclr/debug/cdaclite/cdaclite.h b/src/coreclr/debug/cdaclite/cdaclite.h new file mode 100644 index 00000000000000..a2a0323d297ab0 --- /dev/null +++ b/src/coreclr/debug/cdaclite/cdaclite.h @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +//***************************************************************************** +// cdaclite.h +// +// cdac-lite: a minimal data access component implementing just enough of the +// classic DAC entry point (CLRDataCreateInstance + ICLRDataEnumMemoryRegions) +// to drive crash-dump memory enumeration without a version-matched mscordaccore. +// +// The implementation is split by concern: +// * datatarget.{h,cpp} -- the data-target source (memory reads / symbol lookup) +// * cdaclite.cpp -- creation (CLRDataCreateInstance) + COM object lifetime +// * enumerate.cpp -- the memory enumeration (EnumMemoryRegions + contracts) +//***************************************************************************** + +#ifndef CDACLITE_CDACLITE_H +#define CDACLITE_CDACLITE_H + +#include +#include +#include +#include + +namespace cdac +{ + // Minimal ICLRDataEnumMemoryRegions implementation backed by the runtime's + // contract descriptor. Created by CLRDataCreateInstance (cdaclite.cpp); the + // enumeration itself lives in enumerate.cpp. + class CDacLite : public ICLRDataEnumMemoryRegions + { + public: + CDacLite(ICLRDataTarget* target, uint64_t contractDescriptorAddr); + + // IUnknown + STDMETHOD(QueryInterface)(REFIID riid, void** ppvObject) override; + STDMETHOD_(ULONG, AddRef)() override; + STDMETHOD_(ULONG, Release)() override; + + // ICLRDataEnumMemoryRegions (implemented in enumerate.cpp) + STDMETHOD(EnumMemoryRegions)(ICLRDataEnumMemoryRegionsCallback* callback, ULONG32 miniDumpFlags, CLRDataEnumMemoryFlags clrFlags) override; + + private: + virtual ~CDacLite(); + + // Logs to the callback's ICLRDataLoggingCallback if present, else to stderr. + void Log(ICLRDataEnumMemoryRegionsCallback* callback, const char* format, ...); + + // State shared with the region sinks while an enumeration is in progress. + struct RegionSinkState + { + CDacLite* owner; + ICLRDataEnumMemoryRegionsCallback* callback; + uint32_t count; + }; + + // Region sink (contract-reported regions) + EnumMem sink (implicitly-read structs). + static void RegionSinkThunk(void* context, const char* kind, uint64_t start, uint64_t size); + static void EnumMemThunk(void* context, uint64_t address, uint32_t size); + + LONG m_ref; + ICLRDataTarget* m_target; + uint64_t m_contractDescriptorAddr; + }; +} + +#endif // CDACLITE_CDACLITE_H diff --git a/src/coreclr/debug/cdaclite/cdaclite.src b/src/coreclr/debug/cdaclite/cdaclite.src new file mode 100644 index 00000000000000..57f4ebb0430761 --- /dev/null +++ b/src/coreclr/debug/cdaclite/cdaclite.src @@ -0,0 +1,5 @@ +; Licensed to the .NET Foundation under one or more agreements. +; The .NET Foundation licenses this file to you under the MIT license. + +EXPORTS + CLRDataCreateInstance diff --git a/src/coreclr/debug/cdaclite/cdaclite_unixexports.src b/src/coreclr/debug/cdaclite/cdaclite_unixexports.src new file mode 100644 index 00000000000000..cc752f57ce71f3 --- /dev/null +++ b/src/coreclr/debug/cdaclite/cdaclite_unixexports.src @@ -0,0 +1 @@ +CLRDataCreateInstance diff --git a/src/coreclr/debug/cdaclite/cdaclitetest.cpp b/src/coreclr/debug/cdaclite/cdaclitetest.cpp new file mode 100644 index 00000000000000..a27dc9f2575cd7 --- /dev/null +++ b/src/coreclr/debug/cdaclite/cdaclitetest.cpp @@ -0,0 +1,660 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +//***************************************************************************** +// cdaclitetest.cpp +// +// A tiny standalone harness to exercise cdaclite against a live .NET process +// on Windows. It implements a minimal ICLRDataTarget backed by +// ReadProcessMemory and module enumeration, loads cdaclite.dll, calls +// CLRDataCreateInstance, and invokes EnumMemoryRegions. Memory regions and +// log messages are printed to stdout. +// +// Usage: cdaclitetest [path-to-cdaclite.dll] +//***************************************************************************** + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +// sospriv.h references T_CONTEXT (target CONTEXT); for a same-arch harness it's the OS CONTEXT. +#define T_CONTEXT CONTEXT +#include +#include + +typedef HRESULT(STDAPICALLTYPE* PFN_CLRDataCreateInstance)(REFIID iid, ICLRDataTarget* target, void** iface); + +struct Region { uint64_t base; uint32_t size; }; + +namespace +{ + // Minimal ICLRDataTarget over a live process handle. + class LiveDataTarget : public ICLRDataTarget + { + private: + LONG m_ref; + HANDLE m_process; + DWORD m_pid; + + public: + LiveDataTarget(HANDLE process, DWORD pid) + : m_ref(1), m_process(process), m_pid(pid) + { + } + + // IUnknown + STDMETHOD(QueryInterface)(REFIID riid, void** ppvObject) + { + if (ppvObject == nullptr) + { + return E_POINTER; + } + if (riid == IID_IUnknown || riid == __uuidof(ICLRDataTarget)) + { + *ppvObject = static_cast(this); + AddRef(); + return S_OK; + } + *ppvObject = nullptr; + return E_NOINTERFACE; + } + + STDMETHOD_(ULONG, AddRef)() { return InterlockedIncrement(&m_ref); } + STDMETHOD_(ULONG, Release)() + { + LONG ref = InterlockedDecrement(&m_ref); + if (ref == 0) + { + delete this; + } + return ref; + } + + // ICLRDataTarget + STDMETHOD(GetMachineType)(ULONG32* machine) + { +#if defined(_M_ARM64) + *machine = IMAGE_FILE_MACHINE_ARM64; +#else + *machine = IMAGE_FILE_MACHINE_AMD64; +#endif + return S_OK; + } + + STDMETHOD(GetPointerSize)(ULONG32* size) + { + *size = sizeof(void*); + return S_OK; + } + + STDMETHOD(GetImageBase)(LPCWSTR moduleName, CLRDATA_ADDRESS* baseAddress) + { + HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, m_pid); + if (snapshot == INVALID_HANDLE_VALUE) + { + return E_FAIL; + } + + HRESULT hr = E_FAIL; + MODULEENTRY32W me; + me.dwSize = sizeof(me); + if (Module32FirstW(snapshot, &me)) + { + do + { + if (_wcsicmp(me.szModule, moduleName) == 0) + { + *baseAddress = (CLRDATA_ADDRESS)(ULONG_PTR)me.modBaseAddr; + hr = S_OK; + break; + } + } while (Module32NextW(snapshot, &me)); + } + CloseHandle(snapshot); + return hr; + } + + STDMETHOD(ReadVirtual)(CLRDATA_ADDRESS address, PBYTE buffer, ULONG32 size, ULONG32* done) + { + SIZE_T read = 0; + if (!ReadProcessMemory(m_process, (LPCVOID)(ULONG_PTR)address, buffer, size, &read)) + { + if (done != nullptr) + { + *done = 0; + } + return HRESULT_FROM_WIN32(GetLastError()); + } + if (done != nullptr) + { + *done = (ULONG32)read; + } + return S_OK; + } + + STDMETHOD(WriteVirtual)(CLRDATA_ADDRESS, PBYTE, ULONG32, ULONG32*) { return E_NOTIMPL; } + STDMETHOD(GetTLSValue)(ULONG32, ULONG32, CLRDATA_ADDRESS*) { return E_NOTIMPL; } + STDMETHOD(SetTLSValue)(ULONG32, ULONG32, CLRDATA_ADDRESS) { return E_NOTIMPL; } + STDMETHOD(GetCurrentThreadID)(ULONG32*) { return E_NOTIMPL; } + STDMETHOD(GetThreadContext)(ULONG32, ULONG32, ULONG32, PBYTE) { return E_NOTIMPL; } + STDMETHOD(SetThreadContext)(ULONG32, ULONG32, PBYTE) { return E_NOTIMPL; } + STDMETHOD(Request)(ULONG32, ULONG32, BYTE*, ULONG32, BYTE*) { return E_NOTIMPL; } + }; + + // Callback that prints enumerated regions and log messages. + class PrintCallback : public ICLRDataEnumMemoryRegionsCallback, public ICLRDataLoggingCallback + { + private: + LONG m_ref; + + public: + ULONG m_regionCount = 0; + + PrintCallback() : m_ref(1) {} + + STDMETHOD(QueryInterface)(REFIID riid, void** ppvObject) + { + if (ppvObject == nullptr) + { + return E_POINTER; + } + if (riid == IID_IUnknown || riid == __uuidof(ICLRDataEnumMemoryRegionsCallback)) + { + *ppvObject = static_cast(this); + AddRef(); + return S_OK; + } + if (riid == __uuidof(ICLRDataLoggingCallback)) + { + *ppvObject = static_cast(this); + AddRef(); + return S_OK; + } + *ppvObject = nullptr; + return E_NOINTERFACE; + } + + STDMETHOD_(ULONG, AddRef)() { return InterlockedIncrement(&m_ref); } + STDMETHOD_(ULONG, Release)() { return InterlockedDecrement(&m_ref); } + + // ICLRDataEnumMemoryRegionsCallback + STDMETHOD(EnumMemoryRegion)(CLRDATA_ADDRESS address, ULONG32 size) + { + m_regionCount++; + printf("SEG 0x%llx 0x%llx\n", (unsigned long long)address, (unsigned long long)(address + size)); + return S_OK; + } + + // ICLRDataLoggingCallback + STDMETHOD(LogMessage)(LPCSTR message) + { + printf("[log] %s\n", message); + return S_OK; + } + }; + + // Callback that collects enumerated regions into a vector (for dump writing). + class CollectCallback : public ICLRDataEnumMemoryRegionsCallback + { + private: + LONG m_ref; + public: + std::vector m_regions; + CollectCallback() : m_ref(1) {} + + STDMETHOD(QueryInterface)(REFIID riid, void** ppvObject) + { + if (ppvObject == nullptr) { return E_POINTER; } + if (riid == IID_IUnknown || riid == __uuidof(ICLRDataEnumMemoryRegionsCallback)) + { + *ppvObject = static_cast(this); + AddRef(); + return S_OK; + } + *ppvObject = nullptr; + return E_NOINTERFACE; + } + STDMETHOD_(ULONG, AddRef)() { return InterlockedIncrement(&m_ref); } + STDMETHOD_(ULONG, Release)() { return InterlockedDecrement(&m_ref); } + + STDMETHOD(EnumMemoryRegion)(CLRDATA_ADDRESS address, ULONG32 size) + { + Region r; r.base = (uint64_t)address; r.size = size; + m_regions.push_back(r); + return S_OK; + } + }; + + // Feeds cdac-lite regions to MiniDumpWriteDump via the MemoryCallback protocol. + struct DumpCallbackState + { + const std::vector* regions; + size_t index; + }; + + BOOL CALLBACK DumpMemoryCallback(PVOID param, const PMINIDUMP_CALLBACK_INPUT input, PMINIDUMP_CALLBACK_OUTPUT output) + { + DumpCallbackState* state = (DumpCallbackState*)param; + switch (input->CallbackType) + { + case MemoryCallback: + // Supply one cdac-lite region per call; MemorySize == 0 ends enumeration. + if (state->index < state->regions->size()) + { + const Region& r = (*state->regions)[state->index++]; + output->MemoryBase = r.base; + output->MemorySize = r.size; + } + else + { + output->MemoryBase = 0; + output->MemorySize = 0; + } + return TRUE; + default: + return TRUE; + } + } +} + +// Oracle: drive the real DAC via ISOSDacInterface to list GC segment [mem, highAllocMark) +// ranges. Prints "SEG start end" lines matching cdac-lite's output for diffing. +static int RunDacOracle(PFN_CLRDataCreateInstance pfnCreate, LiveDataTarget* target, DWORD pid) +{ + ISOSDacInterface* sos = nullptr; + HRESULT hr = pfnCreate(__uuidof(ISOSDacInterface), target, (void**)&sos); + if (FAILED(hr) || sos == nullptr) + { + fwprintf(stderr, L"DAC: QI ISOSDacInterface failed: 0x%08x\n", hr); + return 5; + } + + DacpGcHeapData heapData; + if (FAILED(hr = heapData.Request(sos))) + { + fwprintf(stderr, L"DAC: GetGCHeapData failed: 0x%08x\n", hr); + sos->Release(); + return 6; + } + + printf("cdaclitetest[dac]: pid %lu server=%d heaps=%u maxGen=%u\n", + pid, heapData.bServerMode, heapData.HeapCount, heapData.g_max_generation); + + // Collect the per-heap details (WKS: one static heap; SVR: iterate heap list). + DacpGcHeapDetails details[64]; + unsigned heapCount = 1; + if (heapData.bServerMode) + { + heapCount = heapData.HeapCount <= 64 ? heapData.HeapCount : 64; + CLRDATA_ADDRESS heaps[64] = {}; + unsigned needed = 0; + if (FAILED(hr = sos->GetGCHeapList(heapCount, heaps, &needed))) + { + fwprintf(stderr, L"DAC: GetGCHeapList failed: 0x%08x\n", hr); + sos->Release(); + return 7; + } + for (unsigned i = 0; i < heapCount; i++) + { + details[i] = DacpGcHeapDetails(); + details[i].Request(sos, heaps[i]); + } + } + else + { + details[0] = DacpGcHeapDetails(); + details[0].Request(sos); + } + + int count = 0; + for (unsigned h = 0; h < heapCount; h++) + { + const DacpGcHeapDetails& heap = details[h]; + // Walk each generation's segment list (gen0..maxGen+2 covers SOH + LOH + POH). + for (int g = 0; g < DAC_NUMBERGENERATIONS; g++) + { + CLRDATA_ADDRESS segAddr = heap.generation_table[g].start_segment; + for (int i = 0; segAddr != 0 && i < 1000000; i++) + { + DacpHeapSegmentData seg; + if (FAILED(seg.Request(sos, segAddr, heap))) + { + break; + } + if (seg.mem != 0 && seg.highAllocMark > seg.mem) + { + printf("SEG 0x%llx 0x%llx\n", (unsigned long long)seg.mem, (unsigned long long)seg.highAllocMark); + count++; + } + segAddr = seg.next; + } + } + } + + printf("cdaclitetest[dac]: %d segment(s)\n", count); + + // Enumerate modules: AppDomainList -> AssemblyList -> ModuleList -> ilBase. + unsigned adNeeded = 0; + CLRDATA_ADDRESS appDomains[64] = {}; + int modCount = 0; + if (SUCCEEDED(sos->GetAppDomainList(64, appDomains, &adNeeded))) + { + unsigned adCount = adNeeded < 64 ? adNeeded : 64; + for (unsigned a = 0; a < adCount; a++) + { + int asmNeeded = 0; + if (FAILED(sos->GetAssemblyList(appDomains[a], 0, nullptr, &asmNeeded)) || asmNeeded <= 0) + { + continue; + } + if (asmNeeded > 4096) { asmNeeded = 4096; } + CLRDATA_ADDRESS* assemblies = new CLRDATA_ADDRESS[asmNeeded]; + int got = 0; + if (SUCCEEDED(sos->GetAssemblyList(appDomains[a], asmNeeded, assemblies, &got))) + { + for (int s = 0; s < got; s++) + { + unsigned modNeeded = 0; + if (FAILED(sos->GetAssemblyModuleList(assemblies[s], 0, nullptr, &modNeeded)) || modNeeded == 0) + { + continue; + } + if (modNeeded > 1024) { modNeeded = 1024; } + CLRDATA_ADDRESS* modules = new CLRDATA_ADDRESS[modNeeded]; + unsigned gotMods = 0; + if (SUCCEEDED(sos->GetAssemblyModuleList(assemblies[s], modNeeded, modules, &gotMods))) + { + for (unsigned m = 0; m < gotMods; m++) + { + DacpModuleData md; + if (SUCCEEDED(md.Request(sos, modules[m])) && md.ilBase != 0) + { + printf("MOD 0x%llx\n", (unsigned long long)md.ilBase); + modCount++; + } + } + } + delete[] modules; + } + } + delete[] assemblies; + } + } + printf("cdaclitetest[dac]: %d module(s)\n", modCount); + + // Enumerate handles: each handle address must fall within a handle-table segment. + ISOSHandleEnum* handleEnum = nullptr; + if (SUCCEEDED(sos->GetHandleEnum(&handleEnum)) && handleEnum != nullptr) + { + SOSHandleData buffer[256]; + unsigned fetched = 0; + int handleCount = 0; + while (SUCCEEDED(handleEnum->Next(256, buffer, &fetched)) && fetched > 0) + { + for (unsigned i = 0; i < fetched; i++) + { + printf("HND 0x%llx\n", (unsigned long long)buffer[i].Handle); + handleCount++; + } + if (fetched < 256) { break; } + } + handleEnum->Release(); + printf("cdaclitetest[dac]: %d handle(s)\n", handleCount); + } + + sos->Release(); + return 0; +} + +// Checks whether the CLR DAC (mscordaccore.dll / mscordacwks.dll) is currently mapped into +// THIS (the dumper) process. dbghelp's CLR-awareness works by loading the version-matched DAC +// and calling its ICLRDataEnumMemoryRegions; if that had happened, the module would be present. +static bool DacLoadedInThisProcess() +{ + HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, GetCurrentProcessId()); + if (snap == INVALID_HANDLE_VALUE) { return false; } + + bool found = false; + MODULEENTRY32W me = {}; + me.dwSize = sizeof(me); + if (Module32FirstW(snap, &me)) + { + do + { + if (_wcsicmp(me.szModule, L"mscordaccore.dll") == 0 || + _wcsicmp(me.szModule, L"mscordacwks.dll") == 0) + { + found = true; + break; + } + } while (Module32NextW(snap, &me)); + } + CloseHandle(snap); + return found; +} + +// Verifies a written minidump contains 'region' bases: parses MemoryListStream and +// Memory64ListStream and counts how many cdac-lite regions are covered. +static int VerifyDump(const wchar_t* path, const std::vector& regions) +{ + HANDLE hFile = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); + if (hFile == INVALID_HANDLE_VALUE) { return -1; } + HANDLE hMap = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL); + if (hMap == NULL) { CloseHandle(hFile); return -1; } + void* base = MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0); + if (base == nullptr) { CloseHandle(hMap); CloseHandle(hFile); return -1; } + + // Collect all [start,end) memory ranges present in the dump. + std::vector present; + PVOID stream = nullptr; ULONG size = 0; MINIDUMP_DIRECTORY* dir = nullptr; + if (MiniDumpReadDumpStream(base, MemoryListStream, &dir, &stream, &size) && stream != nullptr) + { + MINIDUMP_MEMORY_LIST* list = (MINIDUMP_MEMORY_LIST*)stream; + for (ULONG i = 0; i < list->NumberOfMemoryRanges; i++) + { + const MINIDUMP_MEMORY_DESCRIPTOR& d = list->MemoryRanges[i]; + Region r; r.base = d.StartOfMemoryRange; r.size = d.Memory.DataSize; present.push_back(r); + } + } + if (MiniDumpReadDumpStream(base, Memory64ListStream, &dir, &stream, &size) && stream != nullptr) + { + MINIDUMP_MEMORY64_LIST* list = (MINIDUMP_MEMORY64_LIST*)stream; + for (ULONG64 i = 0; i < list->NumberOfMemoryRanges; i++) + { + const MINIDUMP_MEMORY_DESCRIPTOR64& d = list->MemoryRanges[i]; + Region r; r.base = d.StartOfMemoryRange; r.size = (uint32_t)d.DataSize; present.push_back(r); + } + } + + int covered = 0; + for (size_t i = 0; i < regions.size(); i++) + { + uint64_t b = regions[i].base; + bool found = false; + for (size_t j = 0; j < present.size(); j++) + { + if (b >= present[j].base && b < present[j].base + present[j].size) { found = true; break; } + } + if (found) { covered++; } + else { printf("cdaclitetest[dump]: MISSING region 0x%llx (size 0x%x)\n", + (unsigned long long)b, regions[i].size); } + } + + UnmapViewOfFile(base); CloseHandle(hMap); CloseHandle(hFile); + return covered; +} + +// Writes a minidump for the target using cdac-lite to select the managed regions, +// fed to MiniDumpWriteDump via a MemoryCallback -- no version-matched DAC required. +static int RunDump(PFN_CLRDataCreateInstance pfnCreate, LiveDataTarget* target, DWORD pid, const wchar_t* outPath) +{ + ICLRDataEnumMemoryRegions* enumRegions = nullptr; + HRESULT hr = pfnCreate(__uuidof(ICLRDataEnumMemoryRegions), target, (void**)&enumRegions); + if (FAILED(hr) || enumRegions == nullptr) + { + fwprintf(stderr, L"CLRDataCreateInstance failed: 0x%08x\n", hr); + return 5; + } + + CollectCallback collect; + hr = enumRegions->EnumMemoryRegions(&collect, 0x00000200 /*HEAP2*/, CLRDATA_ENUM_MEM_DEFAULT); + enumRegions->Release(); + printf("cdaclitetest[dump]: cdac-lite selected %zu managed region(s)\n", collect.m_regions.size()); + + HANDLE hProc = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid); + if (hProc == nullptr) { fwprintf(stderr, L"OpenProcess failed\n"); return 4; } + + // Baseline: write a MiniDumpNormal with NO memory callback. If dbghelp were driving the + // CLR DAC (mscordaccore) itself, the managed regions would appear even without our + // callback. We expect ~0 of the cdac-lite regions to be present in this baseline. + std::wstring basePath = std::wstring(outPath) + L".baseline"; + HANDLE hBase = CreateFileW(basePath.c_str(), GENERIC_READ | GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + int baselineCovered = -1; + if (hBase != INVALID_HANDLE_VALUE) + { + BOOL bok = MiniDumpWriteDump(hProc, pid, hBase, MiniDumpNormal, NULL, NULL, NULL); + CloseHandle(hBase); + if (bok) + { + baselineCovered = VerifyDump(basePath.c_str(), collect.m_regions); + printf("cdaclitetest[dump]: baseline (no callback) contains %d/%zu cdac-lite regions\n", + baselineCovered, collect.m_regions.size()); + } + DeleteFileW(basePath.c_str()); + } + + HANDLE hFile = CreateFileW(outPath, GENERIC_READ | GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + if (hFile == INVALID_HANDLE_VALUE) { fwprintf(stderr, L"CreateFile(dump) failed\n"); CloseHandle(hProc); return 4; } + + DumpCallbackState cbState = { &collect.m_regions, 0 }; + MINIDUMP_CALLBACK_INFORMATION ci = {}; + ci.CallbackRoutine = &DumpMemoryCallback; + ci.CallbackParam = &cbState; + + // Option A: MiniDumpWithPrivateReadWriteMemory sweeps all private R/W pages (the heaps); + // cdac-lite's MemoryCallback adds the RX/image/frozen memory the sweep misses. This matches + // the production createdump path. (No MiniDumpWithoutAuxiliaryState -- it breaks the module + // export directory capture that ClrMD needs to find the contract descriptor.) + MINIDUMP_TYPE dumpType = (MINIDUMP_TYPE)(MiniDumpNormal | MiniDumpWithPrivateReadWriteMemory); + BOOL ok = MiniDumpWriteDump(hProc, pid, hFile, dumpType, NULL, NULL, &ci); + CloseHandle(hFile); + CloseHandle(hProc); + if (!ok) + { + fwprintf(stderr, L"MiniDumpWriteDump failed: 0x%08x\n", GetLastError()); + return 6; + } + + // Confirm dbghelp did not load the CLR DAC into this process. If dbghelp had invoked + // the runtime's ICLRDataEnumMemoryRegions, mscordaccore.dll would be mapped here. + bool dacLoaded = DacLoadedInThisProcess(); + printf("cdaclitetest[dump]: mscordaccore loaded in dumper process: %s\n", dacLoaded ? "YES" : "no"); + + int covered = VerifyDump(outPath, collect.m_regions); + printf("cdaclitetest[dump]: wrote %ls; %d/%zu cdac-lite regions present in the dump\n", + outPath, covered, collect.m_regions.size()); + + // The baseline (MiniDumpNormal, no callback) inherently captures module images and thread + // stacks -- that is dbghelp's normal behavior, NOT the CLR DAC. The regions that ONLY appear + // once cdac-lite feeds the MemoryCallback (GC heaps, loader heaps, JIT code, handle segments) + // are the managed-only memory that cdac-lite uniquely contributes. + int cdacOnly = (baselineCovered >= 0) ? (covered - baselineCovered) : covered; + printf("cdaclitetest[dump]: cdac-lite added %d managed region(s) beyond the no-callback baseline\n", + cdacOnly); + + bool regionsOk = covered > 0; + size_t uncapturable = collect.m_regions.size() - (size_t)covered; + if (uncapturable > 0) + { + printf("cdaclitetest[dump]: %zu region(s) not captured (reserved/uncommitted stack-limit pages)\n", + uncapturable); + } + // Answering "is dbghelp pulling in the mscordaccore enumeration?": the DAC must not be + // loaded, and cdac-lite must be the source of the managed-only regions (baseline lacked them). + bool dacFree = !dacLoaded && cdacOnly > 0; + if (!dacFree) + { + fwprintf(stderr, L"cdaclitetest[dump]: FAIL -- managed memory not exclusively from cdac-lite " + L"(dacLoaded=%d cdacOnly=%d)\n", dacLoaded ? 1 : 0, cdacOnly); + } + return (regionsOk && dacFree) ? 0 : 7; +} + +int wmain(int argc, wchar_t** argv) +{ + if (argc < 2) + { + fwprintf(stderr, L"Usage: cdaclitetest [path-to-dll] [--dac | --dump ]\n"); + return 1; + } + + DWORD pid = (DWORD)_wtoi(argv[1]); + const wchar_t* dllPath = (argc >= 3) ? argv[2] : L"cdaclite.dll"; + bool dacMode = (argc >= 4 && _wcsicmp(argv[3], L"--dac") == 0); + bool dumpMode = (argc >= 5 && _wcsicmp(argv[3], L"--dump") == 0); + const wchar_t* dumpPath = dumpMode ? argv[4] : nullptr; + + HMODULE mod = LoadLibraryW(dllPath); + if (mod == nullptr) + { + fwprintf(stderr, L"Failed to load %s (error %lu)\n", dllPath, GetLastError()); + return 2; + } + + PFN_CLRDataCreateInstance pfnCreate = (PFN_CLRDataCreateInstance)GetProcAddress(mod, "CLRDataCreateInstance"); + if (pfnCreate == nullptr) + { + fwprintf(stderr, L"CLRDataCreateInstance not found in %s\n", dllPath); + return 3; + } + + HANDLE process = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid); + if (process == nullptr) + { + fwprintf(stderr, L"OpenProcess(%lu) failed (error %lu)\n", pid, GetLastError()); + return 4; + } + + LiveDataTarget* target = new LiveDataTarget(process, pid); + + int rc; + if (dacMode) + { + rc = RunDacOracle(pfnCreate, target, pid); + } + else if (dumpMode) + { + rc = RunDump(pfnCreate, target, pid, dumpPath); + } + else + { + ICLRDataEnumMemoryRegions* enumRegions = nullptr; + HRESULT hr = pfnCreate(__uuidof(ICLRDataEnumMemoryRegions), target, (void**)&enumRegions); + if (FAILED(hr) || enumRegions == nullptr) + { + fwprintf(stderr, L"CLRDataCreateInstance failed: 0x%08x\n", hr); + target->Release(); + CloseHandle(process); + return 5; + } + + printf("cdaclitetest: enumerating memory regions for pid %lu\n", pid); + + PrintCallback callback; + // MiniDumpWithPrivateReadWriteMemory (0x00000200) => the "heap" (HEAP2) path. + hr = enumRegions->EnumMemoryRegions(&callback, 0x00000200, CLRDATA_ENUM_MEM_DEFAULT); + printf("cdaclitetest: EnumMemoryRegions returned 0x%08x, %lu region(s)\n", hr, callback.m_regionCount); + enumRegions->Release(); + rc = SUCCEEDED(hr) ? 0 : 6; + } + + target->Release(); + CloseHandle(process); + return rc; +} + diff --git a/src/coreclr/debug/cdaclite/contracts/CMakeLists.txt b/src/coreclr/debug/cdaclite/contracts/CMakeLists.txt new file mode 100644 index 00000000000000..54f534f534c8c8 --- /dev/null +++ b/src/coreclr/debug/cdaclite/contracts/CMakeLists.txt @@ -0,0 +1,18 @@ +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +add_library_clr(cdacgc STATIC + gc.cpp + thread.cpp + loader.cpp + loaderheaps.cpp + handles.cpp + jit.cpp + statics.cpp + syncblock.cpp + stresslog.cpp + interop.cpp + stackscan.cpp +) + +target_include_directories(cdacgc PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_link_libraries(cdacgc PUBLIC cdacdata) diff --git a/src/coreclr/debug/cdaclite/contracts/contracts.h b/src/coreclr/debug/cdaclite/contracts/contracts.h new file mode 100644 index 00000000000000..14c56e408274e1 --- /dev/null +++ b/src/coreclr/debug/cdaclite/contracts/contracts.h @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +//***************************************************************************** +// contracts.h +// +// Shared types for the cdac-lite memory-enumeration contracts. Each contract +// walks a subset of runtime structures (GC heaps, threads, modules, ...) and +// reports the memory regions that a heap dump should include. +//***************************************************************************** + +#ifndef CDACLITE_CONTRACTS_H +#define CDACLITE_CONTRACTS_H + +#include + +namespace cdac +{ +namespace contracts +{ + // Reports one enumerated memory region [start, start+size). 'kind' is a short + // label identifying the source (e.g. "gc-gen0", "thread-stack", "module"). + typedef void (*RegionCallback)(void* context, const char* kind, uint64_t start, uint64_t size); +} +} // namespace contracts + +#endif // CDACLITE_CONTRACTS_H diff --git a/src/coreclr/debug/cdaclite/contracts/gc.cpp b/src/coreclr/debug/cdaclite/contracts/gc.cpp new file mode 100644 index 00000000000000..5254a677c43a0b --- /dev/null +++ b/src/coreclr/debug/cdaclite/contracts/gc.cpp @@ -0,0 +1,444 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +//***************************************************************************** +// gc.cpp +// +// Implementation of the GC heap-region walk declared in gc.h. +//***************************************************************************** + +#include "gc.h" +#include "runtimetypes.h" + +#include +#include +#include + +namespace cdac +{ +namespace contracts +{ + namespace + { + // GC globals (src/coreclr/gc/datadescriptor/datadescriptor.inc). + const char* const GlobalGCIdentifiers = "GCIdentifiers"; + const char* const GlobalTotalGenerationCount = "TotalGenerationCount"; + const char* const GlobalNumHeaps = "NumHeaps"; + const char* const GlobalHeaps = "Heaps"; + const char* const GlobalGenerationTable = "GCHeapGenerationTable"; + const char* const GlobalEphemeralHeapSegment = "GCHeapEphemeralHeapSegment"; + const char* const GlobalAllocAllocated = "GCHeapAllocAllocated"; + const char* const GlobalFinalizeQueue = "GCHeapFinalizeQueue"; + const char* const GlobalFillPointersLength = "CFinalizeFillPointersLength"; + + // Guard against runaway walks over corrupt segment lists. + const int MaxSegmentsPerList = 1024 * 1024; + const uint32_t DefaultGenerationCount = 5; // gen0, gen1, gen2, LOH, POH + + // Per-heap parameters gathered either from globals (WKS) or heap fields (SVR). + struct HeapContext + { + uint64_t generationTableBase = 0; + uint64_t ephemeralSegment = 0; + uint64_t allocAllocated = 0; + uint64_t finalizeQueue = 0; + }; + + // Reads the CFinalize struct at 'finalizeQueueAddr' (captured via EnumMem) and emits the + // inline fill-pointers array the cDAC re-reads in GetFillPointers. Mirrors GetGCHeapDataFromHeap. + void EnumFinalizeQueue(const Target& target, uint64_t finalizeQueueAddr) + { + if (finalizeQueueAddr == 0) + { + return; + } + data::CFinalize cfinalize; + if (!target.TryRead(finalizeQueueAddr, cfinalize)) + { + return; + } + uint32_t fillPointersLength = 0; + uint64_t len = 0; + if (target.TryGetGlobalValue(GlobalFillPointersLength, len)) + { + fillPointersLength = (uint32_t)len; + } + if (fillPointersLength > 0) + { + target.EmitMemory(cfinalize.FillPointers, fillPointersLength * (uint32_t)target.PointerSize()); + } + } + + // Emits a WKS gc_heap data array (InterestingData/CompactReasons/etc.). 'pointerGlobal' is a + // GLOBAL_POINTER whose resolved value is the inline array's address (GCHeapWKS reads it via + // ReadGlobalPointer with no extra deref); 'lengthGlobal' is a direct-value length. Each element + // is pointer-sized (TargetNUInt). Mirrors GetGCHeapDataFromHeap.ReadGCHeapDataArray. + void EnumDataArray(const Target& target, const char* pointerGlobal, const char* lengthGlobal) + { + uint64_t arrayStart = 0; + if (!target.TryGetGlobalValue(pointerGlobal, arrayStart) || arrayStart == 0) + { + return; + } + uint64_t length = 0; + if (target.TryGetGlobalValue(lengthGlobal, length) && length > 0) + { + target.EmitMemory(arrayStart, (uint32_t)length * (uint32_t)target.PointerSize()); + } + } + + // Walks a HeapSegment.Next list from 'start', reading each segment (captured via EnumMem). + // Mirrors GC_1.AddSegmentList. Bounded by MaxSegmentsPerList. + void EnumSegmentList(const Target& target, uint64_t start, std::set& visited) + { + uint64_t curr = start; + for (int i = 0; curr != 0 && i < MaxSegmentsPerList; i++) + { + if (!visited.insert(curr).second) + { + break; + } + data::HeapSegment segment; + if (!target.TryRead(curr, segment)) + { + break; + } + curr = segment.Next; + } + } + + // Reads a RegionFreeList at 'freeListAddr' (captured via EnumMem) and walks its + // HeadFreeRegion segment list. Mirrors GC_1.AddFreeList. + void EnumFreeList(const Target& target, uint64_t freeListAddr, std::set& visited) + { + if (freeListAddr == 0) + { + return; + } + data::RegionFreeList freeList; + if (!target.TryRead(freeListAddr, freeList)) + { + return; + } + if (freeList.HeadFreeRegion != 0) + { + EnumSegmentList(target, freeList.HeadFreeRegion, visited); + } + } + + // Enumerates the GC bookkeeping card-table info list. Mirrors GC_1.GetGCBookkeepingMemoryRegions: + // read CardTableInfo at bookkeeping_start, then walk NextCardTable (each node at next - size). + void EnumBookkeeping(const Target& target) + { + uint64_t bookkeepingStart = 0; + if (!target.TryReadGlobalPointer("BookkeepingStart", bookkeepingStart) || bookkeepingStart == 0) + { + return; + } + uint64_t cardTableInfoSize = 0; + target.TryGetGlobalValue("CardTableInfoSize", cardTableInfoSize); + + data::CardTableInfo first; + if (!target.TryRead(bookkeepingStart, first)) + { + return; + } + uint64_t next = first.NextCardTable; + uint64_t firstNext = next; + for (int i = 0; next != 0 && next > cardTableInfoSize && i < 4096; i++) + { + uint64_t ctAddr = next - cardTableInfoSize; + data::CardTableInfo ct; + if (!target.TryRead(ctAddr, ct)) + { + break; + } + next = ct.NextCardTable; + if (next == firstNext) + { + break; + } + } + } + + // Enumerates GC free regions (global + per-heap). Mirrors GC_1.GetGCFreeRegions: reads the + // RegionFreeList arrays and freeable segment lists the cDAC traverses. + void EnumFreeRegions(const Target& target, bool isServer, uint64_t heapTable, uint32_t numHeaps, + std::set& visited) + { + uint64_t countFreeRegionKinds64 = 0; + target.TryGetGlobalValue("CountFreeRegionKinds", countFreeRegionKinds64); + uint32_t countFreeRegionKinds = (countFreeRegionKinds64 > 16) ? 16 : (uint32_t)countFreeRegionKinds64; + + uint32_t regionFreeListSize = 0; + target.TryGetTypeSize("RegionFreeList", regionFreeListSize); + + // Global free huge regions (a single inline RegionFreeList). + uint64_t hugeBase = 0; + if (target.TryGetGlobalValue("GlobalFreeHugeRegions", hugeBase) && hugeBase != 0) + { + EnumFreeList(target, hugeBase, visited); + } + + // Global regions to decommit (an inline array of countFreeRegionKinds RegionFreeLists). + uint64_t decommitBase = 0; + if (target.TryGetGlobalValue("GlobalRegionsToDecommit", decommitBase) && decommitBase != 0 && regionFreeListSize != 0) + { + for (uint32_t i = 0; i < countFreeRegionKinds; i++) + { + EnumFreeList(target, decommitBase + (uint64_t)i * regionFreeListSize, visited); + } + } + + if (isServer) + { + for (uint32_t h = 0; h < numHeaps; h++) + { + uint64_t heapAddress = 0; + if (!target.TryReadPointer(heapTable + (uint64_t)h * target.PointerSize(), heapAddress) || heapAddress == 0) + { + continue; + } + data::GCHeap gcHeap; + if (!target.TryRead(heapAddress, gcHeap)) + { + continue; + } + if (gcHeap.FreeRegions != 0 && regionFreeListSize != 0) + { + for (uint32_t j = 0; j < countFreeRegionKinds; j++) + { + EnumFreeList(target, gcHeap.FreeRegions + (uint64_t)j * regionFreeListSize, visited); + } + } + if (gcHeap.FreeableSohSegment != 0) + { + EnumSegmentList(target, gcHeap.FreeableSohSegment, visited); + } + if (gcHeap.FreeableUohSegment != 0) + { + EnumSegmentList(target, gcHeap.FreeableUohSegment, visited); + } + } + } + else + { + // Workstation GC: free regions from globals (inline RegionFreeList array). + uint64_t freeRegionsBase = 0; + if (target.TryGetGlobalValue("GCHeapFreeRegions", freeRegionsBase) && freeRegionsBase != 0 && regionFreeListSize != 0) + { + for (uint32_t i = 0; i < countFreeRegionKinds; i++) + { + EnumFreeList(target, freeRegionsBase + (uint64_t)i * regionFreeListSize, visited); + } + } + // Freeable SOH/UOH segments (pointer globals -> deref to segment). + uint64_t soh = 0; + if (target.TryReadGlobalPointer("GCHeapFreeableSohSegment", soh) && soh != 0) + { + EnumSegmentList(target, soh, visited); + } + uint64_t uoh = 0; + if (target.TryReadGlobalPointer("GCHeapFreeableUohSegment", uoh) && uoh != 0) + { + EnumSegmentList(target, uoh, visited); + } + } + } + + bool GcIdentifiersContains(const std::string& identifiers, const char* token) + { + // GCIdentifiers is a comma-separated list, e.g. "workstation, segments". + std::string needle(token); + size_t pos = 0; + while (pos < identifiers.size()) + { + size_t comma = identifiers.find(',', pos); + size_t end = (comma == std::string::npos) ? identifiers.size() : comma; + + size_t start = pos; + while (start < end && (identifiers[start] == ' ' || identifiers[start] == '\t')) { start++; } + size_t stop = end; + while (stop > start && (identifiers[stop - 1] == ' ' || identifiers[stop - 1] == '\t')) { stop--; } + + if (identifiers.compare(start, stop - start, needle) == 0) + { + return true; + } + if (comma == std::string::npos) { break; } + pos = comma + 1; + } + return false; + } + + // Walks one generation's segment list (following HeapSegment.Next), reporting + // each segment's used range. 'visited' de-duplicates segments shared across + // generation lists. + int WalkSegmentList(const Target& target, const HeapContext& heap, unsigned generation, uint64_t startSegment, + std::set& visited, RegionCallback sink, void* sinkContext) + { + char kind[16]; + snprintf(kind, sizeof(kind), "gc-gen%u", generation); + + int count = 0; + uint64_t seg = startSegment; + for (int i = 0; seg != 0 && i < MaxSegmentsPerList; i++) + { + if (!visited.insert(seg).second) + { + break; // already seen (or a cycle) + } + + data::HeapSegment segment; + if (!target.TryRead(seg, segment)) + { + break; + } + + // The ephemeral segment's live end is alloc_allocated, not the segment's + // own allocated pointer. + uint64_t end = (seg == heap.ephemeralSegment && heap.allocAllocated != 0) + ? heap.allocAllocated + : segment.Allocated; + if (segment.Mem != 0 && end > segment.Mem) + { + // Extend the low bound by one pointer to include the first object's header + // (the sync-block-index DWORD sits just below segment.Mem; the cDAC reads it + // via Object.TryGetHashCode -> ObjectHeader at object-sizeof(header)). + uint64_t start = segment.Mem - target.PointerSize(); + sink(sinkContext, kind, start, end - start); + count++; + } + + seg = segment.Next; + } + return count; + } + + int WalkHeap(const Target& target, const HeapContext& heap, uint32_t generationCount, + std::set& visited, RegionCallback sink, void* sinkContext) + { + uint32_t generationSize = 0; + if (!target.TryGetTypeSize(data::Generation().TypeName(), generationSize) || generationSize == 0) + { + return 0; + } + + int count = 0; + for (uint32_t i = 0; i < generationCount; i++) + { + uint64_t generationAddress = heap.generationTableBase + (uint64_t)i * generationSize; + data::Generation generation; + if (!target.TryRead(generationAddress, generation)) + { + continue; + } + count += WalkSegmentList(target, heap, i, generation.StartSegment, visited, sink, sinkContext); + } + return count; + } + } + + int EnumerateGCHeapRegions(const Target& target, RegionCallback sink, void* sinkContext) + { + std::string identifiers; + if (!target.TryGetGlobalString(GlobalGCIdentifiers, identifiers)) + { + return -1; + } + + bool isServer = GcIdentifiersContains(identifiers, "server"); + bool isWorkstation = GcIdentifiersContains(identifiers, "workstation"); + if (!isServer && !isWorkstation) + { + return -1; + } + + uint32_t generationCount = DefaultGenerationCount; + uint64_t generationCount64 = 0; + if (target.TryGetGlobalValue(GlobalTotalGenerationCount, generationCount64) && + generationCount64 != 0 && generationCount64 <= 64) + { + generationCount = (uint32_t)generationCount64; + } + + std::set visited; + int total = 0; + + if (isWorkstation) + { + HeapContext heap; + if (!target.TryGetGlobalValue(GlobalGenerationTable, heap.generationTableBase)) + { + return -1; + } + target.TryReadGlobalPointer(GlobalEphemeralHeapSegment, heap.ephemeralSegment); + target.TryReadGlobalPointer(GlobalAllocAllocated, heap.allocAllocated); + + // The finalize queue's CFinalize* value is at the GCHeapFinalizeQueue global. + uint64_t finalizeQueue = 0; + target.TryReadGlobalPointer(GlobalFinalizeQueue, finalizeQueue); + EnumFinalizeQueue(target, finalizeQueue); + + // WKS gc_heap data arrays re-read by GetGCHeapDataFromHeap. + EnumDataArray(target, "GCHeapInterestingData", "InterestingDataLength"); + EnumDataArray(target, "GCHeapCompactReasons", "CompactReasonsLength"); + EnumDataArray(target, "GCHeapExpandMechanisms", "ExpandMechanismsLength"); + EnumDataArray(target, "GCHeapInterestingMechanismBits", "InterestingMechanismBitsLength"); + + total += WalkHeap(target, heap, generationCount, visited, sink, sinkContext); + + // Bookkeeping card tables + free regions (GetGCBookkeepingMemoryRegions / GetGCFreeRegions). + EnumBookkeeping(target); + EnumFreeRegions(target, /*isServer*/ false, /*heapTable*/ 0, /*numHeaps*/ 0, visited); + } + else + { + // Server GC: read NumHeaps and the Heaps array of GCHeap*. + uint32_t numHeaps = 0; + if (!target.TryReadGlobalUInt32(GlobalNumHeaps, numHeaps) || numHeaps == 0) + { + return total; + } + + uint64_t heapTable = 0; + if (!target.TryReadGlobalPointer(GlobalHeaps, heapTable)) + { + return total; + } + + // Emit the Heaps pointer array the cDAC re-reads (GetGCHeaps / GetGCFreeRegions). + target.EmitMemory(heapTable, numHeaps * (uint32_t)target.PointerSize()); + + for (uint32_t i = 0; i < numHeaps; i++) + { + uint64_t heapAddress = 0; + if (!target.TryReadPointer(heapTable + (uint64_t)i * target.PointerSize(), heapAddress) || heapAddress == 0) + { + continue; + } + + data::GCHeap gcHeap; + if (!target.TryRead(heapAddress, gcHeap)) + { + continue; + } + + HeapContext heap; + heap.generationTableBase = gcHeap.GenerationTable; + heap.ephemeralSegment = gcHeap.EphemeralHeapSegment; + heap.allocAllocated = gcHeap.AllocAllocated; + heap.finalizeQueue = gcHeap.FinalizeQueue; + + EnumFinalizeQueue(target, heap.finalizeQueue); + total += WalkHeap(target, heap, generationCount, visited, sink, sinkContext); + } + + // Bookkeeping card tables + free regions (GetGCBookkeepingMemoryRegions / GetGCFreeRegions). + EnumBookkeeping(target); + EnumFreeRegions(target, /*isServer*/ true, heapTable, numHeaps, visited); + } + + return total; + } +} +} // namespace contracts diff --git a/src/coreclr/debug/cdaclite/contracts/gc.h b/src/coreclr/debug/cdaclite/contracts/gc.h new file mode 100644 index 00000000000000..443c6ed2950f4f --- /dev/null +++ b/src/coreclr/debug/cdaclite/contracts/gc.h @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +//***************************************************************************** +// gc.h +// +// GC contract: walks the .NET GC heaps and enumerates their memory regions by +// reading GC types/globals from a Target. Modeled on the managed cDAC GC +// contract (see docs/design/datacontracts/GC.md and +// src/native/managed/cdac/.../Contracts/GC/GC_1.cs). +//***************************************************************************** + +#ifndef CDACLITE_CONTRACTS_GC_H +#define CDACLITE_CONTRACTS_GC_H + +#include + +#include "contracts.h" +#include "target.h" + +namespace cdac +{ +namespace contracts +{ + // Walks all GC heaps and their segments, invoking 'sink' for each segment's + // used range ([Mem, Allocated), or [Mem, AllocAllocated) for the ephemeral + // segment). The region 'kind' is "gc-gen". Returns the number of regions + // reported, or -1 if the GC type could not be determined from the descriptor. + int EnumerateGCHeapRegions(const Target& target, RegionCallback sink, void* sinkContext); +} +} // namespace contracts + +#endif // CDACLITE_CONTRACTS_GC_H diff --git a/src/coreclr/debug/cdaclite/contracts/handles.cpp b/src/coreclr/debug/cdaclite/contracts/handles.cpp new file mode 100644 index 00000000000000..fca11f50102520 --- /dev/null +++ b/src/coreclr/debug/cdaclite/contracts/handles.cpp @@ -0,0 +1,148 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +//***************************************************************************** +// handles.cpp +// +// Implementation of the GC handle-table walk declared in handles.h. +//***************************************************************************** + +#include "handles.h" +#include "runtimetypes.h" + +#include +#include + +namespace cdac +{ +namespace contracts +{ + namespace + { + // GC handle-table globals (src/coreclr/gc/datadescriptor/datadescriptor.inc, + // merged into the target from the GC sub-descriptor). + const char* const GlobalHandleTableMap = "HandleTableMap"; // address of g_HandleTableMap + const char* const GlobalBucketCount = "InitialHandleTableArraySize"; // direct: bucket array length + const char* const GlobalSegmentSize = "HandleSegmentSize"; // direct: bytes per segment + const char* const GlobalTotalCpuCount = "TotalCpuCount"; // pointer: server table count + const char* const GlobalGCIdentifiers = "GCIdentifiers"; + + const int MaxMaps = 100000; + const int MaxSegments = 1000000; + const uint32_t MaxBuckets = 4096; + const uint32_t MaxSegmentSize = 64u * 1024 * 1024; + } + + int EnumerateHandleRegions(const Target& target, RegionCallback sink, void* sinkContext) + { + uint64_t mapAddr = 0; + if (!target.TryGetGlobalValue(GlobalHandleTableMap, mapAddr) || mapAddr == 0) + { + return -1; + } + + uint64_t bucketCount64 = 0; + if (!target.TryGetGlobalValue(GlobalBucketCount, bucketCount64) || bucketCount64 == 0 || bucketCount64 > MaxBuckets) + { + return -1; + } + uint32_t bucketCount = (uint32_t)bucketCount64; + + uint64_t segmentSize64 = 0; + target.TryGetGlobalValue(GlobalSegmentSize, segmentSize64); + uint32_t segmentSize = (segmentSize64 > 0 && segmentSize64 <= MaxSegmentSize) ? (uint32_t)segmentSize64 : 0; + if (segmentSize == 0) + { + return -1; // without a segment size we can't report meaningful regions + } + + // Handle tables per bucket: 1 for workstation GC, TotalCpuCount for server GC. + uint32_t tableCount = 1; + std::string identifiers; + if (target.TryGetGlobalString(GlobalGCIdentifiers, identifiers) && + identifiers.find("server") != std::string::npos) + { + uint32_t cpuCount = 0; + if (target.TryReadGlobalUInt32(GlobalTotalCpuCount, cpuCount) && cpuCount > 0) + { + tableCount = cpuCount; + } + } + + const uint64_t ptrSize = target.PointerSize(); + std::set visited; + int count = 0; + + uint64_t currentMap = mapAddr; + for (int m = 0; currentMap != 0 && m < MaxMaps; m++) + { + data::HandleTableMap map; + if (!target.TryRead(currentMap, map)) + { + break; + } + + // Emit the bucket pointer array the cDAC re-reads (HandleTableMap.OnInit walks + // [BucketsPtr, BucketsPtr + bucketCount*ptrSize)). EnumMem only captures the map + // struct itself; this array is separate memory. + target.EmitMemory(map.BucketsPtr, bucketCount * (uint32_t)ptrSize); + + for (uint32_t b = 0; b < bucketCount; b++) + { + uint64_t bucketPtr = 0; + if (!target.TryReadPointer(map.BucketsPtr + b * ptrSize, bucketPtr) || bucketPtr == 0) + { + continue; + } + + data::HandleTableBucket bucket; + if (!target.TryRead(bucketPtr, bucket)) + { + continue; + } + + // Emit the per-bucket handle-table pointer array ([Table, Table + tableCount*ptrSize)). + target.EmitMemory(bucket.Table, tableCount * (uint32_t)ptrSize); + + for (uint32_t t = 0; t < tableCount; t++) + { + uint64_t handleTablePtr = 0; + if (!target.TryReadPointer(bucket.Table + t * ptrSize, handleTablePtr) || handleTablePtr == 0) + { + continue; + } + + data::HandleTable handleTable; + if (!target.TryRead(handleTablePtr, handleTable)) + { + continue; + } + + uint64_t segment = handleTable.SegmentList; + for (int s = 0; segment != 0 && s < MaxSegments; s++) + { + if (!visited.insert(segment).second) + { + break; // cycle + } + + data::TableSegment tableSegment; + if (!target.TryRead(segment, tableSegment)) + { + break; + } + + sink(sinkContext, "handle-segment", segment, segmentSize); + count++; + + segment = tableSegment.NextSegment; + } + } + } + + currentMap = map.Next; + } + + return count; + } +} +} // namespace contracts diff --git a/src/coreclr/debug/cdaclite/contracts/handles.h b/src/coreclr/debug/cdaclite/contracts/handles.h new file mode 100644 index 00000000000000..64c5022733fee1 --- /dev/null +++ b/src/coreclr/debug/cdaclite/contracts/handles.h @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +//***************************************************************************** +// handles.h +// +// GC handles contract: walks the handle table and reports each handle table +// segment's memory (the GC roots storage). Modeled on the managed cDAC +// IGC.GetHandles walk (Contracts/GC/GC_1.cs). +//***************************************************************************** + +#ifndef CDACLITE_CONTRACTS_HANDLES_H +#define CDACLITE_CONTRACTS_HANDLES_H + +#include + +#include "contracts.h" +#include "target.h" + +namespace cdac +{ +namespace contracts +{ + // Walks HandleTableMap -> buckets -> HandleTable -> TableSegment list and + // reports each segment's memory ([segment, segment+HandleSegmentSize)) with + // kind "handle-segment". Returns the number of regions reported, or -1 if the + // handle table could not be located. + int EnumerateHandleRegions(const Target& target, RegionCallback sink, void* sinkContext); +} +} // namespace contracts + +#endif // CDACLITE_CONTRACTS_HANDLES_H diff --git a/src/coreclr/debug/cdaclite/contracts/interop.cpp b/src/coreclr/debug/cdaclite/contracts/interop.cpp new file mode 100644 index 00000000000000..3e5cd9bd4d6bfc --- /dev/null +++ b/src/coreclr/debug/cdaclite/contracts/interop.cpp @@ -0,0 +1,92 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +//***************************************************************************** +// interop.cpp +// +// Implementation of the COM interop enumeration declared in interop.h. +//***************************************************************************** + +#include "interop.h" +#include "runtimetypes.h" + +#include + +namespace cdac +{ +namespace contracts +{ + namespace + { + // COM interop globals (src/coreclr/vm/datadescriptor/datadescriptor.inc). + const char* const GlobalRCWCleanupList = "RCWCleanupList"; // &g_pRCWCleanupList (ptr-ptr) + + const int MaxBuckets = 1000000; + const int MaxRCWsPerBucket = 1000000; + } + + int EnumerateInteropRegions(const Target& target) + { + // The cleanup list head: ReadPointer(ReadGlobalPointer(RCWCleanupList)). + uint64_t listAddr = 0; + if (!target.TryReadGlobalPointer(GlobalRCWCleanupList, listAddr)) + { + return -1; + } + if (listAddr == 0) + { + return 0; // no cleanup list allocated + } + + data::RCWCleanupList list; + if (!target.TryRead(listAddr, list)) + { + return -1; + } + + std::set visited; + int count = 0; + + uint64_t bucketPtr = list.FirstBucket; + for (int b = 0; bucketPtr != 0 && b < MaxBuckets; b++) + { + if (!visited.insert(bucketPtr).second) + { + break; + } + data::RCW bucket; + if (!target.TryRead(bucketPtr, bucket)) + { + break; + } + + // GetSTAThread reads the bucket's CtxEntry; capture it too. + if (bucket.CtxEntry != 0) + { + data::CtxEntry ctxEntry; + target.TryRead(bucket.CtxEntry, ctxEntry); + } + + // Each bucket heads a chain of RCWs linked by NextRCW (starting with the bucket itself). + uint64_t rcwPtr = bucketPtr; + for (int r = 0; rcwPtr != 0 && r < MaxRCWsPerBucket; r++) + { + data::RCW rcw; + if (!target.TryRead(rcwPtr, rcw)) + { + break; + } + count++; + if (rcwPtr != bucketPtr && !visited.insert(rcwPtr).second) + { + break; + } + rcwPtr = rcw.NextRCW; + } + + bucketPtr = bucket.NextCleanupBucket; + } + + return count; + } +} +} // namespace contracts diff --git a/src/coreclr/debug/cdaclite/contracts/interop.h b/src/coreclr/debug/cdaclite/contracts/interop.h new file mode 100644 index 00000000000000..5337b7492d1515 --- /dev/null +++ b/src/coreclr/debug/cdaclite/contracts/interop.h @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +//***************************************************************************** +// interop.h +// +// COM interop contract: enumerates the RCW cleanup list. Modeled on the managed +// cDAC BuiltInCOM contract (Contracts/BuiltInCOM_1.cs GetRCWCleanupList). +//***************************************************************************** + +#ifndef CDACLITE_CONTRACTS_INTEROP_H +#define CDACLITE_CONTRACTS_INTEROP_H + +#include + +#include "contracts.h" +#include "target.h" + +namespace cdac +{ +namespace contracts +{ + // Reads the RCWCleanupList and its RCW bucket/cleanup chains so the cDAC's BuiltInCOM + // contract can re-read them from the dump. Memory is captured via the Target's EnumMem + // sink. Returns the number of RCWs captured, 0 if the cleanup list is empty, or -1 if + // the list global could not be located. + int EnumerateInteropRegions(const Target& target); +} +} // namespace contracts + +#endif // CDACLITE_CONTRACTS_INTEROP_H diff --git a/src/coreclr/debug/cdaclite/contracts/jit.cpp b/src/coreclr/debug/cdaclite/contracts/jit.cpp new file mode 100644 index 00000000000000..1104436983b273 --- /dev/null +++ b/src/coreclr/debug/cdaclite/contracts/jit.cpp @@ -0,0 +1,83 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +//***************************************************************************** +// jit.cpp +// +// Implementation of the JIT code-heap walk declared in jit.h. +//***************************************************************************** + +#include "jit.h" +#include "runtimetypes.h" + +#include + +namespace cdac +{ +namespace contracts +{ + namespace + { + // Pointer-to-pointer global: &g_pEEJitManager (deref once for the manager). + const char* const GlobalEEJitManager = "EEJitManagerAddress"; + + const int MaxCodeHeaps = 1000000; + } + + int EnumerateJitCodeRegions(const Target& target, RegionCallback sink, void* sinkContext) + { + // ReadGlobalPointer = ReadPointer(ReadGlobalPointer(name)) -> the EEJitManager. + uint64_t jitManagerAddr = 0; + if (!target.TryReadGlobalPointer(GlobalEEJitManager, jitManagerAddr) || jitManagerAddr == 0) + { + return -1; + } + + data::EEJitManager jitManager; + if (!target.TryRead(jitManagerAddr, jitManager)) + { + return -1; + } + + std::set visited; + int count = 0; + uint64_t node = jitManager.AllCodeHeaps; + + for (int i = 0; node != 0 && i < MaxCodeHeaps; i++) + { + if (!visited.insert(node).second) + { + break; // cycle + } + + data::CodeHeapListNode heap; + if (!target.TryRead(node, heap)) + { + break; + } + + if (heap.StartAddress != 0 && heap.EndAddress > heap.StartAddress) + { + sink(sinkContext, "jit-code", heap.StartAddress, heap.EndAddress - heap.StartAddress); + count++; + } + + // The cDAC's GetCodeHeapInfos reads node.Heap -> CodeHeap (HeapType) and then the + // Loader/Host code-heap struct. Read them so they are captured (EnumMem). Both overlay + // the same address; reading both avoids depending on the HeapType enum value. + if (heap.Heap != 0) + { + data::CodeHeap codeHeap; + target.TryRead(heap.Heap, codeHeap); + data::LoaderCodeHeap loaderCodeHeap; + target.TryRead(heap.Heap, loaderCodeHeap); + data::HostCodeHeap hostCodeHeap; + target.TryRead(heap.Heap, hostCodeHeap); + } + + node = heap.Next; + } + + return count; + } +} +} // namespace contracts diff --git a/src/coreclr/debug/cdaclite/contracts/jit.h b/src/coreclr/debug/cdaclite/contracts/jit.h new file mode 100644 index 00000000000000..796453dee3ed08 --- /dev/null +++ b/src/coreclr/debug/cdaclite/contracts/jit.h @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +//***************************************************************************** +// jit.h +// +// JIT code contract: reports the runtime's JIT code heaps. These hold executable +// (RX) code which a private-read-write memory capture does NOT include, so a heap +// dump must enumerate them explicitly. Matches the DAC HEAP2 behavior +// (EECodeGenManager::EnumMemoryRegions in vm/codeman.cpp). +//***************************************************************************** + +#ifndef CDACLITE_CONTRACTS_JIT_H +#define CDACLITE_CONTRACTS_JIT_H + +#include + +#include "contracts.h" +#include "target.h" + +namespace cdac +{ +namespace contracts +{ + // Walks EEJitManager -> AllCodeHeaps (CodeHeapListNode list) and reports each + // code heap's range ([StartAddress, EndAddress)) with kind "jit-code". Returns + // the number of regions reported, or -1 if the JIT manager could not be located. + int EnumerateJitCodeRegions(const Target& target, RegionCallback sink, void* sinkContext); +} +} // namespace contracts + +#endif // CDACLITE_CONTRACTS_JIT_H diff --git a/src/coreclr/debug/cdaclite/contracts/loader.cpp b/src/coreclr/debug/cdaclite/contracts/loader.cpp new file mode 100644 index 00000000000000..28434f5aa058f5 --- /dev/null +++ b/src/coreclr/debug/cdaclite/contracts/loader.cpp @@ -0,0 +1,149 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +//***************************************************************************** +// loader.cpp +// +// Implementation of the Loader module walk declared in loader.h. +//***************************************************************************** + +#include "loader.h" +#include "runtimetypes.h" + +#include + +namespace cdac +{ +namespace contracts +{ + namespace + { + // Global pointing at the AppDomain*: &AppDomain::m_pTheAppDomain. + const char* const GlobalAppDomain = "AppDomain"; + + const int MaxBlocks = 1024; // guard against corrupt block lists + const uint32_t MaxAssemblies = 1u << 20; + } + + int ForEachModule(const Target& target, ModuleCallback callback, void* context) + { + // AppDomain global is a pointer-to-pointer: deref once to get the AppDomain. + uint64_t appDomainAddr = 0; + if (!target.TryReadGlobalPointer(GlobalAppDomain, appDomainAddr) || appDomainAddr == 0) + { + return -1; + } + + data::AppDomain appDomain; + if (!target.TryRead(appDomainAddr, appDomain)) + { + return -1; + } + + // AssemblyList is an embedded ArrayListBase (a block list of Assembly*). + data::ArrayListBase list; + if (!target.TryRead(appDomain.AssemblyList, list)) + { + return -1; + } + + uint32_t total = list.Count; + if (total > MaxAssemblies) + { + total = MaxAssemblies; + } + + int visitedModules = 0; + uint32_t seen = 0; + uint64_t blockAddr = list.FirstBlock; + + for (int b = 0; blockAddr != 0 && b < MaxBlocks && seen < total; b++) + { + data::ArrayListBlock block; + if (!target.TryRead(blockAddr, block)) + { + break; + } + + for (uint32_t i = 0; i < block.Size && seen < total; i++) + { + seen++; + uint64_t assemblyAddr = 0; + if (!target.TryReadPointer(block.ArrayStart + (uint64_t)i * target.PointerSize(), assemblyAddr) || + assemblyAddr == 0) + { + continue; + } + + data::Assembly assembly; + if (!target.TryRead(assemblyAddr, assembly) || assembly.Module == 0) + { + continue; + } + + callback(context, assembly.Module); + visitedModules++; + } + + blockAddr = block.Next; + } + + return visitedModules; + } + + namespace + { + struct ModuleImageState + { + const Target* target; + std::set visited; + RegionCallback sink; + void* sinkContext; + int emitted; + }; + + // Iterates modules and captures only the memory that is NOT available from the on-disk + // binaries: in-memory symbol (PDB) streams. The DAC does not dump file-backed module + // images -- the analyzer re-reads image bytes (code, R2R, ECMA metadata) from the binary + // on disk -- so cdac-lite doesn't either, keeping dumps DAC-sized. + void EmitModuleExtras(void* context, uint64_t moduleAddr) + { + ModuleImageState* state = (ModuleImageState*)context; + const Target& target = *state->target; + + data::Module module; + if (!target.TryRead(moduleAddr, module)) + { + return; + } + + // If the module has an in-memory symbol stream, capture its buffer (ILoader.TryGetSymbolStream). + // In-memory PDBs have no on-disk backing, so they must be in the dump. + if (module.GrowableSymbolStream != 0) + { + data::CGrowableSymbolStream symStream; + if (target.TryRead(module.GrowableSymbolStream, symStream) && + symStream.Buffer != 0 && (uint32_t)symStream.Size != 0) + { + target.EmitMemory(symStream.Buffer, (uint32_t)symStream.Size); + state->emitted++; + } + } + } + } + + int EnumerateModuleRegions(const Target& target, RegionCallback sink, void* sinkContext) + { + ModuleImageState state; + state.target = ⌖ + state.sink = sink; + state.sinkContext = sinkContext; + state.emitted = 0; + + if (ForEachModule(target, &EmitModuleExtras, &state) < 0) + { + return -1; + } + return state.emitted; + } +} +} // namespace contracts diff --git a/src/coreclr/debug/cdaclite/contracts/loader.h b/src/coreclr/debug/cdaclite/contracts/loader.h new file mode 100644 index 00000000000000..5d6089e447d6ea --- /dev/null +++ b/src/coreclr/debug/cdaclite/contracts/loader.h @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +//***************************************************************************** +// loader.h +// +// Loader contract: walks the AppDomain's assembly list and reports each module's +// loaded PE image (code + metadata) -- the file-backed memory that a heap dump +// needs but that a private-read-write memory capture does not include. Modeled on +// the managed cDAC Loader contract (Contracts/Loader_1.cs). +//***************************************************************************** + +#ifndef CDACLITE_CONTRACTS_LOADER_H +#define CDACLITE_CONTRACTS_LOADER_H + +#include + +#include "contracts.h" +#include "target.h" + +namespace cdac +{ +namespace contracts +{ + // Invoked with each Module* discovered by ForEachModule. + typedef void (*ModuleCallback)(void* context, uint64_t moduleAddr); + + // Enumerates every Module in the (default) AppDomain's assembly list, invoking + // 'callback' with each Module address. Returns the module count, or -1 if the + // AppDomain could not be located. + int ForEachModule(const Target& target, ModuleCallback callback, void* context); + + // Walks AppDomain -> AssemblyList -> Assembly -> Module -> PEImageLayout and + // reports each module's loaded image range ([Base, Base+Size)) with kind + // "module-image". Returns the number of regions reported, or -1 if the + // AppDomain could not be located. + int EnumerateModuleRegions(const Target& target, RegionCallback sink, void* sinkContext); +} +} // namespace contracts + +#endif // CDACLITE_CONTRACTS_LOADER_H diff --git a/src/coreclr/debug/cdaclite/contracts/loaderheaps.cpp b/src/coreclr/debug/cdaclite/contracts/loaderheaps.cpp new file mode 100644 index 00000000000000..8fe96cef302992 --- /dev/null +++ b/src/coreclr/debug/cdaclite/contracts/loaderheaps.cpp @@ -0,0 +1,148 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +//***************************************************************************** +// loaderheaps.cpp +// +// Implementation of the LoaderAllocator heaps walk declared in loaderheaps.h. +//***************************************************************************** + +#include "loaderheaps.h" +#include "loader.h" // ForEachModule +#include "runtimetypes.h" + +#include + +namespace cdac +{ +namespace contracts +{ + namespace + { + // Global pointing at the SystemDomain*: cdac_data::SystemDomainPtr. + const char* const GlobalSystemDomain = "SystemDomain"; + + const int MaxHeapBlocks = 1000000; + + struct CollectState + { + const Target* target; + std::set* allocators; + }; + + // ForEachModule callback: record each Module's LoaderAllocator. + void CollectModuleAllocator(void* context, uint64_t moduleAddr) + { + CollectState* state = (CollectState*)context; + data::Module module; + if (state->target->TryRead(moduleAddr, module) && module.LoaderAllocator != 0) + { + state->allocators->insert(module.LoaderAllocator); + } + } + + // Walks one LoaderHeap's block list, reporting each block's committed range. + int WalkLoaderHeap(const Target& target, uint64_t heapAddr, + std::set& visitedBlocks, RegionCallback sink, void* sinkContext) + { + if (heapAddr == 0) + { + return 0; + } + + data::LoaderHeap heap; + if (!target.TryRead(heapAddr, heap)) + { + return 0; + } + + int count = 0; + uint64_t block = heap.FirstBlock; + for (int i = 0; block != 0 && i < MaxHeapBlocks; i++) + { + if (!visitedBlocks.insert(block).second) + { + break; // cycle + } + + data::LoaderHeapBlock heapBlock; + if (!target.TryRead(block, heapBlock)) + { + break; + } + + if (heapBlock.VirtualAddress != 0 && heapBlock.VirtualSize != 0) + { + sink(sinkContext, "loader-heap", heapBlock.VirtualAddress, heapBlock.VirtualSize); + count++; + } + + block = heapBlock.Next; + } + return count; + } + + int WalkAllocator(const Target& target, uint64_t allocatorAddr, + std::set& visitedBlocks, RegionCallback sink, void* sinkContext) + { + data::LoaderAllocator allocator; + if (!target.TryRead(allocatorAddr, allocator)) + { + return 0; + } + + const uint64_t heaps[] = { + allocator.HighFrequencyHeap, + allocator.LowFrequencyHeap, + allocator.StaticsHeap, + allocator.StubHeap, + allocator.ExecutableHeap, + allocator.FixupPrecodeHeap, + allocator.NewStubPrecodeHeap, + allocator.DynamicHelpersStubHeap, + }; + + int count = 0; + for (uint64_t heap : heaps) + { + count += WalkLoaderHeap(target, heap, visitedBlocks, sink, sinkContext); + } + return count; + } + } + + int EnumerateLoaderHeapRegions(const Target& target, RegionCallback sink, void* sinkContext) + { + std::set allocators; + + // The global loader allocator is embedded in the SystemDomain. + uint64_t systemDomainAddr = 0; + if (target.TryReadGlobalPointer(GlobalSystemDomain, systemDomainAddr) && systemDomainAddr != 0) + { + data::SystemDomain systemDomain; + if (target.TryRead(systemDomainAddr, systemDomain) && systemDomain.GlobalLoaderAllocator != 0) + { + allocators.insert(systemDomain.GlobalLoaderAllocator); + } + } + + // Per-module loader allocators (collectible assemblies each have their own). + CollectState collect; + collect.target = ⌖ + collect.allocators = &allocators; + ForEachModule(target, &CollectModuleAllocator, &collect); + + if (allocators.empty()) + { + return -1; + } + + std::set visitedBlocks; + int total = 0; + for (uint64_t allocatorAddr : allocators) + { + total += WalkAllocator(target, allocatorAddr, visitedBlocks, sink, sinkContext); + } + return total; + } +} +} // namespace contracts diff --git a/src/coreclr/debug/cdaclite/contracts/loaderheaps.h b/src/coreclr/debug/cdaclite/contracts/loaderheaps.h new file mode 100644 index 00000000000000..f54a213fcd1aa2 --- /dev/null +++ b/src/coreclr/debug/cdaclite/contracts/loaderheaps.h @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +//***************************************************************************** +// loaderheaps.h +// +// LoaderAllocator heaps contract: reports the runtime's loader heap blocks +// (high/low-frequency, statics, stub, executable, precode heaps). These hold the +// type system (MethodTables, MethodDescs) and executable stubs/precode -- some of +// which are RX and thus missed by a private-read-write memory capture. Modeled on +// the DAC EnumMemDumpAppDomainInfo -> LoaderAllocator::EnumMemoryRegions path. +//***************************************************************************** + +#ifndef CDACLITE_CONTRACTS_LOADERHEAPS_H +#define CDACLITE_CONTRACTS_LOADERHEAPS_H + +#include + +#include "contracts.h" +#include "target.h" + +namespace cdac +{ +namespace contracts +{ + // Collects LoaderAllocators (SystemDomain global + per-module) and walks each + // LoaderHeap's block list, reporting each block ([VirtualAddress, +VirtualSize)) + // with kind "loader-heap". Returns the number of regions reported, or -1 on + // failure to locate the loader allocators. + int EnumerateLoaderHeapRegions(const Target& target, RegionCallback sink, void* sinkContext); +} +} // namespace contracts + +#endif // CDACLITE_CONTRACTS_LOADERHEAPS_H diff --git a/src/coreclr/debug/cdaclite/contracts/stackscan.cpp b/src/coreclr/debug/cdaclite/contracts/stackscan.cpp new file mode 100644 index 00000000000000..48d1577d0882c6 --- /dev/null +++ b/src/coreclr/debug/cdaclite/contracts/stackscan.cpp @@ -0,0 +1,369 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +//***************************************************************************** +// stackscan.cpp +// +// Implementation of the Normal-tier conservative stack scan declared in +// stackscan.h. Ports the ExecutionManager RangeSectionMap radix lookup and the +// EEJitManager NibbleMap (IP -> method code) so that, for each code pointer found +// on a thread stack, only that method's code + header + MethodDesc is captured -- +// avoiding the bulk emission of every JIT/loader heap. +//***************************************************************************** + +#include "stackscan.h" +#include "runtimetypes.h" + +#include + +namespace cdac +{ +namespace contracts +{ + namespace + { + const char* const GlobalThreadStore = "ThreadStore"; + const char* const GlobalCodeRangeMap = "ExecutionManagerCodeRangeMapAddress"; + const char* const GlobalStubCodeBlockLast = "StubCodeBlockLast"; + + const int MaxThreads = 100000; + const int MaxFragments = 100000; + + // RangeSection.Flags + const int32_t RangeSectionFlag_CodeHeap = 0x02; + + // NibbleMap constants (see NibbleMapHelpers.cs). + const uint64_t MapUnitSizeInBytes = 4; + const uint64_t MapUnitSizeInNibbles = 8; + const uint64_t BytesPerBucket = 8 * MapUnitSizeInBytes; // 32 + const uint32_t NibbleMask = 0x0Fu; + + // Bound how much code we emit around a stack IP (covers the method body + code header). + const uint32_t MaxMethodCodeEmit = 64 * 1024; + // Conservative fixed emission for a MethodDesc (its exact size is chunk-dependent). + const uint32_t MethodDescEmit = 512; + + // --- RangeSectionMap radix lookup (ExecutionManagerHelpers.RangeSectionMap) --------- + + int MapLevels(const Target& target) { return target.PointerSize() == 8 ? 5 : 2; } + int MaxSetBit(const Target& target) { return target.PointerSize() == 8 ? 56 : 31; } + + int GetIndexForLevel(const Target& target, uint64_t address, int level) + { + const int bitsPerLevel = 8; + uint64_t used = address >> (MaxSetBit(target) + 1 - (MapLevels(target) * bitsPerLevel)); + uint64_t shifted = used >> ((level - 1) * bitsPerLevel); + return (int)(255u & shifted); + } + + // Walks the radix map for 'addr' and returns the RangeSection covering it, or 0. + // Emits each map-node pointer slot it reads (the reader re-walks the same slots). + uint64_t FindRangeSection(const Target& target, uint64_t topMap, uint64_t addr, + RegionCallback sink, void* sinkContext) + { + const uint64_t ptrSize = target.PointerSize(); + uint64_t levelMap = topMap; + int level = MapLevels(target); + uint64_t firstFragment = 0; + for (;;) + { + int index = GetIndexForLevel(target, addr, level); + uint64_t slot = levelMap + (uint64_t)index * ptrSize; + uint64_t value = 0; + if (!target.TryReadPointer(slot, value)) + { + return 0; + } + // The reader reads this same slot during its own lookup. + sink(sinkContext, "code-rangemap", slot, (uint64_t)ptrSize); + value &= ~1ull; // low bit is the collectible flag + if (level == 1) + { + firstFragment = value; + break; + } + if (value == 0) + { + return 0; + } + levelMap = value; + level--; + } + + // Walk the fragment list at the leaf to find the covering RangeSection. + uint64_t fragment = firstFragment; + for (int i = 0; fragment != 0 && i < MaxFragments; i++) + { + data::RangeSectionFragment f; + if (!target.TryRead(fragment, f)) // struct read -> captured by EnumMem + { + break; + } + if (addr >= f.RangeBegin && addr < f.RangeEndOpen) + { + return f.RangeSection; + } + fragment = f.Next; + } + return 0; + } + + // --- NibbleMap: IP -> start of the containing method's code (NibbleMapConstantLookup) -- + + uint32_t ReadMapUnit(const Target& target, uint64_t mapStart, uint64_t mapIdx, + RegionCallback sink, void* sinkContext) + { + uint64_t unitAddr = mapStart + (mapIdx / MapUnitSizeInNibbles) * MapUnitSizeInBytes; + uint32_t value = 0; + target.TryReadUInt32(unitAddr, value); + // The reader re-reads the same map units. + sink(sinkContext, "code-nibblemap", unitAddr, (uint64_t)MapUnitSizeInBytes); + return value; + } + + bool IsPointerUnit(uint32_t unit) { return (unit & NibbleMask) > 8; } + + uint64_t DecodePointer(uint64_t baseAddress, uint32_t unit) + { + uint32_t nibble = unit & NibbleMask; + uint32_t relativePointer = (unit & ~NibbleMask) + ((nibble - 9) << 2); + return baseAddress + relativePointer; + } + + uint32_t GetNibbleShift(uint64_t mapIdx) + { + uint32_t nibbleIndexInMapUnit = (uint32_t)(mapIdx & (MapUnitSizeInNibbles - 1)); + return 28 - (nibbleIndexInMapUnit * 4); + } + + uint64_t GetAbsoluteAddress(uint64_t baseAddress, uint64_t mapIdx, uint32_t nibble) + { + uint64_t mapIdxByteOffset = mapIdx * BytesPerBucket; + uint64_t nibbleByteOffset = (uint64_t)(nibble - 1) * MapUnitSizeInBytes; + return baseAddress + mapIdxByteOffset + nibbleByteOffset; + } + + // Returns the code start address for 'currentPC', or 0. Faithful port of + // NibbleMapConstantLookup.FindMethodCode. + uint64_t NibbleMapFindMethodCode(const Target& target, uint64_t mapBase, uint64_t mapStart, + uint64_t currentPC, RegionCallback sink, void* sinkContext) + { + uint64_t relativeAddress = currentPC - mapBase; + uint64_t mapIdx = relativeAddress / BytesPerBucket; + uint32_t bucketByteIndex = (uint32_t)((relativeAddress & (BytesPerBucket - 1)) / MapUnitSizeInBytes) + 1; + + uint32_t t = ReadMapUnit(target, mapStart, mapIdx, sink, sinkContext); + if (IsPointerUnit(t)) + { + return DecodePointer(mapBase, t); + } + + // Focus on the indexed nibble. + t = t >> GetNibbleShift(mapIdx); + uint32_t nibble = t & NibbleMask; + if (nibble != 0 && nibble <= bucketByteIndex) + { + return GetAbsoluteAddress(mapBase, mapIdx, nibble); + } + + // Search backwards through the current map unit. + t = t >> 4; // shift to next nibble + if (t != 0) + { + if (mapIdx == 0) + { + return 0; + } + mapIdx = mapIdx - 1; + while ((t & NibbleMask) == 0) + { + t = t >> 4; + if (mapIdx == 0) + { + break; + } + mapIdx = mapIdx - 1; + } + return GetAbsoluteAddress(mapBase, mapIdx, t & NibbleMask); + } + + // We finished the current map unit; if we were in the first, stop. + if (mapIdx < MapUnitSizeInNibbles) + { + return 0; + } + + // Align down to the map unit, then move back one nibble into the previous unit. + mapIdx = mapIdx & (~(MapUnitSizeInNibbles - 1)); + mapIdx = mapIdx - 1; + + t = ReadMapUnit(target, mapStart, mapIdx, sink, sinkContext); + if (t == 0) + { + return 0; + } + if (IsPointerUnit(t)) + { + return DecodePointer(mapBase, t); + } + + while (mapIdx != 0 && (t & NibbleMask) == 0) + { + t = t >> 4; + mapIdx = mapIdx - 1; + } + return GetAbsoluteAddress(mapBase, mapIdx, t & NibbleMask); + } + + // Resolves a code pointer 'ip' to its method and emits the code + header + MethodDesc. + // Returns the MethodDesc address (deduped by the caller), or 0 if not resolved. + uint64_t ResolveCodeIP(const Target& target, uint64_t codeRangeMap, uint64_t stubCodeBlockLast, + uint64_t ip, RegionCallback sink, void* sinkContext) + { + uint64_t rangeSectionAddr = FindRangeSection(target, codeRangeMap, ip, sink, sinkContext); + if (rangeSectionAddr == 0) + { + return 0; + } + + data::RangeSection rangeSection; + if (!target.TryRead(rangeSectionAddr, rangeSection)) + { + return 0; + } + + // Only EEJit code heaps are resolved here; R2R code + method info live in the module + // image (available to the reader from the on-disk binary). + if (((int32_t)rangeSection.Flags & RangeSectionFlag_CodeHeap) == 0 || rangeSection.HeapList == 0) + { + return 0; + } + + data::CodeHeapListNode heapNode; + if (!target.TryRead(rangeSection.HeapList, heapNode)) + { + return 0; + } + if (ip < heapNode.StartAddress || ip > heapNode.EndAddress) + { + return 0; + } + + uint64_t codeStart = NibbleMapFindMethodCode(target, heapNode.MapBase, heapNode.HeaderMap, ip, sink, sinkContext); + if (codeStart == 0 || codeStart > ip) + { + return 0; + } + + // The code header pointer is stored one pointer before the code start. + uint64_t codeHeaderIndirect = codeStart - target.PointerSize(); + uint64_t codeHeaderAddress = 0; + if (!target.TryReadPointer(codeHeaderIndirect, codeHeaderAddress) || codeHeaderAddress == 0) + { + return 0; + } + sink(sinkContext, "code-header-ptr", codeHeaderIndirect, (uint64_t)target.PointerSize()); + + // Stub code blocks have a small sentinel value instead of a real header. + if (codeHeaderAddress <= stubCodeBlockLast) + { + return 0; + } + + data::RealCodeHeader codeHeader; + if (!target.TryRead(codeHeaderAddress, codeHeader)) // captured by EnumMem + { + return 0; + } + + // Emit the method's code (code start through the IP, bounded). + uint64_t emitEnd = ip + target.PointerSize(); + uint64_t codeSize = emitEnd - codeStart; + if (codeSize > MaxMethodCodeEmit) + { + codeSize = MaxMethodCodeEmit; + } + sink(sinkContext, "jit-code", codeStart, codeSize); + + // Emit the MethodDesc (its exact size is chunk-dependent; a bounded blob suffices). + if (codeHeader.MethodDesc != 0) + { + sink(sinkContext, "methoddesc", codeHeader.MethodDesc, MethodDescEmit); + } + + return codeHeader.MethodDesc; + } + } + + int EnumerateStackScanRegions(const Target& target, RegionCallback sink, void* sinkContext) + { + uint64_t codeRangeMapPtr = 0; + if (!target.TryReadGlobalPointer(GlobalCodeRangeMap, codeRangeMapPtr) || codeRangeMapPtr == 0) + { + return -1; + } + // The global points at the RangeSectionMap; its TopLevelData is the radix map root. + data::RangeSectionMap rangeSectionMap; + if (!target.TryRead(codeRangeMapPtr, rangeSectionMap) || rangeSectionMap.TopLevelData == 0) + { + return -1; + } + uint64_t topMap = rangeSectionMap.TopLevelData; + + uint64_t stubCodeBlockLast = 0; + target.TryGetGlobalValue(GlobalStubCodeBlockLast, stubCodeBlockLast); + + uint64_t threadStoreAddr = 0; + if (!target.TryReadGlobalPointer(GlobalThreadStore, threadStoreAddr) || threadStoreAddr == 0) + { + return -1; + } + data::ThreadStore threadStore; + if (!target.TryRead(threadStoreAddr, threadStore)) + { + return -1; + } + + const uint64_t ptrSize = target.PointerSize(); + std::set visitedThreads; + std::set capturedMethods; + + uint64_t threadAddr = threadStore.FirstThreadLink; + for (int i = 0; threadAddr != 0 && i < MaxThreads; i++) + { + if (!visitedThreads.insert(threadAddr).second) + { + break; + } + data::Thread thread; + if (!target.TryRead(threadAddr, thread)) + { + break; + } + + uint64_t stackLow = thread.CachedStackLimit; + uint64_t stackHigh = thread.CachedStackBase; + if (stackLow != 0 && stackHigh > stackLow) + { + // Conservatively scan every pointer-aligned slot for code pointers. + for (uint64_t sp = stackLow; sp + ptrSize <= stackHigh; sp += ptrSize) + { + uint64_t value = 0; + if (!target.TryReadPointer(sp, value) || value == 0) + { + continue; + } + uint64_t methodDesc = ResolveCodeIP(target, topMap, stubCodeBlockLast, value, sink, sinkContext); + if (methodDesc != 0) + { + capturedMethods.insert(methodDesc); + } + } + } + + threadAddr = thread.LinkNext; + } + + return (int)capturedMethods.size(); + } +} +} // namespace contracts diff --git a/src/coreclr/debug/cdaclite/contracts/stackscan.h b/src/coreclr/debug/cdaclite/contracts/stackscan.h new file mode 100644 index 00000000000000..36834ac9fb2ee8 --- /dev/null +++ b/src/coreclr/debug/cdaclite/contracts/stackscan.h @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +//***************************************************************************** +// stackscan.h +// +// Normal-tier conservative stack scan. Instead of bulk-emitting every JIT code +// heap and loader heap (which does not scale to large programs), this walks each +// managed thread's stack, and for every pointer-aligned value that resolves to +// managed code (via the ExecutionManager RangeSectionMap), emits just that +// method's code + code header + MethodDesc chain -- the analog of the DAC's +// stack-walk-driven DumpAllInstances for MiniDumpNormal. +//***************************************************************************** + +#ifndef CDACLITE_CONTRACTS_STACKSCAN_H +#define CDACLITE_CONTRACTS_STACKSCAN_H + +#include + +#include "contracts.h" +#include "target.h" + +namespace cdac +{ +namespace contracts +{ + // Scans all managed thread stacks and captures the code + method metadata reachable from + // code pointers found on them. Returns the number of distinct methods captured, or -1 if + // the ExecutionManager range map could not be located. + int EnumerateStackScanRegions(const Target& target, RegionCallback sink, void* sinkContext); +} +} // namespace contracts + +#endif // CDACLITE_CONTRACTS_STACKSCAN_H diff --git a/src/coreclr/debug/cdaclite/contracts/statics.cpp b/src/coreclr/debug/cdaclite/contracts/statics.cpp new file mode 100644 index 00000000000000..1dd4fe831ef012 --- /dev/null +++ b/src/coreclr/debug/cdaclite/contracts/statics.cpp @@ -0,0 +1,114 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +//***************************************************************************** +// statics.cpp +// +// Implementation of the contract-bootstrap walk declared in statics.h. +//***************************************************************************** + +#include "statics.h" +#include "datadescriptor.h" + +#include +#include +#include + +namespace cdac +{ +namespace contracts +{ + namespace + { + // Reports one ContractDescriptor's struct + JSON + pointer_data. + int EmitDescriptor(const Target& target, uint64_t address, RegionCallback sink, void* sinkContext) + { + const uint32_t ptrSize = target.PointerSize(); + + // ContractDescriptor layout (contract-descriptor.h): + // +0 magic (8) +8 flags (4) +12 descriptor_size (4) + // +16 descriptor (ptr) +16+ptr pointer_data_count (4) +pad +pointer_data (ptr) + uint32_t descriptorSize = 0; + uint64_t descriptorPtr = 0; + uint32_t pointerDataCount = 0; + uint64_t pointerDataPtr = 0; + target.TryReadUInt32(address + 12, descriptorSize); + target.TryReadPointer(address + 16, descriptorPtr); + target.TryReadUInt32(address + 16 + ptrSize, pointerDataCount); + target.TryReadPointer(address + 16 + ptrSize + 8, pointerDataPtr); + + int count = 0; + + // The descriptor struct itself. + sink(sinkContext, "contract-descriptor", address, 16 + 2 * ptrSize + 8); + count++; + + // The UTF-8 JSON data descriptor. + if (descriptorPtr != 0 && descriptorSize != 0) + { + sink(sinkContext, "descriptor-json", descriptorPtr, descriptorSize); + count++; + } + + // The pointer_data array: holds the addresses of the indirect globals. + if (pointerDataPtr != 0 && pointerDataCount != 0) + { + sink(sinkContext, "pointer-data", pointerDataPtr, (uint64_t)pointerDataCount * ptrSize); + count++; + } + + return count; + } + } + + int EnumerateStaticRegions(const Target& target, uint64_t contractDescriptorAddr, + RegionCallback sink, void* sinkContext) + { + if (contractDescriptorAddr == 0) + { + return 0; + } + + int count = 0; + const uint32_t ptrSize = target.PointerSize(); + const DataDescriptor* descriptor = target.Descriptor(); + + // Emit every ContractDescriptor the reader loaded: the main descriptor AND + // each recursively-loaded sub-descriptor (e.g. the GC descriptor). Without the + // sub-descriptors, a contract tool reading the dump could follow the + // sub-descriptor pointer but find no descriptor/JSON/pointer_data there. + if (descriptor != nullptr && !descriptor->DescriptorAddresses().empty()) + { + const std::vector& addresses = descriptor->DescriptorAddresses(); + for (size_t i = 0; i < addresses.size(); i++) + { + count += EmitDescriptor(target, addresses[i], sink, sinkContext); + } + } + else + { + // Fall back to just the main descriptor if the address list is unavailable. + count += EmitDescriptor(target, contractDescriptorAddr, sink, sinkContext); + } + + // The global variable storage referenced by pointer_data. A contract tool + // reading the dump resolves an indirect global by dereferencing its + // pointer_data entry, so the memory at each of those addresses must be present. + // (Merged globals include both main and sub-descriptor globals.) + if (descriptor != nullptr) + { + const std::map& globals = descriptor->Globals(); + for (std::map::const_iterator it = globals.begin(); it != globals.end(); ++it) + { + const GlobalInfo& global = it->second; + if (global.isAddress && global.numericValue != 0) + { + sink(sinkContext, "global-var", global.numericValue, ptrSize); + count++; + } + } + } + + return count; + } +} +} // namespace contracts diff --git a/src/coreclr/debug/cdaclite/contracts/statics.h b/src/coreclr/debug/cdaclite/contracts/statics.h new file mode 100644 index 00000000000000..4569c4a9cbc42d --- /dev/null +++ b/src/coreclr/debug/cdaclite/contracts/statics.h @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +//***************************************************************************** +// statics.h +// +// Contract bootstrap contract: reports the runtime's self-description so any +// contract-based tool can re-bootstrap the data contracts from the dump alone. +// +// cdac-lite is a DAC replacement, so it does NOT report the legacy DAC globals +// table. It reports only what the data-contract system needs: +// 1. the contract descriptor struct +// 2. the UTF-8 JSON data descriptor +// 3. the pointer_data table (addresses of the indirect globals) +// 4. the global variable storage those pointer_data entries reference +//***************************************************************************** + +#ifndef CDACLITE_CONTRACTS_STATICS_H +#define CDACLITE_CONTRACTS_STATICS_H + +#include + +#include "contracts.h" +#include "target.h" + +namespace cdac +{ +namespace contracts +{ + // Reports the contract descriptor (kinds "contract-descriptor", "descriptor-json", + // "pointer-data") and the global variable storage referenced by pointer_data + // (kind "global-var"). Returns the number of regions reported. + int EnumerateStaticRegions(const Target& target, uint64_t contractDescriptorAddr, + RegionCallback sink, void* sinkContext); +} +} // namespace contracts + +#endif // CDACLITE_CONTRACTS_STATICS_H diff --git a/src/coreclr/debug/cdaclite/contracts/stresslog.cpp b/src/coreclr/debug/cdaclite/contracts/stresslog.cpp new file mode 100644 index 00000000000000..a1a08c10031fcc --- /dev/null +++ b/src/coreclr/debug/cdaclite/contracts/stresslog.cpp @@ -0,0 +1,116 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +//***************************************************************************** +// stresslog.cpp +// +// Implementation of the stress-log enumeration declared in stresslog.h. +//***************************************************************************** + +#include "stresslog.h" +#include "runtimetypes.h" + +#include + +namespace cdac +{ +namespace contracts +{ + namespace + { + // Stress-log globals (src/coreclr/vm/datadescriptor/datadescriptor.inc). + const char* const GlobalStressLogEnabled = "StressLogEnabled"; // direct byte + const char* const GlobalStressLog = "StressLog"; // resolved -> StressLog struct + const char* const GlobalChunkSize = "StressLogChunkSize"; // direct: bytes per chunk buffer + + const int MaxThreadLogs = 100000; + const int MaxChunksPerThread = 1000000; + } + + int EnumerateStressLogRegions(const Target& target) + { + // Stress log is opt-in; if disabled there is nothing to capture. + // COPILOT TODO: We shouldn't need this fallback for different sized things. Either it is pointer sized or a set size. + uint64_t enabled = 0; + if (!target.TryGetGlobalValue(GlobalStressLogEnabled, enabled)) + { + // Fall back to reading it as a byte at the global's address. + uint32_t enabled32 = 0; + if (target.TryReadGlobalUInt32(GlobalStressLogEnabled, enabled32)) + { + enabled = enabled32 & 0xFF; + } + } + if (enabled == 0) + { + return 0; + } + + // The StressLog struct: its resolved global value is the struct address (the cDAC uses + // ReadGlobalPointer(StressLog) directly as the struct address). + uint64_t stressLogAddr = 0; + if (!target.TryGetGlobalValue(GlobalStressLog, stressLogAddr) || stressLogAddr == 0) + { + return -1; + } + data::StressLog stressLog; + if (!target.TryRead(stressLogAddr, stressLog)) + { + return -1; + } + + uint64_t chunkSize = 0; + target.TryGetGlobalValue(GlobalChunkSize, chunkSize); + // Emit the whole chunk (header + message buffer + trailing signatures). A generous header + // pad covers the fields around the inline Buf array. + uint32_t chunkEmitSize = (uint32_t)chunkSize + 8u * (uint32_t)target.PointerSize(); + + std::set visitedThreads; + std::set visitedChunks; + int chunkCount = 0; + + uint64_t threadLog = stressLog.Logs; + for (int t = 0; threadLog != 0 && t < MaxThreadLogs; t++) + { + if (!visitedThreads.insert(threadLog).second) + { + break; + } + data::ThreadStressLog tsl; + if (!target.TryRead(threadLog, tsl)) + { + break; + } + + // Walk the chunk ring (ChunkListHead -> Next), capturing each chunk's buffer. + uint64_t chunk = tsl.ChunkListHead; + for (int c = 0; chunk != 0 && c < MaxChunksPerThread; c++) + { + if (!visitedChunks.insert(chunk).second) + { + break; // ring wrapped + } + data::StressLogChunk chunkData; + if (!target.TryRead(chunk, chunkData)) + { + break; + } + if (chunkEmitSize > 0) + { + target.EmitMemory(chunk, chunkEmitSize); + } + chunkCount++; + + if (chunk == tsl.ChunkListTail) + { + break; + } + chunk = chunkData.Next; + } + + threadLog = tsl.Next; + } + + return chunkCount; + } +} +} // namespace contracts diff --git a/src/coreclr/debug/cdaclite/contracts/stresslog.h b/src/coreclr/debug/cdaclite/contracts/stresslog.h new file mode 100644 index 00000000000000..37b80fb257f819 --- /dev/null +++ b/src/coreclr/debug/cdaclite/contracts/stresslog.h @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +//***************************************************************************** +// stresslog.h +// +// StressLog contract: enumerates the per-thread stress logs and their message +// chunk buffers. Modeled on the managed cDAC StressLog contract +// (Contracts/StressLog.cs). Only meaningful when the stress log is enabled. +//***************************************************************************** + +#ifndef CDACLITE_CONTRACTS_STRESSLOG_H +#define CDACLITE_CONTRACTS_STRESSLOG_H + +#include + +#include "contracts.h" +#include "target.h" + +namespace cdac +{ +namespace contracts +{ + // Reads the StressLog, the ThreadStressLog list, and each StressLogChunk (including its + // message buffer) so the cDAC can re-read them from the dump. Memory is captured via the + // Target's EnumMem sink. Returns the number of chunks captured, 0 if the stress log is + // disabled, or -1 if the stress log could not be located. + int EnumerateStressLogRegions(const Target& target); +} +} // namespace contracts + +#endif // CDACLITE_CONTRACTS_STRESSLOG_H diff --git a/src/coreclr/debug/cdaclite/contracts/syncblock.cpp b/src/coreclr/debug/cdaclite/contracts/syncblock.cpp new file mode 100644 index 00000000000000..1ae82c64d6e2ed --- /dev/null +++ b/src/coreclr/debug/cdaclite/contracts/syncblock.cpp @@ -0,0 +1,91 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +//***************************************************************************** +// syncblock.cpp +// +// Implementation of the sync-block enumeration declared in syncblock.h. +//***************************************************************************** + +#include "syncblock.h" +#include "runtimetypes.h" + +namespace cdac +{ +namespace contracts +{ + namespace + { + // Sync-block globals (src/coreclr/vm/datadescriptor/datadescriptor.inc). + const char* const GlobalSyncTableEntries = "SyncTableEntries"; // &g_pSyncTable (ptr-ptr) + const char* const GlobalSyncBlockCache = "SyncBlockCache"; // &s_pSyncBlockCache (ptr-ptr) + + const uint32_t MaxSyncBlocks = 4u * 1024 * 1024; + } + + int EnumerateSyncBlockRegions(const Target& target) + { + // The sync-block cache holds the number of used entries (FreeSyncTableIndex). + uint64_t cacheAddr = 0; + if (!target.TryReadGlobalPointer(GlobalSyncBlockCache, cacheAddr) || cacheAddr == 0) + { + return -1; + } + data::SyncBlockCache cache; + if (!target.TryRead(cacheAddr, cache)) + { + return -1; + } + + uint32_t freeIndex = (uint32_t)cache.FreeSyncTableIndex; + if (freeIndex == 0) + { + return 0; + } + uint32_t count = freeIndex - 1; + if (count > MaxSyncBlocks) + { + count = MaxSyncBlocks; + } + + // The sync table itself: an array of SyncTableEntry indexed by sync-block index. + uint64_t tableAddr = 0; + if (!target.TryReadGlobalPointer(GlobalSyncTableEntries, tableAddr) || tableAddr == 0) + { + return -1; + } + uint32_t entrySize = 0; + if (!target.TryGetTypeSize(data::SyncTableEntry().TypeName(), entrySize) || entrySize == 0) + { + return -1; + } + + // Emit the whole entry array (indices 0..count) that the cDAC indexes into. + target.EmitMemory(tableAddr, (count + 1) * entrySize); + + int inUse = 0; + for (uint32_t index = 1; index <= count; index++) + { + uint64_t entryAddr = tableAddr + (uint64_t)index * entrySize; + data::SyncTableEntry entry; + if (!target.TryRead(entryAddr, entry)) + { + continue; + } + + // Low bit of Object set => the entry is free. + if ((entry.Object & 1) != 0 || entry.SyncBlock == 0) + { + continue; + } + + data::SyncBlock syncBlock; + if (target.TryRead(entry.SyncBlock, syncBlock)) + { + inUse++; + } + } + + return inUse; + } +} +} // namespace contracts diff --git a/src/coreclr/debug/cdaclite/contracts/syncblock.h b/src/coreclr/debug/cdaclite/contracts/syncblock.h new file mode 100644 index 00000000000000..f113627515d737 --- /dev/null +++ b/src/coreclr/debug/cdaclite/contracts/syncblock.h @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +//***************************************************************************** +// syncblock.h +// +// SyncBlock contract: enumerates the sync-block table (SyncTableEntry array), +// the SyncBlockCache, and the SyncBlock structs for in-use entries. Modeled on +// the managed cDAC SyncBlock_1 contract (Contracts/SyncBlock_1.cs). +//***************************************************************************** + +#ifndef CDACLITE_CONTRACTS_SYNCBLOCK_H +#define CDACLITE_CONTRACTS_SYNCBLOCK_H + +#include + +#include "contracts.h" +#include "target.h" + +namespace cdac +{ +namespace contracts +{ + // Reads the SyncBlockCache + SyncTableEntry array and each in-use SyncBlock so the + // cDAC's SyncBlock contract can re-read them from the dump. Memory is captured via the + // Target's EnumMem sink (no explicit region kind). Returns the number of in-use sync + // blocks captured, or -1 if the sync table could not be located. + int EnumerateSyncBlockRegions(const Target& target); +} +} // namespace contracts + +#endif // CDACLITE_CONTRACTS_SYNCBLOCK_H diff --git a/src/coreclr/debug/cdaclite/contracts/thread.cpp b/src/coreclr/debug/cdaclite/contracts/thread.cpp new file mode 100644 index 00000000000000..ef1c59d16205b6 --- /dev/null +++ b/src/coreclr/debug/cdaclite/contracts/thread.cpp @@ -0,0 +1,73 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +//***************************************************************************** +// thread.cpp +// +// Implementation of the Thread stack-region walk declared in thread.h. +//***************************************************************************** + +#include "thread.h" +#include "runtimetypes.h" + +#include + +namespace cdac +{ +namespace contracts +{ + namespace + { + // Global pointing at the ThreadStore*: &ThreadStore::s_pThreadStore. + const char* const GlobalThreadStore = "ThreadStore"; + + const int MaxThreads = 100000; // guard against corrupt thread lists + } + + int EnumerateThreadRegions(const Target& target, RegionCallback sink, void* sinkContext) + { + // ThreadStore global is a pointer-to-pointer: deref once to get the ThreadStore. + uint64_t threadStoreAddr = 0; + if (!target.TryReadGlobalPointer(GlobalThreadStore, threadStoreAddr) || threadStoreAddr == 0) + { + return -1; + } + + data::ThreadStore threadStore; + if (!target.TryRead(threadStoreAddr, threadStore)) + { + return -1; + } + + std::set visited; + int count = 0; + uint64_t threadAddr = threadStore.FirstThreadLink; // head Thread* + + for (int i = 0; threadAddr != 0 && i < MaxThreads; i++) + { + if (!visited.insert(threadAddr).second) + { + break; // cycle + } + + data::Thread thread; + if (!target.TryRead(threadAddr, thread)) + { + break; + } + + // Stack grows down: CachedStackLimit is the low address, CachedStackBase + // the high address. Report the committed stack range. + if (thread.CachedStackBase > thread.CachedStackLimit && thread.CachedStackLimit != 0) + { + sink(sinkContext, "thread-stack", thread.CachedStackLimit, + thread.CachedStackBase - thread.CachedStackLimit); + count++; + } + + threadAddr = thread.LinkNext; + } + + return count; + } +} +} // namespace contracts diff --git a/src/coreclr/debug/cdaclite/contracts/thread.h b/src/coreclr/debug/cdaclite/contracts/thread.h new file mode 100644 index 00000000000000..b549c54f8c7532 --- /dev/null +++ b/src/coreclr/debug/cdaclite/contracts/thread.h @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +//***************************************************************************** +// thread.h +// +// Thread contract: walks the ThreadStore's thread list and reports each managed +// thread's stack range. Modeled on the managed cDAC Thread contract +// (docs/design/datacontracts/Thread.md, Contracts/Thread_1.cs). +//***************************************************************************** + +#ifndef CDACLITE_CONTRACTS_THREAD_H +#define CDACLITE_CONTRACTS_THREAD_H + +#include + +#include "contracts.h" +#include "target.h" + +namespace cdac +{ +namespace contracts +{ + // Walks ThreadStore -> Thread list, reporting each thread's stack range + // ([CachedStackLimit, CachedStackBase)) with kind "thread-stack". Returns the + // number of regions reported, or -1 if the ThreadStore could not be located. + int EnumerateThreadRegions(const Target& target, RegionCallback sink, void* sinkContext); +} +} // namespace contracts + +#endif // CDACLITE_CONTRACTS_THREAD_H diff --git a/src/coreclr/debug/cdaclite/data/CMakeLists.txt b/src/coreclr/debug/cdaclite/data/CMakeLists.txt new file mode 100644 index 00000000000000..5ca264e6141fde --- /dev/null +++ b/src/coreclr/debug/cdaclite/data/CMakeLists.txt @@ -0,0 +1,9 @@ +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +add_library_clr(cdacdata STATIC + datadescriptor.cpp + target.cpp +) + +target_include_directories(cdacdata PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_link_libraries(cdacdata PUBLIC cdacjson) diff --git a/src/coreclr/debug/cdaclite/data/datadescriptor.cpp b/src/coreclr/debug/cdaclite/data/datadescriptor.cpp new file mode 100644 index 00000000000000..6a4e4e5ba9819d --- /dev/null +++ b/src/coreclr/debug/cdaclite/data/datadescriptor.cpp @@ -0,0 +1,464 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +//***************************************************************************** +// datadescriptor.cpp +// +// Implementation of the DataDescriptor model declared in datadescriptor.h. +//***************************************************************************** + +#include "datadescriptor.h" + +#include +#include +#include + +namespace cdac +{ + namespace + { + // Contract descriptor magic: "DNCCDAC\0" in target (== native) endianness. + const uint64_t ContractDescriptorMagic = 0x0043414443434e44ull; + // Guard against reading absurd JSON blobs from a possibly-corrupt target. + const uint32_t MaxDescriptorSize = 8 * 1024 * 1024; + // Guard against sub-descriptor cycles. + const int MaxSubDescriptorDepth = 8; + } + + const TypeInfo* DataDescriptor::FindType(const std::string& name) const + { + std::map::const_iterator it = m_types.find(name); + return (it == m_types.end()) ? nullptr : &it->second; + } + + bool DataDescriptor::TryGetFieldOffset(const std::string& typeName, const std::string& fieldName, uint32_t& offset) const + { + const TypeInfo* type = FindType(typeName); + if (type == nullptr) + { + return false; + } + std::map::const_iterator it = type->fields.find(fieldName); + if (it == type->fields.end()) + { + return false; + } + offset = it->second.offset; + return true; + } + + bool DataDescriptor::TryGetGlobalValue(const std::string& name, uint64_t& value) const + { + std::map::const_iterator it = m_globals.find(name); + if (it == m_globals.end() || it->second.isIndirect || !it->second.hasNumericValue) + { + return false; + } + value = it->second.numericValue; + return true; + } + + void DataDescriptor::ParseTypes(const json::Value& types) + { + if (!types.IsObject()) + { + return; + } + + for (std::map::const_iterator it = types.object.begin(); it != types.object.end(); ++it) + { + const std::string& typeName = it->first; + const json::Value& fieldDict = it->second; + if (!fieldDict.IsObject()) + { + continue; + } + + TypeInfo info; + for (std::map::const_iterator f = fieldDict.object.begin(); f != fieldDict.object.end(); ++f) + { + const std::string& key = f->first; + const json::Value& fieldValue = f->second; + + // The special "!" key gives the total size of the struct. + if (key == "!") + { + uint64_t size = 0; + if (fieldValue.TryGetUInt64(size)) + { + info.hasSize = true; + info.size = (uint32_t)size; + } + continue; + } + + FieldInfo field; + if (fieldValue.IsArray()) + { + // [offset, "type"] + if (fieldValue.array.size() >= 1) + { + uint64_t offset = 0; + if (fieldValue.array[0].TryGetUInt64(offset)) + { + field.offset = (uint32_t)offset; + } + } + if (fieldValue.array.size() >= 2 && fieldValue.array[1].IsString()) + { + field.type = fieldValue.array[1].string; + } + } + else + { + // Just an offset. + uint64_t offset = 0; + if (!fieldValue.TryGetUInt64(offset)) + { + continue; + } + field.offset = (uint32_t)offset; + } + + info.fields[key] = field; + } + + m_types[typeName] = info; + } + } + + void DataDescriptor::ParseGlobalValue(const json::Value& value, GlobalInfo& info) + { + // Grammar (compact form, see data_descriptor.md): + // = | | [ ] (1-elem array = indirect) + // with an optional type: [ , "type" ] + if (value.IsArray()) + { + if (value.array.size() == 1) + { + // [index] -> indirect value in the pointer_data array. + uint64_t index = 0; + if (value.array[0].TryGetUInt64(index)) + { + info.isIndirect = true; + info.pointerIndex = (uint32_t)index; + } + return; + } + if (value.array.size() >= 2) + { + // [value, "type"] where value itself may be [index], a number, or a string. + if (value.array[1].IsString()) + { + info.type = value.array[1].string; + } + const json::Value& inner = value.array[0]; + if (inner.IsArray() && inner.array.size() == 1) + { + uint64_t index = 0; + if (inner.array[0].TryGetUInt64(index)) + { + info.isIndirect = true; + info.pointerIndex = (uint32_t)index; + } + } + else + { + uint64_t numeric = 0; + if (inner.TryGetUInt64(numeric)) + { + info.hasNumericValue = true; + info.numericValue = numeric; + } + else if (inner.IsString()) + { + info.isString = true; + info.stringValue = inner.string; + } + } + } + return; + } + + uint64_t numeric = 0; + if (value.TryGetUInt64(numeric)) + { + info.hasNumericValue = true; + info.numericValue = numeric; + } + else if (value.IsString()) + { + info.isString = true; + info.stringValue = value.string; + } + } + + void DataDescriptor::ParseGlobals(const json::Value& globals) + { + if (!globals.IsObject()) + { + return; + } + + for (std::map::const_iterator it = globals.object.begin(); it != globals.object.end(); ++it) + { + GlobalInfo info; + ParseGlobalValue(it->second, info); + m_globals[it->first] = info; + } + } + + void DataDescriptor::ParseContracts(const json::Value& contracts) + { + if (!contracts.IsObject()) + { + return; + } + + for (std::map::const_iterator it = contracts.object.begin(); it != contracts.object.end(); ++it) + { + const json::Value& version = it->second; + if (version.IsNumber()) + { + m_contracts[it->first] = version.rawNumber; + } + else if (version.IsString()) + { + m_contracts[it->first] = version.string; + } + } + } + + void DataDescriptor::ParseSubDescriptors(const json::Value& subDescriptors) + { + if (!subDescriptors.IsObject()) + { + return; + } + + // Each sub-descriptor value is an indirect pointer, e.g. [[44],"pointer"] or [44]. + for (std::map::const_iterator it = subDescriptors.object.begin(); it != subDescriptors.object.end(); ++it) + { + GlobalInfo info; + ParseGlobalValue(it->second, info); + if (info.isIndirect) + { + m_subDescriptors[it->first] = info; + } + } + } + + void DataDescriptor::ResolveIndirectGlobals(const uint64_t* pointerData, uint32_t pointerDataCount) + { + for (std::map::iterator it = m_globals.begin(); it != m_globals.end(); ++it) + { + GlobalInfo& global = it->second; + if (!global.isIndirect) + { + continue; + } + if (pointerData != nullptr && global.pointerIndex < pointerDataCount) + { + global.numericValue = pointerData[global.pointerIndex]; + global.hasNumericValue = true; + global.isAddress = true; // numericValue is now a target variable address + } + else + { + global.hasNumericValue = false; + } + global.isIndirect = false; + } + } + + void DataDescriptor::Merge(const DataDescriptor& other) + { + for (std::map::const_iterator it = other.m_types.begin(); it != other.m_types.end(); ++it) + { + m_types.insert(*it); // existing entries take precedence + } + for (std::map::const_iterator it = other.m_globals.begin(); it != other.m_globals.end(); ++it) + { + m_globals.insert(*it); + } + for (std::map::const_iterator it = other.m_contracts.begin(); it != other.m_contracts.end(); ++it) + { + m_contracts.insert(*it); + } + } + + bool DataDescriptor::Load(ReadMemoryCallback readMemory, void* context, uint64_t contractDescriptorAddr, std::string& error) + { + m_types.clear(); + m_globals.clear(); + m_contracts.clear(); + m_subDescriptors.clear(); + m_descriptorAddresses.clear(); + return LoadMerged(readMemory, context, contractDescriptorAddr, 0, error); + } + + bool DataDescriptor::LoadMerged(ReadMemoryCallback readMemory, void* context, uint64_t address, int depth, std::string& error) + { + if (readMemory == nullptr) + { + error = "no read callback"; + return false; + } + if (depth > MaxSubDescriptorDepth) + { + return true; // stop recursing; not fatal + } + + // Read the ContractDescriptor header. Same-platform build: pointer size is native. + const uint32_t ptrSize = (uint32_t)sizeof(void*); + uint64_t magic = 0; + uint32_t descriptorSize = 0; + uint64_t descriptorPtr = 0; + uint32_t pointerDataCount = 0; + uint64_t pointerDataPtr = 0; + if (!readMemory(context, address + 0, &magic, sizeof(magic)) || + !readMemory(context, address + 12, &descriptorSize, sizeof(descriptorSize)) || + !readMemory(context, address + 16, &descriptorPtr, ptrSize) || + !readMemory(context, address + 16 + ptrSize, &pointerDataCount, sizeof(pointerDataCount)) || + !readMemory(context, address + 16 + ptrSize + 8, &pointerDataPtr, ptrSize)) + { + error = "failed to read contract descriptor header"; + return false; + } + + if (magic != ContractDescriptorMagic) + { + error = "bad contract descriptor magic"; + return false; + } + if (descriptorSize == 0 || descriptorSize > MaxDescriptorSize) + { + error = "descriptor_size out of range"; + return false; + } + + // Record this descriptor's address (main + each sub-descriptor) so the + // bootstrap contract can include every descriptor's struct/JSON/pointer_data. + m_descriptorAddresses.push_back(address); + + // Read the JSON blob and parse it into a temporary descriptor. + std::vector json(descriptorSize + 1); + if (!readMemory(context, descriptorPtr, json.data(), descriptorSize)) + { + error = "failed to read JSON descriptor blob"; + return false; + } + json[descriptorSize] = '\0'; + + if (depth == 0) + { + const char* dumpPath = getenv("CDACLITE_DUMP_JSON"); + if (dumpPath != nullptr) + { + FILE* f = fopen(dumpPath, "wb"); + if (f != nullptr) + { + fwrite(json.data(), 1, descriptorSize, f); + fclose(f); + } + } + } + + DataDescriptor local; + if (!local.Parse(json.data(), descriptorSize, error)) + { + return false; + } + + // Read pointer_data and resolve this descriptor's indirect globals. + std::vector pointerData(pointerDataCount, 0); + for (uint32_t i = 0; i < pointerDataCount; i++) + { + if (!readMemory(context, pointerDataPtr + (uint64_t)i * ptrSize, &pointerData[i], ptrSize)) + { + error = "failed to read pointer_data entry"; + return false; + } + } + local.ResolveIndirectGlobals(pointerData.empty() ? nullptr : pointerData.data(), pointerDataCount); + + // Merge into the accumulated descriptor (existing entries take precedence). + Merge(local); + + // Recurse into sub-descriptors. Each is an index into this descriptor's + // pointer_data; that slot holds the address of a variable whose value is the + // sub ContractDescriptor's address. + for (std::map::const_iterator it = local.m_subDescriptors.begin(); + it != local.m_subDescriptors.end(); ++it) + { + uint32_t index = it->second.pointerIndex; + if (index >= pointerData.size()) + { + continue; + } + uint64_t variableAddr = pointerData[index]; + uint64_t subDescriptorAddr = 0; + if (variableAddr == 0 || + !readMemory(context, variableAddr, &subDescriptorAddr, ptrSize) || + subDescriptorAddr == 0) + { + continue; // sub-descriptor not populated yet (e.g. GC not initialized) + } + + std::string subError; + LoadMerged(readMemory, context, subDescriptorAddr, depth + 1, subError); + // A failed/absent sub-descriptor is non-fatal; keep what we have. + } + + return true; + } + + bool DataDescriptor::Parse(const char* json, size_t length, std::string& error) + { + m_types.clear(); + m_globals.clear(); + m_contracts.clear(); + m_subDescriptors.clear(); + + json::Value root; + if (!json::Parse(json, length, root, error)) + { + return false; + } + if (!root.IsObject()) + { + error = "descriptor root is not an object"; + return false; + } + + const json::Value* types = root.Find("types"); + if (types != nullptr) + { + ParseTypes(*types); + } + + const json::Value* globals = root.Find("globals"); + if (globals != nullptr) + { + ParseGlobals(*globals); + } + + const json::Value* contracts = root.Find("contracts"); + if (contracts != nullptr) + { + ParseContracts(*contracts); + } + + const json::Value* subDescriptors = root.Find("subDescriptors"); + if (subDescriptors == nullptr) + { + subDescriptors = root.Find("sub-descriptors"); + } + if (subDescriptors != nullptr) + { + ParseSubDescriptors(*subDescriptors); + } + + error.clear(); + return true; + } +} diff --git a/src/coreclr/debug/cdaclite/data/datadescriptor.h b/src/coreclr/debug/cdaclite/data/datadescriptor.h new file mode 100644 index 00000000000000..18b5160de77ca8 --- /dev/null +++ b/src/coreclr/debug/cdaclite/data/datadescriptor.h @@ -0,0 +1,124 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +//***************************************************************************** +// datadescriptor.h +// +// Parses the in-memory data descriptor JSON (compact format, no baseline) into +// a structured model: types (with field offsets), globals, and contracts. +// +// See docs/design/datacontracts/data_descriptor.md for the format. +//***************************************************************************** + +#ifndef CDACLITE_DATADESCRIPTOR_H +#define CDACLITE_DATADESCRIPTOR_H + +#include +#include +#include +#include + +#include "json.h" + +namespace cdac +{ + // Reads 'size' bytes at 'address' from the target into 'buffer'. Returns true + // only if all bytes were read. 'context' is caller-defined. + typedef bool (*ReadMemoryCallback)(void* context, uint64_t address, void* buffer, uint32_t size); + + struct FieldInfo + { + uint32_t offset = 0; + std::string type; // may be empty if not specified in the descriptor + }; + + struct TypeInfo + { + bool hasSize = false; + uint32_t size = 0; + std::map fields; + }; + + struct GlobalInfo + { + // Direct: a numeric or string value is embedded in the descriptor. + // Indirect: the value lives in the contract descriptor's pointer_data + // array at 'pointerIndex' and must be read from the target. + bool isIndirect = false; + uint32_t pointerIndex = 0; + + bool hasNumericValue = false; + uint64_t numericValue = 0; + + // True once an indirect global has been resolved: numericValue holds a + // target address (the location of the runtime variable), not a constant. + bool isAddress = false; + + bool isString = false; + std::string stringValue; + + std::string type; // may be empty + }; + + class DataDescriptor + { + public: + // Reads the contract descriptor at 'contractDescriptorAddr' from the target + // (via 'readMemory'), parses its in-memory JSON, resolves its indirect globals, + // and recursively loads and merges any sub-descriptors (e.g. the GC descriptor). + // The result is a single merged descriptor. Returns false and fills 'error' on + // failure to read/parse the root descriptor. + bool Load(ReadMemoryCallback readMemory, void* context, uint64_t contractDescriptorAddr, std::string& error); + + // Parses the descriptor JSON. Returns false and fills 'error' on failure. + bool Parse(const char* json, size_t length, std::string& error); + + // Converts this descriptor's indirect globals ([index]) into direct values by + // reading pointerData[index]. After this, globals carry absolute addresses/values + // and no longer depend on a pointer_data array. Indices out of range are dropped. + void ResolveIndirectGlobals(const uint64_t* pointerData, uint32_t pointerDataCount); + + // Merges another (already-resolved) descriptor's types, globals, and contracts + // into this one. Existing entries take precedence (are not overwritten). + void Merge(const DataDescriptor& other); + + const std::map& Types() const { return m_types; } + const std::map& Globals() const { return m_globals; } + const std::map& Contracts() const { return m_contracts; } + + // Sub-descriptor references (name -> pointer_data index), e.g. "GC". + const std::map& SubDescriptors() const { return m_subDescriptors; } + + const TypeInfo* FindType(const std::string& name) const; + + // Convenience: looks up a field offset for a given type. + bool TryGetFieldOffset(const std::string& typeName, const std::string& fieldName, uint32_t& offset) const; + + // Convenience: looks up a global's direct numeric value. Returns false for + // indirect globals (which require a target read) or non-numeric globals. + bool TryGetGlobalValue(const std::string& name, uint64_t& value) const; + + // Addresses of every ContractDescriptor loaded by Load() -- the main + // descriptor plus each recursively-loaded sub-descriptor. The bootstrap + // contract emits struct/JSON/pointer_data for each of these. + const std::vector& DescriptorAddresses() const { return m_descriptorAddresses; } + + private: + void ParseTypes(const json::Value& types); + void ParseGlobals(const json::Value& globals); + void ParseGlobalValue(const json::Value& value, GlobalInfo& info); + void ParseContracts(const json::Value& contracts); + void ParseSubDescriptors(const json::Value& subDescriptors); + + // Recursively reads the descriptor at 'address', merges it into this descriptor, + // and follows its sub-descriptors. 'depth' guards against cycles. + bool LoadMerged(ReadMemoryCallback readMemory, void* context, uint64_t address, int depth, std::string& error); + + std::map m_types; + std::map m_globals; + std::map m_contracts; + std::map m_subDescriptors; + std::vector m_descriptorAddresses; + }; +} + +#endif // CDACLITE_DATADESCRIPTOR_H diff --git a/src/coreclr/debug/cdaclite/data/datatype.h b/src/coreclr/debug/cdaclite/data/datatype.h new file mode 100644 index 00000000000000..2590d01738f9ca --- /dev/null +++ b/src/coreclr/debug/cdaclite/data/datatype.h @@ -0,0 +1,168 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +//***************************************************************************** +// datatype.h +// +// Infrastructure for descriptor-driven Data types (see runtimetypes.h for the +// actual type definitions). A Data type derives from Struct and declares its +// fields as typed members via the CDAC_* macros, e.g.: +// +// struct Thread : Struct +// { +// Thread() : Struct("Thread") {} +// CDAC_PTR(LinkNext) +// CDAC_PTR(CachedStackBase) +// }; +// +// Each field member self-registers with its owning Struct, so Struct::Load() +// can read every field generically. Field OFFSETS come from the target's +// in-memory data descriptor at runtime -- they are never written here. A field's +// descriptor name is its member name (stringized once by the macro), so the two +// can never drift apart. Access is ergonomic: `thread.LinkNext` implicitly +// converts to the field value. +//***************************************************************************** + +#ifndef CDACLITE_DATATYPE_H +#define CDACLITE_DATATYPE_H + +#include + +#include "target.h" + +namespace cdac +{ +namespace data +{ + enum class FieldKind { Ptr, U32, U64, Addr }; + + class Struct; + + // Base for a single field: knows its descriptor name and kind, stores the read + // value, and links into its owner's field list. Optional fields do not fail + // Struct::Load when absent from the descriptor (value stays 0). + class FieldBase + { + public: + FieldBase(Struct* owner, const char* name, FieldKind kind, bool optional = false); + + const char* Name() const { return m_name; } + FieldKind Kind() const { return m_kind; } + bool Optional() const { return m_optional; } + uint64_t Raw() const { return m_value; } + void SetRaw(uint64_t value) { m_value = value; } + FieldBase* Next() const { return m_next; } + + private: + friend class Struct; + const char* m_name; + FieldKind m_kind; + bool m_optional; + uint64_t m_value; + FieldBase* m_next; + }; + + // Base for a Data type. Holds the descriptor type name and the intrusive list + // of self-registered fields. Non-copyable (fields hold owner-relative links). + class Struct + { + public: + Struct(const Struct&) = delete; + Struct& operator=(const Struct&) = delete; + + const char* TypeName() const { return m_typeName; } + + // Reads every registered field at 'address' using descriptor offsets. + // Returns false if any field is missing from the descriptor or unreadable. + bool Load(const Target& target, uint64_t address) + { + for (FieldBase* field = m_head; field != nullptr; field = field->Next()) + { + uint64_t value = 0; + bool ok = false; + switch (field->Kind()) + { + case FieldKind::Ptr: + ok = target.TryReadFieldPointer(address, m_typeName, field->Name(), value); + break; + case FieldKind::U64: + ok = target.TryReadFieldUInt64(address, m_typeName, field->Name(), value); + break; + case FieldKind::U32: + { + uint32_t value32 = 0; + ok = target.TryReadFieldUInt32(address, m_typeName, field->Name(), value32); + value = value32; + break; + } + case FieldKind::Addr: + ok = target.TryGetFieldAddress(address, m_typeName, field->Name(), value); + break; + } + + if (!ok && !field->Optional()) + { + return false; + } + field->SetRaw(ok ? value : 0); + } + return true; + } + + protected: + explicit Struct(const char* typeName) : m_typeName(typeName), m_head(nullptr) {} + + private: + friend class FieldBase; + void Register(FieldBase* field) { field->m_next = m_head; m_head = field; } + + const char* m_typeName; + FieldBase* m_head; + }; + + inline FieldBase::FieldBase(Struct* owner, const char* name, FieldKind kind, bool optional) + : m_name(name), m_kind(kind), m_optional(optional), m_value(0), m_next(nullptr) + { + owner->Register(this); + } + + // Typed field members. Implicit conversion gives ergonomic value access. + class Ptr : public FieldBase + { + public: + Ptr(Struct* owner, const char* name, bool optional = false) : FieldBase(owner, name, FieldKind::Ptr, optional) {} + operator uint64_t() const { return Raw(); } + }; + + class U64 : public FieldBase + { + public: + U64(Struct* owner, const char* name, bool optional = false) : FieldBase(owner, name, FieldKind::U64, optional) {} + operator uint64_t() const { return Raw(); } + }; + + class U32 : public FieldBase + { + public: + U32(Struct* owner, const char* name, bool optional = false) : FieldBase(owner, name, FieldKind::U32, optional) {} + operator uint32_t() const { return (uint32_t)Raw(); } + }; + + // Address of an inline field/array (== managed [FieldAddress]). + class Addr : public FieldBase + { + public: + Addr(Struct* owner, const char* name, bool optional = false) : FieldBase(owner, name, FieldKind::Addr, optional) {} + operator uint64_t() const { return Raw(); } + }; + + // Declare a field member whose descriptor field name equals the member name. + // CDAC_OPT_* variants do not fail Load() when the field is absent (value 0). + #define CDAC_PTR(name) ::cdac::data::Ptr name { this, #name }; + #define CDAC_U64(name) ::cdac::data::U64 name { this, #name }; + #define CDAC_U32(name) ::cdac::data::U32 name { this, #name }; + #define CDAC_ADDR(name) ::cdac::data::Addr name { this, #name }; + #define CDAC_OPT_PTR(name) ::cdac::data::Ptr name { this, #name, true }; +} +} + +#endif // CDACLITE_DATATYPE_H diff --git a/src/coreclr/debug/cdaclite/data/runtimetypes.h b/src/coreclr/debug/cdaclite/data/runtimetypes.h new file mode 100644 index 00000000000000..dcf8959e630e5b --- /dev/null +++ b/src/coreclr/debug/cdaclite/data/runtimetypes.h @@ -0,0 +1,370 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +//***************************************************************************** +// runtimetypes.h +// +// Data types: typed readers for descriptor-defined runtime structures, mirroring +// the managed cDAC Data. classes. Each type is a small self-contained block: +// a constructor naming its descriptor type, then one line per field. Field +// offsets are read from the target's in-memory descriptor at runtime (never +// written here). Access is ergonomic, e.g. `segment.Mem`, `thread.LinkNext`. +// +// See datatype.h for the Struct/field infrastructure and the CDAC_* macros. +//***************************************************************************** + +#ifndef CDACLITE_RUNTIMETYPES_H +#define CDACLITE_RUNTIMETYPES_H + +#include "datatype.h" + +namespace cdac +{ +namespace data +{ + // --- GC --------------------------------------------------------------- + + struct Generation : Struct + { + Generation() : Struct("Generation") {} + CDAC_PTR(StartSegment) + }; + + struct HeapSegment : Struct + { + HeapSegment() : Struct("HeapSegment") {} + CDAC_PTR(Mem) // start of the segment's used range + CDAC_PTR(Allocated) // end of the used range + CDAC_PTR(Next) // next segment in the generation list + }; + + struct GCHeap : Struct + { + GCHeap() : Struct("GCHeap") {} + CDAC_ADDR(GenerationTable) // inline array (field address) + CDAC_PTR(EphemeralHeapSegment) + CDAC_PTR(AllocAllocated) + CDAC_PTR(FinalizeQueue) + CDAC_OPT_PTR(FreeRegions) + CDAC_OPT_PTR(FreeableSohSegment) + CDAC_OPT_PTR(FreeableUohSegment) + }; + + // Finalize queue. FillPointers is an inline array (field address); its length is + // the global CFinalizeFillPointersLength. The cDAC re-reads this array in GetFillPointers. + struct CFinalize : Struct + { + CFinalize() : Struct("CFinalize") {} + CDAC_ADDR(FillPointers) + }; + + // GC bookkeeping card-table info node (linked via NextCardTable). + struct CardTableInfo : Struct + { + CardTableInfo() : Struct("CardTableInfo") {} + CDAC_PTR(NextCardTable) + }; + + // A region free list head. The cDAC walks HeadFreeRegion as a HeapSegment.Next list. + struct RegionFreeList : Struct + { + RegionFreeList() : Struct("RegionFreeList") {} + CDAC_PTR(HeadFreeRegion) + }; + + // --- Threads ---------------------------------------------------------- + + struct ThreadStore : Struct + { + ThreadStore() : Struct("ThreadStore") {} + CDAC_PTR(FirstThreadLink) // head Thread* + CDAC_U32(ThreadCount) + }; + + struct Thread : Struct + { + Thread() : Struct("Thread") {} + CDAC_PTR(LinkNext) // next Thread* + CDAC_PTR(CachedStackBase) // high address (stack grows down) + CDAC_PTR(CachedStackLimit) // low address + CDAC_PTR(Frame) + }; + + // --- Loader / modules ------------------------------------------------- + + struct AppDomain : Struct + { + AppDomain() : Struct("AppDomain") {} + CDAC_PTR(RootAssembly) + CDAC_ADDR(AssemblyList) // embedded ArrayListBase + }; + + // Intrusive block-list container (arraylist.h). AssemblyList is one of these. + struct ArrayListBase : Struct + { + ArrayListBase() : Struct("ArrayListBase") {} + CDAC_U32(Count) // total element count across all blocks + CDAC_ADDR(FirstBlock) // embedded first block + }; + + struct ArrayListBlock : Struct + { + ArrayListBlock() : Struct("ArrayListBlock") {} + CDAC_PTR(Next) // next block, or null + CDAC_U32(Size) // element capacity of this block + CDAC_ADDR(ArrayStart) // embedded element array (pointer-sized elements) + }; + + struct Assembly : Struct + { + Assembly() : Struct("Assembly") {} + CDAC_PTR(Module) + }; + + struct Module : Struct + { + Module() : Struct("Module") {} + CDAC_PTR(Assembly) + CDAC_PTR(PEAssembly) + CDAC_PTR(LoaderAllocator) + CDAC_OPT_PTR(GrowableSymbolStream) // in-memory symbol (PDB) stream, if any + }; + + // In-memory symbol stream (module's growable PDB buffer). + struct CGrowableSymbolStream : Struct + { + CGrowableSymbolStream() : Struct("CGrowableSymbolStream") {} + CDAC_PTR(Buffer) + CDAC_U32(Size) + }; + + struct PEAssembly : Struct + { + PEAssembly() : Struct("PEAssembly") {} + CDAC_PTR(PEImage) + }; + + struct PEImage : Struct + { + PEImage() : Struct("PEImage") {} + CDAC_PTR(LoadedImageLayout) + }; + + struct PEImageLayout : Struct + { + PEImageLayout() : Struct("PEImageLayout") {} + CDAC_PTR(Base) // loaded image base (code + metadata; file-backed) + CDAC_U32(Size) + CDAC_U32(Flags) + }; + + // --- GC handles (roots) ----------------------------------------------- + + struct HandleTableMap : Struct + { + HandleTableMap() : Struct("HandleTableMap") {} + CDAC_PTR(BucketsPtr) // array of HandleTableBucket* (InitialHandleTableArraySize entries) + CDAC_PTR(Next) // next map, or null + }; + + struct HandleTableBucket : Struct + { + HandleTableBucket() : Struct("HandleTableBucket") {} + CDAC_PTR(Table) // array of HandleTable* (1 for WKS, TotalCpuCount for SVR) + }; + + struct HandleTable : Struct + { + HandleTable() : Struct("HandleTable") {} + CDAC_PTR(SegmentList) // first TableSegment + }; + + struct TableSegment : Struct + { + TableSegment() : Struct("TableSegment") {} + CDAC_PTR(NextSegment) + }; + + // --- JIT code heaps --------------------------------------------------- + + struct EEJitManager : Struct + { + EEJitManager() : Struct("EEJitManager") {} + CDAC_PTR(AllCodeHeaps) // head of the CodeHeapListNode list + }; + + struct CodeHeapListNode : Struct + { + CodeHeapListNode() : Struct("CodeHeapListNode") {} + CDAC_PTR(Next) + CDAC_PTR(StartAddress) // start of the JIT code range + CDAC_PTR(EndAddress) // end of the JIT code range + CDAC_PTR(Heap) // the CodeHeap (Loader/Host) backing this node + CDAC_PTR(MapBase) // base address the NibbleMap is relative to + CDAC_PTR(HeaderMap) // the NibbleMap (IP -> code header) storage + }; + + // --- ExecutionManager / code lookup (for the Normal-tier stack scan) ---- + + // Top of the multi-level range-section radix map (ExecutionManagerCodeRangeMapAddress). + struct RangeSectionMap : Struct + { + RangeSectionMap() : Struct("RangeSectionMap") {} + CDAC_PTR(TopLevelData) + }; + + struct RangeSectionFragment : Struct + { + RangeSectionFragment() : Struct("RangeSectionFragment") {} + CDAC_PTR(RangeBegin) + CDAC_PTR(RangeEndOpen) + CDAC_PTR(RangeSection) + CDAC_PTR(Next) + }; + + struct RangeSection : Struct + { + RangeSection() : Struct("RangeSection") {} + CDAC_PTR(RangeBegin) + CDAC_PTR(RangeEndOpen) + CDAC_PTR(JitManager) + CDAC_U32(Flags) + CDAC_OPT_PTR(HeapList) // EEJitManager: CodeHeapListNode for this range + CDAC_OPT_PTR(R2RModule) // ReadyToRun: the module (image on disk) + }; + + // The code header immediately preceding a JIT method body; MethodDesc identifies the method. + struct RealCodeHeader : Struct + { + RealCodeHeader() : Struct("RealCodeHeader") {} + CDAC_PTR(MethodDesc) + }; + + // Code heap base type; HeapType selects LoaderCodeHeap vs HostCodeHeap. + struct CodeHeap : Struct + { + CodeHeap() : Struct("CodeHeap") {} + CDAC_U32(HeapType) + }; + + struct LoaderCodeHeap : Struct + { + LoaderCodeHeap() : Struct("LoaderCodeHeap") {} + CDAC_PTR(LoaderHeap) + }; + + struct HostCodeHeap : Struct + { + HostCodeHeap() : Struct("HostCodeHeap") {} + CDAC_PTR(BaseAddress) + CDAC_PTR(CurrentAddress) + }; + + // --- Loader heaps ----------------------------------------------------- + + struct SystemDomain : Struct + { + SystemDomain() : Struct("SystemDomain") {} + CDAC_ADDR(GlobalLoaderAllocator) // embedded LoaderAllocator + }; + + struct LoaderAllocator : Struct + { + LoaderAllocator() : Struct("LoaderAllocator") {} + // Each heap is a LoaderHeap*. Some are configuration/architecture-specific, + // so they are optional (absent -> 0, skipped). + CDAC_OPT_PTR(HighFrequencyHeap) + CDAC_OPT_PTR(LowFrequencyHeap) + CDAC_OPT_PTR(StaticsHeap) + CDAC_OPT_PTR(StubHeap) + CDAC_OPT_PTR(ExecutableHeap) + CDAC_OPT_PTR(FixupPrecodeHeap) + CDAC_OPT_PTR(NewStubPrecodeHeap) + CDAC_OPT_PTR(DynamicHelpersStubHeap) + }; + + struct LoaderHeap : Struct + { + LoaderHeap() : Struct("LoaderHeap") {} + CDAC_PTR(FirstBlock) + }; + + struct LoaderHeapBlock : Struct + { + LoaderHeapBlock() : Struct("LoaderHeapBlock") {} + CDAC_PTR(Next) + CDAC_PTR(VirtualAddress) + CDAC_PTR(VirtualSize) // nuint (pointer-sized) + }; + + // --- Sync blocks ------------------------------------------------------ + + struct SyncTableEntry : Struct + { + SyncTableEntry() : Struct("SyncTableEntry") {} + CDAC_PTR(SyncBlock) + CDAC_PTR(Object) // low bit set => entry is free + }; + + struct SyncBlockCache : Struct + { + SyncBlockCache() : Struct("SyncBlockCache") {} + CDAC_U32(FreeSyncTableIndex) + CDAC_OPT_PTR(CleanupBlockList) + }; + + struct SyncBlock : Struct + { + SyncBlock() : Struct("SyncBlock") {} + CDAC_PTR(Lock) // ObjectHandle to the managed Lock object + }; + + // --- Stress log ------------------------------------------------------- + + struct StressLog : Struct + { + StressLog() : Struct("StressLog") {} + CDAC_PTR(Logs) // head of the ThreadStressLog list + }; + + struct ThreadStressLog : Struct + { + ThreadStressLog() : Struct("ThreadStressLog") {} + CDAC_PTR(Next) + CDAC_PTR(ChunkListHead) + CDAC_PTR(ChunkListTail) + CDAC_PTR(CurrentWriteChunk) + }; + + struct StressLogChunk : Struct + { + StressLogChunk() : Struct("StressLogChunk") {} + CDAC_PTR(Prev) + CDAC_PTR(Next) + }; + + // --- COM interop ------------------------------------------------------ + + struct RCWCleanupList : Struct + { + RCWCleanupList() : Struct("RCWCleanupList") {} + CDAC_PTR(FirstBucket) + }; + + struct RCW : Struct + { + RCW() : Struct("RCW") {} + CDAC_PTR(NextCleanupBucket) + CDAC_PTR(NextRCW) + CDAC_OPT_PTR(CtxEntry) + }; + + // Apartment (STA) context entry referenced by an RCW bucket (GetSTAThread). + struct CtxEntry : Struct + { + CtxEntry() : Struct("CtxEntry") {} + CDAC_PTR(STAThread) + }; +} +} + +#endif // CDACLITE_RUNTIMETYPES_H diff --git a/src/coreclr/debug/cdaclite/data/target.cpp b/src/coreclr/debug/cdaclite/data/target.cpp new file mode 100644 index 00000000000000..47173b58d2f8a5 --- /dev/null +++ b/src/coreclr/debug/cdaclite/data/target.cpp @@ -0,0 +1,190 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +//***************************************************************************** +// target.cpp +// +// Implementation of the Target abstraction declared in target.h. +//***************************************************************************** + +#include "target.h" + +#include + +namespace cdac +{ + Target::Target(const DataDescriptor* descriptor, + ReadMemoryCallback readMemory, + void* readContext) + : m_descriptor(descriptor) + , m_readMemory(readMemory) + , m_readContext(readContext) + { + } + + bool Target::ReadBuffer(uint64_t address, void* buffer, uint32_t size) const + { + if (m_readMemory == nullptr) + { + return false; + } + return m_readMemory(m_readContext, address, buffer, size); + } + + bool Target::TryReadUInt8(uint64_t address, uint8_t& value) const + { + return ReadBuffer(address, &value, sizeof(value)); + } + + bool Target::TryReadUInt16(uint64_t address, uint16_t& value) const + { + return ReadBuffer(address, &value, sizeof(value)); + } + + bool Target::TryReadUInt32(uint64_t address, uint32_t& value) const + { + return ReadBuffer(address, &value, sizeof(value)); + } + + bool Target::TryReadUInt64(uint64_t address, uint64_t& value) const + { + return ReadBuffer(address, &value, sizeof(value)); + } + + bool Target::TryReadPointer(uint64_t address, uint64_t& value) const + { + // Same-platform build: a pointer is exactly sizeof(void*) bytes, native endianness. + value = 0; + return ReadBuffer(address, &value, (uint32_t)sizeof(void*)); + } + + bool Target::TryGetFieldAddress(uint64_t baseAddress, const std::string& typeName, const std::string& fieldName, uint64_t& address) const + { + if (m_descriptor == nullptr) + { + return false; + } + uint32_t offset = 0; + if (!m_descriptor->TryGetFieldOffset(typeName, fieldName, offset)) + { + return false; + } + address = baseAddress + offset; + return true; + } + + bool Target::TryReadFieldUInt32(uint64_t baseAddress, const std::string& typeName, const std::string& fieldName, uint32_t& value) const + { + uint64_t address = 0; + return TryGetFieldAddress(baseAddress, typeName, fieldName, address) && TryReadUInt32(address, value); + } + + bool Target::TryReadFieldUInt64(uint64_t baseAddress, const std::string& typeName, const std::string& fieldName, uint64_t& value) const + { + uint64_t address = 0; + return TryGetFieldAddress(baseAddress, typeName, fieldName, address) && TryReadUInt64(address, value); + } + + bool Target::TryReadFieldPointer(uint64_t baseAddress, const std::string& typeName, const std::string& fieldName, uint64_t& value) const + { + uint64_t address = 0; + return TryGetFieldAddress(baseAddress, typeName, fieldName, address) && TryReadPointer(address, value); + } + + bool Target::TryGetTypeSize(const std::string& typeName, uint32_t& size) const + { + if (m_descriptor == nullptr) + { + return false; + } + const TypeInfo* type = m_descriptor->FindType(typeName); + if (type == nullptr || !type->hasSize) + { + return false; + } + size = type->size; + return true; + } + + void Target::EmitStructMemory(const char* typeName, uint64_t address) const + { + if (m_enumMemSink == nullptr || typeName == nullptr || m_descriptor == nullptr) + { + return; + } + + // Prefer the descriptor's exact type size (the analog of the DAC's sizeof(type)). + uint32_t size = 0; + if (TryGetTypeSize(typeName, size) && size > 0) + { + m_enumMemSink(m_enumMemContext, address, size); + return; + } + + // Most runtime types are CDAC_TYPE_INDETERMINATE (offsets only, no size). Approximate + // the footprint from the largest declared field: max(offset) + 8 covers every + // scalar/pointer field (8 == largest primitive on this 64-bit build). This mirrors the + // set of bytes any cDAC consumer can read via the descriptor's field offsets. + const TypeInfo* type = m_descriptor->FindType(typeName); + if (type == nullptr || type->fields.empty()) + { + return; + } + uint32_t maxEnd = 0; + for (std::map::const_iterator it = type->fields.begin(); it != type->fields.end(); ++it) + { + uint32_t end = it->second.offset + (uint32_t)sizeof(uint64_t); + if (end > maxEnd) + { + maxEnd = end; + } + } + if (maxEnd > 0) + { + m_enumMemSink(m_enumMemContext, address, maxEnd); + } + } + + bool Target::TryGetGlobalValue(const std::string& name, uint64_t& value) const + { + if (m_descriptor == nullptr) + { + return false; + } + const std::map& globals = m_descriptor->Globals(); + std::map::const_iterator it = globals.find(name); + if (it == globals.end() || !it->second.hasNumericValue) + { + return false; + } + value = it->second.numericValue; + return true; + } + + bool Target::TryReadGlobalPointer(const std::string& name, uint64_t& value) const + { + uint64_t address = 0; + return TryGetGlobalValue(name, address) && TryReadPointer(address, value); + } + + bool Target::TryReadGlobalUInt32(const std::string& name, uint32_t& value) const + { + uint64_t address = 0; + return TryGetGlobalValue(name, address) && TryReadUInt32(address, value); + } + + bool Target::TryGetGlobalString(const std::string& name, std::string& value) const + { + if (m_descriptor == nullptr) + { + return false; + } + const std::map& globals = m_descriptor->Globals(); + std::map::const_iterator it = globals.find(name); + if (it == globals.end() || !it->second.isString) + { + return false; + } + value = it->second.stringValue; + return true; + } +} diff --git a/src/coreclr/debug/cdaclite/data/target.h b/src/coreclr/debug/cdaclite/data/target.h new file mode 100644 index 00000000000000..eca00e23ba77ca --- /dev/null +++ b/src/coreclr/debug/cdaclite/data/target.h @@ -0,0 +1,164 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +//***************************************************************************** +// target.h +// +// The Target abstraction: typed reads of a target process/dump's memory, +// combined with the parsed data descriptor. Modeled on the managed cDAC's +// Target type, but simplified: cdac-lite is built for the *same* platform as +// the target it inspects, so reads use the native pointer size and endianness +// (no byte-swapping or pointer-size branching). +// +// The Target is intentionally decoupled from any COM/ICLRDataTarget type. It +// reads memory through a caller-provided callback so this module can be unit +// tested and reused independently. +//***************************************************************************** + +#ifndef CDACLITE_TARGET_H +#define CDACLITE_TARGET_H + +#include +#include +#include + +#include "datadescriptor.h" + +namespace cdac +{ + // Reads 'size' bytes at 'address' from the target into 'buffer'. Returns true + // only if all bytes were read. 'context' is passed through from Target::Create. + typedef bool (*ReadMemoryCallback)(void* context, uint64_t address, void* buffer, uint32_t size); + + // The memory-enumeration tier requested for a dump. Normal captures only the state a stack + // walk reaches (stacks, code, method metadata); Heap additionally captures the GC heap and + // the heap-side structures (handles, sync blocks, stress log, COM interop). + enum class DumpTier + { + Normal, + Heap, + }; + + class Target + { + public: + // Builds a Target from a descriptor whose globals have already been resolved to + // absolute addresses/values (see DataDescriptor::ResolveIndirectGlobals). Reads + // memory through the provided callback. Does not take ownership of 'descriptor'. + Target(const DataDescriptor* descriptor, + ReadMemoryCallback readMemory, + void* readContext); + + // Native pointer size of the target (== this build's pointer size). + uint32_t PointerSize() const { return (uint32_t)sizeof(void*); } + + const DataDescriptor* Descriptor() const { return m_descriptor; } + + // The enumeration tier for the current dump. Contracts can query this to scope their + // work; the enumeration driver uses it to decide which contracts to run. + DumpTier Tier() const { return m_tier; } + void SetTier(DumpTier tier) { m_tier = tier; } + + // --- Typed struct reads (Data-type layer) --------------------------- + + // Reads a Data type (any struct with `bool Load(const Target&, uint64_t)`) + // at 'address'. Mirrors the managed cDAC ProcessedData.GetOrAdd() pattern. + // When an EnumMem sink is set (see SetEnumMemSink), a successful read also records + // the structure's own memory -- the cdac-lite analog of the DAC's DPTR::EnumMem(). + template + bool TryRead(uint64_t address, TData& data) const + { + if (!data.Load(*this, address)) + { + return false; + } + if (m_enumMemSink != nullptr) + { + EmitStructMemory(data.TypeName(), address); + } + return true; + } + + // --- Implicit metadata enumeration (DAC DPTR::EnumMem() analog) ------- + + // Observer invoked with [address, address+size) for each structure read while a walk + // is in progress. Lets a walk implicitly capture the metadata structures it traverses, + // exactly as the DAC records every DPTR it dereferences during EnumMemoryRegions. + typedef void (*EnumMemCallback)(void* context, uint64_t address, uint32_t size); + + // Sets (or clears, with nullptr) the EnumMem observer. Const because it toggles a + // transient "enumeration mode" rather than logical target state. + void SetEnumMemSink(EnumMemCallback sink, void* context) const + { + m_enumMemSink = sink; + m_enumMemContext = context; + } + + // Records [address, address+size) through the EnumMem sink, if one is set. Use for + // memory a walk reads that is not a Data-type struct (e.g. pointer arrays). + void EmitMemory(uint64_t address, uint32_t size) const + { + if (m_enumMemSink != nullptr && size > 0) + { + m_enumMemSink(m_enumMemContext, address, size); + } + } + + // --- Raw reads ------------------------------------------------------- + + // Reads 'size' bytes into 'buffer'. Returns true iff all bytes were read. + bool ReadBuffer(uint64_t address, void* buffer, uint32_t size) const; + + bool TryReadUInt8(uint64_t address, uint8_t& value) const; + bool TryReadUInt16(uint64_t address, uint16_t& value) const; + bool TryReadUInt32(uint64_t address, uint32_t& value) const; + bool TryReadUInt64(uint64_t address, uint64_t& value) const; + + // Reads a native-pointer-sized value. + bool TryReadPointer(uint64_t address, uint64_t& value) const; + + // --- Descriptor-aware reads ----------------------------------------- + + // Reads a field of a type instance at 'baseAddress'. The field offset comes + // from the data descriptor. Returns false if the type/field is unknown or + // the read fails. + bool TryReadFieldUInt32(uint64_t baseAddress, const std::string& typeName, const std::string& fieldName, uint32_t& value) const; + bool TryReadFieldUInt64(uint64_t baseAddress, const std::string& typeName, const std::string& fieldName, uint64_t& value) const; + bool TryReadFieldPointer(uint64_t baseAddress, const std::string& typeName, const std::string& fieldName, uint64_t& value) const; + + // Computes the absolute address of a field within an instance. + bool TryGetFieldAddress(uint64_t baseAddress, const std::string& typeName, const std::string& fieldName, uint64_t& address) const; + + // Returns the size of a type, if known. + bool TryGetTypeSize(const std::string& typeName, uint32_t& size) const; + + // --- Globals --------------------------------------------------------- + + // Returns the resolved value stored for a global. For a global that points at a + // variable this is the variable's address; for a direct value global it is the + // value itself. Returns false if the global is absent/unresolved. + bool TryGetGlobalValue(const std::string& name, uint64_t& value) const; + + // Reads a native pointer at the global's resolved address (one dereference). + bool TryReadGlobalPointer(const std::string& name, uint64_t& value) const; + + // Reads a uint32 at the global's resolved address (one dereference). + bool TryReadGlobalUInt32(const std::string& name, uint32_t& value) const; + + // Reads a string global's value, if present. + bool TryGetGlobalString(const std::string& name, std::string& value) const; + + private: + // Records a Data-type struct's own memory through the EnumMem sink. Uses the + // descriptor type size (the analog of the DAC's sizeof(type)). + void EmitStructMemory(const char* typeName, uint64_t address) const; + + const DataDescriptor* m_descriptor; + ReadMemoryCallback m_readMemory; + void* m_readContext; + DumpTier m_tier = DumpTier::Heap; + mutable EnumMemCallback m_enumMemSink = nullptr; + mutable void* m_enumMemContext = nullptr; + }; +} + +#endif // CDACLITE_TARGET_H diff --git a/src/coreclr/debug/cdaclite/datatarget.cpp b/src/coreclr/debug/cdaclite/datatarget.cpp new file mode 100644 index 00000000000000..5dc11773dd1415 --- /dev/null +++ b/src/coreclr/debug/cdaclite/datatarget.cpp @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +//***************************************************************************** +// datatarget.cpp +// +// Implementation of the data-target memory-read callback declared in datatarget.h. +//***************************************************************************** + +#include "datatarget.h" + +namespace cdac +{ + bool ReadFromDataTarget(void* context, uint64_t address, void* buffer, uint32_t size) + { + ICLRDataTarget* target = (ICLRDataTarget*)context; + ULONG32 read = 0; + HRESULT hr = target->ReadVirtual((CLRDATA_ADDRESS)address, (PBYTE)buffer, size, &read); + return SUCCEEDED(hr) && read == size; + } +} diff --git a/src/coreclr/debug/cdaclite/datatarget.h b/src/coreclr/debug/cdaclite/datatarget.h new file mode 100644 index 00000000000000..56e4c818e42e24 --- /dev/null +++ b/src/coreclr/debug/cdaclite/datatarget.h @@ -0,0 +1,92 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +//***************************************************************************** +// datatarget.h +// +// The data-target source: how cdac-lite reads the target process/dump memory. +// +// * DataTargetAdapter exposes the classic ICLRDataTarget (handed to +// CLRDataCreateInstance by dbghelp / createdump) as the ICorDebugDataTarget +// that dbgutil's TryGetSymbol requires. +// * ReadFromDataTarget is the memory-read callback the data layer (cdac::Target, +// cdac::DataDescriptor) uses; its 'context' is the ICLRDataTarget. +//***************************************************************************** + +#ifndef CDACLITE_DATATARGET_H +#define CDACLITE_DATATARGET_H + +#include +#include +#include +#include +#include + +namespace cdac +{ + // Adapter that exposes an ICLRDataTarget as the ICorDebugDataTarget required by + // dbgutil's TryGetSymbol. Only ReadVirtual is exercised, but the full vtable is + // implemented defensively. Stack-lifetime: AddRef/Release count but never free. + class DataTargetAdapter : public ICorDebugDataTarget + { + private: + LONG m_ref; + ICLRDataTarget* m_target; + + public: + DataTargetAdapter(ICLRDataTarget* target) + : m_ref(1), m_target(target) + { + } + + // IUnknown + STDMETHOD(QueryInterface)(REFIID riid, void** ppvObject) + { + if (ppvObject == nullptr) + { + return E_POINTER; + } + if (riid == IID_IUnknown || riid == __uuidof(ICorDebugDataTarget)) + { + *ppvObject = static_cast(this); + AddRef(); + return S_OK; + } + *ppvObject = nullptr; + return E_NOINTERFACE; + } + + STDMETHOD_(ULONG, AddRef)() + { + return InterlockedIncrement(&m_ref); + } + + STDMETHOD_(ULONG, Release)() + { + // Stack-allocated; do not delete. + return InterlockedDecrement(&m_ref); + } + + // ICorDebugDataTarget + STDMETHOD(GetPlatform)(CorDebugPlatform* pTargetPlatform) + { + // Not needed by TryGetSymbol (architecture is inferred from the PE header). + return E_NOTIMPL; + } + + STDMETHOD(ReadVirtual)(CORDB_ADDRESS address, BYTE* pBuffer, ULONG32 bytesRequested, ULONG32* pBytesRead) + { + return m_target->ReadVirtual((CLRDATA_ADDRESS)address, pBuffer, bytesRequested, pBytesRead); + } + + STDMETHOD(GetThreadContext)(DWORD dwThreadID, ULONG32 contextFlags, ULONG32 contextSize, BYTE* pContext) + { + return E_NOTIMPL; + } + }; + + // Memory-read callback used by the data layer (cdac::Target / cdac::DataDescriptor). + // 'context' is the ICLRDataTarget. Returns true only if all 'size' bytes were read. + bool ReadFromDataTarget(void* context, uint64_t address, void* buffer, uint32_t size); +} + +#endif // CDACLITE_DATATARGET_H diff --git a/src/coreclr/debug/cdaclite/enumerate.cpp b/src/coreclr/debug/cdaclite/enumerate.cpp new file mode 100644 index 00000000000000..f0a90457093d00 --- /dev/null +++ b/src/coreclr/debug/cdaclite/enumerate.cpp @@ -0,0 +1,227 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +//***************************************************************************** +// enumerate.cpp +// +// The memory enumeration: CDacLite::EnumMemoryRegions drives the cdac-lite +// contracts (GC, threads, modules, handles, JIT, loader heaps, ...) to select +// the managed memory a crash dump should include, and the region sinks that +// forward selected regions to the ICLRDataEnumMemoryRegions callback. +//***************************************************************************** + +#include "cdaclite.h" +#include "datatarget.h" + +#include + +#include "data/datadescriptor.h" +#include "data/target.h" +#include "contracts/gc.h" +#include "contracts/thread.h" +#include "contracts/loader.h" +#include "contracts/loaderheaps.h" +#include "contracts/handles.h" +#include "contracts/jit.h" +#include "contracts/statics.h" +#include "contracts/syncblock.h" +#include "contracts/stresslog.h" +#include "contracts/interop.h" +#include "contracts/stackscan.h" + +namespace cdac +{ + // Region sink: a contract reported a memory region [start, start+size). Forward it to the + // COM callback and log it. + void CDacLite::RegionSinkThunk(void* context, const char* kind, uint64_t start, uint64_t size) + { + RegionSinkState* state = (RegionSinkState*)context; + state->callback->EnumMemoryRegion((CLRDATA_ADDRESS)start, (ULONG32)size); + state->count++; + state->owner->Log(state->callback, "region %s [0x%llx, 0x%llx)", + kind, (unsigned long long)start, (unsigned long long)(start + size)); + } + + // EnumMem observer (DAC DPTR::EnumMem() analog): records the metadata structures a walk + // traverses. Forwarded to the same COM callback, but not logged per-region (there are many + // small structs) -- only counted. + void CDacLite::EnumMemThunk(void* context, uint64_t address, uint32_t size) + { + RegionSinkState* state = (RegionSinkState*)context; + state->callback->EnumMemoryRegion((CLRDATA_ADDRESS)address, (ULONG32)size); + state->count++; + } + + HRESULT STDMETHODCALLTYPE CDacLite::EnumMemoryRegions( + ICLRDataEnumMemoryRegionsCallback* callback, ULONG32 miniDumpFlags, CLRDataEnumMemoryFlags clrFlags) + { + UNREFERENCED_PARAMETER(clrFlags); + + Log(callback, "EnumMemoryRegions: contract descriptor @ 0x%llx (miniDumpFlags=0x%x)", + (unsigned long long)m_contractDescriptorAddr, (unsigned)miniDumpFlags); + + // Read + parse the contract descriptor, recursively merging sub-descriptors + // (e.g. the GC descriptor) and resolving all indirect globals. + cdac::DataDescriptor descriptor; + std::string error; + if (!descriptor.Load(&cdac::ReadFromDataTarget, m_target, m_contractDescriptorAddr, error)) + { + Log(callback, "EnumMemoryRegions: descriptor load failed: %s", error.c_str()); + return E_FAIL; + } + + Log(callback, "EnumMemoryRegions: loaded %zu types, %zu globals, %zu contracts", + descriptor.Types().size(), descriptor.Globals().size(), descriptor.Contracts().size()); + + // Build the Target (globals are all resolved to absolute values). + cdac::Target target(&descriptor, &cdac::ReadFromDataTarget, m_target); + + RegionSinkState sinkState = { this, callback, 0 }; + + // Enable implicit metadata enumeration: every structure the walks read is now captured + // (the cdac-lite analog of the DAC recording each DPTR it dereferences). + target.SetEnumMemSink(&CDacLite::EnumMemThunk, &sinkState); + + // Dump tier: HEAP2 (MiniDumpWithPrivateReadWriteMemory) requests the full GC heap + + // heap-side structures. A Normal dump (no HEAP2 flag) needs only what a stack walk + // reaches -- stacks, code, and method metadata -- so the GC heap and the heap-only + // contracts (handles, sync blocks, stress log, COM interop) are skipped. + cdac::DumpTier tier = (miniDumpFlags & 0x200 /*MiniDumpWithPrivateReadWriteMemory*/) != 0 + ? cdac::DumpTier::Heap + : cdac::DumpTier::Normal; + target.SetTier(tier); + bool heapTier = (tier == cdac::DumpTier::Heap); + Log(callback, "EnumMemoryRegions: tier = %s", heapTier ? "heap" : "normal (stack walk)"); + + if (heapTier) + { + int gcRegions = cdac::contracts::EnumerateGCHeapRegions(target, &CDacLite::RegionSinkThunk, &sinkState); + if (gcRegions < 0) + { + Log(callback, "EnumMemoryRegions: GC walk skipped (GC type unknown)"); + } + else + { + Log(callback, "EnumMemoryRegions: GC walk reported %d region(s)", gcRegions); + } + } + + // Walk the managed threads and report their stack ranges. + int threadRegions = cdac::contracts::EnumerateThreadRegions(target, &CDacLite::RegionSinkThunk, &sinkState); + if (threadRegions < 0) + { + Log(callback, "EnumMemoryRegions: thread walk skipped (ThreadStore not found)"); + } + else + { + Log(callback, "EnumMemoryRegions: thread walk reported %d region(s)", threadRegions); + } + + // Walk loaded modules and capture in-memory symbol streams (file-backed images come from disk). + int moduleRegions = cdac::contracts::EnumerateModuleRegions(target, &CDacLite::RegionSinkThunk, &sinkState); + if (moduleRegions < 0) + { + Log(callback, "EnumMemoryRegions: module walk skipped (AppDomain not found)"); + } + else + { + Log(callback, "EnumMemoryRegions: module walk reported %d region(s)", moduleRegions); + } + + // Walk the GC handle table and report its segments (roots storage). Heap tier only. + if (heapTier) + { + int handleRegions = cdac::contracts::EnumerateHandleRegions(target, &CDacLite::RegionSinkThunk, &sinkState); + if (handleRegions < 0) + { + Log(callback, "EnumMemoryRegions: handle walk skipped (handle table not found)"); + } + else + { + Log(callback, "EnumMemoryRegions: handle walk reported %d region(s)", handleRegions); + } + } + + // Code + method metadata for stack walks. Heap tier bulk-emits every JIT code heap and + // loader heap (fine when the whole heap is captured anyway). Normal tier instead does a + // conservative stack scan, capturing only the code + MethodDesc reachable from pointers on + // the thread stacks -- this scales to large programs where the code/loader heaps are huge. + if (heapTier) + { + int jitRegions = cdac::contracts::EnumerateJitCodeRegions(target, &CDacLite::RegionSinkThunk, &sinkState); + if (jitRegions < 0) + { + Log(callback, "EnumMemoryRegions: JIT walk skipped (EEJitManager not found)"); + } + else + { + Log(callback, "EnumMemoryRegions: JIT walk reported %d region(s)", jitRegions); + } + + int loaderHeapRegions = cdac::contracts::EnumerateLoaderHeapRegions(target, &CDacLite::RegionSinkThunk, &sinkState); + if (loaderHeapRegions < 0) + { + Log(callback, "EnumMemoryRegions: loader-heap walk skipped (allocators not found)"); + } + else + { + Log(callback, "EnumMemoryRegions: loader-heap walk reported %d region(s)", loaderHeapRegions); + } + } + else + { + int scannedMethods = cdac::contracts::EnumerateStackScanRegions(target, &CDacLite::RegionSinkThunk, &sinkState); + if (scannedMethods < 0) + { + Log(callback, "EnumMemoryRegions: stack scan skipped (code range map not found)"); + } + else + { + Log(callback, "EnumMemoryRegions: stack scan captured %d method(s)", scannedMethods); + } + } + + // Heap-only structures: sync-block table, stress log, and COM interop. A Normal + // (stack-walk) dump does not need these. + if (heapTier) + { + int syncBlockRegions = cdac::contracts::EnumerateSyncBlockRegions(target); + if (syncBlockRegions < 0) + { + Log(callback, "EnumMemoryRegions: sync-block walk skipped (sync table not found)"); + } + else + { + Log(callback, "EnumMemoryRegions: sync-block walk captured %d in-use block(s)", syncBlockRegions); + } + + int stressLogChunks = cdac::contracts::EnumerateStressLogRegions(target); + if (stressLogChunks < 0) + { + Log(callback, "EnumMemoryRegions: stress-log walk skipped (stress log not found)"); + } + else + { + Log(callback, "EnumMemoryRegions: stress-log walk captured %d chunk(s)", stressLogChunks); + } + + int interopRegions = cdac::contracts::EnumerateInteropRegions(target); + if (interopRegions < 0) + { + Log(callback, "EnumMemoryRegions: interop walk skipped (RCW cleanup list not found)"); + } + else + { + Log(callback, "EnumMemoryRegions: interop walk captured %d RCW(s)", interopRegions); + } + } + + // Report the contract self-description (descriptor + JSON + pointer_data + the global + // storage it references) so a contract tool can bootstrap from the dump alone. cdac-lite + // is a DAC replacement, so no DAC globals table. + int staticRegions = cdac::contracts::EnumerateStaticRegions(target, m_contractDescriptorAddr, + &CDacLite::RegionSinkThunk, &sinkState); + Log(callback, "EnumMemoryRegions: contract-bootstrap reported %d region(s)", staticRegions); + + return S_OK; + } +} diff --git a/src/coreclr/debug/cdaclite/json/CMakeLists.txt b/src/coreclr/debug/cdaclite/json/CMakeLists.txt new file mode 100644 index 00000000000000..bd473b819e5285 --- /dev/null +++ b/src/coreclr/debug/cdaclite/json/CMakeLists.txt @@ -0,0 +1,5 @@ +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +add_library_clr(cdacjson STATIC json.cpp) + +target_include_directories(cdacjson PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/src/coreclr/debug/cdaclite/json/json.cpp b/src/coreclr/debug/cdaclite/json/json.cpp new file mode 100644 index 00000000000000..b5ae88e87396e8 --- /dev/null +++ b/src/coreclr/debug/cdaclite/json/json.cpp @@ -0,0 +1,441 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +//***************************************************************************** +// json.cpp +// +// Implementation of the tiny JSON parser declared in json.h. +//***************************************************************************** + +#include "json.h" + +#include +#include + +namespace cdac +{ +namespace json +{ + const Value* Value::Find(const std::string& key) const + { + if (type != Type::Object) + { + return nullptr; + } + std::map::const_iterator it = object.find(key); + return (it == object.end()) ? nullptr : &it->second; + } + + bool Value::TryGetUInt64(uint64_t& out) const + { + if (type == Type::Number) + { + if (isInteger) + { + out = (uint64_t)integer; + return true; + } + if (number >= 0.0) + { + out = (uint64_t)number; + return true; + } + return false; + } + + if (type == Type::String) + { + const char* text = string.c_str(); + char* end = nullptr; + int base = 10; + if (string.size() > 2 && text[0] == '0' && (text[1] == 'x' || text[1] == 'X')) + { + base = 16; + } + errno = 0; + unsigned long long parsed = strtoull(text, &end, base); + if (errno != 0 || end == text || *end != '\0') + { + return false; + } + out = (uint64_t)parsed; + return true; + } + + return false; + } + + namespace + { + class Parser + { + private: + const char* m_cur; + const char* m_end; + std::string m_error; + + public: + Parser(const char* text, size_t length) + : m_cur(text), m_end(text + length) + { + } + + const std::string& Error() const { return m_error; } + + bool ParseDocument(Value& root) + { + SkipWhitespace(); + if (!ParseValue(root)) + { + return false; + } + SkipWhitespace(); + if (m_cur != m_end) + { + return Fail("trailing content after JSON value"); + } + return true; + } + + private: + bool Fail(const char* message) + { + if (m_error.empty()) + { + m_error = message; + } + return false; + } + + bool AtEnd() const { return m_cur >= m_end; } + char Peek() const { return AtEnd() ? '\0' : *m_cur; } + + void SkipWhitespace() + { + while (!AtEnd()) + { + char c = *m_cur; + if (c == ' ' || c == '\t' || c == '\r' || c == '\n') + { + m_cur++; + } + else if (c == '/' && (m_cur + 1) < m_end && m_cur[1] == '/') + { + m_cur += 2; + while (!AtEnd() && *m_cur != '\n') + { + m_cur++; + } + } + else if (c == '/' && (m_cur + 1) < m_end && m_cur[1] == '*') + { + m_cur += 2; + while ((m_cur + 1) < m_end && !(m_cur[0] == '*' && m_cur[1] == '/')) + { + m_cur++; + } + if ((m_cur + 1) < m_end) + { + m_cur += 2; + } + else + { + m_cur = m_end; + } + } + else + { + break; + } + } + } + + bool ParseValue(Value& value) + { + SkipWhitespace(); + if (AtEnd()) + { + return Fail("unexpected end of input"); + } + + char c = Peek(); + switch (c) + { + case '{': + return ParseObject(value); + case '[': + return ParseArray(value); + case '"': + value.type = Type::String; + return ParseString(value.string); + case 't': + case 'f': + return ParseBool(value); + case 'n': + return ParseNull(value); + default: + if (c == '-' || (c >= '0' && c <= '9')) + { + return ParseNumber(value); + } + return Fail("unexpected character"); + } + } + + bool ParseObject(Value& value) + { + value.type = Type::Object; + m_cur++; // consume '{' + SkipWhitespace(); + if (Peek() == '}') + { + m_cur++; + return true; + } + + for (;;) + { + SkipWhitespace(); + if (Peek() != '"') + { + return Fail("expected object key"); + } + std::string key; + if (!ParseString(key)) + { + return false; + } + SkipWhitespace(); + if (Peek() != ':') + { + return Fail("expected ':' after object key"); + } + m_cur++; // consume ':' + + Value child; + if (!ParseValue(child)) + { + return false; + } + value.object[key] = child; + + SkipWhitespace(); + char c = Peek(); + if (c == ',') + { + m_cur++; + continue; + } + if (c == '}') + { + m_cur++; + return true; + } + return Fail("expected ',' or '}' in object"); + } + } + + bool ParseArray(Value& value) + { + value.type = Type::Array; + m_cur++; // consume '[' + SkipWhitespace(); + if (Peek() == ']') + { + m_cur++; + return true; + } + + for (;;) + { + Value child; + if (!ParseValue(child)) + { + return false; + } + value.array.push_back(child); + + SkipWhitespace(); + char c = Peek(); + if (c == ',') + { + m_cur++; + continue; + } + if (c == ']') + { + m_cur++; + return true; + } + return Fail("expected ',' or ']' in array"); + } + } + + bool ParseString(std::string& out) + { + m_cur++; // consume opening quote + out.clear(); + while (!AtEnd()) + { + char c = *m_cur++; + if (c == '"') + { + return true; + } + if (c == '\\') + { + if (AtEnd()) + { + return Fail("unterminated escape"); + } + char esc = *m_cur++; + switch (esc) + { + case '"': out.push_back('"'); break; + case '\\': out.push_back('\\'); break; + case '/': out.push_back('/'); break; + case 'b': out.push_back('\b'); break; + case 'f': out.push_back('\f'); break; + case 'n': out.push_back('\n'); break; + case 'r': out.push_back('\r'); break; + case 't': out.push_back('\t'); break; + case 'u': + { + if ((m_cur + 4) > m_end) + { + return Fail("truncated \\u escape"); + } + unsigned int code = 0; + for (int i = 0; i < 4; i++) + { + char h = *m_cur++; + code <<= 4; + if (h >= '0' && h <= '9') code |= (unsigned)(h - '0'); + else if (h >= 'a' && h <= 'f') code |= (unsigned)(h - 'a' + 10); + else if (h >= 'A' && h <= 'F') code |= (unsigned)(h - 'A' + 10); + else return Fail("invalid \\u escape"); + } + // Emit as UTF-8 (BMP only; the descriptor uses ASCII keys/values). + if (code < 0x80) + { + out.push_back((char)code); + } + else if (code < 0x800) + { + out.push_back((char)(0xC0 | (code >> 6))); + out.push_back((char)(0x80 | (code & 0x3F))); + } + else + { + out.push_back((char)(0xE0 | (code >> 12))); + out.push_back((char)(0x80 | ((code >> 6) & 0x3F))); + out.push_back((char)(0x80 | (code & 0x3F))); + } + break; + } + default: + return Fail("invalid escape character"); + } + } + else + { + out.push_back(c); + } + } + return Fail("unterminated string"); + } + + bool ParseNumber(Value& value) + { + const char* start = m_cur; + bool isFloat = false; + + if (Peek() == '-') + { + m_cur++; + } + while (!AtEnd()) + { + char c = *m_cur; + if (c >= '0' && c <= '9') + { + m_cur++; + } + else if (c == '.' || c == 'e' || c == 'E' || c == '+' || c == '-') + { + isFloat = (c == '.' || c == 'e' || c == 'E') ? true : isFloat; + m_cur++; + } + else + { + break; + } + } + + if (m_cur == start) + { + return Fail("invalid number"); + } + + value.type = Type::Number; + value.rawNumber.assign(start, (size_t)(m_cur - start)); + + value.number = strtod(value.rawNumber.c_str(), nullptr); + if (!isFloat) + { + char* end = nullptr; + errno = 0; + long long parsed = strtoll(value.rawNumber.c_str(), &end, 10); + if (errno == 0 && end != value.rawNumber.c_str() && *end == '\0') + { + value.integer = (int64_t)parsed; + value.isInteger = true; + } + } + return true; + } + + bool ParseBool(Value& value) + { + if ((size_t)(m_end - m_cur) >= 4 && strncmp(m_cur, "true", 4) == 0) + { + m_cur += 4; + value.type = Type::Boolean; + value.boolean = true; + return true; + } + if ((size_t)(m_end - m_cur) >= 5 && strncmp(m_cur, "false", 5) == 0) + { + m_cur += 5; + value.type = Type::Boolean; + value.boolean = false; + return true; + } + return Fail("invalid literal"); + } + + bool ParseNull(Value& value) + { + if ((size_t)(m_end - m_cur) >= 4 && strncmp(m_cur, "null", 4) == 0) + { + m_cur += 4; + value.type = Type::Null; + return true; + } + return Fail("invalid literal"); + } + }; + } + + bool Parse(const char* text, size_t length, Value& root, std::string& error) + { + Parser parser(text, length); + if (!parser.ParseDocument(root)) + { + error = parser.Error(); + return false; + } + error.clear(); + return true; + } +} +} diff --git a/src/coreclr/debug/cdaclite/json/json.h b/src/coreclr/debug/cdaclite/json/json.h new file mode 100644 index 00000000000000..40c888317007d2 --- /dev/null +++ b/src/coreclr/debug/cdaclite/json/json.h @@ -0,0 +1,71 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +//***************************************************************************** +// json.h +// +// A tiny, dependency-free JSON parser used to read the in-memory data +// descriptor. It supports the standard JSON grammar plus `//` and `/* */` +// comments (the "jsonc" superset used by the data descriptor docs). +//***************************************************************************** + +#ifndef CDACLITE_JSON_H +#define CDACLITE_JSON_H + +#include +#include +#include +#include + +namespace cdac +{ +namespace json +{ + enum class Type + { + Null, + Boolean, + Number, + String, + Array, + Object + }; + + class Value + { + public: + Type type = Type::Null; + + bool boolean = false; + + // Numbers keep both the raw text and the parsed forms so callers can + // pick the interpretation they need. + std::string rawNumber; + double number = 0.0; + int64_t integer = 0; + bool isInteger = false; + + std::string string; + std::vector array; + std::map object; + + bool IsNull() const { return type == Type::Null; } + bool IsObject() const { return type == Type::Object; } + bool IsArray() const { return type == Type::Array; } + bool IsString() const { return type == Type::String; } + bool IsNumber() const { return type == Type::Number; } + + // Returns the child value for an object key, or nullptr if absent / not an object. + const Value* Find(const std::string& key) const; + + // Interprets this value as an unsigned 64-bit integer. Accepts JSON numbers, + // decimal strings, and hex strings ("0x..."). Returns false otherwise. + bool TryGetUInt64(uint64_t& out) const; + }; + + // Parses the entire text as a single JSON value. On failure returns false and + // fills 'error' with a short description. + bool Parse(const char* text, size_t length, Value& root, std::string& error); +} +} + +#endif // CDACLITE_JSON_H diff --git a/src/coreclr/debug/createdump/createdumpwindows.cpp b/src/coreclr/debug/createdump/createdumpwindows.cpp index 652b8ab048e525..d81fbb09173e73 100644 --- a/src/coreclr/debug/createdump/createdumpwindows.cpp +++ b/src/coreclr/debug/createdump/createdumpwindows.cpp @@ -14,6 +14,319 @@ typedef struct _PROCESS_BASIC_INFORMATION_ { ULONG_PTR InheritedFromUniqueProcessId; } PROCESS_BASIC_INFORMATION_; +typedef HRESULT (STDAPICALLTYPE* PFN_CLRDataCreateInstance)(REFIID iid, ICLRDataTarget* target, void** iface); + +// +// cdac-lite integration: instead of letting dbghelp's auxiliary provider drive the legacy DAC +// (mscordaccore) to select managed memory for heap dumps, we ask cdac-lite -- a small native +// component that reads the runtime's contract/data descriptors -- to enumerate the managed +// regions, and feed those to MiniDumpWriteDump via a memory callback. Enabled with +// DOTNET_DbgUseCdacLite=1. The cdac-lite DLL is loaded from DOTNET_DbgCdacLitePath if set, +// otherwise from next to coreclr.dll in the target process. +// + +// Minimal ICLRDataTarget over a live target process (ReadProcessMemory + module base lookup). +class ProcessDataTarget : public ICLRDataTarget +{ + LONG m_ref; + HANDLE m_process; + +public: + ProcessDataTarget(HANDLE process) : m_ref(1), m_process(process) { } + + STDMETHOD(QueryInterface)(REFIID riid, void** ppvObject) + { + if (ppvObject == nullptr) + { + return E_POINTER; + } + if (riid == IID_IUnknown || riid == __uuidof(ICLRDataTarget)) + { + *ppvObject = static_cast(this); + AddRef(); + return S_OK; + } + *ppvObject = nullptr; + return E_NOINTERFACE; + } + + STDMETHOD_(ULONG, AddRef)() { return InterlockedIncrement(&m_ref); } + STDMETHOD_(ULONG, Release)() + { + LONG ref = InterlockedDecrement(&m_ref); + if (ref == 0) + { + delete this; + } + return ref; + } + + STDMETHOD(GetMachineType)(ULONG32* machine) + { +#if defined(_M_ARM64) + *machine = IMAGE_FILE_MACHINE_ARM64; +#elif defined(_M_ARM) + *machine = IMAGE_FILE_MACHINE_ARMNT; +#elif defined(_M_IX86) + *machine = IMAGE_FILE_MACHINE_I386; +#else + *machine = IMAGE_FILE_MACHINE_AMD64; +#endif + return S_OK; + } + + STDMETHOD(GetPointerSize)(ULONG32* size) + { + *size = sizeof(void*); + return S_OK; + } + + STDMETHOD(GetImageBase)(LPCWSTR moduleName, CLRDATA_ADDRESS* baseAddress) + { + HMODULE modules[1024]; + DWORD needed = 0; + if (!EnumProcessModulesEx(m_process, modules, sizeof(modules), &needed, LIST_MODULES_ALL)) + { + return E_FAIL; + } + DWORD count = needed / sizeof(HMODULE); + if (count > ARRAY_SIZE(modules)) + { + count = ARRAY_SIZE(modules); + } + WCHAR name[MAX_PATH]; + for (DWORD i = 0; i < count; i++) + { + if (GetModuleBaseNameW(m_process, modules[i], name, ARRAY_SIZE(name)) > 0 && + _wcsicmp(name, moduleName) == 0) + { + *baseAddress = (CLRDATA_ADDRESS)(ULONG_PTR)modules[i]; + return S_OK; + } + } + return E_FAIL; + } + + STDMETHOD(ReadVirtual)(CLRDATA_ADDRESS address, PBYTE buffer, ULONG32 size, ULONG32* done) + { + SIZE_T read = 0; + if (!ReadProcessMemory(m_process, (LPCVOID)(ULONG_PTR)address, buffer, size, &read)) + { + if (done != nullptr) + { + *done = 0; + } + return HRESULT_FROM_WIN32(GetLastError()); + } + if (done != nullptr) + { + *done = (ULONG32)read; + } + return S_OK; + } + + STDMETHOD(WriteVirtual)(CLRDATA_ADDRESS, PBYTE, ULONG32, ULONG32*) { return E_NOTIMPL; } + STDMETHOD(GetTLSValue)(ULONG32, ULONG32, CLRDATA_ADDRESS*) { return E_NOTIMPL; } + STDMETHOD(SetTLSValue)(ULONG32, ULONG32, CLRDATA_ADDRESS) { return E_NOTIMPL; } + STDMETHOD(GetCurrentThreadID)(ULONG32*) { return E_NOTIMPL; } + STDMETHOD(GetThreadContext)(ULONG32, ULONG32, ULONG32, PBYTE) { return E_NOTIMPL; } + STDMETHOD(SetThreadContext)(ULONG32, ULONG32, PBYTE) { return E_NOTIMPL; } + STDMETHOD(Request)(ULONG32, ULONG32, BYTE*, ULONG32, BYTE*) { return E_NOTIMPL; } +}; + +// Collects the [address, size) regions reported by cdac-lite's EnumMemoryRegions. +class CdacRegionCollector : public ICLRDataEnumMemoryRegionsCallback +{ + LONG m_ref; + +public: + std::vector m_regions; + + CdacRegionCollector() : m_ref(1) { } + + STDMETHOD(QueryInterface)(REFIID riid, void** ppvObject) + { + if (ppvObject == nullptr) + { + return E_POINTER; + } + if (riid == IID_IUnknown || riid == __uuidof(ICLRDataEnumMemoryRegionsCallback)) + { + *ppvObject = static_cast(this); + AddRef(); + return S_OK; + } + *ppvObject = nullptr; + return E_NOINTERFACE; + } + + STDMETHOD_(ULONG, AddRef)() { return InterlockedIncrement(&m_ref); } + STDMETHOD_(ULONG, Release)() { return InterlockedDecrement(&m_ref); } + + STDMETHOD(EnumMemoryRegion)(CLRDATA_ADDRESS address, ULONG32 size) + { + MINIDUMP_MEMORY_DESCRIPTOR64 region; + region.StartOfMemoryRange = address; + region.DataSize = size; + m_regions.push_back(region); + return S_OK; + } +}; + +struct CdacMemoryCallbackState +{ + const std::vector* regions; + size_t index; +}; + +// MiniDumpWriteDump memory callback: supplies one cdac-lite region per MemoryCallback invocation. +static BOOL CALLBACK +CdacMemoryCallback(PVOID param, const PMINIDUMP_CALLBACK_INPUT input, PMINIDUMP_CALLBACK_OUTPUT output) +{ + CdacMemoryCallbackState* state = (CdacMemoryCallbackState*)param; + if (input->CallbackType == MemoryCallback) + { + if (state->index < state->regions->size()) + { + const MINIDUMP_MEMORY_DESCRIPTOR64& region = (*state->regions)[state->index++]; + output->MemoryBase = region.StartOfMemoryRange; + output->MemorySize = (ULONG)region.DataSize; + } + else + { + output->MemoryBase = 0; + output->MemorySize = 0; + } + } + return TRUE; +} + +// Determines the cdac-lite DLL path: DOTNET_DbgCdacLitePath env var, else next to the target's coreclr.dll. +static bool +GetCdacLitePath(HANDLE hProcess, std::string& path) +{ + char envPath[MAX_LONGPATH]; + DWORD envLen = GetEnvironmentVariableA("DOTNET_DbgCdacLitePath", envPath, ARRAY_SIZE(envPath)); + if (envLen > 0 && envLen < ARRAY_SIZE(envPath)) + { + path.assign(envPath, envLen); + return true; + } + + HMODULE modules[1024]; + DWORD needed = 0; + if (!EnumProcessModulesEx(hProcess, modules, sizeof(modules), &needed, LIST_MODULES_ALL)) + { + return false; + } + DWORD count = needed / sizeof(HMODULE); + if (count > ARRAY_SIZE(modules)) + { + count = ARRAY_SIZE(modules); + } + char name[MAX_PATH]; + for (DWORD i = 0; i < count; i++) + { + if (GetModuleBaseNameA(hProcess, modules[i], name, ARRAY_SIZE(name)) > 0 && + _stricmp(name, MAKEDLLNAME_A("coreclr")) == 0) + { + char fullPath[MAX_LONGPATH]; + if (GetModuleFileNameExA(hProcess, modules[i], fullPath, ARRAY_SIZE(fullPath)) > 0) + { + std::string coreclrPath(fullPath); + size_t sep = coreclrPath.find_last_of("\\/"); + if (sep != std::string::npos) + { + path.assign(coreclrPath, 0, sep + 1); + path.append(MAKEDLLNAME_A("cdaclite")); + return true; + } + } + } + } + return false; +} + +// Writes a dump for the target process using cdac-lite for managed-memory selection instead of +// the legacy DAC. 'heapTier' selects the region set: true = heap dump (GC heaps + private R/W +// sweep); false = Normal dump (stack-walk-reachable state only, no GC heap sweep). Returns false +// if cdac-lite could not be used (the caller falls back to the normal MiniDumpWriteDump path). +static bool +TryCreateDumpWithCdacLite(HANDLE hProcess, DWORD pid, HANDLE hFile, bool heapTier) +{ + std::string cdacLitePath; + if (!GetCdacLitePath(hProcess, cdacLitePath)) + { + printf_error("cdac-lite: could not locate cdaclite.dll (set DOTNET_DbgCdacLitePath)\n"); + return false; + } + + HMODULE cdacLite = LoadLibraryA(cdacLitePath.c_str()); + if (cdacLite == nullptr) + { + printf_error("cdac-lite: LoadLibrary(%s) FAILED - %s\n", cdacLitePath.c_str(), GetLastErrorString().c_str()); + return false; + } + + bool result = false; + PFN_CLRDataCreateInstance pfnCreate = (PFN_CLRDataCreateInstance)GetProcAddress(cdacLite, "CLRDataCreateInstance"); + if (pfnCreate == nullptr) + { + printf_error("cdac-lite: GetProcAddress(CLRDataCreateInstance) FAILED\n"); + return false; + } + + ReleaseHolder dataTarget = new ProcessDataTarget(hProcess); + ReleaseHolder enumRegions; + HRESULT hr = pfnCreate(__uuidof(ICLRDataEnumMemoryRegions), dataTarget, (void**)&enumRegions); + if (FAILED(hr) || enumRegions == nullptr) + { + printf_error("cdac-lite: CLRDataCreateInstance(ICLRDataEnumMemoryRegions) FAILED (%08x)\n", hr); + return false; + } + + CdacRegionCollector collector; + // miniDumpFlags: MiniDumpWithPrivateReadWriteMemory (0x200) => heap tier (full GC heap + + // R/W sweep); MiniDumpNormal (0) => Normal tier (stack-walk-reachable state only). + ULONG32 enumFlags = heapTier ? MiniDumpWithPrivateReadWriteMemory : MiniDumpNormal; + hr = enumRegions->EnumMemoryRegions(&collector, enumFlags, CLRDATA_ENUM_MEM_DEFAULT); + if (FAILED(hr)) + { + printf_error("cdac-lite: EnumMemoryRegions FAILED (%08x)\n", hr); + return false; + } + printf_status("cdac-lite: selected %zu managed region(s) [%s tier]\n", + collector.m_regions.size(), heapTier ? "heap" : "normal"); + + // Heap tier (DAC heap-dump model): let dbghelp sweep all private read/write pages + // (MiniDumpWithPrivateReadWriteMemory) to capture the GC/loader/handle heaps -- object bytes + // included -- the same way the DAC path relies on that sweep. cdac-lite's memory callback adds + // the memory the sweep misses (executable JIT/stub RX pages, image-backed contract descriptor). + // Normal tier: MiniDumpNormal (stacks + module headers); cdac-lite supplies the stack-walk + // code + method metadata via the callback, no R/W heap sweep. + // (MiniDumpWithoutAuxiliaryState is intentionally NOT set: it breaks ClrMD's module export + // directory lookup for the contract descriptor. dbghelp does not load the legacy DAC anyway.) + MINIDUMP_TYPE dumpType = heapTier + ? (MINIDUMP_TYPE)(MiniDumpNormal | MiniDumpWithPrivateReadWriteMemory) + : MiniDumpNormal; + + CdacMemoryCallbackState state = { &collector.m_regions, 0 }; + MINIDUMP_CALLBACK_INFORMATION callbackInfo = {}; + callbackInfo.CallbackRoutine = &CdacMemoryCallback; + callbackInfo.CallbackParam = &state; + + if (MiniDumpWriteDump(hProcess, pid, hFile, dumpType, NULL, NULL, &callbackInfo)) + { + result = true; + } + else + { + printf_error("cdac-lite: MiniDumpWriteDump - %s\n", GetLastErrorString().c_str()); + } + + return result; +} + // // The Windows create dump code // @@ -64,26 +377,50 @@ CreateDump(const CreateDumpOptions& options) goto exit; } - int retryCount = 10; - // Retry the write dump on ERROR_PARTIAL_COPY - for (int i = 0; i <= retryCount; i++) + bool cdacHandled = false; { - if (MiniDumpWriteDump(hProcess, pid, hFile, GetMiniDumpType(options.DumpType), NULL, NULL, NULL)) + char envVal[8]; + DWORD envLen = GetEnvironmentVariableA("DOTNET_DbgUseCdacLite", envVal, ARRAY_SIZE(envVal)); + bool useCdacLite = (envLen == 1 && envVal[0] == '1'); + // cdac-lite selects managed memory for non-full dumps; full dumps already capture everything. + if (useCdacLite && options.DumpType != DumpType::Full) { - result = true; - break; + // Heap tier for withheap dumps; Normal tier for normal/triage (stack-walk-reachable only). + bool heapTier = (options.DumpType == DumpType::Heap); + printf_status("cdac-lite: collecting managed memory (DOTNET_DbgUseCdacLite=1, %s tier)\n", + heapTier ? "heap" : "normal"); + result = TryCreateDumpWithCdacLite(hProcess, pid, hFile, heapTier); + cdacHandled = result; + if (!result) + { + printf_error("cdac-lite: collection failed; falling back to default dump path\n"); + } } - else + } + + if (!cdacHandled) + { + int retryCount = 10; + // Retry the write dump on ERROR_PARTIAL_COPY + for (int i = 0; i <= retryCount; i++) { - int err = GetLastError(); - if (err != ERROR_PARTIAL_COPY || i == retryCount) + if (MiniDumpWriteDump(hProcess, pid, hFile, GetMiniDumpType(options.DumpType), NULL, NULL, NULL)) { - printf_error("MiniDumpWriteDump - %s\n", GetLastErrorString().c_str()); + result = true; break; } else { - printf_error("Retry %d of MiniDumpWriteDump due to - %s\n", i, GetLastErrorString().c_str()); + int err = GetLastError(); + if (err != ERROR_PARTIAL_COPY || i == retryCount) + { + printf_error("MiniDumpWriteDump - %s\n", GetLastErrorString().c_str()); + break; + } + else + { + printf_error("Retry %d of MiniDumpWriteDump due to - %s\n", i, GetLastErrorString().c_str()); + } } } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs index d0efe07cabba36..c086e2b98af3d7 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs @@ -28,6 +28,23 @@ public sealed unsafe class ContractDescriptorTarget : Target { private const int StackAllocByteThreshold = 1024; + // Opt-in diagnostic: when DOTNET_CDAC_LogReadFailures=1, log the failing address and the + // managed stack trace to stderr whenever a virtual read fails. Useful for discovering which + // memory regions a memory-selective dump (e.g. cdac-lite) is missing. + private static readonly bool s_logReadFailures = + Environment.GetEnvironmentVariable("DOTNET_CDAC_LogReadFailures") == "1"; + + [DoesNotReturn] + private static VirtualReadException ThrowReadFailure(string message) + { + if (s_logReadFailures) + { + Console.Error.WriteLine($"[cdac] VirtualReadException: {message}"); + Console.Error.WriteLine(new StackTrace(fNeedFileInfo: true).ToString()); + } + throw new VirtualReadException(message); + } + private readonly struct Configuration { public bool IsLittleEndian { get; init; } @@ -437,7 +454,7 @@ public override bool TrySetThreadContext(ulong threadId, ReadOnlySpan cont public override T Read(ulong address) { if (!TryRead(address, _config.IsLittleEndian, _dataTargetDelegates, out T value)) - throw new VirtualReadException($"Failed to read {typeof(T)} at 0x{address:x8}."); + throw ThrowReadFailure($"Failed to read {typeof(T)} at 0x{address:x8}."); return value; } @@ -451,7 +468,7 @@ public override T Read(ulong address) public override T ReadLittleEndian(ulong address) { if (!TryRead(address, true, _dataTargetDelegates, out T value)) - throw new VirtualReadException($"Failed to read {typeof(T)} at 0x{address:x8}."); + throw ThrowReadFailure($"Failed to read {typeof(T)} at 0x{address:x8}."); return value; } @@ -544,7 +561,7 @@ private static T Read(ReadOnlySpan bytes, bool isLittleEndian) where T public override void ReadBuffer(ulong address, Span buffer) { if (!TryReadBuffer(address, buffer)) - throw new VirtualReadException($"Failed to read {buffer.Length} bytes at 0x{address:x8}."); + throw ThrowReadFailure($"Failed to read {buffer.Length} bytes at 0x{address:x8}."); } private bool TryReadBuffer(ulong address, Span buffer) @@ -588,7 +605,7 @@ private static bool IsSigned() where T : struct, INumberBase, IMinMaxValue public override TargetPointer ReadPointer(ulong address) { if (!TryReadPointer(address, _config, _dataTargetDelegates, out TargetPointer pointer)) - throw new VirtualReadException($"Failed to read pointer at 0x{address:x8}."); + throw ThrowReadFailure($"Failed to read pointer at 0x{address:x8}."); return pointer; } @@ -619,7 +636,7 @@ public override TargetCodePointer ReadCodePointer(ulong address) { return new TargetCodePointer(Read(address)); } - throw new VirtualReadException($"Failed to read code pointer at 0x{address:x8} because CodePointer size is not 4 or 8"); + throw ThrowReadFailure($"Failed to read code pointer at 0x{address:x8} because CodePointer size is not 4 or 8"); } public override bool TryReadCodePointer(ulong address, out TargetCodePointer value) @@ -720,7 +737,7 @@ public override string ReadUtf16String(ulong address) public override TargetNUInt ReadNUInt(ulong address) { if (!TryReadNUInt(address, _config, _dataTargetDelegates, out ulong value)) - throw new VirtualReadException($"Failed to read nuint at 0x{address:x8}."); + throw ThrowReadFailure($"Failed to read nuint at 0x{address:x8}."); return new TargetNUInt(value); } @@ -728,7 +745,7 @@ public override TargetNUInt ReadNUInt(ulong address) public override TargetNInt ReadNInt(ulong address) { if (!TryReadNInt(address, _config, _dataTargetDelegates, out long value)) - throw new VirtualReadException($"Failed to read nint at 0x{address:x8}."); + throw ThrowReadFailure($"Failed to read nint at 0x{address:x8}."); return new TargetNInt(value); } diff --git a/src/native/managed/cdac/scripts/DumpHelpers.cs b/src/native/managed/cdac/scripts/DumpHelpers.cs index e4b1c93d7e7326..3eb926ba5cad7c 100644 --- a/src/native/managed/cdac/scripts/DumpHelpers.cs +++ b/src/native/managed/cdac/scripts/DumpHelpers.cs @@ -55,12 +55,13 @@ public static ContractDescriptorTarget CreateCdacTarget(DataTarget dt) (uint threadId, uint contextFlags, Span buffer) => dt.DataReader.GetThreadContext(threadId, contextFlags, buffer) ? 0 : -1, (uint threadId, ReadOnlySpan context) => -1, + (ulong size, out ulong allocatedAddress) => { allocatedAddress = 0; return -1; }, [CoreCLRContracts.Register], out ContractDescriptorTarget? target)) { throw new InvalidOperationException("Failed to create cDAC target."); } - return target; + return target!; } } diff --git a/src/native/managed/cdac/scripts/StacksCommand.cs b/src/native/managed/cdac/scripts/StacksCommand.cs index dd3a5aff1da81f..39c7f664572924 100644 --- a/src/native/managed/cdac/scripts/StacksCommand.cs +++ b/src/native/managed/cdac/scripts/StacksCommand.cs @@ -76,7 +76,7 @@ private static void Execute(string dumpPath) break; } - Console.WriteLine($"Thread {idx} (OS ID: 0x{td.OSId:x}):"); + Console.WriteLine($"Thread {idx} (OS ID: 0x{td.OSId.Value:x}):"); try { diff --git a/src/native/managed/cdac/scripts/ThreadsCommand.cs b/src/native/managed/cdac/scripts/ThreadsCommand.cs index 6afbc60360a7cb..ab483b3badf5f2 100644 --- a/src/native/managed/cdac/scripts/ThreadsCommand.cs +++ b/src/native/managed/cdac/scripts/ThreadsCommand.cs @@ -66,7 +66,7 @@ private static void Execute(string dumpPath) try { ThreadData td = threadContract.GetThreadData(threadAddr); - Console.WriteLine($"Thread {idx}: OS ID=0x{td.OSId:x}, State=0x{(uint)td.State:x}, Addr={threadAddr}"); + Console.WriteLine($"Thread {idx}: OS ID=0x{td.OSId.Value:x}, State=0x{(uint)td.State:x}, Addr={threadAddr}"); threadAddr = td.NextThread; } catch (Exception ex) diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/Directory.Build.props b/src/native/managed/cdac/tests/DumpTests/Debuggees/Directory.Build.props index 54760600c2fb6f..64c74fb99eaab5 100644 --- a/src/native/managed/cdac/tests/DumpTests/Debuggees/Directory.Build.props +++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/Directory.Build.props @@ -11,7 +11,7 @@ $(NoWarn);CA1852 + Supported values: "Mini", "Heap", "Full", or any combination separated by ';'. --> Heap + + <_DumpInfoMini Include="@(_DumpInfoDebuggeeTypes)" Condition="$([System.String]::new('%(DumpTypes)').Contains('Mini')) AND '$(DumpRuntimeVersion)' != 'net10.0'" DumpDir="mini" /> <_DumpInfoHeap Include="@(_DumpInfoDebuggeeTypes)" Condition="$([System.String]::new('%(DumpTypes)').Contains('Heap')) AND '$(DumpRuntimeVersion)' != 'net10.0'" DumpDir="heap" /> <_DumpInfoFull Include="@(_DumpInfoDebuggeeTypes)" Condition="$([System.String]::new('%(DumpTypes)').Contains('Full'))" DumpDir="full" /> - <_DumpInfoByType Include="@(_DumpInfoHeap);@(_DumpInfoFull)" /> + <_DumpInfoByType Include="@(_DumpInfoMini);@(_DumpInfoHeap);@(_DumpInfoFull)" /> <_DumpInfoR2R Include="@(_DumpInfoByType)" Condition="$([System.String]::new('%(R2RModes)').Contains('R2R'))" R2RDir="r2r" /> <_DumpInfoJit Include="@(_DumpInfoByType)" Condition="$([System.String]::new('%(R2RModes)').Contains('Jit'))" R2RDir="jit" /> <_DumpInfoAll Include="@(_DumpInfoR2R);@(_DumpInfoJit)" /> @@ -222,8 +224,10 @@ + <_MiniDumpType Condition="'$(_DumpTypeName)' == 'Mini'">1 <_MiniDumpType Condition="'$(_DumpTypeName)' == 'Heap'">2 <_MiniDumpType Condition="'$(_DumpTypeName)' == 'Full'">4 + <_DumpTypeDirName Condition="'$(_DumpTypeName)' == 'Mini'">mini <_DumpTypeDirName Condition="'$(_DumpTypeName)' == 'Heap'">heap <_DumpTypeDirName Condition="'$(_DumpTypeName)' == 'Full'">full @@ -265,8 +269,8 @@ - - + + + Condition="!Exists('$(_DumpFile)') AND '$(DumpRuntimeVersion)' == 'net10.0' AND '$(_DumpTypeName)' != 'Heap' AND '$(_DumpTypeName)' != 'Mini'" /> diff --git a/src/native/managed/cdac/tests/DumpTests/MiniDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/MiniDumpTests.cs new file mode 100644 index 00000000000000..343ae7db94da3e --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/MiniDumpTests.cs @@ -0,0 +1,206 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.DataContractReader.TestInfrastructure; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.DumpTests; + +/// +/// Dump-based integration tests that validate the cDAC reader against a Mini (normal) +/// minidump (MiniDumpNormal, DOTNET_DbgMiniDumpType=1). +/// +/// A mini dump only contains the memory the runtime reports while walking each thread's +/// stack — it does NOT include the GC heap. Per the runtime's dump-generation design +/// (EnumMemoryRegionsWorkerSkinny) and the documented SOS behavior for triage/normal +/// dumps, the supported scenarios are limited to: +/// * stack traces for all threads — clrstack (see ) +/// * managed thread enumeration — clrthreads (see ) +/// * current exception viewing — !pe +/// * partial module info — lm / !eeheap -loader +/// Heap-dependent scenarios (!dumpheap, !dumpobj, !gcroot, locals) are +/// intentionally NOT covered because the backing memory is absent from a mini dump. +/// +public class MiniDumpStackWalkTests : DumpTestBase +{ + protected override string DebuggeeName => "StackWalk"; + protected override string DumpType => "mini"; + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "InlinedCallFrame.Datum was added after net10.0")] + public void CanWalkCrashingThread(TestConfiguration config) + { + InitializeDumpTest(config); + IStackWalk stackWalk = Target.Contracts.StackWalk; + + ThreadData crashingThread = DumpTestHelpers.FindFailFastThread(Target); + + List frames = DumpTestStackWalker.LegacyVisibleFrames(stackWalk, crashingThread).ToList(); + + Assert.True(frames.Count > 0, "Expected at least one stack frame on the crashing thread"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "InlinedCallFrame.Datum was added after net10.0")] + public void HasMultipleFrames(TestConfiguration config) + { + InitializeDumpTest(config); + IStackWalk stackWalk = Target.Contracts.StackWalk; + + ThreadData crashingThread = DumpTestHelpers.FindFailFastThread(Target); + + List frames = DumpTestStackWalker.LegacyVisibleFrames(stackWalk, crashingThread).ToList(); + + // The debuggee has Main → MethodA → MethodB → MethodC → FailFast, + // plus runtime helper and native transition frames. + Assert.True(frames.Count >= 5, + $"Expected multiple stack frames from the crashing thread, got {frames.Count}"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "InlinedCallFrame.Datum was added after net10.0")] + public void ContainsExpectedFrames(TestConfiguration config) + { + InitializeDumpTest(config); + + ThreadData crashingThread = DumpTestHelpers.FindFailFastThread(Target); + + DumpTestStackWalker.Walk(Target, crashingThread) + .ExpectFrame("MethodC") + .ExpectAdjacentFrame("MethodB") + .ExpectAdjacentFrame("MethodA") + .ExpectAdjacentFrame("Main") + .Verify(); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "InlinedCallFrame.Datum was added after net10.0")] + public void ManagedFramesHaveValidMethodDescs(TestConfiguration config) + { + InitializeDumpTest(config); + IStackWalk stackWalk = Target.Contracts.StackWalk; + IRuntimeTypeSystem rts = Target.Contracts.RuntimeTypeSystem; + + ThreadData crashingThread = DumpTestHelpers.FindFailFastThread(Target); + + foreach (IStackDataFrameHandle frame in DumpTestStackWalker.LegacyVisibleFrames(stackWalk, crashingThread)) + { + TargetPointer methodDescPtr = stackWalk.GetMethodDescPtr(frame); + if (methodDescPtr == TargetPointer.Null) + continue; + + MethodDescHandle mdHandle = rts.GetMethodDescHandle(methodDescPtr); + uint token = rts.GetMethodToken(mdHandle); + Assert.Equal(0x06000000u, token & 0xFF000000u); + } + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "InlinedCallFrame.Datum was added after net10.0")] + public void FramesHaveRawContext(TestConfiguration config) + { + InitializeDumpTest(config); + IStackWalk stackWalk = Target.Contracts.StackWalk; + + ThreadData crashingThread = DumpTestHelpers.FindFailFastThread(Target); + + IStackDataFrameHandle? firstFrame = DumpTestStackWalker.LegacyVisibleFrames(stackWalk, crashingThread).FirstOrDefault(); + Assert.NotNull(firstFrame); + + byte[] context = stackWalk.GetRawContext(firstFrame); + Assert.NotNull(context); + Assert.True(context.Length > 0, "Expected non-empty raw context for stack frame"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "InlinedCallFrame.Datum was added after net10.0")] + public void GetContext_ReturnsNonEmptyContext(TestConfiguration config) + { + InitializeDumpTest(config); + + ThreadData crashingThread = DumpTestHelpers.FindFailFastThread(Target); + uint allFlags = Contracts.StackWalkHelpers.IPlatformAgnosticContext.GetContextForPlatform(Target).AllContextFlags; + byte[] context = Target.Contracts.StackWalk.GetContext(crashingThread, ThreadContextSource.None, allFlags); + + Assert.NotNull(context); + Assert.True(context.Length > 0, "Expected non-empty context"); + + var ctx = Contracts.StackWalkHelpers.IPlatformAgnosticContext.GetContextForPlatform(Target); + ctx.FillFromBuffer(context); + Assert.NotEqual(TargetCodePointer.Null, ctx.InstructionPointer); + } +} + +/// +/// Mini-tier coverage for the clrthreads scenario. A normal minidump iterates the +/// thread store while enumerating each thread's stack (EnumMemDumpAllThreadsStack), +/// so the thread store and every record it touches are captured. +/// This validates that the Thread contract can enumerate the thread list from a mini dump +/// even though the GC heap is absent. +/// +public class MiniDumpThreadTests : DumpTestBase +{ + protected override string DebuggeeName => "StackWalk"; + protected override string DumpType => "mini"; + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "InlinedCallFrame.Datum was added after net10.0")] + public void ThreadStoreData_IsReadable(TestConfiguration config) + { + InitializeDumpTest(config); + IThread threadContract = Target.Contracts.Thread; + Assert.NotNull(threadContract); + + ThreadStoreData storeData = threadContract.GetThreadStoreData(); + + Assert.True(storeData.ThreadCount > 0, $"Expected at least one thread, got {storeData.ThreadCount}"); + Assert.NotEqual(TargetPointer.Null, storeData.FirstThread); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "InlinedCallFrame.Datum was added after net10.0")] + public void EnumerateThreads_CanWalkThreadList(TestConfiguration config) + { + InitializeDumpTest(config); + IThread threadContract = Target.Contracts.Thread; + + ThreadStoreData storeData = threadContract.GetThreadStoreData(); + + int count = 0; + HashSet seenIds = new(); + TargetPointer currentThread = storeData.FirstThread; + while (currentThread != TargetPointer.Null) + { + ThreadData threadData = threadContract.GetThreadData(currentThread); + count++; + Assert.True(seenIds.Add(threadData.Id), $"Duplicate thread ID: {threadData.Id}"); + currentThread = threadData.NextThread; + } + + Assert.Equal(storeData.ThreadCount, count); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "InlinedCallFrame.Datum was added after net10.0")] + public void ThreadStoreData_HasFinalizerThread(TestConfiguration config) + { + InitializeDumpTest(config); + IThread threadContract = Target.Contracts.Thread; + + ThreadStoreData storeData = threadContract.GetThreadStoreData(); + + Assert.NotEqual(TargetPointer.Null, storeData.FinalizerThread); + } +} diff --git a/src/native/managed/cdac/tests/DumpTests/README.md b/src/native/managed/cdac/tests/DumpTests/README.md index 5c637320b35911..d275ad25080587 100644 --- a/src/native/managed/cdac/tests/DumpTests/README.md +++ b/src/native/managed/cdac/tests/DumpTests/README.md @@ -29,7 +29,7 @@ features and then calls `Environment.FailFast()` to produce a crash dump. | BasicThreads | Thread management, thread store | Heap | | GCRoots | GC object graphs, pinned handles | Heap | | ServerGC | Server GC mode heap structures | Heap | -| StackWalk | Deterministic call stack (Main→A→B→C→FailFast) | Heap | +| StackWalk | Deterministic call stack (Main→A→B→C→FailFast) | Heap, Mini | | MultiModule | Multi-assembly metadata resolution | Heap | | TypeHierarchy | Type inheritance, method tables | Heap | | PInvokeStub | P/Invoke with SetLastError ILStub | Full | @@ -44,9 +44,20 @@ features and then calls `Environment.FailFast()` to produce a crash dump. | AsyncContinuation | Reading async continuation method tables | Heap | The dump type is configured per-debuggee via the `DumpTypes` property in each debuggee's -`.csproj` (default: `Heap`, set in `Debuggees/Directory.Build.props`). Debuggees that +`.csproj` (default: `Heap`, set in `Debuggees/Directory.Build.props`). Supported values are +`Mini` (normal minidump, type 1), `Heap` (type 2), and `Full` (type 4); a debuggee can +request several at once by separating them with `;` (e.g. `Heap;Mini`). Debuggees that need full memory content (e.g., metadata-heavy scenarios) override this to `Full`. +A `Mini` dump contains only the memory the runtime reports while walking each thread's +stack — it does NOT include the GC heap. Mini-tier tests therefore validate only the +scenarios documented to work against a normal/triage dump: stack traces (`clrstack`), +managed thread enumeration (`clrthreads`), current-exception viewing (`!pe`), and partial +module info. Heap-dependent scenarios (`!dumpheap`, `!dumpobj`, `!gcroot`, locals) are NOT +expected to work. This scope mirrors the runtime's own Normal-tier enumeration +(`EnumMemoryRegionsWorkerSkinny` in `enummem.cpp`), which captures thread stacks, the module +list, CLR statics, AppDomain info, and any memory implicitly reached from those roots. + ### Test Classes Each test class targets a specific cDAC contract and specifies which debuggee dump to @@ -59,6 +70,8 @@ use. Tests are `[ConditionalTheory]` methods parameterized by `TestConfiguration | WorkstationGCDumpTests | GC (Workstation) | GCRoots | | ServerGCDumpTests | GC (Server) | ServerGC | | StackWalkDumpTests | StackWalk | StackWalk | +| MiniDumpStackWalkTests | StackWalk (mini/normal dump — `clrstack`) | StackWalk | +| MiniDumpThreadTests | Thread (mini/normal dump — `clrthreads`) | StackWalk | | RuntimeTypeSystemDumpTests | RuntimeTypeSystem | TypeHierarchy | | LoaderDumpTests | Loader | MultiModule | | EcmaMetadataDumpTests | EcmaMetadata | MultiModule | @@ -130,6 +143,11 @@ For example: artifacts/dumps/cdac/ local/ dump-info.json + mini/ + r2r/ + StackWalk/StackWalk.dmp + jit/ + StackWalk/StackWalk.dmp heap/ r2r/ BasicThreads/BasicThreads.dmp @@ -279,14 +297,15 @@ The pipeline has three stages: `Environment.FailFast("message")` to trigger a crash dump. 3. The `.csproj` inherits defaults from `Debuggees/Directory.Build.props` (output path, target frameworks, `DumpTypes=Heap`). -4. Override `Full` if your test needs full memory dumps. +4. Override `` (e.g. `Full`, `Mini`, or `Heap;Mini`) to change which dump + tiers are generated. 5. The debuggee is auto-discovered by `DumpTests.targets` — no list to update. ## Adding a New Test 1. Create a new test class inheriting from `DumpTestBase`. 2. Override `DebuggeeName` to match the debuggee directory name. -3. Override `DumpType` if the debuggee uses full dumps (`"full"`). +3. Override `DumpType` if the debuggee uses non-heap dumps (`"full"` or `"mini"`). 4. Write tests as `[ConditionalTheory]` with `[MemberData(nameof(TestConfigurations))]`. 5. Call `InitializeDumpTest(config)` as the first line of every test method. 6. Use `[SkipOnVersion("net10.0", "reason")]` for version-specific skipping. diff --git a/src/native/managed/cdac/tests/TestInfrastructure/DumpTestBase.cs b/src/native/managed/cdac/tests/TestInfrastructure/DumpTestBase.cs index c98afbe8b41c96..7475433e45ffee 100644 --- a/src/native/managed/cdac/tests/TestInfrastructure/DumpTestBase.cs +++ b/src/native/managed/cdac/tests/TestInfrastructure/DumpTestBase.cs @@ -159,9 +159,9 @@ public void Dispose() /// private void EvaluateSkipAttributes(TestConfiguration config, string callerName, string? dumpType = null) { - if (config.RuntimeVersion is "net10.0" && (dumpType ?? DumpType) == "heap") + if (config.RuntimeVersion is "net10.0" && ((dumpType ?? DumpType) is "heap" or "mini")) { - throw new SkipTestException($"[net10.0] Skipping heap dump tests due to outdated dump generation."); + throw new SkipTestException($"[net10.0] Skipping {dumpType ?? DumpType} dump tests due to outdated dump generation."); } MethodInfo? method = GetType().GetMethod(callerName, BindingFlags.Public | BindingFlags.Instance); From abb2dcf65f2e624c6d8f9f1167dbe94a5de1f065 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Mon, 6 Jul 2026 22:05:42 -0400 Subject: [PATCH 02/13] [prototype] cdac-lite: SOS coverage, dumpdomain/GC-info, tier-aware enumerators, CI flag Builds on the base cdac-lite prototype commit with this session's work: - Refactor stackscan into a CodeEnumerator class (one deduped Emit + per-type Enum* methods mirroring the DAC EnumMemoryRegions pattern). - Rearchitect the driver into a uniform table of tier-aware enumerators; each self-selects by target.Tier() (Target now carries ClrBase/ContractDescriptorAddr). New contracts/bootstrap.cpp for the export dir + global singletons. - Close the JIT stack-walk gap (native-code-slot re-resolution, frame-chain transition frames, unwind info) and emit GC info + full dumpdomain state (global LoaderAllocator + assembly-list arrays) so SOS clrthreads/clrstack/ dumpmd/ip2md/dumpmt/dumpmodule/dumpdomain work on cdac-lite normal dumps. - Add MiniDumpSosDacTests exercising the ISOSDacInterface APIs those commands use. - CI: UseCdacLite flag (DumpTests.targets + RunDumpTests.ps1) and an opt-in cdacUseCdacLite parameter wiring the runtime-diagnostics pipeline to generate + validate cdac-lite dumps on Helix (Windows). 30/30 MiniDumpTests pass; cdac-lite builds clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../cdac/prepare-cdac-helix-steps.yml | 23 + eng/pipelines/runtime-diagnostics.yml | 7 +- src/coreclr/debug/cdaclite/cdaclite.cpp | 6 +- src/coreclr/debug/cdaclite/cdaclite.h | 3 +- .../debug/cdaclite/contracts/CMakeLists.txt | 1 + .../debug/cdaclite/contracts/bootstrap.cpp | 121 +++ .../debug/cdaclite/contracts/bootstrap.h | 32 + src/coreclr/debug/cdaclite/contracts/gc.cpp | 4 + .../debug/cdaclite/contracts/handles.cpp | 4 + .../debug/cdaclite/contracts/interop.cpp | 6 +- .../debug/cdaclite/contracts/interop.h | 2 +- .../debug/cdaclite/contracts/loader.cpp | 49 + .../debug/cdaclite/contracts/stackscan.cpp | 999 +++++++++++++++--- .../debug/cdaclite/contracts/statics.cpp | 4 +- .../debug/cdaclite/contracts/statics.h | 3 +- .../debug/cdaclite/contracts/stresslog.cpp | 6 +- .../debug/cdaclite/contracts/stresslog.h | 2 +- .../debug/cdaclite/contracts/syncblock.cpp | 6 +- .../debug/cdaclite/contracts/syncblock.h | 2 +- .../debug/cdaclite/contracts/thread.cpp | 13 + .../debug/cdaclite/data/runtimetypes.h | 66 +- src/coreclr/debug/cdaclite/data/target.cpp | 38 +- src/coreclr/debug/cdaclite/data/target.h | 21 + src/coreclr/debug/cdaclite/enumerate.cpp | 202 ++-- .../debug/createdump/createdumpwindows.cpp | 11 +- .../managed/cdac/scripts/DumpHelpers.cs | 108 +- .../cdac/tests/DumpTests/DumpTests.targets | 23 +- .../cdac/tests/DumpTests/MiniDumpTests.cs | 182 ++++ .../cdac/tests/DumpTests/RunDumpTests.ps1 | 15 + .../cdac/tests/DumpTests/cdac-dump-helix.proj | 4 + .../tests/TestInfrastructure/ClrMdDumpHost.cs | 47 +- 31 files changed, 1660 insertions(+), 350 deletions(-) create mode 100644 src/coreclr/debug/cdaclite/contracts/bootstrap.cpp create mode 100644 src/coreclr/debug/cdaclite/contracts/bootstrap.h diff --git a/eng/pipelines/cdac/prepare-cdac-helix-steps.yml b/eng/pipelines/cdac/prepare-cdac-helix-steps.yml index 0721fc5ac65061..9d293788cefe74 100644 --- a/eng/pipelines/cdac/prepare-cdac-helix-steps.yml +++ b/eng/pipelines/cdac/prepare-cdac-helix-steps.yml @@ -7,6 +7,10 @@ parameters: buildDebuggees: true skipDebuggeeCopy: false runtimeConfiguration: 'Checked' + # When true, copy cdaclite.dll into the testhost payload (next to coreclr.dll) so createdump can + # select managed memory with cdac-lite instead of the legacy DAC. Paired with the + # DOTNET_DbgUseCdacLite pre-command in cdac-dump-helix.proj. Windows-only (no-op elsewhere). + useCdacLite: false # Configuration the cDAC managed assemblies are built at during payload # prep. The dump tests load the managed cDAC directly via ProjectReference, # so passing /p:Configuration=Debug here makes MSBuild walk the @@ -69,3 +73,22 @@ steps: Write-Host "Helix queue: $queue" Write-Host "##vso[task.setvariable variable=CdacHelixQueue]$queue" displayName: 'Find TestHost Directory and Helix Queue' + +- ${{ if parameters.useCdacLite }}: + # cdac-lite mode: place cdaclite.dll next to the testhost's coreclr.dll so createdump (which runs + # on the Helix machine when a debuggee crashes) loads it and selects the managed memory instead of + # the legacy DAC. Windows-only -- the createdump cdac-lite path is Windows-only. + - pwsh: | + if ("$(osGroup)" -ne "windows") { + Write-Host "cdac-lite mode is Windows-only; skipping cdaclite.dll copy for $(osGroup)." + exit 0 + } + $coreclr = Get-ChildItem -Recurse -ErrorAction SilentlyContinue ` + -Path "$(Build.SourcesDirectory)/artifacts/bin/testhost/net*-$(osGroup)-*-$(archType)/shared/Microsoft.NETCore.App" ` + -Filter "coreclr.dll" | Select-Object -First 1 + if (-not $coreclr) { Write-Error "coreclr.dll not found in the testhost shared framework."; exit 1 } + $src = "$(Build.SourcesDirectory)/artifacts/bin/coreclr/$(osGroup).$(archType).${{ parameters.runtimeConfiguration }}/cdaclite.dll" + if (-not (Test-Path $src)) { Write-Error "cdaclite.dll not found at $src (was it built by the CdacBuild -s clr subset?)."; exit 1 } + Copy-Item $src $coreclr.DirectoryName -Force + Write-Host "Copied cdaclite.dll into $($coreclr.DirectoryName)" + displayName: 'Copy cdac-lite into TestHost payload' diff --git a/eng/pipelines/runtime-diagnostics.yml b/eng/pipelines/runtime-diagnostics.yml index 319a1aeee553b4..836d405847467c 100644 --- a/eng/pipelines/runtime-diagnostics.yml +++ b/eng/pipelines/runtime-diagnostics.yml @@ -65,6 +65,10 @@ parameters: values: - single-leg - xplat +- name: cdacUseCdacLite + displayName: Use cdac-lite for dump generation (Windows only) + type: boolean + default: false - name: cdacStressPlatforms displayName: cDAC Stress Test Platforms type: object @@ -261,8 +265,9 @@ extends: platforms: ${{ parameters.cdacDumpPlatforms }} prepareParameters: cdacTestConfig: Debug + useCdacLite: ${{ parameters.cdacUseCdacLite }} sendDisplayName: 'Send cDAC Dump Tests to Helix' - sendParams: $(Build.SourcesDirectory)/src/native/managed/cdac/tests/DumpTests/cdac-dump-helix.proj /t:Test /p:TargetOS=$(osGroup) /p:TargetArchitecture=$(archType) /p:HelixTargetQueues="$(CdacHelixQueue)" /p:TestHostPayload=$(TestHostPayloadDir) /p:DumpTestsPayload=$(Build.SourcesDirectory)/artifacts/helixPayload/cdac /bl:$(Build.SourcesDirectory)/artifacts/log/SendToHelix.binlog + sendParams: $(Build.SourcesDirectory)/src/native/managed/cdac/tests/DumpTests/cdac-dump-helix.proj /t:Test /p:TargetOS=$(osGroup) /p:TargetArchitecture=$(archType) /p:HelixTargetQueues="$(CdacHelixQueue)" /p:TestHostPayload=$(TestHostPayloadDir) /p:DumpTestsPayload=$(Build.SourcesDirectory)/artifacts/helixPayload/cdac /p:UseCdacLite=${{ parameters.cdacUseCdacLite }} /bl:$(Build.SourcesDirectory)/artifacts/log/SendToHelix.binlog failErrorMessage: 'One or more cDAC dump test failures were detected. Failing the job.' publishDumpsArtifactName: CdacDumps_$(osGroup)_$(archType) diff --git a/src/coreclr/debug/cdaclite/cdaclite.cpp b/src/coreclr/debug/cdaclite/cdaclite.cpp index cb68d1f0e14f6c..c99659643374ee 100644 --- a/src/coreclr/debug/cdaclite/cdaclite.cpp +++ b/src/coreclr/debug/cdaclite/cdaclite.cpp @@ -26,8 +26,8 @@ extern "C" bool TryGetSymbol(ICorDebugDataTarget* dataTarget, uint64_t baseAddre namespace cdac { - CDacLite::CDacLite(ICLRDataTarget* target, uint64_t contractDescriptorAddr) - : m_ref(1), m_target(target), m_contractDescriptorAddr(contractDescriptorAddr) + CDacLite::CDacLite(ICLRDataTarget* target, uint64_t contractDescriptorAddr, uint64_t clrBase) + : m_ref(1), m_target(target), m_contractDescriptorAddr(contractDescriptorAddr), m_clrBase(clrBase) { m_target->AddRef(); } @@ -142,7 +142,7 @@ STDAPI CLRDataCreateInstance(REFIID iid, ICLRDataTarget* pLegacyTarget, void** i return E_FAIL; } - cdac::CDacLite* instance = new (std::nothrow) cdac::CDacLite(pLegacyTarget, contractDescriptorAddr); + cdac::CDacLite* instance = new (std::nothrow) cdac::CDacLite(pLegacyTarget, contractDescriptorAddr, (uint64_t)base); if (instance == nullptr) { return E_OUTOFMEMORY; diff --git a/src/coreclr/debug/cdaclite/cdaclite.h b/src/coreclr/debug/cdaclite/cdaclite.h index a2a0323d297ab0..a367f2220979d5 100644 --- a/src/coreclr/debug/cdaclite/cdaclite.h +++ b/src/coreclr/debug/cdaclite/cdaclite.h @@ -29,7 +29,7 @@ namespace cdac class CDacLite : public ICLRDataEnumMemoryRegions { public: - CDacLite(ICLRDataTarget* target, uint64_t contractDescriptorAddr); + CDacLite(ICLRDataTarget* target, uint64_t contractDescriptorAddr, uint64_t clrBase); // IUnknown STDMETHOD(QueryInterface)(REFIID riid, void** ppvObject) override; @@ -60,6 +60,7 @@ namespace cdac LONG m_ref; ICLRDataTarget* m_target; uint64_t m_contractDescriptorAddr; + uint64_t m_clrBase; }; } diff --git a/src/coreclr/debug/cdaclite/contracts/CMakeLists.txt b/src/coreclr/debug/cdaclite/contracts/CMakeLists.txt index 54f534f534c8c8..ba17547afbe00a 100644 --- a/src/coreclr/debug/cdaclite/contracts/CMakeLists.txt +++ b/src/coreclr/debug/cdaclite/contracts/CMakeLists.txt @@ -12,6 +12,7 @@ add_library_clr(cdacgc STATIC stresslog.cpp interop.cpp stackscan.cpp + bootstrap.cpp ) target_include_directories(cdacgc PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/src/coreclr/debug/cdaclite/contracts/bootstrap.cpp b/src/coreclr/debug/cdaclite/contracts/bootstrap.cpp new file mode 100644 index 00000000000000..8acf17e9b8e6db --- /dev/null +++ b/src/coreclr/debug/cdaclite/contracts/bootstrap.cpp @@ -0,0 +1,121 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +//***************************************************************************** +// bootstrap.cpp +// +// Implementation of the bootstrap-singleton enumeration declared in bootstrap.h. +//***************************************************************************** + +#include "bootstrap.h" +#include "runtimetypes.h" + +#include + +namespace cdac +{ +namespace contracts +{ + namespace + { + // Emits the runtime module's PE headers + export directory. A reader (ClrMD) resolves the + // DotNetRuntimeContractDescriptor export from the module's export table to bootstrap; that + // memory is NOT part of a MiniDumpNormal's default module capture. cdac-lite emits it + // explicitly so it is self-sufficient (and createdump can exclude the legacy DAC via + // MiniDumpWithoutAuxiliaryState). Returns the number of regions emitted. + int EmitModuleExportDirectory(const Target& target, RegionCallback sink, void* sinkContext) + { + uint64_t clrBase = target.ClrBase(); + if (clrBase == 0) + { + return 0; + } + + uint8_t dos[0x40]; + if (!target.ReadBuffer(clrBase, dos, sizeof(dos)) || dos[0] != 'M' || dos[1] != 'Z') + { + return 0; + } + uint32_t e_lfanew = 0; + memcpy(&e_lfanew, dos + 0x3C, sizeof(e_lfanew)); + + // NT headers: Signature(4) + IMAGE_FILE_HEADER(20) + IMAGE_OPTIONAL_HEADER. The data + // directory array begins at offset 112 (PE32+) or 96 (PE32) within the optional header; + // the export directory is entry 0 (RVA, then Size). + uint8_t peHdr[0x18 + 0xF0]; + if (!target.ReadBuffer(clrBase + e_lfanew, peHdr, sizeof(peHdr)) || + peHdr[0] != 'P' || peHdr[1] != 'E') + { + return 0; + } + uint16_t optMagic = 0; + memcpy(&optMagic, peHdr + 0x18, sizeof(optMagic)); + uint32_t dataDirOffset = 0x18 + ((optMagic == 0x20B) ? 112 : 96); + uint32_t exportRVA = 0, exportSize = 0; + memcpy(&exportRVA, peHdr + dataDirOffset, sizeof(exportRVA)); + memcpy(&exportSize, peHdr + dataDirOffset + 4, sizeof(exportSize)); + + // PE headers (import/export directory pointers, section table) live in the first page. + sink(sinkContext, "pe-headers", clrBase, 0x1000); + int emitted = 1; + // Export directory + function/name/ordinal tables + name strings (spanned by Size). + if (exportRVA != 0 && exportSize != 0) + { + sink(sinkContext, "export-dir", clrBase + exportRVA, exportSize); + emitted++; + } + return emitted; + } + } + + int EnumerateBootstrapRegions(const Target& target, RegionCallback sink, void* sinkContext) + { + int emitted = EmitModuleExportDirectory(target, sink, sinkContext); + + // SystemDomain: reading a transition frame (e.g. InlinedCallFrame on a P/Invoke-parked + // thread) lazily resolves a well-known managed type once, which calls + // Loader.GetSystemAssembly -> SystemDomain.SystemAssembly. Emit the SystemDomain object. + uint64_t systemDomainAddr = 0; + if (target.TryReadGlobalPointer("SystemDomain", systemDomainAddr) && systemDomainAddr != 0) + { + target.EmitStruct("SystemDomain", systemDomainAddr); + emitted++; + + // The global LoaderAllocator is embedded in the SystemDomain (GlobalLoaderAllocator is a + // field address). SOS dumpdomain (ISOSDacInterface.GetAppDomainData) reads its + // High/Low/Stub loader-heap pointers, so emit the full LoaderAllocator struct. + uint64_t globalLoaderAllocator = 0; + if (target.TryGetFieldAddress(systemDomainAddr, "SystemDomain", "GlobalLoaderAllocator", globalLoaderAllocator) && + globalLoaderAllocator != 0) + { + target.EmitStruct("LoaderAllocator", globalLoaderAllocator); + emitted++; + } + } + + // PlatformMetadata: resolving an R2R runtime function reads the cDAC platform metadata + // (IPlatformMetadata.GetCodePointerFlags). This global holds the struct address directly + // (like the code range map), so read it with TryGetGlobalValue -- no extra deref. + uint64_t platformMetadataAddr = 0; + if (target.TryGetGlobalValue("PlatformMetadata", platformMetadataAddr) && platformMetadataAddr != 0) + { + target.EmitStruct("PlatformMetadata", platformMetadataAddr); + // The PrecodeMachineDescriptor is embedded at the start of the PlatformMetadata; the + // MethodDesc validation reads it to identify precode stubs. + target.EmitStruct("PrecodeMachineDescriptor", platformMetadataAddr); + emitted++; + } + + // Debugger data: the stack walk's hijack check (IDebugger.GetHijackKind, called per frame) + // reads g_pDebugger. The DAC's skinny enumeration emits it via g_pDebugger->EnumMemoryRegions; + // cdac-lite emits it here so the walk works without the DAC. + uint64_t debuggerAddr = 0; + if (target.TryReadGlobalPointer("Debugger", debuggerAddr) && debuggerAddr != 0) + { + target.EmitStruct("Debugger", debuggerAddr); + emitted++; + } + + return emitted; + } +} +} // namespace contracts diff --git a/src/coreclr/debug/cdaclite/contracts/bootstrap.h b/src/coreclr/debug/cdaclite/contracts/bootstrap.h new file mode 100644 index 00000000000000..d56298296ccca1 --- /dev/null +++ b/src/coreclr/debug/cdaclite/contracts/bootstrap.h @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +//***************************************************************************** +// bootstrap.h +// +// Bootstrap enumerator: emits the standalone singletons a reader needs to +// bootstrap from the dump alone -- the runtime module's PE headers + export +// directory (to resolve the DotNetRuntimeContractDescriptor export) and the +// well-known global objects the stack walk / R2R resolution reference +// (SystemDomain, the global LoaderAllocator, PlatformMetadata, Debugger). +// Runs in every tier. +//***************************************************************************** + +#ifndef CDACLITE_CONTRACTS_BOOTSTRAP_H +#define CDACLITE_CONTRACTS_BOOTSTRAP_H + +#include + +#include "contracts.h" +#include "target.h" + +namespace cdac +{ +namespace contracts +{ + // Emits the runtime module export directory + global singletons. Returns the number of + // singletons emitted (>= 0). + int EnumerateBootstrapRegions(const Target& target, RegionCallback sink, void* sinkContext); +} +} // namespace contracts + +#endif // CDACLITE_CONTRACTS_BOOTSTRAP_H diff --git a/src/coreclr/debug/cdaclite/contracts/gc.cpp b/src/coreclr/debug/cdaclite/contracts/gc.cpp index 5254a677c43a0b..89fd73fb06eecd 100644 --- a/src/coreclr/debug/cdaclite/contracts/gc.cpp +++ b/src/coreclr/debug/cdaclite/contracts/gc.cpp @@ -340,6 +340,10 @@ namespace contracts int EnumerateGCHeapRegions(const Target& target, RegionCallback sink, void* sinkContext) { + if (target.Tier() != DumpTier::Heap) + { + return 0; + } std::string identifiers; if (!target.TryGetGlobalString(GlobalGCIdentifiers, identifiers)) { diff --git a/src/coreclr/debug/cdaclite/contracts/handles.cpp b/src/coreclr/debug/cdaclite/contracts/handles.cpp index fca11f50102520..23d0ef03dec52b 100644 --- a/src/coreclr/debug/cdaclite/contracts/handles.cpp +++ b/src/coreclr/debug/cdaclite/contracts/handles.cpp @@ -34,6 +34,10 @@ namespace contracts int EnumerateHandleRegions(const Target& target, RegionCallback sink, void* sinkContext) { + if (target.Tier() != DumpTier::Heap) + { + return 0; + } uint64_t mapAddr = 0; if (!target.TryGetGlobalValue(GlobalHandleTableMap, mapAddr) || mapAddr == 0) { diff --git a/src/coreclr/debug/cdaclite/contracts/interop.cpp b/src/coreclr/debug/cdaclite/contracts/interop.cpp index 3e5cd9bd4d6bfc..dfe1b1b80c8413 100644 --- a/src/coreclr/debug/cdaclite/contracts/interop.cpp +++ b/src/coreclr/debug/cdaclite/contracts/interop.cpp @@ -24,8 +24,12 @@ namespace contracts const int MaxRCWsPerBucket = 1000000; } - int EnumerateInteropRegions(const Target& target) + int EnumerateInteropRegions(const Target& target, RegionCallback, void*) { + if (target.Tier() != DumpTier::Heap) + { + return 0; + } // The cleanup list head: ReadPointer(ReadGlobalPointer(RCWCleanupList)). uint64_t listAddr = 0; if (!target.TryReadGlobalPointer(GlobalRCWCleanupList, listAddr)) diff --git a/src/coreclr/debug/cdaclite/contracts/interop.h b/src/coreclr/debug/cdaclite/contracts/interop.h index 5337b7492d1515..0a593c65381c58 100644 --- a/src/coreclr/debug/cdaclite/contracts/interop.h +++ b/src/coreclr/debug/cdaclite/contracts/interop.h @@ -23,7 +23,7 @@ namespace contracts // contract can re-read them from the dump. Memory is captured via the Target's EnumMem // sink. Returns the number of RCWs captured, 0 if the cleanup list is empty, or -1 if // the list global could not be located. - int EnumerateInteropRegions(const Target& target); + int EnumerateInteropRegions(const Target& target, RegionCallback sink, void* sinkContext); } } // namespace contracts diff --git a/src/coreclr/debug/cdaclite/contracts/loader.cpp b/src/coreclr/debug/cdaclite/contracts/loader.cpp index 28434f5aa058f5..82cd50e493be5b 100644 --- a/src/coreclr/debug/cdaclite/contracts/loader.cpp +++ b/src/coreclr/debug/cdaclite/contracts/loader.cpp @@ -64,6 +64,11 @@ namespace contracts break; } + // The block's inline Assembly* array (ArrayStart, Size pointers) extends past the fixed + // ArrayListBlock struct, so emit it explicitly -- the reader re-reads it to enumerate the + // domain's assemblies (ISOSDacInterface.GetAppDomainData / SOS dumpdomain). + target.EmitMemory(block.ArrayStart, (uint32_t)(block.Size * target.PointerSize())); + for (uint32_t i = 0; i < block.Size && seen < total; i++) { seen++; @@ -116,6 +121,25 @@ namespace contracts return; } + // Emit the module's metadata-locator chain: Module -> PEAssembly -> PEImage -> + // PEImageLayout. The reader follows this (EcmaMetadata.GetMetadata -> + // Loader.TryGetPEImage -> TryGetLoadedImageContents) to find where a module's ECMA + // metadata lives; the metadata bytes themselves come from the on-disk image, but these + // small locator structs are not otherwise captured in a Normal dump. + if (module.PEAssembly != 0) + { + data::PEAssembly peAssembly; + if (target.TryRead(module.PEAssembly, peAssembly) && peAssembly.PEImage != 0) + { + data::PEImage peImage; + if (target.TryRead(peAssembly.PEImage, peImage) && peImage.LoadedImageLayout != 0) + { + data::PEImageLayout layout; + target.TryRead(peImage.LoadedImageLayout, layout); // struct read -> auto-emitted + } + } + } + // If the module has an in-memory symbol stream, capture its buffer (ILoader.TryGetSymbolStream). // In-memory PDBs have no on-disk backing, so they must be in the dump. if (module.GrowableSymbolStream != 0) @@ -128,6 +152,31 @@ namespace contracts state->emitted++; } } + + // ReadyToRun modules: resolving an R2R frame (ExecutionManager.GetCodeBlockHandle -> + // ReadyToRunJitManager) reads ReadyToRunInfo -> ReadyToRunHeader / DebugInfoSection. + // The RuntimeFunctions table it indexes lives in the on-disk image, but these runtime + // locator structs must be in the dump. + if (module.ReadyToRunInfo != 0) + { + data::ReadyToRunInfo r2r; + if (target.TryRead(module.ReadyToRunInfo, r2r)) + { + if (r2r.ReadyToRunHeader != 0) + { + target.EmitStruct("ReadyToRunHeader", r2r.ReadyToRunHeader); + } + if (r2r.DebugInfoSection != 0) + { + target.EmitStruct("ImageDataDirectory", r2r.DebugInfoSection); + } + if (r2r.CompositeInfo != 0 && r2r.CompositeInfo != module.ReadyToRunInfo) + { + target.EmitStruct("ReadyToRunInfo", r2r.CompositeInfo); + } + state->emitted++; + } + } } } diff --git a/src/coreclr/debug/cdaclite/contracts/stackscan.cpp b/src/coreclr/debug/cdaclite/contracts/stackscan.cpp index 48d1577d0882c6..7ec8ec3994fc15 100644 --- a/src/coreclr/debug/cdaclite/contracts/stackscan.cpp +++ b/src/coreclr/debug/cdaclite/contracts/stackscan.cpp @@ -8,12 +8,21 @@ // EEJitManager NibbleMap (IP -> method code) so that, for each code pointer found // on a thread stack, only that method's code + header + MethodDesc is captured -- // avoiding the bulk emission of every JIT/loader heap. +// +// The traversal is organized as a `CodeEnumerator`: mirroring the DAC's per-type +// EnumMemoryRegions, each Enum*/method reports the memory for one kind of runtime +// structure (JIT code, MethodDesc, MethodTable, precode, R2R map) and recurses +// into the structures it references. The enumerator owns the shared context (the +// target, the region sink, the code range map and the stub-code sentinel) plus +// the dedup/visited state, so the individual steps need not thread it through. //***************************************************************************** #include "stackscan.h" #include "runtimetypes.h" #include +#include +#include namespace cdac { @@ -27,6 +36,14 @@ namespace contracts const int MaxThreads = 100000; const int MaxFragments = 100000; + const int MaxFrames = 100000; // guard against a corrupt Frame chain + + // Stacks are scanned page-by-page: a thread's [limit, base) range spans the whole + // RESERVED stack (which can be very large, e.g. a Thread created with a big maxStackSize), + // but only the committed portion near the base is real. Probing one slot per page and + // skipping unreadable pages avoids millions of failing reads across the reserved-but- + // uncommitted range. + const uint64_t StackScanPageSize = 4096; // RangeSection.Flags const int32_t RangeSectionFlag_CodeHeap = 0x02; @@ -39,11 +56,21 @@ namespace contracts // Bound how much code we emit around a stack IP (covers the method body + code header). const uint32_t MaxMethodCodeEmit = 64 * 1024; - // Conservative fixed emission for a MethodDesc (its exact size is chunk-dependent). - const uint32_t MethodDescEmit = 512; - // --- RangeSectionMap radix lookup (ExecutionManagerHelpers.RangeSectionMap) --------- + // Bound how much of a method's GC info blob to emit. The blob is variable-length and its + // size is only known by decoding it; SOS GetCodeHeaderData decodes just enough to read the + // method's code length (the header prefix), so a generous fixed span covers the typical + // method's whole blob and always covers the header the length decode needs. + const uint32_t MaxGcInfoEmit = 4 * 1024; + + // Sentinel returned by a PtrHashMap probe for a deleted/invalid entry. + const uint32_t HashInvalidEntry = 0xFFFFFFFFu; + // --- Stateless helpers ------------------------------------------------ + // Pure math + descriptor-only reads with no memory emission. They take the Target + // directly; the emitting traversal lives in CodeEnumerator below. + + // RangeSectionMap radix lookup (ExecutionManagerHelpers.RangeSectionMap). int MapLevels(const Target& target) { return target.PointerSize() == 8 ? 5 : 2; } int MaxSetBit(const Target& target) { return target.PointerSize() == 8 ? 56 : 31; } @@ -55,71 +82,7 @@ namespace contracts return (int)(255u & shifted); } - // Walks the radix map for 'addr' and returns the RangeSection covering it, or 0. - // Emits each map-node pointer slot it reads (the reader re-walks the same slots). - uint64_t FindRangeSection(const Target& target, uint64_t topMap, uint64_t addr, - RegionCallback sink, void* sinkContext) - { - const uint64_t ptrSize = target.PointerSize(); - uint64_t levelMap = topMap; - int level = MapLevels(target); - uint64_t firstFragment = 0; - for (;;) - { - int index = GetIndexForLevel(target, addr, level); - uint64_t slot = levelMap + (uint64_t)index * ptrSize; - uint64_t value = 0; - if (!target.TryReadPointer(slot, value)) - { - return 0; - } - // The reader reads this same slot during its own lookup. - sink(sinkContext, "code-rangemap", slot, (uint64_t)ptrSize); - value &= ~1ull; // low bit is the collectible flag - if (level == 1) - { - firstFragment = value; - break; - } - if (value == 0) - { - return 0; - } - levelMap = value; - level--; - } - - // Walk the fragment list at the leaf to find the covering RangeSection. - uint64_t fragment = firstFragment; - for (int i = 0; fragment != 0 && i < MaxFragments; i++) - { - data::RangeSectionFragment f; - if (!target.TryRead(fragment, f)) // struct read -> captured by EnumMem - { - break; - } - if (addr >= f.RangeBegin && addr < f.RangeEndOpen) - { - return f.RangeSection; - } - fragment = f.Next; - } - return 0; - } - - // --- NibbleMap: IP -> start of the containing method's code (NibbleMapConstantLookup) -- - - uint32_t ReadMapUnit(const Target& target, uint64_t mapStart, uint64_t mapIdx, - RegionCallback sink, void* sinkContext) - { - uint64_t unitAddr = mapStart + (mapIdx / MapUnitSizeInNibbles) * MapUnitSizeInBytes; - uint32_t value = 0; - target.TryReadUInt32(unitAddr, value); - // The reader re-reads the same map units. - sink(sinkContext, "code-nibblemap", unitAddr, (uint64_t)MapUnitSizeInBytes); - return value; - } - + // NibbleMap decode helpers. bool IsPointerUnit(uint32_t unit) { return (unit & NibbleMask) > 8; } uint64_t DecodePointer(uint64_t baseAddress, uint32_t unit) @@ -142,162 +105,780 @@ namespace contracts return baseAddress + mapIdxByteOffset + nibbleByteOffset; } - // Returns the code start address for 'currentPC', or 0. Faithful port of - // NibbleMapConstantLookup.FindMethodCode. - uint64_t NibbleMapFindMethodCode(const Target& target, uint64_t mapBase, uint64_t mapStart, - uint64_t currentPC, RegionCallback sink, void* sinkContext) + // Computes a MethodDesc's exact size from the data descriptor: the classification base size + // (MethodDesc::GetBaseSize / s_ClassificationSizeTable) plus any present optional slots. + // Mirrors MethodDescOptionalSlots; no hardcoded MethodDesc size. + uint32_t MethodDescSize(const Target& target, uint64_t methodDesc) { - uint64_t relativeAddress = currentPC - mapBase; - uint64_t mapIdx = relativeAddress / BytesPerBucket; - uint32_t bucketByteIndex = (uint32_t)((relativeAddress & (BytesPerBucket - 1)) / MapUnitSizeInBytes) + 1; - - uint32_t t = ReadMapUnit(target, mapStart, mapIdx, sink, sinkContext); - if (IsPointerUnit(t)) + uint64_t flagsAddr = 0; + uint16_t flags = 0; + if (!target.TryGetFieldAddress(methodDesc, "MethodDesc", "Flags", flagsAddr) || + !target.TryReadUInt16(flagsAddr, flags)) { - return DecodePointer(mapBase, t); + return 0; + } + // MethodClassification (Flags & 0x7) -> the concrete MethodDesc subclass type. + static const char* const classTypes[8] = { + "MethodDesc", "FCallMethodDesc", "PInvokeMethodDesc", "EEImplMethodDesc", + "ArrayMethodDesc", "InstantiatedMethodDesc", "CLRToCOMCallMethodDesc", "DynamicMethodDesc" + }; + uint32_t size = 0; + if (!target.TryGetTypeSize(classTypes[flags & 0x7], size)) + { + return 0; } + // Optional slots that follow the base MethodDesc (flag bits from MethodDescFlags). + uint32_t slot = 0; + if ((flags & 0x08) && target.TryGetTypeSize("NonVtableSlot", slot)) { size += slot; } // HasNonVtableSlot + if ((flags & 0x10) && target.TryGetTypeSize("MethodImpl", slot)) { size += slot; } // HasMethodImpl + if ((flags & 0x20) && target.TryGetTypeSize("NativeCodeSlot", slot)) { size += slot; } // HasNativeCodeSlot + if ((flags & 0x40) && target.TryGetTypeSize("AsyncMethodData", slot)) { size += slot; } // HasAsyncMethodData + return size; + } - // Focus on the indexed nibble. - t = t >> GetNibbleShift(mapIdx); - uint32_t nibble = t & NibbleMask; - if (nibble != 0 && nibble <= bucketByteIndex) + // Computes the address of a MethodDesc's native code slot (if present), mirroring + // MethodDescOptionalSlots.GetAddressOfNativeCodeSlot: the classification base size plus the + // NonVtableSlot / MethodImpl optional slots that precede it. Returns false if absent. + bool TryGetNativeCodeSlotAddress(const Target& target, uint64_t methodDesc, uint64_t& slotAddr) + { + uint64_t flagsAddr = 0; + uint16_t flags = 0; + if (!target.TryGetFieldAddress(methodDesc, "MethodDesc", "Flags", flagsAddr) || + !target.TryReadUInt16(flagsAddr, flags)) + { + return false; + } + if ((flags & 0x20) == 0) // HasNativeCodeSlot { - return GetAbsoluteAddress(mapBase, mapIdx, nibble); + return false; } + static const char* const classTypes[8] = { + "MethodDesc", "FCallMethodDesc", "PInvokeMethodDesc", "EEImplMethodDesc", + "ArrayMethodDesc", "InstantiatedMethodDesc", "CLRToCOMCallMethodDesc", "DynamicMethodDesc" + }; + uint32_t offset = 0; + if (!target.TryGetTypeSize(classTypes[flags & 0x7], offset)) + { + return false; + } + uint32_t slot = 0; + if ((flags & 0x08) && target.TryGetTypeSize("NonVtableSlot", slot)) { offset += slot; } // HasNonVtableSlot + if ((flags & 0x10) && target.TryGetTypeSize("MethodImpl", slot)) { offset += slot; } // HasMethodImpl + slotAddr = methodDesc + offset; + return true; + } - // Search backwards through the current map unit. - t = t >> 4; // shift to next nibble - if (t != 0) + // Finds the largest RUNTIME_FUNCTION index whose BeginAddress <= rva (sorted table). + bool FindRuntimeFunctionIndex(const Target& target, uint64_t rfTable, uint32_t rfStride, + uint32_t count, uint32_t rva, uint32_t& index) + { + if (count == 0) + { + return false; + } + uint32_t left = 0; + uint32_t right = count - 1; + bool found = false; + index = 0; + while (left <= right) { - if (mapIdx == 0) + uint32_t mid = left + (right - left) / 2; + uint32_t begin = 0; + target.TryReadUInt32(rfTable + (uint64_t)mid * rfStride, begin); + if (begin <= rva) { - return 0; + index = mid; + found = true; + if (mid == 0xFFFFFFFFu) break; + left = mid + 1; } - mapIdx = mapIdx - 1; - while ((t & NibbleMask) == 0) + else { - t = t >> 4; - if (mapIdx == 0) - { - break; - } - mapIdx = mapIdx - 1; + if (mid == 0) break; + right = mid - 1; } - return GetAbsoluteAddress(mapBase, mapIdx, t & NibbleMask); } + return found; + } - // We finished the current map unit; if we were in the first, stop. - if (mapIdx < MapUnitSizeInNibbles) + // Enumerates the memory a stack walk reaches. Mirroring the DAC's per-type + // EnumMemoryRegions, each Enum* method reports the memory for one kind of runtime structure + // and recurses into the structures it references. Shared context (target, region sink, code + // range map, stub-code sentinel) and the dedup/visited state are owned here so the steps + // stay small and free of parameter threading. + class CodeEnumerator + { + public: + CodeEnumerator(const Target& target, RegionCallback sink, void* sinkContext, + uint64_t codeRangeMap, uint64_t stubCodeBlockLast) + : m_target(target), m_sink(sink), m_sinkContext(sinkContext), + m_codeRangeMap(codeRangeMap), m_stubCodeBlockLast(stubCodeBlockLast) { - return 0; } - // Align down to the map unit, then move back one nibble into the previous unit. - mapIdx = mapIdx & (~(MapUnitSizeInNibbles - 1)); - mapIdx = mapIdx - 1; - - t = ReadMapUnit(target, mapStart, mapIdx, sink, sinkContext); - if (t == 0) + // Resolves a code pointer to its method and reports the method's code + header + + // MethodDesc graph. Returns the MethodDesc, or 0 if not resolved. Dispatches an EEJit + // code heap (NibbleMap) vs an R2R section (runtime-function table + map probe). + uint64_t EnumCodeForIP(uint64_t ip) { - return 0; + uint64_t rangeSectionAddr = FindRangeSection(ip); + if (rangeSectionAddr == 0) + { + return 0; + } + data::RangeSection rangeSection; + if (!m_target.TryRead(rangeSectionAddr, rangeSection)) + { + return 0; + } + // R2R sections: resolve via the R2R runtime-function table + EntryPointToMethodDescMap + // probe (image code comes from disk). EEJit code heaps: resolve via NibbleMap. + if (((int32_t)rangeSection.Flags & RangeSectionFlag_CodeHeap) == 0 || rangeSection.HeapList == 0) + { + return EnumR2RMethodDesc(rangeSection, ip); + } + uint64_t methodDescPtr = EnumJitCodeAt(rangeSection, ip); + if (methodDescPtr != 0) + { + EnumMethodDesc(methodDescPtr, /*emitNativeCode*/ true); + } + return methodDescPtr; } - if (IsPointerUnit(t)) + + // Resolves + reports the MethodDesc carried by a transition Frame (see + // ResolveFrameMethodDesc). Records it as captured so the driver's count includes it. + void EnumFrameMethodDesc(uint64_t frameAddr, uint64_t identifier) { - return DecodePointer(mapBase, t); + uint64_t md = ResolveFrameMethodDesc(frameAddr, identifier); + if (md != 0) + { + EnumMethodDesc(md, /*emitNativeCode*/ true); + m_captured.insert(md); + } } - while (mapIdx != 0 && (t & NibbleMask) == 0) + // Whether 'value' was already scanned; also records it. Deep/recursive stacks push the + // same return address thousands of times, so deduping the scanned values collapses the + // work to O(distinct code pointers). + bool AlreadyProcessed(uint64_t value) { return !m_processed.insert(value).second; } + void NoteCapturedMethod(uint64_t md) { m_captured.insert(md); } + size_t CapturedMethodCount() const { return m_captured.size(); } + + private: + // Reports one raw memory region [start, start+size), deduped by start address. The scan + // re-reads the same radix/nibble/code/methoddesc addresses across many stack slots + // (recursion is the extreme case); forwarding each only once keeps the region list small. + void Emit(const char* kind, uint64_t start, uint64_t size) { - t = t >> 4; - mapIdx = mapIdx - 1; + if (start != 0 && size > 0 && m_emitted.insert(start).second) + { + m_sink(m_sinkContext, kind, start, size); + } } - return GetAbsoluteAddress(mapBase, mapIdx, t & NibbleMask); - } - // Resolves a code pointer 'ip' to its method and emits the code + header + MethodDesc. - // Returns the MethodDesc address (deduped by the caller), or 0 if not resolved. - uint64_t ResolveCodeIP(const Target& target, uint64_t codeRangeMap, uint64_t stubCodeBlockLast, - uint64_t ip, RegionCallback sink, void* sinkContext) - { - uint64_t rangeSectionAddr = FindRangeSection(target, codeRangeMap, ip, sink, sinkContext); - if (rangeSectionAddr == 0) + // Walks the radix map for 'addr' and returns the RangeSection covering it, or 0. + // Reports each map-node pointer slot it reads (the reader re-walks the same slots). + uint64_t FindRangeSection(uint64_t addr) { + const uint64_t ptrSize = m_target.PointerSize(); + uint64_t levelMap = m_codeRangeMap; + int level = MapLevels(m_target); + uint64_t firstFragment = 0; + for (;;) + { + int index = GetIndexForLevel(m_target, addr, level); + uint64_t slot = levelMap + (uint64_t)index * ptrSize; + uint64_t value = 0; + if (!m_target.TryReadPointer(slot, value)) + { + return 0; + } + // The reader reads this same slot during its own lookup. + Emit("code-rangemap", slot, ptrSize); + value &= ~1ull; // low bit is the collectible flag + if (level == 1) + { + firstFragment = value; + break; + } + if (value == 0) + { + return 0; + } + levelMap = value; + level--; + } + + // Walk the fragment list at the leaf to find the covering RangeSection. + uint64_t fragment = firstFragment; + for (int i = 0; fragment != 0 && i < MaxFragments; i++) + { + data::RangeSectionFragment f; + if (!m_target.TryRead(fragment, f)) // struct read -> captured by EnumMem + { + break; + } + if (addr >= f.RangeBegin && addr < f.RangeEndOpen) + { + return f.RangeSection; + } + fragment = f.Next & ~1ull; // low bit is the collectible flag (managed masks it too) + } return 0; } - data::RangeSection rangeSection; - if (!target.TryRead(rangeSectionAddr, rangeSection)) + // --- NibbleMap: IP -> start of the containing method's code ------- + + // Reads one map unit and reports it (the reader re-reads the same units). + uint32_t ReadMapUnit(uint64_t mapStart, uint64_t mapIdx) { - return 0; + uint64_t unitAddr = mapStart + (mapIdx / MapUnitSizeInNibbles) * MapUnitSizeInBytes; + uint32_t value = 0; + m_target.TryReadUInt32(unitAddr, value); + Emit("code-nibblemap", unitAddr, MapUnitSizeInBytes); + return value; } - // Only EEJit code heaps are resolved here; R2R code + method info live in the module - // image (available to the reader from the on-disk binary). - if (((int32_t)rangeSection.Flags & RangeSectionFlag_CodeHeap) == 0 || rangeSection.HeapList == 0) + // Returns the code start address for 'currentPC', or 0. Faithful port of + // NibbleMapConstantLookup.FindMethodCode. + uint64_t NibbleMapFindMethodCode(uint64_t mapBase, uint64_t mapStart, uint64_t currentPC) { - return 0; + uint64_t relativeAddress = currentPC - mapBase; + uint64_t mapIdx = relativeAddress / BytesPerBucket; + uint32_t bucketByteIndex = (uint32_t)((relativeAddress & (BytesPerBucket - 1)) / MapUnitSizeInBytes) + 1; + + uint32_t t = ReadMapUnit(mapStart, mapIdx); + if (IsPointerUnit(t)) + { + return DecodePointer(mapBase, t); + } + + // Focus on the indexed nibble. + t = t >> GetNibbleShift(mapIdx); + uint32_t nibble = t & NibbleMask; + if (nibble != 0 && nibble <= bucketByteIndex) + { + return GetAbsoluteAddress(mapBase, mapIdx, nibble); + } + + // Search backwards through the current map unit. + t = t >> 4; // shift to next nibble + if (t != 0) + { + if (mapIdx == 0) + { + return 0; + } + mapIdx = mapIdx - 1; + while ((t & NibbleMask) == 0) + { + t = t >> 4; + if (mapIdx == 0) + { + break; + } + mapIdx = mapIdx - 1; + } + return GetAbsoluteAddress(mapBase, mapIdx, t & NibbleMask); + } + + // We finished the current map unit; if we were in the first, stop. + if (mapIdx < MapUnitSizeInNibbles) + { + return 0; + } + + // Align down to the map unit, then move back one nibble into the previous unit. + mapIdx = mapIdx & (~(MapUnitSizeInNibbles - 1)); + mapIdx = mapIdx - 1; + + t = ReadMapUnit(mapStart, mapIdx); + if (t == 0) + { + return 0; + } + if (IsPointerUnit(t)) + { + return DecodePointer(mapBase, t); + } + + while (mapIdx != 0 && (t & NibbleMask) == 0) + { + t = t >> 4; + mapIdx = mapIdx - 1; + } + return GetAbsoluteAddress(mapBase, mapIdx, t & NibbleMask); } - data::CodeHeapListNode heapNode; - if (!target.TryRead(rangeSection.HeapList, heapNode)) + // Reports the NibbleMap units, code-header pointer, RealCodeHeader (+ its unwind-info + // array), and method code bytes for a JIT'd code address in a known CodeHeap + // RangeSection. Returns the code header's MethodDesc (or 0). Does NOT report the + // MethodDesc graph -- callers do that -- so it can be reused both for a stack return + // address AND for a MethodDesc's own native-code entry point (the reader re-resolves the + // latter during validation) without recursing. + uint64_t EnumJitCodeAt(const data::RangeSection& rangeSection, uint64_t ip) { - return 0; + data::CodeHeapListNode heapNode; + if (!m_target.TryRead(rangeSection.HeapList, heapNode)) + { + return 0; + } + if (ip < heapNode.StartAddress || ip > heapNode.EndAddress) + { + return 0; + } + + uint64_t codeStart = NibbleMapFindMethodCode(heapNode.MapBase, heapNode.HeaderMap, ip); + if (codeStart == 0 || codeStart > ip) + { + return 0; + } + + // The code header pointer is stored one pointer before the code start. + uint64_t codeHeaderIndirect = codeStart - m_target.PointerSize(); + uint64_t codeHeaderAddress = 0; + if (!m_target.TryReadPointer(codeHeaderIndirect, codeHeaderAddress) || codeHeaderAddress == 0) + { + return 0; + } + Emit("code-header-ptr", codeHeaderIndirect, m_target.PointerSize()); + + // Stub code blocks have a small sentinel value instead of a real header. + if (codeHeaderAddress <= m_stubCodeBlockLast) + { + return 0; + } + + data::RealCodeHeader codeHeader; + if (!m_target.TryRead(codeHeaderAddress, codeHeader)) // captured by EnumMem + { + return 0; + } + + // The AMD64 unwinder reads the method's unwind info (RealCodeHeader.UnwindInfos, an + // inline array of NumUnwindInfos RuntimeFunction entries) to unwind the frame. That + // trailing array extends past the fixed RealCodeHeader, so emit it explicitly. + uint64_t unwindInfosAddr = 0; + uint32_t rfStride = 0; + if (codeHeader.NumUnwindInfos != 0 && + m_target.TryGetFieldAddress(codeHeaderAddress, "RealCodeHeader", "UnwindInfos", unwindInfosAddr) && + m_target.TryGetTypeSize("RuntimeFunction", rfStride) && rfStride != 0) + { + Emit("unwind-infos", unwindInfosAddr, (uint64_t)codeHeader.NumUnwindInfos * rfStride); + } + + // SOS GetCodeHeaderData decodes the method's GC info blob (RealCodeHeader.GCInfo) to + // report MethodSize (GcInfoDecoder reads the method's code length from it). The blob + // is a separate, variable-length allocation the code header only points to, so emit a + // bounded span for it here. + if (codeHeader.GCInfo != 0) + { + Emit("gc-info", codeHeader.GCInfo, MaxGcInfoEmit); + } + + // Emit the method's code (code start through the IP, bounded). + uint64_t emitEnd = ip + m_target.PointerSize(); + uint64_t codeSize = emitEnd - codeStart; + if (codeSize > MaxMethodCodeEmit) + { + codeSize = MaxMethodCodeEmit; + } + Emit("jit-code", codeStart, codeSize); + + return codeHeader.MethodDesc; } - if (ip < heapNode.StartAddress || ip > heapNode.EndAddress) + + // Reports a MethodTable and the structures the reader touches while validating a + // MethodDesc (RuntimeTypeSystem.SlotIsVtableSlot / GetTypeHandle): the MethodTable + // header + a generous span for inline vtable slots, plus its Module and EEClass/ + // canonical MethodTable. These are Data-type reads captured via the Target's EnumMem. + void EnumMethodTable(uint64_t mtPtr) { - return 0; + if (mtPtr == 0) + { + return; + } + m_target.EmitStruct("MethodTable", mtPtr); + // Inline vtable slots follow the fixed MethodTable fields; emit a generous span. + m_target.EmitMemory(mtPtr, 0x400); + + uint64_t module = 0; + if (m_target.TryReadFieldPointer(mtPtr, "MethodTable", "Module", module) && module != 0) + { + m_target.EmitStruct("Module", module); + } + // EEClassOrCanonMT: low bits are a discriminator (0 = EEClass, 1 = canonical MethodTable). + uint64_t eeClassOrCanon = 0; + if (m_target.TryReadFieldPointer(mtPtr, "MethodTable", "EEClassOrCanonMT", eeClassOrCanon) && eeClassOrCanon != 0) + { + uint64_t masked = eeClassOrCanon & ~(uint64_t)0x3; + if ((eeClassOrCanon & 0x3) == 0) + { + m_target.EmitStruct("EEClass", masked); + } + else + { + // Canonical MethodTable: emit its header too (bounded; canon usually == self). + if (masked != mtPtr) + { + m_target.EmitStruct("MethodTable", masked); + m_target.EmitMemory(masked, 0x400); + } + } + } } - uint64_t codeStart = NibbleMapFindMethodCode(target, heapNode.MapBase, heapNode.HeaderMap, ip, sink, sinkContext); - if (codeStart == 0 || codeStart > ip) + // Reports the precode stub the reader reads to round-trip a MethodDesc's temporary entry + // point (PrecodeStubs.GetMethodDescFromStubAddress): the stub instruction bytes + the + // StubPrecodeData stored one code page later. The PrecodeMachineDescriptor is emitted + // once in enumerate.cpp. + void EnumPrecodeStub(uint64_t entryPoint) { - return 0; + if (entryPoint == 0) + { + return; + } + uint64_t platformMetadata = 0; + if (!m_target.TryGetGlobalValue("PlatformMetadata", platformMetadata) || platformMetadata == 0) + { + return; + } + uint64_t descAddr = 0; + if (!m_target.TryGetFieldAddress(platformMetadata, "PlatformMetadata", "PrecodeMachineDescriptor", descAddr) || descAddr == 0) + { + descAddr = platformMetadata; // embedded at offset 0 + } + uint32_t stubCodePageSize = 0; + m_target.TryReadFieldUInt32(descAddr, "PrecodeMachineDescriptor", "StubCodePageSize", stubCodePageSize); + + // Stub instruction bytes (type identification reads a few bytes at the entry point). + m_target.EmitMemory(entryPoint, 64); + // The precode data (StubPrecodeData/FixupPrecodeData) is stored one code page later. + if (stubCodePageSize != 0) + { + m_target.EmitMemory(entryPoint + stubCodePageSize, 128); + } } - // The code header pointer is stored one pointer before the code start. - uint64_t codeHeaderIndirect = codeStart - target.PointerSize(); - uint64_t codeHeaderAddress = 0; - if (!target.TryReadPointer(codeHeaderIndirect, codeHeaderAddress) || codeHeaderAddress == 0) + // Reports a MethodDesc and everything the reader dereferences while validating it + // (RuntimeTypeSystem.ValidateMethodDescPointer): the MethodDesc, its MethodDescChunk + // (which sits BEFORE the MethodDesc at md - sizeof(chunk) - ChunkIndex*alignment), + // MethodDescCodeData, the MethodTable graph, and the temporary-entry-point precode stub. + // For JIT'd methods (emitNativeCode) it also covers the MethodDesc's own native-code + // entry point: validation's HasNativeCode -> GetCodePointer -> GetCodeBlockHandle reads + // the native code slot (pCode) then re-resolves that entry via RangeSection+NibbleMap+ + // code-header, and pCode can differ from any stack return address (e.g. a tiered/OSR + // version not live on a stack), so its NibbleMap/header were never emitted by the scan. + // (R2R native code is served from the on-disk image, so callers pass emitNativeCode=false.) + void EnumMethodDesc(uint64_t methodDesc, bool emitNativeCode) { - return 0; + if (methodDesc == 0) + { + return; + } + // Emit the MethodDesc using its exact descriptor-computed size (classification + slots). + uint32_t mdSize = MethodDescSize(m_target, methodDesc); + if (mdSize > 0) + { + Emit("methoddesc", methodDesc, mdSize); + } + + // The MethodDescChunk sits BEFORE the MethodDesc: + // chunk = methodDesc - sizeof(MethodDescChunk) - ChunkIndex * MethodDescAlignment. + // All three come from the data descriptor / globals. + uint64_t chunkIndexAddr = 0; + uint8_t chunkIndex = 0; + if (m_target.TryGetFieldAddress(methodDesc, "MethodDesc", "ChunkIndex", chunkIndexAddr)) + { + m_target.TryReadUInt8(chunkIndexAddr, chunkIndex); + } + uint64_t alignment = 0; + uint32_t chunkSize = 0; + if (!m_target.TryGetGlobalValue("MethodDescAlignment", alignment) || alignment == 0 || + !m_target.TryGetStructSpan("MethodDescChunk", chunkSize)) + { + return; + } + uint64_t chunkAddr = methodDesc - chunkSize - (uint64_t)chunkIndex * alignment; + Emit("methoddesc-chunk", chunkAddr, chunkSize); + + // The MethodDescChunk holds the MethodTable; validation reads it (and its graph). + uint64_t mtPtr = 0; + if (m_target.TryReadFieldPointer(chunkAddr, "MethodDescChunk", "MethodTable", mtPtr) && mtPtr != 0) + { + EnumMethodTable(mtPtr); + } + + // MethodDesc validation reads MethodDescCodeData (temporary entry point / tier) via + // the MethodDesc's CodeData pointer, then round-trips the temporary entry point + // through the precode stub. + uint64_t codeData = 0; + if (m_target.TryReadFieldPointer(methodDesc, "MethodDesc", "CodeData", codeData) && codeData != 0) + { + m_target.EmitStruct("MethodDescCodeData", codeData); + uint64_t tempEntryPoint = 0; + if (m_target.TryReadFieldPointer(codeData, "MethodDescCodeData", "TemporaryEntryPoint", tempEntryPoint)) + { + EnumPrecodeStub(tempEntryPoint); + } + } + + // JIT'd code: also cover the MethodDesc's own native-code entry point (see comment). + if (emitNativeCode) + { + uint64_t nativeCodeSlotAddr = 0; + uint64_t pCode = 0; + if (TryGetNativeCodeSlotAddress(m_target, methodDesc, nativeCodeSlotAddr) && + m_target.TryReadPointer(nativeCodeSlotAddr, pCode) && pCode != 0) + { + uint64_t rsAddr = FindRangeSection(pCode); + if (rsAddr != 0) + { + data::RangeSection rs; + if (m_target.TryRead(rsAddr, rs) && + ((int32_t)rs.Flags & RangeSectionFlag_CodeHeap) != 0 && rs.HeapList != 0) + { + EnumJitCodeAt(rs, pCode); + } + } + } + } } - sink(sinkContext, "code-header-ptr", codeHeaderIndirect, (uint64_t)target.PointerSize()); - // Stub code blocks have a small sentinel value instead of a real header. - if (codeHeaderAddress <= stubCodeBlockLast) + // PtrHashMap probe (HashMapLookup.GetValue + PtrHashMapLookup). Buckets touched during + // the probe are emitted ONLY if the key is found -- the conservative stack scan probes + // with many false-positive "keys" (stack data that happens to fall in an R2R range), and + // emitting their probe chains would touch most of a large map. Buffering + emit-on-hit + // keeps emission proportional to real frames (what the DAC does). Returns the value, or + // 0 if not present. + uint64_t HashMapProbe(uint64_t mapAddr, uint64_t key, uint64_t valueMask) { - return 0; + data::HashMap hm; + if (!m_target.TryRead(mapAddr, hm) || hm.Buckets == 0) + { + return 0; + } + // PtrHashMap::SanitizeKey + if (key <= 1) + { + key += 100; + } + uint32_t size = 0; + if (!m_target.TryReadUInt32(hm.Buckets, size) || size <= 1) + { + return 0; + } + + // Bucket layout comes from the descriptor: total size, slot count, and the Values + // field offset (Keys are the first field). No hardcoded 64/4/32. + uint32_t bucketSize = 0; + if (!m_target.TryGetTypeSize("Bucket", bucketSize) || bucketSize == 0) + { + return 0; + } + uint64_t slotsPerBucket = 0; + m_target.TryGetGlobalValue("HashMapSlotsPerBucket", slotsPerBucket); + uint64_t valuesOffset = 0; + m_target.TryGetFieldAddress(0, "Bucket", "Values", valuesOffset); + if (slotsPerBucket == 0 || valuesOffset == 0) + { + return 0; + } + const uint64_t ptrSize = m_target.PointerSize(); + + uint32_t seed = (uint32_t)(key >> 2); + uint32_t increment = (uint32_t)(1 + (((uint32_t)(key >> 5) + 1) % (size - 1))); + uint64_t buckets = hm.Buckets + bucketSize; + + std::vector touched; // bucket addresses visited on the probe chain + for (uint32_t i = 0; i < size; i++) + { + uint64_t bucketAddr = buckets + (uint64_t)bucketSize * (seed % size); + touched.push_back(bucketAddr); + + uint64_t values0 = 0; + for (uint64_t slot = 0; slot < slotsPerBucket; slot++) + { + uint64_t k = 0; + if (m_target.TryReadPointer(bucketAddr + slot * ptrSize, k) && k == key) + { + uint64_t v = 0; + m_target.TryReadPointer(bucketAddr + valuesOffset + slot * ptrSize, v); + // Found: emit the count slot + the probe-chain buckets so the reader can re-probe. + Emit("r2r-hashcount", hm.Buckets, bucketSize); + for (uint64_t b : touched) + { + Emit("r2r-hashbucket", b, bucketSize); + } + return (v & valueMask) << 1; // PtrHashMap stores value >> 1 + } + } + m_target.TryReadPointer(bucketAddr + valuesOffset, values0); + + seed += increment; + // IsCollision: high (mask) bit of Values[0] set => probe continues. + if ((values0 & ~valueMask) == 0) + { + break; + } + } + return 0; // not found: emit nothing } - data::RealCodeHeader codeHeader; - if (!target.TryRead(codeHeaderAddress, codeHeader)) // captured by EnumMem + // Resolves an R2R IP to its MethodDesc, reporting the probe-touched buckets + the + // MethodDesc. For an IP in an R2R RangeSection we resolve the MethodDesc the way the + // reader does (ExecutionManagerHelpers.ReadyToRunJitManager), so the memory the reader + // touches (probe-chain hash buckets + the MethodDesc) is captured. We do NOT emit the + // whole EntryPointToMethodDescMap -- it can be MBs; the DAC likewise only probes it + // (dotnet/diagnostics#5910). The RUNTIME_FUNCTION table is image-backed, so it is read + // (from the live target) but not emitted. + uint64_t EnumR2RMethodDesc(const data::RangeSection& rangeSection, uint64_t ip) { + if (rangeSection.R2RModule == 0) + { + return 0; + } + data::Module module; + if (!m_target.TryRead(rangeSection.R2RModule, module) || module.ReadyToRunInfo == 0) + { + return 0; + } + data::ReadyToRunInfo r2r; + if (!m_target.TryRead(module.ReadyToRunInfo, r2r) || r2r.RuntimeFunctions == 0 || r2r.NumRuntimeFunctions == 0) + { + return 0; + } + + uint64_t imageBase = rangeSection.RangeBegin; + if (ip < imageBase) + { + return 0; + } + uint32_t rva = (uint32_t)(ip - imageBase); + + uint32_t rfStride = 0; + if (!m_target.TryGetTypeSize("RuntimeFunction", rfStride) || rfStride == 0) + { + return 0; + } + + uint32_t index = 0; + if (!FindRuntimeFunctionIndex(m_target, r2r.RuntimeFunctions, rfStride, r2r.NumRuntimeFunctions, rva, index)) + { + return 0; + } + + uint64_t compositeInfo = r2r.CompositeInfo != 0 ? r2r.CompositeInfo : module.ReadyToRunInfo; + uint64_t mapAddr = 0; + if (!m_target.TryGetFieldAddress(compositeInfo, "ReadyToRunInfo", "EntryPointToMethodDescMap", mapAddr)) + { + return 0; + } + + uint64_t valueMask = 0x7FFFFFFFFFFFFFFFull; + m_target.TryGetGlobalValue("HashMapValueMask", valueMask); + + // Funclets have no map entry; walk back to the enclosing function's entry point. + // Bound the walk: real funclets are within a few runtime functions of their parent, + // and an unbounded walk would spin for a false-positive (non-code) key with a high index. + const uint32_t MaxFuncletWalk = 16; + uint32_t walkEnd = index > MaxFuncletWalk ? index - MaxFuncletWalk : 0; + for (uint32_t i = index; ; i--) + { + uint32_t beginRva = 0; + m_target.TryReadUInt32(r2r.RuntimeFunctions + (uint64_t)i * rfStride, beginRva); + uint64_t entryPoint = imageBase + beginRva; + uint64_t methodDesc = HashMapProbe(mapAddr, entryPoint, valueMask); + if (methodDesc != 0 && (uint32_t)methodDesc != HashInvalidEntry) + { + // R2R native code is image-backed (served from disk), so emitNativeCode=false + // (only JIT'd code needs native-code-entry re-emission). + EnumMethodDesc(methodDesc, /*emitNativeCode*/ false); + return methodDesc; + } + if (i == 0 || i == walkEnd) + { + break; + } + } return 0; } - // Emit the method's code (code start through the IP, bounded). - uint64_t emitEnd = ip + target.PointerSize(); - uint64_t codeSize = emitEnd - codeStart; - if (codeSize > MaxMethodCodeEmit) + // Resolves the MethodDesc carried by a transition Frame, mirroring + // FrameHelpers.GetMethodDescPtr. Transition frames store their MethodDesc in the + // on-stack Frame object (not as a code return address), so the conservative stack scan + // never resolves them. 'identifier' is the vtable pointer at the frame address + // (Data.Frame.Identifier). + uint64_t ResolveFrameMethodDesc(uint64_t frameAddr, uint64_t identifier) { - codeSize = MaxMethodCodeEmit; - } - sink(sinkContext, "jit-code", codeStart, codeSize); + if (identifier == 0) + { + return 0; + } + const uint64_t ptrSize = m_target.PointerSize(); + uint64_t id = 0; - // Emit the MethodDesc (its exact size is chunk-dependent; a bounded blob suffices). - if (codeHeader.MethodDesc != 0) - { - sink(sinkContext, "methoddesc", codeHeader.MethodDesc, MethodDescEmit); + // InlinedCallFrame (P/Invoke): MethodDesc is Datum, when the frame has an active call + // (CallerReturnAddress != 0) to a real function (Datum != 0 and low bit clear). + if (m_target.TryGetGlobalValue("InlinedCallFrameIdentifier", id) && id == identifier) + { + uint64_t callerRet = 0, datum = 0; + m_target.TryReadFieldPointer(frameAddr, "InlinedCallFrame", "CallerReturnAddress", callerRet); + m_target.TryReadFieldPointer(frameAddr, "InlinedCallFrame", "Datum", datum); + if (callerRet != 0 && datum != 0 && (datum & 0x1) == 0) + { + return datum & ~(ptrSize - 1); + } + return 0; + } + + // FramedMethodFrame and its subclasses share the layout; read MethodDescPtr. + static const char* const framedIds[] = { + "FramedMethodFrameIdentifier", "DynamicHelperFrameIdentifier", + "ExternalMethodFrameIdentifier", "PrestubMethodFrameIdentifier", + "CallCountingHelperFrameIdentifier" + }; + for (const char* gid : framedIds) + { + if (m_target.TryGetGlobalValue(gid, id) && id == identifier) + { + uint64_t md = 0; + m_target.TryReadFieldPointer(frameAddr, "FramedMethodFrame", "MethodDescPtr", md); + return md; + } + } + + // StubDispatchFrame: MethodDescPtr (the MT+slot fallback is omitted -- uncommon on + // crash stacks and would pull in the type-system slot machinery). + if (m_target.TryGetGlobalValue("StubDispatchFrameIdentifier", id) && id == identifier) + { + uint64_t md = 0; + m_target.TryReadFieldPointer(frameAddr, "StubDispatchFrame", "MethodDescPtr", md); + return md; + } + return 0; } - return codeHeader.MethodDesc; - } + const Target& m_target; + RegionCallback m_sink; + void* m_sinkContext; + uint64_t m_codeRangeMap; // RangeSectionMap.TopLevelData (the radix map root) + uint64_t m_stubCodeBlockLast; + std::set m_emitted; // deduped explicit region starts + std::set m_processed; // distinct scanned stack values + std::set m_captured; // distinct MethodDescs resolved + }; } int EnumerateStackScanRegions(const Target& target, RegionCallback sink, void* sinkContext) { + // The code range map global holds the RangeSectionMap address directly (the managed + // ExecutionManager contract uses ReadGlobalPointer(ExecutionManagerCodeRangeMapAddress) + // without an extra deref). Use TryGetGlobalValue -- NOT TryReadGlobalPointer, which would + // add a spurious dereference (correct for globals like ThreadStore whose value is the + // address of a pointer variable, but wrong here). uint64_t codeRangeMapPtr = 0; - if (!target.TryReadGlobalPointer(GlobalCodeRangeMap, codeRangeMapPtr) || codeRangeMapPtr == 0) + if (!target.TryGetGlobalValue(GlobalCodeRangeMap, codeRangeMapPtr) || codeRangeMapPtr == 0) { return -1; } @@ -324,8 +905,8 @@ namespace contracts } const uint64_t ptrSize = target.PointerSize(); + CodeEnumerator enumerator(target, sink, sinkContext, topMap, stubCodeBlockLast); std::set visitedThreads; - std::set capturedMethods; uint64_t threadAddr = threadStore.FirstThreadLink; for (int i = 0; threadAddr != 0 && i < MaxThreads; i++) @@ -344,26 +925,100 @@ namespace contracts uint64_t stackHigh = thread.CachedStackBase; if (stackLow != 0 && stackHigh > stackLow) { - // Conservatively scan every pointer-aligned slot for code pointers. - for (uint64_t sp = stackLow; sp + ptrSize <= stackHigh; sp += ptrSize) + // The stack grows DOWN: committed pages are contiguous from the current stack + // pointer up to the base, with reserved-but-uncommitted pages below. Scan pages + // top-down starting at the base and stop at the first uncommitted (unreadable) + // page. This is O(committed stack) rather than O(reserved stack), which matters + // when a thread was created with a very large maxStackSize (e.g. hundreds of MB). + // Each page is read in ONE bulk read and scanned in-memory, rather than issuing a + // separate target read per 8-byte slot (which dominated collection time on deep + // stacks -- hundreds of thousands of individual reads). + uint8_t pageBuf[StackScanPageSize]; + uint64_t page = (stackHigh - 1) & ~(StackScanPageSize - 1); + for (;;) { - uint64_t value = 0; - if (!target.TryReadPointer(sp, value) || value == 0) + uint64_t pageStart = page < stackLow ? stackLow : page; + uint64_t pageEnd = page + StackScanPageSize; + if (pageEnd > stackHigh) + { + pageEnd = stackHigh; + } + uint32_t pageLen = (uint32_t)(pageEnd - pageStart); + if (!target.ReadBuffer(pageStart, pageBuf, pageLen)) { - continue; + break; // reached uncommitted stack; nothing deeper is live } - uint64_t methodDesc = ResolveCodeIP(target, topMap, stubCodeBlockLast, value, sink, sinkContext); - if (methodDesc != 0) + + // Conservatively scan every pointer-aligned slot in this committed page. + for (uint32_t off = 0; off + ptrSize <= pageLen; off += (uint32_t)ptrSize) { - capturedMethods.insert(methodDesc); + uint64_t value = 0; + memcpy(&value, pageBuf + off, sizeof(uint64_t)); + if (value == 0) + { + continue; + } + // Skip values that point into this thread's own stack: saved frame pointers + // (RBP chain), spilled locals holding stack addresses, etc. These are never + // code, and on a deep stack they are the dominant source of distinct values; + // filtering them here avoids a radix-map probe per frame. + if (value >= stackLow && value < stackHigh) + { + continue; + } + // Skip values already resolved on this or a prior thread (recursion pushes + // the same return address many times; other slots repeat too). + if (enumerator.AlreadyProcessed(value)) + { + continue; + } + uint64_t methodDesc = enumerator.EnumCodeForIP(value); + if (methodDesc != 0) + { + enumerator.NoteCapturedMethod(methodDesc); + } + } + + if (page <= stackLow || page < StackScanPageSize) + { + break; } + page -= StackScanPageSize; + } + } + + // Walk the explicit Frame chain (Thread.m_pFrame -> Frame.Next). Transition frames + // (P/Invoke InlinedCallFrame, FramedMethodFrame, StubDispatchFrame, ...) carry a + // MethodDesc in the on-stack Frame object rather than as a code return address, so the + // conservative stack scan above -- which only resolves code IPs -- never captures them. + // The reader resolves these frames' MethodDescs during the walk, so emit each frame's + // MethodDesc graph. (The frame objects themselves live on the stack and are already + // captured by the committed-page scan above.) + const uint64_t frameTerminator = (ptrSize == 8) ? 0xFFFFFFFFFFFFFFFFull : 0xFFFFFFFFull; + uint64_t frameAddr = thread.Frame; + std::set visitedFrames; + for (int f = 0; frameAddr != 0 && frameAddr != frameTerminator && f < MaxFrames; f++) + { + if (!visitedFrames.insert(frameAddr).second) + { + break; // cycle guard + } + uint64_t identifier = 0; + target.TryReadPointer(frameAddr, identifier); // vtable ptr at offset 0 == Frame.Identifier + enumerator.EnumFrameMethodDesc(frameAddr, identifier); + + uint64_t next = 0; + if (!target.TryReadFieldPointer(frameAddr, "Frame", "Next", next)) + { + break; } + frameAddr = next; } threadAddr = thread.LinkNext; } - return (int)capturedMethods.size(); + return (int)enumerator.CapturedMethodCount(); } } } // namespace contracts diff --git a/src/coreclr/debug/cdaclite/contracts/statics.cpp b/src/coreclr/debug/cdaclite/contracts/statics.cpp index 1dd4fe831ef012..23e7addbe9015d 100644 --- a/src/coreclr/debug/cdaclite/contracts/statics.cpp +++ b/src/coreclr/debug/cdaclite/contracts/statics.cpp @@ -60,9 +60,9 @@ namespace contracts } } - int EnumerateStaticRegions(const Target& target, uint64_t contractDescriptorAddr, - RegionCallback sink, void* sinkContext) + int EnumerateStaticRegions(const Target& target, RegionCallback sink, void* sinkContext) { + uint64_t contractDescriptorAddr = target.ContractDescriptorAddr(); if (contractDescriptorAddr == 0) { return 0; diff --git a/src/coreclr/debug/cdaclite/contracts/statics.h b/src/coreclr/debug/cdaclite/contracts/statics.h index 4569c4a9cbc42d..5bff1520dfe430 100644 --- a/src/coreclr/debug/cdaclite/contracts/statics.h +++ b/src/coreclr/debug/cdaclite/contracts/statics.h @@ -29,8 +29,7 @@ namespace contracts // Reports the contract descriptor (kinds "contract-descriptor", "descriptor-json", // "pointer-data") and the global variable storage referenced by pointer_data // (kind "global-var"). Returns the number of regions reported. - int EnumerateStaticRegions(const Target& target, uint64_t contractDescriptorAddr, - RegionCallback sink, void* sinkContext); + int EnumerateStaticRegions(const Target& target, RegionCallback sink, void* sinkContext); } } // namespace contracts diff --git a/src/coreclr/debug/cdaclite/contracts/stresslog.cpp b/src/coreclr/debug/cdaclite/contracts/stresslog.cpp index a1a08c10031fcc..1252c0f640902a 100644 --- a/src/coreclr/debug/cdaclite/contracts/stresslog.cpp +++ b/src/coreclr/debug/cdaclite/contracts/stresslog.cpp @@ -26,8 +26,12 @@ namespace contracts const int MaxChunksPerThread = 1000000; } - int EnumerateStressLogRegions(const Target& target) + int EnumerateStressLogRegions(const Target& target, RegionCallback, void*) { + if (target.Tier() != DumpTier::Heap) + { + return 0; + } // Stress log is opt-in; if disabled there is nothing to capture. // COPILOT TODO: We shouldn't need this fallback for different sized things. Either it is pointer sized or a set size. uint64_t enabled = 0; diff --git a/src/coreclr/debug/cdaclite/contracts/stresslog.h b/src/coreclr/debug/cdaclite/contracts/stresslog.h index 37b80fb257f819..2bc6cfa376de31 100644 --- a/src/coreclr/debug/cdaclite/contracts/stresslog.h +++ b/src/coreclr/debug/cdaclite/contracts/stresslog.h @@ -24,7 +24,7 @@ namespace contracts // message buffer) so the cDAC can re-read them from the dump. Memory is captured via the // Target's EnumMem sink. Returns the number of chunks captured, 0 if the stress log is // disabled, or -1 if the stress log could not be located. - int EnumerateStressLogRegions(const Target& target); + int EnumerateStressLogRegions(const Target& target, RegionCallback sink, void* sinkContext); } } // namespace contracts diff --git a/src/coreclr/debug/cdaclite/contracts/syncblock.cpp b/src/coreclr/debug/cdaclite/contracts/syncblock.cpp index 1ae82c64d6e2ed..f310077c3a6311 100644 --- a/src/coreclr/debug/cdaclite/contracts/syncblock.cpp +++ b/src/coreclr/debug/cdaclite/contracts/syncblock.cpp @@ -22,8 +22,12 @@ namespace contracts const uint32_t MaxSyncBlocks = 4u * 1024 * 1024; } - int EnumerateSyncBlockRegions(const Target& target) + int EnumerateSyncBlockRegions(const Target& target, RegionCallback, void*) { + if (target.Tier() != DumpTier::Heap) + { + return 0; + } // The sync-block cache holds the number of used entries (FreeSyncTableIndex). uint64_t cacheAddr = 0; if (!target.TryReadGlobalPointer(GlobalSyncBlockCache, cacheAddr) || cacheAddr == 0) diff --git a/src/coreclr/debug/cdaclite/contracts/syncblock.h b/src/coreclr/debug/cdaclite/contracts/syncblock.h index f113627515d737..4a45643acf605e 100644 --- a/src/coreclr/debug/cdaclite/contracts/syncblock.h +++ b/src/coreclr/debug/cdaclite/contracts/syncblock.h @@ -24,7 +24,7 @@ namespace contracts // cDAC's SyncBlock contract can re-read them from the dump. Memory is captured via the // Target's EnumMem sink (no explicit region kind). Returns the number of in-use sync // blocks captured, or -1 if the sync table could not be located. - int EnumerateSyncBlockRegions(const Target& target); + int EnumerateSyncBlockRegions(const Target& target, RegionCallback sink, void* sinkContext); } } // namespace contracts diff --git a/src/coreclr/debug/cdaclite/contracts/thread.cpp b/src/coreclr/debug/cdaclite/contracts/thread.cpp index ef1c59d16205b6..2ff62e92d95063 100644 --- a/src/coreclr/debug/cdaclite/contracts/thread.cpp +++ b/src/coreclr/debug/cdaclite/contracts/thread.cpp @@ -21,6 +21,11 @@ namespace contracts const char* const GlobalThreadStore = "ThreadStore"; const int MaxThreads = 100000; // guard against corrupt thread lists + + // The reader's Data.Thread eagerly reads the Thread's RuntimeThreadLocals (which embeds + // EEAllocContext -> GCAllocContext). Emit a blob at that pointer covering the embedded + // alloc-context chain (the descriptor field-span logic under-covers embedded structs). + const uint32_t RuntimeThreadLocalsEmit = 512; } int EnumerateThreadRegions(const Target& target, RegionCallback sink, void* sinkContext) @@ -55,6 +60,14 @@ namespace contracts break; } + // The reader's Data.Thread ctor dereferences RuntimeThreadLocals; emit it so + // GetThreadData succeeds from the dump without the legacy DAC. + uint64_t rtlPtr = 0; + if (target.TryReadFieldPointer(threadAddr, "Thread", "RuntimeThreadLocals", rtlPtr) && rtlPtr != 0) + { + sink(sinkContext, "thread-locals", rtlPtr, RuntimeThreadLocalsEmit); + } + // Stack grows down: CachedStackLimit is the low address, CachedStackBase // the high address. Report the committed stack range. if (thread.CachedStackBase > thread.CachedStackLimit && thread.CachedStackLimit != 0) diff --git a/src/coreclr/debug/cdaclite/data/runtimetypes.h b/src/coreclr/debug/cdaclite/data/runtimetypes.h index dcf8959e630e5b..cfa32f15729a54 100644 --- a/src/coreclr/debug/cdaclite/data/runtimetypes.h +++ b/src/coreclr/debug/cdaclite/data/runtimetypes.h @@ -89,6 +89,39 @@ namespace data CDAC_PTR(Frame) }; + // --- Frame chain (Thread.m_pFrame -> Frame.Next) ---------------------- + // Transition frames carry a MethodDesc in the on-stack Frame object rather than as a code + // return address; the reader resolves them during the stack walk (FrameHelpers.GetMethodDescPtr). + + struct Frame : Struct + { + Frame() : Struct("Frame") {} + CDAC_PTR(Next) // next Frame* (chain terminates at ~0) + }; + + // FramedMethodFrame and its subclasses (DynamicHelper/ExternalMethod/PrestubMethod/ + // CallCountingHelper) share this layout; the MethodDesc is read from MethodDescPtr. + struct FramedMethodFrame : Struct + { + FramedMethodFrame() : Struct("FramedMethodFrame") {} + CDAC_PTR(MethodDescPtr) + }; + + struct StubDispatchFrame : Struct + { + StubDispatchFrame() : Struct("StubDispatchFrame") {} + CDAC_PTR(MethodDescPtr) + }; + + // P/Invoke transition frame: MethodDesc lives in Datum (when the frame has an active call + // to a known function). This is the common case that carries a JIT'd P/Invoke stub MethodDesc. + struct InlinedCallFrame : Struct + { + InlinedCallFrame() : Struct("InlinedCallFrame") {} + CDAC_PTR(CallerReturnAddress) + CDAC_PTR(Datum) + }; + // --- Loader / modules ------------------------------------------------- struct AppDomain : Struct @@ -127,6 +160,34 @@ namespace data CDAC_PTR(PEAssembly) CDAC_PTR(LoaderAllocator) CDAC_OPT_PTR(GrowableSymbolStream) // in-memory symbol (PDB) stream, if any + CDAC_OPT_PTR(ReadyToRunInfo) // ReadyToRun modules: the R2R runtime info + }; + + // ReadyToRun runtime info for an R2R module (needed to resolve R2R frames on the stack). + struct ReadyToRunInfo : Struct + { + ReadyToRunInfo() : Struct("ReadyToRunInfo") {} + CDAC_OPT_PTR(CompositeInfo) + CDAC_OPT_PTR(ReadyToRunHeader) + CDAC_OPT_PTR(DebugInfoSection) + CDAC_OPT_PTR(RuntimeFunctions) // RUNTIME_FUNCTION[] table (in the R2R image) + CDAC_U32(NumRuntimeFunctions) + }; + + // A single RUNTIME_FUNCTION table entry (BeginAddress is the method-start RVA). + struct RuntimeFunction : Struct + { + RuntimeFunction() : Struct("RuntimeFunction") {} + CDAC_U32(BeginAddress) + }; + + // Open-addressing pointer hash map (HashMap/PtrHashMap). Buckets points at an array whose + // first slot holds the bucket count; real buckets follow. Each bucket is 64 bytes (4 key + + // 4 value pointer slots). + struct HashMap : Struct + { + HashMap() : Struct("HashMap") {} + CDAC_PTR(Buckets) }; // In-memory symbol stream (module's growable PDB buffer). @@ -209,7 +270,7 @@ namespace data struct RangeSectionMap : Struct { RangeSectionMap() : Struct("RangeSectionMap") {} - CDAC_PTR(TopLevelData) + CDAC_ADDR(TopLevelData) // managed [FieldAddress]: the inline radix root lives AT this field's address }; struct RangeSectionFragment : Struct @@ -237,6 +298,9 @@ namespace data { RealCodeHeader() : Struct("RealCodeHeader") {} CDAC_PTR(MethodDesc) + CDAC_PTR(GCInfo) // -> GC info blob (decoded for MethodSize / GC roots) + CDAC_U32(NumUnwindInfos) // count of trailing RuntimeFunction (unwind) entries + // UnwindInfos is a [FieldAddress] inline array read via TryGetFieldAddress. }; // Code heap base type; HeapType selects LoaderCodeHeap vs HostCodeHeap. diff --git a/src/coreclr/debug/cdaclite/data/target.cpp b/src/coreclr/debug/cdaclite/data/target.cpp index 47173b58d2f8a5..95f4c32a2b19f1 100644 --- a/src/coreclr/debug/cdaclite/data/target.cpp +++ b/src/coreclr/debug/cdaclite/data/target.cpp @@ -105,29 +105,24 @@ namespace cdac return true; } - void Target::EmitStructMemory(const char* typeName, uint64_t address) const + bool Target::TryGetStructSpan(const std::string& typeName, uint32_t& span) const { - if (m_enumMemSink == nullptr || typeName == nullptr || m_descriptor == nullptr) + if (m_descriptor == nullptr) { - return; + return false; } - // Prefer the descriptor's exact type size (the analog of the DAC's sizeof(type)). uint32_t size = 0; if (TryGetTypeSize(typeName, size) && size > 0) { - m_enumMemSink(m_enumMemContext, address, size); - return; + span = size; + return true; } - - // Most runtime types are CDAC_TYPE_INDETERMINATE (offsets only, no size). Approximate - // the footprint from the largest declared field: max(offset) + 8 covers every - // scalar/pointer field (8 == largest primitive on this 64-bit build). This mirrors the - // set of bytes any cDAC consumer can read via the descriptor's field offsets. + // Indeterminate types (offsets only): max(field offset) + 8 covers every scalar/pointer. const TypeInfo* type = m_descriptor->FindType(typeName); if (type == nullptr || type->fields.empty()) { - return; + return false; } uint32_t maxEnd = 0; for (std::map::const_iterator it = type->fields.begin(); it != type->fields.end(); ++it) @@ -138,9 +133,24 @@ namespace cdac maxEnd = end; } } - if (maxEnd > 0) + if (maxEnd == 0) + { + return false; + } + span = maxEnd; + return true; + } + + void Target::EmitStructMemory(const char* typeName, uint64_t address) const + { + if (m_enumMemSink == nullptr || typeName == nullptr) + { + return; + } + uint32_t span = 0; + if (TryGetStructSpan(typeName, span) && span > 0) { - m_enumMemSink(m_enumMemContext, address, maxEnd); + m_enumMemSink(m_enumMemContext, address, span); } } diff --git a/src/coreclr/debug/cdaclite/data/target.h b/src/coreclr/debug/cdaclite/data/target.h index eca00e23ba77ca..1b5b24f03fd72c 100644 --- a/src/coreclr/debug/cdaclite/data/target.h +++ b/src/coreclr/debug/cdaclite/data/target.h @@ -58,6 +58,14 @@ namespace cdac DumpTier Tier() const { return m_tier; } void SetTier(DumpTier tier) { m_tier = tier; } + // The runtime module base and root contract-descriptor address, carried on the Target so + // that every memory enumerator has a uniform (const Target&, sink, ctx) signature and can + // reach the bootstrap/self-description state without a separate context object. + uint64_t ClrBase() const { return m_clrBase; } + void SetClrBase(uint64_t clrBase) { m_clrBase = clrBase; } + uint64_t ContractDescriptorAddr() const { return m_contractDescriptorAddr; } + void SetContractDescriptorAddr(uint64_t addr) { m_contractDescriptorAddr = addr; } + // --- Typed struct reads (Data-type layer) --------------------------- // Reads a Data type (any struct with `bool Load(const Target&, uint64_t)`) @@ -131,6 +139,12 @@ namespace cdac // Returns the size of a type, if known. bool TryGetTypeSize(const std::string& typeName, uint32_t& size) const; + // Returns the descriptor-driven memory footprint of a struct type: the exact type size if + // the descriptor provides one, else max(field offset) + 8 (the bytes any cDAC consumer can + // read via the descriptor's field offsets). Lets callers emit a struct through their own + // sink using descriptor sizes rather than hardcoded constants. + bool TryGetStructSpan(const std::string& typeName, uint32_t& span) const; + // --- Globals --------------------------------------------------------- // Returns the resolved value stored for a global. For a global that points at a @@ -147,6 +161,11 @@ namespace cdac // Reads a string global's value, if present. bool TryGetGlobalString(const std::string& name, std::string& value) const; + // Emits a descriptor-defined struct's memory (public wrapper over EmitStructMemory) for + // contract walks that must capture a structure the reader will read but this component + // doesn't otherwise dereference (e.g. the Debugger object). + void EmitStruct(const char* typeName, uint64_t address) const { EmitStructMemory(typeName, address); } + private: // Records a Data-type struct's own memory through the EnumMem sink. Uses the // descriptor type size (the analog of the DAC's sizeof(type)). @@ -156,6 +175,8 @@ namespace cdac ReadMemoryCallback m_readMemory; void* m_readContext; DumpTier m_tier = DumpTier::Heap; + uint64_t m_clrBase = 0; + uint64_t m_contractDescriptorAddr = 0; mutable EnumMemCallback m_enumMemSink = nullptr; mutable void* m_enumMemContext = nullptr; }; diff --git a/src/coreclr/debug/cdaclite/enumerate.cpp b/src/coreclr/debug/cdaclite/enumerate.cpp index f0a90457093d00..75360ae1be4a49 100644 --- a/src/coreclr/debug/cdaclite/enumerate.cpp +++ b/src/coreclr/debug/cdaclite/enumerate.cpp @@ -13,6 +13,7 @@ #include "datatarget.h" #include +#include #include "data/datadescriptor.h" #include "data/target.h" @@ -27,6 +28,7 @@ #include "contracts/stresslog.h" #include "contracts/interop.h" #include "contracts/stackscan.h" +#include "contracts/bootstrap.h" namespace cdac { @@ -51,6 +53,54 @@ namespace cdac state->count++; } + // Memory-enumerator dispatch: a uniform table of subsystem enumerators, each of which decides + // what to emit based on the Target's dump tier. The driver (EnumMemoryRegions) just runs them + // all in order. + namespace + { + // Execution/code enumerator: one entry point that chooses its strategy by dump tier. A Heap + // dump captures the whole process, so bulk-emit every JIT code heap + loader heap. A Normal + // dump captures only what a stack walk reaches, so do the conservative stack scan instead + // (which scales to large programs where the code/loader heaps are huge). + int EnumerateCodeRegions(const cdac::Target& target, cdac::contracts::RegionCallback sink, void* sinkContext) + { + if (target.Tier() == cdac::DumpTier::Heap) + { + int jit = cdac::contracts::EnumerateJitCodeRegions(target, sink, sinkContext); + int loaderHeaps = cdac::contracts::EnumerateLoaderHeapRegions(target, sink, sinkContext); + if (jit < 0 || loaderHeaps < 0) + { + return -1; + } + return jit + loaderHeaps; + } + return cdac::contracts::EnumerateStackScanRegions(target, sink, sinkContext); + } + + // The memory enumerators, run in order for every dump. Each is uniform -- it takes the + // Target (which carries the dump tier, the runtime base, and the contract-descriptor + // address) plus the region sink -- and decides internally what to do for the current tier + // (heap-only enumerators return 0 in a Normal dump). Adding a subsystem is one table entry. + struct MemoryEnumerator + { + const char* name; + int (*enumerate)(const cdac::Target&, cdac::contracts::RegionCallback, void*); + }; + + const MemoryEnumerator s_enumerators[] = { + { "bootstrap", cdac::contracts::EnumerateBootstrapRegions }, + { "thread", cdac::contracts::EnumerateThreadRegions }, + { "module", cdac::contracts::EnumerateModuleRegions }, + { "code", EnumerateCodeRegions }, + { "gc", cdac::contracts::EnumerateGCHeapRegions }, + { "handle", cdac::contracts::EnumerateHandleRegions }, + { "syncblock", cdac::contracts::EnumerateSyncBlockRegions }, + { "stresslog", cdac::contracts::EnumerateStressLogRegions }, + { "interop", cdac::contracts::EnumerateInteropRegions }, + { "descriptor", cdac::contracts::EnumerateStaticRegions }, + }; + } + HRESULT STDMETHODCALLTYPE CDacLite::EnumMemoryRegions( ICLRDataEnumMemoryRegionsCallback* callback, ULONG32 miniDumpFlags, CLRDataEnumMemoryFlags clrFlags) { @@ -72,156 +122,44 @@ namespace cdac Log(callback, "EnumMemoryRegions: loaded %zu types, %zu globals, %zu contracts", descriptor.Types().size(), descriptor.Globals().size(), descriptor.Contracts().size()); - // Build the Target (globals are all resolved to absolute values). + // Build the Target (globals are all resolved to absolute values). It carries the dump tier, + // the runtime module base, and the contract-descriptor address so every enumerator has the + // same (Target, sink) signature and can self-select its behavior for the current tier. cdac::Target target(&descriptor, &cdac::ReadFromDataTarget, m_target); - - RegionSinkState sinkState = { this, callback, 0 }; - - // Enable implicit metadata enumeration: every structure the walks read is now captured - // (the cdac-lite analog of the DAC recording each DPTR it dereferences). - target.SetEnumMemSink(&CDacLite::EnumMemThunk, &sinkState); + target.SetContractDescriptorAddr(m_contractDescriptorAddr); + target.SetClrBase(m_clrBase); // Dump tier: HEAP2 (MiniDumpWithPrivateReadWriteMemory) requests the full GC heap + - // heap-side structures. A Normal dump (no HEAP2 flag) needs only what a stack walk - // reaches -- stacks, code, and method metadata -- so the GC heap and the heap-only - // contracts (handles, sync blocks, stress log, COM interop) are skipped. + // heap-side structures. A Normal dump (no HEAP2 flag) needs only what a stack walk reaches + // -- stacks, code, and method metadata -- so the heap-only enumerators return 0. cdac::DumpTier tier = (miniDumpFlags & 0x200 /*MiniDumpWithPrivateReadWriteMemory*/) != 0 ? cdac::DumpTier::Heap : cdac::DumpTier::Normal; target.SetTier(tier); - bool heapTier = (tier == cdac::DumpTier::Heap); - Log(callback, "EnumMemoryRegions: tier = %s", heapTier ? "heap" : "normal (stack walk)"); + Log(callback, "EnumMemoryRegions: tier = %s", + tier == cdac::DumpTier::Heap ? "heap" : "normal (stack walk)"); - if (heapTier) - { - int gcRegions = cdac::contracts::EnumerateGCHeapRegions(target, &CDacLite::RegionSinkThunk, &sinkState); - if (gcRegions < 0) - { - Log(callback, "EnumMemoryRegions: GC walk skipped (GC type unknown)"); - } - else - { - Log(callback, "EnumMemoryRegions: GC walk reported %d region(s)", gcRegions); - } - } - - // Walk the managed threads and report their stack ranges. - int threadRegions = cdac::contracts::EnumerateThreadRegions(target, &CDacLite::RegionSinkThunk, &sinkState); - if (threadRegions < 0) - { - Log(callback, "EnumMemoryRegions: thread walk skipped (ThreadStore not found)"); - } - else - { - Log(callback, "EnumMemoryRegions: thread walk reported %d region(s)", threadRegions); - } - - // Walk loaded modules and capture in-memory symbol streams (file-backed images come from disk). - int moduleRegions = cdac::contracts::EnumerateModuleRegions(target, &CDacLite::RegionSinkThunk, &sinkState); - if (moduleRegions < 0) - { - Log(callback, "EnumMemoryRegions: module walk skipped (AppDomain not found)"); - } - else - { - Log(callback, "EnumMemoryRegions: module walk reported %d region(s)", moduleRegions); - } - - // Walk the GC handle table and report its segments (roots storage). Heap tier only. - if (heapTier) - { - int handleRegions = cdac::contracts::EnumerateHandleRegions(target, &CDacLite::RegionSinkThunk, &sinkState); - if (handleRegions < 0) - { - Log(callback, "EnumMemoryRegions: handle walk skipped (handle table not found)"); - } - else - { - Log(callback, "EnumMemoryRegions: handle walk reported %d region(s)", handleRegions); - } - } - - // Code + method metadata for stack walks. Heap tier bulk-emits every JIT code heap and - // loader heap (fine when the whole heap is captured anyway). Normal tier instead does a - // conservative stack scan, capturing only the code + MethodDesc reachable from pointers on - // the thread stacks -- this scales to large programs where the code/loader heaps are huge. - if (heapTier) - { - int jitRegions = cdac::contracts::EnumerateJitCodeRegions(target, &CDacLite::RegionSinkThunk, &sinkState); - if (jitRegions < 0) - { - Log(callback, "EnumMemoryRegions: JIT walk skipped (EEJitManager not found)"); - } - else - { - Log(callback, "EnumMemoryRegions: JIT walk reported %d region(s)", jitRegions); - } + RegionSinkState sinkState = { this, callback, 0 }; - int loaderHeapRegions = cdac::contracts::EnumerateLoaderHeapRegions(target, &CDacLite::RegionSinkThunk, &sinkState); - if (loaderHeapRegions < 0) - { - Log(callback, "EnumMemoryRegions: loader-heap walk skipped (allocators not found)"); - } - else - { - Log(callback, "EnumMemoryRegions: loader-heap walk reported %d region(s)", loaderHeapRegions); - } - } - else - { - int scannedMethods = cdac::contracts::EnumerateStackScanRegions(target, &CDacLite::RegionSinkThunk, &sinkState); - if (scannedMethods < 0) - { - Log(callback, "EnumMemoryRegions: stack scan skipped (code range map not found)"); - } - else - { - Log(callback, "EnumMemoryRegions: stack scan captured %d method(s)", scannedMethods); - } - } + // Enable implicit metadata enumeration: every structure an enumerator reads via TryRead is + // captured (the cdac-lite analog of the DAC recording each DPTR it dereferences). + target.SetEnumMemSink(&CDacLite::EnumMemThunk, &sinkState); - // Heap-only structures: sync-block table, stress log, and COM interop. A Normal - // (stack-walk) dump does not need these. - if (heapTier) + // Run every enumerator in order. Each decides -- from the Target's tier -- what to emit + // (heap-only enumerators return 0 in a Normal dump); adding a subsystem is one table entry. + for (const MemoryEnumerator& e : s_enumerators) { - int syncBlockRegions = cdac::contracts::EnumerateSyncBlockRegions(target); - if (syncBlockRegions < 0) - { - Log(callback, "EnumMemoryRegions: sync-block walk skipped (sync table not found)"); - } - else - { - Log(callback, "EnumMemoryRegions: sync-block walk captured %d in-use block(s)", syncBlockRegions); - } - - int stressLogChunks = cdac::contracts::EnumerateStressLogRegions(target); - if (stressLogChunks < 0) + int n = e.enumerate(target, &CDacLite::RegionSinkThunk, &sinkState); + if (n < 0) { - Log(callback, "EnumMemoryRegions: stress-log walk skipped (stress log not found)"); + Log(callback, "EnumMemoryRegions: %s enumerator skipped (root not found)", e.name); } else { - Log(callback, "EnumMemoryRegions: stress-log walk captured %d chunk(s)", stressLogChunks); - } - - int interopRegions = cdac::contracts::EnumerateInteropRegions(target); - if (interopRegions < 0) - { - Log(callback, "EnumMemoryRegions: interop walk skipped (RCW cleanup list not found)"); - } - else - { - Log(callback, "EnumMemoryRegions: interop walk captured %d RCW(s)", interopRegions); + Log(callback, "EnumMemoryRegions: %s enumerator reported %d region(s)", e.name, n); } } - // Report the contract self-description (descriptor + JSON + pointer_data + the global - // storage it references) so a contract tool can bootstrap from the dump alone. cdac-lite - // is a DAC replacement, so no DAC globals table. - int staticRegions = cdac::contracts::EnumerateStaticRegions(target, m_contractDescriptorAddr, - &CDacLite::RegionSinkThunk, &sinkState); - Log(callback, "EnumMemoryRegions: contract-bootstrap reported %d region(s)", staticRegions); - return S_OK; } } diff --git a/src/coreclr/debug/createdump/createdumpwindows.cpp b/src/coreclr/debug/createdump/createdumpwindows.cpp index d81fbb09173e73..42c473ba4ef995 100644 --- a/src/coreclr/debug/createdump/createdumpwindows.cpp +++ b/src/coreclr/debug/createdump/createdumpwindows.cpp @@ -302,13 +302,14 @@ TryCreateDumpWithCdacLite(HANDLE hProcess, DWORD pid, HANDLE hFile, bool heapTie // (MiniDumpWithPrivateReadWriteMemory) to capture the GC/loader/handle heaps -- object bytes // included -- the same way the DAC path relies on that sweep. cdac-lite's memory callback adds // the memory the sweep misses (executable JIT/stub RX pages, image-backed contract descriptor). - // Normal tier: MiniDumpNormal (stacks + module headers); cdac-lite supplies the stack-walk - // code + method metadata via the callback, no R/W heap sweep. - // (MiniDumpWithoutAuxiliaryState is intentionally NOT set: it breaks ClrMD's module export - // directory lookup for the contract descriptor. dbghelp does not load the legacy DAC anyway.) + // Normal tier: MiniDumpNormal + MiniDumpWithoutAuxiliaryState. The latter stops dbghelp from + // loading the legacy DAC (mscordaccore) as an auxiliary provider -- otherwise, when the DAC is + // present, dbghelp runs the DAC's OWN enumeration and its memory would mask whether cdac-lite + // is self-sufficient (and adds ~350ms). cdac-lite explicitly emits the module export directory + // (EmitModuleExportDirectory) that the DAC would otherwise have supplied for descriptor lookup. MINIDUMP_TYPE dumpType = heapTier ? (MINIDUMP_TYPE)(MiniDumpNormal | MiniDumpWithPrivateReadWriteMemory) - : MiniDumpNormal; + : (MINIDUMP_TYPE)(MiniDumpNormal | MiniDumpWithoutAuxiliaryState); CdacMemoryCallbackState state = { &collector.m_regions, 0 }; MINIDUMP_CALLBACK_INFORMATION callbackInfo = {}; diff --git a/src/native/managed/cdac/scripts/DumpHelpers.cs b/src/native/managed/cdac/scripts/DumpHelpers.cs index 3eb926ba5cad7c..afaf876ab916c1 100644 --- a/src/native/managed/cdac/scripts/DumpHelpers.cs +++ b/src/native/managed/cdac/scripts/DumpHelpers.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Reflection.PortableExecutable; using Microsoft.Diagnostics.DataContractReader; using Microsoft.Diagnostics.DataContractReader.Contracts; using Microsoft.Diagnostics.Runtime; @@ -50,7 +51,7 @@ public static ContractDescriptorTarget CreateCdacTarget(DataTarget dt) if (!ContractDescriptorTarget.TryCreate( contractAddr, - (ulong address, Span buffer) => dt.DataReader.Read(address, buffer) == buffer.Length ? 0 : -1, + (ulong address, Span buffer) => ReadWithImageFallback(dt, address, buffer), (ulong address, Span buffer) => -1, (uint threadId, uint contextFlags, Span buffer) => dt.DataReader.GetThreadContext(threadId, contextFlags, buffer) ? 0 : -1, @@ -64,4 +65,109 @@ public static ContractDescriptorTarget CreateCdacTarget(DataTarget dt) return target!; } + + /// + /// Reads memory from the dump, falling back to the on-disk PE image when the dump + /// does not contain the requested bytes. Minidumps (and the DAC's Normal dumps) omit + /// most module content — R2R code/metadata in particular — so the reader must re-read + /// it from the module file recorded in the dump. Returns 0 on success, -1 on failure. + /// + private static int ReadWithImageFallback(DataTarget dt, ulong address, Span buffer) + { + try + { + int bytesRead = dt.DataReader.Read(address, buffer); + if (bytesRead == buffer.Length) + return 0; + + ModuleInfo? info = GetModuleForAddress(dt, address); + if (info?.FileName is null) + return -1; + + string? foundFile = FindFileOnDisk(info.FileName); + if (foundFile is null) + return -1; + + using FileStream fs = File.OpenRead(foundFile); + using PEReader peReader = new(fs); + + int sizeOfHeaders = peReader.PEHeaders.PEHeader?.SizeOfHeaders ?? 0; + PEMemoryBlock wholeImage = default; + bool wholeImageLoaded = false; + + int filled = bytesRead; + ulong current = address + (ulong)bytesRead; + while (filled < buffer.Length) + { + int rva = (int)(current - info.ImageBase); + PEMemoryBlock block = peReader.GetSectionData(rva); + if (block.Length > 0) + { + int toCopy = Math.Min(block.Length, buffer.Length - filled); + unsafe + { + new ReadOnlySpan(block.Pointer, toCopy).CopyTo(buffer.Slice(filled)); + } + filled += toCopy; + current += (ulong)toCopy; + } + else if (rva >= 0 && rva < sizeOfHeaders) + { + // PE header region (before the first section): GetSectionData doesn't cover it. + // For a loaded image the headers sit at file offset == RVA, so serve them from + // the raw file image. Needed to read a module's PE/COR headers to locate ECMA + // metadata when the headers aren't captured in the dump. + if (!wholeImageLoaded) + { + wholeImage = peReader.GetEntireImage(); + wholeImageLoaded = true; + } + int available = Math.Min(sizeOfHeaders, wholeImage.Length) - rva; + int toCopy = Math.Min(available, buffer.Length - filled); + if (toCopy <= 0) + return -1; + unsafe + { + new ReadOnlySpan(wholeImage.Pointer + rva, toCopy).CopyTo(buffer.Slice(filled)); + } + filled += toCopy; + current += (ulong)toCopy; + } + else + { + return -1; + } + } + + return 0; + } + catch + { + return -1; + } + } + + private static ModuleInfo? GetModuleForAddress(DataTarget dt, ulong address) + { + foreach (ModuleInfo module in dt.DataReader.EnumerateModules()) + { + if (address >= module.ImageBase && address < module.ImageBase + (ulong)module.ImageSize) + return module; + } + + return null; + } + + private static string? FindFileOnDisk(string modulePath) + { + // For local runs the path recorded in the dump usually still exists on disk. + if (File.Exists(modulePath)) + return modulePath; + + // Otherwise look next to the executing tool (module file copied alongside). + int lastSep = Math.Max(modulePath.LastIndexOf('/'), modulePath.LastIndexOf('\\')); + string fileName = lastSep >= 0 ? modulePath[(lastSep + 1)..] : modulePath; + string candidate = Path.Combine(AppContext.BaseDirectory, fileName); + return File.Exists(candidate) ? candidate : null; + } } diff --git a/src/native/managed/cdac/tests/DumpTests/DumpTests.targets b/src/native/managed/cdac/tests/DumpTests/DumpTests.targets index da6cb50e7ace04..329e1644d7b18d 100644 --- a/src/native/managed/cdac/tests/DumpTests/DumpTests.targets +++ b/src/native/managed/cdac/tests/DumpTests/DumpTests.targets @@ -64,6 +64,27 @@ <_DebuggeePublishProps Condition="'$(BuildArchitecture)' != ''">$(_DebuggeePublishProps) /p:BuildArchitecture=$(BuildArchitecture) + + + false + <_CdacLiteConfig>$(_DebuggeeRuntimeConfig) + <_CdacLiteConfig Condition="'$(_CdacLiteConfig)' == ''">Release + <_CdacLiteArch>$(TargetArchitecture) + <_CdacLiteArch Condition="'$(_CdacLiteArch)' == ''">x64 + $([MSBuild]::NormalizePath('$(RepoRoot)', 'artifacts', 'bin', 'coreclr', '$(HostOS).$(_CdacLiteArch).$(_CdacLiteConfig)', 'cdaclite.dll')) + + <_CdacLiteEnvVars Condition="'$(UseCdacLite)' == 'true'">;DOTNET_DbgUseCdacLite=1;DOTNET_DbgCdacLitePath=$(CdacLitePath) + + + <_TestHostConfig Condition="'$(TestHostConfiguration)' == ''">Release @@ -303,7 +324,7 @@ + EnvironmentVariables="DOTNET_DbgEnableMiniDump=1;DOTNET_DbgMiniDumpType=$(_MiniDumpType);DOTNET_DbgMiniDumpName=$(_DumpFile);DOTNET_ReadyToRun=$(_R2RValue);$([MSBuild]::Unescape('$(_DebuggeeEnvVars)'))$(_CdacLiteEnvVars)" /> diff --git a/src/native/managed/cdac/tests/DumpTests/MiniDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/MiniDumpTests.cs index 343ae7db94da3e..a7db205c42cbb3 100644 --- a/src/native/managed/cdac/tests/DumpTests/MiniDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/MiniDumpTests.cs @@ -4,8 +4,10 @@ using System.Collections.Generic; using System.Linq; using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.DataContractReader.Legacy; using Microsoft.Diagnostics.DataContractReader.TestInfrastructure; using Xunit; +using static Microsoft.Diagnostics.DataContractReader.TestInfrastructure.TestHelpers; namespace Microsoft.Diagnostics.DataContractReader.DumpTests; @@ -204,3 +206,183 @@ public void ThreadStoreData_HasFinalizerThread(TestConfiguration config) Assert.NotEqual(TargetPointer.Null, storeData.FinalizerThread); } } + +/// +/// Exercises the APIs that back the SOS commands supported on a +/// normal minidump, driving them through the same the shipping cDAC +/// exposes to SOS. Each test maps to a command and validates a cdac-lite normal dump captures +/// enough memory for it: +/// +/// clrthreads → GetThreadStoreData / GetThreadData +/// clrstack / dumpmd → GetMethodDescData / GetMethodDescName +/// ip2md / u → GetMethodDescPtrFromIP / GetCodeHeaderData (the latter decodes +/// the method's GC info for MethodSize, which the scan emits alongside the code) +/// dumpmt → GetMethodTableData / GetMethodTableName +/// dumpmodule → GetModuleData +/// dumpdomain → GetAppDomainStoreData / GetAppDomainList / GetAppDomainData +/// (the domain object, global LoaderAllocator heaps, and the assembly/module list) +/// +/// Out of scope for a stack-scoped mini dump (backing memory absent): all heap-dependent commands +/// (dumpheap, dumpobj, gcroot, object locals). +/// +public class MiniDumpSosDacTests : DumpTestBase +{ + protected override string DebuggeeName => "StackWalk"; + protected override string DumpType => "mini"; + + // Returns an app frame (JIT'd, frameless) resolved to a MethodDesc on the crashing thread. + private ResolvedFrame FindAppFrame(string methodName) + { + ThreadData crashingThread = DumpTestHelpers.FindFailFastThread(Target); + return DumpTestStackWalker.Walk(Target, crashingThread).Frames + .First(f => f.Name == methodName && f.MethodDescPtr != TargetPointer.Null); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "InlinedCallFrame.Datum was added after net10.0")] + public unsafe void ClrThreads_GetThreadStoreAndThreadData(TestConfiguration config) + { + InitializeDumpTest(config); + ISOSDacInterface sosDac = new SOSDacImpl(Target, legacyObj: null); + IThread threadContract = Target.Contracts.Thread; + + DacpThreadStoreData tsData; + AssertHResult(System.HResults.S_OK, sosDac.GetThreadStoreData(&tsData)); + Assert.True(tsData.threadCount > 0, $"Expected thread count > 0, got {tsData.threadCount}"); + Assert.NotEqual(0ul, tsData.firstThread.Value); + + // Every thread record the thread store references must be readable (clrthreads walks them). + int walked = 0; + TargetPointer thread = threadContract.GetThreadStoreData().FirstThread; + while (thread != TargetPointer.Null) + { + DacpThreadData tdata; + AssertHResult(System.HResults.S_OK, sosDac.GetThreadData(thread.ToClrDataAddress(Target), &tdata)); + walked++; + thread = threadContract.GetThreadData(thread).NextThread; + } + Assert.Equal(tsData.threadCount, walked); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "InlinedCallFrame.Datum was added after net10.0")] + public unsafe void ClrStack_GetMethodDescDataAndName(TestConfiguration config) + { + InitializeDumpTest(config); + ISOSDacInterface sosDac = new SOSDacImpl(Target, legacyObj: null); + + ResolvedFrame frame = FindAppFrame("MethodC"); + ClrDataAddress md = frame.MethodDescPtr.ToClrDataAddress(Target); + + DacpMethodDescData mdData; + uint rejitNeeded; + AssertHResult(System.HResults.S_OK, sosDac.GetMethodDescData(md, default, &mdData, 0, null, &rejitNeeded)); + Assert.Equal(md.Value, mdData.MethodDescPtr.Value); + Assert.NotEqual(0ul, mdData.MethodTablePtr.Value); + + char* name = stackalloc char[512]; + uint nameNeeded; + AssertHResult(System.HResults.S_OK, sosDac.GetMethodDescName(md, 512, name, &nameNeeded)); + string methodName = new string(name); + Assert.Contains("MethodC", methodName); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "InlinedCallFrame.Datum was added after net10.0")] + public unsafe void Ip2Md_GetMethodDescPtrFromIPAndCodeHeaderData(TestConfiguration config) + { + InitializeDumpTest(config); + ISOSDacInterface sosDac = new SOSDacImpl(Target, legacyObj: null); + IStackWalk stackWalk = Target.Contracts.StackWalk; + + ResolvedFrame frame = FindAppFrame("MethodC"); + TargetCodePointer ip = stackWalk.GetInstructionPointer(frame.FrameHandle); + Assert.NotEqual(TargetCodePointer.Null, ip); + ClrDataAddress ipAddr = ip.ToClrDataAddress(Target); + + ClrDataAddress mdFromIp; + AssertHResult(System.HResults.S_OK, sosDac.GetMethodDescPtrFromIP(ipAddr, &mdFromIp)); + Assert.Equal(frame.MethodDescPtr.ToClrDataAddress(Target).Value, mdFromIp.Value); + + DacpCodeHeaderData chData; + AssertHResult(System.HResults.S_OK, sosDac.GetCodeHeaderData(ipAddr, &chData)); + Assert.Equal(frame.MethodDescPtr.ToClrDataAddress(Target).Value, chData.MethodDescPtr.Value); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "InlinedCallFrame.Datum was added after net10.0")] + public unsafe void DumpMt_GetMethodTableDataAndName(TestConfiguration config) + { + InitializeDumpTest(config); + ISOSDacInterface sosDac = new SOSDacImpl(Target, legacyObj: null); + IRuntimeTypeSystem rts = Target.Contracts.RuntimeTypeSystem; + + ResolvedFrame frame = FindAppFrame("MethodC"); + TargetPointer mt = rts.GetMethodTable(rts.GetMethodDescHandle(frame.MethodDescPtr)); + ClrDataAddress mtAddr = mt.ToClrDataAddress(Target); + + DacpMethodTableData mtData; + AssertHResult(System.HResults.S_OK, sosDac.GetMethodTableData(mtAddr, &mtData)); + Assert.NotEqual(0ul, mtData.module.Value); + + char* mtName = stackalloc char[512]; + uint nameNeeded; + AssertHResult(System.HResults.S_OK, sosDac.GetMethodTableName(mtAddr, 512, mtName, &nameNeeded)); + Assert.False(string.IsNullOrEmpty(new string(mtName)), "Expected a non-empty MethodTable name"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "InlinedCallFrame.Datum was added after net10.0")] + public unsafe void DumpModule_GetModuleData(TestConfiguration config) + { + InitializeDumpTest(config); + ISOSDacInterface sosDac = new SOSDacImpl(Target, legacyObj: null); + IRuntimeTypeSystem rts = Target.Contracts.RuntimeTypeSystem; + + ResolvedFrame frame = FindAppFrame("MethodC"); + TargetPointer mt = rts.GetMethodTable(rts.GetMethodDescHandle(frame.MethodDescPtr)); + + DacpMethodTableData mtData; + AssertHResult(System.HResults.S_OK, sosDac.GetMethodTableData(mt.ToClrDataAddress(Target), &mtData)); + + DacpModuleData modData; + AssertHResult(System.HResults.S_OK, sosDac.GetModuleData(mtData.module, &modData)); + Assert.Equal(mtData.module.Value, modData.Address.Value); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "InlinedCallFrame.Datum was added after net10.0")] + public unsafe void DumpDomain_GetAppDomainStoreListAndData(TestConfiguration config) + { + InitializeDumpTest(config); + ISOSDacInterface sosDac = new SOSDacImpl(Target, legacyObj: null); + + // The cDAC intentionally reports sharedDomain/systemDomain as 0 (deprecated concepts); the + // real dumpdomain flow enumerates the app domain via GetAppDomainList + GetAppDomainData. + DacpAppDomainStoreData adStore; + AssertHResult(System.HResults.S_OK, sosDac.GetAppDomainStoreData(&adStore)); + Assert.True(adStore.DomainCount >= 1, $"Expected at least one app domain, got {adStore.DomainCount}"); + + ClrDataAddress[] domains = new ClrDataAddress[adStore.DomainCount]; + uint needed; + AssertHResult(System.HResults.S_OK, sosDac.GetAppDomainList((uint)adStore.DomainCount, domains, &needed)); + Assert.True(needed >= 1, $"Expected GetAppDomainList to return at least one domain, got {needed}"); + + ClrDataAddress domain = domains[0]; + Assert.NotEqual(0ul, domain.Value); + + // Full GetAppDomainData: reads the domain object, the global LoaderAllocator's loader heaps, + // and enumerates the domain's assembly/module list (AssemblyCount). cdac-lite emits the + // LoaderAllocator + each ArrayList block's assembly-pointer array so this succeeds. + DacpAppDomainData adData; + AssertHResult(System.HResults.S_OK, sosDac.GetAppDomainData(domain, &adData)); + Assert.Equal(domain.Value, adData.AppDomainPtr.Value); + Assert.True(adData.AssemblyCount >= 1, $"Expected at least one assembly, got {adData.AssemblyCount}"); + } +} diff --git a/src/native/managed/cdac/tests/DumpTests/RunDumpTests.ps1 b/src/native/managed/cdac/tests/DumpTests/RunDumpTests.ps1 index 2783f448be2b28..d37958e08619fd 100644 --- a/src/native/managed/cdac/tests/DumpTests/RunDumpTests.ps1 +++ b/src/native/managed/cdac/tests/DumpTests/RunDumpTests.ps1 @@ -60,6 +60,14 @@ 'reportgenerator' to be restored (dotnet tool restore). The report is written to artifacts\coverage\cdac-dump-tests\. +.PARAMETER UseCdacLite + When set, generates the crash dumps with cdac-lite selecting the managed memory + instead of the legacy DAC (createdump honors DOTNET_DbgUseCdacLite and loads the + cdac-lite DLL from the coreclr build output). The cDAC DumpTests then run unchanged + against the cdac-lite-generated dumps. Windows only. Use a clean dump directory + (delete artifacts\dumps\cdac or pass -Force) when switching between DAC and cdac-lite + modes, since existing dumps are reused as-is. + .PARAMETER VerboseOutput When set, increases MSBuild and dotnet verbosity to normal (default: minimal/quiet). @@ -109,6 +117,8 @@ param( [switch]$Coverage, + [switch]$UseCdacLite, + [switch]$VerboseOutput ) @@ -340,6 +350,11 @@ if ($Action -in @("dumps", "all")) { $msbuildArgs += "/p:SetDisableAuxProviderSignatureCheck=true" } + if ($UseCdacLite) { + # Generate dumps with cdac-lite (instead of the legacy DAC) selecting the managed memory. + $msbuildArgs += "/p:UseCdacLite=true" + } + & $dotnet @msbuildArgs 2>&1 | ForEach-Object { Write-Host " $_" } if ($LASTEXITCODE -ne 0) { Write-Host "" diff --git a/src/native/managed/cdac/tests/DumpTests/cdac-dump-helix.proj b/src/native/managed/cdac/tests/DumpTests/cdac-dump-helix.proj index b20906b5449610..1832370278c0f7 100644 --- a/src/native/managed/cdac/tests/DumpTests/cdac-dump-helix.proj +++ b/src/native/managed/cdac/tests/DumpTests/cdac-dump-helix.proj @@ -131,6 +131,10 @@ + + diff --git a/src/native/managed/cdac/tests/TestInfrastructure/ClrMdDumpHost.cs b/src/native/managed/cdac/tests/TestInfrastructure/ClrMdDumpHost.cs index 341a4b2a8c8a20..594193295fcdd0 100644 --- a/src/native/managed/cdac/tests/TestInfrastructure/ClrMdDumpHost.cs +++ b/src/native/managed/cdac/tests/TestInfrastructure/ClrMdDumpHost.cs @@ -77,23 +77,52 @@ public int ReadFromTarget(ulong address, Span buffer) using FileStream fs = File.OpenRead(foundFile); using PEReader peReader = new PEReader(fs); + int sizeOfHeaders = peReader.PEHeaders.PEHeader?.SizeOfHeaders ?? 0; + PEMemoryBlock wholeImage = default; + bool wholeImageLoaded = false; + int filled = bytesRead; ulong current = address + (ulong)bytesRead; while (filled < buffer.Length) { - PEMemoryBlock block = peReader.GetSectionData((int)(current - info.ImageBase)); - if (block.Length == 0) + int rva = (int)(current - info.ImageBase); + PEMemoryBlock block = peReader.GetSectionData(rva); + if (block.Length > 0) { - return -1; + int toCopy = Math.Min(block.Length, buffer.Length - filled); + unsafe + { + new ReadOnlySpan(block.Pointer, toCopy).CopyTo(buffer.Slice(filled)); + } + filled += toCopy; + current += (ulong)toCopy; } - - int toCopy = Math.Min(block.Length, buffer.Length - filled); - unsafe + else if (rva >= 0 && rva < sizeOfHeaders) { - new ReadOnlySpan(block.Pointer, toCopy).CopyTo(buffer.Slice(filled)); + // PE header region (before the first section): GetSectionData doesn't cover it. + // For a loaded image the headers sit at file offset == RVA, so serve them from + // the raw file image (needed to read a module's PE/COR headers when the dump + // doesn't capture them, e.g. cdac-lite Normal dumps without the legacy DAC). + if (!wholeImageLoaded) + { + wholeImage = peReader.GetEntireImage(); + wholeImageLoaded = true; + } + int available = Math.Min(sizeOfHeaders, wholeImage.Length) - rva; + int toCopy = Math.Min(available, buffer.Length - filled); + if (toCopy <= 0) + return -1; + unsafe + { + new ReadOnlySpan(wholeImage.Pointer + rva, toCopy).CopyTo(buffer.Slice(filled)); + } + filled += toCopy; + current += (ulong)toCopy; + } + else + { + return -1; } - filled += toCopy; - current += (ulong)toCopy; } return 0; From 4a60ef6f653210d216e96b1f39cadf45a1ab3f73 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Tue, 7 Jul 2026 08:55:58 -0400 Subject: [PATCH 03/13] [prototype] cdac-lite: fix cross-platform build (x86 wmain __cdecl, macOS -dead_strip) CI runtime-diagnostics (build 1496310) surfaced two prototype build breaks on platforms I hadn't built locally (windows-x64 + linux were fine): - windows-x86: cdaclitetest.cpp(588) 'wmain must be __cdecl' (C4007) -- x86 enforces the calling convention on wmain; add __cdecl. - macOS: 'ld: unknown options: --gc-sections' -- Apple's ld doesn't support --gc-sections; use -dead_strip on APPLE, keep --gc-sections on other Unix. The windows-x64 CdacDumpTest and windows-arm64 CdacDumpTest legs PASSED with cdacUseCdacLite=true, so cdac-lite dump generation + validation works in CI on Windows. (The windows-x64 AllSubsets failure is the known 'VSIX Auto Update' infra error -- it fails identically in the DAC baseline run and has no compile error.) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/debug/cdaclite/CMakeLists.txt | 4 ++++ src/coreclr/debug/cdaclite/cdaclitetest.cpp | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/coreclr/debug/cdaclite/CMakeLists.txt b/src/coreclr/debug/cdaclite/CMakeLists.txt index 64d1f153cffcab..35050690fd9b07 100644 --- a/src/coreclr/debug/cdaclite/CMakeLists.txt +++ b/src/coreclr/debug/cdaclite/CMakeLists.txt @@ -67,6 +67,10 @@ target_link_libraries(cdaclite PRIVATE ${CDACLITE_LIBRARIES}) if(CLR_CMAKE_HOST_WIN32) set(CDACLITE_SIZE_COMPILE_OPTS /O1 /Gw /Gy) set(CDACLITE_SIZE_LINK_OPTS "/OPT:REF /OPT:ICF") +elseif(CLR_CMAKE_HOST_APPLE) + # Apple's ld does not understand --gc-sections; the equivalent is -dead_strip. + set(CDACLITE_SIZE_COMPILE_OPTS -Os -ffunction-sections -fdata-sections) + set(CDACLITE_SIZE_LINK_OPTS "-Wl,-dead_strip") else() set(CDACLITE_SIZE_COMPILE_OPTS -Os -ffunction-sections -fdata-sections) set(CDACLITE_SIZE_LINK_OPTS "-Wl,--gc-sections") diff --git a/src/coreclr/debug/cdaclite/cdaclitetest.cpp b/src/coreclr/debug/cdaclite/cdaclitetest.cpp index a27dc9f2575cd7..2b5d0c2393050e 100644 --- a/src/coreclr/debug/cdaclite/cdaclitetest.cpp +++ b/src/coreclr/debug/cdaclite/cdaclitetest.cpp @@ -585,7 +585,7 @@ static int RunDump(PFN_CLRDataCreateInstance pfnCreate, LiveDataTarget* target, return (regionsOk && dacFree) ? 0 : 7; } -int wmain(int argc, wchar_t** argv) +int __cdecl wmain(int argc, wchar_t** argv) { if (argc < 2) { From 8e98399c982b724edecf1417dbe2a733ce2b1df5 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Tue, 7 Jul 2026 09:47:01 -0400 Subject: [PATCH 04/13] [prototype] cdac-lite: drop the custom size-optimization flags Removes the per-platform CDACLITE_SIZE_* block. It only bought size-over-speed (/O1, -Os) + /OPT:ICF folding -- negligible for a small component and not important yet -- while dead-code stripping (/OPT:REF, --gc-sections, -dead_strip) is already applied per-platform by configurecompiler.cmake. Removing it also eliminates the hand-rolled platform flags that broke the macOS build. cdac-lite now just uses coreclr's default Release optimization. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/debug/cdaclite/CMakeLists.txt | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/coreclr/debug/cdaclite/CMakeLists.txt b/src/coreclr/debug/cdaclite/CMakeLists.txt index 35050690fd9b07..ea45f0bb2eca46 100644 --- a/src/coreclr/debug/cdaclite/CMakeLists.txt +++ b/src/coreclr/debug/cdaclite/CMakeLists.txt @@ -61,26 +61,6 @@ endif(CLR_CMAKE_HOST_WIN32) target_link_libraries(cdaclite PRIVATE ${CDACLITE_LIBRARIES}) -# Optimize cdac-lite for binary size: the whole point of cdac-lite is to be a small, -# standalone native package. Favor size over speed for the cdac-lite-specific targets -# (the shared coreclr static libs it links keep their default optimization). -if(CLR_CMAKE_HOST_WIN32) - set(CDACLITE_SIZE_COMPILE_OPTS /O1 /Gw /Gy) - set(CDACLITE_SIZE_LINK_OPTS "/OPT:REF /OPT:ICF") -elseif(CLR_CMAKE_HOST_APPLE) - # Apple's ld does not understand --gc-sections; the equivalent is -dead_strip. - set(CDACLITE_SIZE_COMPILE_OPTS -Os -ffunction-sections -fdata-sections) - set(CDACLITE_SIZE_LINK_OPTS "-Wl,-dead_strip") -else() - set(CDACLITE_SIZE_COMPILE_OPTS -Os -ffunction-sections -fdata-sections) - set(CDACLITE_SIZE_LINK_OPTS "-Wl,--gc-sections") -endif() - -foreach(_cdaclite_target cdaclite cdacgc cdacdata cdacjson) - target_compile_options(${_cdaclite_target} PRIVATE ${CDACLITE_SIZE_COMPILE_OPTS}) -endforeach() -set_property(TARGET cdaclite APPEND_STRING PROPERTY LINK_FLAGS " ${CDACLITE_SIZE_LINK_OPTS}") - install_clr(TARGETS cdaclite DESTINATIONS . sharedFramework COMPONENT debug) # Standalone test harness (Windows-only): drives cdaclite against a live process. From 4a3196069a87f1a604c34cbb3a9038e5bf5bd064 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Tue, 7 Jul 2026 12:09:57 -0400 Subject: [PATCH 05/13] cdac-lite: add cdaclite to the shared framework platform manifest cdaclite is installed into the CoreCLR runtime pack (install_clr ... DESTINATIONS sharedFramework COMPONENT debug), so the shared-framework manifest validation (sharedfx.targets) requires a PlatformManifestFileEntry for it, the same way mscordaccore/mscordbi are listed. Without it the product build (SOSTests / AllSubsets_CoreCLR legs) fails with 'files are missing entries in the templated manifest: cdaclite.dll'. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../pkg/sfx/Microsoft.NETCore.App/Directory.Build.props | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/installer/pkg/sfx/Microsoft.NETCore.App/Directory.Build.props b/src/installer/pkg/sfx/Microsoft.NETCore.App/Directory.Build.props index bd717001f5e476..9055a34e62de0a 100644 --- a/src/installer/pkg/sfx/Microsoft.NETCore.App/Directory.Build.props +++ b/src/installer/pkg/sfx/Microsoft.NETCore.App/Directory.Build.props @@ -132,6 +132,9 @@ + + + From 00f1b402542f84fbdee42f312747ad40598127ac Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Tue, 7 Jul 2026 13:43:26 -0400 Subject: [PATCH 06/13] cdac-lite: embed FX version resource so shared framework validation passes After adding cdaclite.dll to the platform manifest, sharedfx.targets(519) additionally requires each Windows shared-framework file to carry a FileVersion resource ('Missing FileVersion in 1 shared framework files: cdaclite.dll'). Add a Native.rc (FX_VER_FILEDESCRIPTION_STR + fxver.h/fxver.rc) and, on Windows, define FX_VER_INTERNALNAME_STR=cdaclite.dll and compile Native.rc into the DLL -- the same pattern mscordaccore/mscordbi use. Verified locally: cdaclite.dll now reports FileVersion 42.42.42.42424. The resource is Windows-only; Unix .so/.dylib ship without one, matching the peer debug components. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/debug/cdaclite/CMakeLists.txt | 6 ++++++ src/coreclr/debug/cdaclite/Native.rc | 7 +++++++ 2 files changed, 13 insertions(+) create mode 100644 src/coreclr/debug/cdaclite/Native.rc diff --git a/src/coreclr/debug/cdaclite/CMakeLists.txt b/src/coreclr/debug/cdaclite/CMakeLists.txt index ea45f0bb2eca46..8cd119cef026b1 100644 --- a/src/coreclr/debug/cdaclite/CMakeLists.txt +++ b/src/coreclr/debug/cdaclite/CMakeLists.txt @@ -14,6 +14,12 @@ set(CDACLITE_SOURCES ) if(CLR_CMAKE_HOST_WIN32) + add_definitions(-DFX_VER_INTERNALNAME_STR=cdaclite.dll) + + # Embed the FX version resource so the shared framework manifest validation + # (sharedfx.targets) finds a FileVersion, matching mscordaccore/mscordbi. + list(APPEND CDACLITE_SOURCES Native.rc) + set(DEF_SOURCES cdaclite.src) set(CURRENT_BINARY_DIR_FOR_CONFIG ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}) diff --git a/src/coreclr/debug/cdaclite/Native.rc b/src/coreclr/debug/cdaclite/Native.rc new file mode 100644 index 00000000000000..e96d9225bfeae0 --- /dev/null +++ b/src/coreclr/debug/cdaclite/Native.rc @@ -0,0 +1,7 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#define FX_VER_FILEDESCRIPTION_STR ".NET Runtime cDAC Lite Memory Enumeration" + +#include +#include From e68d2e5864714288228eaf9787117682128e7a3a Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Tue, 7 Jul 2026 22:00:03 -0400 Subject: [PATCH 07/13] cdac-lite: enable createdump cdac-lite path on Linux/macOS The createdump cdac-lite integration previously existed only on Windows (createdumpwindows.cpp), so the CI runtime-diagnostics runs exercised cdac-lite dump collection on Windows only. Wire it into the Unix createdump so the Linux/macOS CdacDumpTest legs exercise it too and can uncover cross-platform bugs. - crashinfo.cpp InitializeDAC: when DOTNET_DbgUseCdacLite=1 (non-full dump), load libcdaclite.so/.dylib (from DOTNET_DbgCdacLitePath or next to coreclr) instead of mscordaccore and create ICLRDataEnumMemoryRegions from it. cdac-lite does not implement IXCLRDataProcess, so it is left null; the two consumers (EnumerateManagedModules, UnwindAllThreads) are already null-guarded. - prepare-cdac-helix-steps.yml: copy the platform cdac-lite library (cdaclite.dll / libcdaclite.so / libcdaclite.dylib) into the testhost payload on all platforms, not just Windows. - cdac-dump-helix.proj: add the Unix 'export DOTNET_DbgUseCdacLite=1' pre-command. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../cdac/prepare-cdac-helix-steps.yml | 29 ++++++----- src/coreclr/debug/createdump/crashinfo.cpp | 51 ++++++++++++++++--- .../cdac/tests/DumpTests/cdac-dump-helix.proj | 6 ++- 3 files changed, 64 insertions(+), 22 deletions(-) diff --git a/eng/pipelines/cdac/prepare-cdac-helix-steps.yml b/eng/pipelines/cdac/prepare-cdac-helix-steps.yml index 9d293788cefe74..09f97d91d1b14f 100644 --- a/eng/pipelines/cdac/prepare-cdac-helix-steps.yml +++ b/eng/pipelines/cdac/prepare-cdac-helix-steps.yml @@ -7,9 +7,9 @@ parameters: buildDebuggees: true skipDebuggeeCopy: false runtimeConfiguration: 'Checked' - # When true, copy cdaclite.dll into the testhost payload (next to coreclr.dll) so createdump can - # select managed memory with cdac-lite instead of the legacy DAC. Paired with the - # DOTNET_DbgUseCdacLite pre-command in cdac-dump-helix.proj. Windows-only (no-op elsewhere). + # When true, copy the cdac-lite native library into the testhost payload (next to coreclr) so + # createdump can select managed memory with cdac-lite instead of the legacy DAC. Paired with the + # DOTNET_DbgUseCdacLite pre-command in cdac-dump-helix.proj. Supported on Windows, Linux, and macOS. useCdacLite: false # Configuration the cDAC managed assemblies are built at during payload # prep. The dump tests load the managed cDAC directly via ProjectReference, @@ -75,20 +75,21 @@ steps: displayName: 'Find TestHost Directory and Helix Queue' - ${{ if parameters.useCdacLite }}: - # cdac-lite mode: place cdaclite.dll next to the testhost's coreclr.dll so createdump (which runs - # on the Helix machine when a debuggee crashes) loads it and selects the managed memory instead of - # the legacy DAC. Windows-only -- the createdump cdac-lite path is Windows-only. + # cdac-lite mode: place the cdac-lite native library next to the testhost's coreclr so createdump + # (which runs on the Helix machine when a debuggee crashes) loads it and selects the managed memory + # instead of the legacy DAC. Supported on Windows, Linux, and macOS. - pwsh: | - if ("$(osGroup)" -ne "windows") { - Write-Host "cdac-lite mode is Windows-only; skipping cdaclite.dll copy for $(osGroup)." - exit 0 + $names = switch ("$(osGroup)") { + "windows" { @{ coreclr = "coreclr.dll"; cdaclite = "cdaclite.dll" } } + "osx" { @{ coreclr = "libcoreclr.dylib"; cdaclite = "libcdaclite.dylib" } } + default { @{ coreclr = "libcoreclr.so"; cdaclite = "libcdaclite.so" } } } $coreclr = Get-ChildItem -Recurse -ErrorAction SilentlyContinue ` -Path "$(Build.SourcesDirectory)/artifacts/bin/testhost/net*-$(osGroup)-*-$(archType)/shared/Microsoft.NETCore.App" ` - -Filter "coreclr.dll" | Select-Object -First 1 - if (-not $coreclr) { Write-Error "coreclr.dll not found in the testhost shared framework."; exit 1 } - $src = "$(Build.SourcesDirectory)/artifacts/bin/coreclr/$(osGroup).$(archType).${{ parameters.runtimeConfiguration }}/cdaclite.dll" - if (-not (Test-Path $src)) { Write-Error "cdaclite.dll not found at $src (was it built by the CdacBuild -s clr subset?)."; exit 1 } + -Filter $names.coreclr | Select-Object -First 1 + if (-not $coreclr) { Write-Error "$($names.coreclr) not found in the testhost shared framework."; exit 1 } + $src = "$(Build.SourcesDirectory)/artifacts/bin/coreclr/$(osGroup).$(archType).${{ parameters.runtimeConfiguration }}/$($names.cdaclite)" + if (-not (Test-Path $src)) { Write-Error "$($names.cdaclite) not found at $src (was it built by the CdacBuild -s clr subset?)."; exit 1 } Copy-Item $src $coreclr.DirectoryName -Force - Write-Host "Copied cdaclite.dll into $($coreclr.DirectoryName)" + Write-Host "Copied $($names.cdaclite) into $($coreclr.DirectoryName)" displayName: 'Copy cdac-lite into TestHost payload' diff --git a/src/coreclr/debug/createdump/crashinfo.cpp b/src/coreclr/debug/createdump/crashinfo.cpp index a34b5924ee741b..a304706bae941b 100644 --- a/src/coreclr/debug/createdump/crashinfo.cpp +++ b/src/coreclr/debug/createdump/crashinfo.cpp @@ -307,12 +307,44 @@ CrashInfo::InitializeDAC(DumpType dumpType) PFN_CLRDataCreateInstance pfnCLRDataCreateInstance = nullptr; PFN_DLLMAIN pfnDllMain = nullptr; bool result = false; + bool useCdacLite = false; HRESULT hr = S_OK; - // We assume that the DAC is in the same location as the libcoreclr.so module + // cdac-lite: when DOTNET_DbgUseCdacLite=1 (and this isn't a full dump), load the small native + // cdac-lite component instead of the legacy DAC to select managed memory. cdac-lite implements + // ICLRDataEnumMemoryRegions (the memory-enumeration path below) but not IXCLRDataProcess, so + // managed module enumeration and managed stack unwinding -- both already guarded on + // m_pClrDataProcess -- are skipped. + if (dumpType != DumpType::Full) + { + CLRConfigNoCache cdacLiteConfig = CLRConfigNoCache::Get("DbgUseCdacLite", /* noprefix */ false, &getenv); + DWORD enabled = 0; + useCdacLite = cdacLiteConfig.IsSet() && cdacLiteConfig.TryAsInteger(10, enabled) && enabled == 1; + } + + // We assume that the DAC (or cdac-lite) is in the same location as the libcoreclr.so module, + // unless an explicit cdac-lite path is provided via DOTNET_DbgCdacLitePath. std::string dacPath; - dacPath.append(m_coreclrPath); - dacPath.append(MAKEDLLNAME_A("mscordaccore")); + if (useCdacLite) + { + CLRConfigNoCache cdacLitePath = CLRConfigNoCache::Get("DbgCdacLitePath", /* noprefix */ false, &getenv); + if (cdacLitePath.IsSet()) + { + dacPath.append(cdacLitePath.AsString()); + } + else + { + dacPath.append(m_coreclrPath); + dacPath.append(MAKEDLLNAME_A("cdaclite")); + } + printf_status("cdac-lite: collecting managed memory (DOTNET_DbgUseCdacLite=1, %s tier)\n", + dumpType == DumpType::Heap ? "heap" : "normal"); + } + else + { + dacPath.append(m_coreclrPath); + dacPath.append(MAKEDLLNAME_A("mscordaccore")); + } // Load and initialize the DAC. We don't use the LoadLibraryA here because the PAL may not be // initialized properly in the forked process for the statically linked single-file scenario. @@ -357,11 +389,16 @@ CrashInfo::InitializeDAC(DumpType dumpType) printf_error("InitializeDAC: CLRDataCreateInstance(ICLRDataEnumMemoryRegions) FAILED %s (%08x)\n", GetHResultString(hr), hr); goto exit; } - hr = pfnCLRDataCreateInstance(__uuidof(IXCLRDataProcess), dataTarget, (void**)&m_pClrDataProcess); - if (FAILED(hr)) + // cdac-lite provides only ICLRDataEnumMemoryRegions; the legacy DAC additionally provides + // IXCLRDataProcess (used for managed module and managed thread enumeration). + if (!useCdacLite) { - printf_error("InitializeDAC: CLRDataCreateInstance(IXCLRDataProcess) FAILED %s (%08x)\n", GetHResultString(hr), hr); - goto exit; + hr = pfnCLRDataCreateInstance(__uuidof(IXCLRDataProcess), dataTarget, (void**)&m_pClrDataProcess); + if (FAILED(hr)) + { + printf_error("InitializeDAC: CLRDataCreateInstance(IXCLRDataProcess) FAILED %s (%08x)\n", GetHResultString(hr), hr); + goto exit; + } } result = true; exit: diff --git a/src/native/managed/cdac/tests/DumpTests/cdac-dump-helix.proj b/src/native/managed/cdac/tests/DumpTests/cdac-dump-helix.proj index 1832370278c0f7..31569fc51e7a32 100644 --- a/src/native/managed/cdac/tests/DumpTests/cdac-dump-helix.proj +++ b/src/native/managed/cdac/tests/DumpTests/cdac-dump-helix.proj @@ -133,7 +133,7 @@ + by prepare-cdac-helix-steps.yml). --> @@ -143,6 +143,10 @@ + + From e0a0c97e7d17bbb99b5e6f0babb5424ac4ba6ee9 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Tue, 7 Jul 2026 23:19:03 -0400 Subject: [PATCH 08/13] cdac-lite: export CLRDataCreateInstance with default visibility on Unix CI (runtime-diagnostics on Linux arm64/arm32) revealed the Unix createdump cdac-lite path failing at load: GetProcAddress(CLRDataCreateInstance) FAILED -- libcdaclite.so: undefined symbol: CLRDataCreateInstance. libcdaclite.so dlopen'd fine (no PAL-init problem), but CLRDataCreateInstance was not in its dynamic symbol table. coreclr compiles with -fvisibility=hidden, so the extern C symbol was hidden; the --version-script cannot un-hide a compile-time hidden symbol. The legacy DAC avoids this by declaring the entry point STDAPI DLLEXPORT (default visibility on Unix). Add an equivalent default-visibility attribute, guarded so it is a no-op on Windows (export comes from cdaclite.def). Verified cdaclite.dll still builds + links on Windows. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/debug/cdaclite/cdaclite.cpp | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/coreclr/debug/cdaclite/cdaclite.cpp b/src/coreclr/debug/cdaclite/cdaclite.cpp index c99659643374ee..70726517f7bae7 100644 --- a/src/coreclr/debug/cdaclite/cdaclite.cpp +++ b/src/coreclr/debug/cdaclite/cdaclite.cpp @@ -101,7 +101,18 @@ namespace cdac // The classic DAC factory entry point. dbghelp (Windows) and createdump // (Unix/macOS) call this to obtain an ICLRDataEnumMemoryRegions for a target. // -STDAPI CLRDataCreateInstance(REFIID iid, ICLRDataTarget* pLegacyTarget, void** iface) +// On Unix the symbol needs explicit default visibility so it survives +// -fvisibility=hidden and is exported for createdump's dlsym; the version script +// alone cannot un-hide a hidden symbol. On Windows the export comes from +// cdaclite.def, so no attribute is needed. Matches the DAC's CLRDataCreateInstance. +// +#ifdef HOST_WINDOWS +#define CDACLITE_EXPORT +#else +#define CDACLITE_EXPORT __attribute__((visibility("default"))) +#endif + +STDAPI CDACLITE_EXPORT CLRDataCreateInstance(REFIID iid, ICLRDataTarget* pLegacyTarget, void** iface) { if (pLegacyTarget == nullptr || iface == nullptr) { From 597e6812deb306f1c6c83f0ff43b34f061633b9a Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Wed, 8 Jul 2026 12:05:13 -0400 Subject: [PATCH 09/13] cdac-lite: emit ECMA metadata for managed modules (Linux/macOS dump readability) CI on Linux/macOS revealed the cdac-lite createdump path generating dumps the managed cDAC reader could not read: 133 VirtualReadException failures across frames, stackwalk, appdomain, and metadata tests (Windows cdac-lite passed 220/257, Linux/macOS ~116/257). Root-caused with a local WSL repro + ELF-core analysis: a managed assembly on Linux is mapped as multiple non-contiguous file-backed segments, and createdump's ELF/ Mach-O core omits read-only file-backed segments (metadata). The managed cDAC reader (EcmaMetadata_1.GetReadOnlyMetadataAddress) reads read-only metadata directly from target memory with no on-disk fallback (unlike the legacy DAC), so the metadata bytes must be in the dump. On Windows they happen to be captured; on Linux/macOS they are not. loader.cpp previously emitted only the PEImageLayout locator struct (comment: "the metadata bytes themselves come from the on-disk image"), which holds on Windows but not for the managed reader on Linux/macOS. Emit the bytes the reader touches: - mapped image: PE headers [Base,SizeOfHeaders] + CLI(COR20) header + ECMA metadata (RVA == offset-from-base), parsed from the module's PE headers - flat image: the whole (small) file Managed assemblies are PE on every platform, so the PE parse is valid on Linux/macOS. Verified in a local WSL build: the CoreLib and debuggee metadata regions the reader reads flipped from OUT to IN in the ELF core, with only a lean (~4MB) dump-size increase (vs ~17MB for whole-image emission). Builds clean on Windows and Linux/clang. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../debug/cdaclite/contracts/loader.cpp | 131 +++++++++++++++--- 1 file changed, 113 insertions(+), 18 deletions(-) diff --git a/src/coreclr/debug/cdaclite/contracts/loader.cpp b/src/coreclr/debug/cdaclite/contracts/loader.cpp index 82cd50e493be5b..5032d36e817f79 100644 --- a/src/coreclr/debug/cdaclite/contracts/loader.cpp +++ b/src/coreclr/debug/cdaclite/contracts/loader.cpp @@ -9,6 +9,7 @@ #include "loader.h" #include "runtimetypes.h" +#include #include namespace cdac @@ -106,6 +107,116 @@ namespace contracts int emitted; }; + // Emits the ECMA metadata (and the PE/CLI headers needed to locate it) for a managed + // module's loaded image. On Windows the managed reader reads metadata from the on-disk + // image, but on Linux/macOS EcmaMetadata_1.GetReadOnlyMetadataAddress reads it directly from + // target memory (no on-disk fallback), and createdump's ELF/Mach-O core omits read-only + // file-backed segments. So cdac-lite emits the bytes the reader will touch: + // - PE headers [Base, SizeOfHeaders] (the reader's PEReader parses these) + // - CLI (COR20) header [Base+corRVA, corSize] + // - ECMA metadata [Base+metadataRVA, metadataSize] + // For a mapped image RVA == offset-from-base. For a flat (file-layout) image the whole file + // is contiguous and small, so emit it wholesale. Managed assemblies are PE on every platform, + // so this PE parse is valid on Linux/macOS. + void EmitModuleMetadata(const Target& target, uint64_t base, uint32_t size, uint32_t flags) + { + if (base == 0) + { + return; + } + + const bool isMapped = (flags & 0x1) != 0; // FLAG_MAPPED + if (!isMapped) + { + // Flat layout: the whole file is mapped contiguously at Base (small). + if (size != 0) + { + target.EmitMemory(base, size); + } + return; + } + + uint8_t dos[0x40]; + if (!target.ReadBuffer(base, dos, sizeof(dos)) || dos[0] != 'M' || dos[1] != 'Z') + { + return; + } + uint32_t e_lfanew = 0; + memcpy(&e_lfanew, dos + 0x3C, sizeof(e_lfanew)); + + // PE signature(4) + COFF header(20) + optional header. Read enough to cover the optional + // header + the full data directory (PE32+: 112 + 16*8 = 240). + uint8_t peHdr[4 + 20 + 240]; + if (!target.ReadBuffer(base + e_lfanew, peHdr, sizeof(peHdr)) || peHdr[0] != 'P' || peHdr[1] != 'E') + { + return; + } + + const uint32_t optOffset = 4 + 20; // within peHdr + uint16_t optMagic = 0; + memcpy(&optMagic, peHdr + optOffset, sizeof(optMagic)); + + uint32_t sizeOfHeaders = 0; + memcpy(&sizeOfHeaders, peHdr + optOffset + 60, sizeof(sizeOfHeaders)); + + const uint32_t dataDirOffset = optOffset + ((optMagic == 0x20B) ? 112 : 96); + uint32_t corRVA = 0, corSize = 0; + memcpy(&corRVA, peHdr + dataDirOffset + 14 * 8, sizeof(corRVA)); + memcpy(&corSize, peHdr + dataDirOffset + 14 * 8 + 4, sizeof(corSize)); + + // PE headers (DOS + NT + section headers) that the reader's PEReader parses. + if (sizeOfHeaders != 0) + { + target.EmitMemory(base, sizeOfHeaders); + } + + if (corRVA == 0 || corSize == 0) + { + return; + } + + // CLI (COR20) header, then its MetaData directory (RVA at +8, Size at +12). + target.EmitMemory(base + corRVA, corSize); + uint8_t corHdr[16]; + if (target.ReadBuffer(base + corRVA, corHdr, sizeof(corHdr))) + { + uint32_t metadataRVA = 0, metadataSize = 0; + memcpy(&metadataRVA, corHdr + 8, sizeof(metadataRVA)); + memcpy(&metadataSize, corHdr + 12, sizeof(metadataSize)); + if (metadataRVA != 0 && metadataSize != 0) + { + target.EmitMemory(base + metadataRVA, metadataSize); + } + } + } + + // Emit the module's metadata-locator chain: Module -> PEAssembly -> PEImage -> + // PEImageLayout. The reader follows this (EcmaMetadata.GetMetadata -> + // Loader.TryGetPEImage -> TryGetLoadedImageContents) to find where a module's ECMA + // metadata lives, then reads the metadata bytes (see EmitModuleMetadata). + void EmitModuleMetadataChain(const Target& target, const data::Module& module) + { + if (module.PEAssembly == 0) + { + return; + } + data::PEAssembly peAssembly; + if (!target.TryRead(module.PEAssembly, peAssembly) || peAssembly.PEImage == 0) + { + return; + } + data::PEImage peImage; + if (!target.TryRead(peAssembly.PEImage, peImage) || peImage.LoadedImageLayout == 0) + { + return; + } + data::PEImageLayout layout; + if (target.TryRead(peImage.LoadedImageLayout, layout)) // struct read -> auto-emitted + { + EmitModuleMetadata(target, layout.Base, (uint32_t)layout.Size, (uint32_t)layout.Flags); + } + } + // Iterates modules and captures only the memory that is NOT available from the on-disk // binaries: in-memory symbol (PDB) streams. The DAC does not dump file-backed module // images -- the analyzer re-reads image bytes (code, R2R, ECMA metadata) from the binary @@ -121,24 +232,8 @@ namespace contracts return; } - // Emit the module's metadata-locator chain: Module -> PEAssembly -> PEImage -> - // PEImageLayout. The reader follows this (EcmaMetadata.GetMetadata -> - // Loader.TryGetPEImage -> TryGetLoadedImageContents) to find where a module's ECMA - // metadata lives; the metadata bytes themselves come from the on-disk image, but these - // small locator structs are not otherwise captured in a Normal dump. - if (module.PEAssembly != 0) - { - data::PEAssembly peAssembly; - if (target.TryRead(module.PEAssembly, peAssembly) && peAssembly.PEImage != 0) - { - data::PEImage peImage; - if (target.TryRead(peAssembly.PEImage, peImage) && peImage.LoadedImageLayout != 0) - { - data::PEImageLayout layout; - target.TryRead(peImage.LoadedImageLayout, layout); // struct read -> auto-emitted - } - } - } + // Emit the module's metadata-locator chain and the ECMA metadata bytes. + EmitModuleMetadataChain(target, module); // If the module has an in-memory symbol stream, capture its buffer (ILoader.TryGetSymbolStream). // In-memory PDBs have no on-disk backing, so they must be in the dump. From 6a8e268e5026449d6dfa4a1c4e06f3e4a3d816dd Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Wed, 8 Jul 2026 15:35:01 -0400 Subject: [PATCH 10/13] cdac-lite: emit PE headers (both layouts) matching the DAC, read metadata from disk Replace the Linux/macOS metadata-embedding approach with DAC-faithful PE header emission. PEImage::EnumMemoryRegions enumerates both m_pLayouts[IMAGE_FLAT] and [IMAGE_LOADED], and PEDecoder::EnumMemoryRegions emits the DOS header, NT headers, section table, COR (CLI) header and R2R header; the loaded layout additionally emits the debug directory. cdac-lite now mirrors this exactly for each layout and does NOT emit the ECMA metadata bytes -- a reader reads those from the on-disk image, just like the DAC. createdump excludes module image content from heap dumps by design (only full dumps write module mappings), so re-emitting these headers is required for the reader's on-disk metadata fallback to resolve the module. Expose PEImage::m_pLayouts[IMAGE_FLAT] as cdac_data::FlatImageLayout in the data descriptor so the flat layout's headers can be emitted. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../debug/cdaclite/contracts/loader.cpp | 188 +++++++++++++----- .../debug/cdaclite/data/runtimetypes.h | 1 + .../vm/datadescriptor/datadescriptor.inc | 1 + src/coreclr/vm/peimage.h | 2 + 4 files changed, 141 insertions(+), 51 deletions(-) diff --git a/src/coreclr/debug/cdaclite/contracts/loader.cpp b/src/coreclr/debug/cdaclite/contracts/loader.cpp index 5032d36e817f79..c04fdc96e2929a 100644 --- a/src/coreclr/debug/cdaclite/contracts/loader.cpp +++ b/src/coreclr/debug/cdaclite/contracts/loader.cpp @@ -107,36 +107,57 @@ namespace contracts int emitted; }; - // Emits the ECMA metadata (and the PE/CLI headers needed to locate it) for a managed - // module's loaded image. On Windows the managed reader reads metadata from the on-disk - // image, but on Linux/macOS EcmaMetadata_1.GetReadOnlyMetadataAddress reads it directly from - // target memory (no on-disk fallback), and createdump's ELF/Mach-O core omits read-only - // file-backed segments. So cdac-lite emits the bytes the reader will touch: - // - PE headers [Base, SizeOfHeaders] (the reader's PEReader parses these) - // - CLI (COR20) header [Base+corRVA, corSize] - // - ECMA metadata [Base+metadataRVA, metadataSize] - // For a mapped image RVA == offset-from-base. For a flat (file-layout) image the whole file - // is contiguous and small, so emit it wholesale. Managed assemblies are PE on every platform, - // so this PE parse is valid on Linux/macOS. - void EmitModuleMetadata(const Target& target, uint64_t base, uint32_t size, uint32_t flags) + // PE structure sizes on the target. The DAC emits each of these as a fixed struct via + // DPTR::EnumMem(), so we mirror the exact sizes rather than a coarse [Base, SizeOfHeaders]. + constexpr uint32_t kDosHeaderSize = 0x40; // sizeof(IMAGE_DOS_HEADER) + constexpr uint32_t kSectionHeaderSize = 40; // sizeof(IMAGE_SECTION_HEADER) + constexpr uint32_t kCor20HeaderSize = 72; // sizeof(IMAGE_COR20_HEADER) + constexpr uint32_t kReadyToRunHeaderSize = 16; // sizeof(READYTORUN_HEADER) + constexpr uint32_t kDebugDirEntrySize = 28; // sizeof(IMAGE_DEBUG_DIRECTORY) + constexpr uint32_t kReadyToRunSignature = 0x00525452; // 'RTR\0' + constexpr uint32_t kMaxSections = 96; + + // Translates an RVA to a target address within an image layout. For a mapped image + // (FLAG_MAPPED) RVA == offset-from-base. For a flat (raw file) image the RVA must be + // converted to a file offset by walking the section table, matching PEDecoder's + // RvaToAddr for a flat layout (and Loader_1.RvaToOffset in the managed cDAC). + uint64_t RvaToAddr(uint64_t base, uint32_t rva, bool isMapped, const uint8_t* sections, uint32_t numSections) { - if (base == 0) + if (isMapped) { - return; + return base + rva; } - - const bool isMapped = (flags & 0x1) != 0; // FLAG_MAPPED - if (!isMapped) + for (uint32_t i = 0; i < numSections; i++) { - // Flat layout: the whole file is mapped contiguously at Base (small). - if (size != 0) + const uint8_t* s = sections + (size_t)i * kSectionHeaderSize; + uint32_t va = 0, rawSize = 0, rawPtr = 0; + memcpy(&va, s + 12, sizeof(va)); // VirtualAddress + memcpy(&rawSize, s + 16, sizeof(rawSize)); // SizeOfRawData + memcpy(&rawPtr, s + 20, sizeof(rawPtr)); // PointerToRawData + if (rva >= va && rva < va + rawSize) { - target.EmitMemory(base, size); + return base + (rva - va) + rawPtr; } + } + return base + rva; + } + + // Mirrors PEDecoder::EnumMemoryRegions for a single image layout (pedecoder.cpp): emits the + // DOS header, NT headers, section table, COR (CLI) header, and R2R header -- the exact regions + // the legacy DAC records. When emitDebugDir is set (the loaded layout, per + // PEImage::EnumMemoryRegions) it also emits the debug directory and the blobs its entries + // point to (used to locate managed PDBs). The ECMA metadata and code bytes are deliberately + // NOT emitted; a reader reads those from the on-disk image, exactly like the DAC. Managed + // assemblies are PE on every platform, so this PE parse is valid on Linux/macOS. + void EmitPEDecoderRegions(const Target& target, uint64_t base, uint32_t flags, bool emitDebugDir) + { + if (base == 0) + { return; } + const bool isMapped = (flags & 0x1) != 0; // FLAG_MAPPED - uint8_t dos[0x40]; + uint8_t dos[kDosHeaderSize]; if (!target.ReadBuffer(base, dos, sizeof(dos)) || dos[0] != 'M' || dos[1] != 'Z') { return; @@ -144,56 +165,113 @@ namespace contracts uint32_t e_lfanew = 0; memcpy(&e_lfanew, dos + 0x3C, sizeof(e_lfanew)); - // PE signature(4) + COFF header(20) + optional header. Read enough to cover the optional - // header + the full data directory (PE32+: 112 + 16*8 = 240). + // PE signature(4) + COFF header(20) + optional header (PE32+: 240). Covers all data dirs. uint8_t peHdr[4 + 20 + 240]; if (!target.ReadBuffer(base + e_lfanew, peHdr, sizeof(peHdr)) || peHdr[0] != 'P' || peHdr[1] != 'E') { return; } + uint16_t numberOfSections = 0; + memcpy(&numberOfSections, peHdr + 4 + 2, sizeof(numberOfSections)); // IMAGE_FILE_HEADER.NumberOfSections + uint16_t sizeOfOptionalHeader = 0; + memcpy(&sizeOfOptionalHeader, peHdr + 4 + 16, sizeof(sizeOfOptionalHeader)); // IMAGE_FILE_HEADER.SizeOfOptionalHeader const uint32_t optOffset = 4 + 20; // within peHdr uint16_t optMagic = 0; memcpy(&optMagic, peHdr + optOffset, sizeof(optMagic)); + const bool isPE32Plus = (optMagic == 0x20B); + + // DOS header + NT headers, exactly as PEDecoder::EnumMemoryRegions records them + // (DacEnumMemoryRegion(m_base, sizeof(IMAGE_DOS_HEADER)) then m_pNTHeaders.EnumMem()). + // createdump excludes module image content from heap dumps by design -- only Full dumps + // write m_moduleMappings (crashinfo.cpp GatherCrashInfo) -- so, just like the DAC, we + // re-emit these header structures into the dump; the ECMA metadata and code are read from + // the on-disk image. createdump page-rounds each emitted region, so these discrete regions + // land as the module's first header page. + target.EmitMemory(base, kDosHeaderSize); + target.EmitMemory(base + e_lfanew, 4 + 20 + (isPE32Plus ? 240u : 224u)); // sizeof(IMAGE_NT_HEADERS) + + // Section table (immediately after the optional header) -- also read for RVA->offset mapping. + const uint64_t firstSection = base + e_lfanew + optOffset + sizeOfOptionalHeader; + uint32_t numSec = numberOfSections; + if (numSec > kMaxSections) + { + numSec = kMaxSections; + } + uint8_t sections[kMaxSections * kSectionHeaderSize]; + if (numSec != 0 && !target.ReadBuffer(firstSection, sections, kSectionHeaderSize * numSec)) + { + numSec = 0; + } + if (numSec != 0) + { + target.EmitMemory(firstSection, kSectionHeaderSize * numSec); + } - uint32_t sizeOfHeaders = 0; - memcpy(&sizeOfHeaders, peHdr + optOffset + 60, sizeof(sizeOfHeaders)); + const uint32_t dataDirOffset = optOffset + (isPE32Plus ? 112 : 96); - const uint32_t dataDirOffset = optOffset + ((optMagic == 0x20B) ? 112 : 96); - uint32_t corRVA = 0, corSize = 0; + // COR (CLI) header (data directory 14 = IMAGE_DIRECTORY_ENTRY_COMHEADER). + uint32_t corRVA = 0; memcpy(&corRVA, peHdr + dataDirOffset + 14 * 8, sizeof(corRVA)); - memcpy(&corSize, peHdr + dataDirOffset + 14 * 8 + 4, sizeof(corSize)); - - // PE headers (DOS + NT + section headers) that the reader's PEReader parses. - if (sizeOfHeaders != 0) + if (corRVA != 0) { - target.EmitMemory(base, sizeOfHeaders); - } + uint64_t corAddr = RvaToAddr(base, corRVA, isMapped, sections, numSec); + target.EmitMemory(corAddr, kCor20HeaderSize); - if (corRVA == 0 || corSize == 0) - { - return; + // R2R header via the COR header's ManagedNativeHeader directory (RVA @+64, Size @+68). + uint8_t cor[kCor20HeaderSize]; + if (target.ReadBuffer(corAddr, cor, sizeof(cor))) + { + uint32_t r2rRVA = 0, r2rSize = 0; + memcpy(&r2rRVA, cor + 64, sizeof(r2rRVA)); + memcpy(&r2rSize, cor + 68, sizeof(r2rSize)); + if (r2rRVA != 0 && r2rSize >= kReadyToRunHeaderSize) + { + uint64_t r2rAddr = RvaToAddr(base, r2rRVA, isMapped, sections, numSec); + uint32_t sig = 0; + if (target.ReadBuffer(r2rAddr, &sig, sizeof(sig)) && sig == kReadyToRunSignature) + { + target.EmitMemory(r2rAddr, kReadyToRunHeaderSize); + } + } + } } - // CLI (COR20) header, then its MetaData directory (RVA at +8, Size at +12). - target.EmitMemory(base + corRVA, corSize); - uint8_t corHdr[16]; - if (target.ReadBuffer(base + corRVA, corHdr, sizeof(corHdr))) + // Debug directory (data directory 6): loaded layout only, matching PEImage::EnumMemoryRegions. + // Emits the directory plus each entry's raw data (e.g. CodeView records for managed PDBs). + if (emitDebugDir) { - uint32_t metadataRVA = 0, metadataSize = 0; - memcpy(&metadataRVA, corHdr + 8, sizeof(metadataRVA)); - memcpy(&metadataSize, corHdr + 12, sizeof(metadataSize)); - if (metadataRVA != 0 && metadataSize != 0) + uint32_t dbgRVA = 0, dbgSize = 0; + memcpy(&dbgRVA, peHdr + dataDirOffset + 6 * 8, sizeof(dbgRVA)); + memcpy(&dbgSize, peHdr + dataDirOffset + 6 * 8 + 4, sizeof(dbgSize)); + if (dbgRVA != 0 && dbgSize != 0) { - target.EmitMemory(base + metadataRVA, metadataSize); + uint64_t dbgAddr = RvaToAddr(base, dbgRVA, isMapped, sections, numSec); + target.EmitMemory(dbgAddr, dbgSize); + for (uint32_t i = 0; i < dbgSize / kDebugDirEntrySize; i++) + { + uint8_t entry[kDebugDirEntrySize]; + if (!target.ReadBuffer(dbgAddr + (uint64_t)i * kDebugDirEntrySize, entry, sizeof(entry))) + { + break; + } + uint32_t sizeOfData = 0, addrOfRawData = 0; + memcpy(&sizeOfData, entry + 16, sizeof(sizeOfData)); // IMAGE_DEBUG_DIRECTORY.SizeOfData + memcpy(&addrOfRawData, entry + 20, sizeof(addrOfRawData)); // .AddressOfRawData (RVA) + if (addrOfRawData != 0 && sizeOfData != 0) + { + target.EmitMemory(RvaToAddr(base, addrOfRawData, isMapped, sections, numSec), sizeOfData); + } + } } } } // Emit the module's metadata-locator chain: Module -> PEAssembly -> PEImage -> - // PEImageLayout. The reader follows this (EcmaMetadata.GetMetadata -> - // Loader.TryGetPEImage -> TryGetLoadedImageContents) to find where a module's ECMA - // metadata lives, then reads the metadata bytes (see EmitModuleMetadata). + // PEImageLayout, plus the PE header regions of the image, matching the legacy DAC. + // PEImage::EnumMemoryRegions (peimage.cpp) enumerates BOTH m_pLayouts[IMAGE_FLAT] and + // [IMAGE_LOADED] and additionally emits the debug directory for the loaded layout, so we + // do the same. The reader then reads ECMA metadata from the on-disk image via these headers. void EmitModuleMetadataChain(const Target& target, const data::Module& module) { if (module.PEAssembly == 0) @@ -210,10 +288,18 @@ namespace contracts { return; } - data::PEImageLayout layout; - if (target.TryRead(peImage.LoadedImageLayout, layout)) // struct read -> auto-emitted + data::PEImageLayout loaded; + if (target.TryRead(peImage.LoadedImageLayout, loaded)) // struct read -> auto-emitted { - EmitModuleMetadata(target, layout.Base, (uint32_t)layout.Size, (uint32_t)layout.Flags); + EmitPEDecoderRegions(target, loaded.Base, (uint32_t)loaded.Flags, /*emitDebugDir*/ true); + } + if (peImage.FlatImageLayout != 0 && peImage.FlatImageLayout != peImage.LoadedImageLayout) + { + data::PEImageLayout flat; + if (target.TryRead(peImage.FlatImageLayout, flat)) // struct read -> auto-emitted + { + EmitPEDecoderRegions(target, flat.Base, (uint32_t)flat.Flags, /*emitDebugDir*/ false); + } } } diff --git a/src/coreclr/debug/cdaclite/data/runtimetypes.h b/src/coreclr/debug/cdaclite/data/runtimetypes.h index cfa32f15729a54..4a9ad60bcef797 100644 --- a/src/coreclr/debug/cdaclite/data/runtimetypes.h +++ b/src/coreclr/debug/cdaclite/data/runtimetypes.h @@ -208,6 +208,7 @@ namespace data { PEImage() : Struct("PEImage") {} CDAC_PTR(LoadedImageLayout) + CDAC_OPT_PTR(FlatImageLayout) // m_pLayouts[IMAGE_FLAT], used for reading metadata }; struct PEImageLayout : Struct diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index 8481dd43b88885..e991cd0e556b96 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -451,6 +451,7 @@ CDAC_TYPE_END(AssemblyBinder) CDAC_TYPE_BEGIN(PEImage) CDAC_TYPE_INDETERMINATE(PEImage) CDAC_TYPE_FIELD(PEImage, T_POINTER, LoadedImageLayout, cdac_data::LoadedImageLayout) +CDAC_TYPE_FIELD(PEImage, T_POINTER, FlatImageLayout, cdac_data::FlatImageLayout) CDAC_TYPE_FIELD(PEImage, TYPE(ProbeExtensionResult), ProbeExtensionResult, cdac_data::ProbeExtensionResult) CDAC_TYPE_END(PEImage) diff --git a/src/coreclr/vm/peimage.h b/src/coreclr/vm/peimage.h index ea56c0d8d594c7..72468d4525b6a4 100644 --- a/src/coreclr/vm/peimage.h +++ b/src/coreclr/vm/peimage.h @@ -326,6 +326,8 @@ class PEImage final template<> struct cdac_data { + // The flat PEImageLayout is m_pLayouts[IMAGE_FLAT] (used for reading metadata). + static constexpr size_t FlatImageLayout = offsetof(PEImage, m_pLayouts); // The loaded PEImageLayout is m_pLayouts[IMAGE_LOADED] static constexpr size_t LoadedImageLayout = offsetof(PEImage, m_pLayouts) + sizeof(PTR_PEImageLayout); static constexpr size_t ProbeExtensionResult = offsetof(PEImage, m_probeExtensionResult); From 9a561acf1bbbe5bebcc60895a75e20a059f70ace Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Wed, 8 Jul 2026 19:34:48 -0400 Subject: [PATCH 11/13] cDAC dump tests: resolve modules from the Loader contract, not ClrMD The dump-test memory host resolved on-disk module images via ClrMD's raw module list. For a managed ReadyToRun assembly on Linux/macOS that has two mappings -- a flat/file layout and a separate loaded/converted layout far apart in the address space -- ClrMD registers the module at the flat base only. Reads of loaded-layout addresses (e.g. ECMA metadata, which heap/mini dumps do not capture) then fail their on-disk fallback, non-deterministically depending on ASLR ordering. Source managed module mappings from the cDAC Loader contract instead, mirroring dotnet/diagnostics' ManagedModuleService: DumpTestBase enumerates modules via ILoader (loaded base + size via TryGetLoadedImageContents, path via GetPath, and the PE identity key via GetFileHeadersInfo) and registers them with the host. ClrMdDumpHost's read fallback now resolves the owning module solely from this table and no longer consults ClrMD's module list. ClrMD is retained only for raw memory reads, thread context, and the one-time contract-descriptor export lookup needed to bootstrap the Target. Each module's on-disk file is located and verified against its PE identity key (COFF TimeDateStamp + optional-header SizeOfImage) once at registration, so a stale or mismatched same-named assembly is rejected rather than read silently. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tests/TestInfrastructure/ClrMdDumpHost.cs | 217 ++++++++++++------ .../tests/TestInfrastructure/DumpTestBase.cs | 51 ++++ 2 files changed, 195 insertions(+), 73 deletions(-) diff --git a/src/native/managed/cdac/tests/TestInfrastructure/ClrMdDumpHost.cs b/src/native/managed/cdac/tests/TestInfrastructure/ClrMdDumpHost.cs index 594193295fcdd0..c9be8818e5f86a 100644 --- a/src/native/managed/cdac/tests/TestInfrastructure/ClrMdDumpHost.cs +++ b/src/native/managed/cdac/tests/TestInfrastructure/ClrMdDumpHost.cs @@ -24,9 +24,21 @@ public sealed class ClrMdDumpHost : IDisposable private readonly DataTarget _dataTarget; private readonly string[] _searchPaths; + private readonly List _modules = new(); public string DumpPath { get; } + /// + /// Describes a managed module's loaded image mapping, sourced from the cDAC Loader contract: + /// the loaded (converted) image base, the image size, the on-disk assembly path, and the PE + /// identity key (COFF TimeDateStamp and optional-header SizeOfImage) used to + /// verify that the on-disk file matches the module captured in the dump. + /// + public readonly record struct ManagedModuleImage(ulong Base, ulong Size, string FileName, uint TimeStamp, uint ImageSize); + + /// A module mapping whose on-disk file has already been located and key-verified. + private readonly record struct ResolvedModule(ulong Base, ulong Size, string? FilePath); + private ClrMdDumpHost(string dumpPath, DataTarget dataTarget, string[] searchPaths) { DumpPath = dumpPath; @@ -61,75 +73,114 @@ public int ReadFromTarget(ulong address, Span buffer) if (bytesRead == buffer.Length) return 0; // success - // If we couldn't read the full buffer, maybe it's in a PE image - ModuleInfo? info = GetModuleForAddress(address); - if (info is null || info.FileName is null) + // The dump didn't fully cover the range. Serve the remaining bytes from the owning + // module's on-disk image -- exactly as the legacy DAC and dotnet-dump do for PE/COR + // headers and ECMA metadata, which heap/mini dumps do not capture. Module ownership is + // resolved solely from the cDAC Loader contract (see RegisterManagedModules); ClrMD's + // raw module list is deliberately not consulted, because it registers managed R2R + // modules at their flat/file base and cannot represent the separate loaded mapping. + ResolvedModule? module = FindModule(address + (ulong)bytesRead); + if (module is null || module.Value.FilePath is null) { return -1; } - string? foundFile = FindFileOnDisk(info.FileName); - if (foundFile is null) + return FillFromImage(module.Value.FilePath, module.Value.Base, address, buffer, bytesRead); + } + catch + { + return -1; + } + } + + private ResolvedModule? FindModule(ulong address) + { + foreach (ResolvedModule module in _modules) + { + if (address >= module.Base && address < module.Base + module.Size) + return module; + } + return null; + } + + private static int FillFromImage(string foundFile, ulong moduleBase, ulong address, Span buffer, int bytesRead) + { + using FileStream fs = File.OpenRead(foundFile); + using PEReader peReader = new PEReader(fs); + + int sizeOfHeaders = peReader.PEHeaders.PEHeader?.SizeOfHeaders ?? 0; + PEMemoryBlock wholeImage = default; + bool wholeImageLoaded = false; + + int filled = bytesRead; + ulong current = address + (ulong)bytesRead; + while (filled < buffer.Length) + { + long rvaLong = (long)(current - moduleBase); + if (rvaLong < 0 || rvaLong > int.MaxValue) { return -1; } + int rva = (int)rvaLong; - using FileStream fs = File.OpenRead(foundFile); - using PEReader peReader = new PEReader(fs); - - int sizeOfHeaders = peReader.PEHeaders.PEHeader?.SizeOfHeaders ?? 0; - PEMemoryBlock wholeImage = default; - bool wholeImageLoaded = false; - - int filled = bytesRead; - ulong current = address + (ulong)bytesRead; - while (filled < buffer.Length) + // GetSectionData maps a (loaded-layout) RVA to the raw section bytes in the on-disk + // file, so this works whether the requested address is in the flat or loaded layout. + PEMemoryBlock block = peReader.GetSectionData(rva); + if (block.Length > 0) { - int rva = (int)(current - info.ImageBase); - PEMemoryBlock block = peReader.GetSectionData(rva); - if (block.Length > 0) + int toCopy = Math.Min(block.Length, buffer.Length - filled); + unsafe { - int toCopy = Math.Min(block.Length, buffer.Length - filled); - unsafe - { - new ReadOnlySpan(block.Pointer, toCopy).CopyTo(buffer.Slice(filled)); - } - filled += toCopy; - current += (ulong)toCopy; + new ReadOnlySpan(block.Pointer, toCopy).CopyTo(buffer.Slice(filled)); } - else if (rva >= 0 && rva < sizeOfHeaders) + filled += toCopy; + current += (ulong)toCopy; + } + else if (rva >= 0 && rva < sizeOfHeaders) + { + // PE header region (before the first section): GetSectionData doesn't cover it. + // For a loaded image the headers sit at file offset == RVA, so serve them from + // the raw file image (needed to read a module's PE/COR headers when the dump + // doesn't capture them, e.g. cdac-lite Normal dumps without the legacy DAC). + if (!wholeImageLoaded) { - // PE header region (before the first section): GetSectionData doesn't cover it. - // For a loaded image the headers sit at file offset == RVA, so serve them from - // the raw file image (needed to read a module's PE/COR headers when the dump - // doesn't capture them, e.g. cdac-lite Normal dumps without the legacy DAC). - if (!wholeImageLoaded) - { - wholeImage = peReader.GetEntireImage(); - wholeImageLoaded = true; - } - int available = Math.Min(sizeOfHeaders, wholeImage.Length) - rva; - int toCopy = Math.Min(available, buffer.Length - filled); - if (toCopy <= 0) - return -1; - unsafe - { - new ReadOnlySpan(wholeImage.Pointer + rva, toCopy).CopyTo(buffer.Slice(filled)); - } - filled += toCopy; - current += (ulong)toCopy; + wholeImage = peReader.GetEntireImage(); + wholeImageLoaded = true; } - else - { + int available = Math.Min(sizeOfHeaders, wholeImage.Length) - rva; + int toCopy = Math.Min(available, buffer.Length - filled); + if (toCopy <= 0) return -1; + unsafe + { + new ReadOnlySpan(wholeImage.Pointer + rva, toCopy).CopyTo(buffer.Slice(filled)); } + filled += toCopy; + current += (ulong)toCopy; + } + else + { + return -1; } - - return 0; } - catch + + return 0; + } + + /// + /// Registers managed module images sourced from the cDAC Loader contract so that reads of + /// loaded-layout addresses (e.g. ECMA metadata, which heap/mini dumps do not capture) resolve + /// to the on-disk assembly. Mirrors dotnet/diagnostics' ManagedModuleService, which sources + /// loaded bases from the runtime rather than from ClrMD's raw module list. Each module's + /// on-disk file is located and verified against its PE identity key (TimeDateStamp + + /// SizeOfImage) here, once, so a stale or mismatched same-named assembly is never used. + /// + public void RegisterManagedModules(IEnumerable modules) + { + _modules.Clear(); + foreach (ManagedModuleImage module in modules) { - return -1; + _modules.Add(new ResolvedModule(module.Base, module.Size, ResolveFile(module))); } } @@ -142,16 +193,54 @@ public int GetThreadContext(uint threadId, uint contextFlags, Span buffer) return _dataTarget.DataReader.GetThreadContext(threadId, contextFlags, buffer) ? 0 : -1; } - private ModuleInfo? GetModuleForAddress(ulong address) + /// + /// Locates the on-disk file for a module and verifies it matches the module captured in the + /// dump via its PE identity key (COFF TimeDateStamp + optional-header SizeOfImage) -- the same + /// key dotnet-dump/ClrMD use for symbol-store lookups. Searches the module's recorded path + /// first, then the configured symbol paths, and returns the first candidate whose key matches. + /// Returns null if no matching file is found. + /// + private string? ResolveFile(ManagedModuleImage module) { - foreach (ModuleInfo module in _dataTarget.DataReader.EnumerateModules()) + foreach (string candidate in EnumerateCandidatePaths(module.FileName)) { - if (address >= module.ImageBase && address < module.ImageBase + (ulong)module.ImageSize) - return module; + if (File.Exists(candidate) && KeyMatches(candidate, module.TimeStamp, module.ImageSize)) + return candidate; } return null; } + private IEnumerable EnumerateCandidatePaths(string modulePath) + { + // The path recorded in the runtime (may be absolute; only meaningful on the collecting host). + yield return modulePath; + + int lastSep = Math.Max(modulePath.LastIndexOf('/'), modulePath.LastIndexOf('\\')); + string fileName = lastSep >= 0 ? modulePath[(lastSep + 1)..] : modulePath; + foreach (string searchPath in _searchPaths) + yield return Path.Combine(searchPath, fileName); + } + + private static bool KeyMatches(string path, uint expectedTimeStamp, uint expectedImageSize) + { + // A zero key means the module didn't report identity info; accept a name match in that case. + if (expectedTimeStamp == 0 && expectedImageSize == 0) + return true; + try + { + using FileStream fs = File.OpenRead(path); + using PEReader peReader = new PEReader(fs); + PEHeaders headers = peReader.PEHeaders; + uint actualTimeStamp = (uint)headers.CoffHeader.TimeDateStamp; + uint actualImageSize = (uint)(headers.PEHeader?.SizeOfImage ?? 0); + return actualTimeStamp == expectedTimeStamp && actualImageSize == expectedImageSize; + } + catch + { + return false; + } + } + /// /// Locate the DotNetRuntimeContractDescriptor symbol address in the dump. /// Uses ClrMD's built-in export resolution which handles PE, ELF, and Mach-O formats. @@ -188,24 +277,6 @@ public ulong FindContractDescriptorAddress() throw new InvalidOperationException("Could not find DotNetRuntimeContractDescriptor export in any runtime module in the dump."); } - private string? FindFileOnDisk(string modulePath) - { - // for local runs - if (File.Exists(modulePath)) - return modulePath; - int lastSep = Math.Max(modulePath.LastIndexOf('/'), modulePath.LastIndexOf('\\')); - string fileName = lastSep >= 0 ? modulePath[(lastSep + 1)..] : modulePath; - - foreach (string searchPath in _searchPaths) - { - string candidate = Path.Combine(searchPath, fileName); - if (File.Exists(candidate)) - return candidate; - } - - return null; - } - private static bool IsRuntimeModule(string fileName) { foreach (string name in s_runtimeModuleNames) diff --git a/src/native/managed/cdac/tests/TestInfrastructure/DumpTestBase.cs b/src/native/managed/cdac/tests/TestInfrastructure/DumpTestBase.cs index 7475433e45ffee..dd57e573719a91 100644 --- a/src/native/managed/cdac/tests/TestInfrastructure/DumpTestBase.cs +++ b/src/native/managed/cdac/tests/TestInfrastructure/DumpTestBase.cs @@ -119,6 +119,7 @@ protected void InitializeDumpTest(TestConfiguration config, string debuggeeName, out _target); Assert.True(created, $"Failed to create ContractDescriptorTarget from dump: {dumpPath}"); + RegisterManagedModules(); } /// @@ -145,6 +146,7 @@ protected void InitializeDumpTestFromPath(string dumpPath) out _target); Assert.True(created, $"Failed to create ContractDescriptorTarget from dump: {dumpPath}"); + RegisterManagedModules(); } public void Dispose() @@ -153,6 +155,55 @@ public void Dispose() System.GC.SuppressFinalize(this); } + /// + /// Enumerates managed modules via the cDAC Loader contract and registers their loaded image + /// mappings (loaded base + size + on-disk path + PE identity key) with the dump host. This + /// lets reads of loaded-layout addresses (e.g. ECMA metadata, which heap/mini dumps do not + /// capture) resolve to the on-disk assembly, mirroring dotnet/diagnostics' ManagedModuleService + /// which sources loaded bases from the runtime rather than from ClrMD's raw module list. + /// + private void RegisterManagedModules() + { + if (_host is null || _target is null) + return; + + ILoader loader = _target.Contracts.Loader; + TargetPointer appDomain = loader.GetAppDomain(); + if (appDomain == TargetPointer.Null) + return; + + List modules = new(); + foreach (var handle in loader.GetModuleHandles(appDomain, AssemblyIterationFlags.IncludeLoaded | AssemblyIterationFlags.IncludeExecution)) + { + try + { + if (loader.IsDynamic(handle)) + continue; + if (!loader.TryGetLoadedImageContents(handle, out TargetPointer baseAddress, out uint size, out _) + || baseAddress == TargetPointer.Null || size == 0) + continue; + + string fileName = loader.GetPath(handle); + if (string.IsNullOrEmpty(fileName)) + fileName = loader.GetFileName(handle); + if (string.IsNullOrEmpty(fileName)) + continue; + + // PE identity key (COFF TimeDateStamp + optional-header SizeOfImage) used to + // verify the on-disk file matches the module captured in the dump. + loader.GetFileHeadersInfo(handle, out uint timeStamp, out uint imageSize); + + modules.Add(new ClrMdDumpHost.ManagedModuleImage(baseAddress, size, fileName, timeStamp, imageSize)); + } + catch (System.Exception) + { + // Skip modules the Loader contract can't fully describe (e.g. in-memory/dynamic). + } + } + + _host.RegisterManagedModules(modules); + } + /// /// Checks the calling test method for skip attributes and throws /// if the current configuration matches. From 8dcf456ca7eb37820cbf7644e9523f96fe398c08 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Wed, 8 Jul 2026 20:12:14 -0400 Subject: [PATCH 12/13] cDAC dump tests: read flat-layout module metadata by file offset A managed module's loaded image can be either a mapped (loaded/converted) layout or a flat (file) layout. For a flat layout -- e.g. an IL-only assembly whose loaded layout is the raw file mapping -- the runtime reports metadata and other data as base + file offset, not base + RVA. The on-disk fallback assumed a mapped layout and translated the offset via GetSectionData, which fails for a flat module (the file offset is not a section RVA), so reads that crossed out of the captured header page into the assembly's metadata failed. Thread the module's FLAG_MAPPED bit from the Loader contract through to the fallback and branch on it: mapped layouts translate the RVA via the section table, flat layouts read the raw image at the offset directly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tests/TestInfrastructure/ClrMdDumpHost.cs | 68 +++++++++++-------- .../tests/TestInfrastructure/DumpTestBase.cs | 5 +- 2 files changed, 41 insertions(+), 32 deletions(-) diff --git a/src/native/managed/cdac/tests/TestInfrastructure/ClrMdDumpHost.cs b/src/native/managed/cdac/tests/TestInfrastructure/ClrMdDumpHost.cs index c9be8818e5f86a..346dc4e857927d 100644 --- a/src/native/managed/cdac/tests/TestInfrastructure/ClrMdDumpHost.cs +++ b/src/native/managed/cdac/tests/TestInfrastructure/ClrMdDumpHost.cs @@ -30,14 +30,15 @@ public sealed class ClrMdDumpHost : IDisposable /// /// Describes a managed module's loaded image mapping, sourced from the cDAC Loader contract: - /// the loaded (converted) image base, the image size, the on-disk assembly path, and the PE - /// identity key (COFF TimeDateStamp and optional-header SizeOfImage) used to - /// verify that the on-disk file matches the module captured in the dump. + /// the loaded image base, the image size, whether that layout is mapped (loaded/converted) or + /// flat (file layout), the on-disk assembly path, and the PE identity key (COFF + /// TimeDateStamp and optional-header SizeOfImage) used to verify that the on-disk + /// file matches the module captured in the dump. /// - public readonly record struct ManagedModuleImage(ulong Base, ulong Size, string FileName, uint TimeStamp, uint ImageSize); + public readonly record struct ManagedModuleImage(ulong Base, ulong Size, bool IsMapped, string FileName, uint TimeStamp, uint ImageSize); /// A module mapping whose on-disk file has already been located and key-verified. - private readonly record struct ResolvedModule(ulong Base, ulong Size, string? FilePath); + private readonly record struct ResolvedModule(ulong Base, ulong Size, bool IsMapped, string? FilePath); private ClrMdDumpHost(string dumpPath, DataTarget dataTarget, string[] searchPaths) { @@ -85,7 +86,7 @@ public int ReadFromTarget(ulong address, Span buffer) return -1; } - return FillFromImage(module.Value.FilePath, module.Value.Base, address, buffer, bytesRead); + return FillFromImage(module.Value.FilePath, module.Value.Base, module.Value.IsMapped, address, buffer, bytesRead); } catch { @@ -103,29 +104,43 @@ public int ReadFromTarget(ulong address, Span buffer) return null; } - private static int FillFromImage(string foundFile, ulong moduleBase, ulong address, Span buffer, int bytesRead) + private static int FillFromImage(string foundFile, ulong moduleBase, bool isMapped, ulong address, Span buffer, int bytesRead) { using FileStream fs = File.OpenRead(foundFile); using PEReader peReader = new PEReader(fs); - - int sizeOfHeaders = peReader.PEHeaders.PEHeader?.SizeOfHeaders ?? 0; - PEMemoryBlock wholeImage = default; - bool wholeImageLoaded = false; + PEMemoryBlock wholeImage = peReader.GetEntireImage(); int filled = bytesRead; ulong current = address + (ulong)bytesRead; while (filled < buffer.Length) { - long rvaLong = (long)(current - moduleBase); - if (rvaLong < 0 || rvaLong > int.MaxValue) + long offsetLong = (long)(current - moduleBase); + if (offsetLong < 0 || offsetLong > int.MaxValue) { return -1; } - int rva = (int)rvaLong; + int offset = (int)offsetLong; - // GetSectionData maps a (loaded-layout) RVA to the raw section bytes in the on-disk - // file, so this works whether the requested address is in the flat or loaded layout. - PEMemoryBlock block = peReader.GetSectionData(rva); + if (!isMapped) + { + // Flat (file) layout: the runtime reports addresses as base + file offset, so the + // computed offset is already a file offset into the raw image -- read it directly. + if (offset >= wholeImage.Length) + return -1; + int toCopy = Math.Min(wholeImage.Length - offset, buffer.Length - filled); + unsafe + { + new ReadOnlySpan(wholeImage.Pointer + offset, toCopy).CopyTo(buffer.Slice(filled)); + } + filled += toCopy; + current += (ulong)toCopy; + continue; + } + + // Mapped (loaded/converted) layout: the offset is a virtual RVA. GetSectionData maps it + // to the raw section bytes in the on-disk file. + int sizeOfHeaders = peReader.PEHeaders.PEHeader?.SizeOfHeaders ?? 0; + PEMemoryBlock block = peReader.GetSectionData(offset); if (block.Length > 0) { int toCopy = Math.Min(block.Length, buffer.Length - filled); @@ -136,24 +151,17 @@ private static int FillFromImage(string foundFile, ulong moduleBase, ulong addre filled += toCopy; current += (ulong)toCopy; } - else if (rva >= 0 && rva < sizeOfHeaders) + else if (offset >= 0 && offset < sizeOfHeaders) { - // PE header region (before the first section): GetSectionData doesn't cover it. - // For a loaded image the headers sit at file offset == RVA, so serve them from - // the raw file image (needed to read a module's PE/COR headers when the dump - // doesn't capture them, e.g. cdac-lite Normal dumps without the legacy DAC). - if (!wholeImageLoaded) - { - wholeImage = peReader.GetEntireImage(); - wholeImageLoaded = true; - } - int available = Math.Min(sizeOfHeaders, wholeImage.Length) - rva; + // PE header region (before the first section): GetSectionData doesn't cover it, and + // in a loaded image the headers sit at file offset == RVA, so serve from the raw image. + int available = Math.Min(sizeOfHeaders, wholeImage.Length) - offset; int toCopy = Math.Min(available, buffer.Length - filled); if (toCopy <= 0) return -1; unsafe { - new ReadOnlySpan(wholeImage.Pointer + rva, toCopy).CopyTo(buffer.Slice(filled)); + new ReadOnlySpan(wholeImage.Pointer + offset, toCopy).CopyTo(buffer.Slice(filled)); } filled += toCopy; current += (ulong)toCopy; @@ -180,7 +188,7 @@ public void RegisterManagedModules(IEnumerable modules) _modules.Clear(); foreach (ManagedModuleImage module in modules) { - _modules.Add(new ResolvedModule(module.Base, module.Size, ResolveFile(module))); + _modules.Add(new ResolvedModule(module.Base, module.Size, module.IsMapped, ResolveFile(module))); } } diff --git a/src/native/managed/cdac/tests/TestInfrastructure/DumpTestBase.cs b/src/native/managed/cdac/tests/TestInfrastructure/DumpTestBase.cs index dd57e573719a91..3e9652bde1dced 100644 --- a/src/native/managed/cdac/tests/TestInfrastructure/DumpTestBase.cs +++ b/src/native/managed/cdac/tests/TestInfrastructure/DumpTestBase.cs @@ -179,9 +179,10 @@ private void RegisterManagedModules() { if (loader.IsDynamic(handle)) continue; - if (!loader.TryGetLoadedImageContents(handle, out TargetPointer baseAddress, out uint size, out _) + if (!loader.TryGetLoadedImageContents(handle, out TargetPointer baseAddress, out uint size, out uint imageFlags) || baseAddress == TargetPointer.Null || size == 0) continue; + bool isMapped = (imageFlags & 0x1) != 0; // FLAG_MAPPED string fileName = loader.GetPath(handle); if (string.IsNullOrEmpty(fileName)) @@ -193,7 +194,7 @@ private void RegisterManagedModules() // verify the on-disk file matches the module captured in the dump. loader.GetFileHeadersInfo(handle, out uint timeStamp, out uint imageSize); - modules.Add(new ClrMdDumpHost.ManagedModuleImage(baseAddress, size, fileName, timeStamp, imageSize)); + modules.Add(new ClrMdDumpHost.ManagedModuleImage(baseAddress, size, isMapped, fileName, timeStamp, imageSize)); } catch (System.Exception) { From 7b2489d869aa2f162b8635262ba4ded6de20dfd8 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Wed, 8 Jul 2026 20:12:27 -0400 Subject: [PATCH 13/13] cdac-lite: emit only the info needed to walk managed PEImages cdac-lite previously emitted DAC-faithful PE header regions (DOS/NT/COR/R2R headers, section table, and debug directory) for both the flat and loaded image layouts of every managed module. Now that the reader pages a module's data in from the on-disk assembly (located and key-verified via the Loader contract), the dump only needs enough to identify the module and reach its loaded image. Emit just the Module -> PEAssembly -> PEImage -> PEImageLayout locator chain (auto- emitted as the walk reads each struct) plus the loaded image's PE header page ([Base, SizeOfHeaders]) -- enough for a reader to read the module's identity key (COFF TimeDateStamp + OptionalHeader SizeOfImage) and section table. The COR/R2R headers, ECMA metadata, debug directory, code, and the flat layout are no longer emitted; a reader reads those from the on-disk image, exactly like the DAC. Also drops the now-unused PEImage.FlatImageLayout data descriptor field. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../debug/cdaclite/contracts/loader.cpp | 176 +++--------------- .../debug/cdaclite/data/runtimetypes.h | 1 - .../vm/datadescriptor/datadescriptor.inc | 1 - src/coreclr/vm/peimage.h | 2 - 4 files changed, 25 insertions(+), 155 deletions(-) diff --git a/src/coreclr/debug/cdaclite/contracts/loader.cpp b/src/coreclr/debug/cdaclite/contracts/loader.cpp index c04fdc96e2929a..2515f9abddaab7 100644 --- a/src/coreclr/debug/cdaclite/contracts/loader.cpp +++ b/src/coreclr/debug/cdaclite/contracts/loader.cpp @@ -107,57 +107,23 @@ namespace contracts int emitted; }; - // PE structure sizes on the target. The DAC emits each of these as a fixed struct via - // DPTR::EnumMem(), so we mirror the exact sizes rather than a coarse [Base, SizeOfHeaders]. - constexpr uint32_t kDosHeaderSize = 0x40; // sizeof(IMAGE_DOS_HEADER) - constexpr uint32_t kSectionHeaderSize = 40; // sizeof(IMAGE_SECTION_HEADER) - constexpr uint32_t kCor20HeaderSize = 72; // sizeof(IMAGE_COR20_HEADER) - constexpr uint32_t kReadyToRunHeaderSize = 16; // sizeof(READYTORUN_HEADER) - constexpr uint32_t kDebugDirEntrySize = 28; // sizeof(IMAGE_DEBUG_DIRECTORY) - constexpr uint32_t kReadyToRunSignature = 0x00525452; // 'RTR\0' - constexpr uint32_t kMaxSections = 96; - - // Translates an RVA to a target address within an image layout. For a mapped image - // (FLAG_MAPPED) RVA == offset-from-base. For a flat (raw file) image the RVA must be - // converted to a file offset by walking the section table, matching PEDecoder's - // RvaToAddr for a flat layout (and Loader_1.RvaToOffset in the managed cDAC). - uint64_t RvaToAddr(uint64_t base, uint32_t rva, bool isMapped, const uint8_t* sections, uint32_t numSections) - { - if (isMapped) - { - return base + rva; - } - for (uint32_t i = 0; i < numSections; i++) - { - const uint8_t* s = sections + (size_t)i * kSectionHeaderSize; - uint32_t va = 0, rawSize = 0, rawPtr = 0; - memcpy(&va, s + 12, sizeof(va)); // VirtualAddress - memcpy(&rawSize, s + 16, sizeof(rawSize)); // SizeOfRawData - memcpy(&rawPtr, s + 20, sizeof(rawPtr)); // PointerToRawData - if (rva >= va && rva < va + rawSize) - { - return base + (rva - va) + rawPtr; - } - } - return base + rva; - } - - // Mirrors PEDecoder::EnumMemoryRegions for a single image layout (pedecoder.cpp): emits the - // DOS header, NT headers, section table, COR (CLI) header, and R2R header -- the exact regions - // the legacy DAC records. When emitDebugDir is set (the loaded layout, per - // PEImage::EnumMemoryRegions) it also emits the debug directory and the blobs its entries - // point to (used to locate managed PDBs). The ECMA metadata and code bytes are deliberately - // NOT emitted; a reader reads those from the on-disk image, exactly like the DAC. Managed - // assemblies are PE on every platform, so this PE parse is valid on Linux/macOS. - void EmitPEDecoderRegions(const Target& target, uint64_t base, uint32_t flags, bool emitDebugDir) + // Emits ONLY the PE header page of a managed module's loaded image layout: the DOS header, + // NT headers, and section table (i.e. [Base, SizeOfHeaders]). This is the minimal + // information a reader needs to identify the module (COFF TimeDateStamp + OptionalHeader + // SizeOfImage form the PE identity key) and to parse its section table. Everything else -- + // the COR/R2R headers, ECMA metadata, debug directory, and code -- is deliberately NOT + // emitted; a reader pages those in from the on-disk assembly, exactly like the DAC does. + // createdump excludes module image content from heap/mini dumps by design (only Full dumps + // write m_moduleMappings), so cdac-lite re-emits just this header page. Managed assemblies + // are PE on every platform, so this parse is valid on Linux/macOS. + void EmitLoadedImageHeaders(const Target& target, uint64_t base) { if (base == 0) { return; } - const bool isMapped = (flags & 0x1) != 0; // FLAG_MAPPED - uint8_t dos[kDosHeaderSize]; + uint8_t dos[0x40]; // sizeof(IMAGE_DOS_HEADER) if (!target.ReadBuffer(base, dos, sizeof(dos)) || dos[0] != 'M' || dos[1] != 'Z') { return; @@ -165,113 +131,29 @@ namespace contracts uint32_t e_lfanew = 0; memcpy(&e_lfanew, dos + 0x3C, sizeof(e_lfanew)); - // PE signature(4) + COFF header(20) + optional header (PE32+: 240). Covers all data dirs. + // PE signature(4) + COFF header(20) + optional header. SizeOfHeaders lives at optional + // header offset 60 for both PE32 and PE32+. uint8_t peHdr[4 + 20 + 240]; if (!target.ReadBuffer(base + e_lfanew, peHdr, sizeof(peHdr)) || peHdr[0] != 'P' || peHdr[1] != 'E') { return; } + uint32_t sizeOfHeaders = 0; + memcpy(&sizeOfHeaders, peHdr + 4 + 20 + 60, sizeof(sizeOfHeaders)); - uint16_t numberOfSections = 0; - memcpy(&numberOfSections, peHdr + 4 + 2, sizeof(numberOfSections)); // IMAGE_FILE_HEADER.NumberOfSections - uint16_t sizeOfOptionalHeader = 0; - memcpy(&sizeOfOptionalHeader, peHdr + 4 + 16, sizeof(sizeOfOptionalHeader)); // IMAGE_FILE_HEADER.SizeOfOptionalHeader - const uint32_t optOffset = 4 + 20; // within peHdr - uint16_t optMagic = 0; - memcpy(&optMagic, peHdr + optOffset, sizeof(optMagic)); - const bool isPE32Plus = (optMagic == 0x20B); - - // DOS header + NT headers, exactly as PEDecoder::EnumMemoryRegions records them - // (DacEnumMemoryRegion(m_base, sizeof(IMAGE_DOS_HEADER)) then m_pNTHeaders.EnumMem()). - // createdump excludes module image content from heap dumps by design -- only Full dumps - // write m_moduleMappings (crashinfo.cpp GatherCrashInfo) -- so, just like the DAC, we - // re-emit these header structures into the dump; the ECMA metadata and code are read from - // the on-disk image. createdump page-rounds each emitted region, so these discrete regions - // land as the module's first header page. - target.EmitMemory(base, kDosHeaderSize); - target.EmitMemory(base + e_lfanew, 4 + 20 + (isPE32Plus ? 240u : 224u)); // sizeof(IMAGE_NT_HEADERS) - - // Section table (immediately after the optional header) -- also read for RVA->offset mapping. - const uint64_t firstSection = base + e_lfanew + optOffset + sizeOfOptionalHeader; - uint32_t numSec = numberOfSections; - if (numSec > kMaxSections) - { - numSec = kMaxSections; - } - uint8_t sections[kMaxSections * kSectionHeaderSize]; - if (numSec != 0 && !target.ReadBuffer(firstSection, sections, kSectionHeaderSize * numSec)) - { - numSec = 0; - } - if (numSec != 0) - { - target.EmitMemory(firstSection, kSectionHeaderSize * numSec); - } - - const uint32_t dataDirOffset = optOffset + (isPE32Plus ? 112 : 96); - - // COR (CLI) header (data directory 14 = IMAGE_DIRECTORY_ENTRY_COMHEADER). - uint32_t corRVA = 0; - memcpy(&corRVA, peHdr + dataDirOffset + 14 * 8, sizeof(corRVA)); - if (corRVA != 0) - { - uint64_t corAddr = RvaToAddr(base, corRVA, isMapped, sections, numSec); - target.EmitMemory(corAddr, kCor20HeaderSize); - - // R2R header via the COR header's ManagedNativeHeader directory (RVA @+64, Size @+68). - uint8_t cor[kCor20HeaderSize]; - if (target.ReadBuffer(corAddr, cor, sizeof(cor))) - { - uint32_t r2rRVA = 0, r2rSize = 0; - memcpy(&r2rRVA, cor + 64, sizeof(r2rRVA)); - memcpy(&r2rSize, cor + 68, sizeof(r2rSize)); - if (r2rRVA != 0 && r2rSize >= kReadyToRunHeaderSize) - { - uint64_t r2rAddr = RvaToAddr(base, r2rRVA, isMapped, sections, numSec); - uint32_t sig = 0; - if (target.ReadBuffer(r2rAddr, &sig, sizeof(sig)) && sig == kReadyToRunSignature) - { - target.EmitMemory(r2rAddr, kReadyToRunHeaderSize); - } - } - } - } - - // Debug directory (data directory 6): loaded layout only, matching PEImage::EnumMemoryRegions. - // Emits the directory plus each entry's raw data (e.g. CodeView records for managed PDBs). - if (emitDebugDir) + // The header page (DOS + NT headers + section table). createdump page-rounds the region, + // so [Base, SizeOfHeaders] lands as the module's first header page in the dump. + if (sizeOfHeaders != 0) { - uint32_t dbgRVA = 0, dbgSize = 0; - memcpy(&dbgRVA, peHdr + dataDirOffset + 6 * 8, sizeof(dbgRVA)); - memcpy(&dbgSize, peHdr + dataDirOffset + 6 * 8 + 4, sizeof(dbgSize)); - if (dbgRVA != 0 && dbgSize != 0) - { - uint64_t dbgAddr = RvaToAddr(base, dbgRVA, isMapped, sections, numSec); - target.EmitMemory(dbgAddr, dbgSize); - for (uint32_t i = 0; i < dbgSize / kDebugDirEntrySize; i++) - { - uint8_t entry[kDebugDirEntrySize]; - if (!target.ReadBuffer(dbgAddr + (uint64_t)i * kDebugDirEntrySize, entry, sizeof(entry))) - { - break; - } - uint32_t sizeOfData = 0, addrOfRawData = 0; - memcpy(&sizeOfData, entry + 16, sizeof(sizeOfData)); // IMAGE_DEBUG_DIRECTORY.SizeOfData - memcpy(&addrOfRawData, entry + 20, sizeof(addrOfRawData)); // .AddressOfRawData (RVA) - if (addrOfRawData != 0 && sizeOfData != 0) - { - target.EmitMemory(RvaToAddr(base, addrOfRawData, isMapped, sections, numSec), sizeOfData); - } - } - } + target.EmitMemory(base, sizeOfHeaders); } } - // Emit the module's metadata-locator chain: Module -> PEAssembly -> PEImage -> - // PEImageLayout, plus the PE header regions of the image, matching the legacy DAC. - // PEImage::EnumMemoryRegions (peimage.cpp) enumerates BOTH m_pLayouts[IMAGE_FLAT] and - // [IMAGE_LOADED] and additionally emits the debug directory for the loaded layout, so we - // do the same. The reader then reads ECMA metadata from the on-disk image via these headers. + // Emit the module's locator chain: Module -> PEAssembly -> PEImage -> PEImageLayout, plus + // the loaded image's PE header page. The struct reads auto-emit each structure into the + // dump (via the Target's EnumMem sink), giving a reader the path from a Module to the + // loaded image base. The reader then pages the ECMA metadata and code in from the on-disk + // assembly, so only the header page (needed to identify the module) is emitted here. void EmitModuleMetadataChain(const Target& target, const data::Module& module) { if (module.PEAssembly == 0) @@ -291,15 +173,7 @@ namespace contracts data::PEImageLayout loaded; if (target.TryRead(peImage.LoadedImageLayout, loaded)) // struct read -> auto-emitted { - EmitPEDecoderRegions(target, loaded.Base, (uint32_t)loaded.Flags, /*emitDebugDir*/ true); - } - if (peImage.FlatImageLayout != 0 && peImage.FlatImageLayout != peImage.LoadedImageLayout) - { - data::PEImageLayout flat; - if (target.TryRead(peImage.FlatImageLayout, flat)) // struct read -> auto-emitted - { - EmitPEDecoderRegions(target, flat.Base, (uint32_t)flat.Flags, /*emitDebugDir*/ false); - } + EmitLoadedImageHeaders(target, loaded.Base); } } diff --git a/src/coreclr/debug/cdaclite/data/runtimetypes.h b/src/coreclr/debug/cdaclite/data/runtimetypes.h index 4a9ad60bcef797..cfa32f15729a54 100644 --- a/src/coreclr/debug/cdaclite/data/runtimetypes.h +++ b/src/coreclr/debug/cdaclite/data/runtimetypes.h @@ -208,7 +208,6 @@ namespace data { PEImage() : Struct("PEImage") {} CDAC_PTR(LoadedImageLayout) - CDAC_OPT_PTR(FlatImageLayout) // m_pLayouts[IMAGE_FLAT], used for reading metadata }; struct PEImageLayout : Struct diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index e991cd0e556b96..8481dd43b88885 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -451,7 +451,6 @@ CDAC_TYPE_END(AssemblyBinder) CDAC_TYPE_BEGIN(PEImage) CDAC_TYPE_INDETERMINATE(PEImage) CDAC_TYPE_FIELD(PEImage, T_POINTER, LoadedImageLayout, cdac_data::LoadedImageLayout) -CDAC_TYPE_FIELD(PEImage, T_POINTER, FlatImageLayout, cdac_data::FlatImageLayout) CDAC_TYPE_FIELD(PEImage, TYPE(ProbeExtensionResult), ProbeExtensionResult, cdac_data::ProbeExtensionResult) CDAC_TYPE_END(PEImage) diff --git a/src/coreclr/vm/peimage.h b/src/coreclr/vm/peimage.h index 72468d4525b6a4..ea56c0d8d594c7 100644 --- a/src/coreclr/vm/peimage.h +++ b/src/coreclr/vm/peimage.h @@ -326,8 +326,6 @@ class PEImage final template<> struct cdac_data { - // The flat PEImageLayout is m_pLayouts[IMAGE_FLAT] (used for reading metadata). - static constexpr size_t FlatImageLayout = offsetof(PEImage, m_pLayouts); // The loaded PEImageLayout is m_pLayouts[IMAGE_LOADED] static constexpr size_t LoadedImageLayout = offsetof(PEImage, m_pLayouts) + sizeof(PTR_PEImageLayout); static constexpr size_t ProbeExtensionResult = offsetof(PEImage, m_probeExtensionResult);