Scripting LLDB with Python

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:

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:

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.

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:

  1. *debugger*: An SBDebugger object.

  2. *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.

  3. *result*: An SBCommandReturnObject that can be used to provide a status and return value for the command.

  4. *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:

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.

Written by
Josh Kugelmann — Josh Kugelmann is an iOS Developer for Itty Bitty Apps. You can follow him on Twitter @protosphere_