diff --git a/docs/design/datacontracts/EcmaMetadata.md b/docs/design/datacontracts/EcmaMetadata.md index 3c62208b3dd524..dfddb68b82adbf 100644 --- a/docs/design/datacontracts/EcmaMetadata.md +++ b/docs/design/datacontracts/EcmaMetadata.md @@ -68,6 +68,12 @@ TargetSpan GetReadOnlyMetadataAddress(ModuleHandle handle) return default; } + // Webcil (flat) images -- e.g. a ReadyToRun corelib on WASM -- are a stripped/rewrapped PE that + // cannot be parsed as a standard PE. They begin with the magic 'WbIL'. For those, the webcil + // header's PeCliHeaderRva locates the CLI (COR20) header, whose metadata directory (RVA + size at + // offset 8) locates the ECMA-335 metadata. RVAs are resolved via the loader's webcil-aware + // GetILAddr. For non-webcil images, read the CLI header from the PE headers as below. + // Read CLR header per https://learn.microsoft.com/windows/win32/debug/pe-format ulong clrHeaderRVA = ... diff --git a/docs/design/datacontracts/ExecutionManager.md b/docs/design/datacontracts/ExecutionManager.md index a80b5289e7c216..763325b2654433 100644 --- a/docs/design/datacontracts/ExecutionManager.md +++ b/docs/design/datacontracts/ExecutionManager.md @@ -207,6 +207,7 @@ Data descriptors used: | `ReadyToRunInfo` | `LoadedImageBase` | Base address of the loaded R2R image | | `ReadyToRunInfo` | `Composite` | Pointer to the `ReadyToRunCoreInfo` used for section lookup | | `ReadyToRunInfo` | `ExceptionInfoSection` | Pointer to the `ImageDataDirectory` for R2R exception info section | +| `ReadyToRunInfo` | `MinVirtualIP` | (WASM only) Base virtual IP for the module's ReadyToRun functions; a function-table index is mapped to a virtual IP relative to this base | | `ReadyToRunHeader` | `MajorVersion` | ReadyToRun major version | | `ReadyToRunHeader` | `MinorVersion` | ReadyToRun minor version | | `ImageDataDirectory` | `VirtualAddress` | Virtual address of the image data directory | @@ -239,6 +240,10 @@ Data descriptors used: | `ReadyToRunSection` | `Section` | `IMAGE_DATA_DIRECTORY` for the section data | | `ExceptionLookupTableEntry` | `MethodStartRVA` | RVA of the method start | | `ExceptionLookupTableEntry` | `ExceptionInfoRVA` | RVA of the exception clause data | +| `FunctionTableIndexRangeSection` | `MinFunctionTableIndex` | (WASM only) Lowest ReadyToRun function-table index covered by this range | +| `FunctionTableIndexRangeSection` | `NumRuntimeFunctions` | (WASM only) Number of runtime functions in the range | +| `FunctionTableIndexRangeSection` | `R2RModule` | (WASM only) Pointer to the owning ReadyToRun module | +| `FunctionTableIndexRangeSection` | `Next` | (WASM only) Pointer to the next `FunctionTableIndexRangeSection` in the list | Global variables used: | Global Name | Type | Purpose | @@ -253,6 +258,7 @@ Global variables used: | `FeatureOnStackReplacement` | uint8 | 1 if FEATURE_ON_STACK_REPLACEMENT is enabled, 0 otherwise | | `FeaturePortableEntrypoints` | uint8 | 1 if FEATURE_PORTABLE_ENTRYPOINTS is enabled, 0 otherwise | | `ObjectMethodTable` | TargetPointer | Pointer to the `System.Object` MethodTable, used for catch-all handler detection | +| `FunctionTableIndexRangeList` | TargetPointer | (WASM only) Head of the linked list of `FunctionTableIndexRangeSection`, mapping ReadyToRun function-table indices to their owning module for virtual-IP stack walking | Contract constants used: | Name | Type | Purpose | Value | diff --git a/docs/design/datacontracts/Loader.md b/docs/design/datacontracts/Loader.md index 8598bfb7c2847c..17ab178e22da1d 100644 --- a/docs/design/datacontracts/Loader.md +++ b/docs/design/datacontracts/Loader.md @@ -181,6 +181,7 @@ enum ClrModifiableAssemblies : uint | `PEAssembly` | `AssemblyBinder` | Pointer to the PEAssembly's binder | | `AssemblyBinder` | `AssemblyLoadContext` | Pointer to the AssemblyBinder's AssemblyLoadContext | | `PEImage` | `LoadedImageLayout` | Pointer to the PEImage's loaded PEImageLayout | +| `PEImage` | `FlatImageLayout` | Pointer to the PEImage's flat PEImageLayout (used when there is no loaded layout, e.g. webcil images) | | `PEImage` | `ProbeExtensionResult` | PEImage's ProbeExtensionResult | | `ProbeExtensionResult` | `Type` | Type of ProbeExtensionResult | | `PEImageLayout` | `Base` | Base address of the image layout | @@ -436,6 +437,14 @@ bool TryGetLoadedImageContents(ModuleHandle handle, out TargetPointer baseAddres // try to get loaded PE image (peImage), if not loaded return false TargetPointer peImageLayout = target.ReadPointer(peImage + /* PEImage::LoadedImageLayout offset */); + if (peImageLayout == TargetPointer.Null) + { + // Images that are never mapped/loaded (e.g. a webcil ReadyToRun image on WASM) have no + // loaded layout; their metadata lives in the flat layout (m_pLayouts[IMAGE_FLAT]). + peImageLayout = target.ReadPointer(peImage + /* PEImage::FlatImageLayout offset */); + if (peImageLayout == TargetPointer.Null) + return false; + } baseAddress = target.ReadPointer(peImageLayout + /* PEImageLayout::Base offset */); size = target.Read(peImageLayout + /* PEImageLayout::Size offset */); @@ -472,7 +481,13 @@ private TargetPointer GetRvaData(TargetPointer peAssemblyPtr, int rva, bool isNu TargetPointer peImageLayout = target.ReadPointer(peImage + /* PEImage::LoadedImageLayout offset */); if(peImageLayout == TargetPointer.Null) - throw new InvalidOperationException("PEImage does not have a LoadedImageLayout associated with it."); + { + // Images that are never mapped/loaded (e.g. a webcil ReadyToRun image on WASM) have no + // loaded layout; fall back to the flat layout (m_pLayouts[IMAGE_FLAT]). + peImageLayout = target.ReadPointer(peImage + /* PEImage::FlatImageLayout offset */); + if(peImageLayout == TargetPointer.Null) + throw new InvalidOperationException("PEImage does not have a usable image layout associated with it."); + } // Get base address and flags from PEImageLayout TargetPointer baseAddress = target.ReadPointer(peImageLayout + /* PEImageLayout::Base offset */); @@ -700,8 +715,12 @@ ModuleLookupTables GetLookupTables(ModuleHandle handle) MethodDefToDescMap: target.ReadPointer(handle.Address + /* Module::MethodDefToDescMap */), TypeDefToMethodTableMap: target.ReadPointer(handle.Address + /* Module::TypeDefToMethodTableMap */), TypeRefToMethodTableMap: target.ReadPointer(handle.Address + /* Module::TypeRefToMethodTableMap */), - MethodDefToILCodeVersioningState: target.ReadPointer(handle.Address + /* - Module::MethodDefToILCodeVersioningState */), + // Module::MethodDefToILCodeVersioningState is only present when the target was built + // with code versioning (FEATURE_CODE_VERSIONING). When absent (e.g. on WASM) it is + // treated as a null (empty) table. + MethodDefToILCodeVersioningState: HasField(Module::MethodDefToILCodeVersioningState) + ? target.ReadPointer(handle.Address + /* Module::MethodDefToILCodeVersioningState */) + : TargetPointer.Null, TableDataOffset: tableDataOffset); } diff --git a/src/coreclr/debug/datadescriptor-shared/contractdescriptorstub.c b/src/coreclr/debug/datadescriptor-shared/contractdescriptorstub.c index 85e11e6ec9c66e..bc44f3dfe887a7 100644 --- a/src/coreclr/debug/datadescriptor-shared/contractdescriptorstub.c +++ b/src/coreclr/debug/datadescriptor-shared/contractdescriptorstub.c @@ -22,7 +22,7 @@ DLLEXPORT struct ContractDescriptor CONTRACT_NAME; DLLEXPORT struct ContractDescriptor CONTRACT_NAME = { .magic = 0x0043414443434e44ull, // "DNCCDAC\0" - .flags = 0x1u & (sizeof(void*) == 4 ? 0x02u : 0x00u), + .flags = 0x1u | (sizeof(void*) == 4 ? 0x02u : 0x00u), .descriptor_size = sizeof(STUB_DESCRIPTOR), .descriptor = STUB_DESCRIPTOR, .pointer_data_count = 1, diff --git a/src/coreclr/vm/codeman.h b/src/coreclr/vm/codeman.h index 5fffee92256cfd..c96fecef5d105e 100644 --- a/src/coreclr/vm/codeman.h +++ b/src/coreclr/vm/codeman.h @@ -2724,7 +2724,21 @@ struct cdac_data { static constexpr void* const CodeRangeMapAddress = (void*)&ExecutionManager::g_codeRangeMap.Data[0]; static constexpr PTR_EEJitManager* EEJitManagerAddress = &ExecutionManager::m_pEEJitManager; +#ifdef TARGET_WASM + static constexpr FunctionTableIndexRangeSection** FunctionTableIndexRangeListAddress = &ExecutionManager::s_pFunctionTableIndexRangeList; +#endif // TARGET_WASM +}; + +#ifdef TARGET_WASM +template<> +struct cdac_data +{ + static constexpr size_t MinFunctionTableIndex = offsetof(FunctionTableIndexRangeSection, minFunctionTableIndex); + static constexpr size_t NumRuntimeFunctions = offsetof(FunctionTableIndexRangeSection, numRuntimeFunctions); + static constexpr size_t R2RModule = offsetof(FunctionTableIndexRangeSection, pR2RModule); + static constexpr size_t Next = offsetof(FunctionTableIndexRangeSection, pNext); }; +#endif // TARGET_WASM #endif inline CodeHeader * EEJitManager::GetCodeHeader(const METHODTOKEN& MethodToken) diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index f6ed92ee2d4319..fd945fcb67b621 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -453,6 +453,7 @@ CDAC_TYPE_END(AssemblyBinder) CDAC_TYPE_BEGIN(PEImage) CDAC_TYPE_INDETERMINATE(PEImage) +CDAC_TYPE_FIELD(PEImage, T_POINTER, FlatImageLayout, cdac_data::FlatImageLayout) CDAC_TYPE_FIELD(PEImage, T_POINTER, LoadedImageLayout, cdac_data::LoadedImageLayout) CDAC_TYPE_FIELD(PEImage, TYPE(ProbeExtensionResult), ProbeExtensionResult, cdac_data::ProbeExtensionResult) CDAC_TYPE_END(PEImage) @@ -937,8 +938,21 @@ CDAC_TYPE_FIELD(ReadyToRunInfo, T_UINT32, NumImportSections, cdac_data::EntryPointToMethodDescMap) CDAC_TYPE_FIELD(ReadyToRunInfo, T_POINTER, LoadedImageBase, cdac_data::LoadedImageBase) CDAC_TYPE_FIELD(ReadyToRunInfo, T_POINTER, Composite, cdac_data::Composite) +#ifdef TARGET_WASM +CDAC_TYPE_FIELD(ReadyToRunInfo, T_POINTER, MinVirtualIP, cdac_data::MinVirtualIP) +#endif // TARGET_WASM CDAC_TYPE_END(ReadyToRunInfo) +#ifdef TARGET_WASM +CDAC_TYPE_BEGIN(FunctionTableIndexRangeSection) +CDAC_TYPE_INDETERMINATE(FunctionTableIndexRangeSection) +CDAC_TYPE_FIELD(FunctionTableIndexRangeSection, T_UINT32, MinFunctionTableIndex, cdac_data::MinFunctionTableIndex) +CDAC_TYPE_FIELD(FunctionTableIndexRangeSection, T_UINT32, NumRuntimeFunctions, cdac_data::NumRuntimeFunctions) +CDAC_TYPE_FIELD(FunctionTableIndexRangeSection, T_POINTER, R2RModule, cdac_data::R2RModule) +CDAC_TYPE_FIELD(FunctionTableIndexRangeSection, T_POINTER, Next, cdac_data::Next) +CDAC_TYPE_END(FunctionTableIndexRangeSection) +#endif // TARGET_WASM + CDAC_TYPE_BEGIN(ReadyToRunHeader) CDAC_TYPE_INDETERMINATE(ReadyToRunHeader) CDAC_TYPE_FIELD(ReadyToRunHeader, T_UINT16, MajorVersion, offsetof(READYTORUN_HEADER, MajorVersion)) @@ -1766,6 +1780,9 @@ CDAC_GLOBAL(StressLogEnabled, T_UINT8, 0) #endif CDAC_GLOBAL_POINTER(ExecutionManagerCodeRangeMapAddress, cdac_data::CodeRangeMapAddress) CDAC_GLOBAL_POINTER(EEJitManagerAddress, cdac_data::EEJitManagerAddress) +#ifdef TARGET_WASM +CDAC_GLOBAL_POINTER(FunctionTableIndexRangeList, cdac_data::FunctionTableIndexRangeListAddress) +#endif // TARGET_WASM CDAC_GLOBAL_POINTER(PlatformMetadata, &::g_cdacPlatformMetadata) #ifdef PROFILING_SUPPORTED CDAC_GLOBAL_POINTER(ProfilerControlBlock, &::g_profControlBlock) diff --git a/src/coreclr/vm/peimage.h b/src/coreclr/vm/peimage.h index ea56c0d8d594c7..94fdcec15f121b 100644 --- a/src/coreclr/vm/peimage.h +++ b/src/coreclr/vm/peimage.h @@ -326,7 +326,8 @@ class PEImage final template<> struct cdac_data { - // The loaded PEImageLayout is m_pLayouts[IMAGE_LOADED] + // Layouts are stored in m_pLayouts[], indexed by IMAGE_FLAT (0) and IMAGE_LOADED (1). + static constexpr size_t FlatImageLayout = offsetof(PEImage, m_pLayouts); static constexpr size_t LoadedImageLayout = offsetof(PEImage, m_pLayouts) + sizeof(PTR_PEImageLayout); static constexpr size_t ProbeExtensionResult = offsetof(PEImage, m_probeExtensionResult); }; diff --git a/src/coreclr/vm/readytoruninfo.h b/src/coreclr/vm/readytoruninfo.h index a5ab695970b2d6..4f2b17c31a3c49 100644 --- a/src/coreclr/vm/readytoruninfo.h +++ b/src/coreclr/vm/readytoruninfo.h @@ -442,6 +442,9 @@ struct cdac_data static constexpr size_t EntryPointToMethodDescMap = offsetof(ReadyToRunInfo, m_entryPointToMethodDescMap); static constexpr size_t LoadedImageBase = offsetof(ReadyToRunInfo, m_pLoadedImageBase); static constexpr size_t Composite = offsetof(ReadyToRunInfo, m_pComposite); +#ifdef TARGET_WASM + static constexpr size_t MinVirtualIP = offsetof(ReadyToRunInfo, m_minVirtualIP); +#endif // TARGET_WASM }; class DynamicHelpers diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs index c25170f26f75b2..5199076ad6684b 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs @@ -12,6 +12,7 @@ public static class Globals public const string SystemDomain = nameof(SystemDomain); public const string ThreadStore = nameof(ThreadStore); public const string FinalizerThread = nameof(FinalizerThread); + public const string FunctionTableIndexRangeList = nameof(FunctionTableIndexRangeList); public const string GCThread = nameof(GCThread); public const string Debugger = nameof(Debugger); public const string MaxHijackFunctions = nameof(MaxHijackFunctions); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/EcmaMetadata_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/EcmaMetadata_1.cs index 567e018ac045e5..0171b4e493db2b 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/EcmaMetadata_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/EcmaMetadata_1.cs @@ -41,20 +41,58 @@ public TargetSpan GetReadOnlyMetadataAddress(ModuleHandle handle) { throw new InvalidOperationException("Module is not loaded."); } - bool isMapped = (imageFlags & 0x1) != 0; // FLAG_MAPPED = 0x1 - PEStreamOptions isLoaded = isMapped ? PEStreamOptions.IsLoadedImage : PEStreamOptions.Default; - TargetStream stream = new(target, baseAddress, size); - using PEReader peReader = new PEReader(stream, isLoaded); + TargetSpan result; + if (IsWebcilImage(baseAddress)) + { + // Webcil (flat) images -- e.g. ReadyToRun corelib on WASM -- are a stripped/rewrapped PE + // that System.Reflection.Metadata's PEReader cannot parse. Locate the metadata via the + // webcil header instead. + result = GetWebcilReadOnlyMetadataAddress(handle, baseAddress); + } + else + { + bool isMapped = (imageFlags & 0x1) != 0; // FLAG_MAPPED = 0x1 + PEStreamOptions isLoaded = isMapped ? PEStreamOptions.IsLoadedImage : PEStreamOptions.Default; + + TargetStream stream = new(target, baseAddress, size); + using PEReader peReader = new PEReader(stream, isLoaded); - int metadataStartOffset = peReader.PEHeaders.MetadataStartOffset; - int metadataSize = peReader.PEHeaders.MetadataSize; + int metadataStartOffset = peReader.PEHeaders.MetadataStartOffset; + int metadataSize = peReader.PEHeaders.MetadataSize; + + result = new TargetSpan(baseAddress + (ulong)metadataStartOffset, (ulong)metadataSize); + } - TargetSpan result = new TargetSpan(baseAddress + (ulong)metadataStartOffset, (ulong)metadataSize); _readOnlyMetadataAddress[handle] = result; return result; } + // 'W','b','I','L' little-endian -- the magic at the start of a webcil header (see docs/design/mono/webcil.md). + private const uint WebcilMagic = 0x4C49_6257; + + private bool IsWebcilImage(TargetPointer baseAddress) + => target.ReadLittleEndian(baseAddress) == WebcilMagic; + + private TargetSpan GetWebcilReadOnlyMetadataAddress(ModuleHandle handle, TargetPointer webcilBase) + { + // The webcil header points to the PE CLI (COR20) header; the metadata directory (RVA + size + // at offset 8 in the COR20 header) locates the ECMA-335 metadata blob. RVAs are resolved + // through the loader, which understands the webcil section layout. + Data.WebcilHeader header = target.ProcessedData.GetOrAdd(webcilBase); + Data.Module module = target.ProcessedData.GetOrAdd(handle.Address); + ILoader loader = target.Contracts.Loader; + + TargetPointer cliHeader = loader.GetILAddr(module.PEAssembly, checked((int)header.PeCliHeaderRva)); + + // IMAGE_COR20_HEADER: cb (4) + MajorRuntimeVersion (2) + MinorRuntimeVersion (2) then the + // MetaData IMAGE_DATA_DIRECTORY (RVA @ 8, Size @ 12). + Data.ImageDataDirectory metadataDirectory = target.ProcessedData.GetOrAdd(cliHeader + 8); + + TargetPointer metadataAddress = loader.GetILAddr(module.PEAssembly, checked((int)metadataDirectory.VirtualAddress)); + return new TargetSpan(metadataAddress, metadataDirectory.Size); + } + public MetadataReader? GetMetadata(ModuleHandle handle) { uint generation = GetMetadataGeneration(handle); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Loader_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Loader_1.cs index 448b4a1e40958b..e1e82656a026c4 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Loader_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Loader_1.cs @@ -195,6 +195,26 @@ private bool TryGetPEImage(ModuleHandle handle, [NotNullWhen(true)] out Data.PEI return true; } + // Resolves the PEImageLayout used to read a module's image contents. Prefers the mapped/loaded + // layout; when that is absent (e.g. a webcil ReadyToRun image on WASM is only ever flat) falls + // back to the flat layout, whose section data still backs the image's RVAs and metadata. + private bool TryGetUsableImageLayout(Data.PEImage peImage, [NotNullWhen(true)] out Data.PEImageLayout? imageLayout) + { + imageLayout = null; + + TargetPointer imageLayoutPtr = peImage.LoadedImageLayout; + if (imageLayoutPtr == TargetPointer.Null) + { + if (peImage.FlatImageLayout is not TargetPointer flatLayoutPtr || flatLayoutPtr == TargetPointer.Null) + return false; + + imageLayoutPtr = flatLayoutPtr; + } + + imageLayout = _target.ProcessedData.GetOrAdd(imageLayoutPtr); + return true; + } + bool ILoader.TryGetLoadedImageContents(ModuleHandle handle, out TargetPointer baseAddress, out uint size, out uint imageFlags) { baseAddress = TargetPointer.Null; @@ -204,10 +224,8 @@ bool ILoader.TryGetLoadedImageContents(ModuleHandle handle, out TargetPointer ba if (!TryGetPEImage(handle, out Data.PEImage? peImage)) return false; // no PE image - if (peImage.LoadedImageLayout == TargetPointer.Null) - return false; // no loaded image layout - - Data.PEImageLayout peImageLayout = _target.ProcessedData.GetOrAdd(peImage.LoadedImageLayout); + if (!TryGetUsableImageLayout(peImage, out Data.PEImageLayout? peImageLayout)) + return false; // no usable image layout baseAddress = peImageLayout.Base; size = peImageLayout.Size; @@ -319,9 +337,8 @@ private TargetPointer GetRvaData(TargetPointer peAssemblyPtr, int rva, bool isNu if (assembly.PEImage == TargetPointer.Null) throw new InvalidOperationException("PEAssembly does not have a PEImage associated with it."); Data.PEImage peImage = _target.ProcessedData.GetOrAdd(assembly.PEImage); - if (peImage.LoadedImageLayout == TargetPointer.Null) - throw new InvalidOperationException("PEImage does not have a LoadedImageLayout associated with it."); - Data.PEImageLayout peImageLayout = _target.ProcessedData.GetOrAdd(peImage.LoadedImageLayout); + if (!TryGetUsableImageLayout(peImage, out Data.PEImageLayout? peImageLayout)) + throw new InvalidOperationException("PEImage does not have a usable image layout associated with it."); uint offset; if (IsMapped(peImageLayout)) offset = (uint)rva; @@ -533,7 +550,8 @@ ModuleLookupTables ILoader.GetLookupTables(ModuleHandle handle) module.MethodDefToDescMap, module.TypeDefToMethodTableMap, module.TypeRefToMethodTableMap, - module.MethodDefToILCodeVersioningStateMap, + // Absent on builds without code versioning (e.g. WASM); treat as an empty table. + module.MethodDefToILCodeVersioningStateMap ?? TargetPointer.Null, tableDataOffset); } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformAgnosticContext.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformAgnosticContext.cs index 3e45d381ecbc0d..d0cd2264d1772d 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformAgnosticContext.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformAgnosticContext.cs @@ -50,6 +50,7 @@ public static IPlatformAgnosticContext GetContextForPlatform(Target target) RuntimeInfoArchitecture.Arm64 => new ContextHolder(), RuntimeInfoArchitecture.LoongArch64 => new ContextHolder(), RuntimeInfoArchitecture.RiscV64 => new ContextHolder(), + RuntimeInfoArchitecture.Wasm => new ContextHolder(), RuntimeInfoArchitecture.Unknown => throw new InvalidOperationException($"Processor architecture is required for creating a platform specific context and is not provided by the target"), _ => throw new InvalidOperationException($"Unsupported architecture {runtimeInfo.GetTargetArchitecture()}"), }; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/Wasm/WasmR2RInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/Wasm/WasmR2RInfo.cs new file mode 100644 index 00000000000000..786e5875f42e57 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/Wasm/WasmR2RInfo.cs @@ -0,0 +1,105 @@ +// 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.ExecutionManagerHelpers; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers.Wasm; + +/// +/// cDAC implementation of , mirroring the native +/// ExecutionManager::{FindFunctionTableIndexRangeSection, IsFuncletFunctionIndex, +/// GetWasmVirtualIPFromFunctionTableIndex} in src/coreclr/vm/codeman.cpp. It resolves an +/// R2R function table entry index against the FunctionTableIndexRangeList to its owning +/// module's , then reads the corresponding +/// RUNTIME_FUNCTION for the funclet flag, base virtual IP, and unwind data. +/// +internal sealed class WasmR2RInfo : IWasmR2RInfo +{ + // RUNTIME_FUNCTION__IsFunclet: the funclet flag is the high bit of BeginAddress (clrnt.h). + private const uint FuncletFlag = 0x80000000; + + private readonly Target _target; + private readonly RuntimeFunctionLookup _runtimeFunctions; + + public WasmR2RInfo(Target target) + { + _target = target; + _runtimeFunctions = RuntimeFunctionLookup.Create(target); + } + + // Mirrors ExecutionManager::FindFunctionTableIndexRangeSection. + private Data.FunctionTableIndexRangeSection? FindSection(uint functionTableIndex) + { + if (!_target.TryReadGlobalPointer(Constants.Globals.FunctionTableIndexRangeList, out TargetPointer? listHeadSlot)) + return null; + + // The global holds the address of the s_pFunctionTableIndexRangeList slot (a pointer-to- + // pointer); dereference it once to obtain the actual list head. + TargetPointer current = _target.ReadPointer(listHeadSlot.Value); + while (current != TargetPointer.Null) + { + Data.FunctionTableIndexRangeSection section = _target.ProcessedData.GetOrAdd(current); + if (functionTableIndex >= section.MinFunctionTableIndex && + functionTableIndex < section.MinFunctionTableIndex + section.NumRuntimeFunctions) + { + return section; + } + current = section.Next; + } + + return null; + } + + private Data.ReadyToRunInfo GetReadyToRunInfo(Data.FunctionTableIndexRangeSection section) + { + Data.Module module = _target.ProcessedData.GetOrAdd(section.R2RModule); + return _target.ProcessedData.GetOrAdd(module.ReadyToRunInfo); + } + + private Data.RuntimeFunction GetRuntimeFunction(Data.ReadyToRunInfo r2rInfo, uint localIndex) + => _runtimeFunctions.GetRuntimeFunction(r2rInfo.RuntimeFunctions, localIndex); + + public bool TryGetVirtualIPBase(uint functionTableIndex, out ulong baseVirtualIP) + { + baseVirtualIP = 0; + Data.FunctionTableIndexRangeSection? section = FindSection(functionTableIndex); + if (section is null) + return false; + + Data.ReadyToRunInfo r2rInfo = GetReadyToRunInfo(section); + if (r2rInfo.MinVirtualIP is not TargetPointer minVirtualIP) + return false; + + // Funclets' function-local virtual IPs are relative to their controlling function, so index + // backwards past funclet entries to the controlling (non-funclet) function. + uint localIndex = functionTableIndex - section.MinFunctionTableIndex; + while (true) + { + Data.RuntimeFunction runtimeFunction = GetRuntimeFunction(r2rInfo, localIndex); + if ((runtimeFunction.BeginAddress & FuncletFlag) != 0) + { + if (localIndex == 0) + return false; + localIndex--; + continue; + } + + baseVirtualIP = minVirtualIP.Value + runtimeFunction.BeginAddress; + return true; + } + } + + public bool TryGetUnwindData(uint functionTableIndex, out TargetPointer unwindDataAddress) + { + unwindDataAddress = TargetPointer.Null; + Data.FunctionTableIndexRangeSection? section = FindSection(functionTableIndex); + if (section is null) + return false; + + Data.ReadyToRunInfo r2rInfo = GetReadyToRunInfo(section); + uint localIndex = functionTableIndex - section.MinFunctionTableIndex; + Data.RuntimeFunction runtimeFunction = GetRuntimeFunction(r2rInfo, localIndex); + unwindDataAddress = new TargetPointer(r2rInfo.LoadedImageBase.Value + runtimeFunction.UnwindData); + return true; + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/Wasm/WasmUnwinder.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/Wasm/WasmUnwinder.cs new file mode 100644 index 00000000000000..a7f0dddcb5ed35 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/Wasm/WasmUnwinder.cs @@ -0,0 +1,194 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers.Wasm; + +/// +/// Information the WASM ReadyToRun unwinder needs about R2R function-table entries. +/// Mirrors the ExecutionManager APIs used by the native WASM stack walk in +/// src/coreclr/vm/wasm/helpers.cpp (GetWasmVirtualIPFromFunctionTableIndex) plus +/// access to the per-function unwind data from which the fixed frame size is decoded. +/// +internal interface IWasmR2RInfo +{ + /// + /// Returns the base virtual IP for an R2R function table entry + /// (ExecutionManager::GetWasmVirtualIPFromFunctionTableIndex). Returns false, or a + /// base of 0, when the index does not map to a known R2R function. + /// + bool TryGetVirtualIPBase(uint functionTableIndex, out ulong baseVirtualIP); + + /// + /// Returns the address of the WASM unwind blob for an R2R function table entry + /// (RUNTIME_FUNCTION.UnwindData + ImageBase). The blob begins with a ULEB128 fixed + /// frame size. Returns false when the index does not map to a known R2R function. + /// + bool TryGetUnwindData(uint functionTableIndex, out TargetPointer unwindDataAddress); +} + +/// +/// Walks CoreCLR WASM ReadyToRun frames over the managed linear stack ($sp), mirroring +/// the native implementation in src/coreclr/vm/wasm/helpers.cpp and the ABI documented in +/// docs/design/coreclr/botr/clr-abi.md. +/// +/// +/// Each R2R frame base stores its R2R function table entry index at offset 0 and its +/// function-local virtual IP (divided by 2) at offset 4. A frame whose first word is +/// is a localloc frame whose real base +/// pointer is stored one pointer-sized slot later. A frame whose first word is +/// is not R2R code (an interpreter transition or the stack +/// top), at which point R2R walking stops and the caller falls back to the explicit Frame chain +/// / interpreter frame chain. +/// +internal sealed class WasmUnwinder +{ + // Sp values at or below the lowest linear-memory page carry nothing meaningful. + private const ulong LinearStackFloor = 0x1000; + + // WASM_STACKFRAME_FUNCTION_INDEX_OFFSET: R2R function table entry index (32-bit). + private const ulong FunctionIndexOffset = 0; + + // WASM_STACKFRAME_VIRTUALIP_OFFSET: function-local virtual IP / 2 (always 32-bit). + private const ulong VirtualIpOffset = 4; + + // STACK_WALK_INDIRECT_TO_FRAMEPOINTER: this slot is not the frame base; the real base + // pointer follows one pointer-sized slot later (localloc frames). + private const uint StackWalkIndirectToFramePointer = 0; + + // TERMINATE_R2R_STACK_WALK: this frame is not R2R-generated managed code. + private const uint TerminateR2RStackWalk = 1; + + private readonly Target _target; + private readonly IWasmR2RInfo _r2rInfo; + private readonly ulong _pointerSize; + + public WasmUnwinder(Target target, IWasmR2RInfo r2rInfo) + { + _target = target; + _r2rInfo = r2rInfo; + _pointerSize = (ulong)target.PointerSize; + } + + /// + /// Resolves the R2R frame base for a stack pointer, mirroring + /// GetWasmFramePointerFromStackPointer_Internal. Returns false when there is no R2R + /// frame at (below the linear-stack floor, or a + /// marker). + /// + public bool TryGetFramePointer(TargetPointer sp, out TargetPointer frameBase) + { + frameBase = TargetPointer.Null; + if (sp.Value <= LinearStackFloor) + return false; + + ulong current = sp.Value; + if (_target.Read(current + FunctionIndexOffset) == StackWalkIndirectToFramePointer) + { + current = _target.ReadPointer(current + _pointerSize).Value; + // Re-apply the linear-stack floor after following the localloc indirection: a null or + // out-of-range saved frame pointer is not a valid frame base. + if (current <= LinearStackFloor) + return false; + } + + if (_target.Read(current + FunctionIndexOffset) == TerminateR2RStackWalk) + return false; + + frameBase = new TargetPointer(current); + return true; + } + + /// + /// Recovers the establishing (method) frame pointer stored beside a + /// marker by CallFuncletWith[out]Throwable, + /// mirroring GetWasmEstablishingFramePointerFromTerminator. must + /// point at such a synthetic terminator frame. + /// + public TargetPointer GetEstablishingFramePointerFromTerminator(TargetPointer sp) + => _target.ReadPointer(sp.Value + _pointerSize); + + /// + /// Computes the current R2R virtual IP for a stack pointer, mirroring + /// GetWasmVirtualIPFromStackPointer. Returns when + /// there is no R2R frame or the function index does not map to a known base virtual IP. + /// + public TargetCodePointer GetVirtualIP(TargetPointer sp) + { + if (!TryGetFramePointer(sp, out TargetPointer frameBase)) + return TargetCodePointer.Null; + + uint functionIndex = _target.Read(frameBase.Value + FunctionIndexOffset); + // Virtual IPs are stored divided by 2; the low bit distinguishes virtual IPs from + // interpreter addresses / portable entrypoints. + uint functionLocalVirtualIP = _target.Read(frameBase.Value + VirtualIpOffset) * 2; + + if (!_r2rInfo.TryGetVirtualIPBase(functionIndex, out ulong baseVirtualIP) || baseVirtualIP == 0) + return TargetCodePointer.Null; + + return new TargetCodePointer(baseVirtualIP + functionLocalVirtualIP); + } + + /// + /// Advances by one R2R frame and produces the caller's virtual IP, + /// mirroring WasmUnwindStackFrameCore. Returns false when the R2R walk terminates + /// (no R2R frame at ), in which case is set to + /// . + /// + public bool TryUnwindOneFrame(ref TargetPointer sp, out TargetCodePointer ip) + { + ip = TargetCodePointer.Null; + if (!TryGetFramePointer(sp, out TargetPointer frameBase)) + { + sp = TargetPointer.Null; + return false; + } + + uint functionIndex = _target.Read(frameBase.Value + FunctionIndexOffset); + if (!_r2rInfo.TryGetUnwindData(functionIndex, out TargetPointer unwindData)) + { + sp = TargetPointer.Null; + return false; + } + + uint frameSize = DecodeULEB128(unwindData.Value); + if (frameSize == 0) + { + // A zero frame size makes no progress; terminate rather than risk an unbounded walk. + sp = TargetPointer.Null; + return false; + } + + sp = new TargetPointer(frameBase.Value + frameSize); + ip = GetVirtualIP(sp); + if (ip == TargetCodePointer.Null) + { + // The caller is not R2R-generated code (an interpreter transition or the stack top); + // the R2R walk is exhausted. + sp = TargetPointer.Null; + return false; + } + + return true; + } + + // Standard little-endian base-128 varint, matching the native DecodeULEB128AsU32. A ULEB128 + // uint32 is at most 5 bytes (5 * 7 = 35 >= 32 bits); a longer encoding is malformed. + private uint DecodeULEB128(ulong address) + { + const int MaxBytes = 5; + uint result = 0; + int shift = 0; + for (ulong offset = 0; offset < MaxBytes; offset++) + { + byte b = _target.Read(address + offset); + result |= (uint)(b & 0x7F) << shift; + if ((b & 0x80) == 0) + return result; + shift += 7; + } + + throw new InvalidOperationException("Malformed ULEB128 value in WASM unwind data."); + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/WasmContext.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/WasmContext.cs new file mode 100644 index 00000000000000..24ac092843f18b --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/WasmContext.cs @@ -0,0 +1,162 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; + +/// +/// Platform context for CoreCLR on WebAssembly. +/// +/// +/// WebAssembly has no native register context: the runtime's DT_CONTEXT is an empty +/// struct and REGDISPLAY is zeroed (see src/coreclr/debug/inc/dbgtargetcontext.h +/// and src/coreclr/inc/regdisp.h). Instead, the context is driven by the managed linear +/// stack pointer ($sp): ReadyToRun frames are unwound over the linear stack with a +/// frameSize-based virtual unwind (see ) using synthetic virtual +/// IPs, and interpreter frames are the explicit +/// InterpreterFrame.TopInterpMethodContextFrame -> InterpMethodContextFrame.pParent +/// chain. A real stack is a mix of the two. +/// +/// The instruction/stack/frame pointer slots are 32-bit (wasm32): is +/// the managed linear stack pointer and is the current virtual IP. +/// advances the context by one ReadyToRun frame. +/// +[StructLayout(LayoutKind.Sequential)] +internal struct WasmContext : IPlatformContext +{ + // Field order and size mirror the native wasm T_CONTEXT (src/coreclr/pal/inc/pal.h, + // HOST_WASM branch) so that a serialized WasmContext is byte-compatible with the + // runtime's context blob: + // ContextFlags @0, InterpreterWalkFramePointer @4, InterpreterSP @8, + // InterpreterFP @12, InterpreterIP @16 (20 bytes, all 32-bit / wasm32). + // There is no native register file; these slots are populated by the R2R virtual + // unwind and the interpreter frame-chain walker. + private uint _contextFlags; + private uint _interpreterWalkFramePointer; + private uint _interpreterSP; + private uint _interpreterFP; + private uint _interpreterIP; + + // Name of the synthetic "first argument register" the interpreter stack walk uses to + // stash the owning InterpreterFrame address (native SetFirstArgReg / GetFirstArgReg in + // src/coreclr/vm/wasm/cgencpu.h write context->InterpreterWalkFramePointer). + internal const string InterpreterWalkFramePointerRegister = "interpreterwalkframepointer"; + + // Size matches the serialized native wasm T_CONTEXT so that ContextHolder.GetBytes() + // and Size stay consistent. + public readonly uint Size => 5 * sizeof(uint); + + public readonly uint ContextControlFlags => 0; + + public readonly uint FullContextFlags => 0; + + public readonly uint AllContextFlags => 0; + + // No register file: there is no stack-pointer register index. + public readonly int StackPointerRegister => -1; + + public TargetPointer StackPointer + { + readonly get => new(_interpreterSP); + set => _interpreterSP = (uint)value.Value; + } + + public TargetCodePointer InstructionPointer + { + readonly get => new(_interpreterIP); + set => _interpreterIP = (uint)value.Value; + } + + public TargetPointer FramePointer + { + readonly get => new(_interpreterFP); + set => _interpreterFP = (uint)value.Value; + } + + public uint RawContextFlags { readonly get => _contextFlags; set => _contextFlags = value; } + + public void Unwind(Target target) + { + // Advance one ReadyToRun frame over the managed linear stack. When the R2R walk + // terminates (an interpreter transition or the stack top), StackPointer becomes null and + // the caller falls back to the explicit Frame chain / interpreter frame chain. + Wasm.WasmUnwinder unwinder = new(target, new Wasm.WasmR2RInfo(target)); + TargetPointer sp = StackPointer; + if (unwinder.TryUnwindOneFrame(ref sp, out TargetCodePointer ip)) + { + StackPointer = sp; + InstructionPointer = ip; + } + else + { + StackPointer = TargetPointer.Null; + InstructionPointer = TargetCodePointer.Null; + } + } + + // WASM has no hardware single-step flag; like other architectures without one (ARM, LoongArch64, + // RISC-V) this is a no-op. Callers (e.g. Debugger_1.PrepareExceptionHijack) invoke it + // unconditionally, so it must not throw. + public void UnsetSingleStepFlag() { } + + public bool TrySetRegister(string name, TargetNUInt value) + { + if (name.Equals("pc", StringComparison.OrdinalIgnoreCase) || name.Equals("ip", StringComparison.OrdinalIgnoreCase)) + { + _interpreterIP = (uint)value.Value; + return true; + } + if (name.Equals("sp", StringComparison.OrdinalIgnoreCase)) + { + _interpreterSP = (uint)value.Value; + return true; + } + if (name.Equals("fp", StringComparison.OrdinalIgnoreCase)) + { + _interpreterFP = (uint)value.Value; + return true; + } + if (name.Equals(InterpreterWalkFramePointerRegister, StringComparison.OrdinalIgnoreCase)) + { + _interpreterWalkFramePointer = (uint)value.Value; + return true; + } + return false; + } + + public readonly bool TryReadRegister(string name, out TargetNUInt value) + { + if (name.Equals("pc", StringComparison.OrdinalIgnoreCase) || name.Equals("ip", StringComparison.OrdinalIgnoreCase)) + { + value = new TargetNUInt(_interpreterIP); + return true; + } + if (name.Equals("sp", StringComparison.OrdinalIgnoreCase)) + { + value = new TargetNUInt(_interpreterSP); + return true; + } + if (name.Equals("fp", StringComparison.OrdinalIgnoreCase)) + { + value = new TargetNUInt(_interpreterFP); + return true; + } + if (name.Equals(InterpreterWalkFramePointerRegister, StringComparison.OrdinalIgnoreCase)) + { + value = new TargetNUInt(_interpreterWalkFramePointer); + return true; + } + value = default; + return false; + } + + public bool TrySetRegister(int number, TargetNUInt value) => false; + + public readonly bool TryReadRegister(int number, out TargetNUInt value) + { + value = default; + return false; + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs index afed34ea85f511..6a3a361fa1fe8c 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs @@ -367,6 +367,7 @@ private IPlatformFrameHandler GetFrameHandler(IPlatformAgnosticContext context) ContextHolder contextHolder => new ARM64FrameHandler(_target, contextHolder), ContextHolder contextHolder => new RISCV64FrameHandler(_target, contextHolder), ContextHolder contextHolder => new LoongArch64FrameHandler(_target, contextHolder), + ContextHolder contextHolder => new WasmFrameHandler(_target, contextHolder), _ => throw new InvalidOperationException("Unsupported context type"), }; } @@ -560,6 +561,7 @@ private string GetFirstArgRegisterName() RuntimeInfoArchitecture.X86 => "ecx", RuntimeInfoArchitecture.LoongArch64 => "a0", RuntimeInfoArchitecture.RiscV64 => "a0", + RuntimeInfoArchitecture.Wasm => WasmContext.InterpreterWalkFramePointerRegister, var arch => throw new NotSupportedException( $"Unsupported architecture for first argument register: {arch}"), }; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/WasmFrameHandler.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/WasmFrameHandler.cs new file mode 100644 index 00000000000000..35ffcfc3b4f430 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/WasmFrameHandler.cs @@ -0,0 +1,48 @@ +// 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 Microsoft.Diagnostics.DataContractReader.Data; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; + +/// +/// Frame handler for CoreCLR on WebAssembly. +/// +/// +/// WebAssembly has no native register context (see ). Seeding the +/// initial stack walk context therefore comes from the explicit Frame chain rather than a +/// captured DT_CONTEXT: the innermost transition frame carries the managed linear stack +/// pointer. The base already reads that +/// InlinedCallFrame.CallSiteSP (plus the caller return address and callee-saved frame +/// pointer) into the three synthetic slots, which is the common +/// P/Invoke-boundary seeding path. The software/faulting exception frame handlers likewise read a +/// serialized blob from the frame's TargetContext. +/// +/// Hijack frames are a debugger / GC-suspension concept that is not yet supported on WASM. +/// +internal sealed class WasmFrameHandler(Target target, ContextHolder contextHolder) + : BaseFrameHandler(target, contextHolder), IPlatformFrameHandler +{ + private readonly ContextHolder _holder = contextHolder; + + public override void HandleInlinedCallFrame(InlinedCallFrame inlinedCallFrame) + { + base.HandleInlinedCallFrame(inlinedCallFrame); + + // When the frame directly above this P/Invoke transition is an InterpreterFrame, stash its + // address in the synthetic first-argument register so the subsequent interpreter virtual + // unwind (InterpreterVirtualUnwind -> GetFirstArgReg) can recover the owning InterpreterFrame. + // Mirrors the per-architecture handlers (e.g. AMD64FrameHandler) and the native + // SetFirstArgReg(context->InterpreterWalkFramePointer) contract on WASM. + Data.Frame? next = GetNextFrame(inlinedCallFrame.Address); + if (next is not null && _frameHelpers.GetFrameType(next.Identifier) == FrameType.InterpreterFrame) + { + if (!_holder.Context.TrySetRegister(WasmContext.InterpreterWalkFramePointerRegister, new TargetNUInt(next.Address.Value))) + throw new InvalidOperationException($"Failed to set WASM interpreter frame-pointer register '{WasmContext.InterpreterWalkFramePointerRegister}'."); + } + } + + public void HandleHijackFrame(HijackFrame frame) + => throw new PlatformNotSupportedException("HijackFrame handling is not supported on WASM."); +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/FunctionTableIndexRangeSection.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/FunctionTableIndexRangeSection.cs new file mode 100644 index 00000000000000..41fb03717fb957 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/FunctionTableIndexRangeSection.cs @@ -0,0 +1,16 @@ +// 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; + +// A linked-list node tracking a range of WASM R2R function table indices, mirroring the native +// FunctionTableIndexRangeSection in src/coreclr/vm/codeman.h. The list head is the +// FunctionTableIndexRangeList global (ExecutionManager::s_pFunctionTableIndexRangeList). +[CdacType(nameof(DataType.FunctionTableIndexRangeSection))] +internal sealed partial class FunctionTableIndexRangeSection : IData +{ + [Field] public uint MinFunctionTableIndex { get; } + [Field] public uint NumRuntimeFunctions { get; } + [Field] public TargetPointer R2RModule { get; } + [Field] public TargetPointer Next { get; } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Module.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Module.cs index 2f0a5b69928097..1c89fc01acfca6 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Module.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Module.cs @@ -30,7 +30,11 @@ internal sealed partial class Module : IData [FieldAddress] public TargetPointer MethodDefToDescMap { get; } [FieldAddress] public TargetPointer TypeDefToMethodTableMap { get; } [FieldAddress] public TargetPointer TypeRefToMethodTableMap { get; } - [FieldAddress] public TargetPointer MethodDefToILCodeVersioningStateMap { get; } + + // Present only when the target was built with code versioning (FEATURE_CODE_VERSIONING); + // absent on builds where it is disabled (e.g. WASM), where it reads as null. + [FieldAddress] public TargetPointer? MethodDefToILCodeVersioningStateMap { get; } + [FieldAddress] public TargetPointer? EnCClassList { get; } [Field] public TargetPointer DynamicILBlobTable { get; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/PEImage.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/PEImage.cs index d16ad4fa5b931d..df20995c1b55ef 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/PEImage.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/PEImage.cs @@ -6,6 +6,9 @@ namespace Microsoft.Diagnostics.DataContractReader.Data; [CdacType(nameof(DataType.PEImage))] internal sealed partial class PEImage : IData { + // The flat image layout (m_pLayouts[IMAGE_FLAT]). Present since the field was added to the + // descriptor; nullable so older descriptors that predate it simply read as null. + [Field] public TargetPointer? FlatImageLayout { get; } [Field] public TargetPointer LoadedImageLayout { get; } [Field] public ProbeExtensionResult ProbeExtensionResult { get; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ReadyToRunInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ReadyToRunInfo.cs index 859dcacd98cbf1..385e9fc901b5ae 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ReadyToRunInfo.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ReadyToRunInfo.cs @@ -19,6 +19,9 @@ internal sealed partial class ReadyToRunInfo : IData [Field] public TargetPointer Composite { get; } [Field] public uint NumImportSections { get; } + // WASM-only: base virtual IP for this module's R2R function table (m_minVirtualIP). + [Field] public TargetPointer? MinVirtualIP { get; } + public TargetPointer RuntimeFunctions { get; private set; } public TargetPointer HotColdMap { get; private set; } public TargetPointer ImportSections { get; private set; } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/WebcilHeader.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/WebcilHeader.cs index 77f0b48301afb0..63e20a245d61f8 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/WebcilHeader.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/WebcilHeader.cs @@ -9,6 +9,7 @@ internal sealed partial class WebcilHeader : IData // See docs/design/mono/webcil.md for the layout. [RawOffset(4)] public ushort VersionMajor { get; } [RawOffset(8)] public ushort CoffSections { get; } + [RawOffset(12)] public uint PeCliHeaderRva { get; } public uint Size => VersionMajor >= 1 ? (uint)32 : (uint)28; } 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 6f718370bc2976..a3fbe6bc8326e2 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs @@ -107,6 +107,7 @@ public enum DataType InterpByteCodeStart, InterpMethod, InterpMethodContextFrame, + FunctionTableIndexRangeSection, Array, Delegate, TypedByRef, diff --git a/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs b/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs index a8666ca5f5a5b9..591b89b1be412b 100644 --- a/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs @@ -291,6 +291,54 @@ public void GetMethodDesc_R2R_OneRuntimeFunction(string version, MockTarget.Arch } } + // On WASM there are no native code pointers: a "code address" is a synthetic virtual IP + // (ExecutionManager::GetWasmVirtualIPFromStackPointer, base + function-local offset). R2R + // modules are registered in the RangeSectionMap by their virtual-IP range, so resolving a + // virtual IP to its MethodDesc uses the same generic RangeSection.Find -> + // ReadyToRunJitManager path as any other architecture -- there is no WASM-specific IP->MethodDesc + // code path (MinVirtualIP / FunctionTableIndexRangeSection are only consumed by the unwinder's + // function-table-index -> base-virtual-IP mapping). This verifies that resolution on a wasm32 + // (32-bit little-endian) target, treating the code address as a virtual IP, and confirms the + // R2R classification. + [Theory] + [InlineData("c1")] + [InlineData("c2")] + public void GetMethodDesc_R2R_WasmVirtualIP(string version) + { + MockTarget.Architecture wasmArch = new() { IsLittleEndian = true, Is64Bit = false }; + + const ulong virtualIPBase = 0x0050_0000u; // R2R module base virtual IP + const uint virtualIPRangeSize = 0xc000u; + const ulong jitManagerAddress = 0x000b_ff00; + const ulong expectedMethodDescAddress = 0x0101_aaa0; + + uint functionLocalVirtualIP = 0x100; // offset of the R2R function within the module + + IExecutionManager em = CreateExecutionManagerContract( + version, + wasmArch, + emBuilder => + { + var jittedCode = emBuilder.AllocateJittedCodeRange(virtualIPBase, virtualIPRangeSize); + MockReadyToRunInfo r2rInfo = emBuilder.AddReadyToRunInfo([functionLocalVirtualIP], []); + MockHashMapBuilder hashMapBuilder = new(emBuilder.Builder); + hashMapBuilder.PopulatePtrMap( + r2rInfo.EntryPointToMethodDescMapAddress, + [(jittedCode.RangeStart + functionLocalVirtualIP, expectedMethodDescAddress)]); + + MockLoaderModule r2rModule = emBuilder.AddReadyToRunModule(r2rInfo.Address); + MockRangeSection rangeSection = emBuilder.AddReadyToRunRangeSection(jittedCode, jitManagerAddress, r2rModule.Address); + _ = emBuilder.AddRangeSectionFragment(jittedCode, rangeSection.Address); + }); + + TargetCodePointer virtualIP = new(virtualIPBase + functionLocalVirtualIP); + + var handle = em.GetCodeBlockHandle(virtualIP); + Assert.NotNull(handle); + Assert.Equal(new TargetPointer(expectedMethodDescAddress), em.GetMethodDesc(handle.Value)); + Assert.Equal(CodeKind.ReadyToRun, em.GetCodeKind(virtualIP)); + } + [Theory] [MemberData(nameof(StdArchAllVersions))] public void GetMethodDesc_R2R_MultipleRuntimeFunctions(string version, MockTarget.Architecture arch) diff --git a/src/native/managed/cdac/tests/UnitTests/LoaderTests.cs b/src/native/managed/cdac/tests/UnitTests/LoaderTests.cs index 5042157e99ea18..ad98e709da1391 100644 --- a/src/native/managed/cdac/tests/UnitTests/LoaderTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/LoaderTests.cs @@ -71,6 +71,30 @@ public void GetPath(MockTarget.Architecture arch) Assert.Equal(expected, contract.GetPath(handle)); } + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void Module_NoCodeVersioning_MethodDefToILCodeVersioningStateMapIsNull(MockTarget.Architecture arch) + { + // On builds without code versioning (e.g. WASM, FEATURE_CODE_VERSIONING off) the Module + // layout omits MethodDefToILCodeVersioningStateMap. Reading it must yield null rather than + // throwing "Field not found in any layout", so type/module resolution keeps working. + var targetBuilder = new TestPlaceholderTarget.Builder(arch); + MockLoaderBuilder loader = new(targetBuilder.MemoryBuilder, (0x0001_0000, 0x0002_0000), includeCodeVersioning: false); + + ulong moduleAddr = loader.AddModule().Address; + + var target = targetBuilder + .AddTypes(CreateContractTypes(loader)) + .AddContract(version: "c1") + .Build(); + + Data.Module module = target.ProcessedData.GetOrAdd(new TargetPointer(moduleAddr)); + + // The absent code-versioning map reads as null; a present map still resolves to an address. + Assert.Null(module.MethodDefToILCodeVersioningStateMap); + Assert.NotEqual(TargetPointer.Null, module.MethodDefToDescMap); + } + [Theory] [ClassData(typeof(MockTarget.StdArch))] public void GetFileName(MockTarget.Architecture arch) @@ -494,7 +518,8 @@ private static (TestPlaceholderTarget Target, TargetPointer PEAssemblyAddr, Targ MockTarget.Architecture arch, ushort coffSections, SectionDef[] sections, - ushort versionMajor = 0) + ushort versionMajor = 0, + bool useFlatLayout = false) { TargetTestHelpers helpers = new(arch); var targetBuilder = new TestPlaceholderTarget.Builder(arch); @@ -510,6 +535,7 @@ private static (TestPlaceholderTarget Target, TargetPointer PEAssemblyAddr, Targ new(nameof(Data.PEAssembly.MDImport), DataType.pointer), ]); var peImageLayout = helpers.LayoutFields([ + new(nameof(Data.PEImage.FlatImageLayout), DataType.pointer), new(nameof(Data.PEImage.LoadedImageLayout), DataType.pointer), new(nameof(Data.PEImage.ProbeExtensionResult), DataType.ProbeExtensionResult, probeExtLayout.Stride), ]); @@ -586,7 +612,8 @@ private static (TestPlaceholderTarget Target, TargetPointer PEAssemblyAddr, Targ helpers.Write(layoutFrag.Data.AsSpan().Slice(imageLayoutLayout.Fields[nameof(Data.PEImageLayout.Format)].Offset, sizeof(uint)), 1u); var peImageFrag = allocator.Allocate(peImageLayout.Stride, "PEImage"); - helpers.WritePointer(peImageFrag.Data.AsSpan().Slice(peImageLayout.Fields[nameof(Data.PEImage.LoadedImageLayout)].Offset, helpers.PointerSize), layoutFrag.Address); + string imageLayoutField = useFlatLayout ? nameof(Data.PEImage.FlatImageLayout) : nameof(Data.PEImage.LoadedImageLayout); + helpers.WritePointer(peImageFrag.Data.AsSpan().Slice(peImageLayout.Fields[imageLayoutField].Offset, helpers.PointerSize), layoutFrag.Address); var peAssemblyFrag = allocator.Allocate(peAssemblyLayout.Stride, "PEAssembly"); helpers.WritePointer(peAssemblyFrag.Data.AsSpan().Slice(peAssemblyLayout.Fields[nameof(Data.PEAssembly.PEImage)].Offset, helpers.PointerSize), peImageFrag.Address); @@ -621,6 +648,23 @@ public void GetILAddr_WebcilRvaToOffset(MockTarget.Architecture arch) Assert.Equal((TargetPointer)(imageBase + 0x2700u), contract.GetILAddr(peAssemblyAddr, 0x4500)); } + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetILAddr_WebcilFlatLayout_ResolvesViaFlatFallback(MockTarget.Architecture arch) + { + // On WASM a webcil ReadyToRun image has no loaded layout -- only the flat layout. RVA + // resolution must fall back to the flat layout instead of throwing "no loaded layout". + SectionDef[] sections = + [ + new(VirtualSize: 0x2000, VirtualAddress: 0x1000, SizeOfRawData: 0x2000, PointerToRawData: 0x200), + ]; + var (target, peAssemblyAddr, imageBase) = CreateWebcilTarget(arch, (ushort)sections.Length, sections, useFlatLayout: true); + ILoader contract = target.Contracts.Loader; + + // RVA in first section resolves through the flat layout: offset = (0x1100 - 0x1000) + 0x200 = 0x300 + Assert.Equal((TargetPointer)(imageBase + 0x300u), contract.GetILAddr(peAssemblyAddr, 0x1100)); + } + [Theory] [ClassData(typeof(MockTarget.StdArch))] public void GetILAddr_WebcilNegativeRvaThrows(MockTarget.Architecture arch) @@ -788,6 +832,79 @@ public void IsModuleMapped_NoPEAssembly_ReturnsFalse(MockTarget.Architecture arc Assert.False(contract.IsModuleMapped(handle)); } + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void TryGetLoadedImageContents_NoLoadedLayout_FallsBackToFlatLayout(MockTarget.Architecture arch) + { + // Images that are never mapped/loaded (e.g. a webcil ReadyToRun image on WASM) have a null + // LoadedImageLayout; their metadata lives in the flat layout. TryGetLoadedImageContents must + // fall back to FlatImageLayout instead of reporting "not loaded". + const ulong expectedBase = 0x0012_3000; + const uint expectedSize = 0x4560; + const uint flatFlags = 0; // flat layouts are not FLAG_MAPPED + + TargetTestHelpers helpers = new(arch); + var targetBuilder = new TestPlaceholderTarget.Builder(arch); + MockMemorySpace.Builder builder = targetBuilder.MemoryBuilder; + MockLoaderBuilder loader = new(builder); + var allocator = builder.CreateAllocator(0x0010_0000, 0x0020_0000); + + MockLoaderModule module = loader.AddModule(); + + var probeExtLayout = helpers.LayoutFields([ + new(nameof(Data.ProbeExtensionResult.Type), DataType.int32), + ]); + var peAssemblyLayout = helpers.LayoutFields([ + new(nameof(Data.PEAssembly.PEImage), DataType.pointer), + new(nameof(Data.PEAssembly.AssemblyBinder), DataType.pointer), + new(nameof(Data.PEAssembly.MDImport), DataType.pointer), + ]); + var peImageLayout = helpers.LayoutFields([ + new(nameof(Data.PEImage.FlatImageLayout), DataType.pointer), + new(nameof(Data.PEImage.LoadedImageLayout), DataType.pointer), + new(nameof(Data.PEImage.ProbeExtensionResult), DataType.ProbeExtensionResult, probeExtLayout.Stride), + ]); + var imageLayoutLayout = helpers.LayoutFields([ + new(nameof(Data.PEImageLayout.Base), DataType.pointer), + new(nameof(Data.PEImageLayout.Size), DataType.uint32), + new(nameof(Data.PEImageLayout.Flags), DataType.uint32), + new(nameof(Data.PEImageLayout.Format), DataType.uint32), + ]); + + var flatLayoutFrag = allocator.Allocate(imageLayoutLayout.Stride, "FlatPEImageLayout"); + helpers.WritePointer(flatLayoutFrag.Data.AsSpan().Slice(imageLayoutLayout.Fields[nameof(Data.PEImageLayout.Base)].Offset, helpers.PointerSize), expectedBase); + helpers.Write(flatLayoutFrag.Data.AsSpan().Slice(imageLayoutLayout.Fields[nameof(Data.PEImageLayout.Size)].Offset, sizeof(uint)), expectedSize); + helpers.Write(flatLayoutFrag.Data.AsSpan().Slice(imageLayoutLayout.Fields[nameof(Data.PEImageLayout.Flags)].Offset, sizeof(uint)), flatFlags); + + // LoadedImageLayout is left null; only the flat layout is populated. + var peImageFrag = allocator.Allocate(peImageLayout.Stride, "PEImage"); + helpers.WritePointer(peImageFrag.Data.AsSpan().Slice(peImageLayout.Fields[nameof(Data.PEImage.FlatImageLayout)].Offset, helpers.PointerSize), flatLayoutFrag.Address); + + var peAssemblyFrag = allocator.Allocate(peAssemblyLayout.Stride, "PEAssembly"); + helpers.WritePointer(peAssemblyFrag.Data.AsSpan().Slice(peAssemblyLayout.Fields[nameof(Data.PEAssembly.PEImage)].Offset, helpers.PointerSize), peImageFrag.Address); + + module.PEAssembly = peAssemblyFrag.Address; + + var types = CreateContractTypes(loader); + types[DataType.PEAssembly] = new() { Fields = peAssemblyLayout.Fields, Size = peAssemblyLayout.Stride }; + types[DataType.PEImage] = new() { Fields = peImageLayout.Fields, Size = peImageLayout.Stride }; + types[DataType.PEImageLayout] = new() { Fields = imageLayoutLayout.Fields, Size = imageLayoutLayout.Stride }; + types[DataType.ProbeExtensionResult] = new() { Fields = probeExtLayout.Fields, Size = probeExtLayout.Stride }; + + var target = targetBuilder + .AddTypes(types) + .AddContract(version: "c1") + .Build(); + + ILoader contract = target.Contracts.Loader; + Contracts.ModuleHandle handle = contract.GetModuleHandleFromModulePtr(new TargetPointer(module.Address)); + + Assert.True(contract.TryGetLoadedImageContents(handle, out TargetPointer baseAddress, out uint size, out uint imageFlags)); + Assert.Equal(expectedBase, baseAddress.Value); + Assert.Equal(expectedSize, size); + Assert.Equal(flatFlags, imageFlags); + } + [Theory] [MemberData(nameof(GetDebuggerInfoBitsData))] public void GetDebuggerInfoBits(uint rawFlags, DebuggerAssemblyControlFlags expectedBits, MockTarget.Architecture arch) 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 4e964f04867fd9..4766720c37fdf1 100644 --- a/src/native/managed/cdac/tests/UnitTests/MockDescriptors/MockDescriptors.ExecutionManager.cs +++ b/src/native/managed/cdac/tests/UnitTests/MockDescriptors/MockDescriptors.ExecutionManager.cs @@ -398,6 +398,7 @@ internal sealed class MockReadyToRunInfo : TypedView private const string ImportSectionsFieldName = "ImportSections"; private const string NumImportSectionsFieldName = "NumImportSections"; + private const string MinVirtualIPFieldName = "MinVirtualIP"; public static Layout CreateLayout(MockTarget.Architecture architecture, int hashMapStride) => new SequentialLayoutBuilder("ReadyToRunInfo", architecture) @@ -415,6 +416,8 @@ public static Layout CreateLayout(MockTarget.Architecture ar .AddField(EntryPointToMethodDescMapFieldName, hashMapStride) .AddPointerField(LoadedImageBaseFieldName) .AddPointerField(CompositeFieldName) + // WASM-only: base virtual IP for the module's ReadyToRun functions (nullable field). + .AddPointerField(MinVirtualIPFieldName) .Build(); public ulong CompositeInfo @@ -460,6 +463,18 @@ public ulong DelayLoadMethodCallThunks } public ulong EntryPointToMethodDescMapAddress => GetFieldAddress(EntryPointToMethodDescMapFieldName); + + public ulong LoadedImageBase + { + get => ReadPointerField(LoadedImageBaseFieldName); + set => WritePointerField(LoadedImageBaseFieldName, value); + } + + public ulong MinVirtualIP + { + get => ReadPointerField(MinVirtualIPFieldName); + set => WritePointerField(MinVirtualIPFieldName, value); + } } internal sealed class MockImageDataDirectory : TypedView diff --git a/src/native/managed/cdac/tests/UnitTests/MockDescriptors/MockDescriptors.Frame.cs b/src/native/managed/cdac/tests/UnitTests/MockDescriptors/MockDescriptors.Frame.cs index af675b194f3435..99aaabb822883d 100644 --- a/src/native/managed/cdac/tests/UnitTests/MockDescriptors/MockDescriptors.Frame.cs +++ b/src/native/managed/cdac/tests/UnitTests/MockDescriptors/MockDescriptors.Frame.cs @@ -56,6 +56,18 @@ public ulong CallerReturnAddress get => ReadPointerField(CallerReturnAddressFieldName); set => WritePointerField(CallerReturnAddressFieldName, value); } + + public ulong CallSiteSP + { + get => ReadPointerField(CallSiteSPFieldName); + set => WritePointerField(CallSiteSPFieldName, value); + } + + public ulong CalleeSavedFP + { + get => ReadPointerField(CalleeSavedFPFieldName); + set => WritePointerField(CalleeSavedFPFieldName, value); + } } internal sealed class MockFramedMethodFrame : MockFrame @@ -76,6 +88,31 @@ public ulong MethodDescPtr } } +internal sealed class MockInterpMethodContextFrame : TypedView +{ + // Field order mirrors src/coreclr/vm/interpexec.h InterpMethodContextFrame. + private const string StartIpFieldName = "StartIp"; + private const string ParentPtrFieldName = "ParentPtr"; + private const string IpFieldName = "Ip"; + private const string NextPtrFieldName = "NextPtr"; + private const string StackFieldName = "Stack"; + + public static Layout CreateLayout(MockTarget.Architecture architecture) + => new SequentialLayoutBuilder("InterpMethodContextFrame", architecture) + .AddPointerField(StartIpFieldName) + .AddPointerField(ParentPtrFieldName) + .AddPointerField(IpFieldName) + .AddPointerField(NextPtrFieldName) + .AddPointerField(StackFieldName) + .Build(); + + public ulong StartIp { get => ReadPointerField(StartIpFieldName); set => WritePointerField(StartIpFieldName, value); } + public ulong ParentPtr { get => ReadPointerField(ParentPtrFieldName); set => WritePointerField(ParentPtrFieldName, value); } + public ulong Ip { get => ReadPointerField(IpFieldName); set => WritePointerField(IpFieldName, value); } + public ulong NextPtr { get => ReadPointerField(NextPtrFieldName); set => WritePointerField(NextPtrFieldName, value); } + public ulong Stack { get => ReadPointerField(StackFieldName); set => WritePointerField(StackFieldName, value); } +} + internal sealed class MockFuncEvalFrame : MockFrame { // Mirrors the cDAC FuncEvalFrame data class which reads DebuggerEvalPtr and @@ -188,6 +225,7 @@ internal sealed class MockFrameBuilder public Layout FuncEvalFrameLayout { get; } public Layout DebuggerEvalLayout { get; } public Layout ResumableFrameLayout { get; } + public Layout InterpMethodContextFrameLayout { get; } public MockFrameBuilder(MockMemorySpace.Builder builder) : this(builder, (DefaultAllocationRangeStart, DefaultAllocationRangeEnd)) @@ -207,6 +245,7 @@ public MockFrameBuilder(MockMemorySpace.Builder builder, (ulong Start, ulong End FuncEvalFrameLayout = MockFuncEvalFrame.CreateLayout(FrameLayout); DebuggerEvalLayout = MockDebuggerEval.CreateLayout(_helpers.Arch); ResumableFrameLayout = MockResumableFrame.CreateLayout(FrameLayout); + InterpMethodContextFrameLayout = MockInterpMethodContextFrame.CreateLayout(_helpers.Arch); } public ulong FrameTopTerminator => _terminator; @@ -228,13 +267,15 @@ public MockFrame AddFrame(ulong identifierValue, string allocName) /// Allocates an InlinedCallFrame. set non-zero /// makes the frame "active" (matching native InlinedCallFrame::HasActiveCall). /// - public MockInlinedCallFrame AddInlinedCallFrame(ulong callerReturnAddress, ulong datum) + public MockInlinedCallFrame AddInlinedCallFrame(ulong callerReturnAddress, ulong datum, ulong callSiteSP = 0, ulong calleeSavedFP = 0) { MockInlinedCallFrame frame = InlinedCallFrameLayout.Create(_allocator.Allocate((ulong)InlinedCallFrameLayout.Size, "InlinedCallFrame")); frame.Identifier = InlinedCallFrameIdentifierValue; frame.Next = _terminator; frame.CallerReturnAddress = callerReturnAddress; frame.Datum = datum; + frame.CallSiteSP = callSiteSP; + frame.CalleeSavedFP = calleeSavedFP; return frame; } @@ -247,6 +288,19 @@ public MockFramedMethodFrame AddFramedMethodFrame(ulong methodDescPtr) return frame; } + /// + /// Allocates an InterpMethodContextFrame -- a node in the interpreter's per-thread + /// call chain walked by the interpreter virtual unwind (via pParent). + /// + public MockInterpMethodContextFrame AddInterpMethodContextFrame(ulong parentPtr, ulong ip, ulong stack) + { + MockInterpMethodContextFrame frame = InterpMethodContextFrameLayout.Create(_allocator.Allocate((ulong)InterpMethodContextFrameLayout.Size, "InterpMethodContextFrame")); + frame.ParentPtr = parentPtr; + frame.Ip = ip; + frame.Stack = stack; + return frame; + } + public MockResumableFrame AddRedirectedThreadFrame(ulong targetContextPtr) { MockResumableFrame frame = ResumableFrameLayout.Create(_allocator.Allocate((ulong)ResumableFrameLayout.Size, "RedirectedThreadFrame")); diff --git a/src/native/managed/cdac/tests/UnitTests/MockDescriptors/MockDescriptors.Loader.cs b/src/native/managed/cdac/tests/UnitTests/MockDescriptors/MockDescriptors.Loader.cs index 58b792eb29af91..8ed58116ae636f 100644 --- a/src/native/managed/cdac/tests/UnitTests/MockDescriptors/MockDescriptors.Loader.cs +++ b/src/native/managed/cdac/tests/UnitTests/MockDescriptors/MockDescriptors.Loader.cs @@ -81,8 +81,9 @@ internal sealed class MockLoaderModule : TypedView private const string MethodDefToILCodeVersioningStateMapFieldName = "MethodDefToILCodeVersioningStateMap"; private const string DynamicILBlobTableFieldName = "DynamicILBlobTable"; - public static Layout CreateLayout(MockTarget.Architecture architecture) - => new SequentialLayoutBuilder("Module", architecture) + public static Layout CreateLayout(MockTarget.Architecture architecture, bool includeCodeVersioning = true) + { + SequentialLayoutBuilder builder = new SequentialLayoutBuilder("Module", architecture) .AddPointerField(AssemblyFieldName) .AddPointerField(PEAssemblyFieldName) .AddPointerField(BaseFieldName) @@ -102,10 +103,20 @@ public static Layout CreateLayout(MockTarget.Architecture arch .AddPointerField(MemberRefToDescMapFieldName) .AddPointerField(MethodDefToDescMapFieldName) .AddPointerField(TypeDefToMethodTableMapFieldName) - .AddPointerField(TypeRefToMethodTableMapFieldName) - .AddPointerField(MethodDefToILCodeVersioningStateMapFieldName) + .AddPointerField(TypeRefToMethodTableMapFieldName); + + // MethodDefToILCodeVersioningStateMap is only emitted when the target was built with + // code versioning (FEATURE_CODE_VERSIONING). Builds where it is disabled (e.g. WASM) + // omit it from the Module layout entirely. + if (includeCodeVersioning) + { + builder = builder.AddPointerField(MethodDefToILCodeVersioningStateMapFieldName); + } + + return builder .AddPointerField(DynamicILBlobTableFieldName) .Build(); + } public ulong Assembly { @@ -242,14 +253,14 @@ public MockLoaderBuilder(MockMemorySpace.Builder builder) { } - public MockLoaderBuilder(MockMemorySpace.Builder builder, (ulong Start, ulong End) allocationRange) + public MockLoaderBuilder(MockMemorySpace.Builder builder, (ulong Start, ulong End) allocationRange, bool includeCodeVersioning = true) { ArgumentNullException.ThrowIfNull(builder); Builder = builder; _allocator = Builder.CreateAllocator(allocationRange.Start, allocationRange.End); - ModuleLayout = MockLoaderModule.CreateLayout(builder.TargetTestHelpers.Arch); + ModuleLayout = MockLoaderModule.CreateLayout(builder.TargetTestHelpers.Arch, includeCodeVersioning); AssemblyLayout = MockLoaderAssembly.CreateLayout(builder.TargetTestHelpers.Arch); EEConfigLayout = MockEEConfig.CreateLayout(builder.TargetTestHelpers.Arch); LoaderHeapLayout = MockLoaderHeap.CreateLayout(builder.TargetTestHelpers.Arch); diff --git a/src/native/managed/cdac/tests/UnitTests/StackWalkTests.cs b/src/native/managed/cdac/tests/UnitTests/StackWalkTests.cs index c8945685dbac78..5494f6116f22b8 100644 --- a/src/native/managed/cdac/tests/UnitTests/StackWalkTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/StackWalkTests.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; using Microsoft.Diagnostics.DataContractReader.TestInfrastructure; using Moq; using Xunit; @@ -16,7 +17,8 @@ public unsafe class StackWalkTests private static TestPlaceholderTarget CreateTarget( MockTarget.Architecture arch, Action configure, - Action? configureFrames = null) + Action? configureFrames = null, + RuntimeInfoArchitecture? runtimeArchitecture = null) { TestPlaceholderTarget.Builder targetBuilder = new(arch); MockThreadBuilder threadBuilder = new(targetBuilder.MemoryBuilder); @@ -53,6 +55,15 @@ private static TestPlaceholderTarget CreateTarget( ("HijackFrameIdentifier", MockFrameBuilder.HijackFrameIdentifierValue)); } + // Some paths (e.g. the interpreter virtual unwind's first-argument-register lookup) + // consult IRuntimeInfo for the target architecture. Register a mock when the test needs it. + if (runtimeArchitecture is RuntimeInfoArchitecture rtArch) + { + Mock runtimeInfo = new(); + runtimeInfo.Setup(r => r.GetTargetArchitecture()).Returns(rtArch); + targetBuilder.AddMockContract(runtimeInfo.Object); + } + return targetBuilder .AddContract(version: "c1") .AddContract(version: "c1") @@ -84,6 +95,7 @@ private static TestPlaceholderTarget CreateTarget( [DataType.FramedMethodFrame] = TargetTestHelpers.CreateTypeInfo(frameBuilder.FramedMethodFrameLayout), [DataType.FuncEvalFrame] = TargetTestHelpers.CreateTypeInfo(frameBuilder.FuncEvalFrameLayout), [DataType.DebuggerEval] = TargetTestHelpers.CreateTypeInfo(frameBuilder.DebuggerEvalLayout), + [DataType.InterpMethodContextFrame] = TargetTestHelpers.CreateTypeInfo(frameBuilder.InterpMethodContextFrameLayout), }; [Theory] @@ -289,4 +301,165 @@ public void GetDebuggerEvalData_ReturnsTokenAndAssemblyFromDebuggerEval(MockTarg Assert.Equal(expectedToken, data.MethodToken); Assert.Equal(expectedAssembly, data.AssemblyPtr.Value); } + + // WASM is a 32-bit little-endian target with no native register context; the initial + // stack walk context is seeded from the Frame chain. This verifies that the degenerate + // WasmContext is routed through WasmFrameHandler and that an active InlinedCallFrame at a + // P/Invoke transition seeds the synthetic IP/SP/FP slots from CallSiteSP / CallerReturnAddress + // / CalleeSavedFP -- the common context-seeding path on WASM. + [Fact] + public void UpdateContextFromFrame_WasmInlinedCallFrame_SeedsContextFromCallSiteSP() + { + MockTarget.Architecture wasmArch = new() { IsLittleEndian = true, Is64Bit = false }; + + const ulong callSiteSP = 0x0004_1000; + const ulong callerReturnAddress = 0x0004_2000; + const ulong calleeSavedFP = 0x0004_3000; + + ulong icfAddr = 0; + TestPlaceholderTarget target = CreateTarget( + wasmArch, + threadBuilder => threadBuilder.AddThread(1, 1234), + frameBuilder => + { + icfAddr = frameBuilder.AddInlinedCallFrame(callerReturnAddress, datum: 0, callSiteSP, calleeSavedFP).Address; + }); + + ContextHolder context = new(); + FrameHelpers frameHelpers = new(target); + Data.Frame frame = target.ProcessedData.GetOrAdd(icfAddr); + frameHelpers.UpdateContextFromFrame(frame, context); + + Assert.Equal(callSiteSP, context.StackPointer.Value); + Assert.Equal(callerReturnAddress, context.InstructionPointer.Value); + Assert.Equal(calleeSavedFP, context.FramePointer.Value); + } + + // The WasmContext mirrors the native wasm T_CONTEXT (src/coreclr/pal/inc/pal.h): five + // 32-bit slots (ContextFlags, InterpreterWalkFramePointer, InterpreterSP/FP/IP). Verify the + // serialized size and that the synthetic first-argument register (InterpreterWalkFramePointer) + // and context flags round-trip. + [Fact] + public void WasmContext_MirrorsNativeLayoutAndRoundTripsRegisters() + { + WasmContext context = default; + + Assert.Equal(5u * sizeof(uint), context.Size); + + Assert.True(context.TrySetRegister(WasmContext.InterpreterWalkFramePointerRegister, new TargetNUInt(0x0004_9000))); + Assert.True(context.TryReadRegister(WasmContext.InterpreterWalkFramePointerRegister, out TargetNUInt walkFp)); + Assert.Equal(0x0004_9000ul, walkFp.Value); + + context.StackPointer = new TargetPointer(0x0004_1000); + context.InstructionPointer = new TargetCodePointer(0x0004_2000); + context.FramePointer = new TargetPointer(0x0004_3000); + context.RawContextFlags = 0x8000000; // CONTEXT_EXCEPTION_ACTIVE + + Assert.Equal(0x0004_1000ul, context.StackPointer.Value); + Assert.Equal(0x0004_2000ul, context.InstructionPointer.Value); + Assert.Equal(0x0004_3000ul, context.FramePointer.Value); + Assert.Equal(0x8000000u, context.RawContextFlags); + } + + // When an active InlinedCallFrame is directly followed by an InterpreterFrame, WasmFrameHandler + // stashes the InterpreterFrame address into the synthetic first-argument register + // (InterpreterWalkFramePointer) so the subsequent interpreter virtual unwind can recover the + // owning frame -- mirroring native SetFirstArgReg on the P/Invoke-into-interpreter transition. + [Fact] + public void UpdateContextFromFrame_WasmInlinedCallFrameOverInterpreterFrame_StashesInterpreterFrame() + { + MockTarget.Architecture wasmArch = new() { IsLittleEndian = true, Is64Bit = false }; + + ulong icfAddr = 0; + ulong interpAddr = 0; + TestPlaceholderTarget target = CreateTarget( + wasmArch, + threadBuilder => threadBuilder.AddThread(1, 1234), + frameBuilder => + { + interpAddr = frameBuilder.AddFrame(MockFrameBuilder.InterpreterFrameIdentifierValue, "InterpreterFrame").Address; + MockInlinedCallFrame icf = frameBuilder.AddInlinedCallFrame(callerReturnAddress: 0x0004_2000, datum: 0, callSiteSP: 0x0004_1000); + icf.Next = interpAddr; + icfAddr = icf.Address; + }); + + ContextHolder context = new(); + FrameHelpers frameHelpers = new(target); + Data.Frame frame = target.ProcessedData.GetOrAdd(icfAddr); + frameHelpers.UpdateContextFromFrame(frame, context); + + Assert.True(context.TryReadRegister(WasmContext.InterpreterWalkFramePointerRegister, out TargetNUInt stashed)); + Assert.Equal(interpAddr, stashed.Value); + } + + // Interpreter virtual unwind on WASM: with the WasmContext SP pointing at an + // InterpMethodContextFrame, each InterpreterVirtualUnwind step follows pParent to the next + // interpreted method, setting IP/SP/FP from the parent frame (matching native + // VirtualUnwindInterpreterCallFrame). Walks a three-node chain to the point of exhaustion. + [Fact] + public void InterpreterVirtualUnwind_WasmChain_StepsThroughInterpMethodContextFrames() + { + MockTarget.Architecture wasmArch = new() { IsLittleEndian = true, Is64Bit = false }; + + const ulong ip1 = 0x0005_1000, fp1 = 0x0006_1000; + const ulong ip2 = 0x0005_2000, fp2 = 0x0006_2000; + + ulong frame0 = 0, frame1 = 0, frame2 = 0; + TestPlaceholderTarget target = CreateTarget( + wasmArch, + threadBuilder => threadBuilder.AddThread(1, 1234), + frameBuilder => + { + // Build leaf-to-root so parent addresses are known when linking children. + frame2 = frameBuilder.AddInterpMethodContextFrame(parentPtr: 0, ip: ip2, stack: fp2).Address; + frame1 = frameBuilder.AddInterpMethodContextFrame(parentPtr: frame2, ip: ip1, stack: fp1).Address; + frame0 = frameBuilder.AddInterpMethodContextFrame(parentPtr: frame1, ip: 0, stack: 0).Address; + }); + + ContextHolder context = new(); + context.StackPointer = new TargetPointer(frame0); + FrameHelpers frameHelpers = new(target); + + // Step 1: frame0 -> parent frame1; context takes frame1's IP/SP/FP. + frameHelpers.InterpreterVirtualUnwind(context); + Assert.Equal(ip1, context.InstructionPointer.Value); + Assert.Equal(frame1, context.StackPointer.Value); + Assert.Equal(fp1, context.FramePointer.Value); + + // Step 2: frame1 -> parent frame2. + frameHelpers.InterpreterVirtualUnwind(context); + Assert.Equal(ip2, context.InstructionPointer.Value); + Assert.Equal(frame2, context.StackPointer.Value); + Assert.Equal(fp2, context.FramePointer.Value); + } + + // When the InterpMethodContextFrame chain is exhausted (pParent == null) and no owning + // InterpreterFrame is stashed in the synthetic first-argument register, the WASM interpreter + // virtual unwind terminates gracefully without applying a transition. This also guards the + // WASM first-argument-register wiring: before it was mapped to InterpreterWalkFramePointer, + // this path threw NotSupportedException from GetFirstArgRegisterName. + [Fact] + public void InterpreterVirtualUnwind_WasmExhaustedChainNoOwningFrame_TerminatesGracefully() + { + MockTarget.Architecture wasmArch = new() { IsLittleEndian = true, Is64Bit = false }; + + ulong frame0 = 0; + TestPlaceholderTarget target = CreateTarget( + wasmArch, + threadBuilder => threadBuilder.AddThread(1, 1234), + frameBuilder => + { + frame0 = frameBuilder.AddInterpMethodContextFrame(parentPtr: 0, ip: 0x0005_1000, stack: 0x0006_1000).Address; + }, + runtimeArchitecture: RuntimeInfoArchitecture.Wasm); + + ContextHolder context = new(); + context.StackPointer = new TargetPointer(frame0); + FrameHelpers frameHelpers = new(target); + + frameHelpers.InterpreterVirtualUnwind(context); + + // Chain exhausted with a null owning frame: context SP is left unchanged, no throw. + Assert.Equal(frame0, context.StackPointer.Value); + } } diff --git a/src/native/managed/cdac/tests/UnitTests/WasmR2RInfoTests.cs b/src/native/managed/cdac/tests/UnitTests/WasmR2RInfoTests.cs new file mode 100644 index 00000000000000..3e58e32cea6e48 --- /dev/null +++ b/src/native/managed/cdac/tests/UnitTests/WasmR2RInfoTests.cs @@ -0,0 +1,113 @@ +// 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.StackWalkHelpers.Wasm; +using Microsoft.Diagnostics.DataContractReader.TestInfrastructure; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.Tests; + +public class WasmR2RInfoTests +{ + // WASM is a 32-bit little-endian target. + private static readonly MockTarget.Architecture WasmArch = new() { IsLittleEndian = true, Is64Bit = false }; + + private const uint MinFunctionTableIndex = 5; + private const uint FunctionTableIndex = 5; // localIndex 0 + private const ulong MinVirtualIP = 0x0005_0000; + private const ulong LoadedImageBase = 0x0090_0000; + private const uint FunctionBeginAddress = 0x100; + private const uint FunctionUnwindData = 0x40; + + // Builds a target whose FunctionTableIndexRangeList global points at a *slot* (pointer-to-pointer), + // matching the CDAC_GLOBAL_POINTER contract. WasmR2RInfo must dereference the slot to reach the + // list head; walking from the slot address directly reads garbage and finds nothing. + private static TestPlaceholderTarget CreateTarget() + { + TargetTestHelpers helpers = new(WasmArch); + var targetBuilder = new TestPlaceholderTarget.Builder(WasmArch); + MockMemorySpace.Builder builder = targetBuilder.MemoryBuilder; + var allocator = builder.CreateAllocator(0x0010_0000, 0x0080_0000); + + int hashMapStride = MockHashMap.CreateLayout(WasmArch).Size; + var moduleLayout = MockLoaderModule.CreateLayout(WasmArch); + var r2rInfoLayout = MockReadyToRunInfo.CreateLayout(WasmArch, hashMapStride); + var runtimeFunctionLayout = helpers.LayoutFields([ + new("BeginAddress", DataType.uint32), + new("UnwindData", DataType.uint32), + ]); + var rangeSectionLayout = helpers.LayoutFields([ + new("MinFunctionTableIndex", DataType.uint32), + new("NumRuntimeFunctions", DataType.uint32), + new("R2RModule", DataType.pointer), + new("Next", DataType.pointer), + ]); + + var runtimeFuncFrag = allocator.Allocate(runtimeFunctionLayout.Stride, "RuntimeFunction"); + helpers.Write(runtimeFuncFrag.Data.AsSpan().Slice(runtimeFunctionLayout.Fields["BeginAddress"].Offset, sizeof(uint)), FunctionBeginAddress); + helpers.Write(runtimeFuncFrag.Data.AsSpan().Slice(runtimeFunctionLayout.Fields["UnwindData"].Offset, sizeof(uint)), FunctionUnwindData); + + MockReadyToRunInfo r2rInfo = r2rInfoLayout.Create(allocator.Allocate((ulong)r2rInfoLayout.Size, "ReadyToRunInfo")); + r2rInfo.CompositeInfo = r2rInfo.Address; + r2rInfo.NumRuntimeFunctions = 1; + r2rInfo.RuntimeFunctions = runtimeFuncFrag.Address; + r2rInfo.LoadedImageBase = LoadedImageBase; + r2rInfo.MinVirtualIP = MinVirtualIP; + + MockLoaderModule module = moduleLayout.Create(allocator.Allocate((ulong)moduleLayout.Size, "Module")); + module.ReadyToRunInfo = r2rInfo.Address; + + var sectionFrag = allocator.Allocate(rangeSectionLayout.Stride, "FunctionTableIndexRangeSection"); + var secFields = rangeSectionLayout.Fields; + helpers.Write(sectionFrag.Data.AsSpan().Slice(secFields["MinFunctionTableIndex"].Offset, sizeof(uint)), MinFunctionTableIndex); + helpers.Write(sectionFrag.Data.AsSpan().Slice(secFields["NumRuntimeFunctions"].Offset, sizeof(uint)), 1u); + helpers.WritePointer(sectionFrag.Data.AsSpan().Slice(secFields["R2RModule"].Offset, helpers.PointerSize), module.Address); + helpers.WritePointer(sectionFrag.Data.AsSpan().Slice(secFields["Next"].Offset, helpers.PointerSize), 0ul); + + // The slot holds the pointer to the list head. The global points at the slot, not the head. + var slotFrag = allocator.Allocate((uint)helpers.PointerSize, "FunctionTableIndexRangeListSlot"); + helpers.WritePointer(slotFrag.Data.AsSpan().Slice(0, helpers.PointerSize), sectionFrag.Address); + + var types = new Dictionary + { + [DataType.RuntimeFunction] = new() { Fields = runtimeFunctionLayout.Fields, Size = runtimeFunctionLayout.Stride }, + [DataType.ReadyToRunInfo] = TargetTestHelpers.CreateTypeInfo(r2rInfoLayout), + [DataType.Module] = TargetTestHelpers.CreateTypeInfo(moduleLayout), + [DataType.FunctionTableIndexRangeSection] = new() { Fields = rangeSectionLayout.Fields, Size = rangeSectionLayout.Stride }, + }; + + return targetBuilder + .AddTypes(types) + .AddGlobals(("FunctionTableIndexRangeList", slotFrag.Address)) + .Build(); + } + + [Fact] + public void TryGetVirtualIPBase_ResolvesThroughDereferencedGlobal() + { + WasmR2RInfo info = new(CreateTarget()); + + Assert.True(info.TryGetVirtualIPBase(FunctionTableIndex, out ulong baseVirtualIP)); + // MinVirtualIP + RuntimeFunction.BeginAddress (non-funclet). + Assert.Equal(MinVirtualIP + FunctionBeginAddress, baseVirtualIP); + } + + [Fact] + public void TryGetUnwindData_ReturnsImageBasePlusUnwindData() + { + WasmR2RInfo info = new(CreateTarget()); + + Assert.True(info.TryGetUnwindData(FunctionTableIndex, out TargetPointer unwindData)); + Assert.Equal(LoadedImageBase + FunctionUnwindData, unwindData.Value); + } + + [Fact] + public void TryGetVirtualIPBase_IndexNotInAnySection_ReturnsFalse() + { + WasmR2RInfo info = new(CreateTarget()); + + Assert.False(info.TryGetVirtualIPBase(MinFunctionTableIndex + 100, out _)); + } +} diff --git a/src/native/managed/cdac/tests/UnitTests/WasmUnwinderTests.cs b/src/native/managed/cdac/tests/UnitTests/WasmUnwinderTests.cs new file mode 100644 index 00000000000000..cd215f35489e87 --- /dev/null +++ b/src/native/managed/cdac/tests/UnitTests/WasmUnwinderTests.cs @@ -0,0 +1,267 @@ +// 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.StackWalkHelpers.Wasm; +using Microsoft.Diagnostics.DataContractReader.TestInfrastructure; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.Tests; + +public class WasmUnwinderTests +{ + // WASM is a 32-bit little-endian target. + private static readonly MockTarget.Architecture WasmArch = new() { IsLittleEndian = true, Is64Bit = false }; + + private const ulong FramesBase = 0x10000; + private const ulong BlobsBase = 0x20000; + private const ulong VirtualIpBase = 0x50000; + + // Function table indices 0 and 1 are reserved for the STACK_WALK_INDIRECT_TO_FRAMEPOINTER + // and TERMINATE_R2R_STACK_WALK sentinels, so real indices start at 2. + private const uint FuncIndexLeaf = 10; + private const uint FuncIndexCaller = 11; + + private sealed class FakeWasmR2RInfo : IWasmR2RInfo + { + public Dictionary VirtualIpBases { get; } = new(); + public Dictionary UnwindData { get; } = new(); + + public bool TryGetVirtualIPBase(uint functionTableIndex, out ulong baseVirtualIP) + => VirtualIpBases.TryGetValue(functionTableIndex, out baseVirtualIP); + + public bool TryGetUnwindData(uint functionTableIndex, out TargetPointer unwindDataAddress) + { + if (UnwindData.TryGetValue(functionTableIndex, out ulong addr)) + { + unwindDataAddress = new TargetPointer(addr); + return true; + } + unwindDataAddress = TargetPointer.Null; + return false; + } + } + + private static TestPlaceholderTarget CreateTarget(MockMemorySpace.HeapFragment[] fragments) + { + TestPlaceholderTarget.Builder builder = new(WasmArch); + foreach (MockMemorySpace.HeapFragment fragment in fragments) + builder.MemoryBuilder.AddHeapFragment(fragment); + return builder.Build(); + } + + // Builds an R2R frame: [0] = function index, [4] = function-local virtual IP / 2. + private static MockMemorySpace.HeapFragment Frame(ulong address, uint functionIndex, uint localVirtualIPHalf, string name) + { + TargetTestHelpers helpers = new(WasmArch); + byte[] data = new byte[16]; + helpers.Write(data.AsSpan(0, sizeof(uint)), functionIndex); + helpers.Write(data.AsSpan(4, sizeof(uint)), localVirtualIPHalf); + return new MockMemorySpace.HeapFragment { Address = address, Data = data, Name = name }; + } + + private static MockMemorySpace.HeapFragment Blob(ulong address, byte[] uleb128, string name) + => new() { Address = address, Data = uleb128, Name = name }; + + [Fact] + public void TryGetFramePointer_NormalFrame_ReturnsSelf() + { + TestPlaceholderTarget target = CreateTarget([Frame(FramesBase, FuncIndexLeaf, 3, "leaf")]); + WasmUnwinder unwinder = new(target, new FakeWasmR2RInfo()); + + Assert.True(unwinder.TryGetFramePointer(new TargetPointer(FramesBase), out TargetPointer fp)); + Assert.Equal(FramesBase, fp.Value); + } + + [Fact] + public void TryGetFramePointer_BelowFloor_ReturnsFalse() + { + TestPlaceholderTarget target = CreateTarget([Frame(FramesBase, FuncIndexLeaf, 3, "leaf")]); + WasmUnwinder unwinder = new(target, new FakeWasmR2RInfo()); + + Assert.False(unwinder.TryGetFramePointer(new TargetPointer(0x800), out _)); + } + + [Fact] + public void TryGetFramePointer_TerminateMarker_ReturnsFalse() + { + // A frame whose first word is TERMINATE_R2R_STACK_WALK (1). + TestPlaceholderTarget target = CreateTarget([Frame(FramesBase, 1, 0, "terminator")]); + WasmUnwinder unwinder = new(target, new FakeWasmR2RInfo()); + + Assert.False(unwinder.TryGetFramePointer(new TargetPointer(FramesBase), out _)); + } + + [Fact] + public void TryGetFramePointer_LocallocIndirect_FollowsSavedFramePointer() + { + // localloc frame: first word is STACK_WALK_INDIRECT_TO_FRAMEPOINTER (0), and the real + // frame base pointer follows one pointer-sized slot later. + TargetTestHelpers helpers = new(WasmArch); + ulong indirectSp = FramesBase; + ulong realFp = FramesBase + 0x100; + + byte[] indirect = new byte[16]; + helpers.Write(indirect.AsSpan(0, sizeof(uint)), StackWalkSentinelIndirect); + helpers.WritePointer(indirect.AsSpan((int)helpers.PointerSize, helpers.PointerSize), realFp); + + TestPlaceholderTarget target = CreateTarget( + [ + new MockMemorySpace.HeapFragment { Address = indirectSp, Data = indirect, Name = "indirect" }, + Frame(realFp, FuncIndexLeaf, 3, "realFrame"), + ]); + WasmUnwinder unwinder = new(target, new FakeWasmR2RInfo()); + + Assert.True(unwinder.TryGetFramePointer(new TargetPointer(indirectSp), out TargetPointer fp)); + Assert.Equal(realFp, fp.Value); + } + + [Fact] + public void GetEstablishingFramePointerFromTerminator_ReturnsStoredFramePointer() + { + TargetTestHelpers helpers = new(WasmArch); + ulong terminatorSp = FramesBase; + ulong establishingFp = FramesBase + 0x200; + + byte[] terminator = new byte[16]; + helpers.Write(terminator.AsSpan(0, sizeof(uint)), 1u); // TERMINATE_R2R_STACK_WALK + helpers.WritePointer(terminator.AsSpan((int)helpers.PointerSize, helpers.PointerSize), establishingFp); + + TestPlaceholderTarget target = CreateTarget( + [new MockMemorySpace.HeapFragment { Address = terminatorSp, Data = terminator, Name = "terminator" }]); + WasmUnwinder unwinder = new(target, new FakeWasmR2RInfo()); + + Assert.Equal(establishingFp, unwinder.GetEstablishingFramePointerFromTerminator(new TargetPointer(terminatorSp)).Value); + } + + [Fact] + public void GetVirtualIP_ResolvesBasePlusLocalTimesTwo() + { + FakeWasmR2RInfo info = new(); + info.VirtualIpBases[FuncIndexLeaf] = VirtualIpBase; + + TestPlaceholderTarget target = CreateTarget([Frame(FramesBase, FuncIndexLeaf, 3, "leaf")]); + WasmUnwinder unwinder = new(target, info); + + // baseVirtualIP + (localVirtualIPHalf * 2) == 0x50000 + 6 + Assert.Equal(VirtualIpBase + 6, unwinder.GetVirtualIP(new TargetPointer(FramesBase)).Value); + } + + [Fact] + public void TryUnwindOneFrame_AdvancesBySingleByteFrameSize_AndYieldsCallerVirtualIP() + { + const uint leafFrameSize = 0x20; + ulong callerBase = FramesBase + leafFrameSize; + + FakeWasmR2RInfo info = new(); + info.VirtualIpBases[FuncIndexLeaf] = VirtualIpBase; + info.VirtualIpBases[FuncIndexCaller] = VirtualIpBase; + info.UnwindData[FuncIndexLeaf] = BlobsBase; + + TestPlaceholderTarget target = CreateTarget( + [ + Frame(FramesBase, FuncIndexLeaf, 3, "leaf"), + Frame(callerBase, FuncIndexCaller, 7, "caller"), + Blob(BlobsBase, [(byte)leafFrameSize], "leafUnwind"), // ULEB128 0x20 == 32 + ]); + WasmUnwinder unwinder = new(target, info); + + TargetPointer sp = new(FramesBase); + Assert.True(unwinder.TryUnwindOneFrame(ref sp, out TargetCodePointer ip)); + Assert.Equal(callerBase, sp.Value); + Assert.Equal(VirtualIpBase + 14, ip.Value); // caller local VIP 7*2 + } + + [Fact] + public void TryUnwindOneFrame_DecodesMultiByteFrameSize() + { + const uint leafFrameSize = 200; // ULEB128: 0xC8 0x01 + ulong callerBase = FramesBase + leafFrameSize; + + FakeWasmR2RInfo info = new(); + info.VirtualIpBases[FuncIndexLeaf] = VirtualIpBase; + info.VirtualIpBases[FuncIndexCaller] = VirtualIpBase; + info.UnwindData[FuncIndexLeaf] = BlobsBase; + + TestPlaceholderTarget target = CreateTarget( + [ + Frame(FramesBase, FuncIndexLeaf, 0, "leaf"), + Frame(callerBase, FuncIndexCaller, 1, "caller"), + Blob(BlobsBase, [0xC8, 0x01], "leafUnwind"), + ]); + WasmUnwinder unwinder = new(target, info); + + TargetPointer sp = new(FramesBase); + Assert.True(unwinder.TryUnwindOneFrame(ref sp, out _)); + Assert.Equal(callerBase, sp.Value); + } + + [Fact] + public void TryUnwindOneFrame_AtTerminator_ReturnsFalse() + { + TestPlaceholderTarget target = CreateTarget([Frame(FramesBase, 1, 0, "terminator")]); + WasmUnwinder unwinder = new(target, new FakeWasmR2RInfo()); + + TargetPointer sp = new(FramesBase); + Assert.False(unwinder.TryUnwindOneFrame(ref sp, out _)); + Assert.Equal(TargetPointer.Null, sp); + } + + [Fact] + public void TryGetFramePointer_LocallocToBelowFloor_ReturnsFalse() + { + // localloc frame whose saved real frame pointer is below the linear-stack floor. + TargetTestHelpers helpers = new(WasmArch); + ulong indirectSp = FramesBase; + + byte[] indirect = new byte[16]; + helpers.Write(indirect.AsSpan(0, sizeof(uint)), StackWalkSentinelIndirect); + helpers.WritePointer(indirect.AsSpan((int)helpers.PointerSize, helpers.PointerSize), 0x10ul); // below LinearStackFloor + + TestPlaceholderTarget target = CreateTarget( + [new MockMemorySpace.HeapFragment { Address = indirectSp, Data = indirect, Name = "indirect" }]); + WasmUnwinder unwinder = new(target, new FakeWasmR2RInfo()); + + Assert.False(unwinder.TryGetFramePointer(new TargetPointer(indirectSp), out _)); + } + + [Fact] + public void TryUnwindOneFrame_ZeroFrameSize_TerminatesCleanly() + { + FakeWasmR2RInfo info = new(); + info.VirtualIpBases[FuncIndexLeaf] = VirtualIpBase; + info.UnwindData[FuncIndexLeaf] = BlobsBase; + + TestPlaceholderTarget target = CreateTarget( + [ + Frame(FramesBase, FuncIndexLeaf, 3, "leaf"), + Blob(BlobsBase, [0x00], "zeroFrameSize"), // ULEB128 0 -> no progress + ]); + WasmUnwinder unwinder = new(target, info); + + TargetPointer sp = new(FramesBase); + Assert.False(unwinder.TryUnwindOneFrame(ref sp, out _)); + Assert.Equal(TargetPointer.Null, sp); + } + + [Fact] + public void TryUnwindOneFrame_MalformedUleb128_Throws() + { + FakeWasmR2RInfo info = new(); + info.UnwindData[FuncIndexLeaf] = BlobsBase; + + TestPlaceholderTarget target = CreateTarget( + [ + Frame(FramesBase, FuncIndexLeaf, 3, "leaf"), + // 5 continuation bytes with no terminator -> exceeds the 5-byte uint32 ULEB128 limit. + Blob(BlobsBase, [0x80, 0x80, 0x80, 0x80, 0x80], "malformed"), + ]); + WasmUnwinder unwinder = new(target, info); + + TargetPointer sp = new(FramesBase); + Assert.Throws(() => unwinder.TryUnwindOneFrame(ref sp, out _)); + } + + private const uint StackWalkSentinelIndirect = 0; +}