From a9208dd026ba99aeae4107cf5edd85d6052c3c09 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Thu, 8 Oct 2026 17:58:31 +0200 Subject: [PATCH] [cDAC] Use diagnostic code start in GetMethodVarInfo; make ReJIT optional in IsVersionable GetMethodVarInfo computed codeOffset relative to the native-code entry point. With portable entry points the entry point is not the code start, so map it with IExecutionManager.GetDiagnosticCodeStartFromEntryPoint (the cDAC mirror of the runtime's GetDiagnosticCodeStartFromEntryPoint). IsVersionable required the ReJIT contract, which is only advertised with profiling support. Without it, native IsEligibleForReJIT is false, so treat a missing ReJIT contract as not ReJIT-eligible. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/design/datacontracts/DebugInfo.md | 2 + .../design/datacontracts/RuntimeTypeSystem.md | 5 +- .../Contracts/DebugInfo/DebugInfo_1.cs | 10 ++-- .../Contracts/RuntimeTypeSystem_1.cs | 5 +- .../ExecutionManager/ExecutionManagerTests.cs | 44 +++++++++++++++- .../cdac/tests/UnitTests/MethodDescTests.cs | 51 +++++++++++++++++-- 6 files changed, 101 insertions(+), 16 deletions(-) diff --git a/docs/design/datacontracts/DebugInfo.md b/docs/design/datacontracts/DebugInfo.md index cf83e203cac90d..b99fc3d2b99a1c 100644 --- a/docs/design/datacontracts/DebugInfo.md +++ b/docs/design/datacontracts/DebugInfo.md @@ -198,6 +198,8 @@ public readonly struct DebugVarInfo IEnumerable GetMethodVarInfo(TargetCodePointer pCode, out uint codeOffset); ``` +`codeOffset` is `pCode` minus the method's code start. The code start is the entry point of the native code version containing `pCode` (`ICodeVersions.GetNativeCode`), mapped by `IExecutionManager.GetDiagnosticCodeStartFromEntryPoint`: to the interpreter bytecode for an interpreted method, and on portable-entrypoint targets to the R2R virtual IP. Native `ClrDataAccess::GetMethodVarInfo` maps only interpreter entry points (`GetInterpreterCodeFromEntryPointIfPresent`); the R2R portable-entrypoint mapping follows the runtime's `GetDiagnosticCodeStartFromEntryPoint`. + ### Vars Data Encoding Each variable entry in the Vars section is nibble-encoded as follows: diff --git a/docs/design/datacontracts/RuntimeTypeSystem.md b/docs/design/datacontracts/RuntimeTypeSystem.md index 598cb467aa3c70..92dd8d9d50465b 100644 --- a/docs/design/datacontracts/RuntimeTypeSystem.md +++ b/docs/design/datacontracts/RuntimeTypeSystem.md @@ -2002,8 +2002,9 @@ Determining if a method supports multiple code versions: MethodDesc md = _methodDescs[methodDesc.Address]; if (md.IsEligibleForTieredCompilation) return true; - // MethodDesc::IsEligibleForReJIT - if (_target.Contracts.ReJIT.IsEnabled()) + // MethodDesc::IsEligibleForReJIT. The ReJIT contract is advertised only with PROFILING_SUPPORTED; + // targets without it (wasm) also lack FEATURE_REJIT, so nothing is ReJIT-eligible. + if (_target.Contracts.TryGetContract(out IReJIT reJit) && reJit.IsEnabled()) { if (!md.IsIL) return false; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/DebugInfo/DebugInfo_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/DebugInfo/DebugInfo_1.cs index 14d78f9f09e4ae..67f451c711f7a0 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/DebugInfo/DebugInfo_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/DebugInfo/DebugInfo_1.cs @@ -123,16 +123,14 @@ IEnumerable IDebugInfo.GetMethodVarInfo(TargetCodePointer pCode, o throw new InvalidOperationException($"No CodeBlockHandle found for native code {pCode}."); TargetPointer debugInfo = _eman.GetDebugInfo(cbh, out bool _); - // Compute code offset from the method's native code entry point, not from the code block start. - // GetStartAddress returns the start of the current code block (which may be a funclet for exception - // handlers). Variable location offsets are always relative to the method entry point, so we must use - // GetNativeCode from the NativeCodeVersion, matching the native DAC's GetMethodVarInfo which uses - // NativeCodeVersion::GetNativeCode() for this purpose + // Variable offsets are relative to the method's code start, not the code block start + // (GetStartAddress may be a funclet). Map the native code version's entry point to the code + // it starts: interpreter bytecode, or on portable-entrypoint targets the R2R virtual IP. ICodeVersions cv = _target.Contracts.CodeVersions; NativeCodeVersionHandle ncvh = cv.GetNativeCodeVersionForIP(pCode); if (!ncvh.Valid) throw new InvalidOperationException($"No NativeCodeVersion found for native code {pCode}."); - TargetCodePointer nativeCodeStart = cv.GetNativeCode(ncvh); + TargetCodePointer nativeCodeStart = _eman.GetDiagnosticCodeStartFromEntryPoint(cv.GetNativeCode(ncvh)); codeOffset = (uint)(CodePointerUtils.AddressFromCodePointer(pCode, _target) - CodePointerUtils.AddressFromCodePointer(nativeCodeStart, _target)); if (debugInfo == TargetPointer.Null) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs index 3ed46f8c11e726..15d02e2b0f80c2 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs @@ -1997,8 +1997,9 @@ bool IRuntimeTypeSystem.IsVersionable(MethodDescHandle methodDesc) MethodDesc md = _methodDescs[methodDesc.Address]; if (md.IsEligibleForTieredCompilation) return true; - // MethodDesc::IsEligibleForReJIT - if (_target.Contracts.ReJIT.IsEnabled()) + // MethodDesc::IsEligibleForReJIT. The ReJIT contract is advertised only with PROFILING_SUPPORTED; + // targets without it (wasm) also lack FEATURE_REJIT, so nothing is ReJIT-eligible. + if (_target.Contracts.TryGetContract(out IReJIT reJit) && reJit.IsEnabled()) { if (!md.IsIL) return false; diff --git a/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs b/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs index aff57ebe5864e9..2a9ffed4e9c2ea 100644 --- a/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs @@ -332,6 +332,43 @@ public void GetMethodDesc_R2R_OneRuntimeFunction(string version, MockTarget.Arch } } + /// + /// Variable offsets are relative to the code that a method's entry point starts, as native + /// ClrDataAccess::GetMethodVarInfo computes with GetInterpreterCodeFromEntryPointIfPresent: + /// an interpreter precode or portable entry point maps to the code it runs (interpreter bytecode + /// or the R2R virtual IP), while a JIT entry point is already the code start. + /// + [Theory] + [InlineData(0x0040_0000ul, 0x0040_0000ul)] + [InlineData(0x0040_0000ul, 0x8001_0100ul)] + public void GetMethodVarInfo_CodeOffsetIsRelativeToDiagnosticCodeStart(ulong entryPoint, ulong codeStart) + { + const uint CodeOffset = 0x2; + TargetCodePointer pCode = new(codeStart + CodeOffset); + CodeBlockHandle codeBlock = new(new TargetPointer(0x7000)); + NativeCodeVersionHandle nativeCodeVersion = NativeCodeVersionHandle.CreateSynthetic(new TargetPointer(0x1000)); + + Mock executionManager = new(); + executionManager.Setup(e => e.GetCodeBlockHandle(pCode)).Returns(codeBlock); + executionManager.Setup(e => e.GetDebugInfo(codeBlock, out It.Ref.IsAny)).Returns(TargetPointer.Null); + executionManager + .Setup(e => e.GetDiagnosticCodeStartFromEntryPoint(new TargetCodePointer(entryPoint))) + .Returns(new TargetCodePointer(codeStart)); + Mock codeVersions = new(); + codeVersions.Setup(c => c.GetNativeCodeVersionForIP(pCode)).Returns(nativeCodeVersion); + codeVersions.Setup(c => c.GetNativeCode(nativeCodeVersion)).Returns(new TargetCodePointer(entryPoint)); + + Target target = new TestPlaceholderTarget.Builder(new MockTarget.Architecture { IsLittleEndian = true, Is64Bit = false }) + .AddMockContract(executionManager) + .AddMockContract(codeVersions) + .AddMockContract(new Mock()) + .AddContract(version: "c1") + .Build(); + + Assert.Empty(target.Contracts.DebugInfo.GetMethodVarInfo(pCode, out uint codeOffset)); + Assert.Equal(CodeOffset, codeOffset); + } + [Theory] [MemberData(nameof(StdArchAllVersions))] public void GetDebugInfo_R2R_NoDebugInfoSection_ReturnsNull(string version, MockTarget.Architecture arch) @@ -365,10 +402,15 @@ public void GetDebugInfo_R2R_NoDebugInfoSection_ReturnsNull(string version, Mock codeVersions.Setup(c => c.GetNativeCode(nativeCodeVersion)).Returns(methodStart); MockMemorySpace.MemoryContext memoryContext = emBuilder.Builder.GetMemoryContext(); + Mock precodeStubs = new(); + precodeStubs.Setup(p => p.GetInterpreterCodeFromInterpreterPrecodeIfPresent(methodStart)).Returns(methodStart); Target target = CreateTarget(emBuilder, configureTarget: targetBuilder => targetBuilder .UseReader(ReadWithReadableZeroPage) + .AddGlobals((Constants.Globals.FeaturePortableEntrypoints, 0ul)) + .AddContract(version: "c1") .AddContract(version: "c1") - .AddMockContract(codeVersions)); + .AddMockContract(codeVersions) + .AddMockContract(precodeStubs)); IExecutionManager em = target.Contracts.ExecutionManager; CodeBlockHandle? handle = em.GetCodeBlockHandle(pCode); diff --git a/src/native/managed/cdac/tests/UnitTests/MethodDescTests.cs b/src/native/managed/cdac/tests/UnitTests/MethodDescTests.cs index 43a31e7bfa9e9f..92cd7eb6f04729 100644 --- a/src/native/managed/cdac/tests/UnitTests/MethodDescTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/MethodDescTests.cs @@ -78,7 +78,8 @@ private static IRuntimeTypeSystem CreateRuntimeTypeSystemContract( MockTarget.Architecture arch, Action configure, Mock? mockExecutionManager = null, - Mock? mockPrecodeStubs = null) + Mock? mockPrecodeStubs = null, + Action? configureTarget = null) { var targetBuilder = new TestPlaceholderTarget.Builder(arch); MockDescriptors.RuntimeTypeSystem rtsBuilder = new(targetBuilder.MemoryBuilder); @@ -89,16 +90,16 @@ private static IRuntimeTypeSystem CreateRuntimeTypeSystemContract( mockExecutionManager ??= new Mock(); mockPrecodeStubs ??= new Mock(); - var target = targetBuilder + TestPlaceholderTarget.Builder target = targetBuilder .AddTypes(CreateContractTypes(methodDescBuilder)) .AddGlobals(CreateContractGlobals(methodDescBuilder)) .AddContract(version: "c1") .AddContract(version: "c1") .AddMockContract(new Mock()) .AddMockContract(mockExecutionManager) - .AddMockContract(mockPrecodeStubs) - .Build(); - return target.Contracts.RuntimeTypeSystem; + .AddMockContract(mockPrecodeStubs); + configureTarget?.Invoke(target); + return target.Build().Contracts.RuntimeTypeSystem; } [Theory] @@ -210,6 +211,46 @@ public void IsArrayMethod(MockTarget.Architecture arch) } } + /// + /// The ReJIT contract is advertised only by runtimes built with profiler support. Without it, + /// native MethodDesc::IsEligibleForReJIT is false, so a method that is not eligible for + /// tiered compilation is not versionable. + /// + [Theory] + [InlineData(false, false, false)] + [InlineData(true, false, false)] + [InlineData(true, true, true)] + public void IsVersionable_ReJITContractOptional(bool advertiseReJIT, bool reJITEnabled, bool expected) + { + MockTarget.Architecture arch = new() { IsLittleEndian = true, Is64Bit = true }; + TargetPointer methodDescAddress = TargetPointer.Null; + Mock reJIT = new(); + reJIT.Setup(r => r.IsEnabled()).Returns(reJITEnabled); + Mock codeVersions = new(); + codeVersions.Setup(c => c.CodeVersionManagerSupportsMethod(It.IsAny())).Returns(true); + + IRuntimeTypeSystem rts = CreateRuntimeTypeSystemContract( + arch, + methodDescBuilder => + { + MockMethodTable objectMethodTable = methodDescBuilder.RTSBuilder.SystemObjectMethodTable; + byte methodDescSize = (byte)(methodDescBuilder.MethodDescLayout.Size / methodDescBuilder.MethodDescAlignment); + MockMethodDescChunk chunk = methodDescBuilder.AddMethodDescChunk("testMethod", methodDescSize); + chunk.MethodTable = objectMethodTable.Address; + chunk.Size = methodDescSize; + chunk.Count = 1; + methodDescAddress = new TargetPointer(chunk.GetMethodDescAtChunkIndex(0, methodDescBuilder.MethodDescLayout).Address); + }, + configureTarget: targetBuilder => + { + targetBuilder.AddMockContract(codeVersions); + if (advertiseReJIT) + targetBuilder.AddMockContract(reJIT); + }); + + Assert.Equal(expected, rts.IsVersionable(rts.GetMethodDescHandle(methodDescAddress))); + } + [Theory] [ClassData(typeof(MockTarget.StdArch))] public void IsDynamicMethod(MockTarget.Architecture arch)