0%

mips cp0

MIPS 的协处理器 CP0 (Section 1)

在 MIPS 体系结构中,最多支持 4 个协处理器 (Co-Processor)。其中,协处理器 CP0 是体系结构中必须实现的。它起到控制 CPU 的作用。MMU、异常处理、乘除法等功能,都依赖于协处理器 CP0 来实现。它是 MIPS 的精髓之一,也是打开 MIPS 特权级模式的大门。
  MIPS 的 CP0 包含 32 个寄存器。关于它们的资料可以参照 MIPS 官方的资料 MIPS32(R) Architecture For Programmers Volume III: The MIPS32(R) Privileged Resource Architecture 的 Chap7 和 Chap8。本文中,仅讨论常见的一些寄存器。

  • Register 0: Index,作为 MMU 的索引用。将来讨论 MMU 和 TLB 时会详解之
  • Register 2, EntryLo0,访问 TLB Entry 偶数页中的地址低 32Bit 用。同上,在 MMU 和 TLB 的相关章节中详解。
  • Register 3, EntryLo1,访问 TLB Entry 奇数页中的地址低 32Bit 用。
  • Register 4, Context,用以加速 TLB Miss 异常的处理。
  • Register 5, PageMask,用以在 MMU 中分配可变大小的内存页。
  • Register 8, BadVAddr,在系统捕获到 TLB Miss 或 Address Error 这两种 Exception 时,发生错误地址会储存在该的虚拟寄存器中。对于引发 Exception 的 Bug 的定位来说,这个寄存器非常重要。
  • Register 9, Count,这个寄存器是 R4000 以后的 MIPS 系统引入的。它是一个计数器,计数频率是系统主频的 1/2。BCM1125/1250,RMI XLR 系列以及 Octeon 的 Cavium 处理器均支持该寄存器。对于操作系统来说,可以通过读取该寄存器的值来获取 tick 的时基。在系统性能测试中,利用该寄存器也可以实现打点计数。
  • Register 10,EntryHi,这个寄存器同 EntryLo0/1 一样,用于 MMU 中。以后会详述。
  • Register 11,Compare,配合 Count 使用。当 Compare 和 Count 的值相等的时候,会触发一个硬件中断 (Hardware Interrupt),并且总是使用 Cause 寄存器的 IP7 位。
  • Register 12,Status,用于处理器状态的控制。
  • Register 13,Cause,这个寄存器体现了处理器异常发生的原因。
  • Register 14,EPC,这个寄存器存放异常发生时,系统正在执行的指令的地址
  • Register 15,PRID,这个寄存器是只读的,标识处理器的版本信息。向其中写入无意义。
  • Register 18/19,WatchLo/WatchHi,这对寄存器用于设置硬件数据断点 (Hardware Data Breakpoint)。该断点一旦设定,当 CPU 存取这个地址时,系统就会发生一个异常。这个功能广泛应用于调试定位内存写坏的错误。
  • Register 28/29,TagLo 和 TagHi,用于高速缓存 (Cache) 管理。

#查看cp寄存器(openocd)

mon mips32 cp0

CP0 常用寄存器

下面,我们来详解 CP0 中常用的几个寄存器,它们是:BadVAddr,Count/Compare,Status/Cause,EPC,WatchLo/WatchHi。

MIPS 的异常与中断 (Section 2)

![[mips 大核手册#^7a2f66]]
其中,Exception 0-5, 8-11, 13, 23 这几个异常类型较为常见。

  • Exception 0:Interrupt,外部中断。

    它是唯一一个异步发生的异常。之所以说中断是异步发生的,是因为相对于其他异常来说,从时序上看,中断的发生是不可预料的,无法确定中断的发生是在流水线的哪一个阶段。MIPS 的五级流水线设计如下:
    IF, RD, ALU, MEM, WB。

    MIPS 处理器的中断控制部分有这样的设计:

    在中断发生时,如果该指令已经完成了 MEM 阶段的操作,则保证该指令执行完毕。反之,则丢弃流水线对这条指令的工作。除 NMI 外,所有的内部或外部硬件中断 (Hardware Interrupt) 均共用这一个异常向量(Exception Vector)。前面提到的 CP0 中的 Counter/Compare 这一对计数寄存器,当 Counter 计数值和 Compare 门限值相等时,即触发一个硬件中断。

  • Exception 1:TLB Modified,内存修改异常。

    如果一块内存在 TLB 映射时,其属性设定为 Read Only,那么,在试图修改这块内存内容时,处理器就会进入这个异常。显然,这个异常是在 Memory 阶段发生的。但是,按 “精确异常” 的原则,在异常发生时,ALU 阶段的操作均无效,也就是说,向内存地址中的写入操作,实际上是不会被真正执行的。这一判断原则,也适用于后面的内存读写相关的异常,包括 TLB Miss/Address Error/Watch 等。

  • Exception 2/3:TLB Miss Load/Write,

    如果试图访问没有在 MMU 的 TLB 中映射的内存地址,会触发这个异常。在支持虚拟内存的操作系统中,这会触发内存的页面倒换,系统的 Exception Handler 会将所需要的内存页从虚拟内存中调入物理内存,并更新相应的 TLB 表项。

  • Exception 4/5:Address Error Load/Write

    如果试图访问一个非对齐的地址,例如 lw/sw 指令的地址非 4 字节对齐,或 lh/sh 的地址非 2 字节对齐,就会触发这个异常。一般地,操作系统在 Exception Handler 中对这个异常的处理,是分开两次读取 / 写入这个地址。虽然一般的操作系统内核都处理了这个异常,最后能够完成期待的操作,但是由于会引起用户态到内核态的切换,以及异常的退出,当这样非对齐操作较多时会严重影响程序的运行效率。因此,编译器在定义局部和全局变量时,都会自动考虑到对齐的情况,而程序员在设计数据结构时,则需要对对齐做特别的斟酌。

  • Exception 6/7:Instruction/Data Bus Error

    一般地原因是 Cache 尚未初始化的时候访问了 Cached 的内存空间所致。因此,要注意在系统上电后,Cache 初始化之前,只访问 Uncached 的地址空间,也就是 0xA0000000-0xBFFFFFFF 这一段。默认地,上电初始化的入口点 0xBFC00000 就位于这一段。(某些 MIPS 实现中可以通过外部硬线连接修改入口点地址,但为了不引发无法预料的问题,不要将入口点地址修改为 Uncached 段以外的地址)

  • Exception 8:Syscall,系统调用的正规入口,也就是在用户模式下进入内核态的正规方式。

    我们可以类比 x86 下 Linux 的系统调用 0x80 来理解它。它是由一条专用指令 syscall 触发的。

  • Exception 9:Break Point,绝对断点指令。

    和 syscall 指令类似,它也是由专用指令 break 触发的。它指示了系统的一些异常情况,编程人员可以在某些不应当出现的异常分支里面加入这个指令,便于及早发现问题和调试。我们可以用高级语言中的 assert 机制来类比理解它。最常见的 Break 异常的子类型为 0x07,它是编译器在编译除法运算时自动加入的。如果除数为 0 则执行一条 break 0x07 指令。这样,当出现被 0 除的情况时,系统就会抛出一个异常,并执行 Coredump,以便于程序员定位除 0 错误的根因。

  • Exception 10:RI,执行了没有定义的指令,系统就会发生这个异常。

  • Exception 11,Co-Processor Unaviliable,试图访问的协处理器不存在。比如,在没有实现 CP2 的处理器上执行对 CP2 的操作,就会触发这个异常。

  • Exception 12,Overflow,算术溢出。会引起这个异常的指令,仅限于加减法中的带符号运算,如 add/addi 这样的指令。因此,一般地,编译器总是将加减法运算编译为 addiu 这样的无符号指令。由于 MIPS 处理异常需要一定的开销,这样可以避免浪费。

  • Exception 13,Trap,条件断点指令。

    它由 trap 系列指令引发。与 Break 指令不同的是,只有满足断点指令中的条件,才会触发这个异常。我们可以类比 x86 下的 int 3 断点异常来理解它。

  • Exception 14,VCEI,(不明白!谁知道是干嘛使的?)

  • Exceotion 15,Float Point Exception,浮点协处理器 1 的异常。它由 CP1 自行定义,与 CP1 的具体实现相关。其实就是专门为 CP1 保留的异常入口。

  • Exception 16,协处理器 2 的异常,和前一个异常一样,是和 CP2 的具体实现相关的。

  • Exception 23,Watch 异常。前面讲到 Watch 寄存器可以监控一段内存,当访问 / 修改这段内存时,就会触发这个异常。在异常处理例程中,通过异常栈可以反推出是什么地方对这段内存进行了读 / 写操作。这个异常是用来定位内存意外写坏问题的一柄利器。

MIPS 的异常与中断 (Section 3)

异常发生的时候,系统会完成一个从用户态到内核态的切换。我们前面提到,对系统某些资源的访问(如 CP0 等协处理器,Kernel Segment 内存),是必须在内核态进行的。因此,如果希望使系统从用户态进入内核态,那么,就必须产生一个异常。
MIPS 的异常处理,通常来说,和其他体系结构的异常 / 中断 / 陷阱处理,没有太多的区别,总的来说分为三段:

  1. 保存现场寄存器组(Register File)。在堆栈中开辟一段区域,将 32 个通用寄存器和 CP0 的相关寄存器,如 Status,BadVaddr,Cause 等,保存在这段内存中。其中尤为重要的是 EPC,EPC 指向引发异常时的指令。
    在这个步骤中,首先保存的应该是通用寄存器组,随后是 epc/cause/status/badvaddr 这几个 epc0 中的寄存器。从 cp0 到内存的数据传输必须通过通用寄存器。一般地,编程时的约定是使用 k0 和 k1 这两个寄存器暂存。如下例:(适用于 32 位 MIPS 模式)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    sw zero, 0(sp)
    sw at, 4(sp)
    sw v0, 8(sp)
    ...
    sw ra, 124(sp) / *先保存通用寄存器组* /
    mfc k0, epc
    nop / *mfc 太慢,要在延迟槽中加一个 nop* /
    sw k0, 128(sp)
    mfc k0, cause
    nop
    sw k0, 132(sp)
    ...

2,异常处理部分。以 Address Error 异常为例,当异常发生时,根据保存的 BadVaddr,调用两次非对齐加载 / 存储指令,对内存地址进行数据的读写操作。
3,返回。将保存在堆栈中的寄存器组内容恢复。

​ 从异常状态返回的这个动作,是由硬件完成的。它必须同时完成三个操作:

  1. 将 SR 寄存器恢复;
  2. 返回到 EPC 寄存器所指向的地址继续执行;
  3. 恢复到用户态。

如《See MIPS Run》提到的,如果这三个过程没有能够 “原子地” 执行完毕,那么将会导致一个安全漏洞,用户有可能在某种情况下僭越 CPU 内核态设定的壁垒,从而非法获得管理员权限。
在 MIPS I 和 MIPS II 处理器中,使用 rfe 这条指令,来进行 “从异常中恢复”,也就是恢复 SR 寄存器,并且将系统从内核态恢复到用户态。但这条指令并没有将执行的指令地址返回到异常发生的指令处。这项工作应当由在此之前的一条 JR 指令来执行。这样,从异常中返回的相关汇编代码应当为:

1
2
3
mfc k0, epc
jr k0
rfe / *在上一条 jr 指令的延迟槽中执行,这样可以保证原子性* /

在 MIPS III 及以后的处理器中,从异常中返回不再需要这样的繁文缛节,只需要一条 eret 指令便万事俱备了。