Chapters

Hide chapters

Advanced Apple Debugging & Reverse Engineering

Third Edition · iOS 12 · Swift 4.2 · Xcode 10

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Section III: Low Level

Section 3: 7 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 Derek Selander

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. It’s time to fix that!

In this chapter, you’ll learn how to move the debugger in and out of functions while LLDB is currently paused.

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 a basic understanding (or refresher) of how a process keeps track of code and variables when executing. This knowledge will come in handy as you’re using LLDB to navigate around code.

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.

In this diagram, the high address is shown at the top (0xFFFFFFFF) and the low address is shown at the bottom (0x00000000) showcasing the stack would grow downwards.

Some illustrations like to have the high address at the bottom to match with the plate analogy as the stack would be shown growing upwards. However, I believe any diagrams showcasing the stack should be shown growing downwards from a high address because this will cause less headaches later on when talking about offsets from the stack pointer.

You’ll take an in depth look at the stack pointer and other registers in Chapter 13, “Assembly and 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.

You’ll glimpse some assembly in this chapter. Don’t get scared! It’s not that bad. However, be sure to use the iPhone X Simulator for this chapter since the assembly will be different if you were to generate the code on say, an actual iOS device.

This is because a device uses the ARM architecture, whereas the simulator uses your Mac’s native instruction set, x86_64 (or i386 if you are compiling on something lower than the iPhone 5s Simulator).

Open the Signals project in Xcode. Next, add a symbolic breakpoint with the following function name. Be sure to honor the spaces in the function signature or else the breakpoint will not be recognized.

Signals.MasterViewController.viewWillAppear(Swift.Bool) -> ()

This creates a symbolic breakpoint on MasterViewController’s viewWillAppear(_:) method.

Build and run the program. As expected, the debugger will pause the program on the viewWillAppear(_:) method of MasterViewController. 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 (alternatively, 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 MasterViewController.viewWillAppear(Bool) -> ():. This method is automatically generated so Objective-C can reach into Swift code.

After that, there’s a few stack frames of Objective-C code coming from UIKit. Dig a little deeper, and you’ll see some C++ code belonging to CoreAnimation. 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 simply a pretty printed version of what LLDB can tell you. Let’s see that now.

In the LLDB console, type the following:

(lldb) thread backtrace

You could also simply type bt if you wished, which does the same. It’s actually a different command and you can see the difference if you pull out your trusty friend, help.

After the command above, you’ll see a stack trace much like you see in Xcode’s Debug Navigator.

Type the following into LLDB:

(lldb) frame info

You’ll get a bit of output similar to the following:

frame #0: 0x000000010ba1f8dc Signals`MasterViewController.viewWillAppear(animated=false, self=0x00007fd286c0af10) at MasterViewController.swift:50

As you can see, this output matches the content found in the Debug Navigator. So why is this even important if you can just see everything from the Debug Navigator? Well, using the LLDB console gives you finer-grained control of what information you want to see. In addition, you’ll be making custom LLDB scripts in which these commands will become very useful. It’s also nice to know where Xcode gets its information from, right?

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 stack by typing the following:

(lldb) frame select 1

Xcode will jump to the @objc bridging method, the method located at index 1 in the stack. What’s an @objc bridging method? It’s a method that’s generated by the Swift compiler to interact with Objective-C’s dynamic nature. In earlier versions of Swift (Swift <= 3.2) any NSObject implied @objc bridging methods being generated. With the default build settings in Swift 4, even an Objective-C NSObject needs to have @objc (or @objcMembers) attribute for the Swift compiler to generate the bridging methods.

Provided you’re using the Simulator and not an actual device, you’ll get some assembly looking similar to the following.

Take note of the green line in the assembly. Right before that line is the callq instruction that is 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

When mastering LLDB, the three most important navigation actions you can do while the program is paused revolve around stepping through a program. Through LLDB, you can step over, step in, or step out of code.

Each of these allow you to continue executing your program’s code, but in small chunks to allow you to examine how the program is executing.

Stepping over

Stepping over allows you to step to the next code statement (usually, the next line) in the context where the debugger is currently paused. 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. Neat! Xcode will 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.

Let’s see this in action.

Relaunch the Breakpoints 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, however, a setting that specifies how LLDB should behave when stepping into a function for which no debug symbols exist. 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 the following into LLDB:

(lldb) step -a0 

This tells LLDB to step in regardless of whether you have the required debug symbols or not.

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.

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

You’ll notice that the debugger is now paused one function up in the stack trace. Try executing this command a few more times.

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 they disappear one by one.

Stepping in the Xcode GUI

Although you get much more finer-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 the back of 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 out for that particular stack frame. 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 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.MasterViewController) self = 0x00007fb3d160aad0 {
  UIKit.UITableViewController = {
    baseUIViewController@0 = <extracting data from value failed>

    _tableViewStyle = 0
    _keyboardSupport = nil
    _staticDataSource = nil
    _filteredDataSource = 0x000061800005f0b0
    _filteredDataType = 0
  }
  detailViewController = nil
}

This dumps the variables available to the current stack frame and line of code. If possible, it’ll also dump all the instance variables, both public and private, from the current available variables.

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.

Next, type the following:

(lldb) frame variable -F self 

This is an easier way to look at all the private variables available to MasterViewController. It uses the -F option, which stands for “flat”.

This will keep the indentation to 0 and only print out information about self, in MasterViewController.swift.

You’ll get output similar to the truncated output below:

self = 0x00007fff5540eb40
self =
self =
self =
self = {}
self.detailViewController = 0x00007fc728816e00
self.detailViewController.some =
self.detailViewController.some =
self.detailViewController.some = {}
self.detailViewController.some.signal = 0x00007fc728509de0

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

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, thread jump, and thread return subcommands. 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.