检索 ARM Cortex M0 异常的 return 地址

Retrieving return address of an exception on ARM Cortex M0

我试图在我的代码中检索 IRQ 处理程序的 return 地址。 我的目标是使用 WDT_IRQHandler() 在看门狗定时器到期之前和为调试目的重置之前保存 PC 的值。我还在用其他 IRQ 测试这种方法,以检查我是否理解了这个想法。 但是我好像没有。

我已阅读可用的 documentation。 我理解当异常发生时,8 个寄存器被压入堆栈: R0、R1、R2、R3、R12、LR、PC 和 XPSR。

我还读到堆栈是自动双字对齐的。所以在我看来,检索 return 地址就像:

检查附加的调试器,似乎不是这种情况,该内存地址处的内容并不总是指向闪存区域,甚至不指向有效区域,而且无论如何它都不是那个值PC 将在 POP 指令之后假设。

代码工作正常,所以我认为我在理解它的工作原理方面遇到了问题。

如果我检查反汇编,在某些 IRQ 中,在 POP 之前向 sp 添加了一个常量 (?)

00001924: 0x000009b0 ...TE_IRQHandler+280   add     sp, #36 ; 0x24
00001926: 0x0000f0bd ...TE_IRQHandler+282   pop     {r4, r5, r6, r7, pc}

在其他 IRQ 中不会发生这种情况。

我知道更多的寄存器可能会被压入堆栈,所以我如何确定在哪个偏移量处检索 PC?

如果我在代码仍在 IRQ 处理程序中时检查 SP 周围的内存转储,我可以发现 return 地址,但它总是位于一个奇怪的位置,与SP。我不明白如何获取正确的地址。

您不能依赖 C 处理程序内部的堆栈指针,原因有二:

  1. 对于被抢占的代码,寄存器总是被压入活动堆栈。处理程序始终使用主堆栈 (MSP)。如果中断从进程堆栈 (PSP) 中抢占 运行 的线程模式代码,那么寄存器将被推送到 PSP,您将永远不会在处理程序堆栈中找到它们;
  2. C 例程可能会为局部变量保留一些堆栈 space,而您不知道它有多少,因此您将无法定位寄存器。

我通常是这样做的:

void WDT_IRQHandler_real(uint32_t *sp)
{
    /* PC is sp[6] (sp + 0x18) */
    /* ... your code ... */
}

/* Cortex M3/4 */
__attribute__((naked)) void WDT_IRQHandler()
{
    asm volatile (
        "TST   LR, #4\n\t"
        "ITE   EQ\n\t"
        "MRSEQ R0, MSP\n\t"
        "MRSNE R0, PSP\n\t"
        "LDR   R1, =WDT_IRQHandler_real\n\t"
        "BX    R1"
    );
}

/* Cortex M0/1 */
__attribute__((naked)) void WDT_IRQHandler()
{
    asm volatile (
        "MRS R0, MSP\n\t"
        "MOV R1, LR\n\t"
        "MOV R2, #4\n\t"
        "TST R1, R2\n\t"
        "BEQ WDT_IRQHandler_call_real\n\t"
        "MRS R0, PSP\n"
    "WDT_IRQHandler_call_real:\n\t"
        "LDR R1, =WDT_IRQHandler_real\n\t"
        "BX  R1"
    );
}

这里的技巧是处理程序是一小段程序集(我使用了 GCC asm 的裸函数,你也可以使用单独的 asm 文件)将堆栈指针传递给真正的处理程序。这是它的工作原理(M3/4):

  • 异常处理程序中 LR 的初始值称为 EXC_RETURN(更多信息 here)。它的位有不同的含义,我们感兴趣的事实是,如果活动堆栈是 MSP,则 EXC_RETURN[2]0,如果活动堆栈是 [=,则 1 12=];
  • TST LR, #4 检查 EXC_RETURN[2] 并设置条件标志;
  • MRSEQ R0, MSPMSP 移动到 R0 if EXC_RETURN[2] == 0;
  • MRSNE R0, PSPPSP 移动到 R0 if EXC_RETURN[2] == 1;
  • 最后,LDR/BX跳转到真正的函数(R0是第一个参数)。

M0/1 变体是相似的,但使用分支,因为核心 does not support IT blocks

这解决了 MSP/PSP 问题,并且由于它在任何编译器生成的堆栈操作之前运行,因此它将提供可靠的指针。 我对函数使用了一个简单的(非链接的)分支,因为我不需要在它之后做任何事情,并且 LR 已经可以了。它节省了几个周期和 LR push/pop。此外,所有使用的寄存器都在 R0-R3 scratch 范围内,因此无需保留它们。