Hi guys,
I’d like to expose some debugging magic today. There’s pretty much information can be found all over the internet about how to debug crashes, how to debug leaks and other memory issues. You can start from the official apple’s tech note and go further.
What I’d like to talk about today is something different from that. I stuck recently with a very hard-to-investigate bug in my project, and lldb’s ability to show me objective-c internals was of a very good help.
The Problem
But let me first describe what was the issue. It doesn’t matter what the project was and was it’s main functionality, but the essentials for the bug I faced were:
- from time to time I needed to switch main window’s
rootViewController
- there was a separate component for showing some content fullscreen, with orientation handling support built-in. It is added as subview to main window and can handle rotation regardless of orientation support by controller currently used by window as
rootViewController
So, the bug was being reproduced as follows:
- Show fullscreen component.
- Hide fullscreen component temporally (leave it’s current state, just remove it’s view from window)
- Switch window’s
rootViewController
- Try rotating device, although current
rootViewController
supports rotation, it doesn’t get all that rotation notifications.
The key was that fullscreen component being shown. Before showing it, the viewController mentioned gets rotated as expected. I couldn’t even thing from where to start with this bug.
See inside, there is nothing to hide
And then I started from seeing internals. It’s obvious, that there’s some point in UIKit’s internals where decision is taken, whether to show rotation notifications or not. I tried to find this place.
So I put the breakpoint at the start of willRotateToInterfaceOrientation: duration:
method to see the point in internals from where it is being called. Then start app and try rotate in usual way to get this breakpoint hit. Here’s the stack:
As you can see, there’s a bunch of methods in stack, I don’t know where is this my point of interest, so I decided to just to put breakpoint at every method’s start and at the place where next method is being called. I will use these path when bug is reproduced to figure out when this workflow goes out of track.
Next, let our bug occur and let’s step along this stack and find the breakpoint that is not fired. After some attempts, I have finally figured out, that following fragment is responsible for going further and perform rotation, or quit without doing anything:
0x5e424d: movl 8(%ebp), %ebx 0x5e4250: movl 6841556(%edi), %eax 0x5e4256: cmpl (%ebx,%eax), %esi 0x5e4259: je 0x5e42b4 ; -[[UIWindow _updateInterfaceOrientationFromDeviceOrientation:]] + 170
As we can see there’s something being compared. First some local variable is placed in n $ebx, than something is loaded from data segment in $eax, next variable placed within given offset in object from $ebx is compared with contents of $esi. Let’s try to see what the object is:
(lldb) po *(id*)$ebx (id) $27 = 0x00c63324 UIWindow
Aha, it’s UIWindow instance! so, offset from the beginning of this object is :
(lldb) print $eax (unsigned int) $28 = 84
Honestly, I haven’t tried to see what’s the variable is placed there computing offsets. I tried to see inside the UIWindow’s instance and guess. To do this I add new expression to variables view:
(UIWindow*)(id*)$ebx
Here’s what I have:
It’s not that easy now to figure out what member variable is placed under 82 offset. The trick I used is to look inside of value. The value at this offset should be :
(lldb) print *(int*)($eax+$ebx) (int) $45 = 2 print (UIInterfaceOrientation)*(int*)($eax+$ebx) (UIInterfaceOrientation) $46 = UIInterfaceOrientationPortraitUpsideDown
Why did I cast it to UIInterfaceOrientation? Well, I guessed that it might be member variable _viewOrientation
. And yes, it’s value the same we found. And remembering the context of the problem it definitely makes sense: before performing orientation change handing UIWindow checks if it is already in that orientation. But where the value in $esi
comes from? Let’s see asm dump before this fragment:
0x5e4219: movl 6805500(%edi), %eax 0x5e421f: movl 6757808(%edi), %ecx 0x5e4225: movl %ecx, 4(%esp) 0x5e4229: movl %eax, (%esp) 0x5e422c: calll 0xa6371a ; symbol stub for: objc_msgSend 0x5e4231: movl 6759088(%edi), %ecx 0x5e4237: movl %ecx, 4(%esp) 0x5e423b: movl %eax, (%esp) 0x5e423e: calll 0xa6371a ; symbol stub for: objc_msgSend 0x5e4243: movl %eax, %esi 0x5e4245: leal -1(%esi), %eax 0x5e4248: cmpl $3, %eax 0x5e424b: ja 0x5e42ac ; -[UIWindow _updateInterfaceOrientationFromDeviceOrientation:] + 162 0x5e424d: movl 8(%ebp), %ebx 0x5e4250: movl 6841556(%edi), %eax 0x5e4256: cmpl (%ebx,%eax), %esi
As seen, $esi
gets it’s value at line 0x5e423e from $eax
, which takes it’s value as result of function call before. See, debugger even tells us that this function call is an objective-c method calling. If you’re not yet familiar with how objective-c methods are called, well it’s pretty well described here, at this point we’ll take as fact that $esp
contains address of an object whom message is being sent, and $esp+4
is a selector name (as plain c string). So, let debugger run till line 0x5e423e and let’s see what’s is in the stack:
(lldb) po *(id*)$esp (id) $7 = 0x07d66470 <UIDevice: 0x7d66470> (lldb) print *(char**)($esp+4) (char *) $10 = 0x04136f36 "orientation"
Performing the same actions some lines before would show that $eax
value (address of object) is obtained through the call to [UIDevice currentDevice]
.
And now the question is why this member variable has already new orientation’s value? Well, it’s not so hard to find out: lets’ put watch on it and see.
Ok, I put the watch, rotated the simulator and gotcha! Watch was hit, here’s the stack:
See solid-black coloured line? Yes, it’s the line in my code, the exact line of code which leads the bug to appear. Going forward, the problem of my bug was that I observed deviceOrientation changes in that place and set statusBarOrientation manually, which apparently modified current UIWindow’s _viewOrientation property. Not so obvious to guess without seeing these internals, but now it’s clear for me.
Conclusion
Although this technic is rarely used, it shows how powerful lldb debugger can be in certain situations. If you’re not yet familiar with it — seriously, take some time and learn how to use it. Some day it will save you week of guessing what could be wrong in your code.