Loading

第一章 语言的切换

在RISC-V中,每个物理内核通常被称为hart(硬件线程),复位后所有hart都会从同一入口地址(例如0x80000000)开始执行。为了简化我们的工作,我们的目标是设计在一个hart中·运行的内核,那么就需要关停其他hart。

要关停其他物理内核,其实很简单,我们只需要从多个hart中选择一个使用就行。假设我们选择ID为0的hart为我们要用的hart,可以使用特权指令 csrr 将hart id读取到 t0 中,然后将t0的值赋值到 tp 中,接着使用 benz 指令判断 t0 内的值是不是0,不是就跳转到park块。park是一个中断死循环,内有 wfi 指令。之所以要把hartid存到 tp 中,是因为 tp 寄存器在当前上下文中被临时用作“当前 hart ID 的缓存。这样后续代码想判断自己是哪个 hart 时,直接读 tp 即可,无需再次执行 csrrcsrr作为特权指令相对来说时间花销比较大,存在 tp 中后续调用就非常快了。另外需要指出的是,现在用的寄存器不是所有hart共用的全局寄存器,而是私有的寄存器。

具体的汇编代码如下:

csrr t0, mhartid   # 读取当前 hart 的 ID
mv   tp, t0        # 将 hart id 存入 tp 寄存器
bnez t0, park      # 如果 hart id != 0,则跳转到 park 循环

park块的设计主要是中断休眠和循环:

park:
	wfi
	j	park

现在关停了其他的hart之后,还需要给每个hart初始化,使其栈指针 tp 能够指向对应的栈顶的位置。虽然我们现在不使用其hart,但是万一后面我们需要将操作系统从单核升级到多核,这一步骤是可以提前做的。tp 需要指向物理内存上的最低位。另外,预留的空间需要给所有的hart用。因此,每个hart都要有自己的栈空间。我们假设给每一个hart预留1024B的大小。如下图所示:

image1

slli   t0, t0, 10          # t0 = hart_id * 1024
la     sp, stacks + STACK_SIZE   # sp = stacks + STACK_SIZE(第一个 hart 的栈顶)
add    sp, sp, t0          # sp = stacks + STACK_SIZE + hart_id * 1024

上一段提到为所有 hart 分配了一块连续的栈内存区域,代码如下:

.balign 16               # 使下一个符号(stacks)的地址是 16 的整数倍
stacks:
    .skip   STACK_SIZE * MAXNUM_CPU   # 为所有 hart 预留连续的栈空间
.end                     # 文件结束

这些准备工作就绪了之后,我们就可以从繁琐的汇编语言编程切换到c语言的编程了,只需要使用 j 命令即可跳转到c语言的入口。

j      start_kernel        # 跳转到 C 内核入口

完整的代码如下:

// 包含平台相关的头文件,定义了 MAXNUM_CPU 等常量
#include "../../platform.h"

	# size of each hart's stack is 1024 bytes
	.equ	STACK_SIZE, 1024 # 给 STACK_SIZE 这个名称赋值为 1024

	.global	_start
	# .global 或 .globl 是汇编伪指令,用于将一个符号声明为全局可见,使链接器能够识别它。

	.text
_start:
	# park harts with id != 0
	csrr	t0, mhartid		# read current hart id
	mv	tp, t0			# keep CPU's hartid in its tp for later usage.
	                    # 将 hartid 保存在 tp 寄存器(线程指针)中,
						# 后续 C 代码可通过 tp 获取当前核心编号
						# 每个硬件线程(hart)都有自己独立的一套寄存器,包括 t0 和 tp。
	bnez	t0, park		# if we're not on the hart 0
					        # we park the hart
							# 如果 hartid 不为 0(即不是主核),则跳转到
							# park 循环,让该核进入休眠
	# Setup stacks, the stack grows from bottom to top, so we put the
	# stack pointer to the very end of the stack range.
	# 栈在内存中是从低地址向高地址增长的,但 RISC-V 的栈是向下增长(从高地址向低地址):
	# 入栈时 sp 先减小,然后存放数据。所以初始 sp 应指向栈区域的最高地址。
	slli	t0, t0, 10		# shift left the hart id by 1024
	                        # 将寄存器 t0 中的值左移 10 位,空出的低位补 0,然后将结果写回 t0。
	la	sp, stacks + STACK_SIZE	# set the initial stack pointer
					# to the end of the first stack space
					# 实际效果:sp = &stacks + STACK_SIZE(即 stacks 符号的地址加上偏移量 STACK_SIZE)
	add	sp, sp, t0		# move the current hart stack pointer
					# to its place in the stack space

	j	start_kernel		# hart 0 jump to c

park:
	wfi #(Wait For Interrupt):RISC-V 特权指令,使当前硬件线程(hart)进入低功耗待机状态,直到收
	    # 到一个中断(或调试请求)才被唤醒。
	j	park # jump 指令 用来跳转的,跳转到park等于死循环
	# In the standard RISC-V calling convention, the stack pointer sp
	# is always 16-byte aligned.
.balign 16 # 使下一个符号(stacks)的地址是 16 的整数倍。
stacks:
	.skip	STACK_SIZE * MAXNUM_CPU # allocate space for all the harts stacks
   # .skip(也称 .space):在 BSS 段中保留指定字节数,不初始化。
   # 栈区域起始地址为 stacks。
   # 第 i 个 hart 的栈顶通常计算为:stacks + (i+1) * STACK_SIZE
	.end				# End of file


@ .text(代码段)只需要 可执行 + 可读,不应允许写入(防止代码被篡改或注入)。

@ .rodata(只读数据,如字符串常量、const 变量)只需 可读,写操作会触发异常。

@ .data(已初始化全局变量)需要 可读可写。

@ .bss(未初始化全局变量)同样需要 可读可写,但初始值为零。
posted @ 2026-05-28 11:57  写代码的伊隆  阅读(3)  评论(0)    收藏  举报