Item 35: Use Zombies to Help Debug Memory-Management Problems
Debugging memory-management issues can be painful. Sending a message to a deallocated object is completely unsafe, as one would expect. But sometimes it works, and sometimes it doesn’t. It all depends on whether the memory where the object used to reside has been overwritten. Whether or not the memory is reused is nondeterministic, so a crash may happen only occasionally. Other times, the memory will be only partially reused, so certain bits of the object are still valid. Yet other times, the memory will by sheer fluke have been overwritten with another valid, live object. In these cases, the runtime will send the message to the new object, to which it may or may not respond. If it does, the app won’t crash, but you’ll wonder why objects you didn’t expect to be receiving messages are. If it doesn’t respond to that selector, the application will usually crash.
Fortunately, Cocoa’s “zombies” feature can come in handy. When this debugging feature isenabled, the runtime turns all deallocated instances into a special zombie object rather than deallocating them. The core memory where the object is located is not made available for reuse; therefore, nothing will ever overwrite it. When it receives any message, a zombie object throws an exception saying exactly what message was sent and what the object used to be when it was still alive. Using zombies is the best way to debug memory-management problems.
The feature is turned on by setting the
NSZombieEnabled
environment variable to
YES
. For example, if you’re using bash and running an application on Mac OS X, you would do something like this:
export NSZombieEnabled="YES"
./app
When a message is sent to a zombie, a message will be printed to the console, and the application will terminate. The message will look like this:
Click here to view code image
*** -[CFString respondsToSelector:]: message sent to
deallocated instance 0x7ff9e9c080e0
It is also possible to turn on the option in Xcode such that the environment variable is automatically set when the application is run from within Xcode. To do this, you edit theapplication’s scheme, select the Run configuration, then the Diagnostics tab, and finally turn onEnable Zombie Objects. Figure 5.7 shows the dialog that you should see in Xcode, with the option to enable zombies turned on.
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI21kbjhGbyoFcC5mWwQ2VOVXTzo1dn1WYsRGRjpWOD1kdNhVY2hTeadXUYJGNNd1TxMmeNlnTY1keFJzY3dGRapXWUlFbCh1T90WZ0l2PlxWamRXZn9CXt92YuUmbpxmbvN3av9mYpJXYmF2cukXbvw1LcpDc0RHaiojIsJye.jpg)
Figure 5.7 Enabling zombie objects from Xcode’s scheme editor
So how does the zombies feature work? It is implemented deep within the Objective-C runtime and the Foundation and CoreFoundation frameworks. When an object is being deallocated, an additional step is made by using the environment variable if this feature is enabled. This extra step turns the object into a zombie rather than fully deallocating it.
To see what this extra step does, consider the following code:
Click here to view code image
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface EOCClass : NSObject
@end
@implementation EOCClass
@end
void PrintClassInfo(id obj) {
Class cls = object_getClass(obj);
Class superCls = class_getSuperclass(cls);
NSLog(@"=== %s : %s ===",
class_getName(cls), class_getName(superCls));
}
int main(int argc, char *argv[]) {
EOCClass *obj = [[EOCClass alloc] init];
NSLog(@"Before release:");
PrintClassInfo(obj);
[obj release];
NSLog(@"After release:");
PrintClassInfo(obj);
}
This code uses manual reference counting to make it easier to show what happens when an object becomes a zombie. ARC would ensure that the
str
object was alive for as long as it needed to be, meaning that it would never become a zombie in this simple scenario. That’s not to say that objects can never become zombies under ARC. This type of memory bug can still occur with ARC but usually manifests itself through slightly more complex code.
The code in the example has a function to print out the class and superclass names of a given object. The code uses
object_getClass(),
a runtime function, rather than sending the class Objective-C message. If the object is a zombie, sending any Objective-C message will cause the zombie error message to be printed out and the application to crash. The output of the code looks like this:
Before release:
=== EOCClass : NSObject ===
After release:
=== _NSZombie_EOCClass : nil ===
The object’s class has changed from
EOCClass
to
_NSZombie_EOCClass
. But where did this class come from? It hasn’t been defined in the code. Also, it would be fairly inefficient for the compiler to create an extra class for every class it finds, just in case zombies are enabled. What happens is that this
_NSZombie_EOCClass
is generated at runtime the first time an object of class
EOCClass
is turned into a zombie. This uses the powerful runtime functions that can manipulate the class list.
The zombie class is a duplicate of a template class called
_NSZombie_
. These zombie classes don’t do much but simply act as a marker. You’ll see how they act as a marker shortly. First, consider the following pseudocode showing how the zombie class is created if necessary and then how it is used to turn the deallocating object into a zombie.
Click here to view code image
// Obtain the class of the object being deallocated
Class cls = object_getClass(self);
// Get the class's name
const char *clsName = class_getName(cls);
// Prepend _NSZombie_ to the class name
const char *zombieClsName = "_NSZombie_" + clsName;
// See if the specific zombie class exists
Class zombieCls = objc_lookUpClass(zombieClsName);
// If the specific zombie class doesn't exist,
// then it needs to be created
if (!zombieCls) {
// Obtain the template zombie class called _NSZombie_
Class baseZombieCls = objc_lookUpClass("_NSZombie_");
// Duplicate the base zombie class, where the new class's
// name is the prepended string from above
zombieCls = objc_duplicateClass(baseZombieCls,
zombieClsName, 0);
}
// Perform normal destruction of the object being deallocated
objc_destructInstance(self);
// Set the class of the object being deallocated
// to the zombie class
objc_setClass(self, zombieCls);
// The class of 'self' is now _NSZombie_OriginalClass
This routine is what becomes
NSObject
’s
dealloc
method. When it sees that the
NSZombieEnabled
environment variable is set, the runtime swizzles (see Item 13) the
dealloc
method for a version that performs the preceding code. At the end of this routine, the class of the object has been changed to
_NSZombie_OriginalClass
, where
OriginalClass
is the name of the class that once was.
Crucially, the memory the object lives in is not freed (through a call to
free()
); therefore, the memory will not be available for use again. Although this is leaking memory, this is a debugging tool only and would never be turned on for production-running applications, so it doesn’t matter.
But why create a new class for each class that is turned into a zombie? This is done so that the original class can be determined when a message is sent to a zombie. If all zombies were of class
_NSZombie_,
the original class name would be lost. Creating a new class is done by using the runtime’s function
objc_duplicateClass()
, which copies the entire class but gives it a new name. The superclass, instance variables, and methods of the duplicate class will be identical to the one being copied. Another way to achieve the same goal of maintaining the old class name would be to create the new class as inheriting from
_NSZombie_
rather than copying it. However, the functions to do this are less efficient than performing a direct copy.
The zombie class comes into action within the forwarding routines (see Item 12). The
_NSZombie_
class (and therefore all its copies) do not implement any methods. The class does not have a superclass and is therefore a root class, just like
NSObject
, with a single instance variable, called
isa
, which all Objective-C root classes must have. This lightweight class does not implement any methods, so whenever it is sent any message, it will go through the full forwarding mechanism (see Item 12).
At the heart of the full forwarding mechanism is
___forwarding___
, a function you may have seen in backtraces while debugging. One of the first things that this function does is check the name of the class of the object being sent a message. If this name is prefixed with
_NSZombie_
, a zombie has been detected, and something special happens. The application is killed at this point, after printing out a message (shown at the start of this item) to indicate what message was sent and to what type of class. That’s where the fact that the class name has the original class name within it comes in handy. The
_NSZombie_
is removed from the start of the zombie class name to leave just the original name. Pseudocode showing what happens is as follows:
Click here to view code image
// Obtain the object's class
Class cls = object_getClass(self);
// Get the class's name
const char *clsName = class_getName(cls);
// Check if the class is prefixed with _NSZombie_
if (string_has_prefix(clsName, "_NSZombie_") {
// If so, this object is a zombie
// Get the original class name by skipping past the
// _NSZombie_, i.e. taking the substring from character 10
const char *originalClsName = substring_from(clsName, 10);
// Get the selector name of the message
const char *selectorName = sel_getName(_cmd);
// Log a message to indicate which selector is
// being sent to which zombie
Log("*** -[%s %s]: message sent to deallocated instance %p",
originalClsName, selectorName, self);
// Kill the application
abort();
}
The action of this routine can be seen if the example is extended to attempt to message the zombie
EOCClass
object:
Click here to view code image
EOCClass *obj = [[EOCClass alloc] init];
NSLog(@"Before release:");
PrintClassInfo(obj);
[obj release];
NSLog(@"After release:");
PrintClassInfo(obj);
NSString *desc = [obj description];
If this is run with zombies enabled, the following is seen on the console:
Click here to view code image
Before release:
=== EOCClass : NSObject ===
After release:
=== _NSZombie_EOCClass : nil ===
*** -[EOCClass description]: message sent to deallocated
instance 0x7fc821c02a00
As you can see, this clearly shows what selector was sent and the original class of the object, as well as the pointer value of the dead object that was messaged. This information can be used if you are in a debugger for further analysis, if required, and can prove invaluable with the correct tools, such as Instruments, which ships with Xcode.
Things to Remember
When an object is deallocated, it can optionally be turned into a zombie instead of being deallocated. This feature is turned on by using the environment flag
NSZombieEnabled
.
An object is turned into a zombie by manipulating its
isa
pointer to change the object’s class to a special zombie class. A zombie class responds to all selectors by aborting the application after printing a message to indicate what message was sent to what object.