The mystery of the stack frame

Call a subfunction and a new stack frame will be loaded into memory. When the subfunction completes, the current frame will go out of the stack. At run time, how to implement the stack frame out and on logic?

This is a very interesting question, is also an important knowledge point, it is the necessary skills for troubleshooting Crash.

ARM64 special register

Frame loading and unloading depend on three special registers, fp, LR and SP, which correspond to X29, X30 and X31 in ARM assembly

Special register role
LR (X30) Link Register Holds the address of the function called at the previous level
FP (X29) Frame Point points to the bottom of the stack and holds the address of the Frame
SP (x31) A Stack point refers to the top of the Stack and can be used for addressing
PC An address that points to the currently executing code. We cannot access the PC register
CPSR Status register. Different from the if else in the programming language, the assembly needs to control the execution of branches according to some states in the state register.

Case analysis

The following analysis is based on a Demo

void func2() { } int func1(int a, int b,int c) { int x = 7+18; int y = a + b + c; func2(); return y; } int main(int argc, char * argv[]) {func1(5,7,9); }Copy the code

Debug assembly code:

XCode is set to Debug->Debug Workflow->Always Show Disassembly, and then run the Demo to check the ARM64 assembly instructions for each method.

Main function assembly code

OCSimpleTest`main:
    0x104a5a008 <+0>:  sub    sp, sp, #0x20             ; =0x20 
    0x104a5a00c <+4>:  stp    x29, x30, [sp, #0x10]
    0x104a5a010 <+8>:  add    x29, sp, #0x10            ; =0x10 
    0x104a5a014 <+12>: stur   w0, [x29, #-0x4]
    0x104a5a018 <+16>: str    x1, [sp]
    0x104a5a01c <+20>: mov    w0, #0x5
    0x104a5a020 <+24>: mov    w1, #0x7
    0x104a5a024 <+28>: mov    w2, #0x9
->  0x104a5a028 <+32>: bl     0x104a59fd4               ; func1 at main.m:60
    0x104a5a02c <+36>: mov    w8, #0x0
    0x104a5a030 <+40>: mov    x0, x8
    0x104a5a034 <+44>: ldp    x29, x30, [sp, #0x10]
    0x104a5a038 <+48>: add    sp, sp, #0x20             ; =0x20 
    0x104a5a03c <+52>: ret    
Copy the code

Main function instruction parsing

Lines 5 through 7

The mov instruction assigns a value to a register. Func1, as a function of main, passes three parameters. Therefore, before switching func1, three parameters are stored in registers w0,w1, and w2.

Line 8

bl 0x102a41fd8 ; func1 at main.m:60

Bl is the jump instruction, jumping from main to the next function func1

Function A assembly code

OCSimpleTest`func1:
    0x10428dfb8 <+0>:  sub    sp, sp, #0x30             ; =0x30 
    0x10428dfbc <+4>:  stp    x29, x30, [sp, #0x20]
    0x10428dfc0 <+8>:  add    x29, sp, #0x20            ; =0x20 
    0x10428dfc4 <+12>: stur   w0, [x29, #-0x4]
    0x10428dfc8 <+16>: stur   w1, [x29, #-0x8]
    0x10428dfcc <+20>: stur   w2, [x29, #-0xc]
    0x10428dfd0 <+24>: mov    w8, #0x19
->  0x10428dfd4 <+28>: str    w8, [sp, #0x10]
    0x10428dfd8 <+32>: ldur   w8, [x29, #-0x4]
    0x10428dfdc <+36>: ldur   w9, [x29, #-0x8]
    0x10428dfe0 <+40>: add    w8, w8, w9
    0x10428dfe4 <+44>: ldur   w9, [x29, #-0xc]
    0x10428dfe8 <+48>: add    w8, w8, w9
    0x10428dfec <+52>: str    w8, [sp, #0xc]
    0x10428dff0 <+56>: bl     0x10428dfb4               ; func2 at main.m:58:1
    0x10428dff4 <+60>: ldr    w0, [sp, #0xc]
    0x10428dff8 <+64>: ldp    x29, x30, [sp, #0x20]
    0x10428dffc <+68>: add    sp, sp, #0x30             ; =0x30 
    0x10428e000 <+72>: ret       
  

Copy the code

Function A instruction parsing

Line 1

sub sp, sp, #0x30 ; =0x30

Sub is a subtraction instruction. The value of the SP register is offset by 48 bytes (0x30) to the lower address. At this point SP is already pointing to the top of the new frame.

Line 2

stp x29, x30, [sp, #0x20]

STP is a value store instruction that stores two values to store the values of the last stack frame FP register (X29) and LR register (x30) at a 32 byte offset (0x20) from the sp register address to the high address.

It is an important design to store the values of frame FP and LR, which can be read after the next function is executed to return to the original logic.

Because the stack grows from a high address to a low address, address offsets are always negative when pushed onto the stack. In ARM64, the registers are 64 bits, that is 8 bytes. Fp and LR are stored here, so the offset is 16 bytes.Copy the code
Think: which comes first and which comes last when fp_A or lrA is stored, and why?Copy the code

Line 3

add x29, sp, #0x20 ; =0x20

Add is the addition instruction. Set the FP (X29) register to point to the sp register offset 32 bytes to the high address (0x20).

At this point, the frame of function A has been laid out. Fp_A points to the bottom of the stack and sp_A points to the top of the stack, occupying 16 bytes. Pointers to fp and LR from the previous frame are stored before frame A and also take up 16 bytes.

Consider: why does stack frame A have only 32 bytes of space? The memory between FP and SP is mainly used to store the input parameters brought by the register and local variables in the function. Function A has three input arguments, each of which takes four bytes. Two local variables, 4 bytes each, 20 bytes in total. Memory is byte aligned, so a total of 32 bytes of space are requested.Copy the code
Consider: if function A has more than 10 input parameters of any type other than int, what is the space for stack frame?Copy the code

Line 16

ldr w0, [sp, #0xc]

LDR is a value instruction. Read out the value of sp offset 12 bytes (0xC) to the high address and store it in register W0. Sp +0xc stores the result of “a + b + C”, which is the result y returned by function A.

Line 17

ldp x29, x30, [sp, #0x20]

LDP is a value instruction that takes two values that offset sp by 32 bytes to the higher address and stores them in fp registers (X29) and LR registers (X30).

This corresponds to the second line, which fetches fp and LR from main

Line 18

ret

Function A unframes the stack and executes the instruction address pointed to by the LR register, that is, main jumps to the next line of fun_A.

summary

The main function calls the process of pushing function A

  1. The parameters passed to function A are stored in the register starting with w0
  2. Save main’s bottom pointer fp and return address LR.
  3. The fp and SP Pointers are offset to open up the frame space of function A

Function A completes the stack exit process

  1. Fetch the return value from memory and store it in register W0
  2. Fetch the FP and LR of main from memory
  3. Execute LR’s instructions

conclusion

This paper introduces the basic implementation of the function on and off the stack, which special registers are used specifically, and how to complete the management of the call stack step by step through simple ARM instruction operation. By understanding this process, we can appreciate the subtlety of Arm design and see the big picture when we troubleshoot problems.

In order to facilitate readers to understand, a previous article also introduced the basic concepts of stack, stack layout in memory, memory layout of the call stack. In-depth analysis of the memory layout of Crash call stack

reference

Introduction to the FP register and frame Pointer

ARM64 Function Calling Conventions

Stack Frames

Procedure Call Standard for the ARM 64-bit Architecture (AArch64)

ARM Reference Manual:

The article summary

IOS internal power series

[iOS internal functions] Crash analysis model

In-depth analysis of the memory layout of Crash call stack

[iOS internal functions] ARM black magic – stack frame on and off the stack

Architecture series

What do architects often mean by “technical architecture”?

How to build APP stability system?

Series of thinking

The “Work Mindset Model” of high Performance Engineers