Back to Journal List
nangege


Have you ever been so frustrated with understanding your code that you tried to print the value of a variable?

NSLog(@"%@", whatIsInsideThisThing);
Copy the code

Or skip a function call to simplify the program’s behavior?

NSNumber *n = @7; // This function should actually be called: Foo();Copy the code

Or short-circuit a logic check?

if (1 || theBooleanAtStake) { ... }
Copy the code

Or fake a function implementation?

int calculateTheTrickyValue { return 9; /* For now... }Copy the code

And every time you have to recompile, from scratch?

Building software is complex, and bugs are always present. A common fix cycle is to modify the code, compile it, re-run it, and hope for the best.

But it doesn’t have to be that way. You can use a debugger. And even if you already know how to use the debugger to examine variables, it can do a lot more.

This article will try to challenge what you know about debugging, explain in detail some of the fundamentals you may not know yet, and then show a series of interesting examples. Now let’s begin a waltz with the debugger and see where we end up.

LLDB

LLDB is an open source debugger with REPL features and C++,Python plug-ins. The LLDB binding is within Xcode and exists in the console at the bottom of the main window. The debugger allows you to pause a program at a particular point in its run. You can view the values of variables, execute custom instructions, and follow the program’s progress as you see fit. (Here’s a general explanation of how the debugger works.)

Chances are you’ve used the debugger before, even if it’s just adding breakpoints to Xcode’s interface. But with a few tricks, you can do some really cool things. The GDB to LLDB reference is a very good overview of the commands available to the debugger. You can also install Chisel, which is an open source collection of LLDB plug-ins, which makes debugging even more fun.

In the meantime, let’s start our journey by printing variables in the debugger.

basis

Here’s a simple little program that prints a string. Notice that the breakpoint has been added on line 8. Breakpoints can be created by clicking on the side slot of Xcode’s source window.

The program stops running at this line and the console opens, allowing us to interact with the debugger. So what should we type?

help

The simplest command is help, which lists all the commands. If you forget what a command does, or want to know more, you can use help to learn more details, such as help print or Help Thread. If you’ve even forgotten what the help command does, you can try help help. But if you know how to do this, you probably haven’t forgotten it all yet. 😛

print

Printing values is easy; Just try the print command:

LLDB actually does prefix matching. So you can also use prin, pri, or P. But you can’t use PR, because LLDB doesn’t disambiguate process (luckily p doesn’t).

You might also notice that there’s a $0 in there. You can actually use it to point to this result. Try print $0 + 7 and you’ll see 106. Anything that starts with a dollar character exists in the LLDB namespace to help you debug.

expression

What if I want to change a value? You might guess modify. In this case, expression is the convenient command.

Not only does this change the value in the debugger, it actually changes the value in the program. Continuing with the program at this point will print 42 red Balloons. Magic.

Notice that from now on, we’ll be lazy and replace print and expression with p and e, respectively.

What is theprintThe command

Consider an interesting expression: p count = 18. If we run this command, then print the contents of count. We will see that the result is the same as expression count = 18.

Unlike expression, the print command takes no arguments. For example, in e-h +17, it’s hard to tell if you’re just doing +17 with -h, or if you’re computing the difference between 17 and H. The hyphen is really confusing, and you might not get what you want.

Fortunately, the solution is simple. Use — to indicate the end of the identifier and the beginning of the input. If you want -h as the identifier, you use e-h — +17, and if you want to calculate the difference, you use e — -h +17. Because you don’t usually use identifiers, there’s a shorthand for e –, which is print.

Type Help print and scroll down to see:

'print' is an abbreviation for 'expression --'.Copy the code

Print the object

Try to enter

p objects
Copy the code

The output will be a bit verbose

(NSString *) $7 = 0x0000000104da4040 @"red balloons"
Copy the code

If we tried to print objects with more complex structures, the results would be even worse

(lldb) p @[ @"foo", @"bar" ]

(NSArray *) $8 = 0x00007fdb9b71b3e0 @"2 objects" 
Copy the code

In fact, what we want to see is the result of the object’s description method. We need to use the -o flag (the letter O, not the number 0) to tell expression to print the result as an Object.

(lldb) e -O -- $8
<__NSArrayI 0x7fdb9b71b3e0>(
foo,
bar
)
Copy the code

Luckily, e-o — has a separate name, Po (short for print Object), which we can use to simplify things:

(lldb) po $8
<__NSArrayI 0x7fdb9b71b3e0>(
foo,
bar
)
(lldb) po @"lunar"
lunar
(lldb) p @"lunar"
(NSString *) $13 = 0x00007fdb9d0003b0 @"lunar"
Copy the code

Print the variable

You can specify different print formats for print. They are all written in print/< FMT > or simplified p/< FMT > format. Here are some examples:

Default format

(lldb) p 16
16
Copy the code

Hexadecimal:

(lldb) p/x 16
0x10
Copy the code

Binary (t stands for two) :

(lldb) p/t 16
0b00000000000000000000000000010000
(lldb) p/t (char)16
0b00010000
Copy the code

You can also use p/c to print characters, or p/s to print null-terminated strings. Here is the full list of formats.

variable

Now you can print objects and simple types and know how to modify them in the debugger using the expression command. Now let’s use some variables to reduce the amount of input. Just as you can declare a variable with int a = 0 in C, you can do the same thing in LLDB. However, in order to use a declared variable, the variable must begin with a dollar character.

(lldb) e int $a = 2
(lldb) p $a * 19
38
(lldb) e NSArray *$array = @[ @"Saturday", @"Sunday", @"Monday" ]
(lldb) p [$array count]
2
(lldb) po [[$array objectAtIndex:0] uppercaseString]
SATURDAY
(lldb) p [[$array objectAtIndex:$a] characterAtIndex:0]
error: no known method '-characterAtIndex:'; cast the message send to the method's return type
error: 1 errors parsing expression
Copy the code

Unfortunately, the LLDB cannot determine the type involved. This kind of thing happens all the time, and it would be nice to note:

(lldb) p (char)[[$array objectAtIndex:$a] characterAtIndex:0]
'M'
(lldb) p/d (char)[[$array objectAtIndex:$a] characterAtIndex:0]
77
Copy the code

Variables make the debugger much easier to use, guess what? 😉

Process control

When you insert a breakpoint through the side slot of Xcode’s source editor (or by following the method below), the program will stop running when it reaches the breakpoint.

The debug bar displays four buttons that you can use to control the flow of your application.

From left to right, the four buttons are: Continue, Step over, Step Into, and Step Out.

The first, the Continue button, unsuspends the program and allows it to execute normally (either through or to the next breakpoint). In LLDB, you can achieve the same effect using the process continue command, which is alias continue, or can be abbreviated to C.

The second, the Step over button, executes a line of code as a black box. If this line of code is a function call, instead of jumping into the function, it executes the function and continues. For LLDB, you can use thread step-over, next, or n commands.

If you really want to jump into a function call to debug or check the execution of the program, use the third button, step in, or use thread step in, step, or s commands in the LLDB. Note that when the current line is not a function call, next and step have the same effect.

Most people know about C, N and S, but there’s actually a fourth button, step out. If you’ve ever accidentally jumped into a function, but you actually want to skip it, a common reaction is to repeatedly run n until the function returns. In this case, the Step out button is your savior. It continues to the next return statement (until the end of a stack frame) and then stops again.

example

Consider the following program:

Suppose we run the program, stop it at a breakpoint, and execute the following list of commands:

p i
n
s
p i
finish
p i
frame info
Copy the code

Here, frame Info tells you the current line count and the source file, among other things; See Help Frames, Help Threads, and Help Processes for more information. What is the result of this sequence of commands? Please think before you read the answer.

(lldb) p i
(int) $0 = 99
(lldb) n
2014-11-22 10:49:26.445 DebuggerDance[60182:4832768] 101 is odd!
(lldb) s
(lldb) p i
(int) $2 = 110
(lldb) finish
2014-11-22 10:49:35.978 DebuggerDance[60182:4832768] 110 is even!
(lldb) p i
(int) $4 = 99
(lldb) frame info
frame #0: 0x000000010a53bcd4 DebuggerDance`main + 68 at main.m:17
Copy the code

The reason it remains on line 17 is that the finish command runs until the return of isEven() and then stops immediately. Notice that even though it’s still on line 17, this line has already been executed.

Thread Return

There is also a great function for controlling the flow of your program during debugging: Thread Return. It takes an optional argument, and when executed it loads the optional argument into the return register, and then immediately executes the return command to exit the current stack frame. This means that the rest of the function will not be executed. This can cause some problems with ARC reference counting, or it can invalidate the cleanup part of the function. But executing this command at the beginning of a function is a great way to isolate the function and falsify the return value.

Let’s modify the above code snippet slightly and run it:

p i
s
thread return NO
n
p even0
frame info
Copy the code

Think before you look at the answer. Here are the answers:

(lldb) p i (int) $0 = 99 (lldb) s (lldb) thread return NO (lldb) n (lldb) p even0 (BOOL) $2 = NO (lldb) frame info frame  #0: 0x00000001009a5cc4 DebuggerDance`main + 52 at main.m:17Copy the code

The breakpoint

We all use breakpoints as a way to stop programs, check the current state, and track bugs. But if we change the way we interact with breakpoints, a lot of things become possible.

Breakpoints allow you to control when the program stops and then allow the command to run.

Imagine placing a breakpoint at the beginning of a function, overriding the behavior of the function with the Thread Return command, and continuing. Imagine automating the process. Sounds good, doesn’t it?

Manage the breakpoint

Xcode provides a number of tools to create and manage breakpoints. We’ll look at the LLDB equivalents one by one (yes, you can add breakpoints inside the debugger).

In the left pane of Xcode, there is a set of buttons. One of them looks like a breakpoint. Click on it to open the breakpoints navigation, a panel that allows you to quickly manage all breakpoints.

Here you can see all the breakpoints – do the same in LLDB with the breakpoint list (or br li) command. You can also click on a single breakpoint to turn it on or off – use breakpoint enable

and breakpoint disable

in LLDB:

(lldb) br li Current breakpoints: 1: file = '/Users/arig/Desktop/DebuggerDance/DebuggerDance/main.m', line = 16, locations = 1, resolved = 1, Hit count = 1 where = DebuggerDance`main + 27 at main.m:16, address = 0x000000010a3f6cab, resolved, hit count = 1 (lldb) br dis 1 1 breakpoints disabled. (lldb) br li Current breakpoints: 1: The file = '/ Users/arig/Desktop/DebuggerDance/DebuggerDance/main m', the line = 16, locations = 1 Options: disabled 1.1: where = DebuggerDance`main + 27 at main.m:16, address = 0x000000010a3f6cab, unresolved, hit count = 1 (lldb) br del 1 1 breakpoints deleted; 0 breakpoint locations disabled. (lldb) br li No breakpoints currently set.Copy the code

Create a breakpoint

In the example above, we created the breakpoint by clicking on slot 16 of the source page viewer. You can remove breakpoints by dragging them out of the slot and then releasing the mouse (there is a cute poof animation when they disappear). You can also select breakpoints from the Breakpoints navigation page and press the Delete key to delete them.

To create breakpoints in the debugger, use the breakpoint set command.

(lldb) breakpoint set -f main.m -l 16
Breakpoint 1: where = DebuggerDance`main + 27 at main.m:16, address = 0x000000010a3f6cab
Copy the code

You can also use the abbreviated form BR. Although b is a completely different command (short for _regexp-break), it does exactly the same thing.

(lldb) b main.m:17
Breakpoint 2: where = DebuggerDance`main + 52 at main.m:17, address = 0x000000010a3f6cc4
Copy the code

It is also possible to create breakpoints on a symbol (a C language function) without specifying a line at all

(lldb) b isEven
Breakpoint 3: where = DebuggerDance`isEven + 16 at main.m:4, address = 0x000000010a3f6d00
(lldb) br s -F isEven
Breakpoint 4: where = DebuggerDance`isEven + 16 at main.m:4, address = 0x000000010a3f6d00
Copy the code

These breakpoints will stop exactly at the beginning of the function. Objective-c methods also work perfectly:

(lldb) breakpoint set -F "-[NSArray objectAtIndex:]"
Breakpoint 5: where = CoreFoundation`-[NSArray objectAtIndex:], address = 0x000000010ac7a950
(lldb) b -[NSArray objectAtIndex:]
Breakpoint 6: where = CoreFoundation`-[NSArray objectAtIndex:], address = 0x000000010ac7a950
(lldb) breakpoint set -F "+[NSSet setWithObject:]"
Breakpoint 7: where = CoreFoundation`+[NSSet setWithObject:], address = 0x000000010abd3820
(lldb) b +[NSSet setWithObject:]
Breakpoint 8: where = CoreFoundation`+[NSSet setWithObject:], address = 0x000000010abd3820
Copy the code

To create symbolic breakpoints on Xcode’s UI, you can click the + button to the left of the breakpoints bar.

Then select the third option:

A pop-up box will appear where you can add symbolic breakpoints such as -[NSArray objectAtIndex:]. So every time you call this function, it stops, whether you call it or Apple calls it.

If you right-click on any Breakpoint in Xcode’s UI and select “Edit Breakpoint”, there are some very tempting options.

Here, the breakpoint has been changed to stop only when I is 99. You can also use the “ignore” option to tell the breakpoint not to stop for the first n calls (when the condition is true).

Next, the ‘Add Action’ button…

Breakpoint Behavior (Action)

In the example above, you might want to know the value of I every time you hit a breakpoint. We can use PI I as the breakpoint behavior. This command is automatically run every time a breakpoint is reached.

You can also add multiple behaviors, be it debugger commands, shell commands, or more direct printing:

You can see it prints I, then says the sentence out loud, and then prints the custom expression.

Here’s what it looks like when you do this in the LLDB UI instead of Xcode.

(lldb) breakpoint set -F isEven Breakpoint 1: where = DebuggerDance`isEven + 16 at main.m:4, address = 0x00000001083b5d00 (lldb) breakpoint modify -c 'i == 99' 1 (lldb) breakpoint command add 1 Enter your debugger  command(s). Type 'DONE' to end. > p i > DONE (lldb) br li 1 1: Name = 'isEven', locations = 1, resolved = 1, hit count = 0 Breakpoint commands: p I Condition: I == 99 where = DebuggerDance`isEven + 16 at main.m:4, address = 0x00000001083b5d00, resolved, hit count = 0Copy the code

Now automation.

Assign and continue

If you look at the bottom of the Edit Breakpoints pop-up, you will also see an option: “Automatically continue after evaluation actions.” It’s just a selection box, but it’s powerful. Select it, and the debugger will run all your commands, then continue. It looks as if no breakpoints were executed (unless there are so many breakpoints that it takes a while to run and slows down your program).

This box has the same effect as making the last behavior of the last breakpoint continue. Checkboxes just make it easier. The debugger output is:

(lldb) breakpoint set -F isEven Breakpoint 1: where = DebuggerDance`isEven + 16 at main.m:4, address = 0x00000001083b5d00 (lldb) breakpoint command add 1 Enter your debugger command(s). Type 'DONE' to end. > continue > DONE (lldb) br li 1 1: name = 'isEven', locations = 1, resolved = 1, hit count = 0 Breakpoint commands: Continue 1.1: WHERE = DebuggerDance 'isEven + 16 at main.m:4, address = 0x00000001083b5d00, Resolved, hit Count = 0Copy the code

Breakpoints continue automatically, allowing you to modify the program entirely through breakpoints! You can stop on a line, run an expression command to change the variable, and then continue running.

example

Think of a technique called “print debugging”. Don’t do this:

NSLog(@"%@", whatIsInsideThisThing);
Copy the code

Instead, replace the log statement with a breakpoint that prints the variable and continue.

Don’t:

int calculateTheTrickyValue {
  return 9;

  /*
   Figure this out later.
   ...
}
Copy the code

Instead, add a breakpoint using thread Return 9 and let it continue.

Symbolic breakpoints plus action are really powerful. You can also add breakpoints to your friend’s Xcode project and add actions to read something out loud. See how long it takes them to figure out what happened. 😄

Runs completely inside the debugger

Before we start dancing, there’s one more thing to see. You can actually execute any C/Objective-C/C++/Swift command in the debugger. The only drawback is that you can’t create new functions… This means you can’t create new classes, blocks, functions, C++ classes with virtual functions, and so on. It can do everything else.

We can request to allocate some bytes:

(lldb) e char *$str = (char *)malloc(8)
(lldb) e (void)strcpy($str, "munkeys")
(lldb) e $str[1] = 'o'
(char) $0 = 'o'
(lldb) p $str
(char *) $str = 0x00007fd04a900040 "monkeys"
Copy the code

We can look in memory (using the x command) to see four bytes in the new array:

(lldb) x/4c $str
0x7fd04a900040: monk
Copy the code

We can also remove 3 bytes (the x command requires oblique quotes because it has only one memory address argument, not an expression; Use help X for more information) :

(lldb) x/1w `$str + 3`
0x7fd04a900043: keys
Copy the code

When you’re done, be sure to free the memory so it doesn’t leak. (Ha, although this is the memory used by the debugger) :

(lldb) e (void)free($str)
Copy the code

Let’s dance

Now that we know the basic pace, it’s time to start dancing and do some crazy things. I wrote a blog about NSArray in depth. This blog post uses a lot of NSLog statements, but virtually all of my exploration is done inside the debugger. See if you can figure out how to do that, it’ll be a fun exercise.

Debugging without breakpoints

When the program is running, Xcode’s debug bar will display a pause button instead of a continue button:

Clicking the button suspends the app (this runs the process Interrupt command because the LLDB is always running behind it). This gives you access to the debugger, but there doesn’t seem to be much to do because there are no variables in the current scope and no specific code for you to look at.

This is where it gets interesting. If you’re running an iOS app, you can try this: (because global variables are accessible)

(lldb) po [[[UIApplication sharedApplication] keyWindow] recursiveDescription] <UIWindow: 0x7f82b1fa8140; frame = (0 0; 320, 568); gestureRecognizers = <NSArray: 0x7f82b1fa92d0>; layer = <UIWindowLayer: 0x7f82b1fa8400>> | <UIView: 0x7f82b1d01fd0; frame = (0 0; 320, 568); autoresize = W+H; layer = <CALayer: 0x7f82b1e2e0a0>>Copy the code

You can see the whole hierarchy. This is how pviews are implemented in Chisel.

Update the UI

With the output above, we can get this view:

(lldb) e id $myView = (id)0x7f82b1d01fd0
Copy the code

Then change its background color in the debugger:

(lldb) e (void)[$myView setBackgroundColor:[UIColor blueColor]]
Copy the code

But you won’t see the interface change until the program continues running. Because the changed content must be sent to the render service before the display is updated.

The render service is actually a separate process (called backboardd). This means that backboardd continues to run even if the process in which we are debugging is interrupted.

This means you can run the following command without continuing the program:

(lldb) e (void)[CATransaction flush]
Copy the code

Even if you’re still in the debugger, the UI will be updated in real time in the emulator or on the real machine. Chisel provides an alias for this called caflush, and this command is used to implement other shortcut commands such as hide

, show

and many others. All Chisel commands are documented, so feel free to run help show after installation to see more.

Push a View Controller

Imagine an application with UINavigationController as root ViewController. You can easily obtain it by using the following command:

(lldb) e id $nvc = [[[UIApplication sharedApplication] keyWindow] rootViewController]
Copy the code

Then push a Child View controller:

(lldb) e id $vc = [UIViewController new] (lldb) e (void)[[$vc view] setBackgroundColor:[UIColor yellowColor]] (lldb) e (void)[$vc setTitle:@"Yay!"]  (lldb) e (void)[$nvc pushViewContoller:$vc animated:YES]Copy the code

Finally, run the following command:

(lldb) caflush // e (void)[CATransaction flush]
Copy the code

The Navigation Controller will be pushed right in front of you.

Find the target of the button

Imagine you have a $myButton variable in the debugger, either created, pulled from the UI, or a local variable when you stop at a breakpoint. You want to know who will receive the action when the button is pressed. Very simple:

(lldb) po [$myButton allTargets]
{(
    <MagicEventListener: 0x7fb58bd2e240>
)}
(lldb) po [$myButton actionsForTarget:(id)0x7fb58bd2e240 forControlEvent:0]
<__NSArrayM 0x7fb58bd2aa40>(
_handleTap:
)
Copy the code

Now you might want to add a break point when it happens. Set a symbolic breakpoint on -[MagicEventListener _handleTap:], either in Xcode or LLDB, and then click the button and stop where you want.

Observe changes in instance variables

Suppose you have a UIView and for some reason its _layer instance variable has been overwritten (oops). We cannot use symbolic breakpoints because methods may not be involved. Instead, we want to monitor when the address is written.

First, we need to find the relative position of the _layer variable on the object:

(lldb) p (ptrdiff_t)ivar_getOffset((struct Ivar *)class_getInstanceVariable([MyView class], "_layer"))
(ptrdiff_t) $0 = 8
Copy the code

Now we know that ($myView + 8) is the memory address being written:

(lldb) watchpoint set expression -- (int *)$myView + 8
Watchpoint created: Watchpoint 3: addr = 0x7fa554231340 size = 8 state = enabled type = w
    new value: 0x0000000000000000
Copy the code

This is added to Chisel with wivar $myView _layer.

Symbolic breakpoints for non-overridden methods

Suppose you want to know when -[MyViewController viewDidAppear:] is called. What if this method is not implemented in MyViewController, but in its parent class? If you try to set a breakpoint, the following results appear:

(lldb) b -[MyViewController viewDidAppear:]
Breakpoint 1: no locations (pending).
WARNING:  Unable to resolve breakpoint to any actual locations.
Copy the code

Because the LLDB looks for a symbol that is not actually found on the class, the breakpoint will never fire. All you need to do is set a condition for the breakpoint [self isKindOfClass:[MyViewController Class]] and then put the breakpoint on UIViewController. Normally this setting of a condition will work. But not here, because we don’t have an implementation of the superclass.

ViewDidAppear: is the method apple implements, so it doesn’t have its notation; There’s no self inside the method. If you want to use self on a symbol breakpoint, you have to know where it is (it could be in a register or on the stack; On x86, you can find it at $ESP +4). But this is painful, because now you have to know at least four architectures (x86, x86-64, ARMV7, armV64). Imagine how much time it would take you to learn the command set and the calling conventions for each of them, and then write a command that sets breakpoints on your superclass with the right conditions. Fortunately, this was taken care of in Chisel. This is called a bmessage:

(lldb) bmessage -[MyViewController viewDidAppear:]
Setting a breakpoint at -[UIViewController viewDidAppear:] with condition (void*)object_getClass((id)$rdi) == 0x000000010e2f4d28
Breakpoint 1: where = UIKit`-[UIViewController viewDidAppear:], address = 0x000000010e11533c
Copy the code

LLDB and Python

LLDB has built-in, full Python support. Typing script in LLDB opens a Python REPL. You can also enter a line of Python statements as arguments to a script command, which can run Python statements without entering the REPL:

(lldb) script import os
(lldb) script os.system("open http://www.objc.io/")
Copy the code

This allows you to create all sorts of cool commands. Put the following statement in the file ~/myCommands.py:

def caflushCommand(debugger, command, result, internal_dict):
  debugger.HandleCommand("e (void)[CATransaction flush]")
Copy the code

Then run in LLDB:

command script import ~/myCommands.py
Copy the code

Or place this command in /.lldbinit so that it is automatically run every time you enter the LLDB. Chisel is simply a collection of Python scripts that concatenate (command) strings and then have the LLDB execute them. Simple, isn’t it?

Grasp the debugger as a weapon

The LLDB can do a lot of things. Most people are used to using P, Po, N, S, and C, but LLDB can actually do a lot more than that. Knowing all the commands (there aren’t many of them) gives you more power when it comes to revealing the state of your code at runtime, finding bugs, and enforcing specific runtime paths. You can even build simple interaction prototypes – like what if a View Controller popped up modally right now? With the debugger, just try it.

The purpose of this article is to show you the power of LLDB and encourage you to explore typing commands on the console.

Open LLDB and type help to take a look at the listed commands. How many have you tried? How many?

Hopefully, NSLog doesn’t look as attractive to use anymore, and running it again every time you edit it isn’t fun or time-consuming.

Happy debugging!


Dancing in the Debugger — A Waltz with LLDB

nangege

I’m a programmer now, and I want to be a designer who can write code. Like simple things.


OneV’s Den
ObjC China



@onevcat