TUNIVERSE

HUST系统能力培养之虚拟机

字数统计: 5k阅读时长: 19 min
2023/12/17 675

系统能力培养虚拟机实验教程

写在前面

这是一个速通教程,高尚的人请你立刻叉出去。

抄也要花一些时间,建议抄的同时弄懂,所以尽早开始。

pa0

VirtualBox安装包和老师给的前置打包环境root.vdi文件请自行在课程官网获取,创建并导入后无需安装任务书中所写的pa0环境依赖。

本章内容:在你的电脑本机使用ssh免密丝滑连接你的虚拟机,此方法可推广至服务器连接。如果你想直接在虚拟机里下载Vscode写代码可以跳过此章。

虚拟机网络配置

给你的虚拟机新增一块网卡,启用网络连接、连接方式选择仅主机。

image-20231219013901608

添加后重新启动虚拟机并打开终端,依赖安装:

1
2
sudo apt-get install net-tools
ifconfig

获取信息如下:enp0s8后第二行的inet 192.168.56.101,记住它尤其是前缀。

image-20231219012358447

打开你的本机windows,设置 -> 网络和Internet -> 高级网络设置

找到virtualBox,并选择 查看其他属性,观察你的IPv4地址的前四位和上面在虚拟机里看到的前24位是否相同,不同的话手动改一下本机的。

image-20231219014226124

相同后ping一下试试,一般没问题,有问题我也不知道为什么。

image-20231219014504625

回到你的虚拟机,继续配置

1
2
3
4
5
sudo apt-get install openssh-server
sudo /etc/init.d/ssh start
sudo systemctl enable ssh
sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.backup # 备份
sudo vim /etc/ssh/sshd_config

追加以下内容,注意倒三行是写上你的用户名,不过按理来说大家应该都叫hust

1
2
3
4
5
6
7
8
9
10
Port 22
AddressFamily any
ListenAddress 0.0.0.0
ListenAddress ::
UsePrivilegeSeparation no
PasswordAuthentication yes
PermitRootLogin yes
AllowUsers hust
RSAAuthentication yes
PubKeyAuthentication yes

大概长这样

image-20231219005718404

写完保存退出后重启一下ssh服务

1
sudo service ssh --full-restart

回到你的windows终端里测试一下连接 ssh -p 22 hust@your ip,如下表示成功:

image-20231219005910190

免密登录配置

让我用vim写代码,不可能的事情

重复的东西不想写了,请参考我的另一篇配置Vmware的博客VsCode免密连接虚拟机一章

pa1

完成这个课设的关键是:认真阅读手册和代码

cd到 ./nemu 文件夹下,修改Makefile文件,ISA改成riscv32。

因为pa1比较简单,新手保护期课程手册给的很详细、代码也不复杂很容易就能看懂。

速通秘诀:所有要修改的文件如下,自己抄答案对着复制(详细内容等我写完报告回来更新)

pa-1.1

image-20240110024944765

image-20240110025835972

image-20240110025906493

结果提交

image-20231225201012675

修改完后使用 make submit 提交,输入初始化的账号密码。如下,提交成功。

image-20231225201405072

pa2

前置

要改的文件见本章最后“提交结果”,以下是实验过程记录。

按照GitBook教程创建新的分支;

下载好交叉编译器到本地(注意版本),传到虚拟机里,注意路径。

image-20240102152841211

传完了之后解压,然后按照教程更改环境变量,改完记得要先激活一下,出现如下情况说明成功。

image-20240102153216666

pa-2.1

这部分要求实现dummy.c程序的运行,运行起来后是 ABORT 的状态

image-20240102154028356

通过make生成的 nexus-am/tests/cputest/build/dummy-riscv32-nemu.txt 文件找到看起来需要实现的指令有: li、auipc、addi、jal、lui、mv、sw、ret。

image-20240102154808201

通过手册我们可以发现 li 指令是luiaddi 指令的组合,因此不需要单独实现;最后的 ret 指令是伪指令,其实需要实现的是 jalrmv 指令也是伪指令,会被扩展成 addi rd, rs1, 0,不需要单独实现。通过阅读代码发现 sw 也已经实现(st和store,st执行辅助函数在 ldst.c文件)

image-20240102172203578

实现新指令的方法

  1. opcode_table中填写正确的译码辅助函数, 执行辅助函数以及操作数宽度;
  2. 用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 都已经帮我们实现了。

image-20240102173017738

image-20240102173457143

这里填13是因为 lui 指令的操作码是0110111,第6到2位是01101,即十进制的13,所以对应opcode_table[13]。注释里的00/01/10/11是指它的操作码的第七位和第六位,先根据这个索引再往后看三位,所以一行有八个空。

image-20240102180028847

auipc

属于U型指令,译码辅助函数上面lui已经实现,这里只需要实现执行辅助函数即可和填表即可

1
2
3
4
5
make_EHelper(auipc) {
rtl_add(&id_dest->val, &cpu.pc, &id_src->val);
rtl_sr(id_dest->reg, &id_dest->val, 4);
print_asm_template2(auipc);
}

image-20240102174032127

第五位填表

image-20240102175148979

addi

属于I型指令,要实现一个I型译码辅助函数,如下:

1
2
3
4
5
make_DHelper(I) {
decode_op_r(id_src, decinfo.isa.instr.rs1, true);
decode_op_i(id_src2, decinfo.isa.instr.simm11_0, true);
decode_op_r(id_dest, decinfo.isa.instr.rd, false);
}

再实现一个执行辅助函数

1
2
3
4
5
make_EHelper(addi) {
rtl_add(&id_dest->val, &id_src->val, &id_src2->val);
rtl_sr(id_dest->reg, &id_dest->val, 4);
print_asm_template3(addi);
}

第四位填表。

后面的 jalrjal流程也差不多。

填完的表是这样的:

image-20240102192107533

结果

这里遇到了一个bug:

image-20240102193301193

根据错误信息“riscv-none-embed-ld: Command not found”得到信息应该是构建过程中找不到 riscv-none-embed-ld 命令,所以是环境变量的问题,重新激活一下环境变量即可。

重新make run,显示成功:

image-20240102193058260

pa-2.2

上面pa2-1对应的那几个文件现在除了 system 系统调用指令其它都可以复制了。

这部分要求完善AM项目下的代码,实现更多的指令和一些必要的库函数。

使用 runall.sh 脚本测试一下,会在本地反汇编出所有没有通过的测试文件。

image-20240103094841583

同时会给出测试结果

image-20240102211231467

I型指令

要实现好多I型指令,不妨尝试抽象成整个I型执行辅助函数,避免代码的重复,使用switch函数先对 decinfo.isa.instr.funct3 的值进行分支跳转。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
switch (decinfo.isa.instr.funct3) {
case 0: // addi相关
rtl_addi(&id_dest->val, &id_src->val, decinfo.isa.instr.simm11_0);
if (decinfo.isa.instr.rs1 == 0) { // li伪指令
print_asm_template2(li);
} else if (decinfo.isa.instr.simm11_0 == 0) { // mv伪指令
print_asm_template2(mv);
} else { // addi本身
print_asm_template3(addi);
}
break;
default:
assert(0 && "Unfind the opcode"); break;
}

再在此基础上进行完善,此过程就是不断的查表和调试。

然后更改opcode_table对应的字段,其实就是把原来的addi改成i即可。

R型指令

同理,实现译码辅助函数和执行辅助函数然后填表,不再赘述。

注意mulhsu指令实现的时候要自己封装一个rtl函数

1
2
3
4
5
6
7
8
// ./nemu/include/rtl/c_op.h
#define c_mul_hsu(a, b) (((int64_t)(a) * (uint64_t)(b)) >> 32)

// ./nemu/include/rtl/rtl-wrapper.h
#define rtl_mul_hsu concat(RTL_PREFIX, _rtl_mul_hsu )

// ./nemu/include/rtl/rtl.h
make_rtl_arith_logic(mul_hsu)

B型指令

在control.c添加执行辅助函数,老地方添加译码辅助函数,然后填表。

到这一步测试一下,结果是这样的。

image-20240103101942561

Load和Store指令

这部分需要在 ldst.c 文件里完成执行辅助函数,填充load_table,store_table。

store指令的执行函数已经实现了没什么问题

1
2
3
4
5
6
7
static OpcodeEntry load_table [8] = {
EXW(ld, 1), EXW(ld, 2), EXW(ld, 4), EMPTY, EXW(ld, 1), EXW(ld, 2), EMPTY, EMPTY
};

static OpcodeEntry store_table [8] = {
EXW(st, 1), EXW(st, 2), EXW(st, 4), EMPTY, EMPTY, EMPTY, EMPTY, EMPTY
};

这部分完成后测试发现load-store文件和string文件无法通过,查看教程发现string.c需要实现 nexus-am/libs/klib/src/string. 而根据load-store-log.txt出错的原因如下,即没有找到rtl_sext,所以我们需要补充一下。

image-20240103111453042

1
2
3
4
5
6
7
8
9
10
static inline void rtl_sext(rtlreg_t* dest, const rtlreg_t* src1, int width) {
int32_t temp = *src1;
switch(width) {
case 4: *dest = *src1; return;
case 3: temp = temp << 8; *dest = temp >> 8; return;
case 2: temp = temp << 16; *dest = temp >> 16; return;
case 1: temp = temp << 24; *dest = temp >> 24; return;
default: assert(0);
}
}

测试结果:成功

image-20240103112221893

字符串处理函数

各个函数功能和实现思路下:

函数 功能 实现思路
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

一共需要下列几个辅助函数:

  1. vsprintf 函数:根据格式字符串 fmt 和可变参数列表中的参数,将格式化后的字符串存储在目标字符串 out 中。通过遍历格式字符串的每个字符,根据不同的格式符进行相应的处理。当遇到普通字符时,将其直接复制到目标字符串中;当遇到格式符 % 时,根据后续字符来解析相应的参数,并调用相应的函数进行处理。函数返回格式化后的字符串长度 out_len
  2. add_string 函数:将字符串添加到目标字符串中。
  3. add_char 函数:将字符添加到目标字符串中。
  4. is_digit 函数:判断一个字符是否为数字;
  5. add_number 函数:将数字添加到目标字符串中。

完成这部分之后就能通过全部的测试文件。

image-20240103150857113

pa-2.3

串口

运行Hello World

修改文件,打开HAS_IOE宏定义,关闭调试。

image-20240103152240263

运行结果

image-20240103154056515

实现printf

根据之前实现 sprintf() 的思路,调用 vsprintf() 即可。

1
2
3
4
5
6
7
8
9
10
11
int printf(const char *fmt, ...) {
va_list args;
va_start(args, fmt);
char outBuf[256] = {'\0'};
int length = vsprintf(outBuf, fmt, args);
for (size_t i = 0; outBuf[i]; i++) {
_putc(outBuf[i]);
}
va_end(args);
return length;
}

时钟

实现

先在初始化函数 __am_timer_init 中对 boot_time 进行初始化, 然后在 _DEVREG_TIMER_UPTIME 条件分支下将 uptime->lo 更新为当前时间和初始时间的差。

测试一下,显示成功。

1
make mainargs=t run

image-20240103170944921

跑分结果

coremark

image-20240103193835049

dhrystone

image-20240103193917212

microbench

image-20240103194108336

键盘

先通过 inl(KBD_ADDR) 从 MMIO 中获取键盘码,通过键盘码和KEYDOWN_MASK 相与得到当前键盘的状态, 通过键盘码和 ~KEYDOWN_MASK 相与得到没有按键时的状态。代码如下。

1
2
3
4
uint32_t keyboard_code = inl(KBD_ADDR);
kbd->keydown = keyboard_code & KEYDOWN_MASK ? 1 : 0;
kbd->keycode = keyboard_code & ~KEYDOWN_MASK;
return sizeof(_DEV_INPUT_KBD_t);

这里要回虚拟机进行测试,因为在Vscode里没法进行弹窗

1
2
cd ~/ics2019/nexus-am/tests/amtest/
make ARCH=riscv32-nemu mainargs=k run

image-20240103201730935

VGA

这里有两个工作:

  1. 完善 nemu/src/device/vga.c 中的 vga_io_handler() 函数;
  2. 实现 _DEVREG_VIDEO_INFO_DEVREG_VIDEO_FBCTL 的功能。

测试结果:

1
make ARCH=riscv32-nemu mainargs=v run

image-20240103203649913

其它结果展示

slider

image-20240103210243102

image-20240103210415000

typing

image-20240103210605118

提交结果

image-20240103210841567

merge到master

image-20240103211049445

pa3

pa-3.1

实现自陷操作

nanos-lite/include/common.h中定义 HAS_CTE

定义了宏HAS_CTE后, Nanos-lite会在panic()前调用_yield()来触发自陷操作. 为了支撑这次自陷操作, 需要在NEMU中实现raise_intr()函数来模拟异常响应机制。

修改 ISADecodeInfo 结构:

1
2
3
4
5
6
7
struct ISADecodeInfo {
Instr instr;
uint32_t sepc;
uint32_t sstatus;
uint32_t scause;
uint32_t stvec;
};

最终实现的 raise_intr() 函数如下:

1
2
3
4
5
6
void raise_intr(uint32_t NO, vaddr_t epc) {
decinfo.isa.sepc = epc;
decinfo.isa.scause = NO;
decinfo.jmp_pc = decinfo.isa.stvec;
rtl_j(decinfo.jmp_pc);
}

然后我们需要实现ecall环境调用指令、sret 管理员模式例外返指令,及一系列的控制状态寄存器相关操作的指令。

步骤同pa2一样逐步完成译码辅助函数、执行辅助函数的编写和对应的声明,最后在opcode_table填表。

1
2
3
4
5
6
7
// 译码辅助函数
make_DHelper(SYSTEM) {
decode_op_r(id_src, decinfo.isa.instr.rs1, true);
decode_op_i(id_src2, decinfo.isa.instr.csr, true);
decode_op_r(id_dest, decinfo.isa.instr.rd, false);
print_Dop(id_src->str, OP_STR_SIZE, "0x%x", decinfo.isa.instr.csr);
}

执行辅助函数的部分我们需要两个控制状态寄存器相关指令,一个负责读取一个负责写入。写入时接受一个CSR编号和一个要写入的值作为参数,根据CSR编号将给定的值写入相应的CSR;读取则只需要一个CSR编号作为参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void write_csr(int csr, int32_t val) {
switch(csr) {
case 0x100: decinfo.isa.sstatus = val; break;
case 0x105: decinfo.isa.stvec = val; break;
case 0x141: decinfo.isa.sepc = val; break;
case 0x142: decinfo.isa.scause = val; break;
default: assert(!"Unfind the csr");
}
}

int32_t read_csr(int csr) {
switch(csr) {
case 0x100: return decinfo.isa.sstatus;
case 0x105: return decinfo.isa.stvec;
case 0x141: return decinfo.isa.sepc;
case 0x142: return decinfo.isa.scause;
default: assert(!"Unfind the csr");
}
}

最后调用上述两个寄存器控制指令和前面完成的 raise_intr() 函数来完成触发异常后的响应,完成执行辅助函数。

image-20240104132130382

成功跳转:

image-20240104143223004

保存上下文

顺序是gpr, cause, status, epc, as

image-20240105153308810

事件分发

这一部分需要我们先在 __am_irq_handle() 函数中通过异常号识别出自陷异常, 然后将event 设置为编号为 _EVENT_YIELD 的自陷事件;然后通过do_event()函数根据事件类型再次进行分发。注意这里不要全复制了,只复制 case _EVENT_YIELDcase -1 部分,其它是pa3-2的。

image-20240104185658130

实现成功,可以看到在下图中成功打印了我们的提示语句“Self trap!”。

image-20240104192458814

恢复上下文

这部分需要实现sret指令即可,前面已经实现了。

image-20240104192553664

pa-3.2

加载用户程序

在这里先补充 ./nanos-lite/src/loader.c 中的loader函数,实现用户程序加载。

resource.S文件里用汇编将ramdisk.img放在了静态存储区data section(实际上程序应该放在硬盘中,但就如文档所说当前用ram来模拟,因此放在了内存中的静态存储区),我们需要负责判断elf合法性,如果合法的话,我们需要做的是把程序加载到它应该处于的内存段中,以及将.bss段(全局变量区)清零,其中phdr(program header)中的p_vaddr即程序段应该加载到的内存段的首地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static uintptr_t loader(PCB *pcb, const char *filename) {
Elf_Ehdr ehdr;
ramdisk_read(&ehdr, 0, sizeof(Elf_Ehdr));
assert((*(uint32_t *)ehdr.e_ident == 0x464c457f));

Elf_Phdr phdr[ehdr.e_phnum];
ramdisk_read(phdr, ehdr.e_phoff, sizeof(Elf_Phdr)*ehdr.e_phnum);
for (int i = 0; i < ehdr.e_phnum; i++) {
if (phdr[i].p_type == PT_LOAD) {
ramdisk_read((void*)phdr[i].p_vaddr, phdr[i].p_offset, phdr[i].p_memsz);
memset((void*)(phdr[i].p_vaddr+phdr[i].p_filesz), 0, phdr[i].p_memsz - phdr[i].p_filesz);
}
}
return ehdr.e_entry;
}

然后在 proc.c 中添加教程要求的 naive_uload(NULL, NULL),调用刚刚实现的loader来加载第一个用户程序,可以看到执行dummy程序时在Nanos-lite中触发了一个未处理的1号事件。同时我们可以看到绿色框上面显示调用了 naive_uload 的信息。

image-20240105152409549

识别系统调用

观察_syscall_的代码,发现是从a0寄存器取得系统调用的返回结果,因此修改GPRx的宏定义,将其改成寄存器a0的下标,就可以在操作系统中通过c->GPRx根据实际情况设置返回值了

然后要在 syscall.c 文件中实现 do_event() 调用的 do_syscall() 函数。调用 _yield 实现SYS_yield 。根据手册我们需要对它进行封装,SYS_yield系统调用直接调用CTE的_yield()即可,然后返回0,系统调用的返回值我们通过GPRx来进行设置。

同时我们还需要实现一个退出的系统调用,它接收一个退出状态的参数, 用这个参数调用_halt()

上述过程的代码如下:

image-20240105163109990

运行后我们成功得到一个 GOOD TRAP!

image-20240106221357794

标准输出

因为前面埋下的雷所以被迫回头修bug^^ 这里一开始出错的原因是前面识别系统调用部分的 GPRx 的寄存器设置出错了,能通过上一小关但是在这里会导致一个bug无法正确输出。

解决问题之后这部分只要按手册实现 SYS_write 即可,然后在 nanos.c 文件中添加调用。

输出结果如下:

image-20240106155932827

堆区管理

我们程序加载进内存之后,其中存在一个名为用户程序的数据段的段,并且链接的时候ld会默认添加一个名为_end的符号, 来指示程序的数据段结束的位置(program break),在这个结束位置之后的位置用户不能再随便往里面存东西, 必须通过sbrk来向系统申请,本质就是更新这个program break,通过man 3 end查阅了_end用法后,在用户层维护并更新这个program break即可:

1
2
3
4
5
6
7
8
9
10
extern uint32_t _end;
void *_sbrk(intptr_t increment) {
static int program_break = &_end;
int ret = program_break;
if(!_syscall_(SYS_brk, program_break + increment, 0, 0)){
program_break += increment;
return (void *)ret;
}
return (void *)-1;
}

image-20240106171754119

pa-3.3

简易文件系统

修改Makefile文件,执行 make clean 清除build/ramdisk.img,然后update更新一下。

这部分就是完善 fs.c 中的各个函数,和上面类似完善 syscall.cnanos.c 文件,还要修改原来的loader函数,不再使用 ramdisk_disk().

image-20240106221107709

image-20240106220454526

操作系统之上的IOE

把串口抽象成文件

补充 serial_write,修改 file_table.

把设备输入抽象成文件

补充events_read,然后在VFS中添加对/dev/events的支持,加载 /bin/events

image-20240106234616123

把VGA显存抽象成文件

按手册说的做,这里清晰且简单

image-20240107004500077

pa-3.4

运行仙剑奇侠传

下载游戏数据然后传到虚拟机里并解压到要求的路径

image-20240107170311082

image-20240107165254108

image-20240107165549573

image-20240107165709639

批处理系统

实现 sys_execve 系统调用

image-20240107180955729

image-20240107181434820

结果提交

image-20240107181702557

image-20240107181749020

pa4

pa-4.1 实现基本的多道程序系统

实现上下文切换

image-20240108161306497

实现多道程序系统

image-20240108191057493

image-20240108191526459

一山不容二虎

结果如下

image-20240109161357337

Report

2024.01.16:提交了!结束本科最后一门课程!

报告已上传GITHUB => here

image-20240116161004692

CATALOG
  1. 1. 写在前面
  2. 2. pa0
  3. 3. pa1
  4. 4. pa2
  5. 5. pa3
  6. 6. pa4
  7. 7. Report