Skip to content

PHANTOM-2004/my-ics-pa

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

My implementation for ICS2023-pa

项目说明以及概要请参照2023分支. (For more information, please refer to branch 2023)

本项目由我个人独立完成, 如果正在写PA的NJU的同学恰好看到这个仓库, 请注意学术诚信(Do your homework by yourself, TOO).


What is it?

nemu

  1. 实现了nemu, 这是一个emulator. 我选择了riscv32作为ISA. nemu(类似于qemu)模拟cpu取指, 译码, 执行的过程.

  2. 其中包含一个简单的monitor, monitor中实现debugtrace系统. 支持简单的表达式求值, 监视点等功能.

  • ITRACE: 对指令调用的跟踪, 采用环形缓冲区
  • FTRACE: 对函数调用的跟踪. 这里实现一个简单的elf_parser解析elf文件, 读取符号表中的函数信息
  • MTRACE: 对指令访存的跟踪.
  • DTRACE: 访问外设的跟踪.
  • ETRACE: 对异常的跟踪.
  • DEBUG: 实现单步执行, 监视点, 表达式求值等. 同时表达式求值部分进行了充分测试.
  1. 支持IO类型:
  • 5 devices
    • serial, timer, keyboard, VGA, audio
    • most of them are simplified and unprogrammable
  • 2 types of I/O
    • port-mapped I/O and memory-mapped I/O

Abstract Machine

实现了abstract-machine 对于完整的computer而言, 只有cpu是不够的. 因此AM中包含了程序运行需要的runtime(在这里也就是klib). 同时还需要包含与外界的IOE(Input Output Extension). 这里通过静态链接, 把交叉编译的程序链接到 klib. 同时在AM中提供内部程序与键盘, VGA等固件交互的接口.

  • klib: 简单的标准库, 包括printf系列以及string系列的常用函数.
  • IOE: 实现对nemu的扩展, 与外设的交互. 通过抽象寄存器实现, 将接口抽象为寄存器, 通过io_read/write进行读写.
  • CTE: 实现CTE(Context Transfer Extension). 设置中断向量, 实现上下文的保存和恢复. trap通过汇编实现, 保存上下文, 调用__am_irq_handler根据约定寄存器的值识别异常类型, 最后恢复上下文.

navy

提供了nanos-lite所需要的runtime. 我在其中完善了:

  • libndl: 对系统调用进行封装, 实现VGA现存以及时钟的交互
  • libminiSDL: 仿照SDL库, 在NDL上进一步封装, 实现时钟, 事件等待(阻塞与非阻塞), 画布相关操作(画布填充, 画布复制)等api.
  • libos: 这部分主要是为操作系统的syscall提供进一步的封装. 比如
int _open(const char *path, int flags, mode_t mode) {
  return _syscall_(SYS_open, (intptr_t)path, flags, mode);
}

系统调用的实现在nanos-lite中实现, 在am中实现cte_init传入do_syscall这一回调函数, 具体的处理通过do_syscall这一回调函数进行处理. 封装了输入输出, 时钟, 文件系统, 堆栈管理的相关接口.

nanos-lite

一个非常简易的操作系统.

  • elf loader: 负责解析elf文件, 将文件加载进入内存, 并且跳入程序执行入口.
  • STRACE: 实现对系统调用的跟踪.
  • 简单的文件系统: 文件大小固定, 文件数量固定. 实现虚拟文件系统(一切皆文件的思想), 将键盘输入, 标准输出, VGA显存等与外设交互的接口抽象为文件的读写.
  • 系统调用的实现: 这部分与am, libos紧密联系, 实现的是do_syscall这一回调函数. 支持如下系统调用:
void do_syscall(Context *c) {
  uintptr_t a[4] = {c->GPR1, c->GPR2, c->GPR3, c->GPR4};

#ifdef CONFIG_STRACE
  strace(a[0], a[1], a[2], a[3], c->mepc);
#endif

  switch (a[0]) {
  case SYS_yield:
    sys_yield();
    break;
  case SYS_exit:
    sys_exit(a[1]);
    break;
  case SYS_write:
    c->GPRx = (uintptr_t)sys_write(a[1], (void const *)a[2], a[3]);
    break;
  case SYS_brk:
    c->GPRx = (uintptr_t)sys_brk(a[1]);
    break;
  case SYS_read:
    c->GPRx = (uintptr_t)sys_read(a[1], (void *)a[2], a[3]);
    break;
  case SYS_open:
    c->GPRx = (uintptr_t)sys_open((char const *)a[1], a[2], a[3]);
    break;
  case SYS_close:
    c->GPRx = (uintptr_t)sys_close(a[1]);
    break;
  case SYS_lseek:
    c->GPRx = (uintptr_t)sys_lseek(a[1], a[2], a[3]);
    break;
  case SYS_gettimeofday:
    c->GPRx = (uintptr_t)sys_gettimeofday((struct timeval *)a[1],
                                          (struct timezone *)a[2]);
    break;
  default:
    panic("Unhandled syscall ID = %d", a[0]);
  }
}

Summary

抽象层次

nemu平台支持裸指令的运行, abstract machine提供与硬件交互的抽象api,klib中的简单库为裸机上的程序提供简单的runtime.

当在nemu中运行nanos-lite时, amklibOS提供与硬件交互的接口, OS取裸机上的一段内存作为ramdisk.

navy中的libc(来自Newlib)提供了操作系统中运行程序的runtime, navy中的libos为操作系统提供系统调用的接口, 作为libc的底层支持. 在运行复杂程序时, 使用navy中的libminiSDL等库提供更复杂的IO交互.

收获

实际上这次的PA让我对OS与体系结构有了更加深入的思考. 把指令集, 汇编, 以及抽象机相关的知识串联. 为什么trap.S需要使用汇编而不是纯C, 为什么恢复上下文需要使用asm而不是C, 为什么需要klib, 又为什么需要libos, 以及为什么需要中断, 为什么需要syscall.

代码能力和对体系结构的理解都有深化. 如何利用日志, 以及assert提高编码质量, 方便调试, 提早定位错误.

一些遗憾

PA3的最后, 之前为了保证正确性一直打开了difftest. 可是当关闭的时候, 指令执行确出现了错误, 这是一个nemu层面的bug, 但仍然没有定位出来. 根据指令执行日志定位到了一个函数调用. 但原因尚未查明. 不过也刚好PA的主要内容也都认真走了一遍流程. 在这个bug暴露出来的时候, 主要影响就是无法关闭difftest, 对性能造成了损失.

实际上这样也是一个教训, 编码需要更多的assert, 以及不同环境的测试. difftest带有的版本相当于一个DEBUG版本, 关闭difftest相当于RELEASE. 实际上我们需要更充分的测试, 关闭所有调试信息, 以及打开所有调试信息分别测试. 因为很可能在哪里, 就写出来一个UB. 而在-O2甚至-O3优化之下, 这样的问题就可能暴露出来.

在起始阶段需要更充分的测试, 或者至少需要通过Assert把问题尽可能早的暴露出来.

以上是之前的想法, 当我换用gccclang均发现同样的错误的时候, 我实际上不应该怀疑UB. 因为不同编译器对于UB的处理大概率不一样. 我从寄存器角度排查, 发现使用了没有初始化的内存. 当打开difftest的时候内存是默认全部初始化为0的. 但是用脚趾头想一想都知道真实的计算机不可能上来内存全部初始化为0. 那么问题出现在哪里?

正是ELF Loader, 程序中没有清零, 而是由OS清零的.bss段. 我采用的申请buffer清零,在把清零过得buffer拷贝过去的方法. 但是拷贝的时候拷贝的大小拷贝错了, 导致需要清零的一段没有清零.

测试是极为重要的, ELF Loader完成后不能只看跑不跑, 而要检查对应的区间是否设置为0. 而且在difftest的影响下的确是0, 如果我真的去检查还真不一定检查出来. 但是如果我在这一段设置一部分assert, 这样就能在我关闭difftest的时候发现. 这也启示着要在不同环境进行测试, 并且使用assert使错误尽可能早的暴露出来.

这正应验了, 机器永远是对的, 没有调试不出来的错误


Toolchain && Environment

使用正确的工具

  • gcc: gcc (GCC) 14.1.1 20240720
  • clang: clang version 18.1.8
  • llvm toolchain, clangd + bear实现精准的补全与跳转
  • Editor: Neovim
  • OS && DE: archlinux + KDE(X11).

About

My implementation for NJU ICS PA.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • C 92.7%
  • C++ 2.9%
  • Makefile 2.2%
  • Assembly 0.9%
  • Yacc 0.7%
  • Lex 0.4%
  • Other 0.2%