I am writing an unwinder for a Cortex-M (an ARM processor) running FreeRTOS. It mostly works, and I can trace the stack in many cases, but I have encountered a few issues that I haven’t been able to resolve.
-
Unwinding After an Error Interrupt:
I need to unwind after an error interrupt to retrieve the call trace. I’ve noticed that several registers are pushed between the address indicated by the frame pointer (
FP
) and the address where the next frame pointer is located. I don’t know how to determine the offset to retrieve the next frame pointer (and the link register,LR
, for backtrace). The number of pushed registers varies between compilations, likely due to optimizations.The offset between the two frame pointer
-
Stopping Condition for Unwinding:
When I hardcode the starting address, the unwinding seems mostly correct, but I am unsure when to stop the unwinder. I suspect it should stop at the program’s entry point, which I can retrieve in some way. However, in practice, it seems like it should stop when
LR = FP
. This is based on my observations, and I would appreciate any authoritative sources or explanations on this topic.Successfull unwinding by hand
I tried to use libgcc’s _Unwind_Backtrace
function with the compilation flag -funwind-tables
but it does not work great. So I decided to write the unwinder on my own :
unwind.h
#define CALL_STACK_SIZE 10u
// TODO : Change the registers to use an array for r0-r3
typedef struct {
uint32_t r0;
uint32_t r1;
uint32_t r2;
uint32_t r3;
uint32_t sp;
uint32_t lr;
uint32_t pc;
uint32_t xpsr;
uint32_t cfsr;
uint32_t hfsr;
uint32_t call_stack[CALL_STACK_SIZE];
} savedRegisters_t;
unwind.c
savedRegisters_t IN_CORE_DATA_SECTION saved_registers = {0};
void IN_CORE_TEXT_SECTION SaveCallStack() {
volatile uint32_t* call_stack_ptr = saved_registers.call_stack;
volatile uint32_t lr = 0;
volatile uint32_t fp = 0;
volatile uint32_t i = 0;
// Restore the `fp` value before the entry in `SaveCallStack` function from interrupt handler. (I don't know why is the offset 24 but it works)
// Warning /! : maybe it is compiler dependent)
__asm volatile (
"add %0, r7, #24"
: "=r" (fp)
);
do {
if (i == 1) {
// The same code as when `i` != 1 but with a different offset (hardcoded but this need to change to fit ARM interrupt logic)
__asm volatile (
"ldr r0, [%1, #12] n" // 12 is the offset that is hardcoded when i == 1
"mov %0, r0 n"
"ldr r1, [%1] n"
"mov %1, r1 n"
: "=r" (lr), "+r" (fp)
);
} else {
__asm volatile (
"ldr r0, [%1, #4] n" // Get LR at [FP] + 1
"mov %0, r0 n" // Save LR into `lr`
"ldr r1, [%1] n" // Get the new FP
"mov %1, r1 n" // Set it into `fp`
: "=r" (lr), "+r" (fp)
);
}
// Save lr into the array of `saved_registers`
call_stack_ptr[i] = lr;
i++;
} while (i < CALL_STACK_SIZE && fp != lr);
}
Any insights or references on these issues would be greatly appreciated.
Thank you!
Theo Bessel is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.