Originally posted June 2010. This debugging procedure — zombies plus malloc stack history — saved me more than once in the manual-retain/release days, so I documented it thoroughly. The problem it solves is eternal: an object receives a message after being deallocated, and you need to know who allocated it (and who freed it). The tools below have changed, but the workflow maps one-to-one onto what you would do in 2026, which I describe after the original.
The telltale sign in the debugger console looks like this:
-[NSFetchedResultsController class]: message sent to deallocated instance 0x11957d0
Sometimes it says “freed” instead of “deallocated” — same thing. That hex address at the end is the key.
The 2010 procedure (Xcode 3 / GDB era)
To find where the object was allocated, you enabled two environment variables on the executable: NSZombieEnabled YES and MallocStackLoggingNoCompact 1 (in those days: right-click the executable, Get Info, Arguments tab). Then run, reproduce the crash, and ask GDB for the allocation stack:
(gdb) info malloc-history 0x11957d0
And you would get the full allocation backtrace:
Stack - pthread: 0xa0416720 number of frames: 28
0: 0x9264382d in malloc_zone_calloc
1: 0x92643782 in calloc
2: 0x93611618 in _internal_class_createInstanceFromZone
3: 0x9361ab08 in _internal_class_createInstance
4: 0x3020275c in +[NSObject allocWithZone:]
5: 0x3020264a in +[NSObject alloc]
6: 0x3d92 in -[AMRAPViewController fetchedResultsController] at CrossFit Timer/Classes/AMRAPViewController.m:146
7: 0x3a18 in -[AMRAPViewController viewWillAppear:] at CrossFit Timer/Classes/AMRAPViewController.m:96
8: 0x3097c945 in -[UINavigationController _startTransition:fromViewController:toViewController:]
9: 0x30977c33 in -[UINavigationController _startDeferredTransitionIfNeeded]
10: 0x3097d01e in -[UINavigationController pushViewController:transition:forceImmediate:]
Frame 6 (below the alloc machinery) showed exactly where the doomed object was created — which told you which over-release or premature release to hunt down.
The 2026 procedure
Manual retain/release is gone (ARC has been the default since 2011), and GDB is gone too — but over-releases still happen through bridging, unsafe unowned references, and C interop. The same two-stage workflow survives:
1. Zombies: instead of the NSZombieEnabled environment variable, use the Zombies instrument in Instruments, or edit the scheme and check Diagnostics → Zombie Objects. Deallocated objects become “zombies” that log instead of vanishing, so the message-to-deallocated-instance crash becomes a diagnosable event.
2. Allocation history: instead of info malloc-history, use the Allocations instrument with Record reference counts and Detect VM allocations enabled, or tick Diagnostics → Malloc Stack Logging in the scheme. Then click the zombie’s address in the console and Xcode jumps straight to the allocation and release stack traces — what used to require pasting addresses into GDB is now a click in the memory graph. Apple’s archived malloc debugging documentation explains the underlying machinery, and TN2124 is still the classic deep-dive into what the zombies actually do.
3. The Memory Graph Debugger (Xcode 8+): when you want to see who still references a suspicious object, pause the app and open Debug → Debug Memory Graph. It draws the live object graph — the fastest way I know to find the retain cycle or the stray strong reference behind an over-release.
Same warnings as always
Zombies and malloc stack logging are debugging aids, not optimizations — they make every allocation hold its stack trace and never truly free memory. Never ship a build with them enabled; profile with them, release without them. (In practice: leave them off in the Release scheme and check the archive configuration before you ship, exactly as you had to in 2010.)
References: Apple QA1788, Building with Zombie mode enabled is not allowed in the App Store; Finding memory leaks with Instruments; Instruments tutorials.