计算机要素--第八章 虚拟机II:程序控制

计算机系统要素,从零开始构建现代计算机(nand2tetris)
如果完成了本书所有的项目 你将会获得以下成就

  • 构建出一台计算机(在模拟器上运行)
  • 实现一门语言和相应的语言标准库
  • 实现一个简单的编译器

而且,这本书的门槛非常低,只要你能熟练运用一门编程语言即可。本课程综合了数字电路,计算机组成原理,计算机体系架构,操作系统,编译原理,数据结构等的主要内容,搭建了计算机平台的构建的框架,并未深入细节,如果需要了解细节,可由本书作为主线,逐步完善的知识体系。

QQ交流群(含资料):39014053
课程连接
项目地址Github


本章要实现的内容

  • 程序控制流命令
  • 子程序调用命令

详细内容

第七章介绍算术表达式和布尔表达式是如何利用基本堆栈操作来进行计算的。本章将继续介绍这个简单的数据结构是如何支持像嵌套子程序调用、参数传递、递归和内存分配技术这样的复杂任务。

底层细节

对于子程序调用,底层必须处理的一些细节。这些细节都可以利用堆栈来实现。如下:

  • 将参数从调用者传递给被调用者。(参数传递)
  • 在跳转并执行被调用者之前,先保存调用者的状态。(现场保护)
  • 为被调用者使用的局部变量分配内存空间。(内存分配)
  • 跳转并执行被调用者。(子程序执行)
  • 将被调用者的运行结果返回给调用者。(参数传递)
  • 在从被调用者返回之前,回收其使用的内存空空间。
  • 恢复调用者的状态。(现场恢复)
  • 返回到调用语句之后的下一条语句继续执行。
程序控制流

主要有两种,无条件跳转和有条件跳转,在Hack计算机平台提供的汇编语言中,提供了一些条件跳转和无条件跳转指令,利用这些指令和L-Command,这部分是很容易实现的。

子程序调用

子程序调用主要包含两种,调用内置指令和调用用户自己定义的子程序。调用内置指令,比如:add,sub等,在第7章都已经实现。调用用户自己定义的子程序与调用内置指令的区别在于需要使用call关键字。

如何实现嵌套调用和递归调用的内存管理机制?
主程序会调用子程序,子程序还会调用子程序,子程序也会调用子程序自身,这就形成了嵌套调用和递归调用。
这里要介绍一个概念:,它表示子程序的局部变量的集合。在这一章中,堆栈是指全局堆栈,它包括所有子程序的帧组成的,包含了第7章中介绍的堆栈,但又大于之前的堆栈。
在调用子程序时,需要先将call xxx命令的下一个命令的地址保存起来,作为子程序返回地址,然后将调用者的帧保存到堆栈中,这就完成了现场保护。然后调用子程序,为子程序分配堆栈空间,子程序的入口地址用xxx标记指出。当执行完子程序之后,就将被调用者的帧销毁,这样就会回到调用者的帧,这就实现了现场恢复。而被调用者可以通过返回地址回到调用者的入口地址,完成调用返回。其中的参数传递都是通过栈顶完成的。

具体而言,如何实现之前介绍的八个细节?
执行一个函数就需要将与该函数相关的local段,argument段,this段,that段的基址加载到RAM[1-4]中,这都是与函数状态相关的参数。另外还需要开辟该函数的工作栈,一般从栈顶开始的单元都可以供当前函数作为工作栈使用。

  1. 在调用函数之前,我们首先要进行传递参数和现场保护。
    传递参数是很容易实现的,具体来讲我们要记录的参数值和参数的个数,当调用函数后,被调用者根据参数的个数来计算参数的基址并保存在ARG单元中,通过基址和偏移量去参数段获取参数。这就完成了参数传递。
    现场保护主要是保存5个单元的数据:returnAddress,local,argument,this,that。returnAddress可以通过设置标识来实现,其他的都是将数据从RAM[0-4]中推入栈,这就完成了现场保护。 这5个单元的数据组成的就是帧。
  2. 当调用函数时,会执行VM命令:call functionName nArgs
    执行函数时,首先需要完成与本函数相关的local段,argument段,this段,that段的设置,以及工作栈的开辟。当执行call命令后,被调用函数会根据nArgs知道传递的参数的个数,argument段的基址可以根据此公式计算出来:ARG=SP-5-nArgs。而LCL=SP。this段和that段暂时还未使用到。然后根据调用的函数名调转到指定的地址即可。Hack汇编程序提供了L-Command,利用L-Command很容易实现。这就完成了程序跳转,示意图如下:
    在这里插入图片描述
  3. 进入函数执行程序:function functionName nVars
    进入子程序后,首先遇到的就是函数名的声明,程序根据nVars知道本函数需要开辟多大的局部变量区,实际操作可以通过重复执行nVars次push 0来实现,或者直接执行SP=SP+nVars,这样没有对局部变量区进行初始化,但一般来说效果是差不多的,不推荐使用后者。在这个过程SP会发生变化,LCL的值不会发生变化。**这样就完成了局部变量区的开辟,**从栈顶开始之后的栈空间都可以当做工作栈使用。当完成子函数的执行后,位于栈顶的就是返回值,从实现机制上来看,只支持一个返回值。返回之后,调用函数从栈顶可以很容易的取到返回值,这就完成了返回值的传递,示意图如下:
    在这里插入图片描述
  4. 程序返回:return
    首先需要得到返回地址,然后将返回值复制到argument 0中。从前面可以知道LCL是本函数相关的数据看是的地方,是调用者相关的数据结束的地方,因此我们可以用此公式来得到返回地址:endFrame=*(LCL),retAddr=*(endFrame-5)。此时栈顶就是返回值,因此pop argument即可完成返回值的传递。然后将栈顶设置在argument 0之后的单元,这一步实际上就限制了返回值只能有一个,并且完成了对内存的回收,因为调用者的帧及工作栈已经没有指针指向,变成不可操作的了。 虽然是这样,但实际上我们在之前的endFrame计算时,要对endFrame和retAddr进行临时保存,因为通过endFrame我们需要进行现场恢复,原理如下:THAT=*(endFrame-1),THIS=*(endFrame-2),ARG=*(endFrame-3),LCL=*(endFrame-5)这就完成了现场恢复。然后有retAddr跳转到下一步要执行的程序处。
    在这里插入图片描述

在图片的旁边就是实现的伪码。在之后的部分将给出具体的实现代码。

总结

在写本项目的过程中,除了调小的bug之外,最令人头疼的是符号的分配,以及函数递归调用时各种符号标记。比前面两个项目难度明显上升了许多。

另外,有个不解的地方就是测试文件时,StaticsTestVME.tst设置sp=261,StaticsTest.tst设置sp=256,两个测试文件在SP初值的设置时不同,导致了测试结果出问题。但是从比较文件StaticsTest.cmp上来看,他需要的确实是sp=261,而实际上,sp=261时,测试才会正确。不知是程序逻辑的问题,还是作业的漏洞。。。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值