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

[PLAT-13024] Send native mapped information in stack traces from system exceptions #855

Open
wants to merge 4 commits into
base: next
Choose a base branch
from
Open
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: 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
Loading