|
改中断返回地址这件事,何修在单片机开发里属于“非常规操作”。改单大部分时候你不需要动它,片机中断该返回哪儿就返回哪儿,外部硬件自动把PC压栈、中断弹栈,地址干干净净。何修但有些场景就是改单得硬来——比如你想在中断里跳过一条出错指令、想实现一个简易的片机任务调度、或者调试时想强行改变程序流向。外部这时候就得知道栈里那个返回地址到底躺在哪儿,中断以及怎么安全地把它换掉。地址 先说清楚一个前提:不同内核的何修单片机,做法差异很大。改单51单片机、片机AVR、STM32(Cortex-M)的栈结构和中断响应机制完全不同。我拿Cortex-M3/M4来举例,因为用的人多,资料也好找。STM32进中断时,硬件会自动把8个寄存器压入当前栈(MSP或PSP),顺序是R0、R1、R2、R3、R12、LR、PC、xPSR。注意,那个PC就是中断返回后要执行的指令地址。它被压在栈帧的固定偏移位置——从栈顶往下数第6个字(偏移0x14字节,如果栈是满递减的话)。 假设你在某个外设中断服务函数里,想修改返回地址,让程序返回到另一个地方。代码大概长这样: void EXTI0_IRQHandler(void) { __asm volatile ( "LDR R0, =0x20001000 \n" // 假设栈指针指向这里附近 "LDR R1, [R0, #0x14] \n" // 取出压栈的PC值 "ADD R1, R1, #4 \n" // 跳过当前指令,比如跳过一条2字节指令 "STR R1, [R0, #0x14] \n" // 写回栈帧 ); EXTI->PR = 1; // 清中断标志 } 但这里有个坑:你没法直接知道硬件压栈后SP指向哪里。在中断入口,SP可能已经又调整过了(比如编译器插入的压栈操作)。更靠谱的办法是读当前SP,然后根据编译器行为反推。或者用内联汇编直接操作栈指针,但得小心别把编译器搞晕。 另一个常见场景是51单片机。51的中断响应会把当前PC压入堆栈,但它的堆栈在内部RAM里,由SP寄存器指向栈顶。中断返回时执行RETI,从栈里弹出两个字节到PC。如果你想改返回地址,就得在中断函数里找到SP指向的位置,把那里两个字节改掉。比如: void timer0_isr(void) interrupt 1 { unsigned char *p; p = (unsigned char *)SP; // 注意SP是8位寄存器 // 51的栈是向上生长的,压栈时SP先加1 // 中断响应时压入的PC低字节在高地址,高字节在低地址?实际是低字节先压,高字节后压 // 所以SP指向高字节,SP-1指向低字节 *(p-1) = (unsigned char)(new_addr & 0xFF); // 低字节 *p = (unsigned char)((new_addr >> 8) & 0xFF); // 高字节 } 看起来简单,但51的栈空间很浅,中断嵌套时SP会变,你得确保操作的是当前中断帧。而且编译器可能优化掉你的指针操作,得加volatile或者用汇编。 我实际调试时遇到过一个案例:某工业控制器用STM32F103,电机堵转时会产生一个错误中断,但硬件设计缺陷导致中断标志清不掉,程序反复进中断。临时解决办法就是在中断里把返回地址改成跳过触发中断的那条指令。改完后系统能继续跑,虽然不优雅,但产线等不起。这种操作就像给程序打补丁,能救急,别当常规手段。 还有一点:修改返回地址后,中断退出时的自动弹栈会把你改过的PC加载进去。但如果你改得不对齐(比如Cortex-M要求指令地址最低位为1表示Thumb状态),会直接HardFault。所以改之前先确认目标地址的bit0是1。另外,如果开了中断嵌套或者用了RTOS,栈可能不是MSP而是PSP,你得先判断当前用的是哪个栈指针。 最后提醒一句:这类操作调试器单步时经常失效,因为调试器可能自己改了栈或者中断行为。想验证的话,用GPIO翻转加示波器看波形更实在。改返回地址本质上是跟硬件和编译器抢控制权,能不用就不用,非用不可时,把栈帧结构吃透再动手。 |