Takeaways from Advanced Debugging with Xcode and LLDB
I found this talk from WWDC 2018 very enlightening and wanted to capture notes from it easier reference. The existence of this document should not preclude one from watching this fantastic presentation!
LLDB Magic
A key theme from this talk is how to avoid changing code (and thus recompiling) for debugging purposes or testing changes. This is done primarily with breakpoints that automatically continue after running their actions in conjunction with the expression
lldb command (and various aliases thereof).
expression
executes an arbitrary target language expression in the context of the current stack frame and thread. help expression
in lldb gives more, and more precise, information.
For instance, to always set a variable to a particular value, regardless of input
- Create a breakpoint at a place where you need the variable to be that value
- Edit the breakpoint and add an action of
expression [variable] = [value]
- Check
Automatically continue after evaluating actions
This general technique can also be used to try out code changes without recompiling. If you want to replace code first create a breakpoint, add the expression
actions with the new code, and add an action to bump the instruction pointer past the old code (thread jump --by n
, where n
is the number of lines of code) 1.
Symbolic breakpoints
When a symbolic breakpoint is created it should have at least one child breakpoint -- child breakpoints show that the debugger found (a) matching symbol(s) in the program. No child breakpoints means the debugger found no matching symbols and will never be triggered.
When a symbolic breakpoint is triggered you can inspect the arguments passed in by inspecting the appropriate registers, which depends on your target architecture 2. More simply though, if you are in an objective-c method you can use lldb's built-in pseudo-registers: $arg1, $arg2, $arg3,... $argn. (e.g., poc $arg1
prints out the object the message was passed to)
In Objective-C, the first argument ($arg1) is the object the message is being sent to, the second ($arg2) is the selector (you may need to cast it as type SEL), and the rest are the method/function parameters.
Temporary breakpoints
Sometimes you want to hit a breakpoint based on some condition or action taken 3, but for some reason you can't use the 'Conditon' field in the breakpoint editor 4. You can set temporary breakpoints using an action. The --one-shot true
(or -o true
) argument to the breakpoint
command will create a breakpoint that is removed when it's hit.
Object descriptions
Overview of lldb print commands:
po <expression>
: alias ofexpression --object-description --
(--object-description
is equivalent to-O
). Prints out the custom debugDescription ofexpression
s result if the object conforms toCustomDebugStringConvertable
p <expression>
: alias ofexpression --
. Uses lldb built-in formattersframe variable <name>
Reads value ofname
from memory, uses lldb's formatters, doesn't evaluate an expression
expression -l objc -O -- <objective-c expression>
will evaluate the expression in the context of objective-c, which can be necessary doing something more dynamic than swift allows. This pattern is fairly useful but onerous to type every time, so it can be aliased to, say, poc
with command alias poc expresson -l objc -O --
poc
can be used to dereference a pointer address and print the description of the object at that address.
A way to do almost the same thing in Swift is to use unsafeBitCast
like so: po unsafeBitCast(0x1234abcde, to: ClassName.self)
.
Unlike the raw address in poc 0x1234abcde
, unsafeBitCast(0x1234abcde, to: ClassName.self)
returns a typed result, so the properties of the object at 0x1234abcde
can be inspected. In the demo, the object is a ScoreboardView
so you can do things like po unsafeBitCast(0x7fb3[...], to: ScoreboardView.self).center
to get the value of the the center CGPoint or po unsafeBitCast(0x7fb3[...], to: ScoreboardView.self).center.y = 300
to move the center of the view.
View debugging
After you move views around, you can update the screen (while the app is paused!) by flushing the current CATransaction with expression CATransaction.flush()
-- this is a handy command, so it's a good idea to alias it.
Check out the "nudge.py" lldb command, downloadable from https://developer.apple.com/sample-code/wwdc/2018/UseScriptsToAddCustomCommandsToLLDB.zip Adding this to your lldb session will allow you to "nudge" views around while execution is paused and see the result immediately.
Malloc stack logging - set to "All Allocation and Free History". This will allow you to see where any allocated instance in either the view debugger or memory graph was allocated using the "Backtrace" inspector.
⌘-click allows you to click "through" views in the view debugger, in case you want to click a view that is not highest in the z-plane.
In addition to using the view debugger to find a particular view, you can call -[UIView recursiveDescription] on a view -- you have to use the objective-c message passing syntax, not the swift method call. This is because recursiveDescription
is for debugging only and not part of the public UIView API which means it isn't passed through to swift. You can call using objective-c by using the poc
alias mentioned previously, like so: poc [`self.view` recursiveDescription]
The backticks around self.view
are there because evaluating an expression in objective-c creates a new context which doesn't contain all of our variables. The backticks tell expression to evaluate whatever is between them in the current context and replace them with the result.
Misc
Create a "Debug" tab automatically on pausing at a breakpoint by setting up a behavior (not Xcode 10 specific).
e
is a short alias ofexpression
.To permanently add aliases to your lldb sessions, put the command to create them in your ~/.lldbinit file. You can also use this to load custom lldb commands written in python with
command script import [path-to-script]
.Watchpoints are useful for finding where a particular variable is modified.
Further Reading
Some more Readings in Advanced LLDB:
Objc.io - Dancing in the Debugger — A Waltz with LLDB
Radoslaw Cieciwa - How improve your skills in lldb debugging
Sozin's Comet (SO comment) - Setting a register value to NSString in lldb
Daniel Martin - How to Extend LLDB to Provide a Better Debugging Experience
The LLDB project doesn't have a comprehensive list of lldb commands (the documentation in general is fairly poor) but this map of GDB to LLDB commands gives an overview of some of them.
If you just want to move the instruction pointer once, grab its handle in the IDE (it's the green label on right of the editor when paused at a breakpoint) and drag it.⏎
Ray Wenderlich has a tutorial covering register calling conventions.⏎
For instance, you have a label that can be updated due to several things happening but you only want to breakpoint when one of those several ways occurs. You can create a breakpoint with a
breakpoint set -o true -f <file name> -l <line number>
action that is hit when the thing occurs, where<file name>
and<line number>
correspond to the code what updates the label.⏎Perhaps the variable isn't in scope or there is no variable associated with the action triggering the breakpoint.⏎