4

I created a new project, enabled Zombie Objects (Edit Scheme -> Diagnostics). I initialized two objects: ZombieTest and ZombieTest2(inherit from NSObject). After running the app, I opened debug memory graph and only the object that inherit from NSObject appears as NSZombie.

Debug memory graph

Hen Shabat
  • 519
  • 1
  • 6
  • 19
  • How can you call pure swift object after it deallocated? – Mojtaba Hosseini Dec 08 '19 at 15:22
  • Zombies aren't implemented for Swift objects, as far as I can tell. Swift class don't typically use message passing (except for `dynamic @objc` functions), so a Zombie wouldn't really serve much purpose. – Alexander Dec 08 '19 at 15:44

2 Answers2

6

tl;dr: because NSZombies are implemented to only affect NSObject and its subclasses. (This doesn't have to do with Swift, either: Obj-C objects which aren't subclasses of NSObject also won't become zombies.)


Upon initialization (__CFInitialize, called when the framework is loaded), the CoreFoundation framework sets up a lot low-level Foundation and CoreFoundation behaviors; among other things, it looks for the NSZombieEnabled environment variable, and if present, enables zombies by calling the __CFZombifyNSObject function:

; Disassembly from Hopper on /System/Library/Frameworks/CoreFoundation.framework/CoreFoundation on macOS Catalina
0000000000001cc0         lea        rdi, qword [aNszombieenable]                ; argument #1 for method ___CFgetenv, "NSZombieEnabled"
0000000000001cc7         call       ___CFgetenv                                 ; ___CFgetenv
0000000000001ccc         test       rax, rax
0000000000001ccf         je         loc_1cee

0000000000001cd1         mov        al, byte [rax]                              ; DATA XREF=sub_1bb8ec
0000000000001cd3         or         al, 0x20
0000000000001cd5         cmp        al, 0x79
0000000000001cd7         jne        loc_1cee

0000000000001cd9         cmp        byte [___CFZombieEnabled], 0x0              ; ___CFZombieEnabled
0000000000001ce0         jne        loc_1cee

0000000000001ce2         mov        byte [___CFZombieEnabled], 0xff             ; ___CFZombieEnabled
0000000000001ce9         call       ___CFZombifyNSObject                        ; ___CFZombifyNSObject

When zombies are enabled, __CFZombifyNSObject replaces the implementation of -[NSObject dealloc] with a different implementation (__dealloc_zombie):

// Hopper-generated pseudo-code:
void ___CFZombifyNSObject() {
    rax = objc_lookUpClass("NSObject");
    method_exchangeImplementations(class_getInstanceMethod(rax, @selector(dealloc)), class_getInstanceMethod(rax, @selector(__dealloc_zombie)));
    return;
}

This means that all subclasses of NSObject and their descendants, upon deallocation, will call through to __dealloc_zombie. So what does __dealloc_zombie do?

// Hopper-generated pseudo-code:
/* @class NSObject */
-(void)__dealloc_zombie {
    rbx = self;
    if ((rbx & 0x1) != 0x0) goto loc_175ed5;

loc_175e3f:
    if (*(int8_t *)___CFZombieEnabled == 0x0) goto loc_175eee;

loc_175e4c:
    rax = object_getClass(rbx);
    var_20 = 0x0;
    rax = asprintf(&var_20, "_NSZombie_%s", class_getName(rax));
    rax = objc_lookUpClass(var_20);
    r14 = rax;
    if (rax == 0x0) {
            r14 = objc_duplicateClass(objc_lookUpClass("_NSZombie_"), var_20, 0x0);
    }
    free(var_20);
    objc_destructInstance(rbx);
    object_setClass(rbx, r14);
    if (*(int8_t *)___CFDeallocateZombies != 0x0) {
            free(rbx);
    }
    goto loc_175ed5;

loc_175ed5:
    if (**___stack_chk_guard != **___stack_chk_guard) {
            __stack_chk_fail();
    }
    return;

loc_175eee:
    if (**___stack_chk_guard == **___stack_chk_guard) {
            _objc_rootDealloc(rbx);
    }
    else {
            __stack_chk_fail();
    }
    return;
}

In more human-readable terms, it:

  1. Looks up [self class]
  2. If there isn't already a class named _NSZombie_<our class name>, it creates one by duplicating the _NSZombie_ class and gives the duplicate a new name (this creates a copy of the class with all of its method implementations, or lack thereof)
  3. It tears down self, and replaces its class with the new class, so that if you message it in the future, you dispatch to _NSZombie_<whatever>

_NSZombie_ is a class which implements no methods, so sending it any message (method call) ends up falling into a code path in message forwarding which prints out the "message sent to deallocated instance" message.


Effectively, this method of implementing zombies hinges on inheritance from NSObject (because all NSObject subclasses should call [super dealloc] on deallocation, eventually reaching [NSObject dealloc]); things which don't inherit from NSObject don't inherit this implementation. (You can also actually see this by implementing an NSObject subclass which doesn't call [super dealloc] in its -dealloc implementation — the object won't get zombified on release.)

Do NSZombies have to be implemented this way? No, it's certainly possible to imagine other schemes which would allow pure Swift objects to participate (Swift runtime initialization could also look up the NSZombieEnabled environment variable and do something similar), but there's somewhat less of a benefit to putting in the effort. As Rob mentions in his answer, this works largely because we're able to swizzle out the class of the deallocated instance (this is actually possible with the Swift runtime, but not exposed externally), but crucially, even if we did this, it wouldn't help cases of static method dispatch, which is possible on object types in Swift (e.g. for final classes). [Alexander alludes to this in his comment.]

So, largely, it's really easy to implement this way for Obj-C, and there are somewhat limited benefits for taking the time to do this for pure Swift classes too.

Itai Ferber
  • 28,308
  • 5
  • 77
  • 83
  • Zombies objects can cause crashes, all objects inherit from NSObject in some way are marked as NSZombie by Xcode's debug memory graph. For example when my class inherits from Operation it's been marked as NSZombie and I couldn't find a way to unmark it as NSZombie. – Hen Shabat Dec 09 '19 at 09:26
  • You cannot "unmark" an object as an `NSZombie`, because the fact that it turned into a zombie means that it's already gone for good (because it's been deallocated). If you're seeing many crashes in your app due to messaging zombies after turning them on, it means that your code is relying on using deallocated objects when they are not zombies, which is just as bad! If this is the case, another tool like the [Address Sanitizer](https://developer.apple.com/documentation/code_diagnostics/address_sanitizer) can help you figure out where your objects are being released and reused. – Itai Ferber Dec 09 '19 at 14:01
  • You might not be retaining objects which need to be retained, which would lead to them being released prematurely. – Itai Ferber Dec 09 '19 at 14:02
3

Conversion to NSZombie is handled by the ObjC runtime, and requires an ISA swizzle (i.e. the class of the object is changed at runtime). Pure Swift classes don't support that.

There are cases where a pure Swift object might still become an NSZombie, since pure Swift objects are sometimes bridged to the ObjC runtime, but those are implementation details that can't be relied on.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610