检索 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 地址就像:
- 通过__builtin_frame_address(0);
检索sp地址
- 加上堆栈 PC 的偏移量 (0x18),并读取值,这应该是处理程序 returns.
时将恢复到 PC 的值
检查附加的调试器,似乎不是这种情况,该内存地址处的内容并不总是指向闪存区域,甚至不指向有效区域,而且无论如何它都不是那个值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 处理程序内部的堆栈指针,原因有二:
- 对于被抢占的代码,寄存器总是被压入活动堆栈。处理程序始终使用主堆栈 (
MSP
)。如果中断从进程堆栈 (PSP
) 中抢占 运行 的线程模式代码,那么寄存器将被推送到 PSP
,您将永远不会在处理程序堆栈中找到它们;
- 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, MSP
将 MSP
移动到 R0
if EXC_RETURN[2] == 0
;
MRSNE R0, PSP
将 PSP
移动到 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 范围内,因此无需保留它们。
我试图在我的代码中检索 IRQ 处理程序的 return 地址。 我的目标是使用 WDT_IRQHandler() 在看门狗定时器到期之前和为调试目的重置之前保存 PC 的值。我还在用其他 IRQ 测试这种方法,以检查我是否理解了这个想法。 但是我好像没有。
我已阅读可用的 documentation。 我理解当异常发生时,8 个寄存器被压入堆栈: R0、R1、R2、R3、R12、LR、PC 和 XPSR。
我还读到堆栈是自动双字对齐的。所以在我看来,检索 return 地址就像:
- 通过__builtin_frame_address(0); 检索sp地址
- 加上堆栈 PC 的偏移量 (0x18),并读取值,这应该是处理程序 returns. 时将恢复到 PC 的值
检查附加的调试器,似乎不是这种情况,该内存地址处的内容并不总是指向闪存区域,甚至不指向有效区域,而且无论如何它都不是那个值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 处理程序内部的堆栈指针,原因有二:
- 对于被抢占的代码,寄存器总是被压入活动堆栈。处理程序始终使用主堆栈 (
MSP
)。如果中断从进程堆栈 (PSP
) 中抢占 运行 的线程模式代码,那么寄存器将被推送到PSP
,您将永远不会在处理程序堆栈中找到它们; - 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, MSP
将MSP
移动到R0
ifEXC_RETURN[2] == 0
;MRSNE R0, PSP
将PSP
移动到R0
ifEXC_RETURN[2] == 1
;- 最后,
LDR
/BX
跳转到真正的函数(R0
是第一个参数)。
M0/1 变体是相似的,但使用分支,因为核心 does not support IT blocks。
这解决了 MSP
/PSP
问题,并且由于它在任何编译器生成的堆栈操作之前运行,因此它将提供可靠的指针。
我对函数使用了一个简单的(非链接的)分支,因为我不需要在它之后做任何事情,并且 LR
已经可以了。它节省了几个周期和 LR
push/pop。此外,所有使用的寄存器都在 R0-R3
scratch 范围内,因此无需保留它们。