Why you should use -Wstrict-selector-match
This morning I’ve read an interesting post on Oliver Drobnik’s blog and since answering properly on Twitter is kind of hard I decided to write a post about my understanding of the issue.
In a nutshell, his post shows how invoking the length
method on an object conforming to the UILayoutSupport
protocol might not return what one would expect if the reference is typed as id
.
The reason for this is that the compiler doesn’t have enough information about the type of the object this method will be invoked onto and it needs to assume things by picking a method signature to set up the calling environment. In this case, it will likely pick the length
method on NSString
which happens to return an NSUInteger
. However, in the UILayoutSupport
case, the length
method returns a CGFloat
.
Now, you might think this is not a big deal but it actually is and let’s try and figure why 2 very distinct numbers are returned when using the correctly typed reference or not.
Since I have a much better understanding of the x86-64 assembly, ABI and calling conventions than I have of ARM or i386, I will try and illustrate the issue in a little Cocoa application. Luckily AppKit has a similar CGFloat
returning length
function in NSStatusItem
that we can use to reproduce a similar issue (NSString
is itself available in Foundation in both iOS and OS X).
Let’s assume we have the following program:
@implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)notification
{
NSStatusItem *statusItem = [[NSStatusItem alloc] init];
[statusItem setLength:60.0];
CGFloat correctLength = [self _checkCorrectLength:statusItem];
CGFloat wrongLength = [self _checkWrongLength:statusItem];
NSParameterAssert(correctLength == wrongLength);
}
- (CGFloat)_checkCorrectLength:(NSStatusItem *)statusItem
{
return [statusItem length];
}
- (CGFloat)_checkWrongLength:(id)statusItem
{
return [statusItem length];
}
@end
This is a simple enough program and if we run it we will fail on the assertion that 60 is not equal to 16. No matter what value we set for the length, the incorrect value will always be 16.
Similarly as what Oliver describes in his post, we have here two different cases. In the _checkCorrectLength
, the first argument is correctly typed as a reference to an NSStatusItem
object on which the length
method is later invoked on. In the the _checkWrongLength
, the argument is typed as id
and when the length
method is invoked, we let the compiler figure things out on its own.
Before diving into the assembly generated by the compiler for our own code, let’s disassemble the length
function in NSStatusItem
.
AppKit`-[NSStatusItem length]:
0x7fff906bd264: pushq %rbp
0x7fff906bd265: movq %rsp, %rbp
0x7fff906bd268: movq -442811119(%rip), %rax ; NSStatusItem._fLength
0x7fff906bd26f: movsd (%rdi,%rax), %xmm0
0x7fff906bd274: popq %rbp
0x7fff906bd275: ret
Before explaining what is going on, it is worth having a quick look at the NSStatusItem
instance variables structure.
@interface NSStatusItem : NSObject
{
@private
NSStatusBar* _fStatusBar;
CGFloat _fLength;
NSWindow* _fWindow;
NSView* _fView;
int _fPriority;
etc...
}
Coming back to the disassembly of -[NSStatusItem length]
, we can see that it is pretty simple. The offset of the _fLength
iVar in the class struct is moved into the %rax
register. At this stage it is worth noting that %rdi
still contains the receiver hence the NSStatusItem
object reference. Further on, the value at the address object pointer + iVar offset
is dereferenced into the %xmm0
register, which is a register used for floating point calculations. The stack is cleaned up and the function then returns.
At this point, it is very important to note that, as per the x86-64 ABI, the %xmm0
register is documented as used to pass and return floating point arguments. In the usual case where an integer argument is returned the %rax
is used to return values from a function.
If you are used into looking at disassembly you might have figured out what went wrong by now but for sake of completeness let’s have a look at the disassembly of the _checkCorrectLength:
and _checkWrongLength
methods.
Reg`-[AppDelegate _checkCorrectLength:] at AppDelegate.m:26:
0x100001420: pushq %rbp
0x100001421: movq %rsp, %rbp
0x100001424: subq $32, %rsp
0x100001428: leaq 5281(%rip), %rax
0x10000142f: movq %rdi, -8(%rbp)
0x100001433: movq %rsi, -16(%rbp)
0x100001437: movq %rdx, -24(%rbp)
0x10000143b: movq -24(%rbp), %rdx
0x10000143f: movq %rdx, %rdi
0x100001442: movq %rax, %rsi
0x100001445: callq *5253(%rip)
0x10000144b: addq $32, %rsp
0x10000144f: popq %rbp
0x100001450: ret
Reg`-[AppDelegate _checkWrongLength:] at AppDelegate.m:31:
0x100001460: pushq %rbp
0x100001461: movq %rsp, %rbp
0x100001464: subq $48, %rsp
0x100001468: movq %rdi, -8(%rbp)
0x10000146c: movq %rsi, -16(%rbp)
0x100001470: movq %rdx, -24(%rbp)
0x100001474: movq 5205(%rip), %rsi
0x10000147b: leaq 5198(%rip), %rdi
0x100001482: movq %rdi, -32(%rbp)
0x100001486: movq %rdx, %rdi
0x100001489: movq -32(%rbp), %rdx
0x10000148d: movq %rsi, -40(%rbp)
0x100001491: movq %rdx, %rsi
0x100001494: movq -40(%rbp), %rax
0x100001498: callq *%rax
0x10000149a: movd %rax, %xmm0
0x10000149f: movaps 218(%rip), %xmm1
0x1000014a6: punpckldq %xmm1, %xmm0
0x1000014aa: movapd 222(%rip), %xmm1
0x1000014b2: subpd %xmm1, %xmm0
0x1000014b6: haddpd %xmm0, %xmm0
0x1000014ba: addq $48, %rsp
0x1000014be: popq %rbp
0x1000014bf: ret
Keep in mind that both methods are returning a CGFloat
so the the %xmm0
register will be used to hold the return (floating point) argument. Also, in both method implementations, the callq
instruction is a call to objc_msgSend
with the object reference and the length
selector.
In the correct case, we can see that upon return from the objc_msgSend
function call for length
, the stack is quickly cleaned up and the method implementation returns. Since the %xmm0
register hasn’t been touched, upon return of the method implementation it will still contain the value that was stored in the length
method implementation.
In the incorrect case, we can clearly see that the compiler has emitted code as if the length
method implementation returned an integer, by retrieving the return value in the %rax
register. Since the _checkWrongLength
is supposed to return a floating point, the value is then moved to the %xmm0
register and after a few instructions I honestly don’t have the knowledge to understand and a bit of stack cleanup, the function returns. So basically, the function moved the value stored by the length
implementation into %rax
into %xmm0
and returned.
If you followed up to here you have the answer: the value returned by the length
method on an NSStatusItem
object reference typed to id
is the offset of the _fLength
instance variable in the NSStatusItem
class structure.
In your case Oliver, I also suspect 97 to be the offset of some instance variable into some layout guide object class structure. However, it's also important to note that this would be completely coincidental. %rax
(or its counterpart in ARM) is not preserved across function calls and for the case of a function returning a floating point argument (and thus writing its return value in xmm0
or xmm1
, or their ARM counterpart) %rax
could contain any garbage.
There is a very easy way to avoid such issues by trusting the compiler and enabling the -Wstrict-selector-match
warning. The compiler will warn you that it cannot make a smart decision about it and will likely result in some unexpected behavior.