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

23. Script Bridging With Options & Arguments
Written by Walter Tyree

When you’re creating a custom debugging command, you’ll often want to slightly tweak functionality based upon options or arguments supplied to your command. A custom LLDB command that can do a job only one way is a boring one-trick pony.

In this chapter, you’ll explore how to pass optional parameters (a.k.a. options) as well as arguments (parameters which are expected) to your custom command to alter functionality or logic in your custom LLDB scripts.

You’ll continue working with the bar, “break-after-regex”, command you created in the previous chapter. In this chapter, you’ll finish up the bar command by adding logic to handle options in your script.

By the end of this chapter, the bar command will have logic to handle the following optional parameters:

  • Non-regular expression search: Using the -n or --non_regex option will result in the bar command using a non-regular expression breakpoint search instead. This option will not take any additional parameters.
  • Filter by module: Using the -m or --module option will only search for breakpoints in that particular module. This option will expect an additional parameter which specifies the name of the module.

This will be a dense but fun chapter. Make sure you’ve got a good supply of caffeine!

Setting Up

If you’ve gone through the previous chapter and your bar command is working, then you can continue using that script and ignore this part. Otherwise, head on over to the starter folder in this chapter’s resources, and copy the BreakAfterRegex.py file into your ~/lldb folder. Make sure your ~/.lldbinit file has the following line which you should have from the previous chapter:

command script import ~/lldb/BreakAfterRegex.py

If you’ve any doubts if this command loaded successfully into LLDB, simply fire up a new LLDB instance in Terminal:

lldb

Then check for the help docstring of the bar command:

(lldb) help bar

If you get an error, it’s not successfully loaded. However, even if it is loaded, you don’t get some very useful help. You’ll fix that now. Remember earlier in the book, you added -h and -H flag to add some help string. Another way to add help text is using a docstring. Add this in BreakAfterRegex.py right after the line that reads def breakAfterRegex(debugger, command, result, internal_dict): but before the first line of code:

  '''Creates a regular expression breakpoint and adds it.
  Once the breakpoint is hit, control will step out of the current
  function and print the return value. Useful for stopping on
  getter/accessor/initialization methods
  '''

Now, save your work and restart lldb and run help bar again. You should see your nice, more robust help text now.

The RWDevCon Project

For this chapter, you’ll use an app called RWDevcon.

This app was the companion app for the RWDevCon conference a few years ago.

For this project, I’ve forked from commit 84167c68 which can be found in the starter folder. However, you can get a more up-to-date, though still somewhat ancient, version on GitHub.

Navigate to the starter folder then open, build, then run this application. Take a look around to get acquainted with the project.

There’s no need to explore any of the source code. With the aid of the bar command, you’ll be able to explore different items of interest with smart breakpoint queries.

But before we can do that, let’s talk about how to make this bar command much more powerful.

The optparse Python Module

The lovely thing about LLDB Python scripts is you have all the power of Python — and its modules — at your disposal.

There are three notable modules that ship with Python that are worth looking into when parsing options and arguments: getopt, optparse, and argparse.

getopt is kind of low level and optparse is on its way out since it’s been deprecated after Python 2.7. Unfortunately argparse is mostly designed to work with Python’s sys.argv — which is not available to your Python LLDB command scripts directly. This means optparse will be your go-to option. Facebook’s Chisel, Apple’s own custom LLDB scripts, and I all use this module. So, it’s kinda the de-facto standard for parsing arguments. ;]

The optparse module will let you define an instance of type OptionParser, a class responsible for parsing all your arguments. For this class to work, you need to declare what arguments and options your command supports. This makes sense because optional parameters may or may not take additional values for that particular option.

Take a brief look at an example. Consider the following:

some_command woot -b 34 -a "hello world"

The command is named some_command. But what are the arguments and options being passed into this command?

If you didn’t give any context to the parser, then this statement is ambiguous. The parser doesn’t know whether or not the -b or -a option should take in parameters for the option. For example, the parser could think this command is passed three arguments: ['woot', '34', 'hello world'], and two options -b, -a with no parameters. However, if the parser expected -b and -a to take parameters, the parser would give you the argument of ['woot'], '34' for the -b option and 'hello world' for -a.

Let’s dive into optparse some more, and see how we can use it to handle cases like this.

Adding Options Without Params

With the knowledge you need to educate your parser with what arguments are expected, it’s time to add your first option which will alter the functionality of the bar command to apply the SBBreakpoint without using a regular expression, but instead use a normal expression.

This argument will be backed by a Python boolean value, so no parameters are needed for this option. The existence (or lack thereof) of this option is all the information you need to determine the boolean value. If the argument exists, then it’ll be True. Otherwise, False.

It’s worth noting some script authors will engineer an option that will encourage a boolean option which explicitly requires a parameter for the Boolean value and default to either True or False if the option is not supplied.

For example, the following command takes an option, -f with no parameters:

some_command -f

This would then turn into:

some_command -f true

That’s not really my style. But you might want to consider this design decision if you’re building scripts for a wider audience, since it gives the user more explicit intentions.

OK, enough chit-chat. Let’s get to implementing this parser thing.

Open up BreakAfterRegex.py and add the following import statements at the top of the file either after or before the import lldb line:

import optparse
import shlex

The optparse module was just covered previously, it contains the OptionParser class to parse any extra input given to your command.

The shlex module has a nice little Python function that conveniently splits up the arguments supplied to your command on your behalf while keeping string arguments intact. It acts in the same way as a shell does, like bash or zsh.

For example, consider the following Python code:

import shlex
command = '"hello world" "2nd parameter" 34'
shlex.split(command)

This will produce the following output:

['hello world', '2nd parameter', '34']

This returns a Python list of parsed Python strs.

If you had split this by spaces, then you would have "hello and world" as the first two items in the list. This is clearly not what was intended. This shows the power of shlex.

But before you go using this split method, you’ll need to create the parser itself. Head to the very bottom of BreakAfterRegex.py and add the following method:

def generateOptionParser():
  '''Gets the return register as a string for lldb
    based upon the hardware
  '''
  usage = "usage: %prog [options] breakpoint_query\n" +\
          "Use 'bar -h' for option desc"
  # 1
  parser = optparse.OptionParser(usage=usage, prog='bar')
  # 2
  parser.add_option("-n", "--non_regex",
                    # 3
                    action="store_true",
                    # 4
                    default=False,
                    # 5
                    dest="non_regex",
                    # 6
                    help="Use a non-regex breakpoint instead")
  # 7
  return parser

Let’s break this down, parameter by parameter:

  1. You’re creating the OptionParser instance and supplying it a usage param and a prog param. The usage will get displayed if you screw up and give the parser an argument it doesn’t know how to handle. The prog option is used to address the name of the program. I always incorporate it because it resolves a weird little issue which lets you run the -h or --help option to get all the supported options for a custom command. If the prog arg is not in there, the -h command will not work correctly. It’s one of life’s little mysteries. :]
  2. This line (followed by the next four lines of non-commented code) add the --non_regex or -n parameter to the parser.
  3. The action param describes what action should be done when this param is supplied. "store_true" informs the parser to store the Python Boolean True when this option is supplied.
  4. The default param indicates that the initial value will be False. If this option is not given, this will be the value.
  5. The dest parameter will determine the name, non_regex, that you’re giving to the property when the OptionParser parses your input.

As you’ll see shortly, the parse_args method produces a Python tuple containing a list of options (called options) and a list of arguments (called args). The options variable will now contain the non_regex property.

  1. help will give you help documentation. You can get all the parameters and their info with the --help option. For example, when this is correctly set up in the bar command, all you have to do is type bar -h to see a list of all the options and what they do.
  2. Once you’ve created the OptionParser and added the -n option, you’re returning the instance of the OptionParser.

You’ve just created a method that will generate this OptionParser instance you need to start parsing those arguments. Now it’s time to use this thing.

Jump back to the beginning of the breakAfterRegex function. Remove the following two lines:

target = debugger.GetSelectedTarget()
breakpoint = target.BreakpointCreateByRegex(command)

Then, in their place, add the following code:

# 1
command = command.replace('\\', '\\\\')
# 2
command_args = shlex.split(command, posix=False)

# 3
parser = generateOptionParser()

# 4
try:
  # 5
  (options, args) = parser.parse_args(command_args)
except:
  result.SetError(parser.usage)
  return

target = debugger.GetSelectedTarget()

# 6
clean_command = shlex.split(args[0])[0]

# 7
if options.non_regex:
  breakpoint = target.BreakpointCreateByName(
                      clean_command)
else:
  breakpoint = target.BreakpointCreateByRegex(
                      clean_command)

# The rest remains unchanged

Make sure you have your indentation correct! This should be indented by two spaces, or whatever your single-tab width of choice is, as it’s all part of the function.

Here’s what that code does:

  1. When parsing your input to the OptionParser, it will interpret slashes as escaping characters. For example, "\'" is interpreted as just "'". This means you’ll need to escape any backslash characters in your commands.
  2. As you learned in a previous chapter, the command parameter passed into your custom LLDB scripts is a Python str, which contains all input that is passed into your argument. You’ll pass this variable into the shlex.split method to obtain a Python list of Python strs. In addition, there’s that posix=False which helps combat any input which contains special characters like a dash; otherwise, OptionParser will incorrectly assume that’s an option being passed in. This is important because Objective-C has dashes in instance methods, so you don’t want the dash to be incorrectly interpreted as an option!
  3. Using the newly created generateOptionParser function, you create a parser to handle the command’s input.
  4. Parsing input can be error-prone. Python’s usual approach to error handling is throwing exceptions. It’s no surprise that optparse throws if it finds an error. If you don’t catch exceptions in your scripts, LLDB will go down, which will also tank the process! Therefore, the parsing is contained in a try-except block to prevent LLDB from dying due to bad input.
  5. The OptionParser class has a parse_args method. You’re passing in your command_args variable to this method, and will receive a tuple in return. This tuple consists of two values: options, which consists of all option arguments, which is only the non_regex option right now. The other half of the tuple contains all of the args which consists of any other input parsed by the parser.
  6. You’re taking the first captured argument (the breakpoint query) and assigning it to a variable called clean_command. Remember that posix=False mentioned in bullet 2? That logic will maintain the quotes around your captured argument which preserves your exact syntax. If you didn’t have that posix=False, you could just use args[0], but then you’d forfeit a lot of power in your regex by not being able to use the escape backslash character in your regex query.
  7. You’re putting your first option to use! You’re checking the truthiness of options.non_regex. If True, you’ll execute the BreakpointCreateByName method in SBTarget to implement a non-regular expression breakpoint. If the non_regex is False, then your script will use a regex search. Again, all you need to do is add the -n to your input for the bar command to make the non_regex True.

Testing Out Your First Option

Enough code. Time to test this script out.

Instead of using that reload_script command you’ve used in the previous chapters, try an alternative tactic that you might appreciate to reload the script.

Jump to Xcode and create a new symbolic breakpoint.

Make sure the Breakpoint Navigator tab is selected, then hunt down that lonely + icon in the lower left corner. Then select Symbolic breakpoint…. Alternatively for you cool kids, press *Command-Control-*

In the Symbol section type getenv.

Add two actions. The first action adds the following command:

br dis 1

Click the + icon to add a second action. In this action, add your bar command:

bar -n "-[NSUserDefaults(NSUserDefaults) objectForKey:]"

Finally select Automatically continue after evaluating actions.

When all is said and done, your symbolic breakpoint should look like the following:

Can you figure out what you’ve just done? You’ve created a symbolic breakpoint on the getenv C function. If I want to setup breakpoints or run some scripts before “my” code starts executing, or before reverse engineering an app, this is a good go-to to hook any logic for custom commands you want in LLDB.

I’m not a fan of setting breakpoints like this in main, since a lot of executables contain the function main, and the primary executable’s main symbol might be stripped in a production build of an executable. We know that getenv will get hit for sure and will get hit before my code starts running.

What about those actions? The first action says to get rid of that getenv breakpoint. You’re not deleting it; you’re just disabling it. This is ideal since getenv gets called a fair bit and you need to get rid of this breakpoint once you’ve setup your LLDB logic. The use of 1 is mentioned because this breakpoint is the first breakpoint created for this session, which disables this symbolic breakpoint after it has run once.

After that, you’re creating a non regular expression breakpoint on NSUSerDefaults’s objectForKey: method. We expect this method to return an id or nil, so let’s see what this RWDevCon app is reading (or writing) to our NSUserDefaults.

Build and run the application.

If you haven’t taken a deep dive into the app, you’ll likely get a lot of nil values. This means that this method is definitely getting read by some code in this app. Keep clicking the resume button in Xcode or type c or continue into the (lldb) console until you finally get to the main view for the app.

Tap on any one of the workshops to bring up the detail view controller. Keep resuming the app until the detail view appears.

Before you continue, clear the LLDB window by pressing Command-K.

From there, tap Add to my Schedule while keeping an eye on the console output.

Resume the app a few more times until you can see there’s an object that gets added to the NSUserDefaults that matches the When time.

Adding Options With Params

You’ve learned how to add an option that expects no arguments. You’ll now add another option that expects a parameter. This next option will be the --module option to specify which module you want to constrain your regular expression query to.

This is very similar to breakpoint set’s -s or --shlib option where it expects the name of the module immediately after the option. You explored this back in Chapter 4, “Stopping in Code.”

In the BreakAfterRegex.py script jump back down to the generateOptionParser function and add the following code right before return parser:

# 1
parser.add_option("-m", "--module",
                  # 2
                  action="store",
                  # 3
                  default=None,
                  # 4
                  dest="module",
                  help="Filter a breakpoint by only searching within a specified Module")
  1. You’re adding a new option -m or --module to the OptionParser instance.
  2. In the previous option, the action was "store_true"; this time it is "store". This means this option expects a parameter.
  3. This parameter’s default value is None.
  4. The name of this property will be module.

Jump back to the breakAfterRegex function and scan for the following lines:

if options.non_regex:
  breakpoint = target.BreakpointCreateByName(clean_command)
else:
  breakpoint = target.BreakpointCreateByRegex(clean_command)

Add options.module as the second parameter to both of these functions.

if options.non_regex:
  breakpoint = target.BreakpointCreateByName(clean_command, options.module)
else:
  breakpoint = target.BreakpointCreateByRegex(clean_command, options.module)

So how does this work? Let’s print out the method signature right now for BreakpointCreateByRegex. Type the following in LLDB:

(lldb) script help (lldb.SBTarget.BreakpointCreateByRegex)

This will dump the small amount of documentation for this function. Although there is no help documentation for this method, it does give you a list of its method signatures.

The following signature is worth discussing:

BreakpointCreateByRegex(SBTarget self, str symbol_name_regex, str module_name=None) -> SBBreakpoint

Take note of the final parameter: module_name=None. The fact it’s an optional parameter means if you don’t supply a parameter, the module_name will take the value as None. This means when the OptionParser instance parses the options, you can supply options.module into the BreakpointCreateByRegex method regardless, since the default value of options.module will be None, which is the same as not applying an extra argument.

Time to test this out. Save your work in your script. Jump over to Xcode and modify that getenv Symbolic breakpoint. Replace the second action with the following line of code:

bar @objc.*.init -m RWDevCon

Make sure that 'C' in 'Con' is capitalized! Also, remember that first action br dis 1? It’s disabling the breakpoint every time, so don’t forget to check the box in the top left corner to “Enable Symbolic Breakpoint”. There aren’t any references to -NSUserDefaults(NSUserDefaults) objectForKey:] in the RWDevCon module, so we’ll need to experiment with a different breakpoint.

This will create a regex breakpoint on all Objective-C objects that are subclassed by a Swift object and stick a breakpoint on their initializer. You are filtering this breakpoint query to only search for breakpoints inside the RWDevCon module.

Run the application and check out all the Objective-C objects that are subclassed by Swift objects.

Take a quick look at the output. Keep resuming the app as it works through all of the classes before displaying the main view. You’ll get a lot of __ObjC.NSEntityDescription hits. That must mean there’s some CoreData logic that’s written in Swift, right?

Right!

Keep resuming the app as it starts loading the Session objects from the CoreData store. Notice that the last frame in the stack trace that is Swift is pretty far down the list, 22 in the screenshot. Now pick out any of the Session objects that you like.

Copy the address into your clipboard.

Before you paste in your address to a command, let’s dump all the methods implemented by this Session class. Since it’s an Objective-C subclass, it’s fair game to all those introspection commands you’ve made earlier.

In LLDB type the following:

(lldb) methods Session

If you didn’t create the methods command in an earlier chapter, recall it’s just using the private _shortMethodDescription introspection helper method. You can type out the whole thing:

(lldb) expression -lobjc -O -- [Session _shortMethodDescription]

This will dump all the methods the Session class implements that the Objective-C runtime knows about. Note that I said Objective-C runtime. There still could be Swift methods that this class implements that the Objective-C runtime doesn’t even know about if the class inherits from NSObject!

You can of course execute any of these methods on your valid Session instance by replacing “Session” with the memory address you copied earlier.

This is just your regular reminder that beneath the shiny SwiftUI and Swift code is still a whole lot of Objective-C code and objects.

Key Points

  • Placing some help at the beginning of a Python def within ''' quotes will be treated as the function’s documentation. This documentation is shown when you run help <my_function>.
  • The optparse module is deprecated in Python, but is still widely used with the lldb modules. Watch for an official switch to argparse, but it hasn’t happened quite yet.
  • The shlex lexer offers .split and .join commands you can use to manipulate arguments passed to your function, treating them in a way you’d expect as a shell user. For example when passing strings with spaces in them you can wrap in quotes.
  • The .add_option command of a parser allows you to supply default values and help text for that option.
  • Placing a symbolic breakpoint in getenv or main is a common technique to set up lldb commands and breakpoints every time your code runs. You could also use it to create an .lldbinit type of file for a specific project.

Where to Go From Here?

That was pretty intense, but you’ve learned how to incorporate options into your own Python scripts.

In the very unlikely chance you still have energy after reading this chapter, you should implement some sort of backtrace option for the bar command. There are many times, when debugging, where I wish I’d known the stack trace of an interesting object!

However, adding options like this and using .HandleCommand to execute code becomes tedious quickly. In the next chapter you’ll see how to use SBValue to interact with the objects in your code.

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.