Chapters

Hide chapters

Advanced Apple Debugging & Reverse Engineering

Fourth Edition · iOS 16, macOS 13.3 · Swift 5.8, Python 3 · Xcode 14

Section I: Beginning LLDB Commands

Section 1: 10 chapters
Show chapters Hide chapters

Section IV: Custom LLDB Commands

Section 4: 8 chapters
Show chapters Hide chapters

5. Expression
Written by Walter Tyree

Now that you’ve learned how to set breakpoints, it’s time to learn how to query and manipulate the software you’re debugging. In this chapter, you’ll learn about the expression command, which allows you to inspect variables and execute arbitrary code. This is kind of a big deal, because you can declare, initialize and inject code on the fly without recompiling your program!

Formatting With p and po

You might be familiar with the common debugging command, po. po is used to display information in your program or execute code. If lldb knows how to interpret the value you’ve po‘d, it will be able to interpret that value and display meaningful information to you. For example lldb can interpret: a C int, Objective-C NSObject or a Swift struct, a variable in source code, a register’s value, etc.

If you do a quick help po in the lldb console, you’ll find po is actually a shorthand expression for expression -O --. The -O argument is used to print the object’s description.

po’s often overlooked sibling, p, is another abbreviation with the -O option omitted, resulting in expression --. The format of what p will print out is dependent on the LLDB type system. LLDB’s type formatting helps determine a variable’s description in lldb and is fully customizable.

In the following sections, you’ll learn how to modify the output of both p and po to be more useful for your debugging needs.

You can influence the content of po in the source code of a debugged program. Likewise, one can control the formatting of what p displays via lldb’s options/public APIs.

Modifying an Object’s Description

In order to change how lldb displays an object using po, you needs to modify an object’s description in the source code. You will continue using the Signals project for this chapter.

Open the Signals project in Xcode. Open MainViewController.swift and add the following code above viewDidLoad():

override var description: String {
  return "Yay! debugging " + super.description
}

In viewDidLoad, add the following line of code below super.viewDidLoad():

print("\(self)")

Put a breakpoint just after the print method you created in the viewDidLoad() of MainViewController.swift. Do this using the Xcode GUI breakpoint side panel.

Build and run the application.

Once the Signals project stops at viewDidLoad(), type the following into the lldb console:

(lldb) po self

You’ll get output similar to the following:

Yay! debugging <Signals.MainViewController: 0x14851c520>

Take note of the output of the print statement and how it matches the po self you just executed in the debugger.

You can also take it a step further. NSObject has an additional method description used for debugging called debugDescription. Add the following below your description variable definition:

override var debugDescription: String {
  return "debugDescription: " + super.debugDescription
}

Build and run the application. When the debugger stops at the breakpoint, print self again:

(lldb) po self

The output from the lldb console will look similar to the following:

debugDescription: Yay! debugging <Signals.MainViewController: 0x15c608650>

Notice how the po self and the output of self from the print command now differ, since you implemented debugDescription. When you print an object from lldb, it’s debugDescription that gets called, rather than description. Neat!

Note: The description and debugDescription actually originate from Objective-C logic integrated into lldb. Swift does provide an elegant wrapper around this idea with a public protocol called CustomDebugStringConvertible, which requires the adopter to implement the debugDescription method. This protocol is only required for Swift classes that don’t explicitly inherit from NSObject (i.e. class A { }).

As you can see, having a description or debugDescription when working with an NSObject class or subclass will influence the output of po.

Note: A surprisingly easy and simple anti-debugging measure to frustrate script kiddies is to return an empty string for description and debugDescription. r/jailbreakdeveloperss won’t know what hit ’em. :]

So which NSObject classes override these description methods? Using image lookup command from the previous chapter can answer this. Your learnings from previous chapters are already coming in handy!

If, say, you wanted to know all the Objective-C classes that override debugDescription, you can query all the methods with:

(lldb) image lookup -rn '\ debugDescription\]'

Based upon the output, it seems the authors of the Foundation framework have added the debugDescription to a lot of foundation types like NSArray, to make our debugging lives easier. In addition, there are also private classes that have overridden debugDescription methods as well.

You may notice one of them in the listing is CALayer. This a public class used for rendering 2D UI on iOS, and performing efficient animations. Take a look at the difference between description and debugDescription in CALayer.

In your lldb console, type the following:

(lldb) po self.view!.layer.description

You’ll see something similar to the following:

"<CALayer: 0x600002e9eb00>"

That’s a little boring. Now type the following:

(lldb) po self.view!.layer

You’ll see something similar to the following:

<CALayer:0x600001829a80; name = "VC:Signals.MainViewController"; position = CGPoint (195 422); bounds = CGRect (0 0; 390 844); delegate = <UITableView: 0x131065400; frame = (0 0; 390 844); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x600001608960>; layer = <CALayer: 0x600001829a80>; contentOffset: {0, 0}; contentSize: {0, 0}; adjustedContentInset: {0, 0, 0, 0}; dataSource: Yay! debugging <Signals.MainViewController: 0x132020a70>>; sublayers = (<CALayer: 0x600001829d40>, <CALayer: 0x600001829d80>); masksToBounds = YES; allowsGroupOpacity = YES; name = VC:Signals.MainViewController; backgroundColor = <CGColor 0x600003c56dc0> [<CGColorSpace 0x600003c721c0> (kCGColorSpaceICCBased; kCGColorSpaceModelRGB; sRGB IEC61966-2.1; extended range)] ( 0.980392 0.980392 0.980392 1 )>

That’s much more interesting — and much more useful! Obviously the developers of Core Animation decided the plain description should be just the object reference, but if you’re in the debugger, you’ll want to see more information.

Next, while you’re still stopped in the debugger (and if not, get back to the viewDidLoad() breakpoint), execute the p command on self, like so:

(lldb) p self

You’ll get something similar to the truncated output below:

Signals.MainViewController) $R2 = 0x0000000132020a70 {
  UIKit.UITableViewController = {
    baseUIViewController@0 = {
      baseUIResponder@0 = {
        baseNSObject@0 = {
          isa = Signals.MainViewController
        }
      }
      _overrideTransitioningDelegate = 0x0000000000000000
      _view = some {
        some = 0x0000000131065400 {
          baseUIScrollView@0 = {
...

This will dump the full internals of the MainViewController instance along with all the internal variables from a UIViewController. This might look scary, but break it down.

First, lldb spits out the class name of self. In this case, Signals.MainViewController.

Next follows a reference ($R2) which you can use to refer to this object from now on within this lldb session. Yours will vary as this is a number lldb increments each time you use p or po. This reference is useful if you ever want to get back to this object later in the session, perhaps when you’re in a different scope and self is no longer the same object. In that case, you can refer back to this object as $R2. To see how, type the following:

(lldb) p $R2

You’ll see the same information printed out again. You’ll learn more about these lldb variables later in this chapter.

After the $R2 variable name is the address to this object in memory, followed by some output specific to this type of class. In this case, it shows the details relevant to UITableViewController, which is the superclass of MainViewController. The internal variables of UITableViewController are displayed as well in a higher indentation level.

Note: Normally, internal variables of a class are hidden from end users when you don’t have the source code. However, the nice thing about Objective-C is lldb (and other reverse engineering tools) can extract this private information to rebuild the class’s internal layout. The same couldn’t be said for a C struct whose names have been removed from a binary.

As you can see, the meat of the output of the p command is different to the po command. The output of p is dependent upon type formatting: internal data structures the lldb authors have added to every (noteworthy) data structure in Objective-C, Swift, and other languages. It’s important to note the formatting for Swift is under active development with every LLVM release, so the output of p for MainViewController might be different for you when you try on your version of Xcode.

Fortunately, you have the power to change lldb’s default type formatters if you so desire. In your lldb session, type the following:

(lldb) type summary add Signals.MainViewController --summary-string "Wahoo!"

You’ve now told lldb you just want to return the static string, "Wahoo!", whenever you print out an instance of the MainViewController class. The Signals prefix is essential for Swift classes since Swift includes the module in the classname to prevent namespace collisions. Use lldb to print out self now:

(lldb) p self

The output should look similar to the following:

(lldb) (Signals.MainViewController) $R7 = 0x0000000132020a70 Wahoo!

This formatting will be remembered by lldb across app launches, so be sure to remove it when you’re done playing with the p command. This can be done via the following:

(lldb) type summary clear

Typing p self will now go back to the default implementation created by the lldb formatting authors. Type formatting is an extensive topic and the lldb authors provide a good resource describing how to use it.

Another way to get information about an object is to insert a dump command into your source code.

In MainViewController.swift, replace:

print("\(self)")

With the following:

dump(self)

Finally, build and run the app. The dump command prints the debugDescription of the object as well as data about its hierarchy and variables, kind of a mixture of what you get from print, po and p.

Swift vs Objective-C Debugging Contexts

It’s important to note there are two debugging contexts when debugging your program: a non-Swift debugging context and a Swift context. By default, when you stop in Objective-C code, lldb will use the non-Swift (Objective-C, C, C++) debugging context, and if you’re stopped in Swift code, lldb will use the Swift context. Sounds logical, right?

If you stop the debugger out of the blue (for example, if you click the process pause button in Xcode), lldb will choose the non-Swift context by default.

Make sure the GUI Swift breakpoint you’ve created in the previous section is still enabled and build and run the app. When the breakpoint hits, type the following into your LLDB session:

(lldb) po [UIApplication sharedApplication]

lldb will throw a cranky error at you:

error: <EXPR>:8:16: error: expected ',' separator
[UIApplication sharedApplication]
               ^
              ,

You’ve stopped in Swift code, so you’re in the Swift context. But you’re trying to execute Objective-C code. That won’t work. Similarly, in the Objective-C context, doing a po on a Swift object will not work.

You can force the expression to use the Objective-C context with the -l option to select the language. However, since the po expression is mapped to expression -O --, you’ll be unable to use the po command since the arguments you provide come after the --, which means you’ll have to type out the expression. In lldb, type the following:

(lldb) expression -l objc -O -- [UIApplication sharedApplication]

Here you’ve told lldb to use the objc language for Objective-C. You can also use objc++ for Objective-C++ if necessary.

lldb will now display the memory location of the shared application object. Try the same thing in Swift. Since you’re already stopped in the Swift context, try to print the UIApplication reference using Swift syntax, like so:

(lldb) po UIApplication.shared

You’ll get the same address as you did printing with the Objective-C context. Resume the program, by typing c or continue, then pause the Signals application out of the blue.

From there, press the up arrow to bring up the same Swift command you just executed and see what happens:

(lldb) po UIApplication.shared

Again, lldb will be cranky:

error: <user expression 2>:1:15: property 'shared' not found on object of type 'UIApplication'
UIApplication.shared

Remember, stopping out of the blue will put lldb in the Objective-C context. That’s why you’re getting this error when trying to execute Swift code.

You should always be aware of which language lldb expects when you are paused in the debugger.

User Defined Variables

As you saw earlier, lldb will automatically create local variables on your behalf when printing out objects. You can create your own variables as well.

Remove all the breakpoints from the program and build and run the app. Stop the debugger out of the blue so it defaults to the Objective-C context. From there type:

(lldb) po id test = [NSObject new]

lldb will execute this code, which creates a new NSObject and stores it to the test variable. Now, print the test variable in the console:

(lldb) po test

You’ll get an error like the following:

error: <user expression 4>:1:1: function 'test' with unknown type must be given a function type
test
^~~~

This is because you need to prepend variables you want lldb to remember with the $ character.

Declare test again with the $ in front:

(lldb) po id $test = [NSObject new]
(lldb) po $test

Now, lldb will happily display the memory location and type of your new object. This variable was created in the Objective-C object. But what happens if you try to access this from the Swift context? Try it, by typing the following:

(lldb) expression -l swift -O -- $test

So far so good. Now try executing a Swift-styled method on this Objective-C class.

(lldb) expression -l swift -O -- $test.description

You’ll get an error like this:

error: <EXPR>:3:1: error: cannot find '$test' in scope
$test
^~~~~

If you create an lldb variable in the Objective-C context, then move to the Swift context, don’t expect everything to “just work”, as a different context is used.

So how could creating references in lldb actually be used in a real life situation? You can grab the reference to an object and execute (as well as debug!) arbitrary methods of your choosing. To see this in action, create a symbolic breakpoint on MainViewController’s parent view controller, MainContainerViewController using an Xcode symbolic breakpoint for MainContainerViewController’s viewDidLoad.

In the Symbol section, type the following:

Signals.MainContainerViewController.viewDidLoad

Your breakpoint should look like the following:

Build and run the app. Notice that Xcode creates two breakpoints for your symbol. It will first stop on the @objc Signals.MainContainerViewController.viewDidLoad(). This is a little too early. Either click the continue button in Xcode or type c in the lldb window to continue execution. Xcode will now break on MainContainerViewController.viewDidLoad() in the swift context. From there, type the following:

(lldb) p self

Since this is the first argument you executed in the Swift debugging context, lldb will create the variable, $R0. Resume execution of the program by typing c or continue in LLDB.

Note: Remember in the last chapter when you saw the compiler creating synthesized getters and setters for your breakpoints? The same thing is happening here. There is an underlying Objective-C class because this code is using UIKit and MainContainerViewController is subclassing UIViewController. So, your symbol gets two matches. You can disable the @objc version in the Breakpoint navigator by clicking on the breakpoint icon or in the lldb console by typing breakpoint disable and supplying the id number. So, if the main breakpoint was ID 1, the two child breakpoints will be ID 1.1 and 1.2. Alternatively, just remember to continue when you stop at the Objective-C breakpoint.

Now you don’t have a reference to the instance of MainContainerViewController through the use of self since the execution has left viewDidLoad() and moved on to bigger and better run loop events.

Oh, wait, you still have that $R0 variable! You can now reference MainContainerViewController and even execute arbitrary methods to help debug your code.

Pause the app in the debugger manually, then type the following:

(lldb) po $R0.title

Unfortunately, you get:

error: use of undeclared identifier '$R0'

You stopped the debugger out of the blue! Remember, LLDB will default to Objective-C; you’ll need to use the -l option to stay in the Swift context:

(lldb) expression -O -l swift -- $R0.title

The output will be similar to the following:

▿ Optional<String>
  - some : "Quarterback"

Of course, this is the title of the view controller, shown in the navigation bar.

Now, type the following:

(lldb) expression -l swift -- $R0.title = "💩💩💩💩💩"

Resume the app by typing c or pressing the play button in Xcode.

Note: To quickly access a poop emoji on your macOS machine, press Command-Control-Space. From there, you can easily hunt down the correct emoji by searching for the phrase “poop.”

It’s the small things in life you cherish!

As you can see, you can easily manipulate variables as you wish.

In addition, you can also create a breakpoint on code, execute the code, and cause the breakpoint to be hit. This can be useful if you’re in the middle of debugging something and want to step through a function with certain inputs to see how it operates.

For example, you still have the symbolic breakpoint in viewDidLoad(), so try executing that method to inspect the code. Pause execution of the program, then type:

(lldb) expression -l swift -O -- $R0.viewDidLoad()

Nothing happened. The breakpoint didn’t hit. What gives? In fact, MainContainerViewController did execute the method, but by default, lldb will ignore any breakpoints when executing commands. You can disable this option with the -i option.

Type the following into your lldb session:

(lldb) expression -l swift -O -i 0 -- $R0.viewDidLoad()

lldb will now break on the viewDidLoad() symbolic breakpoint you created earlier. This tactic is a great way to test the logic of methods. For example, you can implement test-driven debugging, by giving a function different parameters to see how it handles different input. This is a great tactic when testing complicated conditional logic!

Code Injection

You’re not just limited to defining data in lldb. You can also create functions, classes, and methods on the fly through lldb! In order to persist these values, you’ll need to prepend a dollar sign to the code/class just like you did with the test variable earlier.

Here’s an example using the Swift context to create executable code:

(lldb) expression -l swift -- func $donothing() -> Int { return 4 }
(lldb) exp -l swift -- $donothing()
(Int) $R5 = 4

The above declares a Swift function called donothing() which returns the value 4. Upon inspecting this function, it looks perfectly valid for executable memory.

(lldb) exp -l swift -- $donothing
() $R6 = 0x0000000102e85770
(lldb) memory region 0x0000000102e85770
[0x0000000102e84000-0x0000000102e88000) r-x

Again, this is pretty cool. You can not only modify memory in existing code (just like with breakpoints), but you can inject executable code into an existing process. Let that idea simmer for a bit; you’ll use that knowledge in an upcoming chapter for interposing code…

Type Formatting

One of the nice options lldb has is the ability to format the output of basic data types. This makes lldb a great tool to learn how the compiler formats basic C types. This is a must to know when you’re exploring at the assembly level, which you’ll do later in this book.

First, remove the previous symbolic breakpoint. Next, build and run the app and pause the debugger out of the blue to make sure you’re in the Objective-C context.

Type the following into your lldb session:

(lldb) expression -G x -- 10

This -G option tells lldb what format you want the output in. The G stands for GDB format. If you’re not aware, GDB is the debugger that preceded lldb. This, therefore, is saying whatever you specify is a GDB format specifier. In this case, x is used which indicates hexadecimal.

You’ll see the following output:

(int) $0 = 0x0000000a

This is decimal 10 printed as hexadecimal. Wow!

But wait! There’s more! lldb lets you format types using a neat shorthand syntax. Type the following:

(lldb) p/x 10

You’ll see the same output as before. But that’s a lot less typing!

This is great for learning the representations behind C datatypes. For example, what’s the binary representation of the integer 10?

(lldb) p/t 10

The /t specifies binary format. You’ll see what decimal 10 looks like in binary. This can be particularly useful when you’re dealing with a bit field.

What about negative 10?

(lldb) p/t -10

Decimal 10 in two’s complement. Neat!

What about the floating point binary representation of 10.0?

(lldb) p/t 10.0

That could come in handy!

How about the ASCII value of the character ’D’?

(lldb) p/d 'D'

Ah so ’D’ is 68! The /d specifies decimal format.

Finally, what is the acronym hidden behind this integer?

(lldb) p/c 2051829580

The /c specifies char format. It takes the number in binary, splits into 8 bit (1 byte) chunks, and converts each chunk into an ASCII character. In this case, it’s a 4 character code (FourCC), saying LoLz. :]

The full list of output formats is below (taken from GDB online docs):

  • x: hexadecimal
  • d: decimal
  • u: unsigned decimal
  • o: octal
  • t: binary
  • a: address
  • c: character constant
  • f: float
  • s: string

If these formats aren’t enough for you, you can use lldb’s extra formatters, although you’ll be unable to use the GDB formatting syntax.

lldb’s formatters can be used like this:

(lldb) expression -f Y -- 2051829580

This gives you the following output:

(int) $0 = 4c 6f 4c 7a             LoLz

lldb uses these formatters (taken from the lldb online docs):

  • B: boolean
  • b: binary
  • y: bytes
  • Y: bytes with ASCII
  • c: character
  • C: printable character
  • F: complex float
  • s: c-string
  • i: decimal
  • E: enumeration
  • x: hex
  • f: float
  • o: octal
  • O: OSType
  • U: unicode16
  • u: unsigned decimal
  • p: pointer

Key Points

  • The po command, like Swift’s print function, allows you to view the description and debugDescription properties of objects.
  • The p command, like Swift’s dump function, gives you information about the internals of an object.
  • Variables in an lldb session begin with a $ and are valid for the entire session.
  • Switch between language contexts in lldb using expression -l <language> -O --.
  • When you pause the debugger using the button in Xcode you will probably be in an Objective-C context.
  • You can use the expression command to add functions and inject code into an application without recompiling.
  • expression supports GDB type formatters using -G as well as its own using -f.

Where to Go From Here?

Pat yourself on the back — this was another jam-packed round of what you can do with the expression command. Try exploring some of the other expression options yourself by executing help expression and see if you can figure out what they do.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.