20.
Hello, Script Bridging
Written by Walter Tyree
LLDB has several ways you can use to create your own customized commands. The first way is through the easy-to-use command alias you saw in Chapter 9, “Persisting & Customizing Commands”. This command simply creates an alias for a static command. While easy to implement, it really only allowed you to execute commands with no input.
After that came command regex, which let you specify a regular expression to capture input then apply it to a command. You learned about this command in Chapter 10, “Regex Commands”. This command works well when you want to feed input to an LLDB command, but it was inconvenient to execute multiline commands and supplying multiple, optional parameters could get really messy.
Next up in the tradeoff between convenience and complexity is LLDB’s script bridging. With script bridging, you can do nearly anything you like. Script bridging is a Python interface LLDB uses to help extend the debugger to accomplish your wildest debugging dreams.
However, there’s a cost to the script bridging interface. It has a steep learning curve, and the documentation, to put it professionally, sucks. Fortunately, you’ve got this book in your hands to help guide you through learning script bridging. Once you’ve a grasp on LLDB’s Python module, you can do some very cool (and excitingly dangerous!) things.
Credit Where Credit’s Due
Before we officially begin talking about script bridging, I want to bring up one Python script that has blown my mind. If it wasn’t for this script, this book would not be in your hands.
/Applications/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Versions/A/Resources/Python/lldb/macosx/heap.py
This is the script that made me take a deep dive into learning LLDB. I’ve never had a mental butt-kicking as good as I did trying to initially understand what was happening in this code.
This script has it all: finding stack traces for malloc’d objects (malloc_info -s), getting all instances of a particular subclass of NSObject (obj_refs -O), finding all pointers to a particular reference in memory (ptr_refs), finding C strings in memory (cstr_ref).
You can load the contents of this script into any LLDB session, and use its functions, with the following LLDB command:
(lldb) command script import lldb.macosx.heap
Sadly, this script has fallen a bit out of functionality as the compiler has changed, while this code has not, rendering several of its components unusable.
When you’re done reading this section, I would strongly encourage you to attempt to understand the contents of this script. You can learn a lot from it.
Ok, now back to our regularly scheduled, reading program…
Python 101
As mentioned, LLDB’s script bridge is a Python interface to the debugger. This means you can load and execute Python scripts in LLDB. In those Python scripts, you include the lldb module to interface with the debugger to obtain information such as the arguments to a custom command.
Don’t know Python? Don’t fret. Python is one of the most friendly languages to learn. And just like the Swift Playgrounds everyone’s losing their mind over, Python has an attractive REPL for learning.
Note: LLDB has completely transitioned from Python version 2 to Python 3. However, there are still lots of tutorials and blog posts out in the world that were written for Python 2. Since there are breaking changes between these different versions, pay careful attention to what version of Python you’re using and what version someone is blogging about. At the time of writing, LLDB uses Python 3.9.6.
Let’s figure out which version of Python LLDB is using. Open a Terminal window and type the following:
lldb
As expected, LLDB will start. From there, execute the following commands to find out which Python version is linked to LLDB:
(lldb) script print (sys.version)
The script command brings up the Python interpreter for LLDB. If you just typed in script without arguments, you’d be greeted with LLDB’s Python REPL.
If LLDB’s Python version is different than 3.x.x, freak out and complain loudly on the book’s forum.
Note: Your system-installed Python version does not have to match 3.9.6 exactly; bug fix releases work fine also.
Now you know the Python version LLDB works with, ensure you have the correct version of Python symlinked to the python Terminal command. Open a new Terminal window and type the following:
python3 --version
If the Python version matches the one that LLDB has, then launch Python with no arguments in the Terminal:
python3
If you have a different version of Python symlinked (i.e. 3.X.Y), you need to launch Python with the correct version number. For example, in Terminal, type python and press Tab. Different version(s) of Python might pop up with the correct version number.
Enter the correct version number associated with the LLDB version of Python:
python3.9.6
Either way, ensure the LLDB version of Python matches the one you have in your Terminal:
>>> import sys
>>> print (sys.version)
Notice in the actual Python REPL there’s no need to prefix any of the commands with the LLDB script command.
Playing Around in Python
If you are unfamiliar with Python, this section will help you get familiar with the language quickly. If you’re already knowledgeable about Python, feel free to jump to the next section.
In your Terminal session, open a Python REPL by typing the following:
python3
Next, in the Python REPL, type the following:
>>> h = "hello world"
>>> h
You’ll see the following output:
'hello world'
Python lets you assign variables without needing to declare the type beforehand. Unlike Swift, Python doesn’t really have the notion of constants, so there’s no need for a var or let declaration for a variable.
Note: If you have a different version of Python, then some of the commands might have different syntax. You’ll need to consult Google to figure out the correct equivalent command.
Going a step further, play around with the variable h and do some basic string manipulation:
>>> h.split()
['hello', 'world']
This will give a Python list, which is somewhat like an array that can store different types of objects.
If you need your Swift fix equivalent, then imagine a list is something similar to the following Swift code:
var h: [Any] = []
You can verify this by looking up the Python’s class type. In the Python REPL, press the up arrow to bring up the previous command and append the .__class__ call to the end like so:
>>> h.split(" ").__class__
<type 'list'>
Note there’s two underscores preceding and following the word class.
What type of class is the h variable?
>>> h.__class__
<type 'str'>
That’s good to know; a string is called str. You can get help on the str object by typing the following:
>>> help (str)
This will dump all the info pertaining to str, which is too much to digest at the moment.
Exit out of this documentation by typing the q character and narrow your search by looking only for the split function used previously:
>>> help (str.split)
You’ll get some documentation output similar to the following:
Help on method_descriptor:
split(self, /, sep=None, maxsplit=-1)
Return a list of the words in the string, using sep as the delimiter string.
sep
The delimiter according which to split the string.
None (the default value) means split according to any whitespace,
and discard empty strings from the result.
maxsplit
Maximum number of splits to do.
-1 (the default value) means no limit.
Reading the above documentation, you can see the first optional argument expects a string, and an optional second argument to indicate the maximum upper limit to split the string.
What do you think will happen when you try to execute the following command? Try your best to figure it out before executing it.
>>> h.split(" ", 0)
Now to turn your attention towards functions. Python uses indentation to define scope, instead of the braces that many other languages use, including Swift and Objective-C. This is a nice feature of Python, since it forces developers to not be lazy slobs with their code indentation.
Declare a function in the REPL:
>>> def test(a):
...
You’ll get an ellipsis as output, which indicates you have started creating a function. Type two spaces and then enter the following code. If you don’t have a consistent indentation, the python function will produce an error.
... print(a + " world!")
Press Enter again to exit out of the function. Now, test out your newly created test function:
>>> test("hello")
You’ll get the expected hello world! printed out.
Now that you can “truthfully” put three years of Python experience on your resume, it’s time to create an LLDB Python script.
Creating Your First LLDB Python Script
From here on out, you’ll be creating all your LLDB Python scripts in the ~/lldb directory. If you want to have them in a different directory, every time I say ~/lldb, you’ll need to invoke your “mental symlink” to whatever directory you’ve decided to use.
In Terminal, create the ~/lldb directory:
mkdir ~/lldb
In your favorite ASCII text editor, create a new file named helloworld.py in your newly created ~/lldb directory. For this particular example, I’ll use the my-editor-is-better-neutral-argument, nano.
Note: If you’ve looked ahead to Appendix B “Python Environment Setup” and have started to or want to start using a more powerful Python IDE than
nano, employ another “mental symlink” and start using it now. Even Apple has a symlink fornanothat points topicosince both text editors are not installed on macOS systems anymore. Throughout this book, if you see any screenshots of Python code, it’ll be from VS Code or vim since those are what I use.
nano ~/lldb/helloworld.py
Add the following code to the file:
def your_first_command(debugger, command, result, internal_dict):
print ("hello world!")
Make sure you indent the print ("hello world") line (ideally with two spaces) or else it won’t be included as part of the function!
For now, ignore the parameters passed into the function. Remember when you learned about your hello_world.c or hello_world.java, and the instructor (or the internet) said to just ignore the params in main for now? Yeah, same thing here. These params are the defined way LLDB interacts with your Python code. You’ll explore them in upcoming chapters.
Save the file. If you’re using nano, pressing Control-O will write to disk.
Create a new tab in Terminal and launch a new LLDB session:
lldb
This will launch a blank, unattached LLDB session.
In this new LLDB session, import the script you created:
(lldb) command script import ~/lldb/helloworld.py
If the script is imported successfully, there will be no output.
But how do you execute the command? The only thing the above command did was bring the helloworld (yes, named after the file) module’s path in as a candidate to use for Python.
If you plan to use any of the code in helloworld, you’ll need to import the module. This is a similar concept to Swift: you can link a Swift package by adding it to the Swift Package Manager list, but you can’t actually use the code until you import it into a Swift file. Type the following into LLDB:
(lldb) script import helloworld
You can verify you’ve successfully imported the module by dumping all the methods in the helloworld python module:
(lldb) script dir(helloworld)
The dir function will dump the contents of the module. If you successfully imported the module, you’ll see the following output:
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'your_first_command']
Take note, the function you created earlier: your_first_command is listed in the output.
Although the above two commands weren’t necessary to set up the command, it does show you how this script bridging works. You imported the helloworld module into the Python context of LLDB, but when you execute normal commands, you aren’t executing in a Python context (although the command logic underneath could be using Python).
So how do you make your command available only through LLDB, and not through the Python context of LLDB?
Head back to LLDB and type the following:
(lldb) command script add -f helloworld.your_first_command yay
This adds a command to LLDB, which is implemented in the helloworld Python module with the function your_first_command. This scripted function is assigned to the LLDB command yay.
Execute the yay command now:
(lldb) yay
Provided everything worked, you’ll get the expected hello world! output.
Setting Up Commands Efficiently
Once the high of creating a custom function in script bridging has worn off, you’ll come to realize you don’t want to type this stuff each time you start LLDB. You want those commands to be there ready for you as soon as LLDB starts.
Fortunately, LLDB has a lovely function named __lldb_init_module, which is a hook function called as soon as your module loads into LLDB.
This means you can stick your logic for creating the LLDB command in this function, eliminating the need to manually set up your LLDB function every time LLDB starts!
Open the helloworld.py class you created and add the following function below your_first_command’s definition:
def __lldb_init_module(debugger, internal_dict):
debugger.HandleCommand('command script add -f helloworld.your_first_command yay')
Here you’re using a parameter passed into the function named debugger. With this object, an instance of SBDebugger, you’re using a method available to it called HandleCommand. Calling debugger.HandleCommand is pretty much equivalent to typing something directly into LLDB.
For example, to get the command po "hello world" from the LLDB console into a script, the equivalent command would be debugger.HandleCommand('po "hello world"')
Remember the python help command you used earlier? You can get help documentation for this command by typing:
(lldb) script help(lldb.SBDebugger.HandleCommand)
At the time of writing, you’ll get a rather disappointing amount of help documentation:
HandleCommand(self, command)
HandleCommand(SBDebugger self, char const * command)
This is why there’s such a steep learning curve to this stuff, and the reason not many people venture into learning about script bridging. That’s why you picked up this book, right?
Save your helloworld.py file and open up your ~/.lldbinit file in your favorite editor.
You’re now going to specify you want the helloworld module to load at startup every time LLDB loads up.
At the end of the file, add the following line to the end of you r ~/.lldbinit:
command script import ~/lldb/helloworld.py
Save and close the file.
Open Terminal and start up another tab with LLDB in it like so:
lldb
Since you specified to have the helloworld module imported into LLDB upon startup, and you also specified to create the yay function as soon as the helloworld python module loads through the __lldb_init_module module, the yay LLDB command will be available immediately to you.
Try it out now:
(lldb) yay
If everything went well you’ll see the following output:
hello world!
Awesome! You now have a foundation for building some very complex scripts into LLDB. In the following chapters, you’ll explore more of how to use this incredibly powerful tool.
For now, close all those Terminal tabs and give yourself a pat on the back.
Key Points
- MacOS and the embedded
lldbuse Python3, which is not Python 2. Be mindful of what version someone is referencing in blog posts and online tutorials. - Run
lldband typescript print(sys.version)to check which version of Python LLDB is using. - In Terminal, use
python3 --versionto check which version of Python your system is using by default. - Python uses whitespace instead of braces to denote different scopes in code.
- In the Python REPL (run
python3in a terminal) typehelp (<whatever>)to view the online documentation for a keyword or object. - This book will assume you’re putting all of your scripts in a
~/lldbfolder on your system. - In a
.pyfile, create a__lldb_init_modulefunction to load your commands intolldbsessions automatically.
Where to Go From Here?
If you don’t feel comfortable with Python, now is the time to start brushing up on it. If you have past development experience, you’ll find Python to be a fun and friendly language to learn. It’s a great language for quickly building other tools to help with everyday programming tasks.