Sometimes you find yourself in a situation where you have to fix a bug or add a new features to a codebase you know little about. Wether you moved to a new job, downloaded an open source project, or came back to one of your old projects, you may not know where things are.
In these situations, in order to start editing code, you need to understand the codebase and find which parts of the code are responsible for which screens/features.
Imagine you are trying to find the method that is executed when a table cell is selected on a given screen.
You could use a brute force approach, by searching for the
didSelectRow and narrow the result down to the file that seems closer to the screen at hand, and then probably add breakpoints and run the action to make sure you got the correct location. This approach, that I am guilty of using, is error prone and on the long run, it teaches nothing.
A more sane approach would be to use the debugger, lldb, and Xcode view debugging to easily and more precisely discovering the code.
Using lldb as a discovery mechanism has some advantages:
- It tests and improve your knowledge of cocoa touch framework and objective-c runtime.
- It improves your general debugging skills.
- It is a more scientific approach, since you can quickly make assumptions and accept or reject them.
- It is much faster to add and remove breakpoints, than is to search text and read code.
For this article, I am going to discover and fix a couple of issues in an unknown codebase, by using lldb and Xcode view debugging and never going to the source code, except when breakpoints are hit.
Lets start by cloning the project and installing the pods
git clone https://github.com/oarrabi/hniosreader cd hniosreader pod install
We will use Xcode view debugging, that means we need Xcode 6 and iOS 8+, and we are going to use a 32-bit iPhone simulator device, since we will stick with x86 ABI calling conventions for lldb.
Lets start by opening the project and looking at the first bug.
Switch to bug1 branch of the git repo.
The bug description is:
Selecting any table row always displays the first row content.
Since it happens when we select a row, we start by adding a break point on
didSelectRow. To do that, Run the app, then pause the execution using "Debug -> Pause" menu item, and write the following in the console.
breakpoint set -r "didSelectRow"
The above adds a regular expression breakpoint for any method that contains the string
Next, continue the app execution by writing
continue (and hitting enter) in lldb or "Debug -> Continue" menu item. Tap on any cell to reproduce the issue, Xcode now should be pointing at
EntryListController line 242.
self.selectedRow = indexPath; [self showWebViewAtIndexPath:nil];
Now since this issue was introduced by me, I know that I should pass indexPath instead of nil, lets fix the bug by passing
Switch to "bug2" branch to reproduce this bug.
The bug description is:
Tapping on the user name button displays the wrong user name
For this bug, we need to figure out what action is called when the user name is tapped, in order to add a breakpoint, we need to know the cocoa touch class for the user name view.
Lets start by hitting on debug view in Xcode.
After Xcode shows the view debugging screen, we select any user name view and notice that it is a UIButton object. That means we need to add a breakpoint on the action that is called when a UIButton is tapped.
When a button is tapped,
UIApplication sends a
sendAction:to:forEvent: message to the
UIControl. Lets proceed to add a breakpoint on that message.
breakpoint set -r "UIControl sendAction:to:forEvent:"
If we continue the app execution, and tap on any user name view, Xcode will stop us on an assembly section instead of actual source code; This happens because we don’t have
UIControl sendAction:to:forEvent source code.
In order to proceed, we need to know the action associated with this
UIControl. In the
sendAction:to:forEvent: method call, the
UIControl action is the first parameter passed to the method. In order to access the parameters when stopped on an assembly section, we have to understand the ABI calling convention.
These conventions explain how are parameters packaged and passed to method calls, for a breakpoint stopped on a method prologue on a 32-bit simulator, accessing the method parameters is summarised in this table:
Returning to our problem, we need to print the first parameter passed to
sendAction:to:forEvent:, since in objective-c method calls the first and second parameters are
cmd, what we really need to print is the third parameter. So executing the following in the lldb console prints:
po *(int*)($esp + 4) -> prints self po (SEL)*(int*)($esp + 8) -> prints __cmd "sendAction:to:forEvent" po (SEL)*(int*)($esp + 12) -> prints the first parameter po *(int*)($esp + 16) -> prints the second ... so on
Notice that we had to cast to
(SEL) since lldb will not know how to represent a
SEL without explicit casting.
When we execute
po (SEL)*(int*)($esp + 12) we get
"userNameButtonPressed:" which is the name of the action that will be called when a user name button is tapped.
Lets add a breakpoint on that.
breakpoint set -r "userNameButtonPressed"
Notice the flag
-o which means a one shot breakpoint: A breakpoint that deleted after the first stop.
Continue the execution of the app, and you should be stopped on
TableViewController line 420
If you scan this method, you will find the culprit snippet that caused this issue on line 425
NSNumber *itemNumber = self.top100StoriesIds[item + 2];
To fix it just remove the + 2 which I introduced for this example.
Switch to "bug3" branch to reproduce this bug.
Sometimes we get a "-- points" string instead of the number of points
First lets use Xcode view debugging to get the class of the view that is displaying the
-- points string.
We notice that the view is a
UILabel. That means we need to set a breakpoint on
setText. However, since we have lots of labels on screen,
setText will be called in too many places, this looks like a job for a conditional breakpoint.
We need to check the value of the first parameter passed to setText and execute the breakpoint when it
@"-- points" string.
We learned from the previous bug that the parameters passed to a method can be accessed through the
esp register. In order to catch the text desired, we need to add the following almost self-explanatory breakpoint.
breakpoint set -r "setText:" -c '(BOOL)[*(int*)($esp + 12) isEqual:@"-- points"]'
The above will call
isEqual on the first parameter (third parameter if counting self and
__cmd) passed to setText.
If we continue executing the project, and scroll the table, Xcode should stop on an assembly section again. However this time the call stack will show a location in our code. Make sure you are displaying the Debug Navigator, by pressing command + 6, or selecting View -> Navigators -> Show Debug Navigator menu item.
If you select the next call stack you should find yourself in
TableViewController line 315.
The culprit line is:
cell.pointsLabel.text = [NSString stringWithFormat:@"%@ points", points < 100? @(points) : @"--"];
Since this is a bogus bug that I introduced, fixing it is as simple as changing the above line to the following.
cell.pointsLabel.text = [NSString stringWithFormat:@"%@ points", points];
lldb is a very flexible and versatile debugger, this article didn't even scratch the surface of what lldb can do. As always, in order to learn more about lldb refer to lldb documentation. Another great resource is apple iOS debugging magic article that can be found here. Also, objc.io last article Dancing in the Debugger — A Waltz with LLDB has great information on how to use lldb.