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

6. Thread, Frame & Stepping Around
Written by Walter Tyree

You’ve learned how to create breakpoints, how to print and modify values, as well as how to execute code while paused in the debugger. But so far, you’ve been left high and dry on how to move around in the debugger and inspect data beyond the immediate scope. It’s time to fix that!

In this chapter, you’ll learn how to move the debugger in and out of code while lldb has suspended a program.

This is a critical skill to have since you often want to inspect values as they change over time when entering or exiting snippets of code.

Stack 101

When a computer program executes, it stores values in the stack and the heap. Both have their merits. As an advanced debugger, you’ll need to have a good understanding of how these work. Right now, let’s take a brief look at the stack.

You may already know the whole spiel about what a stack is in computer science terms. In any case, it’s worth having an intro/refresher of how a process keeps track of code and variables when executing.

The stack is a LIFO (Last-In-First-Out) queue that stores references to your currently executing code. This LIFO ordering means that whatever is added most recently, is removed first. Think of a stack of plates. Add a plate to the top, and it will be the one you take off first.

The stack pointer points to the current top of the stack. In the plate analogy, the stack pointer points to that top plate, telling you where to take the next plate from, or where to put the next plate on.

Migrating the plates analogy to the stack in computer memory, now imagine the plates had Velcro and were attached to the ceiling. Each time you added a plate onto the last one, the stack would grow downwards towards the floor.

In this diagram, the high address is shown at the top (0xFFFFFFFF) and the low address is shown at the bottom (0x00000000) demonstrating that the stack grows downwards.

You’ll take an in depth look at the stack pointer and other registers in Chapter 13, “Assembly & the Stack”, but in this chapter you’ll explore various ways to step through code that is on the stack.

Examining the Stack’s Frames

You’ll continue to use the Signals project for this chapter.

Open the Signals project in Xcode. Next, add a symbolic breakpoint to MainViewController‘s viewWillAppear(_:) function. Be sure to honor the spaces in the function signature or else the breakpoint will not be recognized. You can always tell in the GUI if you have matched symbols, because the flag next to the name of your breakpoint will be filled in to show it’s active. If the breakpoint flag is just an outline, lldb hasn’t found a match and if it looks disabled, well, it is.

Signals.MainViewController.viewWillAppear

Alternatively, just set a regular breakpoint on the viewWillAppear signature of the MainViewController.swift file.

Remember from the last chapter, if you set a symbolic breakpoint it will break twice. Build and run the program. We want the break in the Swift context, so if you broke in the Objective-C context, type c or click the resume button. As expected, the debugger will pause the program on the viewWillAppear(_:) method of MainViewController. Next, take a look at the stack trace in the left panel of Xcode. If you don’t see it already, click on the Debug Navigator in the left panel or press Command-7, if you have the default Xcode keymap.

Make sure the three buttons in the bottom right corner are all disabled. These help filter stack functions to only functions you have source code for. Since you’re learning about public as well as private code, you should always have these buttons disabled so you can see the full stack trace.

Within the Debug navigator panel, the stack trace will appear, showing the list of stack frames, the first one being viewWillAppear(_:). Following that is the Swift/Objective-C bridging method, @objc MainViewController.viewWillAppear(_:). This method is automatically generated so Objective-C can reach into Swift code. This is that place your symbolic breakpoint always hits when you are making your symbolic breakpoints.

After that, there’s a few stack frames of Objective-C code coming from UIKit(Core). Dig a little deeper, and you’ll see some C++ code belonging to CoreAnimation. C++ frames have lots of ::’s in their names. Even deeper, you’ll see a couple of methods all containing the name CFRunLoop that belong to CoreFoundation. Finally, to cap it all off, is the main function (yes, Swift programs still have a main function, it’s just hidden from you).

The stack trace you see in Xcode is a pretty printed version of what lldb is doing underneath Xcode’s covers.

In the lldb console, type the following:

(lldb) thread backtrace

You’ll see a stack trace much like you see in Xcode’s Debug Navigator. You could also simply type bt if you wish, which does the same. bt is actually a different command and you can see the difference if you pull out your trusty friend, help.

bt and thread backtrace will display the entire stack trace, but you can also explore individual frames. Type the following into lldb:

(lldb) frame info

You’ll get output similar to the following:

frame #0: 0x0000000104a7edcc Signals`MainViewController.viewWillAppear(animated=false, self=0x0000000000000000) at MainViewController.swift:53

As you can see, this output matches the content found in the Debug navigator of Xcode. Using the lldb console gives you finer-grained control of what information you want to view. Remember, lots of what Xcode shows you is just a GUI wrapper around things you can find from the command line.

Taking a look back at the Debug navigator, you’ll see some numbers starting from 0 and incrementing as you go down the call stack. This numbering helps you associate which stack frame you’re looking at. Select a different frame in the stack by typing the following:

(lldb) frame select 1

Note: A shortcut to doing the exact same frame select 1 command is by just typing f 1.

Upon executing this lldb command, Xcode will jump to the @objc MainViewController.viewWillAppear(_:) bridging method, the method located at index 1 in the stack. A bridging method, also known as a thunk, is required because Swift has a different calling convention than Objective-C, C, C++. That is, the registers that are expected as input (or stored on the stack) need to be moved around. In order to work with non-Swift code, the compiler generates an intermediate function so Objective-C code can interact with the Swift code.

Since this method is auto compiled, you will not have the source code and will be stuck viewing the assembly of the @objc method.

Note: Depending on your macOS computer, you will have either an Intel or Apple Silicon CPU. The assembly for your computer will be different based upon that hardware. Both Intel and Apple Silicon machines can compile code for each other. Intel machines can run only Intel instructions (x86_64) whereas Apple Silicon machines can run both Apple Silicon instructions (arm64) and Intel instructions (x86_64). It runs x86_64 through the Rosetta 2 translation engine.

Depending on your computer’s hardware, your assembly will look similar to one of the following flavors.

Here is the ARM64 from an M1 Macbook Air:

Here is the same breakpoint in the same Xcode project with x86_64 assembly from an Intel Macbook Air:

Notice how the assembly looks different? You’ll take a much deeper dive into ARM64 assembly in later chapters of this book as Apple has almost completely moved away from x86_64.

No matter the macOS computer you have, take note of the green line in the assembly. Right before that line is the call[q] (x86_64) or the bl (ARM664) instruction that’s responsible for executing viewWillAppear(_:) you set a breakpoint on earlier.

Don’t let the assembly blur your eyes too much. You’re not out of the assembly woods just yet…

Stepping

There are 3 essential stepping actions you can do while a program is paused to execute one “chunk” of code and then re-suspend program execution. Through lldb, you can step over, step in, or step out of code.

Being able to step through code is great for understanding how variables change in your program and is a great way to verify logic is executing correctly.

Stepping Over

Stepping over allows you to step to the next code statement in a particular frame. This means if the current statement is calling another function, lldb will run until this function has completed and returned.

Let’s see this in action.

Type the following in the lldb console:

(lldb) run

This will relaunch the Signals program without Xcode having to recompile. This is a great trick if you have long build times for your app and you don’t want to wait for a new build to test unmodified code. :]

When the dialog box appears, click “Replace”. Xcode will launch a new instance of the Signals app and stop on your symbolic breakpoint as before.

Next, type the following:

(lldb) next

The debugger will move one line forward. This is how you step over. Simple, but useful!

Stepping In

Stepping in means if the next statement is a function call, the debugger will move into the start of that function and then pause again.

Relaunch the Signals program from lldb:

(lldb) run

Next, type the following:

(lldb) step

No luck. The program should’ve stepped in, because the line it’s on contains a function call (well, actually it contains a few!).

In this case, lldb acted more like a “step over” instead of a “step into”. This is because lldb will, by default, ignore stepping into a function if there are no debug symbols for that function. In this case, the function calls are all going into UIKit, for which you don’t have debug symbols.

There is a setting that specifies how lldb should behave when stepping into a function for which no debug symbols exist. Pause the app and execute the following command in lldb to see where this setting is held:

(lldb) settings show target.process.thread.step-in-avoid-nodebug

If true, then stepping in will act as a step over in these instances. You can either change this setting (which you’ll do in the future), or tell the debugger to ignore the setting, which you’ll do now. Type run to reload the app and when it hits your breakpoint, type the following into lldb:

(lldb) step -a0

This tells LLDB to step in regardless of whether you have the required debug symbols or not. Because you don’t have the debug symbols for this code, you’re just seeing memory addresses and assembly instructions. Don’t worry, in a few chapters all of what you’re seeing will make sense. Well, at least you’ll know what you’re looking at. :]

Stepping Out

Stepping out means a function will continue for its duration then stop when it has returned. From a stack viewpoint, execution continues until the stack frame is popped off. If you’re having a hard time visualizing this, remember the plates attached to the ceiling. A plate is removed and the stack pointer is now higher as it’s closer to the ceiling.

Run the Signals project again, and this time when the debugger pauses, take a quick look at the stack trace. Next, type the following into lldb:

(lldb) finish

If you’re at the top of the stack, the app will just resume. With the app running, try setting a breakpoint at tableView(_:numberOfRowsInSection:), which is line 87 of MainViewController.swift in the Starter project. This will likely pause the application with a more interesting stack trace.

Remember, by simply pressing Enter, lldb will execute the last command you typed. The finish command will instruct lldb to step out of the current function.

Pay attention to the stack frames in the left panel as you step back from tableView(_:numberOfRowsInSection:) to the handleNotification(notification:) function that called it and then even into the code that was watching for the SIGSTOP.

Run the app again and this time, instead of using finish try the thread return command. Notice on the stack trace that you’re popping each frame and diving down one level. Eventually you’ll wind up at main.

Stepping in the Xcode GUI

Although you get much more fine-grained control using the console, Xcode already provides these options for you as buttons just above the lldb console. These buttons appear when an application is running.

They appear, in order, as step over, step in, and step out.

Finally, the step over and step in buttons have one more cool trick. You can manually control the execution of different threads, by holding down Control and Shift while clicking on these buttons.

This will result in stepping through the thread on which the debugger is paused, while the rest of the threads remain paused. This is a great trick to have in your toolbox if you are working with some hard-to-debug concurrency code like networking or something with Grand Central Dispatch.

Of course lldb has the command line equivalent to do the same from the console by using the --run-mode option, or more simply -m followed by the appropriate option.

Examining Data in the Stack

A very interesting option of the frame command is the frame variable subcommand. This command will take the debug symbol information found in the headers of your executable, or a dYSM if your app is stripped… more on that later, and dump information for that particular stack frame into the console. Thanks to the debug information, the frame variable command can easily tell you the scope of all the variables in your function as well as any global variables within your program using the appropriate options.

Run the Signals project again and make sure you hit the viewWillAppear(_:) breakpoint. Next, navigate to the top of the stack if you’re not already there by either clicking on the top stack frame in Xcode’s Debug Navigator or by entering frame select 0 in the console, or use LLDB’s shorthand command f 0.

Next, type the following:

(lldb) frame variable

You’ll get output similar to the following:

(Bool) animated = false
(Signals.MainViewController) self = 0x0000000138f0a770 {
  UIKit.UITableViewController = {
    baseUIViewController@0 = {
      baseUIResponder@0 = {
        baseNSObject@0 = {
          isa = Signals.MainViewController
        }
      }
      _overrideTransitioningDelegate = 0x0000000000000000
      _view = some {
        some = 0x000000013a016e00 {
          baseUIScrollView@0 = {
            baseUIView@0 = {
              baseUIResponder@0 = {
... etc ...

This dumps the variables available to the current stack frame and line of code. It’ll also dump all the instance variables, both public and private (if available), from the current variables.

Note: A shortcut for frame variable is v.

If you add a variable name after frame variable, for example frame variable self, then it will only print out that particular object. Just like p & po, there’s a frame variable -O --, with a shortcut of vo, which prints a simplified description of a variable.

p & v default to listing all instance variables inside an object, while po & vo print a simplified description of an object.

Remember, lldb has to compile code based upon the expression it’s executing. vo doesn’t compile and execute code, which allows for significant boost in lldb speed when a developer is only interested in dumping information about an object. That is why using vo has gained traction for debugging in Swift over the last several years due to the fact that vo doesn’t require code to be compiled.

To sum up: Use po when executing code and use vo when extracting information about an object.

Xcode also uses frame variable inside of its debugger windows. You, being the observant reader you are, might notice the output of frame variable also matches the content found in the Variables view, the panel to the left of the console window.

If it’s not already, expand the Variables View by clicking on the left icon in the lower right corner of Xcode. You can compare the output of frame variable to the Variables View. You might notice frame variable will actually give you more information about the ivars of Apple’s private API than the Variables view will.

As you can see, this is an attractive way to explore public variables when working with Apple’s frameworks.

You are encouraged to look at the options of frame variable (via a help v) on your own time and find an output format that is easiest for you workflow. Below are some highlighted options and their output

Key Points

  • In a stack trace, each level is called a frame.
  • The thread backtrace and frame info commands provide similar information to Xcode’s Debug navigator pane.
  • frame select lets you switch lldb to a different frame in the stack trace.
  • The run command relaunches your application without recompiling it.
  • Use the next command to advance your app by one line.
  • The step command will step into a function. Add the -a0 switch to step into functions that are not your code.
  • Use finish or thread return to step out of a function.
  • frame variable, v and vo are all ways to dump information about the current frame into the console.

Where to Go From Here?

In this chapter, you’ve explored stack frames and the content in them. You’ve also learned how to navigate the stack by stepping in, out, and over code.

There are a lot of options in the thread command you didn’t cover. Try exploring some of them with the help thread command, and seeing if you can learn some cool options.

Take a look at the thread until and thread jump subcommands for navigating the stack. You’ll use them later, but they are fun commands so give them a shot now to see what they do!

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.