Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Capture Debug Images and StackFrame fields for Portable PDB symbolication #1785

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/Sentry/DebugImage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ public sealed class DebugImage : IJsonSerializable
/// </summary>
public string? DebugId { get; set; }

/// <summary>
/// Checksum of the companion debug file.
/// </summary>
public string? DebugChecksum { get; set; }

/// <summary>
/// Path and name of the debug companion file.
/// </summary>
Expand All @@ -57,6 +62,7 @@ public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger)
writer.WriteStringIfNotWhiteSpace("image_addr", ImageAddress);
writer.WriteNumberIfNotNull("image_size", ImageSize);
writer.WriteStringIfNotWhiteSpace("debug_id", DebugId);
writer.WriteStringIfNotWhiteSpace("debug_checksum", DebugChecksum);
writer.WriteStringIfNotWhiteSpace("debug_file", DebugFile);
writer.WriteStringIfNotWhiteSpace("code_id", CodeId);
writer.WriteStringIfNotWhiteSpace("code_file", CodeFile);
Expand All @@ -73,6 +79,7 @@ public static DebugImage FromJson(JsonElement json)
var imageAddress = json.GetPropertyOrNull("image_addr")?.GetString();
var imageSize = json.GetPropertyOrNull("image_size")?.GetInt64();
var debugId = json.GetPropertyOrNull("debug_id")?.GetString();
var debugChecksum = json.GetPropertyOrNull("debug_checksum")?.GetString();
var debugFile = json.GetPropertyOrNull("debug_file")?.GetString();
var codeId = json.GetPropertyOrNull("code_id")?.GetString();
var codeFile = json.GetPropertyOrNull("code_file")?.GetString();
Expand All @@ -83,6 +90,7 @@ public static DebugImage FromJson(JsonElement json)
ImageAddress = imageAddress,
ImageSize = imageSize,
DebugId = debugId,
DebugChecksum = debugChecksum,
DebugFile = debugFile,
CodeId = codeId,
CodeFile = codeFile,
Expand Down
8 changes: 8 additions & 0 deletions src/Sentry/Extensibility/ISentryStackTraceFactory.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Collections.Generic;

namespace Sentry.Extensibility;

/// <summary>
Expand All @@ -11,4 +13,10 @@ public interface ISentryStackTraceFactory
/// <param name="exception">The exception to create the stacktrace from.</param>
/// <returns>A Sentry stack trace.</returns>
SentryStackTrace? Create(Exception? exception = null);

/// <summary>
/// Returns a list of <see cref="DebugImage" />s referenced from the previously processed <see cref="Exception" />s.
/// </summary>
/// <returns>A list of referenced debug images.</returns>
List<DebugImage>? DebugImages() { return null; }
}
116 changes: 114 additions & 2 deletions src/Sentry/Extensibility/SentryStackTraceFactory.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using System.Text;
using System.Text.RegularExpressions;
using Sentry.Internal.Extensions;
Expand All @@ -13,6 +17,9 @@ public class SentryStackTraceFactory : ISentryStackTraceFactory
{
private readonly SentryOptions _options;

private Dictionary<Guid, Int32> _debugImageIndexByModule = new Dictionary<Guid, Int32>();
private List<DebugImage> _debugImages = new List<DebugImage>();

/*
* NOTE: While we could improve these regexes, doing so might break exception grouping on the backend.
* Specifically, RegexAsyncFunctionName would be better as: @"^(.*)\+<(\w*|<\w*>b__\d*)>d(?:__\d*)?$"
Expand All @@ -33,6 +40,16 @@ public class SentryStackTraceFactory : ISentryStackTraceFactory
/// </summary>
public SentryStackTraceFactory(SentryOptions options) => _options = options;

/// <summary>
/// Returns a list of <see cref="DebugImage" />s referenced from the previously processed <see cref="Exception" />s.
/// </summary>
/// <returns>A list of referenced debug images.</returns>
public virtual List<DebugImage>? DebugImages()
{
// create a shallow copy, as we want to still mutate our local copy
return _debugImages.ToList();
}

/// <summary>
/// Creates a <see cref="SentryStackTrace" /> from the optional <see cref="Exception" />.
/// </summary>
Expand Down Expand Up @@ -96,7 +113,7 @@ internal IEnumerable<SentryStackFrame> CreateFrames(StackTrace stackTrace, bool
{
StackTraceMode.Enhanced => EnhancedStackTrace.GetFrames(stackTrace).Select(p => p as StackFrame),
_ => stackTrace.GetFrames()
// error CS8619: Nullability of reference types in value of type 'StackFrame?[]' doesn't match target type 'IEnumerable<StackFrame>'.
// error CS8619: Nullability of reference types in value of type 'StackFrame?[]' doesn't match target type 'IEnumerable<StackFrame>'.
#if NETCOREAPP3_0
.Where(f => f is not null)
#endif
Expand Down Expand Up @@ -184,6 +201,23 @@ protected SentryStackFrame InternalCreateFrame(StackFrame stackFrame, bool deman
}

AttributeReader.TryGetProjectDirectory(method.Module.Assembly, out projectPath);

var moduleIdx = GetModuleIndex(method.Module);
if (moduleIdx != null)
{
frame.AddressMode = String.Format("rel:{0}", moduleIdx);
}

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)
{
var recordId = token & 0x00ffffff;
frame.FunctionId = String.Format("0x{0:x}", recordId);
}
}

frame.ConfigureAppFrame(_options);
Expand All @@ -203,7 +237,7 @@ protected SentryStackFrame InternalCreateFrame(StackFrame stackFrame, bool deman
var ilOffset = stackFrame.GetILOffset();
if (ilOffset != StackFrame.OFFSET_UNKNOWN)
{
frame.InstructionOffset = ilOffset;
frame.InstructionAddress = String.Format("0x{0:x}", ilOffset);
}

var lineNo = stackFrame.GetFileLineNumber();
Expand Down Expand Up @@ -236,6 +270,84 @@ protected SentryStackFrame InternalCreateFrame(StackFrame stackFrame, bool deman
return frame;
}


private Int32? GetModuleIndex(Module module)
{
var id = module.ModuleVersionId;
Int32 idx = 0;

if (_debugImageIndexByModule.TryGetValue(id, out idx))
{
return idx;
}
idx = _debugImages.Count;

var codeFile = module.FullyQualifiedName;
if (!File.Exists(codeFile))
{
return null;
}
using var stream = File.OpenRead(codeFile);
var peReader = new PEReader(stream);


var headers = peReader.PEHeaders;
var peHeader = headers.PEHeader;

String? codeId = null;
if (peHeader != null)
{
codeId = String.Format("{0:X8}{1:x}", headers.CoffHeader.TimeDateStamp, peHeader.SizeOfImage);
}

String? debugId = null;
String? debugFile = null;
String? debugChecksum = null;

var debugDirs = peReader.ReadDebugDirectory();
foreach (var entry in debugDirs)
{
if (entry.Type == DebugDirectoryEntryType.PdbChecksum)
{
var checksum = peReader.ReadPdbChecksumDebugDirectoryData(entry);
var checksumHex = string.Concat(checksum.Checksum.Select(b => b.ToString("x2")));
debugChecksum = String.Format("{0}:{1:x}", checksum.AlgorithmName, checksumHex);
}
if (!entry.IsPortableCodeView)
{
continue;
}
var codeView = peReader.ReadCodeViewDebugDirectoryData(entry);

// Together 16B of the Guid concatenated with 4B of the TimeDateStamp field of the entry form a PDB ID that
// should be used to match the PE/COFF image with the associated PDB (instead of Guid and Age).
// Matching PDB ID is stored in the #Pdb stream of the .pdb file.
// See https://github.com/dotnet/runtime/blob/main/docs/design/specs/PE-COFF.md#codeview-debug-directory-entry-type-2
debugId = String.Format("{0}-{1:x}", codeView.Guid, entry.Stamp);
debugFile = codeView.Path;
}


// well, we are out of luck :-(
if (debugId == null)
{
return null;
}

_debugImages.Add(new DebugImage
{
Type = "pe_dotnet",
CodeId = codeId,
CodeFile = codeFile,
DebugId = debugId,
DebugChecksum = debugChecksum,
DebugFile = debugFile,
});
_debugImageIndexByModule.Add(id, idx);

return idx;
}

/// <summary>
/// Get a <see cref="MethodBase"/> from <see cref="StackFrame"/>.
/// </summary>
Expand Down
1 change: 1 addition & 0 deletions src/Sentry/Internal/MainExceptionProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public void Process(Exception exception, SentryEvent sentryEvent)
MoveExceptionExtrasToEvent(sentryEvent, sentryExceptions);

sentryEvent.SentryExceptions = sentryExceptions;
sentryEvent.DebugImages = SentryStackTraceFactoryAccessor().DebugImages();
}

// SentryException.Extra is not supported by Sentry yet.
Expand Down
18 changes: 18 additions & 0 deletions src/Sentry/Internal/MainSentryEventProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,24 @@ public SentryEvent Process(SentryEvent @event)
}
}

// Add all the Debug Images that were referenced from stack traces
// to the Event
// XXX: Ideally the StackTraceFactory would write these directly when
// creating the StackTrace, but that interface does not work that way.
// As we need our indices to work, we add these images to the *beginning*,
// shifting any existing image to the end.
// This should work for example with the Unity il2cpp processor.
// However if any other processor is adding (or rather, referencing)
// indexed images, things *will* break. Whatever code is creating/adding
// stack traces needs to modify the list of debug images at the same
// time!
var debugImages = SentryStackTraceFactoryAccessor().DebugImages() ?? new List<DebugImage>();
if (@event.DebugImages != null && !debugImages.SequenceEqual(@event.DebugImages))
{
debugImages.AddRange(@event.DebugImages);
}
@event.DebugImages = debugImages;

if (_options.ReportAssembliesMode != ReportAssembliesMode.None)
{
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
Expand Down
11 changes: 11 additions & 0 deletions src/Sentry/SentryStackFrame.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,14 @@ public sealed class SentryStackFrame : IJsonSerializable
/// </summary>
public string? AddressMode { get; set; }

/// <summary>
/// The optional Function Id.<br/>
/// This is derived from the `MetadataToken`, and should be the record id
/// of a `MethodDef`.<br/>
/// This should be a string with a hexadecimal number that includes a <b>0x</b> prefix.<br/>
/// </summary>
public string? FunctionId { get; set; }

/// <inheritdoc />
public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger)
{
Expand All @@ -163,6 +171,7 @@ public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger)
writer.WriteStringIfNotWhiteSpace("instruction_addr", InstructionAddress);
writer.WriteNumberIfNotNull("instruction_offset", InstructionOffset);
writer.WriteStringIfNotWhiteSpace("addr_mode", AddressMode);
writer.WriteStringIfNotWhiteSpace("function_id", FunctionId);

writer.WriteEndObject();
}
Expand Down Expand Up @@ -214,6 +223,7 @@ public static SentryStackFrame FromJson(JsonElement json)
var instructionAddress = json.GetPropertyOrNull("instruction_addr")?.GetString();
var instructionOffset = json.GetPropertyOrNull("instruction_offset")?.GetInt64();
var addressMode = json.GetPropertyOrNull("addr_mode")?.GetString();
var functionId = json.GetPropertyOrNull("function_id")?.GetString();

return new SentryStackFrame
{
Expand All @@ -236,6 +246,7 @@ public static SentryStackFrame FromJson(JsonElement json)
InstructionAddress = instructionAddress,
InstructionOffset = instructionOffset,
AddressMode = addressMode,
FunctionId = functionId,
};
}
}