Skip to content

Commit

Permalink
Merge pull request #855 from bugsnag/PLAT-13024-native-stack-trace
Browse files Browse the repository at this point in the history
[PLAT-13024] Send native mapped information in stack traces from system exceptions
  • Loading branch information
kstenerud authored Dec 23, 2024
2 parents afc20b4 + dbd06bd commit 694290d
Show file tree
Hide file tree
Showing 11 changed files with 286 additions and 48 deletions.
8 changes: 1 addition & 7 deletions Bugsnag/Assets/Bugsnag/Runtime/INativeClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,13 +103,7 @@ interface INativeClient : IFeatureFlagStore

IDictionary<string, object> GetNativeMetadata();

/// <summary>
/// Find the native loaded image that corresponds to a native instruction address
/// supplied by il2cpp_native_stack_trace().
/// </summary>
/// <param name="address">The address to find the corresponding image of</param>
/// <returns>The corresponding image, or null</returns>
LoadedImage FindImageAtAddress(UInt64 address);
StackTraceLine[] ToStackFrames(System.Exception exception);

bool ShouldAttemptDelivery();

Expand Down
4 changes: 3 additions & 1 deletion Bugsnag/Assets/Bugsnag/Runtime/LoadedImage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,19 @@ namespace BugsnagUnity
{
class LoadedImage
{
public LoadedImage(UInt64 loadAddress, UInt64 size, string fileName, string uuid)
public LoadedImage(UInt64 loadAddress, UInt64 size, string fileName, string uuid, bool isMainImage)
{
LoadAddress = loadAddress;
Size = size;
FileName = fileName;
Uuid = uuid;
IsMainImage = isMainImage;
}

public readonly UInt64 LoadAddress;
public readonly UInt64 Size;
public readonly string FileName;
public readonly string Uuid;
public readonly bool IsMainImage;
}
}
4 changes: 2 additions & 2 deletions Bugsnag/Assets/Bugsnag/Runtime/Native/Android/NativeClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -164,9 +164,9 @@ public void RegisterForOnSessionCallbacks()
NativeInterface.RegisterForOnSessionCallbacks();
}

public LoadedImage FindImageAtAddress(UInt64 address)
public StackTraceLine[] ToStackFrames(System.Exception exception)
{
return null;
return new StackTraceLine[0];
}
}

Expand Down
88 changes: 65 additions & 23 deletions Bugsnag/Assets/Bugsnag/Runtime/Native/Cocoa/LoadedImages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,40 +5,77 @@

namespace BugsnagUnity
{
/// <summary>
/// Caches the list of loaded images as reported by the native runtime, and provides searching by address.
/// </summary>
class LoadedImages
{
/// <summary>
/// Refresh the list of loaded images to match what the native side currently says.
/// </summary>
/// <param name="mainImageFileName">The file name of the main image file</param>
/// <remarks>
/// Note: You MUST call this at least once before using an instance of this class!
/// Note: If anything goes wrong during the refresh, the currently cached state won't change.
/// </remarks>
public void Refresh()
#nullable enable
public void Refresh(String? mainImageFileName)
{
UInt64 loadedNativeImagesAt = NativeCode.bugsnag_lastChangedLoadedImages();
if (loadedNativeImagesAt == lastLoadedNativeImagesAt)
if (loadedNativeImagesAt == LastLoadedNativeImagesAt)
{
// Only refresh if something changed.
return;
}

var imageCount = NativeCode.bugsnag_getLoadedImageCount();
if (imageCount == 0)
{
return;
}
lastLoadedNativeImagesAt = loadedNativeImagesAt;

// Ask for the current count * 2 in case new images get added between calls
var nativeImages = new NativeLoadedImage[NativeCode.bugsnag_getLoadedImageCount() * 2];
var nativeImages = new NativeLoadedImage[imageCount * 2];
var count = NativeCode.bugsnag_getLoadedImages(nativeImages, (UInt64)nativeImages.LongLength);
var images = new LoadedImage[count];
for (UInt64 i = 0; i < count; i++)
try
{
if (count == 0)
{
return;
}

UInt64 mainLoadAddress = 0;
var images = new LoadedImage[count];
for (UInt64 i = 0; i < count; i++)
{
var nativeImage = nativeImages[i];
var uuid = new byte[16];
Marshal.Copy(nativeImage.UuidBytes, uuid, 0, 16);
var fileName = Marshal.PtrToStringAnsi(nativeImage.FileName);
var isMainImage = fileName == mainImageFileName;

var image = new LoadedImage(nativeImage.LoadAddress,
nativeImage.Size,
fileName,
new Guid(uuid).ToString(),
isMainImage);
if (isMainImage)
{
mainLoadAddress = image.LoadAddress;
}
images[i] = image;
}

// Update cache
Images = images;
MainImageLoadAddress = mainLoadAddress;
LowestImageLoadAddress = images[0].LoadAddress;
LastLoadedNativeImagesAt = loadedNativeImagesAt;
}
finally
{
var nativeImage = nativeImages[i];
var uuid = new byte[16];
Marshal.Copy(nativeImage.UuidBytes, uuid, 0, 16);
images[i] = new LoadedImage(nativeImage.LoadAddress,
nativeImage.Size,
Marshal.PtrToStringAnsi(nativeImage.FileName),
new Guid(uuid).ToString());
// bugsnag_getLoadedImages() locks a mutex, so we must call bugsnag_unlockLoadedImages()
NativeCode.bugsnag_unlockLoadedImages();
}
Images = images;
// bugsnag_getLoadedImages() locks a mutex, so we must call bugsnag_unlockLoadedImages()
NativeCode.bugsnag_unlockLoadedImages();
}

/// <summary>
Expand All @@ -47,8 +84,14 @@ public void Refresh()
/// </summary>
/// <param name="address">The address to find the corresponding image of</param>
/// <returns>The corresponding image, or null</returns>
public LoadedImage FindImageAtAddress(UInt64 address)
#nullable enable
public LoadedImage? FindImageAtAddress(UInt64 address)
{
if (address < LowestImageLoadAddress)
{
address += MainImageLoadAddress;
}

int idx = Array.BinarySearch(Images, address, new AddressToImageComparator());
if (idx < 0)
{
Expand All @@ -57,11 +100,10 @@ public LoadedImage FindImageAtAddress(UInt64 address)
return Images[idx];
}

/// <summary>
/// The currently loaded images, as of the last call to Refresh().
/// </summary>
public LoadedImage[] Images;
private UInt64 lastLoadedNativeImagesAt = 0;
private LoadedImage[] Images = new LoadedImage[0];
private UInt64 MainImageLoadAddress = 0;
private UInt64 LowestImageLoadAddress = 0;
private UInt64 LastLoadedNativeImagesAt = 0;

private class AddressToImageComparator : IComparer
{
Expand Down
118 changes: 114 additions & 4 deletions Bugsnag/Assets/Bugsnag/Runtime/Native/Cocoa/NativeClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
Expand All @@ -20,7 +21,7 @@ class NativeClient : INativeClient
private static NativeClient _instance;
private bool _registeredForSessionCallbacks;

private LoadedImages loadedImages;
private LoadedImages loadedImages = new LoadedImages();

public NativeClient(Configuration configuration)
{
Expand Down Expand Up @@ -488,12 +489,121 @@ public void RegisterForOnSessionCallbacks()
NativeCode.bugsnag_registerForSessionCallbacksAfterStart(HandleSessionCallbacks);
}

public LoadedImage FindImageAtAddress(UInt64 address)
#nullable enable
private static string? ExtractString(IntPtr pString)
{
loadedImages.Refresh();
return loadedImages.FindImageAtAddress(address);
return (pString == IntPtr.Zero) ? null : Marshal.PtrToStringAnsi(pString);
}

private StackTraceLine[] ToStackFrames(System.Exception exception, IntPtr[] nativeAddresses)
{
var unityTrace = new PayloadStackTrace(exception.StackTrace).StackTraceLines;
var length = nativeAddresses.Length < unityTrace.Length ? nativeAddresses.Length : unityTrace.Length;
var stackFrames = new StackTraceLine[length];
for (int i = 0; i < length; i++)
{
var method = unityTrace[i].Method;
var address = (UInt64)nativeAddresses[i].ToInt64();
var image = loadedImages.FindImageAtAddress(address);

var trace = new StackTraceLine();
trace.FrameAddress = address.ToString();
trace.Method = method.ToString();
if (image != null)
{
if (address < image.LoadAddress)
{
// It's a relative address
trace.FrameAddress = (address + image.LoadAddress).ToString();
}
trace.MachoFile = image.FileName;
trace.MachoLoadAddress = image.LoadAddress.ToString();
trace.MachoUuid = image.Uuid;
trace.InProject = image.IsMainImage;
}
stackFrames[i] = trace;
}
return stackFrames;
}

#if ENABLE_IL2CPP && UNITY_2021_3_OR_NEWER
[DllImport("__Internal")]
private static extern IntPtr il2cpp_gchandle_get_target(int gchandle);

[DllImport("__Internal")]
private static extern void il2cpp_free(IntPtr ptr);

[DllImport("__Internal")]
private static extern void il2cpp_native_stack_trace(IntPtr exc, out IntPtr addresses, out int numFrames, out IntPtr imageUUID, out IntPtr imageName);
#endif

public StackTraceLine[] ToStackFrames(System.Exception exception)
{
var notFound = new StackTraceLine[0];

if (exception == null)
{
return notFound;
}

#if ENABLE_IL2CPP && UNITY_2021_3_OR_NEWER
var hException = GCHandle.Alloc(exception);
var pNativeAddresses = IntPtr.Zero;
var pImageUuid = IntPtr.Zero;
var pImageName = IntPtr.Zero;
try
{
if (hException == null)
{
return notFound;
}

var pException = il2cpp_gchandle_get_target(GCHandle.ToIntPtr(hException).ToInt32());
if (pException == IntPtr.Zero)
{
return notFound;
}

var frameCount = 0;
string? mainImageFileName = null;

il2cpp_native_stack_trace(pException, out pNativeAddresses, out frameCount, out pImageUuid, out pImageName);
if (pNativeAddresses == IntPtr.Zero)
{
return notFound;
}

mainImageFileName = ExtractString(pImageName);
var nativeAddresses = new IntPtr[frameCount];
Marshal.Copy(pNativeAddresses, nativeAddresses, 0, frameCount);

loadedImages.Refresh(mainImageFileName);
return ToStackFrames(exception, nativeAddresses);
}
finally
{
if (pImageUuid != IntPtr.Zero)
{
il2cpp_free(pImageUuid);
}
if (pImageName != IntPtr.Zero)
{
il2cpp_free(pImageName);
}
if (pNativeAddresses != IntPtr.Zero)
{
il2cpp_free(pNativeAddresses);
}
if (hException != null)
{
hException.Free();
}
}
#else
return notFound;
#endif
}
}
}

#endif
Original file line number Diff line number Diff line change
Expand Up @@ -171,9 +171,9 @@ public void RegisterForOnSessionCallbacks()
// Not Used on this platform
}

public LoadedImage FindImageAtAddress(UInt64 address)
public StackTraceLine[] ToStackFrames(System.Exception exception)
{
return null;
return new StackTraceLine[0];
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions Bugsnag/Assets/Bugsnag/Runtime/Native/Windows/NativeClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -218,9 +218,9 @@ public void RegisterForOnSessionCallbacks()
// Not Used on this platform
}

public LoadedImage FindImageAtAddress(UInt64 address)
public StackTraceLine[] ToStackFrames(System.Exception exception)
{
return null;
return new StackTraceLine[0];
}
}
}
Expand Down
10 changes: 8 additions & 2 deletions Bugsnag/Assets/Bugsnag/Runtime/Payload/ErrorBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,19 @@ internal Error FromSystemException(System.Exception exception, string stackTrace
return new Error(errorClass, exception.Message, lines);
}


internal Error FromSystemException(System.Exception exception, System.Diagnostics.StackFrame[] alternativeStackTrace)
{
var frames = NativeClient.ToStackFrames(exception);
var errorClass = exception.GetType().Name;

// JVM exceptions in the main thread are handled by unity and require extra formatting
if (errorClass == ANDROID_JAVA_EXCEPTION_CLASS)
if (frames.Length > 0)
{
return new Error(errorClass, exception.Message, frames);
}
else if (errorClass == ANDROID_JAVA_EXCEPTION_CLASS)
{
// JVM exceptions in the main thread are handled by unity and require extra formatting
var androidErrorData = ProcessAndroidError(exception.Message);
var androidErrorClass = androidErrorData[0];
var androidErrorMessage = androidErrorData[1];
Expand Down
Loading

0 comments on commit 694290d

Please sign in to comment.