One of the more obscure features of LLDB is its ability to be scripted using Python. LLDB includes Python bindings to its C++ API (LLDB.Framework/liblldb).

These can be used in two ways:

From within LLDB itself to provide extended functionality such as adding custom commands or performing more complex operations.

As a standalone library used outside LLDB to create anything from fully automated debug sessions, to new debugger front-ends.

In this post, I’m going to show you how extending LLDB can allow you to reduce a string of repetitive commands into a single action, and expose a whole new level of power that you can use to improve your debugging productivity.

Adding Commands

The most simple, and arguably the most common use of the scripting functionality, is to add customised commands to use from within the debugger interface.

For this example, we’re going to create a custom command that returns the bundle identifier of the application being debugged.

Custom commands can be added using the command script add command. For example:

command script add -h "Returns the bundle identifier of the application being debugged." -f iba_lldb.GetBundleIdentifier bundle_id

Would add a command named bundle_id, bound to the function GetBundleIdentifier in the iba_lldb module.

command script add takes the following arguments:

(Optional) -f: The first specifies the name of the Python function that the command should be bound to. This is in the format {module name}.{function name}.



In Python, a module is just a .py file containing Python code. So if your script was named iba_lldb.py, the module name would be iba_lldb.



If this option is not specified, LLDB will prompt you to enter your Python code in the debugger itself.

The first specifies the name of the Python function that the command should be bound to. This is in the format {module name}.{function name}. In Python, a module is just a .py file containing Python code. So if your script was named iba_lldb.py, the module name would be iba_lldb. If this option is not specified, LLDB will prompt you to enter your Python code in the debugger itself. (Optional) -h: Specifies the help text for the command displayed when the user types help {command name}

Specifies the help text for the command displayed when the user types help {command name} (Required): The last (unnamed) argument specifies the name of the command as used in LLDB.

Creating a Command Handler

So how do we define command handler functions? In your Python script, first make sure to import the lldb module:

import lldb

Command handlers must take 4 parameters:

debugger: An SBDebugger object. command: A string containing all of the arguments passed to your command. This string can be parsed into a list of options using the standard shlex and optparse modules. result: An SBCommandReturnObject that can be used to provide a status and return value for the command. internal_dict: A dictionary containing the variables and functions that are available in the scripting session. As its name suggests, it is for internal use only.

Following on with our example command, the function body ends up looking like this:

def GetBundleIdentifier(debugger, command, result, internal_dict):

target = debugger.GetSelectedTarget()

process = target.GetProcess()

mainThread = process.GetThreadAtIndex(0)

currentFrame = mainThread.GetSelectedFrame() bundleIdentifier = currentFrame.EvaluateExpression("(NSString *)[[NSBundle mainBundle] bundleIdentifier]").GetObjectDescription() result.AppendMessage(bundleIdentifier)

Executing our custom command in the console gives us exactly what we’d expect:

(lldb) bundle_id

com.ittybittyapps.revert

While not the most useful command, it does serve as a good demonstration of how to use the LLDB APIs.

Custom Type Summaries

Another powerful feature that LLDB allows through its scripting functionality is the ability to add customised summaries for non-standard data-types.

Say we have a hypothetical struct IBARange:

typedef struct IBARange {

int startIndex;

int endIndex;

} IBARange;

By default, printing a variable of type IBARange using the print command will give the following:

(IBARange) $1 = (startIndex = 42, endIndex = 100)

While printing using the po command gives us nothing at all!

If we wanted to find out the length of this range, we could do something like this:

print range.endIndex - range.startIndex

But why do this every time, when we can have the debugger do the work for us? In a similar fashion to adding commands, we can use the type summary add command to instruct LLDB to use a custom summary that displays the start index, end index and the length of our IBARange struct:

type summary add -F iba_lldb.IBARangeSummary IBARange

Your type summary handler function should take two arguments:

value: An SBValue object representing the value of the variable in question.

An SBValue object representing the value of the variable in question. internal_dict: A dictionary containing the variables and functions that are available in the scripting session. As its name suggests, it is for internal use only.

Following on from above, we can now add a type summary handler named IBARangeSummary in iba_lldb.py like so:

def IBARangeSummary(value, internal_dict):

start = value.GetChildMemberWithName("startIndex").GetValueAsSigned()

end = value.GetChildMemberWithName("endIndex").GetValueAsSigned()

length = end - start return "(start = {0}, end = {1}, length = {2})".format(start, end, length)

Now when we print a variable of the IBARange type, we get:

(IBARange) $2 = (start = 42, end = 100, length = 58)

Much better!

As an added bonus, our summary will also be used for pointers pointing to instances of IBARange. Before adding our custom summary, printing a pointer to an IBARange would yield the following, for example:

(IBARange *) $3 = 0x00007fff51c2ae18

With our custom summary, we get much nicer results:

(IBARange *) $4 = (start = 42, end = 100, length = 58)

Finishing Touches

When your script is loaded by LLDB, it will call the __lldb_init_module function. From here, we can perform any needed setup, such as adding our commands and type summaries.

This cannot be done purely with the Python API, as no targets are defined at the stage __lldb_init_module is called. Instead, we have to ask LLDB to execute the appropriate commands for us:

def __lldb_init_module(debugger, internal_dict):

debugger.HandleCommand("command script add -f " + __name__ + ".GetBundleIdentifier bundle_id")

debugger.HandleCommand("summary type add -F " + __name__ + ".IBARangeSummary IBARange")

Here, we fetch the module name dynamically using the __name__ variable. This means that you won’t need to update your script every time you change its file name.

If you want your commands to be available in every LLDB session, you can import the script in your ~/.lldbinit file, as follows (assuming iba_lldb.py is in your home directory):

command script import iba_lldb.py

If you’re using Xcode and only want your commands to be available from inside there, you can import the script in your ~/.lldbinit-Xcode. This file is read by Xcode whenever a debug session is started, but not by regular LLDB builds.

References

Some rudimentary documentation for the Python API is available here. The C++ reference may also be helpful, as the API translates pretty much 1:1 across both languages.

Based in Melbourne, Australia, Itty Bitty Apps builds great software on mobile and Mac for clients big and small. It also makes Reveal — Amazingly powerful runtime view debugging for iOS developers.