引言:
系统课从Lab3开始,实验重心从硬件开始向软件转移。本实验主要实现利用RiscV编写简单的内核代码,包括实现一些跳转和函数的传参。
1 实验目的

2 实验步骤
2-1 实验环境准备
本实验要用到qemu-system-riscv64,在终端中输入以下命令进行相应环境的安装:
1  | sudo apt update;  | 
安装后检查相应的qemu版本:

2-2 head.S
head.S要求我们去开辟一块有4KB大小的栈空间,并跳转到start kernel。
在实验指导中给我们指出了vmlinux.lds的作用与基础的实现方法:
1  | OUTPUT_ARCH( "riscv" )  | 
嗯,拿.text段的内存分配来仔细分析一下lds文件的代码实现方法
 1
2
3
4
5
6
7
8 .text : ALIGN(0x1000){ // 4kb对其
_stext = .; // _s + 段名称表示该段的起始
*(.text.entry) // .text段的入口,可能是供外部文件进行段地址的调转与访问
*(.text .text.*) // 这个应该也一样
_etext = .; // _e + 段名称表示段的末尾
}假如我们要利用lds文件再开辟一块空间,我们需要明确:
- 该段的名称
 - 所要开辟空间的大小(开辟的空间必须8字节对齐)
 - 在段内标注段起始和段结束
 - 有段的入口表示,并要求此标识有外部调用。
 
现在,要求我们在head.S文件内再开辟一块0x1000大小的空间,所以我们不妨将此空间在lds文件中就事先分配出来,然后再在head.S文件中将sp指向_end+0x1000(也就是栈顶)即可:
由于仓库中没有给出vmlinux.lds文件,我们把实验指导上的代码扒下来自己写一个:
1  | 
  | 
- 现在我们要在head.S中去进行栈段的调用与跳转:
 
在此之前我们需要明确链接器是如何识别程序中的标识符的:
> graph LR; .S文件 --> g((标识符)); g --> l((linker)) --> k((lds)) -.查找标识符.-> 分配空间与链接程序即,linker可以识别我们.S文件中用标识符定义的段名称。
代码实现如下:
1  | .extern start_kernel  | 
2-3 sbi.c
对于sbi.c的实现只需要对着实验指导抄就好了

给了一段示例代码的编写:
1  | unsigned long long s_example(unsigned long long type,unsigned long long arg0) {  | 
嗯,照葫芦画瓢:
1  | 
  | 
需要注意的是,本函数的返回类型必须是结构体,所以要先定义一个返回结构体,再给结构体里的变量赋值。
同时利用ecall并按照实验指导实现三个函数:
1  | void sbi_set_timer(uint64 time){  | 

2-5 puts和puti
直接上代码不解释:
1  | // 函数puts:用于输出一个字符串到控制台  | 
其中,puts我们利用一个循环实现,整数输出利用递归。
需要注意的是,对于整数的输出我们必须把要输出的数字+'0'转化为字符类型。
2-6 defs.h
先弄清楚这两个特权指令是怎么回事csrr, csrw
在RISC-V体系结构中,
csrr(Control and Status Register Read)和csrw(Control and Status Register Write)是两个用于操作控制和状态寄存器(CSR)的指令。
csrr:这个指令从CSR读取一个值到一个通用寄存器。它的常见格式是csrr rd, csr,其中rd是目的寄存器,csr是控制和状态寄存器的地址。csrw:这个指令将一个通用寄存器的值写入到CSR。它的常见格式是csrw csr, rs,其中rs是源寄存器,csr是控制和状态寄存器的地址。
先观察例子中给出的csrw的展开:
1  | 
通过查找资料得知:
"csrw " #csr ", %0": 这是汇编指令本身。"csrw"是RISC-V的“控制状态寄存器写”指令,#csr是预处理器操作,它会将宏参数csr转换成对应的字符串。%0是内联汇编的操作数占位符,对应于输入部分的__v。: : "r" (__v): 这部分是指令的操作数。"r"表示使用任意通用寄存器,(__v)是该寄存器的值。在这个宏中,__v是一个宏参数,它将被宏调用者提供的实际值替换。
所以该条汇编代码的结构就是:
csrw .., ..(代码段本身) + :: ..(指令的操作数) + : "memory"
照葫芦画瓢:
1  | 
一开始想看着csrw的展开写csrr并且没怎么理解csrw那个指令的结构是怎么回事,就导致,写出来各种奇形怪状的展开......下次写还是得先弄清原理.....
2-7 Makefile
关于/lib文件夹下面的Makefile文件的编写,我们只需要!把/arch/riscv/include下面的Makefile复制过来就好了....
嗯,还是认真分析一下吧:
在/lib文件夹下面的Makefile的作用就是将该目录下的所有文件进行编译包括
- .S --> .o
 - .c --> .o
 
由于在/lib目录下没有.S文件,所以只需将所有.c文件编译成.o即可:
在/arch/riscv/include下的Makefile具体实现如下:
1  | # 通过通配符获取所有.S汇编源文件的列表,并使用sort函数对文件列表排序  | 
嗯,把.S的部分删了就得到了在/lib目录下的Makefile实现(笑):
1  | # 通过通配符获取所有.c C源文件的列表,并使用sort函数对文件列表排序  | 
3 实现结果
执行make指令:

执行make run,成功输出目标字符串2022 ZJU Computer System II

4 Questions
4-1 编译之后,通过 System.map 查看 vmlinux.lds 中自定义符号的值,比较他们的地址是否符合你的预期?
查看System.map符号表:

对照vmlinux.lds,并观察符号表
4-1-1 符号的属性标记
bss,data,rodata段均标记为R(只读)text及代码段中的一些函数标记为T(代码段)sbp,stp为我们自己定义的符号,用n表示
段标识符合预期
4-1-2 段的空间大小
_sstack指向0x80202000,_estack指向0x80203000,为开辟的栈空间大小,且栈起始地址为 。 bss,data段由于没有数据,故空间被合并且 puti,puts,sbi_*,start_kernel等函数地址均在_etext,_stest间,表明他们处于代码段中。且由于text段大小不足0x1000,自动进行0x1000字节对齐,即- 按照顺序
_srodata在_etext之后,且rodata中存在数据,,由于不满0x1000,进行0x1000对齐,即  
综上所述,System.map所展示的符号表符合预期。
4-2 在你的第一条指令处添加断点,观察你的程序开始执行时的特权态是多少,中断的开启情况是怎么样的?
要查看程序运行的特权态,先了解一下什么是特权寄存器:
在计算机架构中,特权(
priv)寄存器是指那些只能在操作系统的内核模式或管理模式下访问的寄存器,而在用户模式下是不允许直接访问的。这些寄存器通常用于控制重要的系统资源和设置,例如内存管理单元(MMU)的配置、中断管理和处理器状态控制。在RISC-V架构中,有一套特权寄存器,用于控制和管理特权级别的行为,比如
mstatus、mepc、mtvec和mie等。其中:
mstatus寄存器包含了全局中断使能位和其他用于控制处理器状态的位。mepc寄存器包含了最后一次异常发生时的程序计数器(PC)的值。mtvec寄存器包含了中断向量表的起始地址。mie寄存器包含了中断使能位。
首先在一个终端中执行make debug

在另一个终端中,执行gdb-multiarch vmlinux指令
然后远程连接qemutarget remote localhost:1234

在_start处打断点,c运行至断点。
由于_start在System.map文件中可以找到对应的符号,所以可以在gdb中直接根据symbols打断点。且start指向程序的第一条指令。
查看特权寄存器的值,可以看到,priv寄存器为1,表示当前特权态是Supervisor。
且mie寄存器的值是8,表示中断开启。
mie寄存器的不同的位置1通常代表不同的中断情况,其中:
- 第0位代表软件中断的使能状态。
 - 第1位代表定时器中断的使能状态。
 - 第3位设置为1代表某个具体类型的中断,如外部中断的使能状态。
 在本程序中,mie的低三位为100,所以应该属于第三类终端,外部中断。

在第一条指令处插入:csrr a0, mstatus

第一条指令处设置断点,单步运行

特权态变为machine。
4-3 在你的第一条指令处添加断点,观察内存中 text、data、bss 段的内容是怎样的?
text段:

rodata段:

- bss段中没有数据,该段不存在:
 

4-4 尝试从汇编代码中给 C 函数 start_kernel 传递参数
Risc-V64的函数传参规范如下:
对于整数和指针参数,RISC-V 的标准调用约定使用以下寄存器:
a0-a7(x10-x17): 用于传递前八个整数或指针参数。- 返回值也通过
 a0(和a1如果返回值是 128 位)返回。如果函数参数超过八个,则溢出的参数将通过调用函数的栈传递。返回值(如果是整数或指针类型)通常通过
a0(和a1如果需要)寄存器返回给调用者。
- head.S:
 
1  | .extern start_kernel  | 
- main.c
 
1  | 
  | 
输出结果:

我们尝试使用内联汇编模仿参数的传递过程,像这样:
1  | 
  | 
运行结果如图:
