3.
Attaching with LLDB
Written by Derek Selander
Now that you’ve learned about the two most essential commands, help and apropos, it’s time to investigate how LLDB attaches itself to processes. You’ll learn all the different ways you can attach LLDB to processes using various options, as well as what happens behind the scenes when attaching to processes.
The phrase of LLDB “attaching” is actually a bit misleading. A program named debugserver (found in Xcode.app/Contents/SharedFrameworks/LLDB.framework/Resources/) is responsible for attaching to a target process.
If it’s a remote process, such as an iOS, watchOS or tvOS application running on a remote device, a remote debugserver gets launched on that remote device. It’s LLDB’s job to launch, connect, and coordinate with the debugserver to handle all the interactions in debugging an application.
Attaching to an existing process
As you’ve already seen in Chapter 1, you can attach to a process like so:
lldb -n Xcode
However, there are other ways to do the same thing. You can attach to Xcode by providing the process identifier, or PID, of a running program.
Note: Just a reminder, if you didn’t disable SIP on your macOS computer, you will not be able to attach LLDB to Apple applications. Attaching to 3rd party applications (even from the App Store!) is still possible as of version 10.14.1 in Mojave provided there are no anti-debugging techniques in the application.
Open Xcode, then open a new Terminal session, and finally run the following:
pgrep -x Xcode
This will output the PID of the Xcode process.
Next, run the following, replacing 89944 with the number output from the command above:
lldb -p 89944
This tells LLDB to attach to the process with the given PID. In this case, this is your running Xcode process.
Attaching to a future process
The previous command only addresses a running process. If Xcode isn’t running, or is already attached to a debugger, the previous commands will fail. How can you catch a process that’s about to be launched, if you don’t know the PID yet?
You can do that with the -w argument, which causes LLDB to wait until a process launches with a PID or executable name matching the criteria supplied using the -p or -n argument.
For example, kill your existing LLDB session by pressing Ctrl + D in your Terminal window then type the following:
lldb -n Finder -w
This will tell LLDB to attach to the process named Finder whenever it next launches. Next, open a new Terminal tab, and enter the following:
pkill Finder
This will kill the Finder process and force it to restart. macOS will automatically relaunch Finder when it’s killed. Switch back to your first Terminal tab and you’ll notice LLDB has now attached itself to the newly created Finder process.
Another way to attach to a process is to specify the path to the executable and manually launch the process at your convenience:
lldb -f /System/Library/CoreServices/Finder.app/Contents/MacOS/Finder
This will set Finder as the executable to launch.
Once you’re ready to begin the debug session, simply type the following into the LLDB session:
(lldb) process launch
Note: An interesting side effect is that
stderroutput (i.e. Swift’sNSLog, C’sprintfand company) are automatically sent to the Terminal window when manually launching a process. Other LLDB attaching configurations don’t do this automatically.
Options while launching
The process launch command comes with a suite of options worth further exploration. If you’re curious and want to see the full list of available options for process launch, simply type help process launch.
Close previous LLDB sessions, open a new Terminal window and type the following:
lldb -f /bin/ls
This tells LLDB to use /bin/ls (the file listing command) as the target executable.
Note: If you omit the
-foption, LLDB will automatically infer the first argument to be the executable to launch and debug. When debugging Terminal executables, I’ll oftentimes typelldb $(which ls)(or equivalent), which then gets translated tolldb /bin/ls.
You’ll see the following output:
(lldb) target create "/bin/ls"
Current executable set to '/bin/ls' (x86_64).
Since ls is a quick program (it launches, does its job, then exits) you’ll run this program multiple times with different arguments to explore what each does.
To launch ls from LLDB with no arguments. Enter the following:
(lldb) process launch
You’ll see the following output:
Process 7681 launched: '/bin/ls' (x86_64)
... # Omitted directory listing output
Process 7681 exited with status = 0 (0x00000000)
An ls process will launch in the directory you started in. You can change the current working directory by telling LLDB where to launch with the -w option. Enter the following:
(lldb) process launch -w /Applications
This will launch ls from within the /Applications directory. This is equivalent to the following:
$ cd /Applications
$ ls
There’s yet another way to do this. Instead of telling LLDB to change to a directory then run the program, you can pass arguments to the program directly.
Try the following:
(lldb) process launch -- /Applications
This has the same effect as the previous command, but this time it’s doing the following:
$ ls /Applications
Again, this spits out all your macOS programs, but you specified an argument instead of changing the starting directory. What about specifying your desktop directory as a launch argument? Try running this:
(lldb) process launch -- ~/Desktop
You’ll see the following:
Process 8103 launched: '/bin/ls' (x86_64)
ls: ~/Desktop: No such file or directory
Process 8103 exited with status = 1 (0x00000001)
Uh-oh, that didn’t work. You need the shell to expand the tilde in the argument. Try this instead:
(lldb) process launch -X true -- ~/Desktop
The -X option expands any shell arguments you provide, such as the tilde. There’s a shortcut in LLDB for this: simply type run. To learn more about creating your own command shortcuts, check out Chapter 9, “Persisting and Customizing Commands”.
Type the following to see the documentation for run:
(lldb) help run
You’ll see the following:
...
Command Options Usage:
run [<run-args>]
'run' is an abbreviation for 'process launch -X true --'
See? It’s an abbreviation of the command you just ran! Give the command a go by typing the following:
(lldb) run ~/Desktop
Environment variables
For Terminal programs, environment variables can be equally as important as the program’s arguments. If you were to consult the man 1 ls, you’ll see at that the ls command can display output in color so long as the color environment variable is enabled (CSICOLOR) and you have the “color pallete” environment variable LSCOLORS to tell how to display certain filetypes.
With a target in LLDB, you can launch and set a program with any combination of environement variables.
For example, to display all the environment variables that the ls command will launch with, run the following command in LLDB:
(lldb) env
This will display all the environment variables for the target. It’s important to note that LLDB will not display the environment variables until the target is run at least once. If you don’t see any output, just give lldb a simple run before executing the env command.
You can inspect and augment these environment variables before launch using the settings set|show|replace|clear|list target.env-vars command, but you can also just specify them at launch with the -v option from the process launch command!
Time to display the /Applications directory in a horrible red color!
(lldb) process launch -v LSCOLORS=Ab -v CLICOLOR=1 -- /Applications/
Wow! Doesn’t that just burn the eyes? Try a different color with the following:
(lldb) process launch -v LSCOLORS=Af -v CLICOLOR=1 -- /Applications/
This would equivalent to you executing the following in Terminal without LLDB:
LSCOLORS=Af CLICOLOR=1 ls /Applications/
Lots of Terminal commands will contain environment variables and their descriptions in the command’s man page. Always make sure to read about how you’d expect an environment variable to augment a program.
In addition, many commands (and Apple frameworks!) have “private” environment variables not discussed in any documentation or man page. You’ll look at how you can extract this information out of executables later on in this book.
stdin, stderr, and stout
What about changing the standard streams to a different location? You’ve already tried changing stderr to a different Terminal tab in Chapter 1 using the -e flag, but how about stdout?
Type the following:
(lldb) process launch -o /tmp/ls_output.txt -- /Applications
The -o option tells LLDB to pipe stdout to the given file.
You’ll see the following output:
Process 15194 launched: '/bin/ls' (x86_64)
Process 15194 exited with status = 0 (0x00000000)
Notice there’s no output directly from ls.
Open another Terminal tab and run the following:
cat /tmp/ls_output.txt
It’s your applications directory output again, as expected!
There is also an option -i for stdin as well. First, type the following:
(lldb) target delete
This removes ls as the target. Next, type this:
(lldb) target create /usr/bin/wc
This sets /usr/bin/wc as the new target. wc can be used to count characters, words or lines in the input given to stdin.
You’ve swapped target executables for your LLDB session from ls to wc. Now you need some data to provide to wc. Open a new Terminal tab and enter the following:
echo "hello world" > /tmp/wc_input.txt
You’ll use this file to give wc some input.
Switch back to the LLDB session and enter the following:
(lldb) process launch -i /tmp/wc_input.txt
You’ll see the following output:
Process 24511 launched: '/usr/bin/wc' (x86_64)
1 2 12
Process 24511 exited with status = 0 (0x00000000)
This would be functionally equivalent to the following:
$ wc < /tmp/wc_input.txt
Sometimes you don’t want a stdin (standard input). This is useful for GUI programs such as Xcode, but doesn’t really help for Terminal commands such as ls and wc.
To illustrate, run the wc target with no arguments, like so:
(lldb) run
The program will just sit there and hang because it’s expecting to read something from stdin.
Give it some input by typing in hello world, press Return, then press Control + D, which is the end of transmission character. wc will parse the input and exit. You’ll see the same output as you did earlier when using the file as the input.
Now, launch the process like this:
(lldb) process launch -n
You’ll see that wc exits immediately with the following output:
Process 28849 launched: '/usr/bin/wc' (x86_64)
Process 28849 exited with status = 0 (0x00000000)
The -n option tells LLDB not to create a stdin; therefore wc has no data to work with and exits immediately.
Where to go from here?
There are a few more interesting options to play with (which you can find via the help command), but that’s for you to explore on your own time.
For now, try attaching to GUI and non-GUI programs alike. It might seem like you can’t understand much without the source code, but you’ll find out in the upcoming sections how much information and control you have over these programs.