Why do nil / NULL blocks cause bus errors when run?

zoul picture zoul · Nov 10, 2010 · Viewed 14.7k times · Source

I started using blocks a lot and soon noticed that nil blocks cause bus errors:

typedef void (^SimpleBlock)(void);
SimpleBlock aBlock = nil;
aBlock(); // bus error

This seems to go against the usual behaviour of Objective-C that ignores messages to nil objects:

NSArray *foo = nil;
NSLog(@"%i", [foo count]); // runs fine

Therefore I have to resort to the usual nil check before I use a block:

if (aBlock != nil)
    aBlock();

Or use dummy blocks:

aBlock = ^{};
aBlock(); // runs fine

Is there another option? Is there a reason why nil blocks couldn’t be simply a nop?

Answer

mattjgalloway picture mattjgalloway · Feb 5, 2013

I'd like to explain this a bit more, with a more complete answer. First let's consider this code:

#import <Foundation/Foundation.h>
int main(int argc, char *argv[]) {    
    void (^block)() = nil;
    block();
}

If you run this then you'll see a crash on the block() line that looks something like this (when run on a 32-bit architecture - that's important):

EXC_BAD_ACCESS (code=2, address=0xc)

So, why is that? Well, the 0xc is the most important bit. The crash means that the processor has tried to read the information at memory address 0xc. This is almost definitely an entirely incorrect thing to do. It's unlikely there's anything there. But why did it try to read this memory location? Well, it's due to the way in which a block is actually constructed under the hood.

When a block is defined, the compiler actually creates a structure on the stack, of this form:

struct Block_layout {
    void *isa;
    int flags;
    int reserved;
    void (*invoke)(void *, ...);
    struct Block_descriptor *descriptor;
    /* Imported variables. */
};

The block is then a pointer to this structure. The fourth member, invoke, of this structure is the interesting one. It is a function pointer, pointing to the code where the block's implementation is held. So the processor tries to jump to that code when a block is invoked. Notice that if you count the number of bytes in the structure before the invoke member, you'll find that there are 12 in decimal, or C in hexadecimal.

So when a block is invoked, the processor takes the address of the block, adds 12 and tries to load the value held at that memory address. It then tries to jump to that address. But if the block is nil then it'll try to read the address 0xc. This is a duff address, clearly, and so we get the segmentation fault.

Now the reason it must be a crash like this rather than silently failing like an Objective-C message call does is really a design choice. Since the compiler is doing the work of deciding how to invoke the block, it would have to inject nil checking code everywhere a block is invoked. This would increase code size and lead to bad performance. Another option would be to use a trampoline which does the nil checking. However this would also incur performance penalty. Objective-C messages already go through a trampoline since they need to look up the method that will actually be invoked. The runtime allows for lazy injection of methods and changing of method implementations, so it's already going through a trampoline anyway. The extra penalty of doing the nil checking is not significant in this case.

I hope that helps a little bit to explain the rationale.

For more information, see my blog posts.