基本指令集

你应在本节中完成以下任务
  1. 由于内容较多,务必反复仔细阅读本节内容;
  2. 跟随讲义和思考题的引导阅读项目中和本节相关的源代码,理解并掌握本节大量使用的框架函数(或宏);
  3. 对宏保持高度警惕,多通过手算学习遇到的宏定义;
  4. 实现所有剩余的基本指令;
  5. 实现标志寄存器及基本指令中对标志寄存器的更新;
  6. 跑通所有测试用例;
  7. 回答所有思考题(部分思考题需要在完全阅读完本节内容后回答);

现代指令系统

我们已经成功在 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程序时, 你敲入一条命令(或者点击一下鼠标), 程序就成功运行了, 但这背后其实隐藏着操作系统开发者和库函数开发者的无数汗水. 一个事实是, 应用程序的运行都需要运行时环境的支持, 包括加载, 销毁程序, 以及提供程序运行时的各种动态链接库(你经常使用的库函数就是运行时环境提供的)等. 为了让客户程序在NEMU中运行, 现在轮到你来提供相应的运行时环境的支持了. 不用担心, 由于 NEMU 目前的功能并不完善, 我们必定无法向用户程序提供 GNU/Linux 般的运行时环境.

我们先来讨论一下程序执行究竟需要些什么.

  1. 程序需要有地方存放代码和数据, 于是需要内存;
  2. 程序需要执行, 于是需要 CPU 以及指令集;
  3. 对于需要运行结束的程序, 需要有一种结束运行的方法。

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

什么是 API?

我们经常在各种地方听说过 API 这个词语,那么你能解释一下什么是 API 吗?如果你没有听说过,可以在网络上搜索相关信息并回答这个问题。

什么是AM?

你或许会觉得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 的概念有所理解了,那么请你思考一下,AM 属于硬件还是软件呢?经过 PA1,我们了解到 NEMU 的实质是模拟硬件。另外,我们都知道,操作系统(OS)是直接运行在硬件之上的,是一个软件。那么 AM 和你认为的操作系统是否一样?如果一样,请说明他们的共同点;如果不一样,请说明他们之间的区别。

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/testsnexus-am/apps - 一些功能测试和直接运行AM上的应用程序
  • nexus-am/libs - 一些体系结构无关的, 可以直接运行在 AM 上的库, 方便应用程序的开发

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

解决这个问题的方法是交叉编译, 我们需要在 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() 函数处执行.
  2. _trm_init() 中调用 main() 函数执行程序的主体功能.
  3. 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.mdnexus-am/SPEC.md.

堆和栈在哪里?

我们知道代码和数据都在可执行文件里面, 但却没有提到堆(heap)和栈(stack).为什么堆和栈的内容没有放入可执行文件里面?那程序运行时刻用到的堆和栈又是怎么来的? AM 的代码是否能给你带来一些启发?

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

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

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 中:

--- 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/ 目录下执行

make ALL=xxx run

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

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

--- 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 脚本改回来.

回忆运行过程

我们知道当打入命令

make ARCH=x86-nemu ALL=dummy run

并敲击回车后,NEMU 将被编译并载入 dummy 这个用户程序进入运行,那么请你根据 Makefile 和上文的介绍思考一下,从你输入完这条命令敲击回车,直到出现 NEMU 的提示符 (nemu) 之间,都做了哪些工作?请你用条目式依次列举出来。

任务1.1:实现更多的指令

你需要实现上文中提到的更多指令, 以通过上述测试用例. 请结合下列几个并列的任务一起实现本部分任务。

你可以自由选择按照什么顺序来实现指令. 经过 PA1 的训练之后, 你应该不会实现所有指令之后才进行测试了. 要养成尽早做测试的好习惯, 一般原则都是"实现尽可能少的指令来进行下一次的测试". 你不需要实现所有指令的所有形式, 只需要通过这些测试即可. 如果将来仍然遇到了未实现的指令, 就到时候再实现它们.

需要注意的是, push imm8 指令需要对立即数进行符号扩展, 这一点在 i386 手册中并没有明确说明. 在 IA-32 手册中关于 push 指令有如下说明:

If the source operand is an immediate and its size is less than the operand size, a sign-extended value is pushed on the stack.

由于部分测试用例需要实现较多指令, 建议按照以下顺序进行测试:

  1. 其它所有测试用例
  2. string
  3. hello-str

任务 1.2:启用已实现指令

框架中已经实现了部分指令,但是没有将它们填入指令表中,部分需要设置标志位的指令也没有做设置标志位的操作,请你在逐个通过测试样例的过程中,将它们补充完整。

任务 1.3:逐个通过测试样例

正常来说,你应该按照如下思路来实现剩余的所有指令:

  • 从未测试的测试用例中选取一个新的测试用例;
  • 将该测试用例送入 NEMU 中运行;
  • 运行过程中,自前往后逐个实现未实现的指令(或已实现指令中未实现的形式);
  • 重复如上步骤直至所有测试用例都能够正常运行。

需要注意的是,你需要在使用这些测试用例的过程中,确保你的指令实现能够通过 diff-test 的测试,以免在今后出现意外的问题。

神奇的eflags(2)

当你实现了 jcc 指令之后,是不是对 eflags 的作用有了新的认识呢?对的,eflags 在为指令跳转提供判断依据,希望你能回答如下问题来加深你对跳转指令的理解。

cmp 指令是用减法实现的,与 sub 指令的区别是 cmp 指令只改变 eflags,不会存结果。这里我们用减法来表示两个数比较的过程。 例如 cmp 1,2可以用 1-2 来表示。

OFSF 结合可以比较有符号数的大小,请根据样例补充下表,补充完成之后比较一下两个数的大小,你会发现eflags的神奇之处:

+-----+-----+------------------------------------+
| SF  |  OF |                   实例                  |
+-----+-----+------------------------------------+
|  0  |  0  |               2 - 1                |
+-----+-----+------------------------------------+
|  0  |  1  |                                    |
+-----+-----+------------------------------------+
|  1  |  0  |                                    |
+-----+-----+------------------------------------+
|  1  |  1  |                                    |
+-----+-----+------------------------------------+

这是巧合吗?

观察 i386 手册中关于 jcc 的指令集细节,我们发现 jcc 指令有许多种形式,例如 ja, jb, jbe 等等……从手册上我们也知道 jajump above,或者 jbe 意为 jump below or equal

此外,我们还知道 cmp 指令的形式为:cmp op1, op2。那么请你思考一下,op1, op2之间的大小关系,和 jcc 指令中的 above, below, greater, less 这些大小关系词之间有什么关联呢?

一键回归测试

在实现指令的过程中, 你不仅需要调试, 你还需要逐个测试用例地运行. 但在指令实现正确之后, 是不是意味着可以和这些测试用例说再见呢? 显然不是. 以后你还需要在 NEMU 中加入新的功能, 为了保证加入的新功能没有影响到已有功能的实现, 你还需要重新运行这些测试用例. 在软件测试中, 这个过程称为回归测试.

既然将来还要重复运行这些测试用例, 而手动重新运行每一个测试显然是一种效率低下的做法. 为了提高效率, 我们提供了一个用于一键回归测试的脚本. 在 nemu/ 目录下运行

bash runall.sh

来自动批量运行 nexus-am/tests/cputest/ 中的所有测试, 并报告每个测试用例的运行结果. 如果一个测试用例运行失败, 脚本将会保留相应的日志文件; 当使用脚本通过这个测试用例的时候, 日志文件将会被移除.

任务 2:通过一键回归测试

也许你在尝试跑通下个测试用例的时候修改了某些指令的实现代码,而你却并不知情。而这些修改可能导致之前本来可以正常运行的测试用例出现了问题,因此你需要使用框架提供的一键回归测试脚本,将所有测试用例均成功运行。当你成功运行的时候,将看到满屏的 PASS!!,这时候你就可以放松一下了。

NEMU的本质

你已经知道, NEMU 是一个用来执行其它程序的程序. 在可计算理论中, 这种程序有一个专门的名词, 叫通用程序(Universal Program), 它的通俗含义是: 其它程序能做的事情, 它也能做. 通用程序的存在性有专门的证明, 我们在这里不做深究, 但是, 我们可以写出 NEMU, 可以用 Docker / 虚拟机做实验, 乃至我们可以在计算机上做各种各样的事情, 其背后都蕴含着通用程序的思想: NEMU 和各种模拟器只不过是通用程序的实例化, 我们也可以毫不夸张地说, 计算机就是一个通用程序的实体化. 通用程序的存在性为计算机的出现奠定了理论基础, 是可计算理论中一个极其重要的结论, 如果通用程序的存在性得不到证明, 我们就没办法放心地使用计算机, 同时也不能义正辞严地说"机器永远是对的".

我们编写的 NEMU 最终会被编译成 x86 机器代码, 用 x86 指令来模拟 x86 程序的执行. 事实上在 30 多年前(1983年), Martin Davis教授 就在他出版的"Computability, complexity, and languages: fundamentals of theoretical computer science"一书中提出了一种仅有三种指令的程序设计语言 L 语言, 并且证明了 L 语言和其它所有编程语言的计算能力等价. L语言中的三种指令分别是:

V = V + 1
V = V - 1
IF V != 0 GOTO LABEL

用x86指令来描述, 就是inc, decjne三条指令. 假设除了输入变量之外, 其它变量的初值都是0, 并且假设程序执行到最后一条指令就结束, 你可以仅用这三种指令写一个计算两个正整数相加的程序吗? 试试下面这个小题目:

    # Assume a = 0, x and y are initialized with some positive integers.
    # Other temporary variables are initialized with 0.
    # Let "jne" carries a variable: jne v, label.
    # It means "jump to label if v != 0".
    # Compute a = x + y used only these three instructions: inc, dec, jnz.
    # No other instructions can be used.
    # The result should be stored in variable "a".
    # Have a try?

令人更惊讶的是, Martin Davis 教授还证明了, 在不考虑物理限制的情况下(认为内存容量无限多, 每一个内存单元都可以存放任意大的数), 用L语言也可以编写出一个和 NEMU 类似的通用程序! 而且这个用 L 语言编写的通用程序的框架, 竟然还和 NEMU 中的 cpu_exec() 函数如出一辙: 取指, 译码, 执行... 这其实并不是巧合, 而是模拟(Simulation)在计算机科学中的应用.

早在 Martin Davis 教授提出L语言之前, 科学家们就已经在探索什么问题是可以计算的了. 回溯到19世纪30年代, 为了试图回答这个问题, 不同的科学家提出并研究了不同的计算模型, 包括Gödel, HerbrandKleen研究的递归函数, Church提出的λ-演算, Turing提出的图灵机, 后来发现这些模型在计算能力上都是等价的; 到了40年代, 计算机就被制造出来了. 后来甚至还有人证明了, 如果使用无穷多个算盘拼接起来进行计算, 其计算能力和图灵机等价! 我们可以从中得出一个推论, 通用程序在不同的计算模型中有不同的表现形式. NEMU作为一个通用程序, 在19世纪30年代有着非凡的意义. 如果你能在80年前设计出NEMU, 说不定"图灵奖"就要用你的名字来命名了. 计算的极限这一篇科普文章叙述了可计算理论的发展过程, 我们强烈建议你阅读它, 体会人类的文明(当然一些数学功底还是需要的). 如果你对可计算理论感兴趣, 可以选修宋方敏老师的计算理论导引课程.

把思绪回归到PA中, 通用程序的性质告诉我们, NEMU的潜力是无穷的. 为了创造出一个缤纷多彩的世界, 你觉得NEMU还缺少些什么呢?

选做任务:捕捉死循环(有点难度)

NEMU除了作为模拟器之外, 还具有简单的调试功能, 可以设置断点, 查看程序状态. 如果让你为 NEMU 添加如下功能:当用户程序陷入死循环时, 让用户程序暂停下来, 并输出相应的提示信息

你觉得应该如何实现? 如果你感到疑惑, 在互联网上搜索相关信息. 如果可以,期望看到你能在 NEMU 中添加一定代码将本功能实现。


以上是 PA2.2 的所有内容.

results matching ""

    No results matching ""