計算機要素--第八章 虛擬機II:程序控制

計算機系統要素,從零開始構建現代計算機(nand2tetris)
如果完成了本書所有的項目 你將會獲得以下成就

  • 構建出一臺計算機(在模擬器上運行)
  • 實現一門語言和相應的語言標準庫
  • 實現一個簡單的編譯器

而且,這本書的門檻非常低,只要你能熟練運用一門編程語言即可。本課程綜合了數字電路,計算機組成原理,計算機體系架構,操作系統,編譯原理,數據結構等的主要內容,搭建了計算機平臺的構建的框架,並未深入細節,如果需要了解細節,可由本書作爲主線,逐步完善的知識體系。

QQ交流羣(含資料):289682057
課程連接
項目地址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時,測試纔會正確。不知是程序邏輯的問題,還是作業的漏洞。。。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章