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

3. Attaching With LLDB
Written by Walter Tyree

Now that you’ve learned about the two most essential commands, help and apropos, it’s time to investigate all the ways LLDB can attach itself to a process.

As used in the previous chapters, the phrase LLDB “attaching” is actually a bit misleading. A program named debugserver — found in Xcode.app/Contents/SharedFrameworks/LLDB.framework/Resources/ for macOS — is responsible for “attaching” to a target process. It’s LLDB’s job to bring up and coordinate with debugserver.

Creating a Debuggee Program

To understand how LLDB attaches to a program, you need to have a simple program that you control — just in case you didn’t disable SIP in the first chapter.

Open Terminal and navigate to the global tmp directory:

$ cd /tmp

The contents of this directory are erased when your computer reboots. It’s a great spot for throwaway programs or making content you don’t need to stick around.

Use your favorite text editor to create a file named hello_world.swift. For simplicity, the steps are described using the nano editor.

$ nano hello_world.swift

Add the following Swift code:

import Foundation

print("hello, world!")
CFRunLoopRun()

To save the file in nano, use Control-O. To exit, use Control-X.

Exit the text editor, and compile the program:

$ swiftc hello_world.swift

Upon success, you’ll have an executable named hello_world that prints "hello, world!" and waits forever in a loop thanks to the CFRunLoopRun() call.

Test the hello_world program by typing:

$ ./hello_world

Your terminal window should display “hello, world!” and then nothing else. Use Control-C to terminate this program when the euphoria has worn off. You’ll use this program in a second to discover the different ways LLDB can attach to a process.

Attaching to an Existing Process

Now, you’ll see how to attach LLDB to an existing process. First, you’ll need to create a process to debug, and then you’ll attach to it.

In your terminal, launch hello_world in the background by typing the following:

$ ./hello_world &

The ampersand means hello_world executes as a background process. This allows you to continue using Terminal without blocking input. An alternative is to just use a different Terminal tab or window to launch the hello_world executable.

You now have a process to debug. Use LLDB to attach to this program via specifying the program name:

$ lldb -n hello_world

Your computer may ask for a password before continuing. Attaching to random processes can be dangerous, of course. Once it attaches, LLDB will pause the program and display some information about what the program was doing when you attached. For now, detach from the hello_world program by typing quit or simply q:

(lldb) q

LLDB will ask you if you’re sure you want to quit. Respond that you do want to quit the debugger, and you’ll return to the normal Terminal prompt.

Alternatively, you can attach to hello_world by providing the PID of a running program. Since you ran the program in the background earlier, the program’s PID was displayed to you when it launched.

Note: Just a reminder — if you didn’t disable SIP on your macOS computer, you won’t be able to attach LLDB to Apple applications. On more recent macOS versions, you won’t be able to attach to third-party apps released from the App Store either.

If you forgot the PID or you cleared your Terminal, you can find the hello_world executable’s PID with the process grep command:

$ pgrep -x hello_world

This will output the PID of every process named “hello_world”. Note that this can be more than one process.

Next, launch LLDB using the -p argument, replacing 57086 with the number output from the command above:

$ lldb -p 57086

This tells LLDB to attach to the process with the given PID. In this case, this is your running hello_world process. LLDB will again pause the program and show you where it was when LLDB attached.

Attaching to a Future Process

The previous command only addresses a running process. If the process you want to debug 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 without directly launching the process?

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.

Exit LLDB using q or quit or by pressing Control-D to get back to a Terminal prompt. Now, kill the hello_world process using the pkill command:

pkill hello_world

Once you’re back at the command line, type the following into Terminal:

$ lldb -n hello_world -w

This tells LLDB to attach to the process named hello_world whenever it launches next. Next, open a new Terminal tab, and execute a new instance of the hello_world program:

$ /tmp/hello_world

Switch back to your first Terminal tab, and you’ll notice LLDB has now attached itself to the newly created hello_world process.

Note: Though you killed the process before using the -w argument, this wasn’t completely necessary. LLDB watches for the next launch of the process. It just seemed like it would be less confusing this way.

Launching a Process Using LLDB

Another way to attach to a process is to specify the path to the executable and then launch the process from within LLDB:

$ lldb -f /tmp/hello_world

This launches LLDB and tells it that it will attach to the hello_world executable at some point.

Once you’re ready to begin the debug session, simply type the following into the LLDB session:

(lldb) process launch

You should see the — now familiar — attachment confirmation in LLDB.

Note: An interesting side effect is that stderr output — i.e., Swift’s print, Objective-C’s NSLog, C’s printf and company — is 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 in an LLDB session.

Close previous LLDB sessions, remove any running instances of hello_world via pkill hello_world, 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 -f option, LLDB automatically infers the first argument as the executable to launch and debug. When debugging Terminal executables, it can be helpful to type lldb $(which ls) (or equivalent), which is then translated to lldb /bin/ls.

You’ll see the following output:

(lldb) target create "/bin/ls"
Current executable set to '/bin/ls' (arm64e).

Since ls is a quick program — it launches, does its job, then exits — it’s a good specimen for this next part since you’ll run this program multiple times with different arguments to explore what each does.

On the other hand, you need to have disabled SIP using the instructions in Chapter 1, “Getting Started”, because you aren’t the owner of the ls executable. So, if you can’t or don’t want to disable SIP, you’ll just need to follow along without executing the commands.

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' (arm64e)
... # Omitted directory listing output
Process 7681 exited with status = 0 (0x00000000)

An ls process will launch in the directory you started in. To change the current working directory, tell LLDB where to launch with the -w option. Enter the following:

(lldb) process launch -w /Applications

This launches 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 and 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 57442 launched: '/bin/ls' (arm64e)
ls: ~/Desktop: No such file or directory
Process 57442 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. LLDB has a shortcut for this: Simply type run. To learn more about creating your own command shortcuts, check out Chapter 9, “Persisting & 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’d see that the ls command can display output in color so long as the color environment variable, CLICOLOR, is enabled. You also have the “color palette” environment variable LSCOLORS to tell how to display certain file types.

With a target in LLDB, you can launch and set a program with any combination of environment 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 won’t display the environment variables until the target runs 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. However, you can also just specify them at launch with the -E option from the process launch command!

Time to display the /usr/share directory in a garish color!

(lldb) process launch -E LSCOLORS=Db -E CLICOLOR=1  -- /usr/share

Wow! Doesn’t that just burn the eyes? Try a different color with the following:

(lldb) process launch -E LSCOLORS=Af -E CLICOLOR=1  -- /usr/share

This would be 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 to extract this information from executables later in this book.

stdin, stderr and stout

Using the launch options, you can control where to send the output of a program.

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' (arm64e)
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 application’s directory output again, as expected!

stdin, or standard input, also has a similar option, -i. To see it in action, 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 counts 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' (arm64e)
       1       2      12
Process 24511 exited with status = 0 (0x00000000)

This is functionally equivalent to the following:

$ wc < /tmp/wc_input.txt

Sometimes, you don’t want a stdin. 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 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' (arm64e)
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.

The curses Interface

If you’ve spent a lot of time debugging in Xcode, you might’ve become comfortable seeing the stack trace, the variables window and other data as you work with the debugger. In Terminal, you can use a curses-style GUI for a similar experience. At an LLDB prompt, type:

(lldb) gui

And a window will appear:

From here, you can step through code using the N key or step into code using S. You can also use the function keys to examine variables, stacks and frames.

To exit back to the regular LLDB console, press F1 to bring up the LLDB menu, and then press X to exit.

Key Points

  • Launch LLDB and attach to processes using -n, -p or -w switches.
  • Use the -f switch to launch LLDB and then explicitly launch the process from within LLDB.
  • Use target create and target delete to load and detach an executable to a running LLDB session.
  • Use process launch to launch a process from within LLDB.
  • Use -E flags to set environment variables for a target process.
  • Use -i and -o to control where a target process should get its input and output.
  • The run command is an alias for process launch --.
  • The gui command gives an Xcode-esque interface for when you’re using Terminal for your lldb session.

Where to Go From Here?

You can find more interesting options to play with via the help command, but that’s for you to explore on your own.

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 discover in the upcoming sections how much information and control you have over these programs.

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.