1. 前置知识
本部分内容主要分析cortex-m处理器在异常或中断发生及退出时的软硬件行为,需首先了解arm架构程序调用标准(AAPCS),可参考我之前写的文章c语言函数及AAPCS的理解-基于arm 分析,或参考官方文档。
关于AAPCS,我将主要内容总结如下图,在阅读本文前,应首先掌握AAPCS,达到能使用汇编写出可供C语言安全调用的函数 以及 使用汇编安全调用C语言,至少要能理解 “调用者保护的寄存器” 和 “被调用者保护寄存器” 的概念。
2. 中断服务函数与普通函数有区别吗?
2.1 思考1 - 为什某些架构或某些编译器编写中断服务函数时需要使用特殊的关键字修饰?
- 在51开发时我们会对中断服务函数使用 “interrupt + 中断号” 的形式进行修饰。
- gcc在其手册6.33章针对不同架构函数声明一些扩展属性进行描述,下图是gcc针对针对arm架构的中断服务程序的属性扩展描述
下图gcc关于arm中断函数的扩展属性只适用于armv7-r 和 armv7-r(暂不谈论armv8), 并不适用于armv7-m,事实上cortex-m系列处理器因其硬件特性,中断服务函数和普通函数是一样的,并不需要额外的修饰。
- armclang针对中断服务函数的扩展语法(与gcc基本一样)
2.2 思考2 - 为什么在某些架构中需要对中断服务函数进行修饰,而有些则不需要?对中断服务函数进行修饰时编译器究竟在干什么?
直接说答案,中断发生时是需要保存一些上下文的, 保存上下文的过程在有些架构中是通过硬件来完的,有些架构硬件则不会做这部分工作,需要软件来做。
- 针对硬件保存中断上下文的架构,中断服务函数和普通函数没有区别。
- 需要软件保存上下文的的架构中,一般有两种方式对中断服务函数进行处理
- 利用编译器针对当前架构提供的扩展关键字,经过扩展关键字修饰的函数,编译器在编译阶段会在该函数入口和出口处插入保存和恢复上下文的代码。
- 程序员直接使用汇编程序在中断服务函数调用前和调用后加入保存和恢复山下文的代码,这样就不用使用编译器提供的关键字了。
注:无论是硬件保存上下文还是软件保存上下文,在中断发生时都必须有保存上下文的过程,思考一下为什么?
3. 中断发生时为什么要保存上下文?保存上下文是在保存什么?保存在哪里?
如果熟悉函数调用规范(如ARM的AAPCS) 对于异常发生时要保存的上下文是什么,要保存到哪里?相信是很容易理解的。
- 异常发生时,上下文保存到哪里?答案是栈中。
- 思考一下再异常发生的那一刻栈里面有什么?以ARM为例,当前栈中保存着当前调用函数的栈帧,每个函数都有自己的栈帧
理解每个函数都有自己栈帧的一个思路:在AAPCS中SP是一个被调用者保存的寄存器,所以SP在函数中如果被修改了(压栈),则在函数退出时sp必须恢复(弹栈)。
- 中断发生时为什么要保存上下文?保存上下文是在保存什么?这需要了解一下当前体系架构的函数调用规范(如 arm的AAPCS),理解调用者保存的寄存器和被调用者保存的寄存器的概念,同时应该了解中断/异常可能在任何时候发生,例如可能在“调用者还未保存好要保存的寄存器”或者“编译器认为调用者保存的寄存器不会被破坏,所以没有保存”的时候,同时我们应该知道“由调用者保存的寄存器被调用者是可以随意破坏的”,所以如果中断函数如普通函数一般编写,就存在被中断打断的函数的被调用保护的寄存器被破坏,从而导致函数执行产生不可预支的后果,所以中断发生时要保存上下文,保存的上下文就是被调用保护的寄存器,再次将这个图复制过来,以arm为例,要保存的上下文就是下图中红色部分的寄存器。
4. 以cortex-m为例分析
4.1 cortex-m 异常向量表分析
-
下图是cortex-m7的异常向量表,DCD是arm汇编器的语法,意思是定义一个数据,不难发现cortex-m系列的异常向量中存储的并不是汇编指令,而是一个数据,这个数据在汇编中是一个符号(符号最终由连接器分配地址),在C语言中是一个函数指针(指针编译完成后就是一个符号,最终交给链接器分配地址),当发生异常时,由硬件读取异常向量表中的地址,由硬件直接跳转到向量表对应的地址。
注1:这个条转过程由硬件完成,向量表中只要写入异常向量的地址
注2:在cortex-m架构中异常向量表可以由C语言实现(下图是由汇编实现),定义一个函数指针数组数字放到VTOR寄存器中指示的地址即可
4.2 硬件保存上下文
- 在cortex-m架构中,在编写中断服务函数时直接只用向量表中的符号名称写函数即可,如下所示,
void SysTick_Handler (void) { } void WWDG_TRQHandler (void) { }
- 函数不需要任何修饰,编译器会将这个函数当做普通的函数进行编译,启动文件中也没有保存上下文的过程,当发生异常时硬件会直接跳转到c函数中,那么这样会不会有调用者保护的寄存器被破坏的风险?答案是不会的,事实上在cortex-m结构中保存上下文和恢复上下文都是由硬件完成的,下图是异常发生时硬件保存的栈帧:
-
无浮点寄存器的栈帧
-
有浮点寄存器的栈帧
-
大家可以对比一下硬件所保存的上下文正是AAPCS所规定的调用者保护的寄存器外加一个返回地址,返回地址正是执行完异常后下一条应该执行指令的地址。
-
4.3 硬件恢复上下文
- 思考一个问题,异常发生时硬件压入的栈帧相较与每个函数的栈帧是特殊的,弹栈方式也不一样,且异常栈帧可能出现在栈的任何位置,那么硬件是怎么知道什么时候应该恢复异常呢?事实上在异常发生的时候,硬件除了将异常上下文保存到栈中,还在LR寄存器中存放了一个特殊的值然后才会进入C语言的世界,当函数按照AAPCS所规定的方式调用返回函数是,当硬件发现LR寄存器中存在一个特殊的值,这个时候硬件就知道此处应该进行异常返回了,然后会将栈中保存的寄存器逐个恢复,然后继续异常前的程序执行,arm将这个异常的值命名为EXC_RETURN。
- EXC_RETURN定义了返回的模式(线程模式/处理模式),异常栈帧的类型(有无浮点寄存器)。
CONTROL寄存器的FPCA位指示上下文中有无浮点运算,若有则应该保存浮点寄存器。
- 思考一个问题:中断抢占硬件会怎么处理?有了上面的描述,这应该很容易想到。
提示:返回模式
4.4 总结
cortex-m在异常处理中将不符和AAPCS规定的部分都是用硬件实现,这使得编程变得非常方便,使得在整个程序中无需使用一条汇编便可进行编程。
cortex-a和cortex-r并不是这样,cortex-a和cortex-r在异常处理时硬件参与的很少,上下文的保存和恢复都由软件完成,所以编程中也不可避免的需要使用汇编。
5.对比cortex-m7和cortex-a9的启动文件
下图是cortex-a9的异常向量表。
- 与cortex-m不同,cortex-a9的异常向量表存储的是跳转指令(必须用汇编实现),硬件只会在异常发生后跳转到对应的地址继续执行程序,并不会做多余的动作。
- cortex-a9在发生异常时,保存和恢复上下文的工作由软件完成,这部分也必须用汇编实现。
- 在cortex-a9中异常发生时,不同异常要保存的上下文是不同的,这是由于cortex-a9在不同的模式下有自己不同的备份寄存器,在此先不展开,以后有机会专门写一篇文章进行分析