# myPa **Repository Path**: wkling/my-pa ## Basic Information - **Project Name**: myPa - **Description**: 2025春季学期,nuaaOS本科课程lab,nuaa做了精简版移植。lab主要目的是编码实现一个简单的PC:NEMU(在QEMU上跑NEMU,相当于模拟器上再实现一个模拟器)。 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2023-05-29 - **Last Updated**: 2025-09-29 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # ICS2021 Programming Assignment refer to https://nuaa-pa-2021.gitee.io/ics-pa-gitbook/ * https://github.com/NJU-ProjectN/nemu) * [Nexus-am](https://github.com/NJU-ProjectN/nexus-am) * [Nanos-lite](https://github.com/NJU-ProjectN/nanos-lite) * [Navy-apps](https://github.com/NJU-ProjectN/navy-apps) PA的目的是要实现NEMU, 一款经过简化的x86模拟器(类似QEMU),基于C语言实现的,可以用来模拟x86部分功能的程序,我们可以在NEMU上执行其他程序。 **NEMU 就是一个虚拟出来的计算机硬件,物理计算机中的基本功能在 NEMU 中都是通过程序来实现的。** # 架构 ``` ics2021 ├── init.sh # 初始化脚本 ├── Makefile # 用于工程打包提交 ├── nanos-lite # 微型操作系统内核 ├── navy-apps # 应用程序集 ├── nemu # NEMU └── nexus-am # 抽象计算机 ``` NEMU 主要由 4 个模块构成: - CPU(寄存器设计,指令译码表,一些rtl语言和指令执行操作) - memory(内存IO映射) - device(按键检测,计时器返回时间,in,out指令) - monitor 完成了表达式求值(PA1.3),在NEMU上跑通C语言程序(PA2.1),IO操作(PA2.3) 注:monitor位于这个虚拟计算机系统之外,主要用于监视这个虚拟计算机系统是否正确运行。 monitor 从概念上并不属于一个计算机的必要组成部分,但对 NEMU 来说,它是必要的基础设施。 内存通过在 `nemu/src/memory/memory.c` 中定义的大数组 `pmem` 来模拟。在客户程序运行的过程中,总是使用 `vaddr_read()` 和 `vaddr_write()` 访问模拟的内存。`vaddr`, `paddr`分别代表虚拟地址和物理地址。 **NEMU执行过程:** 1. main函数有两个:init_monitor与ui_mainloop 2. init_monitor(),初始化monitor,调用一系列检测,reg_test()检查寄存器实现与否 3. load_img(),模拟现实计算机的BIOS初始化,把镜像读到固定的内存位置:0x100000 4. 调用restart(),模拟计算机启动,eip设置为0x100000, 5. 接下来的工作包括: - 调用 `init_regex()` 函数来编译正则表达式(PA1.2内容) - 调用 `init_wp_pool()` 函数来初始化监控池(PA1.3内容) - 调用 `init_device()` 函数来初始化设备(不做要求) - 调用 `welcome()` 函数输出欢迎信息和 NEMU 的编译时间 6. monitor 的初始化工作结束后, NEMU 会进入用户界面主循环 `ui_mainloop()`,可以读入指令 7. 执行指令的相关代码在 `nemu/src/cpu/exec` 目录下。 # PA0 **运行:**nemu/下 make run **连接FileZilla:** 127.0.0.1, wangkunlin, 12345, prot:22 **gdb** 简单步骤: 1. 在编译前先`gcc -g`:以调试模式运行。 2. 运行a.out `gdb a.out` 3. 设置断点 `br main(函数入口)` OR `br 行号` 看源码`l` 看目前的断点`info b` 运行`r` 会停在断点 查看某个变量值`p i(变量)` 执行下一步`n 步过函数`, `s 步入函数` 执行到下一个断点`c` 4. 查看反汇编 `disas` **git分支** 重命名分支:`git branch -m master` 创建新分支并切换: `git checkout -b pa0` 推送分支`git push orign branch` 拉取分支`git pull origin paX` # PA1.1 **PA1.1实现:** 构造NEMU的CPU寄存器:用union和struct构建; 完善简易调试器:三个函数,strtok分词。 计算机要做的唯一一件事情: ``` while (1) { 从PC指示的存储器位置取出指令; 执行指令; 更新PC; } ``` 最简单的图灵完备的计算机:结构上,有存储器,PC,寄存器,加法器。工作方式如上。 虚拟机和模拟器有什么区别: ``` 底层技术不同:虚拟机使用的是主机操作系统的虚拟化技术,可以在主机操作系统上运行多个虚拟机操作系统实例,每个实例都有自己的内存、CPU、磁盘等资源。而模拟器则是使用软件来模拟硬件,可以在一个操作系统上模拟另一个操作系统的运行环境。 运行效率不同:由于虚拟机使用的是主机操作系统的虚拟化技术,因此在运行虚拟机时,虚拟机和主机操作系统之间的通信效率较高,虚拟机的性能也比较高。而模拟器则需要使用软件来模拟硬件,因此在运行模拟器时,会存在一定的性能损失。 应用场景不同:虚拟机主要用于服务器虚拟化、开发环境隔离和测试等场景,可以让多个操作系统实例共享主机硬件资源,提高服务器的利用率和管理效率。而模拟器主要用于游戏、移动设备开发和跨平台应用开发等场景,可以在一个操作系统上模拟其他操作系统的运行环境,便于开发和测试。 ``` 实现CPU寄存器 ```c //union中套struct原因:方便标识gpr[0] = eax, 因为gpr[8]占用的字节和8个uint32_t一样,访问的时候等价 union{ union{ //这些寄存器不能同时存在,所以使用union来搭建 uint32_t _32; uint16_t _16; uint8_t _8[2]; }gpr[8]; struct{ uint32_t eax, ecx, edx, ebx, esp, ebp, esi, edi; }; }; ``` 完善简易调试器,略 # PA1.2 **PA1.2实现**:表达式求值 过程:完成字符串正则匹配规则,根据规则用switch case拆出token,然后找出词法单元(自定义函数,比较优先级,遍历的方式找出子表达式的中心操作符),最后递归计算。 make_token()函数,用 `position` 变量来指示当前处理到的位置, 且按顺序尝试用不同的规则来匹配当前位置的字符串. 当一条规则匹配成功, 并且匹配出的子串正好是`position`所在位置的时候, 我们就成功地识别出一个 `token`。 ```c static struct rule { char *regex; int token_type; } rules[] = { /* TODO: Add more rules. * Pay attention to the precedence level of different rules. */ {" +", TK_NOTYPE}, // spaces,表示空格重复1-n次 {"\\+", '+'}, // plus 两个\ c语言中字符串内表示\要用'\\' {"-",'-'}, {"\\*",'*'}, {"/",'/'}, {"==", TK_EQ}, // equal {"\\(",'('}, {"\\)",')'}, {"0[Xx][0-9a-fA-F]{1,8}",TK_hexNumber}, //hexNumber {"[0-9]{1,9}",TK_decNumber}, //decNumber {"\\$e[a-dsi]{1}[xpi]{1}",TK_register}, //register 注意$也是一个元字符 {"!=",TK_UEQ}, {"&&",TK_AND}, //logical AND {"\\|\\|",TK_OR}, //logical OR {"!",TK_NOT}, //logical NOT }; ``` # PA1.3 完善调试器,实现监视点 有BUG,实现后不好用。 # PA2.1 **编写基本指令集,在NEMU上跑通了第一个C程序** 只需要完成译码表(查手册),其余交给框架。 取指:`instr_fetch()`(在`nemu/include/cpu/exec.h` 中定义)专门负责取指令的工作。 译码:`exec_real()`先取出第一个字节,记录在全局译码信息里,之后看操作数宽度(有些指令有2个字节),查表 最后执行:一堆宏最后到执行函数(框架已给出)。 具体执行:NEMU 使用 [RTL](https://en.wikipedia.org/wiki/Register_transfer_language) (寄存器传输语言)来描述 x86 指令的行为。 需要完成一些RTL函数(pop,mv,sext符号扩展等),eflag寄存器(查手册写数据结构),完成一些opcode_table(查手册)。 指令执行过程:取指,译码(CPU读出操作数,操作码),执行,更新EIP。 只要有`inc`, `dec`, `jne`这三条指令, 就可以实现"所有"的算法。 **RI** **SC与CISC** ``` CISC 始于 CPU 发展的初期, 当时编写软件只能通过汇编、机器语言实现, CISC 的设计初衷就是为了降低开发的难度。 CISC 的思想就是指令集越丰富, 每个指令实现的功能越多, 使用指令集编写程序时就越方便,这样就避免了开发人员需要额外开发大量指令。 CISC 的典型代表: x86 架构 CISC 优势: 少量指令实现大量功能,同时节省了程序本身的大小,降低内存空间的占用。 ``` ``` 什么是 RISC: 我们可以理解 RISC 是 CISC 的反向操作, 是一个简单指令集架构, 通常有 20 多个简化的指令集,指令长度固定, 并通过专门的 load 和 store 进行内存寻址。RISC 在 cpu 中通过配备大量的寄存器, 然后让大多数运算的指令只能访问操作寄存器,同时选择的指令都是那 20% 高频使用的指令,从而使得 cpu 的执行效率大幅度提升。 RISC 的典型代表: ARM 架构、THUMB 架构、RISC-V 架构。RISC 优势: CPU 一个时钟周期能同时运行多条指令,效率得以提升, 同时指令集相对较少, 便于理解。RISC 劣势:完成同样的功能,相比 CISC 来说要使用更多的指令, 导致内存空间要消耗的更多, 但是随着目前内存空间的持续增加,其实也不是什么大的问题。 ``` 完成译码查找表:`nemu/src/cpu/exec/exec.c` 中的 `opcode_table` 数组 具体来说,运行一段给定的程序,当运行到没有实现的时候,会给出`good trap`以及反汇编提示,查手册来完成相关的指令填表和实现。 # PA2.2 完成其他指令 ``` AM = TRM + IOE + ASYE + PTE + MPE ``` - TRM(Turing Machine) - 图灵机, 为计算机提供基本的计算能力 - IOE(I/O Extension) - 输入输出扩展, 为计算机提供输出输入的能力 - ASYE(Asynchronous Extension) - 异步处理扩展, 为计算机提供处理中断异常的能力 - PTE(Protection Extension) - 保护扩展, 为计算机提供存储保护的能力 - MPE(Multi-Processor Extension) - 多处理器扩展, 为计算机提供多处理器通信的能力 (MPE超出了ICS课程的范围, 在PA中不会涉及) # PA2.3 完成IO(框架已经给出大部分IO扩展的API,),完成了按键状态检测,和内存映射IO的API。 NEMU的VGA显存位于物理地址区间`[0xa1000000, 0xa1080000)` 设备也有自己的状态寄存器(相当于CPU的寄存器), 也有自己的功能部件(相当于CPU的运算器) IO编址方式:端口映射IO(独立方式)与内存映射IO(统一方式) **端口映射IO**:内存和I/O设备有各自的地址空间,每个控制寄存器被分配一个I/O端口号,用特殊的指令来操作IO设备。 **内存映射IO**:内存和I/O设备共享同一个地址空间,I/O设备的内存和寄存器被映射到与之相关联的地址,CPU访问某个内存地址时,它可能是物理内存,也可以是某个I/O设备的内存。现代计算机主流的 I/O 编址方式。