diff --git a/src/tests/JIT/Directed/debugging/debuginfo/README.txt b/src/tests/JIT/Directed/debugging/debuginfo/README.txt new file mode 100644 index 00000000000000..5f617336c94ae1 --- /dev/null +++ b/src/tests/JIT/Directed/debugging/debuginfo/README.txt @@ -0,0 +1,24 @@ +This directory contains tests for debugging information generated by the JIT. +The tests are written in IL inside the tests.il file by creating a method in the +DebugInfoMethods class and marking them with the ExpectedILMappings attribute. +In that attribute, the IL offsets at which mappings are expected to be generated +can be specified for both debug (DebuggableAttribute with +DisableOptimizations) and optimized builds. + +To debug these tests, run the 'tester' project, which will JIT all methods in +tests.il in both debug and release (you may need to turn off tiered +compilation). + +* attribute.cs/csproj: Project containing ExpectedILMappingsAttribute, to avoid + circular dependencies + +* tests.il: File containing the tests marked with ExpectedILMappings + +* tests_d.ilproj/tests_r.ilproj: Both these projects just add tests.il, the only + difference is that the former has DebuggableAttribute with + DisableOptimizations and the latter does not. + +* tester.cs/csproj: The orchestrator of the tests, references tests_d and + tests_r and jits the test methods using RuntimeHelpers.PrepareMethod, collects + the IL mappings emitted using runtime events, and validates that the mappings + match the data in ExpectedILMappings. diff --git a/src/tests/JIT/Directed/debugging/debuginfo/attribute.cs b/src/tests/JIT/Directed/debugging/debuginfo/attribute.cs new file mode 100644 index 00000000000000..a1eb62d17c805f --- /dev/null +++ b/src/tests/JIT/Directed/debugging/debuginfo/attribute.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +[AttributeUsage(AttributeTargets.Method)] +public class ExpectedILMappings : Attribute +{ + public int[] Debug { get; set; } + public int[] Opts { get; set; } +} diff --git a/src/tests/JIT/Directed/debugging/debuginfo/attribute.csproj b/src/tests/JIT/Directed/debugging/debuginfo/attribute.csproj new file mode 100644 index 00000000000000..46f3565c362fea --- /dev/null +++ b/src/tests/JIT/Directed/debugging/debuginfo/attribute.csproj @@ -0,0 +1,10 @@ + + + Library + false + BuildOnly + + + + + diff --git a/src/tests/JIT/Directed/debugging/debuginfo/isdebug.il b/src/tests/JIT/Directed/debugging/debuginfo/isdebug.il new file mode 100644 index 00000000000000..2998979d1ad194 --- /dev/null +++ b/src/tests/JIT/Directed/debugging/debuginfo/isdebug.il @@ -0,0 +1 @@ +#define DEBUG \ No newline at end of file diff --git a/src/tests/JIT/Directed/debugging/debuginfo/tester.cs b/src/tests/JIT/Directed/debugging/debuginfo/tester.cs new file mode 100644 index 00000000000000..6f501c96f0020b --- /dev/null +++ b/src/tests/JIT/Directed/debugging/debuginfo/tester.cs @@ -0,0 +1,149 @@ +extern alias tests_d; +extern alias tests_r; + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.Tracing; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using Microsoft.Diagnostics.Tools.RuntimeClient; +using Microsoft.Diagnostics.Tracing; +using Microsoft.Diagnostics.Tracing.Parsers; +using Microsoft.Diagnostics.Tracing.Parsers.Clr; +using Tracing.Tests.Common; +using DebugInfoMethodsD = tests_d::DebugInfoMethods; +using DebugInfoMethodsR = tests_r::DebugInfoMethods; + +public unsafe class DebugInfoTest +{ + public static unsafe int Main() + { + var keywords = + ClrTraceEventParser.Keywords.Jit | ClrTraceEventParser.Keywords.JittedMethodILToNativeMap; + + var dotnetRuntimeProvider = new List + { + new Provider("Microsoft-Windows-DotNETRuntime", eventLevel: EventLevel.Verbose, keywords: (ulong)keywords) + }; + + var config = new SessionConfiguration(1024, EventPipeSerializationFormat.NetTrace, dotnetRuntimeProvider); + + return + IpcTraceTest.RunAndValidateEventCounts( + new Dictionary(), + JitMethods, + config, + ValidateMappings); + } + + private static void JitMethods() + { + ProcessType(typeof(DebugInfoMethodsD)); + ProcessType(typeof(DebugInfoMethodsR)); + } + + private static void ProcessType(Type t) + { + foreach (MethodInfo mi in t.GetMethods()) + { + if (mi.GetCustomAttribute() != null) + { + RuntimeHelpers.PrepareMethod(mi.MethodHandle); + } + } + } + + private static Func ValidateMappings(EventPipeEventSource source) + { + List<(long MethodID, OptimizationTier Tier, (int ILOffset, int NativeOffset)[] Mappings)> methodsWithMappings = new(); + Dictionary methodTier = new(); + + source.Clr.MethodLoad += e => methodTier[e.MethodID] = e.OptimizationTier; + source.Clr.MethodLoadVerbose += e => methodTier[e.MethodID] = e.OptimizationTier; + source.Clr.MethodILToNativeMap += e => + { + var mappings = new (int, int)[e.CountOfMapEntries]; + for (int i = 0; i < mappings.Length; i++) + mappings[i] = (e.ILOffset(i), e.NativeOffset(i)); + + if (!methodTier.TryGetValue(e.MethodID, out OptimizationTier tier)) + tier = OptimizationTier.Unknown; + + methodsWithMappings.Add((e.MethodID, tier, mappings)); + }; + + return () => + { + int result = 100; + foreach ((long methodID, OptimizationTier tier, (int ILOffset, int NativeOffset)[] mappings) in methodsWithMappings) + { + MethodBase meth = s_getMethodBaseByHandle(null, (IntPtr)(void*)methodID); + ExpectedILMappings attrib = meth.GetCustomAttribute(); + if (attrib == null) + { + continue; + } + + string name = $"[{meth.DeclaringType.Assembly.GetName().Name}]{meth.DeclaringType.FullName}.{meth.Name}"; + + // If DebuggableAttribute is saying that the assembly must be debuggable, then verify debug mappings. + // Otherwise verify release mappings. + // This may seem a little strange since we do not use the tier at all -- however, we expect debug + // to never tier and in release, we expect the release mappings to be the "least common denominator", + // i.e. tier0 and tier1 mappings should both be a superset. + // Note that tier0 and MinOptJitted differs in mappings generated exactly due to DebuggableAttribute. + DebuggableAttribute debuggableAttrib = meth.DeclaringType.Assembly.GetCustomAttribute(); + bool debuggableMappings = debuggableAttrib != null && debuggableAttrib.IsJITOptimizerDisabled; + + Console.WriteLine("{0}: Validate mappings for {1} codegen (tier: {2})", name, debuggableMappings ? "debuggable" : "optimized", tier); + + int[] expected = debuggableMappings ? attrib.Debug : attrib.Opts; + if (expected == null) + { + continue; + } + + if (!ValidateSingle(expected, mappings)) + { + Console.WriteLine(" Validation failed: expected mappings at IL offsets {0}", string.Join(", ", expected.Select(il => $"{il:x3}"))); + Console.WriteLine(" Actual (IL <-> native):"); + foreach ((int ilOffset, int nativeOffset) in mappings) + { + string ilOffsetName = Enum.IsDefined((SpecialILOffset)ilOffset) ? ((SpecialILOffset)ilOffset).ToString() : $"{ilOffset:x3}"; + Console.WriteLine(" {0:x3} <-> {1:x3}", ilOffsetName, nativeOffset); + } + + result = -1; + } + } + + return result; + }; + } + + // Validate that all IL offsets we expected had mappings generated for them. + private static bool ValidateSingle(int[] expected, (int ILOffset, int NativeOffset)[] mappings) + { + return expected.All(il => mappings.Any(t => t.ILOffset == il)); + } + + private enum SpecialILOffset + { + NoMapping = -1, + Prolog = -2, + Epilog = -3, + } + + static DebugInfoTest() + { + Type runtimeMethodHandleInternalType = typeof(RuntimeMethodHandle).Assembly.GetType("System.RuntimeMethodHandleInternal"); + Type runtimeTypeType = typeof(RuntimeMethodHandle).Assembly.GetType("System.RuntimeType"); + MethodInfo getMethodBaseMethod = runtimeTypeType.GetMethod("GetMethodBase", BindingFlags.NonPublic | BindingFlags.Static, new[] { runtimeTypeType, runtimeMethodHandleInternalType }); + s_getMethodBaseByHandle = (delegate*)getMethodBaseMethod.MethodHandle .GetFunctionPointer(); + } + + // Needed to go from MethodID -> MethodBase + private static readonly delegate* s_getMethodBaseByHandle; +} diff --git a/src/tests/JIT/Directed/debugging/debuginfo/tester.csproj b/src/tests/JIT/Directed/debugging/debuginfo/tester.csproj new file mode 100644 index 00000000000000..f3501e7c79878d --- /dev/null +++ b/src/tests/JIT/Directed/debugging/debuginfo/tester.csproj @@ -0,0 +1,15 @@ + + + Exe + PdbOnly + True + True + + + + + + + + + diff --git a/src/tests/JIT/Directed/debugging/debuginfo/tests.il b/src/tests/JIT/Directed/debugging/debuginfo/tests.il new file mode 100644 index 00000000000000..3af18c10d2aed0 --- /dev/null +++ b/src/tests/JIT/Directed/debugging/debuginfo/tests.il @@ -0,0 +1,259 @@ + +// Metadata version: v4.0.30319 +.assembly extern System.Runtime +{ + .publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A ) // .?_....: + .ver 7:0:0:0 +} +.assembly extern attribute +{ + .ver 0:0:0:0 +} +.assembly extern System.Console +{ + .publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A ) // .?_....: + .ver 7:0:0:0 +} +#ifdef DEBUG +.module tests_d.dll +.assembly tests_d +#else +.module tests_r.dll +.assembly tests_r +#endif +{ + .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilationRelaxationsAttribute::.ctor(int32) = ( 01 00 08 00 00 00 00 00 ) + .custom instance void [System.Runtime]System.Runtime.CompilerServices.RuntimeCompatibilityAttribute::.ctor() = ( 01 00 01 00 54 02 16 57 72 61 70 4E 6F 6E 45 78 // ....T..WrapNonEx + 63 65 70 74 69 6F 6E 54 68 72 6F 77 73 01 ) // ceptionThrows. + + .hash algorithm 0x00008004 + .ver 0:0:0:0 +} +// MVID: {D4625784-B5B9-4F12-8B8C-0423DDB744AA} +.imagebase 0x10000000 +.file alignment 0x00000200 +.stackreserve 0x00100000 +.subsystem 0x0003 // WINDOWS_CUI +.corflags 0x00000001 // ILONLY + +.class public auto ansi beforefieldinit DebugInfoMethods + extends [System.Runtime]System.Object +{ + .method public hidebysig static void TestUninlined() cil managed + { + .custom instance void [attribute]ExpectedILMappings::.ctor() = { + property int32[] Debug = int32[2]( 0 6 ) + property int32[] Opts = int32[2]( 0 6 ) + } + .maxstack 8 + IL_0000: call int32 DebugInfoMethods::NeverInlined() + IL_0005: pop + IL_0006: call int32 DebugInfoMethods::NeverInlined() + IL_000b: pop + IL_000c: ret + } + + .method public hidebysig static int32 TestLateFailingInlineCandidate() cil managed + { + .custom instance void [attribute]ExpectedILMappings::.ctor() = { + property int32[] Debug = int32[2]( 0 6 ) + // property int32[] Opts = int32[2]( 0 6 ) // Currently the 6 will not get a mapping due to a bug + } + .maxstack 2 + .locals init (int32 V_0) + IL_0000: call int32 DebugInfoMethods::NeverInlined() + IL_0005: pop + IL_0006: call int32 DebugInfoMethods::NotInlinedByAnalysis() + IL_000b: ldc.i4.5 + IL_000c: add + IL_000d: stloc.0 + IL_000e: ldloc.0 + IL_000f: ret + } + + .method public hidebysig static int32 TestSucceedingInlineCandidate() cil managed + { + // We still expect a mapping where the call was inlined. + .custom instance void [attribute]ExpectedILMappings::.ctor() = { + property int32[] Debug = int32[2]( 0 6 ) + property int32[] Opts = int32[2]( 0 6 ) + } + .maxstack 2 + .locals init (int32 V_0) + IL_0000: call int32 DebugInfoMethods::NeverInlined() + IL_0005: pop + IL_0006: call int32 DebugInfoMethods::AggressivelyInlined() + IL_000b: ldc.i4.5 + IL_000c: add + IL_000d: stloc.0 + IL_000e: ldloc.0 + IL_000f: ret + } + + .method public hidebysig static int32 TestControlFlow(int32 arg) cil managed + { + .custom instance void [attribute]ExpectedILMappings::.ctor() = { + // Note: We really would want to validate that we are generating mappings for every call instruction in debug, + // as this is used for the managed-ret-val feature, but the debugger filters out these mappings and does not + // report them in the ETW event. We should probably change this, those mappings should be useful in any case. + property int32[] Debug = int32[10]( 0x0 0x6 0xe 0x12 0x1a 0x1c 0x24 0x28 0x2c 0x34 ) + // some entries commented due to a bug with some failing inline candidates + property int32[] Opts = int32[3]( 0x0 0x6 /* 0x12 */ 0x1c /* 0x2c */ ) + } + .maxstack 2 + .locals init (int32 V_0) + IL_0000: ldc.i4.0 + IL_0001: stloc.0 + IL_0002: ldarg.0 + IL_0003: ldc.i4.0 + IL_0004: ble.s IL_001c + + IL_0006: ldloc.0 + IL_0007: call int32 DebugInfoMethods::NeverInlined() + IL_000c: add + IL_000d: stloc.0 + IL_000e: ldloc.0 + IL_000f: ldc.i4.1 + IL_0010: add + IL_0011: stloc.0 + IL_0012: ldloc.0 + IL_0013: call int32 DebugInfoMethods::NotInlinedByAnalysis() + IL_0018: add + IL_0019: stloc.0 + IL_001a: br.s IL_0034 + + IL_001c: ldloc.0 + IL_001d: call int32 DebugInfoMethods::NeverInlined() + IL_0022: add + IL_0023: stloc.0 + IL_0024: ldloc.0 + IL_0025: ldc.i4.1 + IL_0026: add + IL_0027: stloc.0 + IL_0028: ldloc.0 + IL_0029: ldc.i4.3 + IL_002a: add + IL_002b: stloc.0 + IL_002c: ldloc.0 + IL_002d: call int32 DebugInfoMethods::NotInlinedByAnalysis() + IL_0032: add + IL_0033: stloc.0 + IL_0034: ldloc.0 + IL_0035: ret + } + + + .method private hidebysig static int32 NeverInlined() cil managed noinlining + { + .maxstack 8 + IL_0000: ldc.i4.5 + IL_0001: ret + } + + .method private hidebysig static int32 NotInlinedByAnalysis() cil managed + { + .maxstack 5 + .locals init (int32 V_0, + int32 V_1, + int32 V_2, + int32 V_3) + IL_0000: ldc.i4.0 + IL_0001: stloc.0 + IL_0002: br.s IL_006f + + IL_0004: ldc.i4.0 + IL_0005: stloc.1 + IL_0006: br.s IL_0062 + + IL_0008: ldc.i4.0 + IL_0009: stloc.2 + IL_000a: br.s IL_0055 + + IL_000c: ldc.i4.0 + IL_000d: stloc.3 + IL_000e: br.s IL_0048 + + IL_0010: ldstr "{0} {1} {2} {3}" + IL_0015: ldc.i4.4 + IL_0016: newarr [System.Runtime]System.Object + IL_001b: dup + IL_001c: ldc.i4.0 + IL_001d: ldloc.0 + IL_001e: box [System.Runtime]System.Int32 + IL_0023: stelem.ref + IL_0024: dup + IL_0025: ldc.i4.1 + IL_0026: ldloc.1 + IL_0027: box [System.Runtime]System.Int32 + IL_002c: stelem.ref + IL_002d: dup + IL_002e: ldc.i4.2 + IL_002f: ldloc.2 + IL_0030: box [System.Runtime]System.Int32 + IL_0035: stelem.ref + IL_0036: dup + IL_0037: ldc.i4.3 + IL_0038: ldloc.3 + IL_0039: box [System.Runtime]System.Int32 + IL_003e: stelem.ref + IL_003f: call void [System.Console]System.Console::WriteLine(string, + object[]) + IL_0044: ldloc.3 + IL_0045: ldc.i4.1 + IL_0046: add + IL_0047: stloc.3 + IL_0048: ldloc.3 + IL_0049: call int32 [System.Runtime]System.Environment::get_TickCount() + IL_004e: neg + IL_004f: blt.s IL_0010 + + IL_0051: ldloc.2 + IL_0052: ldc.i4.1 + IL_0053: add + IL_0054: stloc.2 + IL_0055: ldloc.2 + IL_0056: call int32 [System.Runtime]System.Environment::get_TickCount() + IL_005b: neg + IL_005c: blt.s IL_000c + + IL_005e: ldloc.1 + IL_005f: ldc.i4.1 + IL_0060: add + IL_0061: stloc.1 + IL_0062: ldloc.1 + IL_0063: call int32 [System.Runtime]System.Environment::get_TickCount() + IL_0068: neg + IL_0069: blt.s IL_0008 + + IL_006b: ldloc.0 + IL_006c: ldc.i4.1 + IL_006d: add + IL_006e: stloc.0 + IL_006f: ldloc.0 + IL_0070: call int32 [System.Runtime]System.Environment::get_TickCount() + IL_0075: neg + IL_0076: blt.s IL_0004 + + IL_0078: ldc.i4.5 + IL_0079: ret + } + + .method private hidebysig static int32 AggressivelyInlined() cil managed aggressiveinlining + { + call int32 DebugInfoMethods::NeverInlined() + pop + call int32 DebugInfoMethods::NeverInlined() + ret + } + + .method public hidebysig specialname rtspecialname + instance void .ctor() cil managed + { + // Code size 7 (0x7) + .maxstack 8 + IL_0000: ldarg.0 + IL_0001: call instance void [System.Runtime]System.Object::.ctor() + IL_0006: ret + } + +} \ No newline at end of file diff --git a/src/tests/JIT/Directed/debugging/debuginfo/tests_d.ilproj b/src/tests/JIT/Directed/debugging/debuginfo/tests_d.ilproj new file mode 100644 index 00000000000000..ef6b20b8008737 --- /dev/null +++ b/src/tests/JIT/Directed/debugging/debuginfo/tests_d.ilproj @@ -0,0 +1,12 @@ + + + library + $(IlasmFlags) -DEBUG=IMPL + BuildOnly + + + + + + + diff --git a/src/tests/JIT/Directed/debugging/debuginfo/tests_r.ilproj b/src/tests/JIT/Directed/debugging/debuginfo/tests_r.ilproj new file mode 100644 index 00000000000000..0efd259b28c5ca --- /dev/null +++ b/src/tests/JIT/Directed/debugging/debuginfo/tests_r.ilproj @@ -0,0 +1,11 @@ + + + library + $(IlasmFlags) -DEBUG=OPT + BuildOnly + + + + + + diff --git a/src/tests/JIT/Directed/debugging/poison.cs b/src/tests/JIT/Directed/debugging/poisoning/poison.cs similarity index 100% rename from src/tests/JIT/Directed/debugging/poison.cs rename to src/tests/JIT/Directed/debugging/poisoning/poison.cs diff --git a/src/tests/JIT/Directed/debugging/poison.csproj b/src/tests/JIT/Directed/debugging/poisoning/poison.csproj similarity index 100% rename from src/tests/JIT/Directed/debugging/poison.csproj rename to src/tests/JIT/Directed/debugging/poisoning/poison.csproj