Debugging a native crash in a Xamarin.Mac app
Sometimes programming errors can cause crashes in the native Objective-C runtime. Unlike C# exceptions, these don't point to a specific line in your code you can look to fix. Sometimes they can be trivial to find and fix, and other times they can be extremely difficult to track down.
Let's walk through a few real native crash examples and take a look.
Here is the first few lines of a crash in a simple test application (this information will be in the Application Output Pad):
2014-10-15 16:18:02.364 NSOutlineViewHottness[79111:1304993] *** Assertion failure in -[NSTableView _uncachedRectHeightOfRow:], /SourceCache/AppKit/AppKit-1343.13/TableView.subproj/NSTableView.m:1855
2014-10-15 16:18:02.364 NSOutlineViewHottness[79111:1304993] NSTableView variable rowHeight error: The value must be > 0 for row 0, but the delegate <NSOutlineViewHottness_HotnessViewDelegate: 0xaa01860> gave -1.000.
2014-10-15 16:18:02.378 NSOutlineViewHottness[79111:1304993] *** Assertion failure in -[NSTableView _uncachedRectHeightOfRow:], /SourceCache/AppKit/AppKit-1343.13/TableView.subproj/NSTableView.m:1855
2014-10-15 16:18:02.378 NSOutlineViewHottness[79111:1304993] NSTableView variable rowHeight error: The value must be > 0 for row 0, but the delegate <NSOutlineViewHottness_HotnessViewDelegate: 0xaa01860> gave -1.000.
2014-10-15 16:18:02.381 NSOutlineViewHottness[79111:1304993] (
0 CoreFoundation 0x91888343 __raiseError + 195
1 libobjc.A.dylib 0x9a5e6a2a objc_exception_throw + 276
2 CoreFoundation 0x918881ca +[NSException raise:format:arguments:] + 138
3 Foundation 0x950742b1 -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:] + 118
4 AppKit 0x975db476 -[NSTableView _uncachedRectHeightOfRow:] + 373
5 AppKit 0x975db2f8 -[_NSTableRowHeightStorage _uncachedRectHeightOfRow:] + 143
6 AppKit 0x975db206 -[_NSTableRowHeightStorage _cacheRowHeights] + 167
7 AppKit 0x975db130 -[_NSTableRowHeightStorage _createRowHeightsArray] + 226
8 AppKit 0x975b5851 -[_NSTableRowHeightStorage _ensureRowHeights] + 73
9 AppKit 0x975b5790 -[_NSTableRowHeightStorage computeTableHeightForNumberOfRows:] + 89
10 AppKit 0x975b4c38 -[NSTableView _totalHeightOfTableView] + 220
The lines prefixed with numbers is the native stack trace. From that you can see the crash occurred somewhere inside NSTableView
handling the row heights. Then NSAssertionHandler
fires an NSException (objc_exception_throw)
and we see the Assertion failure:
rowHeight error: The value must be > 0 for row 0, but the delegate
<NSOutlineView_ViewDelegate: 0xaa01860> gave -1.000
Once you see this, it’s pretty clear that some NSOutlineViewDelegate
method is returning a negative number. This was the problem:
public override nfloat GetRowHeight (NSTableView tableView, nint row)
{
return -1;
}
Stacktrace:
at <unknown> <0xffffffff>
at (wrapper managed-to-native) MonoMac.AppKit.NSApplication.NSApplicationMain (int,string[]) <IL 0x000a4, 0xffffffff>
at MonoMac.AppKit.NSApplication.Main (string[]) [0x00041] in /Users/donblas/Programming/xamcore-master/src/AppKit/NSApplication.cs:107
at NSOutlineViewHottness.MainClass.Main (string[]) [0x00007] in /Users/donblas/Programming/Local/NSOutlineViewHottness/NSOutlineViewHottness/Main.cs:14
at (wrapper runtime-invoke) <Module>.runtime_invoke_void_object (object,intptr,intptr,intptr) <IL 0x00050, 0xffffffff>
Native stacktrace:
Debug info from gdb:
(lldb) command source -s 0 '/tmp/mono-gdb-commands.qrHllW'
Executing commands in '/private/tmp/mono-gdb-commands.qrHllW'.
(lldb) process attach --pid 79229
Process 79229 stopped
Executable module set to "/Users/donblas/Programming/Local/NSOutlineViewHottness/NSOutlineViewHottness/bin/Debug/NSOutlineViewHottness.app/Contents/MacOS/NSOutlineViewHottness".
Architecture set to: i386-apple-macosx.
(lldb) thread list
Process 79229 stopped
* thread #1: tid = 0x142776, 0x9af75e1a libsystem_kernel.dylib`__wait4 + 10, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
thread #2: tid = 0x142790, 0x9af768d2 libsystem_kernel.dylib`kevent64 + 10, queue = 'com.apple.libdispatch-manager'
thread #3: tid = 0x142792, 0x9af75e6e libsystem_kernel.dylib`__workq_kernreturn + 10
thread #4: tid = 0x142794, 0x9af6fa6a libsystem_kernel.dylib`semaphore_wait_trap + 10
thread #5: tid = 0x142795, 0x9af75772 libsystem_kernel.dylib`__recvfrom + 10
thread #6: tid = 0x142799, 0x9af75e6e libsystem_kernel.dylib`__workq_kernreturn + 10
thread #7: tid = 0x14279a, 0x9af75e6e libsystem_kernel.dylib`__workq_kernreturn + 10
thread #8: tid = 0x14279b, 0x9af75e6e libsystem_kernel.dylib`__workq_kernreturn + 10
thread #9: tid = 0x1427f8, 0x9af75e6e libsystem_kernel.dylib`__workq_kernreturn + 10
thread #10: tid = 0x1427fe, 0x9af6fa2e libsystem_kernel.dylib`mach_msg_trap + 10
(lldb) thread backtrace all
* thread #1: tid = 0x142776, 0x9af75e1a libsystem_kernel.dylib`__wait4 + 10, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
* frame #0: 0x9af75e1a libsystem_kernel.dylib`__wait4 + 10
frame #1: 0x986bfb25 libsystem_c.dylib`waitpid$UNIX2003 + 48
frame #2: 0x028ba36d libmono-2.0.dylib`mono_handle_native_sigsegv(signal=11, ctx=0x03115fe0) + 541 at mini-exceptions.c:2323
frame #3: 0x0290a8bb libmono-2.0.dylib`mono_arch_handle_altstack_exception(sigctx=<unavailable>, fault_addr=<unavailable>, stack_ovf=0) + 155 at exceptions-x86.c:1159
frame #4: 0x0280b4fd libmono-2.0.dylib`mono_sigsegv_signal_handler(_dummy=<unavailable>, info=<unavailable>, context=<unavailable>) + 445 at mini.c:6861
frame #5: 0x91ef403b libsystem_platform.dylib`_sigtramp + 43
frame #6: 0x9a5dd0bd libobjc.A.dylib`objc_msgSend + 45
frame #7: 0x96bcec03 libsystem_trace.dylib`_os_activity_initiate + 89
frame #8: 0x9773ba91 AppKit`-[NSApplication sendAction:to:from:] + 548
frame #9: 0x9773b82d AppKit`-[NSControl sendAction:to:] + 102
frame #10: 0x97934d36 AppKit`__26-[NSCell _sendActionFrom:]_block_invoke + 176
frame #11: 0x96bcec03 libsystem_trace.dylib`_os_activity_initiate + 89
frame #12: 0x97787975 AppKit`-[NSCell _sendActionFrom:] + 161
frame #13: 0x979188ea AppKit`-[NSButtonCell _sendActionFrom:] + 55
frame #14: 0x979366e6 AppKit`__48-[NSCell trackMouse:inRect:ofView:untilMouseUp:]_block_invoke965 + 43
frame #15: 0x96bcec03 libsystem_trace.dylib`_os_activity_initiate + 89
frame #16: 0x977a3d00 AppKit`-[NSCell trackMouse:inRect:ofView:untilMouseUp:] + 2815
frame #17: 0x977a2df4 AppKit`-[NSButtonCell trackMouse:inRect:ofView:untilMouseUp:] + 524
frame #18: 0x977a233b AppKit`-[NSControl mouseDown:] + 762
frame #19: 0x97cbc112 AppKit`-[_NSThemeWidget mouseDown:] + 378
frame #20: 0x97d36d74 AppKit`-[NSWindow _reallySendEvent:] + 12353
frame #21: 0x977201f9 AppKit`-[NSWindow sendEvent:] + 409
frame #22: 0x976cdc67 AppKit`-[NSApplication sendEvent:] + 4679
frame #23: 0x9754807c AppKit`-[NSApplication run] + 1003
This is an issue that was much more difficult to track down. When you see at <unknown> <0xffffffff>
or MonoMac.ObjCRuntime.Runtime.GetNSObject (IntPtr ptr)
at the top of the managed stack trace, it suggests we are trying to execute some managed code with an object that has been garbage collected. The native stack trace shows trackMouse:inRect:ofView:untilMouseUp
into NSCell _sendActionFrom
so we are somewhere handling a click event, trying to call back into C# and dying.
In general, errors like this are difficult to track down. I added GC.Collect(2)
to a button handler to help track down this issue (forcing a garbage collection) to make the issue reproducible.
mainWindowController.Window.StandardWindowButton (NSWindowButton.CloseButton).Activated += HandleActivated;
The NSButton
returned by StandardWindowButton()
was being collected even though an event was registered to it (that's the bug). When we try to call that event by clicking, if the button has been garbage collected we crash.
Although it wasn't the root cause of this particular issue, stack traces like this can also be caused by incorrect method signatures in functions [Export]
ed to Objective-C. For example, if a method expects a parameter to be an out string
and you type it as string
, we can crash in the same way.
Many Cocoa APIs involve being “called back” by the library when some event occurs, giving you a chance to respond, or when some data is needed to carry out a task. Though you may think primarily of the Delegate and DataSource patterns, there are a multitude of APIs that work this way. For example, when you override the methods of an NSView
and then insert it into the visual tree, you expect AppKit to call you back when certain events occur.
In almost all cases, Xamarin.Mac will correctly prevent the managed object target of these callbacks from being Garbage Collected while they still could be called back. However, rarely bugs in the binding can disrupt this. When this happens, you can see unpleasant crashes similar to this:
Thread 0 Crashed:: Dispatch queue: com.apple.main-thread
0 libsystem_kernel.dylib 0x98c2f69a __pthread_kill + 10
1 libsystem_pthread.dylib 0x90341f19 pthread_kill + 101
2 libsystem_c.dylib 0x9453feee abort + 156
3 libmonosgen-2.0.dylib 0x020bfba5 mono_handle_native_sigsegv + 757
4 libmonosgen-2.0.dylib 0x0210b812 mono_arch_handle_altstack_exception + 162
5 libmonosgen-2.0.dylib 0x0200c55e mono_sigsegv_signal_handler + 446
6 libsystem_platform.dylib 0x9513003b _sigtramp + 43
7 ??? 0xffffffff 0 + 4294967295
8 libmonosgen-2.0.dylib 0x0200c3a0 mono_sigill_signal_handler + 48
9 com.apple.AppKit 0x99f76041 -[NSView setFrame:] + 448
10 com.apple.AppKit 0x9a1fd4ea -[NSToolbarView adjustToWindow:attachedToEdge:] + 198
11 com.apple.AppKit 0x9a1fd414 -[NSToolbar _adjustViewToWindow] + 68
12 com.apple.AppKit 0x9a01eb0d -[NSToolbar _windowWillShowToolbar] + 79
This guide will help you track down bugs of this nature if they crop up, correctly report them so they can be fixed, and work around them in your code until then.
In almost every case with bugs of this nature, the primary symptom is native crashes, normally with something similar to mono_sigsegv_signal_handler
, or _sigtrap
in the top frames of the stack. Cocoa is attempting to call back into your C# code, hitting a garbage collected object, and crashing. However, not every crash with these symbols is caused by a binding issue like this, you’ll need to do some additional digging to confirm this is the problem.
What makes these bugs difficult to track down is that they only occur after a garbage collection has disposed of the object in question. If you believe you’ve hit one of these bugs, add the following code somewhere in your startup sequence:
new System.Threading.Thread (() =>
{
while (true) {
System.Threading.Thread.Sleep (1000);
GC.Collect ();
}
}).Start ();
This will force your application to run the garbage collector every second. Re-run your application and try to reproduce the bug. If you crash immediately, or consistently instead of randomly, you are on the right track.
The next step is to report the issue to Xamarin so the binding can be fixed for future releases. If you are a business or enterprise license holder, open a ticket at
visualstudio.microsoft.com/vs/support/
Otherwise, search for an existing issue:
- Check the current bugs
- Search the issue repository
- If you cannot find a matching issue, please file a new issue in the GitHub issue repository.
GitHub issues are all public. It’s not possible to hide comments or attachments.
Please include as much of the following as possible:
- A simple example reproducing the issue. This is invaluable where possible.
- The full stack trace of the crash.
- The C# code surrounding the crash.
Once you’ve tracked down the issue, patching the issue with a work around until the binding can be fixed can be simple. The aim is to prevent the object (View, Delegate, DataSource) that is incorrectly being disposed from leaving memory by keeping an open reference.
For simple cases where there is only a single instance of the object, change the code from this:
void AddObject ()
{
item.View = new MyView ();
...
}
To using a static variable such as this:
static NSObject view;
...
void AddObject ()
{
view = new MyView ();
item.View = view;
...
}
In cases where multiple instances may be created, a static HashSet
can be employed:
static HashSet<NSObject> collection = new HashSet<NSObject> ();
...
void AddObject ()
{
item.View = new MyView ();
collection.Add (item.View );
...
}
Once the binding has been fixed and you’ve upgrade to the version of Xamarin.Mac that includes the fix, the work-around code can be removed.
You should never allow a C# Exception to "escape" managed code to the calling Objective-C method. If you do, the results are undefined but generally involve crashing. In general, we do everything we can to bubble up useful information for both native and managed crashes to help you solve your issues quickly.
Without getting too bogged down with the technical reasons why, setting up the infrastructure to catch managed exceptions at every managed/native boundary is non-trivially expensive and there are a lot of transitions that happen in many common operations. Many operations, specifically ones that involve the UI thread must finish quickly or your app will stutter and have unacceptable performance characteristics. Many of those callbacks do very simple things that rarely have the possibility of throwing, so this overhead would both be expensive and unnecessary in those cases.
Thus, we'd don't set up those try / catches for you. For places where you code does non-trivial things (beyond say returning a booleans or simple math), you can try catch yourself.