diff --git a/docs/design/datacontracts/ExecutionManager.md b/docs/design/datacontracts/ExecutionManager.md index d0de293a21b763..9513b50818058c 100644 --- a/docs/design/datacontracts/ExecutionManager.md +++ b/docs/design/datacontracts/ExecutionManager.md @@ -28,6 +28,9 @@ public struct CodeBlockHandle void GetMethodRegionInfo(CodeBlockHandle codeInfoHandle, out uint hotSize, out TargetPointer coldStart, out uint coldSize); // Attempt to get the method desc of an entrypoint TargetPointer NonVirtualEntry2MethodDesc(TargetCodePointer entrypoint); + // Map a method entry point to the code start that diagnostics report and that resolves through + // GetCodeBlockHandle. Other addresses are returned unchanged. + TargetCodePointer GetDiagnosticCodeStartFromEntryPoint(TargetCodePointer entryPoint); // Gets the unwind info of the code block at the specified code pointer TargetPointer GetUnwindInfo(CodeBlockHandle codeInfoHandle); @@ -216,6 +219,10 @@ virtual-IP ranges bypass native thunk classification. | `ExceptionLookupTableEntry` | *(type size)* | `uint32` | Size of an exception lookup table entry in bytes | | `ExceptionLookupTableEntry` | `ExceptionInfoRVA` | `uint32` | RVA of the exception clause data | | `ExceptionLookupTableEntry` | `MethodStartRVA` | `uint32` | RVA of the method start | +| `FunctionTableIndexRangeSection` | `MinFunctionTableIndex` | `uint32` | First runtime-global shared function-table index owned by the R2R module | +| `FunctionTableIndexRangeSection` | `Next` | `pointer` | Pointer to the next registered WASM R2R function-table range | +| `FunctionTableIndexRangeSection` | `NumRuntimeFunctions` | `uint32` | Number of consecutive RUNTIME_FUNCTION entries owned by the R2R module | +| `FunctionTableIndexRangeSection` | `R2RModule` | `pointer` | Pointer to the Module that owns this function-table range | | `HashMap` | `Buckets` | `pointer` | Pointer to the buckets of a `HashMap` | | `HostCodeHeap` | `BaseAddress` | `pointer` | Pointer to the base of the committed memory region | | `HostCodeHeap` | `CurrentAddress` | `pointer` | Pointer to the last available committed byte in the region | @@ -226,7 +233,12 @@ virtual-IP ranges bypass native thunk classification. | `InterpreterRealCodeHeader` | `JitEHInfo` | `pointer` | Pointer to the `EE_ILEXCEPTION` containing exception clauses for interpreter code | | `InterpreterRealCodeHeader` | `MethodDesc` | `pointer` | Pointer to the corresponding `MethodDesc` for interpreter code | | `LoaderCodeHeap` | `LoaderHeap` | `pointer` | Offset of the embedded `ExplicitControlLoaderHeap` within the `LoaderCodeHeap` object; adding this to the object's base address yields the loader heap address | +| `MethodDesc` | `CodeData` | `pointer` | Pointer to per-method code data containing entry-point and code-versioning state | +| `MethodDesc` | `InterpreterCode` | `pointer` | Pointer to the method's `InterpByteCodeStart`, or the poison value 1 if the method will never be interpreted (only defined if `FEATURE_INTERPRETER` is enabled) | +| `MethodDescCodeData` | `TemporaryEntryPoint` | `CodePointer` | Temporary code entry point used before the method has a stable entry point | | `Module` | `ReadyToRunInfo` | `pointer` | Pointer to the module's ReadyToRun information | +| `PortableEntryPoint` | `ActualCode` | `pointer` | Native code for the entrypoint; on WebAssembly R2R code this is a function-table index (only defined if `FeaturePortableEntrypoints` is enabled) | +| `PortableEntryPoint` | `Flags` | `int32` | Portable entrypoint flags; `0x4` means the interpreter entrypoint is preferred over `ActualCode` (only defined if `FeaturePortableEntrypoints` is enabled) | | `PortableEntryPoint` | `MethodDesc` | `pointer` | Method desc of portable entrypoint (only defined if `FeaturePortableEntrypoints` is enabled) | | `R2RExceptionClause` | *(type size)* | `uint32` | Size of a ReadyToRun exception clause in bytes | | `R2RExceptionClause` | `ClassToken` | `uint32` | Union field: ClassToken or FilterOffset | @@ -287,6 +299,7 @@ virtual-IP ranges bypass native thunk classification. | --- | --- | --- | | `EEJitManagerAddress` | `pointer` | Address of the global pointer to the EEJitManager instance (read a TargetPointer from this address to obtain the instance address) | | `ExecutionManagerCodeRangeMapAddress` | `pointer` | Pointer to the global RangeSectionMap | +| `FunctionTableIndexRangeList` | `pointer` | Pointer to the head pointer of the registered WASM R2R function-table range list | | `GCInfoVersion` | `uint32` | JITted code GCInfo version | | `HashMapSlotsPerBucket` | `uint32` | Number of slots in each bucket of a `HashMap` | | `HashMapValueMask` | `uint64` | Bitmask used when storing values in a `HashMap` | @@ -491,6 +504,60 @@ TargetPointer IExecutionManager.NonVirtualEntry2MethodDesc(TargetCodePointer ent ``` +`GetDiagnosticCodeStartFromEntryPoint` mirrors the native `GetDiagnosticCodeStartFromEntryPoint` (`src/coreclr/vm/precode.cpp`). Without portable entrypoints it maps an interpreter precode to its bytecode through `PrecodeStubs.GetInterpreterCodeFromInterpreterPrecodeIfPresent`. With portable entrypoints (WebAssembly), an entry point is a `PortableEntryPoint` rather than code: an interpreted method maps to its `InterpByteCodeStart`, and a native ReadyToRun method maps its function-table index to the synthetic virtual IP registered in the virtual-IP range list, so that the result resolves through `GetCodeBlockHandle`. + +```csharp +// Constants from native code +const int PortableEntryPointPrefersInterpreterEntryPoint = 0x4; // PortableEntryPoint::kPrefersInterpreterEntryPoint +const ulong InterpreterCodePoison = 1; // INTERPRETER_CODE_POISON + +TargetCodePointer IExecutionManager.GetDiagnosticCodeStartFromEntryPoint(TargetCodePointer entryPoint) +{ + if (entryPoint == TargetCodePointer.Null) + return entryPoint; + + if (!FeatureFlags.IsEnabled(RuntimeFeature.PortableEntrypoints)) + return PrecodeStubs.GetInterpreterCodeFromInterpreterPrecodeIfPresent(entryPoint); + + // On any read failure, entryPoint is returned unchanged. + // An address in a code range (including a Wasm R2R virtual IP) is already a code start. + if (/* range section found for entryPoint - see RangeSectionMap and the virtual-IP range list */) + return entryPoint; + + Data.PortableEntryPoint pep = // read PortableEntryPoint at entryPoint + if (pep.MethodDesc == TargetPointer.Null) + return entryPoint; + + Data.MethodDesc md = // read MethodDesc at pep.MethodDesc + if (md.InterpreterCode is TargetPointer interpreterCode && interpreterCode != TargetPointer.Null && interpreterCode != InterpreterCodePoison) + return new TargetCodePointer(interpreterCode); + + // Native R2R portable entry points store a Wasm function-table index in ActualCode. As in native code, + // this applies only to the method's own portable entry point, which is currently its temporary entry point. + if (pep.ActualCode == TargetPointer.Null + || (pep.Flags & PortableEntryPointPrefersInterpreterEntryPoint) != 0 + || md.CodeData == TargetPointer.Null + || /* MethodDescCodeData.TemporaryEntryPoint at md.CodeData */ != entryPoint) + { + return entryPoint; + } + + return TryGetWasmVirtualIPFromFunctionTableIndex((uint)pep.ActualCode, out ulong virtualIP) + ? new TargetCodePointer(virtualIP) + : entryPoint; +} + +// Mirrors ExecutionManager::GetWasmVirtualIPFromFunctionTableIndex. Also used by the WebAssembly stack walk. +bool TryGetWasmVirtualIPFromFunctionTableIndex(uint functionTableIndex, out ulong virtualIP) +{ + // Walk the FunctionTableIndexRangeSection list headed by *FunctionTableIndexRangeList (with cycle + // detection) to the section where MinFunctionTableIndex <= functionTableIndex < MinFunctionTableIndex + NumRuntimeFunctions. + // Starting at RuntimeFunctions[functionTableIndex - MinFunctionTableIndex] of the section's + // R2RModule's ReadyToRunInfo, step back past funclet entries to the controlling function, then + // virtualIP = ReadyToRunInfo.MinVirtualIP + RuntimeFunction.BeginAddress. +} +``` + The `CodeBlock` encapsulates the `MethodDesc` data from the target runtime together with the start of the jitted method ```csharp diff --git a/docs/design/datacontracts/RuntimeTypeSystem.md b/docs/design/datacontracts/RuntimeTypeSystem.md index b6bbb7aee425b8..821056bd324ab6 100644 --- a/docs/design/datacontracts/RuntimeTypeSystem.md +++ b/docs/design/datacontracts/RuntimeTypeSystem.md @@ -603,6 +603,7 @@ static class RuntimeTypeSystem_1_Helpers | `MethodDesc` | `Flags` | `uint16` | The method's flags | | `MethodDesc` | `Flags3AndTokenRemainder` | `uint16` | More flags for the method, and the low bits of the method's token's RID | | `MethodDesc` | `GCCoverageInfo` | `pointer` | The method's GCCover debug info, if supported | +| `MethodDesc` | `InterpreterCode` | `pointer` | Pointer to the method's `InterpByteCodeStart`, or the poison value 1 if the method will never be interpreted (only defined if `FEATURE_INTERPRETER` is enabled) | | `MethodDesc` | `Slot` | `uint16` | The method's slot | | `MethodDescChunk` | *(type size)* | `uint32` | Size of the data descriptor layout | | `MethodDescChunk` | `Count` | `uint8` | The number of MethodDesc entries in this chunk, minus 1. | diff --git a/docs/design/datacontracts/data-descriptor-meanings.json b/docs/design/datacontracts/data-descriptor-meanings.json index ca3300efc44b64..501f65d419a707 100644 --- a/docs/design/datacontracts/data-descriptor-meanings.json +++ b/docs/design/datacontracts/data-descriptor-meanings.json @@ -199,6 +199,10 @@ "FramedMethodFrame.TransitionBlockPtr": "Pointer to Frame's TransitionBlock", "FuncEvalFrame.DebuggerEvalPtr": "Pointer to the Frame's DebuggerEval object", "FuncEvalFrame.ReturnAddress": "Return address of the frame", + "FunctionTableIndexRangeSection.MinFunctionTableIndex": "First runtime-global shared function-table index owned by the R2R module", + "FunctionTableIndexRangeSection.Next": "Pointer to the next registered WASM R2R function-table range", + "FunctionTableIndexRangeSection.NumRuntimeFunctions": "Number of consecutive RUNTIME_FUNCTION entries owned by the R2R module", + "FunctionTableIndexRangeSection.R2RModule": "Pointer to the Module that owns this function-table range", "GCAllocContext.AllocBytes": "Number of bytes allocated on SOH by this context", "GCAllocContext.AllocBytesLoh": "Number of bytes allocated not on SOH by this context", "GCAllocContext.Limit": "Allocation limit pointer", @@ -362,6 +366,7 @@ "MethodDesc.Flags": "The method's flags", "MethodDesc.Flags3AndTokenRemainder": "More flags for the method, and the low bits of the method's token's RID", "MethodDesc.GCCoverageInfo": "The method's GCCover debug info, if supported", + "MethodDesc.InterpreterCode": "Pointer to the method's `InterpByteCodeStart`, or the poison value 1 if the method will never be interpreted (only defined if `FEATURE_INTERPRETER` is enabled)", "MethodDesc.Size": "Base size for mcIL classification", "MethodDesc.Slot": "The method's slot", "MethodDescChunk.Count": "The number of MethodDesc entries in this chunk, minus 1.", @@ -457,6 +462,8 @@ "PInvokeMethodDesc.Size": "Base size for mcPInvoke classification", "PlatformMetadata.CodePointerFlags": "fields describing the behavior of target code pointers", "PlatformMetadata.PrecodeMachineDescriptor": "precode stub-related platform specific properties", + "PortableEntryPoint.ActualCode": "Native code for the entrypoint; on WebAssembly R2R code this is a function-table index (only defined if `FeaturePortableEntrypoints` is enabled)", + "PortableEntryPoint.Flags": "Portable entrypoint flags; `0x4` means the interpreter entrypoint is preferred over `ActualCode` (only defined if `FeaturePortableEntrypoints` is enabled)", "PortableEntryPoint.MethodDesc": "Method desc of portable entrypoint (only defined if `FeaturePortableEntrypoints` is enabled)", "PrecodeMachineDescriptor.FixupBytes": "Assembly code of a FixupStub", "PrecodeMachineDescriptor.FixupIgnoredBytes": "Bytes to ignore when comparing FixupBytes to an actual block of memory in the target process.", @@ -760,6 +767,7 @@ "FieldOffsetDynamicRVA": "Sentinel FieldDesc offset for an EnC-added RVA field whose enclosing type is not yet loaded", "FieldOffsetNewEnc": "Sentinel offset value stored in FieldDesc::DWord2 for added fields whose storage has not yet been allocated.", "FinalizerThread": "Pointer to the finalizer thread", + "FunctionTableIndexRangeList": "Pointer to the head pointer of the registered WASM R2R function-table range list", "FreeObjectMethodTable": "A pointer to the address of a MethodTable used by the GC to indicate reclaimed memory", "GCGlobalMechanisms": "Pointer to counters recording use of global GC mechanisms", "GCHeapAllocAllocated": "Highest address allocated by Alloc (in workstation builds)", diff --git a/src/coreclr/debug/daccess/dacdbiimpl.cpp b/src/coreclr/debug/daccess/dacdbiimpl.cpp index dab05de698687e..22763fd1875190 100644 --- a/src/coreclr/debug/daccess/dacdbiimpl.cpp +++ b/src/coreclr/debug/daccess/dacdbiimpl.cpp @@ -7715,11 +7715,11 @@ HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::EnumerateAsyncLocals(VMPTR_Method { return hr; } - nativeCodeStartAddr = PCODEToPINSTR(requestedNativeCodeVersion.GetNativeCode()); + nativeCodeStartAddr = PCODEToPINSTR(GetInterpreterCodeFromEntryPointIfPresent(requestedNativeCodeVersion.GetNativeCode())); } else { - nativeCodeStartAddr = PCODEToPINSTR(pMethodDesc->GetNativeCode()); + nativeCodeStartAddr = PCODEToPINSTR(GetInterpreterCodeFromEntryPointIfPresent(pMethodDesc->GetNativeCode())); } DebugInfoRequest request; diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index 0285901c45c0ab..3cb739d9a1003a 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -782,6 +782,9 @@ CDAC_TYPE_FIELD(MethodDesc, T_UINT16, Flags, cdac_data::Flags) CDAC_TYPE_FIELD(MethodDesc, T_UINT16, Flags3AndTokenRemainder, cdac_data::Flags3AndTokenRemainder) CDAC_TYPE_FIELD(MethodDesc, T_UINT8, EntryPointFlags, cdac_data::EntryPointFlags) CDAC_TYPE_FIELD(MethodDesc, T_POINTER, CodeData, cdac_data::CodeData) +#ifdef FEATURE_INTERPRETER +CDAC_TYPE_FIELD(MethodDesc, T_POINTER, InterpreterCode, cdac_data::InterpreterCode) +#endif // FEATURE_INTERPRETER #ifdef HAVE_GCCOVER CDAC_TYPE_FIELD(MethodDesc, T_POINTER, GCCoverageInfo, offsetof(MethodDesc, m_GcCover)) #endif // HAVE_GCCOVER @@ -862,7 +865,9 @@ CDAC_TYPE_END(CodePointer) #ifdef FEATURE_PORTABLE_ENTRYPOINTS CDAC_TYPE_BEGIN(PortableEntryPoint) CDAC_TYPE_INDETERMINATE(PortableEntryPoint) +CDAC_TYPE_FIELD(PortableEntryPoint, T_POINTER, ActualCode, cdac_data::ActualCode) CDAC_TYPE_FIELD(PortableEntryPoint, T_POINTER, MethodDesc, cdac_data::MethodDesc) +CDAC_TYPE_FIELD(PortableEntryPoint, T_INT32, Flags, cdac_data::Flags) CDAC_TYPE_END(PortableEntryPoint) #endif diff --git a/src/coreclr/vm/method.hpp b/src/coreclr/vm/method.hpp index 1c6e13fd9c1c73..c49974094cd398 100644 --- a/src/coreclr/vm/method.hpp +++ b/src/coreclr/vm/method.hpp @@ -1963,6 +1963,7 @@ class MethodDesc WORD m_wFlags; // See MethodDescFlags PTR_MethodDescCodeData m_codeData; #ifdef FEATURE_INTERPRETER +// [cDAC] [ExecutionManager]: Contract depends on the value of INTERPRETER_CODE_POISON. #define INTERPRETER_CODE_POISON 1 PTR_InterpByteCodeStart m_interpreterCode; public: @@ -2408,6 +2409,9 @@ template<> struct cdac_data static constexpr size_t Flags3AndTokenRemainder = offsetof(MethodDesc, m_wFlags3AndTokenRemainder); static constexpr size_t EntryPointFlags = offsetof(MethodDesc, m_bFlags4); static constexpr size_t CodeData = offsetof(MethodDesc, m_codeData); +#ifdef FEATURE_INTERPRETER + static constexpr size_t InterpreterCode = offsetof(MethodDesc, m_interpreterCode); +#endif // FEATURE_INTERPRETER }; #ifndef DACCESS_COMPILE diff --git a/src/coreclr/vm/precode_portable.hpp b/src/coreclr/vm/precode_portable.hpp index 976700f6519da7..7853b46844fb79 100644 --- a/src/coreclr/vm/precode_portable.hpp +++ b/src/coreclr/vm/precode_portable.hpp @@ -30,6 +30,7 @@ class PortableEntryPoint final MethodDesc* _pMD; Volatile _pInterpreterData; + // [cDAC] [ExecutionManager]: Contract depends on the value of kPrefersInterpreterEntryPoint. enum PortableEntryPointFlag { kNone = 0, @@ -257,7 +258,9 @@ static_assert(offsetof(ClosedStaticRetBufPortableEntryPoint, _entryPoint) == TAR template<> struct cdac_data { + static constexpr size_t ActualCode = offsetof(PortableEntryPoint, _pActualCode); static constexpr size_t MethodDesc = offsetof(PortableEntryPoint, _pMD); + static constexpr size_t Flags = offsetof(PortableEntryPoint, _flags); static_assert(offsetof(PortableEntryPoint, _pActualCode) == 0, "CLR ABI requires _pActualCode to be at offset 0 of PortableEntryPoint"); }; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IExecutionManager.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IExecutionManager.cs index ee41ee369afae3..e138bf9ab5dc4c 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IExecutionManager.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IExecutionManager.cs @@ -110,6 +110,11 @@ public interface IExecutionManager : IContract TargetPointer GetFuncletStartAddress(CodeBlockHandle codeInfoHandle) => throw new NotImplementedException(); void GetMethodRegionInfo(CodeBlockHandle codeInfoHandle, out uint hotSize, out TargetPointer coldStart, out uint coldSize) => throw new NotImplementedException(); TargetPointer NonVirtualEntry2MethodDesc(TargetCodePointer entrypoint) => throw new NotImplementedException(); + // Map a method entry point to the code start that diagnostics report and that resolves through + // GetCodeBlockHandle: interpreter bytecode for interpreter precodes and interpreted portable entry + // points, or the synthetic virtual IP for Wasm ReadyToRun portable entry points. Other addresses + // are returned unchanged. Mirrors GetDiagnosticCodeStartFromEntryPoint in native code (precode.cpp). + TargetCodePointer GetDiagnosticCodeStartFromEntryPoint(TargetCodePointer entryPoint) => throw new NotImplementedException(); bool IsFunclet(CodeBlockHandle codeInfoHandle) => throw new NotImplementedException(); bool IsFilterFunclet(CodeBlockHandle codeInfoHandle) => throw new NotImplementedException(); TargetPointer GetUnwindInfo(CodeBlockHandle codeInfoHandle) => throw new NotImplementedException(); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs index ac4f7600e4df6c..513728c9fc0776 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs @@ -24,6 +24,13 @@ internal sealed partial class ExecutionManagerCore : IExecutionManager private readonly InterpreterJitManager _interpreterJitManager; private readonly CachedValue _thePreStub; private readonly TargetPointer _virtualIPRangeListAddress; + private WasmFunctionTableIndexLookup? _wasmFunctionTableIndexLookup; + + // Mirrors PortableEntryPoint::kPrefersInterpreterEntryPoint in src/coreclr/vm/precode_portable.hpp. + private const int PortableEntryPointPrefersInterpreterEntryPoint = 0x4; + + // Mirrors INTERPRETER_CODE_POISON in src/coreclr/vm/method.hpp. + private const ulong InterpreterCodePoison = 1; public ExecutionManagerCore(Target target, TargetPointer topRangeSectionMapAddress) { @@ -418,6 +425,64 @@ TargetPointer IExecutionManager.NonVirtualEntry2MethodDesc(TargetCodePointer ent return TargetPointer.Null; } + TargetCodePointer IExecutionManager.GetDiagnosticCodeStartFromEntryPoint(TargetCodePointer entryPoint) + { + if (entryPoint == TargetCodePointer.Null) + return entryPoint; + + if (!_target.Contracts.FeatureFlags.IsEnabled(RuntimeFeature.PortableEntrypoints)) + return _target.Contracts.PrecodeStubs.GetInterpreterCodeFromInterpreterPrecodeIfPresent(entryPoint); + + try + { + return GetDiagnosticCodeStartFromPortableEntryPoint(entryPoint); + } + catch (VirtualReadException) + { + return entryPoint; + } + } + + // Mirrors the FEATURE_PORTABLE_ENTRYPOINTS paths of GetInterpreterCodeFromEntryPointIfPresent and + // GetDiagnosticCodeStartFromEntryPoint in src/coreclr/vm/precode.cpp. + private TargetCodePointer GetDiagnosticCodeStartFromPortableEntryPoint(TargetCodePointer entryPoint) + { + // An address in a code range (including a Wasm R2R virtual IP) is already a code start. + RangeSection range = RangeSection.Find(_target, _topRangeSectionMapAddress, _rangeSectionMapLookup, _virtualIPRangeListAddress, entryPoint); + if (range.Data != null) + return entryPoint; + + Data.PortableEntryPoint portableEntryPoint = _target.ProcessedData.GetOrAdd(entryPoint.AsTargetPointer); + if (portableEntryPoint.MethodDesc == TargetPointer.Null) + return entryPoint; + + Data.MethodDesc methodDesc = _target.ProcessedData.GetOrAdd(portableEntryPoint.MethodDesc); + if (methodDesc.InterpreterCode is TargetPointer interpreterCode + && interpreterCode != TargetPointer.Null + && interpreterCode.Value != InterpreterCodePoison) + { + return new TargetCodePointer(interpreterCode); + } + + // Native R2R portable entry points store a Wasm function-table index rather than an address + // registered with the ExecutionManager. Map it to the corresponding synthetic virtual IP. As in + // native code, this applies only to the method's own portable entry point, which is currently + // its temporary entry point. + if (portableEntryPoint.ActualCode == TargetPointer.Null + || (portableEntryPoint.Flags & PortableEntryPointPrefersInterpreterEntryPoint) != 0 + || methodDesc.CodeData == TargetPointer.Null + || _target.ProcessedData.GetOrAdd(methodDesc.CodeData).TemporaryEntryPoint != entryPoint) + { + return entryPoint; + } + + _wasmFunctionTableIndexLookup ??= new WasmFunctionTableIndexLookup(_target); + uint functionTableIndex = (uint)portableEntryPoint.ActualCode.Value; + return _wasmFunctionTableIndexLookup.TryGetVirtualIPBase(functionTableIndex, out ulong virtualIP) + ? new TargetCodePointer(virtualIP) + : entryPoint; + } + bool IExecutionManager.IsFunclet(CodeBlockHandle codeInfoHandle) { // Interpreter code has no native unwind info and therefore no funclets. diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManager_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManager_1.cs index 8c435ac9ed16c3..e1ba31d35be73f 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManager_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManager_1.cs @@ -22,6 +22,7 @@ internal ExecutionManager_1(Target target) public TargetPointer GetFuncletStartAddress(CodeBlockHandle codeInfoHandle) => _executionManagerCore.GetFuncletStartAddress(codeInfoHandle); public void GetMethodRegionInfo(CodeBlockHandle codeInfoHandle, out uint hotSize, out TargetPointer coldStart, out uint coldSize) => _executionManagerCore.GetMethodRegionInfo(codeInfoHandle, out hotSize, out coldStart, out coldSize); public TargetPointer NonVirtualEntry2MethodDesc(TargetCodePointer entrypoint) => _executionManagerCore.NonVirtualEntry2MethodDesc(entrypoint); + public TargetCodePointer GetDiagnosticCodeStartFromEntryPoint(TargetCodePointer entryPoint) => _executionManagerCore.GetDiagnosticCodeStartFromEntryPoint(entryPoint); public bool IsFunclet(CodeBlockHandle codeInfoHandle) => _executionManagerCore.IsFunclet(codeInfoHandle); public bool IsFilterFunclet(CodeBlockHandle codeInfoHandle) => _executionManagerCore.IsFilterFunclet(codeInfoHandle); public TargetPointer GetUnwindInfo(CodeBlockHandle codeInfoHandle) => _executionManagerCore.GetUnwindInfo(codeInfoHandle); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/Helpers/WasmFunctionTableIndexLookup.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/Helpers/WasmFunctionTableIndexLookup.cs new file mode 100644 index 00000000000000..3137eaaaef66d5 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/Helpers/WasmFunctionTableIndexLookup.cs @@ -0,0 +1,109 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace Microsoft.Diagnostics.DataContractReader.ExecutionManagerHelpers; + +/// +/// Resolves WebAssembly ReadyToRun function-table indices, mirroring the native +/// ExecutionManager::{FindFunctionTableIndexRangeSection, GetWasmVirtualIPFromFunctionTableIndex} +/// in src/coreclr/vm/codeman.cpp. An index is resolved against the +/// FunctionTableIndexRangeList to its owning module's , and the +/// corresponding RUNTIME_FUNCTION supplies the funclet flag, base virtual IP, and unwind data. +/// +internal sealed class WasmFunctionTableIndexLookup +{ + // Reader resource budget, not a limit imposed by native range registration. + private const int MaxFunctionTableIndexRangeNodes = 65_536; + + private readonly Target _target; + private readonly RuntimeFunctionLookup _runtimeFunctions; + + public WasmFunctionTableIndexLookup(Target target) + { + _target = target; + _runtimeFunctions = RuntimeFunctionLookup.Create(target, isWasm: true); + } + + // 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); + HashSet visited = new(); + while (current != TargetPointer.Null) + { + if (visited.Count == MaxFunctionTableIndexRangeNodes || !visited.Add(current)) + return 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); + + // Mirrors ExecutionManager::GetWasmVirtualIPFromFunctionTableIndex. + 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 (_runtimeFunctions.IsFunclet(runtimeFunction)) + { + if (localIndex == 0) + return false; + localIndex--; + continue; + } + + baseVirtualIP = minVirtualIP.Value + _runtimeFunctions.GetBeginAddress(runtimeFunction); + 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/WasmR2RInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/Wasm/WasmR2RInfo.cs index f18d6c4bdf7fb6..d0c3d88455a11b 100644 --- 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 @@ -6,97 +6,21 @@ namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers.Wasm; /// -/// cDAC implementation of , mirroring the native -/// ExecutionManager::{FindFunctionTableIndexRangeSection, -/// 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. +/// cDAC implementation of , backed by the ExecutionManager's +/// . /// internal sealed class WasmR2RInfo : IWasmR2RInfo { - private readonly Target _target; - private readonly RuntimeFunctionLookup _runtimeFunctions; + private readonly WasmFunctionTableIndexLookup _lookup; public WasmR2RInfo(Target target) { - _target = target; - _runtimeFunctions = RuntimeFunctionLookup.Create(target, isWasm: true); + _lookup = new WasmFunctionTableIndexLookup(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 (_runtimeFunctions.IsFunclet(runtimeFunction)) - { - if (localIndex == 0) - return false; - localIndex--; - continue; - } - - baseVirtualIP = minVirtualIP.Value + _runtimeFunctions.GetBeginAddress(runtimeFunction); - return true; - } - } + => _lookup.TryGetVirtualIPBase(functionTableIndex, out baseVirtualIP); 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; - } + => _lookup.TryGetUnwindData(functionTableIndex, out unwindDataAddress); } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/MethodDesc.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/MethodDesc.cs index e2026546cea6aa..70973cfb953aa6 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/MethodDesc.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/MethodDesc.cs @@ -12,6 +12,7 @@ internal sealed partial class MethodDesc : IData [Field] public partial ushort Flags3AndTokenRemainder { get; } [Field] public partial byte EntryPointFlags { get; } [Field] public partial TargetPointer CodeData { get; } + [Field] public partial TargetPointer? InterpreterCode { get; } [Field] public partial TargetPointer? GCCoverageInfo { get; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/PortableEntryPoint.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/PortableEntryPoint.cs index 103f7f0ab689e4..576e0635dc1755 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/PortableEntryPoint.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/PortableEntryPoint.cs @@ -6,5 +6,7 @@ namespace Microsoft.Diagnostics.DataContractReader.Data; [CdacType(nameof(DataType.PortableEntryPoint))] internal sealed partial class PortableEntryPoint : IData { + [Field] public partial TargetPointer ActualCode { get; } [Field] public partial TargetPointer MethodDesc { get; } + [Field] public partial int Flags { get; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataMethodInstance.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataMethodInstance.cs index 41f60d58b2ed13..f3c64bbc836e0b 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataMethodInstance.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataMethodInstance.cs @@ -370,7 +370,7 @@ int IXCLRDataMethodInstance.GetAddressRangesByILOffset( } TargetCodePointer nativeCode = _target.Contracts.RuntimeTypeSystem.GetNativeCode(_methodDesc); - TargetCodePointer pCode = _target.Contracts.PrecodeStubs.GetInterpreterCodeFromInterpreterPrecodeIfPresent(nativeCode); + TargetCodePointer pCode = _target.Contracts.ExecutionManager.GetDiagnosticCodeStartFromEntryPoint(nativeCode); TargetPointer codeStart = pCode.ToAddress(_target); if (!_target.Contracts.DebugInfo.HasDebugInfo(pCode)) throw Marshal.GetExceptionForHR(HResults.E_FAIL)!; @@ -460,7 +460,7 @@ int IXCLRDataMethodInstance.GetILAddressMap(uint mapLen, uint* mapNeeded, [In, O try { TargetCodePointer nativeCode = _target.Contracts.RuntimeTypeSystem.GetNativeCode(_methodDesc); - TargetCodePointer pCode = _target.Contracts.PrecodeStubs.GetInterpreterCodeFromInterpreterPrecodeIfPresent(nativeCode); + TargetCodePointer pCode = _target.Contracts.ExecutionManager.GetDiagnosticCodeStartFromEntryPoint(nativeCode); TargetPointer codeStart = pCode.ToAddress(_target); // No debug info exists at all (e.g. ILStubs). @@ -548,7 +548,7 @@ private ClrDataAddressRange GetMethodExtent() { IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem; TargetCodePointer nativeCode = rts.GetNativeCode(_methodDesc); - TargetCodePointer code = _target.Contracts.PrecodeStubs.GetInterpreterCodeFromInterpreterPrecodeIfPresent(nativeCode); + TargetCodePointer code = _target.Contracts.ExecutionManager.GetDiagnosticCodeStartFromEntryPoint(nativeCode); if (code == TargetCodePointer.Null) { code = nativeCode; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs index 86b12f4bea666b..d2bf34e23325c7 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs @@ -2921,7 +2921,7 @@ public int GetNativeCodeInfo(ulong vmAssembly, uint functionToken, NativeCodeFun TargetCodePointer nativeCode = rts.GetNativeCode(methodDescHandle); if (nativeCode != TargetCodePointer.Null) { - nativeCode = _target.Contracts.PrecodeStubs.GetInterpreterCodeFromInterpreterPrecodeIfPresent(nativeCode); + nativeCode = _target.Contracts.ExecutionManager.GetDiagnosticCodeStartFromEntryPoint(nativeCode); pCodeInfo->hotRegion.pAddress = nativeCode.ToAddress(_target).ToClrDataAddress(_target); IExecutionManager executionManager = _target.Contracts.ExecutionManager; @@ -2975,7 +2975,7 @@ public int GetNativeCodeInfoForAddr(ulong codeAddress, NativeCodeFunctionData* p TargetCodePointer code = ((ClrDataAddress)codeAddress).ToTargetCodePointer(_target); try { - code = _target.Contracts.PrecodeStubs.GetInterpreterCodeFromInterpreterPrecodeIfPresent(code); + code = _target.Contracts.ExecutionManager.GetDiagnosticCodeStartFromEntryPoint(code); } catch (VirtualReadException) { @@ -6483,6 +6483,7 @@ public int EnumerateAsyncLocals(ulong vmMethod, ulong codeAddr, uint state, if (pCode != TargetCodePointer.Null) { + pCode = _target.Contracts.ExecutionManager.GetDiagnosticCodeStartFromEntryPoint(pCode); IReadOnlyList suspensionPoints = _target.Contracts.DebugInfo.GetAsyncSuspensionPoints(pCode); if (state < (uint)suspensionPoints.Count) { diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.IXCLRDataProcess.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.IXCLRDataProcess.cs index 0104de6c03b401..dfe7b2ce59de5c 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.IXCLRDataProcess.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.IXCLRDataProcess.cs @@ -298,7 +298,7 @@ int IXCLRDataProcess.GetRuntimeNameByAddress( IExecutionManager eman = _target.Contracts.ExecutionManager; string? resultName = null; - TargetCodePointer managedCodeAddr = _target.Contracts.PrecodeStubs.GetInterpreterCodeFromInterpreterPrecodeIfPresent(codeAddr); + TargetCodePointer managedCodeAddr = _target.Contracts.ExecutionManager.GetDiagnosticCodeStartFromEntryPoint(codeAddr); if (eman.GetCodeBlockHandle(managedCodeAddr) is CodeBlockHandle codeBlock) { if (displacement is not null) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs index ede4279d218063..0083167031b474 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs @@ -2397,7 +2397,7 @@ int ISOSDacInterface.GetMethodDescData(ClrDataAddress addr, ClrDataAddress ip, D if (nativeCodeAddr != TargetCodePointer.Null) { data->bHasNativeCode = 1; - data->NativeCodeAddr = _target.Contracts.PrecodeStubs.GetInterpreterCodeFromInterpreterPrecodeIfPresent(nativeCodeAddr).ToAddress(_target).ToClrDataAddress(_target); + data->NativeCodeAddr = _target.Contracts.ExecutionManager.GetDiagnosticCodeStartFromEntryPoint(nativeCodeAddr).ToAddress(_target).ToClrDataAddress(_target); } else { @@ -2609,7 +2609,7 @@ private void CopyNativeCodeVersionToReJitData( ILCodeVersionHandle ilCodeVersion = cv.GetILCodeVersion(nativeCodeVersion); TargetCodePointer nativeCode = cv.GetNativeCode(nativeCodeVersion); - pReJitData->NativeCodeAddr = _target.Contracts.PrecodeStubs.GetInterpreterCodeFromInterpreterPrecodeIfPresent(nativeCode).Value; + pReJitData->NativeCodeAddr = _target.Contracts.ExecutionManager.GetDiagnosticCodeStartFromEntryPoint(nativeCode).Value; if (cv.GetSource(ilCodeVersion) != CodeVersionSource.ReJIT) { pReJitData->rejitID = 0; @@ -5485,7 +5485,7 @@ int ISOSDacInterface5.GetTieredVersions( int count = 0; foreach (NativeCodeVersionHandle nativeCodeVersionHandle in codeVersions.GetNativeCodeVersions(methodDescPtr, ilCodeVersionHandle)) { - TargetCodePointer nativeCode = _target.Contracts.PrecodeStubs.GetInterpreterCodeFromInterpreterPrecodeIfPresent(codeVersions.GetNativeCode(nativeCodeVersionHandle)); + TargetCodePointer nativeCode = _target.Contracts.ExecutionManager.GetDiagnosticCodeStartFromEntryPoint(codeVersions.GetNativeCode(nativeCodeVersionHandle)); TargetPointer nativeCodeAddr = nativeCode.ToAddress(_target); nativeCodeAddrs[count].nativeCodeAddr = nativeCodeAddr.ToClrDataAddress(_target); nativeCodeAddrs[count].nativeCodeVersionNodePtr = nativeCodeVersionHandle.CodeVersionNodeAddress.ToClrDataAddress(_target); diff --git a/src/native/managed/cdac/tests/UnitTests/DacDbiImplTests.cs b/src/native/managed/cdac/tests/UnitTests/DacDbiImplTests.cs index f107f1654f23c6..e6ea5ab5f9d38f 100644 --- a/src/native/managed/cdac/tests/UnitTests/DacDbiImplTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/DacDbiImplTests.cs @@ -983,6 +983,78 @@ private static DacDbiImpl CreateNativeCodeDacDbi( return new DacDbiImpl(target, legacyObj: null, new()); } + [UnmanagedCallersOnly] + private static unsafe void CollectAsyncLocalCallback(AsyncLocalData* pLocal, nint pUserData) + { + GCHandle handle = GCHandle.FromIntPtr(pUserData); + ((List)handle.Target!).Add(*pLocal); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void EnumerateAsyncLocals_MapsEntryPointToDiagnosticCodeStart(bool useCodeAddress) + { + MockTarget.Architecture arch = new() { IsLittleEndian = true, Is64Bit = true }; + TargetPointer methodDesc = new(0xf0003000); + TargetCodePointer entryPoint = new(0xf0005000); + TargetCodePointer codeStart = new(0xf0006100); + const ulong CodeAddress = 0xf0006140; + MethodDescHandle methodDescHandle = new(methodDesc); + NativeCodeVersionHandle nativeCodeVersion = NativeCodeVersionHandle.CreateSynthetic(methodDesc); + + var rts = new Mock(); + rts.Setup(r => r.GetMethodDescHandle(methodDesc)).Returns(methodDescHandle); + rts.Setup(r => r.GetAsyncMethodFlags(methodDescHandle)).Returns(AsyncMethodFlags.None); + rts.Setup(r => r.GetNativeCode(methodDescHandle)).Returns(entryPoint); + + var codeVersions = new Mock(); + codeVersions.Setup(c => c.GetNativeCodeVersionForIP(new TargetCodePointer(CodeAddress))).Returns(nativeCodeVersion); + codeVersions.Setup(c => c.GetNativeCode(nativeCodeVersion)).Returns(entryPoint); + + var executionManager = new Mock(); + executionManager.Setup(e => e.GetDiagnosticCodeStartFromEntryPoint(entryPoint)).Returns(codeStart); + + var debugInfo = new Mock(MockBehavior.Strict); + debugInfo.Setup(d => d.GetAsyncSuspensionPoints(codeStart)).Returns( + [ + new AsyncSuspensionInfo + { + NativeOffset = 0x10, + Locals = [new AsyncLocalInfo { Offset = 8, ILVarNumber = 1 }, new AsyncLocalInfo { Offset = 16, ILVarNumber = 3 }], + }, + ]); + + var target = new TestPlaceholderTarget.Builder(arch) + .UseReader((_, _) => -1) + .AddMockContract(rts) + .AddMockContract(codeVersions) + .AddMockContract(executionManager) + .AddMockContract(debugInfo) + .Build(); + DacDbiImpl dacDbi = new(target, legacyObj: null, new()); + + List locals = new(); + GCHandle gcHandle = GCHandle.Alloc(locals); + try + { + int hr = dacDbi.EnumerateAsyncLocals( + methodDesc.Value, + useCodeAddress ? CodeAddress : 0, + state: 0, + &CollectAsyncLocalCallback, + GCHandle.ToIntPtr(gcHandle)); + + Assert.Equal(System.HResults.S_OK, hr); + } + finally + { + gcHandle.Free(); + } + + Assert.Equal([(8u, 1u), (16u, 3u)], locals.ConvertAll(l => (l.Offset, l.IlVarNum))); + } + [Theory] [ClassData(typeof(MockTarget.StdArch))] public void GetNativeCodeInfo_FillsDataForAsyncVariant(MockTarget.Architecture arch) @@ -1018,9 +1090,9 @@ public void GetNativeCodeInfo_FillsDataForAsyncVariant(MockTarget.Architecture a rts.Setup(r => r.GetMethodTable(asyncVariantHandle)).Throws(new VirtualReadException()); var precodeStubs = new Mock(); - precodeStubs.Setup(p => p.GetInterpreterCodeFromInterpreterPrecodeIfPresent(interpreterPrecode)).Returns(nativeCode); var executionManager = new Mock(); + executionManager.Setup(e => e.GetDiagnosticCodeStartFromEntryPoint(interpreterPrecode)).Returns(nativeCode); executionManager.Setup(e => e.GetCodeBlockHandle(nativeCode)).Returns(codeBlock); executionManager .Setup(e => e.GetMethodRegionInfo(codeBlock, out It.Ref.IsAny, out It.Ref.IsAny, out It.Ref.IsAny)) @@ -1103,9 +1175,9 @@ public void GetNativeCodeInfoForAddr_FillsData(MockTarget.Architecture arch) ILCodeVersionHandle ilCodeVersion = ILCodeVersionHandle.CreateExplicit(new TargetPointer(0x8000)); var precodeStubs = new Mock(); - precodeStubs.Setup(p => p.GetInterpreterCodeFromInterpreterPrecodeIfPresent(new TargetCodePointer(CodeAddress))).Returns(nativeCode); var executionManager = new Mock(); + executionManager.Setup(e => e.GetDiagnosticCodeStartFromEntryPoint(new TargetCodePointer(CodeAddress))).Returns(nativeCode); executionManager.Setup(e => e.GetCodeBlockHandle(nativeCode)).Returns(codeBlock); executionManager.Setup(e => e.GetStartAddress(codeBlock)).Returns(codeStart); executionManager.Setup(e => e.GetMethodDesc(codeBlock)).Returns(methodDesc); @@ -1163,9 +1235,9 @@ public void GetNativeCodeInfoForAddr_MissingRegionData_PreservesHotStart(MockTar ITypeHandle typeHandle = new TargetTypeHandle(methodTable); var precodeStubs = new Mock(); - precodeStubs.Setup(p => p.GetInterpreterCodeFromInterpreterPrecodeIfPresent(new TargetCodePointer(CodeAddress))).Throws(new VirtualReadException()); var executionManager = new Mock(); + executionManager.Setup(e => e.GetDiagnosticCodeStartFromEntryPoint(new TargetCodePointer(CodeAddress))).Throws(new VirtualReadException()); executionManager.Setup(e => e.GetCodeBlockHandle(new TargetCodePointer(CodeAddress))).Returns(codeBlock); executionManager.Setup(e => e.GetStartAddress(codeBlock)).Returns(codeStart); executionManager.Setup(e => e.GetMethodDesc(codeBlock)).Returns(methodDesc); diff --git a/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs b/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs index 6cb2a4f8d91034..ef4ba1257978aa 100644 --- a/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs @@ -1622,7 +1622,7 @@ public void GetCodeBlockHandle_InterpreterPrecode_ReturnsNull(string version, Mo // GetCodeBlockHandle should return null for a precode address. // Callers are responsible for resolving interpreter precodes via - // PrecodeStubs.GetInterpreterCodeFromInterpreterPrecodeIfPresent before calling GetCodeBlockHandle. + // GetDiagnosticCodeStartFromEntryPoint before calling GetCodeBlockHandle. TargetCodePointer precodeAddress = new(precodeRangeStart + 0x100); var eeInfo = em.GetCodeBlockHandle(precodeAddress); Assert.Null(eeInfo); @@ -1822,4 +1822,238 @@ public void GetDynamicFunctionTableEntries_UnsupportedPlatform_ReturnsEmpty( Assert.Empty(entries); } + + private const int PortableEntryPointPrefersInterpreterEntryPoint = 0x4; + private const ulong PortableVirtualIPRangeStart = 0x8001_0001; + private const uint PortableRootBeginAddress = 0x100; + private const uint PortableMinFunctionTableIndex = 5; + // Local index 1 is the controlling function; local index 2 is its funclet. + private const uint PortableRootFunctionTableIndex = PortableMinFunctionTableIndex + 1; + private const uint PortableFuncletFunctionTableIndex = PortableMinFunctionTableIndex + 2; + private const ulong PortableMethodDescAddress = 0x0101_aaa0; + private const ulong PortableInterpreterCode = 0x0061_0000; + private const ulong PortableCodeRangeStart = 0x0a0a_0000; + + private sealed class PortableEntryPointFixture + { + public required Target Target { get; init; } + public required TargetCodePointer EntryPoint { get; init; } + public required TargetCodePointer EntryPointInCodeRange { get; init; } + public required Mock PrecodeStubs { get; init; } + } + + // Builds a Wasm target with a ReadyToRun virtual-IP range, a FunctionTableIndexRangeSection for the + // same module, and a PortableEntryPoint for a MethodDesc. The same PortableEntryPoint bytes are also + // placed inside a registered code range, where they must not be interpreted as a portable entry point. + private static PortableEntryPointFixture CreatePortableEntryPointFixture( + ulong interpreterCode, + ulong actualCode, + int flags = 0, + bool isTemporaryEntryPoint = true, + bool portableEntrypointsEnabled = true, + bool cyclicFunctionTableIndexRangeList = false) + { + MockTarget.Architecture wasmArch = new() { IsLittleEndian = true, Is64Bit = false }; + MockExecutionManagerBuilder emBuilder = new("c1", wasmArch, MockExecutionManagerBuilder.DefaultAllocationRange, isWasm: true); + TargetTestHelpers helpers = emBuilder.Builder.TargetTestHelpers; + MockMemorySpace.BumpAllocator allocator = emBuilder.Builder.CreateAllocator(0x0060_0000, 0x0061_0000); + + MockExecutionManagerBuilder.JittedCodeRange virtualIPRange = emBuilder.AllocateJittedCodeRange(PortableVirtualIPRangeStart, 0x400); + MockReadyToRunInfo r2rInfo = emBuilder.AddReadyToRunInfo([0x80, PortableRootBeginAddress, 0x8000_0000 | 0x120], []); + r2rInfo.MinVirtualIP = PortableVirtualIPRangeStart; + r2rInfo.LoadedImageBase = 0x0090_0000; + new MockHashMapBuilder(emBuilder.Builder).PopulatePtrMap( + r2rInfo.EntryPointToMethodDescMapAddress, + [(PortableVirtualIPRangeStart + PortableRootBeginAddress, PortableMethodDescAddress)]); + MockLoaderModule module = emBuilder.AddReadyToRunModule(r2rInfo.Address); + _ = emBuilder.AddVirtualIPRangeSection(virtualIPRange, jitManagerAddress: 0x000b_ff00, module.Address); + + MockExecutionManagerBuilder.JittedCodeRange codeRange = emBuilder.AllocateJittedCodeRange(PortableCodeRangeStart, 0x1000); + MockRangeSection codeRangeSection = emBuilder.AddRangeSection(codeRange, jitManagerAddress: 0x000b_ff00, codeHeapListNodeAddress: 0); + _ = emBuilder.AddRangeSectionFragment(codeRange, codeRangeSection.Address); + + TargetTestHelpers.LayoutResult sectionLayout = helpers.LayoutFields([ + new(nameof(Data.FunctionTableIndexRangeSection.MinFunctionTableIndex), DataType.uint32), + new(nameof(Data.FunctionTableIndexRangeSection.NumRuntimeFunctions), DataType.uint32), + new(nameof(Data.FunctionTableIndexRangeSection.R2RModule), DataType.pointer), + new(nameof(Data.FunctionTableIndexRangeSection.Next), DataType.pointer), + ]); + TargetTestHelpers.LayoutResult portableEntryPointLayout = helpers.LayoutFields([ + new(nameof(Data.PortableEntryPoint.ActualCode), DataType.pointer), + new(nameof(Data.PortableEntryPoint.MethodDesc), DataType.pointer), + new(nameof(Data.PortableEntryPoint.Flags), DataType.int32), + ]); + TargetTestHelpers.LayoutResult methodDescLayout = helpers.LayoutFields([ + new(nameof(Data.MethodDesc.CodeData), DataType.pointer), + new(nameof(Data.MethodDesc.InterpreterCode), DataType.pointer), + ]); + // TemporaryEntryPoint is a CodePointer-typed field, so it is declared without a pointer type name. + Target.TypeInfo codeDataType = new() + { + Fields = new Dictionary { [nameof(Data.MethodDescCodeData.TemporaryEntryPoint)] = new() { Offset = 0 } }, + Size = (uint)helpers.PointerSize, + }; + + MockMemorySpace.HeapFragment section = allocator.Allocate(sectionLayout.Stride, "FunctionTableIndexRangeSection"); + helpers.Write(section.Data.AsSpan(sectionLayout.Fields[nameof(Data.FunctionTableIndexRangeSection.MinFunctionTableIndex)].Offset, sizeof(uint)), PortableMinFunctionTableIndex); + helpers.Write(section.Data.AsSpan(sectionLayout.Fields[nameof(Data.FunctionTableIndexRangeSection.NumRuntimeFunctions)].Offset, sizeof(uint)), 3u); + helpers.WritePointer(section.Data.AsSpan(sectionLayout.Fields[nameof(Data.FunctionTableIndexRangeSection.R2RModule)].Offset, helpers.PointerSize), module.Address); + helpers.WritePointer( + section.Data.AsSpan(sectionLayout.Fields[nameof(Data.FunctionTableIndexRangeSection.Next)].Offset, helpers.PointerSize), + cyclicFunctionTableIndexRangeList ? section.Address : 0); + MockMemorySpace.HeapFragment listSlot = allocator.Allocate((ulong)helpers.PointerSize, "FunctionTableIndexRangeListSlot"); + helpers.WritePointer(listSlot.Data.AsSpan(), section.Address); + + emBuilder.Builder.AddHeapFragment(new MockMemorySpace.HeapFragment + { + Address = PortableMethodDescAddress, + Data = new byte[methodDescLayout.Stride], + Name = "MethodDesc", + }); + Span methodDesc = emBuilder.Builder.BorrowAddressRange(PortableMethodDescAddress, (int)methodDescLayout.Stride); + MockMemorySpace.HeapFragment codeData = allocator.Allocate(codeDataType.Size!.Value, "MethodDescCodeData"); + helpers.WritePointer(methodDesc.Slice(methodDescLayout.Fields[nameof(Data.MethodDesc.CodeData)].Offset, helpers.PointerSize), codeData.Address); + helpers.WritePointer(methodDesc.Slice(methodDescLayout.Fields[nameof(Data.MethodDesc.InterpreterCode)].Offset, helpers.PointerSize), interpreterCode); + + void WritePortableEntryPoint(Span pep) + { + helpers.WritePointer(pep.Slice(portableEntryPointLayout.Fields[nameof(Data.PortableEntryPoint.ActualCode)].Offset, helpers.PointerSize), actualCode); + helpers.WritePointer(pep.Slice(portableEntryPointLayout.Fields[nameof(Data.PortableEntryPoint.MethodDesc)].Offset, helpers.PointerSize), PortableMethodDescAddress); + helpers.Write(pep.Slice(portableEntryPointLayout.Fields[nameof(Data.PortableEntryPoint.Flags)].Offset, sizeof(int)), flags); + } + + MockMemorySpace.HeapFragment portableEntryPoint = allocator.Allocate(portableEntryPointLayout.Stride, "PortableEntryPoint"); + WritePortableEntryPoint(portableEntryPoint.Data); + MockMemorySpace.HeapFragment portableEntryPointInCodeRange = codeRange.Allocator.Allocate(portableEntryPointLayout.Stride, "PortableEntryPoint-shaped code"); + WritePortableEntryPoint(portableEntryPointInCodeRange.Data); + + ulong temporaryEntryPoint = isTemporaryEntryPoint ? portableEntryPoint.Address : portableEntryPoint.Address + 0x100; + helpers.WritePointer(codeData.Data.AsSpan(), temporaryEntryPoint); + + Mock precodeStubs = new(MockBehavior.Strict); + Target target = CreateTarget( + emBuilder, + RuntimeInfoOperatingSystem.Windows, + RuntimeInfoArchitecture.Wasm, + targetBuilder => targetBuilder + .AddTypes(new Dictionary + { + [DataType.FunctionTableIndexRangeSection] = new() { Fields = sectionLayout.Fields, Size = sectionLayout.Stride }, + [DataType.PortableEntryPoint] = new() { Fields = portableEntryPointLayout.Fields, Size = portableEntryPointLayout.Stride }, + [DataType.MethodDesc] = new() { Fields = methodDescLayout.Fields, Size = methodDescLayout.Stride }, + [DataType.MethodDescCodeData] = codeDataType, + }) + .AddGlobals( + (Constants.Globals.FunctionTableIndexRangeList, listSlot.Address), + (Constants.Globals.FeaturePortableEntrypoints, portableEntrypointsEnabled ? 1ul : 0ul)) + .AddContract(version: "c1") + .AddMockContract(precodeStubs)); + + return new PortableEntryPointFixture + { + Target = target, + EntryPoint = new TargetCodePointer(portableEntryPoint.Address), + EntryPointInCodeRange = new TargetCodePointer(portableEntryPointInCodeRange.Address), + PrecodeStubs = precodeStubs, + }; + } + + public static IEnumerable PortableEntryPointDiagnosticCodeStartCases() + { + const ulong Unchanged = 0; + ulong virtualIP = PortableVirtualIPRangeStart + PortableRootBeginAddress; + + // interpreterCode, actualCode, flags, isTemporaryEntryPoint, expected + yield return [PortableInterpreterCode, 0ul, PortableEntryPointPrefersInterpreterEntryPoint, true, PortableInterpreterCode]; + yield return [PortableInterpreterCode, (ulong)PortableRootFunctionTableIndex, 0, true, PortableInterpreterCode]; + // Like native, the interpreter mapping does not require the address to be the temporary entry point. + yield return [PortableInterpreterCode, 0ul, 0, false, PortableInterpreterCode]; + yield return [0ul, (ulong)PortableRootFunctionTableIndex, 0, true, virtualIP]; + yield return [1ul /* INTERPRETER_CODE_POISON */, (ulong)PortableRootFunctionTableIndex, 0, true, virtualIP]; + yield return [0ul, (ulong)PortableFuncletFunctionTableIndex, 0, true, virtualIP]; + yield return [0ul, (ulong)PortableRootFunctionTableIndex, PortableEntryPointPrefersInterpreterEntryPoint, true, Unchanged]; + yield return [0ul, (ulong)PortableRootFunctionTableIndex, 0, false, Unchanged]; + yield return [0ul, (ulong)PortableMinFunctionTableIndex + 100, 0, true, Unchanged]; + yield return [0ul, 0ul, 0, true, Unchanged]; + } + + [Theory] + [MemberData(nameof(PortableEntryPointDiagnosticCodeStartCases))] + public void GetDiagnosticCodeStartFromEntryPoint_PortableEntryPoint( + ulong interpreterCode, + ulong actualCode, + int flags, + bool isTemporaryEntryPoint, + ulong expected) + { + PortableEntryPointFixture fixture = CreatePortableEntryPointFixture(interpreterCode, actualCode, flags, isTemporaryEntryPoint); + IExecutionManager em = fixture.Target.Contracts.ExecutionManager; + + TargetCodePointer actual = em.GetDiagnosticCodeStartFromEntryPoint(fixture.EntryPoint); + + Assert.Equal(expected == 0 ? fixture.EntryPoint : new TargetCodePointer(expected), actual); + } + + [Fact] + public void GetDiagnosticCodeStartFromEntryPoint_WasmR2RPortableEntryPoint_ResolvesToCodeBlock() + { + PortableEntryPointFixture fixture = CreatePortableEntryPointFixture(interpreterCode: 0, actualCode: PortableRootFunctionTableIndex); + IExecutionManager em = fixture.Target.Contracts.ExecutionManager; + + Assert.Null(em.GetCodeBlockHandle(fixture.EntryPoint)); + TargetCodePointer codeStart = em.GetDiagnosticCodeStartFromEntryPoint(fixture.EntryPoint); + CodeBlockHandle? handle = em.GetCodeBlockHandle(codeStart); + + Assert.NotNull(handle); + Assert.Equal(new TargetPointer(PortableMethodDescAddress), em.GetMethodDesc(handle.Value)); + Assert.Equal(new TargetPointer(codeStart.Value), em.GetStartAddress(handle.Value)); + } + + [Fact] + public void GetDiagnosticCodeStartFromEntryPoint_AddressInCodeRange_ReturnsOriginalAddress() + { + PortableEntryPointFixture fixture = CreatePortableEntryPointFixture(PortableInterpreterCode, PortableRootFunctionTableIndex); + IExecutionManager em = fixture.Target.Contracts.ExecutionManager; + + // The readable, PortableEntryPoint-shaped bytes in a registered code range are code, not an entry point. + Assert.Equal(fixture.EntryPointInCodeRange, em.GetDiagnosticCodeStartFromEntryPoint(fixture.EntryPointInCodeRange)); + + TargetCodePointer virtualIP = new(PortableVirtualIPRangeStart + PortableRootBeginAddress); + Assert.Equal(virtualIP, em.GetDiagnosticCodeStartFromEntryPoint(virtualIP)); + } + + [Fact] + public void GetDiagnosticCodeStartFromEntryPoint_UnreadableAddress_ReturnsOriginalAddress() + { + PortableEntryPointFixture fixture = CreatePortableEntryPointFixture(PortableInterpreterCode, PortableRootFunctionTableIndex); + TargetCodePointer unreadable = new(0x0070_0000); + + Assert.Equal(unreadable, fixture.Target.Contracts.ExecutionManager.GetDiagnosticCodeStartFromEntryPoint(unreadable)); + } + + [Fact] + public void GetDiagnosticCodeStartFromEntryPoint_WithoutPortableEntryPoints_UsesPrecodeStubs() + { + PortableEntryPointFixture fixture = CreatePortableEntryPointFixture( + PortableInterpreterCode, PortableRootFunctionTableIndex, portableEntrypointsEnabled: false); + TargetCodePointer precode = new(0x0a0b_0000); + TargetCodePointer byteCode = new(0x0a0c_0000); + fixture.PrecodeStubs.Setup(p => p.GetInterpreterCodeFromInterpreterPrecodeIfPresent(precode)).Returns(byteCode); + IExecutionManager em = fixture.Target.Contracts.ExecutionManager; + + Assert.Equal(byteCode, em.GetDiagnosticCodeStartFromEntryPoint(precode)); + Assert.Equal(TargetCodePointer.Null, em.GetDiagnosticCodeStartFromEntryPoint(TargetCodePointer.Null)); + } + + [Fact] + public void WasmFunctionTableIndexLookup_CyclicRangeList_ReturnsFalse() + { + PortableEntryPointFixture fixture = CreatePortableEntryPointFixture( + interpreterCode: 0, actualCode: PortableMinFunctionTableIndex + 100, cyclicFunctionTableIndexRangeList: true); + ExecutionManagerHelpers.WasmFunctionTableIndexLookup lookup = new(fixture.Target); + + Assert.True(lookup.TryGetVirtualIPBase(PortableRootFunctionTableIndex, out _)); + Assert.False(lookup.TryGetVirtualIPBase(PortableMinFunctionTableIndex + 100, out _)); + Assert.Equal(fixture.EntryPoint, fixture.Target.Contracts.ExecutionManager.GetDiagnosticCodeStartFromEntryPoint(fixture.EntryPoint)); + } } diff --git a/src/native/managed/cdac/tests/UnitTests/SOSDacInterface5Tests.cs b/src/native/managed/cdac/tests/UnitTests/SOSDacInterface5Tests.cs index 370b21090b143f..75c38d07824861 100644 --- a/src/native/managed/cdac/tests/UnitTests/SOSDacInterface5Tests.cs +++ b/src/native/managed/cdac/tests/UnitTests/SOSDacInterface5Tests.cs @@ -105,9 +105,9 @@ private static ISOSDacInterface5 CreateDac5( .Setup(c => c.GetNativeCodeVersions(s_methodDescAddr, It.IsAny())) .Returns(nativeVersionHandles); - var mockPrecodeStubs = new Mock(); - mockPrecodeStubs - .Setup(p => p.GetInterpreterCodeFromInterpreterPrecodeIfPresent(It.IsAny())) + var mockExecutionManager = new Mock(); + mockExecutionManager + .Setup(e => e.GetDiagnosticCodeStartFromEntryPoint(It.IsAny())) .Returns((TargetCodePointer ep) => ep); var mockPlatformMetadata = new Mock(); @@ -121,7 +121,7 @@ private static ISOSDacInterface5 CreateDac5( .AddMockContract(mockRts) .AddMockContract(mockLoader) .AddMockContract(mockReJIT) - .AddMockContract(mockPrecodeStubs) + .AddMockContract(mockExecutionManager) .AddMockContract(mockPlatformMetadata) .Build();