如何在 Cortex M4 中保存来自异常处理程序的调用堆栈?
How can I save the call stack from an exception handler in Cortex M4?
这是我想要实现的目标:每当我收到硬故障或看门狗中断时,我会将前一条指令的地址保存到某个 RAM 位置,该位置将在复位后继续存在。
Kinetis M64 看门狗在发出重置之前给了我 256 CPU 个周期,这应该足以保存一些东西。问题是我在哪里可以找到这些地址?当一个 IRQ 发生时,LR 保存一个异常值而不是实际的 return 地址。
我想在不连接 SWD 探针的情况下执行此操作,这样设备可以在出现问题时自行报告。
我认为,你能做的最好的事情就是找到处理程序将 return 到的指令的地址,如果它被允许 return。这通常是导致错误的指令之后的指令,但不能保证这一点(例如,如果错误是由分支指令引起的)。
在进入处理程序时,link 寄存器包含一个代码,它告诉您异常发生时正在使用哪个堆栈。例如,参见 here 的 Cortex-M4。
就在分支到异常处理程序之前,CPU 将推送 r0-r3
、r12
、LR
(r14
)、PC
(r15
) 和 xPSR
到活动堆栈。如果你的设备有一个 FPU 并且它被启用,浮点上下文也可能被推送,或者 space 留给它。 ARM 堆栈是完全递减的,寄存器按寄存器编号的升序存储在升序内存地址中,因此在进入异常处理程序时,无论使用哪个堆栈指针,都将指向 r0
的堆栈值;因此,按理说,上面的 6 个字(24 字节)将是 PC
的堆栈值,这是异常处理程序的 return 地址。
所以在导致错误的指令之后查找指令的过程,假设它不是由分支引起的,是:
- 检查
LR
找出正在使用的堆栈
- 将适当的堆栈指针加载到空闲寄存器中(
r0-r3
全部可用,因为它们在进入处理程序时被推送)
- 读取此堆栈指针上方 24 个字节的字以找到处理程序的 return 地址
导致错误的指令是位于此 return 地址之前的 2 字节还是 4 字节当然取决于指令,Thumb-2 指令集混合了 16 位和 32 位。当然,它可能完全位于其他地方!
请记住,如果 MSP
在故障之前正在使用,则处理程序将使用相同的堆栈,并且所有这些只有在没有任何内容被推送到堆栈时才会起作用处理函数的序言。最简单的方法可能是用汇编语言编写处理程序。它总是可以在处理完堆栈后调用 C 函数来完成您想要的任何终止过程。
最后一件事,可能还值得保存 LR
的堆叠值。如果 PC
的堆叠值告诉您没有任何用处(例如,因为它为零,代码试图跳转到无效地址),那么 LR
的堆叠值至少会告诉您在哪里遇到了最后的 BL
指令,如果幸运的话,这将是导致错误的分支。即使您没有那么幸运,它也可以帮助您缩小搜索范围。
代码
这里有一些(未经测试的)代码可能会满足您的需求。它是用 ARMASM 语法编写的,因此如果您使用不同的工具链,则需要更改一些奇怪的东西:
IMPORT cHandler
TST lr, 0x4 ; Is bit 2 of LR clear?
ITE eq
MRSEQ r3, MSP ; If so, MSP was in use
MRSNE r3, PSP ; Otherwise, PSP was in use
LDR r0, [r3, #24] ; Load the stacked PC into r0
LDR r1, [r3, #20] ; Load the stacked LR into r1
B cHandler ; Tail-call a C function to finish the job
如果C函数cHandler
有原型
void cHandler(void * PC, void * LR);
然后上面汇编语言处理程序的最后一行将调用此函数,将恢复的堆栈 PC 作为第一个参数传递,将恢复的堆栈 LR 作为第二个参数传递。
我在 GCC 中使用了以下代码。
void McuHardFault_HandlerC(uint32_t *hardfault_args)
{
static volatile unsigned long stacked_r0 __attribute__((unused));
static volatile unsigned long stacked_r1 __attribute__((unused));
static volatile unsigned long stacked_r2 __attribute__((unused));
static volatile unsigned long stacked_r3 __attribute__((unused));
static volatile unsigned long stacked_r12 __attribute__((unused));
static volatile unsigned long stacked_lr __attribute__((unused));
static volatile unsigned long stacked_pc __attribute__((unused));
static volatile unsigned long stacked_psr __attribute__((unused));
static volatile unsigned long _CFSR __attribute__((unused));
static volatile unsigned long _HFSR __attribute__((unused));
static volatile unsigned long _DFSR __attribute__((unused));
static volatile unsigned long _AFSR __attribute__((unused));
static volatile unsigned long _BFAR __attribute__((unused));
static volatile unsigned long _MMAR __attribute__((unused));
stacked_r0 = ((unsigned long)hardfault_args[0]);
stacked_r1 = ((unsigned long)hardfault_args[1]);
stacked_r2 = ((unsigned long)hardfault_args[2]);
stacked_r3 = ((unsigned long)hardfault_args[3]);
stacked_r12 = ((unsigned long)hardfault_args[4]);
stacked_lr = ((unsigned long)hardfault_args[5]);
stacked_pc = ((unsigned long)hardfault_args[6]);
stacked_psr = ((unsigned long)hardfault_args[7]);
/* Configurable Fault Status Register */
/* Consists of MMSR, BFSR and UFSR */
_CFSR = (*((volatile unsigned long *)(0xE000ED28)));
/* Hard Fault Status Register */
_HFSR = (*((volatile unsigned long *)(0xE000ED2C)));
/* Debug Fault Status Register */
_DFSR = (*((volatile unsigned long *)(0xE000ED30)));
/* Auxiliary Fault Status Register */
_AFSR = (*((volatile unsigned long *)(0xE000ED3C)));
/* Read the Fault Address Registers. */
/* These may not contain valid values. */
/* Check BFARVALID/MMARVALID to see */
/* if they are valid values */
/* MemManage Fault Address Register */
_MMAR = (*((volatile unsigned long *)(0xE000ED34)));
/* Bus Fault Address Register */
_BFAR = (*((volatile unsigned long *)(0xE000ED38)));
__asm("BKPT #0\n") ; /* cause the debugger to stop */
}
void HardFault_Handler(void) __attribute__((naked));
void HardFault_Handler(void)
{
__asm volatile (
".syntax unified \n" /* needed for the 'adds r1,#2' below */
" movs r0,#4 \n" /* load bit mask into R0 */
" mov r1, lr \n" /* load link register into R1 */
" tst r0, r1 \n" /* compare with bitmask */
" beq _MSP \n" /* if bitmask is set: stack pointer is in PSP. Otherwise in MSP */
" mrs r0, psp \n" /* otherwise: stack pointer is in PSP */
" b _GetPC \n" /* go to part which loads the PC */
"_MSP: \n" /* stack pointer is in MSP register */
" mrs r0, msp \n" /* load stack pointer into R0 */
"_GetPC: \n" /* find out where the hard fault happened */
" ldr r1,[r0,#24] \n" /* load program counter into R1. R1 contains address of the next instruction where the hard fault happened */
" b McuHardFault_HandlerC \n" /* decode more information. R0 contains pointer to stack frame */
);
}
这是我想要实现的目标:每当我收到硬故障或看门狗中断时,我会将前一条指令的地址保存到某个 RAM 位置,该位置将在复位后继续存在。
Kinetis M64 看门狗在发出重置之前给了我 256 CPU 个周期,这应该足以保存一些东西。问题是我在哪里可以找到这些地址?当一个 IRQ 发生时,LR 保存一个异常值而不是实际的 return 地址。
我想在不连接 SWD 探针的情况下执行此操作,这样设备可以在出现问题时自行报告。
我认为,你能做的最好的事情就是找到处理程序将 return 到的指令的地址,如果它被允许 return。这通常是导致错误的指令之后的指令,但不能保证这一点(例如,如果错误是由分支指令引起的)。
在进入处理程序时,link 寄存器包含一个代码,它告诉您异常发生时正在使用哪个堆栈。例如,参见 here 的 Cortex-M4。
就在分支到异常处理程序之前,CPU 将推送 r0-r3
、r12
、LR
(r14
)、PC
(r15
) 和 xPSR
到活动堆栈。如果你的设备有一个 FPU 并且它被启用,浮点上下文也可能被推送,或者 space 留给它。 ARM 堆栈是完全递减的,寄存器按寄存器编号的升序存储在升序内存地址中,因此在进入异常处理程序时,无论使用哪个堆栈指针,都将指向 r0
的堆栈值;因此,按理说,上面的 6 个字(24 字节)将是 PC
的堆栈值,这是异常处理程序的 return 地址。
所以在导致错误的指令之后查找指令的过程,假设它不是由分支引起的,是:
- 检查
LR
找出正在使用的堆栈 - 将适当的堆栈指针加载到空闲寄存器中(
r0-r3
全部可用,因为它们在进入处理程序时被推送) - 读取此堆栈指针上方 24 个字节的字以找到处理程序的 return 地址
导致错误的指令是位于此 return 地址之前的 2 字节还是 4 字节当然取决于指令,Thumb-2 指令集混合了 16 位和 32 位。当然,它可能完全位于其他地方!
请记住,如果 MSP
在故障之前正在使用,则处理程序将使用相同的堆栈,并且所有这些只有在没有任何内容被推送到堆栈时才会起作用处理函数的序言。最简单的方法可能是用汇编语言编写处理程序。它总是可以在处理完堆栈后调用 C 函数来完成您想要的任何终止过程。
最后一件事,可能还值得保存 LR
的堆叠值。如果 PC
的堆叠值告诉您没有任何用处(例如,因为它为零,代码试图跳转到无效地址),那么 LR
的堆叠值至少会告诉您在哪里遇到了最后的 BL
指令,如果幸运的话,这将是导致错误的分支。即使您没有那么幸运,它也可以帮助您缩小搜索范围。
代码
这里有一些(未经测试的)代码可能会满足您的需求。它是用 ARMASM 语法编写的,因此如果您使用不同的工具链,则需要更改一些奇怪的东西:
IMPORT cHandler
TST lr, 0x4 ; Is bit 2 of LR clear?
ITE eq
MRSEQ r3, MSP ; If so, MSP was in use
MRSNE r3, PSP ; Otherwise, PSP was in use
LDR r0, [r3, #24] ; Load the stacked PC into r0
LDR r1, [r3, #20] ; Load the stacked LR into r1
B cHandler ; Tail-call a C function to finish the job
如果C函数cHandler
有原型
void cHandler(void * PC, void * LR);
然后上面汇编语言处理程序的最后一行将调用此函数,将恢复的堆栈 PC 作为第一个参数传递,将恢复的堆栈 LR 作为第二个参数传递。
我在 GCC 中使用了以下代码。
void McuHardFault_HandlerC(uint32_t *hardfault_args)
{
static volatile unsigned long stacked_r0 __attribute__((unused));
static volatile unsigned long stacked_r1 __attribute__((unused));
static volatile unsigned long stacked_r2 __attribute__((unused));
static volatile unsigned long stacked_r3 __attribute__((unused));
static volatile unsigned long stacked_r12 __attribute__((unused));
static volatile unsigned long stacked_lr __attribute__((unused));
static volatile unsigned long stacked_pc __attribute__((unused));
static volatile unsigned long stacked_psr __attribute__((unused));
static volatile unsigned long _CFSR __attribute__((unused));
static volatile unsigned long _HFSR __attribute__((unused));
static volatile unsigned long _DFSR __attribute__((unused));
static volatile unsigned long _AFSR __attribute__((unused));
static volatile unsigned long _BFAR __attribute__((unused));
static volatile unsigned long _MMAR __attribute__((unused));
stacked_r0 = ((unsigned long)hardfault_args[0]);
stacked_r1 = ((unsigned long)hardfault_args[1]);
stacked_r2 = ((unsigned long)hardfault_args[2]);
stacked_r3 = ((unsigned long)hardfault_args[3]);
stacked_r12 = ((unsigned long)hardfault_args[4]);
stacked_lr = ((unsigned long)hardfault_args[5]);
stacked_pc = ((unsigned long)hardfault_args[6]);
stacked_psr = ((unsigned long)hardfault_args[7]);
/* Configurable Fault Status Register */
/* Consists of MMSR, BFSR and UFSR */
_CFSR = (*((volatile unsigned long *)(0xE000ED28)));
/* Hard Fault Status Register */
_HFSR = (*((volatile unsigned long *)(0xE000ED2C)));
/* Debug Fault Status Register */
_DFSR = (*((volatile unsigned long *)(0xE000ED30)));
/* Auxiliary Fault Status Register */
_AFSR = (*((volatile unsigned long *)(0xE000ED3C)));
/* Read the Fault Address Registers. */
/* These may not contain valid values. */
/* Check BFARVALID/MMARVALID to see */
/* if they are valid values */
/* MemManage Fault Address Register */
_MMAR = (*((volatile unsigned long *)(0xE000ED34)));
/* Bus Fault Address Register */
_BFAR = (*((volatile unsigned long *)(0xE000ED38)));
__asm("BKPT #0\n") ; /* cause the debugger to stop */
}
void HardFault_Handler(void) __attribute__((naked));
void HardFault_Handler(void)
{
__asm volatile (
".syntax unified \n" /* needed for the 'adds r1,#2' below */
" movs r0,#4 \n" /* load bit mask into R0 */
" mov r1, lr \n" /* load link register into R1 */
" tst r0, r1 \n" /* compare with bitmask */
" beq _MSP \n" /* if bitmask is set: stack pointer is in PSP. Otherwise in MSP */
" mrs r0, psp \n" /* otherwise: stack pointer is in PSP */
" b _GetPC \n" /* go to part which loads the PC */
"_MSP: \n" /* stack pointer is in MSP register */
" mrs r0, msp \n" /* load stack pointer into R0 */
"_GetPC: \n" /* find out where the hard fault happened */
" ldr r1,[r0,#24] \n" /* load program counter into R1. R1 contains address of the next instruction where the hard fault happened */
" b McuHardFault_HandlerC \n" /* decode more information. R0 contains pointer to stack frame */
);
}