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

9. Persisting & Customizing Commands
Written by Walter Tyree

With watchpoints covered, you now have an excellent foundation with the basic workflows within lldb, but there’s two problems that haven’t been addressed: persisting your lldb commands and creating shortcuts for them!

In this chapter, you’ll learn how to create simple, custom commands and save them for future lldb sessions!

Persisting… How?

Whenever lldb is invoked, it searches several directories for special initialization files. If found, these files will be loaded into lldb as soon as lldb launches but before lldb attaches to a process. This is an important detail if you’re trying to execute arbitrary code in the init file.

You can use these files to specify settings or create custom commands to do your debugging bidding.

lldb searches for an initialization file in the following places:

  1. ~/.lldbinit-[context] where [context] is Xcode, if you are debugging with Xcode, or lldb if you are using the command line incarnation of lldb.

    For example, if you wanted commands that were only available in lldb while debugging in the Terminal, you’d add content to ~/.lldbinit-lldb, while if you wanted to have commands only available to Xcode you’d use ~/.lldbinit-Xcode.

  2. Next, lldb searches for content found in ~/.lldbinit. This is the ideal file for most of your logic, since you want to use commands in both Xcode and Terminal sessions of lldb.

  3. Finally, lldb will search the directory where it was invoked. Unfortunately, when Xcode launches lldb, it’ll launch lldb at the / root directory. This isn’t an ideal place to stick an .lldbinit file, so this particular implementation will be ignored throughout the book.

Creating the .lldbinit File

In this section you’re going to create your first .lldbinit file.

First, open a Terminal window and type the following:

nano ~/.lldbinit

This uses the nano text editor to open up your .lldbinit file. If you already have an existing file in the location, nano will open the file instead of creating a new one.

Note: You really should be using some form of vi or emacs for editing .lldbinit, and then angrily blog about how unconventional the other editor is. I’m suggesting nano to stay out of the great debate.

Once the file is open, add the following line of code to the end of your .lldbinit file:

command alias -- Yay_Autolayout expression -l objc -O -- [[[[[UIApplication sharedApplication] keyWindow] rootViewController] view] recursiveDescription]

You’ve just created an alias — a shortcut command for a longer expression. It’s named Yay_Autolayout and it’ll execute an expression command to get the root UIView (iOS only) and dump the position and layout of the root view and all of its subviews.

Save your work by pressing Ctrl + O, but don’t exit nano just yet.

Open the Signals Xcode project — you know, the one you’ve been using throughout this section. Build and run the Signals application. Once running, pause execution and type the alias in the debugger:

(lldb) Yay_Autolayout

This will dump out all the views in the applications! Neat!

Note: The cool thing about this command is it’ll work equally well for apps you do — and don’t — have source code for. You could, hypothetically, attach lldb to the Simulator’s SpringBoard and dump all the views using the exact same method.

Now, use lldb to get help for this new command:

(lldb) help Yay_Autolayout

The output will look kinda meh. You can do better. Go back to the nano Terminal window and rewrite the command alias to include some helpful information, like so:

command alias -H "Yay_Autolayout will get the root view and recursively dump all the subviews and their frames" -h "Recursively dump views" -- Yay_Autolayout expression -l objc -O -- [[[[[UIApplication sharedApplication] keyWindow] rootViewController] view] recursiveDescription]

Make sure nano saves the file by pressing Ctrl + O. Next, build and run the Signals project.

Now when you stop the debugger and type help Yay_Autolayout, you’ll get help text at the bottom of the output. This is done with the -H command.

You can also get a brief summary by just typing help, which gives the -h description along with the rest of the commands.

Note: You also may see a lot of extra “help” text that you didn’t write. This appears to be a defect (feature?) in lldb’s help as it’s providing you with the help text for the command you’ve aliased in addition to what you specify with the -h and -H switches. For example, using the help command with the alias above, the entire help text for the expression command appears. Unfortunately, none of the options listed are usable because of how the alias is constructed…so defect? Creating an alias for breakpoint list however, the switches that help displays continue to work because they normally appear after the command anyway…so feature? The techniques you learn in later in the book to create more complex, custom commands will not have this behavior. They will only display the help text that you supply.

This may seem a bit pointless now, but when you have many, many custom commands in your .lldbinit file, you’ll be thankful you provided documentation for yourself.

Command Aliases With Arguments

You’ve just created a standalone command alias that doesn’t require any arguments. However, you’ll often want to create aliases to which you can supply input.

Go back to the nano window in Terminal. Add the following at the bottom of the file:

command alias cpo expression -l objc -O --

You’ve just created a new command called cpo. The cpo command will do a normal po (print object), but it’ll use the Objective-C context instead. This is an ideal command to use when you’re in a Swift context, but want to use Objective-C to print out an address or register of something you know is a valid Objective-C object.

Save your work in nano, and jump over to the Signals project. Navigate to MainViewController’s viewDidLoad and set a breakpoint at the top of the function. Build and run the application.

To best understand the importance of the cpo command, first get the reference to the MainViewController.

(lldb) po self

You’ll get output similar to the following:

<Signals.MainViewController: 0x15210c180>

Take the memory address you get at the end of the output (as usual, yours will likely be different), and try printing that in the debugger.

(lldb) po 0x15210c180

This will not produce any meaningful output, since you’ve stopped in a Swift file, and Swift is a type-safe language. Simply printing an address in Swift will not do anything. This is why the Objective-C context is so useful when debugging, especially when working in assembly where there are only references to memory addresses.

Now, use the new command you’ve just created on the address:

(lldb) cpo 0x15210c180

You’ll see the same output as you did with po self:

<Signals.MainViewController: 0x15210c180>

This is a helpful command to get a NSObject’s description, whether it’s created with Objective-C or Swift.

You can also add your own substitution arguments using a % substitution pattern. Here is a contrived example to illustrate. Add a new command that takes your preferred language as an argument:

command alias lpo expression -l %1 -O --

When this new lpo alias executes it will replace the %1 with the first argument provided after the command. Using %1, %2, etc. you can specify as many substitutions as you like. Now repeat the previous po self exercise but this time you’ll provide the language swift or objc each time. When the Signals app pauses in viewDidLoad, it’s in the Swift context, so execute the command with swift:

(lldb) lpo swift self

You should get similar output as before.

<Signals.MainViewController: 0x152b0bc20>

Now take the memory address and execute the command again and specify the Swift context.

(lldb) lpo swift 0x152b0bc20

Again, same as before, you get the uninteresting output. But switching to the objc context provides the class name and memory location again:

(lldb) lpo objc 0x152b0bc20
<Signals.MainViewController: 0x152b0bc20>

Key Points

  • Use command alias to make shortcuts for commands you use often.
  • lldb will load initialization commands from a contextual .lldbinit file for Xcode (.lldbinit-Xcode) or for terminal (.lldbinit-lldb).
  • lldb will also load initialization commands from a generic .lldbinit file on launch. This file is read after the contextual one mentioned above.
  • You can specify aliases within an lldb session, but they will only live for that session.
  • For aliases, be sure to add -h and -H help text to remind future you why you made them and how to use them.

Where to Go From Here?

You’ve learned how to create aliases for simple commands as well as persist them in the .lldbinit file. This will work across both Xcode and Terminal invocations of lldb.

As an exercise, add help messages to your newly created cpo command in the ~/.lldbinit file so you’ll be able to remember how to use it when you have an onslaught of custom commands. Remember the -h option is the short help message that’s displayed when you just type help, while the -H option is the longer help command used when you type help command. Remember to use the -- to separate your help input arguments from the rest of your command.

In addition, write a command alias for something you use often. Put this alias in your ~/.lldbinit file and try it out!

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.