13.
Assembly & the Stack
Written by Walter Tyree
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.
Here’s a visual of the stack pointer changing when a function is called.
In the above image, the sequence of the stack pointer follows:
- The stack pointer currently points to Frame 3.
- The code pointed to by the instruction pointer register calls a new function. The stack pointer gets updated to point to a new frame, Frame 4, which is potentially responsible for scratchspace and data inside this newly called function from the instruction pointer.
- Execution is completed in Frame 4 and control resumes back in Frame 3. The stack pointer’s previous reference to Frame 4 gets popped off and resumes pointing to Frame 3.
The frame pointer is another important register and is related to the stack pointer. While the stack pointer points to where the head of the stack currently is, the frame pointer points to a location above the stack pointer just below any space the function used for saving registers in the prologue.
The other important register, the link register, lr. It points to the next line to be executed after this function is done. lr is actually just a convenience name for the x30 register.
The interesting thing here is the previous contents of the x29 and x30 are stored on the stack before it’s set to the value of the current function. This is the first thing that happens in the function prologue. You can traverse the stack just by knowing the value in the link register. A debugger does this when it shows you the stack trace.
Note: Some systems don’t use a link register, and it’s possible to compile your application to omit using the link register. The logic is it might be beneficial to have an extra register to use. But this means you can’t unwind the stack easily, which makes debugging much harder.
Note: When you jump to a different stack frame by clicking on a frame in Xcode or using LLDB, both the
sp&lrregisters will change values to correspond to the new frame! This is expected because local variables for a function use offsets ofspto get their values.If the
spdidn’t change, you’d be unable to print local variables to that function, and the program might even crash. This might result in a source of confusion when exploring thelr&spregisters, so always keep this in mind. You can verify this in LLDB by selecting different frames and typingcpx $lrorcpx $spin the LLDB console.
So why are these two registers important to learn about? When a program is compiled with debug information, the debug information references offsets from the stack pointer register to get a variable. These offsets are given names, the same names you gave your variables in your source code.
When a program is compiled and optimized for release, the debug information that comes packaged into the binary is removed. Although the names to the references of these variables and parameters are removed, you can still use offsets of the stack pointer and base pointer to find the location of where these references are stored.
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.
To see at a concrete example, consider the following opcode:
str 0x00000005, [sp]
This stores the value of 5 at the location pointed to by the stack pointer. It’s your responsibility as the coder to ensure there is room on the stack for the value.
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.
Below is an example of ldr:
ldr x0, [sp, #0x8]
This stores the value of the sp register offset by 0x8 into the x0. The ARM64 layout really wants things to stay in alignment for efficiency, so you’ll often see offsets of 0x8, 0x10, 0x20 as different values are pulled from the stack.
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.
Imagine a function at 0x7fffb34df410 in memory like so:
0x7fffb34de913 <+227>: call 0x7fffb34df410
0x7fffb34de918 <+232>: mov edx, eax
When an instruction is executed, first the pc register (program counter) is incremented, then the instruction is executed. So, when the call instruction is executed, the pc register will increment to 0x7fffb34de918, then execute the instruction pointed to by 0x7fffb34de913. Since this is a call instruction, the pc register is pushed onto the stack (just as if a push had been executed) then the pc register is set to the value 0x7fffb34df410, the address of the function to be executed.
From there, execution continues at the location 0x7fffb34df410.
Computers are pretty cool, aren’t they?
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.
Now that you have a basic understanding of these four important opcodes, it’s time to see them in action.
It’s very important to have all stp opcodes in your function prologue match your ldp opcodes in your function epilogue, or else the stack will get out of sync. For example, if there was no corresponding ldp for a stp, when the ret happened at the end of the function it would jump to the wrong location. Execution would return to some random place, potentially not even a valid place in the program.
Fortunately, the compiler will take care of synchronizing your stp and ldp opcodes when it compiles your Swift or Objective-C into assembly. You only need to worry about this when you’re writing your own assembly.
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.
In the Registers application lives a function named StackWalkthrough(int). This C function takes one integer as a parameter and is written in assembly and is located in StackWalkthrough.s. Open this file and have a look around; there’s no need to understand it all just now. You’ll learn how it works in a minute.
This function is made available to Swift through a bridging header Registers-Bridging-Header.h, so you can call this method written in assembly from Swift.
Now to make use of this.
Open ViewController.swift, and add the following below viewDidLoad():
override func awakeFromNib() {
super.awakeFromNib()
StackWalkthrough(42)
}
This will call StackWalkthrough with a parameter of 42. The 42 is simply a value used to show how the stack works. 42 in hex is 0x2a, so you’ll be looking for that in the memory dumps.
Before you begin, here is Stackwalkthrough’s code:
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
Here’s what’s going on in the code:
- Make room in the stack to store four 8-byte things. Notice you’re
subtracting from the pointer value. - Now, store a pair of values, the old frame pointer and the link register to the stack at the end of the room you’ve just made for things.
- Add 0x10 to the stack pointer and store it in
x29, the frame pointer. This therefore sets the frame pointer to the end of where registers were saved to. - Clear out the space in the remainder of the allocated stack space. Remember you just moved the stack pointer, who knows what values existed at these locations.
zxris equal to0x0but makes the code easier to reason. You are clearing out the space, you’re not setting a value of zero to be used in some maths. - Store our function argument to the tip of the stack.
- Put the value of
0xF0into registerx0, overwriting whatever was there. - Now put the value from head of the stack into register
x0, overwriting whatever was there. - Replace the frame pointer and the link register.
- Remove the room in the stack for the four 8-byte things (Opposite of what you did on the first line)
- Jump to the value of the link register.
Read it through and try to understand it if you can. You’re already familiar with the mov instruction, and the rest of the assembly consists of function related opcodes you’ve just learned about.
This function takes the integer parameter passed into it as you’ll recall, the first parameter is passed in x0, and pushes this parameter onto the stack. x0 is then set to 0xF0, then the value popped off the stack is stored back into the x0 register. It’ll be a sure hit in the App Store. :]
Make sure you have a good mental understanding of what is happening in this function, as you’ll be exploring the registers in LLDB next.
Back in Xcode, create a breakpoint using Xcode’s GUI on the StackWalkthrough(42) line in the awakeFromNib function of ViewController.swift. Also create a symbolic breakpoint on StackWalkthrough, since you’ll want to stop at the beginning of the StackWalkthrough function when exploring the registers.
Build and run and wait for the GUI breakpoint to trigger.
Now click Debug ▸ Debug Workflow ▸Always Show Disassembly, to show the disassembly. You’ll be greeted with exciting looking stuff!
Wow! Look at that! You’ve landed right on a bl opcode instruction. Do you wonder what function you’re about to enter?
Note: If you didn’t land right on the
blinstruction using the Xcode GUI breakpoint, you can either use LLDB’s thread step-inst or more simply, si to single step through assembly instructions. Alternatively, you can create a GUI breakpoint on the memory address thatbls theStackWalkthroughfunction.
Recall that Stackwalkthrough takes an argument and that simple arguments get passed in using the registers. Confirm that x0 contains the value of 42, or anything you changed it to:
(lldb) register read x0 lr sp
You should get some output like this:
x0 = 0x000000000000002a
lr = 0x4f238001044cb56c (0x00000001044cb56c) Registers`Registers.ViewController.awakeFromNib() -> () + 80 at ViewController.swift:65:11
sp = 0x000000016b936170
The x0 register contains the argument for the function, sp is pointing to the top of the stack for the current function and lr is pointing to where this frame will return.
In LLDB, type the following:
(lldb) si
This is an alias for thread step-inst, which tells LLDB to execute the next instruction and then pause the debugger. You’ve now stepped into StackWalkthrough.
From here on out, you’ll step through every assembly instruction while monitoring the stack memory. You’ll also check the values of some of the registers. To help with this, type the following into LLDB:
(lldb) command alias dumpstack memory read $sp
This creates the command dumpstack that will dump the top four addresses of the stack. Execute dumpstack now:
(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
Since you are at the very beginning of the function, sp is still pointing to the location it had back in awakeFromNib.
Now type si in the lldb console to make some room on the stack for this function to work.
Type dumpstack again. Notice that the memory addresses have decreased by 20.
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
What’s in the memory addresses is meaningless. Sometimes you’ll see zeros, sometimes repeating patterns of noise, sometimes recognizable data. As the stack pointer moves up and down during program execution, the same parts of memory get used over and over.
Execute si again to store the old lr and sp values to the stack. Recall that x29 holds the frame pointer and local changes to the stack pointer are done with offsets from sp.
Type dumpstack again and then confirm that the lr and sp values are stored safely away. Use register read to match up the values.
The next step in the function prologue is to put the local sp value into x29, the frame pointer, so that if another bl happens somewhere in this function, the chain of frames will maintain references to each other. Type si again to execute that command. Then type:
register read sp x29
You should see something similar to the following:
sp = 0x000000016ce76150
fp = 0x000000016ce76160
The x29 register is pointing to the end of the stack for this frame and sp is pointing to the working area. The last part of the prologue is to clean any old bits out of the stack. Type si two times to execute the two str xzr commands. Then type dumpstack again. Look at this nice stack, ready to work:
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
Your green line breakpoint should be pointing to the str x0, [sp] which is the first line of actual work for the function. Type si to store the argument from x0 into the stack. Then type dumpstack and look for your argument.
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
The str command made a copy of the value. You could look in register x0 and it would still hold 2a. Type si again to execute the next command and replace the value in x0 with the constant, 0xF0. Now type register read x0 to confirm it holds the new value. Type si again to pull the value off of the stack and put it in x0.
Type register read x0 and confirm that 0xF0 has been replaced with 2a from the stack. The work is done in the function, now on to the epilogue to clean everything up. Type register read x29 sp x30 to see where things are now:
fp = 0x000000016ce76160
sp = 0x000000016ce76150
lr = 0x0000000102f8b574 Registers`Registers.ViewController.awakeFromNib() -> () + 88 at ViewController.swift:67:3
Now type si again and then execute the register read x29 sp 30 command again.
fp = 0x000000016ce76190
sp = 0x000000016ce76150
lr = 0x0000000102f8b574 Registers`Registers.ViewController.awakeFromNib() -> () + 88 at ViewController.swift:67:3
The frame pointer is now pointing to where it was when this function started. The last step is to reset the sp value. Type si again to execute the command to reset the sp. Before you type si one last time to leave this function, notice that we’re not going to do anything to clean up changes we made to the stack. But, type si a final time and jump back to awakeFromNib.
Wowza! That was fun! A simple function, but it illustrates how the stack works through stp, ldp and ret instructions.
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.
Note: The stack may also need to be used when a large struct is passed to a function. Each parameter register can only hold 8 bytes (on 64-bit architecture), so if the struct needs more than 8 bytes, it will need to be passed on the stack as well. There are strict rules defining how this works in the calling convention, which all compilers must adhere to.
Open ViewController.swift and find the function named executeLotsOfArguments(one:two:three:four:five:six:seven:eight:nine:ten:). You used this function in Chapter 11 to explore the registers. You’ll use it again now to see how parameters 7 and beyond get passed to the function.
Add the following code to the end of viewDidLoad:
_ = self.executeLotsOfArguments(one: 1, two: 2, three: 3,
four: 4, five: 5, six: 6,
seven: 7, eight: 8, nine: 9,
ten: 10)
Next, using the Xcode GUI, create a breakpoint on the line you just added. Delete the other breakpoints if you don’t want to experience the glory of StackWalkthrough again. Build and run the app, and wait for this breakpoint to hit. You should see the disassembly view again, but if you don’t, use the Always Show Disassembly option.
As you’ve learned in the Stack Related Opcodes section, bl is responsible for the execution of a function. There’s only one bl opcode between where the app is paused right now and the start of viewDidLoad’s function epilogue, this means this bl must be the one responsible for calling executeLotsOfArguments(one:two:three:four:five:six:seven: eight:nine:ten:).
But what are all the rest of the instructions before bl? Let’s find out.
These instructions set up the stack as necessary to pass the additional parameters. You have your parameters being put into the appropriate registers, as seen by the mov instructions for each of the values. Notice that because you’re passing small values for the Int that the compiler is using w sized registers so it can go faster.
But parameters nine and ten need to be passed on the stack. This is done with the following instructions:
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]
Looks scary, doesn’t it? I’ll explain.
The brackets containing x9 and an optional value indicate reading from a memory location, just like *, the de-referencing operator would do in C programming. The first line above says “put sp into x9.” The second line says “put 0x9 into the lower part of x8, w8”. The third line says “put x8 into the memory address pointed to by x9”. The process then repeats but puts x8 into the memory address pointed to by x9 plus 0x8. And so on. ARM uses x8 and x9 as scratch space as it’s moving things around. x9 gets assigned the value of sp because the compiler doesn’t want to accidentally move sp.
You can easily determine if extra scratch space is allocated for a stack frame by looking for the very first instruction in the function prologue. For example, click on the viewDidLoad stack frame and scroll to the top. Observe how much scratch space has been created:
The compiler has allocated 64 bytes. 16 of those bytes will be used to store the sp and lr, which leaves 48 bytes of space for it to work with.
Time to look at this scratch space in more depth.
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?
Let’s find out!
Clear all the breakpoints you’ve set and create a new Symbolic breakpoint on executeLotsOfArguments.
Build and run the app, then wait for the breakpoint to hit.
As expected, control should stop at the ever-so-short name of a function: executeLotsOfArguments(one:two:three:four:five:six:seven:eight:nine:ten:), from here on, now referred to as executeLotsOfArguments, because its full name is a bit of a mouthful!
In the lower right corner of Xcode, click on Show the Variables View:
From there, look at the value pointed at by the one variable… it definitely ain’t holding the value of 0x1 at the moment. This value seems to be gibberish!
Why is one referencing a seemingly random value?
The answer is stored by the DWARF Debugging Information embedded into the debug build of the Registers application. You can dump this information to help give you insight into what the one variable is referencing in memory.
In LLDB, type the following:
(lldb) image dump symfile Registers
You’ll get a crazy amount of output. Search for (Cmd + F) the word “one”; include the quotes within your search.
Below is a (very) truncated output that includes the relevant information:
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
Based upon the output, the variable named one is of type Swift.Int, found in executeLotsOfArguments, whose location can be found at DW_OP_fbreg -24. This rather obfuscated code actually means frame pointer minus 24, i.e. x29 - 24. Or in hexadecimal, x29 - 0x18.
This is important information. It tells the debugger the variable called one can always be found in this memory address. Well, not always, but always when that variable is valid, i.e. it’s in scope.
You may wonder why it can’t just be x0, since that’s where the value is passed to the function, and it’s also the first parameter. Well, x0 may need to be reused later on within the function, so using the stack is a safer bet for storage.
The debugger should still be stopped on executeLotsOfArguments. Make sure you’re viewing the Always Show Disassembly output and hunt for the assembly:
stur x0, [x29, #-0x18]
Once you’ve found it in the assembly output of executeLotsOfArguments, create a breakpoint on this line of assembly. You may have found a false hit a little earlier that contains xzr. Can you remember why that might be?
Continue execution so LLDB will stop on this line of assembly.
Try printing out the output of one in LLDB:
(lldb) po one
Gibberish, still. Hmph.
Remember, x0 will contain the first parameter passed into the function. So to make the debugger be able to see the value that one should be, x0 needs to be written to the address where one is stored. In this case, x29 - 0x18.
Now, perform an assembly instruction step in LLDB:
(lldb) si
Print the value of one again.
(lldb) po one
Awwww…. yeah! It’s working! The value one is referencing is correctly holding the value 0x1.
You may be wondering what happens if one changes. Well, x29 - 0x18 needs to change in that case too. This would potentially be another instruction needed to write it there as well as wherever the value is used. This is why debug builds are so much slower than release builds.
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
spandlronto the stack so that it can get back to the right place when it’s done working. - The
strandstpare 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
spor the stack will become corrupt. - Look for
xzras 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.