予焦啦!scratch 控制暂存器

本节是以 Golang 上游 8854368cb076ea9a2b71c8b3c8f675a8e19b751c 为基准做的实验

予焦啦!上下文(context),代表的是一个行程(process)的状态,作业系统负责维持这些状态的一致性,好让使用者空间程序可以按照它自己被设计的方式执行,而毋需考量中断、缺页例外之类的事件。理想上应该做到这个程度,

但是,我们昨天开始导入了中断机制之後,一切都变了。行程将会随时被中断,改变程序流程到 stvec 所在之处。在 early_halt 之中,暂存器被任意使用,但万一那些暂存器中断前正在被使用的话,这个中断处理本身就铸下无法恢复的大错。

本节重点概念

  • RISC-V
    • scratch:暂用控制暂存器
    • Linux 如何使用 sscratch
    • OpensbI 如何使用 mscratch

为何需要暂用控制暂存器?

试想一个突然被中断的程序,CPU 怀抱着当下的状态,被迫将程序指标指向了 stvec。以我们一直仰赖它回报错误状态的 early_halt 为例:

TEXT early_halt(SB),NOSPLIT|NOFRAME,$0
        MOV     $0x49, A0
        MOV     $1, A7
        MOV     $0, A6
        ECALL
        CSRRS   CSR_SCAUSE, ZERO, A0
        CALL    dump(SB)
        ...

是的,我们至今还残留着最一开始随意印出的那个 I 字元。不过这无所谓。我们很明显看到的是,A0A6A7 三个暂存器,就这样直接被覆写掉了。也就是说,除非有将原本的暂存器的值储存下来,否则原本被中断的程序已经失去了部份资讯,就算能够回到被中断时的程序指标继续下去,也很可能已无法顺利执行原先的逻辑了

所以这就是为什麽我们需要一个额外的空间腾出来储存资讯,这个东西就是作业系统模式底下的 sscratch 与机器模式底下的 mscratch

有趣的是,我们目前为止看到的所有控制与状态暂存器的使用,都需要仰赖 CSR* 系列指令,而其中都必然会牵涉到一般的暂存器。若是为了保护一般暂存器作为行程的上下文而使用 scratch,但使用之又必然需要破坏一般的暂存器,这又该怎麽办?话又说回来,这一个暂存器也只有 64 个位元大小,要怎麽储存一堆暂存器的内容呢?

以下,我们将观察两个风格略有不同的系统软件如何使用这个控制暂存器。

Linux

在 Linux 的例外与中断入口,是这样的程序码:

ENTRY(handle_exception)
        /*
         * If coming from userspace, preserve the user thread pointer and load
         * the kernel thread pointer.  If we came from the kernel, the scratch
         * register will contain 0, and we should continue on the current TP.
         */
        csrrw tp, CSR_SCRATCH, tp
        bnez tp, _save_context

_restore_kernel_tpsp:
        csrr tp, CSR_SCRATCH
        REG_S sp, TASK_TI_KERNEL_SP(tp)
_save_context:
        REG_S sp, TASK_TI_USER_SP(tp)
        REG_L sp, TASK_TI_KERNEL_SP(tp)
        addi sp, sp, -(PT_SIZE_ON_STACK)

首先,tp 暂存器与 sscratch 控制暂存器的内容会互换,从而没有任何资讯被破坏;这是因为 CSRRW 能够确保原子性的读写,所以让 tp 同为来源与目标,即可完成两者的互换。这也解答了先前的第一个问题。

之後的流程,基本上注解就解释了一切。如果是从使用者模式进来的,使用者空间用到一半的 tp 被存放到 sscratch 之中,而原先在 sscratch 当中的值,恰好是 Linux 核心用来表达该使用者行程的结构体(型别为 struct task_struct)的指标。透过这个指标,就能够存取到核心管理的资讯,比方说这个行程在核心之中有资格使用的堆叠位址(KERNEL_SP),就可以提取出来放置到堆叠指标暂存器 sp 之中,从而继续执行下去呼叫 C 语言函数也不会有问题。

因为更後面的程序码保证 sscratch 会被清成 0,所以如果是从作业系统模式进入,也就是说在核心内部触发了例外或是中断的情况,就能够透过 tp 是否为 0 来判断来自何处。而之後无论如何,核心本身都可以使用 tp 暂存器存取所需的资讯,其中最重要的可以说是堆叠位址。

因为,颇为大量的暂存器内容,放在堆叠之中是最理想的。如果设计了某个特别用来放置的空间,在巢状中断(nested interrupt)发生的情况,就又得特别处理了。

OpenSBI

OpenSBI 是不同的风格。在机器模式初始化时,OpenSBI 就会配置一块空间,且确保每一个处理器核都有自己专属的 mscratch 指标。

进入点 mtvec 之後马上用到的巨集名为 TRAP_SAVE_AND_SETUP_SP_T0,所以如果说 Linux 的策略是针对 tp,那麽 OpenSBI 的策略就是一次搞定 sp 堆叠指标和 t0

以下分为外壳与内部的两个小节,分别拆解之。

处理 t0sp 并暂时挪用 tp 的外壳

	/* Swap TP and MSCRATCH */
	csrrw	tp, CSR_MSCRATCH, tp

	/* Save T0 in scratch space */
	REG_S	t0, SBI_SCRATCH_TMP0_OFFSET(tp)
	
    ...
    /*内部的炫技部分*/
    ...
	/* Save original SP on exception stack */
	REG_S	sp, (SBI_TRAP_REGS_OFFSET(sp) - SBI_TRAP_REGS_SIZE)(t0)

	/* Set SP to exception stack and make room for trap registers */
	add	sp, t0, -(SBI_TRAP_REGS_SIZE)

	/* Restore T0 from scratch space */
	REG_L	t0, SBI_SCRATCH_TMP0_OFFSET(tp)

	/* Save T0 on stack */
	REG_S	t0, SBI_TRAP_REGS_OFFSET(t0)(sp)

	/* Swap TP and MSCRATCH */
	csrrw	tp, CSR_MSCRATCH, tp
.endm

一开始还是直接交换了 tpmscratch。这个 tp ,对於每个核来说,都指向自己专属的区域。里面预留了一个空间可以暂时存放 t0,偏移量在 SBI_SCRATCH_TMP0_OFFSET,前半用以存放,後半晚期将之提取出。因此,t0 在这之间的内部区域,就能够用来当作计算时候的暂时空间使用。

在内部结束之後,其实 t0 是成为一个当前可运作的堆叠指标。从下半部第二行开始解释比较简单。下半部第二行,是将之後将要使用的堆叠指标计算出来。减去的 SBI_TRAP_REGS_SIZE 是所需存放的上下文的总量,t0 则是当前的堆叠指标。回头看下半部第一行,很复杂的偏移量计算是为了先不调整 sp 本身(因为第二行才要调整),但又要将 sp 存到对的地方去。

下半部第三行提取先前存放的 t0(相对於 OpenSBI 的 scratch 结构体,这里省略细节),并相对於新的堆叠,存放到属於 t0 的位置去。最後,再将 tp 切换回原本的内容。

内部的位元操作:判定可用堆叠

Linux 比较单纯,是因为从行程结构体当中,可以在不同的偏移量找到使用者模式或是作业系统模式的堆叠指标,但 OpenSBI 的 scratch 结构体没有这麽方便,并且也有诸多不同的低权限等级要处理。所以这里,单单是藉助一个可以用的 t0

	/*
	 * Set T0 to appropriate exception stack
	 *
	 * Came_From_M_Mode = ((MSTATUS.MPP < PRV_M) ? 1 : 0) - 1;
	 * Exception_Stack = TP ^ (Came_From_M_Mode & (SP ^ TP))
	 *
	 * Came_From_M_Mode = 0    ==>    Exception_Stack = TP
	 * Came_From_M_Mode = -1   ==>    Exception_Stack = SP
	 */
	csrr	t0, CSR_MSTATUS
	srl	t0, t0, MSTATUS_MPP_SHIFT
	and	t0, t0, PRV_M
	slti	t0, t0, PRV_M
	add	t0, t0, -1
	xor	sp, sp, tp
	and	t0, t0, sp
	xor	sp, sp, tp
	xor	t0, tp, t0

注解一样提供比较多的指引。简单来说这一段结束之後,即要符合前一小节中的下半部的预期,也就是 t0 必须是 CPU 来到机器模式处理例外或中断时,接着使用的堆叠指标。如何做到?

首先,先做一个判断。因应机器模式所需面对的诸多低权限等级,以及可能来自机器模式本身,先设法创造一个区别出来。slti 指令之前就是在做这个区分。但是由於该指令只能产生 0 或是 1 的数值,对於接下来的互斥或(eXclusive OR)技巧帮助不大,所以再做了一个减 1 的算术转换,使之成为 -1(原本来自机器模式)与 0(来自其他权限等级)。

且看注解中的 Exception_stack 虚拟码变数。若是前述判断是 -1,那麽它之後的逻辑且(& 符号,and)相当於是每一个位元都继续留存,而整个叙述就会变成 tp 参与了两次互斥或而抵消效应,成为 sp 留存的运算结果;而若是判断为 0,後面的部分就全部被清掉,使得最後的结果为与 0 进行互斥或的 tp

之所以从机器模式进入就应使用 sp 是因为,sp 本就是机器模式运行期间使用的堆叠指标,继续使用不会有权限的问题;其余模式进入者,与其说是使用 tp,不如说是使用来自 mscratch 的内容。但无论是何者,由於还没有正式决定要存放上下文的堆叠,所以真正要使用的堆叠指标还是存放在 t0 当中。

之所以说这一段是在炫技,是因为 sptp 都参与其中,但却都没有被损坏;真正被拿来使用的,以结果来说只有 t0 而已。还有为了使用互斥或技法而使用的减一,都很精彩。注解的部分也提供一个范本,当你必须使用一连串可读性不高的方法来写程序的时候,应该如何透过注解教育後进。

小结

予焦啦!今日分别介绍了作业系统模式与机器模式两大系统软件的进入点处理时,如何透过 scratch 控制暂存器完成上下文存放前的安排。说是安排,实际上是决定了接下来任何的例外处理或是中断处理所需要的堆叠之所在,因为那是最适合存放上下文的地方。

怀抱着这份理解,我们一如往常的必须回到 Hoddarla 本位来思考,那就是 Golang 执行期里面,是否有什麽框架可以套用呢?今天我们没有写什麽新的程序码,明天因为要延续这个思绪继续探索的缘故,应该也不会有吧。

各位读者,我们明天再会!


<<:  [Java Day17] 4.5. 多载

>>:  [Day13] 补充说明 – csrf

企划实现(18)

在撰写程序时我发现了一个以前没有遇到过的事情,我原先一直以为是因为环境导致的但是後来我发现跟环境没有...

What's radiance?

目录 零、要讨论的主题 一、图解什麽是L 二、为什麽是L 三、参考资料 四、彩蛋 零、要讨论的主题 ...

2021 WordPress虚拟主机推荐-从SiteGround搬到CLOUDWAYS

想记录一下搬家到Cloudways的心路历程以及经验分享... 老实说,我非常庆幸自己没有经历过Su...

Day9-"格式化符号"

昨天在练习scanf时,题目规定说输入为字串,一开始都是以%d,做为字串的格式,但在printf时发...

【JavaScript】在JavaScript中使用switch(true)

开发专案时,在其中的某个环节,想说除了switch之外,是否有更好的写法,上网一查发现,还有swit...