Arm Linux中断溯源(一)
Linux中断相比单片机实在是复杂了太多但本质原理是相通的这里写文章记录一下学习中的一些思考后面想起来再继续补充。文章主要基于《图解Linux内核》、《Linux内核设计与实现》这两本书由于它们都是基于x86进行讲解的但我需要从事ARM Linux嵌入式开发所以遇到与x86的不同之处选择教程比较丰富的I.MX6ULL和STM32MP157内核进行比较。写这篇文章也是为了给自己学习东西的动力毕竟不懂的东西就说不清也不能乱说写上来也就说明我应该是明白了。本篇部分内容是和AI“讨论”出来的如有错误欢迎指正。引言无论使用何种架构中断处理永远可以概括为三步保存现场-跳转执行中断处理函数-恢复现场其中跳转执行中断处理函数是与开发者关系最密切的事情而保存现场、恢复现场都是必不可少的流程并且在Linux内核中有很大篇幅。一般开发者使用中断就是向内核在某条中断线上注册触发时的处理函数为了知道能在中断处理函数做什么、不能做什么就必须理解中断上下文。而Linux中从中断触发到执行中断处理函数的调用关系非常复杂无疑为理解中断上下文制造了麻烦这也就是为什么要去溯源。我学习中断还有一个目标就是弄清楚Linux内核到底是怎么做到适配那么多架构的学习它的编程思想。异常与中断这两个名词都是形容打断当前执行流PC不断1的正常流程跳转执行新的执行流的行为但根据产生的原因可以分为中断是指来自CPU之外的硬件设备打断CPU的执行流是异步无法预知在执行哪个指令时会到达的异常是指来自CPU内部由当前正在执行的指令导致的是同步可以预见只要执行这条指令必然会产生的还有个名词IRQInterrupt Request说的就是中断而不是异常Cortex-M3中断原理先用单片机热热身虽然与Linux处理方式很不同但是原理是共通的Cortex-M3支持11个异常和最多240个外部中断如下表摘自《Cortex-M3权威指南》这节后面的图也一样搞单片机的肯定得知道这些这里说的中断就是中断CPU执行流的行为而不仅仅是外部中断、IRQ了注意区分保存什么现场保存在哪里硬件会自动保存一部分寄存器硬件会把8个寄存器地址入栈较真的话Cortex-M3有2组栈指针MSP主栈和PSP进程栈此时硬件会就近保存到正在使用的那个栈里分别为xPSR、PC、LR、R12、R3-R0并且严格的说这个顺序是入栈后、从高到底的栈地址里面的、值的意义并不是时间上的入栈的顺序时间上的压栈顺序是有讲究的是优化过的。有个疑问为什么图里的地址HADDR和数据HWDATA是错开的这个是总线流水线在第1个周期地址N-8被发出去第2个周期地址N-4被发出去同时N-8对应的数据PC被送到数据总线上第3个周期…流水线可以保证每个周期都在推进压栈流程缩短中断响应时间。CPU状态有什么变化Cortex-M3有2种操作模式和2个特权等级操作模式处理者模式Handler mode和线程模式Thread mode特权等级特权级Privileged和用户级Unprivileged或User运行在Thread mode时既可以是Privileged也可以是User。运行在Handler mode时必定是Privileged。操作模式和特权等级的状态转移如下图初学单片机大家都从裸机开始没有手动切换过特权等级从上电复位之后一直都运行在特权级线程模式。而一旦发生异常CPU处理中断期间是在特权级handler模式下进行的可以理解为处理中断时拥有最高权限所以中断确实要小心编程。事实上裸机编程状态就在图里上面两个圈之间转换使用的栈是唯一的MSP。当加入RTOS后图里3个状态都会存在了也开始使用PSP。RTOS下的CPU状态转换这里就不再赘述以后可能会写一篇FreeRTOS的学习笔记。只需要关注发生中断行为时CPU的状态确实发生了变化特权等级会提升就好。中断上下文的函数用的什么栈甭管发生中断前一刻、被打断的现场用的什么栈进入中断上下文后一律用的是MSP说的是Cortex M3其他架构后面再细究。既然被打断跳转去执行新的代码那跳到哪对于ARM架构就是跳转到异常向量表中断向量表。说是个表其实是一段区域里面记录的是发生异常时应该执行的函数排列整齐像个表1个表项只有4个字节的大小这里放的是什么东西确实放不了什么东西只有1条跳转指令正好4个字节为什么是用偏移量0x04表示而不是真正的绝对地址如内存中的0x00000004因为中断向量表所处的区域是可以自定义的有个寄存器叫VTOR向量表偏移寄存器可以控制向量表整体偏移这个表里面的东西对应的是跳转到某个函数的指令对吧那在何时放置的这些指令在startup.s里被指定参与编译、链接成二进制文件后由烧录器将指令、指令所跳转的函数下载到正确的地址有关startup.s这里就不写了网上很多可以自己找一份丢给AI打打注释研究研究Cortex-A7中断原理Cortex-A7相比Cortex M3已经复杂了很多这里引用正点原子《I.MX6U嵌入式Linux驱动开发指南》虽然他们也是引用的其他手册但整理在一起还是更方便一些还记得Cortex M3的2种模式吗Thread mode和Handler modeCortex A7有9种工作模式除了USR用户模式外其他全是特权模式。和Cortex M3类似的用户模式想要切换到其他模式就只能触发异常。Cortex M3只有SPR13寄存器在2种模式下有不同的备份MSP和PSP但Cortex A7有很多寄存器组在不同的模式下都有不同的备份。下面图里蓝绿色的就是各个模式独有的寄存器灰色的是所有模式公共的寄存器。CPU发生的模式转变毫无疑问的中断发生时CPU会切换到IRQ模式就像Cortex M3里面切换到handler模式一样。查看CPSR寄存器程序状态寄存器就可以知道CPU现在处于什么模式。保存的现场Cortex A7完全依靠软件压栈这点和Cortex M3完全不一样它的现场是没有硬件去自动保存的。因此具体做了什么还需要去溯源Linux内核代码。跳转的向量表Cortex A7只有8个异常向量没错只有8个Cortex M3可是有11个异常至多240个IRQ怎么性能越强反而越少了其实是越来越高级了这里面一个IRQ Interrupt就把上百个IRQ全都包含了。具体怎么细分的到下一章再写吧。对应的汇编入口I.MX6ULL内核 arch/arm/kernel/entry-armv.S /* * 定义一个名为 .vectors 的段 * ax 可分配(allocate) 可执行(executable)是代码段 * %progbits 段里存真实机器码不是未初始化数据 */ .section .vectors, ax, %progbits /* * __vectors_start * 这就是 **i.MX6ULL Linux 的异常向量表基地址** * 共 8 个异常向量每个占 4 字节对应 ARM 标准顺序 * 0: 复位 * 1: 未定义指令 * 2: SWI/SVC软中断 * 3: 预取中止 * 4: 数据中止 * 5: 保留 * 6: IRQ通用中断 * 7: FIQ */ __vectors_start: W(b) vector_rst 0x00 复位异常 W(b) vector_und 0x04 未定义指令异常 W(ldr) pc, __vectors_start 0x1000 0x08 SVC/SWI 软中断**特殊** W(b) vector_pabt 0x0c 预取中止 W(b) vector_dabt 0x10 数据中止 W(b) vector_addrexcptn 0x14 保留地址异常 W(b) vector_irq 0x18 IRQ 中断 W(b) vector_fiq 0x1c FIQ 快速中断 /* W (x) 让指令同时支持 ARM/Thumb 模式内核通用宏。 W(b)和W(ldr)就是通用的b和ldr的意思 那为什么SVC/SWI异常不使用W(b) vector_svc 因为SVC Linux 给它单独做了一张表 */以下就是定义vector##xxx的宏组合成一个名字这意味着这些vector_xxx所做的东西其实高度重复I.MX6ULL内核 arch/arm/kernel/entry-armv.S /* * Vector stubs. * 向量桩异常/中断的底层入口 * * This code is copied to 0xffff1000 so we can use branches in the * vectors, rather than ldrs. Note that this code must not exceed * a page size. * 这段代码会被复制到内存 0xffff1000 地址 * 目的是让向量表能用简单的跳转指令B而不是复杂加载LDR * 注意代码大小不能超过一页内存 * 原因B指令是相对跳转只能跳转到当前范围的一页内比LDR更快更小 * * Common stub entry macro: * 通用的异常入口宏定义 * Enter in IRQ mode, spsr 发生异常前的 CPSR * lr 发生异常前的 PC * 这里说的SPSR其实就是正点原子那张图里CPSR的备份。切换模式前的CPSR被暂存在SPSR内 * 返回时可以恢复到CPSR。同样还有LR、SP指的是LR_IRQ、SP_IRQ而不是USER里的那一套 * * SP points to a minimal amount of processor-private memory, the address * of which is copied into r0 for the mode specific abort handler. * SPSP_IRQ 指向一小块 CPU 私有内存 * 这块地址会传给 r0供异常处理函数使用 * 人话把异常发生时的硬件现场指针SP_IRQ当作C 语言函数的第一个参数 * 对应mov r0, sp * 这有伏笔意味着在进入C函数前SP_IRQ里面首先肯定需要保存、构造什么东西 */ // 宏定义vector_stub 名字模式修正值默认0 .macro vector_stub, name, mode, correction0 .align 5 按 32 字节对齐ARM 要求异常向量对齐 vector_\name: 拼接成函数名例如 vector_irq .if \correction sub lr, lr, #\correction 如果需要修正把返回地址减修正值 **因为异常时PC会超前必须修正才能正确返回** .endif 第一步保存最关键的现场这一步就是在构造栈 SP_IRQ Save r0, lr_exception (parent PC) and spsr_exception (parent CPSR) 保存 r0通用寄存器 lr异常发生前的PC stmia sp, {r0, lr} 把 spsr异常前的 CPSR 状态读到 lr mrs lr, spsr 把 spsr 存到栈的 [sp8] 位置 str lr, [sp, #8] 第二步准备切回 SVC 模式内核态 Prepare for SVC32 mode. IRQs remain disabled. 进入 IRQ 中断的瞬间ARM 硬件会自动、强制、无条件关闭 IRQ 从进入中断到现在IRQ 一直被硬件关着从来没打开过 所以任何IRQ在此时都无法嵌套 /* 补充Linux 内核所有 C 语言代码只跑在 SVC 模式为了栈和统一的内核上下文 IRQ 模式的栈很小、很特殊不能跑 C 语言 Linux 设计时就定死了 用户态 USR 模式 内核态 SVC 模式 IRQ / FIQ / ABT 等模式 只用来进一下门立刻切走 因为 SVC 模式有内核的主栈很大、很安全、C 语言能正常跑。 */ 读取当前 CPSR 状态 → r0 mrs r0, cpsr 计算新的 CPSR切换到 SVC 模式 eor r0, r0, #(\mode ^ SVC_MODE | PSR_ISETSTATE) 把新状态写入 spsr msr spsr_cxsf, r0 //很重要这里写的是SPSR而不是CPSR //写的是备份寄存器不影响CPSR仍然是IRQ模式 //直到movs pc, lr 会自动从SPSR中恢复到CPSR自动完成模式切换 第三步根据异常类型跳转到真正的C处理函数这时仍是IRQ模式 从 spsr 里取出低4位异常类型→ lr the branch table must immediately follow this code 跳转表必须在这段代码之后这个宏结束伏笔 宏后面必须跟跳转表 and lr, lr, #0x0f THUMB( adr r0, 1f ) THUMB( ldr lr, [r0, lr, lsl #2] ) 把栈指针传给 r0给C函数用 //伏笔回收SP_IRQ里确实塞了参数毫无疑问这就是保存的现场 mov r0, sp ARM 模式查表根据异常类型找到处理函数地址 → lr //人话把 内存地址 PC (LR 左移2位) 里的值读出来放到 LR 中 //lr *( pc (lr 2) ) //到这里时lrSPSR的低4位代表中断前发生的CPU工作模式 // ARM( ldr lr, [pc, lr, lsl #2] ) 跳转并切换CPU状态到 SVC 模式 **真正跳转到 C 语言的中断/异常处理函数** movs pc, lr ENDPROC(vector_\name) 宏结束代码里面还有一句话the branch table must immediately follow this code所以紧跟着I.MX6ULL内核 arch/arm/kernel/entry-armv.S 伏笔回收5张跳转表 /* * Interrupt dispatcher */ vector_stub irq, IRQ_MODE, 4 .long __irq_usr 0 (USR_26 / USR_32) .long __irq_invalid 1 (FIQ_26 / FIQ_32) .long __irq_invalid 2 (IRQ_26 / IRQ_32) .long __irq_svc 3 (SVC_26 / SVC_32) .long __irq_invalid 4 .long __irq_invalid 5 .long __irq_invalid 6 .long __irq_invalid 7 .long __irq_invalid 8 .long __irq_invalid 9 .long __irq_invalid a .long __irq_invalid b .long __irq_invalid c .long __irq_invalid d .long __irq_invalid e .long __irq_invalid f ... /* * Data abort dispatcher * Enter in ABT mode, spsr USR CPSR, lr USR PC */ vector_stub dabt, ABT_MODE, 8 .long __dabt_usr 0 (USR_26 / USR_32) .long __dabt_invalid 1 (FIQ_26 / FIQ_32) .long __dabt_invalid 2 (IRQ_26 / IRQ_32) .long __dabt_svc 3 (SVC_26 / SVC_32) .long __dabt_invalid 4 .long __dabt_invalid 5 .long __dabt_invalid 6 .long __dabt_invalid 7 .long __dabt_invalid 8 .long __dabt_invalid 9 .long __dabt_invalid a .long __dabt_invalid b .long __dabt_invalid c .long __dabt_invalid d .long __dabt_invalid e .long __dabt_invalid f ... /* * Prefetch abort dispatcher * Enter in ABT mode, spsr USR CPSR, lr USR PC */ vector_stub pabt, ABT_MODE, 4 .long __pabt_usr 0 (USR_26 / USR_32) .long __pabt_invalid 1 (FIQ_26 / FIQ_32) .long __pabt_invalid 2 (IRQ_26 / IRQ_32) .long __pabt_svc 3 (SVC_26 / SVC_32) .long __pabt_invalid 4 .long __pabt_invalid 5 .long __pabt_invalid 6 .long __pabt_invalid 7 .long __pabt_invalid 8 .long __pabt_invalid 9 .long __pabt_invalid a .long __pabt_invalid b .long __pabt_invalid c .long __pabt_invalid d .long __pabt_invalid e .long __pabt_invalid f ... /* * Undef instr entry dispatcher * Enter in UND mode, spsr SVC/USR CPSR, lr SVC/USR PC */ vector_stub und, UND_MODE .long __und_usr 0 (USR_26 / USR_32) .long __und_invalid 1 (FIQ_26 / FIQ_32) .long __und_invalid 2 (IRQ_26 / IRQ_32) .long __und_svc 3 (SVC_26 / SVC_32) .long __und_invalid 4 .long __und_invalid 5 .long __und_invalid 6 .long __und_invalid 7 .long __und_invalid 8 .long __und_invalid 9 .long __und_invalid a .long __und_invalid b .long __und_invalid c .long __und_invalid d .long __und_invalid e .long __und_invalid f ... /* * FIQ NMI handler *----------------------------------------------------------------------------- * Handle a FIQ using the SVC stack allowing FIQ act like NMI on x86 * systems. */ vector_stub fiq, FIQ_MODE, 4 .long __fiq_usr 0 (USR_26 / USR_32) .long __fiq_svc 1 (FIQ_26 / FIQ_32) .long __fiq_svc 2 (IRQ_26 / IRQ_32) .long __fiq_svc 3 (SVC_26 / SVC_32) .long __fiq_svc 4 .long __fiq_svc 5 .long __fiq_svc 6 .long __fiq_abt 7 .long __fiq_svc 8 .long __fiq_svc 9 .long __fiq_svc a .long __fiq_svc b .long __fiq_svc c .long __fiq_svc d .long __fiq_svc e .long __fiq_svc f现在疑问越来越多了一条一条解决发生中断时这部分汇编的跳转流程异常向量表-向量桩vector_stub-发生中断前对应模式的跳转表比如__irq_usr意思是IRQ中断打断了原来的USR模式那最终就跳到这里这部分入口代码用的栈是什么是SP_IRQ一个独立的栈中断的现场不是像Cortex M3那样、硬件就近保存到MSP/PSP的而是需要vector_stub的代码软件保存在SP_IRQ中。汇编宏怎么看的和内联函数inline一样原地展开比如下面这个调用vector_stub irq, IRQ_MODE, 4就带入到vector_stub定义中在这里原地展开这也就是为什么它的后面紧跟跳转表因为PC确实是在一条一条1的执行到汇编宏最后的指令//lr *( pc (lr 2) ) ARM( ldr lr, [pc, lr, lsl #2] ) **真正跳转到 C 语言的中断/异常处理函数** movs pc, lr这个时候pc就是指向紧随其后的那个跳转表表头lr是从中断前的spsr取出的低4位代表异常类型两个一相加正好指向vector_stub irq, IRQ_MODE, 4宏后面紧跟的跳转表地址完美这个过程中的CPU模式切换重复一下这个过程是指异常向量表-向量桩vector_stub-由当前模式和打断的模式细分的、下划线开头的中断处理函数我们只讨论IRQ中断的情况发生IRQ的那一刻毫无疑问的不管之前是什么模式此刻CPU切换到了IRQ模式跳转到向量桩vector_stub irq, IRQ_MODE, 4汇编宏需要展开vector_stub内部给spsr赋了SVC模式的值但SPSR不是CPSR直到最后跳转到__irq_xxx函数前毫无疑问的仍然处于IRQ模式直到最后调用movs pc, lr硬件会恢复SPSR中的值到全局CPSR那这下不得不切换到SVC模式了也就是说__irq_xxx函数是在SVC模式执行的。也好理解因为内核代码都是在SVC模式执行的。总之触发IRQ中断、跳到异常向量表、执行向量桩vector_stub这段时间都是IRQ模式直到调用__irq_xxx内核函数就自动切换成了SVC模式这一过程CPSR、SPSR发生的变化发生IRQ中断一瞬间CPSR假设为USR就拷贝到了SPSR_IRQ之后CPSR变成IRQ模式此时CPSR IRQSPSR_IRQ USRvector_stub里面会把SPSR_IRQ先存到栈里用USR模式去计算__irq_xxx跳转表的偏移最终精确的调用__irq_usr。那SPSR_IRQ本体也没闲着存到栈里之后vector_stub会给SPSR_IRQ赋值SVC此时SPSR_IRQ SVCCPSR IRQ最后movs pc, lr将SPSR_IRQ恢复到CPSR那当然CPSR SVC了之后执行__irq_usr时CPSR SVCCortex M3有抢占的规则异常是可以嵌套的那这段代码会被嵌套吗不会当发生IRQ的时候CPSR.I 1硬件自动关IRQ直到跳入__irq_xxxIRQ都没有被重新打开这就意味着这段代码根本不会发生嵌套。原因我想也很好解释因为中断嵌套会导致栈爆发这段代码是在SP_IRQ中保存的本来就小中断嵌套必然会导致SP_IRQ爆炸。尤其是Linux不把进来时的SP_IRQ带走就别想再往里面写新东西。见勘误那最后SP_IRQ出栈销毁了吗没有因为后面还用它作为参数R0传入__irq_xxx函数了这里面的东西仍然有用见勘误为什么 vector_stub irq, IRQ_MODE, 4 的跳转向量里面除了__irq_usr和__irq_svc其他全都是__irq_invalid感觉是很好的问题首先选择进入__irq_usr还是进入__irq_svc是根据IRQ中断打断的现场CPSR决定的既然只有__irq_usr和__irq_svc那就说明IRQ能打断的模式必然只可能是USR和SVC更进一步的Linux用户程序永远跑在USR内核代码永远跑在SVC更更进一步的其实那么多异常很多Linux内核都没怎么实现只要搞懂IRQ和SVC就撑起了整个用户态 、内核态 、硬件的全部关系。__vectors_start入口中除了SVC/SWI异常是用W(ldr)到一个固定地址其他都是W(b)到一个vector_stub为什么它画风不一样因为vector_swi在另一个文件中不在同一个pageb指令跳不过去。精力有限实在不想分析了我写这个是为了研究中断不是系统调用中断我编驱动可能还用得到但系统调用可能这辈子都不会修改了所以这些以后再研究吧/arch/arm/kernel/entry-common.S /* * IMX6ULL (Cortex-A7 ARMv7-A) 专属 SWI/SVC 系统调用入口 * 功能用户态 - 内核态 系统调用总入口open/read/write 等 *----------------------------------------------------------------------------- */ .align 5 ENTRY(vector_swi) 1. 保存用户态所有寄存器到内核栈 (IMX6ULL 硬件上下文) sub sp, sp, #S_FRAME_SIZE 开辟栈空间存储全部寄存器 stmia sp, {r0 - r12} 保存 r0~r12 通用寄存器 add r8, sp, #S_PC stmdb r8, {sp, lr}^ 保存用户态 SP、LR mrs r8, spsr 读取 SPSR (中断前的CPSR) str lr, [sp, #S_PC] 保存用户态 PC str r8, [sp, #S_PSR] 保存用户态 CPSR str r0, [sp, #S_OLD_R0] 保存原始 r0 2. IMX6ULL EABI 标准系统调用号存在 r7 中 EABI 规则r7 系统调用号 (无需读取指令直接用r7) 3. 初始化内核环境 zero_fp 清空帧指针 alignment_trap r10, ip, __cr_alignment enable_irq 开启中断内核态允许中断 ct_user_exit get_thread_info tsk 获取当前进程信息 4. 加载系统调用表基地址 adr tbl, sys_call_table 指向 IMX6ULL 内核系统调用表 5. 检查系统调用跟踪调试用 local_restart: ldr r10, [tsk, #TI_FLAGS] stmdb sp!, {r4, r5} tst r10, #_TIF_SYSCALL_WORK bne __sys_trace 进入跟踪流程调试 6. 校验调用号 执行系统调用 cmp scno, #NR_syscalls 检查系统调用号是否合法 adr lr, BSYM(ret_fast_syscall) 系统调用完成后的返回地址 ldrcc pc, [tbl, scno, lsl #2] 【核心】跳转到对应 sys_xxx 函数 7. 非法系统调用处理 add r1, sp, #S_OFF cmp scno, #(__ARM_NR_BASE - __NR_SYSCALL_BASE) eor r0, scno, #__NR_SYSCALL_BASE bcs arm_syscall mov why, #0 b sys_ni_syscall 未实现的系统调用 ENDPROC(vector_swi)为什么异常向量入口__vectors_start有8个但是vector_stub只有5个刚才说的SVC没有用vector_stub实现剩下7个vector_rst没有用vector_stub实现就两行代码剩下6个vector_rst: ARM( swi SYS_ERROR0 ) THUMB( svc #0 ) THUMB( nop ) b vector_und还有1个vector_addrexcptn没用vector_stub实现那就正好剩5个了/* * Address exception handler *----------------------------------------------------------------------------- * These arent too critical. * (theyre not supposed to happen, and wont happen in 32-bit data mode). */ vector_addrexcptn: b vector_addrexcptn小节终于把异常向量的入口分析完了到现在这么长甚至还没见到内核IRQ的统一入口还在架构里晃悠不过这些分析还是有很多收获的这里写一些可以记忆的结论性的东西作为小节字数一多Gridea卡的不行了这章就结束另起一章吧Cortex A7的异常向量Cortex A7的异常入口为__vectors_start有8种异常vector_rst、vector_und、__vectors_start 0x1000vector_swi、vector_pabt、vector_dabt、vector_addrexcptn、vector_irq、vector_fiq其中2个特别重要vector_irq和vector_swi一个是硬件中断的入口一个是系统调用的入口别的基本用不到。Cortex A7 IRQ处理流程__vectors_start-vector_stub-__irq_usr/__irq_svc(视被中断的现场所处模式选择进入)上下文非常重要是否会嵌套从触发IRQ那一刻CPSR.I由硬件置1到进入__irq_usr或者__irq_svc这段时间都不可能被IRQ继续抢占或嵌套。现场保存在哪SP寄存器的IRQ模式副本SP_IRQ软件保存的现场销毁了吗到进入__irq_usr或者__irq_svc时是要把SP_IRQ当参数传进去的那当然没销毁CPU模式是什么从触发IRQ那一刻到进入__irq_usr或者__irq_svc前都是IRQ模式但随着进入__irq_usr或者__irq_svc就自动切换成了SVC模式后续都是在SVC模式了。其他细节内核里有__irq_svc函数说明IRQ是可以打断SVC的这似乎说明IRQ的优先级比SVC高。勘误IRQ所打断的现场保存在SP_IRQ中这句话确实没错但是vector_stub保存现场的方式根本没移动SP指针 Save r0, lr_exception (parent PC) and spsr_exception (parent CPSR) stmia sp, {r0, lr} save r0, lr mrs lr, spsr str lr, [sp, #8] save spsr没有移动SP指针的汇编代码这就意味着根本就不用惦记着出栈下次进入IRQ自会覆盖由于本章代码在执行时IRQ是关闭的所以不会被错误抢占导致SP_IRQ被覆盖在接下来的__irq_usr或__irq_svc中SP_IRQ的现场就被拷贝到SP_SVC了之后就算随便覆盖也没事了