MIPS架构深入理解3-协处理器0

理解CP0协处理器的作用以及如何控制CPU的行为

Posted by Tupelo Shen on May 19, 2020

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 特殊寄存器的使用场景

  1. 上电后:需要设置SR寄存器,使CPU进入一个可工作的状态。

  2. 处理异常:

    在异常入口处,不会保存任何程序计数器,只把返回地址存入EPC寄存器中。MIPS架构CPU硬件对于堆栈一无所知,所以发生异常时,无法打印堆栈中的数据。(ARM和X86硬件可以保存堆栈,所以,发生异常时,可以打印堆栈中的关键数据)。对于MIPS架构,程序发生异常时,只能看EPC寄存器中的值,然后通过反汇编得到执行代码的地址,从而获取到导致异常的代码大概位置。充分利用异常发生时的信息,是调试程序的一种有效手段。

    MIPS架构也为异常处理程序保留了2个寄存器v0v1。我们的程序可以把一些异常需要的重要信息保存在这儿。但是,通用寄存器极易发生变化,大部分时候,这两个寄存器不建议使用。

    可以通过查看Cause寄存器,判断属于哪类异常,从而做相应的处理。

  3. 从异常返回时:

    保存返回地址到EPC寄存器中。

    不论是何种异常,返回时,都要恢复SR寄存器和特权等级、使能中断并消除异常带来的影响。最后eret指令返回用户程序并复位SR(EXL)寄存器。

  4. 中断:

    通过SR寄存器中的中断控制位,可以设置哪些中断具有更高的优先级。虽然,MIPS架构硬件没有提供中断优先级,但是软件可以任意设置。

  5. 一些特殊的指令:

    比如系统调用(syscall)和调试断点(break),还有一些CPU实现了一些特殊的指令。

4 CP0协处理器操作时可能发生的问题

我们知道CPU的指令是按照流水线的方式执行。有可能,操作协处理器的指令还没执行彻底,其它指令就已经开始执行了。如何才能保证CP0的操作生效后,再执行相关指令呢?

因为MIPS架构的设计理念是 硬件尽量简单,辅以软件实现。所以,早期的软件开发人员使用nop操作,保证操作协处理器的正确性。但是,这无疑增加了软件开发人员的难度。于是,MIPS32/64架构定义了新的指令:避险指令。

三个避险指令:

  1. ehb指令

    消除执行危险。早期的MIPS架构CPU把这个当做一个nop操作。

  2. jr.hb和jalr.hb指令

    跳转寄存器指令,用来消除指令危险。最常见的使用方式就是替换普通的子程序返回和子程序调用指令。

    旧架构上,这两个指令还是会被解释成jr和jalr指令。在这些CPU上,指令会清除CPU的管道流水线。而且大部分时候,对于不遵守MIPS32/64架构规范的CPU还会提供必要的延时。

4.1 指令危险

指令危险和用户危险通常发生在改变CP0状态的时候(比如,改变某个寄存器、TLB项、或者一个cache行),这会影响我们普通的取值指令(在某些情况下,还会影响load/store指令访问内存的方式)。

我们必须规避这种不可控的风险。在改变CP0操作之后,添加危险屏障指令,消除这种可能产生的不可控的危险。

这类危险都有:

  1. 改变TLB项:

    在受影响的内存页上取指、加载和存储数据。

  2. 改变EntryHi寄存器(ASID域)

    非全局映射内存区域上的取指、加载和存储数据。

  3. 改变到ERL模式

    从kuseg内存区域取指、加载和存储数据。

  4. cache指令改变cache行

    在受影响的line上取指、加载和存储数据。

  5. 改变watchpoint寄存器

    在匹配的地址上取指、加载和存储数据

  6. 影子寄存器设置发生改变

    任何使用通用寄存器的情况(执行危险)

  7. 修改CP0寄存器,禁止中断

    仍然能够被中断的指令(异常危险)

它们中大部分都是指令危险,可以使用jr.hbjalr.hb指令避免这种指令危险。

4.2 CP0指令间的危险

mfc0tlbwitlbwrtlbr指令、读取CP0的cache指令以及tlbp指令都依赖于CP0寄存器中的值。所以,这些指令执行时,有可能发生执行危险。为了保证安全,可以在

可以在读取CP0寄存器值的指令之前,添加ehb指令。