Weird iPhone Compiler/Architecture Bug
-
Jim B., Engineering Manager
- Feb 23, 2011
Alan on our iPhone team recently encountered a tricky bug that had to do with the ObjectiveC compiler and differences in architecture between ARM and x86.
The Problem
On the device (but not the sim), the following code would crash on views that implemented drawInRect methods that returned a CGPoint (instead of having a void return type like other drawInRect methods).
for (id view in _subviews) {
[view drawInRect:[view frame]];
}
To make matters weirder, the pointer ‘view’ before stepping into drawInRect was not the same as the pointer ‘self’ after stepping into drawInRect! However, by typecasting view to the correct class before calling drawInRect, you could avoid the crash.
for (id view in _subviews) {
if ([view isKindOfClass:[YPUserBadgeView class]]) [(YPUserBadgeView *)view drawInRect:[view frame]];
else [view drawInRect:[view frame]];
}
The skinny
This bug occurred because the compiler doesn’t know for sure the return type of drawInRect. It made a guess and it guessed incorrectly. This caused problems on ARM and not x86 because of differing calling conventions.
Why is the compiler dumb?
Because Objective-C is dynamically typed, it can’t be sure of the return type. According to Mike Ash “[the compiler] makes a guess at the method prototype based on the methods it can see from the declarations that it has parsed so far. If it can’t find one, or there’s a mismatch between the declarations it sees and the method that will actually be executed at runtime, Bad Things Happen.” Because the compiler found other methods named ` - (void)drawInRect:(CGRect)rect`, it made a guess to expect drawInRect to return void. Bad things happened.
Normally the compiler warns us of this, but in this case, it didn’t know about our implementation of drawInRect that returned CGPoint. If you put #import "YPUserBadgeView.h"
above the position where drawInRect:
was called, we get a warning that reads “ Multiple methods named ‘-drawInRect:’ found”. So at least in most cases, the compiler will tell us when it’s taking a guess.
Why did bad things happen on the device and not the sim?
This caused problems on the device and not the simulator because of difference in calling conventions between x86 and ARM (see http://en.wikipedia.org/wiki/Calling_convention). x86 stores the return value (or a pointer to it), in the eAX register. It therefore doesn’t care if there was or was not a return value. You may get garbage in your return value if you try to read a function with no return value, but it shouldn’t have a serious impact.
ARM however is different. It uses the first four registers for parameters and the return value. According to the aforementioned wikipedia article, “if the type of value returned is too large to fit in r0 to r3, or whose size cannot be determined statically at compile time, then the caller must allocate space for that value at run time, and pass a pointer to that space in r0.” TODO(johnb): reference http://infocenter.arm.com/help/topic/com.arm.doc.ihi0042d/IHI0042D_aapcs.pdf
http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.subset.swdev.abi/index.html instead of wikipedia. This means that depending on the the return type, r0 may contain a pointer to the return value, or it may contain the parameter self. TODO(johnb): Figure out why CGPoint wasn’t stored in registers and whether it is if you turn on optimizations. As an example the registers when a function like - (void)drawInRect:(CGRect)rect
is called might look like
r0 = self
r1 = _cmd (@selector(drawInRect:))
r2 = &rect
while the registers when a function like - (CGPoint)drawInRect:(CGRect)rect
is called might look like
r0 = &retVal // the memory location at which to store the returned CGPoint struct
r1 = self
r2 = _cmd (@selector(drawInRect:))
r3 = &rect
This causes obvious problems if a method is expecting the registers to look like the second example, but actually gets registers that look like the first example. Self is then read as the SEL value @selector(drawInRect:)!
What we should do about it?
According to
“http://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/ObjectiveC/Articles/ocStaticBehavior.html#//apple_ref/doc/uid/TP30001163-CH16-TPXREF161”>this Apple reference page: “In general, methods in different classes that have the same selector (the same name) must also share the same return and argument types.” Therefore we should avoid implementing methods that have the same name, but different argument types. Because our naming scheme usually mentions argument types, this is probably most likely to occur from different return types. In the case of this bug, we shouldn’t have methods like
- (void)drawInRect:(CGRect)rect;
and
- (CGPoint)drawInRect:(CGRect)rect;