diff --git a/docs/design/datacontracts/ExecutionManager.md b/docs/design/datacontracts/ExecutionManager.md index 1b7af30c943748..d595006d988041 100644 --- a/docs/design/datacontracts/ExecutionManager.md +++ b/docs/design/datacontracts/ExecutionManager.md @@ -164,6 +164,7 @@ Within a range section fragment, a [nibble map](#nibblemap) structure is used to | `Bucket` | `Keys` | `pointer` | Array of keys of `HashMapSlotsPerBucket` length | | `Bucket` | `Values` | `pointer` | Array of values of `HashMapSlotsPerBucket` length | | `CodeHeap` | `HeapType` | `uint8` | `uint8` discriminant identifying the concrete heap type | +| `CodeHeapListNode` | `CLRPersonalityRoutine` | `pointer` | Address of the CLR personality routine; when non-null, this is the module base for a Windows dynamic function table | | `CodeHeapListNode` | `EndAddress` | `pointer` | End address of the used portion of the code heap | | `CodeHeapListNode` | `HeaderMap` | `pointer` | Bit array used to find the start of methods - relative to `MapBase` | | `CodeHeapListNode` | `Heap` | `pointer` | Pointer to the `CodeHeap` object managed by this node | @@ -171,6 +172,8 @@ Within a range section fragment, a [nibble map](#nibblemap) structure is used to | `CodeHeapListNode` | `Next` | `pointer` | Next node | | `CodeHeapListNode` | `StartAddress` | `pointer` | Start address of the used portion of the code heap | | `CodeRangeMapRangeList` | `RangeListType` | `int32` | Integer identifying the stub code block kind for this range list | +| `DynamicFunctionTable` | `Context` | `pointer` | Tagged pointer to the owning `EEJitManager`; low bits are flags | +| `DynamicFunctionTable` | `MinimumAddress` | `pointer` | Module base address covered by the dynamic function table | | `EEExceptionClause` | *(type size)* | `uint32` | Size of an exception clause in bytes | | `EEExceptionClause` | `Flags` | `uint32` | Exception clause flags (`COR_ILEXCEPTION_CLAUSE_*` bit flags) | | `EEExceptionClause` | `HandlerEndPC` | `uint32` | Native offset of the end of the handler | @@ -610,6 +613,19 @@ IEnumerable IExecutionManager.GetCodeHeapInfos() } ``` +### Dynamic Function Table Entries + +`GetDynamicFunctionTableEntries` returns the target addresses of the +`RUNTIME_FUNCTION` records for a Windows dynamic function table. These records describe +the unwind information for dynamically generated JIT code and are consumed by +out-of-process unwinders. + +`tableAddress` identifies a target `DYNAMIC_FUNCTION_TABLE`. Its `Context` identifies +the owning `EEJitManager`, and its `MinimumAddress` identifies the associated code heap. +The result contains the entries for that heap, ordered by descending method start address +and then by ascending entry address within a method. If the table does not identify a +known code heap, or if the target is not Windows non-x86, the result is empty. + ### RangeSectionMap The range section map logically partitions the entire 32-bit or 64-bit addressable space into chunks. diff --git a/docs/design/datacontracts/data-descriptor-meanings.json b/docs/design/datacontracts/data-descriptor-meanings.json index 449c132ac5f0dc..dee81caa0f4a0f 100644 --- a/docs/design/datacontracts/data-descriptor-meanings.json +++ b/docs/design/datacontracts/data-descriptor-meanings.json @@ -49,6 +49,7 @@ "CMiniMdSchema.RecordCounts": "Address of the inline per-table row count array", "CMiniMdSchema.Sorted": "Sorted-table bit mask", "CodeHeap.HeapType": "`uint8` discriminant identifying the concrete heap type", + "CodeHeapListNode.CLRPersonalityRoutine": "Address of the CLR personality routine; when non-null, this is the module base for a Windows dynamic function table", "CodeHeapListNode.EndAddress": "End address of the used portion of the code heap", "CodeHeapListNode.HeaderMap": "Bit array used to find the start of methods - relative to `MapBase`", "CodeHeapListNode.Heap": "Pointer to the `CodeHeap` object managed by this node", @@ -92,6 +93,8 @@ "Delegate.MethodPtrAux": "Auxiliary method pointer", "Delegate.Target": "Bound `this` reference for closed delegates", "DynamicHelperFrame.DynamicHelperFrameFlags": "Flags indicating which argument registers contain GC references", + "DynamicFunctionTable.Context": "Tagged pointer to the owning `EEJitManager`; low bits are flags", + "DynamicFunctionTable.MinimumAddress": "Module base address covered by the dynamic function table", "DynamicILBlobTable.EntryIL": "Offset of the IL pointer within each dynamic IL table entry", "DynamicILBlobTable.EntryMethodToken": "Offset of the method token within each dynamic IL table entry", "DynamicILBlobTable.Size": "Size in bytes of each table entry", diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index 3e68f245ad45a5..ba955009bf5978 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -1135,8 +1135,19 @@ CDAC_TYPE_FIELD(CodeHeapListNode, T_POINTER, EndAddress, offsetof(HeapList, endA CDAC_TYPE_FIELD(CodeHeapListNode, T_POINTER, MapBase, offsetof(HeapList, mapBase)) CDAC_TYPE_FIELD(CodeHeapListNode, T_POINTER, HeaderMap, offsetof(HeapList, pHdrMap)) CDAC_TYPE_FIELD(CodeHeapListNode, T_POINTER, Heap, offsetof(HeapList, pHeap)) +#ifdef TARGET_64BIT +CDAC_TYPE_FIELD(CodeHeapListNode, T_POINTER, CLRPersonalityRoutine, offsetof(HeapList, CLRPersonalityRoutine)) +#endif CDAC_TYPE_END(CodeHeapListNode) +#if !defined(TARGET_UNIX) && !defined(TARGET_X86) +CDAC_TYPE_BEGIN(DynamicFunctionTable) +CDAC_TYPE_INDETERMINATE(DynamicFunctionTable) +CDAC_TYPE_FIELD(DynamicFunctionTable, T_POINTER, MinimumAddress, offsetof(DYNAMIC_FUNCTION_TABLE, MinimumAddress)) +CDAC_TYPE_FIELD(DynamicFunctionTable, T_POINTER, Context, offsetof(DYNAMIC_FUNCTION_TABLE, Context)) +CDAC_TYPE_END(DynamicFunctionTable) +#endif // !defined(TARGET_UNIX) && !defined(TARGET_X86) + CDAC_TYPE_BEGIN(CodeHeap) CDAC_TYPE_INDETERMINATE(CodeHeap) CDAC_TYPE_FIELD(CodeHeap, T_UINT8, HeapType, cdac_data::HeapType) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IExecutionManager.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IExecutionManager.cs index d7c65a9f06d788..ef0a13cbce4d7b 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IExecutionManager.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IExecutionManager.cs @@ -114,6 +114,7 @@ public interface IExecutionManager : IContract uint GetStackParameterSize(CodeBlockHandle codeInfoHandle) => throw new NotImplementedException(); JitManagerInfo GetEEJitManagerInfo() => throw new NotImplementedException(); IEnumerable GetCodeHeapInfos() => throw new NotImplementedException(); + IReadOnlyList GetDynamicFunctionTableEntries(TargetPointer tableAddress) => throw new NotImplementedException(); // Classify a code address as a known stub kind (precode, jump stub, VSD stub, etc.) // or as managed code. Returns Unknown if the address is not recognized. CodeKind GetCodeKind(TargetCodePointer codeAddress) => throw new NotImplementedException(); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.EEJitManager.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.EEJitManager.cs index b21c9331dc9382..0106c510b87b70 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.EEJitManager.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.EEJitManager.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using Microsoft.Diagnostics.DataContractReader.ExecutionManagerHelpers; @@ -159,6 +160,43 @@ private TargetPointer FindMethodCode(RangeSection rangeSection, TargetCodePointe return _nibbleMap.FindMethodCode(heapListNode, codeAddress); } + public List EnumerateFunctionTableEntries(Data.CodeHeapListNode heapListNode) + { + // Port of the reverse code-header walk in OutOfProcessFunctionTableCallbackEx. Starting from + // the end of the used portion of the code heap, walk backwards through the nibble map to visit + // each method, skip stub code blocks, and collect the RUNTIME_FUNCTION entries of the real code + // headers. Entries are ordered by descending method start address, ascending within a method. + uint runtimeFunctionSize = Target.GetTypeInfo(DataType.RuntimeFunction).Size!.Value; + List entries = []; + + TargetCodePointer current = new(heapListNode.EndAddress.Value); + while (true) + { + TargetPointer codeStart = _nibbleMap.FindMethodCode(heapListNode, current); + if (codeStart == TargetPointer.Null) + break; + + // The real code header pointer is stored immediately before the code start. + TargetPointer codeHeaderIndirect = codeStart - (ulong)Target.PointerSize; + TargetPointer codeHeaderAddress = Target.ReadPointer(codeHeaderIndirect); + + // Only real code headers (not stub code blocks) contribute unwind info entries. + if (!RangeSection.IsStubCodeBlock(Target, codeHeaderAddress)) + { + Data.RealCodeHeader realCodeHeader = Target.ProcessedData.GetOrAdd(codeHeaderAddress); + for (uint i = 0; i < realCodeHeader.NumUnwindInfos; i++) + entries.Add(realCodeHeader.UnwindInfos + (ulong)(i * runtimeFunctionSize)); + } + + if (codeStart.Value <= heapListNode.StartAddress.Value) + break; + + current = new TargetCodePointer(codeStart.Value - 1); + } + + return entries; + } + private TargetPointer GetCodeHeaderAddress(RangeSection rangeSection, TargetPointer codeStart) { // EEJitManager::JitCodeToMethodInfo diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs index cd78199b015e5e..8c8f933f948e62 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs @@ -496,6 +496,45 @@ IEnumerable IExecutionManager.GetCodeHeapInfos() } } + IReadOnlyList IExecutionManager.GetDynamicFunctionTableEntries(TargetPointer tableAddress) + { + IRuntimeInfo runtimeInfo = _target.Contracts.RuntimeInfo; + if (runtimeInfo.GetTargetOperatingSystem() != RuntimeInfoOperatingSystem.Windows || + runtimeInfo.GetTargetArchitecture() == RuntimeInfoArchitecture.X86) + { + return []; + } + + // Port of the DAC's OutOfProcessFunctionTableCallbackEx (see vm/../debug/daccess/fntableaccess.cpp). + // The dynamic-function-table header identifies both the owning JIT manager (via its Context) and + // the module base (via MinimumAddress) of the code heap whose function table is requested. + Data.DynamicFunctionTable table = _target.ProcessedData.GetOrAdd(tableAddress); + + // The low bits of Context are flags; the remaining bits point at the owning EEJitManager. + TargetPointer jitManagerAddress = new(table.Context.Value & ~(ulong)3); + TargetPointer minimumAddress = table.MinimumAddress; + + Data.EEJitManager jitManager = _target.ProcessedData.GetOrAdd(jitManagerAddress); + + TargetPointer nodeAddr = jitManager.AllCodeHeaps; + while (nodeAddr != TargetPointer.Null) + { + Data.CodeHeapListNode node = _target.ProcessedData.GetOrAdd(nodeAddr); + + // HeapList::GetModuleBase - the personality routine on 64-bit targets when set, + // otherwise the map base. This matches the value used to register the function table. + TargetPointer moduleBase = node.CLRPersonalityRoutine is { } personalityRoutine && personalityRoutine != TargetPointer.Null + ? personalityRoutine + : node.MapBase; + if (moduleBase == minimumAddress) + return _eeJitManager.EnumerateFunctionTableEntries(node); + + nodeAddr = node.Next; + } + + return []; + } + private RangeSection RangeSectionFromCodeBlockHandle(CodeBlockHandle codeInfoHandle) { if (!_codeInfos.TryGetValue(codeInfoHandle.Address, out CodeBlock? info)) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManager_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManager_1.cs index 36750401adaf4f..8da20d8714bb89 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManager_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManager_1.cs @@ -34,6 +34,7 @@ internal ExecutionManager_1(Target target) public List GetExceptionClauses(CodeBlockHandle codeInfoHandle) => _executionManagerCore.GetExceptionClauses(codeInfoHandle); public JitManagerInfo GetEEJitManagerInfo() => _executionManagerCore.GetEEJitManagerInfo(); public IEnumerable GetCodeHeapInfos() => _executionManagerCore.GetCodeHeapInfos(); + public IReadOnlyList GetDynamicFunctionTableEntries(TargetPointer tableAddress) => _executionManagerCore.GetDynamicFunctionTableEntries(tableAddress); public CodeKind GetCodeKind(TargetCodePointer codeAddress) => _executionManagerCore.GetCodeKind(codeAddress); public TargetPointer FindReadyToRunModule(TargetPointer address) => _executionManagerCore.FindReadyToRunModule(address); public void Flush(FlushScope scope) => _executionManagerCore.Flush(scope); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManager_2.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManager_2.cs index 378829ac1adadc..648dcef5419ee8 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManager_2.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManager_2.cs @@ -34,6 +34,7 @@ internal ExecutionManager_2(Target target) public List GetExceptionClauses(CodeBlockHandle codeInfoHandle) => _executionManagerCore.GetExceptionClauses(codeInfoHandle); public JitManagerInfo GetEEJitManagerInfo() => _executionManagerCore.GetEEJitManagerInfo(); public IEnumerable GetCodeHeapInfos() => _executionManagerCore.GetCodeHeapInfos(); + public IReadOnlyList GetDynamicFunctionTableEntries(TargetPointer tableAddress) => _executionManagerCore.GetDynamicFunctionTableEntries(tableAddress); public CodeKind GetCodeKind(TargetCodePointer codeAddress) => _executionManagerCore.GetCodeKind(codeAddress); public TargetPointer FindReadyToRunModule(TargetPointer address) => _executionManagerCore.FindReadyToRunModule(address); public void Flush(FlushScope scope) => _executionManagerCore.Flush(scope); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/CodeHeapListNode.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/CodeHeapListNode.cs index 185ca8683932c7..2d78950d01f812 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/CodeHeapListNode.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/CodeHeapListNode.cs @@ -12,4 +12,8 @@ internal sealed partial class CodeHeapListNode : IData [Field] public partial TargetPointer MapBase { get; } [Field] public partial TargetPointer HeaderMap { get; } [Field] public partial TargetPointer Heap { get; } + + // 64-bit only: jump thunk to the personality routine. Used as the module base + // when matching a dynamic function table's minimum address. + [Field] public partial TargetPointer? CLRPersonalityRoutine { get; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/DynamicFunctionTable.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/DynamicFunctionTable.cs new file mode 100644 index 00000000000000..de459c4f391080 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/DynamicFunctionTable.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.DataContractReader.Data; + +[CdacType(nameof(DataType.DynamicFunctionTable))] +internal sealed partial class DynamicFunctionTable : IData +{ + [Field] public partial TargetPointer MinimumAddress { get; } + [Field] public partial TargetPointer Context { get; } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs index a3fbe6bc8326e2..be5194e0a537a6 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs @@ -135,6 +135,7 @@ public enum DataType CodeHeap, LoaderCodeHeap, HostCodeHeap, + DynamicFunctionTable, MethodDescVersioningState, ILCodeVersioningState, NativeCodeVersionNode, 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 31ec7f6f4d91f5..07da4638d817ea 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 @@ -39,7 +39,43 @@ int IXCLRDataProcess3.GetFunctionTable( *bytesNeeded = 0; *entries = 0; - return HResults.E_NOTIMPL; + try + { + IExecutionManager executionManager = _target.Contracts.ExecutionManager; + IReadOnlyList functionEntries = + executionManager.GetDynamicFunctionTableEntries(tableAddress.ToTargetPointer(_target)); + + uint runtimeFunctionSize = _target.GetTypeInfo(DataType.RuntimeFunction).Size!.Value; + uint count = (uint)functionEntries.Count; + ulong totalBytes = (ulong)count * runtimeFunctionSize; + if (totalBytes > uint.MaxValue) + return HResults.E_FAIL; + + *entries = count; + *bytesNeeded = (uint)totalBytes; + + // An empty or unmatched table reports zero entries and succeeds. + if (count == 0) + return HResults.S_OK; + + // A size query (null buffer) or a buffer that is too small writes nothing and reports + // the required sizes so the caller can retry with adequate storage. + if (buffer is null || bufferSize < totalBytes) + return HResults.S_FALSE; + + int entrySize = checked((int)runtimeFunctionSize); + for (int i = 0; i < functionEntries.Count; i++) + { + Span destination = new(buffer + ((nint)i * entrySize), entrySize); + _target.ReadBuffer(functionEntries[i].Value, destination); + } + + return HResults.S_OK; + } + catch (System.Exception ex) + { + return ex.HResult; + } } int IXCLRDataProcess.Flush() diff --git a/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs b/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs index 8f0ef0769f5198..2b61bfd9a789ed 100644 --- a/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs @@ -13,7 +13,7 @@ namespace Microsoft.Diagnostics.DataContractReader.Tests.ExecutionManager; public class ExecutionManagerTests { - private static Dictionary CreateContractTypes(MockExecutionManagerBuilder emBuilder) + internal static Dictionary CreateContractTypes(MockExecutionManagerBuilder emBuilder) { TargetTestHelpers helpers = emBuilder.Builder.TargetTestHelpers; var types = new Dictionary @@ -29,6 +29,7 @@ public class ExecutionManagerTests [DataType.InterpreterRealCodeHeader] = TargetTestHelpers.CreateTypeInfo(emBuilder.InterpreterRealCodeHeaderLayout), [DataType.ReadyToRunInfo] = TargetTestHelpers.CreateTypeInfo(emBuilder.ReadyToRunInfoLayout), [DataType.EEJitManager] = TargetTestHelpers.CreateTypeInfo(emBuilder.EEJitManagerLayout), + [DataType.DynamicFunctionTable] = TargetTestHelpers.CreateTypeInfo(emBuilder.DynamicFunctionTableLayout), [DataType.Module] = TargetTestHelpers.CreateTypeInfo(emBuilder.ModuleLayout), [DataType.CodeRangeMapRangeList] = TargetTestHelpers.CreateTypeInfo(emBuilder.CodeRangeMapRangeListLayout), [DataType.ImageDataDirectory] = TargetTestHelpers.CreateTypeInfo(emBuilder.ImageDataDirectoryLayout), @@ -42,11 +43,39 @@ public class ExecutionManagerTests return types; } - private static Target CreateTarget( + internal static Target CreateTarget( MockExecutionManagerBuilder emBuilder, params (string Name, ulong Value)[] additionalGlobals) + => CreateTarget( + emBuilder, + RuntimeInfoOperatingSystem.Windows, + targetArchitecture: null, + additionalGlobals); + + internal static Target CreateTarget( + MockExecutionManagerBuilder emBuilder, + RuntimeInfoOperatingSystem operatingSystem = RuntimeInfoOperatingSystem.Windows, + RuntimeInfoArchitecture? targetArchitecture = null) + => CreateTarget( + emBuilder, + operatingSystem, + targetArchitecture, + []); + + private static Target CreateTarget( + MockExecutionManagerBuilder emBuilder, + RuntimeInfoOperatingSystem operatingSystem, + RuntimeInfoArchitecture? targetArchitecture, + (string Name, ulong Value)[] additionalGlobals) { var arch = emBuilder.Builder.TargetTestHelpers.Arch; + RuntimeInfoArchitecture architecture = targetArchitecture ?? (arch.Is64Bit + ? RuntimeInfoArchitecture.X64 + : RuntimeInfoArchitecture.Arm); + Mock runtimeInfo = new(); + runtimeInfo.Setup(r => r.GetTargetOperatingSystem()).Returns(operatingSystem); + runtimeInfo.Setup(r => r.GetTargetArchitecture()).Returns(architecture); + return new TestPlaceholderTarget.Builder(arch) .UseReader(emBuilder.Builder.GetMemoryContext().ReadFromTarget) .AddTypes(CreateContractTypes(emBuilder)) @@ -54,6 +83,7 @@ private static Target CreateTarget( .AddGlobals(additionalGlobals) .AddContract(version: emBuilder.Version) .AddMockContract(Mock.Of()) + .AddMockContract(runtimeInfo) .Build(); } @@ -963,4 +993,199 @@ public void GetCodeBlockHandle_InterpreterPrecode_ReturnsNull(string version, Mo var eeInfo = em.GetCodeBlockHandle(precodeAddress); Assert.Null(eeInfo); } + + [Theory] + [MemberData(nameof(StdArchAllVersions))] + public void GetDynamicFunctionTableEntries_MultipleMethods(string version, MockTarget.Architecture arch) + { + const ulong codeRangeStart = 0x0a0a_0000u; + const uint codeRangeSize = 0xc000u; + const uint methodSize = 0x100; + const ulong personalityRoutine = 0x00cc_0000u; + + MockExecutionManagerBuilder emBuilder = new(version, arch, MockExecutionManagerBuilder.DefaultAllocationRange); + + MockExecutionManagerBuilder.JittedCodeRange jittedCode = emBuilder.AllocateJittedCodeRange(codeRangeStart, codeRangeSize); + NibbleMapTestBuilderBase nibBuilder = emBuilder.CreateNibbleMap(codeRangeStart, codeRangeSize); + + // Three methods with varying unwind info counts, allocated in ascending address order. + MockExecutionManagerBuilder.JittedMethodWithUnwindInfo m0 = emBuilder.AddJittedMethodWithUnwindInfo(jittedCode, methodSize, 0x0101_0000, [0x10]); + MockExecutionManagerBuilder.JittedMethodWithUnwindInfo m1 = emBuilder.AddJittedMethodWithUnwindInfo(jittedCode, methodSize, 0x0101_1000, [0x20, 0x40]); + MockExecutionManagerBuilder.JittedMethodWithUnwindInfo m2 = emBuilder.AddJittedMethodWithUnwindInfo(jittedCode, methodSize, 0x0101_2000, [0x80]); + + foreach (MockExecutionManagerBuilder.JittedMethodWithUnwindInfo m in new[] { m0, m1, m2 }) + nibBuilder.AllocateCodeChunk(new TargetCodePointer(m.CodeAddress), methodSize); + + ulong endAddress = m2.CodeAddress + methodSize; + ulong moduleBase = arch.Is64Bit ? personalityRoutine : codeRangeStart; + + MockCodeHeapListNode node = emBuilder.AddCodeHeapListNode( + next: 0, + startAddress: codeRangeStart, + endAddress: endAddress, + mapBase: codeRangeStart, + headerMap: nibBuilder.NibbleMapFragment.Address, + clrPersonalityRoutine: personalityRoutine); + emBuilder.SetAllCodeHeaps(node.Address); + + // Context carries flags in its low bits; the contract must strip them to find the JIT manager. + MockDynamicFunctionTable table = emBuilder.AddDynamicFunctionTable(moduleBase, emBuilder.EEJitManagerAddress | 2); + + Target target = CreateTarget(emBuilder); + IExecutionManager em = target.Contracts.ExecutionManager; + + uint rfSize = (uint)emBuilder.RuntimeFunctionLayout.Size; + IReadOnlyList entries = em.GetDynamicFunctionTableEntries(new TargetPointer(table.Address)); + + // Entries are ordered by descending method start address, ascending within each method. + List expected = []; + foreach (MockExecutionManagerBuilder.JittedMethodWithUnwindInfo m in new[] { m2, m1, m0 }) + { + for (uint i = 0; i < m.NumUnwindInfos; i++) + expected.Add(new TargetPointer(m.UnwindInfosAddress + i * rfSize)); + } + + Assert.Equal(expected, entries); + } + + [Theory] + [MemberData(nameof(StdArchAllVersions))] + public void GetDynamicFunctionTableEntries_SkipsStubCodeBlocks(string version, MockTarget.Architecture arch) + { + const ulong codeRangeStart = 0x0a0a_0000u; + const uint codeRangeSize = 0xc000u; + const uint methodSize = 0x100; + const ulong personalityRoutine = 0x00cc_0000u; + const int stubCodeBlockKind = 4; // STUB_CODE_BLOCK_STUBPRECODE (<= StubCodeBlockLast) + + MockExecutionManagerBuilder emBuilder = new(version, arch, MockExecutionManagerBuilder.DefaultAllocationRange); + + MockExecutionManagerBuilder.JittedCodeRange jittedCode = emBuilder.AllocateJittedCodeRange(codeRangeStart, codeRangeSize); + NibbleMapTestBuilderBase nibBuilder = emBuilder.CreateNibbleMap(codeRangeStart, codeRangeSize); + + MockExecutionManagerBuilder.JittedMethodWithUnwindInfo m0 = emBuilder.AddJittedMethodWithUnwindInfo(jittedCode, methodSize, 0x0101_0000, [0x10]); + MockJittedMethod stub = emBuilder.AddStubCodeBlock(jittedCode, methodSize, stubCodeBlockKind); + MockExecutionManagerBuilder.JittedMethodWithUnwindInfo m1 = emBuilder.AddJittedMethodWithUnwindInfo(jittedCode, methodSize, 0x0101_1000, [0x20]); + + nibBuilder.AllocateCodeChunk(new TargetCodePointer(m0.CodeAddress), methodSize); + nibBuilder.AllocateCodeChunk(new TargetCodePointer(stub.CodeAddress), methodSize); + nibBuilder.AllocateCodeChunk(new TargetCodePointer(m1.CodeAddress), methodSize); + + ulong endAddress = m1.CodeAddress + methodSize; + ulong moduleBase = arch.Is64Bit ? personalityRoutine : codeRangeStart; + + MockCodeHeapListNode node = emBuilder.AddCodeHeapListNode( + next: 0, + startAddress: codeRangeStart, + endAddress: endAddress, + mapBase: codeRangeStart, + headerMap: nibBuilder.NibbleMapFragment.Address, + clrPersonalityRoutine: personalityRoutine); + emBuilder.SetAllCodeHeaps(node.Address); + + MockDynamicFunctionTable table = emBuilder.AddDynamicFunctionTable(moduleBase, emBuilder.EEJitManagerAddress); + + Target target = CreateTarget(emBuilder); + IExecutionManager em = target.Contracts.ExecutionManager; + + uint rfSize = (uint)emBuilder.RuntimeFunctionLayout.Size; + IReadOnlyList entries = em.GetDynamicFunctionTableEntries(new TargetPointer(table.Address)); + + // The stub in the middle contributes no entries; only the two real methods do. + List expected = + [ + new TargetPointer(m1.UnwindInfosAddress), + new TargetPointer(m0.UnwindInfosAddress), + ]; + Assert.Equal(expected, entries); + } + + [Theory] + [MemberData(nameof(StdArchAllVersions))] + public void GetDynamicFunctionTableEntries_NoMatchingHeap_ReturnsEmpty(string version, MockTarget.Architecture arch) + { + const ulong codeRangeStart = 0x0a0a_0000u; + const uint codeRangeSize = 0xc000u; + const uint methodSize = 0x100; + const ulong personalityRoutine = 0x00cc_0000u; + + MockExecutionManagerBuilder emBuilder = new(version, arch, MockExecutionManagerBuilder.DefaultAllocationRange); + + MockExecutionManagerBuilder.JittedCodeRange jittedCode = emBuilder.AllocateJittedCodeRange(codeRangeStart, codeRangeSize); + NibbleMapTestBuilderBase nibBuilder = emBuilder.CreateNibbleMap(codeRangeStart, codeRangeSize); + MockExecutionManagerBuilder.JittedMethodWithUnwindInfo m0 = emBuilder.AddJittedMethodWithUnwindInfo(jittedCode, methodSize, 0x0101_0000, [0x10]); + nibBuilder.AllocateCodeChunk(new TargetCodePointer(m0.CodeAddress), methodSize); + + MockCodeHeapListNode node = emBuilder.AddCodeHeapListNode( + next: 0, + startAddress: codeRangeStart, + endAddress: m0.CodeAddress + methodSize, + mapBase: codeRangeStart, + headerMap: nibBuilder.NibbleMapFragment.Address, + clrPersonalityRoutine: personalityRoutine); + emBuilder.SetAllCodeHeaps(node.Address); + + // MinimumAddress matches no code heap's module base. + MockDynamicFunctionTable table = emBuilder.AddDynamicFunctionTable(0xdead_0000, emBuilder.EEJitManagerAddress); + + Target target = CreateTarget(emBuilder); + IExecutionManager em = target.Contracts.ExecutionManager; + + IReadOnlyList entries = em.GetDynamicFunctionTableEntries(new TargetPointer(table.Address)); + Assert.Empty(entries); + } + + [Theory] + [MemberData(nameof(StdArchAllVersions))] + public void GetDynamicFunctionTableEntries_NullPersonalityRoutine_FallsBackToMapBase(string version, MockTarget.Architecture arch) + { + // When the personality routine is null (e.g. an interpreter code heap on 64-bit, or any + // 32-bit heap where the field is absent), the module base falls back to the map base - + // matching HeapList::GetModuleBase and the value used to register the function table. + const ulong codeRangeStart = 0x0a0a_0000u; + const uint codeRangeSize = 0xc000u; + const uint methodSize = 0x100; + + MockExecutionManagerBuilder emBuilder = new(version, arch, MockExecutionManagerBuilder.DefaultAllocationRange); + + MockExecutionManagerBuilder.JittedCodeRange jittedCode = emBuilder.AllocateJittedCodeRange(codeRangeStart, codeRangeSize); + NibbleMapTestBuilderBase nibBuilder = emBuilder.CreateNibbleMap(codeRangeStart, codeRangeSize); + MockExecutionManagerBuilder.JittedMethodWithUnwindInfo m0 = emBuilder.AddJittedMethodWithUnwindInfo(jittedCode, methodSize, 0x0101_0000, [0x10]); + nibBuilder.AllocateCodeChunk(new TargetCodePointer(m0.CodeAddress), methodSize); + + // Explicit zero personality routine; module base must resolve to MapBase (codeRangeStart). + MockCodeHeapListNode node = emBuilder.AddCodeHeapListNode( + next: 0, + startAddress: codeRangeStart, + endAddress: m0.CodeAddress + methodSize, + mapBase: codeRangeStart, + headerMap: nibBuilder.NibbleMapFragment.Address, + clrPersonalityRoutine: 0); + emBuilder.SetAllCodeHeaps(node.Address); + + MockDynamicFunctionTable table = emBuilder.AddDynamicFunctionTable(codeRangeStart, emBuilder.EEJitManagerAddress); + + Target target = CreateTarget(emBuilder); + IExecutionManager em = target.Contracts.ExecutionManager; + + IReadOnlyList entries = em.GetDynamicFunctionTableEntries(new TargetPointer(table.Address)); + Assert.Equal([new TargetPointer(m0.UnwindInfosAddress)], entries); + } + + [Theory] + [InlineData(RuntimeInfoOperatingSystem.Unix, RuntimeInfoArchitecture.X64)] + [InlineData(RuntimeInfoOperatingSystem.Windows, RuntimeInfoArchitecture.X86)] + public void GetDynamicFunctionTableEntries_UnsupportedPlatform_ReturnsEmpty( + RuntimeInfoOperatingSystem operatingSystem, + RuntimeInfoArchitecture architecture) + { + MockTarget.Architecture targetArchitecture = new() { IsLittleEndian = true, Is64Bit = true }; + MockExecutionManagerBuilder emBuilder = new("c1", targetArchitecture, MockExecutionManagerBuilder.DefaultAllocationRange); + Target target = CreateTarget(emBuilder, operatingSystem, architecture); + + IReadOnlyList entries = + target.Contracts.ExecutionManager.GetDynamicFunctionTableEntries(new TargetPointer(0xdead_beef)); + + Assert.Empty(entries); + } } diff --git a/src/native/managed/cdac/tests/UnitTests/FunctionTableAccessTests.cs b/src/native/managed/cdac/tests/UnitTests/FunctionTableAccessTests.cs index 15cb7323460607..3de95c2c8ad9dc 100644 --- a/src/native/managed/cdac/tests/UnitTests/FunctionTableAccessTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/FunctionTableAccessTests.cs @@ -2,16 +2,91 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; using System.Runtime.InteropServices; using System.Runtime.InteropServices.Marshalling; using Microsoft.Diagnostics.DataContractReader.Legacy; using Microsoft.Diagnostics.DataContractReader.TestInfrastructure; +using Microsoft.Diagnostics.DataContractReader.Tests.ExecutionManager; using Xunit; namespace Microsoft.Diagnostics.DataContractReader.Tests; public unsafe class FunctionTableAccessTests { + public static IEnumerable StdArchAllVersions() + { + const int highestVersion = 2; + foreach (object[] arr in new MockTarget.StdArch()) + { + MockTarget.Architecture arch = (MockTarget.Architecture)arr[0]; + for (int version = 1; version <= highestVersion; version++) + { + yield return new object[] { $"c{version}", arch }; + } + } + } + + private sealed class FunctionTableScenario + { + public required Target Target { get; init; } + public required ulong TableAddress { get; init; } + public required IReadOnlyList ExpectedEntryAddresses { get; init; } + public required uint RuntimeFunctionSize { get; init; } + + public uint ExpectedBytes => (uint)(ExpectedEntryAddresses.Count * RuntimeFunctionSize); + } + + private static FunctionTableScenario BuildScenario(string version, MockTarget.Architecture arch) + { + const ulong codeRangeStart = 0x0a0a_0000u; + const uint codeRangeSize = 0xc000u; + const uint methodSize = 0x100; + const ulong personalityRoutine = 0x00cc_0000u; + + MockExecutionManagerBuilder emBuilder = new(version, arch, MockExecutionManagerBuilder.DefaultAllocationRange); + + MockExecutionManagerBuilder.JittedCodeRange jittedCode = emBuilder.AllocateJittedCodeRange(codeRangeStart, codeRangeSize); + NibbleMapTestBuilderBase nibBuilder = emBuilder.CreateNibbleMap(codeRangeStart, codeRangeSize); + + MockExecutionManagerBuilder.JittedMethodWithUnwindInfo m0 = emBuilder.AddJittedMethodWithUnwindInfo(jittedCode, methodSize, 0x0101_0000, [0x10]); + MockExecutionManagerBuilder.JittedMethodWithUnwindInfo m1 = emBuilder.AddJittedMethodWithUnwindInfo(jittedCode, methodSize, 0x0101_1000, [0x20, 0x40]); + + foreach (MockExecutionManagerBuilder.JittedMethodWithUnwindInfo m in new[] { m0, m1 }) + nibBuilder.AllocateCodeChunk(new TargetCodePointer(m.CodeAddress), methodSize); + + ulong moduleBase = arch.Is64Bit ? personalityRoutine : codeRangeStart; + + MockCodeHeapListNode node = emBuilder.AddCodeHeapListNode( + next: 0, + startAddress: codeRangeStart, + endAddress: m1.CodeAddress + methodSize, + mapBase: codeRangeStart, + headerMap: nibBuilder.NibbleMapFragment.Address, + clrPersonalityRoutine: personalityRoutine); + emBuilder.SetAllCodeHeaps(node.Address); + + MockDynamicFunctionTable table = emBuilder.AddDynamicFunctionTable(moduleBase, emBuilder.EEJitManagerAddress); + + uint rfSize = (uint)emBuilder.RuntimeFunctionLayout.Size; + + // Entries are returned in descending method start order, ascending within a method. + List expected = + [ + m1.UnwindInfosAddress, + m1.UnwindInfosAddress + rfSize, + m0.UnwindInfosAddress, + ]; + + return new FunctionTableScenario + { + Target = ExecutionManagerTests.CreateTarget(emBuilder), + TableAddress = table.Address, + ExpectedEntryAddresses = expected, + RuntimeFunctionSize = rfSize, + }; + } + [Theory] [ClassData(typeof(MockTarget.StdArch))] public void QueryInterfaceFromIXCLRDataProcess_ReturnsProcess3(MockTarget.Architecture arch) @@ -62,48 +137,6 @@ public void QueryInterfaceFromIXCLRDataProcess_ReturnsProcess3(MockTarget.Archit { Marshal.Release(identity); } - - IXCLRDataProcess3 process3Interface = - ComInterfaceMarshaller.ConvertToManaged((void*)process3)!; - uint bytesNeeded = uint.MaxValue; - uint entries = uint.MaxValue; - - hr = process3Interface.GetFunctionTable( - new ClrDataAddress(0), - 0, - null, - &bytesNeeded, - &entries); - - Assert.Equal(HResults.E_NOTIMPL, hr); - Assert.Equal(0u, bytesNeeded); - Assert.Equal(0u, entries); - - entries = uint.MaxValue; - hr = process3Interface.GetFunctionTable( - new ClrDataAddress(0), - 0, - null, - null, - &entries); - - Assert.Equal(HResults.E_POINTER, hr); - Assert.Equal(0u, entries); - - bytesNeeded = uint.MaxValue; - hr = process3Interface.GetFunctionTable( - new ClrDataAddress(0), - 0, - null, - &bytesNeeded, - null); - - Assert.Equal(HResults.E_POINTER, hr); - Assert.Equal(0u, bytesNeeded); - - Assert.Equal( - HResults.E_POINTER, - process3Interface.GetFunctionTable(new ClrDataAddress(0), 0, null, null, null)); } finally { @@ -115,4 +148,136 @@ public void QueryInterfaceFromIXCLRDataProcess_ReturnsProcess3(MockTarget.Archit ComInterfaceMarshaller.Free(process); } } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetFunctionTable_NullOutParameters_ReturnsEPointer(MockTarget.Architecture arch) + { + TestPlaceholderTarget target = new TestPlaceholderTarget.Builder(arch).Build(); + IXCLRDataProcess3 process3 = new SOSDacImpl(target, legacyObj: null); + + uint bytesNeeded = uint.MaxValue; + uint entries = uint.MaxValue; + + Assert.Equal(HResults.E_POINTER, process3.GetFunctionTable(new ClrDataAddress(0), 0, null, null, &entries)); + Assert.Equal(0u, entries); + + entries = uint.MaxValue; + Assert.Equal(HResults.E_POINTER, process3.GetFunctionTable(new ClrDataAddress(0), 0, null, &bytesNeeded, null)); + Assert.Equal(0u, bytesNeeded); + + Assert.Equal(HResults.E_POINTER, process3.GetFunctionTable(new ClrDataAddress(0), 0, null, null, null)); + } + + [Theory] + [MemberData(nameof(StdArchAllVersions))] + public void GetFunctionTable_SizeQuery_ReturnsRequiredSize(string version, MockTarget.Architecture arch) + { + FunctionTableScenario scenario = BuildScenario(version, arch); + IXCLRDataProcess3 process3 = new SOSDacImpl(scenario.Target, legacyObj: null); + + uint bytesNeeded = 0; + uint entries = 0; + int hr = process3.GetFunctionTable(new ClrDataAddress(scenario.TableAddress), 0, null, &bytesNeeded, &entries); + + Assert.Equal(HResults.S_FALSE, hr); + Assert.Equal((uint)scenario.ExpectedEntryAddresses.Count, entries); + Assert.Equal(scenario.ExpectedBytes, bytesNeeded); + } + + [Theory] + [MemberData(nameof(StdArchAllVersions))] + public void GetFunctionTable_BufferTooSmall_ReturnsSFalseAndWritesNothing(string version, MockTarget.Architecture arch) + { + FunctionTableScenario scenario = BuildScenario(version, arch); + IXCLRDataProcess3 process3 = new SOSDacImpl(scenario.Target, legacyObj: null); + + uint bytesNeeded = 0; + uint entries = 0; + byte[] buffer = new byte[scenario.RuntimeFunctionSize]; // room for only one entry + Array.Fill(buffer, (byte)0xcc); + + int hr; + fixed (byte* pBuffer = buffer) + { + hr = process3.GetFunctionTable(new ClrDataAddress(scenario.TableAddress), scenario.RuntimeFunctionSize, pBuffer, &bytesNeeded, &entries); + } + + Assert.Equal(HResults.S_FALSE, hr); + Assert.Equal((uint)scenario.ExpectedEntryAddresses.Count, entries); + Assert.Equal(scenario.ExpectedBytes, bytesNeeded); + Assert.All(buffer, b => Assert.Equal((byte)0xcc, b)); // nothing written + } + + [Theory] + [MemberData(nameof(StdArchAllVersions))] + public void GetFunctionTable_SufficientBuffer_WritesEntries(string version, MockTarget.Architecture arch) + { + FunctionTableScenario scenario = BuildScenario(version, arch); + IXCLRDataProcess3 process3 = new SOSDacImpl(scenario.Target, legacyObj: null); + + uint bytesNeeded = 0; + uint entries = 0; + byte[] buffer = new byte[scenario.ExpectedBytes]; + + int hr; + fixed (byte* pBuffer = buffer) + { + hr = process3.GetFunctionTable(new ClrDataAddress(scenario.TableAddress), (uint)buffer.Length, pBuffer, &bytesNeeded, &entries); + } + + Assert.Equal(HResults.S_OK, hr); + Assert.Equal((uint)scenario.ExpectedEntryAddresses.Count, entries); + Assert.Equal(scenario.ExpectedBytes, bytesNeeded); + + // The buffer must hold each RUNTIME_FUNCTION entry, in order, exactly as stored in the target. + byte[] expected = new byte[scenario.ExpectedBytes]; + for (int i = 0; i < scenario.ExpectedEntryAddresses.Count; i++) + { + scenario.Target.ReadBuffer( + scenario.ExpectedEntryAddresses[i], + expected.AsSpan(i * (int)scenario.RuntimeFunctionSize, (int)scenario.RuntimeFunctionSize)); + } + + Assert.Equal(expected, buffer); + } + + [Theory] + [MemberData(nameof(StdArchAllVersions))] + public void GetFunctionTable_UnmatchedTable_ReturnsSOkWithZeroEntries(string version, MockTarget.Architecture arch) + { + // Reuse the scenario but point at an address with no matching heap by building a fresh table. + const ulong codeRangeStart = 0x0a0a_0000u; + const uint codeRangeSize = 0xc000u; + const uint methodSize = 0x100; + const ulong personalityRoutine = 0x00cc_0000u; + + MockExecutionManagerBuilder emBuilder = new(version, arch, MockExecutionManagerBuilder.DefaultAllocationRange); + MockExecutionManagerBuilder.JittedCodeRange jittedCode = emBuilder.AllocateJittedCodeRange(codeRangeStart, codeRangeSize); + NibbleMapTestBuilderBase nibBuilder = emBuilder.CreateNibbleMap(codeRangeStart, codeRangeSize); + MockExecutionManagerBuilder.JittedMethodWithUnwindInfo m0 = emBuilder.AddJittedMethodWithUnwindInfo(jittedCode, methodSize, 0x0101_0000, [0x10]); + nibBuilder.AllocateCodeChunk(new TargetCodePointer(m0.CodeAddress), methodSize); + + MockCodeHeapListNode node = emBuilder.AddCodeHeapListNode( + next: 0, + startAddress: codeRangeStart, + endAddress: m0.CodeAddress + methodSize, + mapBase: codeRangeStart, + headerMap: nibBuilder.NibbleMapFragment.Address, + clrPersonalityRoutine: personalityRoutine); + emBuilder.SetAllCodeHeaps(node.Address); + + MockDynamicFunctionTable table = emBuilder.AddDynamicFunctionTable(0xbaad_0000, emBuilder.EEJitManagerAddress); + Target target = ExecutionManagerTests.CreateTarget(emBuilder); + + IXCLRDataProcess3 process3 = new SOSDacImpl(target, legacyObj: null); + + uint bytesNeeded = uint.MaxValue; + uint entries = uint.MaxValue; + int hr = process3.GetFunctionTable(new ClrDataAddress(table.Address), 0, null, &bytesNeeded, &entries); + + Assert.Equal(HResults.S_OK, hr); + Assert.Equal(0u, entries); + Assert.Equal(0u, bytesNeeded); + } } diff --git a/src/native/managed/cdac/tests/UnitTests/MockDescriptors/MockDescriptors.ExecutionManager.cs b/src/native/managed/cdac/tests/UnitTests/MockDescriptors/MockDescriptors.ExecutionManager.cs index 6fe24e8351b546..ceef50377468c8 100644 --- a/src/native/managed/cdac/tests/UnitTests/MockDescriptors/MockDescriptors.ExecutionManager.cs +++ b/src/native/managed/cdac/tests/UnitTests/MockDescriptors/MockDescriptors.ExecutionManager.cs @@ -181,16 +181,29 @@ internal sealed class MockCodeHeapListNode : TypedView private const string MapBaseFieldName = "MapBase"; private const string HeaderMapFieldName = "HeaderMap"; private const string HeapFieldName = "Heap"; + private const string CLRPersonalityRoutineFieldName = "CLRPersonalityRoutine"; public static Layout CreateLayout(MockTarget.Architecture architecture) - => new SequentialLayoutBuilder("CodeHeapListNode", architecture) + { + SequentialLayoutBuilder builder = new SequentialLayoutBuilder("CodeHeapListNode", architecture) .AddPointerField(NextFieldName) .AddPointerField(StartAddressFieldName) .AddPointerField(EndAddressFieldName) .AddPointerField(MapBaseFieldName) .AddPointerField(HeaderMapFieldName) - .AddPointerField(HeapFieldName) - .Build(); + .AddPointerField(HeapFieldName); + + // The personality routine only exists on 64-bit targets (see HeapList in codeman.h). + if (architecture.Is64Bit) + { + builder.AddPointerField(CLRPersonalityRoutineFieldName); + } + + return builder.Build(); + } + + public bool HasCLRPersonalityRoutine + => Array.Exists(Layout.Fields, static f => f.Name == CLRPersonalityRoutineFieldName); public ulong Next { @@ -227,6 +240,12 @@ public ulong Heap get => ReadPointerField(HeapFieldName); set => WritePointerField(HeapFieldName, value); } + + public ulong CLRPersonalityRoutine + { + get => ReadPointerField(CLRPersonalityRoutineFieldName); + set => WritePointerField(CLRPersonalityRoutineFieldName, value); + } } internal sealed class MockCodeHeap : TypedView @@ -525,6 +544,30 @@ public ulong AllCodeHeaps } } +internal sealed class MockDynamicFunctionTable : TypedView +{ + private const string MinimumAddressFieldName = "MinimumAddress"; + private const string ContextFieldName = "Context"; + + public static Layout CreateLayout(MockTarget.Architecture architecture) + => new SequentialLayoutBuilder("DynamicFunctionTable", architecture) + .AddPointerField(MinimumAddressFieldName) + .AddPointerField(ContextFieldName) + .Build(); + + public ulong MinimumAddress + { + get => ReadPointerField(MinimumAddressFieldName); + set => WritePointerField(MinimumAddressFieldName, value); + } + + public ulong Context + { + get => ReadPointerField(ContextFieldName); + set => WritePointerField(ContextFieldName, value); + } +} + internal sealed class MockJittedMethod : TypedView { private const string CodeHeaderFieldName = "CodeHeader"; @@ -618,6 +661,7 @@ internal readonly struct JittedCodeRange internal Layout InterpreterRealCodeHeaderLayout { get; } internal Layout ReadyToRunInfoLayout { get; } internal Layout EEJitManagerLayout { get; } + internal Layout DynamicFunctionTableLayout { get; } internal Layout ModuleLayout { get; } internal Layout CodeRangeMapRangeListLayout { get; } internal Layout ImageDataDirectoryLayout { get; } @@ -675,6 +719,7 @@ internal MockExecutionManagerBuilder(string version, MockMemorySpace.Builder bui InterpreterRealCodeHeaderLayout = MockInterpreterRealCodeHeader.CreateLayout(architecture); ReadyToRunInfoLayout = MockReadyToRunInfo.CreateLayout(architecture, hashMapStride); EEJitManagerLayout = MockEEJitManager.CreateLayout(architecture); + DynamicFunctionTableLayout = MockDynamicFunctionTable.CreateLayout(architecture); ModuleLayout = MockLoaderModule.CreateLayout(architecture); CodeRangeMapRangeListLayout = MockCodeRangeMapRangeList.CreateLayout(architecture); ImageDataDirectoryLayout = MockImageDataDirectory.CreateLayout(architecture); @@ -787,7 +832,7 @@ public MockRangeSectionFragment AddRangeSectionFragmentWithCollectibleNext(Jitte return rangeSectionFragment; } - public MockCodeHeapListNode AddCodeHeapListNode(ulong next, ulong startAddress, ulong endAddress, ulong mapBase, ulong headerMap, ulong heap = 0) + public MockCodeHeapListNode AddCodeHeapListNode(ulong next, ulong startAddress, ulong endAddress, ulong mapBase, ulong headerMap, ulong heap = 0, ulong clrPersonalityRoutine = 0) { MockCodeHeapListNode codeHeapListNode = AllocateAndCreate(CodeHeapListNodeLayout, "CodeHeapListNode", _rangeSectionMapAllocator); codeHeapListNode.Next = next; @@ -796,9 +841,21 @@ public MockCodeHeapListNode AddCodeHeapListNode(ulong next, ulong startAddress, codeHeapListNode.MapBase = mapBase; codeHeapListNode.HeaderMap = headerMap; codeHeapListNode.Heap = heap; + if (codeHeapListNode.HasCLRPersonalityRoutine) + { + codeHeapListNode.CLRPersonalityRoutine = clrPersonalityRoutine; + } return codeHeapListNode; } + public MockDynamicFunctionTable AddDynamicFunctionTable(ulong minimumAddress, ulong context) + { + MockDynamicFunctionTable table = AllocateAndCreate(DynamicFunctionTableLayout, "DynamicFunctionTable"); + table.MinimumAddress = minimumAddress; + table.Context = context; + return table; + } + public MockLoaderCodeHeap AddLoaderCodeHeap() { ulong allocationSize = (ulong)Math.Max(CodeHeapLayout.Size, LoaderCodeHeapLayout.Size); @@ -841,6 +898,57 @@ public MockJittedMethod AddJittedMethod(JittedCodeRange jittedCodeRange, uint co return jittedMethod; } + internal readonly struct JittedMethodWithUnwindInfo + { + public ulong CodeAddress { get; init; } + public ulong UnwindInfosAddress { get; init; } + public uint NumUnwindInfos { get; init; } + } + + // Adds a jitted method whose real code header stores its RUNTIME_FUNCTION entries inline + // (mirroring the flexible array member RealCodeHeader::unwindInfos). Used to exercise dynamic + // function table enumeration, where entries are addressed as UnwindInfos + i * sizeof(RUNTIME_FUNCTION). + public JittedMethodWithUnwindInfo AddJittedMethodWithUnwindInfo(JittedCodeRange jittedCodeRange, uint codeSize, ulong methodDescAddress, uint[] runtimeFunctionRvas) + { + MockJittedMethod jittedMethod = AllocateJittedMethod(jittedCodeRange, codeSize); + + uint numUnwindInfos = checked((uint)runtimeFunctionRvas.Length); + int unwindInfosOffset = RealCodeHeaderLayout.GetField("UnwindInfos").Offset; + int runtimeFunctionSize = RuntimeFunctionLayout.Size; + + // Over-allocate the header so the inline RUNTIME_FUNCTION array fits past the fixed fields. + ulong headerSize = checked((ulong)(unwindInfosOffset + (int)numUnwindInfos * runtimeFunctionSize)); + headerSize = Math.Max(headerSize, (ulong)RealCodeHeaderLayout.Size); + MockMemorySpace.HeapFragment headerFragment = _allocator.Allocate(headerSize, "RealCodeHeader"); + MockRealCodeHeader codeHeader = RealCodeHeaderLayout.Create( + headerFragment.Data.AsMemory(0, RealCodeHeaderLayout.Size), + headerFragment.Address); + + jittedMethod.CodeHeader = codeHeader.Address; + codeHeader.MethodDesc = methodDescAddress; + codeHeader.DebugInfo = 0; + codeHeader.EHInfo = 0; + codeHeader.GCInfo = 0; + codeHeader.NumUnwindInfos = numUnwindInfos; + + ulong unwindInfosAddress = headerFragment.Address + (ulong)unwindInfosOffset; + for (uint i = 0; i < numUnwindInfos; i++) + { + int entryOffset = unwindInfosOffset + (int)i * runtimeFunctionSize; + MockRuntimeFunction runtimeFunction = RuntimeFunctionLayout.Create( + headerFragment.Data.AsMemory(entryOffset, runtimeFunctionSize), + headerFragment.Address + (ulong)entryOffset); + runtimeFunction.BeginAddress = runtimeFunctionRvas[i]; + } + + return new JittedMethodWithUnwindInfo + { + CodeAddress = jittedMethod.CodeAddress, + UnwindInfosAddress = unwindInfosAddress, + NumUnwindInfos = numUnwindInfos, + }; + } + public MockJittedMethod AddInterpretedMethod(JittedCodeRange jittedCodeRange, uint codeSize, ulong methodDescAddress) { MockJittedMethod jittedMethod = AllocateJittedMethod(jittedCodeRange, codeSize, "Interpreter Method Header & Code");