diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 11998dfe324006..77c82967694d33 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -10,6 +10,8 @@ If you make code changes, do not complete without checking the relevant code bui Before completing, use the `code-review` skill to review your code changes. Any issues flagged as errors or warnings should be addressed before completing. +Before making changes to a directory, search for `README.md` files in that directory and its parent directories up to the repository root. Read any you find — they contain conventions, patterns, and architectural context relevant to your work. + If the changes are intended to improve performance, or if they could negatively impact performance, use the `performance-benchmark` skill to validate the impact before completing. You MUST follow all code-formatting and naming conventions defined in [`.editorconfig`](/.editorconfig). diff --git a/docs/design/datacontracts/Thread.md b/docs/design/datacontracts/Thread.md index c8e17a88ebf152..e3aca18dc58e53 100644 --- a/docs/design/datacontracts/Thread.md +++ b/docs/design/datacontracts/Thread.md @@ -98,7 +98,7 @@ The contract additionally depends on these data descriptors | `Thread` | `ExceptionTracker` | Pointer to exception tracking information | | `Thread` | `RuntimeThreadLocals` | Pointer to some thread-local storage | | `Thread` | `ThreadLocalDataPtr` | Pointer to thread local data structure | -| `Thread` | `UEWatsonBucketTrackerBuckets` | Pointer to thread Watson buckets data | +| `Thread` | `UEWatsonBucketTrackerBuckets` | Pointer to thread Watson buckets data (optional, Windows only) | | `ThreadLocalData` | `NonCollectibleTlsData` | Count of non-collectible TLS data entries | | `ThreadLocalData` | `NonCollectibleTlsArrayData` | Pointer to non-collectible TLS array data | | `ThreadLocalData` | `CollectibleTlsData` | Count of collectible TLS data entries | @@ -271,7 +271,9 @@ byte[] IThread.GetWatsonBuckets(TargetPointer threadPointer) } else { - readFrom = target.ReadPointer(threadPointer + /* Thread::UEWatsonBucketTrackerBuckets offset */); + readFrom = /* Has Thread::UEWatsonBucketTrackerBuckets offset */ + ? target.ReadPointer(threadPointer + /* Thread::UEWatsonBucketTrackerBuckets offset */) + : TargetPointer.Null; if (readFrom == TargetPointer.Null) { readFrom = target.ReadPointer(exceptionTrackerPtr + /* ExceptionInfo::ExceptionWatsonBucketTrackerBuckets offset */); @@ -284,13 +286,15 @@ byte[] IThread.GetWatsonBuckets(TargetPointer threadPointer) } else { - readFrom = target.ReadPointer(threadPointer + /* Thread::UEWatsonBucketTrackerBuckets offset */); + readFrom = /* Has Thread::UEWatsonBucketTrackerBuckets offset */ + ? target.ReadPointer(threadPointer + /* Thread::UEWatsonBucketTrackerBuckets offset */) + : TargetPointer.Null; } Span span = new byte[_target.ReadGlobal("SizeOfGenericModeBlock")]; if (readFrom == TargetPointer.Null) return Array.Empty(); - + _target.ReadBuffer(readFrom, span); return span.ToArray(); } diff --git a/src/coreclr/debug/daccess/cdac.cpp b/src/coreclr/debug/daccess/cdac.cpp index 738db6aeb6b2ed..d62e134a700fe3 100644 --- a/src/coreclr/debug/daccess/cdac.cpp +++ b/src/coreclr/debug/daccess/cdac.cpp @@ -15,8 +15,19 @@ namespace { // Load cdac from next to current module (DAC binary) PathString path; + + // On Unix, GetCurrentModuleBase() returns a raw dladdr base address, not a PAL HMODULE. + // The DAC is typically loaded externally (e.g. by CLRMD via dlopen) and is not registered + // in the PAL module list. Use PAL_GetPalHostModule() which properly registers the module. +#ifdef HOST_UNIX + HMODULE hMod = PAL_GetPalHostModule(); + if (hMod == NULL || WszGetModuleFileName(hMod, path) == 0) +#else if (WszGetModuleFileName((HMODULE)GetCurrentModuleBase(), path) == 0) +#endif + { return false; + } SString::Iterator iter = path.End(); if (!path.FindBack(iter, DIRECTORY_SEPARATOR_CHAR_W)) diff --git a/src/coreclr/debug/daccess/request.cpp b/src/coreclr/debug/daccess/request.cpp index 310873f6e6232a..a1f800835fc331 100644 --- a/src/coreclr/debug/daccess/request.cpp +++ b/src/coreclr/debug/daccess/request.cpp @@ -1581,7 +1581,6 @@ ClrDataAccess::GetObjectStringData(CLRDATA_ADDRESS obj, unsigned int count, _Ino PTR_StringObject str(TO_TADDR(obj)); ULONG32 needed = (ULONG32)str->GetStringLength() + 1; - HRESULT hr; if (stringData && count > 0) { if (count > needed) @@ -3373,7 +3372,7 @@ HRESULT ClrDataAccess::GetHandleEnumForTypes(unsigned int types[], unsigned int DacHandleWalker *walker = new DacHandleWalker(); - HRESULT hr = walker->Init(this, types, count); + hr = walker->Init(this, types, count); if (SUCCEEDED(hr)) hr = walker->QueryInterface(__uuidof(ISOSHandleEnum), (void**)ppHandleEnum); @@ -3400,7 +3399,7 @@ HRESULT ClrDataAccess::GetHandleEnumForGC(unsigned int gen, ISOSHandleEnum **ppH DacHandleWalker *walker = new DacHandleWalker(); - HRESULT hr = walker->Init(this, types, ARRAY_SIZE(types), gen); + hr = walker->Init(this, types, ARRAY_SIZE(types), gen); if (SUCCEEDED(hr)) hr = walker->QueryInterface(__uuidof(ISOSHandleEnum), (void**)ppHandleEnum); @@ -4871,7 +4870,6 @@ HRESULT ClrDataAccess::GetGenerationTable(unsigned int cGenerations, struct Dacp SOSDacEnter(); - HRESULT hr = S_OK; unsigned int numGenerationTableEntries = (unsigned int)(g_gcDacGlobals->total_generation_count); if (pNeeded != NULL) { @@ -4917,7 +4915,6 @@ HRESULT ClrDataAccess::GetFinalizationFillPointers(unsigned int cFillPointers, C SOSDacEnter(); - HRESULT hr = S_OK; unsigned int numFillPointers = (unsigned int)(g_gcDacGlobals->total_generation_count + dac_finalize_queue::ExtraSegCount); if (pNeeded != NULL) { @@ -4958,7 +4955,6 @@ HRESULT ClrDataAccess::GetGenerationTableSvr(CLRDATA_ADDRESS heapAddr, unsigned SOSDacEnter(); - HRESULT hr = S_OK; #ifdef FEATURE_SVR_GC unsigned int numGenerationTableEntries = (unsigned int)(g_gcDacGlobals->total_generation_count); if (pNeeded != NULL) @@ -5008,7 +5004,6 @@ HRESULT ClrDataAccess::GetFinalizationFillPointersSvr(CLRDATA_ADDRESS heapAddr, SOSDacEnter(); - HRESULT hr = S_OK; #ifdef FEATURE_SVR_GC unsigned int numFillPointers = (unsigned int)(g_gcDacGlobals->total_generation_count + dac_finalize_queue::ExtraSegCount); if (pNeeded != NULL) @@ -5158,7 +5153,7 @@ HRESULT ClrDataAccess::GetObjectComWrappersData(CLRDATA_ADDRESS objAddr, CLRDATA SOSDacEnter(); // Default to having found no information. - HRESULT hr = S_FALSE; + hr = S_FALSE; if (pNeeded != NULL) { @@ -5225,8 +5220,6 @@ HRESULT ClrDataAccess::GetObjectComWrappersData(CLRDATA_ADDRESS objAddr, CLRDATA } } - hr = S_FALSE; - SOSDacLeave(); return hr; #else // FEATURE_COMWRAPPERS diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Thread.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Thread.cs index ccdc5017b5d5b8..4290ab14ec8ba5 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Thread.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Thread.cs @@ -33,7 +33,7 @@ public Thread(Target target, TargetPointer address) // Address of the exception tracker ExceptionTracker = address + (ulong)type.Fields[nameof(ExceptionTracker)].Offset; - // UEWatsonBucketTrackerBuckets does not exist on certain platforms + // UEWatsonBucketTrackerBuckets does not exist on non-Windows platforms UEWatsonBucketTrackerBuckets = type.Fields.TryGetValue(nameof(UEWatsonBucketTrackerBuckets), out Target.FieldInfo watsonFieldInfo) ? target.ReadPointer(address + (ulong)watsonFieldInfo.Offset) : TargetPointer.Null; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataStackWalk.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataStackWalk.cs index b2e29676afea0a..7f25c92e4cc154 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataStackWalk.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataStackWalk.cs @@ -128,13 +128,17 @@ int IXCLRDataStackWalk.Next() hr = ex.HResult; } -#if DEBUG + // Advance the legacy stack walk to keep it in sync with the cDAC walk. + // GetFrame() passes the legacy frame to ClrDataFrame, which delegates + // GetArgumentByIndex/GetLocalVariableByIndex to it. If we don't advance + // the legacy walk here, those calls operate on the wrong frame. if (_legacyImpl is not null) { int hrLocal = _legacyImpl.Next(); +#if DEBUG Debug.Assert(hrLocal == hr, $"cDAC: {hr:x}, DAC: {hrLocal:x}"); - } #endif + } return hr; } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/README.md b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/README.md new file mode 100644 index 00000000000000..372128f85b88e4 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/README.md @@ -0,0 +1,104 @@ +# Microsoft.Diagnostics.DataContractReader.Legacy + +This project contains `SOSDacImpl`, which implements the `ISOSDacInterface*` and +`IXCLRDataProcess` COM-style APIs by delegating to the cDAC contract layer. + +## Implementing a new SOSDacImpl method + +When a method currently delegates to `_legacyImpl` (returning `E_NOTIMPL` when null), +replace it with a cDAC implementation following this pattern: + +```csharp +int ISOSDacInterface8.ExampleMethod(uint* pResult) +{ + // 1. Validate pointer arguments before the try block + if (pResult == null) + return HResults.E_INVALIDARG; + + int hr = HResults.S_OK; + try + { + // 2. Get the relevant contract and call it + IGC gc = _target.Contracts.GC; + *pResult = gc.SomeMethod(); + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + + // 3. Cross-validate with legacy DAC in debug builds +#if DEBUG + if (_legacyImpl8 is not null) + { + uint resultLocal; + int hrLocal = _legacyImpl8.ExampleMethod(&resultLocal); + Debug.Assert(hrLocal == hr, $"cDAC: {hr:x}, DAC: {hrLocal:x}"); + if (hr == HResults.S_OK) + { + Debug.Assert(*pResult == resultLocal); + } + } +#endif + return hr; +} +``` + +### Key conventions + +- **HResult returns**: Methods return `int` HResult codes, not exceptions. + Use `HResults.S_OK`, `HResults.S_FALSE`, `HResults.E_INVALIDARG`, etc. +- **Null pointer checks**: Validate output pointer arguments *before* the try block + and return `E_INVALIDARG`. This matches the native DAC behavior. +- **Exception handling**: Wrap all contract calls in try/catch. The catch converts + exceptions to HResult codes via `ex.HResult`. When the native DAC has an explicit + readability check (e.g., `ptr.IsValid()` or `DACGetMethodTableFromObjectPointer` + returning NULL), catch `VirtualReadException` specifically and return the same + HResult the native DAC returns (typically `E_INVALIDARG`). Avoid catching all + exceptions and mapping to a single HRESULT, as this can mask unrelated bugs. +- **Debug cross-validation**: In `#if DEBUG`, call the legacy implementation (if + available) and assert the results match. This catches discrepancies during testing. + +### Legacy delegation placement + +Some cDAC methods create child objects (e.g., `ClrDataMethodInstance`, +`ClrDataFrame`) that delegate certain operations to a legacy counterpart. This is +a temporary implementation workaround to let us create the cDAC incrementally that +should be removed before cDAC ships to customers. In these cases, the legacy call +that obtains the counterpart **must be outside `#if DEBUG`**, because the result is +used functionally, not just for validation. + +For example, `EnumMethodInstanceByAddress` passes `legacyMethod` to +`ClrDataMethodInstance`, which delegates `GetTokenAndScope` and other calls to it. +If the legacy enumeration only runs inside `#if DEBUG`, those delegated calls fail +in Release builds. + +**Rule of thumb**: if a legacy call's result is stored and passed to another +object, keep it outside `#if DEBUG`. Only the assertion that compares +HResults/values belongs inside `#if DEBUG`. + +### Sized-buffer protocol + +Several `ISOSDacInterface8` methods use a two-call pattern where the caller first +queries the needed buffer size, then calls again with a sufficiently large buffer: + +```csharp +int GetSomeTable(uint count, Data* buffer, uint* pNeeded) +``` + +The protocol is: +1. Always set `*pNeeded` to the required count (if `pNeeded` is not null). +2. If `count > 0 && buffer == null`: return `E_INVALIDARG`. +3. If `count < needed`: return `S_FALSE` (buffer too small, but `*pNeeded` is set). +4. If `count >= needed`: populate `buffer` and return `S_OK`. + +This matches the native implementation in `src/coreclr/debug/daccess/request.cpp`. + +### Pointer conversions + +- `TargetPointer` → `ClrDataAddress`: use `pointer.ToClrDataAddress(_target)`. + On 32-bit targets, this **sign-extends** the value (e.g., `0xAA000000` becomes + `0xFFFFFFFF_AA000000`). This matches native DAC behavior. +- `ClrDataAddress` → `TargetPointer`: use `address.ToTargetPointer(_target)`. + +Both are defined in `ConversionExtensions.cs`. diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.IXCLRDataProcess.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.IXCLRDataProcess.cs index 6fa6cbe173ec1a..6297e6867c9190 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.IXCLRDataProcess.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.IXCLRDataProcess.cs @@ -334,14 +334,15 @@ int IXCLRDataProcess.StartEnumMethodInstancesByAddress(ClrDataAddress address, / int hr = HResults.S_FALSE; *handle = 0; + // Start the legacy enumeration to keep it in sync with the cDAC enumeration. + // EnumMethodInstanceByAddress passes the legacy method instance to ClrDataMethodInstance, + // which delegates some operations to it. ulong handleLocal = default; -#if DEBUG int hrLocal = default; if (_legacyProcess is not null) { hrLocal = _legacyProcess.StartEnumMethodInstancesByAddress(address, appDomain, &handleLocal); } -#endif try { @@ -387,9 +388,9 @@ int IXCLRDataProcess.EnumMethodInstanceByAddress(ulong* handle, out IXCLRDataMet GCHandle gcHandle = GCHandle.FromIntPtr((IntPtr)(*handle)); if (gcHandle.Target is not EnumMethodInstances emi) return HResults.E_INVALIDARG; + // Advance the legacy enumeration to keep it in sync with the cDAC enumeration. + // The legacy method instance is passed to ClrDataMethodInstance for delegation. IXCLRDataMethodInstance? legacyMethod = null; - -#if DEBUG int hrLocal = default; if (_legacyProcess is not null) { @@ -397,7 +398,6 @@ int IXCLRDataProcess.EnumMethodInstanceByAddress(ulong* handle, out IXCLRDataMet hrLocal = _legacyProcess.EnumMethodInstanceByAddress(&legacyHandle, out legacyMethod); emi.LegacyHandle = legacyHandle; } -#endif try { @@ -413,7 +413,28 @@ int IXCLRDataProcess.EnumMethodInstanceByAddress(ulong* handle, out IXCLRDataMet } catch (System.Exception ex) { - hr = ex.HResult; + // The cDAC's IterateMethodInstances() implementation is incomplete compared + // to the native DAC's EnumMethodInstances::Next(). The native DAC uses a + // MethodIterator backed by AppDomain assembly iteration with EX_TRY/EX_CATCH + // error handling around each step. The cDAC re-implements this with + // IterateModules()/IterateMethodInstantiations()/IterateTypeParams() which + // call into IRuntimeTypeSystem and ILoader contracts. These contract calls + // (e.g. GetMethodTable, GetTypeHandle, GetMethodDescForSlot, GetModule, + // GetTypeDefToken) can throw when encountering method descs or type handles + // from assemblies/modules that the cDAC cannot fully process. This has been + // observed for generic method instantiations (cases 2-4 in + // IterateMethodInstances) in the SOS.WebApp3 integration test. + // + // Fall back to the legacy DAC result when available, otherwise propagate the error. + if (_legacyProcess is not null) + { + hr = hrLocal; + method = legacyMethod; + } + else + { + hr = ex.HResult; + } } #if DEBUG 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 f1ee4780f5968e..6f90eb4965a254 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs @@ -241,10 +241,27 @@ int ISOSDacInterface.GetAppDomainName(ClrDataAddress addr, uint count, char* nam try { ILoader loader = _target.Contracts.Loader; - string friendlyName = loader.GetAppDomainFriendlyName(); TargetPointer systemDomainPtr = _target.ReadGlobalPointer(Constants.Globals.SystemDomain); ClrDataAddress systemDomain = _target.ReadPointer(systemDomainPtr).ToClrDataAddress(_target); - if (addr == systemDomain || friendlyName == string.Empty) + + string? friendlyName = null; + if (addr != systemDomain) + { + try + { + friendlyName = loader.GetAppDomainFriendlyName(); + } + catch (VirtualReadException) + { + // The FriendlyName field is a PTR_CWSTR (pointer to wide char string). + // ReadUtf16String throws VirtualReadException when the pointer targets + // unreadable memory (e.g. the name is not yet set during early init). + // The native DAC handles this via PTR_AppDomain->m_friendlyName.IsValid() + // and falls through to return an empty string. Match that behavior here. + } + } + + if (friendlyName is null || friendlyName.Length == 0) { if (pNeeded is not null) { @@ -252,14 +269,14 @@ int ISOSDacInterface.GetAppDomainName(ClrDataAddress addr, uint count, char* nam } if (name is not null && count > 0) { - name[0] = '\0'; // Set the first character to null terminator + name[0] = '\0'; } } else { if (pNeeded is not null) { - *pNeeded = (uint)(friendlyName.Length + 1); // +1 for null terminator + *pNeeded = (uint)(friendlyName.Length + 1); } if (name is not null && count > 0) @@ -2690,6 +2707,16 @@ int ISOSDacInterface.GetObjectData(ClrDataAddress objAddr, DacpObjectData* data) } } + catch (VirtualReadException) + { + // The native DAC returns E_INVALIDARG when it cannot read the object's + // method table pointer (DACGetMethodTableFromObjectPointer returns NULL) + // or when the method table fails structural validation + // (DacValidateMethodTable returns false). Both of these cases surface as + // VirtualReadException in the cDAC when GetMethodTableAddress or + // GetTypeHandle attempt to read unreadable target memory. + hr = HResults.E_INVALIDARG; + } catch (System.Exception ex) { hr = ex.HResult; @@ -3984,17 +4011,205 @@ int ISOSDacInterface8.GetNumberGenerations(uint* pGenerations) return hr; } - // WKS int ISOSDacInterface8.GetGenerationTable(uint cGenerations, /*struct DacpGenerationData*/ void* pGenerationData, uint* pNeeded) - => _legacyImpl8 is not null ? _legacyImpl8.GetGenerationTable(cGenerations, pGenerationData, pNeeded) : HResults.E_NOTIMPL; + { + if (cGenerations > 0 && pGenerationData == null) + return HResults.E_INVALIDARG; + + int hr = HResults.S_OK; + try + { + IGC gc = _target.Contracts.GC; + uint totalGenerationCount = _target.ReadGlobal(Constants.Globals.TotalGenerationCount); + + if (pNeeded != null) + *pNeeded = totalGenerationCount; + + if (cGenerations < totalGenerationCount) + { + hr = HResults.S_FALSE; + } + else + { + GCHeapData heapData = gc.GetHeapData(); + DacpGenerationData* genData = (DacpGenerationData*)pGenerationData; + + for (int i = 0; i < (int)totalGenerationCount && i < heapData.GenerationTable.Count; i++) + { + GCGenerationData gen = heapData.GenerationTable[i]; + genData[i].start_segment = gen.StartSegment.ToClrDataAddress(_target); + genData[i].allocation_start = gen.AllocationStart.ToClrDataAddress(_target); + genData[i].allocContextPtr = gen.AllocationContextPointer.ToClrDataAddress(_target); + genData[i].allocContextLimit = gen.AllocationContextLimit.ToClrDataAddress(_target); + } + } + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + +#if DEBUG + if (_legacyImpl8 is not null) + { + uint pNeededLocal; + int hrLocal = _legacyImpl8.GetGenerationTable(cGenerations, pGenerationData, &pNeededLocal); + Debug.Assert(hrLocal == hr, $"cDAC: {hr:x}, DAC: {hrLocal:x}"); + if (pNeeded is not null) + { + Debug.Assert(*pNeeded == pNeededLocal); + } + } +#endif + return hr; + } + int ISOSDacInterface8.GetFinalizationFillPointers(uint cFillPointers, ClrDataAddress* pFinalizationFillPointers, uint* pNeeded) - => _legacyImpl8 is not null ? _legacyImpl8.GetFinalizationFillPointers(cFillPointers, pFinalizationFillPointers, pNeeded) : HResults.E_NOTIMPL; + { + if (cFillPointers > 0 && pFinalizationFillPointers == null) + return HResults.E_INVALIDARG; + + int hr = HResults.S_OK; + try + { + IGC gc = _target.Contracts.GC; + GCHeapData heapData = gc.GetHeapData(); + uint numFillPointers = (uint)heapData.FillPointers.Count; + + if (pNeeded != null) + *pNeeded = numFillPointers; + + if (cFillPointers < numFillPointers) + { + hr = HResults.S_FALSE; + } + else + { + for (int i = 0; i < (int)numFillPointers; i++) + { + pFinalizationFillPointers[i] = heapData.FillPointers[i].ToClrDataAddress(_target); + } + } + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + +#if DEBUG + if (_legacyImpl8 is not null) + { + uint pNeededLocal; + int hrLocal = _legacyImpl8.GetFinalizationFillPointers(cFillPointers, pFinalizationFillPointers, &pNeededLocal); + Debug.Assert(hrLocal == hr, $"cDAC: {hr:x}, DAC: {hrLocal:x}"); + if (pNeeded is not null) + { + Debug.Assert(*pNeeded == pNeededLocal); + } + } +#endif + return hr; + } - // SVR int ISOSDacInterface8.GetGenerationTableSvr(ClrDataAddress heapAddr, uint cGenerations, /*struct DacpGenerationData*/ void* pGenerationData, uint* pNeeded) - => _legacyImpl8 is not null ? _legacyImpl8.GetGenerationTableSvr(heapAddr, cGenerations, pGenerationData, pNeeded) : HResults.E_NOTIMPL; + { + if (heapAddr == 0 || (cGenerations > 0 && pGenerationData == null)) + return HResults.E_INVALIDARG; + + int hr = HResults.S_OK; + try + { + IGC gc = _target.Contracts.GC; + uint totalGenerationCount = _target.ReadGlobal(Constants.Globals.TotalGenerationCount); + + if (pNeeded != null) + *pNeeded = totalGenerationCount; + + if (cGenerations < totalGenerationCount) + { + hr = HResults.S_FALSE; + } + else + { + GCHeapData heapData = gc.GetHeapData(heapAddr.ToTargetPointer(_target)); + DacpGenerationData* genData = (DacpGenerationData*)pGenerationData; + + for (int i = 0; i < (int)totalGenerationCount && i < heapData.GenerationTable.Count; i++) + { + GCGenerationData gen = heapData.GenerationTable[i]; + genData[i].start_segment = gen.StartSegment.ToClrDataAddress(_target); + genData[i].allocation_start = gen.AllocationStart.ToClrDataAddress(_target); + genData[i].allocContextPtr = gen.AllocationContextPointer.ToClrDataAddress(_target); + genData[i].allocContextLimit = gen.AllocationContextLimit.ToClrDataAddress(_target); + } + } + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + +#if DEBUG + if (_legacyImpl8 is not null) + { + uint pNeededLocal; + int hrLocal = _legacyImpl8.GetGenerationTableSvr(heapAddr, cGenerations, pGenerationData, &pNeededLocal); + Debug.Assert(hrLocal == hr, $"cDAC: {hr:x}, DAC: {hrLocal:x}"); + if (pNeeded is not null) + { + Debug.Assert(*pNeeded == pNeededLocal); + } + } +#endif + return hr; + } + int ISOSDacInterface8.GetFinalizationFillPointersSvr(ClrDataAddress heapAddr, uint cFillPointers, ClrDataAddress* pFinalizationFillPointers, uint* pNeeded) - => _legacyImpl8 is not null ? _legacyImpl8.GetFinalizationFillPointersSvr(heapAddr, cFillPointers, pFinalizationFillPointers, pNeeded) : HResults.E_NOTIMPL; + { + if (heapAddr == 0 || (cFillPointers > 0 && pFinalizationFillPointers == null)) + return HResults.E_INVALIDARG; + + int hr = HResults.S_OK; + try + { + IGC gc = _target.Contracts.GC; + GCHeapData heapData = gc.GetHeapData(heapAddr.ToTargetPointer(_target)); + uint numFillPointers = (uint)heapData.FillPointers.Count; + + if (pNeeded != null) + *pNeeded = numFillPointers; + + if (cFillPointers < numFillPointers) + { + hr = HResults.S_FALSE; + } + else + { + for (int i = 0; i < (int)numFillPointers; i++) + { + pFinalizationFillPointers[i] = heapData.FillPointers[i].ToClrDataAddress(_target); + } + } + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + +#if DEBUG + if (_legacyImpl8 is not null) + { + uint pNeededLocal; + int hrLocal = _legacyImpl8.GetFinalizationFillPointersSvr(heapAddr, cFillPointers, pFinalizationFillPointers, &pNeededLocal); + Debug.Assert(hrLocal == hr, $"cDAC: {hr:x}, DAC: {hrLocal:x}"); + if (pNeeded is not null) + { + Debug.Assert(*pNeeded == pNeededLocal); + } + } +#endif + return hr; + } int ISOSDacInterface8.GetAssemblyLoadContext(ClrDataAddress methodTable, ClrDataAddress* assemblyLoadContext) { diff --git a/src/native/managed/cdac/README.md b/src/native/managed/cdac/README.md new file mode 100644 index 00000000000000..5bd873c63bde87 --- /dev/null +++ b/src/native/managed/cdac/README.md @@ -0,0 +1,224 @@ +# cDAC (Data Contract Reader) + +The cDAC is a managed implementation of the diagnostic data access layer. It enables +diagnostic tools to inspect .NET runtime process state by reading memory through +well-defined data contracts, without requiring version-matched native DAC/DBI libraries. + +See [docs/design/datacontracts/datacontracts_design.md](/docs/design/datacontracts/datacontracts_design.md) +for the full design and motivation. + +## Architecture + +The cDAC has a layered architecture. When implementing or testing, it's important to +understand which layer you're working at: + +``` +ISOSDacInterface* / IXCLRDataProcess (COM-style API surface) + │ + ▼ + SOSDacImpl (Microsoft.Diagnostics.DataContractReader.Legacy) + │ Translates COM APIs into contract calls. + │ Handles HResult protocols, pointer conversions, + │ and #if DEBUG cross-validation with legacy DAC. + ▼ + Contract interfaces (Microsoft.Diagnostics.DataContractReader.Contracts) + │ e.g., IGC, IThread, ILoader — pure managed APIs + │ returning strongly-typed structs. + ▼ + Data types (Microsoft.Diagnostics.DataContractReader.Contracts/Data/) + │ e.g., Data.Generation, Data.CFinalize — read fields + │ from target memory at specified addresses/offsets. + ▼ + Target memory (Microsoft.Diagnostics.DataContractReader.Abstractions) + ReadPointer, ReadGlobal, ReadNUInt, etc. +``` + +- **To implement a new SOSDac API**: work in `SOSDacImpl` (Legacy project), calling + existing contracts. See the [Legacy project README](Microsoft.Diagnostics.DataContractReader.Legacy/README.md). +- **To implement a new contract**: work in the Contracts project. See the + [contract specifications](/docs/design/datacontracts/) for the data descriptors + and algorithms each contract must implement. +- **To write tests**: see the [tests README](tests/README.md). + +## Project structure + +| Directory | Purpose | +|-----------|---------| +| `Microsoft.Diagnostics.DataContractReader.Abstractions` | Core abstractions: `Target`, `TargetPointer`, `DataType`, contract interfaces | +| `Microsoft.Diagnostics.DataContractReader.Contracts` | Contract implementations (e.g., `GC_1`) and data type readers | +| `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 | +| `tests` | Unit tests with mock memory infrastructure | + +## Contract specifications + +Each contract has a specification document in +[docs/design/datacontracts/](/docs/design/datacontracts/) describing: + +- The API surface (C# structs and methods) +- Data descriptors (type layouts and field offsets) +- Global variables (with types and which GC mode they apply to) +- Algorithmic pseudo-code for the implementation + +Key specs: [GC](/docs/design/datacontracts/GC.md) · +[Thread](/docs/design/datacontracts/Thread.md) · +[Loader](/docs/design/datacontracts/Loader.md) · +[RuntimeTypeSystem](/docs/design/datacontracts/RuntimeTypeSystem.md) + +## Unit testing + +### Setting up a solution + +For VS Code and Visual Studio, create a file `cdac.slnx` in the runtime repo root to bring +all the cDAC projects into scope: + +```xml + + + + + + + + + + + + + + + + +``` + +In VS Code, run the ".NET: Open Solution" command and select `cdac.slnx`. In Visual Studio, +open the solution file directly. You can then use Test Explorer to run and debug tests. + +### Running unit tests from the command line + +Use the `dotnet.sh` (or `dotnet.cmd`) script in the repo root: + +```bash +./dotnet.sh build /t:Test \ + src/native/managed/cdac/tests/Microsoft.Diagnostics.DataContractReader.Tests.csproj \ + -c Debug -p:RuntimeConfiguration=Debug -p:LibrariesConfiguration=Release +``` + +> **Note:** If you mix release libraries and a debug runtime, you must pass both +> `-p:RuntimeConfiguration=Debug` and `-p:LibrariesConfiguration=Release` so the test +> project resolves the correct shared framework. If everything is Debug, then just +> `-c Debug` is sufficient. + +## End-to-end testing with WinDbg + +### Building a sample app + +Create a hello-world app to use as a debugger target: + +```cmd +cd C:\helloworld +dotnet new console -f net9.0 +``` + +Add `LatestMajor` to the `.csproj` `` so it can +run on a .NET 10+ checkout. Add a `Console.ReadKey()` in `Program.cs` to keep the process +alive while debugging. + +Create a PowerShell script `debug.ps1` to launch WinDbg with the cDAC enabled: + +```powershell +$env:DOTNET_ENABLE_CDAC=1 +windbgx C:\runtime\artifacts\bin\testhost\net10.0-windows-Debug-x64\dotnet.exe .\bin\Debug\net9.0\helloworld.dll +``` + +Replace `C:\runtime` with your runtime repo checkout path. You can also use `corerun.exe` +with a CORE_ROOT directory instead of the testhost `dotnet.exe`. + +### Debugging the cDAC with Visual Studio + +1. Run `debug.ps1` from above. +2. In WinDbg, hit Run and wait for the app to reach the `Console.ReadKey()` pause. +3. Open Visual Studio and select "Attach to process". +4. Attach to the `enghost.exe` process with mixed native and managed debugging. +5. Set breakpoints in `request.cpp` (native DAC) or `SOSDacImpl.cs` (managed cDAC). + +### Useful SOS commands for testing + +| Command | What it exercises | +|---------|-------------------| +| `!clrthreads` | Thread enumeration APIs | +| `!dumpstack` | Stack walking — calls many SOS APIs in `request.cpp` | +| `!dso` / `!dumpstackobjects` | Object inspection for specific object types | + +Click on thread hyperlinks from `!clrthreads` output to switch the active thread before +running `!dumpstack`. + +## Integration testing with SOS + +The [dotnet/diagnostics](https://github.com/dotnet/diagnostics) repo has SOS tests that +exercise the cDAC end-to-end against a live .NET process. These tests can run in two modes: +with the legacy DAC or with the cDAC enabled. + +### How cDAC is activated + +`SOSDacImpl` has `#if DEBUG` cross-validation that compares cDAC results against the legacy +DAC. To enable this, build the cDAC in Debug configuration while everything else can be +Release. Note that some legacy calls must run outside `#if DEBUG` when their results are +used functionally (not just for validation) — see the +[Legacy project README](Microsoft.Diagnostics.DataContractReader.Legacy/README.md) for +details. + +At runtime, the DAC checks the `ENABLE_CDAC` config knob +([daccess.cpp](/src/coreclr/debug/daccess/daccess.cpp)). When set to `1`, it looks up the +`DotNetRuntimeContractDescriptor` symbol in the target process, creates the managed cDAC +interface via `mscordaccore_universal`, and routes SOS queries through it. + +### Building the runtime for SOS testing + +Build from the runtime repo root: + +```bash +./build.sh clr+clr.hosts+libs+tools.cdac -c Debug -lc Release +``` + +The debug build of the runtime (`-rc Debug`, which is the default when `-c Debug` is used) +is required for the brittle DAC to delegate to the cDAC. Release build of the libraries +(`-lc Release`) is highly recommended for a faster inner loop. + +Once the initial build is done, shorter incremental rebuilds can be done with: + +```bash +./build.sh clr.native+tools.cdac -c Debug -lc Release +``` + +This produces a testhost at: +`artifacts/bin/testhost/net--Debug-/shared/Microsoft.NETCore.App//` + +### Running SOS tests in the diagnostics repo + +See [privatebuildtesting.md](https://github.com/dotnet/diagnostics/blob/main/documentation/privatebuildtesting.md) +in the diagnostics repo for the full procedure. The key steps are: + +```bash +# Build managed code (skip native if already built) +./eng/build.sh -c Release --restore --build -skipnative + +# Install test runtimes, overlay your local build, and run tests with cDAC +./eng/build.sh -c Release -test -useCdac -privatebuild -installruntimes \ + -liveRuntimeDir +``` + +The `-useCdac` flag sets `SOS_TEST_CDAC=true`, which causes the test runner (`SOSRunner.cs`) +to set `DOTNET_ENABLE_CDAC=1` on each test process. + +### CI pipeline + +The `runtime-diagnostics.yml` pipeline runs the SOS tests automatically on every PR that +touches `src/native/managed/cdac/**` or `src/coreclr/debug/runtimeinfo/**`. It runs the +tests twice — once with `-useCdac` (cDAC path) and once without (legacy DAC path) — on +Windows x64. + +> **Note:** The runtime and diagnostics repos must be on the same major version. CLRMD +> validates the DAC binary version against the runtime, so a cross-major-version mismatch +> (e.g., 11.0 runtime with 10.0 diagnostics repo) causes `CreateDacInstance` failures. diff --git a/src/native/managed/cdac/tests/GCTests.cs b/src/native/managed/cdac/tests/GCTests.cs new file mode 100644 index 00000000000000..8e7019ddb9b7db --- /dev/null +++ b/src/native/managed/cdac/tests/GCTests.cs @@ -0,0 +1,111 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.Tests; + +public class GCTests +{ + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetHeapData_ReturnsCorrectGenerationTable(MockTarget.Architecture arch) + { + var generations = new GCHeapBuilder.GenerationInput[] + { + new() { StartSegment = 0xAA00_0000, AllocationStart = 0xAA00_1000, AllocContextPointer = 0xAA00_2000, AllocContextLimit = 0xAA00_3000 }, + new() { StartSegment = 0xBB00_0000, AllocationStart = 0xBB00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + new() { StartSegment = 0xCC00_0000, AllocationStart = 0xCC00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + new() { StartSegment = 0xDD00_0000, AllocationStart = 0xDD00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + }; + + ulong[] fillPointers = [0x1000, 0x2000, 0x3000, 0x4000, 0x5000, 0x6000, 0x7000]; + + Target target = new TestPlaceholderTarget.Builder(arch) + .AddGCHeapWks(gc => gc + .SetGenerations(generations) + .SetFillPointers(fillPointers)) + .Build(); + IGC gc = target.Contracts.GC; + + GCHeapData heapData = gc.GetHeapData(); + + Assert.Equal(generations.Length, heapData.GenerationTable.Count); + for (int i = 0; i < generations.Length; i++) + { + Assert.Equal(generations[i].StartSegment, (ulong)heapData.GenerationTable[i].StartSegment); + Assert.Equal(generations[i].AllocationStart, (ulong)heapData.GenerationTable[i].AllocationStart); + Assert.Equal(generations[i].AllocContextPointer, (ulong)heapData.GenerationTable[i].AllocationContextPointer); + Assert.Equal(generations[i].AllocContextLimit, (ulong)heapData.GenerationTable[i].AllocationContextLimit); + } + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetHeapData_ReturnsCorrectFillPointers(MockTarget.Architecture arch) + { + var generations = new GCHeapBuilder.GenerationInput[] + { + new() { StartSegment = 0xAA00_0000, AllocationStart = 0xAA00_1000, AllocContextPointer = 0xAA00_2000, AllocContextLimit = 0xAA00_3000 }, + new() { StartSegment = 0xBB00_0000, AllocationStart = 0xBB00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + new() { StartSegment = 0xCC00_0000, AllocationStart = 0xCC00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + new() { StartSegment = 0xDD00_0000, AllocationStart = 0xDD00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + }; + + ulong[] fillPointers = [0x1111, 0x2222, 0x3333, 0x4444, 0x5555, 0x6666, 0x7777]; + + Target target = new TestPlaceholderTarget.Builder(arch) + .AddGCHeapWks(gc => gc + .SetGenerations(generations) + .SetFillPointers(fillPointers)) + .Build(); + IGC gc = target.Contracts.GC; + + GCHeapData heapData = gc.GetHeapData(); + + Assert.Equal(fillPointers.Length, heapData.FillPointers.Count); + for (int i = 0; i < fillPointers.Length; i++) + { + Assert.Equal(fillPointers[i], (ulong)heapData.FillPointers[i]); + } + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetHeapData_WithFiveGenerations(MockTarget.Architecture arch) + { + var generations = new GCHeapBuilder.GenerationInput[] + { + new() { StartSegment = 0xA000_0000, AllocationStart = 0xA000_1000, AllocContextPointer = 0xA000_2000, AllocContextLimit = 0xA000_3000 }, + new() { StartSegment = 0xB000_0000, AllocationStart = 0xB000_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + new() { StartSegment = 0xC000_0000, AllocationStart = 0xC000_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + new() { StartSegment = 0xD000_0000, AllocationStart = 0xD000_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + new() { StartSegment = 0xE000_0000, AllocationStart = 0xE000_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + }; + + ulong[] fillPointers = [0x1001, 0x2002, 0x3003, 0x4004, 0x5005, 0x6006, 0x7007]; + + Target target = new TestPlaceholderTarget.Builder(arch) + .AddGCHeapWks(gc => gc + .SetGenerations(generations) + .SetFillPointers(fillPointers)) + .Build(); + IGC gc = target.Contracts.GC; + + GCHeapData heapData = gc.GetHeapData(); + + Assert.Equal(5, heapData.GenerationTable.Count); + for (int i = 0; i < generations.Length; i++) + { + Assert.Equal(generations[i].StartSegment, (ulong)heapData.GenerationTable[i].StartSegment); + Assert.Equal(generations[i].AllocationStart, (ulong)heapData.GenerationTable[i].AllocationStart); + } + + Assert.Equal(fillPointers.Length, heapData.FillPointers.Count); + for (int i = 0; i < fillPointers.Length; i++) + { + Assert.Equal(fillPointers[i], (ulong)heapData.FillPointers[i]); + } + } +} diff --git a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.GC.cs b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.GC.cs new file mode 100644 index 00000000000000..e6100b8c85ed9c --- /dev/null +++ b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.GC.cs @@ -0,0 +1,427 @@ +// 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; + +namespace Microsoft.Diagnostics.DataContractReader.Tests; + +/// +/// Configuration object for GC heap mock data, used with +/// and +/// . +/// +internal class GCHeapBuilder +{ + private GCHeapBuilder.GenerationInput[]? _generations; + private ulong[]? _fillPointers; + + public GCHeapBuilder SetGenerations(params GenerationInput[] generations) + { + _generations = generations; + return this; + } + + public GCHeapBuilder SetFillPointers(params ulong[] fillPointers) + { + _fillPointers = fillPointers; + return this; + } + + internal GenerationInput[] GetGenerationsOrDefault(uint defaultCount) => + _generations ?? new GenerationInput[defaultCount]; + + internal ulong[] GetFillPointersOrDefault(uint generationCount) => + _fillPointers ?? []; + + public record struct GenerationInput + { + public ulong StartSegment; + public ulong AllocationStart; + public ulong AllocContextPointer; + public ulong AllocContextLimit; + } +} + +internal static class GCHeapBuilderExtensions +{ + private const ulong DefaultAllocationRangeStart = 0x0010_0000; + private const ulong DefaultAllocationRangeEnd = 0x0020_0000; + private const uint DefaultGenerationCount = 4; + + public static TestPlaceholderTarget.Builder AddGCHeapWks( + this TestPlaceholderTarget.Builder targetBuilder, + Action configure) + { + var config = new GCHeapBuilder(); + configure(config); + BuildWksHeap(targetBuilder, config); + targetBuilder.AddContract(target => + ((IContractFactory)new GCFactory()).CreateContract(target, 1)); + return targetBuilder; + } + + public static TestPlaceholderTarget.Builder AddGCHeapSvr( + this TestPlaceholderTarget.Builder targetBuilder, + Action configure, + out ulong heapAddress) + { + var config = new GCHeapBuilder(); + configure(config); + heapAddress = BuildSvrHeap(targetBuilder, config); + targetBuilder.AddContract(target => + ((IContractFactory)new GCFactory()).CreateContract(target, 1)); + return targetBuilder; + } + + #region Type field definitions + + private static readonly MockDescriptors.TypeFields GCAllocContextFields = new() + { + DataType = DataType.GCAllocContext, + Fields = + [ + new(nameof(Data.GCAllocContext.Pointer), DataType.pointer), + new(nameof(Data.GCAllocContext.Limit), DataType.pointer), + ] + }; + + private static MockDescriptors.TypeFields GetGenerationFields(TargetTestHelpers helpers) + { + uint allocContextSize = MockDescriptors.GetTypesForTypeFields(helpers, [GCAllocContextFields])[DataType.GCAllocContext].Size!.Value; + return new MockDescriptors.TypeFields() + { + DataType = DataType.Generation, + Fields = + [ + new(nameof(Data.Generation.AllocationContext), DataType.GCAllocContext, allocContextSize), + new(nameof(Data.Generation.StartSegment), DataType.pointer), + new(nameof(Data.Generation.AllocationStart), DataType.pointer), + ] + }; + } + + private static readonly MockDescriptors.TypeFields CFinalizeFields = new() + { + DataType = DataType.CFinalize, + Fields = + [ + new(nameof(Data.CFinalize.FillPointers), DataType.pointer), + ] + }; + + private static readonly MockDescriptors.TypeFields OomHistoryFields = new() + { + DataType = DataType.OomHistory, + Fields = + [ + new(nameof(Data.OomHistory.Reason), DataType.int32), + new(nameof(Data.OomHistory.AllocSize), DataType.nuint), + new(nameof(Data.OomHistory.Reserved), DataType.pointer), + new(nameof(Data.OomHistory.Allocated), DataType.pointer), + new(nameof(Data.OomHistory.GcIndex), DataType.nuint), + new(nameof(Data.OomHistory.Fgm), DataType.int32), + new(nameof(Data.OomHistory.Size), DataType.nuint), + new(nameof(Data.OomHistory.AvailablePagefileMb), DataType.nuint), + new(nameof(Data.OomHistory.LohP), DataType.uint32), + ] + }; + + #endregion + + #region Shared helpers + + private static Dictionary GetBaseTypes(TargetTestHelpers helpers) + { + return MockDescriptors.GetTypesForTypeFields(helpers, + [ + GCAllocContextFields, + GetGenerationFields(helpers), + CFinalizeFields, + OomHistoryFields, + ]); + } + + private static Dictionary GetSvrTypes(TargetTestHelpers helpers, uint totalGenerationCount) + { + var baseTypes = GetBaseTypes(helpers); + + uint genSize = baseTypes[DataType.Generation].Size!.Value; + uint oomSize = baseTypes[DataType.OomHistory].Size!.Value; + + int ptrSize = helpers.PointerSize; + int offset = 0; + + var fields = new Dictionary(); + void AddPointerField(string name) { fields[name] = new Target.FieldInfo() { Offset = offset }; offset += ptrSize; } + + AddPointerField(nameof(Data.GCHeapSVR.MarkArray)); + AddPointerField(nameof(Data.GCHeapSVR.NextSweepObj)); + AddPointerField(nameof(Data.GCHeapSVR.BackgroundMinSavedAddr)); + AddPointerField(nameof(Data.GCHeapSVR.BackgroundMaxSavedAddr)); + AddPointerField(nameof(Data.GCHeapSVR.AllocAllocated)); + AddPointerField(nameof(Data.GCHeapSVR.EphemeralHeapSegment)); + AddPointerField(nameof(Data.GCHeapSVR.CardTable)); + AddPointerField(nameof(Data.GCHeapSVR.FinalizeQueue)); + + fields[nameof(Data.GCHeapSVR.GenerationTable)] = new Target.FieldInfo() { Offset = offset }; + offset += (int)(genSize * totalGenerationCount); + + fields[nameof(Data.GCHeapSVR.OomData)] = new Target.FieldInfo() { Offset = offset }; + offset += (int)oomSize; + + AddPointerField(nameof(Data.GCHeapSVR.InternalRootArray)); + AddPointerField(nameof(Data.GCHeapSVR.InternalRootArrayIndex)); + + fields[nameof(Data.GCHeapSVR.HeapAnalyzeSuccess)] = new Target.FieldInfo() { Offset = offset }; + offset += sizeof(int); + offset = (offset + ptrSize - 1) & ~(ptrSize - 1); + + fields[nameof(Data.GCHeapSVR.InterestingData)] = new Target.FieldInfo() { Offset = offset }; + fields[nameof(Data.GCHeapSVR.CompactReasons)] = new Target.FieldInfo() { Offset = offset }; + fields[nameof(Data.GCHeapSVR.ExpandMechanisms)] = new Target.FieldInfo() { Offset = offset }; + fields[nameof(Data.GCHeapSVR.InterestingMechanismBits)] = new Target.FieldInfo() { Offset = offset }; + + baseTypes[DataType.GCHeap] = new Target.TypeInfo() + { + Fields = fields, + Size = (uint)offset, + }; + + return baseTypes; + } + + private static void WriteGenerationData( + TargetTestHelpers helpers, + Span genSpan, + Dictionary types, + GCHeapBuilder.GenerationInput generation) + { + Target.TypeInfo genTypeInfo = types[DataType.Generation]; + Target.TypeInfo allocCtxTypeInfo = types[DataType.GCAllocContext]; + int allocCtxOffset = genTypeInfo.Fields[nameof(Data.Generation.AllocationContext)].Offset; + + helpers.WritePointer( + genSpan.Slice(allocCtxOffset + allocCtxTypeInfo.Fields[nameof(Data.GCAllocContext.Pointer)].Offset), + generation.AllocContextPointer); + helpers.WritePointer( + genSpan.Slice(allocCtxOffset + allocCtxTypeInfo.Fields[nameof(Data.GCAllocContext.Limit)].Offset), + generation.AllocContextLimit); + helpers.WritePointer( + genSpan.Slice(genTypeInfo.Fields[nameof(Data.Generation.StartSegment)].Offset), + generation.StartSegment); + helpers.WritePointer( + genSpan.Slice(genTypeInfo.Fields[nameof(Data.Generation.AllocationStart)].Offset), + generation.AllocationStart); + } + + private static void WriteFillPointers( + TargetTestHelpers helpers, + Span span, + ulong[] fillPointers) + { + for (int i = 0; i < fillPointers.Length; i++) + { + helpers.WritePointer( + span.Slice(helpers.PointerSize * i), + fillPointers[i]); + } + } + + private static MockMemorySpace.HeapFragment AllocatePointerGlobal( + MockMemorySpace.BumpAllocator allocator, + MockMemorySpace.Builder memBuilder, + TargetTestHelpers helpers, + ulong pointsTo, + string name) + { + MockMemorySpace.HeapFragment fragment = allocator.Allocate((ulong)helpers.PointerSize, $"[global pointer] {name}"); + helpers.WritePointer(fragment.Data, pointsTo); + memBuilder.AddHeapFragment(fragment); + return fragment; + } + + #endregion + + private static void BuildWksHeap(TestPlaceholderTarget.Builder targetBuilder, GCHeapBuilder config) + { + MockMemorySpace.Builder memBuilder = targetBuilder.MemoryBuilder; + TargetTestHelpers helpers = memBuilder.TargetTestHelpers; + MockMemorySpace.BumpAllocator allocator = memBuilder.CreateAllocator(DefaultAllocationRangeStart, DefaultAllocationRangeEnd); + + GCHeapBuilder.GenerationInput[] generations = config.GetGenerationsOrDefault(DefaultGenerationCount); + uint genCount = (uint)generations.Length; + ulong[] fillPointers = config.GetFillPointersOrDefault(genCount); + uint fpLength = (uint)fillPointers.Length; + + var types = GetBaseTypes(helpers); + Target.TypeInfo genTypeInfo = types[DataType.Generation]; + Target.TypeInfo cFinalizeTypeInfo = types[DataType.CFinalize]; + Target.TypeInfo oomTypeInfo = types[DataType.OomHistory]; + uint genSize = genTypeInfo.Size!.Value; + + // Allocate and populate generation table + MockMemorySpace.HeapFragment generationTable = allocator.Allocate(genSize * genCount, "GenerationTable"); + for (int i = 0; i < generations.Length; i++) + { + WriteGenerationData(helpers, + generationTable.Data.AsSpan().Slice((int)(i * genSize), (int)genSize), + types, generations[i]); + } + memBuilder.AddHeapFragment(generationTable); + + // Allocate and populate CFinalize with embedded fill pointers array + int fpFieldOffset = cFinalizeTypeInfo.Fields[nameof(Data.CFinalize.FillPointers)].Offset; + ulong cFinalizeSize = (ulong)fpFieldOffset + (ulong)(helpers.PointerSize * (int)fpLength); + MockMemorySpace.HeapFragment cFinalize = allocator.Allocate(cFinalizeSize, "CFinalize"); + WriteFillPointers(helpers, cFinalize.Data.AsSpan().Slice(fpFieldOffset), fillPointers); + memBuilder.AddHeapFragment(cFinalize); + + // Allocate OomHistory (zero-initialized) + MockMemorySpace.HeapFragment oomHistory = allocator.Allocate(oomTypeInfo.Size!.Value, "OomHistory"); + memBuilder.AddHeapFragment(oomHistory); + + // WKS global pointers (double-indirection) + MockMemorySpace.HeapFragment markArrayGlobal = AllocatePointerGlobal(allocator, memBuilder, helpers, 0, "MarkArray"); + MockMemorySpace.HeapFragment nextSweepObjGlobal = AllocatePointerGlobal(allocator, memBuilder, helpers, 0, "NextSweepObj"); + MockMemorySpace.HeapFragment bgMinGlobal = AllocatePointerGlobal(allocator, memBuilder, helpers, 0, "BgMinSavedAddr"); + MockMemorySpace.HeapFragment bgMaxGlobal = AllocatePointerGlobal(allocator, memBuilder, helpers, 0, "BgMaxSavedAddr"); + MockMemorySpace.HeapFragment allocAllocatedGlobal = AllocatePointerGlobal(allocator, memBuilder, helpers, 0, "AllocAllocated"); + MockMemorySpace.HeapFragment ephSegGlobal = AllocatePointerGlobal(allocator, memBuilder, helpers, 0, "EphemeralHeapSegment"); + MockMemorySpace.HeapFragment cardTableGlobal = AllocatePointerGlobal(allocator, memBuilder, helpers, 0, "CardTable"); + MockMemorySpace.HeapFragment finalizeQueueGlobal = AllocatePointerGlobal(allocator, memBuilder, helpers, cFinalize.Address, "FinalizeQueue"); + + MockMemorySpace.HeapFragment internalRootArrayGlobal = AllocatePointerGlobal(allocator, memBuilder, helpers, 0, "InternalRootArray"); + MockMemorySpace.HeapFragment internalRootArrayIndexGlobal = AllocatePointerGlobal(allocator, memBuilder, helpers, 0, "InternalRootArrayIndex"); + MockMemorySpace.HeapFragment heapAnalyzeSuccessGlobal = allocator.Allocate((ulong)helpers.PointerSize, "[HeapAnalyzeSuccess]"); + helpers.Write(heapAnalyzeSuccessGlobal.Data.AsSpan(0, sizeof(int)), 0); + memBuilder.AddHeapFragment(heapAnalyzeSuccessGlobal); + + MockMemorySpace.HeapFragment interestingDataArray = allocator.Allocate((ulong)helpers.PointerSize, "InterestingDataArray"); + MockMemorySpace.HeapFragment compactReasonsArray = allocator.Allocate((ulong)helpers.PointerSize, "CompactReasonsArray"); + MockMemorySpace.HeapFragment expandMechanismsArray = allocator.Allocate((ulong)helpers.PointerSize, "ExpandMechanismsArray"); + MockMemorySpace.HeapFragment interestingMechBitsArray = allocator.Allocate((ulong)helpers.PointerSize, "InterestingMechBitsArray"); + memBuilder.AddHeapFragments([interestingDataArray, compactReasonsArray, expandMechanismsArray, interestingMechBitsArray]); + + MockMemorySpace.HeapFragment lowestAddrGlobal = AllocatePointerGlobal(allocator, memBuilder, helpers, 0x1000, "LowestAddress"); + MockMemorySpace.HeapFragment highestAddrGlobal = AllocatePointerGlobal(allocator, memBuilder, helpers, 0xFFFF_0000, "HighestAddress"); + MockMemorySpace.HeapFragment structInvalidCountGlobal = allocator.Allocate((ulong)helpers.PointerSize, "[StructureInvalidCount]"); + helpers.Write(structInvalidCountGlobal.Data.AsSpan(0, sizeof(int)), 0); + memBuilder.AddHeapFragment(structInvalidCountGlobal); + + MockMemorySpace.HeapFragment maxGenGlobal = allocator.Allocate((ulong)helpers.PointerSize, "[MaxGeneration]"); + helpers.Write(maxGenGlobal.Data.AsSpan(0, sizeof(uint)), genCount - 1); + memBuilder.AddHeapFragment(maxGenGlobal); + + targetBuilder.AddTypes(types); + targetBuilder.AddGlobals( + (nameof(Constants.Globals.TotalGenerationCount), genCount), + (nameof(Constants.Globals.CFinalizeFillPointersLength), fpLength), + (nameof(Constants.Globals.InterestingDataLength), 0UL), + (nameof(Constants.Globals.CompactReasonsLength), 0UL), + (nameof(Constants.Globals.ExpandMechanismsLength), 0UL), + (nameof(Constants.Globals.InterestingMechanismBitsLength), 0UL), + (nameof(Constants.Globals.GCHeapMarkArray), markArrayGlobal.Address), + (nameof(Constants.Globals.GCHeapNextSweepObj), nextSweepObjGlobal.Address), + (nameof(Constants.Globals.GCHeapBackgroundMinSavedAddr), bgMinGlobal.Address), + (nameof(Constants.Globals.GCHeapBackgroundMaxSavedAddr), bgMaxGlobal.Address), + (nameof(Constants.Globals.GCHeapAllocAllocated), allocAllocatedGlobal.Address), + (nameof(Constants.Globals.GCHeapEphemeralHeapSegment), ephSegGlobal.Address), + (nameof(Constants.Globals.GCHeapCardTable), cardTableGlobal.Address), + (nameof(Constants.Globals.GCHeapFinalizeQueue), finalizeQueueGlobal.Address), + (nameof(Constants.Globals.GCHeapGenerationTable), generationTable.Address), + (nameof(Constants.Globals.GCHeapOomData), oomHistory.Address), + (nameof(Constants.Globals.GCHeapInternalRootArray), internalRootArrayGlobal.Address), + (nameof(Constants.Globals.GCHeapInternalRootArrayIndex), internalRootArrayIndexGlobal.Address), + (nameof(Constants.Globals.GCHeapHeapAnalyzeSuccess), heapAnalyzeSuccessGlobal.Address), + (nameof(Constants.Globals.GCHeapInterestingData), interestingDataArray.Address), + (nameof(Constants.Globals.GCHeapCompactReasons), compactReasonsArray.Address), + (nameof(Constants.Globals.GCHeapExpandMechanisms), expandMechanismsArray.Address), + (nameof(Constants.Globals.GCHeapInterestingMechanismBits), interestingMechBitsArray.Address), + (nameof(Constants.Globals.GCLowestAddress), lowestAddrGlobal.Address), + (nameof(Constants.Globals.GCHighestAddress), highestAddrGlobal.Address), + (nameof(Constants.Globals.StructureInvalidCount), structInvalidCountGlobal.Address), + (nameof(Constants.Globals.MaxGeneration), maxGenGlobal.Address)); + targetBuilder.AddGlobalStrings( + (nameof(Constants.Globals.GCIdentifiers), "workstation,segments")); + } + + private static ulong BuildSvrHeap(TestPlaceholderTarget.Builder targetBuilder, GCHeapBuilder config) + { + MockMemorySpace.Builder memBuilder = targetBuilder.MemoryBuilder; + TargetTestHelpers helpers = memBuilder.TargetTestHelpers; + MockMemorySpace.BumpAllocator allocator = memBuilder.CreateAllocator(DefaultAllocationRangeStart, DefaultAllocationRangeEnd); + + GCHeapBuilder.GenerationInput[] generations = config.GetGenerationsOrDefault(DefaultGenerationCount); + uint genCount = (uint)generations.Length; + ulong[] fillPointers = config.GetFillPointersOrDefault(genCount); + uint fpLength = (uint)fillPointers.Length; + + var types = GetSvrTypes(helpers, genCount); + Target.TypeInfo gcHeapTypeInfo = types[DataType.GCHeap]; + Target.TypeInfo cFinalizeTypeInfo = types[DataType.CFinalize]; + uint genSize = types[DataType.Generation].Size!.Value; + + // Allocate and populate CFinalize with embedded fill pointers array + int fpFieldOffset = cFinalizeTypeInfo.Fields[nameof(Data.CFinalize.FillPointers)].Offset; + ulong cFinalizeSize = (ulong)fpFieldOffset + (ulong)(helpers.PointerSize * (int)fpLength); + MockMemorySpace.HeapFragment cFinalize = allocator.Allocate(cFinalizeSize, "CFinalize_SVR"); + WriteFillPointers(helpers, cFinalize.Data.AsSpan().Slice(fpFieldOffset), fillPointers); + memBuilder.AddHeapFragment(cFinalize); + + // Allocate the GCHeap struct, populate FinalizeQueue pointer and generation table + uint heapSize = gcHeapTypeInfo.Size!.Value; + MockMemorySpace.HeapFragment gcHeap = allocator.Allocate(heapSize, "GCHeap_SVR"); + helpers.WritePointer( + gcHeap.Data.AsSpan().Slice(gcHeapTypeInfo.Fields[nameof(Data.GCHeapSVR.FinalizeQueue)].Offset), + cFinalize.Address); + int genTableOffset = gcHeapTypeInfo.Fields[nameof(Data.GCHeapSVR.GenerationTable)].Offset; + for (int i = 0; i < generations.Length; i++) + { + WriteGenerationData(helpers, + gcHeap.Data.AsSpan().Slice(genTableOffset + (int)(i * genSize), (int)genSize), + types, generations[i]); + } + memBuilder.AddHeapFragment(gcHeap); + + // Heap table (array of pointers to heap structs) + MockMemorySpace.HeapFragment heapTable = allocator.Allocate((ulong)helpers.PointerSize, "HeapTable"); + helpers.WritePointer(heapTable.Data, gcHeap.Address); + memBuilder.AddHeapFragment(heapTable); + + // SVR globals + MockMemorySpace.HeapFragment numHeapsGlobal = AllocatePointerGlobal(allocator, memBuilder, helpers, 0, "NumHeaps"); + helpers.Write(numHeapsGlobal.Data.AsSpan(0, sizeof(int)), 1); + MockMemorySpace.HeapFragment heapsGlobal = AllocatePointerGlobal(allocator, memBuilder, helpers, heapTable.Address, "Heaps"); + + MockMemorySpace.HeapFragment lowestAddrGlobal = AllocatePointerGlobal(allocator, memBuilder, helpers, 0x1000, "LowestAddress"); + MockMemorySpace.HeapFragment highestAddrGlobal = AllocatePointerGlobal(allocator, memBuilder, helpers, 0x7FFF_0000, "HighestAddress"); + MockMemorySpace.HeapFragment structInvalidCountGlobal = allocator.Allocate((ulong)helpers.PointerSize, "[StructureInvalidCount]"); + helpers.Write(structInvalidCountGlobal.Data.AsSpan(0, sizeof(int)), 0); + memBuilder.AddHeapFragment(structInvalidCountGlobal); + + MockMemorySpace.HeapFragment maxGenGlobal = allocator.Allocate((ulong)helpers.PointerSize, "[MaxGeneration]"); + helpers.Write(maxGenGlobal.Data.AsSpan(0, sizeof(uint)), genCount - 1); + memBuilder.AddHeapFragment(maxGenGlobal); + + targetBuilder.AddTypes(types); + targetBuilder.AddGlobals( + (nameof(Constants.Globals.TotalGenerationCount), genCount), + (nameof(Constants.Globals.CFinalizeFillPointersLength), fpLength), + (nameof(Constants.Globals.InterestingDataLength), 0UL), + (nameof(Constants.Globals.CompactReasonsLength), 0UL), + (nameof(Constants.Globals.ExpandMechanismsLength), 0UL), + (nameof(Constants.Globals.InterestingMechanismBitsLength), 0UL), + (nameof(Constants.Globals.NumHeaps), numHeapsGlobal.Address), + (nameof(Constants.Globals.Heaps), heapsGlobal.Address), + (nameof(Constants.Globals.GCLowestAddress), lowestAddrGlobal.Address), + (nameof(Constants.Globals.GCHighestAddress), highestAddrGlobal.Address), + (nameof(Constants.Globals.StructureInvalidCount), structInvalidCountGlobal.Address), + (nameof(Constants.Globals.MaxGeneration), maxGenGlobal.Address)); + targetBuilder.AddGlobalStrings( + (nameof(Constants.Globals.GCIdentifiers), "server,segments")); + + return gcHeap.Address; + } +} diff --git a/src/native/managed/cdac/tests/README.md b/src/native/managed/cdac/tests/README.md new file mode 100644 index 00000000000000..c5e317a6a38166 --- /dev/null +++ b/src/native/managed/cdac/tests/README.md @@ -0,0 +1,136 @@ +# cDAC Tests + +Unit tests for the cDAC data contract reader. Tests use mock memory to simulate +a target process without needing a real runtime. + +## Building and running + +```bash +export PATH="$(pwd)/.dotnet:$PATH" # from repo root +dotnet build src/native/managed/cdac/tests +dotnet test src/native/managed/cdac/tests +``` + +To run a subset: +```bash +dotnet test src/native/managed/cdac/tests --filter "FullyQualifiedName~GCTests" +``` + +## Test layers + +Tests can validate behavior at two layers: + +- **Contract-level tests** (e.g., `GCTests.cs`): Call contract APIs like + `IGC.GetHeapData()` directly. Use these to verify that contracts correctly + read and interpret mock target memory. +- **SOSDacImpl-level tests** (e.g., `SOSDacInterface8Tests.cs`): Call through + `ISOSDacInterface*` on `SOSDacImpl`. Use these to verify the full API surface + including HResult protocols, pointer conversions, and buffer sizing. + +When implementing a new `SOSDacImpl` method backed by an existing contract, write +tests at the SOSDacImpl level. When implementing a new contract, write tests at +the contract level. + +## Architecture support + +Tests run on all four architecture combinations using `[ClassData(typeof(MockTarget.StdArch))]`: +- 64-bit little-endian, 64-bit big-endian +- 32-bit little-endian, 32-bit big-endian + +Be aware that `ClrDataAddress` values are **sign-extended** on 32-bit targets +(see `ConversionExtensions.ToClrDataAddress`). A value like `0xAA000000` becomes +`0xFFFFFFFF_AA000000` on 32-bit. Either use values below `0x80000000` or account +for sign extension in assertions. + +## Creating a test target + +Use `TestPlaceholderTarget.Builder` to construct a mock target. Extension methods +like `AddGCHeapWks` add subsystem-specific mock data, types, globals, and +contracts in a single call. Each extension accepts an `Action<>` to configure +only the data the test needs — everything else defaults to zero. + +```csharp +// Contract-level test +Target target = new TestPlaceholderTarget.Builder(arch) + .AddGCHeapWks(gc => gc + .SetGenerations(gen0, gen1, gen2, gen3) + .SetFillPointers(0x1000, 0x2000, 0x3000)) + .Build(); +IGC gc = target.Contracts.GC; + +// SOSDacImpl-level test +ISOSDacInterface8 dac8 = new SOSDacImpl( + new TestPlaceholderTarget.Builder(arch) + .AddGCHeapWks(gc => gc.SetGenerations(generations).SetFillPointers(fillPointers)) + .Build(), + legacyObj: null); + +// Server GC — heap address returned via out parameter +ISOSDacInterface8 dac8 = new SOSDacImpl( + new TestPlaceholderTarget.Builder(arch) + .AddGCHeapSvr(gc => gc.SetGenerations(generations).SetFillPointers(fillPointers), + out var heapAddr) + .Build(), + legacyObj: null); +``` + +The builder owns the `MockMemorySpace.Builder` internally, accumulates types +and globals from each `Add*` call, and wires up contracts automatically at +`Build()` time via `TestContractRegistry`. + +## MockDescriptors + +`MockDescriptors/` contains helpers that set up mock target memory for each +subsystem. The preferred pattern is an **extension method on +`TestPlaceholderTarget.Builder`** that takes an `Action` parameter: + +1. A configuration class (e.g., `GCHeapBuilder`) accumulates test data via + fluent `Set*()` methods. +2. The extension method allocates mock memory, registers types/globals/contracts + directly on the target builder, and returns the builder for chaining. +3. Unset arrays default to zero-length or zero-initialized so tests only + configure the data they care about. + +See `MockDescriptors.GC.cs` for a complete example (`GCHeapBuilder` + +`GCHeapBuilderExtensions`). + +### Key patterns + +#### Composite (embedded) fields + +When a type contains an embedded struct (not a pointer to it), you must specify +the size explicitly in the field definition: + +```csharp +// Get the size of the embedded type first +uint allocContextSize = MockDescriptors.GetTypesForTypeFields(helpers, [GCAllocContextFields]) + [DataType.GCAllocContext].Size!.Value; + +// Then reference it with an explicit size +new(nameof(Data.Generation.AllocationContext), DataType.GCAllocContext, allocContextSize) +``` + +Without the explicit size, the layout engine won't know how much space to reserve. + +#### Embedded arrays vs pointer fields + +Some fields are pointers to external data, others are the start of an inline array. +Check how the `Data.*` constructor reads the field: + +- **Pointer**: `target.ReadPointer(address + offset)` → allocate separate memory, + write a pointer to it. +- **Embedded/inline**: `address + offset` (no ReadPointer) → write array elements + directly into the struct at that offset. + +#### Global indirection levels + +Globals have different indirection patterns depending on the subsystem. +Check how the contract reads the global: + +- **`ReadGlobal(name)`**: reads a primitive value directly from the global. + The global value in the mock is the value itself. +- **`ReadGlobalPointer(name)`**: reads a pointer stored at the global address. + The global value in the mock is the *address* of a memory fragment containing + the actual pointer value. +- **Double indirection** (`ReadPointer(ReadGlobalPointer(name))`): the global + points to memory that contains a pointer to the actual data. diff --git a/src/native/managed/cdac/tests/SOSDacInterface8Tests.cs b/src/native/managed/cdac/tests/SOSDacInterface8Tests.cs new file mode 100644 index 00000000000000..d003f52066e47b --- /dev/null +++ b/src/native/managed/cdac/tests/SOSDacInterface8Tests.cs @@ -0,0 +1,238 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Diagnostics.DataContractReader.Legacy; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.Tests; + +public unsafe class SOSDacInterface8Tests +{ + private const int S_OK = 0; + private const int S_FALSE = 1; + + private static readonly GCHeapBuilder.GenerationInput[] s_generations = + [ + new() { StartSegment = 0x1A00_0000, AllocationStart = 0x1A00_1000, AllocContextPointer = 0x1A00_2000, AllocContextLimit = 0x1A00_3000 }, + new() { StartSegment = 0x1B00_0000, AllocationStart = 0x1B00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + new() { StartSegment = 0x1C00_0000, AllocationStart = 0x1C00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + new() { StartSegment = 0x1D00_0000, AllocationStart = 0x1D00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + ]; + + private static readonly ulong[] s_fillPointers = [0x1111, 0x2222, 0x3333, 0x4444, 0x5555, 0x6666, 0x7777]; + + private static ISOSDacInterface8 CreateWksDac8(MockTarget.Architecture arch) + { + return new SOSDacImpl( + new TestPlaceholderTarget.Builder(arch) + .AddGCHeapWks(gc => gc + .SetGenerations(s_generations) + .SetFillPointers(s_fillPointers)) + .Build(), + legacyObj: null); + } + + private static ISOSDacInterface8 CreateSvrDac8(MockTarget.Architecture arch, out ulong heapAddr) + { + return new SOSDacImpl( + new TestPlaceholderTarget.Builder(arch) + .AddGCHeapSvr(gc => gc + .SetGenerations(s_generations) + .SetFillPointers(s_fillPointers), out heapAddr) + .Build(), + legacyObj: null); + } + + private static ulong SignExtend(ulong value, MockTarget.Architecture arch) + { + if (arch.Is64Bit) + return value; + + return (ulong)(long)(int)(uint)value; + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetNumberGenerations_ReturnsCorrectCount(MockTarget.Architecture arch) + { + ISOSDacInterface8 dac8 = CreateWksDac8(arch); + + uint numGenerations; + int hr = dac8.GetNumberGenerations(&numGenerations); + Assert.Equal(S_OK, hr); + Assert.Equal((uint)s_generations.Length, numGenerations); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetNumberGenerations_WithFiveGenerations(MockTarget.Architecture arch) + { + var fiveGenerations = new GCHeapBuilder.GenerationInput[] + { + new() { StartSegment = 0xA000_0000, AllocationStart = 0xA000_1000, AllocContextPointer = 0xA000_2000, AllocContextLimit = 0xA000_3000 }, + new() { StartSegment = 0xB000_0000, AllocationStart = 0xB000_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + new() { StartSegment = 0xC000_0000, AllocationStart = 0xC000_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + new() { StartSegment = 0xD000_0000, AllocationStart = 0xD000_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + new() { StartSegment = 0xE000_0000, AllocationStart = 0xE000_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + }; + + ISOSDacInterface8 dac8 = new SOSDacImpl( + new TestPlaceholderTarget.Builder(arch) + .AddGCHeapWks(gc => gc + .SetGenerations(fiveGenerations) + .SetFillPointers(s_fillPointers)) + .Build(), + legacyObj: null); + + uint numGenerations; + int hr = dac8.GetNumberGenerations(&numGenerations); + Assert.Equal(S_OK, hr); + Assert.Equal(5u, numGenerations); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetGenerationTable_ReturnsCorrectData(MockTarget.Architecture arch) + { + ISOSDacInterface8 dac8 = CreateWksDac8(arch); + + uint needed; + int hr = dac8.GetGenerationTable(0, null, &needed); + Assert.Equal(S_FALSE, hr); + Assert.Equal((uint)s_generations.Length, needed); + + DacpGenerationData* genData = stackalloc DacpGenerationData[(int)needed]; + hr = dac8.GetGenerationTable(needed, genData, &needed); + Assert.Equal(S_OK, hr); + + for (int i = 0; i < s_generations.Length; i++) + { + Assert.Equal(SignExtend(s_generations[i].StartSegment, arch), (ulong)genData[i].start_segment); + Assert.Equal(SignExtend(s_generations[i].AllocationStart, arch), (ulong)genData[i].allocation_start); + Assert.Equal(SignExtend(s_generations[i].AllocContextPointer, arch), (ulong)genData[i].allocContextPtr); + Assert.Equal(SignExtend(s_generations[i].AllocContextLimit, arch), (ulong)genData[i].allocContextLimit); + } + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetGenerationTable_InsufficientBuffer_ReturnsSFalseAndNeededCount(MockTarget.Architecture arch) + { + ISOSDacInterface8 dac8 = CreateWksDac8(arch); + + uint needed; + DacpGenerationData* smallBuffer = stackalloc DacpGenerationData[2]; + int hr = dac8.GetGenerationTable(2, smallBuffer, &needed); + + Assert.Equal(S_FALSE, hr); + Assert.Equal((uint)s_generations.Length, needed); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetFinalizationFillPointers_ReturnsCorrectData(MockTarget.Architecture arch) + { + ISOSDacInterface8 dac8 = CreateWksDac8(arch); + + uint needed; + int hr = dac8.GetFinalizationFillPointers(0, null, &needed); + Assert.Equal(S_FALSE, hr); + Assert.Equal((uint)s_fillPointers.Length, needed); + + ClrDataAddress* ptrs = stackalloc ClrDataAddress[(int)needed]; + hr = dac8.GetFinalizationFillPointers(needed, ptrs, &needed); + Assert.Equal(S_OK, hr); + + for (int i = 0; i < s_fillPointers.Length; i++) + { + Assert.Equal(SignExtend(s_fillPointers[i], arch), (ulong)ptrs[i]); + } + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetFinalizationFillPointers_InsufficientBuffer_ReturnsSFalseAndNeededCount(MockTarget.Architecture arch) + { + ISOSDacInterface8 dac8 = CreateWksDac8(arch); + + uint needed; + ClrDataAddress* smallBuffer = stackalloc ClrDataAddress[3]; + int hr = dac8.GetFinalizationFillPointers(3, smallBuffer, &needed); + + Assert.Equal(S_FALSE, hr); + Assert.Equal((uint)s_fillPointers.Length, needed); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetGenerationTableSvr_ReturnsCorrectData(MockTarget.Architecture arch) + { + ISOSDacInterface8 dac8 = CreateSvrDac8(arch, out ulong heapAddr); + + uint needed; + int hr = dac8.GetGenerationTableSvr((ClrDataAddress)heapAddr, 0, null, &needed); + Assert.Equal(S_FALSE, hr); + Assert.Equal((uint)s_generations.Length, needed); + + DacpGenerationData* genData = stackalloc DacpGenerationData[(int)needed]; + hr = dac8.GetGenerationTableSvr((ClrDataAddress)heapAddr, needed, genData, &needed); + Assert.Equal(S_OK, hr); + + for (int i = 0; i < s_generations.Length; i++) + { + Assert.Equal(SignExtend(s_generations[i].StartSegment, arch), (ulong)genData[i].start_segment); + Assert.Equal(SignExtend(s_generations[i].AllocationStart, arch), (ulong)genData[i].allocation_start); + Assert.Equal(SignExtend(s_generations[i].AllocContextPointer, arch), (ulong)genData[i].allocContextPtr); + Assert.Equal(SignExtend(s_generations[i].AllocContextLimit, arch), (ulong)genData[i].allocContextLimit); + } + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetGenerationTableSvr_InsufficientBuffer_ReturnsSFalseAndNeededCount(MockTarget.Architecture arch) + { + ISOSDacInterface8 dac8 = CreateSvrDac8(arch, out ulong heapAddr); + + uint needed; + DacpGenerationData* smallBuffer = stackalloc DacpGenerationData[2]; + int hr = dac8.GetGenerationTableSvr((ClrDataAddress)heapAddr, 2, smallBuffer, &needed); + + Assert.Equal(S_FALSE, hr); + Assert.Equal((uint)s_generations.Length, needed); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetFinalizationFillPointersSvr_ReturnsCorrectData(MockTarget.Architecture arch) + { + ISOSDacInterface8 dac8 = CreateSvrDac8(arch, out ulong heapAddr); + + uint needed; + int hr = dac8.GetFinalizationFillPointersSvr((ClrDataAddress)heapAddr, 0, null, &needed); + Assert.Equal(S_FALSE, hr); + Assert.Equal((uint)s_fillPointers.Length, needed); + + ClrDataAddress* ptrs = stackalloc ClrDataAddress[(int)needed]; + hr = dac8.GetFinalizationFillPointersSvr((ClrDataAddress)heapAddr, needed, ptrs, &needed); + Assert.Equal(S_OK, hr); + + for (int i = 0; i < s_fillPointers.Length; i++) + { + Assert.Equal(SignExtend(s_fillPointers[i], arch), (ulong)ptrs[i]); + } + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetFinalizationFillPointersSvr_InsufficientBuffer_ReturnsSFalseAndNeededCount(MockTarget.Architecture arch) + { + ISOSDacInterface8 dac8 = CreateSvrDac8(arch, out ulong heapAddr); + + uint needed; + ClrDataAddress* smallBuffer = stackalloc ClrDataAddress[3]; + int hr = dac8.GetFinalizationFillPointersSvr((ClrDataAddress)heapAddr, 3, smallBuffer, &needed); + + Assert.Equal(S_FALSE, hr); + Assert.Equal((uint)s_fillPointers.Length, needed); + } +} diff --git a/src/native/managed/cdac/tests/TestPlaceholderTarget.cs b/src/native/managed/cdac/tests/TestPlaceholderTarget.cs index f503c9eeb5adc1..3b7595e1ccc129 100644 --- a/src/native/managed/cdac/tests/TestPlaceholderTarget.cs +++ b/src/native/managed/cdac/tests/TestPlaceholderTarget.cs @@ -7,7 +7,7 @@ using System.Numerics; using System.Runtime.CompilerServices; using System.Text; -using Moq; +using Microsoft.Diagnostics.DataContractReader.Contracts; namespace Microsoft.Diagnostics.DataContractReader.Tests; @@ -30,7 +30,7 @@ public TestPlaceholderTarget(MockTarget.Architecture arch, ReadFromTargetDelegat { IsLittleEndian = arch.IsLittleEndian; PointerSize = arch.Is64Bit ? 8 : 4; - _contractRegistry = new Mock().Object; + _contractRegistry = new TestContractRegistry(); _dataCache = new DefaultDataCache(this); _typeInfoCache = types ?? []; _dataReader = reader; @@ -43,6 +43,71 @@ internal void SetContracts(ContractRegistry contracts) _contractRegistry = contracts; } + /// + /// Fluent builder for . Accumulates types, + /// globals, and contract factories from mock descriptors, then materializes the + /// target and wires contracts in . + /// + internal class Builder + { + private readonly MockTarget.Architecture _arch; + private readonly MockMemorySpace.Builder _memBuilder; + private readonly Dictionary _types = new(); + private readonly List<(string Name, ulong Value)> _globals = new(); + private readonly List<(string Name, string Value)> _globalStrings = new(); + private readonly List<(Type Type, Func Factory)> _contractFactories = new(); + + public Builder(MockTarget.Architecture arch) + { + _arch = arch; + _memBuilder = new MockMemorySpace.Builder(new TargetTestHelpers(arch)); + } + + internal MockMemorySpace.Builder MemoryBuilder => _memBuilder; + + public Builder AddTypes(Dictionary types) + { + foreach (var kvp in types) + _types[kvp.Key] = kvp.Value; + return this; + } + + public Builder AddGlobals(params (string Name, ulong Value)[] globals) + { + _globals.AddRange(globals); + return this; + } + + public Builder AddGlobalStrings(params (string Name, string Value)[] globalStrings) + { + _globalStrings.AddRange(globalStrings); + return this; + } + + public Builder AddContract(Func factory) where TContract : IContract + { + _contractFactories.Add((typeof(TContract), target => factory(target))); + return this; + } + + public TestPlaceholderTarget Build() + { + var target = new TestPlaceholderTarget( + _arch, + _memBuilder.GetMemoryContext().ReadFromTarget, + _types, + _globals.ToArray(), + _globalStrings.ToArray()); + + var registry = new TestContractRegistry(); + foreach (var (type, factory) in _contractFactories) + registry.Add(type, new Lazy(() => factory(target))); + target.SetContracts(registry); + + return target; + } + } + public override int PointerSize { get; } public override bool IsLittleEndian { get; } @@ -372,4 +437,19 @@ public void Clear() } } + private sealed class TestContractRegistry : ContractRegistry + { + private readonly Dictionary> _contracts = new(); + + public void Add(Type type, Lazy contract) => _contracts[type] = contract; + + public override TContract GetContract() + { + if (_contracts.TryGetValue(typeof(TContract), out var lazy)) + return (TContract)lazy.Value; + + throw new NotImplementedException($"Contract {typeof(TContract).Name} is not registered."); + } + } + }