diff --git a/src/libraries/System.Diagnostics.StackTrace/tests/ExceptionTestAssembly/ExceptionTestAssembly.cs b/src/libraries/System.Diagnostics.StackTrace/tests/ExceptionTestAssembly/ExceptionTestAssembly.cs new file mode 100644 index 0000000000000..49902b1000c02 --- /dev/null +++ b/src/libraries/System.Diagnostics.StackTrace/tests/ExceptionTestAssembly/ExceptionTestAssembly.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +class Program +{ + public static void Foo() + { + throw new NullReferenceException(); + } +} diff --git a/src/libraries/System.Diagnostics.StackTrace/tests/ExceptionTestAssembly/ExceptionTestAssembly.csproj b/src/libraries/System.Diagnostics.StackTrace/tests/ExceptionTestAssembly/ExceptionTestAssembly.csproj new file mode 100644 index 0000000000000..d7b60a977d7ce --- /dev/null +++ b/src/libraries/System.Diagnostics.StackTrace/tests/ExceptionTestAssembly/ExceptionTestAssembly.csproj @@ -0,0 +1,10 @@ + + + true + netstandard2.0 + None + + + + + diff --git a/src/libraries/System.Diagnostics.StackTrace/tests/StackTraceTests.cs b/src/libraries/System.Diagnostics.StackTrace/tests/StackTraceTests.cs index 894374ba29b4d..0464a22a44014 100644 --- a/src/libraries/System.Diagnostics.StackTrace/tests/StackTraceTests.cs +++ b/src/libraries/System.Diagnostics.StackTrace/tests/StackTraceTests.cs @@ -3,9 +3,13 @@ using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; using System.Reflection; +using System.Reflection.Emit; using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; +using Microsoft.DotNet.RemoteExecutor; using Xunit; namespace System.Diagnostics @@ -300,6 +304,72 @@ public void ToString_NullFrame_ThrowsNullReferenceException() Assert.Equal(Environment.NewLine, stackTrace.ToString()); } + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/51096", typeof(PlatformDetection), nameof(PlatformDetection.IsMonoInterpreter))] + public void ToString_ShowILOffset() + { + string AssemblyName = "ExceptionTestAssembly.dll"; + string SourceTestAssemblyPath = Path.Combine(Environment.CurrentDirectory, AssemblyName); + string regPattern = @":token 0x([a-f0-9]*)\+0x([a-f0-9]*)"; + + // Normal loading case + RemoteExecutor.Invoke((asmPath, asmName, p) => + { + AppContext.SetSwitch("Switch.System.Diagnostics.StackTrace.ShowILOffsets", true); + var asm = Assembly.LoadFrom(asmPath); + try + { + asm.GetType("Program").GetMethod("Foo").Invoke(null, null); + } + catch (Exception e) + { + Assert.Contains(asmName, e.InnerException.StackTrace); + Assert.True(Regex.Match(e.InnerException.StackTrace, p).Success); + } + }, SourceTestAssemblyPath, AssemblyName, regPattern).Dispose(); + + // Assembly.Load(Byte[]) case + RemoteExecutor.Invoke((asmPath, asmName, p) => + { + AppContext.SetSwitch("Switch.System.Diagnostics.StackTrace.ShowILOffsets", true); + var inMemBlob = File.ReadAllBytes(asmPath); + var asm2 = Assembly.Load(inMemBlob); + try + { + asm2.GetType("Program").GetMethod("Foo").Invoke(null, null); + } + catch (Exception e) + { + Assert.Contains(asmName, e.InnerException.StackTrace); + Assert.True(Regex.Match(e.InnerException.StackTrace, p).Success); + } + }, SourceTestAssemblyPath, AssemblyName, regPattern).Dispose(); + + // AssmblyBuilder.DefineDynamicAssembly() case + RemoteExecutor.Invoke((p) => + { + AppContext.SetSwitch("Switch.System.Diagnostics.StackTrace.ShowILOffsets", true); + AssemblyName asmName = new AssemblyName("ExceptionTestAssembly"); + AssemblyBuilder asmBldr = AssemblyBuilder.DefineDynamicAssembly(asmName, AssemblyBuilderAccess.Run); + ModuleBuilder modBldr = asmBldr.DefineDynamicModule(asmName.Name); + TypeBuilder tBldr = modBldr.DefineType("Program"); + MethodBuilder mBldr = tBldr.DefineMethod("Foo", MethodAttributes.Public | MethodAttributes.Static, null, null); + ILGenerator ilGen = mBldr.GetILGenerator(); + ilGen.ThrowException(typeof(NullReferenceException)); + ilGen.Emit(OpCodes.Ret); + Type t = tBldr.CreateType(); + try + { + t.InvokeMember("Foo", BindingFlags.InvokeMethod, null, null, null); + } + catch (Exception e) + { + Assert.Contains("RefEmit_InMemoryManifestModule", e.InnerException.StackTrace); + Assert.True(Regex.Match(e.InnerException.StackTrace, p).Success); + } + }, regPattern).Dispose(); + } + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] private static StackTrace NoParameters() => new StackTrace(); [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] diff --git a/src/libraries/System.Diagnostics.StackTrace/tests/System.Diagnostics.StackTrace.Tests.csproj b/src/libraries/System.Diagnostics.StackTrace/tests/System.Diagnostics.StackTrace.Tests.csproj index b112648c18363..93673781e705f 100644 --- a/src/libraries/System.Diagnostics.StackTrace/tests/System.Diagnostics.StackTrace.Tests.csproj +++ b/src/libraries/System.Diagnostics.StackTrace/tests/System.Diagnostics.StackTrace.Tests.csproj @@ -2,6 +2,7 @@ $(NetCoreAppCurrent) true + true @@ -17,4 +18,7 @@ - \ No newline at end of file + + + + diff --git a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx index 807cca91a37ae..2b214c82eaf72 100644 --- a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx +++ b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx @@ -3253,6 +3253,9 @@ The timeout must represent a value between -1 and Int32.MaxValue, inclusive. + + in {0}:token 0x{1:x}+0x{2:x} + in {0}:line {1} diff --git a/src/libraries/System.Private.CoreLib/src/System/Diagnostics/StackTrace.cs b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/StackTrace.cs index 3c6ba961c79d3..10865d4432158 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Diagnostics/StackTrace.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/StackTrace.cs @@ -206,6 +206,7 @@ internal void ToString(TraceFormat traceFormat, StringBuilder sb) string word_At = SR.GetResourceString(nameof(SR.Word_At), defaultString: "at"); // We also want to pass in a default for inFileLineNumber. string inFileLineNum = SR.GetResourceString(nameof(SR.StackTrace_InFileLineNumber), defaultString: "in {0}:line {1}"); + string inFileILOffset = SR.GetResourceString(nameof(SR.StackTrace_InFileILOffset), defaultString: "in {0}:token 0x{1:x}+0x{2:x}"); bool fFirstFrame = true; for (int iFrameIndex = 0; iFrameIndex < _numOfFrames; iFrameIndex++) { @@ -323,6 +324,17 @@ internal void ToString(TraceFormat traceFormat, StringBuilder sb) sb.Append(' '); sb.AppendFormat(CultureInfo.InvariantCulture, inFileLineNum, fileName, sf.GetFileLineNumber()); } + else if (LocalAppContextSwitches.ShowILOffsets && mb.ReflectedType != null) + { + string assemblyName = mb.ReflectedType.Module.ScopeName; + try + { + int token = mb.MetadataToken; + sb.Append(' '); + sb.AppendFormat(CultureInfo.InvariantCulture, inFileILOffset, assemblyName, token, sf.GetILOffset()); + } + catch (System.InvalidOperationException) {} + } } // Skip EDI boundary for async diff --git a/src/libraries/System.Private.CoreLib/src/System/LocalAppContextSwitches.cs b/src/libraries/System.Private.CoreLib/src/System/LocalAppContextSwitches.cs index d669bd4cccc4e..ed450d7494369 100644 --- a/src/libraries/System.Private.CoreLib/src/System/LocalAppContextSwitches.cs +++ b/src/libraries/System.Private.CoreLib/src/System/LocalAppContextSwitches.cs @@ -47,5 +47,23 @@ public static bool SerializationGuard [MethodImpl(MethodImplOptions.AggressiveInlining)] get => GetCachedSwitchValue("Switch.System.Runtime.Serialization.SerializationGuard", ref s_serializationGuard); } + + private static int s_showILOffset; + private static bool GetDefaultShowILOffsetSetting() + { + if (s_showILOffset < 0) return false; + if (s_showILOffset > 0) return true; + + bool isSwitchEnabled = AppContextConfigHelper.GetBooleanConfig("Switch.System.Diagnostics.StackTrace.ShowILOffsets", true); + s_showILOffset = isSwitchEnabled ? 1 : -1; + + return isSwitchEnabled; + } + + public static bool ShowILOffsets + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => GetDefaultShowILOffsetSetting(); + } } }