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

13. Assembly & the Stack
Written by Walter Tyree

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

When parameters passed into a function, sometimes they are passed in registers, and sometimes they are passed through the stack, and sometimes both! But what does being passed on the stack mean exactly? It’s time to take a deeper dive into what happens when a function is called from an assembly standpoint by exploring some “stack related” registers as well as the contents in the stack.

Understanding how the stack works is useful when you’re reverse engineering programs, since you can help deduce what parameters are being manipulated in a certain function when no debugging symbols are available.

Let’s begin.

The Stack, Revisited

As discussed previously in Chapter 6, “Thread, Frame & Stepping Around”, when a program executes, the memory is laid out so the stack starts at a “high address” and grows downward, towards a lower address; that is, towards the heap.

Note: In some architectures, the stack grows upwards. But for all Apple devices, the stack grows downwards.

Confused? Here’s an image to help clarify how the stack moves.

The stack starts at a high address. How high, exactly, is determined by the operating system’s kernel. The kernel gives stack space to each running program (well, each thread).

The stack is finite in size and increases by growing downwards in memory address space. As space on the stack is used up, the pointer to the “top” of the stack moves down from the highest address to the lowest address.

Once the stack reaches the finite size given by the kernel, or if it crosses the bounds of the heap, the stack is said to overflow. This is a fatal error, often referred to as a stack overflow. Now you know where your favorite website gets its name from!

Stack Pointer, Frame Pointer and Link Register

Two very important registers you’ve yet to learn about are the sp and lr. The stack pointer register, sp, points to the head of the stack for a particular thread. The head of the stack will grow downwards, so the sp will decrement when it’s time to make more space in the stack. The sp will always point to the head of the stack.

Stack Related Opcodes

So far, you’ve learned about the calling convention and how the memory is laid out, but haven’t really explored what the many opcodes actually do in arm64 assembly. It’s time to focus on several stack related opcodes in more detail.

The str and stp Opcode

When anything such as an int, Objective-C instance, Swift class or a reference needs to be saved onto the stack, the str opcode is used, or its cousin the stp opcode. The str opcode puts a single register on the stack while stp puts a pair of registers onto the stack.

str 0x00000005, [sp]

The ldr and ldp Opcodes

The ldr opcode is the exact opposite of the str opcode. ldr takes the value from the stack and stores it to a destination. You can guess what ldp is for, right? Unlike some other instruction sets, in ARM64, you don’t move the stack pointer to reclaim the space until the end of the function.

ldr x0, [sp, #0x8]

The ‘bl’ Opcode

The bl opcode is responsible for executing a function. bl stands for “branch with link”. It sets the lr register to the location of the next instruction in the calling function. Then bl jumps to the function memory location. When it returns, any return value from that function is in register x0. After bl jumps, the first thing you would expect the new function to do is to store the values of x29 and x30 to keep the stacks and frames all in sync.

0x7fffb34de913 <+227>: call   0x7fffb34df410            
0x7fffb34de918 <+232>: mov    edx, eax

The ‘ret’ Opcode

The ret opcode is the opposite of the bl opcode, in that it jumps to x30 or the link register. Thus execution goes back to where the function was called from.

Observing Registers in Action

Now that you have an understanding of the sp and lr registers, as well as some opcodes that manipulate the stack, it’s time to see it all in action.

override func awakeFromNib() {
  super.awakeFromNib()
  StackWalkthrough(42)
}
sub  sp, sp, #0x20         ; 1
stp  x29, x30, [sp, #0x10] ; 2
add  x29, sp, #0x10        ; 3
str  xzr, [sp, #0x8]
str  xzr, [sp]             ; 4
                           ; end of the function prologue
str  x0, [sp]              ; 5
mov  x0, #0xF0             ; 6
ldr  x0, [sp]              ; 7
                           ; start the epilogue
ldp x29, x30, [sp, #0x10]  ; 8
add sp, sp, #0x20          ; 9
ret                        ; 10

(lldb) register read x0 lr sp
x0 = 0x000000000000002a
lr = 0x4f238001044cb56c (0x00000001044cb56c) Registers`Registers.ViewController.awakeFromNib() -> () + 80 at ViewController.swift:65:11
sp = 0x000000016b936170
(lldb) si
(lldb) command alias dumpstack memory read $sp
(lldb) dumpstack
0x16b936170: b0 41 51 03 00 60 00 00 b0 41 51 03 00 60 00 00
0x16b936180: f0 46 4d 04 01 00 00 00 b0 41 51 03 00 60 00 00
0x16b936150: b0 41 51 03 00 60 00 00 00 e1 91 02 00 60 00 00
0x16b936160: 90 61 93 6b 01 00 00 00 6c b5 4c 04 01 80 23 4f
register read sp x29
sp = 0x000000016ce76150
fp = 0x000000016ce76160
0x16ce76150: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x16ce76160: 90 61 e7 6c 01 00 00 00 74 b5 f8 02 01 00 00 00
0x16ce76150: 2a 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x16ce76160: 90 61 e7 6c 01 00 00 00 74 b5 f8 02 01 00 00 00
fp = 0x000000016ce76160
sp = 0x000000016ce76150
lr = 0x0000000102f8b574  Registers`Registers.ViewController.awakeFromNib() -> () + 88 at ViewController.swift:67:3
fp = 0x000000016ce76190
sp = 0x000000016ce76150
lr = 0x0000000102f8b574  Registers`Registers.ViewController.awakeFromNib() -> () + 88 at ViewController.swift:67:3

The Stack and Extra Parameters

As described in Chapter 11, the calling convention for arm64 will use registers x0 - x7 for function parameters. When a function requires more parameters, the stack needs to be used.

_ = self.executeLotsOfArguments(one: 1, two: 2, three: 3,
                                four: 4, five: 5, six: 6,
                                seven: 7, eight: 8, nine: 9,
                                ten: 10)

0x102b3aed8 <+80>:  mov    x9, sp
0x102b3aedc <+84>:  mov    w8, #0x9
0x102b3aee0 <+88>:  str    x8, [x9]
0x102b3aee4 <+92>:  mov    w8, #0xa
0x102b3aee8 <+96>:  str    x8, [x9, #0x8]

The Stack and Debugging Info

The stack is not only used when calling functions, but it’s also used as a scratch space for a function’s local variables. Speaking of which, how does the debugger know which addresses to reference when printing out the names of variables that belong to that function?

(lldb) image dump symfile Registers
0x106aa0758:   Block{0x3000007db}, ranges = [0x100002f5c-0x100003404)
0x4f875cd88:     Variable{0x3000007f8}, name = "one", type = {0000000300001126} 0x0000600008E87BA0 (Swift.Int), scope = parameter, decl = ViewController.swift:92, location = DW_OP_fbreg -24
stur   x0, [x29, #-0x18]

(lldb) po one
(lldb) si
(lldb) po one

Key Points

  • Stack addresses go down towards zero. The function prologue will move the stack pointer down far enough to make room for the needs of the function.
  • When each new function begins, it stores sp and lr onto the stack so that it can get back to the right place when it’s done working.
  • The str and stp are odd in that the destination is at the end of the line of parameter registers. For most other opcodes, the first register after the opcode is the destination.
  • The function prologue and the function epilogue must match in how much the move the sp or the stack will become corrupt.
  • Look for xzr as a sign that the compiler is zeroing out some space so that new values that get stored don’t pick up any stray bits.
  • Xcode stores variables on the stack during a debug build so that the variables view values don’t accidentally get changed as the register values change.
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.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now