Arm64 stack backtrace
The structure of the AArch64 stack
Arm64 has four types of stacks: Empty Ascendant Stack (EA), Empty Descendant Stack (ED), Full Ascendant Stack (FA), and Full Descendant Stack (FD). The most commonly used is the Full Descendant Stack, which is also used by the Linux kernel.
The following figure is a schematic diagram of a full-reduction stack. The high address is the top of the stack, the low address is the bottom of the stack, and the stack grows toward the low address, as shown by the arrow on the right. The stack pointer SP points to the bottom of the stack (the bottom of the stack stores data). Every time a function call is made, a stack frame is formed in the stack. The stack stores a total of 4 stack frames (Stack Frame). Each stack frame consists of FP, LR and stack parameters (function parameters, function local variables, etc.). All stack frames in the stack can be regarded as a single necklace list. The stack frame at the lowest position of the stack is the head of the linked list, and the stack frame at the highest position of the stack is the tail of the linked list. The entire linked list uses FP indexes. When manually backtracing the stack, all stack frames can be indexed according to FP.
Register usage rules in the AArch64 procedure call standard
The following is how to use the general registers specified by the Arm64 procedure call standard.
-
Parameter register (X0-X7) When the number of function parameters is less than or equal to 8, they are passed using X0-X7. When the number is greater than 8, the excess are passed using the stack. When the function returns, the return value is saved in X0.
-
Temporary registers saved by the caller (X9-X15) If the caller uses the X9-X15 registers, it needs to save the X9-X15 registers to its own stack before calling the sub-function. The sub-function does not need to save and restore these registers when using them.
-
Registers saved by the callee (X19-X29) If the callee uses these registers, it needs to save them to its own stack and restore them from the stack when returning.
-
Special purpose registers
-
X8 is the indirect result register. It is used to pass the address location of an indirect result, for example, a function returns a large structure.
-
X16-X17 procedure call temporary register.
-
X18 platform registers.
-
X29 is the stack frame (FP) register, which stores the stack frame address of the calling function.
-
X30 stores the return address (LR). After the function returns, the program will jump to this address and execute.
Examples
The following figure shows the information printed when the kernel Oops occurs. The first picture shows the register information. The pc register and sp register play an important role in stack backtrace. The second picture shows the binary dump of the kernel thread irq/231-dwc3 stack data. Stack backtrace is to find the stack frame in these binary data and find the address of the called function.
The following figure shows the result of kernel stack backtracing. The address of the function where the exception occurred is saved in the exception stack, not in the kernel thread irq/231-dwc3 stack.
The function where the exception occurred can be obtained based on the pc register. This function is the first function in the stack backtrace. The sp register points to the FP1 register in the first stack frame, which is the address 0xffffffc0ee823b80. FP1 is offset 8 bytes to the higher address to get the LR1 register, which is the address 0xffffff80087369e4. This address is located in the dwc3_ep0_stall_and_restart function, which is the second function in the stack backtrace. FP1 points to the FP2 of the second stack frame. LR2 is found based on the stack frame, and so on. All the stack frames are finally shown in the figure below. A total of 7 stack frames are found. Therefore, there are a total of 8 function calls when the irq/231-dwc3 kernel thread has an exception, which is consistent with the function call relationship output by the kernel. It should be noted that the function is called in the code, but the symbol is not found in the stack backtrace. It must be the compiler optimization that inlines the function. Whether it is inlined can be confirmed by disassembly.