A useful little chunk of iPhone code.

This is something I hacked together for a program I’ve been tinkering with. Add this to your application delegate and it will trap uncaught Objective C exceptions, uncaught signals, and uncaught C++ exceptions (if you like messing around with Objective C++), and write a stack trace and detailed report which your application will offer to e-mail in the event that your program crashes in the field.

The preliminaries: the headers to add, global variables and the like:

#include <execinfo.h>
#include <exception>

/************************************************************************/
/*																		*/
/*	Globals																*/
/*																		*/
/************************************************************************/

static NSString *GBug;

/************************************************************************/
/*																		*/
/*	Crash Reporter														*/
/*																		*/
/************************************************************************/

static NSString *GetBugReport()
{
	NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
	NSString *dir = [paths objectAtIndex:0];
	return [dir stringByAppendingPathComponent:@"bug.txt"];
}

Note that this defines a function which saves any uncaught exceptions to a file ‘bug.txt’ on your phone. If this file is present the next time your application starts up, the user is presented with the option to e-mail the bug to you.

Now to catching uncaught Objective C exceptions:

/*	UncaughtExceptionHandler
 *
 *		Handle uncaught exceptions
 */

static void UncaughtExceptionHandler(NSException *exception)
{
	/*
	 *	Extract the call stack
	 */
	
	NSArray *callStack = [exception callStackReturnAddresses];
	int i,len = [callStack count];
	void **frames = new void *[len];
	
	for (i = 0; i < len; ++i) {
		frames[i] = (void *)[[callStack objectAtIndex:i] unsignedIntegerValue];
	}
	char **symbols = backtrace_symbols(frames,len);
	
	/*
	 *	Now format into a message for sending to the user
	 */
	
	NSMutableString *buffer = [[NSMutableString alloc] initWithCapacity:4096];
	
	NSBundle *bundle = [NSBundle mainBundle];
	[buffer appendFormat:@"PComp version %@ build %@nn",
			[bundle objectForInfoDictionaryKey:@"CFBundleVersion"],
			[bundle objectForInfoDictionaryKey:@"CIMBuildNumber"]];
	[buffer appendString:@"Uncaught Exceptionn"];
	[buffer appendFormat:@"Exception Name: %@n",[exception name]];
	[buffer appendFormat:@"Exception Reason: %@n",[exception reason]];
	[buffer appendString:@"Stack trace:nn"];
	for (i = 0; i < len; ++i) {
		[buffer appendFormat:@"%4d - %sn",i,symbols[i]];
	}
	
	/*
	 *	Get the error file to write this to
	 */
	
	NSError *err;
	[buffer writeToFile:GetBugReport() atomically:YES encoding:NSUTF8StringEncoding error:&err];
	NSLog(@"Error %@",buffer);
	exit(0);
}

This exception handler is called when you have an uncaught Objective C excption. This formats a bug report (note that I use build numbers in the info.plist file, under the key ‘CIMBuildNumber’), and writes the bug report both to the phone’s log and to the log file before force quitting the application.

Next, the signal handler:

/*	SignalHandler
 *
 *		Handle uncaught signals
 */

void SignalHandler(int sig, siginfo_t *info, void *context) 
{
	void *frames[128];
	int i,len = backtrace(frames, 128);
	char **symbols = backtrace_symbols(frames,len);
	
	/*
	 *	Now format into a message for sending to the user
	 */
	
	NSMutableString *buffer = [[NSMutableString alloc] initWithCapacity:4096];
	
	NSBundle *bundle = [NSBundle mainBundle];
	[buffer appendFormat:@"PComp version %@ build %@nn",
			[bundle objectForInfoDictionaryKey:@"CFBundleVersion"],
			[bundle objectForInfoDictionaryKey:@"CIMBuildNumber"]];
	[buffer appendString:@"Uncaught Signaln"];
	[buffer appendFormat:@"si_signo    %dn",info->si_signo];
	[buffer appendFormat:@"si_code     %dn",info->si_code];
	[buffer appendFormat:@"si_value    %dn",info->si_value];
	[buffer appendFormat:@"si_errno    %dn",info->si_errno];
	[buffer appendFormat:@"si_addr     0x%08lXn",info->si_addr];
	[buffer appendFormat:@"si_status   %dn",info->si_status];
	[buffer appendString:@"Stack trace:nn"];
	for (i = 0; i < len; ++i) {
		[buffer appendFormat:@"%4d - %sn",i,symbols[i]];
	}
	
	/*
	 *	Get the error file to write this to
	 */
	
	NSError *err;
	[buffer writeToFile:GetBugReport() atomically:YES encoding:NSUTF8StringEncoding error:&err];
	NSLog(@"Error %@",buffer);
	exit(0);
}

This more or less does the same thing, except for uncaught C signals.

And now C++:

/*	TerminateHandler
 *
 *		C++ exception terminate handler
 */

void TerminateHandler(void)
{
	void *frames[128];
	int i,len = backtrace(frames, 128);
	char **symbols = backtrace_symbols(frames,len);
	
	/*
	 *	Now format into a message for sending to the user
	 */
	
	NSMutableString *buffer = [[NSMutableString alloc] initWithCapacity:4096];
	
	NSBundle *bundle = [NSBundle mainBundle];
	[buffer appendFormat:@"PComp version %@ build %@nn",
			[bundle objectForInfoDictionaryKey:@"CFBundleVersion"],
			[bundle objectForInfoDictionaryKey:@"CIMBuildNumber"]];
	[buffer appendString:@"Uncaught C++ Exceptionn"];
	[buffer appendString:@"Stack trace:nn"];
	for (i = 0; i < len; ++i) {
		[buffer appendFormat:@"%4d - %sn",i,symbols[i]];
	}
	
	/*
	 *	Get the error file to write this to
	 */
	
	NSError *err;
	[buffer writeToFile:GetBugReport() atomically:YES encoding:NSUTF8StringEncoding error:&err];
	NSLog(@"Error %@",buffer);
	exit(0);
}

Which is the same thing for C++.

Next are the routines for hooking into uncaught signals:

/*	SetupUncaughtSignals
 *
 *		Set up the uncaught signals
 */

static void SetupUncaughtSignals()
{
	struct sigaction mySigAction;
	mySigAction.sa_sigaction = SignalHandler;
	mySigAction.sa_flags = SA_SIGINFO;
	
	sigemptyset(&mySigAction.sa_mask);
	sigaction(SIGQUIT, &mySigAction, NULL);
	sigaction(SIGILL, &mySigAction, NULL);
	sigaction(SIGTRAP, &mySigAction, NULL);
	sigaction(SIGABRT, &mySigAction, NULL);
	sigaction(SIGEMT, &mySigAction, NULL);
	sigaction(SIGFPE, &mySigAction, NULL);
	sigaction(SIGBUS, &mySigAction, NULL);
	sigaction(SIGSEGV, &mySigAction, NULL);
	sigaction(SIGSYS, &mySigAction, NULL);
	sigaction(SIGPIPE, &mySigAction, NULL);
	sigaction(SIGALRM, &mySigAction, NULL);
	sigaction(SIGXCPU, &mySigAction, NULL);
	sigaction(SIGXFSZ, &mySigAction, NULL);
}

Once an exception has been caught, it’d be nice to send it to you via e-mail. To do this, define the following Objective-C methods in your app delegate:

- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex 
{
	if (buttonIndex == 1) {
		/*
		 *	Send an e-mail with the specified title.
		 */
		
		NSMutableString *url = [NSMutableString stringWithCapacity:4096];
		[url appendString:@"mailto:bugs@nowhere.meh?subject=Bug%20Report&body="];
		[url appendString:[GBug stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
		[[UIApplication sharedApplication] openURL:[NSURL URLWithString:url]];
	}
	
	[GBug release];
}

- (void)sendBugsIfPresent
{
	NSError *err;
	NSString *path = GetBugReport();
	
	GBug = [[NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&err] retain];
	if (GBug == nil) return;
	[[NSFileManager defaultManager] removeItemAtPath:path error:&err];
	
	UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Unexpected exception" 
			message:@"An unexpected exception was caught the last time this program ran. Send the developer a bug report by e-mail?" 
			delegate:self 
			cancelButtonTitle:@"Cancel" 
			otherButtonTitles:@"Send Report",nil];
	[alert show];
}

Remember to change the e-mail and the message that is displayed. This will display an alert to the user, and if the user selects “Send Report”, he’ll be dropped into the e-mail application to send the e-mail.

Of course you could do other things here, like upload via HTTP to some remote server. I like the e-mail approach; this way the user knows exactly what he’s telling you.

The -sendBugsIfPresent determines if a bug report has been written out, and if one is present, send it.

Now, once you’ve added this code, it’s simply a matter of wiring everything up to catch uncaught exceptions and alert the user if there was a crash the last time. To your -applicationDidFinishLaunching method add:

- (void)applicationDidFinishLaunching:(UIApplication *)application 
{
... Other initialization here ...

	/* Register for uncaught exceptions, signals */
    NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);
	SetupUncaughtSignals();
	std::set_terminate(TerminateHandler);
	
	[self sendBugsIfPresent];
}

And that does it: we set up for uncaught Objective C exceptions, C++ exceptions and uncaught C signals, and we send a bug report if the last time we ran, we crashed.

Hope you find this as useful as I have.

Oh, and if you want to credit me with this, feel free–or not. (For the legally minded, I’m placing the above code snippets into the public domain. Just don’t sue me if it doesn’t work.)

10 thoughts on “A useful little chunk of iPhone code.

  1. Hi,

    I’ve been struggling to implement something similar for the last few days. The main problem is my signal handler never gets called. I just tried using your code but it too does not call the signal handler. When I place a breakpoint within the signalTest() function, the signal handler gets called, otherwise it does not get called.
    Any idea what could be wrong?

    // XCode version 3.1.2 (Simulator iphone 2.2)

    void signalHandler(int sig, siginfo_t* info, void* context)
    {
    printf(“got signal %dn”, sig);
    }

    // if I set a breakpoint in signalTest(), signalHandler() gets called;
    // otherwise it’s skipped
    void signalTest()
    {
    // install for SIGTERM
    struct sigaction sigact;
    sigact.sa_action = signalHandler;
    sigact.sa_flags = SA_SIGINFO;
    sigemptyset(SIGTERM, &sigact, NULL);

    // test signal
    raise(SIGTERM);
    }

    -(void)applicationDidFinishLaunching {

    signalTest();
    }

    Like

  2. Oops, signalTest() is wrong, it should be:

    void signalTest()
    {
    // install for SIGTERM
    struct sigaction sigact;
    sigact.sa_action = signalHandler;
    sigact.sa_flags = SA_SIGINFO;
    sigemptyset(&sigact.sa_mask);
    sigaction(SIGTERM, &sigact, NULL);

    // test signal
    raise(SIGTERM);
    }

    Like

  3. Fixed previous problem. Found another problem: SignalHandler() stack trace does not contain the function or line number where the crash occurred.

    For eg:
    void foo() {
    int* x;
    x = NULL;
    *x = 100; // SIGBUS signal generated
    }

    Generated stack trace does not contain any reference to foo() or several functions in the call stack leading up to it. I think there is some problem with the stack. I tried:
    mySigAction.sa_flags = SA_SIGINFO | SA_ONSTACK;

    but that did not work as well.

    Like

  4. I forgot to mention that the problem with the stack trace only occurs in the Release version of the app; debug version shows a much better stack trace pointing to the right line or at least the function calling the failing function.

    Like

  5. I think part of the problem is that the symbols (information allowing you to get function name and line number) are only present if you compile the application with symbols–which is the default in debug mode. One thing I do is when I compile a release application, I set the settings to generate a map file. The map file then allows you to convert the “MyApp+XXXXX” stuff to the location within a function.

    It’s not perfect: I’d prefer a map symbols file that drops me to the line within an application source code. But it’s the tradeoff of leaving enough information for someone to reverse engineer your application verses having really great debug dumps when someone sends in a bug report.

    Like

  6. Because I was lazy and needed to allocate an array of void pointers, so I used new.

    The code is written in Objective C++: that is, the code has Objective C and C++ features both activated; thus, you can use C++ features (like new) in the Objective C code.

    See, for example, Apple’s Objective C and C++ documentation for more information.

    You can replace this with “malloc(sizeof(void *) * len);” if you wish.

    Like

  7. You have a memory leak in UncaughtExceptionHandler().

    Memory is allocated using “new” and stored in “frames”, but never “delete”d.

    Like

  8. Pingback: How to intercept EXC_BAD_INSTRUCTION when unwrapping Nil

Leave a reply to poseteev Cancel reply