diff --git a/.vscode/settings.json b/.vscode/settings.json
index 47a8ff1f7f..1425c69a44 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,3 +1,4 @@
{
- "omnisharp.useModernNet": true
+ "omnisharp.useModernNet": true,
+ "dotnet.defaultSolution": "Sentry.sln"
}
diff --git a/samples/Sentry.Samples.Console.Basic/Sentry.Samples.Console.Basic.csproj b/samples/Sentry.Samples.Console.Basic/Sentry.Samples.Console.Basic.csproj
index fd327d2f31..ba11fdf51b 100644
--- a/samples/Sentry.Samples.Console.Basic/Sentry.Samples.Console.Basic.csproj
+++ b/samples/Sentry.Samples.Console.Basic/Sentry.Samples.Console.Basic.csproj
@@ -2,9 +2,10 @@
Exe
- net6.0
+ net7.0
enable
enable
+ true
diff --git a/src/Sentry/AttributeReader.cs b/src/Sentry/AttributeReader.cs
index 1fa69e220b..2ad9fc9e0a 100644
--- a/src/Sentry/AttributeReader.cs
+++ b/src/Sentry/AttributeReader.cs
@@ -2,11 +2,6 @@
internal static class AttributeReader
{
- public static bool TryGetProjectDirectory(Assembly assembly, out string? projectDirectory)
- {
- projectDirectory = assembly.GetCustomAttributes()
- .FirstOrDefault(x => x.Key == "Sentry.ProjectDirectory")
- ?.Value;
- return projectDirectory != null;
- }
+ public static string? TryGetProjectDirectory(Assembly assembly) =>
+ assembly.GetCustomAttributes().FirstOrDefault(x => x.Key == "Sentry.ProjectDirectory")?.Value;
}
diff --git a/src/Sentry/Internal/DebugStackTrace.cs b/src/Sentry/Internal/DebugStackTrace.cs
index 1d92182b50..95c4e2c1ec 100644
--- a/src/Sentry/Internal/DebugStackTrace.cs
+++ b/src/Sentry/Internal/DebugStackTrace.cs
@@ -31,6 +31,9 @@ internal class DebugStackTrace : SentryStackTrace
private static readonly Regex RegexAsyncReturn = new(@"^(.+`[0-9]+)\[\[",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
+ private static readonly Regex RegexNativeAOTInfo = new(@"^(.+)\.([^.]+\(.*\)) ?\+ ?0x",
+ RegexOptions.Compiled | RegexOptions.CultureInvariant);
+
internal DebugStackTrace(SentryOptions options)
{
_options = options;
@@ -123,11 +126,14 @@ private IEnumerable CreateFrames(StackTrace stackTrace, bool i
{
var frames = _options.StackTraceMode switch
{
- StackTraceMode.Enhanced => EnhancedStackTrace.GetFrames(stackTrace).Select(p => p as StackFrame),
+ StackTraceMode.Enhanced => EnhancedStackTrace.GetFrames(stackTrace).Select(p => new RealStackFrame(p)),
_ => stackTrace.GetFrames()
// error CS8619: Nullability of reference types in value of type 'StackFrame?[]' doesn't match target type 'IEnumerable'.
#if NETCOREAPP3_0
.Where(f => f is not null)
+ .Select(p => new RealStackFrame(p!))
+#else
+ .Select(p => new RealStackFrame(p))
#endif
};
@@ -153,102 +159,149 @@ private IEnumerable CreateFrames(StackTrace stackTrace, bool i
#endif
// Remove the frames until the call for capture with the SDK
- if (firstFrame
- && isCurrentStackTrace
- && stackFrame.GetMethod() is { } method
- && method.DeclaringType?.AssemblyQualifiedName?.StartsWith("Sentry") == true)
+ if (firstFrame && isCurrentStackTrace)
{
- _options.LogDebug("Skipping initial stack frame '{0}'", method.Name);
- continue;
+ string? frameInfo = null;
+ if (stackFrame.GetMethod() is { } method)
+ {
+ frameInfo = method.DeclaringType?.AssemblyQualifiedName;
+ }
+
+ // Native AOT currently only exposes some method info at runtime via ToString().
+ // See https://github.com/dotnet/runtime/issues/92869
+ if (frameInfo is null && stackFrame.HasNativeImage())
+ {
+ frameInfo = stackFrame.ToString();
+ }
+
+ if (frameInfo?.StartsWith("Sentry") is true)
+ {
+ _options.LogDebug("Skipping initial stack frame '{0}'", frameInfo);
+ continue;
+ }
}
firstFrame = false;
- yield return CreateFrame(stackFrame);
+ if (CreateFrame(stackFrame) is { } frame)
+ {
+ yield return frame;
+ }
+ else
+ {
+ _options.LogDebug("Could not resolve stack frame '{0}'", stackFrame.ToString());
+ }
}
}
///
- /// Create a from a .
+ /// Native AOT implementation of CreateFrame.
+ /// Native frames have only limited method information at runtime (and even that can be disabled).
+ /// We try to parse that and also add addresses for server-side symbolication.
///
- internal SentryStackFrame CreateFrame(StackFrame stackFrame) => InternalCreateFrame(stackFrame, true);
+ private SentryStackFrame? TryCreateNativeAOTFrame(IStackFrame stackFrame)
+ {
+ if (!stackFrame.HasNativeImage())
+ {
+ return null;
+ }
+
+ var frame = ParseNativeAOTToString(stackFrame.ToString());
+ frame.ImageAddress = stackFrame.GetNativeImageBase();
+ frame.InstructionAddress = stackFrame.GetNativeIP();
+ return frame;
+ }
+
+ // Method info is currently only exposed by ToString(), see https://github.com/dotnet/runtime/issues/92869
+ // We only care about the case where the method is available (`StackTraceSupport` property is the default `true`):
+ // https://github.com/dotnet/runtime/blob/254230253da143a082f47cfaf8711627c0bf2faf/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/DeveloperExperience/DeveloperExperience.cs#L42
+ internal static SentryStackFrame ParseNativeAOTToString(string info)
+ {
+ var frame = new SentryStackFrame();
+ var match = RegexNativeAOTInfo.Match(info);
+ if (match is { Success: true, Groups.Count: 3 })
+ {
+ frame.Module = match.Groups[1].Value;
+ frame.Function = match.Groups[2].Value;
+ }
+ return frame;
+ }
///
/// Default the implementation of CreateFrame.
///
- private SentryStackFrame InternalCreateFrame(StackFrame stackFrame, bool demangle)
+ private SentryStackFrame? TryCreateManagedFrame(IStackFrame stackFrame)
{
+ if (stackFrame.GetMethod() is not { } method)
+ {
+ return null;
+ }
+
const string unknownRequiredField = "(unknown)";
- string? projectPath = null;
- var frame = new SentryStackFrame();
- if (stackFrame.GetMethod() is { } method)
+ var frame = new SentryStackFrame
{
- frame.Module = method.DeclaringType?.FullName ?? unknownRequiredField;
- frame.Package = method.DeclaringType?.Assembly.FullName;
+ Module = method.DeclaringType?.FullName ?? unknownRequiredField,
+ Package = method.DeclaringType?.Assembly.FullName
+ };
- if (_options.StackTraceMode == StackTraceMode.Enhanced &&
- stackFrame is EnhancedStackFrame enhancedStackFrame)
- {
- var stringBuilder = new StringBuilder();
- frame.Function = enhancedStackFrame.MethodInfo.Append(stringBuilder, false).ToString();
+ if (stackFrame.Frame is EnhancedStackFrame enhancedStackFrame)
+ {
+ var stringBuilder = new StringBuilder();
+ frame.Function = enhancedStackFrame.MethodInfo.Append(stringBuilder, false).ToString();
- if (enhancedStackFrame.MethodInfo.DeclaringType is { } declaringType)
- {
- stringBuilder.Clear();
- stringBuilder.AppendTypeDisplayName(declaringType);
-
- // Ben.Demystifier doesn't always include the namespace, even when fullName==true.
- // It's important that the module name always be fully qualified, so that in-app frame
- // detection works correctly.
- var module = stringBuilder.ToString();
- frame.Module = declaringType.Namespace is { } ns && !module.StartsWith(ns)
- ? $"{ns}.{module}"
- : module;
- }
- }
- else
+ if (enhancedStackFrame.MethodInfo.DeclaringType is { } declaringType)
{
- frame.Function = method.Name;
+ stringBuilder.Clear();
+ stringBuilder.AppendTypeDisplayName(declaringType);
+
+ // Ben.Demystifier doesn't always include the namespace, even when fullName==true.
+ // It's important that the module name always be fully qualified, so that in-app frame
+ // detection works correctly.
+ var module = stringBuilder.ToString();
+ frame.Module = declaringType.Namespace is { } ns && !module.StartsWith(ns)
+ ? $"{ns}.{module}"
+ : module;
}
+ }
+ else
+ {
+ frame.Function = method.Name;
+ }
- // Originally we didn't skip methods from dynamic assemblies, so not to break compatibility:
- if (_options.StackTraceMode != StackTraceMode.Original && method.Module.Assembly.IsDynamic)
- {
- frame.InApp = false;
- }
+ // Originally we didn't skip methods from dynamic assemblies, so not to break compatibility:
+ if (_options.StackTraceMode != StackTraceMode.Original && method.Module.Assembly.IsDynamic)
+ {
+ frame.InApp = false;
+ }
- AttributeReader.TryGetProjectDirectory(method.Module.Assembly, out projectPath);
+ if (AddDebugImage(method.Module) is { } moduleIdx && moduleIdx != DebugImageMissing)
+ {
+ frame.AddressMode = GetRelativeAddressMode(moduleIdx);
- if (AddDebugImage(method.Module) is { } moduleIdx && moduleIdx != DebugImageMissing)
+ try
{
- frame.AddressMode = GetRelativeAddressMode(moduleIdx);
-
- try
- {
- var token = method.MetadataToken;
- // The top byte is the token type, the lower three bytes are the record id.
- // See: https://docs.microsoft.com/en-us/previous-versions/dotnet/netframework-4.0/ms404456(v=vs.100)#metadata-token-structure
- var tokenType = token & 0xff000000;
- // See https://docs.microsoft.com/en-us/dotnet/framework/unmanaged-api/metadata/cortokentype-enumeration
- if (tokenType == 0x06000000) // CorTokenType.mdtMethodDef
- {
- frame.FunctionId = token & 0x00ffffff;
- }
- }
- catch (InvalidOperationException)
+ var token = method.MetadataToken;
+ // The top byte is the token type, the lower three bytes are the record id.
+ // See: https://docs.microsoft.com/en-us/previous-versions/dotnet/netframework-4.0/ms404456(v=vs.100)#metadata-token-structure
+ var tokenType = token & 0xff000000;
+ // See https://docs.microsoft.com/en-us/dotnet/framework/unmanaged-api/metadata/cortokentype-enumeration
+ if (tokenType == 0x06000000) // CorTokenType.mdtMethodDef
{
- // method.MetadataToken may throw
- // see https://learn.microsoft.com/en-us/dotnet/api/system.reflection.memberinfo.metadatatoken?view=net-6.0
- _options.LogDebug("Could not get MetadataToken for stack frame {0} from {1}", frame.Function, method.Module.Name);
+ frame.FunctionId = token & 0x00ffffff;
}
}
+ catch (InvalidOperationException)
+ {
+ // method.MetadataToken may throw
+ // see https://learn.microsoft.com/en-us/dotnet/api/system.reflection.memberinfo.metadatatoken?view=net-6.0
+ _options.LogDebug("Could not get MetadataToken for stack frame {0} from {1}", frame.Function, method.Module.Name);
+ }
}
- frame.ConfigureAppFrame(_options);
-
if (stackFrame.GetFileName() is { } frameFileName)
{
- if (projectPath != null && frameFileName.StartsWith(projectPath, StringComparison.OrdinalIgnoreCase))
+ if (AttributeReader.TryGetProjectDirectory(method.Module.Assembly) is { } projectPath
+ && frameFileName.StartsWith(projectPath, StringComparison.OrdinalIgnoreCase))
{
frame.AbsolutePath = frameFileName;
frameFileName = frameFileName[projectPath.Length..];
@@ -256,6 +309,23 @@ private SentryStackFrame InternalCreateFrame(StackFrame stackFrame, bool demangl
frame.FileName = frameFileName;
}
+ return frame;
+ }
+
+ ///
+ /// Create a from a .
+ ///
+ internal SentryStackFrame? CreateFrame(IStackFrame stackFrame)
+ {
+ var frame = TryCreateManagedFrame(stackFrame);
+ frame ??= TryCreateNativeAOTFrame(stackFrame);
+ if (frame is null)
+ {
+ return null;
+ }
+
+ frame.ConfigureAppFrame(_options);
+
// stackFrame.HasILOffset() throws NotImplemented on Mono 5.12
var ilOffset = stackFrame.GetILOffset();
if (ilOffset != StackFrame.OFFSET_UNKNOWN)
@@ -270,19 +340,19 @@ private SentryStackFrame InternalCreateFrame(StackFrame stackFrame, bool demangl
}
var colNo = stackFrame.GetFileColumnNumber();
- if (lineNo > 0)
+ if (colNo > 0)
{
frame.ColumnNumber = colNo;
}
- if (demangle && _options.StackTraceMode != StackTraceMode.Enhanced)
+ if (stackFrame.Frame is not EnhancedStackFrame)
{
DemangleAsyncFunctionName(frame);
DemangleAnonymousFunction(frame);
DemangleLambdaReturnType(frame);
}
- if (_options.StackTraceMode == StackTraceMode.Enhanced)
+ if (stackFrame.Frame is EnhancedStackFrame)
{
// In Enhanced mode, Module (which in this case is the Namespace)
// is already prepended to the function, after return type.
diff --git a/src/Sentry/Internal/StackFrame.cs b/src/Sentry/Internal/StackFrame.cs
new file mode 100644
index 0000000000..bb4980eb43
--- /dev/null
+++ b/src/Sentry/Internal/StackFrame.cs
@@ -0,0 +1,131 @@
+using Sentry.Internal.Extensions;
+using Sentry.Extensibility;
+
+namespace Sentry.Internal;
+
+///
+/// Mockable variant of the Diagnostics.StackFrame.
+/// This is necessary to test NativeAOT code that relies on extensions from StackFrameExtensions.
+///
+internal interface IStackFrame
+{
+ StackFrame? Frame { get; }
+
+ ///
+ /// Returns a pointer to the base address of the native image that this stack frame is executing.
+ ///
+ ///
+ /// A pointer to the base address of the native image or System.IntPtr.Zero if you're targeting the .NET Framework.
+ ///
+ public nint GetNativeImageBase();
+
+ ///
+ /// Gets an interface pointer to the start of the native code for the method that is being executed.
+ ///
+ ///
+ /// An interface pointer to the start of the native code for the method that is being
+ /// executed or System.IntPtr.Zero if you're targeting the .NET Framework.
+ ///
+ public nint GetNativeIP();
+
+ ///
+ /// Indicates whether the native image is available for the specified stack frame.
+ ///
+ ///
+ /// true if a native image is available for this stack frame; otherwise, false.
+ ///
+ public bool HasNativeImage();
+
+ ///
+ /// Gets the column number in the file that contains the code that is executing.
+ /// This information is typically extracted from the debugging symbols for the executable.
+ ///
+ ///
+ /// The file column number, or 0 (zero) if the file column number cannot be determined.
+ ///
+ public int GetFileColumnNumber();
+
+ ///
+ /// Gets the line number in the file that contains the code that is executing. This
+ /// information is typically extracted from the debugging symbols for the executable.
+ ///
+ ///
+ /// The file line number, or 0 (zero) if the file line number cannot be determined.
+ ///
+ public int GetFileLineNumber();
+
+ ///
+ /// Gets the file name that contains the code that is executing. This information
+ /// is typically extracted from the debugging symbols for the executable.
+ ///
+ ///
+ /// The file name, or null if the file name cannot be determined.
+ ///
+ public string? GetFileName();
+
+ ///
+ /// Gets the offset from the start of the Microsoft intermediate language (MSIL)
+ /// code for the method that is executing. This offset might be an approximation
+ /// depending on whether or not the just-in-time (JIT) compiler is generating debugging
+ /// code. The generation of this debugging information is controlled by the System.Diagnostics.DebuggableAttribute.
+ ///
+ ///
+ /// The offset from the start of the MSIL code for the method that is executing.
+ ///
+ public int GetILOffset();
+
+ ///
+ /// Gets the method in which the frame is executing.
+ ///
+ ///
+ /// The method in which the frame is executing.
+ ///
+
+ public MethodBase? GetMethod();
+
+ ///
+ /// Builds a readable representation of the stack trace.
+ ///
+ ///
+ /// A readable representation of the stack trace.
+ ///
+ public string ToString();
+}
+
+internal class RealStackFrame : IStackFrame
+{
+ private readonly StackFrame _frame;
+
+ public RealStackFrame(StackFrame frame)
+ {
+ _frame = frame;
+ }
+
+ public StackFrame? Frame => _frame;
+
+ public override string ToString() => _frame.ToString();
+
+ public int GetFileColumnNumber() => _frame.GetFileColumnNumber();
+
+ public int GetFileLineNumber() => _frame.GetFileLineNumber();
+
+ public string? GetFileName() => _frame.GetFileName();
+
+ public int GetILOffset() => _frame.GetILOffset();
+
+ public MethodBase? GetMethod() => _frame.GetMethod();
+
+#if NET5_0_OR_GREATER
+ public nint GetNativeImageBase() => _frame.GetNativeImageBase();
+
+ public nint GetNativeIP() => _frame.GetNativeIP();
+
+ public bool HasNativeImage() => _frame.HasNativeImage();
+#else
+ public nint GetNativeImageBase() => default;
+
+ public nint GetNativeIP() => default;
+
+ public bool HasNativeImage() => false;
+#endif
+}
diff --git a/test/Sentry.Tests/AttributeReaderTests.cs b/test/Sentry.Tests/AttributeReaderTests.cs
index c1d9be89bd..25a5574e75 100644
--- a/test/Sentry.Tests/AttributeReaderTests.cs
+++ b/test/Sentry.Tests/AttributeReaderTests.cs
@@ -6,7 +6,6 @@ public class AttributeReaderTests
public void Simple()
{
var assembly = typeof(AttributeReaderTests).Assembly;
- Assert.True(AttributeReader.TryGetProjectDirectory(assembly, out var projectDirectory));
- Assert.NotNull(projectDirectory);
+ Assert.NotNull(AttributeReader.TryGetProjectDirectory(assembly));
}
}
diff --git a/test/Sentry.Tests/Internals/DebugStackTraceTests.CreateFrame_ForNativeAOT.verified.txt b/test/Sentry.Tests/Internals/DebugStackTraceTests.CreateFrame_ForNativeAOT.verified.txt
new file mode 100644
index 0000000000..d6da3fed6b
--- /dev/null
+++ b/test/Sentry.Tests/Internals/DebugStackTraceTests.CreateFrame_ForNativeAOT.verified.txt
@@ -0,0 +1,7 @@
+{
+ function: DoSomething(int, long),
+ module: Foo.Bar,
+ in_app: true,
+ image_addr: 0x1,
+ instruction_addr: 0x2
+}
\ No newline at end of file
diff --git a/test/Sentry.Tests/Internals/DebugStackTraceTests.cs b/test/Sentry.Tests/Internals/DebugStackTraceTests.verify.cs
similarity index 68%
rename from test/Sentry.Tests/Internals/DebugStackTraceTests.cs
rename to test/Sentry.Tests/Internals/DebugStackTraceTests.verify.cs
index 62e68c4524..36d7b62162 100644
--- a/test/Sentry.Tests/Internals/DebugStackTraceTests.cs
+++ b/test/Sentry.Tests/Internals/DebugStackTraceTests.verify.cs
@@ -1,7 +1,11 @@
+#nullable enable
// ReSharper disable once CheckNamespace
// Stack trace filters out Sentry frames by namespace
+
namespace Other.Tests.Internals;
+[UsesVerify]
+
public class DebugStackTraceTests
{
private class Fixture
@@ -11,7 +15,7 @@ private class Fixture
}
private readonly Fixture _fixture = new();
- private static readonly string ThisNamespace = typeof(SentryStackTraceFactoryTests).Namespace;
+ private static readonly string ThisNamespace = typeof(SentryStackTraceFactoryTests).Namespace!;
[Fact]
public void CreateSentryStackFrame_AppNamespace_InAppFrame()
@@ -19,9 +23,9 @@ public void CreateSentryStackFrame_AppNamespace_InAppFrame()
var frame = new StackFrame();
var sut = _fixture.GetSut();
- var actual = sut.CreateFrame(frame);
+ var actual = sut.CreateFrame(new RealStackFrame(frame));
- Assert.True(actual.InApp);
+ Assert.True(actual?.InApp);
}
[Fact]
@@ -31,9 +35,9 @@ public void CreateSentryStackFrame_AppNamespaceExcluded_NotInAppFrame()
var sut = _fixture.GetSut();
var frame = new StackFrame();
- var actual = sut.CreateFrame(frame);
+ var actual = sut.CreateFrame(new RealStackFrame(frame));
- Assert.False(actual.InApp);
+ Assert.False(actual?.InApp);
}
[Theory]
@@ -52,10 +56,10 @@ public void CreateSentryStackFrame_SystemType_NotInAppFrame(bool useEnhancedStac
Assert.Equal(typeof(Convert), frame.GetMethod()?.DeclaringType);
// Act
- var actual = sut.CreateFrame(frame);
+ var actual = sut.CreateFrame(new RealStackFrame(frame));
// Assert
- Assert.False(actual.InApp);
+ Assert.False(actual?.InApp);
}
[Fact]
@@ -66,9 +70,9 @@ public void CreateSentryStackFrame_NamespaceIncludedAndExcluded_IncludesTakesPre
var sut = _fixture.GetSut();
var frame = new StackFrame();
- var actual = sut.CreateFrame(frame);
+ var actual = sut.CreateFrame(new RealStackFrame(frame));
- Assert.True(actual.InApp);
+ Assert.True(actual?.InApp);
}
// https://github.com/getsentry/sentry-dotnet/issues/64
@@ -180,6 +184,43 @@ void CheckStackTraceIsUnchanged(SentryStackTrace stackTrace)
}
}
+ [Fact]
+ public void ParseNativeAOTToString()
+ {
+ var frame = DebugStackTrace.ParseNativeAOTToString(
+ "System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task) + 0x42 at offset 66 in file:line:column :0:0");
+ Assert.Equal("System.Runtime.CompilerServices.TaskAwaiter", frame.Module);
+ Assert.Equal("HandleNonSuccessAndDebuggerNotification(Task)", frame.Function);
+ Assert.Null(frame.Package);
+
+ frame = DebugStackTrace.ParseNativeAOTToString(
+ "Program.<$>d__0.MoveNext() + 0xdd at offset 221 in file:line:column :0:0");
+ Assert.Equal("Program.<$>d__0", frame.Module);
+ Assert.Equal("MoveNext()", frame.Function);
+ Assert.Null(frame.Package);
+
+ frame = DebugStackTrace.ParseNativeAOTToString(
+ "Sentry.Samples.Console.Basic!+0x4abb3b at offset 283 in file:line:column :0:0");
+ Assert.Null(frame.Module);
+ Assert.Null(frame.Function);
+ Assert.Null(frame.Package);
+ }
+
+ [Fact]
+ public Task CreateFrame_ForNativeAOT()
+ {
+ var sut = _fixture.GetSut();
+ var frame = sut.CreateFrame(new StubNativeAOTStackFrame()
+ {
+ Function = "DoSomething(int, long)",
+ Module = "Foo.Bar",
+ ImageBase = 1,
+ IP = 2,
+ });
+
+ return VerifyJson(frame.ToJsonString());
+ }
+
private class InjectableDebugStackTrace : DebugStackTrace
{
public InjectableDebugStackTrace(SentryOptions options) : base(options) { }
@@ -193,4 +234,41 @@ public void Inject(int identifier)
});
}
}
+ internal class StubNativeAOTStackFrame : IStackFrame
+ {
+ internal string? Function;
+ internal string? Module;
+ internal nint ImageBase;
+ internal nint IP;
+
+ public StackFrame? Frame => null;
+
+ public int GetFileColumnNumber() => 0;
+
+ public int GetFileLineNumber() => 0;
+
+ public string? GetFileName() => null;
+
+ public int GetILOffset() => StackFrame.OFFSET_UNKNOWN;
+
+ public MethodBase? GetMethod() => null;
+
+ public nint GetNativeImageBase() => ImageBase;
+
+ public nint GetNativeIP() => IP;
+
+ public bool HasNativeImage() => true;
+
+ public override string ToString()
+ {
+ if (Function is not null && Module is not null)
+ {
+ return $"{Module}.{Function} + 0x{ImageBase:x} at offset 0x{IP - ImageBase:x} in file:line:column :0:0";
+ }
+ else
+ {
+ return "";
+ }
+ }
+ }
}