Begin main content

Debugging release/autorelease issues in Cocoa

So your code crashes with an EXC_BAD_ACCESS in objc_msgSend and there is *none* of your own code in the stack trace. Bummer. Since all we know from objc_msgSend is that it's trying to call a method on an object. Since we got EXC_BAD_ACCESS you can bet your bottom dollar that it's attempting to call a method on a freed pointer, so there's no obvious way of finding out what the object was. We can, though, easily find the method. Read Greg Parker's excellent post So you crashed in objc_msgSend(). For the iPhone simulator on my Mac I need to do this at the gdb prompt to decode the eax register:

(gdb) x/s $ecx
0x980530c4:	 "release"
you can see that the selector in the second argument is "release". Now we're getting somewhere, we know an already freed object is trying to be released. Since there is none of our code in the stack trace it's a fair bet that it's happening when the autorelease pool is releasing.

There are a few tricks in your toolbox for tracking down over-released objects. There's the NSZombieEnabled, NSAutoreleaseFreedObjectCheckEnabled and other environment variables which will give you better logging rather than just crashing.

I've found this is not always reliable (logs from the above builtin debug options showed that the over-release was on an NSCFDate object, when in fact it was an NSNumber). Sometimes you also just want to trace through exactly what is happening with your retain/release/autorelease cycles. That's what we're about to achieve here.

Ok, so you have a unit test that replicates this problem right? I use the excellent GHUnit by Gabriel Handford. If not you'll have to play around a bit to narrow down your problem code. In my case, I wanted to confirm that a particular unit test was failing during the autorelease pool release. Best way is to wrap the offending code in a new autorelease pool. Then it will be freed inside your code, which you will see in the stack trace, and confirm you have found the reason. My unit test now looks like:

(Note that the code examples, embedded in github gists, are not showing up in my RSS feed. I'll fix that later, but for now please read the original post)

Now when I fail with EXC_BAD_ACCESS my stack trace looks like this:

Where line 91 of CJTestCJCard.m is the [pool release] call above. So something is being added to the autorelease pool, and is *also* being fully released, or maybe it's being added to the pool more times than it is being retained. There doesn't seem to be a way to interrogate the contents of the autorelease pool (which is a shame) but we log some really useful info by overriding a few methods, like NSObject's release and autorelease methods, so we can see when things are being added to the pool and when things are being released. This is some funky stuff, but since we're only doing it in our unit test class, not code we're going to release you can sleep soundly at night. Here's how I effected logging in release and autorelease.

Add this to the top of your unit test class, before your current @implementation:

Now in your unit test class @implementation, you can use the setupClass method to effect the method implementation switcharoo:

Now your log will be filled with log lines like:

This can help when tracking down tricky releasing bugs. Note that not all classes seem to comply. For instance I see plenty of log lines for NSCFString, but never any release lines. I assume that NSCFString release implementation is a bit different since it's a toll free bridged object - getting that to log is an exercise left for the reader!

If you're interested in the functions I used above to interact with the runtime, check out the Introduction to The Objective-C Programming Language and Objective-C Runtime Reference. Note that the code above works fine in the iPhone simulator. It should be fine on 64 bit Mac apps, but you may need to diddle the method structs manually in 32 bit Mac apps since they use the legacy Objective-C runtime (which you can read about in the Objective-C 1 Runtime Reference).

Also note it won't work on the iPhone itself, so to save constantly commenting out code, you can wrap the offending code in #if TARGET_IPHONE_SIMULATOR #endif pairs.

08:38 PM, 13 Jan 2010 by Mark Aufflick Permalink

Thanks!

Hi Mark,

Just wanted to thank you. I used your code, added a retainLog and made a few modifications and finally tracked my NSDate autorelease bug to its source. Here's my code:

static char *_class_to_debug = "__NSDate";

@implementation NSObject (MemDebugging)

- (id) retainLog {
    if (strcmp(_class_to_debug, class_getName([self class])) == 0)
        fprintf(stderr, "retain: %s(%p) retainCount = %u\n", class_getName([self class]), self, [self retainCount]);
    return [self retainLog];
}
 
- (void) releaseLog {
    if (strcmp(_class_to_debug, class_getName([self class])) == 0)
        fprintf(stderr, "release: %s(%p) retainCount = %u\n", class_getName([self class]), self, [self retainCount]);
    [self releaseLog];
}
 
- (id) autoreleaseLog {
    if (strcmp(_class_to_debug, class_getName([self class])) == 0)
        fprintf(stderr, "autorelease: %s(%p) retainCount = %u\n", class_getName([self class]), self, [self retainCount]);
    return [self autoreleaseLog];
}

@end
The setup function is identical (with some lines added for retainLog). I made all this conditionally compilable with an #ifdef MEMDEBUG in my main.c file so I can easily enable/disable it as needed (hopefully not too often).

The nice thing with this new version is that now, when you can something like "-[__NSDate release]: message sent to deallocated instance 0x64f3590", you can actually track the instance address back to its source.

I'm glad I found your post, I was pulling my hair out trying to find this one!

Again, thanks a lot!

Marco

by Unregistered Visitor on 02/11/11

Ouch!

Sorry about the formatting. It looked good in the text box :-) /// Marco

by Unregistered Visitor on 02/11/11

Add comment