1 引言
1.1 什么是协处理器0
前面我们已经对MIPS架构CPU有了粗略的了解。显然,它提供了众多优秀的功能。但是,应用的场景不同,往往需要CPU做的事情也不一样,这就需要必须能够对CPU以及它提供的功能进行有选择的配置。这是协处理器诞生的根本原因。
ARM架构也使用协处理器进行控制,称为协处理器15,(cp15)。
MIPS架构CPU使用协处理器0进行CPU的配置和管理。那么,它到底能够干什么呢?
- CPU配置
- Cache控制
- 异常、中断控制:
中断或异常发生时的行为和处理的定义。 - 内存管理单元控制
- 其它工作:
定时器(timer)、事件计数器(event)、奇偶/错误校验。一些与CPU紧密相关,而又不便通过I/O进行访问的功能,都会被添加到协处理器0中进行控制。
1.2 包含的寄存器
对于相关的寄存器,在此,不再详述。使用时,参阅相关的数据手册即可。
2 CPU控制指令
2.1 写CPU控制寄存器的指令
mtc0 s, <n> # 把数据拷贝到协处理器0
这条指令的作用是把通用寄存器s中的值拷贝到协处理器的寄存器n中,数据位数是32位。大部分的协处理器寄存器是32位的,对于少数的64位协处理器寄存器可以使用dmtc0
指令进行操作。这是设置CPU控制寄存器的唯一方法。
32位架构的时候,最多有32个协处理器寄存器。但是MIPS32/64架构扩展到了256个寄存器,为了向前兼容,在指令中添加select域来控制多个寄存器。比如
mtc0 s, $12, 1
select域的值等于1,其作用就是把通用寄存器s的值写入到协处理器的寄存器12组中的编号为1的寄存器中。也就是说,寄存器12可以有多个具体的寄存器,使用select域选择对应的哪个寄存器。
2.2 读取CPU控制寄存器的指令
mfc0 d, $n # 把协处理器第n个寄存器中的值写入到通用寄存器d中
上述指令的作用是把协处理器0中的第n个寄存器中的内容读取到通用寄存器d中。
2.3 特殊的控制指令eret
所有的架构的CPU在面对特权等级切换的时候(一般就是异常返回时),都会面临一个问题:一方面,在返回用户态程序之前就降低特权等级,那么会立即引发一个异常指令访问的二次异常;另一方面,如果返回到用户程序之后再降低特权等级,那么可能会被恶意程序利用内核态运行某些指令。解决这个问题的办法就是,保证异常返回时的指令是原子操作。MIPS架构的CPU提供了这个指令eret
。
3 特殊寄存器的使用场景
- 上电后:需要设置SR寄存器,使CPU进入一个可工作的状态。
- 处理异常:
在异常入口处,不会保存任何程序计数器,只把返回地址存入EPC寄存器中。MIPS架构CPU硬件对于堆栈一无所知,所以发生异常时,无法打印堆栈中的数据。(ARM和X86硬件可以保存堆栈,所以,发生异常时,可以打印堆栈中的关键数据)。对于MIPS架构,程序发生异常时,只能看EPC寄存器中的值,然后通过反汇编得到执行代码的地址,从而获取到导致异常的代码大概位置。充分利用异常发生时的信息,是调试程序的一种有效手段。
MIPS架构也为异常处理程序保留了2个寄存器v0
和v1
。我们的程序可以把一些异常需要的重要信息保存在这儿。但是,通用寄存器极易发生变化,大部分时候,这两个寄存器不建议使用。
可以通过查看Cause寄存器,判断属于哪类异常,从而做相应的处理。 - 从异常返回时:
保存返回地址到EPC寄存器中。
不论是何种异常,返回时,都要恢复SR寄存器和特权等级、使能中断并消除异常带来的影响。最后eret指令返回用户程序并复位SR(EXL)寄存器。 - 中断:
通过SR寄存器中的中断控制位,可以设置哪些中断具有更高的优先级。虽然,MIPS架构硬件没有提供中断优先级,但是软件可以任意设置。 - 一些特殊的指令:
比如系统调用(syscall)和调试断点(break),还有一些CPU实现了一些特殊的指令。
4 CP0协处理器操作时可能发生的问题
我们知道CPU的指令是按照流水线的方式执行。有可能,操作协处理器的指令还没执行彻底,其它指令就已经开始执行了。如何才能保证CP0的操作生效后,再执行相关指令呢?
因为MIPS架构的设计理念是 硬件尽量简单,辅以软件实现。所以,早期的软件开发人员使用nop操作,保证操作协处理器的正确性。但是,这无疑增加了软件开发人员的难度。于是,MIPS32/64架构定义了新的指令:避险指令。
三个避险指令:
- ehb指令
消除执行危险。早期的MIPS架构CPU把这个当做一个nop操作。 - jr.hb和jalr.hb指令
跳转寄存器指令,用来消除指令危险。最常见的使用方式就是替换普通的子程序返回和子程序调用指令。
旧架构上,这两个指令还是会被解释成jr和jalr指令。在这些CPU上,指令会清除CPU的管道流水线。而且大部分时候,对于不遵守MIPS32/64架构规范的CPU还会提供必要的延时。
4.1 指令危险
指令危险和用户危险通常发生在改变CP0状态的时候(比如,改变某个寄存器、TLB项、或者一个cache行),这会影响我们普通的取值指令(在某些情况下,还会影响load/store指令访问内存的方式)。
我们必须规避这种不可控的风险。在改变CP0操作之后,添加危险屏障指令,消除这种可能产生的不可控的危险。
这类危险都有:
- 改变TLB项:
在受影响的内存页上取指、加载和存储数据。 - 改变EntryHi寄存器(ASID域)
非全局映射内存区域上的取指、加载和存储数据。 - 改变到ERL模式
从kuseg内存区域取指、加载和存储数据。 - cache指令改变cache行
在受影响的line上取指、加载和存储数据。 - 改变watchpoint寄存器
在匹配的地址上取指、加载和存储数据 - 影子寄存器设置发生改变
任何使用通用寄存器的情况(执行危险) - 修改CP0寄存器,禁止中断
仍然能够被中断的指令(异常危险)
它们中大部分都是指令危险,可以使用jr.hb
或jalr.hb
指令避免这种指令危险。
4.2 CP0指令间的危险
mfc0
、tlbwi
、tlbwr
、tlbr
指令、读取CP0的cache指令以及tlbp
指令都依赖于CP0寄存器中的值。所以,这些指令执行时,有可能发生执行危险。为了保证安全,可以在
可以在读取CP0寄存器值的指令之前,添加ehb
指令。