# 程序, 运行时环境与AM

## 现代指令系统

我们已经成功在TRM上运行`dummy`程序了, 然而这个程序什么都没做就结束了, 一点也不过瘾啊. 为了让NEMU支持大部分程序的运行, 你还需要实现更多的指令:

* Data Movement Instructions: `mov`, `push`, `pop`, `leave`, `cltd`(在i386手册中为`cdq`), `movsx`~~,~~ `movzx`
* Binary Arithmetic Instructions: `add`, `inc`, `sub`, `dec`, `cmp`, `neg`, `adc`~~,~~ `sbb`~~,~~ `mul`~~,~~ `imul`~~,~~ `div`~~,~~ `idiv`
* Logical Instructions: `not`, `and`, `or`, `xor`, `sal(shl)`, `shr`, `sar`, `setcc`, `test`
* Control Transfer Instructions: `jmp`~~,~~ `jcc`, `call`, `ret`
* Miscellaneous Instructions: `lea`~~,~~ `nop`

框架代码已经实现了上述删除线标记的指令, 但并没有填写`opcode_table`. 此外, 某些需要更新EFLAGS的指令并没有完全实现好(框架代码中已经插入了`TODO()`作为提示), 你还需要编写相应的功能.

## 运行时环境与AM

但并不是有了足够的指令就能运行更多的程序. 我们之前提到"并不是每一个程序都可以在NEMU中运行", 现在我们来解释一下背后的缘由.

从直觉上来看, 让TRM来支撑一个功能齐全的操作系统的运行还是比较勉强的. 这给我们的感觉就是, 计算机也有一定的"功能强弱"之分, 计算机越"强大", 就能跑越复杂的程序. 换句话说, 程序的运行其实是对计算机的功能有需求的. 在你运行Hello World程序时, 你敲入一条命令(或者点击一下鼠标), 程序就成功运行了, 但这背后其实隐藏着操作系统开发者和库函数开发者的无数汗水. 一个事实是, 应用程序的运行都需要[运行时环境](http://en.wikipedia.org/wiki/Runtime_system)的支持, 包括加载, 销毁程序, 以及提供程序运行时的各种动态链接库(你经常使用的库函数就是运行时环境提供的)等. 为了让客户程序在NEMU中运行, 现在轮到你来提供相应的运行时环境的支持了. 不用担心, 由于NEMU目前的功能并不完善, 我们必定无法向用户程序提供GNU/Linux般的运行时环境.

我们先来讨论一下程序执行究竟需要些什么. 1. 程序需要有地方存放代码和数据, 于是需要内存 1. 程序需要执行, 于是需要CPU以及指令集 1. 对于需要运行结束的程序, 需要有一种结束运行的方法

事实上, 可以在TRM上运行的程序都对计算机有类似的需求. 我们把这些计算机相关的需求抽象成统一的API提供给程序, 这样程序就不需要关心计算机硬件相关的细节了. 这正是AM(Abstract machine)项目的意义所在.

你或许会觉得NEMU与AM的关系有点模糊不清, 让我们还是来看ATM机的例子.

说起ATM机, 你脑海里一定会想起一个可以存款, 取款, 查询余额, 转账的机器. 我们不妨把你脑海里的这个机器的模型称为抽象ATM机. 从用户的角度来说, 用户对ATM机的功能是有期望的: 要能存款, 取款, 查询余额, 转账.

从银行的角度来说, 不同银行的ATM机千差万别: 存款的加密方式, 交易时使用的自定义通信协议, 余额在银行系统里面的表示和组织方式... 不同银行的ATM机之间存在这么多细节上的差异, 怎么样才能让用户方便地使用ATM机呢? 那就是, 为不同银行的ATM机分别实现上文提到的抽象ATM机的功能: 只要ATM机实现了存款, 取款和查询余额的这组统一的功能, 和用户对抽象ATM机的认识匹配上, 用户就可以方便地使用这台ATM机, 而不必关心ATM机的上述细节.

在NEMU和AM的关系中, 程序就像是用户, AM就像是抽象ATM机, 我们实现NEMU这个计算机就像是造一台新的(虚拟的)ATM机, 也就像我们在PA1中提到的, 写一个支付宝APP. 同样的道理, 程序对计算机的功能是有期望的: 要能计算, 输入输出... 这些功能的期望组成了一台抽象计算机AM, 它刻画了一台真实计算机应该具备的功能. 但不同计算机的硬件配置各不相同, ISA也千差万别, 怎么样才能让程序方便地运行呢? 那就是, 为不同的计算机分别实现AM的功能: 只要计算机实现了AM定义的一组统一的API, 就能和程序对计算机的功能期望匹配上, 程序就可以方便地在计算机上运行, 而不必关心计算机的底层细节.

有兴趣折腾的同学还可以来理解一下真机, NEMU和AM这三者的关系. 我们会发现, 无论是真实的ATM机还是支付宝APP, 都符合我们对的抽象ATM机的认知: 它们都能存款, 取款, 查询余额, 转账. 也正因为如此, 支付宝APP刚推出的时候, 我们才能很容易上手: 虽然支付宝APP是个虚拟的ATM机, 但我们还是可以很容易根据我们对抽象ATM机的认知来使用它.

回到NEMU的例子中来, 我们还是用ATM机的例子来比喻: 真机就像是一台真实的ATM机, NEMU这个虚拟机就像是一个支付宝APP, AM还是我们概念上的抽象ATM机. 只要一台机器实现了AM的功能(能计算, 能输出输入...), 程序都可以在上面运行, 不必关心这台机器是真实的, 还是用程序虚拟出来的.

用一句话来总结这三者的关系: AM在概念上定义了一台抽象计算机, 它从运行程序的视角刻画了一台计算机应该具备的功能, 而真机和NEMU都是这台抽象计算机的具体实现, 只是真机是通过物理上存在的数字电路来实现, NEMU是通过程序来实现.

如果你对面向对象程序设计有一些初步的了解, 解释起来就更简单了:

> AM是个抽象类, 真机和虚拟机是由AM这个抽象类派生出来的两个子类, 而x86真机和NEMU则分别是这两个子类的实例化.

AM作为一个计算机的抽象模型, 可以将一个现代计算机从逻辑上划分成以下模块

```
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中不会涉及)

不同程序对计算机的功能需求也不完全一样, 例如只进行纯粹计算任务的程序在TRM上就可以运行; 要运行小游戏, 仅仅是TRM就不够了, 因为小游戏还需要和用户进行交互, 因此还需要IOE; 要运行一个现代操作系统, 还要在此基础上加入ASYE和PTE. 我们知道ISA是计算机系统中的软硬件接口, 而从上述AM的模块划分可以看出, AM描述的恰恰就是ISA本身, 它是不同ISA的抽象.

感谢AM项目的诞生, 让NEMU和程序的界线更加泾渭分明, 同时使得PA的流程更加明确:

```
(在NEMU中)实现硬件功能 -> (在AM中)提供软件抽象 -> (在APP层)运行程序
(在NEMU中)实现更强大的硬件功能 -> (在AM中)提供更丰富的软件抽象 -> (在APP层)运行更复杂的程序
```

这个流程其实与PA1中开天辟地的故事遥相呼应: 先驱希望创造一个计算机的世界, 并赋予它执行程序的使命. 亲自搭建NEMU(硬件)和AM(软件)之间的桥梁来支撑程序的运行, 是"理解程序如何在计算机上运行"这一终极目标的不二选择.

## RTFSC(3)

我们来简单介绍一下AM项目的代码. 代码中`nexus-am`目录下的源文件组织如下(部分目录下的文件并未列出):

```
nexus-am
├── am                               # AM相关
│   ├── am.h
│   ├── arch                         # 不同体系结构-平台的AM实现
│   │   ├── native
│   │   └── x86-nemu                 # x86-nemu的AM实现
│   │       ├── img                  # 构建/运行二进制文件/镜像的脚本
│   │       │   ├── boot
│   │       │   │   ├── Makefile
│   │       │   │   └── start.S      # 程序入口
│   │       │   ├── build            # 构建脚本
│   │       │   ├── loader.ld        # 链接脚本
│   │       │   └── run              # 运行脚本
│   │       ├── include
│   │       ├── README.md
│   │       └── src
│   │           ├── asye.c           # ASYE
│   │           ├── ioe.c            # IOE
│   │           ├── pte.c            # PTE
│   │           ├── trap.S
│   │           └── trm.c            # TRM
│   └── Makefile
├── apps                             # 直接运行在AM上的应用
├── libs                             # 可以直接运行在AM上的库
├── Makefile
├── Makefile.app
├── Makefile.check
├── Makefile.compile
├── Makefile.lib
├── README.md
├── SPEC.md                          # AM接口规范说明
└── tests                            # 直接运行在AM上的测试
```

整个AM项目分为三大部分:

* `nexus-am/am` - 不同计算机架构的AM实现, 在PA中我们只需要关注`nexus-am/am/arch/x86-nemu`即可
* `nexus-am/tests`和`nexus-am/apps` - 一些功能测试和直接运行AM上的应用程序
* `nexus-am/libs` - 一些体系结构无关的, 可以直接运行在AM上的库, 方便应用程序的开发

在让NEMU运行客户程序之前, 我们需要将客户程序的代码编译成可执行文件. 需要说明的是, 我们不能使用gcc的默认选项直接编译, 因为默认选项会根据GNU/Linux的运行时环境将代码编译成运行在GNU/Linux下的可执行文件. 但此时的NEMU并不能为客户程序提供GNU/Linux的运行时环境, 在NEMU中运行上述可执行文件会产生错误, 因此我们不能使用gcc的默认选项来编译用户程序.

解决这个问题的方法是[交叉编译](http://en.wikipedia.org/wiki/Cross_compiler), 我们需要在GNU/Linux下根据AM的运行时环境编译出能够在NEMU中运行的可执行文件. 为了不让链接器ld使用默认的方式链接, 我们还需要提供描述AM运行时环境的链接脚本. AM的框架代码已经把相应的配置准备好了:

* gcc将AM实现的源文件编译成目标文件, 然后通过ar将这些目标文件打包成一个归档文件作为一个库,

  把不同计算机架构的AM实现通过库的方式提供给程序
* gcc把在AM上运行的应用程序源文件编译成目标文件
* 必要的时候通过gcc和ar把程序依赖的运行库也打包成归档文件
* 执行脚本文件`nexus-am/am/arch/x86-nemu/img/build`, 在脚本文件中
  * 将程序入口`nexus-am/am/arch/x86-nemu/img/boot/start.S`编译成目标文件
  * 最后让ld根据链接脚本`nexus-am/am/arch/x86-nemu/img/loader.ld`,

    将上述目标文件和归档文件链接成可执行文件

根据这一链接脚本的指示, 可执行程序重定位后的节从`0x100000`开始, 首先是`.text`节, 其中又以`nexus-am/am/arch/x86-nemu/img/boot/start.o`中自定义的`entry`节开始, 然后接下来是其它目标文件的`.text`节. 这样, 可执行程序的`0x100000`处总是放置`nexus-am/am/arch/x86-nemu/img/boot/start.S`的代码, 而不是其它代码, 保证客户程序总能从`0x100000`开始正确执行. 链接脚本也定义了其它节(包括`.rodata`, `.data`, `.bss`)的链接顺序, 还定义了一些关于位置信息的符号, 包括每个节的末尾, 栈顶位置, 堆区的起始和末尾.

我们对编译得到的可执行文件的行为进行简单的梳理: 1. 第一条指令从`nexus-am/am/arch/x86-nemu/img/boot/start.S`开始, 设置好栈顶之后就跳转到`nexus-am/am/arch/x86-nemu/src/trm.c`的`_trm_init()`函数处执行. 1. 在`_trm_init()`中调用`main()`函数执行程序的主体功能. 1. 从`main()`函数返回后, 调用`_halt()`结束运行.

阅读`nexus-am/am/arch/x86-nemu/src/trm.c`中的代码, 你会发现只需要实现很少的API就可以支撑起程序在TRM上运行了:

* `_Area _heap`结构用于指示堆区的起始和末尾
* `void _putc(char ch)`用于输出一个字符
* `void _halt(int code)`用于结束程序的运行
* `void _trm_init()`用于进行TRM相关的初始化工作

这是因为, TRM所需要的指令集和内存已经被编译器考虑进去了: 编译器认为, 硬件需要提供具体的指令集实现和可用的内存, 编译生成的程序里面只需要包含"使用的指令"和"程序的内存映象"这两方面的信息, 程序就可以在硬件上运行了, 所以我们不需要在`trm.c`里面提供"使用指令集"和"使用内存"的API. 关于AM定义的API, 可以阅读`nexus-am/README.md`和`nexus-am/SPEC.md`.

把`_putc()`作为TRM的API是一个很有趣的考虑, 我们在不久的将来再讨论它, 目前我们暂不打算运行需要调用`_putc()`的程序.

最后来看看`_halt()`. `_halt()`里面是一条[内联汇编](http://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html)语句, 内联汇编语句允许我们在C代码中嵌入汇编语句. 这条指令和我们常见的汇编指令不一样(例如`movl $1, %eax`), 它是直接通过指令的编码给出的, 它只有一个字节, 就是`0xd6`. 如果你在`nemu/src/cpu/exec/exec.c`中查看`opcode_table`, 你会发现, 这条指令正是那条特殊的`nemu_trap`! 这其实也说明了为什么要通过编码来给出这条指令, 如果你使用以下方式来给出指令, 汇编器将会报错:

```c
asm volatile("nemu_trap" : : "a" (0))
```

因为这条特殊的指令是我们人为添加的, 标准的汇编器并不能识别它. 如果你查看objdump的反汇编结果, 你会看到`nemu_trap`指令被标识为`(bad)`, 原因是类似的: objdump并不能识别我们人为添加的`nemu_trap`指令. `"a"(0)`表示在执行内联汇编语句给出的汇编代码之前, 先将`0`读入`%eax`寄存器. 这样, 这段汇编代码的功能就和`nemu/src/cpu/exec/special.c`中的helper函数`nemu_trap()`对应起来了. 此外, `volatile`是C语言的一个关键字, 如果你想了解关于`volatile`的更多信息, 请查阅相关资料.

## 运行更多的程序

未测试代码永远是错的, 你需要足够多的测试用例来测试你的NEMU. 我们在`nexus-am/tests/cputest`目录下准备了一些测试用例. 首先我们让AM项目上的程序默认编译到`x86-nemu`的AM中:

```diff
--- nexus-am/Makefile.check
+++ nexus-am/Makefile.check
@@ -7,2 +7,2 @@
-ARCH ?= native
+ARCH ?= x86-nemu
 ARCH = $(shell ls $(AM_HOME)/am/arch/)
```

然后在`nexus-am/tests/cputest/`目录下执行

```bash
make ALL=xxx run
```

其中`xxx`为测试用例的名称(不包含`.c`后缀).

上述`make run`的命令最终会调用`nexus-am/am/arch/x86-nemu/img/run`来启动NEMU. 为了使用GDB来调试NEMU, 你需要修改这一`run`脚本的内容:

```diff
--- nexus-am/am/arch/x86-nemu/img/run
+++ nexus-am/am/arch/x86-nemu/img/run
@@ -3,1 +3,1 @@
-make -C $NEMU_HOME run ARGS="-l `dirname $1`/nemu-log.txt $1.bin"
+make -C $NEMU_HOME gdb ARGS="-l `dirname $1`/nemu-log.txt $1.bin"
```

然后再执行上述`make run`的命令即可. 无需GDB调试时, 可将上述`run`脚本改回来.
