RTFSC

既然TRM那么简单, 就让我们在NEMU里面实现一个TRM吧.

不过我们还是先来介绍一下框架代码. 框架代码内容众多, 其中包含了很多在后续阶段中才使用的代码. 随着实验进度的推进, 我们会逐渐解释所有的代码. 因此在阅读代码的时候, 你只需要关心和当前进度相关的模块就可以了, 不要纠缠于和当前进度无关的代码, 否则将会给你的心灵带来不必要的恐惧.

ics2018
├── init.sh       # 初始化脚本
├── Makefile      # 用于工程打包提交
├── nanos-lite    # 微型操作系统内核
├── navy-apps     # 应用程序集
├── nemu          # NEMU
├── nexus-am      # 抽象计算机
└── README.md

目前我们只需要关心NEMU的内容, 其它内容会在将来进行介绍. NEMU主要由4个模块构成: monitor, CPU, memory, 设备. 我们已经在上一小节简单介绍了CPU和memory的功能, 设备会在PA2中介绍, 目前不必关心. monitor位于这个虚拟计算机系统之外, 主要用于监视这个虚拟计算机系统是否正确运行. monitor从概念上并不属于一个计算机的必要组成部分, 但对NEMU来说, 它是必要的基础设施. 它除了负责与GNU/Linux进行交互(例如读写文件)之外, 还带有调试器的功能, 为NEMU的调试提供了方便的途径. 缺少monitor模块, 对NEMU的调试将会变得十分困难.

代码中nemu/目录下的源文件组织如下(并未列出所有文件):

nemu
├── include                    # 存放全局使用的头文件
│   ├── common.h               # 公用的头文件
│   ├── cpu
│   │   ├── decode.h           # 译码相关
│   │   ├── exec.h             # 执行相关
│   │   ├── reg.h              # 寄存器结构体的定义
│   │   └── rtl.h              # RTL指令
│   ├── debug.h                # 一些方便调试用的宏
│   ├── device                 # 设备相关
│   ├── macro.h                # 一些方便的宏定义
│   ├── memory                 # 访问内存相关
│   ├── monitor
│   │   ├── expr.h             # 表达式求值相关
│   │   ├── monitor.h
│   │   └── watchpoint.h       # 监视点相关
│   └── nemu.h
├── Makefile                   # 指示NEMU的编译和链接
├── Makefile.git               # git版本控制相关
├── runall.sh                  # 一键测试脚本
└── src                        # 源文件
    ├── cpu
    │   ├── decode             # 译码相关
    │   ├── exec               # 执行相关
    │   ├── intr.c             # 中断处理相关
    │   └── reg.c              # 寄存器相关
    ├── device                 # 设备相关
    ├── main.c                 # 你知道的...
    ├── memory
    │   └── memory.c           # 访问内存的接口函数
    ├── misc
    │   └── logo.c             # "i386"的logo
    └── monitor
        ├── cpu-exec.c         # 指令执行的主循环
        ├── diff-test
        ├── debug              # 简易调试器相关
        │   ├── expr.c         # 表达式求值的实现
        │   ├── ui.c           # 用户界面相关
        │   └── watchpoint.c   # 监视点的实现
        └── monitor.c

为了给出一份可以运行的框架代码, 代码中实现了mov指令的功能, 并附带一个mov指令序列的默认客户程序. 另外, 部分代码中会涉及一些硬件细节(例如nemu/src/cpu/decode/modrm.c). 在你第一次阅读代码的时候, 你需要尽快掌握NEMU的框架, 而不要纠缠于这些细节. 随着PA的进行, 你会反复回过头来探究这些细节.

大致了解上述的目录树之后, 你就可以开始阅读代码了. 至于从哪里开始, 就不用多费口舌了吧.

需要多费口舌吗?

嗯... 如果你觉得提示还不够, 那就来一个劲爆的: 回忆程序设计课的内容, 一个程序从哪里开始执行呢?

如果你不屑于回答这个问题, 不妨先冷静下来. 其实这是一个值得探究的问题, 你会在将来重新审视它.

对vim的使用感到困难?

在PA0的强迫之下, 你不得不开始学习使用vim. 如果现在你已经不再认为vim是个到处是bug的编辑器, 就像简明vim练级攻略里面说的, 你已经通过了存活阶段. 接下来就是漫长的修行阶段了, 每天学习一两个vim中的功能, 累积经验值, 很快你就会发现自己已经连升几级. 不过最重要的还是坚持, 只要你在PA1中坚持使用vim, PA1结束之后, 你就会发现vim的熟练度已经大幅提升! 你还可以搜一搜vim的键盘图, 像英雄联盟中满满的快捷键, 说不定能激发起你学习vim的兴趣.

其实最主要的还是, 你需要有尝试新事物的精神.

NEMU开始执行的时候, 首先会调用init_monitor()函数(在nemu/src/monitor/monitor.c中定义) 进行一些和monitor相关的初始化工作, 我们对其中几项初始化工作进行一些说明. reg_test()函数(在nemu/src/cpu/reg.c中定义)会生成一些随机的数据, 对寄存器实现的正确性进行测试. 若不正确, 将会触发assertion fail.

然后, NEMU通过调用load_img()函数(在nemu/src/monitor/monitor.c中定义)读入带有客户程序的镜像文件. 我们知道内存是一种RAM, 是一种易失性的存储介质, 这意味着计算机刚启动的时候, 内存中的数据都是无意义的; 而BIOS是固化在ROM中的, 它是一种非易失性的存储介质, BIOS中的内容不会因为断电而丢失. 因此在真实的计算机系统中, 计算机启动后首先会把控制权交给BIOS, BIOS经过一系列初始化工作之后, 再从磁盘中将有意义的程序读入内存中执行. 对这个过程的模拟需要了解很多超出本课程范围的细节, 我们在这里做了简化: 让monitor直接把一个有意义的客户程序镜像guest prog读入到一个固定的内存位置0x100000. 这个程序是运行NEMU的一个参数, 在运行NEMU的命令中指定, 缺省时将把上文提到的mov程序作为客户程序(参考load_default_img()函数). 这时内存的布局如下:

0             0x100000
-----------------------------------------------
|                 |                  |
|                 |    guest prog    |
|                 |                  |
-----------------------------------------------
                  ^
                  |
                 eip

接下来调用restart()函数(在nemu/src/monitor/monitor.c中定义), 它模拟了"计算机启动"的功能, 进行一些和"计算机启动"相关的初始化工作, 一个重要的工作就是将%eip的初值设置为刚才我们约定的内存位置0x100000, 这样就可以让CPU从我们约定的内存位置开始执行程序了.

monitor的其它初始化工作我们会在后续实验内容中介绍, 目前可以不必关心它们的细节, 最后通过调用welcome()函数输出欢迎信息.

monitor的初始化工作结束后, NEMU会进入用户界面主循环ui_mainloop()(在nemu/src/monitor/debug/ui.c中定义), 输出NEMU的命令提示符:

(nemu)
实现正确的寄存器结构体

我们在PA0中提到, 运行NEMU会出现assertion fail的错误信息, 这是因为框架代码并没有正确地实现用于模拟寄存器的结构体CPU_state, 现在你需要实现它了(结构体的定义在nemu/include/cpu/reg.h中). 实现正确后, NEMU将不会再触发assertion fail, 而是输出上文提到的命令提示符.

关于i386寄存器的更多细节, 请查阅i386手册. Hint: 使用匿名union.

什么是匿名union?

你有这个疑问是很正常的, 但你接下来应该意识到要去STFW了.

reg_test()是如何测试你的实现的?

阅读reg_test()的代码, 思考代码中的assert()条件是根据什么写出来的.

框架代码已经实现了几个简单的命令, 它们的功能和GDB是很类似的. 输入c之后, NEMU开始进入指令执行的主循环cpu_exec()(在nemu/src/monitor/cpu-exec.c中定义). cpu_exec()模拟了CPU的工作方式: 不断执行指令. exec_wrapper()函数(在nemu/src/cpu/exec/exec.c中定义)让CPU执行当前%eip指向的一条指令, 然后更新%eip. 已经执行的指令会输出到日志文件nemu/build/nemu-log.txt中, 你可以打开日志文件来查看它们.

究竟要执行多久?

cmd_c()函数中, 调用cpu_exec()的时候传入了参数-1, 你知道这是什么意思吗?

潜在的威胁 (建议二周目思考)

"调用cpu_exec()的时候传入了参数-1", 这一做法属于未定义行为吗? 请查阅C99手册确认你的想法.

执行指令的相关代码在nemu/src/cpu/exec/目录下. 其中一个重要的部分定义在nemu/src/cpu/exec/exec.c文件中的opcode_table数组, 在这个数组中, 你可以看到框架代码中都已经实现了哪些指令. 其中EMPTY代表对应的指令还没有实现(也可能是x86中不存在该指令). 在以后的PA中, 随着你实现越来越多的指令, 这个数组会逐渐被它们代替. 关于指令执行的详细解释和exec_wrapper()相关的内容需要涉及很多细节, 目前你不必关心, 我们将会在PA2中进行解释.

温故而知新

opcode_table到底是个什么类型的数组? 如果你感到困惑, 你需要马上复习程序设计的知识了. 这里有一份十分优秀的C语言教程. 事实上, 我们已经在PA0中提到过这份教程了, 如果你觉得你的程序设计知识比较生疏, 而又没有在PA0中阅读这份教程, 请你务必阅读它.

NEMU将不断执行指令, 直到遇到以下情况之一, 才会退出指令执行的循环:

  • 达到要求的循环次数.
  • 客户程序执行了nemu_trap指令. 这是一条特殊的指令, 机器码为0xd6. 如果你查阅i386手册, 你会发现x86中并没有这条指令, 它是为了在NEMU中让客户程序指示执行的结束而加入的.

当你看到NEMU输出以下内容时:

nemu: HIT GOOD TRAP at eip = 0x00100026

说明客户程序已经成功地结束运行. 退出cpu_exec()之后, NEMU将返回到ui_mainloop(), 等待用户输入命令. 但为了再次运行程序, 你需要键入q退出NEMU, 然后重新运行.

谁来指示程序的结束?

在程序设计课上老师告诉你, 当程序执行到main()函数返回处的时候, 程序就退出了, 你对此深信不疑. 但你是否怀疑过, 凭什么程序执行到main()函数的返回处就结束了? 如果有人告诉你, 程序设计课上老师的说法是错的, 你有办法来证明/反驳吗? 如果你对此感兴趣, 请在互联网上搜索相关内容.

有始有终 (建议二周目思考)

对于GNU/Linux上的一个程序, 怎么样才算开始? 怎么样才算是结束? 对于在NEMU中运行的程序, 问题的答案又是什么呢?

与此相关的问题还有: NEMU中为什么要有nemu_trap? 为什么要有monitor?

最后我们聊聊代码中一些值得注意的地方.

  • 三个对调试有用的宏(在nemu/include/debug.h中定义)

    • Log()printf()的升级版, 专门用来输出调试信息, 同时还会输出使用Log()所在的源文件, 行号和函数. 当输出的调试信息过多的时候, 可以很方便地定位到代码中的相关位置
    • Assert()assert()的升级版, 当测试条件为假时, 在assertion fail之前可以输出一些信息
    • panic()用于输出信息并结束程序, 相当于无条件的assertion fail

    代码中已经给出了使用这三个宏的例子, 如果你不知道如何使用它们, RTFSC.

  • 内存通过在nemu/src/memory/memory.c中定义的大数组pmem来模拟. 在客户程序运行的过程中, 总是使用vaddr_read()vaddr_write()访问模拟的内存. vaddr, paddr分别代表虚拟地址和物理地址. 这些概念在将来会用到, 但从现在开始保持接口的一致性可以在将来避免一些不必要的麻烦.
理解框架代码

你需要结合上述文字理解NEMU的框架代码. 需要注意的是, 阅读代码也是有技巧的, 如果你分开阅读框架代码和上述文字, 你可能会觉得阅读之后没有任何效果. 因此, 你需要一边阅读上述文字, 一边阅读相应的框架代码.

如果你不知道"怎么才算是看懂了框架代码", 你可以先尝试进行后面的任务. 如果发现不知道如何下手, 再回来仔细阅读这一页面. 理解框架代码是一个螺旋上升的过程, 不同的阶段有不同的重点. 你不必因为看不懂某些细节而感到沮丧, 更不要试图一次把所有代码全部看明白.

事实上, TRM的实现已经都蕴含在上述的介绍中了.

  • 存储器是个在nemu/src/memory/memory.c中定义的大数组
  • PC和通用寄存器都在nemu/include/cpu/reg.h中的结构体中定义
  • 加法器在... 嗯, 这部分框架代码有点复杂, 不过它并不影响我们对TRM的理解, 我们还是在PA2里面再介绍它吧
  • TRM的工作方式通过cpu_exec()exec_wrapper()体现

在NEMU中, 我们只需要一些很简单的C语言知识就可以理解最简单的计算机的工作方式, 真应该感谢先驱啊.

results matching ""

    No results matching ""