我用公众号记录做PA的过程的初衷本是记录一些个人做项目期间总结的经验, 教训, 以及发生的趣事, 但是到目前为止做的都还算顺利, 没有什么让我过于困扰的点. 因此我在这篇文章中就简单介绍一下目前完成的功能吧.
So far…
目前为止, 我的系统中涉及的硬件和软件都还较为简单: 硬件方面主要模拟了寄存器和内存的简单实现; 软件层面则是主要实现了一个简易调试器的前半部分. <!–
CPU
CPU(Central Processing Unit, 中央处理器)是计算机的重要计算中心. CPU的各种功能可以由软件通过指令集来调用, 而不同的指令集也会对应不同的硬件架构. 在PA中, 老师们十分善良地提供了从三种不同指令集架构的CPU(x86, mips32, Risc-V32(64))中选择其一实现的自由, 而我选择的是Risc-V32指令集架构(ISA). 值得注意的是, ISA并不是硬件, 也不是软件, 而是一套规范手册, 硬件的设计和软件的编写都需要参照ISA.
然而, 对于目前的任务(简易调试器)来说, ISA的选择并不那么重要, 因此我目前也还没实现啥功能就先不再赘述了. –>
寄存器与内存
寄存器是计算机中用于存储与运算相关的数值以及一些运算状态的存储设备, 一般来说寄存器的读写速度极快, 但是存储容量也很小. 寄存器是一个结构化很强的存储部件: 有许多存储位置有特殊的意义, 因此在PA中, 我们用结构体实现寄存器.
内存是用来存储计算机运行过程中所需的数据以及指令的存储设备, 读写速度较快, 但是存储容量也较少(相对而言). 内存在计算机中也是一个连续的物理存储空间(不考虑虚拟内存), 因此在PA中, 我们使用数组来模拟内存.
不知道没有学过数电的同学们有没有好奇过一个问题: 计算机执行程序的时候是怎样的一个过程呢? 简单来说, 我们在电脑上双击一个程序时, 它的二进制程序会被加载到内存.
二进制程序是由许多条指令组成的, 其中每一条指令可以再被拆分为操作码和操作数两部分信息. 操作码定义了一个需要CPU执行的指令, 而操作数则是CPU执行指令的对象, 可以是立即数或地址. 指令的具体格式以及操作码的具体定义和数量都是遵循ISA的设计的, 而CPU具体如何执行这些指令则是硬件设计师的工作内容.
简单来说, 程序, ISA以及硬件可以看作具有以下关系.
flowchart TD A[软件程序: 程序员] --编译器或解释器: ISA-->B[二进制程序: ISA] B --CPU: 硬件设计师--> C[程序执行]
正式由于ISA将硬件功能封装成一个个接口, 程序员才能够设计出在所有机器上都能运行的代码. 同时这也解释了为什么在x86平台上编译的可执行程序不能再arm平台上运行.
当二进制程序被加载到内存后, 有一个特殊的寄存器–PC(Program Counter), 会记录程序执行的进度, 并且在一条指令执行完后会指向下一条指令的位置. 计算机就能一条一条地执行指令, 从而运行我们所编写的程序.
值得一提的是, 在简易调试器的项目中, 代码提供了一个默认程序, 而这个程序不太好找, 我找了半天, 感到很破防烦躁, 望周知.
SDB(Simple DeBuger, 简易调试器)
SDB是PA1中主要需要实现的程序. 调试可以帮助程序员发现代码中的问题, 其功能包括: 断点, 单步执行, 步入步出等. 因此, 对于PA来说, 实现一个简易调试器也是十分重要的.
在SDB中, 我们主要需要复刻一下GDB指令:
命令 | 格式 | 说明 |
---|---|---|
帮助 | help |
打印帮助信息 |
继续运行 | c |
继续运行被暂停的程序 |
退出 | q |
退出NEMU |
单步执行 | si [N] |
让程序单步执行N条指令后暂停执行, 当N没有给出时, 缺省为1 |
打印程序状态 | info r |
打印寄存器状态 |
扫描内存 | x N EXPR |
求出EXPR的值, 结果作为其实内存地址, 以十六进制形式输出N个连续的4字节 |
我们可以通过读取存储指令的内存, 直接读出运行的指令的二进制形式, 从而也可以确定x
命令实现的正确. 这就是为什么我要找SDB中默认加载的程序:(