diff --git a/docs/design/datacontracts/RuntimeTypeSystem.md b/docs/design/datacontracts/RuntimeTypeSystem.md index d153b01d5776a9..1a0b748ab0b03b 100644 --- a/docs/design/datacontracts/RuntimeTypeSystem.md +++ b/docs/design/datacontracts/RuntimeTypeSystem.md @@ -589,17 +589,7 @@ static class RuntimeTypeSystem_1_Helpers | `InstantiatedMethodDesc` | `PerInstInfo` | `pointer` | The pointer to the method's type arguments | | `LayoutEEClass` | `LayoutInfo` | `EEClassLayoutInfo` | Inline `EEClassLayoutInfo` for a type with layout. Only the offset is used - the reader constructs an `EEClassLayoutInfo` at that offset from the `EEClass` address. Only valid when `EEClass.VMFlags` has `VMFLAG_HASLAYOUT` | | `LoaderAllocator` | `CreationNumber` | `uint64` | Monotonically-increasing creation number assigned to each collectible LoaderAllocator. | -| `LoaderAllocator` | `DynamicHelpersStubHeap` | `pointer` | Dynamic-helper stub heap (optional, present when ReadyToRun dynamic-helper stubs are enabled) | -| `LoaderAllocator` | `ExecutableHeap` | `pointer` | Executable-code heap | -| `LoaderAllocator` | `FixupPrecodeHeap` | `pointer` | Fixup-precode heap (optional, present when fixup precodes are supported) | -| `LoaderAllocator` | `HighFrequencyHeap` | `pointer` | High-frequency allocation heap | | `LoaderAllocator` | `IsCollectible` | `uint8` | Non-zero if the LoaderAllocator is collectible. | -| `LoaderAllocator` | `LowFrequencyHeap` | `pointer` | Low-frequency allocation heap | -| `LoaderAllocator` | `NewStubPrecodeHeap` | `pointer` | New-stub-precode heap (optional, absent with portable entry points) | -| `LoaderAllocator` | `ObjectHandle` | `ObjectHandle` | Handle to the managed loader allocator object | -| `LoaderAllocator` | `ReferenceCount` | `uint32` | Reference count of the loader allocator | -| `LoaderAllocator` | `StaticsHeap` | `pointer` | Heap containing statics-related allocations | -| `LoaderAllocator` | `VirtualCallStubManager` | `pointer` | Pointer to the virtual-call stub manager | | `MethodDesc` | *(type size)* | `uint32` | Base size for mcIL classification | | `MethodDesc` | `ChunkIndex` | `uint8` | Offset of this MethodDesc relative to the end of its containing MethodDescChunk - in multiples of MethodDescAlignment | | `MethodDesc` | `CodeData` | `pointer` | Pointer to per-method code data containing entry-point and code-versioning state | @@ -607,7 +597,6 @@ static class RuntimeTypeSystem_1_Helpers | `MethodDesc` | `Flags` | `uint16` | The method's flags | | `MethodDesc` | `Flags3AndTokenRemainder` | `uint16` | More flags for the method, and the low bits of the method's token's RID | | `MethodDesc` | `GCCoverageInfo` | `pointer` | The method's GCCover debug info, if supported | -| `MethodDesc` | `InterpreterCode` | `pointer` | Pointer to the method's `InterpByteCodeStart`, or the poison value 1 if the method will never be interpreted (only defined if `FEATURE_INTERPRETER` is enabled) | | `MethodDesc` | `Slot` | `uint16` | The method's slot | | `MethodDescChunk` | *(type size)* | `uint32` | Size of the data descriptor layout | | `MethodDescChunk` | `Count` | `uint8` | The number of MethodDesc entries in this chunk, minus 1. | @@ -636,7 +625,6 @@ static class RuntimeTypeSystem_1_Helpers | `NativeCodeSlot` | *(type size)* | `uint32` | Size of the native code slot, added when HasNativeCodeSlot flag is set | | `NonVtableSlot` | *(type size)* | `uint32` | Size of the non-vtable slot, added when HasNonVtableSlot flag is set | | `Object` | *(type size)* | `uint32` | Size in bytes of the fixed Object portion through its MethodTable pointer | -| `Object` | `m_pMethTab` | `pointer` | Method table for the object | | `ParamTypeDesc` | `TypeArg` | `pointer` | Associated type argument | | `PInvokeMethodDesc` | *(type size)* | `uint32` | Base size for mcPInvoke classification | | `Signature` | `SignatureLength` | `uint32` | Length in bytes of the raw signature blob. | diff --git a/eng/pipelines/diagnostics/sos-test-leg.yml b/eng/pipelines/diagnostics/sos-test-leg.yml index 92d7aa9b9febd1..3c71eaef25b8ad 100644 --- a/eng/pipelines/diagnostics/sos-test-leg.yml +++ b/eng/pipelines/diagnostics/sos-test-leg.yml @@ -25,6 +25,8 @@ parameters: templatePath: 'templates' templateContext: '' disableComponentGovernance: '' + artifactSuffix: '' + useCdacDumpCollectProvider: false jobs: - template: /eng/common/${{ parameters.templatePath }}/job/job.yml @@ -94,15 +96,15 @@ jobs: - template: /eng/pipelines/common/download-artifact-step.yml parameters: - artifactName: SosRuntimeArtifacts_$(osGroup)$(osSubgroup)_$(archType) - artifactFileName: SosRuntimeArtifacts_$(osGroup)$(osSubgroup)_$(archType)$(archiveExtension) + artifactName: SosRuntimeArtifacts_$(osGroup)$(osSubgroup)_$(archType)${{ parameters.artifactSuffix }} + artifactFileName: SosRuntimeArtifacts_$(osGroup)$(osSubgroup)_$(archType)${{ parameters.artifactSuffix }}$(archiveExtension) unpackFolder: $(Build.SourcesDirectory)/artifacts/runtime displayName: 'SOS Runtime Artifacts' - template: /eng/pipelines/common/download-artifact-step.yml parameters: - artifactName: SosCdacArtifacts_$(osGroup)$(osSubgroup)_$(archType) - artifactFileName: SosCdacArtifacts_$(osGroup)$(osSubgroup)_$(archType)$(archiveExtension) + artifactName: SosCdacArtifacts_$(osGroup)$(osSubgroup)_$(archType)${{ parameters.artifactSuffix }} + artifactFileName: SosCdacArtifacts_$(osGroup)$(osSubgroup)_$(archType)${{ parameters.artifactSuffix }}$(archiveExtension) unpackFolder: $(Build.SourcesDirectory)/artifacts/private-cdac displayName: 'SOS cDAC Artifacts' @@ -203,6 +205,12 @@ jobs: HelixConfiguration: ${{ parameters.targetRid }}_${{ parameters.buildConfig }} HelixTargetQueues: $(SOSHelixQueue) SYSTEM_ACCESSTOKEN: $(System.AccessToken) + # The enumerator replaces the legacy DAC; select cDAC on the Helix worker. + ${{ if eq(parameters.useCdacDumpCollectProvider, true) }}: + ${{ if eq(parameters.osGroup, 'windows') }}: + HelixPreCommands: set SOSHARNESS_ONLY_DAC=CDac + ${{ else }}: + HelixPreCommands: export SOSHARNESS_ONLY_DAC=CDac ${{ if eq(variables['System.TeamProject'], 'public') }}: Creator: dotnet-bot ${{ else }}: diff --git a/eng/pipelines/runtime-diagnostics.yml b/eng/pipelines/runtime-diagnostics.yml index 3648b7f6e4440a..75fd5db4cc0f18 100644 --- a/eng/pipelines/runtime-diagnostics.yml +++ b/eng/pipelines/runtime-diagnostics.yml @@ -6,9 +6,10 @@ # UnitTests + DataGeneratorTests projects (tools.cdactests # subset). Independent of every other stage. # -# SOSTests Per-platform Release runtime + cDAC build and SOS Helix -# submission. SOS.Tests covers both legacy DAC and cDAC -# modes. Independent of every other stage. +# SOSTests Per-platform Release builds with the legacy DAC and the +# EnumMemory provider, each with a matching SOS Helix submission. +# Legacy builds cover both DAC modes; EnumMemory uses cDAC only. +# Independent of every other stage. # # CdacTests Per-platform Checked runtime build + Dump / Stress / # XPlatDumpGen / XPlatDumpTest legs in one stage. Each dump @@ -146,81 +147,85 @@ extends: testResultsFormat: 'xunit' # ---------------------------------------------------------------------- - # SOS tests. Each platform builds a Release runtime and matching universal - # cDAC/DBI, then submits the diagnostics SOS.Tests payload to Helix. + # SOS tests. Each platform builds legacy and EnumMemory Release runtimes with + # matching universal cDAC/DBI, then submits each SOS.Tests payload to Helix. # ---------------------------------------------------------------------- - stage: SOSTests dependsOn: [] jobs: - - template: /eng/pipelines/common/platform-matrix.yml - parameters: - jobTemplate: /eng/pipelines/common/global-build-job.yml - buildConfig: release - platforms: ${{ parameters.sosPlatforms }} - jobParameters: - nameSuffix: SOSBuild - buildArgs: -s clr+libs+tools.cdac+host -c $(_BuildConfig) -rc $(_BuildConfig) -lc $(_BuildConfig) - isOfficialBuild: ${{ variables.isOfficialBuild }} - timeoutInMinutes: 180 - postBuildSteps: - - pwsh: | - $runtimeDirs = @(Get-ChildItem -Directory -Path "$(Build.SourcesDirectory)/artifacts/bin/testhost/net*-$(osGroup)-$(_BuildConfig)-$(archType)/shared/Microsoft.NETCore.App/*") - if ($runtimeDirs.Count -ne 1) { - throw "Expected one SOS runtime directory, but found $($runtimeDirs.Count)." - } - - Write-Host "##vso[task.setvariable variable=sosRuntimeDir]$($runtimeDirs[0].FullName)" + # An empty suffix preserves the standard build and artifact names. + - ${{ each suffix in split(',_EnumMemory', ',') }}: + - template: /eng/pipelines/common/platform-matrix.yml + parameters: + jobTemplate: /eng/pipelines/common/global-build-job.yml + buildConfig: release + platforms: ${{ parameters.sosPlatforms }} + jobParameters: + nameSuffix: SOSBuild${{ suffix }} + buildArgs: -s clr+libs+tools.cdac+host -c $(_BuildConfig) -rc $(_BuildConfig) -lc $(_BuildConfig) /p:UseCdacDumpCollectProvider=${{ eq(suffix, '_EnumMemory') }} + isOfficialBuild: ${{ variables.isOfficialBuild }} + timeoutInMinutes: 180 + postBuildSteps: + - pwsh: | + $runtimeDirs = @(Get-ChildItem -Directory -Path "$(Build.SourcesDirectory)/artifacts/bin/testhost/net*-$(osGroup)-$(_BuildConfig)-$(archType)/shared/Microsoft.NETCore.App/*") + if ($runtimeDirs.Count -ne 1) { + throw "Expected one SOS runtime directory, but found $($runtimeDirs.Count)." + } - $sourceDir = "$(Build.SourcesDirectory)/artifacts/bin/coreclr/$(osGroup).$(archType).$(_BuildConfig)" - $stagingDir = "$(Build.SourcesDirectory)/artifacts/sos-cdac" - New-Item -ItemType Directory -Force -Path $stagingDir | Out-Null + Write-Host "##vso[task.setvariable variable=sosRuntimeDir]$($runtimeDirs[0].FullName)" - $fileNames = switch ("$(osGroup)") { - "windows" { "mscordaccore_universal.dll", "mscordbi_universal.dll" } - "linux" { "libmscordaccore_universal.so", "libmscordbi_universal.so" } - "osx" { "libmscordaccore_universal.dylib", "libmscordbi_universal.dylib" } - default { throw "Unsupported OS group '$(osGroup)'." } - } + $sourceDir = "$(Build.SourcesDirectory)/artifacts/bin/coreclr/$(osGroup).$(archType).$(_BuildConfig)" + $stagingDir = "$(Build.SourcesDirectory)/artifacts/sos-cdac" + New-Item -ItemType Directory -Force -Path $stagingDir | Out-Null - foreach ($fileName in $fileNames) { - $source = Join-Path $sourceDir $fileName - if (-not (Test-Path -PathType Leaf $source)) { - throw "Required private cDAC file '$source' was not found." + $fileNames = switch ("$(osGroup)") { + "windows" { "mscordaccore_universal.dll", "mscordbi_universal.dll" } + "linux" { "libmscordaccore_universal.so", "libmscordbi_universal.so" } + "osx" { "libmscordaccore_universal.dylib", "libmscordbi_universal.dylib" } + default { throw "Unsupported OS group '$(osGroup)'." } } - Copy-Item -Path $source -Destination $stagingDir - } - displayName: 'Find SOS Runtime and cDAC' - - template: /eng/pipelines/common/upload-artifact-step.yml - parameters: - rootFolder: $(sosRuntimeDir) - includeRootFolder: false - archiveType: $(archiveType) - archiveExtension: $(archiveExtension) - tarCompression: $(tarCompression) - artifactName: SosRuntimeArtifacts_$(osGroup)$(osSubgroup)_$(archType) - displayName: SOS Runtime Artifacts - - template: /eng/pipelines/common/upload-artifact-step.yml - parameters: - rootFolder: $(Build.SourcesDirectory)/artifacts/sos-cdac - includeRootFolder: false - archiveType: $(archiveType) - archiveExtension: $(archiveExtension) - tarCompression: $(tarCompression) - artifactName: SosCdacArtifacts_$(osGroup)$(osSubgroup)_$(archType) - displayName: SOS cDAC Artifacts + foreach ($fileName in $fileNames) { + $source = Join-Path $sourceDir $fileName + if (-not (Test-Path -PathType Leaf $source)) { + throw "Required private cDAC file '$source' was not found." + } - - template: /eng/pipelines/common/platform-matrix.yml - parameters: - jobTemplate: /eng/pipelines/diagnostics/sos-test-leg.yml - buildConfig: release - platforms: ${{ parameters.sosPlatforms }} - jobParameters: - nameSuffix: SOS - isOfficialBuild: ${{ variables.isOfficialBuild }} - timeoutInMinutes: 360 - dependsOnGlobalBuilds: - - nameSuffix: SOSBuild + Copy-Item -Path $source -Destination $stagingDir + } + displayName: 'Find SOS Runtime and cDAC' + - template: /eng/pipelines/common/upload-artifact-step.yml + parameters: + rootFolder: $(sosRuntimeDir) + includeRootFolder: false + archiveType: $(archiveType) + archiveExtension: $(archiveExtension) + tarCompression: $(tarCompression) + artifactName: SosRuntimeArtifacts_$(osGroup)$(osSubgroup)_$(archType)${{ suffix }} + displayName: SOS Runtime Artifacts + - template: /eng/pipelines/common/upload-artifact-step.yml + parameters: + rootFolder: $(Build.SourcesDirectory)/artifacts/sos-cdac + includeRootFolder: false + archiveType: $(archiveType) + archiveExtension: $(archiveExtension) + tarCompression: $(tarCompression) + artifactName: SosCdacArtifacts_$(osGroup)$(osSubgroup)_$(archType)${{ suffix }} + displayName: SOS cDAC Artifacts + + - template: /eng/pipelines/common/platform-matrix.yml + parameters: + jobTemplate: /eng/pipelines/diagnostics/sos-test-leg.yml + buildConfig: release + platforms: ${{ parameters.sosPlatforms }} + jobParameters: + nameSuffix: SOS${{ suffix }} + artifactSuffix: ${{ suffix }} + useCdacDumpCollectProvider: ${{ eq(suffix, '_EnumMemory') }} + isOfficialBuild: ${{ variables.isOfficialBuild }} + timeoutInMinutes: 360 + dependsOnGlobalBuilds: + - nameSuffix: SOSBuild${{ suffix }} # ---------------------------------------------------------------------- # cDAC dump + stress + xplat-dump-gen tests. @@ -248,7 +253,7 @@ extends: platforms: ${{ parameters.cdacDumpPlatforms }} jobParameters: nameSuffix: Build - buildArgs: -s clr+libs+tools.cdac+tools.cdacdumptests+tools.cdacstresstests -c $(_BuildConfig) -rc checked -lc $(_BuildConfig) /p:SkipDumpVersions=net10.0 + buildArgs: -s clr+libs+tools.cdac+tools.cdacdumptests+tools.cdacstresstests -c $(_BuildConfig) -rc checked -lc $(_BuildConfig) /p:SkipDumpVersions=net10.0 /p:UseCdacDumpCollectProvider=true isOfficialBuild: ${{ variables.isOfficialBuild }} timeoutInMinutes: 180 postBuildSteps: diff --git a/src/coreclr/debug/createdump/crashinfo.cpp b/src/coreclr/debug/createdump/crashinfo.cpp index 4a40c6d38630d0..2cd0ad07c820cb 100644 --- a/src/coreclr/debug/createdump/crashinfo.cpp +++ b/src/coreclr/debug/createdump/crashinfo.cpp @@ -23,6 +23,8 @@ CrashInfo::CrashInfo(const CreateDumpOptions& options) : m_crashThread(options.CrashThread), m_signal(options.Signal), m_exceptionRecord(options.ExceptionRecord), + m_runtimeBaseAddress(0), + m_contractDescriptorAddress(0), m_moduleInfos(&ModuleInfoCompare), m_mainModule(nullptr), m_cbModuleMappings(0), @@ -30,7 +32,6 @@ CrashInfo::CrashInfo(const CreateDumpOptions& options) : m_enumMemoryPagesAdded(0) { g_crashInfo = this; - m_runtimeBaseAddress = 0; #ifdef __APPLE__ m_task = 0; #else diff --git a/src/coreclr/debug/createdump/crashinfo.h b/src/coreclr/debug/createdump/crashinfo.h index 7a51cceb81d449..0566a519ef4dcc 100644 --- a/src/coreclr/debug/createdump/crashinfo.h +++ b/src/coreclr/debug/createdump/crashinfo.h @@ -62,6 +62,7 @@ class CrashInfo : public ICLRDataEnumMemoryRegionsCallback, public ICLRDataLoggi siginfo_t m_siginfo; // signal info (if any) std::string m_coreclrPath; // the path of the coreclr module or empty if none uint64_t m_runtimeBaseAddress; // base address of the runtime module + uint64_t m_contractDescriptorAddress; // address of the runtime contract descriptor #ifdef __APPLE__ vm_map_t m_task; // the mach task for the process std::set m_allMemoryRegions; // all memory regions on MacOS @@ -123,6 +124,7 @@ class CrashInfo : public ICLRDataEnumMemoryRegionsCallback, public ICLRDataLoggi inline const std::string& Name() const { return m_name; } inline const ModuleInfo* MainModule() const { return m_mainModule; } inline const uint64_t RuntimeBaseAddress() const { return m_runtimeBaseAddress; } + inline const uint64_t ContractDescriptorAddress() const { return m_contractDescriptorAddress; } inline const std::vector& Threads() const { return m_threads; } inline const std::set& ModuleMappings() const { return m_moduleMappings; } diff --git a/src/coreclr/debug/createdump/crashinfomac.cpp b/src/coreclr/debug/createdump/crashinfomac.cpp index d99a8b9878bde6..ad3049ac704237 100644 --- a/src/coreclr/debug/createdump/crashinfomac.cpp +++ b/src/coreclr/debug/createdump/crashinfomac.cpp @@ -244,17 +244,26 @@ void CrashInfo::VisitModule(MachOModule& module) { TRACE("TryLookupSymbol(" DACCESS_TABLE_SYMBOL ") FAILED\n"); } + if (module.TryLookupSymbol(CONTRACT_DESCRIPTOR_SYMBOL, &symbolAddress)) + { + m_contractDescriptorAddress = symbolAddress; + } } else if (m_appModel == AppModelType::SingleFile) { - uint64_t symbolAddress; - if (module.TryLookupSymbol("DotNetRuntimeInfo", &symbolAddress)) + uint64_t runtimeInfoAddress; + if (module.TryLookupSymbol("DotNetRuntimeInfo", &runtimeInfoAddress)) { m_coreclrPath = GetDirectory(module.Name()); m_runtimeBaseAddress = module.BaseAddress(); + uint64_t contractDescriptorAddress; + if (module.TryLookupSymbol(CONTRACT_DESCRIPTOR_SYMBOL, &contractDescriptorAddress)) + { + m_contractDescriptorAddress = contractDescriptorAddress; + } RuntimeInfo runtimeInfo { }; - if (ReadMemory(symbolAddress, &runtimeInfo, sizeof(RuntimeInfo))) + if (ReadMemory(runtimeInfoAddress, &runtimeInfo, sizeof(RuntimeInfo))) { if (strcmp(runtimeInfo.Signature, RUNTIME_INFO_SIGNATURE) == 0) { @@ -266,10 +275,11 @@ void CrashInfo::VisitModule(MachOModule& module) else if (m_appModel == AppModelType::NativeAOT) { uint64_t symbolAddress; - if (module.TryLookupSymbol("DotNetRuntimeContractDescriptor", &symbolAddress)) + if (module.TryLookupSymbol(CONTRACT_DESCRIPTOR_SYMBOL, &symbolAddress)) { m_coreclrPath = GetDirectory(module.Name()); m_runtimeBaseAddress = module.BaseAddress(); + m_contractDescriptorAddress = symbolAddress; TRACE("Found valid NativeAOT runtime module\n"); } } diff --git a/src/coreclr/debug/createdump/crashinfounix.cpp b/src/coreclr/debug/createdump/crashinfounix.cpp index 854301453f4824..ece67c566972ec 100644 --- a/src/coreclr/debug/createdump/crashinfounix.cpp +++ b/src/coreclr/debug/createdump/crashinfounix.cpp @@ -372,21 +372,30 @@ CrashInfo::VisitModule(uint64_t baseAddress, std::string& moduleName) { TRACE("TryLookupSymbol(" DACCESS_TABLE_SYMBOL ") FAILED\n"); } + if (TryLookupSymbol(CONTRACT_DESCRIPTOR_SYMBOL, &symbolOffset)) + { + m_contractDescriptorAddress = baseAddress + symbolOffset; + } } } else if (m_appModel == AppModelType::SingleFile) { if (PopulateForSymbolLookup(baseAddress)) { - uint64_t symbolOffset; - if (TryLookupSymbol("DotNetRuntimeInfo", &symbolOffset)) + uint64_t runtimeInfoOffset; + if (TryLookupSymbol("DotNetRuntimeInfo", &runtimeInfoOffset)) { m_coreclrPath = GetDirectory(moduleName); m_runtimeBaseAddress = baseAddress; + uint64_t contractDescriptorOffset; + if (TryLookupSymbol(CONTRACT_DESCRIPTOR_SYMBOL, &contractDescriptorOffset)) + { + m_contractDescriptorAddress = baseAddress + contractDescriptorOffset; + } // explicit initialization for old gcc support; instead of just runtimeInfo { } RuntimeInfo runtimeInfo { .Signature = { }, .Version = 0, .RuntimeModuleIndex = { }, .DacModuleIndex = { }, .DbiModuleIndex = { }, .RuntimeVersion = { } }; - if (ReadMemory(baseAddress + symbolOffset, &runtimeInfo, sizeof(RuntimeInfo))) + if (ReadMemory(baseAddress + runtimeInfoOffset, &runtimeInfo, sizeof(RuntimeInfo))) { if (strcmp(runtimeInfo.Signature, RUNTIME_INFO_SIGNATURE) == 0) { @@ -401,10 +410,11 @@ CrashInfo::VisitModule(uint64_t baseAddress, std::string& moduleName) if (PopulateForSymbolLookup(baseAddress)) { uint64_t symbolOffset; - if (TryLookupSymbol("DotNetRuntimeContractDescriptor", &symbolOffset)) + if (TryLookupSymbol(CONTRACT_DESCRIPTOR_SYMBOL, &symbolOffset)) { m_coreclrPath = GetDirectory(moduleName); m_runtimeBaseAddress = baseAddress; + m_contractDescriptorAddress = baseAddress + symbolOffset; TRACE("Found valid NativeAOT runtime module\n"); } } diff --git a/src/coreclr/debug/createdump/createdump.h b/src/coreclr/debug/createdump/createdump.h index d647ec49502d1e..c3d43b5db4a9cb 100644 --- a/src/coreclr/debug/createdump/createdump.h +++ b/src/coreclr/debug/createdump/createdump.h @@ -23,6 +23,7 @@ extern void trace_verbose_printf(const char* format, ...) MINIPAL_ATTR_FORMAT_PR // Keep in sync with the definitions in dbgutil.cpp and daccess.h #define DACCESS_TABLE_SYMBOL "g_dacTable" +#define CONTRACT_DESCRIPTOR_SYMBOL "DotNetRuntimeContractDescriptor" #ifdef HOST_64BIT #define PRIA "016" diff --git a/src/coreclr/debug/createdump/datatarget.cpp b/src/coreclr/debug/createdump/datatarget.cpp index 7867967e23777e..af3a652e5c8f3c 100644 --- a/src/coreclr/debug/createdump/datatarget.cpp +++ b/src/coreclr/debug/createdump/datatarget.cpp @@ -33,6 +33,12 @@ DumpDataTarget::QueryInterface( AddRef(); return S_OK; } + else if (InterfaceId == IID_ICLRContractLocator) + { + *Interface = static_cast(this); + AddRef(); + return S_OK; + } else { *Interface = NULL; @@ -225,3 +231,17 @@ DumpDataTarget::GetRuntimeBase( *baseAddress = m_crashInfo.RuntimeBaseAddress(); return S_OK; } + +// ICLRContractLocator + +HRESULT STDMETHODCALLTYPE +DumpDataTarget::GetContractDescriptor( + /* [out] */ CLRDATA_ADDRESS* contractAddress) +{ + if (contractAddress == nullptr) + { + return E_POINTER; + } + *contractAddress = m_crashInfo.ContractDescriptorAddress(); + return *contractAddress != 0 ? S_OK : E_FAIL; +} diff --git a/src/coreclr/debug/createdump/datatarget.h b/src/coreclr/debug/createdump/datatarget.h index 4ad3816c16013e..010ddd7fe84245 100644 --- a/src/coreclr/debug/createdump/datatarget.h +++ b/src/coreclr/debug/createdump/datatarget.h @@ -3,7 +3,7 @@ class CrashInfo; -class DumpDataTarget : public ICLRDataTarget, ICLRRuntimeLocator +class DumpDataTarget : public ICLRDataTarget, ICLRRuntimeLocator, ICLRContractLocator { private: LONG m_ref; // reference count @@ -84,4 +84,9 @@ class DumpDataTarget : public ICLRDataTarget, ICLRRuntimeLocator virtual HRESULT STDMETHODCALLTYPE GetRuntimeBase( /* [out] */ CLRDATA_ADDRESS* baseAddress); + + // ICLRContractLocator + + virtual HRESULT STDMETHODCALLTYPE GetContractDescriptor( + /* [out] */ CLRDATA_ADDRESS* contractAddress); }; diff --git a/src/coreclr/debug/runtimeinfo/CMakeLists.txt b/src/coreclr/debug/runtimeinfo/CMakeLists.txt index a31d4b6f9812c3..330f6de0153bc3 100644 --- a/src/coreclr/debug/runtimeinfo/CMakeLists.txt +++ b/src/coreclr/debug/runtimeinfo/CMakeLists.txt @@ -32,8 +32,12 @@ endfunction(generate_module_index) if(NOT DEFINED CLR_CROSS_COMPONENTS_BUILD) generate_module_index(coreclr ${CMAKE_CURRENT_BINARY_DIR}/runtimemoduleindex.h) - generate_module_index(mscordaccore ${CMAKE_CURRENT_BINARY_DIR}/dacmoduleindex.h) - generate_module_index(mscordbi ${CMAKE_CURRENT_BINARY_DIR}/dbimoduleindex.h) + if(CLR_DUMP_ENUMERATOR_PATH) + target_compile_definitions(runtimeinfo PRIVATE OMIT_DAC_DBI_INDEX) + else() + generate_module_index(mscordaccore ${CMAKE_CURRENT_BINARY_DIR}/dacmoduleindex.h) + generate_module_index(mscordbi ${CMAKE_CURRENT_BINARY_DIR}/dbimoduleindex.h) + endif() endif() # publish runtimeinfo lib diff --git a/src/coreclr/debug/runtimeinfo/runtimeinfo.cpp b/src/coreclr/debug/runtimeinfo/runtimeinfo.cpp index 7dfd0c2afd4b85..2dfe5dd98a2215 100644 --- a/src/coreclr/debug/runtimeinfo/runtimeinfo.cpp +++ b/src/coreclr/debug/runtimeinfo/runtimeinfo.cpp @@ -25,10 +25,14 @@ RuntimeInfo DotNetRuntimeInfo = { #include }, { +#ifndef OMIT_DAC_DBI_INDEX #include +#endif }, { +#ifndef OMIT_DAC_DBI_INDEX #include +#endif }, { RuntimeFileMajorVersion, RuntimeFileMinorVersion, RuntimeFileBuildVersion, RuntimeFileRevisionVersion diff --git a/src/coreclr/dlls/CMakeLists.txt b/src/coreclr/dlls/CMakeLists.txt index b28ef7a0338f92..5ef0c077e5c37f 100644 --- a/src/coreclr/dlls/CMakeLists.txt +++ b/src/coreclr/dlls/CMakeLists.txt @@ -5,9 +5,13 @@ if(CLR_CMAKE_TARGET_WIN32 AND FEATURE_EVENT_TRACE) endif(CLR_CMAKE_TARGET_WIN32 AND FEATURE_EVENT_TRACE) if (NOT (CLR_CMAKE_TARGET_WIN32 AND FEATURE_CROSSBITNESS)) if (NOT CLR_CMAKE_TARGET_ARCH_WASM) - add_subdirectory(mscordbi) add_subdirectory(mscordbi_universal) - add_subdirectory(mscordac) + + # Only build the legacy DAC and DBI when not using the cDAC memory enumerator. + if (NOT CLR_DUMP_ENUMERATOR_PATH) + add_subdirectory(mscordbi) + add_subdirectory(mscordac) + endif() endif() add_subdirectory(mscoree) endif() diff --git a/src/coreclr/dlls/mscoree/coreclr/CMakeLists.txt b/src/coreclr/dlls/mscoree/coreclr/CMakeLists.txt index d510519874a91a..48490aa5b0d36c 100644 --- a/src/coreclr/dlls/mscoree/coreclr/CMakeLists.txt +++ b/src/coreclr/dlls/mscoree/coreclr/CMakeLists.txt @@ -237,24 +237,41 @@ if(CLR_CMAKE_TARGET_WIN32) set(CLRDEBUGINFO_RESOURCE_NAME CLRDEBUGINFOWINDOWSARM64) endif(CLR_CMAKE_TARGET_ARCH_AMD64) + if(NOT CLR_DUMP_ENUMERATOR_PATH) + set(CLRDEBUGINFO_DEPENDENCIES mscordaccore mscordbi) + set(CLRDEBUGINFO_ARGUMENTS -dac $ -dbi $) + + set(MINIDUMP_AUXILIARY_PROVIDER_PATH $) + set(MINIDUMP_AUXILIARY_PROVIDER_NAME "mscordaccore.dll") + else() + set(CLRDEBUGINFO_ARGUMENTS -noNativeDacDbi) + file(TO_CMAKE_PATH "${CLR_DUMP_ENUMERATOR_PATH}" MINIDUMP_AUXILIARY_PROVIDER_PATH) + get_filename_component(MINIDUMP_AUXILIARY_PROVIDER_NAME "${CLR_DUMP_ENUMERATOR_PATH}" NAME) + endif() + add_custom_target( clr_debug_resources - DEPENDS mscordaccore mscordbi - # make CLRDEBUGINFO resource - COMMAND powershell -NoProfile -ExecutionPolicy ByPass -File "${CMAKE_CURRENT_SOURCE_DIR}/GenClrDebugResource.ps1" -dac $ -dbi $ -out ${CLRDEBUGINFO_RESOURCE_PATH} + DEPENDS ${CLRDEBUGINFO_DEPENDENCIES} + BYPRODUCTS ${CLRDEBUGINFO_RESOURCE_PATH} + COMMAND powershell -NoProfile -ExecutionPolicy ByPass -File "${CMAKE_CURRENT_SOURCE_DIR}/GenClrDebugResource.ps1" ${CLRDEBUGINFO_ARGUMENTS} -out ${CLRDEBUGINFO_RESOURCE_PATH} ) + add_dependencies(coreclr clr_debug_resources) + add_dependencies(coreclr_static clr_debug_resources) configure_file(dump_helper_resource.rc.in ${CMAKE_CURRENT_BINARY_DIR}/dump_helper_resource.rc) - set(EMBEDDED_MINIDUMP_AUXILIARY_PROVIDER $) + set(EMBEDDED_MINIDUMP_AUXILIARY_PROVIDER "${MINIDUMP_AUXILIARY_PROVIDER_PATH}") configure_file(dump_helper_resource.rc.in ${CMAKE_CURRENT_BINARY_DIR}/dump_helper_resource_embedded_minidump_provider.rc.gen) file(GENERATE OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/$/dump_helper_resource_embedded_minidump_provider.rc INPUT ${CMAKE_CURRENT_BINARY_DIR}/dump_helper_resource_embedded_minidump_provider.rc.gen) + set(CLRDEBUGINFO_RESOURCE_SOURCES ${CMAKE_CURRENT_BINARY_DIR}/dump_helper_resource.rc) + foreach(CONFIGURATION IN LISTS CMAKE_CONFIGURATION_TYPES CMAKE_BUILD_TYPE) + list(APPEND CLRDEBUGINFO_RESOURCE_SOURCES ${CMAKE_CURRENT_BINARY_DIR}/${CONFIGURATION}/dump_helper_resource_embedded_minidump_provider.rc) + endforeach() + set_source_files_properties(${CLRDEBUGINFO_RESOURCE_SOURCES} PROPERTIES OBJECT_DEPENDS ${CLRDEBUGINFO_RESOURCE_PATH}) + target_sources(coreclr PUBLIC ${CMAKE_CURRENT_BINARY_DIR}/dump_helper_resource.rc) target_sources(coreclr_static PUBLIC ${CMAKE_CURRENT_BINARY_DIR}/$/dump_helper_resource_embedded_minidump_provider.rc) - - add_dependencies(coreclr clr_debug_resources) - add_dependencies(coreclr_static clr_debug_resources mscordaccore) endif(CLR_CMAKE_TARGET_WIN32) # add the install targets diff --git a/src/coreclr/dlls/mscoree/coreclr/GenClrDebugResource.ps1 b/src/coreclr/dlls/mscoree/coreclr/GenClrDebugResource.ps1 index b627c55ae17c5e..9860e718930d9f 100644 --- a/src/coreclr/dlls/mscoree/coreclr/GenClrDebugResource.ps1 +++ b/src/coreclr/dlls/mscoree/coreclr/GenClrDebugResource.ps1 @@ -3,6 +3,8 @@ param ( $dac, [string] $dbi, + [switch] + $noNativeDacDbi, [string] $out ) @@ -15,7 +17,8 @@ function Parse-Int { return [System.Int32]::Parse($hexValue, [System.Globalization.NumberStyles]::HexNumber) } -$clrDebugResource = [System.IO.BinaryWriter]::new([System.IO.File]::OpenWrite($out)) +$resourceStream = [System.IO.MemoryStream]::new() +$clrDebugResource = [System.IO.BinaryWriter]::new($resourceStream) try { # We're creating the resource with the following layout (represented as a C struct) @@ -26,17 +29,24 @@ try { # int dacTimeStamp; # int dacImageSize; # int dbiTimeStamp; - # int dacImageSize; + # int dbiImageSize; # }; # Write the debug resource version $clrDebugResource.Write(0); # Write the GUID for CoreCLR (should match the CLR_ID_ONECORE_CLR GUID in clrinternal.idl) $clrDebugResource.Write([System.Guid]::Parse("{0xb1ee760d,0x6c4a,0x4533,{0xba,0x41,0x6f,0x4f,0x66,0x1f,0xab,0xaf}}").ToByteArray()) - [int]$dacTimeStamp = dumpbin $dac /HEADERS | Select-String "([0-9A-Fa-f]+) time date stamp" | %{ $_.Matches.Groups[1].Value } | Parse-Int - [int]$dacImageSize = dumpbin $dac /HEADERS | Select-String "([0-9A-Fa-f]+) size of image" | %{ $_.Matches.Groups[1].Value } | Parse-Int - [int]$dbiTimeStamp = dumpbin $dbi /HEADERS | Select-String "([0-9A-Fa-f]+) time date stamp" | %{ $_.Matches.Groups[1].Value } | Parse-Int - [int]$dbiImageSize = dumpbin $dbi /HEADERS | Select-String "([0-9A-Fa-f]+) size of image" | %{ $_.Matches.Groups[1].Value } | Parse-Int + # Zero identities allow CLR recognition without advertising native DAC/DBI binaries. + [int]$dacTimeStamp = 0 + [int]$dacImageSize = 0 + [int]$dbiTimeStamp = 0 + [int]$dbiImageSize = 0 + if (-not $noNativeDacDbi) { + $dacTimeStamp = dumpbin $dac /HEADERS | Select-String "([0-9A-Fa-f]+) time date stamp" | %{ $_.Matches.Groups[1].Value } | Parse-Int + $dacImageSize = dumpbin $dac /HEADERS | Select-String "([0-9A-Fa-f]+) size of image" | %{ $_.Matches.Groups[1].Value } | Parse-Int + $dbiTimeStamp = dumpbin $dbi /HEADERS | Select-String "([0-9A-Fa-f]+) time date stamp" | %{ $_.Matches.Groups[1].Value } | Parse-Int + $dbiImageSize = dumpbin $dbi /HEADERS | Select-String "([0-9A-Fa-f]+) size of image" | %{ $_.Matches.Groups[1].Value } | Parse-Int + } $clrDebugResource.Write($dacTimeStamp) $clrDebugResource.Write($dacImageSize) $clrDebugResource.Write($dbiTimeStamp) @@ -44,4 +54,20 @@ try { } finally { $clrDebugResource.Dispose() -} \ No newline at end of file +} + +$resourceBytes = $resourceStream.ToArray() +$writeResource = -not [System.IO.File]::Exists($out) + +if (-not $writeResource) { + $existingBytes = [System.IO.File]::ReadAllBytes($out) + $writeResource = $existingBytes.Length -ne $resourceBytes.Length + + for ($i = 0; -not $writeResource -and $i -lt $resourceBytes.Length; $i++) { + $writeResource = $existingBytes[$i] -ne $resourceBytes[$i] + } +} + +if ($writeResource) { + [System.IO.File]::WriteAllBytes($out, $resourceBytes) +} diff --git a/src/coreclr/dlls/mscoree/coreclr/dump_helper_resource.rc.in b/src/coreclr/dlls/mscoree/coreclr/dump_helper_resource.rc.in index b516c1fcf2437c..ae65b53a63f19b 100644 --- a/src/coreclr/dlls/mscoree/coreclr/dump_helper_resource.rc.in +++ b/src/coreclr/dlls/mscoree/coreclr/dump_helper_resource.rc.in @@ -6,7 +6,7 @@ LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL #ifndef EMBEDDED_MINIDUMP_AUXILIARY_PROVIDER -MINIDUMP_AUXILIARY_PROVIDER RCDATA { "mscordaccore.dll\0" } +MINIDUMP_AUXILIARY_PROVIDER RCDATA { "@MINIDUMP_AUXILIARY_PROVIDER_NAME@\0" } #else @@ -14,5 +14,7 @@ MINIDUMP_EMBEDDED_AUXILIARY_PROVIDER RCDATA "@EMBEDDED_MINIDUMP_AUXILIARY_PROVID #endif -@CLRDEBUGINFO_RESOURCE_NAME@ RCDATA "@CLRDEBUGINFO_RESOURCE_PATH@" -CLRDEBUGINFO RCDATA "@CLRDEBUGINFO_RESOURCE_PATH@" +#cmakedefine CLRDEBUGINFO_RESOURCE_PATH "@CLRDEBUGINFO_RESOURCE_PATH@" + +@CLRDEBUGINFO_RESOURCE_NAME@ RCDATA CLRDEBUGINFO_RESOURCE_PATH +CLRDEBUGINFO RCDATA CLRDEBUGINFO_RESOURCE_PATH diff --git a/src/coreclr/inc/clrdata.idl b/src/coreclr/inc/clrdata.idl index 76c55520315ebe..6e358f53b5ba64 100644 --- a/src/coreclr/inc/clrdata.idl +++ b/src/coreclr/inc/clrdata.idl @@ -211,6 +211,20 @@ interface ICLRRuntimeLocator : IUnknown HRESULT GetRuntimeBase([out] CLRDATA_ADDRESS* baseAddress); }; +[ + object, + local, + uuid(17d5b8c6-34a9-407f-af4f-a930201d4e02), + pointer_default(unique) +] +interface ICLRContractLocator : IUnknown +{ + /* + Returns the address of the runtime contract descriptor. + */ + HRESULT GetContractDescriptor([out] CLRDATA_ADDRESS* contractAddress); +}; + /* * Interface used by the data access services layer to locate metadata * of assemblies in a target. diff --git a/src/coreclr/pal/prebuilt/idl/clrdata_i.cpp b/src/coreclr/pal/prebuilt/idl/clrdata_i.cpp index a234e6e8eb363f..a0655d4ab676da 100644 --- a/src/coreclr/pal/prebuilt/idl/clrdata_i.cpp +++ b/src/coreclr/pal/prebuilt/idl/clrdata_i.cpp @@ -78,6 +78,9 @@ MIDL_DEFINE_GUID(IID, IID_ICLRDataTarget3,0xa5664f95,0x0af4,0x4a1b,0x96,0x0e,0x2 MIDL_DEFINE_GUID(IID, IID_ICLRRuntimeLocator,0xb760bf44,0x9377,0x4597,0x8b,0xe7,0x58,0x08,0x3b,0xdc,0x51,0x46); +MIDL_DEFINE_GUID(IID, IID_ICLRContractLocator,0x17d5b8c6,0x34a9,0x407f,0xaf,0x4f,0xa9,0x30,0x20,0x1d,0x4e,0x02); + + MIDL_DEFINE_GUID(IID, IID_ICLRMetadataLocator,0xaa8fa804,0xbc05,0x4642,0xb2,0xc5,0xc3,0x53,0xed,0x22,0xfc,0x63); @@ -97,5 +100,3 @@ MIDL_DEFINE_GUID(IID, IID_ICLRDataEnumMemoryRegions,0x471c35b4,0x7c2f,0x4ef0,0xa #ifdef __cplusplus } #endif - - diff --git a/src/coreclr/pal/prebuilt/inc/clrdata.h b/src/coreclr/pal/prebuilt/inc/clrdata.h index 7a575eae0cf932..40017089b7a0ce 100644 --- a/src/coreclr/pal/prebuilt/inc/clrdata.h +++ b/src/coreclr/pal/prebuilt/inc/clrdata.h @@ -4,9 +4,9 @@ /* this ALWAYS GENERATED file contains the definitions for the interfaces */ - /* File created by MIDL compiler version 8.01.0626 */ + /* File created by MIDL compiler version 8.01.0628 */ /* Compiler settings for clrdata.idl: - Oicf, W1, Zp8, env=Win64 (32b run), target_arch=AMD64 8.01.0626 + Oicf, W1, Zp8, env=Win64 (32b run), target_arch=AMD64 8.01.0628 protocol : dce , ms_ext, c_ext, robust error checks: allocation ref bounds_check enum stub_data VC __declspec() decoration level: @@ -43,7 +43,7 @@ #endif #ifndef DECLSPEC_XFGVIRT -#if _CONTROL_FLOW_GUARD_XFG +#if defined(_CONTROL_FLOW_GUARD_XFG) #define DECLSPEC_XFGVIRT(base, func) __declspec(xfg_virtual(base, func)) #else #define DECLSPEC_XFGVIRT(base, func) @@ -80,6 +80,13 @@ typedef interface ICLRRuntimeLocator ICLRRuntimeLocator; #endif /* __ICLRRuntimeLocator_FWD_DEFINED__ */ +#ifndef __ICLRContractLocator_FWD_DEFINED__ +#define __ICLRContractLocator_FWD_DEFINED__ +typedef interface ICLRContractLocator ICLRContractLocator; + +#endif /* __ICLRContractLocator_FWD_DEFINED__ */ + + #ifndef __ICLRMetadataLocator_FWD_DEFINED__ #define __ICLRMetadataLocator_FWD_DEFINED__ typedef interface ICLRMetadataLocator ICLRMetadataLocator; @@ -922,6 +929,90 @@ EXTERN_C const IID IID_ICLRRuntimeLocator; #endif /* __ICLRRuntimeLocator_INTERFACE_DEFINED__ */ +#ifndef __ICLRContractLocator_INTERFACE_DEFINED__ +#define __ICLRContractLocator_INTERFACE_DEFINED__ + +/* interface ICLRContractLocator */ +/* [unique][uuid][local][object] */ + + +EXTERN_C const IID IID_ICLRContractLocator; + +#if defined(__cplusplus) && !defined(CINTERFACE) + + MIDL_INTERFACE("17d5b8c6-34a9-407f-af4f-a930201d4e02") + ICLRContractLocator : public IUnknown + { + public: + virtual HRESULT STDMETHODCALLTYPE GetContractDescriptor( + /* [out] */ CLRDATA_ADDRESS *contractAddress) = 0; + + }; + + +#else /* C style interface */ + + typedef struct ICLRContractLocatorVtbl + { + BEGIN_INTERFACE + + DECLSPEC_XFGVIRT(IUnknown, QueryInterface) + HRESULT ( STDMETHODCALLTYPE *QueryInterface )( + ICLRContractLocator * This, + /* [in] */ REFIID riid, + /* [annotation][iid_is][out] */ + _COM_Outptr_ void **ppvObject); + + DECLSPEC_XFGVIRT(IUnknown, AddRef) + ULONG ( STDMETHODCALLTYPE *AddRef )( + ICLRContractLocator * This); + + DECLSPEC_XFGVIRT(IUnknown, Release) + ULONG ( STDMETHODCALLTYPE *Release )( + ICLRContractLocator * This); + + DECLSPEC_XFGVIRT(ICLRContractLocator, GetContractDescriptor) + HRESULT ( STDMETHODCALLTYPE *GetContractDescriptor )( + ICLRContractLocator * This, + /* [out] */ CLRDATA_ADDRESS *contractAddress); + + END_INTERFACE + } ICLRContractLocatorVtbl; + + interface ICLRContractLocator + { + CONST_VTBL struct ICLRContractLocatorVtbl *lpVtbl; + }; + + + +#ifdef COBJMACROS + + +#define ICLRContractLocator_QueryInterface(This,riid,ppvObject) \ + ( (This)->lpVtbl -> QueryInterface(This,riid,ppvObject) ) + +#define ICLRContractLocator_AddRef(This) \ + ( (This)->lpVtbl -> AddRef(This) ) + +#define ICLRContractLocator_Release(This) \ + ( (This)->lpVtbl -> Release(This) ) + + +#define ICLRContractLocator_GetContractDescriptor(This,contractAddress) \ + ( (This)->lpVtbl -> GetContractDescriptor(This,contractAddress) ) + +#endif /* COBJMACROS */ + + +#endif /* C style interface */ + + + + +#endif /* __ICLRContractLocator_INTERFACE_DEFINED__ */ + + #ifndef __ICLRMetadataLocator_INTERFACE_DEFINED__ #define __ICLRMetadataLocator_INTERFACE_DEFINED__ @@ -1290,7 +1381,7 @@ EXTERN_C const IID IID_ICLRDataLoggingCallback; #endif /* __ICLRDataLoggingCallback_INTERFACE_DEFINED__ */ -/* interface __MIDL_itf_clrdata_0000_0008 */ +/* interface __MIDL_itf_clrdata_0000_0009 */ /* [local] */ typedef @@ -1305,8 +1396,8 @@ enum CLRDataEnumMemoryFlags -extern RPC_IF_HANDLE __MIDL_itf_clrdata_0000_0008_v0_0_c_ifspec; -extern RPC_IF_HANDLE __MIDL_itf_clrdata_0000_0008_v0_0_s_ifspec; +extern RPC_IF_HANDLE __MIDL_itf_clrdata_0000_0009_v0_0_c_ifspec; +extern RPC_IF_HANDLE __MIDL_itf_clrdata_0000_0009_v0_0_s_ifspec; #ifndef __ICLRDataEnumMemoryRegions_INTERFACE_DEFINED__ #define __ICLRDataEnumMemoryRegions_INTERFACE_DEFINED__ diff --git a/src/coreclr/runtime.proj b/src/coreclr/runtime.proj index abf14212cec03e..1fd02b377d3945 100644 --- a/src/coreclr/runtime.proj +++ b/src/coreclr/runtime.proj @@ -4,15 +4,24 @@ <_BuildNativeTargetOS>$(TargetOS) <_BuildNativeTargetOS Condition="'$(TargetsLinuxBionic)' == 'true'">linux-bionic true + true GetPgoDataPackagePath $(BuildRuntimeDependsOnTargets);InitializeSourceControlInformationFromSourceControlManager AcquireEmscriptenSdk;$(BuildRuntimeDependsOnTargets);GenerateEmccExports AcquireWasiSdk;$(BuildRuntimeDependsOnTargets) + + + @@ -55,6 +64,7 @@ <_CoreClrBuildArg Condition="'$(BuildSubdirectory)' != ''" Include="-subdir $(BuildSubdirectory)" /> <_CoreClrBuildArg Include="-cmakeargs "-DCLR_DOTNET_HOST_PATH='$(DOTNET_HOST_PATH)'"" /> <_CoreClrBuildArg Condition="'$(HasCdacBuildTool)' == 'true'" Include="-cmakeargs "-DCDAC_BUILD_TOOL_BINARY_PATH=$(RuntimeBinDir)cdac-build-tool\cdac-build-tool.dll"" /> + <_CoreClrBuildArg Condition="'$(HasCdacDumpCollectProvider)' == 'true'" Include="-cmakeargs "-DCLR_DUMP_ENUMERATOR_PATH=$(RuntimeBinDir)$(LibPrefix)mscordaccore$(LibSuffix)"" /> <_CoreClrBuildArg Condition="'$(FeatureXplatEventSource)' == 'false'" Include="-cmakeargs "-DFEATURE_EVENTSOURCE_XPLAT=0"" /> <_CoreClrBuildArg Condition="'$(FeatureInterpreter)' == 'true'" Include="-cmakeargs "-DFEATURE_INTERPRETER=1"" /> <_CoreClrBuildArg Condition="'$(FeatureDynamicCodeCompiled)' == 'true' or '$(FeatureDynamicCodeCompiled)' == ''" Include="-cmakeargs "-DFEATURE_DYNAMIC_CODE_COMPILED=1"" /> diff --git a/src/native/managed/cdac/IData.md b/src/native/managed/cdac/IData.md index 51404ddfcd9a04..1e763871d000b5 100644 --- a/src/native/managed/cdac/IData.md +++ b/src/native/managed/cdac/IData.md @@ -78,9 +78,9 @@ analyzer. It scans for classes carrying `[CdacType]` and emits a * A `private readonly Target _target` field (captured in the constructor) for any type with instance members, so lazy getters and `Write{Name}` methods can read/write without a `Target` parameter. -* An explicit `IReadableData.EnsureAllFieldsRead()` implementation (for - any type with instance members) that touches every field so a caller - can force a full eager read. +* An internal `bool TryReadAllFields()` method (for any type with instance + members) that touches every field so a caller can force a full eager read. + It returns `false` if a target memory read throws `VirtualReadException`. * For types with `HasTypeHandle = true`: a `public static ITypeHandle TypeHandle(Target target)` accessor. * For each `[Field(Writable = true)]` property: a @@ -151,25 +151,25 @@ not at construction, and whatever it throws (an from a failed read, or nothing for an optional value) surfaces at first access. -### Forcing a full read: `IReadableData` +### Forcing a full read: `TryReadAllFields` Because reads are deferred, constructing an instance no longer proves the whole structure is readable. Every generated type with instance members -implements `IReadableData.EnsureAllFieldsRead()`, which touches every -field to force a full eager read. Materializing a field can throw either -lazy-read exception: `InvalidOperationException` if a required field is -missing from the descriptor, or `VirtualReadException` if the field's -target memory cannot be read. A caller catches whichever it cares about -- -for example, to validate that the target memory is readable: +provides an internal `TryReadAllFields()` method, which touches every field +to force a full eager read. It returns `true` when all fields can be +materialized, or `false` if a field's target memory cannot be read. +`InvalidOperationException` still propagates if a required field is missing +from the descriptor. Optional fields absent from the descriptor yield `null` +and do not cause failure. For example, to validate that a method table is +readable: ```csharp -T data = target.ProcessedData.GetOrAdd(address); -try +Data.MethodTable data = target.ProcessedData.GetOrAdd(address); +if (data.TryReadAllFields()) { - (data as IReadableData)?.EnsureAllFieldsRead(); // ... the entire structure is readable } -catch (VirtualReadException) +else { // ... the structure is only partially readable } @@ -177,9 +177,9 @@ catch (VirtualReadException) This is how `RuntimeTypeSystem` validation confirms a candidate `MethodTable` / `EEClass` is fully readable before trusting it. Its -descriptor fields are always present, so only `VirtualReadException` is -relevant there; a caller that also needs to tolerate a missing descriptor -field would additionally catch `InvalidOperationException`. +descriptor fields are always present, so the boolean result is sufficient +there; a caller that also needs to tolerate a missing descriptor field would +additionally catch `InvalidOperationException`. ## Attribute surface diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Data/IData.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Data/IData.cs index 88663686694cd4..bb74a34311fc1e 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Data/IData.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Data/IData.cs @@ -7,8 +7,3 @@ public interface IData where TSelf : IData { static abstract TSelf Create(Target target, TargetPointer address); } - -public interface IReadableData -{ - void EnsureAllFieldsRead(); -} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Target.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Target.cs index 3720399748bf24..6e526df75533f9 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Target.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Target.cs @@ -27,6 +27,11 @@ public abstract class Target /// public abstract bool IsLittleEndian { get; } + /// Attempts to locate the runtime image in the target. + /// When this method returns, contains the image base, or a null target pointer if unavailable. + /// if the image base is available; otherwise, . + public abstract bool TryGetRuntimeImageBase(out TargetPointer imageBase); + /// /// Fills a buffer with the context of the given thread /// diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/PrecodeStubs_Common.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/PrecodeStubs_Common.cs index b80cf28877df7e..854182ebd856c1 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/PrecodeStubs_Common.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/PrecodeStubs_Common.cs @@ -31,8 +31,9 @@ internal interface IPrecodeStubsContractCommonApi internal class PrecodeStubsCommon : IPrecodeStubs where TPrecodeStubsImplementation : IPrecodeStubsContractCommonApi { private readonly Target _target; - private readonly CodePointerFlags _codePointerFlags; - internal readonly Data.PrecodeMachineDescriptor MachineDescriptor; + + internal Data.PrecodeMachineDescriptor MachineDescriptor => + _target.ProcessedData.GetOrAdd(_target.Contracts.PlatformMetadata.GetPrecodeMachineDescriptor()); protected Target Target => _target; @@ -103,19 +104,19 @@ internal override TargetPointer GetMethodDesc(Target target, Data.PrecodeMachine internal TargetPointer CodePointerReadableInstrPointer(TargetCodePointer codePointer) { - if (_codePointerFlags.HasFlag(CodePointerFlags.HasArm32ThumbBit)) + CodePointerFlags codePointerFlags = _target.Contracts.PlatformMetadata.GetCodePointerFlags(); + if (codePointerFlags.HasFlag(CodePointerFlags.HasArm32ThumbBit)) { return codePointer.AsTargetPointer & ~1ul; } - if (_codePointerFlags.HasFlag(CodePointerFlags.HasArm64PtrAuth)) + if (codePointerFlags.HasFlag(CodePointerFlags.HasArm64PtrAuth)) { throw new NotImplementedException("CodePointerReadableInstrPointer for ARM64 with pointer authentication"); } - Debug.Assert(_codePointerFlags == 0); + Debug.Assert(codePointerFlags == 0); return codePointer.AsTargetPointer; } - internal ValidPrecode GetPrecodeFromEntryPoint(TargetCodePointer entryPoint) { TargetPointer instrPointer = CodePointerReadableInstrPointer(entryPoint); @@ -142,10 +143,6 @@ internal ValidPrecode GetPrecodeFromEntryPoint(TargetCodePointer entryPoint) public PrecodeStubsCommon(Target target) { _target = target; - IPlatformMetadata pm = target.Contracts.PlatformMetadata; - TargetPointer descAddr = pm.GetPrecodeMachineDescriptor(); - MachineDescriptor = target.ProcessedData.GetOrAdd(descAddr); - _codePointerFlags = pm.GetCodePointerFlags(); } TargetPointer IPrecodeStubs.GetMethodDescFromStubAddress(TargetCodePointer entryPoint) @@ -158,22 +155,23 @@ TargetPointer IPrecodeStubs.GetMethodDescFromStubAddress(TargetCodePointer entry TargetPointer IPrecodeStubs.GetPrecodeEntryPointFromInteriorAddress(TargetCodePointer interiorAddress, bool isFixupPrecode) { TargetPointer instrPointer = CodePointerReadableInstrPointer(interiorAddress); + Data.PrecodeMachineDescriptor machineDescriptor = MachineDescriptor; uint stubSize; if (isFixupPrecode) { - if (MachineDescriptor.FixupStubPrecodeSize is not byte fixupSize || fixupSize == 0) + if (machineDescriptor.FixupStubPrecodeSize is not byte fixupSize || fixupSize == 0) throw new InvalidOperationException("FixupPrecode size not available"); stubSize = fixupSize; } else { - if (MachineDescriptor.StubPrecodeSize is not byte stubPrecodeSize || stubPrecodeSize == 0) + if (machineDescriptor.StubPrecodeSize is not byte stubPrecodeSize || stubPrecodeSize == 0) throw new InvalidOperationException("StubPrecode size not available"); stubSize = stubPrecodeSize; } - ulong pageMask = MachineDescriptor.StubCodePageSize - 1; + ulong pageMask = machineDescriptor.StubCodePageSize - 1; ulong pageBase = instrPointer.Value & ~pageMask; ulong offset = instrPointer.Value - pageBase; ulong entryPointAddress = pageBase + (offset / stubSize) * stubSize; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformAgnosticContext.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformAgnosticContext.cs index d0cd2264d1772d..25158889d14598 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformAgnosticContext.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformAgnosticContext.cs @@ -42,17 +42,32 @@ public interface IPlatformAgnosticContext public static IPlatformAgnosticContext GetContextForPlatform(Target target) { IRuntimeInfo runtimeInfo = target.Contracts.RuntimeInfo; - return runtimeInfo.GetTargetArchitecture() switch - { - RuntimeInfoArchitecture.X86 => new ContextHolder(), - RuntimeInfoArchitecture.X64 => new ContextHolder(), - RuntimeInfoArchitecture.Arm => new ContextHolder(), - RuntimeInfoArchitecture.Arm64 => new ContextHolder(), - RuntimeInfoArchitecture.LoongArch64 => new ContextHolder(), - RuntimeInfoArchitecture.RiscV64 => new ContextHolder(), - RuntimeInfoArchitecture.Wasm => new ContextHolder(), - RuntimeInfoArchitecture.Unknown => throw new InvalidOperationException($"Processor architecture is required for creating a platform specific context and is not provided by the target"), - _ => throw new InvalidOperationException($"Unsupported architecture {runtimeInfo.GetTargetArchitecture()}"), - }; + RuntimeInfoArchitecture architecture = runtimeInfo.GetTargetArchitecture(); + + if (TargetArchitectureSupport.IsX86Supported && architecture == RuntimeInfoArchitecture.X86) + return new ContextHolder(); + + if (TargetArchitectureSupport.IsX64Supported && architecture == RuntimeInfoArchitecture.X64) + return new ContextHolder(); + + if (TargetArchitectureSupport.IsArmSupported && architecture == RuntimeInfoArchitecture.Arm) + return new ContextHolder(); + + if (TargetArchitectureSupport.IsArm64Supported && architecture == RuntimeInfoArchitecture.Arm64) + return new ContextHolder(); + + if (TargetArchitectureSupport.IsLoongArch64Supported && architecture == RuntimeInfoArchitecture.LoongArch64) + return new ContextHolder(); + + if (TargetArchitectureSupport.IsRiscV64Supported && architecture == RuntimeInfoArchitecture.RiscV64) + return new ContextHolder(); + + if (TargetArchitectureSupport.IsWasmSupported && architecture == RuntimeInfoArchitecture.Wasm) + return new ContextHolder(); + + throw new InvalidOperationException( + architecture == RuntimeInfoArchitecture.Unknown + ? "Processor architecture is required for creating a platform specific context and is not provided by the target" + : $"Unsupported architecture {architecture}"); } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs index b1c59af11e2e8a..47418144e2dbb2 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs @@ -358,17 +358,28 @@ public bool IsExceptionHandlingHelperInlinedCallFrame(TargetPointer framePtr) private IPlatformFrameHandler GetFrameHandler(IPlatformAgnosticContext context) { - return context switch - { - ContextHolder contextHolder => new X86FrameHandler(_target, contextHolder), - ContextHolder contextHolder => new AMD64FrameHandler(_target, contextHolder), - ContextHolder contextHolder => new ARMFrameHandler(_target, contextHolder), - ContextHolder contextHolder => new ARM64FrameHandler(_target, contextHolder), - ContextHolder contextHolder => new RISCV64FrameHandler(_target, contextHolder), - ContextHolder contextHolder => new LoongArch64FrameHandler(_target, contextHolder), - ContextHolder contextHolder => new WasmFrameHandler(_target, contextHolder), - _ => throw new InvalidOperationException("Unsupported context type"), - }; + if (TargetArchitectureSupport.IsX86Supported && context is ContextHolder x86Context) + return new X86FrameHandler(_target, x86Context); + + if (TargetArchitectureSupport.IsX64Supported && context is ContextHolder x64Context) + return new AMD64FrameHandler(_target, x64Context); + + if (TargetArchitectureSupport.IsArmSupported && context is ContextHolder armContext) + return new ARMFrameHandler(_target, armContext); + + if (TargetArchitectureSupport.IsArm64Supported && context is ContextHolder arm64Context) + return new ARM64FrameHandler(_target, arm64Context); + + if (TargetArchitectureSupport.IsRiscV64Supported && context is ContextHolder riscV64Context) + return new RISCV64FrameHandler(_target, riscV64Context); + + if (TargetArchitectureSupport.IsLoongArch64Supported && context is ContextHolder loongArch64Context) + return new LoongArch64FrameHandler(_target, loongArch64Context); + + if (TargetArchitectureSupport.IsWasmSupported && context is ContextHolder wasmContext) + return new WasmFrameHandler(_target, wasmContext); + + throw new InvalidOperationException($"Unsupported context type {context.GetType()}"); } private static bool InlinedCallFrameHasActiveCall(Data.InlinedCallFrame frame) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs index 808d6350d69001..92c6f06c6558c8 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs @@ -62,13 +62,13 @@ public static void Register(ContractRegistry registry) RuntimeInfoArchitecture arch = t.Contracts.RuntimeInfo.GetTargetArchitecture(); return arch switch { - RuntimeInfoArchitecture.X64 => new GCInfo_1(t), - RuntimeInfoArchitecture.X86 => new GCInfoX86_1(t), - RuntimeInfoArchitecture.Arm64 => new GCInfo_1(t), - RuntimeInfoArchitecture.Arm => new GCInfo_1(t), - RuntimeInfoArchitecture.LoongArch64 => new GCInfo_1(t), - RuntimeInfoArchitecture.RiscV64 => new GCInfo_1(t), - RuntimeInfoArchitecture.Wasm => new GCInfo_1(t), + RuntimeInfoArchitecture.X64 when TargetArchitectureSupport.IsX64Supported => new GCInfo_1(t), + RuntimeInfoArchitecture.X86 when TargetArchitectureSupport.IsX86Supported => new GCInfoX86_1(t), + RuntimeInfoArchitecture.Arm64 when TargetArchitectureSupport.IsArm64Supported => new GCInfo_1(t), + RuntimeInfoArchitecture.Arm when TargetArchitectureSupport.IsArmSupported => new GCInfo_1(t), + RuntimeInfoArchitecture.LoongArch64 when TargetArchitectureSupport.IsLoongArch64Supported => new GCInfo_1(t), + RuntimeInfoArchitecture.RiscV64 when TargetArchitectureSupport.IsRiscV64Supported => new GCInfo_1(t), + RuntimeInfoArchitecture.Wasm when TargetArchitectureSupport.IsWasmSupported => new GCInfo_1(t), _ => default(GCInfo), }; }); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/TypeValidation.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/TypeValidation.cs index 68d43c42a1e8b3..729dd1ed88541d 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/TypeValidation.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/TypeValidation.cs @@ -76,7 +76,8 @@ internal TargetPointer CanonMT } } - internal readonly bool ValidateReadable() => ValidateDataReadable(_target, Address); + internal readonly bool ValidateReadable() => new MethodTable(_target, Address).TryReadAllFields(); + internal TargetPointer ParentMethodTable => _target.ReadPointer(Address + (ulong)_type.Fields[nameof(ParentMethodTable)].Offset); } @@ -96,7 +97,7 @@ internal NonValidatedEEClass(Target target, TargetPointer eeClassPointer) internal TargetPointer MethodTable => _target.ReadPointer(Address + (ulong)_type.Fields[nameof(MethodTable)].Offset); - internal readonly bool ValidateReadable() => ValidateDataReadable(_target, Address); + internal readonly bool ValidateReadable() => new EEClass(_target, Address).TryReadAllFields(); } internal static NonValidatedMethodTable GetMethodTableData(Target target, TargetPointer methodTablePointer) @@ -200,22 +201,6 @@ private bool ValidateMethodTableAdHoc(NonValidatedMethodTable methodTable) return true; } - private static bool ValidateDataReadable(Target target, TargetPointer dataAddress) where T : IData - { - try - { - T dataClass = T.Create(target, dataAddress); - // Fields are read lazily, so force a full read to validate that the - // entire structure is readable (an unreadable field throws below). - (dataClass as IReadableData)?.EnsureAllFieldsRead(); - return true; - } - catch (VirtualReadException) - { - return false; - } - } - private TargetPointer GetClassThrowing(NonValidatedMethodTable methodTable) { TargetPointer eeClassOrCanonMT = methodTable.EEClassOrCanonMT; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/TargetArchitectureSupport.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/TargetArchitectureSupport.cs new file mode 100644 index 00000000000000..6f16b24ca74c45 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/TargetArchitectureSupport.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts; + +internal static class TargetArchitectureSupport +{ + private const string ArmSwitchName = "Microsoft.Diagnostics.DataContractReader.Architecture.Arm"; + private const string Arm64SwitchName = "Microsoft.Diagnostics.DataContractReader.Architecture.Arm64"; + private const string LoongArch64SwitchName = "Microsoft.Diagnostics.DataContractReader.Architecture.LoongArch64"; + private const string RiscV64SwitchName = "Microsoft.Diagnostics.DataContractReader.Architecture.RiscV64"; + private const string WasmSwitchName = "Microsoft.Diagnostics.DataContractReader.Architecture.Wasm"; + private const string X64SwitchName = "Microsoft.Diagnostics.DataContractReader.Architecture.X64"; + private const string X86SwitchName = "Microsoft.Diagnostics.DataContractReader.Architecture.X86"; + + [FeatureSwitchDefinition(ArmSwitchName)] + public static bool IsArmSupported { get; } = IsEnabled(ArmSwitchName); + + [FeatureSwitchDefinition(Arm64SwitchName)] + public static bool IsArm64Supported { get; } = IsEnabled(Arm64SwitchName); + + [FeatureSwitchDefinition(LoongArch64SwitchName)] + public static bool IsLoongArch64Supported { get; } = IsEnabled(LoongArch64SwitchName); + + [FeatureSwitchDefinition(RiscV64SwitchName)] + public static bool IsRiscV64Supported { get; } = IsEnabled(RiscV64SwitchName); + + [FeatureSwitchDefinition(WasmSwitchName)] + public static bool IsWasmSupported { get; } = IsEnabled(WasmSwitchName); + + [FeatureSwitchDefinition(X64SwitchName)] + public static bool IsX64Supported { get; } = IsEnabled(X64SwitchName); + + [FeatureSwitchDefinition(X86SwitchName)] + public static bool IsX86Supported { get; } = IsEnabled(X86SwitchName); + + private static bool IsEnabled(string switchName) => + !AppContext.TryGetSwitch(switchName, out bool isEnabled) || isEnabled; +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Entrypoints/EntrypointHelpers.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Entrypoints/EntrypointHelpers.cs new file mode 100644 index 00000000000000..15fee234aeedec --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Entrypoints/EntrypointHelpers.cs @@ -0,0 +1,188 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.Marshalling; +using System.Threading; +using Microsoft.Diagnostics.DataContractReader.Contracts; + +namespace Microsoft.Diagnostics.DataContractReader.Legacy; + +/// +/// Provides shared activation helpers for native cDAC entrypoints. +/// +public static unsafe class EntrypointHelpers +{ + /// + /// Creates a cDAC instance exposing the requested COM interface. + /// + /// A pointer to the requested interface identifier. + /// An interface pointer. + /// An optional legacy pointer, or zero. + /// When this method returns, contains the requested interface pointer on success, or null on failure. The caller must release a successful result. + /// The nonzero target contract descriptor address. + /// The runtime image base address, or when unavailable. + /// An HRESULT indicating whether activation succeeded. + public static int CreateInstance(Guid* pIID, IntPtr pDataTarget, IntPtr legacyImplPtr, void** iface, ulong contractAddress, TargetPointer runtimeImageBase = default) + { + if (iface == null) + return HResults.E_INVALIDARG; + + *iface = null; + if (pIID == null || pDataTarget == IntPtr.Zero || contractAddress == 0) + return HResults.E_INVALIDARG; + + try + { + ICLRDataTarget dataTarget = ComInterfaceMarshaller.ConvertToManaged((void*)pDataTarget)!; + ContractDescriptorTarget target = CreateTarget(dataTarget, contractAddress, runtimeImageBase); + return CreateSosInterface(target, legacyImplPtr, *pIID, iface); + } + catch (System.Exception ex) + { + int hr = ex.HResult; + return hr < 0 ? hr : HResults.E_FAIL; + } + } + + /// + /// Locates the runtime image through the runtime locator, falling back to the data target's module lookup. + /// + /// The data target used to locate the runtime module. + /// When this method returns successfully, contains the runtime image base; otherwise, contains zero. + /// if a nonzero runtime image base was found; otherwise, . + public static bool TryGetRuntimeImageBase(ICLRDataTarget dataTarget, out TargetPointer address) + { + const string RuntimeModuleName = "coreclr.dll"; + + address = TargetPointer.Null; + ulong imageBase = 0; + int hr = HResults.S_OK; + if (dataTarget is not ICLRRuntimeLocator runtimeLocator || runtimeLocator.GetRuntimeBase(&imageBase) != HResults.S_OK) + { + imageBase = 0; + hr = dataTarget.GetImageBase(RuntimeModuleName, &imageBase); + } + if (hr != HResults.S_OK || imageBase == 0) + return false; + + address = new(imageBase); + return true; + } + + /// + /// Finds the contract descriptor through the data target's contract locator. + /// + /// The data target queried for . + /// When this method returns, contains the descriptor address on success, or zero on failure. + /// if the locator returns a nonzero descriptor address; otherwise, . + public static bool TryGetContractDescriptorAddress(ICLRDataTarget dataTarget, out TargetPointer address) + { + address = TargetPointer.Null; + ulong contractAddress = 0; + if (dataTarget is not ICLRContractLocator contractLocator + || contractLocator.GetContractDescriptor(&contractAddress) != HResults.S_OK + || contractAddress == 0) + { + return false; + } + + address = new(contractAddress); + return true; + } + + private static ContractDescriptorTarget CreateTarget(ICLRDataTarget dataTarget, ulong contractAddress, TargetPointer runtimeImageBase) + { + ContractDescriptorTarget.AllocVirtualDelegate allocVirtual = (ulong size, out ulong allocatedAddress) => + { + allocatedAddress = 0; + return HResults.E_NOTIMPL; + }; + + if (dataTarget is ICLRDataTarget2 dataTarget2) + { + const uint MemCommit = 0x1000; + const uint PageReadWrite = 0x04; + + allocVirtual = (ulong size, out ulong allocatedAddress) => + { + ClrDataAddress address; + int result = dataTarget2.AllocVirtual(0, (uint)size, MemCommit, PageReadWrite, &address); + allocatedAddress = address.Value; + return result; + }; + } + + return ContractDescriptorTarget.Create( + contractAddress, + (address, buffer) => + { + fixed (byte* bufferPointer = buffer) + { + uint bytesRead; + int hr = dataTarget.ReadVirtual(address, bufferPointer, (uint)buffer.Length, &bytesRead); + return hr < 0 || bytesRead == (uint)buffer.Length ? hr : HResults.E_FAIL; + } + }, + (address, buffer) => + { + fixed (byte* bufferPointer = buffer) + { + uint bytesWritten; + return dataTarget.WriteVirtual(address, bufferPointer, (uint)buffer.Length, &bytesWritten); + } + }, + dataTarget.GetThreadContext, + (threadId, context) => SetThreadContext(dataTarget, threadId, context), + allocVirtual, + [CoreCLRContracts.Register], + runtimeImageBase); + } + + private static int SetThreadContext(ICLRDataTarget dataTarget, uint threadId, ReadOnlySpan context) + { + const nuint ContextAlignment = 16; + fixed (byte* contextPointer = context) + { + if (((nuint)contextPointer & (ContextAlignment - 1)) == 0) + return dataTarget.SetThreadContext(threadId, (uint)context.Length, contextPointer); + + byte* alignedBuffer = (byte*)NativeMemory.AlignedAlloc((nuint)context.Length, ContextAlignment); + try + { + context.CopyTo(new Span(alignedBuffer, context.Length)); + return dataTarget.SetThreadContext(threadId, (uint)context.Length, alignedBuffer); + } + finally + { + NativeMemory.AlignedFree(alignedBuffer); + } + } + } + + private static int CreateSosInterface(ContractDescriptorTarget target, IntPtr legacyImplPtr, Guid iid, void** iface) + { + Lock apiLock = new(); + CoreCLRContracts.ValidateForDataAccess(target, apiLock); + object? legacyImpl = legacyImplPtr != IntPtr.Zero + ? ComInterfaceMarshaller.ConvertToManaged((void*)legacyImplPtr) + : null; + + SOSDacImpl impl = new(target, legacyImpl, apiLock); + void* ccw = ComInterfaceMarshaller.ConvertToUnmanaged(impl); + try + { + int hr = Marshal.QueryInterface((nint)ccw, iid, out nint requestedInterface); + if (hr < 0) + return hr; + + *iface = (void*)requestedInterface; + return HResults.S_OK; + } + finally + { + ComInterfaceMarshaller.Free(ccw); + } + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/MemoryEnumerator.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/MemoryEnumerator.cs new file mode 100644 index 00000000000000..a5470b0c0026a6 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/MemoryEnumerator.cs @@ -0,0 +1,380 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.DataContractReader.Contracts.Extensions; +using Microsoft.Diagnostics.DataContractReader.Legacy; +using ContractModuleHandle = Microsoft.Diagnostics.DataContractReader.Contracts.ModuleHandle; + +namespace Microsoft.Diagnostics.DataContractReader.Legacy.EnumMemory; + +internal enum DumpType +{ + Mini, + Heap, + Triage, +} + +internal sealed class MemoryEnumerator +{ + private const int MaxSyncBlocks = 1_000_000; + private const int MaxThreads = 1_000_000; + + private readonly Target _target; + private readonly DumpType _dumpType; + private readonly MemoryRegionEmitter _emitter; + private readonly HashSet _loaderAllocators = []; + private readonly MethodCollector _methods; + private readonly ObjectCollector _objects; + private readonly PEImageCollector _peImageCollector; + + private MemoryEnumerator( + Target target, + DumpType dumpType, + MemoryRegionEmitter emitter) + { + _target = target; + _dumpType = dumpType; + _emitter = emitter; + _methods = new(target, emitter); + _objects = new(target, emitter, _methods, _dumpType); + _peImageCollector = new(target); + } + + public static int Enumerate(Target target, MemoryRegionEmitter emitter, DumpType dumpType) + { + if (target is not ContractDescriptorTarget descriptorTarget) + return HResults.E_NOTIMPL; + + target.Flush(FlushScope.All); + using IDisposable readScope = descriptorTarget.RegisterReadCallback((address, size) => + { + if (emitter.ShouldEmitTargetRead(address, size)) + emitter.Add(address, size); + }); + foreach (TargetSpan range in descriptorTarget.EnumerateDescriptorMemory()) + emitter.Add(range.Address.Value, range.Size); + + new MemoryEnumerator(target, dumpType, emitter).EnumerateMemoryRegions(); + return emitter.Result; + } + + private void EnumerateMemoryRegions() + { + TryEnumerate(EnumerateNativeRuntimeModule); + TryEnumerate(EnumerateStatics); + TryEnumerate(EnumerateDebugger); + TryEnumerate(EnumerateManagedModules); + TryEnumerate(EnumerateThreads); + + if (_dumpType == DumpType.Heap) + { + TryEnumerate(EnumerateGC); + TryEnumerate(EnumerateCodeAndLoaderHeaps); + TryEnumerate(EnumerateSyncBlocks); + TryEnumerate(EnumerateAuxiliarySymbols); + TryEnumerate(EnumerateStressLog); + } + + TryEnumerate(WriteMiniMetadata); + } + + private void EnumerateNativeRuntimeModule() + { + if (_target.TryGetRuntimeImageBase(out TargetPointer imageBase)) + _peImageCollector.EnumerateMemoryRegions(imageBase, uint.MaxValue, isMapped: true, _emitter, _dumpType, includeExportsAndResources: true); + } + + private static void TryEnumerate(Action enumerate) + { + try + { + enumerate(); + } + catch (System.Exception ex) when (ex.HResult != HResults.COR_E_OPERATIONCANCELED) + { + // Dump creation is best-effort because the target may be partially unreadable or corrupt. + } + } + + private void EnumerateStatics() + { + IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem; + rts.GetWellKnownMethodTable(WellKnownMethodTable.Array); + rts.GetWellKnownMethodTable(WellKnownMethodTable.Canon); + rts.GetWellKnownMethodTable(WellKnownMethodTable.Exception); + rts.GetWellKnownMethodTable(WellKnownMethodTable.Free); + rts.GetWellKnownMethodTable(WellKnownMethodTable.Object); + rts.GetWellKnownMethodTable(WellKnownMethodTable.String); + } + + private void EnumerateDebugger() + { + IDebugger debugger = _target.Contracts.Debugger; + debugger.TryGetDebuggerData(out _); + debugger.GetAttachStateFlags(); + _target.Contracts.ReJIT.IsEnabled(); + } + + private void EnumerateManagedModules() + { + ILoader loader = _target.Contracts.Loader; + IEcmaMetadata ecmaMetadata = _target.Contracts.EcmaMetadata; + + TargetPointer appDomain = loader.GetAppDomain(); + loader.GetRootAssembly(); + IEnumerable modules = loader.GetModuleHandles( + appDomain, + AssemblyIterationFlags.IncludeLoaded | AssemblyIterationFlags.IncludeExecution); + + foreach (ContractModuleHandle module in modules) + { + loader.GetModule(module); + loader.GetAssembly(module); + TargetPointer peAssembly = loader.GetPEAssembly(module); + ModuleFlags flags = loader.GetFlags(module); + loader.GetSimpleName(module); + loader.GetPath(module); + loader.GetFileName(module); + loader.GetILBase(module); + loader.GetModuleLookupMapBase(module, ModuleLookupMapKind.FieldDefToDesc); + loader.GetModuleLookupMapBase(module, ModuleLookupMapKind.ManifestModuleReferences); + loader.GetModuleLookupMapBase(module, ModuleLookupMapKind.MemberRefToDesc); + loader.GetModuleLookupMapBase(module, ModuleLookupMapKind.MethodDefToDesc); + loader.GetModuleLookupMapBase(module, ModuleLookupMapKind.TypeDefToMethodTable); + loader.GetModuleLookupMapBase(module, ModuleLookupMapKind.TypeRefToMethodTable); + + if (loader.TryGetLoadedImageContents(module, out TargetPointer imageBase, out uint imageSize, out uint imageFlags)) + { + _emitter.RegisterMetadataRange(ecmaMetadata.GetReadOnlyMetadataAddress(module)); + TryEnumerate(() => + { + const uint ImageFlagMapped = 1; + _peImageCollector.EnumerateMemoryRegions(imageBase, imageSize, (imageFlags & ImageFlagMapped) != 0, _emitter, _dumpType); + }); + } + + if (flags.HasFlag(ModuleFlags.ReflectionEmit)) + _emitter.RegisterMetadataRange(ecmaMetadata.GetReadWriteSavedMetadataAddress(module)); + + // Smaller dumps do not include in-memory symbols unless they are otherwise referenced. + if (loader.TryGetSymbolStream(module, out TargetPointer symbolBuffer, out uint symbolSize) && _dumpType == DumpType.Heap) + _emitter.Add(symbolBuffer.Value, symbolSize); + + if (peAssembly != TargetPointer.Null) + ecmaMetadata.HasReadWriteMetadata(peAssembly); + + TargetPointer loaderAllocator = loader.GetLoaderAllocator(module); + if (loaderAllocator != TargetPointer.Null) + _loaderAllocators.Add(loaderAllocator); + + loader.IsProbeExtensionResultValid(module); + } + + TargetPointer globalLoaderAllocator = loader.GetGlobalLoaderAllocator(); + if (globalLoaderAllocator != TargetPointer.Null) + { + _loaderAllocators.Add(globalLoaderAllocator); + loader.GetHighFrequencyHeap(globalLoaderAllocator); + loader.GetLowFrequencyHeap(globalLoaderAllocator); + } + + foreach (ContractModuleHandle module in loader.GetModuleHandles( + appDomain, + AssemblyIterationFlags.IncludeLoading | AssemblyIterationFlags.IncludeLoaded | AssemblyIterationFlags.IncludeExecution)) + { + loader.IsAssemblyLoaded(module); + } + + foreach (ContractModuleHandle _ in loader.GetModuleHandles(appDomain, AssemblyIterationFlags.IncludeFailedToLoad)) + { + } + } + + private void EnumerateThreads() + { + IThread thread = _target.Contracts.Thread; + ThreadStoreData threadStore = thread.GetThreadStoreData(); + _ = thread.GetThreadCounts(); + HashSet visited = []; + TargetPointer threadAddress = threadStore.FirstThread; + + for (int i = 0; threadAddress != TargetPointer.Null && i < MaxThreads; i++) + { + if (!visited.Add(threadAddress)) + break; + + ThreadData threadData = thread.GetThreadData(threadAddress); + thread.GetThreadAllocContext(threadAddress, out _, out _); + EnumerateStack(threadData); + if (threadData.LastThrownObjectHandle != TargetPointer.Null) + { + TargetPointer exceptionObject = + _target.ReadPointer(threadData.LastThrownObjectHandle.Value); + _objects.EnumerateObject(exceptionObject); + } + + threadAddress = threadData.NextThread; + } + } + + private void EnumerateStack(ThreadData threadData) + { + IStackWalk stackWalk = _target.Contracts.StackWalk; + foreach (IStackDataFrameHandle frame in stackWalk.CreateStackWalk(threadData)) + { + TargetCodePointer instructionPointer = stackWalk.GetInstructionPointer(frame); + TargetPointer methodDesc = stackWalk.GetMethodDescPtr(frame); + _methods.CaptureMethod(methodDesc, instructionPointer); + } + } + + private void WriteMiniMetadata() + { + Dictionary names = new(_methods.Names); + foreach ((TargetPointer address, string name) in _objects.Names) + names.TryAdd(address, name); + + MiniMetadataWriter.Write(_target, _emitter, names); + } + + private void EnumerateGC() + { + IGC gc = _target.Contracts.GC; + if (!gc.GetGCStructuresValid()) + return; + + string[] identifiers = gc.GetGCIdentifiers(); + gc.GetGCHeapCount(); + gc.GetMaxGeneration(); + gc.GetGCBounds(out _, out _); + gc.GetCurrentGCState(); + gc.TryGetGCDynamicAdaptationMode(out _); + gc.GetGlobalMechanisms(); + gc.GetGlobalAllocationContext(out _, out _); + + if (Array.IndexOf(identifiers, GCIdentifiers.Workstation) >= 0) + { + gc.GetHeapData(); + gc.GetOomData(); + } + else + { + foreach (TargetPointer heap in gc.GetGCHeaps()) + { + gc.GetHeapData(heap); + gc.GetOomData(heap); + } + } + + foreach ((GCHeapSegmentInfo segment, _) in gc.EnumerateAllSegments()) + { + if (segment.End > segment.Start) + _emitter.Add(segment.Start.Value, segment.End.Value - segment.Start.Value); + } + + AddGCMemoryRegions(gc.GetHandleTableMemoryRegions()); + AddGCMemoryRegions(gc.GetGCBookkeepingMemoryRegions()); + AddGCMemoryRegions(gc.GetGCFreeRegions()); + } + + private void AddGCMemoryRegions(IReadOnlyList regions) + { + foreach (GCMemoryRegionData region in regions) + _emitter.Add(region.Start.Value, region.Size); + } + + private void EnumerateCodeAndLoaderHeaps() + { + IExecutionManager executionManager = _target.Contracts.ExecutionManager; + foreach (JitManagerKind kind in new[] { JitManagerKind.EE, JitManagerKind.Interpreter }) + { + foreach (ICodeHeapInfo codeHeap in executionManager.GetCodeHeapInfos(kind)) + { + switch (codeHeap) + { + case HostCodeHeapInfo host when host.CurrentAddress > host.BaseAddress: + _emitter.Add(host.BaseAddress.Value, host.CurrentAddress.Value - host.BaseAddress.Value); + break; + case LoaderCodeHeapInfo loader: + AddLoaderHeapBlocks(loader.LoaderHeapAddress); + break; + } + } + } + + ILoader loaderContract = _target.Contracts.Loader; + foreach (TargetPointer loaderAllocator in _loaderAllocators) + { + IReadOnlyDictionary heaps = + loaderContract.GetLoaderAllocatorHeaps(loaderAllocator); + foreach (TargetPointer loaderHeap in heaps.Values) + AddLoaderHeapBlocks(loaderHeap); + } + } + + private void AddLoaderHeapBlocks(TargetPointer loaderHeap) + { + if (loaderHeap == TargetPointer.Null) + return; + + ILoader loader = _target.Contracts.Loader; + foreach (LoaderHeapBlock block in loader.EnumerateLoaderHeapBlocks(loaderHeap)) + _emitter.Add(block.Address.Value, block.Size.Value); + } + + private void EnumerateSyncBlocks() + { + ISyncBlock syncBlock = _target.Contracts.SyncBlock; + IThread thread = _target.Contracts.Thread; + uint count = syncBlock.GetSyncBlockCount(); + for (uint i = 1; i <= count; i++) + { + TargetPointer address = syncBlock.GetSyncBlock(i); + if (address == TargetPointer.Null || syncBlock.IsSyncBlockFree(i)) + continue; + + syncBlock.GetSyncBlockObject(i); + if (syncBlock.TryGetLockInfo(address, out uint owningThreadId, out _)) + thread.IdToThread(owningThreadId); + syncBlock.GetAdditionalThreadCount(address); + syncBlock.GetBuiltInComData(address, out _, out _, out _); + } + + TargetPointer cleanup = syncBlock.GetSyncBlockFromCleanupList(); + TraverseTargetLinkedList(cleanup, syncBlock.GetNextSyncBlock); + } + + private void EnumerateAuxiliarySymbols() + { + foreach ((TargetCodePointer _, string _) in _target.Contracts.AuxiliarySymbols.EnumerateAuxiliarySymbols()) + { + } + } + + internal static void TraverseTargetLinkedList( + TargetPointer current, + Func getNext) + { + HashSet visited = []; + for (int i = 0; + current != TargetPointer.Null && i < MaxSyncBlocks && visited.Add(current); + i++) + { + current = getNext(current); + } + } + + private void EnumerateStressLog() + { + IStressLog stressLog = _target.Contracts.StressLog; + if (!stressLog.HasStressLog()) + return; + + StressLogData data = stressLog.GetStressLogData(); + foreach (StressLogMemoryRange range in stressLog.GetStressLogMemoryRanges(data)) + _emitter.Add(range.Start.Value, range.Size); + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/MemoryRegionEmitter.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/MemoryRegionEmitter.cs new file mode 100644 index 00000000000000..53a6fc54bad004 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/MemoryRegionEmitter.cs @@ -0,0 +1,103 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace Microsoft.Diagnostics.DataContractReader.Legacy.EnumMemory; + +internal sealed unsafe class MemoryRegionEmitter(nint callback, uint pointerSize) +{ + private static readonly Guid s_ICLRDataEnumMemoryRegionsCallback2_Iid = new("3721A26F-8B91-4D98-A388-DB17B356FADB"); + + private readonly delegate* unmanaged[MemberFunction] _queryInterface = + (delegate* unmanaged[MemberFunction])(*(nint**)callback)[0]; + // ICLRDataEnumMemoryRegionsCallback::EnumMemoryRegion follows the three IUnknown vtable slots. + private readonly delegate* unmanaged[MemberFunction] _enumMemoryRegion = + (delegate* unmanaged[MemberFunction])(*(nint**)callback)[3]; + private readonly List _metadataRanges = []; + + public int Result { get; private set; } + + public void Add(ulong address, uint size) => Add(address, (ulong)size); + + public void Add(ulong address, ulong size) + { + if (address == 0 || size == 0) + return; + + while (size != 0) + { + uint chunkSize = (uint)Math.Min(size, uint.MaxValue); + int hr = _enumMemoryRegion(callback, ToClrDataAddress(address), chunkSize); + if (hr == HResults.COR_E_OPERATIONCANCELED) + Marshal.ThrowExceptionForHR(hr); + + if (hr < 0 && Result >= 0) + Result = hr; + + address = checked(address + chunkSize); + size -= chunkSize; + } + } + + public void RegisterMetadataRange(TargetSpan range) + { + if (range.Address != TargetPointer.Null && range.Size != 0) + _metadataRanges.Add(range); + } + + public bool ShouldEmitTargetRead(ulong address, ulong size) + { + foreach (TargetSpan range in _metadataRanges) + { + if (address < range.Address.Value) + continue; + + ulong offset = address - range.Address.Value; + if (offset <= range.Size && size <= range.Size - offset) + return false; + } + + return true; + } + + public bool Update(ulong address, ReadOnlySpan buffer) + { + nint callback2 = 0; + Guid iid = s_ICLRDataEnumMemoryRegionsCallback2_Iid; + int hr = _queryInterface(callback, &iid, &callback2); + + if (hr < 0 || callback2 == 0) + return false; + + try + { + delegate* unmanaged[MemberFunction] updateMemoryRegion = + (delegate* unmanaged[MemberFunction])(*(nint**)callback2)[4]; + fixed (byte* bufferPointer = buffer) + { + hr = updateMemoryRegion( + callback2, + ToClrDataAddress(address), + (uint)buffer.Length, + bufferPointer); + } + + if (hr < 0) + return false; + + return true; + } + finally + { + delegate* unmanaged[MemberFunction] release = + (delegate* unmanaged[MemberFunction])(*(nint**)callback2)[2]; + release(callback2); + } + } + + internal ulong ToClrDataAddress(ulong address) => + pointerSize == sizeof(uint) ? (ulong)(long)(int)address : address; +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/MethodCollector.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/MethodCollector.cs new file mode 100644 index 00000000000000..c46d8542160015 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/MethodCollector.cs @@ -0,0 +1,141 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.DataContractReader.Contracts.Extensions; +using Microsoft.Diagnostics.DataContractReader.Legacy; +using ContractModuleHandle = Microsoft.Diagnostics.DataContractReader.Contracts.ModuleHandle; + +namespace Microsoft.Diagnostics.DataContractReader.Legacy.EnumMemory; + +internal sealed class MethodCollector(Target target, MemoryRegionEmitter emitter) +{ + private readonly Target _target = target; + private readonly MemoryRegionEmitter _emitter = emitter; + private readonly HashSet _captured = []; + private readonly HashSet _capturedCodeBlocks = []; + private readonly Dictionary _names = []; + + public IReadOnlyDictionary Names => _names; + + public void CaptureMethod(TargetPointer methodDesc, TargetCodePointer instructionPointer) + { + if (instructionPointer != TargetCodePointer.Null) + { + EnumerateInstructionPointer(instructionPointer); + EnumerateCodeDependencies(instructionPointer); + } + + if (methodDesc == TargetPointer.Null || !_captured.Add(methodDesc)) + return; + + EnumerateMethodDependencies(methodDesc); + EnumerateMethodDescDataDependencies(methodDesc); + CacheMethodName(methodDesc); + } + + private void EnumerateInstructionPointer(TargetCodePointer instructionPointer) + { + ulong address = instructionPointer.ToAddress(_target).Value; + uint size = (uint)_target.PointerSize; + ulong maxAddress = _target.PointerSize == sizeof(uint) ? uint.MaxValue : ulong.MaxValue; + if (address > maxAddress - size) + return; + + _emitter.Add(address, size); + } + + private void EnumerateCodeDependencies(TargetCodePointer instructionPointer) + { + IExecutionManager executionManager = _target.Contracts.ExecutionManager; + if (executionManager.GetCodeBlockHandle(instructionPointer) is not CodeBlockHandle codeBlock + || !_capturedCodeBlocks.Add(codeBlock.Address)) + return; + + executionManager.GetGCInfo(codeBlock, out _, out _); + + IDebugInfo debugInfo = _target.Contracts.DebugInfo; + if (!debugInfo.HasDebugInfo(instructionPointer)) + return; + + foreach (OffsetMapping _ in debugInfo.GetMethodNativeMap(instructionPointer, preferUninstrumented: true, out _)) + { + } + + foreach (DebugVarInfo _ in debugInfo.GetMethodVarInfo(instructionPointer, out _)) + { + } + } + + private void EnumerateMethodDependencies(TargetPointer methodDesc) + { + IRuntimeTypeSystem types = _target.Contracts.RuntimeTypeSystem; + MethodDescHandle method = types.GetMethodDescHandle(methodDesc); + if (types.IsNoMetadataMethod(method, out _)) + return; + + uint token = types.GetMethodToken(method); + TargetPointer methodTable = types.GetMethodTable(method); + TargetPointer module = types.GetModule(types.GetTypeHandle(methodTable)); + ILoader loader = _target.Contracts.Loader; + ContractModuleHandle moduleHandle = loader.GetModuleHandleFromModulePtr(module); + loader.GetAssembly(moduleHandle); + loader.GetFlags(moduleHandle); + loader.GetPath(moduleHandle); + loader.IsProbeExtensionResultValid(moduleHandle); + loader.GetModuleLookupMapElement(moduleHandle, ModuleLookupMapKind.MethodDefToDesc, token, out _); + loader.GetILHeader(moduleHandle, token); + } + + private void EnumerateMethodDescDataDependencies(TargetPointer methodDesc) + { + IRuntimeTypeSystem types = _target.Contracts.RuntimeTypeSystem; + MethodDescHandle method = types.GetMethodDescHandle(methodDesc); + ICodeVersions codeVersions = _target.Contracts.CodeVersions; + NativeCodeVersionHandle codeVersion = codeVersions.GetActiveNativeCodeVersion(methodDesc); + + types.IsDynamicMethod(method); + types.GetSlotNumber(method); + if (codeVersion.Valid) + { + TargetCodePointer nativeCode = codeVersions.GetNativeCode(codeVersion); + if (nativeCode != TargetCodePointer.Null) + _target.Contracts.PrecodeStubs.GetInterpreterCodeFromInterpreterPrecodeIfPresent(nativeCode); + + codeVersions.GetGCStressCodeCopy(codeVersion); + } + if (types.HasNativeCodeSlot(method)) + types.GetAddressOfNativeCodeSlot(method); + } + + private void CacheMethodName(TargetPointer methodDesc) + { + if (_names.ContainsKey(methodDesc)) + return; + + IRuntimeTypeSystem types = _target.Contracts.RuntimeTypeSystem; + MethodDescHandle method = types.GetMethodDescHandle(methodDesc); + if (types.IsNoMetadataMethod(method, out _) && !types.IsILStub(method)) + return; + + try + { + StringBuilder name = new(); + TypeNameBuilder.AppendMethodInternal( + _target, + name, + method, + TypeNameFormat.FormatSignature | + TypeNameFormat.FormatNamespace | + TypeNameFormat.FormatFullInst); + if (name.Length != 0) + _names.Add(methodDesc, name.ToString()); + } + catch (System.Exception ex) when (ex.HResult != HResults.COR_E_OPERATIONCANCELED) + { + } + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/MiniMetadataWriter.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/MiniMetadataWriter.cs new file mode 100644 index 00000000000000..14d1fc997f7437 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/MiniMetadataWriter.cs @@ -0,0 +1,88 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Text; +using Microsoft.Diagnostics.DataContractReader.Contracts; + +namespace Microsoft.Diagnostics.DataContractReader.Legacy.EnumMemory; + +internal static class MiniMetadataWriter +{ + private const uint MiniMetadataSignature = 0x6d727473; + private const uint EENameStreamSignature = 0x614e4545; + private const int StreamsHeaderSize = 12; + private const int EENameStreamHeaderSize = 8; + + public static void Write( + Target target, + MemoryRegionEmitter emitter, + IReadOnlyDictionary names) + { + if (names.Count == 0) + return; + + TargetPointer bufferAddress = + target.ReadPointer(target.ReadGlobalPointer(Constants.Globals.MiniMetaDataBuffAddress)); + uint capacity = + target.Read(target.ReadGlobalPointer(Constants.Globals.MiniMetaDataBuffMaxSize)); + if (bufferAddress == TargetPointer.Null + || capacity < StreamsHeaderSize + EENameStreamHeaderSize) + { + return; + } + + byte[] buffer = new byte[capacity]; + int offset = StreamsHeaderSize + EENameStreamHeaderSize; + uint count = 0; + + foreach ((TargetPointer address, string name) in names) + { + int nameSize = Encoding.UTF8.GetByteCount(name); + int entrySize = checked(target.PointerSize + nameSize + 1); + if (offset > buffer.Length - entrySize) + break; + + WritePointer(buffer.AsSpan(offset, target.PointerSize), address.Value, target); + offset += target.PointerSize; + offset += Encoding.UTF8.GetBytes(name, buffer.AsSpan(offset, nameSize)); + buffer[offset++] = 0; + count++; + } + + WriteUInt32(buffer.AsSpan(0, sizeof(uint)), MiniMetadataSignature, target); + WriteUInt32(buffer.AsSpan(4, sizeof(uint)), checked((uint)offset), target); + WriteUInt32(buffer.AsSpan(8, sizeof(uint)), 1, target); + WriteUInt32(buffer.AsSpan(StreamsHeaderSize, sizeof(uint)), EENameStreamSignature, target); + WriteUInt32(buffer.AsSpan(StreamsHeaderSize + 4, sizeof(uint)), count, target); + + emitter.Add(bufferAddress.Value, checked((uint)offset)); + emitter.Update(bufferAddress.Value, buffer.AsSpan(0, offset)); + } + + private static void WritePointer(Span destination, ulong value, Target target) + { + if (target.PointerSize == sizeof(uint)) + { + WriteUInt32(destination, checked((uint)value), target); + } + else if (target.IsLittleEndian) + { + BinaryPrimitives.WriteUInt64LittleEndian(destination, value); + } + else + { + BinaryPrimitives.WriteUInt64BigEndian(destination, value); + } + } + + private static void WriteUInt32(Span destination, uint value, Target target) + { + if (target.IsLittleEndian) + BinaryPrimitives.WriteUInt32LittleEndian(destination, value); + else + BinaryPrimitives.WriteUInt32BigEndian(destination, value); + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/ObjectCollector.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/ObjectCollector.cs new file mode 100644 index 00000000000000..12cb0f29ca0d19 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/ObjectCollector.cs @@ -0,0 +1,259 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using System.Runtime.InteropServices; +using System.Text; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.DataContractReader.Legacy; + +namespace Microsoft.Diagnostics.DataContractReader.Legacy.EnumMemory; + +internal sealed class ObjectCollector( + Target target, + MemoryRegionEmitter emitter, + MethodCollector methods, + DumpType dumpType) +{ + private const ulong MaxObjectSize = 64 * 1024 * 1024; + private const int MaxInnerExceptionCount = 256; + private const int MaxTypeTraversalDepth = 1_024; + + private readonly Target _target = target; + private readonly DumpType _dumpType = dumpType; + private readonly MemoryRegionEmitter _emitter = emitter; + private readonly MethodCollector _methods = methods; + private readonly Dictionary _names = []; + private readonly Stack _pendingObjects = []; + private readonly HashSet _visitedObjects = []; + private int _remainingInnerExceptions; + + public IReadOnlyDictionary Names => _names; + + public void EnumerateObject(TargetPointer objectAddress) + { + _remainingInnerExceptions = MaxInnerExceptionCount; + _pendingObjects.Push(objectAddress); + try + { + while (_pendingObjects.TryPop(out TargetPointer pendingObject)) + EnumerateObjectCore(pendingObject); + } + finally + { + _pendingObjects.Clear(); + } + } + + private void EnumerateObjectCore(TargetPointer objectAddress) + { + if (objectAddress == TargetPointer.Null || !_visitedObjects.Add(objectAddress)) + { + return; + } + + try + { + IObject objects = _target.Contracts.Object; + IRuntimeTypeSystem types = _target.Contracts.RuntimeTypeSystem; + ulong objectSize = objects.GetSize(objectAddress); + if (objectSize == 0 || objectSize > MaxObjectSize) + return; + + _emitter.Add(objectAddress.Value, objectSize); + objects.GetSyncBlockAddress(objectAddress); + + TargetPointer methodTable = objects.GetMethodTableAddress(objectAddress); + ITypeHandle type = types.GetTypeHandle(methodTable); + bool isException = EnumerateTypeHierarchy(type); + + if (methodTable == types.GetWellKnownMethodTable(WellKnownMethodTable.String)) + { + objects.GetStringValue(objectAddress); + } + else if (types.IsArray(type, out _)) + { + EnumerateArray(objects, objectAddress, type); + } + + if (_target.Contracts.FeatureFlags.IsEnabled(RuntimeFeature.COMInterop)) + objects.GetBuiltInComData(objectAddress, out _, out _, out _); + + if (isException) + EnumerateExceptionData(objectAddress); + + CacheMethodTableName(methodTable); + } + catch (System.Exception ex) when (ex.HResult != HResults.COR_E_OPERATIONCANCELED) + { + } + } + + private bool EnumerateTypeHierarchy(ITypeHandle type) + { + IRuntimeTypeSystem types = _target.Contracts.RuntimeTypeSystem; + TargetPointer exceptionMethodTable = + types.GetWellKnownMethodTable(WellKnownMethodTable.Exception); + bool isException = false; + + for (int depth = 0; type.Address != TargetPointer.Null && depth < MaxTypeTraversalDepth; depth++) + { + EnumerateMethodTableData(type); + isException |= type.Address == exceptionMethodTable; + + TargetPointer parentMethodTable = types.GetParentMethodTable(type); + if (parentMethodTable == TargetPointer.Null) + break; + + type = types.GetTypeHandle(parentMethodTable); + } + + return isException; + } + + private void EnumerateMethodTableData(ITypeHandle type) + { + IRuntimeTypeSystem types = _target.Contracts.RuntimeTypeSystem; + types.GetBaseSize(type); + types.GetComponentSize(type); + if (types.IsFreeObjectMethodTable(type)) + return; + + types.GetModule(type); + types.GetCanonicalMethodTable(type); + types.GetParentMethodTable(type); + types.GetNumInterfaces(type); + types.GetNumMethods(type); + types.GetTypeDefToken(type); + types.GetTypeDefTypeAttributes(type); + types.ContainsGCPointers(type); + types.IsDynamicStatics(type); + } + + private void EnumerateArray(IObject objects, TargetPointer objectAddress, ITypeHandle type) + { + IRuntimeTypeSystem types = _target.Contracts.RuntimeTypeSystem; + objects.GetArrayData(objectAddress, out _, out _, out _, out _, out _); + ITypeHandle elementType = types.GetTypeParam(type); + types.GetSignatureCorElementType(elementType); + for (int depth = 0; depth < MaxTypeTraversalDepth && types.IsArray(elementType, out _); depth++) + elementType = types.GetTypeParam(elementType); + } + + private void EnumerateExceptionData(TargetPointer exceptionObject) + { + IException exceptions = _target.Contracts.Exception; + ExceptionData data = exceptions.GetExceptionData(exceptionObject); + if (data.InnerException != TargetPointer.Null && _remainingInnerExceptions > 0) + { + _remainingInnerExceptions--; + _pendingObjects.Push(data.InnerException); + } + EnumerateStackTraceString(data.StackTraceString); + if (data.RemoteStackTraceString != TargetPointer.Null && + (_dumpType != DumpType.Triage || !ExceptionTypeOverridesStackTraceGetter(exceptionObject))) + { + EnumerateStackTraceString(data.RemoteStackTraceString); + } + _pendingObjects.Push(data.WatsonBuckets); + _pendingObjects.Push(data.StackTrace); + if (_dumpType != DumpType.Triage) + _pendingObjects.Push(data.Message); + + foreach (ExceptionStackFrameInfo frame in exceptions.GetExceptionStackFrames(exceptionObject)) + _methods.CaptureMethod(frame.MethodDesc, new TargetCodePointer(frame.Ip.Value)); + } + + private bool ExceptionTypeOverridesStackTraceGetter(TargetPointer exceptionObject) + { + IRuntimeTypeSystem types = _target.Contracts.RuntimeTypeSystem; + TargetPointer methodTable = _target.Contracts.Object.GetMethodTableAddress(exceptionObject); + TargetPointer exceptionMethodTable = types.GetWellKnownMethodTable(WellKnownMethodTable.Exception); + if (methodTable == exceptionMethodTable) + return false; + + ITypeHandle exceptionType = types.GetTypeHandle(exceptionMethodTable); + ITypeHandle objectType = types.GetTypeHandle(types.GetWellKnownMethodTable(WellKnownMethodTable.Object)); + ITypeHandle derivedType = types.GetTypeHandle(methodTable); + Contracts.ModuleHandle module = _target.Contracts.Loader.GetModuleHandleFromModulePtr(types.GetModule(exceptionType)); + MetadataReader metadata = _target.Contracts.EcmaMetadata.GetMetadata(module) + ?? throw new InvalidOperationException("Exception metadata is required to identify the StackTrace getter."); + + ushort numSlots = types.GetNumVtableSlots(exceptionType); + for (ushort slot = types.GetNumVtableSlots(objectType); slot < numSlots; slot++) + { + TargetPointer methodDesc = types.GetMethodDescForSlot(exceptionType, slot); + if (methodDesc == TargetPointer.Null) + continue; + + uint token = types.GetMethodToken(types.GetMethodDescHandle(methodDesc)); + MethodDefinition method = metadata.GetMethodDefinition(MetadataTokens.MethodDefinitionHandle((int)(token & 0x00FFFFFF))); + if (metadata.StringComparer.Equals(method.Name, "get_StackTrace")) + return types.GetMethodDescForSlot(derivedType, slot) != methodDesc; + } + + throw new InvalidOperationException("Could not find the StackTrace getter on System.Exception."); + } + + private void EnumerateStackTraceString(TargetPointer address) + { + if (address == TargetPointer.Null) + return; + + EnumerateObjectCore(address); + if (_dumpType != DumpType.Triage) + return; + + IObject objects = _target.Contracts.Object; + objects.GetStringData(address, out uint length, out uint offsetToFirstChar); + if (length == 0 || length > MaxObjectSize / sizeof(char)) + return; + + char[] buffer = new char[checked((int)length)]; + Span bytes = MemoryMarshal.AsBytes(buffer.AsSpan()); + _target.ReadBuffer(address + offsetToFirstChar, bytes); + if (_target.IsLittleEndian != BitConverter.IsLittleEndian) + { + for (int i = 0; i < buffer.Length; i++) + buffer[i] = (char)BinaryPrimitives.ReverseEndianness((ushort)buffer[i]); + } + Sanitizer.StripFileInfoFromStackTrace(buffer); + if (_target.IsLittleEndian != BitConverter.IsLittleEndian) + { + for (int i = 0; i < buffer.Length; i++) + buffer[i] = (char)BinaryPrimitives.ReverseEndianness((ushort)buffer[i]); + } + // Native DAC also uses the optional update callback, leaving the string length unchanged. + _emitter.Update(address.Value + offsetToFirstChar, bytes); + } + + private void CacheMethodTableName(TargetPointer methodTable) + { + if (methodTable == TargetPointer.Null || _names.ContainsKey(methodTable)) + return; + + try + { + IRuntimeTypeSystem types = _target.Contracts.RuntimeTypeSystem; + ITypeHandle type = types.GetTypeHandle(methodTable); + if (types.IsFreeObjectMethodTable(type)) + return; + + StringBuilder name = new(); + TypeNameBuilder.AppendType( + _target, + name, + type, + TypeNameFormat.FormatNamespace | TypeNameFormat.FormatFullInst); + if (name.Length != 0) + _names.Add(methodTable, name.ToString()); + } + catch (System.Exception ex) when (ex.HResult != HResults.COR_E_OPERATIONCANCELED) + { + } + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/PEImageCollector.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/PEImageCollector.cs new file mode 100644 index 00000000000000..c8940ac984bea6 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/PEImageCollector.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Reflection.PortableExecutable; + +namespace Microsoft.Diagnostics.DataContractReader.Legacy.EnumMemory; + +internal sealed class PEImageCollector(Target target) +{ + private readonly Target _target = target; + + public void EnumerateMemoryRegions(TargetPointer imageBase, uint imageSize, bool isMapped, MemoryRegionEmitter emitter, DumpType dumpType, bool includeExportsAndResources = false) + { + if (!PEImageInfo.TryCreate(imageBase, imageSize, isMapped, ReadMemory, out PEImageInfo image)) + return; + + foreach (TargetSpan range in image.EnumerateMemoryRegions(includeExportsAndResources)) + emitter.Add(range.Address.Value, range.Size); + + foreach (PEImageInfo.DebugEntry entry in image.EnumerateDebugEntries(ReadMemory)) + { + emitter.Add(entry.Data.Address.Value, entry.Data.Size); + if (dumpType == DumpType.Triage && entry.Type == DebugDirectoryEntryType.CodeView) + Sanitizer.SanitizePdbPath(_target, emitter, entry.Data); + } + } + + private bool ReadMemory(ulong address, Span buffer) + { + _target.ReadBuffer(address, buffer); + return true; + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/Sanitizer.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/Sanitizer.cs new file mode 100644 index 00000000000000..e1de75f35de5be --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/Sanitizer.cs @@ -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. + +using System; +using System.Buffers.Binary; + +namespace Microsoft.Diagnostics.DataContractReader.Legacy.EnumMemory; + +internal static class Sanitizer +{ + internal static void SanitizePdbPath(Target target, MemoryRegionEmitter emitter, TargetSpan codeView) + { + const uint RsdsSignature = 0x53445352; + const uint PdbPathOffset = 24; + if (codeView.Size <= PdbPathOffset) + return; + + Span buffer = stackalloc byte[256]; + target.ReadBuffer(codeView.Address, buffer[..sizeof(uint)]); + if (BinaryPrimitives.ReadUInt32LittleEndian(buffer) != RsdsSignature) + return; + + ulong pathAddress = codeView.Address.Value + PdbPathOffset; + ulong pathSize = codeView.Size - PdbPathOffset; + ulong fileNameStart = 0; + ulong length = 0; + bool terminated = false; + while (length < pathSize && !terminated) + { + int count = (int)Math.Min((ulong)buffer.Length, pathSize - length); + target.ReadBuffer(pathAddress + length, buffer[..count]); + for (int i = 0; i < count; i++, length++) + { + if (buffer[i] == 0) + { + terminated = true; + break; + } + if (buffer[i] is (byte)'\\' or (byte)'/') + fileNameStart = length + 1; + } + } + if (!terminated) + throw new InvalidOperationException("The CodeView PDB path is not null-terminated."); + + ulong fileNameLength = length - fileNameStart; + ulong written = 0; + while (written < fileNameLength) + { + int count = (int)Math.Min((ulong)buffer.Length, fileNameLength - written); + target.ReadBuffer(pathAddress + fileNameStart + written, buffer[..count]); + if (!emitter.Update(pathAddress + written, buffer[..count])) + return; + written += (uint)count; + } + buffer.Clear(); + while (written < pathSize) + { + int count = (int)Math.Min((ulong)buffer.Length, pathSize - written); + if (!emitter.Update(pathAddress + written, buffer[..count])) + return; + written += (uint)count; + } + } + + internal static void StripFileInfoFromStackTrace(Span buffer) + { + // Mirror StripFileInfoFromStackTrace in vm/excep.cpp, including trailing-text removal. + int depth = 0; + int written = 0; + int lastMethodEnd = 0; + for (int i = 0; i < buffer.Length; i++) + { + char c = buffer[i]; + buffer[written++] = c; + if (c == '(') + depth++; + else if (c == ')') + { + if (depth == 1) + { + lastMethodEnd = written; + while (i + 1 < buffer.Length && buffer[i + 1] is not '\r' and not '\n') + i++; + } + depth--; + } + } + + buffer[lastMethodEnd..].Clear(); + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ICLRData.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ICLRData.cs index 2e81d2e00a9f63..e46f33b118139e 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ICLRData.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ICLRData.cs @@ -10,12 +10,21 @@ namespace Microsoft.Diagnostics.DataContractReader.Legacy; // This file contains managed declarations for the IXCLRData interfaces. // See src/coreclr/inc/clrdata.idl +public enum CLRDataEnumMemoryFlags +{ + CLRDATA_ENUM_MEM_DEFAULT = 0x0, + CLRDATA_ENUM_MEM_MINI = CLRDATA_ENUM_MEM_DEFAULT, + CLRDATA_ENUM_MEM_HEAP = 0x1, + CLRDATA_ENUM_MEM_TRIAGE = 0x2, + CLRDATA_ENUM_MEM_HEAP2 = 0x3, +} + [GeneratedComInterface] [Guid("471c35b4-7c2f-4ef0-a945-00f8c38056f1")] public unsafe partial interface ICLRDataEnumMemoryRegions { [PreserveSig] - int EnumMemoryRegions(/*ICLRDataEnumMemoryRegionsCallback*/ void* callback, uint miniDumpFlags, /*CLRDataEnumMemoryFlags*/ int clrFlags); + int EnumMemoryRegions(/*ICLRDataEnumMemoryRegionsCallback*/ void* callback, uint miniDumpFlags, CLRDataEnumMemoryFlags clrFlags); } [GeneratedComInterface] @@ -56,6 +65,34 @@ public unsafe partial interface ICLRDataTarget int Request(uint reqCode, uint inBufferSize, byte* inBuffer, uint outBufferSize, byte* outBuffer); } +internal static unsafe class CLRDataTargetExtensions +{ + internal static int GetThreadContext(this ICLRDataTarget target, uint threadId, uint contextFlags, Span buffer) + { + const nuint ContextAlignment = 16; + fixed (byte* context = buffer) + { + if (((nuint)context & (ContextAlignment - 1)) == 0) + return target.GetThreadContext(threadId, contextFlags, (uint)buffer.Length, context); + + byte* alignedContext = (byte*)NativeMemory.AlignedAlloc((nuint)buffer.Length, ContextAlignment); + NativeMemory.Clear(alignedContext, (nuint)buffer.Length); + try + { + int hr = target.GetThreadContext(threadId, contextFlags, (uint)buffer.Length, alignedContext); + if (hr == HResults.S_OK) + new ReadOnlySpan(alignedContext, buffer.Length).CopyTo(buffer); + + return hr; + } + finally + { + NativeMemory.AlignedFree(alignedContext); + } + } + } +} + [GeneratedComInterface] [Guid("6d05fae3-189c-4630-a6dc-1c251e1c01ab")] public unsafe partial interface ICLRDataTarget2 : ICLRDataTarget diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/PEImageInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/PEImageInfo.cs new file mode 100644 index 00000000000000..9741226237fb28 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/PEImageInfo.cs @@ -0,0 +1,308 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Reflection.PortableExecutable; + +namespace Microsoft.Diagnostics.DataContractReader.Legacy; + +/// +/// Describes a module's PE headers, directories, and exports. +/// +public sealed class PEImageInfo +{ + private const int DosHeaderSize = 64; + private const int PeHeaderSize = 264; + private const int OptionalHeaderOffset = 24; + private const int SizeOfHeadersOffset = OptionalHeaderOffset + 60; + private const int Pe32DataDirectoryOffset = OptionalHeaderOffset + 96; + private const int Pe32PlusDataDirectoryOffset = OptionalHeaderOffset + 112; + private const int ExportDirectoryIndex = 0; + private const int ResourceDirectoryIndex = 2; + private const int DebugDirectoryIndex = 6; + private const ushort DosSignature = 0x5a4d; + private const uint PeSignature = 0x00004550; + private const int SectionHeaderSize = 40; + private const int DebugDirectoryEntrySize = 28; + + private readonly uint _imageSize; + private readonly bool _isMapped; + private readonly ImageSection[] _sections; + + /// + /// Reads memory from the target process. + /// + /// The target address to read. + /// The buffer to fill with target memory. + /// if the entire buffer was read; otherwise, . + public delegate bool TryReadMemory(ulong address, Span buffer); + + private PEImageInfo(ulong imageBase, uint imageSize, uint sizeOfHeaders, bool isMapped, ImageSection[] sections) + { + ImageBase = imageBase; + _imageSize = imageSize; + SizeOfHeaders = sizeOfHeaders; + _isMapped = isMapped; + _sections = sections; + } + + internal ulong ImageBase { get; } + internal uint SizeOfHeaders { get; } + private TargetDirectory ExportDirectory { get; set; } + private TargetDirectory ResourceDirectory { get; set; } + private TargetDirectory DebugDirectory { get; set; } + + internal IEnumerable EnumerateMemoryRegions(bool includeExportsAndResources = true) + { + if (ImageBase != 0 && SizeOfHeaders != 0) + yield return new TargetSpan(ImageBase, SizeOfHeaders); + if (DebugDirectory.Address != 0 && DebugDirectory.Size != 0) + yield return new TargetSpan(DebugDirectory.Address, DebugDirectory.Size); + if (includeExportsAndResources && ResourceDirectory.Address != 0 && ResourceDirectory.Size != 0) + yield return new TargetSpan(ResourceDirectory.Address, ResourceDirectory.Size); + if (includeExportsAndResources && ExportDirectory.Address != 0 && ExportDirectory.Size != 0) + yield return new TargetSpan(ExportDirectory.Address, ExportDirectory.Size); + } + + internal IEnumerable EnumerateDebugEntries(TryReadMemory readMemory) + { + if (DebugDirectory.Size < DebugDirectoryEntrySize) + yield break; + + // IMAGE_DEBUG_DIRECTORY entries refer to payloads outside the directory itself. + // https://learn.microsoft.com/windows/win32/debug/pe-format#debug-directory-image-only + byte[] entry = new byte[DebugDirectoryEntrySize]; + for (uint offset = 0; DebugDirectory.Size - offset >= DebugDirectoryEntrySize; offset += DebugDirectoryEntrySize) + { + if (!readMemory(DebugDirectory.Address + offset, entry)) + throw new InvalidOperationException("Could not read the PE debug directory entry."); + + DebugDirectoryEntryType type = (DebugDirectoryEntryType)BinaryPrimitives.ReadInt32LittleEndian(entry.AsSpan(12)); + uint size = BinaryPrimitives.ReadUInt32LittleEndian(entry.AsSpan(16)); + uint rva = BinaryPrimitives.ReadUInt32LittleEndian(entry.AsSpan(20)); + if (size == 0 || rva == 0) + continue; + if (!TryResolveRva(rva, size, out ulong address)) + throw new InvalidOperationException("The PE debug payload is outside the image."); + + yield return new DebugEntry(type, new TargetSpan(address, size)); + } + } + + /// + /// Reads a module's PE headers and directories. + /// + /// The module's image base address. + /// The maximum readable image size. + /// for a loaded image; otherwise, for a flat image. + /// The callback used to read target memory. + /// When this method returns, contains the module information on success, or on failure. + /// if the headers were read successfully or the image is not PE; otherwise, . + public static bool TryCreate(ulong imageBase, uint imageSize, bool isMapped, TryReadMemory readMemory, out PEImageInfo module) + { + module = null!; + Span dosHeader = stackalloc byte[DosHeaderSize]; + if (imageSize < DosHeaderSize || !TryAdd(imageBase, imageSize, out _) || !readMemory(imageBase, dosHeader)) + return false; + + if (BinaryPrimitives.ReadUInt16LittleEndian(dosHeader) != DosSignature) + { + module = new(imageBase, imageSize, 0, isMapped, []); + return true; + } + + int peHeaderOffset = BinaryPrimitives.ReadInt32LittleEndian(dosHeader[0x3c..]); + if (peHeaderOffset < 0 || (uint)peHeaderOffset > imageSize || imageSize - (uint)peHeaderOffset < PeHeaderSize + || !TryAdd(imageBase, (uint)peHeaderOffset, out ulong peHeaderAddress)) + return false; + + Span peHeader = stackalloc byte[PeHeaderSize]; + if (!readMemory(peHeaderAddress, peHeader) + || BinaryPrimitives.ReadUInt32LittleEndian(peHeader) != PeSignature) + { + return false; + } + + int dataDirectoryOffset = (PEMagic)BinaryPrimitives.ReadUInt16LittleEndian(peHeader[OptionalHeaderOffset..]) switch + { + PEMagic.PE32 => Pe32DataDirectoryOffset, + PEMagic.PE32Plus => Pe32PlusDataDirectoryOffset, + _ => -1, + }; + if (dataDirectoryOffset < 0) + return false; + + uint sizeOfHeaders = BinaryPrimitives.ReadUInt32LittleEndian(peHeader[SizeOfHeadersOffset..]); + if (sizeOfHeaders == 0 || sizeOfHeaders > imageSize) + return false; + + ImageSection[] sections = []; + if (!isMapped) + { + ushort count = BinaryPrimitives.ReadUInt16LittleEndian(peHeader[6..]); + ushort optionalHeaderSize = BinaryPrimitives.ReadUInt16LittleEndian(peHeader[20..]); + ulong sectionOffset = (ulong)peHeaderOffset + OptionalHeaderOffset + optionalHeaderSize; + if (optionalHeaderSize < dataDirectoryOffset - OptionalHeaderOffset + (DebugDirectoryIndex + 1) * 8 + || sectionOffset > sizeOfHeaders || (ulong)count * SectionHeaderSize > sizeOfHeaders - sectionOffset) + return false; + + sections = new ImageSection[count]; + Span section = stackalloc byte[SectionHeaderSize]; + for (int i = 0; i < count; i++) + { + if (!readMemory(imageBase + sectionOffset + (uint)i * SectionHeaderSize, section)) + return false; + sections[i] = new( + BinaryPrimitives.ReadUInt32LittleEndian(section[12..]), + BinaryPrimitives.ReadUInt32LittleEndian(section[20..]), + BinaryPrimitives.ReadUInt32LittleEndian(section[16..])); + } + } + + module = new(imageBase, imageSize, sizeOfHeaders, isMapped, sections); + module.ExportDirectory = module.GetDirectory(peHeader, dataDirectoryOffset, ExportDirectoryIndex); + module.ResourceDirectory = module.GetDirectory(peHeader, dataDirectoryOffset, ResourceDirectoryIndex); + module.DebugDirectory = module.GetDirectory(peHeader, dataDirectoryOffset, DebugDirectoryIndex); + return true; + } + + /// + /// Finds an exported symbol in the runtime module. + /// + /// The callback used to read target memory. + /// The UTF-8 symbol name without a null terminator. + /// When this method returns successfully, contains the exported symbol's address. + /// if the symbol was found; otherwise, . + public bool TryGetExport(TryReadMemory readMemory, ReadOnlySpan symbolName, out ulong address) + { + address = 0; + if (ExportDirectory.Size < 40) + return false; + + Span exportDirectory = stackalloc byte[40]; + if (!readMemory(ExportDirectory.Address, exportDirectory)) + return false; + + uint functionCount = BinaryPrimitives.ReadUInt32LittleEndian(exportDirectory[20..]); + uint nameCount = BinaryPrimitives.ReadUInt32LittleEndian(exportDirectory[24..]); + uint functionTableRva = BinaryPrimitives.ReadUInt32LittleEndian(exportDirectory[28..]); + uint nameTableRva = BinaryPrimitives.ReadUInt32LittleEndian(exportDirectory[32..]); + uint ordinalTableRva = BinaryPrimitives.ReadUInt32LittleEndian(exportDirectory[36..]); + if (functionCount > ExportDirectory.Size / sizeof(uint) + || nameCount > ExportDirectory.Size / sizeof(uint)) + { + return false; + } + + byte[] targetName = new byte[symbolName.Length + 1]; + Span namePointer = stackalloc byte[sizeof(uint)]; + Span ordinalBuffer = stackalloc byte[sizeof(ushort)]; + Span functionRvaBuffer = stackalloc byte[sizeof(uint)]; + for (uint nameIndex = 0; nameIndex < nameCount; nameIndex++) + { + if (!TryAdd(ImageBase, nameTableRva, nameIndex, sizeof(uint), out ulong namePointerAddress) + || !readMemory(namePointerAddress, namePointer)) + { + return false; + } + + uint nameRva = BinaryPrimitives.ReadUInt32LittleEndian(namePointer); + if (nameRva == 0 || !TryAdd(ImageBase, nameRva, out ulong nameAddress) + || !readMemory(nameAddress, targetName)) + { + continue; + } + + if (!targetName.AsSpan(0, symbolName.Length).SequenceEqual(symbolName) + || targetName[^1] != 0) + { + continue; + } + + if (!TryAdd(ImageBase, ordinalTableRva, nameIndex, sizeof(ushort), out ulong ordinalAddress) + || !readMemory(ordinalAddress, ordinalBuffer)) + { + return false; + } + + ushort ordinal = BinaryPrimitives.ReadUInt16LittleEndian(ordinalBuffer); + if (ordinal >= functionCount + || !TryAdd(ImageBase, functionTableRva, ordinal, sizeof(uint), out ulong functionAddress) + || !readMemory(functionAddress, functionRvaBuffer)) + { + return false; + } + + uint functionRva = BinaryPrimitives.ReadUInt32LittleEndian(functionRvaBuffer); + return functionRva != 0 && TryAdd(ImageBase, functionRva, out address); + } + + return false; + } + + private TargetDirectory GetDirectory(ReadOnlySpan peHeader, int dataDirectoryOffset, int directoryIndex) + { + int entryOffset = dataDirectoryOffset + (directoryIndex * 2 * sizeof(uint)); + uint rva = BinaryPrimitives.ReadUInt32LittleEndian(peHeader[entryOffset..]); + uint size = BinaryPrimitives.ReadUInt32LittleEndian(peHeader[(entryOffset + sizeof(uint))..]); + return rva != 0 && size != 0 && TryResolveRva(rva, size, out ulong address) + ? new(address, size) + : default; + } + + private bool TryResolveRva(uint rva, uint size, out ulong address) + { + address = 0; + uint offset = rva; + if (!_isMapped && rva >= SizeOfHeaders) + { + bool found = false; + foreach (ImageSection section in _sections) + { + if (rva < section.VirtualAddress) + continue; + uint delta = rva - section.VirtualAddress; + if (delta > section.RawDataSize || size > section.RawDataSize - delta + || section.RawDataOffset > uint.MaxValue - delta) + continue; + offset = section.RawDataOffset + delta; + found = true; + break; + } + if (!found) + return false; + } + else if (!_isMapped && size > SizeOfHeaders - rva) + { + return false; + } + + return offset <= _imageSize && size <= _imageSize - offset + && TryAdd(ImageBase, offset, out address) && TryAdd(address, size, out _); + } + + private static bool TryAdd(ulong baseAddress, uint offset, out ulong address) + { + address = baseAddress + offset; + return address >= baseAddress; + } + + private static bool TryAdd(ulong baseAddress, uint tableRva, uint index, uint elementSize, out ulong address) + { + ulong tableOffset = (ulong)tableRva + ((ulong)index * elementSize); + address = baseAddress + tableOffset; + return tableOffset >= tableRva && address >= baseAddress; + } + + private readonly struct TargetDirectory(ulong address, uint size) + { + public ulong Address { get; } = address; + public uint Size { get; } = size; + } + + private readonly record struct ImageSection(uint VirtualAddress, uint RawDataOffset, uint RawDataSize); + + internal readonly record struct DebugEntry(DebugDirectoryEntryType Type, TargetSpan Data); +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.ICLRDataEnumMemoryRegions.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.ICLRDataEnumMemoryRegions.cs index 3708b8cfb19af8..fea2521376a20d 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.ICLRDataEnumMemoryRegions.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.ICLRDataEnumMemoryRegions.cs @@ -2,8 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; - using System.Threading; +using Microsoft.Diagnostics.DataContractReader.Legacy.EnumMemory; + namespace Microsoft.Diagnostics.DataContractReader.Legacy; /// @@ -12,10 +13,40 @@ namespace Microsoft.Diagnostics.DataContractReader.Legacy; /// public sealed unsafe partial class SOSDacImpl : ICLRDataEnumMemoryRegions { - int ICLRDataEnumMemoryRegions.EnumMemoryRegions(void* callback, uint miniDumpFlags, int clrFlags) + int ICLRDataEnumMemoryRegions.EnumMemoryRegions(void* callback, uint miniDumpFlags, CLRDataEnumMemoryFlags clrFlags) { using Lock.Scope scope = _apiLock.EnterScope(); - return HResults.E_NOTIMPL; + if (callback is null) + return HResults.E_INVALIDARG; + + try + { + // Like the native DAC, ignore the reserved clrFlags argument. + DumpType dumpType = GetDumpType(miniDumpFlags); + MemoryRegionEmitter emitter = new((nint)callback, (uint)_target.PointerSize); + return MemoryEnumerator.Enumerate(_target, emitter, dumpType); + } + catch (Exception ex) + { + int hr = ex.HResult; + return hr < 0 ? hr : HResults.E_FAIL; + } + } + + internal static DumpType GetDumpType(uint miniDumpFlags) + { + const uint MiniDumpWithPrivateReadWriteMemory = 0x200; + const uint MiniDumpWithFullAuxiliaryState = 0x8000; + const uint MiniDumpFilterTriage = 0x100000; + + if ((miniDumpFlags & MiniDumpWithPrivateReadWriteMemory) != 0) + return DumpType.Heap; + if ((miniDumpFlags & MiniDumpWithFullAuxiliaryState) != 0) + return DumpType.Mini; + if ((miniDumpFlags & MiniDumpFilterTriage) != 0) + return DumpType.Triage; + + return DumpType.Mini; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs index 0083167031b474..cd6fbb088136ae 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs @@ -31,7 +31,7 @@ namespace Microsoft.Diagnostics.DataContractReader.Legacy; /// [GeneratedComClass] public sealed unsafe partial class SOSDacImpl - : ICustomQueryInterface, ISOSDacInterface, ISOSDacInterface2, ISOSDacInterface3, ISOSDacInterface4, ISOSDacInterface5, + : ISOSDacInterface, ISOSDacInterface2, ISOSDacInterface3, ISOSDacInterface4, ISOSDacInterface5, ISOSDacInterface6, ISOSDacInterface7, ISOSDacInterface8, ISOSDacInterface9, ISOSDacInterface10, ISOSDacInterface11, ISOSDacInterface12, ISOSDacInterface13, ISOSDacInterface14, ISOSDacInterface15, ISOSDacInterface16, ISOSDacInterface17 @@ -105,15 +105,6 @@ public SOSDacImpl(Target target, object? legacyObj, Lock apiLock) #endif } - CustomQueryInterfaceResult ICustomQueryInterface.GetInterface(ref Guid iid, out nint ppv) - { - ppv = default; - - return iid == typeof(ICLRDataEnumMemoryRegions).GUID - ? CustomQueryInterfaceResult.Failed - : CustomQueryInterfaceResult.NotHandled; - } - #region ISOSDacInterface int ISOSDacInterface.GetAppDomainConfigFile(ClrDataAddress appDomain, int count, char* configFile, uint* pNeeded) { diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs index 82748adff5b676..fb1c5d673e0900 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs @@ -12,7 +12,6 @@ using System.Text.Json; using Microsoft.Diagnostics.DataContractReader.Data; using Microsoft.Diagnostics.DataContractReader.Contracts; -using System.Collections.Frozen; namespace Microsoft.Diagnostics.DataContractReader; @@ -37,8 +36,9 @@ private readonly struct Configuration } private readonly Configuration _config; + private readonly TargetPointer _runtimeImageBase; - private readonly DataTargetDelegates _dataTargetDelegates; + private DataTargetDelegates _dataTargetDelegates; private readonly List _descriptors = []; @@ -51,9 +51,9 @@ private readonly struct Configuration // queried for whether it has been published yet (IsSubDescriptorResolved). private readonly List<(string Name, TargetPointer Slot)> _pendingSubDescriptors = []; - private IReadOnlyDictionary _contracts = new Dictionary(); - private IReadOnlyDictionary _globals = new Dictionary(); - private IReadOnlyDictionary _types = new Dictionary(); + private Dictionary _contracts = []; + private Dictionary _globals = []; + private Dictionary _types = []; public override ContractRegistry Contracts { get; } public override DataCache ProcessedData { get; } @@ -90,6 +90,7 @@ public override bool IsSubDescriptorResolved(string name) /// A callback to set a thread's context /// A callback to allocate virtual memory in the target /// Registration actions that populate the contract registry (e.g., ) + /// The runtime image base address, or when unavailable. This value is fixed for the lifetime of the target. /// The target object. public static ContractDescriptorTarget Create( ulong contractDescriptor, @@ -98,11 +99,12 @@ public static ContractDescriptorTarget Create( GetTargetThreadContextDelegate getThreadContext, SetTargetThreadContextDelegate setThreadContext, AllocVirtualDelegate allocVirtual, - Action[] contractRegistrations) + Action[] contractRegistrations, + TargetPointer runtimeImageBase = default) { DataTargetDelegates dataTargetDelegates = new DataTargetDelegates(readFromTarget, writeToTarget, getThreadContext, setThreadContext, allocVirtual); Descriptor descriptor = ReadContractDescriptor(contractDescriptor, dataTargetDelegates); - return new ContractDescriptorTarget(descriptor, dataTargetDelegates, contractRegistrations); + return new ContractDescriptorTarget(descriptor, dataTargetDelegates, contractRegistrations, runtimeImageBase); } /// @@ -118,6 +120,7 @@ public static ContractDescriptorTarget Create( /// Whether the target is little-endian /// The size of a pointer in bytes in the target process. /// Registration actions that populate the contract registry (e.g., ) + /// The runtime image base address, or when unavailable. This value is fixed for the lifetime of the target. /// The target object. public static ContractDescriptorTarget Create( ContractDescriptorParser.ContractDescriptor contractDescriptor, @@ -129,7 +132,8 @@ public static ContractDescriptorTarget Create( AllocVirtualDelegate allocVirtual, bool isLittleEndian, int pointerSize, - Action[]? contractRegistrations = null) + Action[]? contractRegistrations = null, + TargetPointer runtimeImageBase = default) { return new ContractDescriptorTarget( new Descriptor @@ -139,21 +143,61 @@ public static ContractDescriptorTarget Create( PointerData = globalPointerValues }, new DataTargetDelegates(readFromTarget, writeToTarget, getThreadContext, setThreadContext, allocVirtual), - contractRegistrations ?? []); + contractRegistrations ?? [], runtimeImageBase); } - private ContractDescriptorTarget(Descriptor mainDescriptor, DataTargetDelegates dataTargetDelegates, Action[] contractRegistrations) + private ContractDescriptorTarget(Descriptor mainDescriptor, DataTargetDelegates dataTargetDelegates, Action[] contractRegistrations, TargetPointer runtimeImageBase) { Contracts = new CachingContractRegistry(this, this.TryGetContractVersion, contractRegistrations); ProcessedData = new DataCache(this); _config = mainDescriptor.Config; + _runtimeImageBase = runtimeImageBase; _dataTargetDelegates = dataTargetDelegates; AddDescriptor(mainDescriptor); BuildDescriptors(forceBuild: true); } + public override bool TryGetRuntimeImageBase(out TargetPointer imageBase) + { + imageBase = _runtimeImageBase; + return imageBase != TargetPointer.Null; + } + + /// Reports successful target memory reads until the returned scope is disposed. + /// Receives successful memory reads as an address and size in bytes. + /// A scope that restores the previous reader when disposed. + /// Access to the target must be serialized. Nested scopes must be disposed in reverse order. + public IDisposable RegisterReadCallback(Action reportRead) + { + ReadCallbackScope scope = new(this, _dataTargetDelegates); + _dataTargetDelegates = _dataTargetDelegates.WithReadCallback(reportRead); + return scope; + } + + /// Enumerates the target memory backing the loaded descriptors and their sub-descriptor pointer slots. + /// The native descriptor headers, JSON, pointer tables, and pointer slots. + /// Does not read target memory. Externally supplied descriptors have no native backing ranges. + public IEnumerable EnumerateDescriptorMemory() + { + foreach (Descriptor descriptor in _descriptors) + { + if (descriptor.HeaderMemory.Size != 0) + yield return descriptor.HeaderMemory; + if (descriptor.JsonMemory.Size != 0) + yield return descriptor.JsonMemory; + if (descriptor.PointerDataMemory.Size != 0) + yield return descriptor.PointerDataMemory; + + foreach ((_, TargetPointer slot) in GetSubDescriptors(descriptor)) + { + if (slot != TargetPointer.Null) + yield return new TargetSpan(slot, (uint)PointerSize); + } + } + } + public override void Flush(FlushScope scope) { base.Flush(scope); @@ -301,9 +345,9 @@ private void BuildDescriptors(bool forceBuild = false) } } - _contracts = contracts.ToFrozenDictionary(); - _globals = globals.ToFrozenDictionary(); - _types = types.ToFrozenDictionary(); + _contracts = contracts; + _globals = globals; + _types = types; } private struct GlobalValue @@ -318,6 +362,9 @@ private struct Descriptor public Configuration Config { get; init; } public ContractDescriptorParser.ContractDescriptor ContractDescriptor { get; init; } public TargetPointer[] PointerData { get; init; } + public TargetSpan HeaderMemory { get; init; } + public TargetSpan JsonMemory { get; init; } + public TargetSpan PointerDataMemory { get; init; } } private static IEnumerable<(string Name, TargetPointer Slot)> GetSubDescriptors(Descriptor descriptor) @@ -349,6 +396,7 @@ private static Descriptor ReadContractDescriptor( ulong address, DataTargetDelegates dataTargetDelegates) { + ulong descriptorAddress = address; // Magic - uint64_t Span buffer = stackalloc byte[sizeof(ulong)]; if (dataTargetDelegates.ReadFromTarget(address, buffer) < 0) @@ -439,7 +487,10 @@ private static Descriptor ReadContractDescriptor( { Config = config, ContractDescriptor = contractDescriptor, - PointerData = pointerData + PointerData = pointerData, + HeaderMemory = new(new(descriptorAddress), address - descriptorAddress + (uint)pointerSize), + JsonMemory = new(descriptorAddr, descriptorSize), + PointerDataMemory = new(pointerDataAddr, (ulong)pointerDataCount * (uint)pointerSize), }; } @@ -989,6 +1040,20 @@ public void Clear() } } + private sealed class ReadCallbackScope(ContractDescriptorTarget target, DataTargetDelegates original) : IDisposable + { + private bool _disposed; + + public void Dispose() + { + if (_disposed) + return; + + target._dataTargetDelegates = original; + _disposed = true; + } + } + private readonly struct DataTargetDelegates( ReadFromTargetDelegate readFromTarget, WriteToTargetDelegate writeToTarget, @@ -1020,5 +1085,20 @@ public int AllocVirtual(ulong size, out ulong allocatedAddress) { return allocVirtual(size, out allocatedAddress); } + + public DataTargetDelegates WithReadCallback(Action reportRead) + { + ReadFromTargetDelegate reader = readFromTarget; + return new( + (address, buffer) => + { + int hr = reader(address, buffer); + if (hr >= 0) + reportRead(address, (ulong)buffer.Length); + + return hr; + }, + writeToTarget, getThreadContext, setThreadContext, allocVirtual); + } } } diff --git a/src/native/managed/cdac/README.md b/src/native/managed/cdac/README.md index e9d325dbc91ee1..8d013e7d7c752a 100644 --- a/src/native/managed/cdac/README.md +++ b/src/native/managed/cdac/README.md @@ -40,6 +40,103 @@ ISOSDacInterface* / IXCLRDataProcess (COM-style API surface) and algorithms each contract must implement. - **To write tests**: see the [tests README](tests/README.md). +## Dump collection + +`SOSDacImpl` implements `ICLRDataEnumMemoryRegions` using the shared collection +engine in `Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory`. +Both the universal cDAC and the separately built +`mscordaccore_enummemory` provider use this implementation. +The separate NativeAOT project controls its exports, architecture restrictions, +size settings, and installation without duplicating collection logic. + +With `UseCdacDumpCollectProvider=true`, the native build omits both the legacy +DAC and legacy DBI, but still builds `mscordbi_universal`. These builds retain +the Windows `CLRDEBUGINFO` resources with the CoreCLR signature and zero +DAC/DBI timestamps and image sizes, allowing dbgshim to recognize the runtime +for universal DBI activation without advertising matching legacy binaries. +Legacy DAC/DBI identity indexes in `DotNetRuntimeInfo` remain empty. +Runtime identity/version information and WER's separate dump-provider resources +are preserved. Discovery paths that require valid legacy identities remain +unsupported. + +Enumeration shares the SOS/process instance's COM identity and API lock. +`MemoryEnumerator.Enumerate` requires a `ContractDescriptorTarget`, returning +`E_NOTIMPL` for other target implementations. Each call flushes all cached target data +and contract state, then uses `RegisterReadCallback` to report successful reads, +including string terminators. Collection uses the existing target and contract instances; +it does not copy registrations or construct a separate target. The scope restores the +previous reader on success, failure, or cancellation. + +`EnumerateDescriptorMemory` separately reports native descriptor headers (including +padding), JSON, pointer tables, and both resolved and pending sub-descriptor pointer slots. +It enumerates stored ranges without rereading descriptors. An externally supplied main +descriptor has no native backing memory, but its pointer slots and any native +sub-descriptors are still included. Flushing before installing the read callback discovers +newly published sub-descriptors without allowing reporting cancellation to interrupt parsing. +This works with both COM-data-target and callback-based activation. +Both target factories accept an optional `runtimeImageBase`, stored for the lifetime of +the target and preserved across flushes and read-callback scopes. The default value +indicates that the image base is unavailable. During collection, `TryGetRuntimeImageBase` +provides the stored runtime image address. The enummemory entrypoint resolves it once through +`EntrypointHelpers.TryGetRuntimeImageBase`, using `ICLRRuntimeLocator` and falling back to +`ICLRDataTarget.GetImageBase` when the locator is unavailable or does not return `S_OK`, +matching the native DAC's lookup order. Enummemory activation requires both a nonzero +descriptor address and a nonzero runtime image base; failure to obtain either fails activation. +Universal COM activation uses only `ICLRRuntimeLocator` and never falls back to +`GetImageBase`. An unsuccessful lookup leaves the image base unavailable for universal activation. +DacDbi activation uses the supplied `runtimeBase`. Inspection scripts use the image base +of the same module that exports the descriptor, including the NativeAOT application-module +fallback. Dump-test targets likewise use the image base of the descriptor's runtime module. +`PEImageCollector` takes a `Target` and enumerates runtime and managed image regions, +without storing image information on `SOSDacImpl` or requiring a separate discovery step. +It uses `PEImageInfo` for PE parsing and reads image memory through the target. +`MemoryEnumerator` resolves the native runtime image base and passes it explicitly; +managed modules supply their own image base, size, and layout through the loader contract. +Managed module collection also includes PE headers, debug-directory tables, and +the payloads referenced by their entries, including CodeView PDB identifiers. +Mapped images resolve RVAs directly; flat images resolve them through section headers. +This preserves file-backed image mapping without including entire managed images. +Method collection includes one target-pointer-sized region starting at each +live-stack and saved exception-stack IP to satisfy method-instance address-readability +checks. Unlike the legacy DAC, it does not capture surrounding instruction windows +or indirect-call slots. Saved exception +frames collect the code, GC, and debug-map dependencies needed for source-line lookup, +even when those methods are no longer on the live stack. +`Sanitizer` owns the PDB-path and stack-trace sanitization helpers. +Triage collection strips CodeView PDB paths to filenames through the optional +memory-update callback, leaving the PDB identifiers unchanged. +Callback-only native activation does not supply an image base and leaves image +mapping to the dump writer. + +Both NativeAOT hosts forward `CLRDataCreateInstance` to shared activation in +`EntrypointHelpers.CreateInstance` in Legacy, which requires a nonzero descriptor address +and accepts the optional runtime image base without performing further discovery. +The entrypoints obtain that address from `ICLRContractLocator`, which `createdump` +implements using the descriptor discovered during module enumeration. +The universal host requires this locator to return `S_OK` and a nonzero address. +The enummemory entrypoint first resolves the required runtime image base, then tries +the locator. If the locator is absent, returns a result other than `S_OK`, or returns +a zero address, enummemory falls back to the runtime's PE exports. This also supports +WER, which does not provide `ICLRContractLocator`. +The enummemory entrypoint performs that export lookup using the already-resolved image base and `PEImageInfo` in +`Microsoft.Diagnostics.DataContractReader.Legacy`. +`PEImageInfo` owns the shared PE parsing and export lookup, allowing activation to read +the image before a `Target` exists. Shared `EntrypointHelpers.TryGetContractDescriptorAddress` +uses only `ICLRContractLocator`. +The universal host's explicit-address dbgshim entrypoint bypasses descriptor discovery. + +The COM entrypoint selects the collection mode from `miniDumpFlags`, as the native DAC does, +and passes the derived `EnumMemory.DumpType` (`Mini`, `Heap`, or `Triage`) through +the collection engine. It also wraps the native callback in `MemoryRegionEmitter`; +`MemoryEnumerator` accepts that managed adapter rather than a callback pointer and owns +the flush, read-observation scope, and descriptor-memory enumeration. The +`CLRDataEnumMemoryFlags` argument to `EnumMemoryRegions` is reserved and ignored. +Triage exception collection omits messages and remote stack traces from types +that override the `StackTrace` getter. Stack-trace strings have source-file +information removed through the optional `ICLRDataEnumMemoryRegionsCallback2` +update callback, when the dump writer supports it. This does not guarantee that +other memory selected by the dump writer is free of personal information. + ## Project structure | Directory | Purpose | @@ -49,6 +146,7 @@ ISOSDacInterface* / IXCLRDataProcess (COM-style API surface) | `Microsoft.Diagnostics.DataContractReader.Legacy` | `SOSDacImpl` — bridges `ISOSDacInterface*` COM APIs to contracts | | `Microsoft.Diagnostics.DataContractReader` | Contract/data descriptor parsing and `Target` construction | | `mscordaccore_universal` | Entry point that wires everything together | +| `mscordaccore_enummemory` | NativeAOT entrypoint host for the shared dump collector; produces `mscordaccore` | | `tests` | Unit tests with mock memory infrastructure | ## Contract specifications diff --git a/src/native/managed/cdac/cdac.slnx b/src/native/managed/cdac/cdac.slnx index 43221efbf6c7f2..7d3544fbc272bc 100644 --- a/src/native/managed/cdac/cdac.slnx +++ b/src/native/managed/cdac/cdac.slnx @@ -10,6 +10,7 @@ + diff --git a/src/native/managed/cdac/gen/Emitter.cs b/src/native/managed/cdac/gen/Emitter.cs index 9cc730a4e102e5..35855a5ad42c21 100644 --- a/src/native/managed/cdac/gen/Emitter.cs +++ b/src/native/managed/cdac/gen/Emitter.cs @@ -49,12 +49,7 @@ public static string Emit(CdacTypeModel model) } sb.AppendLine(BuildClassDoc(model)); - // A partial class may add interfaces from any declaration. Lazily-read - // types implement IReadableData so callers can force a full read. - if (hasInstanceMembers) - sb.AppendLine($"partial class {model.ClassName} : global::Microsoft.Diagnostics.DataContractReader.Data.IReadableData"); - else - sb.AppendLine($"partial class {model.ClassName}"); + sb.AppendLine($"partial class {model.ClassName}"); sb.AppendLine("{"); // Emit a static _typeNames array for LayoutSet.Resolve and ITypeHandle resolution. @@ -114,7 +109,7 @@ public static string Emit(CdacTypeModel model) EmitMemberProperty(sb, member); if (hasInstanceMembers) - EmitEnsureAllFieldsRead(sb, model); + EmitTryReadAllFields(sb, model); foreach (MemberModel member in model.Members) { @@ -364,19 +359,20 @@ private static void EmitDataDescriptorDependencyAttribute(StringBuilder sb, Memb } /// - /// Emits the implementation, - /// which touches every lazily-read member so a caller can eagerly force a full - /// read of the structure (used to validate that the whole struct is readable). + /// Emits TryReadAllFields, which touches every lazily-read member and + /// reports whether the entire structure was readable. /// - private static void EmitEnsureAllFieldsRead(StringBuilder sb, CdacTypeModel model) + private static void EmitTryReadAllFields(StringBuilder sb, CdacTypeModel model) { // Descriptor attributes short-circuit body analysis in CdacUsageGraph. // A custom initializer can also read globals, call helpers, and access // other Data properties, so let the analyzer walk this method's body. if (!model.Members.Any(member => member.Kind == MemberKind.CustomInit)) - EmitEnsureAllFieldsReadDependencyAttributes(sb, model); - sb.AppendLine(" void global::Microsoft.Diagnostics.DataContractReader.Data.IReadableData.EnsureAllFieldsRead()"); + EmitTryReadAllFieldsDependencyAttributes(sb, model); + sb.AppendLine(" internal bool TryReadAllFields()"); sb.AppendLine(" {"); + sb.AppendLine(" try"); + sb.AppendLine(" {"); foreach (MemberModel member in model.Members) { if (member.Kind == MemberKind.Field @@ -384,14 +380,20 @@ private static void EmitEnsureAllFieldsRead(StringBuilder sb, CdacTypeModel mode || member.Kind == MemberKind.InstanceDataStart || member.Kind == MemberKind.CustomInit) { - sb.AppendLine($" _ = {member.Name};"); + sb.AppendLine($" _ = {member.Name};"); } } + sb.AppendLine(" return true;"); + sb.AppendLine(" }"); + sb.AppendLine(" catch (VirtualReadException)"); + sb.AppendLine(" {"); + sb.AppendLine(" return false;"); + sb.AppendLine(" }"); sb.AppendLine(" }"); sb.AppendLine(); } - private static void EmitEnsureAllFieldsReadDependencyAttributes(StringBuilder sb, CdacTypeModel model) + private static void EmitTryReadAllFieldsDependencyAttributes(StringBuilder sb, CdacTypeModel model) { (string FieldName, string NativeType)[] fields = model.Members .Where(member => diff --git a/src/native/managed/cdac/mscordaccore_enummemory/Entrypoints.cs b/src/native/managed/cdac/mscordaccore_enummemory/Entrypoints.cs new file mode 100644 index 00000000000000..29f92ee08aef67 --- /dev/null +++ b/src/native/managed/cdac/mscordaccore_enummemory/Entrypoints.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.Marshalling; +using Microsoft.Diagnostics.DataContractReader.Legacy; + +namespace Microsoft.Diagnostics.DataContractReader; + +internal static class Entrypoints +{ + [UnmanagedCallersOnly(EntryPoint = "CLRDataCreateInstance")] + private static unsafe int CLRDataCreateInstance(Guid* pIID, IntPtr pDataTarget, void** iface) + { + if (iface == null) + return HResults.E_INVALIDARG; + + *iface = null; + if (pIID == null || pDataTarget == IntPtr.Zero) + return HResults.E_INVALIDARG; + + try + { + ICLRDataTarget dataTarget = ComInterfaceMarshaller.ConvertToManaged((void*)pDataTarget)!; + + if (!EntrypointHelpers.TryGetRuntimeImageBase(dataTarget, out TargetPointer runtimeImageBase)) + return HResults.E_FAIL; + + // WER does not provide ICLRContractLocator, so locate the descriptor through the runtime's PE exports if necessary. + EntrypointHelpers.TryGetContractDescriptorAddress(dataTarget, out TargetPointer contractAddress); + if (contractAddress == TargetPointer.Null) + { + PEImageInfo.TryReadMemory readMemory = (address, buffer) => TryReadTarget(dataTarget, address, buffer); + if (!PEImageInfo.TryCreate(runtimeImageBase, uint.MaxValue, isMapped: true, readMemory, out PEImageInfo module) + || !module.TryGetExport(readMemory, "DotNetRuntimeContractDescriptor"u8, out ulong exportAddress)) + { + return CdacHResults.CDAC_E_DESCRIPTOR_NOT_FOUND; + } + contractAddress = new(exportAddress); + } + + return EntrypointHelpers.CreateInstance(pIID, pDataTarget, IntPtr.Zero, iface, contractAddress, runtimeImageBase); + } + catch (Exception ex) + { + int hr = ex.HResult; + return hr < 0 ? hr : HResults.E_FAIL; + } + } + + private static unsafe bool TryReadTarget(ICLRDataTarget dataTarget, ulong address, Span buffer) + { + fixed (byte* bufferPointer = buffer) + { + uint bytesRead; + return dataTarget.ReadVirtual(address, bufferPointer, (uint)buffer.Length, &bytesRead) >= 0 + && bytesRead == (uint)buffer.Length; + } + } +} diff --git a/src/native/managed/cdac/mscordaccore_enummemory/GenerateWindowsVersionResource.ps1 b/src/native/managed/cdac/mscordaccore_enummemory/GenerateWindowsVersionResource.ps1 new file mode 100644 index 00000000000000..1d2a5440ace2d6 --- /dev/null +++ b/src/native/managed/cdac/mscordaccore_enummemory/GenerateWindowsVersionResource.ps1 @@ -0,0 +1,113 @@ +param ( + [Parameter(Mandatory = $true)] + [string] $outputPath, + + [Parameter(Mandatory = $true)] + [string] $fileVersion, + + [Parameter(Mandatory = $true)] + [string] $productVersion, + + [Parameter(Mandatory = $true)] + [string] $companyName, + + [Parameter(Mandatory = $true)] + [string] $productName, + + [Parameter(Mandatory = $true)] + [string] $legalCopyright +) + +function Escape-ResourceString { + param ( + [Parameter(Mandatory = $true)] + [string] $value + ) + + return $value.Replace('\', '\\').Replace('"', '\"') +} + +$versionParts = $fileVersion.Split('.') +if ($versionParts.Length -ne 4) { + throw "The file version '$fileVersion' does not contain four components." +} + +foreach ($part in $versionParts) { + [uint16] $parsedPart = 0 + if (-not [uint16]::TryParse($part, [ref] $parsedPart)) { + throw "The file version component '$part' is not a valid unsigned 16-bit integer." + } +} + +$kitsRoot = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows Kits\Installed Roots').KitsRoot10 +$resourceCompiler = Get-ChildItem -Path (Join-Path $kitsRoot 'bin\*\x64\rc.exe') | + Sort-Object -Property { [version] $_.Directory.Parent.Name } -Descending | + Select-Object -First 1 +if ($null -eq $resourceCompiler) { + throw "Unable to locate the Windows SDK resource compiler under '$kitsRoot'." +} + +$sdkVersion = $resourceCompiler.Directory.Parent.Name +$sdkIncludeRoot = Join-Path $kitsRoot "Include\$sdkVersion" +$sdkIncludeDirectories = @('shared', 'um', 'ucrt', 'winrt') | + ForEach-Object { Join-Path $sdkIncludeRoot $_ } +foreach ($includeDirectory in $sdkIncludeDirectories) { + if (-not [System.IO.Directory]::Exists($includeDirectory)) { + throw "Unable to locate the Windows SDK include directory '$includeDirectory'." + } +} + +$outputDirectory = Split-Path -Parent $outputPath +[System.IO.Directory]::CreateDirectory($outputDirectory) | Out-Null +$resourceSource = Join-Path $outputDirectory 'mscordaccore.version.rc' +$numericVersion = $versionParts -join ',' + +$resourceContents = @" +#include + +1 VERSIONINFO +FILEVERSION $numericVersion +PRODUCTVERSION $numericVersion +FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +FILEFLAGS 0 +FILEOS VOS__WINDOWS32 +FILETYPE VFT_DLL +FILESUBTYPE VFT2_UNKNOWN +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904B0" + BEGIN + VALUE "CompanyName", "$(Escape-ResourceString $companyName)" + VALUE "FileDescription", "mscordaccore" + VALUE "FileVersion", "$(Escape-ResourceString $fileVersion)" + VALUE "InternalName", "mscordaccore.dll" + VALUE "LegalCopyright", "$(Escape-ResourceString $legalCopyright)" + VALUE "OriginalFilename", "mscordaccore.dll" + VALUE "ProductName", "$(Escape-ResourceString $productName)" + VALUE "ProductVersion", "$(Escape-ResourceString $productVersion)" + END + END + + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0409, 1200 + END +END +"@ + +[System.IO.File]::WriteAllText( + $resourceSource, + $resourceContents, + [System.Text.UnicodeEncoding]::new($false, $true)) + +$resourceCompilerArguments = @('/nologo', '/fo', $outputPath) +foreach ($includeDirectory in $sdkIncludeDirectories) { + $resourceCompilerArguments += @('/i', $includeDirectory) +} +$resourceCompilerArguments += $resourceSource + +& $resourceCompiler.FullName $resourceCompilerArguments +if ($LASTEXITCODE -ne 0) { + throw "The Windows SDK resource compiler failed with exit code $LASTEXITCODE." +} diff --git a/src/native/managed/cdac/mscordaccore_enummemory/mscordaccore_enummemory.csproj b/src/native/managed/cdac/mscordaccore_enummemory/mscordaccore_enummemory.csproj new file mode 100644 index 00000000000000..5d06b0a58b5b16 --- /dev/null +++ b/src/native/managed/cdac/mscordaccore_enummemory/mscordaccore_enummemory.csproj @@ -0,0 +1,85 @@ + + + true + + + + + $(LibPrefix)mscordaccore + false + $(CDacTfm) + Microsoft.Diagnostics.DataContractReader + enable + true + false + true + false + Size + false + true + + + + <_TargetHostArchitecture Condition="'$(RuntimeIdentifier)' != ''">$(RuntimeIdentifier.Substring($([MSBuild]::Add($(RuntimeIdentifier.LastIndexOf('-')), 1)))) + <_TargetHostArchitecture Condition="'$(_TargetHostArchitecture)' == ''">$(TargetArchitecture) + + + + + + + + + + + + + + + $(IntermediateOutputPath)mscordaccore.version.res + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/native/managed/cdac/mscordaccore_universal/Entrypoints.cs b/src/native/managed/cdac/mscordaccore_universal/Entrypoints.cs index 54d42dcc1bde8d..2f1416af2ee307 100644 --- a/src/native/managed/cdac/mscordaccore_universal/Entrypoints.cs +++ b/src/native/managed/cdac/mscordaccore_universal/Entrypoints.cs @@ -9,7 +9,10 @@ namespace Microsoft.Diagnostics.DataContractReader; -internal static class Entrypoints +/// +/// Provides native entrypoints and debugger target construction for the universal cDAC. +/// +public static class Entrypoints { private sealed class CdacHandle { @@ -264,7 +267,7 @@ private static unsafe int CreateDacDbiInterface(IntPtr handle, IntPtr legacyImpl [UnmanagedCallersOnly(EntryPoint = "CLRDataCreateInstanceWithFallback")] private static unsafe int CLRDataCreateInstanceWithFallback(Guid* pIID, IntPtr /*ICLRDataTarget*/ pLegacyTarget, IntPtr pLegacyImpl, void** iface) { - return CLRDataCreateInstanceImpl(pIID, pLegacyTarget, pLegacyImpl, iface); + return CreateInstance(pIID, pLegacyTarget, pLegacyImpl, iface); } [UnmanagedCallersOnly(EntryPoint = "DacDbiInterfaceInstance")] @@ -297,7 +300,7 @@ private static unsafe int DacDbiInterfaceInstance( ICorDebugDataTarget dataTarget = UniqueComInterfaceMarshaller.ConvertToManaged((void*)pTarget)!; dataTargetComObject = (ComObject)(object)dataTarget; - ContractDescriptorTarget target = CreateTargetFromCorDebugDataTarget(dataTarget, contractDescriptorAddress); + ContractDescriptorTarget target = CreateTargetFromCorDebugDataTarget(dataTarget, contractDescriptorAddress, runtimeBase); Legacy.DacDbiImpl impl = new(target, legacyObj: null, apiLock: new Lock(), dataTargetComObject: dataTargetComObject); *iface = ComInterfaceMarshaller.ConvertToUnmanaged(impl); dataTargetComObject = null; @@ -322,39 +325,31 @@ private static unsafe int DacDbiInterfaceInstance( [UnmanagedCallersOnly(EntryPoint = "CLRDataCreateInstance")] private static unsafe int CLRDataCreateInstance(Guid* pIID, IntPtr /*ICLRDataTarget*/ pLegacyTarget, void** iface) { - return CLRDataCreateInstanceImpl(pIID, pLegacyTarget, IntPtr.Zero, iface); + return CreateInstance(pIID, pLegacyTarget, IntPtr.Zero, iface); } - // Creates a cDAC data-access instance from an explicit contract descriptor address, - // so the data target does not need to implement ICLRContractLocator. - [UnmanagedCallersOnly(EntryPoint = "DbgShimCreateInstanceFromContractDescriptor")] - private static unsafe int DbgShimCreateInstanceFromContractDescriptor(Guid* pIID, IntPtr /*ICLRDataTarget*/ pLegacyTarget, ulong contractDescriptorAddr, void** iface) + private static unsafe int CreateInstance(Guid* pIID, IntPtr pDataTarget, IntPtr legacyImplPtr, void** iface, TargetPointer contractAddress = default) { - if (pLegacyTarget == IntPtr.Zero || contractDescriptorAddr == 0 || iface == null) + if (iface == null) return HResults.E_INVALIDARG; + *iface = null; + if (pIID == null || pDataTarget == IntPtr.Zero) + return HResults.E_INVALIDARG; try { - object legacyTarget = ComInterfaceMarshaller.ConvertToManaged((void*)pLegacyTarget)!; - return CreateInstanceFromContractDescriptorCore(pIID, legacyTarget, contractDescriptorAddr, legacyImplPtr: IntPtr.Zero, new Lock(), iface); - } - catch (Exception ex) - { - int hr = ex.HResult; - return hr < 0 ? hr : HResults.E_FAIL; - } - } + ICLRDataTarget dataTarget = ComInterfaceMarshaller.ConvertToManaged((void*)pDataTarget)!; + if (contractAddress == TargetPointer.Null && !EntrypointHelpers.TryGetContractDescriptorAddress(dataTarget, out contractAddress)) + { + return CdacHResults.CDAC_E_DESCRIPTOR_NOT_FOUND; + } - private static unsafe int CLRDataCreateInstanceImpl(Guid* pIID, IntPtr /*ICLRDataTarget*/ pLegacyTarget, IntPtr pLegacyImpl, void** iface) - { - if (pLegacyTarget == IntPtr.Zero || iface == null) - return HResults.E_INVALIDARG; - *iface = null; + ulong runtimeImageBase = 0; + if (dataTarget is ICLRRuntimeLocator runtimeLocator && runtimeLocator.GetRuntimeBase(&runtimeImageBase) != HResults.S_OK) + runtimeImageBase = 0; - try - { - return CLRDataCreateInstanceCore(pIID, pLegacyTarget, pLegacyImpl, new Lock(), iface); + return EntrypointHelpers.CreateInstance(pIID, pDataTarget, legacyImplPtr, iface, contractAddress, runtimeImageBase); } catch (Exception ex) { @@ -363,127 +358,28 @@ private static unsafe int CLRDataCreateInstanceImpl(Guid* pIID, IntPtr /*ICLRDat } } - private static unsafe int CLRDataCreateInstanceCore(Guid* pIID, IntPtr /*ICLRDataTarget*/ pLegacyTarget, IntPtr pLegacyImpl, Lock apiLock, void** iface) - { - object legacyTarget = ComInterfaceMarshaller.ConvertToManaged((void*)pLegacyTarget)!; - - ICLRContractLocator contractLocator = legacyTarget as ICLRContractLocator ?? throw new ArgumentException( - $"{nameof(pLegacyTarget)} does not implement {nameof(ICLRContractLocator)}", nameof(pLegacyTarget)); - - ulong contractAddress; - int hr = contractLocator.GetContractDescriptor(&contractAddress); - if (hr != 0) - { - throw new InvalidOperationException( - $"{nameof(ICLRContractLocator)} failed to fetch the contract descriptor with HRESULT: 0x{hr:x}.") - { - HResult = CdacHResults.CDAC_E_DESCRIPTOR_NOT_FOUND - }; - } - - return CreateInstanceFromContractDescriptorCore(pIID, legacyTarget, contractAddress, pLegacyImpl, apiLock, iface); - } - - private static unsafe int CreateInstanceFromContractDescriptorCore(Guid* pIID, object legacyTarget, ulong contractAddress, IntPtr legacyImplPtr, Lock apiLock, void** iface) + // Creates a cDAC data-access instance from an explicit contract descriptor address, + // so the data target does not need to implement ICLRContractLocator. + [UnmanagedCallersOnly(EntryPoint = "DbgShimCreateInstanceFromContractDescriptor")] + private static unsafe int DbgShimCreateInstanceFromContractDescriptor(Guid* pIID, IntPtr /*ICLRDataTarget*/ pLegacyTarget, ulong contractDescriptorAddr, void** iface) { - ICLRDataTarget dataTarget = legacyTarget as ICLRDataTarget ?? throw new ArgumentException( - $"Data target does not implement {nameof(ICLRDataTarget)}", nameof(legacyTarget)); - - // Try to get ICLRDataTarget2 for memory allocation support (optional) - ICLRDataTarget2? dataTarget2 = legacyTarget as ICLRDataTarget2; - - // Build the allocVirtual delegate if the target supports ICLRDataTarget2 - ContractDescriptorTarget.AllocVirtualDelegate allocVirtual = (ulong size, out ulong allocatedAddress) => - { - allocatedAddress = 0; - return HResults.E_NOTIMPL; - }; - - if (dataTarget2 is not null) - { - // Windows virtual memory allocation flags used by ICLRDataTarget2::AllocVirtual. - const uint MEM_COMMIT = 0x1000; - const uint PAGE_READWRITE = 0x04; - - allocVirtual = (ulong size, out ulong allocatedAddress) => - { - ClrDataAddress addr; - int result = dataTarget2.AllocVirtual(0, (uint)size, MEM_COMMIT, PAGE_READWRITE, &addr); - allocatedAddress = (ulong)addr; - return result; - }; - } - - ContractDescriptorTarget target = ContractDescriptorTarget.Create( - contractAddress, - (address, buffer) => - { - fixed (byte* bufferPtr = buffer) - { - uint bytesRead; - return dataTarget.ReadVirtual(address, bufferPtr, (uint)buffer.Length, &bytesRead); - } - }, - (address, buffer) => - { - fixed (byte* bufferPtr = buffer) - { - uint bytesWritten; - return dataTarget.WriteVirtual(address, bufferPtr, (uint)buffer.Length, &bytesWritten); - } - }, - (threadId, contextFlags, bufferToFill) => - { - fixed (byte* bufferPtr = bufferToFill) - { - return dataTarget.GetThreadContext(threadId, contextFlags, (uint)bufferToFill.Length, bufferPtr); - } - }, - (threadId, context) => - { - fixed (byte* contextPtr = context) - { - if (((nuint)contextPtr & (ContextAlignment - 1)) == 0) - { - return dataTarget.SetThreadContext(threadId, (uint)context.Length, contextPtr); - } - - byte* alignedBuffer = (byte*)NativeMemory.AlignedAlloc((nuint)context.Length, ContextAlignment); - try - { - context.CopyTo(new Span(alignedBuffer, context.Length)); - return dataTarget.SetThreadContext(threadId, (uint)context.Length, alignedBuffer); - } - finally - { - NativeMemory.AlignedFree(alignedBuffer); - } - } - }, - allocVirtual, - [Contracts.CoreCLRContracts.Register]); - - Contracts.CoreCLRContracts.ValidateForDataAccess(target, apiLock); - - object? legacyImpl = legacyImplPtr != IntPtr.Zero - ? ComInterfaceMarshaller.ConvertToManaged((void*)legacyImplPtr) - : null; - - Legacy.SOSDacImpl impl = new(target, legacyImpl, apiLock); - void* ccw = ComInterfaceMarshaller.ConvertToUnmanaged(impl); - int hrQI = Marshal.QueryInterface((nint)ccw, *pIID, out nint ptrToIface); - - // Decrement reference count on ccw because QI incremented it - ComInterfaceMarshaller.Free(ccw); - - if (hrQI < 0) - return hrQI; + if (iface != null) + *iface = null; + if (contractDescriptorAddr == 0) + return HResults.E_INVALIDARG; - *iface = (void*)ptrToIface; - return 0; + return CreateInstance(pIID, pLegacyTarget, IntPtr.Zero, iface, contractDescriptorAddr); } - private static unsafe ContractDescriptorTarget CreateTargetFromCorDebugDataTarget(object targetObject, ulong contractAddress) + /// + /// Creates a contract descriptor target backed by a debugger data target. + /// + /// The object implementing . + /// The target contract descriptor address. + /// The runtime image base address, or zero when unavailable. + /// A target registered with the CoreCLR contracts. + /// does not implement . + public static unsafe ContractDescriptorTarget CreateTargetFromCorDebugDataTarget(object targetObject, ulong contractAddress, ulong runtimeBase) { ICorDebugDataTarget dataTarget = targetObject as ICorDebugDataTarget ?? throw new ArgumentException( $"Data target does not implement {nameof(ICorDebugDataTarget)}", nameof(targetObject)); @@ -571,6 +467,7 @@ private static unsafe ContractDescriptorTarget CreateTargetFromCorDebugDataTarge allocatedAddress = 0; return HResults.E_NOTIMPL; }, - [Contracts.CoreCLRContracts.Register]); + [Contracts.CoreCLRContracts.Register], + runtimeBase); } } diff --git a/src/native/managed/cdac/scripts/DescriptorCommand.cs b/src/native/managed/cdac/scripts/DescriptorCommand.cs index a9e850906b6f74..1b111fd55343f2 100644 --- a/src/native/managed/cdac/scripts/DescriptorCommand.cs +++ b/src/native/managed/cdac/scripts/DescriptorCommand.cs @@ -42,7 +42,7 @@ private int Run(ParseResult parse) private static void Execute(string dumpPath) { using DataTarget dt = DataTarget.LoadDump(dumpPath); - ulong contractAddr = DumpHelpers.FindContractDescriptor(dt); + ulong contractAddr = DumpHelpers.FindContractDescriptor(dt, out _); Console.WriteLine($"Dump: {dumpPath}"); Console.WriteLine($"Pointer size: {dt.DataReader.PointerSize}"); diff --git a/src/native/managed/cdac/scripts/DumpHelpers.cs b/src/native/managed/cdac/scripts/DumpHelpers.cs index 9f7c6eedaeb7d7..f55d575ef3655e 100644 --- a/src/native/managed/cdac/scripts/DumpHelpers.cs +++ b/src/native/managed/cdac/scripts/DumpHelpers.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; +using System.Linq; using Microsoft.Diagnostics.DataContractReader; using Microsoft.Diagnostics.DataContractReader.Contracts; using Microsoft.Diagnostics.Runtime; @@ -11,19 +13,24 @@ internal static class DumpHelpers { private static readonly string[] s_coreClrModuleNames = ["coreclr.dll", "libcoreclr.so", "libcoreclr.dylib"]; - public static ulong FindContractDescriptor(DataTarget dt) + public static ulong FindContractDescriptor(DataTarget dt, out ulong runtimeImageBase) { - // First pass: look in known CoreCLR modules. - // Second pass: check all remaining modules (covers NativeAOT where the export is in the app binary). + runtimeImageBase = 0; + // Prefer CoreCLR modules, retaining the first other export for NativeAOT application images. ulong fallback = 0; + ulong fallbackImageBase = 0; foreach (ModuleInfo module in dt.DataReader.EnumerateModules()) { ulong addr = module.GetExportSymbolAddress("DotNetRuntimeContractDescriptor"); if (addr == 0) continue; + ulong imageBase = module.ImageBase; if (dt.DataReader.PointerSize == 4) + { addr &= 0xFFFF_FFFF; + imageBase &= 0xFFFF_FFFF; + } string? fileName = module.FileName; if (fileName is not null) @@ -31,22 +38,31 @@ public static ulong FindContractDescriptor(DataTarget dt) int lastSep = Math.Max(fileName.LastIndexOf('/'), fileName.LastIndexOf('\\')); string name = lastSep >= 0 ? fileName[(lastSep + 1)..] : fileName; if (s_coreClrModuleNames.Contains(name, StringComparer.OrdinalIgnoreCase)) + { + runtimeImageBase = imageBase; return addr; + } } if (fallback == 0) + { fallback = addr; + fallbackImageBase = imageBase; + } } if (fallback != 0) + { + runtimeImageBase = fallbackImageBase; return fallback; + } throw new InvalidOperationException("Could not find DotNetRuntimeContractDescriptor export."); } public static ContractDescriptorTarget CreateCdacTarget(DataTarget dt) { - ulong contractAddr = FindContractDescriptor(dt); + ulong contractAddr = FindContractDescriptor(dt, out ulong runtimeImageBase); return ContractDescriptorTarget.Create( contractAddr, @@ -56,6 +72,7 @@ public static ContractDescriptorTarget CreateCdacTarget(DataTarget dt) dt.DataReader.GetThreadContext(threadId, contextFlags, buffer) ? 0 : -1, (uint threadId, ReadOnlySpan context) => -1, (ulong _, out ulong _) => throw new NotImplementedException("Scripts do not provide AllocVirtual"), - [CoreCLRContracts.Register]); + [CoreCLRContracts.Register], + runtimeImageBase); } } diff --git a/src/native/managed/cdac/tests/DataGenerator/DataGeneratorTests.cs b/src/native/managed/cdac/tests/DataGenerator/DataGeneratorTests.cs index ad00dcddad3bef..96d1e74550a638 100644 --- a/src/native/managed/cdac/tests/DataGenerator/DataGeneratorTests.cs +++ b/src/native/managed/cdac/tests/DataGenerator/DataGeneratorTests.cs @@ -87,31 +87,31 @@ public void GeneratedHelpersDeclareDescriptorDependencies() } [Fact] - public void EnsureAllFieldsReadDeclaresGeneratedPropertyDependencies() + public void TryReadAllFieldsDeclaresGeneratedPropertyDependencies() { - InterfaceMapping map = typeof(TestNative).GetInterfaceMap(typeof(IReadableData)); - int index = Array.IndexOf(map.InterfaceMethods, typeof(IReadableData).GetMethod(nameof(IReadableData.EnsureAllFieldsRead))!); - MethodInfo ensureAllFieldsRead = map.TargetMethods[index]; + MethodInfo tryReadAllFields = typeof(TestNative).GetMethod( + "TryReadAllFields", + BindingFlags.Instance | BindingFlags.NonPublic)!; Assert.Equal( [ (FieldName: "A", NativeType: "uint32"), (FieldName: "B", NativeType: "pointer"), ], - Dependencies(ensureAllFieldsRead) + Dependencies(tryReadAllFields) .Select(attribute => (attribute.FieldName, attribute.NativeType))); - Assert.False(UsesTypeSize(ensureAllFieldsRead)); + Assert.False(UsesTypeSize(tryReadAllFields)); } [Fact] - public void EnsureAllFieldsReadWithCustomInitHasNoShortCircuitingDependencies() + public void TryReadAllFieldsWithCustomInitHasNoShortCircuitingDependencies() { - InterfaceMapping map = typeof(TestCustomInit).GetInterfaceMap(typeof(IReadableData)); - int index = Array.IndexOf(map.InterfaceMethods, typeof(IReadableData).GetMethod(nameof(IReadableData.EnsureAllFieldsRead))!); - MethodInfo ensureAllFieldsRead = map.TargetMethods[index]; + MethodInfo tryReadAllFields = typeof(TestCustomInit).GetMethod( + "TryReadAllFields", + BindingFlags.Instance | BindingFlags.NonPublic)!; - Assert.Empty(Dependencies(ensureAllFieldsRead)); - Assert.False(UsesTypeSize(ensureAllFieldsRead)); + Assert.Empty(Dependencies(tryReadAllFields)); + Assert.False(UsesTypeSize(tryReadAllFields)); } [Fact] diff --git a/src/native/managed/cdac/tests/DataGenerator/TestTarget.cs b/src/native/managed/cdac/tests/DataGenerator/TestTarget.cs index da18f2b7abc335..d3438726b3e63f 100644 --- a/src/native/managed/cdac/tests/DataGenerator/TestTarget.cs +++ b/src/native/managed/cdac/tests/DataGenerator/TestTarget.cs @@ -257,6 +257,7 @@ public override bool TryReadGlobalPointer(string name, [NotNullWhen(true)] out T public override bool IsAlignedToPointerSize(TargetPointer pointer) => throw new NotImplementedException(); public override bool TryGetThreadContext(ulong threadId, uint contextFlags, Span buffer) => throw new NotImplementedException(); public override bool TrySetThreadContext(ulong threadId, ReadOnlySpan context) => throw new NotImplementedException(); + public override bool TryGetRuntimeImageBase(out TargetPointer imageBase) => throw new NotImplementedException(); // --- Stub ContractRegistry ------------------------------------- diff --git a/src/native/managed/cdac/tests/DumpTests/AuxiliarySymbolsDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/AuxiliarySymbolsDumpTests.cs index da2b1b76e558be..30ed1a57730ccf 100644 --- a/src/native/managed/cdac/tests/DumpTests/AuxiliarySymbolsDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/AuxiliarySymbolsDumpTests.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; -using Microsoft.Diagnostics.DataContractReader.Contracts; using Microsoft.Diagnostics.DataContractReader.TestInfrastructure; using Xunit; @@ -11,12 +10,11 @@ namespace Microsoft.Diagnostics.DataContractReader.DumpTests; public class AuxiliarySymbolsDumpTests : DumpTestBase { protected override string DebuggeeName => "BasicThreads"; - protected override string DumpType => "full"; [ConditionalTheory] [MemberData(nameof(TestConfigurations))] [SkipOnVersion("net10.0", "Allocation helpers are not included in the .NET 10 auxiliary symbol table")] - public void JitHelpersAreReachableByAddress(TestConfiguration config) + public void JitHelpersAreIncludedInHeapDump(TestConfiguration config) { InitializeDumpTest(config); @@ -35,21 +33,11 @@ public void JitHelpersAreReachableByAddress(TestConfiguration config) ]; Dictionary helpersByAddress = []; - TargetPointer table = Target.ReadGlobalPointer(Constants.Globals.AuxiliarySymbols); - uint count = Target.Read(Target.ReadGlobalPointer(Constants.Globals.AuxiliarySymbolCount)); - Target.TypeInfo typeInfo = Target.GetTypeInfo(DataType.AuxiliarySymbolInfo); - uint entrySize = typeInfo.Size!.Value; - int addressOffset = typeInfo.Fields["Address"].Offset; - int nameOffset = typeInfo.Fields["Name"].Offset; - - for (uint i = 0; i < count; i++) + foreach ((TargetCodePointer address, string name) in + Target.Contracts.AuxiliarySymbols.EnumerateAuxiliarySymbols()) { - TargetPointer entry = table + ((ulong)i * entrySize); - TargetPointer namePointer = Target.ReadPointer(entry + (ulong)nameOffset); - string name = Target.ReadUtf8String(namePointer); if (expectedHelpers.Remove(name)) { - TargetCodePointer address = Target.ReadCodePointer(entry + (ulong)addressOffset); Assert.NotEqual(TargetCodePointer.Null, address); helpersByAddress.TryAdd(address, name); } diff --git a/src/native/managed/cdac/tests/TestInfrastructure/ClrMdDumpHost.cs b/src/native/managed/cdac/tests/TestInfrastructure/ClrMdDumpHost.cs index 341a4b2a8c8a20..bf0dac6a6386ef 100644 --- a/src/native/managed/cdac/tests/TestInfrastructure/ClrMdDumpHost.cs +++ b/src/native/managed/cdac/tests/TestInfrastructure/ClrMdDumpHost.cs @@ -124,11 +124,13 @@ public int GetThreadContext(uint threadId, uint contextFlags, Span buffer) } /// - /// Locate the DotNetRuntimeContractDescriptor symbol address in the dump. + /// Locate the DotNetRuntimeContractDescriptor symbol address in the dump and return + /// the image base of the module that exports it. /// Uses ClrMD's built-in export resolution which handles PE, ELF, and Mach-O formats. /// - public ulong FindContractDescriptorAddress() + public ulong FindContractDescriptorAddress(out ulong runtimeImageBase) { + runtimeImageBase = 0; foreach (ModuleInfo module in _dataTarget.DataReader.EnumerateModules()) { string? fileName = module.FileName; @@ -146,11 +148,15 @@ public ulong FindContractDescriptorAddress() ulong address = module.GetExportSymbolAddress("DotNetRuntimeContractDescriptor"); if (address != 0) { + runtimeImageBase = module.ImageBase; // ClrMD may return addresses with spurious upper bits on 32-bit targets // (observed on ARM32 ELF). Mask to the target's pointer size. // https://github.com/microsoft/clrmd/issues/1407 if (_dataTarget.DataReader.PointerSize == 4) + { address &= 0xFFFF_FFFF; + runtimeImageBase &= 0xFFFF_FFFF; + } return address; } diff --git a/src/native/managed/cdac/tests/TestInfrastructure/DumpTestBase.cs b/src/native/managed/cdac/tests/TestInfrastructure/DumpTestBase.cs index 865402813cb6b9..f18f4b2ab2a82f 100644 --- a/src/native/managed/cdac/tests/TestInfrastructure/DumpTestBase.cs +++ b/src/native/managed/cdac/tests/TestInfrastructure/DumpTestBase.cs @@ -106,7 +106,7 @@ protected void InitializeDumpTest(TestConfiguration config, string debuggeeName, } _host = ClrMdDumpHost.Open(dumpPath, GetSymbolPaths(debuggeeName, versionDir)); - ulong contractDescriptor = _host.FindContractDescriptorAddress(); + ulong contractDescriptor = _host.FindContractDescriptorAddress(out ulong runtimeImageBase); _target = ContractDescriptorTarget.Create( contractDescriptor, @@ -115,7 +115,8 @@ protected void InitializeDumpTest(TestConfiguration config, string debuggeeName, _host.GetThreadContext, setThreadContext: static (_, _) => -1, allocVirtual: static (ulong _, out ulong _) => throw new NotImplementedException("Dump tests do not provide AllocVirtual"), - [Contracts.CoreCLRContracts.Register]); + [Contracts.CoreCLRContracts.Register], + runtimeImageBase); } /// @@ -129,7 +130,7 @@ protected void InitializeDumpTestFromPath(string dumpPath) throw new SkipTestException($"Dump not found: {dumpPath}"); _host = ClrMdDumpHost.Open(dumpPath, []); - ulong contractDescriptor = _host.FindContractDescriptorAddress(); + ulong contractDescriptor = _host.FindContractDescriptorAddress(out ulong runtimeImageBase); _target = ContractDescriptorTarget.Create( contractDescriptor, @@ -138,7 +139,8 @@ protected void InitializeDumpTestFromPath(string dumpPath) _host.GetThreadContext, setThreadContext: static (_, _) => -1, allocVirtual: static (ulong _, out ulong _) => throw new NotImplementedException("Dump tests do not provide AllocVirtual"), - [Contracts.CoreCLRContracts.Register]); + [Contracts.CoreCLRContracts.Register], + runtimeImageBase); } public void Dispose() diff --git a/src/native/managed/cdac/tests/TestInfrastructure/TestPlaceholderTarget.cs b/src/native/managed/cdac/tests/TestInfrastructure/TestPlaceholderTarget.cs index 7ef4525766ca53..68361e0e4f3c81 100644 --- a/src/native/managed/cdac/tests/TestInfrastructure/TestPlaceholderTarget.cs +++ b/src/native/managed/cdac/tests/TestInfrastructure/TestPlaceholderTarget.cs @@ -552,6 +552,7 @@ public override bool TryGetTypeInfo(string typeName, out Target.TypeInfo info) public override bool TryGetThreadContext(ulong threadId, uint contextFlags, Span bufferToFill) => throw new NotImplementedException(); public override bool TrySetThreadContext(ulong threadId, ReadOnlySpan context) => throw new NotImplementedException(); + public override bool TryGetRuntimeImageBase(out TargetPointer imageBase) => throw new NotImplementedException(); public override Target.IDataCache ProcessedData => _dataCache; public override ContractRegistry Contracts => _contractRegistry; diff --git a/src/native/managed/cdac/tests/UnitTests/ContractDescriptor/TargetTests.cs b/src/native/managed/cdac/tests/UnitTests/ContractDescriptor/TargetTests.cs index 66ded94c2f11f7..c1debd01928470 100644 --- a/src/native/managed/cdac/tests/UnitTests/ContractDescriptor/TargetTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/ContractDescriptor/TargetTests.cs @@ -386,7 +386,7 @@ public void TryGetContract_UnrecognizedVersion_ReturnsContractUnrecognizedExcept // The contracts required by the data-access interfaces, advertised at the versions // CoreCLRContracts registers. Mirrors CoreCLRContracts.ValidateForDataAccess. - private static readonly IReadOnlyDictionary s_requiredDataAccessContracts = + internal static readonly IReadOnlyDictionary s_requiredDataAccessContracts = new Dictionary { ["AuxiliarySymbols"] = "c1", diff --git a/src/native/managed/cdac/tests/UnitTests/EnumMemoryTests.cs b/src/native/managed/cdac/tests/UnitTests/EnumMemoryTests.cs new file mode 100644 index 00000000000000..298f636ea81386 --- /dev/null +++ b/src/native/managed/cdac/tests/UnitTests/EnumMemoryTests.cs @@ -0,0 +1,2064 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Reflection; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using System.Reflection.PortableExecutable; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.Marshalling; +using System.Text; +using System.Threading; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.DataContractReader.Legacy; +using Microsoft.Diagnostics.DataContractReader.Legacy.EnumMemory; +using Microsoft.Diagnostics.DataContractReader.TestInfrastructure; +using Microsoft.Diagnostics.DataContractReader.TestInfrastructure.ContractDescriptor; +using Microsoft.Diagnostics.Runtime; +using Moq; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.Tests; + +public unsafe partial class EnumMemoryTests +{ + [Theory] + [InlineData(false, 0x1234_5678_9000ul)] + [InlineData(true, 0x1234_5678_9000ul)] + [InlineData(false, 0ul)] + [InlineData(true, 0ul)] + public void RuntimeImageBase_PreservesProvidedAddressAcrossFlush(bool nativeDescriptor, ulong address) + { + TargetTestHelpers helpers = new(new() { IsLittleEndian = true, Is64Bit = true }); + ContractDescriptorBuilder builder = new(helpers); + ulong descriptorAddress = new ContractDescriptorBuilder.DescriptorBuilder(builder).CreateSubDescriptor(0x20000, 0x21000, 0x22000); + MockMemorySpace.MemoryContext memory = builder.GetMemoryContext(); + ContractDescriptorTarget target = nativeDescriptor + ? ContractDescriptorTarget.Create(descriptorAddress, memory.ReadFromTarget, memory.WriteToTarget, + (_, _, _) => HResults.E_NOTIMPL, (_, _) => HResults.E_NOTIMPL, + (ulong _, out ulong allocated) => { allocated = 0; return HResults.E_NOTIMPL; }, [], runtimeImageBase: address) + : ContractDescriptorTarget.Create(ContractDescriptorParser.ParseCompact("""{"version":2}"""u8), [], + memory.ReadFromTarget, memory.WriteToTarget, (_, _, _) => HResults.E_NOTIMPL, (_, _) => HResults.E_NOTIMPL, + (ulong _, out ulong allocated) => { allocated = 0; return HResults.E_NOTIMPL; }, + isLittleEndian: true, pointerSize: 8, runtimeImageBase: address); + + bool expected = address != 0; + Assert.Equal(expected, target.TryGetRuntimeImageBase(out TargetPointer actual)); + Assert.Equal(new TargetPointer(address), actual); + target.Flush(FlushScope.All); + Assert.Equal(expected, target.TryGetRuntimeImageBase(out actual)); + Assert.Equal(new TargetPointer(address), actual); + } + + [Theory] + [InlineData("locator", false)] + [InlineData("pe-exports", false)] + [InlineData("explicit", false)] + [InlineData("collector-locator", false)] + [InlineData("fallback", false)] + [InlineData("locator", true)] + [InlineData("pe-exports", true)] + [InlineData("explicit", true)] + [InlineData("collector-locator", true)] + [InlineData("fallback", true)] + public void Activation_DiscoversDescriptorAndRejectsShortReads(string discovery, bool shortRead) + { + TargetTestHelpers helpers = new(new() { IsLittleEndian = true, Is64Bit = true }); + ContractDescriptorBuilder builder = new(helpers); + builder.AddHeapFragment(new() { Address = 0x10000, Data = CreateRuntimeImage(is64Bit: true), Name = "RuntimeImage" }); + ulong descriptorAddress = new ContractDescriptorBuilder.DescriptorBuilder(builder) + .SetContracts(ContractDescriptor.TargetTests.s_requiredDataAccessContracts) + .SetGlobals([("OperatingSystem", null, "unix", "string")]) + .CreateSubDescriptor(0x12000, 0x13000, 0x14000); + MockMemorySpace.MemoryContext memory = builder.GetMemoryContext(); + RuntimeDataTarget dataTarget = discovery == "pe-exports" + ? new RuntimeDataTarget([]) { ReadMemory = memory.ReadFromTarget, ShortRead = shortRead } + : new ContractLocatorDataTarget { ContractAddress = descriptorAddress, ReadMemory = memory.ReadFromTarget, ShortRead = shortRead }; + int hr = Activate(dataTarget, typeof(ICLRDataEnumMemoryRegions).GUID, out nint instance, + discovery == "explicit" ? descriptorAddress : 0, discovery); + + if (shortRead) + { + Assert.True(hr < 0); + Assert.Equal(0, instance); + } + else + { + Assert.Equal(HResults.S_OK, hr); + Assert.NotEqual(0, instance); + try + { + Assert.Equal(HResults.S_OK, Marshal.QueryInterface(instance, typeof(IXCLRDataProcess).GUID, out nint process)); + Marshal.Release(process); + using RecordingCallback callback = new(supportsUpdates: false); + var enumerate = (delegate* unmanaged[MemberFunction])(*(nint**)instance)[3]; + Assert.Equal(HResults.S_OK, enumerate(instance, (void*)callback.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); + Assert.Equal(discovery is "pe-exports" or "collector-locator", callback.Ranges.Contains((dataTarget.ImageBase, 0x300u))); + } + finally + { + Marshal.Release(instance); + } + } + + Assert.Equal(discovery is "pe-exports" or "collector-locator" ? 1 : 0, dataTarget.ImageBaseLookups); + if (dataTarget is ContractLocatorDataTarget locator) + Assert.Equal(discovery == "explicit" ? 0 : 1, locator.ContractLookups); + } + + [Theory] + [InlineData(0ul)] + [InlineData(0x10000ul)] + public void Activation_HelperUsesProvidedImageBaseWithoutLookup(ulong runtimeImageBase) + { + ContractDescriptorBuilder builder = new(new(new() { IsLittleEndian = true, Is64Bit = true })); + builder.AddHeapFragment(new() { Address = 0x10000, Data = CreateRuntimeImage(is64Bit: true), Name = "RuntimeImage" }); + ulong descriptorAddress = new ContractDescriptorBuilder.DescriptorBuilder(builder) + .SetContracts(ContractDescriptor.TargetTests.s_requiredDataAccessContracts) + .SetGlobals([("OperatingSystem", null, "unix", "string")]) + .CreateSubDescriptor(0x12000, 0x13000, 0x14000); + RuntimeDataTarget dataTarget = new([]) { ReadMemory = builder.GetMemoryContext().ReadFromTarget }; + void* target = ComInterfaceMarshaller.ConvertToUnmanaged(dataTarget); + try + { + Guid iid = typeof(ICLRDataEnumMemoryRegions).GUID; + void* instance = null; + Assert.Equal(HResults.S_OK, EntrypointHelpers.CreateInstance(&iid, (nint)target, 0, &instance, descriptorAddress, runtimeImageBase)); + Assert.True(instance != null); + try + { + using RecordingCallback callback = new(supportsUpdates: false); + var enumerate = (delegate* unmanaged[MemberFunction])(*(nint**)instance)[3]; + Assert.Equal(HResults.S_OK, enumerate(instance, (void*)callback.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); + Assert.Equal(runtimeImageBase != 0, callback.Ranges.Contains((0x10000ul, 0x300u))); + } + finally + { + Marshal.Release((nint)instance); + } + Assert.Equal(0, dataTarget.ImageBaseLookups); + } + finally + { + ComInterfaceMarshaller.Free(target); + } + } + + [Theory] + [InlineData(HResults.E_FAIL, 0x10000ul, true)] + [InlineData(HResults.E_NOTIMPL, 0x10000ul, true)] + [InlineData(HResults.S_FALSE, 0x10000ul, true)] + [InlineData(HResults.S_OK, 0ul, true)] + [InlineData(HResults.E_FAIL, 0x10000ul, false)] + [InlineData(HResults.E_NOTIMPL, 0x10000ul, false)] + [InlineData(HResults.S_FALSE, 0x10000ul, false)] + [InlineData(HResults.S_OK, 0ul, false)] + public void Activation_CollectorRejectsUnavailableRuntimeImageBase(int result, ulong imageBase, bool hasContractLocator) + { + ContractDescriptorBuilder builder = new(new(new() { IsLittleEndian = true, Is64Bit = true })); + ulong descriptorAddress = new ContractDescriptorBuilder.DescriptorBuilder(builder) + .SetContracts(ContractDescriptor.TargetTests.s_requiredDataAccessContracts) + .CreateSubDescriptor(0x12000, 0x13000, 0x14000); + RuntimeDataTarget dataTarget = hasContractLocator + ? new ContractLocatorDataTarget { ContractAddress = descriptorAddress, ReadMemory = builder.GetMemoryContext().ReadFromTarget } + : new RuntimeDataTarget([]); + dataTarget.LookupResult = result; + dataTarget.ImageBase = imageBase; + int hr = Activate(dataTarget, typeof(ICLRDataEnumMemoryRegions).GUID, out nint instance, entrypoint: "collector-locator"); + + try + { + Assert.Equal(HResults.E_FAIL, hr); + Assert.Equal(0, instance); + Assert.Equal(1, dataTarget.ImageBaseLookups); + Assert.Equal(0, dataTarget.ReadCount); + if (dataTarget is ContractLocatorDataTarget locator) + Assert.Equal(0, locator.ContractLookups); + } + finally + { + if (instance != 0) + Marshal.Release(instance); + } + } + + [Theory] + [InlineData("locator", null, 0x10000ul)] + [InlineData("explicit", null, 0x10000ul)] + [InlineData("fallback", null, 0x10000ul)] + [InlineData("locator", HResults.S_OK, 0x10000ul)] + [InlineData("explicit", HResults.S_OK, 0x10000ul)] + [InlineData("fallback", HResults.S_OK, 0x10000ul)] + [InlineData("locator", HResults.E_FAIL, 0x10000ul)] + [InlineData("explicit", HResults.E_FAIL, 0x10000ul)] + [InlineData("fallback", HResults.E_FAIL, 0x10000ul)] + [InlineData("locator", HResults.S_FALSE, 0x10000ul)] + [InlineData("explicit", HResults.S_FALSE, 0x10000ul)] + [InlineData("fallback", HResults.S_FALSE, 0x10000ul)] + [InlineData("locator", HResults.S_OK, 0ul)] + [InlineData("explicit", HResults.S_OK, 0ul)] + [InlineData("fallback", HResults.S_OK, 0ul)] + public void Activation_UniversalUsesRuntimeLocatorWithoutImageBaseFallback(string entrypoint, int? locatorResult, ulong imageBase) + { + ContractDescriptorBuilder builder = new(new(new() { IsLittleEndian = true, Is64Bit = true })); + builder.AddHeapFragment(new() { Address = 0x10000, Data = CreateRuntimeImage(is64Bit: true), Name = "RuntimeImage" }); + ulong descriptorAddress = new ContractDescriptorBuilder.DescriptorBuilder(builder) + .SetContracts(ContractDescriptor.TargetTests.s_requiredDataAccessContracts) + .SetGlobals([("OperatingSystem", null, "unix", "string")]) + .CreateSubDescriptor(0x12000, 0x13000, 0x14000); + MockMemorySpace.MemoryContext memory = builder.GetMemoryContext(); + ContractLocatorDataTarget dataTarget = locatorResult is int result + ? new RuntimeAndContractLocatorDataTarget + { + ContractAddress = descriptorAddress, + ReadMemory = memory.ReadFromTarget, + RuntimeLookupResult = result, + ImageBase = imageBase, + } + : new ContractLocatorDataTarget { ContractAddress = descriptorAddress, ReadMemory = memory.ReadFromTarget }; + + Assert.Equal(HResults.S_OK, Activate(dataTarget, typeof(ICLRDataEnumMemoryRegions).GUID, out nint instance, + entrypoint == "explicit" ? descriptorAddress : 0, entrypoint)); + Assert.NotEqual(0, instance); + try + { + using RecordingCallback callback = new(supportsUpdates: false); + var enumerate = (delegate* unmanaged[MemberFunction])(*(nint**)instance)[3]; + Assert.Equal(HResults.S_OK, enumerate(instance, (void*)callback.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); + Assert.Equal(locatorResult == HResults.S_OK && imageBase != 0, callback.Ranges.Contains((0x10000ul, 0x300u))); + Assert.Equal(0, dataTarget.ImageBaseLookups); + Assert.Equal(locatorResult.HasValue ? 1 : 0, dataTarget.RuntimeBaseLookups); + } + finally + { + Marshal.Release(instance); + } + } + + [Theory] + [InlineData(HResults.S_OK, 0ul, "locator")] + [InlineData(HResults.S_FALSE, 0x12000ul, "locator")] + [InlineData(HResults.E_FAIL, 0x12000ul, "locator")] + [InlineData(HResults.S_OK, 0ul, "fallback")] + [InlineData(HResults.S_FALSE, 0x12000ul, "fallback")] + [InlineData(HResults.E_FAIL, 0x12000ul, "fallback")] + public void Activation_UniversalRejectsUnavailableLocatorWithoutFallingBack(int result, ulong address, string entrypoint) + { + ContractLocatorDataTarget dataTarget = new() { ContractResult = result, ContractAddress = address }; + Assert.Equal(CdacHResults.CDAC_E_DESCRIPTOR_NOT_FOUND, + Activate(dataTarget, typeof(ICLRDataEnumMemoryRegions).GUID, out nint instance, entrypoint: entrypoint)); + Assert.Equal(0, instance); + Assert.Equal(1, dataTarget.ContractLookups); + Assert.Equal(0, dataTarget.ImageBaseLookups); + Assert.Equal(0, dataTarget.ReadCount); + } + + [Theory] + [InlineData("locator")] + [InlineData("fallback")] + public void Activation_UniversalRequiresContractLocator(string entrypoint) + { + RuntimeDataTarget dataTarget = new(CreateRuntimeImage(is64Bit: true)); + Assert.Equal(CdacHResults.CDAC_E_DESCRIPTOR_NOT_FOUND, + Activate(dataTarget, typeof(ICLRDataEnumMemoryRegions).GUID, out nint instance, entrypoint: entrypoint)); + Assert.Equal(0, instance); + Assert.Equal(0, dataTarget.ImageBaseLookups); + Assert.Equal(0, dataTarget.ReadCount); + } + + [Theory] + [InlineData("iid")] + [InlineData("target")] + [InlineData("output")] + [InlineData("contract")] + public void Activation_HelperRejectsInvalidArguments(string argument) + { + RuntimeDataTarget dataTarget = new([]); + void* target = ComInterfaceMarshaller.ConvertToUnmanaged(dataTarget); + try + { + Guid iid = typeof(ICLRDataEnumMemoryRegions).GUID; + void* instance = (void*)1; + Assert.Equal(HResults.E_INVALIDARG, EntrypointHelpers.CreateInstance( + argument == "iid" ? null : &iid, + argument == "target" ? IntPtr.Zero : (nint)target, + IntPtr.Zero, + argument == "output" ? null : &instance, + argument == "contract" ? 0ul : 0x12000ul)); + if (argument != "output") + Assert.Equal(0, (nint)instance); + Assert.Equal(0, dataTarget.ImageBaseLookups); + Assert.Equal(0, dataTarget.ReadCount); + } + finally + { + ComInterfaceMarshaller.Free(target); + } + } + + [Theory] + [InlineData("iid", "locator")] + [InlineData("target", "locator")] + [InlineData("output", "locator")] + [InlineData("iid", "collector-locator")] + [InlineData("target", "collector-locator")] + [InlineData("output", "collector-locator")] + [InlineData("iid", "fallback")] + [InlineData("target", "fallback")] + [InlineData("output", "fallback")] + public void Activation_EntrypointRejectsNullArguments(string argument, string entrypoint) + { + RuntimeDataTarget dataTarget = new([]); + void* target = ComInterfaceMarshaller.ConvertToUnmanaged(dataTarget); + try + { + Guid iid = typeof(ICLRDataEnumMemoryRegions).GUID; + void* instance = (void*)1; + Assert.Equal(HResults.E_INVALIDARG, InvokeActivation( + argument == "iid" ? null : &iid, + argument == "target" ? IntPtr.Zero : (nint)target, + argument == "output" ? null : &instance, 0, entrypoint)); + if (argument != "output") + Assert.Equal(0, (nint)instance); + Assert.Equal(0, dataTarget.ImageBaseLookups); + Assert.Equal(0, dataTarget.ReadCount); + } + finally + { + ComInterfaceMarshaller.Free(target); + } + } + + private static int Activate(RuntimeDataTarget dataTarget, Guid iid, out nint instance, ulong contractAddress = 0, string entrypoint = "locator") + { + void* target = ComInterfaceMarshaller.ConvertToUnmanaged(dataTarget); + try + { + void* result = (void*)1; + int hr = InvokeActivation(&iid, (nint)target, &result, contractAddress, entrypoint); + instance = (nint)result; + return hr; + } + finally + { + ComInterfaceMarshaller.Free(target); + } + } + + private static int InvokeActivation(Guid* iid, nint target, void** instance, ulong contractAddress, string entrypoint) + { + Assembly assembly = entrypoint is "pe-exports" or "collector-locator" + ? Assembly.Load(OperatingSystem.IsWindows() ? "mscordaccore" : "libmscordaccore") + : typeof(Entrypoints).Assembly; + string methodName = entrypoint switch + { + "explicit" => "DbgShimCreateInstanceFromContractDescriptor", + "fallback" => "CLRDataCreateInstanceWithFallback", + _ => "CLRDataCreateInstance", + }; + Type type = assembly.GetType("Microsoft.Diagnostics.DataContractReader.Entrypoints", throwOnError: true)!; + MethodInfo method = type.GetMethod(methodName, BindingFlags.Static | BindingFlags.NonPublic)!; + Assert.NotNull(method); + nint address = method.MethodHandle.GetFunctionPointer(); + return entrypoint switch + { + "explicit" => ((delegate* unmanaged)address)(iid, target, contractAddress, instance), + "fallback" => ((delegate* unmanaged)address)(iid, target, 0, instance), + _ => ((delegate* unmanaged)address)(iid, target, instance), + }; + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void RuntimeImageBase_CorDebugTarget_PreservesProvidedBase(MockTarget.Architecture arch) + { + ulong expectedBase = arch.Is64Bit ? 0x1234_5678_9000UL : 0x9000_0000UL; + TargetTestHelpers helpers = new(arch); + ContractDescriptorBuilder builder = new(helpers); + ulong descriptorAddress = new ContractDescriptorBuilder.DescriptorBuilder(builder).CreateSubDescriptor(0x30000, 0x31000, 0x32000); + CorDebugDataTarget dataTarget = new(builder.GetMemoryContext()); + + Target target = Entrypoints.CreateTargetFromCorDebugDataTarget(dataTarget, descriptorAddress, expectedBase); + + Assert.True(target.TryGetRuntimeImageBase(out TargetPointer imageBase)); + Assert.Equal(expectedBase, imageBase.Value); + target.Flush(FlushScope.All); + Assert.True(target.TryGetRuntimeImageBase(out imageBase)); + Assert.Equal(expectedBase, imageBase.Value); + } + + [Theory] + [InlineData(4, @"C:\runtime\coreclr.dll")] + [InlineData(8, @"C:\runtime\coreclr.dll")] + [InlineData(4, "/runtime/libcoreclr.so")] + [InlineData(8, "/runtime/libcoreclr.so")] + [InlineData(4, "/runtime/libcoreclr.dylib")] + [InlineData(8, "/runtime/libcoreclr.dylib")] + public void RuntimeImageBase_ClrMdLookup_UsesDescriptorModule(int pointerSize, string runtimeModuleName) + { + const ulong RawImageBase = 0x1234_5678_9000_0000; + const ulong RawDescriptorAddress = RawImageBase + 0x1000; + var reader = new Mock(); + reader.SetupGet(r => r.PointerSize).Returns(pointerSize); + reader.SetupGet(r => r.Architecture).Returns(pointerSize == 4 ? Architecture.X86 : Architecture.X64); + reader.SetupGet(r => r.TargetPlatform).Returns(OSPlatform.Windows); + reader.Setup(r => r.EnumerateModules()).Returns( + [ + CreateModule(0x10000, "application", 0x11000), + CreateModule(0x20000, runtimeModuleName, 0), + CreateModule(RawImageBase, runtimeModuleName, RawDescriptorAddress), + ]); + using DataTarget dataTarget = new(reader.Object, new DataTargetOptions()); + ClrMdDumpHost host = Assert.IsType(Activator.CreateInstance( + typeof(ClrMdDumpHost), BindingFlags.Instance | BindingFlags.NonPublic, binder: null, + args: ["test.dmp", dataTarget, Array.Empty()], culture: null)); + + ulong descriptorAddress = host.FindContractDescriptorAddress(out ulong imageBase); + ulong mask = pointerSize == 4 ? uint.MaxValue : ulong.MaxValue; + Assert.Equal(RawDescriptorAddress & mask, descriptorAddress); + Assert.Equal(RawImageBase & mask, imageBase); + reader.Verify(r => r.EnumerateModules(), Times.Once); + } + + private static ModuleInfo CreateModule(ulong imageBase, string fileName, ulong descriptorAddress) + { + var module = new Mock(imageBase, fileName); + module.Setup(m => m.GetExportSymbolAddress("DotNetRuntimeContractDescriptor")).Returns(descriptorAddress); + return module.Object; + } + + private sealed class CorDebugDataTarget(MockMemorySpace.MemoryContext memory) : ICorDebugDataTarget + { + public int GetPlatform(int* platform) => throw new NotImplementedException(); + + public int GetThreadContext(uint threadId, uint contextFlags, uint contextSize, byte* context) + => throw new NotImplementedException(); + + public int ReadVirtual(ulong address, byte* buffer, uint bytesRequested, uint* bytesRead) + { + int hr = memory.ReadFromTarget(address, new Span(buffer, checked((int)bytesRequested))); + *bytesRead = hr >= 0 ? bytesRequested : 0; + return hr; + } + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void Enumeration_ReadsRuntimeImageAtProvidedBaseOnEveryCall(MockTarget.Architecture arch) + { + const ulong ImageBase = 0x10000; + TargetTestHelpers helpers = new(arch); + ContractDescriptorBuilder builder = new(helpers); + byte[] image = CreateRuntimeImage(arch.Is64Bit); + builder.AddHeapFragment(new() { Address = ImageBase, Data = image, Name = "RuntimeImage" }); + ulong descriptorAddress = new ContractDescriptorBuilder.DescriptorBuilder(builder).CreateSubDescriptor(0x30000, 0x31000, 0x32000); + MockMemorySpace.MemoryContext memory = builder.GetMemoryContext(); + Lock apiLock = new(); + int imageReadCount = 0; + bool imageReadUnderLock = true; + ContractDescriptorTarget target = ContractDescriptorTarget.Create(descriptorAddress, + (address, buffer) => + { + if (address >= ImageBase && address < ImageBase + (ulong)image.Length) + { + imageReadCount++; + imageReadUnderLock &= apiLock.IsHeldByCurrentThread; + } + return memory.ReadFromTarget(address, buffer); + }, + memory.WriteToTarget, (_, _, _) => HResults.E_NOTIMPL, (_, _) => HResults.E_NOTIMPL, + (ulong _, out ulong address) => { address = 0; return HResults.E_NOTIMPL; }, + [], runtimeImageBase: ImageBase); + ICLRDataEnumMemoryRegions impl = new SOSDacImpl(target, legacyObj: null, apiLock); + Assert.Equal(0, imageReadCount); + + for (int i = 0; i < 2; i++) + { + using RecordingCallback callback = new(supportsUpdates: false); + Assert.Equal(HResults.S_OK, impl.EnumMemoryRegions((void*)callback.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); + Assert.Equal(3 * (i + 1), imageReadCount); + Assert.True(imageReadUnderLock); + Assert.Contains((ImageBase, 64u), callback.Ranges); + Assert.Contains((ImageBase + 0x80, 264u), callback.Ranges); + Assert.Contains((ImageBase, 0x300u), callback.Ranges); + Assert.Contains((ImageBase + 0x400, 0x100u), callback.Ranges); + Assert.Contains((ImageBase + 0x600, 0x40u), callback.Ranges); + Assert.Contains((ImageBase + 0x700, 0x20u), callback.Ranges); + } + + using RecordingCallback cancelled = new(supportsUpdates: false) { CancelAtAddress = ImageBase }; + Assert.Equal(HResults.COR_E_OPERATIONCANCELED, impl.EnumMemoryRegions((void*)cancelled.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); + Assert.Equal(7, imageReadCount); + using RecordingCallback afterCancellation = new(supportsUpdates: false); + Assert.Equal(HResults.S_OK, impl.EnumMemoryRegions((void*)afterCancellation.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); + Assert.Contains((ImageBase, 0x300u), afterCancellation.Ranges); + Assert.Equal(10, imageReadCount); + } + + [Theory] + [InlineData(false, HResults.S_OK, HResults.S_OK, 0x10000ul, true)] + [InlineData(true, HResults.S_OK, HResults.E_FAIL, 0x10000ul, true)] + [InlineData(false, HResults.S_OK, HResults.E_FAIL, 0x10000ul, false)] + [InlineData(true, HResults.E_FAIL, HResults.E_FAIL, 0x10000ul, false)] + [InlineData(false, HResults.S_OK, HResults.S_FALSE, 0x10000ul, false)] + [InlineData(true, HResults.S_FALSE, HResults.S_FALSE, 0x10000ul, false)] + [InlineData(false, HResults.S_OK, HResults.S_OK, 0ul, false)] + [InlineData(true, HResults.S_OK, HResults.S_OK, 0ul, false)] + [InlineData(true, HResults.E_FAIL, HResults.S_OK, 0x10000ul, true)] + [InlineData(true, HResults.S_FALSE, HResults.S_OK, 0x10000ul, true)] + [InlineData(true, HResults.S_FALSE, HResults.E_FAIL, 0x10000ul, false)] + [InlineData(true, HResults.E_FAIL, HResults.S_OK, 0ul, false)] + public void RuntimeImageLookup_UsesHostCapabilityWithoutReadingHeaders(bool hasLocator, int locatorResult, int fallbackResult, ulong imageBase, bool expected) + { + RuntimeDataTarget dataTarget = hasLocator + ? new RuntimeLocatorDataTarget([]) { RuntimeLookupResult = locatorResult } + : new RuntimeDataTarget([]); + dataTarget.LookupResult = fallbackResult; + dataTarget.ImageBase = imageBase; + + bool actualResult = EntrypointHelpers.TryGetRuntimeImageBase(dataTarget, out TargetPointer address); + bool useFallback = !hasLocator || locatorResult != HResults.S_OK; + Assert.Equal(expected, actualResult); + Assert.Equal(expected ? new TargetPointer(imageBase) : TargetPointer.Null, address); + Assert.Equal(useFallback ? 1 : 0, dataTarget.ImageBaseLookups); + Assert.Equal(hasLocator ? 1 : 0, dataTarget.RuntimeBaseLookups); + Assert.Equal(0, dataTarget.ReadCount); + } + + [Theory] + [InlineData(false, false, null)] + [InlineData(true, false, null)] + [InlineData(false, true, null)] + [InlineData(true, true, null)] + [InlineData(false, false, HResults.S_OK)] + [InlineData(true, false, HResults.S_OK)] + [InlineData(false, true, HResults.S_OK)] + [InlineData(true, true, HResults.S_OK)] + [InlineData(false, false, HResults.S_FALSE)] + [InlineData(true, false, HResults.S_FALSE)] + [InlineData(false, true, HResults.S_FALSE)] + [InlineData(true, true, HResults.S_FALSE)] + [InlineData(false, false, HResults.E_FAIL)] + [InlineData(true, false, HResults.E_FAIL)] + [InlineData(false, true, HResults.E_FAIL)] + [InlineData(true, true, HResults.E_FAIL)] + public void RuntimeDescriptorBootstrap_FallsBackToPeExportsAndRejectsShortReads(bool is64Bit, bool shortRead, int? locatorResult) + { + ContractDescriptorBuilder builder = new(new(new() { IsLittleEndian = true, Is64Bit = is64Bit })); + builder.AddHeapFragment(new() { Address = 0x10000, Data = CreateRuntimeImage(is64Bit), Name = "RuntimeImage" }); + new ContractDescriptorBuilder.DescriptorBuilder(builder) + .SetContracts(ContractDescriptor.TargetTests.s_requiredDataAccessContracts) + .CreateSubDescriptor(0x12000, 0x13000, 0x14000); + RuntimeDataTarget dataTarget = locatorResult is int result + ? new ContractLocatorDataTarget + { + ContractResult = result, + ContractAddress = result == HResults.S_OK ? 0ul : 0x12000ul, + ReadMemory = builder.GetMemoryContext().ReadFromTarget, + ShortRead = shortRead, + } + : new RuntimeDataTarget([]) { ReadMemory = builder.GetMemoryContext().ReadFromTarget, ShortRead = shortRead }; + + int hr = Activate(dataTarget, typeof(ICLRDataEnumMemoryRegions).GUID, out nint instance, entrypoint: "pe-exports"); + try + { + Assert.Equal(shortRead ? CdacHResults.CDAC_E_DESCRIPTOR_NOT_FOUND : HResults.S_OK, hr); + Assert.Equal(!shortRead, instance != 0); + Assert.Equal(1, dataTarget.ImageBaseLookups); + if (dataTarget is ContractLocatorDataTarget locator) + Assert.Equal(1, locator.ContractLookups); + if (shortRead) + Assert.Equal(1, dataTarget.ReadCount); + } + finally + { + if (instance != 0) + Marshal.Release(instance); + } + } + + [Fact] + public void RuntimeDescriptorBootstrap_RejectsMissingImageBaseWithoutReading() + { + RuntimeDataTarget dataTarget = new(CreateRuntimeImage(is64Bit: true)) { ImageBase = 0 }; + + Assert.Equal(HResults.E_FAIL, Activate(dataTarget, typeof(ICLRDataEnumMemoryRegions).GUID, out nint instance, entrypoint: "pe-exports")); + Assert.Equal(0, instance); + Assert.Equal(1, dataTarget.ImageBaseLookups); + Assert.Equal(0, dataTarget.ReadCount); + } + + [Theory] + [InlineData("no-base", 0)] + [InlineData("unreadable-dos", 1)] + [InlineData("unreadable-pe", 2)] + [InlineData("not-pe", 1)] + [InlineData("negative-offset", 1)] + [InlineData("bad-signature", 2)] + [InlineData("bad-optional-header", 2)] + [InlineData("zero-headers", 2)] + public void RuntimeImageDiscovery_HandlesMissingOrInvalidImages(string scenario, int expectedReads) + { + const ulong ImageBase = 0x10000; + byte[] image = CreateRuntimeImage(is64Bit: true); + switch (scenario) + { + case "not-pe": + image[0] = 0; + break; + case "negative-offset": + BinaryPrimitives.WriteInt32LittleEndian(image.AsSpan(0x3c), -1); + break; + case "bad-signature": + image[0x80] = 0; + break; + case "bad-optional-header": + BinaryPrimitives.WriteUInt16LittleEndian(image.AsSpan(0x80 + 24), 0); + break; + case "zero-headers": + BinaryPrimitives.WriteUInt32LittleEndian(image.AsSpan(0x80 + 24 + 60), 0); + break; + } + + ContractDescriptorBuilder builder = new(new(new() { Is64Bit = true, IsLittleEndian = true })); + builder.AddHeapFragment(new() { Address = ImageBase, Data = image, Name = "RuntimeImage" }); + ulong descriptorAddress = new ContractDescriptorBuilder.DescriptorBuilder(builder).CreateSubDescriptor(0x20000, 0x21000, 0x22000); + MockMemorySpace.MemoryContext memory = builder.GetMemoryContext(); + int reads = 0; + ContractDescriptorTarget target = ContractDescriptorTarget.Create(descriptorAddress, + (address, buffer) => + { + if (address >= ImageBase && address < ImageBase + (ulong)image.Length) + { + reads++; + if ((scenario == "unreadable-dos" && address == ImageBase) + || (scenario == "unreadable-pe" && address == ImageBase + 0x80)) + { + return HResults.E_FAIL; + } + } + return memory.ReadFromTarget(address, buffer); + }, + memory.WriteToTarget, (_, _, _) => HResults.E_NOTIMPL, (_, _) => HResults.E_NOTIMPL, + (ulong _, out ulong address) => { address = 0; return HResults.E_NOTIMPL; }, [], + runtimeImageBase: scenario == "no-base" ? TargetPointer.Null : new(ImageBase)); + + Assert.Equal(0, reads); + PEImageCollector imageCollector = new(target); + using RecordingCallback callback = new(supportsUpdates: false); + MemoryRegionEmitter emitter = new(callback.Address, 8); + if (scenario == "no-base") + Assert.Equal(HResults.S_OK, MemoryEnumerator.Enumerate(target, emitter, DumpType.Mini)); + else if (scenario is "unreadable-dos" or "unreadable-pe") + Assert.Throws(() => imageCollector.EnumerateMemoryRegions(ImageBase, (uint)image.Length, isMapped: true, emitter, DumpType.Mini)); + else + imageCollector.EnumerateMemoryRegions(ImageBase, (uint)image.Length, isMapped: true, emitter, DumpType.Mini); + Assert.Equal(expectedReads, reads); + if (scenario == "no-base") + Assert.DoesNotContain(callback.Ranges, range => range.Address >= ImageBase && range.Address < ImageBase + (uint)image.Length); + else + Assert.Empty(callback.Ranges); + } + + [Theory] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] + public void RuntimeImageRegions_ReturnsNonemptyRangesWithoutReading(bool is64Bit, bool emptyDirectories) + { + const ulong ImageBase = 0x10000; + byte[] image = CreateRuntimeImage(is64Bit); + if (emptyDirectories) + { + int directoryOffset = 0x80 + 24 + (is64Bit ? 112 : 96); + image.AsSpan(directoryOffset, 8).Clear(); + image.AsSpan(directoryOffset + 2 * 8, 4).Clear(); + image.AsSpan(directoryOffset + 6 * 8 + 4, 4).Clear(); + } + + int reads = 0; + Assert.True(PEImageInfo.TryCreate(ImageBase, (uint)image.Length, isMapped: true, (address, buffer) => + { + reads++; + image.AsSpan(checked((int)(address - ImageBase)), buffer.Length).CopyTo(buffer); + return true; + }, out PEImageInfo module)); + Assert.Equal(2, reads); + List expected = [new(ImageBase, 0x300)]; + if (!emptyDirectories) + { + expected.Add(new(ImageBase + 0x700, 0x20)); + expected.Add(new(ImageBase + 0x600, 0x40)); + expected.Add(new(ImageBase + 0x400, 0x100)); + } + + IEnumerable regions = module.EnumerateMemoryRegions(); + Assert.Equal(expected, regions); + Assert.Equal(expected, regions); + Assert.Equal(2, reads); + } + + public static IEnumerable ModuleDebugPayloadCases() + { + foreach (bool is64Bit in new[] { false, true }) + foreach (bool mapped in new[] { false, true }) + foreach (bool triage in new[] { false, true }) + foreach (bool supportsUpdates in new[] { false, true }) + yield return [is64Bit, mapped, triage, supportsUpdates, @"C:\private\symbols\module.pdb"]; + + foreach (string path in new[] { "/private/symbols/module.pdb", "module.pdb", "", "/private/" + new string('a', 300) + "/module.pdb", "/private/" + new string('a', 300) + ".pdb" }) + yield return [true, true, true, true, path]; + } + + [Theory] + [MemberData(nameof(ModuleDebugPayloadCases))] + public void ModuleCollection_IncludesDebugPayloads(bool is64Bit, bool mapped, bool triage, bool supportsUpdates, string pdbPath) + { + const ulong ImageBase = 0x10000; + byte[] image = CreateManagedDebugImage(is64Bit, mapped, pdbPath); + ContractDescriptorTarget target = CreateManagedImageTarget(image, is64Bit, mapped); + using RecordingCallback callback = new(supportsUpdates); + MemoryRegionEmitter emitter = new(callback.Address, is64Bit ? 8u : 4u); + Assert.Equal(HResults.S_OK, MemoryEnumerator.Enumerate(target, emitter, triage ? DumpType.Triage : DumpType.Mini)); + + uint sectionOffset = mapped ? 0x1000u : 0x400u; + ulong debugAddress = ImageBase + sectionOffset; + ulong payloadAddress = debugAddress + 0x400; + uint payloadSize = 24 + (uint)Encoding.UTF8.GetByteCount(pdbPath) + 1; + Assert.Contains((ImageBase, 0x300u), callback.Ranges); + Assert.Contains((debugAddress, 56u), callback.Ranges); + Assert.Contains((payloadAddress, payloadSize), callback.Ranges); + Assert.Contains((debugAddress + 0x200, 8u), callback.Ranges); + Assert.DoesNotContain(callback.Ranges, r => r.Address == ImageBase && r.Size == image.Length); + Assert.DoesNotContain(callback.Ranges, r => r.Address <= debugAddress + 0x300 && r.Address + r.Size > debugAddress + 0x300); + if (triage && supportsUpdates) + { + byte[] path = image.AsSpan((int)(sectionOffset + 0x400 + 24), (int)payloadSize - 24).ToArray(); + foreach ((ulong address, byte[] update) in callback.Updates) + update.CopyTo(path.AsSpan(checked((int)(address - payloadAddress - 24)))); + string fileName = pdbPath[(Math.Max(pdbPath.LastIndexOf('/'), pdbPath.LastIndexOf('\\')) + 1)..]; + byte[] expected = new byte[path.Length]; + Encoding.UTF8.GetBytes(fileName, expected); + Assert.Equal(expected, path); + } + else + { + Assert.Empty(callback.Updates); + } + Assert.Equal(HResults.S_OK, emitter.Result); + + TestPlaceholderTarget dumpTarget = new TestPlaceholderTarget.Builder(new() { IsLittleEndian = true, Is64Bit = is64Bit }) + .UseReader((ulong address, Span buffer) => + { + int length = buffer.Length; + if (!callback.Ranges.Exists(r => address >= r.Address && address - r.Address <= r.Size && (ulong)length <= r.Size - (address - r.Address))) + return HResults.E_FAIL; + + image.AsSpan(checked((int)(address - ImageBase)), buffer.Length).CopyTo(buffer); + foreach ((ulong updateAddress, byte[] update) in callback.Updates) + { + ulong start = Math.Max(address, updateAddress); + ulong end = Math.Min(address + (uint)buffer.Length, updateAddress + (uint)update.Length); + if (start < end) + update.AsSpan((int)(start - updateAddress), (int)(end - start)).CopyTo(buffer[(int)(start - address)..]); + } + return HResults.S_OK; + }) + .Build(); + using PEReader reader = new(new TargetStream(dumpTarget, ImageBase, image.Length), mapped ? PEStreamOptions.IsLoadedImage : PEStreamOptions.Default); + ImmutableArray entries = reader.ReadDebugDirectory(); + Assert.Equal(2, entries.Length); + CodeViewDebugDirectoryData codeView = reader.ReadCodeViewDebugDirectoryData(entries[0]); + string expectedPath = triage && supportsUpdates + ? pdbPath[(Math.Max(pdbPath.LastIndexOf('/'), pdbPath.LastIndexOf('\\')) + 1)..] + : pdbPath; + Assert.Equal(expectedPath, codeView.Path); + } + + private static ContractDescriptorTarget CreateManagedImageTarget(byte[] image, bool is64Bit, bool mapped) + { + const ulong ImageBase = 0x10000; + TargetPointer imageBase = new(ImageBase); + uint imageSize = (uint)image.Length; + uint imageFlags = mapped ? 1u : 0u; + Contracts.ModuleHandle module = new(new TargetPointer(0x8000)); + Mock loader = new(); + loader.Setup(l => l.GetModuleHandles(It.IsAny(), It.IsAny())).Returns([module]); + loader.Setup(l => l.TryGetLoadedImageContents(module, out imageBase, out imageSize, out imageFlags)).Returns(true); + return ContractDescriptorTarget.Create( + ContractDescriptorParser.ParseCompact("""{"version":2,"contracts":{"Loader":"c1","EcmaMetadata":"c1"}}"""u8), [], + (ulong address, Span buffer) => + { + if (address < ImageBase || address - ImageBase > (ulong)image.Length + || (ulong)buffer.Length > (ulong)image.Length - (address - ImageBase)) + return HResults.E_FAIL; + + image.AsSpan((int)(address - ImageBase), buffer.Length).CopyTo(buffer); + return HResults.S_OK; + }, + (_, _) => HResults.E_NOTIMPL, (_, _, _) => HResults.E_NOTIMPL, (_, _) => HResults.E_NOTIMPL, + (ulong _, out ulong address) => { address = 0; return HResults.E_NOTIMPL; }, + isLittleEndian: true, pointerSize: is64Bit ? 8 : 4, + contractRegistrations: [registry => + { + registry.Register("c1", _ => loader.Object); + registry.Register("c1", _ => new Mock().Object); + }]); + } + + [Theory] + [InlineData(false, false, "valid")] + [InlineData(false, true, "valid")] + [InlineData(true, false, "valid")] + [InlineData(true, true, "valid")] + [InlineData(true, false, "unreadable-directory")] + [InlineData(true, true, "unreadable-directory")] + [InlineData(true, false, "invalid-payload")] + [InlineData(true, true, "invalid-payload")] + [InlineData(true, false, "empty-payload")] + [InlineData(true, true, "empty-payload")] + public void ImageDebugEntries_ReadLazilyAndValidatePayloadRanges(bool is64Bit, bool mapped, string scenario) + { + const ulong ImageBase = 0x10000; + byte[] image = CreateManagedDebugImage(is64Bit, mapped, "module.pdb"); + int sectionOffset = mapped ? 0x1000 : 0x400; + if (scenario == "invalid-payload") + BinaryPrimitives.WriteUInt32LittleEndian(image.AsSpan(sectionOffset + 20), uint.MaxValue - 8); + if (scenario == "empty-payload") + image.AsSpan(sectionOffset + 16, 4).Clear(); + + List reads = []; + bool ReadMemory(ulong address, Span buffer) + { + reads.Add(address); + if (scenario == "unreadable-directory" && address == ImageBase + (uint)sectionOffset) + return false; + image.AsSpan(checked((int)(address - ImageBase)), buffer.Length).CopyTo(buffer); + return true; + } + + Assert.True(PEImageInfo.TryCreate(ImageBase, (uint)image.Length, mapped, ReadMemory, out PEImageInfo module)); + Assert.DoesNotContain(ImageBase + (uint)sectionOffset, reads); + IEnumerable entries = module.EnumerateDebugEntries(ReadMemory); + Assert.DoesNotContain(ImageBase + (uint)sectionOffset, reads); + if (scenario is "unreadable-directory" or "invalid-payload") + { + Assert.Throws(() => new List(entries)); + } + else + { + List expected = []; + if (scenario != "empty-payload") + expected.Add(new(DebugDirectoryEntryType.CodeView, new TargetSpan(ImageBase + (uint)sectionOffset + 0x400, 35))); + expected.Add(new(DebugDirectoryEntryType.PdbChecksum, new TargetSpan(ImageBase + (uint)sectionOffset + 0x200, 8))); + Assert.Equal(expected, entries); + Assert.DoesNotContain(ImageBase + (uint)sectionOffset + 0x400, reads); + } + } + + [Theory] + [InlineData(false, 0x400u)] + [InlineData(false, 0x800u)] + [InlineData(true, 0x1000u)] + [InlineData(true, 0x1400u)] + public void ModuleCollection_PropagatesImageCancellation(bool mapped, uint offset) + { + const ulong ImageBase = 0x10000; + byte[] image = CreateManagedDebugImage(is64Bit: true, mapped, "module.pdb"); + ContractDescriptorTarget target = CreateManagedImageTarget(image, is64Bit: true, mapped); + using RecordingCallback callback = new(supportsUpdates: false) { CancelAtAddress = ImageBase + offset }; + MemoryRegionEmitter emitter = new(callback.Address, 8); + OperationCanceledException exception = Assert.Throws(() => + MemoryEnumerator.Enumerate(target, emitter, DumpType.Mini)); + Assert.Equal(HResults.COR_E_OPERATIONCANCELED, exception.HResult); + } + + private static byte[] CreateManagedDebugImage(bool is64Bit, bool mapped, string pdbPath) + { + byte[] image = new byte[0x3000]; + CreateRuntimeImage(is64Bit).AsSpan(0, 0x300).CopyTo(image); + Span peHeader = image.AsSpan(0x80); + ushort optionalHeaderSize = is64Bit ? (ushort)240 : (ushort)224; + BinaryPrimitives.WriteUInt16LittleEndian(peHeader[6..], 1); + BinaryPrimitives.WriteUInt16LittleEndian(peHeader[20..], optionalHeaderSize); + BinaryPrimitives.WriteUInt32LittleEndian(peHeader[(24 + 56)..], (uint)image.Length); + int directoryOffset = 24 + (is64Bit ? 112 : 96); + BinaryPrimitives.WriteUInt32LittleEndian(peHeader[(directoryOffset - 4)..], 16); + peHeader.Slice(directoryOffset, 16 * 8).Clear(); + BinaryPrimitives.WriteUInt32LittleEndian(peHeader[(directoryOffset + 6 * 8)..], 0x1000); + BinaryPrimitives.WriteUInt32LittleEndian(peHeader[(directoryOffset + 6 * 8 + 4)..], 56); + Span section = peHeader[(24 + optionalHeaderSize)..]; + BinaryPrimitives.WriteUInt32LittleEndian(section[8..], 0x1000); + BinaryPrimitives.WriteUInt32LittleEndian(section[12..], 0x1000); + BinaryPrimitives.WriteUInt32LittleEndian(section[16..], 0x1000); + BinaryPrimitives.WriteUInt32LittleEndian(section[20..], 0x400); + int sectionOffset = mapped ? 0x1000 : 0x400; + Span debug = image.AsSpan(sectionOffset); + BinaryPrimitives.WriteUInt32LittleEndian(debug[12..], (uint)DebugDirectoryEntryType.CodeView); + BinaryPrimitives.WriteUInt32LittleEndian(debug[16..], 24 + (uint)Encoding.UTF8.GetByteCount(pdbPath) + 1); + BinaryPrimitives.WriteUInt32LittleEndian(debug[20..], 0x1400); + BinaryPrimitives.WriteUInt32LittleEndian(debug[24..], 0x800); + BinaryPrimitives.WriteUInt32LittleEndian(debug[(28 + 12)..], (uint)DebugDirectoryEntryType.PdbChecksum); + BinaryPrimitives.WriteUInt32LittleEndian(debug[(28 + 16)..], 8); + BinaryPrimitives.WriteUInt32LittleEndian(debug[(28 + 20)..], 0x1200); + BinaryPrimitives.WriteUInt32LittleEndian(debug[(28 + 24)..], 0x600); + "RSDS"u8.CopyTo(debug[0x400..]); + Encoding.UTF8.GetBytes(pdbPath, debug[(0x400 + 24)..]); + return image; + } + + private static byte[] CreateRuntimeImage(bool is64Bit) + { + byte[] image = new byte[0x1000]; + BinaryPrimitives.WriteUInt16LittleEndian(image, 0x5a4d); + BinaryPrimitives.WriteInt32LittleEndian(image.AsSpan(0x3c), 0x80); + Span peHeader = image.AsSpan(0x80); + BinaryPrimitives.WriteUInt32LittleEndian(peHeader, 0x00004550); + BinaryPrimitives.WriteUInt16LittleEndian(peHeader.Slice(24), (ushort)(is64Bit ? PEMagic.PE32Plus : PEMagic.PE32)); + BinaryPrimitives.WriteUInt32LittleEndian(peHeader.Slice(24 + 60), 0x300); + int directoryOffset = 24 + (is64Bit ? 112 : 96); + foreach ((int index, uint rva, uint size) in new[] { (0, 0x400u, 0x100u), (2, 0x600u, 0x40u), (6, 0x700u, 0x20u) }) + { + BinaryPrimitives.WriteUInt32LittleEndian(peHeader.Slice(directoryOffset + index * 8), rva); + BinaryPrimitives.WriteUInt32LittleEndian(peHeader.Slice(directoryOffset + index * 8 + 4), size); + } + + BinaryPrimitives.WriteUInt32LittleEndian(image.AsSpan(0x400 + 20), 1); + BinaryPrimitives.WriteUInt32LittleEndian(image.AsSpan(0x400 + 24), 1); + BinaryPrimitives.WriteUInt32LittleEndian(image.AsSpan(0x400 + 28), 0x440); + BinaryPrimitives.WriteUInt32LittleEndian(image.AsSpan(0x400 + 32), 0x444); + BinaryPrimitives.WriteUInt32LittleEndian(image.AsSpan(0x400 + 36), 0x448); + BinaryPrimitives.WriteUInt32LittleEndian(image.AsSpan(0x440), 0x2000); + BinaryPrimitives.WriteUInt32LittleEndian(image.AsSpan(0x444), 0x450); + "DotNetRuntimeContractDescriptor\0"u8.CopyTo(image.AsSpan(0x450)); + return image; + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Enumeration_SharesComIdentityWithAnalysisInterfaces(bool enumerateFirst) + { + Lock apiLock = new(); + TargetTestHelpers helpers = new(new() { IsLittleEndian = true, Is64Bit = true }); + ContractDescriptorBuilder builder = new(helpers); + ContractDescriptorTarget target = builder.CreateTarget(new(builder)); + SOSDacImpl impl = new(target, legacyObj: null, apiLock); + Assert.Equal(HResults.E_INVALIDARG, ((ICLRDataEnumMemoryRegions)impl).EnumMemoryRegions(null, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); + Assert.Equal(HResults.E_POINTER, ((IXCLRDataProcess3)impl).GetFunctionTable(default, 0, null, null, null)); + + void* entry = enumerateFirst + ? ComInterfaceMarshaller.ConvertToUnmanaged(impl) + : ComInterfaceMarshaller.ConvertToUnmanaged(impl); + try + { + Guid iidIUnknown = new("00000000-0000-0000-C000-000000000046"); + Assert.Equal(HResults.S_OK, Marshal.QueryInterface((nint)entry, in iidIUnknown, out nint identity)); + try + { + using RecordingCallback callback = new(supportsUpdates: false, apiLock); + foreach (Type interfaceType in new[] + { + typeof(ICLRDataEnumMemoryRegions), typeof(IXCLRDataProcess), typeof(IXCLRDataProcess2), + typeof(IXCLRDataProcess3), typeof(ISOSDacInterface), typeof(ISOSDacInterface17), + }) + { + Guid iid = interfaceType.GUID; + Assert.Equal(HResults.S_OK, Marshal.QueryInterface((nint)entry, in iid, out nint iface)); + try + { + Assert.Equal(HResults.S_OK, Marshal.QueryInterface(iface, in iidIUnknown, out nint interfaceIdentity)); + try + { + Assert.Equal(identity, interfaceIdentity); + } + finally + { + Marshal.Release(interfaceIdentity); + } + + Guid enumerationIid = typeof(ICLRDataEnumMemoryRegions).GUID; + Assert.Equal(HResults.S_OK, Marshal.QueryInterface(iface, in enumerationIid, out nint enumeration)); + try + { + var enumerate = (delegate* unmanaged[MemberFunction])(*(nint**)enumeration)[3]; + Assert.Equal(HResults.E_INVALIDARG, enumerate(enumeration, null, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); + callback.Regions.Clear(); + Assert.Equal(HResults.S_OK, enumerate(enumeration, (void*)callback.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); + Assert.NotEmpty(callback.Regions); + Assert.True(callback.WasLockHeld); + Assert.False(apiLock.IsHeldByCurrentThread); + } + finally + { + Marshal.Release(enumeration); + } + } + finally + { + Marshal.Release(iface); + } + } + } + finally + { + Marshal.Release(identity); + } + } + finally + { + if (enumerateFirst) + ComInterfaceMarshaller.Free(entry); + else + ComInterfaceMarshaller.Free(entry); + } + } + + [Theory] + [InlineData(true, true, true)] + [InlineData(true, false, true)] + [InlineData(false, true, true)] + [InlineData(false, false, true)] + [InlineData(true, true, false)] + [InlineData(true, false, false)] + [InlineData(false, true, false)] + [InlineData(false, false, false)] + public void Enumeration_FlushesCachesAndRestoresCallbacksOnEveryCall(bool nativeDescriptor, bool littleEndian, bool is64Bit) + { + const ulong PointerAddress = 0x1000; + TargetTestHelpers helpers = new(new() { IsLittleEndian = littleEndian, Is64Bit = is64Bit }); + ContractDescriptorBuilder builder = new(helpers); + byte[] pointerBytes = new byte[helpers.PointerSize]; + helpers.WritePointer(pointerBytes, 0x9000); + builder.AddHeapFragment(new() { Address = PointerAddress, Data = pointerBytes, Name = "CachedPointer" }); + ContractDescriptorBuilder.DescriptorBuilder descriptor = new(builder); + descriptor.SetContracts(["RuntimeTypeSystem"]); + ulong descriptorAddress = descriptor.CreateSubDescriptor(0x2000, 0x3000, 0x4000); + MockMemorySpace.MemoryContext memory = builder.GetMemoryContext(); + Action[] registrations = + [ + registry => registry.Register("c1", target => + { + TargetPointer? cached = null; + Mock types = new(); + types.Setup(t => t.GetWellKnownMethodTable(It.IsAny())) + .Returns(() => cached ??= target.ProcessedData.GetOrAdd(new(PointerAddress)).Value); + types.Setup(t => t.Flush(FlushScope.All)).Callback(() => cached = null); + return types.Object; + }) + ]; + ContractDescriptorTarget target = nativeDescriptor + ? ContractDescriptorTarget.Create(descriptorAddress, memory.ReadFromTarget, memory.WriteToTarget, + (_, _, _) => HResults.E_NOTIMPL, (_, _) => HResults.E_NOTIMPL, + (ulong _, out ulong address) => { address = 0; return HResults.E_NOTIMPL; }, []) + : ContractDescriptorTarget.Create( + ContractDescriptorParser.ParseCompact("""{"version":2,"contracts":{"RuntimeTypeSystem":"c1"}}"""u8), + [], memory.ReadFromTarget, memory.WriteToTarget, + (_, _, _) => HResults.E_NOTIMPL, (_, _) => HResults.E_NOTIMPL, + (ulong _, out ulong address) => { address = 0; return HResults.E_NOTIMPL; }, + littleEndian, helpers.PointerSize); + + Assert.False(target.TryGetRuntimeImageBase(out TargetPointer imageBase)); + Assert.Equal(TargetPointer.Null, imageBase); + + // Registrations added after target construction must also participate in collection. + foreach (Action register in registrations) + register(target.Contracts); + + Assert.Equal(new TargetPointer(0x9000), target.Contracts.RuntimeTypeSystem.GetWellKnownMethodTable(WellKnownMethodTable.Object)); + IRuntimeTypeSystem originalContract = target.Contracts.RuntimeTypeSystem; + CachedPointer originalData = target.ProcessedData.GetOrAdd(new(PointerAddress)); + ICLRDataEnumMemoryRegions impl = new SOSDacImpl(target, legacyObj: null, new()); + for (int i = 0; i < 2; i++) + { + using RecordingCallback callback = new(supportsUpdates: false); + Assert.Equal(HResults.S_OK, impl.EnumMemoryRegions((void*)callback.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_HEAP2)); + Assert.Contains(PointerAddress, callback.Regions); + Assert.Same(originalContract, target.Contracts.RuntimeTypeSystem); + CachedPointer currentData = target.ProcessedData.GetOrAdd(new(PointerAddress)); + Assert.NotSame(originalData, currentData); + originalData = currentData; + Mock.Get(originalContract).Verify(contract => contract.Flush(FlushScope.All), Times.Exactly(i + 1)); + if (nativeDescriptor) + Assert.Contains(descriptorAddress, callback.Regions); + + int count = callback.Ranges.Count; + Assert.Equal(new TargetPointer(0x9000), target.ReadPointer(PointerAddress)); + Assert.Equal(count, callback.Ranges.Count); + } + + using RecordingCallback cancelled = new(supportsUpdates: false); + cancelled.Result = HResults.COR_E_OPERATIONCANCELED; + Assert.Equal(HResults.COR_E_OPERATIONCANCELED, impl.EnumMemoryRegions((void*)cancelled.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); + using RecordingCallback afterCancellation = new(supportsUpdates: false); + Assert.Equal(HResults.S_OK, impl.EnumMemoryRegions((void*)afterCancellation.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); + Assert.Contains(PointerAddress, afterCancellation.Regions); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void DescriptorMemory_ReportsCachedRangesWithoutReading(MockTarget.Architecture arch) + { + TargetTestHelpers helpers = new(arch); + ContractDescriptorBuilder builder = new(helpers); + ContractDescriptorBuilder.DescriptorBuilder descriptor = new(builder); + descriptor.SetGlobals([("Value", null, 0, null, null)], [0x9000]); + ulong descriptorAddress = descriptor.CreateSubDescriptor(0x2000, 0x3000, 0x4000); + MockMemorySpace.MemoryContext memory = builder.GetMemoryContext(); + List reads = []; + ContractDescriptorTarget target = ContractDescriptorTarget.Create(descriptorAddress, + (address, buffer) => + { + reads.Add(address); + return memory.ReadFromTarget(address, buffer); + }, + memory.WriteToTarget, (_, _, _) => HResults.E_NOTIMPL, (_, _) => HResults.E_NOTIMPL, + (ulong _, out ulong address) => { address = 0; return HResults.E_NOTIMPL; }, []); + + uint jsonSize = target.Read(descriptorAddress + 12); + for (int i = 0; i < 2; i++) + { + reads.Clear(); + using IDisposable readScope = target.RegisterReadCallback((_, _) => Assert.Fail("Descriptor enumeration must not read target memory.")); + Assert.Collection(target.EnumerateDescriptorMemory(), + range => + { + Assert.Equal(new TargetPointer(descriptorAddress), range.Address); + Assert.Equal((ulong)ContractDescriptorHelpers.Size(arch.Is64Bit), range.Size); + }, + range => + { + Assert.Equal(new TargetPointer(0x3000), range.Address); + Assert.Equal((ulong)jsonSize, range.Size); + }, + range => + { + Assert.Equal(new TargetPointer(0x4000), range.Address); + Assert.Equal((ulong)helpers.PointerSize, range.Size); + }); + Assert.Equal(new TargetPointer(0x9000), target.ReadGlobalPointer("Value")); + Assert.Empty(reads); + } + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void Enumeration_RejectsNonDescriptorTargetWithoutChangingCaches(MockTarget.Architecture arch) + { + const ulong PointerAddress = 0x1000; + TargetTestHelpers helpers = new(arch); + MockMemorySpace.Builder builder = new(helpers); + byte[] pointerBytes = new byte[helpers.PointerSize]; + helpers.WritePointer(pointerBytes, 0x9000); + builder.AddHeapFragment(new() { Address = PointerAddress, Data = pointerBytes, Name = nameof(CachedPointer) }); + TestPlaceholderTarget target = new(arch, builder.GetMemoryContext().ReadFromTarget); + TestPlaceholderTarget.TestContractRegistry registry = target.SetupContractRegistry(registry => + registry.Register("c1", boundTarget => + { + Mock types = new(); + types.Setup(t => t.GetWellKnownMethodTable(It.IsAny())) + .Returns(() => boundTarget.ProcessedData.GetOrAdd(new(PointerAddress)).Value); + return types.Object; + })); + registry.SetVersion("c1"); + IRuntimeTypeSystem analysisContract = target.Contracts.RuntimeTypeSystem; + Assert.Equal(new TargetPointer(0x9000), analysisContract.GetWellKnownMethodTable(WellKnownMethodTable.Object)); + CachedPointer analysisData = target.ProcessedData.GetOrAdd(new(PointerAddress)); + ICLRDataEnumMemoryRegions impl = new SOSDacImpl(target, legacyObj: null, new()); + + for (int i = 0; i < 2; i++) + { + using RecordingCallback callback = new(supportsUpdates: false); + Assert.Equal(HResults.E_NOTIMPL, impl.EnumMemoryRegions((void*)callback.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); + Assert.Empty(callback.Regions); + Assert.Same(analysisContract, target.Contracts.RuntimeTypeSystem); + CachedPointer collectionData = target.ProcessedData.GetOrAdd(new(PointerAddress)); + Assert.Same(analysisData, collectionData); + Mock.Get(analysisContract).Verify(contract => contract.Flush(It.IsAny()), Times.Never); + } + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void ReadShim_ReportsEverySuccessfulMemoryRead(MockTarget.Architecture arch) + { + TargetTestHelpers helpers = new(arch); + ContractDescriptorBuilder builder = new(helpers); + const ulong ValueAddress = 0x1000; + const ulong Utf8Address = 0x2000; + const ulong Utf16Address = 0x3000; + const ulong EmptyAddress = 0x4000; + const ulong InvalidUtf8Address = 0x5000; + const ulong UnterminatedAddress = 0x6000; + const ulong UnreadableAddress = 0x9000; + const string Text = "text \u00e9 \uD83D\uDE00"; + Encoding utf16 = arch.IsLittleEndian ? Encoding.Unicode : Encoding.BigEndianUnicode; + AddBytes(ValueAddress, [1, 2, 3, 4, 5, 6, 7, 8]); + AddBytes(Utf8Address, Encoding.UTF8.GetBytes(Text + '\0')); + AddBytes(Utf16Address, utf16.GetBytes(Text + '\0')); + AddBytes(EmptyAddress, [0, 0]); + AddBytes(InvalidUtf8Address, [0xc0, 0xaf, 0]); + AddBytes(UnterminatedAddress, [0x61, 0x62]); + string longText = new('a', 1100); + AddBytes(0x7000, Encoding.UTF8.GetBytes(longText + '\0')); + AddBytes(0x8000, utf16.GetBytes(longText + '\0')); + + uint codePointerSize = arch.Is64Bit ? 4u : 8u; + ContractDescriptorBuilder.DescriptorBuilder descriptor = new(builder); + descriptor.SetTypes(new Dictionary + { + [DataType.CodePointer] = new() { Size = codePointerSize, Fields = new Dictionary() }, + }); + descriptor.SetGlobals([("Number", 42, null, null), ("Pointer", 0x9876, null, null), ("Text", null, "hello", null)]); + ulong descriptorAddress = descriptor.CreateSubDescriptor(0x10000, 0x11000, 0x12000); + MockMemorySpace.MemoryContext memory = builder.GetMemoryContext(); + List<(ulong Address, ulong Size)> underlyingReads = []; + List<(ulong Address, ulong Size)> recordedReads = []; + ContractDescriptorTarget recording = ContractDescriptorTarget.Create(descriptorAddress, + (address, buffer) => + { + int hr = memory.ReadFromTarget(address, buffer); + if (hr >= 0) + underlyingReads.Add((address, (ulong)buffer.Length)); + return hr; + }, + memory.WriteToTarget, (_, _, _) => HResults.E_NOTIMPL, (_, _) => HResults.E_NOTIMPL, + (ulong _, out ulong address) => { address = 0x1234; return HResults.S_OK; }, []); + bool cancelReads = false; + using IDisposable readScope = recording.RegisterReadCallback((address, size) => + { + if (cancelReads) + throw new OperationCanceledException(); + recordedReads.Add((address, size)); + }); + ulong value32 = arch.IsLittleEndian ? 0x04030201u : 0x01020304u; + ulong value64 = arch.IsLittleEndian ? 0x0807060504030201ul : 0x0102030405060708ul; + ulong pointerValue = arch.Is64Bit ? value64 : value32; + ulong codePointerValue = codePointerSize == 4 ? value32 : value64; + + VerifyRead(() => Assert.Equal((uint)value32, recording.Read(ValueAddress))); + VerifyRead(() => Assert.Equal(value64, recording.Read(ValueAddress))); + VerifyRead(() => + { + Assert.True(recording.TryRead(ValueAddress, out uint value)); + Assert.Equal((uint)value32, value); + }); + VerifyRead(() => Assert.Equal(0x04030201u, recording.ReadLittleEndian(ValueAddress))); + VerifyRead(() => Assert.Equal(new TargetPointer(pointerValue), recording.ReadPointer(ValueAddress))); + VerifyRead(() => + { + Assert.True(recording.TryReadPointer(ValueAddress, out TargetPointer value)); + Assert.Equal(new TargetPointer(pointerValue), value); + }); + VerifyRead(() => Assert.Equal(new TargetCodePointer(codePointerValue), recording.ReadCodePointer(ValueAddress))); + VerifyRead(() => + { + Assert.True(recording.TryReadCodePointer(ValueAddress, out TargetCodePointer value)); + Assert.Equal(new TargetCodePointer(codePointerValue), value); + }); + VerifyRead(() => Assert.Equal(new TargetNUInt(pointerValue), recording.ReadNUInt(ValueAddress))); + VerifyRead(() => Assert.Equal(new TargetNInt((long)pointerValue), recording.ReadNInt(ValueAddress))); + VerifyRead(() => + { + byte[] bytes = new byte[3]; + recording.ReadBuffer(ValueAddress, bytes); + Assert.Equal(new byte[] { 1, 2, 3 }, bytes); + }); + VerifyRead(() => Assert.Equal(Text, recording.ReadUtf8String(Utf8Address))); + VerifyRead(() => Assert.Equal(Text, recording.ReadUtf8String(Utf8Address, strict: true))); + VerifyRead(() => Assert.Equal(Text, recording.ReadUtf16String(Utf16Address))); + VerifyRead(() => Assert.Equal(longText, recording.ReadUtf8String(0x7000))); + VerifyRead(() => Assert.Equal(longText, recording.ReadUtf16String(0x8000))); + VerifyRead(() => Assert.Empty(recording.ReadUtf8String(EmptyAddress))); + VerifyRead(() => Assert.Empty(recording.ReadUtf16String(EmptyAddress))); + VerifyRead(() => Assert.Equal("\ufffd\ufffd", recording.ReadUtf8String(InvalidUtf8Address))); + VerifyRead(() => Assert.Throws(() => recording.ReadUtf8String(InvalidUtf8Address, strict: true))); + VerifyRead(() => Assert.Throws(() => recording.ReadUtf8String(UnterminatedAddress))); + + VerifyRead(() => Assert.False(recording.TryRead(UnreadableAddress, out _)), expectReads: false); + VerifyRead(() => Assert.False(recording.TryReadPointer(UnreadableAddress, out _)), expectReads: false); + VerifyRead(() => Assert.False(recording.TryReadCodePointer(UnreadableAddress, out _)), expectReads: false); + VerifyRead(() => Assert.Throws(() => recording.Read(UnreadableAddress)), expectReads: false); + VerifyRead(() => Assert.Throws(() => recording.ReadLittleEndian(UnreadableAddress)), expectReads: false); + VerifyRead(() => Assert.Throws(() => recording.ReadNUInt(UnreadableAddress)), expectReads: false); + VerifyRead(() => Assert.Throws(() => recording.ReadNInt(UnreadableAddress)), expectReads: false); + VerifyRead(() => Assert.Throws(() => recording.ReadPointer(UnreadableAddress)), expectReads: false); + VerifyRead(() => Assert.Throws(() => recording.ReadCodePointer(UnreadableAddress)), expectReads: false); + VerifyRead(() => Assert.Throws(() => recording.ReadBuffer(UnreadableAddress, new byte[1])), expectReads: false); + + VerifyRead(() => + { + Assert.Equal(42u, recording.ReadGlobal("Number")); + Assert.True(recording.TryReadGlobal("Number", out uint? number)); + Assert.Equal(42u, number); + Assert.Equal(new TargetPointer(0x9876), recording.ReadGlobalPointer("Pointer")); + Assert.True(recording.TryReadGlobalPointer("Pointer", out TargetPointer? pointer)); + Assert.Equal(new TargetPointer(0x9876), pointer); + Assert.Equal("hello", recording.ReadGlobalString("Text")); + Assert.True(recording.TryReadGlobalString("Text", out string? text)); + Assert.Equal("hello", text); + Assert.False(recording.TryReadGlobal("Missing", out _)); + Assert.False(recording.TryReadGlobalPointer("Missing", out _)); + Assert.False(recording.TryReadGlobalString("Missing", out _)); + Assert.Equal(codePointerSize, recording.GetTypeInfo(nameof(DataType.CodePointer)).Size); + Assert.True(recording.TryGetTypeInfo(nameof(DataType.CodePointer), out _)); + Assert.False(recording.TryGetTypeInfo("Missing", out _)); + Assert.Equal(new TargetPointer(pointerValue), recording.ReadPointerFromSpan([1, 2, 3, 4, 5, 6, 7, 8])); + Assert.True(recording.IsAlignedToPointerSize(new TargetPointer(ValueAddress))); + Assert.False(recording.IsAlignedToPointerSize(new TargetPointer(ValueAddress + 1))); + Assert.Equal(new TargetPointer(0x1234), recording.AllocateMemory(16)); + }, expectReads: false); + + VerifyRead(() => recording.Write(ValueAddress, -1), expectReads: false); + VerifyRead(() => Assert.Equal(-1, recording.Read(ValueAddress))); + VerifyRead(() => recording.WritePointer(ValueAddress, new(0x9876)), expectReads: false); + VerifyRead(() => Assert.Equal(new TargetPointer(0x9876), recording.ReadPointer(ValueAddress))); + VerifyRead(() => recording.WriteNUInt(ValueAddress, new(0x7654)), expectReads: false); + VerifyRead(() => Assert.Equal(new TargetNUInt(0x7654), recording.ReadNUInt(ValueAddress))); + VerifyRead(() => recording.WriteBuffer(ValueAddress, [0xff]), expectReads: false); + VerifyRead(() => Assert.Equal(byte.MaxValue, recording.Read(ValueAddress))); + + cancelReads = true; + Assert.Throws(() => recording.TryReadPointer(ValueAddress, out _)); + Assert.Throws(() => recording.TryRead(ValueAddress, out _)); + Assert.Throws(() => recording.TryReadCodePointer(ValueAddress, out _)); + readScope.Dispose(); + Assert.True(recording.TryReadPointer(ValueAddress, out _)); + + void AddBytes(ulong address, byte[] bytes) => builder.AddHeapFragment(new() { Address = address, Data = bytes, Name = $"Data_{address:x}" }); + + void VerifyRead(Action action, bool expectReads = true) + { + underlyingReads.Clear(); + recordedReads.Clear(); + action(); + Assert.Equal(underlyingReads, recordedReads); + if (expectReads) + Assert.NotEmpty(recordedReads); + else + Assert.Empty(recordedReads); + } + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void Enumeration_IncludesNativeMetadataAndNewlyPublishedSubDescriptors(MockTarget.Architecture arch) + { + foreach (bool nativeMainDescriptor in new[] { false, true }) + { + TargetTestHelpers helpers = new(arch); + ContractDescriptorBuilder builder = new(helpers); + const ulong SlotAddress = 0x1000; + builder.AddHeapFragment(new() { Address = SlotAddress, Data = new byte[helpers.PointerSize], Name = "SubDescriptorSlot" }); + ContractDescriptorBuilder.DescriptorBuilder main = new(builder); + main.SetSubDescriptors([("GC", 0)]); + main.SetIndirectValues([SlotAddress]); + ulong mainAddress = main.CreateSubDescriptor(0x2000, 0x3000, 0x4000); + ContractDescriptorBuilder.DescriptorBuilder child = new(builder); + child.SetIndirectValues([0x9876]); + child.SetGlobals([("ChildValue", 0x2345, null)]); + ulong childAddress = child.CreateSubDescriptor(0x5000, 0x6000, 0x7000); + MockMemorySpace.MemoryContext memory = builder.GetMemoryContext(); + ContractDescriptorTarget target = nativeMainDescriptor + ? ContractDescriptorTarget.Create(mainAddress, memory.ReadFromTarget, memory.WriteToTarget, + (_, _, _) => HResults.E_NOTIMPL, (_, _) => HResults.E_NOTIMPL, + (ulong _, out ulong address) => { address = 0; return HResults.E_NOTIMPL; }, []) + : ContractDescriptorTarget.Create(ContractDescriptorParser.ParseCompact("""{"version":2,"subDescriptors":{"GC":[0]}}"""u8), + [new(SlotAddress)], memory.ReadFromTarget, memory.WriteToTarget, + (_, _, _) => HResults.E_NOTIMPL, (_, _) => HResults.E_NOTIMPL, + (ulong _, out ulong address) => { address = 0; return HResults.E_NOTIMPL; }, + arch.IsLittleEndian, helpers.PointerSize); + ICLRDataEnumMemoryRegions impl = new SOSDacImpl(target, legacyObj: null, new()); + using RecordingCallback callback = new(supportsUpdates: false); + Assert.False(target.IsSubDescriptorResolved("GC")); + Assert.Equal(HResults.S_OK, impl.EnumMemoryRegions((void*)callback.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); + Assert.Contains((SlotAddress, (uint)helpers.PointerSize), callback.Ranges); + Assert.DoesNotContain(childAddress, callback.Regions); + Assert.Equal(nativeMainDescriptor, callback.Regions.Contains(mainAddress)); + + target.WritePointer(SlotAddress, new(childAddress)); + callback.Regions.Clear(); + callback.Ranges.Clear(); + Assert.Equal(HResults.S_OK, impl.EnumMemoryRegions((void*)callback.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); + Assert.True(target.IsSubDescriptorResolved("GC")); + Assert.Equal(0x2345u, target.ReadGlobal("ChildValue")); + Assert.Contains((SlotAddress, (uint)helpers.PointerSize), callback.Ranges); + AssertFullyReported(callback, childAddress, (uint)ContractDescriptorHelpers.Size(arch.Is64Bit)); + Assert.Contains((0x7000ul, (uint)helpers.PointerSize), callback.Ranges); + Assert.Contains((0x6000ul, target.Read(childAddress + 12)), callback.Ranges); + Assert.Equal(nativeMainDescriptor, callback.Regions.Contains(mainAddress)); + if (nativeMainDescriptor) + { + AssertFullyReported(callback, mainAddress, (uint)ContractDescriptorHelpers.Size(arch.Is64Bit)); + Assert.Contains((0x4000ul, (uint)helpers.PointerSize), callback.Ranges); + Assert.Contains((0x3000ul, target.Read(mainAddress + 12)), callback.Ranges); + } + + callback.Regions.Clear(); + Assert.Equal(HResults.S_OK, impl.EnumMemoryRegions((void*)callback.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); + Assert.Contains(childAddress, callback.Regions); + Assert.Contains(0x6000ul, callback.Regions); + Assert.Contains(0x7000ul, callback.Regions); + } + } + + [Fact] + public void Enumeration_PreservesContractInstancesAndVersionResolution() + { + ContractDescriptorTarget target = ContractDescriptorTarget.Create( + ContractDescriptorParser.ParseCompact("""{"version":2,"contracts":{"GC":"unsupported","Loader":"unknown"}}"""u8), [], + (_, _) => HResults.E_NOTIMPL, (_, _) => HResults.E_NOTIMPL, (_, _, _) => HResults.E_NOTIMPL, (_, _) => HResults.E_NOTIMPL, + (ulong _, out ulong address) => { address = 0; return HResults.E_NOTIMPL; }, isLittleEndian: true, pointerSize: 8); + target.Contracts.RegisterUnsupported("unsupported"); + target.Contracts.Register(string.Empty, _ => new Mock().Object); + IFeatureFlags original = target.Contracts.FeatureFlags; + ICLRDataEnumMemoryRegions impl = new SOSDacImpl(target, legacyObj: null, new()); + using RecordingCallback callback = new(supportsUpdates: false); + Assert.Equal(HResults.S_OK, impl.EnumMemoryRegions((void*)callback.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); + Assert.Same(original, target.Contracts.FeatureFlags); + Mock.Get(original).Verify(contract => contract.Flush(FlushScope.All), Times.Once); + Assert.Throws(() => target.Contracts.GC); + Assert.Throws(() => target.Contracts.Loader); + Assert.Throws(() => target.Contracts.Thread); + target.Contracts.Register("unsupported", _ => new Mock().Object); + IGC gc = target.Contracts.GC; + Assert.Equal(HResults.S_OK, impl.EnumMemoryRegions((void*)callback.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); + Assert.Same(gc, target.Contracts.GC); + Assert.Same(original, target.Contracts.FeatureFlags); + Mock.Get(gc).Verify(contract => contract.Flush(FlushScope.All), Times.Once); + Mock.Get(original).Verify(contract => contract.Flush(FlushScope.All), Times.Exactly(2)); + } + + [Theory] + [InlineData(0x5000ul)] + [InlineData(0x6000ul)] + [InlineData(0x7000ul)] + [InlineData(0x1100ul)] + public void Enumeration_CompletesDescriptorDiscoveryBeforeReportingCancellation(ulong cancelAtAddress) + { + TargetTestHelpers helpers = new(new() { IsLittleEndian = true, Is64Bit = true }); + ContractDescriptorBuilder builder = new(helpers); + const ulong MainSlot = 0x1000; + const ulong ChildSlot = 0x1100; + builder.AddHeapFragment(new() { Address = MainSlot, Data = new byte[helpers.PointerSize], Name = "MainSlot" }); + builder.AddHeapFragment(new() { Address = ChildSlot, Data = new byte[helpers.PointerSize], Name = "ChildSlot" }); + ContractDescriptorBuilder.DescriptorBuilder main = new(builder); + main.SetSubDescriptors([("Child", 0)]); + main.SetIndirectValues([MainSlot]); + ulong mainAddress = main.CreateSubDescriptor(0x2000, 0x3000, 0x4000); + ContractDescriptorBuilder.DescriptorBuilder child = new(builder); + child.SetSubDescriptors([("Grandchild", 0)]); + child.SetIndirectValues([ChildSlot]); + child.SetGlobals([("ChildValue", 0x2345, null)]); + ulong childAddress = child.CreateSubDescriptor(0x5000, 0x6000, 0x7000); + MockMemorySpace.MemoryContext memory = builder.GetMemoryContext(); + ContractDescriptorTarget target = ContractDescriptorTarget.Create(mainAddress, memory.ReadFromTarget, memory.WriteToTarget, + (_, _, _) => HResults.E_NOTIMPL, (_, _) => HResults.E_NOTIMPL, + (ulong _, out ulong address) => { address = 0; return HResults.E_NOTIMPL; }, []); + target.WritePointer(MainSlot, new(childAddress)); + ICLRDataEnumMemoryRegions impl = new SOSDacImpl(target, legacyObj: null, new()); + using RecordingCallback cancelled = new(supportsUpdates: false) { CancelAtAddress = cancelAtAddress }; + Assert.Equal(HResults.COR_E_OPERATIONCANCELED, impl.EnumMemoryRegions((void*)cancelled.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); + Assert.True(target.IsSubDescriptorResolved("Child")); + int count = cancelled.Ranges.Count; + Assert.Equal(new TargetPointer(childAddress), target.ReadPointer(MainSlot)); + Assert.Equal(count, cancelled.Ranges.Count); + + using RecordingCallback callback = new(supportsUpdates: false); + Assert.Equal(HResults.S_OK, impl.EnumMemoryRegions((void*)callback.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); + Assert.True(target.IsSubDescriptorResolved("Child")); + Assert.False(target.IsSubDescriptorResolved("Grandchild")); + Assert.Equal(0x2345u, target.ReadGlobal("ChildValue")); + AssertFullyReported(callback, childAddress, (uint)ContractDescriptorHelpers.Size(is64Bit: true)); + Assert.Contains((0x6000ul, target.Read(childAddress + 12)), callback.Ranges); + Assert.Contains((0x7000ul, (uint)helpers.PointerSize), callback.Ranges); + Assert.Contains((ChildSlot, (uint)helpers.PointerSize), callback.Ranges); + } + + [Theory] + [InlineData(HResults.S_OK)] + [InlineData(HResults.E_FAIL)] + [InlineData(HResults.COR_E_OPERATIONCANCELED)] + public void Enumeration_RecordsContractReadsAndRestoresReader(int result) + { + const ulong PointerAddress = 0x1000; + ContractDescriptorTarget target = ContractDescriptorTarget.Create( + ContractDescriptorParser.ParseCompact("""{"version":2,"contracts":{"RuntimeTypeSystem":"c1"}}"""u8), [], + (_, buffer) => { buffer.Clear(); return HResults.S_OK; }, + (_, _) => HResults.E_NOTIMPL, (_, _, _) => HResults.E_NOTIMPL, (_, _) => HResults.E_NOTIMPL, + (ulong _, out ulong address) => { address = 0; return HResults.E_NOTIMPL; }, + isLittleEndian: true, pointerSize: 8); + Lock apiLock = new(); + target.Contracts.Register("c1", boundTarget => + { + Assert.True(apiLock.IsHeldByCurrentThread); + Assert.Same(target, boundTarget); + TargetPointer? cached = boundTarget.ReadPointer(PointerAddress); + Mock types = new(); + types.Setup(t => t.GetWellKnownMethodTable(It.IsAny())) + .Returns(() => cached ??= boundTarget.ReadPointer(PointerAddress)); + types.Setup(t => t.Flush(FlushScope.All)).Callback(() => cached = null); + return types.Object; + }); + ICLRDataEnumMemoryRegions impl = new SOSDacImpl(target, legacyObj: null, apiLock); + using RecordingCallback callback = new(supportsUpdates: false, apiLock) { Result = result }; + + Assert.Equal(result, impl.EnumMemoryRegions((void*)callback.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); + Assert.Contains((PointerAddress, (uint)target.PointerSize), callback.Ranges); + Assert.True(callback.WasLockHeld); + Assert.False(apiLock.IsHeldByCurrentThread); + int count = callback.Ranges.Count; + Assert.Equal(TargetPointer.Null, target.ReadPointer(PointerAddress)); + Assert.Equal(count, callback.Ranges.Count); + + using RecordingCallback next = new(supportsUpdates: false); + Assert.Equal(HResults.S_OK, impl.EnumMemoryRegions((void*)next.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); + Assert.Contains(PointerAddress, next.Regions); + Assert.Equal(count, callback.Ranges.Count); + } + + [Fact] + public void ReadShimScopes_RestorePreviousReadersWithoutChangingCaches() + { + ContractDescriptorTarget target = ContractDescriptorTarget.Create( + ContractDescriptorParser.ParseCompact("""{"version":2}"""u8), [], + (_, buffer) => { buffer.Clear(); return HResults.S_OK; }, + (_, _) => HResults.E_NOTIMPL, (_, _, _) => HResults.E_NOTIMPL, (_, _) => HResults.E_NOTIMPL, + (ulong _, out ulong address) => { address = 0; return HResults.E_NOTIMPL; }, + isLittleEndian: true, pointerSize: 8); + int firstReads = 0; + int secondReads = 0; + Assert.Empty(target.EnumerateDescriptorMemory()); + CachedPointer originalData = target.ProcessedData.GetOrAdd(new(0x1000)); + IDisposable first = target.RegisterReadCallback((_, _) => firstReads++); + using (first) + { + Assert.Equal(TargetPointer.Null, target.ReadPointer(0x1000)); + using (target.RegisterReadCallback((_, _) => secondReads++)) + Assert.Equal(TargetPointer.Null, target.ReadPointer(0x1000)); + + Assert.Equal(TargetPointer.Null, target.ReadPointer(0x1000)); + } + using (target.RegisterReadCallback((_, _) => secondReads++)) + { + first.Dispose(); + Assert.Equal(TargetPointer.Null, target.ReadPointer(0x1000)); + } + Assert.Same(originalData, target.ProcessedData.GetOrAdd(new(0x1000))); + Assert.Equal(TargetPointer.Null, target.ReadPointer(0x1000)); + Assert.Equal(3, firstReads); + Assert.Equal(2, secondReads); + } + + [Fact] + public void Enumeration_RejectsOtherTargetImplementationsAtMemoryEnumeratorBoundary() + { + Mock target = new(); + target.SetupGet(t => t.PointerSize).Returns(8); + ICLRDataEnumMemoryRegions impl = new SOSDacImpl(target.Object, legacyObj: null, new()); + using RecordingCallback callback = new(supportsUpdates: false); + + MemoryRegionEmitter emitter = new(callback.Address, (uint)target.Object.PointerSize); + Assert.Equal(HResults.E_NOTIMPL, MemoryEnumerator.Enumerate(target.Object, emitter, DumpType.Mini)); + Assert.Equal(HResults.E_NOTIMPL, impl.EnumMemoryRegions((void*)callback.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); + Assert.Empty(callback.Ranges); + target.Verify(t => t.Flush(It.IsAny()), Times.Never); + } + + [Fact] + public void ReadShim_PreservesDataAccessAndRuntimeImageBase() + { + const ulong RuntimeImageBase = 0x9000; + ContractDescriptorTarget target = ContractDescriptorTarget.Create( + ContractDescriptorParser.ParseCompact("""{"version":2}"""u8), [], + (address, buffer) => + { + Assert.Equal(0x1000ul, address); + buffer.Fill(0x11); + return HResults.S_OK; + }, + (address, buffer) => + { + Assert.Equal(0x2000ul, address); + Assert.Equal(new byte[] { 0x5a }, buffer.ToArray()); + return HResults.S_OK; + }, + (threadId, flags, buffer) => + { + Assert.Equal(5u, threadId); + Assert.Equal(0x10u, flags); + buffer.Fill(0x44); + return HResults.S_OK; + }, + (threadId, context) => + { + Assert.Equal(6u, threadId); + Assert.Equal(new byte[] { 0x99 }, context.ToArray()); + return HResults.S_OK; + }, + (ulong size, out ulong address) => + { + Assert.Equal(16ul, size); + address = 0x8000; + return HResults.S_OK; + }, + isLittleEndian: true, pointerSize: 8, runtimeImageBase: RuntimeImageBase); + List<(ulong Address, ulong Size)> reads = []; + using IDisposable readScope = target.RegisterReadCallback((address, size) => reads.Add((address, size))); + Assert.Equal((byte)0x11, target.Read(0x1000)); + target.WriteBuffer(0x2000, [0x5a]); + byte[] context = new byte[16]; + Assert.True(target.TryGetThreadContext(5, 0x10, context)); + Assert.All(context, value => Assert.Equal((byte)0x44, value)); + Assert.True(target.TrySetThreadContext(6, [0x99])); + Assert.Equal(new TargetPointer(0x8000), target.AllocateMemory(16)); + Assert.True(target.TryGetRuntimeImageBase(out TargetPointer imageBase)); + Assert.Equal(new TargetPointer(RuntimeImageBase), imageBase); + readScope.Dispose(); + Assert.True(target.TryGetRuntimeImageBase(out imageBase)); + Assert.Equal(new TargetPointer(RuntimeImageBase), imageBase); + Assert.Equal((byte)0x11, target.Read(0x1000)); + Assert.Equal(new (ulong, ulong)[] { (0x1000, 1) }, reads); + } + + private static void AssertFullyReported(RecordingCallback callback, ulong address, uint size) + { + for (uint offset = 0; offset < size; offset++) + { + ulong current = address + offset; + Assert.Contains(callback.Ranges, range => current >= range.Address && current - range.Address < range.Size); + } + } + + [Theory] + [InlineData(0, nameof(DumpType.Mini))] + [InlineData(0x200, nameof(DumpType.Heap))] + [InlineData(0x100000, nameof(DumpType.Triage))] + [InlineData(0x100200, nameof(DumpType.Heap))] + [InlineData(0x8000, nameof(DumpType.Mini))] + [InlineData(0x108000, nameof(DumpType.Mini))] + [InlineData(0x108200, nameof(DumpType.Heap))] + public void DumpMode_UsesNativeMiniDumpFlagPrecedence(uint miniDumpFlags, string expected) + { + Assert.Equal(Enum.Parse(expected), SOSDacImpl.GetDumpType(miniDumpFlags)); + } + + [Theory] + [InlineData("", "")] + [InlineData(" at Program.Main()", " at Program.Main()")] + [InlineData(" at Program.Main() in C:\\private\\Program.cs:line 42", " at Program.Main()")] + [InlineData(" at A.M() in /private/a.cs:line 1\n at B.M() in /private/b.cs:line 2\n", " at A.M()\n at B.M()")] + [InlineData(" at A.M() dans /private/a.cs:ligne 1\r\n at B.M()\r\n", " at A.M()\r\n at B.M()")] + [InlineData(" at A.M(F(Int32)) in secret.cs:line 1", " at A.M(F(Int32))")] + [InlineData(" at A.M(\uD83D\uDE00) in secret.cs:line 1", " at A.M(\uD83D\uDE00)")] + [InlineData(" at A.M()\n--- End of stack trace ---\n", " at A.M()")] + [InlineData("unstructured text", "")] + public void TriageStackTrace_RemovesFileInfoAndZerosRemainingCharacters(string original, string expected) + { + char[] buffer = original.ToCharArray(); + Sanitizer.StripFileInfoFromStackTrace(buffer); + Assert.Equal(expected.PadRight(original.Length, '\0'), new string(buffer)); + } + + [Theory] + [InlineData(false, false, false, true, true)] + [InlineData(false, true, true, true, true)] + [InlineData(true, false, false, true, true)] + [InlineData(true, true, false, true, true)] + [InlineData(true, true, true, true, true)] + [InlineData(true, true, false, false, true)] + [InlineData(true, false, false, true, false)] + [InlineData(true, true, true, true, false)] + public void ExceptionCollection_UsesTriagePolicy(bool triage, bool derived, bool overridesStackTrace, bool littleEndian, bool supportsUpdates) + { + TargetPointer exception = new(0x1000); + TargetPointer innerException = new(0x1100); + TargetPointer message = new(0x2000); + TargetPointer stackTrace = new(0x3000); + TargetPointer remoteStackTrace = new(0x4000); + TargetPointer exceptionTable = new(0x100); + TargetPointer derivedTable = new(0x200); + TargetPointer stringTable = new(0x300); + TargetPointer objectTable = new(0x400); + TargetPointer getter = new(0x500); + const uint StringOffset = 12; + const string Original = " at Program.Main() in /private/Program.cs:line 42"; + const string Sanitized = " at Program.Main()"; + Encoding encoding = littleEndian ? Encoding.Unicode : Encoding.BigEndianUnicode; + byte[] stringBytes = encoding.GetBytes(Original); + + Mock objects = new(); + objects.Setup(o => o.GetSize(It.IsAny())).Returns(128); + objects.Setup(o => o.GetMethodTableAddress(It.IsAny())).Returns(stringTable); + objects.Setup(o => o.GetMethodTableAddress(exception)).Returns(derived ? derivedTable : exceptionTable); + objects.Setup(o => o.GetMethodTableAddress(innerException)).Returns(exceptionTable); + objects.Setup(o => o.GetStringValue(It.IsAny())).Returns(Original); + uint stringLength = (uint)Original.Length; + uint stringOffset = StringOffset; + objects.Setup(o => o.GetStringData(It.IsAny(), out stringLength, out stringOffset)); + + Mock types = new(); + types.Setup(t => t.GetTypeHandle(It.IsAny())).Returns((TargetPointer p) => new TestTypeHandle(p)); + types.Setup(t => t.GetWellKnownMethodTable(WellKnownMethodTable.Exception)).Returns(exceptionTable); + types.Setup(t => t.GetWellKnownMethodTable(WellKnownMethodTable.String)).Returns(stringTable); + types.Setup(t => t.GetWellKnownMethodTable(WellKnownMethodTable.Object)).Returns(objectTable); + types.Setup(t => t.GetParentMethodTable(It.Is(t => t.Address == derivedTable))).Returns(exceptionTable); + types.Setup(t => t.GetNumVtableSlots(It.Is(t => t.Address == exceptionTable))).Returns(2); + types.Setup(t => t.GetNumVtableSlots(It.Is(t => t.Address == objectTable))).Returns(1); + types.Setup(t => t.GetMethodDescForSlot(It.Is(t => t.Address == exceptionTable), 1)).Returns(getter); + types.Setup(t => t.GetMethodDescForSlot(It.Is(t => t.Address == derivedTable), 1)) + .Returns(overridesStackTrace ? new TargetPointer(0x600) : getter); + types.Setup(t => t.GetMethodDescHandle(getter)).Returns(new MethodDescHandle(getter)); + types.Setup(t => t.GetMethodToken(It.IsAny())).Returns(0x06000001); + + MetadataBuilder metadata = new(); + metadata.AddModule(0, metadata.GetOrAddString("TestModule"), default, default, default); + metadata.AddMethodDefinition(MethodAttributes.Public | MethodAttributes.Virtual, MethodImplAttributes.IL, + metadata.GetOrAddString("get_StackTrace"), default, 0, default); + BlobBuilder blob = new(); + new MetadataRootBuilder(metadata).Serialize(blob, 0, 0); + using MetadataReaderProvider provider = MetadataReaderProvider.FromMetadataImage(ImmutableArray.Create(blob.ToArray())); + Mock ecmaMetadata = new(); + ecmaMetadata.Setup(m => m.GetMetadata(It.IsAny())).Returns(provider.GetMetadataReader()); + + Mock exceptions = new(); + exceptions.Setup(e => e.GetExceptionData(exception)) + .Returns(new ExceptionData(message, innerException, TargetPointer.Null, TargetPointer.Null, stackTrace, remoteStackTrace, 0, 0)); + exceptions.Setup(e => e.GetExceptionData(innerException)) + .Returns(new ExceptionData(message, TargetPointer.Null, TargetPointer.Null, TargetPointer.Null, TargetPointer.Null, TargetPointer.Null, 0, 0)); + exceptions.Setup(e => e.GetExceptionStackFrames(It.IsAny())).Returns([]); + TestPlaceholderTarget target = new TestPlaceholderTarget.Builder(new() { IsLittleEndian = littleEndian, Is64Bit = true }) + .AddMockContract(objects.Object) + .AddMockContract(types.Object) + .AddMockContract(exceptions.Object) + .AddMockContract(ecmaMetadata.Object) + .AddMockContract(new Mock().Object) + .AddMockContract(new Mock().Object) + .UseReader((ulong address, Span buffer) => + { + Assert.True(address == stackTrace.Value + StringOffset || address == remoteStackTrace.Value + StringOffset); + stringBytes.CopyTo(buffer); + return 0; + }) + .Build(); + + using RecordingCallback callback = new(supportsUpdates); + MemoryRegionEmitter emitter = new(callback.Address, 8); + new ObjectCollector(target, emitter, new MethodCollector(target, emitter), triage ? DumpType.Triage : DumpType.Mini).EnumerateObject(exception); + + Assert.Contains(exception.Value, callback.Regions); + Assert.Contains(innerException.Value, callback.Regions); + Assert.Equal(!triage, callback.Regions.Contains(message.Value)); + Assert.Contains(stackTrace.Value, callback.Regions); + Assert.Equal(!triage || !overridesStackTrace, callback.Regions.Contains(remoteStackTrace.Value)); + if (triage && supportsUpdates) + { + byte[] expected = encoding.GetBytes(Sanitized.PadRight(Original.Length, '\0')); + Assert.Equal(expected, callback.Updates[stackTrace.Value + StringOffset]); + if (!overridesStackTrace) + Assert.Equal(expected, callback.Updates[remoteStackTrace.Value + StringOffset]); + Assert.Equal(overridesStackTrace ? 1 : 2, callback.Updates.Count); + } + else + { + Assert.Empty(callback.Updates); + } + Assert.Equal(0, emitter.Result); + } + + [Theory] + [InlineData(false, nameof(DumpType.Mini))] + [InlineData(true, nameof(DumpType.Mini))] + [InlineData(true, nameof(DumpType.Heap))] + [InlineData(true, nameof(DumpType.Triage))] + [InlineData(false, nameof(DumpType.Mini), CodePointerFlags.HasArm32ThumbBit)] + public void ExceptionCollection_EnumeratesInstructionPointersForSavedFrames(bool is64Bit, string dumpType, CodePointerFlags codePointerFlags = default) + { + TargetPointer exception = new(0x1000); + TargetPointer exceptionTable = new(0x2000); + TargetPointer savedIp = new(0x8001); + ulong expectedIp = codePointerFlags == CodePointerFlags.HasArm32ThumbBit ? 0x8000ul : 0x8001ul; + uint pointerSize = is64Bit ? 8u : 4u; + Mock objects = new(); + objects.Setup(o => o.GetSize(exception)).Returns(128); + objects.Setup(o => o.GetMethodTableAddress(exception)).Returns(exceptionTable); + Mock types = new(); + types.Setup(t => t.GetTypeHandle(exceptionTable)).Returns(new TestTypeHandle(exceptionTable)); + types.Setup(t => t.GetWellKnownMethodTable(WellKnownMethodTable.Exception)).Returns(exceptionTable); + Mock exceptions = new(); + exceptions.Setup(e => e.GetExceptionStackFrames(exception)).Returns( + [ + new ExceptionStackFrameInfo(savedIp, TargetPointer.Null, false), + new ExceptionStackFrameInfo(new TargetPointer(savedIp.Value + 0x100), TargetPointer.Null, false), + ]); + Mock platformMetadata = new(); + platformMetadata.Setup(p => p.GetCodePointerFlags()).Returns(codePointerFlags); + TestPlaceholderTarget target = new TestPlaceholderTarget.Builder(new() { IsLittleEndian = true, Is64Bit = is64Bit }) + .AddMockContract(objects.Object) + .AddMockContract(types.Object) + .AddMockContract(exceptions.Object) + .AddMockContract(platformMetadata.Object) + .AddMockContract(new Mock().Object) + .AddMockContract(new Mock().Object) + .UseReader((ulong _, Span _) => -1) + .Build(); + using RecordingCallback callback = new(supportsUpdates: false); + MemoryRegionEmitter emitter = new(callback.Address, (uint)target.PointerSize); + new ObjectCollector(target, emitter, new MethodCollector(target, emitter), Enum.Parse(dumpType)).EnumerateObject(exception); + + Assert.Contains((expectedIp, pointerSize), callback.Ranges); + Assert.Contains((expectedIp + 0x100, pointerSize), callback.Ranges); + Assert.Equal(HResults.S_OK, emitter.Result); + } + + [Fact] + public void MethodCollection_EnumeratesInstructionPointersBeforeCodeBlockDeduplication() + { + Mock executionManager = new(); + executionManager.Setup(e => e.GetCodeBlockHandle(It.IsAny())) + .Returns(new CodeBlockHandle(new TargetPointer(0x2000))); + TestPlaceholderTarget target = new TestPlaceholderTarget.Builder(new() { IsLittleEndian = true, Is64Bit = true }) + .AddMockContract(new Mock().Object) + .AddMockContract(executionManager.Object) + .AddMockContract(new Mock().Object) + .Build(); + using RecordingCallback callback = new(supportsUpdates: false); + MemoryRegionEmitter emitter = new(callback.Address, (uint)target.PointerSize); + MethodCollector methods = new(target, emitter); + + methods.CaptureMethod(TargetPointer.Null, new TargetCodePointer(0x8000)); + methods.CaptureMethod(TargetPointer.Null, new TargetCodePointer(0x8100)); + methods.CaptureMethod(TargetPointer.Null, new TargetCodePointer(0x8000)); + methods.CaptureMethod(TargetPointer.Null, TargetCodePointer.Null); + + Assert.Equal([(0x8000ul, 8u), (0x8100ul, 8u), (0x8000ul, 8u)], callback.Ranges); + Assert.Single(executionManager.Invocations, invocation => invocation.Method.Name == nameof(IExecutionManager.GetGCInfo)); + } + + [Theory] + [InlineData(false, 0x8000ul)] + [InlineData(true, 0x8000ul)] + [InlineData(false, 7ul)] + [InlineData(true, 7ul)] + [InlineData(false, 0xf0000000ul)] + public void MethodCollection_CapturesOnlyInstructionPointerBytes(bool is64Bit, ulong instructionPointer) + { + TestPlaceholderTarget target = new TestPlaceholderTarget.Builder(new() { IsLittleEndian = true, Is64Bit = is64Bit }) + .AddMockContract(new Mock().Object) + .AddMockContract(new Mock().Object) + .UseReader((ulong _, Span _) => throw new InvalidOperationException("IP collection must not read or decode instructions.")) + .Build(); + using RecordingCallback callback = new(supportsUpdates: false); + MemoryRegionEmitter emitter = new(callback.Address, (uint)target.PointerSize); + new MethodCollector(target, emitter).CaptureMethod(TargetPointer.Null, new TargetCodePointer(instructionPointer)); + + ulong expectedAddress = is64Bit ? instructionPointer : unchecked((ulong)(long)(int)instructionPointer); + Assert.Equal([(expectedAddress, is64Bit ? 8u : 4u)], callback.Ranges); + Assert.Equal(HResults.S_OK, emitter.Result); + } + + [Theory] + [InlineData(true, 0ul)] + [InlineData(true, ulong.MaxValue)] + [InlineData(false, uint.MaxValue)] + public void MethodCollection_InvalidInstructionPointerIsSkipped(bool is64Bit, ulong instructionPointer) + { + TestPlaceholderTarget target = new TestPlaceholderTarget.Builder(new() { IsLittleEndian = true, Is64Bit = is64Bit }) + .AddMockContract(new Mock().Object) + .AddMockContract(new Mock().Object) + .Build(); + using RecordingCallback callback = new(supportsUpdates: false); + MemoryRegionEmitter emitter = new(callback.Address, (uint)target.PointerSize); + new MethodCollector(target, emitter).CaptureMethod(TargetPointer.Null, new TargetCodePointer(instructionPointer)); + + Assert.Empty(callback.Ranges); + Assert.Equal(HResults.S_OK, emitter.Result); + } + + [Fact] + public void MethodCollection_InstructionPointerCancellationPropagates() + { + TestPlaceholderTarget target = new TestPlaceholderTarget.Builder(new() { IsLittleEndian = true, Is64Bit = true }) + .AddMockContract(new Mock().Object) + .Build(); + using RecordingCallback callback = new(supportsUpdates: false) { Result = HResults.COR_E_OPERATIONCANCELED }; + MemoryRegionEmitter emitter = new(callback.Address, (uint)target.PointerSize); + + Assert.Throws(() => + new MethodCollector(target, emitter).CaptureMethod(TargetPointer.Null, new TargetCodePointer(0x8000))); + } + + private sealed record TestTypeHandle(TargetPointer Address) : ITypeHandle; + + private sealed record CachedPointer(TargetPointer Value) : Data.IData + { + public static CachedPointer Create(Target target, TargetPointer address) => new(target.ReadPointer(address)); + } + + [GeneratedComClass] + private partial class RuntimeDataTarget(byte[] image) : ICLRDataTarget + { + public ContractDescriptorTarget.ReadFromTargetDelegate? ReadMemory { get; init; } + public ulong ImageBase { get; set; } = 0x10000; + public int LookupResult { get; set; } + public bool ShortRead { get; init; } + public int ImageBaseLookups { get; private set; } + public int RuntimeBaseLookups { get; protected set; } + public int ReadCount { get; private set; } + + public int GetImageBase(string imagePath, ulong* baseAddress) + { + Assert.Equal("coreclr.dll", imagePath); + ImageBaseLookups++; + *baseAddress = ImageBase; + return LookupResult; + } + + public int ReadVirtual(ulong address, byte* buffer, uint bytesRequested, uint* bytesRead) + { + ReadCount++; + *bytesRead = 0; + if (ReadMemory is not null) + { + int hr = ReadMemory(address, new Span(buffer, checked((int)bytesRequested))); + if (hr >= 0) + *bytesRead = ShortRead && bytesRequested != 0 ? bytesRequested - 1 : bytesRequested; + return hr; + } + + if (address < ImageBase || address - ImageBase > (ulong)image.Length + || bytesRequested > (ulong)image.Length - (address - ImageBase)) + { + return HResults.E_FAIL; + } + + image.AsSpan((int)(address - ImageBase), (int)bytesRequested).CopyTo(new Span(buffer, (int)bytesRequested)); + *bytesRead = ShortRead && bytesRequested != 0 ? bytesRequested - 1 : bytesRequested; + return HResults.S_OK; + } + + public int GetMachineType(uint* machineType) => throw new NotImplementedException(); + public int GetPointerSize(uint* pointerSize) => throw new NotImplementedException(); + public int WriteVirtual(ulong address, byte* buffer, uint bytesRequested, uint* bytesWritten) => throw new NotImplementedException(); + public int GetTLSValue(uint threadID, uint index, ulong* value) => throw new NotImplementedException(); + public int SetTLSValue(uint threadID, uint index, ulong value) => throw new NotImplementedException(); + public int GetCurrentThreadID(uint* threadID) => throw new NotImplementedException(); + public int GetThreadContext(uint threadID, uint contextFlags, uint contextSize, byte* context) + => throw new NotImplementedException(); + public int SetThreadContext(uint threadID, uint contextSize, byte* context) => throw new NotImplementedException(); + public int Request(uint reqCode, uint inBufferSize, byte* inBuffer, uint outBufferSize, byte* outBuffer) => throw new NotImplementedException(); + } + + private sealed class RuntimeLocatorDataTarget(byte[] image) : RuntimeDataTarget(image), ICLRRuntimeLocator + { + public int RuntimeLookupResult { get; init; } + + public int GetRuntimeBase(ulong* baseAddress) + { + RuntimeBaseLookups++; + *baseAddress = ImageBase; + return RuntimeLookupResult; + } + } + + [GeneratedComClass] + private partial class ContractLocatorDataTarget() : RuntimeDataTarget([]), ICLRContractLocator + { + public ulong ContractAddress { get; init; } + public int ContractResult { get; init; } + public int ContractLookups { get; private set; } + + public int GetContractDescriptor(ulong* contractAddress) + { + ContractLookups++; + *contractAddress = ContractAddress; + return ContractResult; + } + } + + [GeneratedComClass] + private sealed partial class RuntimeAndContractLocatorDataTarget : ContractLocatorDataTarget, ICLRRuntimeLocator + { + public int RuntimeLookupResult { get; init; } + + public int GetRuntimeBase(ulong* baseAddress) + { + RuntimeBaseLookups++; + *baseAddress = ImageBase; + return RuntimeLookupResult; + } + } + + private sealed class RecordingCallback : IDisposable + { + private readonly nint* _instance; + private readonly nint* _vtable; + private readonly GCHandle _handle; + private readonly bool _supportsUpdates; + private readonly Lock? _apiLock; + public bool WasLockHeld { get; private set; } = true; + public int Result { get; set; } + public ulong? CancelAtAddress { get; set; } + public HashSet Regions { get; } = []; + public List<(ulong Address, uint Size)> Ranges { get; } = []; + public Dictionary Updates { get; } = []; + public nint Address => (nint)_instance; + + public RecordingCallback(bool supportsUpdates, Lock? apiLock = null) + { + _supportsUpdates = supportsUpdates; + _apiLock = apiLock; + _handle = GCHandle.Alloc(this); + _vtable = (nint*)NativeMemory.AllocZeroed(5, (nuint)sizeof(nint)); + _vtable[0] = (nint)(delegate* unmanaged[MemberFunction])&QueryInterface; + _vtable[2] = (nint)(delegate* unmanaged[MemberFunction])&Release; + _vtable[3] = (nint)(delegate* unmanaged[MemberFunction])&Enumerate; + _vtable[4] = (nint)(delegate* unmanaged[MemberFunction])&Update; + _instance = (nint*)NativeMemory.Alloc(2, (nuint)sizeof(nint)); + _instance[0] = (nint)_vtable; + _instance[1] = GCHandle.ToIntPtr(_handle); + } + + private static RecordingCallback Get(nint self) => (RecordingCallback)GCHandle.FromIntPtr(((nint*)self)[1]).Target!; + + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] + private static int QueryInterface(nint self, Guid* iid, nint* result) + { + *result = Get(self)._supportsUpdates && *iid == new Guid("3721A26F-8B91-4D98-A388-DB17B356FADB") ? self : 0; + return *result != 0 ? 0 : unchecked((int)0x80004002); + } + + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] + private static uint Release(nint self) => 1; + + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] + private static int Enumerate(nint self, ulong address, uint size) + { + RecordingCallback callback = Get(self); + callback.Regions.Add(address); + callback.Ranges.Add((address, size)); + callback.WasLockHeld &= callback._apiLock?.IsHeldByCurrentThread ?? true; + return callback.CancelAtAddress == address ? HResults.COR_E_OPERATIONCANCELED : callback.Result; + } + + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] + private static int Update(nint self, ulong address, uint size, byte* bytes) + { + Get(self).Updates[address] = new ReadOnlySpan(bytes, checked((int)size)).ToArray(); + return 0; + } + + public void Dispose() + { + NativeMemory.Free(_instance); + NativeMemory.Free(_vtable); + _handle.Free(); + } + } +} diff --git a/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs b/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs index aff57ebe5864e9..4fc4c0263aa31c 100644 --- a/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs @@ -456,7 +456,7 @@ public void GetMethodDesc_R2R_WasmVirtualIPRangeList_ResolvesCapturedShape() } [Fact] - public void ReadyToRunInfo_WasmMissingOptionalFields_EnsureAllFieldsRead() + public void ReadyToRunInfo_WasmMissingOptionalFields_TryReadAllFields() { MockTarget.Architecture wasmArch = new() { IsLittleEndian = true, Is64Bit = false }; MockExecutionManagerBuilder emBuilder = new( @@ -474,7 +474,7 @@ public void ReadyToRunInfo_WasmMissingOptionalFields_EnsureAllFieldsRead() Data.ReadyToRunInfo data = target.ProcessedData.GetOrAdd(r2rInfo.Address); - ((Data.IReadableData)data).EnsureAllFieldsRead(); + Assert.True(data.TryReadAllFields()); Assert.Null(data.NumHotColdMap); Assert.Null(data.DelayLoadMethodCallThunks); diff --git a/src/native/managed/cdac/tests/UnitTests/FunctionTableAccessTests.cs b/src/native/managed/cdac/tests/UnitTests/FunctionTableAccessTests.cs index ceddf40e0ab422..598984e0a19c83 100644 --- a/src/native/managed/cdac/tests/UnitTests/FunctionTableAccessTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/FunctionTableAccessTests.cs @@ -145,28 +145,6 @@ public void QueryInterfaceFromIXCLRDataProcess_ReturnsProcess3(MockTarget.Archit } } - [Theory] - [ClassData(typeof(MockTarget.StdArch))] - public void QueryInterfaceFromIXCLRDataProcess_ForMemoryRegionEnumeration_ReturnsNoInterface(MockTarget.Architecture arch) - { - TestPlaceholderTarget target = new TestPlaceholderTarget.Builder(arch).Build(); - SOSDacImpl impl = new(target, legacyObj: null, new()); - void* process = ComInterfaceMarshaller.ConvertToUnmanaged(impl); - - try - { - Guid iid = typeof(ICLRDataEnumMemoryRegions).GUID; - int hr = Marshal.QueryInterface((nint)process, in iid, out nint memoryRegions); - - Assert.Equal(HResults.COR_E_INVALIDCAST, hr); - Assert.Equal(nint.Zero, memoryRegions); - } - finally - { - ComInterfaceMarshaller.Free(process); - } - } - [Theory] [ClassData(typeof(MockTarget.StdArch))] public void GetFunctionTable_NullOutParameters_ReturnsEPointer(MockTarget.Architecture arch) diff --git a/src/native/managed/cdac/tests/UnitTests/Microsoft.Diagnostics.DataContractReader.Tests.csproj b/src/native/managed/cdac/tests/UnitTests/Microsoft.Diagnostics.DataContractReader.Tests.csproj index 41f1bdc70000d6..cf2ad984856c6e 100644 --- a/src/native/managed/cdac/tests/UnitTests/Microsoft.Diagnostics.DataContractReader.Tests.csproj +++ b/src/native/managed/cdac/tests/UnitTests/Microsoft.Diagnostics.DataContractReader.Tests.csproj @@ -9,6 +9,7 @@ +