TUNIVERSE

HUST系统能力培养之虚拟机

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

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

写在前面

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

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

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
    1. 2.1. 虚拟机网络配置
    2. 2.2. 免密登录配置
  3. 3. pa1
    1. 3.1. pa-1.1
    2. 3.2. 结果提交
  4. 4. pa2
    1. 4.1. 前置
    2. 4.2. pa-2.1
      1. 4.2.1. lui
      2. 4.2.2. auipc
      3. 4.2.3. addi
      4. 4.2.4. 结果
    3. 4.3. pa-2.2
      1. 4.3.1. I型指令
      2. 4.3.2. R型指令
      3. 4.3.3. B型指令
      4. 4.3.4. Load和Store指令
      5. 4.3.5. 字符串处理函数
    4. 4.4. pa-2.3
      1. 4.4.1. 串口
        1. 4.4.1.1. 运行Hello World
        2. 4.4.1.2. 实现printf
      2. 4.4.2. 时钟
        1. 4.4.2.1. 实现
        2. 4.4.2.2. 跑分结果
          1. 4.4.2.2.1. coremark
          2. 4.4.2.2.2. dhrystone
          3. 4.4.2.2.3. microbench
      3. 4.4.3. 键盘
      4. 4.4.4. VGA
      5. 4.4.5. 其它结果展示
        1. 4.4.5.1. slider
        2. 4.4.5.2. typing
    5. 4.5. 提交结果
  5. 5. pa3
    1. 5.1. pa-3.1
      1. 5.1.1. 实现自陷操作
      2. 5.1.2. 保存上下文
      3. 5.1.3. 事件分发
      4. 5.1.4. 恢复上下文
    2. 5.2. pa-3.2
      1. 5.2.1. 加载用户程序
      2. 5.2.2. 识别系统调用
      3. 5.2.3. 标准输出
      4. 5.2.4. 堆区管理
    3. 5.3. pa-3.3
      1. 5.3.1. 简易文件系统
      2. 5.3.2. 操作系统之上的IOE
        1. 5.3.2.1. 把串口抽象成文件
        2. 5.3.2.2. 把设备输入抽象成文件
        3. 5.3.2.3. 把VGA显存抽象成文件
    4. 5.4. pa-3.4
      1. 5.4.1. 运行仙剑奇侠传
      2. 5.4.2. 批处理系统
    5. 5.5. 结果提交
  6. 6. pa4
    1. 6.1. pa-4.1 实现基本的多道程序系统
      1. 6.1.1. 实现上下文切换
      2. 6.1.2. 实现多道程序系统
  7. 7. Report