A leaf function is a function that does not call other functions or change any non-volatile registers2. Leaf functions are usually simple operations, such as mathematical operations or logical judgments. The characteristic of leaf functions is that they can be expanded by simulating returns, that is, there is no need to save or restore the state of the register.
Non-leaf functions are functions that call other functions or change non-volatile registers. Non-leaf functions are usually complex operations such as loops or recursion. The characteristic of non-leaf functions is that they need to be annotated with static data to restore the state of the register when handling exceptions.
Non-volatile registers are registers that retain their contents after power is removed. Non-volatile registers are usually used to save some important data or status, such as program counter, stack pointer, return address, etc.
Volatile registers are registers that lose their contents when power is removed. Volatile registers are usually used to store some temporary data or operands, such as general-purpose registers, floating-point registers, vector registers, etc.
Non-volatile registers and volatile registers have different conventions when calling functions. Generally speaking, the called function needs to protect the value of the non-volatile register, that is, push it onto the stack before using it, and pop it off the stack before returning. The called function is free to use volatile registers without saving or restoring their values. This can reduce the overhead and complexity of function calls
When calling functions, you need to follow the following two rules:
- Caller save rule: Before a function call, the caller must save any temporary (t0–t6 and a0–a7) registers that are still needed after the call. After the call, it must restore these registers before using them.
- Callee save rule: Before the callee changes any of the reserved registers (s0–s11 and ra), it must save them. Before returning, it must restore these registers.
The figure above gives a code example of a non-leaf function f1 and a leaf function f2, and how they follow these rules. Non-leaf functions refer to functions that call other functions, and leaf functions refer to functions that do not call other functions.
f1 keeps i at s4 and x at s5; f2 keeps r at s4. f1 uses reserved registers s4, s5, and ra, so it initially pushes them onto the stack to follow the callee save rule. It uses t3 to hold the intermediate results (a–b) so that there is no need to reserve another register for this calculation.
Before calling f2, f1 saves a0 and a1 to the stack to follow the caller save rule because these are non-volatile registers and f2 may change them while f1 still needs them after the call. ra changed because it was overwritten by the call to f2. Although t3 is also a non-volatile register and f2 may overwrite it, f1 no longer needs t3 so it does not have to be saved.
f1 then passes the parameters to f2 in a0, makes a function call, and gets the result in a0. f1 then restores a0 and a1 since it still needs them. When f1 completes, it places the return value in a0, restores registers s4, s5, ra, and sp, and returns. f2 saves and restores s4 (and sp) according to callee save rules.
Upon closer inspection, you may notice that f2 did not modify a1, so f1 does not need to save and restore it. However, the compiler cannot always easily determine which non-volatile registers may have been disturbed during a function call. Therefore, a simple compiler always lets the caller save and restore any non-volatile registers it needs after the call. An optimizing compiler could observe that f2 is a leaf procedure and assign r to a non-volatile register, avoiding the need to save and restore s4. The following figure shows the stack during function execution. For this example, the stack pointer initially starts at 0xBEF7FF0C.