系统能力培养虚拟机实验教程
写在前面
这是一个速通教程,高尚的人请你立刻叉出去。
抄也要花一些时间,建议抄的同时弄懂,所以尽早开始。
pa0
VirtualBox安装包和老师给的前置打包环境root.vdi文件请自行在课程官网获取,创建并导入后无需安装任务书中所写的pa0环境依赖。
本章内容:在你的电脑本机使用ssh免密丝滑连接你的虚拟机,此方法可推广至服务器连接。如果你想直接在虚拟机里下载Vscode写代码可以跳过此章。
虚拟机网络配置
给你的虚拟机新增一块网卡,启用网络连接、连接方式选择仅主机。
添加后重新启动虚拟机并打开终端,依赖安装:
1 | sudo apt-get install net-tools |
获取信息如下:enp0s8
后第二行的inet 192.168.56.101,记住它尤其是前缀。
打开你的本机windows,设置
-> 网络和Internet
-> 高级网络设置
找到virtualBox,并选择 查看其他属性
,观察你的IPv4地址的前四位和上面在虚拟机里看到的前24位是否相同,不同的话手动改一下本机的。
相同后ping一下试试,一般没问题,有问题我也不知道为什么。
回到你的虚拟机,继续配置
1 | sudo apt-get install openssh-server |
追加以下内容,注意倒三行是写上你的用户名,不过按理来说大家应该都叫hust
1 | Port 22 |
大概长这样
写完保存退出后重启一下ssh服务
1 | sudo service ssh --full-restart |
回到你的windows终端里测试一下连接 ssh -p 22 hust@your ip
,如下表示成功:
免密登录配置
让我用vim写代码,不可能的事情
重复的东西不想写了,请参考我的另一篇配置Vmware的博客的VsCode免密连接虚拟机
一章
pa1
完成这个课设的关键是:认真阅读手册和代码
cd到
./nemu
文件夹下,修改Makefile文件,ISA改成riscv32。因为pa1比较简单,新手保护期课程手册给的很详细、代码也不复杂很容易就能看懂。
速通秘诀:所有要修改的文件如下,自己抄答案对着复制(详细内容等我写完报告回来更新)
pa-1.1
结果提交
修改完后使用 make submit
提交,输入初始化的账号密码。如下,提交成功。
pa2
前置
要改的文件见本章最后“提交结果”,以下是实验过程记录。
按照GitBook教程创建新的分支;
下载好交叉编译器到本地(注意版本),传到虚拟机里,注意路径。
传完了之后解压,然后按照教程更改环境变量,改完记得要先激活一下,出现如下情况说明成功。
pa-2.1
这部分要求实现dummy.c程序的运行,运行起来后是 ABORT 的状态
通过make生成的 nexus-am/tests/cputest/build/dummy-riscv32-nemu.txt
文件找到看起来需要实现的指令有: li、auipc、addi、jal、lui、mv、sw、ret。
通过手册我们可以发现 li
指令是lui
和 addi
指令的组合,因此不需要单独实现;最后的 ret
指令是伪指令,其实需要实现的是 jalr
;mv
指令也是伪指令,会被扩展成 addi rd, rs1, 0
,不需要单独实现。通过阅读代码发现 sw
也已经实现(st和store,st执行辅助函数在 ldst.c
文件)
实现新指令的方法
- 在
opcode_table
中填写正确的译码辅助函数, 执行辅助函数以及操作数宽度;- 用RTL实现正确的执行辅助函数, 需要注意使用RTL伪指令时要遵守上文提到的小型调用约定
lui
属于U型指令,opcode_table写入 nemu/src/isa/riscv32/exec/exec.c
、译码辅助函数 nemu/src/isa/riscv32/decode.c
、执行辅助函数 nemu/src/isa/riscv32/exec/compute.c
都已经帮我们实现了。
这里填13是因为 lui
指令的操作码是0110111,第6到2位是01101,即十进制的13,所以对应opcode_table[13]。注释里的00/01/10/11是指它的操作码的第七位和第六位,先根据这个索引再往后看三位,所以一行有八个空。
auipc
属于U型指令,译码辅助函数上面lui已经实现,这里只需要实现执行辅助函数即可和填表即可
1 | make_EHelper(auipc) { |
第五位填表
addi
属于I型指令,要实现一个I型译码辅助函数,如下:
1 | make_DHelper(I) { |
再实现一个执行辅助函数
1 | make_EHelper(addi) { |
第四位填表。
后面的 jalr
、jal
流程也差不多。
填完的表是这样的:
结果
这里遇到了一个bug:
根据错误信息“riscv-none-embed-ld: Command not found”得到信息应该是构建过程中找不到 riscv-none-embed-ld
命令,所以是环境变量的问题,重新激活一下环境变量即可。
重新make run,显示成功:
pa-2.2
上面pa2-1对应的那几个文件现在除了
system
系统调用指令其它都可以复制了。
这部分要求完善AM项目下的代码,实现更多的指令和一些必要的库函数。
使用 runall.sh
脚本测试一下,会在本地反汇编出所有没有通过的测试文件。
同时会给出测试结果
I型指令
要实现好多I型指令,不妨尝试抽象成整个I型执行辅助函数,避免代码的重复,使用switch函数先对 decinfo.isa.instr.funct3
的值进行分支跳转。
1 | switch (decinfo.isa.instr.funct3) { |
再在此基础上进行完善,此过程就是不断的查表和调试。
然后更改opcode_table对应的字段,其实就是把原来的addi改成i即可。
R型指令
同理,实现译码辅助函数和执行辅助函数然后填表,不再赘述。
注意mulhsu指令实现的时候要自己封装一个rtl函数
1 | // ./nemu/include/rtl/c_op.h |
B型指令
在control.c添加执行辅助函数,老地方添加译码辅助函数,然后填表。
到这一步测试一下,结果是这样的。
Load和Store指令
这部分需要在 ldst.c
文件里完成执行辅助函数,填充load_table,store_table。
store指令的执行函数已经实现了没什么问题
1 | static OpcodeEntry load_table [8] = { |
这部分完成后测试发现load-store文件和string文件无法通过,查看教程发现string.c需要实现 nexus-am/libs/klib/src/string
. 而根据load-store-log.txt出错的原因如下,即没有找到rtl_sext,所以我们需要补充一下。
1 | static inline void rtl_sext(rtlreg_t* dest, const rtlreg_t* src1, int width) { |
测试结果:成功
字符串处理函数
各个函数功能和实现思路下:
函数 | 功能 | 实现思路 |
---|---|---|
strlen |
计算字符串的长度 | 遍历字符串,直到遇到字符串结束符,记录遍历的次数作为字符串的长度。 |
strcpy |
将源字符串复制到目标字符串 | 遍历源字符串的每个字符,逐个将字符复制到目标字符串中,直到遇到源字符串的结束符。最后,在目标字符串的末尾添加结束符。 |
strncpy |
将源字符串的一部分复制到目标字符串 | 遍历源字符串的每个字符,在达到指定的复制长度 n 或者遇到源字符串的结束符之前,将字符复制到目标字符串中。如果已经复制了 n 个字符,但源字符串仍未结束,则在目标字符串的末尾添加结束符。 |
strcat |
将源字符串追加到目标字符串的末尾 | 首先找到目标字符串的结束符,然后遍历源字符串的每个字符,将字符追加到目标字符串的末尾,并在最后添加结束符。 |
strcmp |
比较两个字符串的大小 | 逐个比较两个字符串中的字符,直到遇到不相等的字符或者其中一个字符串结束。 |
strncmp |
比较两个字符串的大小,最多比较 n 个字符 |
与 strcmp 函数类似,但是限制了最大比较的字符数量为 n |
memset |
将一块内存区域填充为指定的值 | 将内存区域的每个字节都设置为指定的值 c ,实现了内存的填充操作。 |
memcpy |
将一块内存区域复制到另一块内存区域 | 遍历源内存区域的每个字节,逐个将字节复制到目标内存区域中,实现了内存的复制操作。 |
memcmp |
比较两块内存区域的内容是否相等 | 逐个比较两个内存区域中的字节,直到遇到不相等的字节或者比较的次数达到指定的长度 n 。 |
string.c通过了之后hello-str.c又不能通过了….看教程是要完成 sprintf()
函数,它的参数数目是可变的。
为了实现这个函数我们要根据格式字符串 fmt
和可变参数列表中的参数,将格式化后的字符串存储在目标字符串 out
中。然后调用 vsprintf
函数将格式化的字符串存储在目标字符串中,返回格式化后的字符串长度 len
。
一共需要下列几个辅助函数:
vsprintf
函数:根据格式字符串fmt
和可变参数列表中的参数,将格式化后的字符串存储在目标字符串out
中。通过遍历格式字符串的每个字符,根据不同的格式符进行相应的处理。当遇到普通字符时,将其直接复制到目标字符串中;当遇到格式符%
时,根据后续字符来解析相应的参数,并调用相应的函数进行处理。函数返回格式化后的字符串长度out_len
。add_string
函数:将字符串添加到目标字符串中。add_char
函数:将字符添加到目标字符串中。is_digit
函数:判断一个字符是否为数字;add_number
函数:将数字添加到目标字符串中。
完成这部分之后就能通过全部的测试文件。
pa-2.3
串口
运行Hello World
修改文件,打开HAS_IOE宏定义,关闭调试。
运行结果
实现printf
根据之前实现 sprintf()
的思路,调用 vsprintf()
即可。
1 | int printf(const char *fmt, ...) { |
时钟
实现
先在初始化函数 __am_timer_init
中对 boot_time
进行初始化, 然后在 _DEVREG_TIMER_UPTIME
条件分支下将 uptime->lo
更新为当前时间和初始时间的差。
测试一下,显示成功。
1 | make mainargs=t run |
跑分结果
coremark
dhrystone
microbench
键盘
先通过 inl(KBD_ADDR)
从 MMIO 中获取键盘码,通过键盘码和KEYDOWN_MASK
相与得到当前键盘的状态, 通过键盘码和 ~KEYDOWN_MASK
相与得到没有按键时的状态。代码如下。
1 | uint32_t keyboard_code = inl(KBD_ADDR); |
这里要回虚拟机进行测试,因为在Vscode里没法进行弹窗
1 | cd ~/ics2019/nexus-am/tests/amtest/ |
VGA
这里有两个工作:
- 完善
nemu/src/device/vga.c
中的vga_io_handler()
函数; - 实现
_DEVREG_VIDEO_INFO
和_DEVREG_VIDEO_FBCTL
的功能。
测试结果:
1 | make ARCH=riscv32-nemu mainargs=v run |
其它结果展示
slider
typing
提交结果
merge到master
pa3
pa-3.1
实现自陷操作
在nanos-lite/include/common.h
中定义 HAS_CTE
。
定义了宏HAS_CTE
后, Nanos-lite会在panic()
前调用_yield()
来触发自陷操作. 为了支撑这次自陷操作, 需要在NEMU中实现raise_intr()
函数来模拟异常响应机制。
修改 ISADecodeInfo
结构:
1 | struct ISADecodeInfo { |
最终实现的 raise_intr()
函数如下:
1 | void raise_intr(uint32_t NO, vaddr_t epc) { |
然后我们需要实现ecall环境调用指令、sret 管理员模式例外返指令,及一系列的控制状态寄存器相关操作的指令。
步骤同pa2一样逐步完成译码辅助函数、执行辅助函数的编写和对应的声明,最后在opcode_table填表。
1 | // 译码辅助函数 |
执行辅助函数的部分我们需要两个控制状态寄存器相关指令,一个负责读取一个负责写入。写入时接受一个CSR编号和一个要写入的值作为参数,根据CSR编号将给定的值写入相应的CSR;读取则只需要一个CSR编号作为参数。
1 | void write_csr(int csr, int32_t val) { |
最后调用上述两个寄存器控制指令和前面完成的 raise_intr()
函数来完成触发异常后的响应,完成执行辅助函数。
成功跳转:
保存上下文
顺序是gpr, cause, status, epc, as
事件分发
这一部分需要我们先在 __am_irq_handle()
函数中通过异常号识别出自陷异常, 然后将event 设置为编号为 _EVENT_YIELD
的自陷事件;然后通过do_event()
函数根据事件类型再次进行分发。注意这里不要全复制了,只复制 case _EVENT_YIELD
和 case -1
部分,其它是pa3-2的。
实现成功,可以看到在下图中成功打印了我们的提示语句“Self trap!”。
恢复上下文
这部分需要实现sret指令即可,前面已经实现了。
pa-3.2
加载用户程序
在这里先补充 ./nanos-lite/src/loader.c
中的loader函数,实现用户程序加载。
resource.S
文件里用汇编将ramdisk.img
放在了静态存储区data section(实际上程序应该放在硬盘中,但就如文档所说当前用ram来模拟,因此放在了内存中的静态存储区),我们需要负责判断elf合法性,如果合法的话,我们需要做的是把程序加载到它应该处于的内存段中,以及将.bss段(全局变量区)清零,其中phdr(program header)中的p_vaddr
即程序段应该加载到的内存段的首地址。
1 | static uintptr_t loader(PCB *pcb, const char *filename) { |
然后在 proc.c
中添加教程要求的 naive_uload(NULL, NULL)
,调用刚刚实现的loader来加载第一个用户程序,可以看到执行dummy
程序时在Nanos-lite中触发了一个未处理的1号事件。同时我们可以看到绿色框上面显示调用了 naive_uload
的信息。
识别系统调用
观察_syscall_
的代码,发现是从a0
寄存器取得系统调用的返回结果,因此修改GPRx
的宏定义,将其改成寄存器a0
的下标,就可以在操作系统中通过c->GPRx
根据实际情况设置返回值了
然后要在 syscall.c
文件中实现 do_event()
调用的 do_syscall()
函数。调用 _yield
实现SYS_yield
。根据手册我们需要对它进行封装,SYS_yield
系统调用直接调用CTE的_yield()
即可,然后返回0
,系统调用的返回值我们通过GPRx
来进行设置。
同时我们还需要实现一个退出的系统调用,它接收一个退出状态的参数, 用这个参数调用_halt()
。
上述过程的代码如下:
运行后我们成功得到一个 GOOD TRAP!
标准输出
因为前面埋下的雷所以被迫回头修bug^^ 这里一开始出错的原因是前面识别系统调用部分的 GPRx
的寄存器设置出错了,能通过上一小关但是在这里会导致一个bug无法正确输出。
解决问题之后这部分只要按手册实现 SYS_write
即可,然后在 nanos.c
文件中添加调用。
输出结果如下:
堆区管理
我们程序加载进内存之后,其中存在一个名为用户程序的数据段的段,并且链接的时候ld
会默认添加一个名为_end
的符号, 来指示程序的数据段结束的位置(program break),在这个结束位置之后的位置用户不能再随便往里面存东西, 必须通过sbrk
来向系统申请,本质就是更新这个program break,通过man 3 end
查阅了_end
用法后,在用户层维护并更新这个program break即可:
1 | extern uint32_t _end; |
pa-3.3
简易文件系统
修改Makefile文件,执行 make clean
清除build/ramdisk.img
,然后update更新一下。
这部分就是完善 fs.c
中的各个函数,和上面类似完善 syscall.c
和 nanos.c
文件,还要修改原来的loader函数,不再使用 ramdisk_disk()
.
操作系统之上的IOE
把串口抽象成文件
补充 serial_write
,修改 file_table
.
把设备输入抽象成文件
补充events_read
,然后在VFS中添加对/dev/events
的支持,加载 /bin/events
把VGA显存抽象成文件
按手册说的做,这里清晰且简单
pa-3.4
运行仙剑奇侠传
下载游戏数据然后传到虚拟机里并解压到要求的路径
批处理系统
实现 sys_execve
系统调用
结果提交
pa4
pa-4.1 实现基本的多道程序系统
实现上下文切换
实现多道程序系统
一山不容二虎
结果如下
Report
2024.01.16:提交了!结束本科最后一门课程!
报告已上传GITHUB => here