From 712cc6dee4ab6c847fb72b25c7e34c0464583777 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Mon, 28 Sep 2026 19:41:08 -0500 Subject: [PATCH] [cDAC][wasm] Map portable entry points to interpreter code and R2R virtual IPs > [!IMPORTANT] > Stacked on #134754 (targets its branch). Only the commits above #134754's head belong to this PR; retarget to `main` once #134754 merges. ## Problem On `FEATURE_PORTABLE_ENTRYPOINTS` targets (WebAssembly), a method's entry point slot holds a `PortableEntryPoint` address. Native maps it before handing it to diagnostics (`GetInterpreterCodeFromEntryPointIfPresent` / `GetDiagnosticCodeStartFromEntryPoint` in `precode.cpp`), but the cDAC returned the raw address. SOS values such as `DacpMethodDescData.NativeCodeAddr` and rejit `NativeCodeAddr`, plus the DBI/`ClrDataMethodInstance` paths, therefore reported an address that doesn't resolve through `ExecutionManager` for both interpreted and R2R methods. ## Change Adds `IExecutionManager.GetDiagnosticCodeStartFromEntryPoint`, which mirrors the native `GetDiagnosticCodeStartFromEntryPoint`: - **Without portable entrypoints:** delegates to `PrecodeStubs.GetInterpreterCodeFromInterpreterPrecodeIfPresent`. Behavior is unchanged. - **With portable entrypoints:** 1. Returns the address unchanged if it lies in a code range. This is the native `FindCodeRange` check and includes Wasm R2R virtual-IP ranges from #133917. 2. Maps interpreted methods to `MethodDesc::m_interpreterCode`. 3. Maps native R2R methods from the function-table index in `PortableEntryPoint._pActualCode` to the synthetic virtual IP, stepping back past funclet entries. This matches `ExecutionManager::GetWasmVirtualIPFromFunctionTableIndex`. As in native code, it applies only to the method's own (temporary) entry point, and only when the entry point doesn't prefer the interpreter. The mapping lives in ExecutionManager rather than PrecodeStubs because ExecutionManager owns the virtual-IP ranges and R2R lookup. ExecutionManager already depends on PrecodeStubs, so this adds no contract cycle. Function-table-index resolution moves into `ExecutionManagerHelpers.WasmFunctionTableIndexLookup`, which now bounds the list walk and detects cycles, like the virtual-IP list walk. The stack walk's `WasmR2RInfo` becomes a thin wrapper over it. The Legacy SOS/DBI callers (`SOSDacImpl`, `ClrDataMethodInstance`, `DacDbiImpl`) now call the new API. `DacDbiImpl.EnumerateAsyncLocals` also maps its code address before querying async debug info. The native DAC's `EnumerateAsyncLocals` gets the matching `GetInterpreterCodeFromEntryPointIfPresent` mapping (as `GetMethodVarInfo` already does), so the debug-build cDAC/DAC cross-check stays consistent. ### Data descriptors - `PortableEntryPoint.ActualCode` and `PortableEntryPoint.Flags` (only under `FEATURE_PORTABLE_ENTRYPOINTS`). - `MethodDesc.InterpreterCode` (only under `FEATURE_INTERPRETER`). - The contract relies on `kPrefersInterpreterEntryPoint` and `INTERPRETER_CODE_POISON`; the native side now has `[cDAC]` comments marking that dependency. ### Interaction with #133890 #133890 adds `TryGetFunctionIdentity` and `TryIsFunclet` to `WasmR2RInfo`. Whichever PR lands second should add those two methods to `WasmFunctionTableIndexLookup` and have `WasmR2RInfo` forward to them. The `FunctionTableIndexRange*` descriptor meanings here already use #133890's exact wording, so that JSON should merge cleanly. ## Validation - cDAC: `./build.sh -s tools.cdac+tools.cdactests -c Debug -test` passes: 3196 unit tests (17 new), usage tests (contract cycles and generated docs up to date), and generator tests. - New ExecutionManager tests cover: - interpreted, R2R, funclet, poison, prefers-interpreter, not-own-entry-point and unknown-index cases; - an end-to-end check that the resolved virtual IP maps back to the MethodDesc through `GetCodeBlockHandle`; - readable `PortableEntryPoint`-shaped bytes inside a registered code range staying unchanged. This test fails if the range check is removed. - a cyclic function-table range list; - the non-portable delegation path. - CoreCLR `clr.runtime` builds for osx-arm64 Debug (with `FEATURE_INTERPRETER`) and browser-wasm Debug. The generated wasm descriptor has `PortableEntryPoint {ActualCode@0, MethodDesc@4, Flags@12}`. - A new `DacDbiImplTests` test covers `EnumerateAsyncLocals` mapping for both the code-address and MethodDesc paths. It fails without the fix. - cDAC dump tests (osx-arm64, local runtime): 260 passed, 0 failed. The 746 skips are net10.0 configurations plus by-design skips (Windows-only COM debuggees, and dump types a debuggee doesn't produce). - CoreCLR `clr.runtime` Release (osx-arm64) builds with the `dacdbiimpl.cpp` change. - Not run: any check against a live wasm target. Fixes dotnet/runtime#134753 > [!NOTE] > This PR description was generated with GitHub Copilot. --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/design/datacontracts/ExecutionManager.md | 67 +++++ .../design/datacontracts/RuntimeTypeSystem.md | 1 + .../data-descriptor-meanings.json | 8 + src/coreclr/debug/daccess/dacdbiimpl.cpp | 4 +- .../vm/datadescriptor/datadescriptor.inc | 5 + src/coreclr/vm/method.hpp | 4 + src/coreclr/vm/precode_portable.hpp | 3 + .../Contracts/IExecutionManager.cs | 5 + .../ExecutionManager/ExecutionManagerCore.cs | 65 +++++ .../ExecutionManager/ExecutionManager_1.cs | 1 + .../Helpers/WasmFunctionTableIndexLookup.cs | 109 ++++++++ .../StackWalk/Context/Wasm/WasmR2RInfo.cs | 88 +------ .../Data/MethodDesc.cs | 1 + .../Data/PortableEntryPoint.cs | 2 + .../ClrDataMethodInstance.cs | 6 +- .../Dbi/DacDbiImpl.cs | 5 +- .../SOSDacImpl.IXCLRDataProcess.cs | 2 +- .../SOSDacImpl.cs | 6 +- .../cdac/tests/UnitTests/DacDbiImplTests.cs | 78 +++++- .../ExecutionManager/ExecutionManagerTests.cs | 236 +++++++++++++++++- .../tests/UnitTests/SOSDacInterface5Tests.cs | 8 +- 21 files changed, 603 insertions(+), 101 deletions(-) create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/Helpers/WasmFunctionTableIndexLookup.cs 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();