日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > windows >内容正文

windows

Forth 系统实现

發布時間:2023/12/18 windows 30 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Forth 系统实现 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

Forth 系統實現

原作者 Brad Rodriguez

編譯者 趙宇 張文翠

本文編譯自 Brad Rodriguez 《 Moving Forth 》,原文首次在 The Computer Journal #59 (January/February 1993) 上發表,現在可在下面網站上取得

http://www.zetetics.com/bj/papers/index.html

本文對 Forth 語言在各種處理器上的各種實現方式進行了深入的探討,盡管其中作為例子的處理器非常古老,但對于理解 Forth 系統仍有著很大的參考價值。譯文按原文結構翻譯,省略了各部分的參考文獻,其內容和本文中所列出的源程序代碼都可以在上述網站上得到。

目錄

第一部分 Forth 內核的設計決策

第二部分 內核基準測試和個案研究

第三部分 解密 DOES>

第四部分 匯編器還是 META 編譯器

第五部分 Z80 原語

第六部分 Z80 高級內核

第七部分 8051 的 CamelForth

第八部分 MC6809 CamelForth

第一部分 Forth 內核的設計決策

前言

每一個進入 Forth 圈里的人都說或者聽說“把 Forth 移植到一個新 CPU 上是一件易如反掌的事情”。不過,就像其它許多“易如反掌”的事情一樣,卻沒有多少書面的資料告訴我們如何去做!所以,當 Bill Kibler 建議這個論文題目時,我決定打破 Forth 編寫者只說不練的傳統,給出一個白紙黑字的 Forth 實現 , 包括為 MC6809 、 Intel 8051 和 Z80 實現的 Forth 系統。

在整個文檔中,我準備為 MC6809 、 Intel 8051 和 Zilog Z80 實現 Forth 系統。我會用 MC6809 來解釋一個簡單的和傳統的 Forth 模型,此外,我還將公布一個 MC6809 匯編器,把 6809 Forth 用于未來的 TCJ 計劃,把 8051 作為一個大學項目,其中也解釋了一些非常不同的決策。 Z80 Forth 是為所有的 TCJ CP/M 讀者和許多 TRS-80 的老朋友而編寫的。

有效的硬件

首先,我們必須選擇一個 CPU 。不過,我不想陷入“Forth 運行在這種 CPU 上比運行在那種 CPU 上更有效”的爭論中,因為 CPU 的選擇通常還需要考慮其它因素,并且這篇論文的目標之一就是想說明如何把 Forth 搬到任何一個 CPU 上。

通常, 16 位 Forth 內核需要 8K 字節的程序空間。對于一個能夠真正用來編譯 Forth 語言應用的完整內核來說,應該至少有 1K 字節的 RAM 。如果想使用 Forth 的磁盤存儲器塊管理功能,還應該再增加 3K 字節以上的 RAM 用于緩沖區。對于 32 位系統,這些數值都需要加倍。

以上這些是一個 Forth 內核能夠運行的最小要求。為了在硬件上運行應用程序,你還得按實際需要另外增加 PROM 和 RAM 的大小。

使用 16 位還是 32 位系統

實際的系統并不要求 Forth 的字長度與 CPU 的字長度一致。最小的、實際可用的 Forth 系統都使用 16 位模型,也就是說使用 16 位的整數和 16 位的地址。 Forth 的術語把這種尺寸稱為單元(CELL)而不是我們常說的“字”,因為“字”在 Forth 中是指一個 Forth 定義(可以簡單地理解成其它高級語言的子程序名)。

所有的 8 位 CPU 幾乎都不加改變地支持 16 位的 Forth ,為此,要求編碼進行雙字節算術運算,盡管某些 8 位 CPU 也能夠直接支持其中的一些操作。

有一些技術可以在一個16位的機器上寫出 32 位的 Forth ,但是 16 位的 CPU 通常運行 16 位的 Forth ,雖然我們也看到 32 位的 Forth 可以運行在 Intel 8086/8088 上。

32 位的 CPU 通常運行 32 位的 Forth 。實際應用中,一個更小的模型幾乎不能節省代碼空間和處理器時間。但我也見到過為 MC68000 編寫的 16 位 Forth 。這個系統的代碼長度縮小了 2 倍,因為高級 Forth 定義變成了 16 位的地址串而不再使用 32 位的地址串。不過大多數的 MC68000 系統都有很多 RAM ,好象沒有必要進行這樣的努力。

本文描述的所有例子都是運行在 8 位 CPU 上的 16 位 Forth 系統。

串線編碼技術

“串線編碼”是 Forth 的標志。一個 Forth “串線”就是被執行的子程序地址的列表。你可以把它們想象成一連串省略了 CALL 指令的子程序調用表。長期以來,人們發明了多種串線形式,為了作出選擇,你必須理解所有這些串線形式是如何工作的,以及它們各自的優缺點。

間接串線編碼( ITC )

間接串線編碼(ITC)技術是一種經典的 Forth 串線編碼技術,最早出現在 FIG-Forth 和 F83 系統中,并且在許多關于 Forth 的書中都有描述。后來的串線方式都是直接串線編碼方式的“發展”,所以,你需要先理解這個技術。

讓我們看一個 Forth 字 SQUARE 的定義 :

: SQUARE DUP * ;

在一個典型的 ITC Forth 中,該定義在存儲器中的情況如圖 1 所示(首部將在以后討論,它保存編譯信息,但并不在串線中訪問)

圖 1 ITC Forth 定義的存儲器

假設在執行某個 Forth 字的時候遇到了字 SQUARE , Forth 的解釋指針 IP 將指向存儲器的一個單元,其中包含字 SQUARE 的地址,當然更嚴格地應該說,這個單元包含著 SQUARE 的“代碼域地址”。解釋器讀出這個地址并用這個地址讀出 SQUARE 的代碼域內容。它還是一個地址 -- 這個地址是一個機器語言子程序,由這個子程序來執行定義 SQUARE 。

我們可以把上面的描述通過偽碼表示如下:

(IP)-> W 讀取 IP 指向的存儲器內容到 W 寄存器, W 現在有代碼域的地址;

IP+2->IP 增量 IP, 它就像一個程序計數器,而且假設串線中的地址是 2 個字節長;

(W) -> X 讀取由 W 指向的存儲器內容到 X 寄存器, X 現在指向機器碼地址;

JP (X) 跳到 X 寄存器指向的地址執行;

這里解釋了一個重要的、但卻很少有人說明的原理:進入的 Forth 字的當前地址保存在 W 寄存器中。 CODE 字不需要這個信息,但是其它類型的 Forth 字確實需要其中的信息。

如果 SQUARE 是由機器代碼寫成的,事情也就結束了:這些機器代碼被執行,然后跳回到 Forth 解釋器 -- 由于 IP 已經增量,它將指向下一個將被執行的字。所以 Forth 解釋器通常被稱為 NEXT 。

可是, SQUARE 是一個高級的“冒號”定義 -- 它保持一個“串線”,或者說一個地址列表。為了執行這個定義, Forth 解釋器必須在一個新的位置上重新啟動,這個位置就是 SQUARE 的參數域。當然,解釋器必須保存舊的位置,以便 SQUARE 結束之后能夠恢復“另一個” Forth 字。這實際上與一個子程序調用沒有任何區別!

SQUARE 機器語言的動作就是簡單地把舊的 IP 值保存到堆棧上,并把 IP 指向新的位置,執行解釋器,當 SQUARE 完成后彈出恢復 IP 。(正如你看到的, IP 就是 Forth 高級定義的“程序計數器”),這個過程在不同的 Forth 版本中可能被稱為 DOCOLON 或者 ENTER :

PUSH IP 壓入“返回地址?!?;

W+2 -> IP W 已經指向代碼域,所以 W+2 就是定義體的地址! ( 假設是 2 字節的地址,不同的 Forth 可能不同 )

JUMP NEXT 到解釋器 ( “ NEXT ” )

這樣的一段代碼被用于所有的高級(串線) Forth 定義!于是我們回答了兩個問題:

?? 為什么在 Forth 定義中用一個指針指向代碼段,而不是把代碼段本身直接嵌入到定義中。因為如果有數以百計的定義,就可以節省大量的空間;

?? 為什么這種方式被稱為“間接串線編碼”;

“從子程序返回”動作由字 EXIT 完成,它被 Forth 的分號“;”編譯進定義中(有些 Forth 系統使用 ;S 替代 EXIT)。 EXIT 執行下列的機器語言:

POP IP 從“返回地址?!睆棾鲋羔?

JUMP interpreter 跳轉到解釋器

注意 ITC 的特點:每個 Forth 字都有一個單元的代碼域,冒號定義給定義中的每一個字編譯一個單元。 Forth 解釋器為了執行機器代碼,必須實際執行兩次間接才能取得下一個機器碼的地址(首先通過 IP ,然后通過 W)。

ITC 既不是代碼尺寸最小的、也不是執行速度最快的串線技術。它可能只是最簡單的技術,盡管下面討論的另一種技術DTC 實際上也不是特別復雜。那么為什么有這么多的 Forth 系統都使用間接串線技術呢?主要是由于以前作為原始模型的 Forth 系統都使用間接串線技術,而現在, DTC 技術卻用得最多。

那么什么時候應該使用 ITC 技術呢?很明顯, ITC 形式能夠產生最純凈和最一致的定義:其中只有一種類型,這種類型就是地址。如果你正好就有這樣的需要,那 ITC 技術就是適合的。如果你的代碼關注定義的內部, ITC 技術的簡單性和單一性還能夠增加可移植性。

此外, ITC 是經典的 Forth 模型,它可以非常好地用于教學。

最后,在某些缺少子程序調用指令的早期 CPU 上 -- 比如 1802 -- ITC 常常比 DTC 更有效。

直接串線編碼( DTC )

直接串線編碼(DTC)技術與 ITC 技術的差別只有一點:不像 ITC 在代碼域中包含機器碼的地址, DTC 的代碼域中包含有實際的機器代碼本身。

注意,我并不是說在每個冒號定義中都包含全部的 ENTER 代碼。我的意思是:在“高級” Forth 字中,如圖 2 所示,代碼字段有一個子程序調用指令。例如,冒號定義中將包含一個對 ENTER 子程序的調用。

圖 2 DTC Forth 定義的存儲

直接串線的 NEXT 偽代碼非常簡單:

(IP) -> W 取 IP 指針指向的存儲器內容到 W 寄存器中

IP+2 -> IP 增量 IP ( 假設 2 字節的地址 )

JP (W) 跳轉到 W 寄存器指向的地址執行

DTC 的收益就是速度:解釋程序現在只需要執行一次間接。在 Z80 上,這實際是把 NEXT 子程序 --Forth 內核中最常用的代碼段 -- 從 11 個指令減少到了 7 個指令。

DTC 的成本是空間:在一個 Z80 Forth 中,每個高級定義都將增加一個字節的長度,因為 ITC 中 2 個字節的地址現在被 3 個字節的 CALL 調用指令所取代。當然這個結論也不是廣泛適用的,在 32 位的 MC68000 Forth 中,可以用 4 字節的 BSR 指令代替 4 字節的地址,其中沒有任何差異。而在 Zilog 的 SUPER8 中,有一個直接用于 Forth DTC 的指令,它用一個字節的 ENTER 指令代替 2 字節的地址,使得在 SUPER8 上, DTC Forth 比 ITC Forth 的代碼還要小。

當然 DTC 的 CODE 定義也縮短了 2 個字節,因為它們不再需要指針。

我一直以為 DTC Forth 的高級定義字必須在代碼域中使用子程序調用指令, Frank Sergeant的 Pygmy Forth [SER90] 提出可以使用更簡單的跳轉指令,這樣更容易實現,通常也更快。

Guy Kelly 對 IBM PC 上實現的 Forth 系統進行了很好的總結,這也是我對所有 Forth 編寫者的建議。

在他研究的 19 個 Forth 實現中,有 10 個使用了 DTC 技術, 7 個使用了 ITC 技術, 2 個使用了子程序串線技術(這種技術我們將在下面討論)。所以,我認為所有新實現的 Forth 內核都應該使用直接串線技術,而不要再使用間接串線技術了。

跳轉到 NEXT 還是對 NEXT 使用內嵌編碼?

Forth 的內層解釋器 NEXT 是一個用于所有 CODE 定義的通用子程序。你可以編寫一個子程序,然后讓所有的 CODE 字跳轉到這個子程序上執行(注意:跳轉到 NEXT 而不必通過子程序調用到 NEXT )。

然而, NEXT 的速度對于整個 Forth 系統的速度來說是至關重要的,從這個角度考慮, NEXT 最好是內嵌代碼,于是 NEXT 也可以被定義成一個匯編的宏。

這就是一個常見的速度 / 空間折衷問題:內嵌的 NEXT 總是更快,但也總是更大。全部增加的尺寸數量是內嵌擴展需要的字節數乘以系統中 CODE 字的數量。當然有時也根本不需要考慮折衷:在 MC6809 中,內嵌的 NEXT 總是比一個 JUMP 指令還要短!

子程序串線( STC )

一個高級的 Forth 定義字只不過是“要執行的子程序的列表”,并不一定要通過解釋才能實現它們,你也可以通過簡單地調用一系列子程序而得到同樣的效果:

SQUARE:

CALL DUP

CALL * ; 或者是一個合適的名字,因為有些匯編器不支持把 * 作為子程序名

RET

圖 3 為匯編程序員解釋了 Forth 的 STC 串線技術。 [KOG82].

圖 3 DTC Forth 定義的存儲

STC 有一個統一的表示方式,冒號定義和 CODE 字沒有區別,“定義字”(這是 Forth 的專用術語,像 VARIABLE 、 CONSTANT 這樣一些可以用來定義新字的字被稱為定義字)像 DTC一樣處理 -- 代碼域用一個跳轉或者調用指令轉到其它地方的機器碼。

STC 的一個主要缺點是:子程序調用指令通常比簡單的地址列表址大。比如在 Z80 上,冒號定義的尺寸將增大 50% -- 而你的應用中大部分都是冒號定義。相比在32位的 MC68000 上,如果使用 4 字節的 BSR 代替 4 字節地址,代碼尺寸沒有任何增加,不過,如果你的代碼超過了64K ,一些地址就必須用 6 字節的 JSR 代替。

子程序串線可能比直接串線更快。在 STC 中節省了解釋器執行的時間,但必須花費 Forth 字用于返回的 PUSH 、 POP 時間。而在 DTC Forth 中,只有高級定義才引起返回棧動作,在 MC6809 或者 Zilog SUPER8 中, DTC 比 STC 更快。

STC 還有一個優點:它不需要 IP 寄存器。有些處理器 -- 像 Intel8051 -- 缺少地址寄存器,沒有虛擬機 IP 寄存器可以真正地簡化內核并提高速度。

STC 的內嵌擴展、優化、直接編譯

在一些古老的 8 位 CPU 上,幾乎每個 Forth 原語都需要用幾個機器指令才能實現,但是在更強大的 CPU 上,有時 Forth 原語只需要一個機器指令。例如,在一個 32 位的 MC68000 上, DROP 可以簡化為:

ADDQ #4,An 這里 An 是 Forth 的 PSP 參數棧寄存器

在一個子程序串線的 Forth 中,冒號定義中使用 DROP 將產生這樣的序列:

BSR ...

BSR DROP ...

DROP:

ADDQ #4,An

BSR ...

RTS

ADDQ 本來是一個 2 字節指令,我們為什么要寫一個對這個 2 字節指令的 4 字節子程序調用呢?在這種情況下,不論有多少個 DROP ,通過子程序調用都不會產生任何的節省。而如果把 ADDQ 直接編碼到 BSR 流中,產生的代碼都會更小,運行得更快。有些 Forth 編譯程序已經實現了這樣的 CODE 字“內嵌擴展” [CUR93a] 。

內嵌擴展的缺點是:如果要把代碼反編譯回原始的代碼就會非常困難。如果僅僅是使用子程序串線,我們依然可以得到指向 Forth 字的指針(子程序的地址)。通過字指針,就可以得到它們的名字。但是如果指令字擴展到內嵌編碼中,所有的關于字來源的信息就全部丟失了。

除了速度和空間之外,內嵌擴展還有個優點:潛在的代碼優化。例如: Forth 序列:

3 +

在 68000 STC 被編譯成:

BSR LIT

.DW 3

BSR PLUS

但是,使用內嵌代碼,就可以把它優化成一個機器指令。

Forth 編譯器優化是一個廣闊的領域,也是 Forth 語言研究中一個非?;钴S的領域,這里不能完全討論,可參見 [SCO89] 和 [CUR93b] 。優化 STC 的最終結果是能夠產生“純”機器代碼的 Forth 編譯器,就像 C 或者 Fortran 編譯器一樣。

標記串線編碼( TTC )

DTC 和 STC 技術的目標是用一定的存儲器消耗為代價來增加 Forth 程序的執行速度?,F在讓我們轉向 ITC 的另一個方向:運行速度更慢、但代碼尺寸更小。

Forth 串線的目的是指定一系列將要執行的 Forth 字(子程序)的地址。假設一個 16 位的 Forth 字最大只有 256 個 Forth 字,那么每個 Forth 字都可以用一個 8 位數來標識,我們就可以不使用 16 位的地址列表,而是用一系列的 8 位標識或者稱為“標記( TOKEN )”來代替地址,這樣冒號定義的代碼尺寸就減少了一半。

在一個標記串線編碼的 Forth 系統中,需要有一個記錄所有 Forth 字的表格,如圖 4 所示。標記值就是這個表項的索引,通過它來尋找一個指定標記對應的 Forth 字。這種方法為 Forth 解釋器增加了一次間接訪問,所以它比“地址串線”的 Forth 執行速度更慢。

圖 4 DTC Forth 定義的存儲

標記串線的基本優點是尺寸很小。 TTC 技術在手持計算機和其它對尺寸要求嚴格的應用中極為常見。同時,使用統一的 Forth 字“入口”表也簡化了分開編譯模塊的鏈接。

TTC 的缺點是:速度慢。 TTC 的 Forth 系統速度是所有技術中最慢的, TTC 編譯器也比其它技術的編譯器更復雜一些。如果你的應用有多于 255 個 Forth 字定義,則還需要一些其它的編碼方式來混合 8 位和更大的標記。

說到 TOKEN 串線,也許會想到的情況是 32 位的 Forth 系統通過 TOKEN 串線而使用 16 位的 Forth 代碼,不過,實際上又有多少 32 位系統是存儲器尺寸受限的呢?

段串線編碼

由于曾經有許多的 Intel 8086 派生系統,我們也簡單地提一下段串線技術。這種技術不再使用一個 64K 段內的“一般”字節地址,而是使用節地址(在 Intel 8086 中,一個節的大小是 16 個字節)。這樣,解釋器可以把這些地址裝入段寄存器,而不是通常的地址寄存器。這就允許 16 位的 Forth 模型可以有效地訪問 8086 的 1M 字節存儲器。

段串線模型的基本缺點是 16 字節大小的存儲器“粒度”,因為這種技術要求每個 Forth 字必須在 16 字節的邊界上對齊,而一個 Forth 字又具有隨機的長度,所以平均每個字要浪費 8 個字節。

寄存器分配

在討論了各種串線技術之后, CPU 寄存器的分配和使用就是至關重要的設計考慮了。這可能也是最困難的。 CPU 寄存器的可用性又會反過來決定我們使用哪種串線技術,甚至決定我們使用哪種方式的存儲器映射。

經典的 Forth 寄存器

經典的 Forth 虛擬機模型有 5 個“虛擬寄存器”。它們是 Forth 原語的抽象實體。 NEXT 、 ENTER 、 EXIT 就是用這些抽象寄存器定義的。

每個寄存器的寬度都是一個單元,也就是說,在 16 位 Forth 系統中,它們都是 16 位寄存器。(以后你會看到,也有一些特例)。它們不一定全部都是 CPU 寄存器,如果你的 CPU 沒有足夠的寄存器,其中一些可以保存在存儲器中。本文將按照這些寄存器的重要性來描述,也就是說,在沒有足夠 CPU 物理寄存器的情況下,最后描述的寄存器應該最先考慮被放置到存儲器中。

W 是工作寄存器 它可以被用來做很多事情。首先, W 寄存器應該是一個地址寄存器,應該能用 W 寄存器作為地址來讀取和寫入存儲器;也需要用 W 寄存器做算術運算。在 DTC Forth 中,還要求能用 W 實現間接跳轉。W 寄存器在每個 Forth 字中被解釋器使用,如果 CPU 只有一個寄存器,那你也必須把這個唯一的寄存器用于W 寄存器 ,而把其它的寄存器放到存儲器中,當然,這種實現會使整個系統慢得令人難以置信。

IP 是解釋指針 它被每個 Forth 字使用(通過 NEXT 、 ENTER 、 EXIT )。 IP 必須是一個地址寄存器,你也需要增量 IP。子程序串線的 Forth 系統不需要這個寄存器。

PSP 是參數棧指針(或者叫數據棧指針) 有時也簡稱作 SP 。我使用 PSP 是由于“SP”通常都是 CPU 硬件寄存器的名字,而它們彼此是不能混淆的。大多數 CODE 字需要使用這個寄存器。 PSP 必須是一個堆棧指針,或者是能夠增量和減量的地址寄存器。如果可以通過 PSP 進行索引尋址則會為系統帶來有一些附加的好處。

RSP 是返回棧指針 有時也簡稱RP。在 ITC 和 DTC 的 Forth 系統中, RSP被冒號定義使用,在 STC 的 Forth 系統中,它被所有的字使用。 RSP 必須是一個堆棧指針,或者是能夠增量和減量的地址寄存器。

如果可能,應該把 W 、 IP 、 PSP 、 RSP 都放到實際的 CPU 物理寄存器中,其它的虛擬寄存器可以保存在存儲器中,當然,如果所有的寄存器都保持在 CPU 硬件寄存器中,將帶來速度方面的好處。

X 寄存器是一個工作寄存器 不過這里并沒有把它作為一個經典的 Forth 寄存器考慮,甚至在使用它作為二次間接的經典 ITC 實現中也沒有被當做經典寄存器。在 ITC 中,必須能夠使用 X 寄存器實現間接跳轉。 X 寄存器也被幾個 CODE 字作為算術運算操作的目的地址,在不能使用存儲器作為操作數的處理器上是特別重要的。比如在 Z80 上,需要通過下面的方式來實現加法運算(用偽碼表示):

POP W

POP X

X+W -> W

PUSH W

有時也定義另外一個寄存器 Y 。

UP 是用戶指針 它保持當前任務的用戶區基地址。 UP 通常的用法是加上一個偏移量后在高級 Forth 定義中使用它。如果 CPU 可以通過 UP 寄存器索引尋址, CODE 字就可以更簡單和更快速地訪問用戶變量。如果你有多余的寄存器,可以用其中一個作為 UP 。單任務的 Forth 不需要 UP 。

如果需要 X ,則 X 應該優先于 UP 放入 CPU 物理寄存器。 UP 是 Forth 虛擬寄存器中最適合放入存儲器的。

硬件堆棧的使用

許多 CPU 把堆棧指針作為硬件的一部分用于中斷和子程序調用。如果把堆棧指針作為 Forth 的一個虛擬寄存器將會怎么樣呢?它應該是 PSP 還是 RSP 呢?

這要根據具體情況來考慮。一般認為在 ITC 和 DTC 的 Forth 中, PSP 的使用比 RSP 更加頻繁,如果你的 CPU 只有不多的寄存器, PUSH 和 POP 就會比顯式地引用存儲器速度更快,所以我們可以使用硬件堆棧作為參數棧。

另一方面,如果你的 CPU 有豐富的尋址方式,特別是允許進行索引尋址,就應該為 PSP 分配一個通用的地址寄存器,在這種情況下,應該使用硬件堆棧作為返回棧。

這里的結論對下面的情況不合適。比如在 TMS320C25 中,硬件堆棧的深度只有 8 個單元,這對于 Forth 系統來說基本上沒有什么用途,所以它的硬件堆棧只能用于中斷, PSP 和 RSP 都必須是通用的地址寄存器。注意 ANS Forth 規范中指定最小的參數棧是 32 個單元,返回棧是 24 個單元,而我選擇的數據棧和返回棧都是 64 個單元。

有時你可能會遇到教條的說法,比如硬件堆?!氨仨毷菂禇!被蛘摺氨仨毷欠祷貤!?。在這種情況下,你可以編寫幾個 Forth 原語比如: SWAP 、 OVER 、 @ 、 ! 、 + 、 0= 來看看哪種情況代碼更小、速度更快。

順便說一下,如果要做這種測試,字 DUP 和 DROP 價值不高。

偶爾你也會得到有趣的結論! Gary Bergstrom 指出在 MC6809 的 DTC 實現中,用 MC6809 的用戶堆棧指針作為 IP 可以快幾個周期,這里 NEXT 變成了 POP 。他使用索引指針作為 Forth 的堆棧指針。

把棧頂元素( TOS )放入寄存器

如果能把參數棧棧頂元素 TOS 放到寄存器中,則 Forth 的性能會得到明顯改善。許多 Forth 字(比如 0= )將不再訪問堆棧,其它的 Forth 字做同樣的 PUSH 和 POP ,只不過在代碼中的位置不同。只有不多的 Forth 字(比如 DROP 和 2DROP )變得比較復雜 -- 你必須同時更新 TOS 的內容。

把棧頂元素放到寄存器中之后,編寫 CODE 字時需要遵循這樣幾個規則:

?? 一個字從堆棧上移出一個項目時,必須彈出“新”的 TOS 到寄存器中;

?? 一個字加入一個新的項目到堆棧上,必須把“舊”的 TOS 壓入棧上 ( 當然,除非它被消耗掉 )

如果你的 CPU 至少有 6 個物理寄存器,我建議你保存 TOS 到其中一個寄存器中。我認為 TOS 比 UP 更重要,但它的重要性又次于 W 、 IP 、 PSP 、 和 RSP 寄存器。 TOS 寄存器執行了許多 X 寄存器的功能,如果這個寄存器可以實現存儲器尋址就更加有用。 PDP-11 、 Z8 、 MC68000 處理器都是很好的例子。

Guy Kelly [KEL92] 研究了 19 個 IBM PC 上的 Forth 系統,其中有 9 個使用了 TOS 寄存器。

我認為, TOS 的想法沒有廣泛被接受的原因首先是下面一些錯誤的見解:

?? 增加了指令;

?? 棧頂元素必須通過存儲器訪問。

?? 過分強調了PICK、ROLL 這些價值不高的字,說它們在 TOS 情況下必須進行重新編碼。

如果把兩個棧頂元素都放到寄存器中,結果會怎么樣呢?當你這樣做的時候,操作效率是相同的。一個 PUSH 仍然是一個 PUSH ,不論你在此前和以后進行了什么操作。另一方面,緩沖兩個堆棧元素卻增加了大量的代碼:一個 PUSH 現在變成了一個 PUSH 后隨一個 MOVE 。把兩個元素緩沖到寄存器中,只有在 RTX2000 一類的 Forth 芯片上才有意義,其它的都是一些假想的、聽起來似乎非常聰明、但在實際應用中沒有什么意義的優化。

實際分配的一些例子

這里是一些不同的 CPU 寄存器分配實例,通過這個表,我們可以看出每個 Forth 系統作者的寄存器分配考慮。

[1] F83. [2] Pygmy Forth.

圖 5 寄存器分配

“SP”指硬件堆棧指針。“Zpage”是指保存在 6502 存儲器零頁的值,零頁幾乎和寄存器一樣有用,有時比寄存器更有用。比如,它們可以被用于存儲器尋址?!癋ixed” 指 Payne's 8051 Forth 有一個單一的、不可移動的用戶區, UP 是硬編碼的常數。

寄存器變窄

我們在上面的表格中注意到了什么奇怪的事情嗎? 6502 Forth 是一個 16 位的模型,但是卻使用了 8 位的棧指針。

在實際情況下,使 PSP 、 RSP 和 UP 的尺寸小于 Forth 的單元尺寸是可能的。這是因為堆棧和用戶區相對于整個 CPU 可尋址存儲器來說比較小。每個堆??梢孕〉?64 個單元,而用戶區很少超過 128 個單元。你只需要簡單地相信:

?? 這些數據區被限制在存儲器的一個小的區域中,可以使用短的地址訪問;

?? 高地址位用其它的方式提供,比如,通過頁面選擇的方式來提供;

在 6502 CPU 中,硬件堆棧被 CPU 的設計者限定在 RAM 的一個頁中(地址為 0x1xx )。8 位堆棧指針可以用作返回棧。參數棧保存在 RAM 的零頁中,通過一個 8 位索引寄存器間接訪問。

在 8051 中,你可以使用 8 位的寄存器 R0 和 R2 訪問外部 RAM ,并顯式地提供地址的高 8 位輸出到 PORT 2 。這就允許對兩個堆棧進行“頁選擇”。

UP 與 PSP 的 RSP 是有明顯區別的:它只是簡單地提供一個基地址,從來都不增量和減量。所以,它實際上只是提供這個虛擬寄存器的高位。低位必須借助某種索引技術來實現。例如,在 MC6809 中,你可以使用 DP 寄存器作為 UP 的高 8 位,然后使用直接頁面尋址模式去訪問這個頁面中的 256 個位置。這就強制用戶區域從 0x??00 開始,同時限制用戶區域長度為 128 個單元, 這些都不是什么大問題。而在 Intel 8086 上,你還可以使用一個段寄存器作為用戶區的基地址。

參考文獻

[CUR93a] Curley, Charles, "Life in the FastForth Lane," awaiting publication in Forth Dimensions. Description of a 68000 subroutine-threaded Forth.

[CUR93b] Curley, Charles, "Optimizing in a BSR/JSR Threaded Forth," awaiting publication in Forth Dimensions. Single-pass code optimization for FastForth, in only five screens of code! Includes listing.

[KEL92] Kelly, Guy M., "Forth Systems Comparisons," Forth Dimensions XIII:6 (Mar/Apr 1992). Also published in the 1991 FORML Conference Proceedings . Both available from the Forth Interest Group, P.O. Box 2154, Oakland, CA 94621. Illustrates design tradeoffs of many 8086 Forths with code fragments and benchmarks -- highly recommended!

[KOG82] Kogge, Peter M., "An Architectural Trail to Threaded- Code Systems," IEEE Computer, vol. 15 no. 3 (Mar 1982). Remains the definitive description of various threading techniques.

[ROD91] Rodriguez, B.J., "B.Y.O. Assembler," Part 1, The Computer Journal #52 (Sep/Oct 1991). General principles of writing Forth assemblers.

[ROD92] Rodriguez, B.J., "B.Y.O. Assembler," Part 2, The Computer Journal #54 (Jan/Feb 1992). A 6809 assembler in Forth.

[SCO89] Scott, Andrew, "An Extensible Optimizer for Compiling Forth," 1989 FORML Conference Proceedings , Forth Interest Group, P.O. Box 2154, Oakland, CA 94621. Good description of a 68000 optimizer; no code provided.

Forth 實現

[CUR86] Curley, Charles, real-Forth for the 68000 , privately distributed (1986).

[JAM80] James, John S., fig-Forth for the PDP-11 , Forth Interest Group (1980).

[KUN81] Kuntze, Robert E., MVP-Forth for the Apple II , Mountain View Press (1981).

[LAX84] Laxen, H. and Perry, M., F83 for the IBM PC , version 2.1.0 (1984). Distributed by the authors, available from the Forth Interest Group or GEnie.

[LOE81] Loeliger, R. G., Threaded Interpretive Languages , BYTE Publications (1981), ISBN 0-07-038360-X. May be the only book ever written on the subject of creating a Forth-like kernel (the example used is the Z80). Worth it if you can find a copy.

[MPE92] MicroProcessor Engineering Ltd., MPE Z8/Super8 PowerForth Target , MPE Ltd., 133 Hill Lane, Shirley, Southampton, S01 5AF, U.K. (June 1992). A commercial product.

[PAY90] Payne, William H., Embedded Controller FORTH for the 8051 Family , Academic Press (1990), ISBN 0-12-547570-5. This is a complete "kit" for a 8051 Forth, including a metacompiler for the IBM PC. Hardcopy only; files can be downloaded from GEnie. Not for the novice!

[SER90] Sergeant, Frank, Pygmy Forth for the IBM PC , version 1.3 (1990). Distributed by the author, available from the Forth Interest Group. Version 1.4 is now available on GEnie, and worth the extra effort to obtain.

[TAL80] Talbot, R. J., fig-Forth for the 6809 , Forth Interest Group (1980).

第二部分 內核基準測試和個案研究

基準測試

我們已經回答了每個與 Forth 實現決策有關的問題,現在應該是“編碼并查看結果”的時候了。不過,你肯定不想僅僅為了測試不同的方法就編寫許多個完整的 Forth 內核。幸運的是,僅僅編寫 Forth 內核的小子集就可以得到一些相當好的“感覺”。

Guy Kelly [KEL92] 研究了 19 個不同的 IBM PC 的下列一些代碼樣例:

?? NEXT …… 是鏈接“串線”中一個字到另一個字的“內層解釋器”。用于每一個 CODE 定義的結尾,是決定 Forth 執行速度的一個最重要的因素。你已經看到了它的 ITC 和 DTC 偽碼;在 STC 中,它就是 CALL/RET 指令。

?? ENTER …… 也稱為 DOCOL 或者 DOCOLON ,高級“冒號”定義代碼域動作。它對于速度也是至關重要的;用于每個冒號定義的開始,在 STC 中不需要。

?? EXIT …… 在 FIG-Forth 中稱為 S; 。結束一個冒號定義執行的代碼。它在每個冒號定義的結束處出現,決定高級子程序的返回效率。在 STC 中它就是一個 RET 機器指令。

NEXT 、 ENTER 和 EXIT 表現了串線機制的性能。它們都應該通過實際的編碼來評估實現性能。它們也反映了實現時 IP 、 W 和 RSP 寄存器分配策略是否正確。

?? DOVAR …… “變量”,對于所有 Forth 變量 VARIABLE 的代碼域動作。

?? DOCON …… “常量”,對于所有 Forth 常量 CONSTANT 的代碼域動作。

DOCON 、 DOVAR 和 ENTER 一起顯示了你可以得到一個正在執行的字的參數域地址的效率。這反映了你對 W 寄存器的選擇,在 DTC Forth 中,也指出應該在代碼域中放一個 JUMP 指令還是一個 CALL 指令。

?? LIT …… “文字量”。這個字從 Forth 的高級串線中取一個單元值。有幾個字需要使用這樣的內嵌參數,這很好地顯示了它們的性能。它反映了你對 IP 寄存器的選擇。

?? @ …… Forth 的存儲器讀取操作,顯示了從高級 Forth 中訪問存儲器可以有多快。這個字常常從堆棧的 TOS 中受益。

?? ! …… Forth 的存儲器存操作,從另一方面反映了存儲器訪問的能力。它消耗堆棧的兩個項目,所以能反映參數棧的訪問效率。它也很好地說明了我們把 TOS 放在存儲器還是放在寄存器中的決策。

?? + …… 加法操作,是所有 Forth 算術和邏輯操作的典型代表。

以上是一個非常好的代碼樣例。我還增加了幾個附屬的測試:

?? DODEOS …… 是用 DOES> 構建字的代碼域動作,盡管它沒有反映 W 、 IP 和 RSP 的使用。我包含這個字是因為它是 Forth 內核中最費解的代碼,如果你可以編碼 DODOES 的邏輯,其它的任何東西就都不在話下了。 DODOES 的復雜性將在本文的后面描述。

?? SWAP …… 是一個簡單的堆棧操作符,但能說明問題。

?? ROT …… 是一個更加復雜的堆棧操作符。它為你能簡單地訪問參數棧給出一個好主意。 ROT 好像需要一個外加的臨時寄存器才能完成。如果你能夠在不使用 X 寄存器的情況下實現 ROT ,則其它情況下也不會需要 X 寄存器。

?? 0= …… 是不多的幾個單目算術操作之一,是最有可能從“TOS 在寄存器中 ” 獲益的字之一。

?? +! …… 是最多被說明的操作,組合了堆棧訪問、算術、存儲器取和存儲器存。這是一個非常理想的用于標準測試的字,盡管比上面所列出的其它字使用頻率低。

以上所列的都是最常用的 Forth 字,努力優化它們是值得的。我將給出一個 MC6809 的偽碼例子。對于其它的處理器,我將解釋特別選擇的代碼片段。

個案研究 1 : MC6809

在 8 位 CPU 世界中, MC6809 是 Forth 程序員的甜蜜之夢。它支持 2 個堆棧!還有另外 2 個地址寄存器和大量的只有 PDP-11 才有的正交尋址方式。正交的意思是指所有的地址寄存器有相同的選項和相同的工作方式,而兩個 8 位累加器可以作為一個單一的 16 位累加器使用,并具有許多 16 位操作指令。

MC6809 的程序員模型是

A - 8 bit 累加器

B - 8 bit 累加器 大多數算術操作以累加作為目的寄存器。它們也可以連接在一起作為一個 16 位的累加器 D ( A 是高 8 位, B 是低 8 位)。

X - 16 位索引寄存器

Y - 16 位索寄存器

S - 16 位堆棧指針

U - 16 位堆棧指針 所有用于 X 和 Y 寄存器的尋址模式也可以用于 S 和 U 寄存器。

PC - 16 位程序計數器

CC - 8 位條件標志寄存器

DP - 8 位直接頁訪問寄存器

MC6800 系列的直接尋址模式可以使用一個 8 位寄存器訪問零頁存儲器的任何位置。 MC6809 允許對任何頁進行直接尋址。DP 寄存器提供高 8 位地址(頁地址)。

有 2 個堆棧指針可供 Forth 使用,它們是等效的,但 CPU 設計者把 S 用于子程序調用和中斷。為一致起見,我們把 S 作為返回棧, U 作為參數棧。

W 和 IP 都要求使用地址寄存器,它們邏輯上用于 X 和 Y 寄存器,我們可以任意指定:

X => W 而 Y => IP 。

現在來選擇一個串線模型。我簡單地舍棄 STC 和 TTC ,構造一個“傳統”的 Forth 。性能上的限制因素是 NEXT 子程序。讓我們先看看它的 ITC 和 DTC 實現:

ITC-NEXT:

LDX ,Y ++ (8) (IP) -> W, 增量 IP

JMP [,X] (6) (W) -> temp, 跳轉到臨時單元的地址

DTC-NEXT:

JMP [,Y++] (9) (IP)->temp, 增量 IP, 跳轉到臨時單元地址,臨時單元在 MC6809 的內部。

NEXT 在 DTC 的 MC6809 中只有一條指令!這就意味著你可以用 2 個字節的內嵌編碼,比 JMP NEXT 又快又好。作為比較,子程序串線是這樣的:

RTS (5) ... 在 CODE 字的結尾

JSR nextword (8) ... 在串線中下一個 CODE 字的開始

STC 花費 13 個周期用于串線下的一個字,而 DTC 只需要 9 個周期。這是由于子程序串線需要將返回地址彈出和壓棧,而 CODE 字卻不需要。

決定了使用 DTC 之后,你還有兩個選擇:高級定義字在它的代碼域中使用 JMP 還是 CALL ?決定的因素是我們如何能更快地得到后面的參數域地址。讓我們注意一個冒號定義的 ENTER 編碼:

如果使用 JSR (Call):

JSR ENTER (8)

...

ENTER:

PULS W (7) 得到 JSR 之后的地址到 W 中

PSHS IP (7) 保存舊的 IP 到返回棧

TFR W,IP (6) 參數域地址 -> IP

NEXT (9) JMP [,Y++] 的匯編語言智能

以上總計 37 個周期

如果使用 JMP:

JMP ENTER (4)

...

ENTER:

PSHS IP (7) 保舊的 IP 到返回棧上

LDX -2,IP (6) 重新得到代碼域地址

LEAY 3,X (5) 加 3 存入 IP ( Y )寄存器中

NEXT (9)

以上總計 31 個周期

因為 MC6809 的尋址模式允許另外一級的間接,所以 6809 的 NEXT 不使用 W 寄存器。 ENTER 的 JMP 版本必須再次讀取代碼域的地址 -- NEXT 沒有在任何寄存器中留下這個地址。 JSR 可以通過彈出返回棧直接得到參數域地址。所以, JMP 版本更快。

不論哪一種方式, EXIT 都是一樣的:

EXIT:

PULS IP 從返回棧中彈出“保存的”IP

NEXT 繼續 Forth 解釋

有些寄存器尚未分配。你可以把用戶指針放到存儲器中,這樣的 Forth 也運行得很好。不過 DP 寄存器就浪費了,而 DP 也沒有什么其它的用處。讓我們使用一個“技巧”來實現,我們把 UP 的高位搬到 DP 中(它的低字節是 0 )。

還有一個沒有使用的寄存器 D 寄存器,許多算術操作需要這個寄存器。它應該自由地作為一個可隨意使用的寄存器呢?還是應該作為棧頂元素呢? MC6809 使用存儲器作為一個操作數,所以并不需要第二個工作寄存器。如果臨時需要寄存器,把 D 壓入和彈出也很容易。

所以我們只能對兩種方式編寫測試程序,看看哪個更快。

NEXT 、 ENTER 和 EXIT 不使用堆棧,在各種情況下的代碼都是一樣的。

DOVAR 、 DOCON 和 LIT 在兩種情況下所用的時鐘周期數相同。這就解釋了我們以前談到的把 TOS 放到寄存器中僅僅改變 PUSH 或者 POP 的位置:

SWAP 、 ROT 、 0= 、 @ 特別是 + 通過把 TOS 放到寄存器中而加快 :

但是, ! 和 +! 卻由于 TOS 放到寄存器中而變慢 :

這些字變慢的原因是許多訪問存儲器的 Forth 字希望地址在棧頂,所以需要一個額外的 FTR 指令。這就是為什么 TOS 寄存器必須是一個地址寄存器。不幸的是, MC6809 的地址寄存器都用于更重要的 W 、 IP 、 PSP 和 RSP 了。不過,把 TOS 放到寄存器中對于 ! 和 !+ 的損失可以通過許多算術和堆棧操作運行速度的提高而得到彌補。

個案研究 2 : 8051

如果說 MC6809 是 Forth 系統實現者的美夢,那 Intel 8051 簡直就是 Forth 實現者的惡夢了。它只有一個通用的地址寄存器,一種尋址模式,總是使用一個 8 位累加器。

所有的算術操作、許多的邏輯操作都必須使用累加器。一個唯一的 16 位操作是 INC DPTR 。硬件堆棧必須使用 128 字節的片內寄存器文件,這樣的 CPU 簡直就是一堆破銅爛鐵!

有些 8051 Forth 實現了一個 16 位的 Forth ,但是它們太慢而不能滿足我們的要求。讓我們進行某些權衡,以產生一個更快的 8051 Forth 系統。

我們最初的想法是利用那個唯一的地址寄存器。所以我們用 8051 的程序計數器作為 IP -- 也就是說,我們構造一個子程序串線的 Forth 系統。如果編譯器在所有可能的情況下都使用 2 字節的 ACALL 代替 3 字節的 LCALL ,多數的 STC 代碼將和 ITC/STC 一樣小。

子程序串線意味著返回棧指針就是硬件堆棧指針。片上寄存器文件共有 64 個單元空間,但是這些空間并不足以支持多任務堆棧。面對這種情況下你可以考慮以下幾個策略:

?? 限制這個 Forth 系統為單任務系統;

?? 在所有的 Forth 定義入口處把返回地址保存到一個外部 RAM 軟件堆棧中;

?? 在任務切換的時候把全部返回棧的內容保存到外部 RAM 中。

第二種方法是最慢的!在每個任務切換的時候移動 128 個字節比在每個 Forth 字中移動兩個字節要快得多。現在我選擇 1 ,而將選擇 3 留作以后擴充。

唯一一個真正的地址寄存器 DPTR 將要擔負多種使命。它就是 W ,多用途的工作寄存器。

實際上,還有兩個寄存器可以尋址外部存儲器: R0 和 R1 。它們僅僅提供 8 位地址,高 8 位將顯式地輸出到口 2 上。但是對于堆棧,這是一個可以容忍的限制,因為我們可以把堆棧限制在 256 字節空間。所以我們使用 R0 作為 PSP 。

同樣的 256 字節可以用于用戶數據區,這使得 P2 (口 2 )成為用戶指針的高字節,像 MC6809 一樣,而低字節隱含是 0.

于是 8051 的程序員模型就變成了:

寄存器地址 8051 名字 Forth 使用

0 R0 PSP 的低字節

1 R1

2 R2

3 R3

4 R4

5 R5

6 R6

7 R7

8-7Fh 120 字節的返回棧

81h SP RSP 的低字節(高位字節 = 0 )

82-83h DPTR W 寄存器

A0h P2 UP 和 PSP 的高字節

E0h A

F0h B

注意我們僅僅使用了 BANK0 , 另外的 3 個寄存器 BANK 從 08H 到 1FH , 從 20H 到 2FH 的位尋址寄存器都沒有被 Forth 使用。使用 BANK0 可以為返回棧得到最大的連續空間。如果需要,返回棧還可以縮小。

在子程序串線的 Forth 系統中,不需要 NEXT、ENTER 和 EXIT 。

如何處理棧頂元素呢?在 8051 中,有許多的寄存器,而存儲器操作卻非常昂貴。我們把 TOS 放到 R3:R2 中(按 INTEL 格式,R3 是高字節)。注意,我們不能使用 B:A 寄存器對 -- A 寄存器是一個漏斗,所有的寄存器引用都要通過它進行。

8051 采用了“哈佛”體系結構:程序和數據在分開的存儲器中存放。(Z8 和 TMS320 是哈佛體系結構的另外兩個例子)。但 8051 使用的是一種“野蠻”的退化形式:軟件沒有辦法從物理上向程序存儲器寫,這就意味著 Forth 的開發者只能使用下述兩個方式:

?? 交叉編譯全部程序,包括應用程序,放棄實現一個 8051 交互式 Forth 的努力;

?? 使一部分或者全部的程序存儲器在數據空間可見,最簡單的辦法就是使這兩個空間完全覆蓋。

相比 Z8 和 TMS320 就比較文明,它們允許向程序存儲器寫入。Forth 內核的具體實現將在以后討論。

個案研究 3 : Z80

選擇討論 Z80 是因為它是非正交 CPU 的一個極端例子,它有 4 個不同種類的地址寄存器,有些操作使用寄存器 A 作為目的寄存器,有些則可以是任意的8位寄存器,有些是 HL 寄存器對,有些則可以是任意的16位寄存器,等等。有些操作(比如 EX DE, HL )卻只允許一種寄存器組合。

在 Z80 這類的 CPU 中(或者同樣在 8086 中), Forth 功能的指定必須仔細匹配 CPU 寄存器的能力。許多方案需要評估,而唯一的辦法常常就是對不同的決策方案編寫各種代碼進行測試。為了避免本文變成為一堆“代碼列表”,我選擇了基于許多 Z80 編碼經驗的一種寄存器指定,它說明了這些選擇可以合理地解釋早期討論的一般原理。

我希望得到一個傳統的 Forth ,盡管我使用了直接串線技術。我需要全部的“經典”虛擬寄存器。

忽略其它的寄存器集, Z80 的6個地址寄存器具有下列能力:

?? BC,DE - LD A 間接 , INC, DEC 也交換 DE/HL

?? HL - LD r 間接 , ALU 間接 , INC, DEC, ADD, ADC, SBC, 交換 W/TOS, JP 間接

?? IX,IY - LD r 間接 , ALU 間接 , INC, DEC, ADD, ADC,SBC, 交換 W/TOS, JP 間接 ( 全都很慢 )

?? SP - PUSH/POP 16 位 , ADD/ADC/SUB to HL/IX/IY

BC, DE, 和 HL 也可以作為位寄存器對來處理。

8 位寄存器 A 必須留作臨時寄存器,因為許多 ALU 操作和存儲器引用操作都使用它作為目的。

HL 無疑是最通用的寄存器,可以逐個試著用它作為每個虛擬寄存器。然而,由于它的通用性 -- 它是唯一可以讀取字格式和支持間接跳轉的寄存器 -- HL 應該作為 Forth 的通用工作寄存器 W 。

由于 IX 、 IY 都有索引尋址模式并可用 ALU 操作,所以可以考慮用它們作為堆棧指針 。但是它們有兩個主要的問題:通用的堆棧指針 SP 寄存器沒有用,而 IX/IY 卻特別慢!

在 Forth 的兩個棧上都有許多 16 位的 PUSH / POP 類操作,對于 SP 來說,這些操作只需要一條指令,而 IX 或者 IY 操作卻需要 4 條指令。所以兩個堆棧之一應該用 SP 實現,這應該是參數棧,因為它的使用頻率比返回棧要高。

如何考慮 Forth 的 IP 寄存器呢?在大多數情況下, IP 都是從存儲器讀取并且自動增量的,使用 IX/IY 作為 IP 不會比使用 BC/DE 有任何編程上的好處,考慮 IP 的速度,使用 BC/DE 對卻更快。讓我們把 IP 放到 DE 中:它可以與 HL 的內容交換,而后者是通用的。

需要第二個 Z80 寄存器對(不是 W )進行 16 位的算術運算?,F在只有 BC 了,它可以用于尋址或者與 A 進行 ALU 操作。但是,我們是用 BC 作為第二個工作寄存器“X”、還是作為棧頂元素呢?只有編碼才能得到結論?,F在,讓我們樂觀地假定 BC = TOS 。

只剩下 RSP 和 UP 了,還有 IX 和 IY 寄存器沒有分配。 IX 和 IY 是等效的,我們設 IX = RSP , IY = UP 。

于是, Z80 Forth 系統的寄存器分配如下

BC = TOS IX = RSP

DE = IP IY = UP

HL = W SP = PSP

現在讓我們看看 DTC 的 Forth 系統 NEXT 編碼:

DTC-NEXT:

LD A,(DE) (7) (IP)->W, 增量 IP

LD L,A (4)

INC DE (6)

LD A,(DE) (7)

LD H,A (4)

INC DE (6)

JP (HL) (4) 跳轉到 W 中的地址

還可以有其它的版本(具有同樣的時鐘周期)

DTC-NEXT:

EX DE,HL (4) (IP)->W, 增量 IP

NEXT-HL:

LD E,(HL) (7)

INC HL (6)

LD D,(HL) (7)

INC HL (6)

EX DE,HL (4)

JP (HL) (4) 轉到 W 中的地址

注意單元是以低位字節優先的方式存放在存儲器中的。同樣,盡管看起來把 IP 保存在 HL 寄存器中有許多好處,但實際上卻沒有。這是由于 Z80 不能進行 JP (DE) 。 NEXT-HL 進入將更短一些。

僅僅用于比較,讓我們看一下 ITC NEXT 。以前給出的偽代碼需要另一個臨時寄存器“X”,它的內容用于間接跳轉。令 DE = X, BC = IP, TOS 保存在存儲器中。

ITC-NEXT:

LD A,(BC) (7) (IP)->W, 增量 IP

LD L,A (4)

INC BC (6)

LD A,(BC) (7)

LD H,A (4)

INC BC (6)

LD E,(HL) (7) (W)->X

INC HL (6)

LD D,(HL) (7)

EX DE,HL (4) 跳轉到 X 中的地址

JP (HL) (4)

這就把“W”加 1 并放到在 DE 寄存器中了。只要這是一致的,就不會有任何問題 -- 代碼知道在需要 W 的內容時如何去找到它,以及如何調整它。

ITC 的 NEXT 是 11 個同期, DTC 是 7 個同期。 ITC 沒有將 TOS 保存在寄存器中的能力,所以我選擇 DTC 。

如果使用內嵌編碼, DTC NEXT 在每個 CODE 字中需要 7 個字節。一個直接跳轉到 NEXT 的子程序只需要 3 個字節,但需要附加 10 個時鐘周期,這是一個特別的例子,我們選擇的是內嵌方式的 NEXT 。但有時 NEXT 特別大,或者存儲器很小,更謹慎的決策可能是使用 JMP 到 NEXT 。

現在讓我們來看 ENTER 的代碼。使用一個 CALL ,可以彈出硬件堆棧以得到參數域地址:

CALL ENTER (17)

...

ENTER:

DEC IX (10) 把老的 IP 放到返回棧上

LD (IX+0),D (19)

DEC IX (10)

LD (IX+0),E (19)

POP DE (10) 參數域地址 -> IP

NEXT (38) 7 個機器指令的匯編語言宏

實際上這比 POP HL 快,然而使用最后的 6 個指令(不用 EXDE , HL ):

CALL ENTER (17)

...

ENTER:

DEC IX (10) 把老的 IP 放到返回棧上

LD (IX+0),D (19)

DEC IX (10)

LD (IX+0),E (19)

POP HL (10) 參數域地址 -> HL

NEXT-HL (34) 看上面的 DTC 的 NEXT 代碼

總計 119 個周期

當使用 JP 時, W 寄存器( HL )依然指向代碼域。代碼域是其后的 3 個字節:

JP ENTER (10)

...

ENTER:

DEC IX (10) 把老的 IP 放到返回棧上 LD (IX+0),D (19)

DEC IX (10)

LD (IX+0),E (19)

INC HL ( 6) 參數域地址 -> IP

INC HL ( 6)

INC HL ( 6)

NEXT-HL (34)

總計 120 個周期

由于改變了 NEXT 的入口, IP 的新值就不必放入 DE 寄存器對了。

CALL 版本快了 1 個周期。在嵌入式系統應用 Z80 時,我們還可以使用單字節的 RST 指令來得到速度和空間的雙重收益,但是在基于 Z80 的個人計算機上,這個策略并不可用(操作系統使用了這個特性,即操作系統的系統調用是通過這個接口進入的)。

個案研究 4 : INTEL 8086

Intel 的 8086 是另一個有教育意義的 CPU 。我們不再詳細討論設計過程,只是看一個新的用于 PC 的共享軟件: Pygmy Forth [SER90].

Pygmy 是一個直接串線的 Forth 系統,棧頂元素保存在寄存器中。 8086 寄存器是這樣安排的:

AX = W DI = scratch

BX = TOS SI = IP

CX = scratch BP = RSP

DX = scratch SP = PSP

許多 8086 Forth 系統的實現使用 SI 寄存器作為 IP ,所以 NEXT 可以通過 LODSW 指令實現。在 Pygmy 的 DTC 實現中, NEXT 是這樣的:

NEXT:

LODSW

JMP AX

這已經小得足以嵌入到每個 CODE 字中了 。

高級“定義” Forth 字使用一個 JMP (相對)指令轉向它們的機器碼。 ENTER 子程序(在 Pygmy 中稱為 'docol' )因此需要從 W 中得到參數域地址。

ENTER:

XCHG SP,BP

PUSH SI

XCHG SP,BP

ADD AX,3 參數域地址 -> IP

MOV SI,AX

NEXT

注意交換兩個堆棧指針的 XCHG 用法,這允許對兩個堆棧都使用 PUSH 和 POP 指令,這比使用基于 BP 的直接尋址指令要快。

EXIT:

XCHG SP,BP

POP SI

XCHG SP,BP

NEXT

段模型

Pygmy Forth 是一個單段的 Forth 系統,所有的代碼和數據都在一個 64K 字節的段中, 這相當于 Turbo C 的緊縮模式。到目前為止,我們討論的 Forth 標準都假設所有的東西全部包含在單一的存儲器地址空間,使用同樣的讀寫操作符。然而, IMP PC Forth 開始使用多個段來處理 5 種不同的數據,它們是:

CODE …… 機器代碼

LIST …… 高級 Forth 串線 ( 所以這個段也稱為 THREADS)

HEAD …… Forth 字的首部

STACK …… 參數和返回棧

DATA …… 變量和用戶定義數據

這就允許 PC 機上的 Forth 突破 64K 字節的段限制,而又不需要在一個 16 位的 CPU 上實現一個 32 位的 Forth 系統。但是,實現一個多段的模型、分支到 Forth 核心等等內容已經遠遠超出了本文的討論范圍。

參考文獻

[KEL92] Kelly, Guy M., "Forth Systems Comparisons," Forth Dimensions XIII:6 (Mar/Apr 1992). Also published in the 1991 FORML Conference Proceedings . Both available from the Forth Interest Group, P.O. Box 2154, Oakland, CA 94621. Illustrates design tradeoffs of many 8086 Forths with code fragments and benchmarks -- highly recommended!

[MOT83] Motorola Inc., 8-Bit Microprocessor and Peripheral Data , Motorola data book (1983).

[SIG92] Signetics Inc., 80C51-Based 8-Bit Microcontrollers , Signetics data book (1992).

Forth 實現

[PAY90] Payne, William H., Embedded Controller FORTH for the 8051 Family , Academic Press (1990), ISBN 0-12-547570-5. This is a complete "kit" for a 8051 Forth, including a metacompiler for the IBM PC. Hardcopy only; files can be downloaded from GEnie. Not for the novice!

[SER90] Sergeant, Frank, Pygmy Forth for the IBM PC , version 1.3 (1990). Distributed by the author, available from the Forth Interest Group. Version 1.4 is now available on GEnie, and worth the extra effort to obtain.

[SEY89] Seywerd, H., Elehew, W. R., and Caven, P., LOVE-83Forth for the IBM PC , version 1.20 (1989). A shareware Forth using a five-segment model. Contact Seywerd Associates, 265 Scarboro Cres., Scarborough, Ontario M1M 2J7 Canada.

第三部分 解密 DOES>

更正

上一部分的 MC6809 設計決策中存在一個很大的錯誤,在我編碼 Forth 字 EXECUTE 的時候,它變得非常明顯。

EXECUTE 引起一個 Forth 字的執行,它的地址在參數棧上。更精確地說:編譯地址、或者說代碼域地址在參數棧上給出。這可以是任何類型的 Forth 字: CODE 定義、冒號定義、 CONSTANT 、 VARIBLE 或者是定義字。與通常的 Forth 解釋過程不同的是,執行字的地址在棧上給出,而不是通過“串線”給出(通過 IP 指定)。

在我們的直接串線 MC6809 中,這可以很容易地編碼 :

EXECUTE:

TFR TOS,W 把字的地址放到 W 中

PULU TOS 彈出新的 TOS

JMP ,W 跳到 W 給定的地址

注意:應該是 JMP ,W 而不是 JMP [,W], 因為我們已經有了這個字的代碼地址,不是從高級線程中讀取的。如果 TOS 不在寄存器中, EXECUTE 可以更簡單地實現 JMP [,PSP++] 。現在假設這個被執行的字是一個冒號定義, W 將要指向它的代碼域,其中包含有 JMP ENTER 。 如下所示:

JMP ENTER

...

ENTER:

PSHS IP

LDX -2,IP 重新取得代碼域地址

LEAY 3,X

NEXT

這就是錯誤所在!因為我們不是從串線中執行這個字,所以 IP 并沒有指向代碼域地址的一個拷貝。記住: EXECUTE 字的地址來自于堆棧。這種方式的 ENTER 不能與 EXECUTE 一同工作,因為沒有辦法得到將要執行的字的地址。

這也同時提出了 DTC Forth 的一個新規則:如果 NEXT 沒有把將要執行的字的地址放到一個寄存器中,你就必須在代碼域中使用 CALL 。

于是, MC6809 Forth 只好倒退回在代碼域中使用 JSR 的方法。但是, ENTER 是 Forth 中使用最多的代碼片斷,為了避免速度的損失,我完成了上一章中的“學生練習”。注意當你交換 RSP 和 PSP 時發生了什么:

執行新版本需要 31 個周期,這與我前面使用的 JMP 版本的時間一樣。其中的改進是由于 JSR 版本的 ENTER 同時使用 Forth 的返回棧和 MC6809 子程序返回棧( JSR 棧)。使用兩個不同的堆棧指針意味著我們不必與IP“交換” TOS ,也就不需要任何的臨時寄存器了。

這也解釋了一個新 Forth 內核通常的開發過程:先做出一些設計決策,然后寫出一些簡單的代碼,再找出一個 BUG 或者一個更好的方法做這件事情,改變某些設計策略,重新編寫示例代碼,重復這個過程直到滿意為止。

這給了我們一個教訓:把 EXECUTE 做為一個基準測試字。

Carey Bloodworth of Van Buren, AR 指出了上一版本 MC6809 中的一個小的、但是讓我不好意思的錯誤:

對于 0= 的“ TOS 在存儲器”版本,我應該這樣編寫代碼:

LDD ,PSP

CMPD #0

這是為了測試 TOS 是否為 0 。可是在這種情況下, CMPD 指令完全是多余的,因為 LDD 指令在 D 寄存器為 0 時將設置 Zero 標志。 TOS 在 D 寄存器的版本還是需要 CMPD 指令的,但是比 TOS 在存儲器版本執行速度更快。

現在讓我們開始討論主題

什么是代碼域?

DOES 的概念看起來是 Forth 中最難懂和最神秘的一部分,不過 DOES 也是使 Forth 具有強大能力的一個原因 -- 在許多方面,它是先天面向對象的。 DOES 的行為和能力也與 Forth 最閃亮的方面有著聯系:代碼域。

回憶第一部分, Forth 的定義體由兩個部分組成:代碼域和參數域。你可以從不同的方面來考察這兩個域:

?? 代碼域是這個 Forth 字的動作,參數域是與動作有關的數據;

?? 代碼域是一個子程序調用,參數域是調用后面的“內嵌”參數(匯編程序員觀點);

?? 代碼域是字類的單個“方法”,參數域是某個特別字的“實例變量”(面向對象程序員的觀點);

所有這些觀點都有著共同點:

?? 代碼域子程序在調用時至少有一個參數,它就是這個要執行的 Forth 字的參數域地址,參數域可以包含有任何數目的參數 ;

?? 只有幾個相對不多的特殊動作,或者說,代碼域只引用為數不多的幾個特殊子程序(我們后面將會看到,這對于 CODE 例外)。我們可以回憶一下第 2 部分的 ENTER 子程序:這個通用的子程序被所有的 Forth 冒號定義引用 ;

?? 對參數域的解釋隱含地由代碼域的內容去解釋?;蛘哒f,每個代碼域子程序希望參數域包含一定類型的數據 ;

一個典型的 Forth 內核有以下幾個預定義的代碼域子程序 .

Forth 之所以強大的原因在于 Forth 程序并不限于只能使用這些代碼域子程序(或者只能使用你的 Forth 系統內核所提供的其它子程序集)。程序員可以定義新的代碼域子程序,可以定義一個新的參數域類型與之匹配。用面向對象程序設計方法的“行話”來說,可以創建新的“類”和“方法”(盡管每個類只有一個方法)。同時,就像其它的 Forth 字一樣 -- 代碼域可以用匯編語言定義,也可以用高級 Forth 字來定義。

為了理解代碼域的機制和參數是如何傳遞的,我們首先看看匯編語言(機器代碼)的情況。我們先考察間接串線(ITC)的情況,它是最容易理解的,然后再看看如何修改這些邏輯到直接串線(DTC)和子程序串線的(STC)上。最后,再看如何使用高級 Forth 定義來描述代碼域的動作。

Forth 的編寫者在使用術語時有些混亂,所以,我使用我自己的術語來解釋,如圖 1 所示。首部包含有字典信息,與一個 Forth 字的執行沒有關系。體是這個字的“工作”部分,包含有固定長度的代碼域和可變長度的參數域。對于任何一個給定的字,這兩個域在存儲器中的位置分別被稱為代碼域地址(CFA)和參數域地址(PFA)。一個字的代碼域地址就是這個字在存儲器中的位置。不要把這個與代碼域的內容相混淆,在 ITC 中,內容是另一個不同的地址。

需要明確的是:代碼域的內容是另外一片存儲器的地址,在那一片存儲器中是機器代碼。我把這個地址稱為代碼地址。最后,當討論 DTC 和 STC Forth 時,我也引用“代碼域內容”,它的含義比代碼域地址更多。

圖 1 一個 ITC Forth 字

機器代碼動作

Forth 的 CONSTANT 可能是最簡單的機器代碼例子。讓我們考察一個法語的例子:

1 CONSTANT UN

2 CONSTANT DEUX

3 CONSTANT TROIS

執行 UN 會把值 1 壓入堆棧,執行 DEUX 把 2 壓入堆棧等等。(不要把參數棧和參數域混淆,它們是完全獨立的)

在 Forth 內核中有一個字稱為 CONSTANT 。這并不是一個常數類的字本身,它是一個高級 Forth 定義。 CONSTANT 是一個“定義字”:它在 Forth 字典中創建一個新字,通過它我們能夠創建新的“常數類”字 UN 、 DEUX 和 TROIS 。你也可以把它們理解成常數“類”的一個個“實例”。這三個字都有自己的代碼域,都指向同樣的 COSNTANT 動作的機器代碼片斷。

這個代碼片斷應該執行什么動作呢?圖 2 給出了這三個常數的存儲器表示。所有這三個字都指向共同的動作子程序。這些字的區別在于它們的參數域,這里簡單地包含有常數的值,或者用面向對象的說法是“實例變量”。所以,這三個字的動作都應該是讀取參數域的內容,并把它們放到棧頂。這段代碼也隱含地知道參數域包含一個單元大小的值。

圖 2 三個常數

為了寫出做這件事情的機器代碼片斷,我們需要知道怎樣才能找到參數域的地址,之后 Forth 的解釋器就可以跳轉到機器代碼。那么,PFA 是如何傳遞給機器代碼子程序的呢?并且, Forth 解釋器的 NEXT 是如何編碼的呢?這依賴于不同的實現。為了寫出機器代碼動作,我們首先需要理解 NEXT 。

ITC 的 NEXT 在第一部分已經用偽碼描述了,以下是 MC6809 的實現,使用 Y=IP,X=W:

NEXT: LDX ,Y++ ; (IP) -> W, IP+2 -> IP

JMP [,X] ; (W) -> temp, JMP (temp)

假設我們的高級串線中有這樣的代碼:

... SWAP DEUX + ...

當 NEXT 被執行時,使用 IP 解釋指針指向 DEUX “指令”(緊接在 SWAP 之后),圖 3 解釋了發生的事情。 IP (寄存器 Y )指向高級串線內部的一個存儲器單元,它包含有 Forth 字 DEUX 的地址。更精確地說,這個單元包含有字 DEUX 的代碼域地址。于是,當我們使用 Y 讀取一個單元時,自動增量 Y ,我們就得到了 DEUX 的代碼域地址。把它寫入 W (寄存器 X ), W 現在已經指向了代碼域,是一個機器代碼片斷的地址。我們可以讀取這個單元的內容,然后使用一條MC6809 指令跳轉到相應的機器代碼處執行。這個過程并沒有改變寄存器 X ,所以 W 仍然指向 DEUX 的 CFA ,我們就可以得到參數域地址,它在代碼域之后兩個字節的位置。

圖 3 ITC 在 NEXT 之前和之后的情況

所以,機器代碼片斷只需要把 W 加 2 ,讀取這個地址的單元內容,把它壓到棧上。這個代碼片斷通常被稱為 DOCON

DOCON:

LDD 2,X ; 讀取 W+2 處的單元

PSHU D ; 把它放到參數棧是

NEXT ; ( 宏 ) 跳轉到下一個高級字

這個例子中, TOS 在存儲器中。注意前面的 NEXT 已經把 IP 增加了 2 ,所以當 DOCON 做 NEXT 時,它已經指向了串線的下一個單元(“+” 的 CFA )。

通常, ITC Forth 會在 W 寄存器中留下參數域地址或者一些“鄰近”的地址。在這種情況下, W 包含有 CFA ,它在這個 Forth 實現中總是 PFA - 2 。由于除了 CODE 之外的每類 Forth 字都需要使用參數域地址,許多 NEXT 實現方法都是增量 W 使它指向 PFA 。我們可以在 MC6809 上做一些小的改變:

NEXT:

LDX ,Y++ ; (IP) -> W, IP + 2 -> IP

JMP [,X++] ; (W) -> temp, JMP (temp), W+2 -> W

這使 NEXT 增加了 3 個周期,但是把參數域地址放入了 W 寄存器。對于代碼域子程序它做了些什么呢?

W=CFA W=PFA

DOCON:

LDD 2,X (6) LDD ,X (5)

PSHU D PSHU D

NEXT NEXT

DOVAR:

LEAX 2,X (5) ; 沒有操作

PSHU X PSHU X

NEXT NEXT

ENTER:

PSHS Y PSHS Y

LEAY 2,X (5) LEAY ,X (4, 比 TFR X,Y 快 )

NEXT NEXT

從 NEXT 增加 3 個周期的代價中我們得到了什么收益呢? DOCON 減少了 1 個周期, DOVAR 減少了 5 個周期, ENTER 減少了 1 個周期。 CODE 字不使用 W 中的值,所以它們沒有從自動增量中受益。速度的增加或者損失要通過 Forth 字的混合執行來考察。通常的規則是執行最多的字是 CODE 字,這樣,在 NEXT 中增量 W 會有一點點速度上的損失 -- 當然也節省了存儲器 -- 不過 DOCON , DOVAR 和 ENTER 只出現一次,得到的收益并不明顯。

說來說去,最好的結論還是依賴于具體的處理器。比如像 Z80 這樣的處理器只能通過字節訪問存儲器,它沒有自動增量指令,所以通常的情況下,最好是保留 W 指向 IP+1 (從代碼域讀取的最后一個字節)。而在有些機器上,自動增量是“免費的”,這時讓 W 指向參數域就是最方便的。

注意:在一個系統中決策必須一致。如果 NEXT 讓 W 在執行時指向 PFA ,則 EXECUTE 也必須這樣做(這就是為什么我在本文的開頭拼命更正的原因)。

直接串線

直接串線和間接串線差不多,除了代碼域的內容:它不再是一些機器代碼的地址,而是 JUMP 或者 CALL 。這樣做可能會使得代碼域更大 -- 比如在 MC6809 上要大 1 個字節,但是,它省去了 NEXT 子程序中的一級間接。

在代碼域中選擇 JUMP 還是 CALL 指令依賴于機器碼子程序如何得到參數域地址。為了跳轉到代碼域,許多 CPU 要求把它的地址放在一個寄存器中。例如, Intel 8086 的間接跳轉指令是 JMP AX (或者其它的寄存器),在 Z80 上是 JP ( HL 或者 IX 或者 IY)。在這些處理器上, DTC 的 NEXT 包括兩個操作,在 MC6809 上將變成:

NEXT:

LDX ,Y++ ; (IP) -> W, IP + 2 -> IP

JMP ,X ; JMP (W)

在 Intel 8086 上,這兩條指令可以是 LODSW 和 JMP AX ,其中的影響可以通過圖 4 的 CASE1 說明。 DEUX 的代碼域地址是從高級串線中讀取的, IP 被增量。然后,不再進行讀取操作,而是用一個 JUMP 指令跳轉到代碼域。也就是說, CPU 直接跳轉到代碼域。 CFA 被留在 W 寄存器中,就像上面 ITC 的第一個例子。由于這個地址已經在寄存器中了,我們可以簡單地把 JUMP 放到 DOCON 的代碼域中, DOCON 的代碼片斷將和上面描述一樣地工作。

圖 4 DTC 中 NEXT 之前和之后的情況

不過,我們也許會注意到:在有些處理器上,比如 MC6809 和 PDP-11上,可以用一個指令來實現這個 DTC NEXT

NEXT:

JMP [,Y++] ; (IP) -> temp, IP+2 -> IP, JMP (temp)

這也能使 CPU 跳轉到 DEUX 的代碼域。但其中有一個巨大的差異:任何寄存器中都沒有留下 CFA !那么機器代碼片斷如何得到參數域的地址呢?答案是:通過使用 CALL (或者 JSR )指令來替代 JUMP 。在許多 CPU 上, CALL 指令會把返回地址放到返回棧上 -- 這就是緊隨在 CALL 指令之后的地址 。

如圖 4 所示的 CASE2 ,這個地址就是我們所需要的參數域地址!所以 DOCON 要做的就是從返回棧得到地址 -- 滿足代碼域放置 JSR 的要求 -- 然后使用這個地址來讀取常量,于是:

DOCON:

PULS X ; 從返回棧彈出 PFA

LDD ,X ; 讀取參數域的單元

PSHU D ; 壓入參數棧

NEXT ; ( 宏 ) 轉到下一個高級字

把這個同 ITC 版本相比較。 DOCON 多了 1 個指令,但是 NEXT 少了 1 個指令。 DOVAR 和 NEXT 也多了 1 個指令:

DOVAR:

PULS X ; 彈出這個字的 PFA

PSHU X ; 把那個地址放到參數棧上

NEXT

ENTER:

PULS X ; 彈出這個字的 PFA

PSHS Y ; 壓入老的 IP

TFR X,Y ; PFA 變成了新的 IP

NEXT

現在回到本文的開頭,重新讀一下我的“更正”,看一看為什么我們不能通過 IP 來重新讀 CFA 。同時也要注意,把 Forth 的堆棧指針給 MC6809 的 U 寄存器而 S 保留的情況與這里討論的不同。

子程序串線

子程序串線(STC)和 DTC 非常相似,都是 CPU 直接跳轉到一個 Forth 字的代碼域。但是現在不再有 NEXT 代碼,不再有 IP 寄存器,也沒有 W 寄存器。所以,只能在代碼域中使用 JSR 而不可能有其它的選擇,這是可以得到參數域地址的唯一辦法。這個過程如圖 5 所示。

圖 5 STC 的串線編碼

高級串線是被 CPU 執行的一系列子程序調用。當一個 JSR DEUX 被執行的時候,串線中下一個指令的地址被推進返回棧。接著,在字 DEUX 中的 JSR DOCON 被執行,它使得另一個返回地址 -- DEUX 的 PFA 被推入堆棧。 DOCON 可以彈出這個地址,使用它來讀取常數,把常數保存在堆棧上,然后用一個 RTS 指令返回到串線:

DOCON:

PULS X ; 從返回棧彈出 PFA

LDD ,X ; 讀取參數域單元

PSHU D ; 把它壓入參數棧

RTS ; 執行下一個高級字

在子程序串線代碼中,我們仍然可以沿用代碼域和參數域這樣的術語。除了 CODE 和冒號定義之外的每一個 Forth 字的類中,代碼域是被 JSR 或者 CALL 占用的空間(就像 DTC )一樣,而參數域就是它后面的空間。所以,在 MC6809 上, PFA 等于 CFA+3 。于是, CODE 和冒號定義的“參數域”含意變得有點兒模糊,在本文的后面可以看到這一點。

特例: CODE 字

在以上所有的一般性討論中,有一個明顯的例外,這就是 CODE 定義 -- 用匯編碼子程序定義的 Forth 字。用“匯編語言來定義一個字” -- 這個神奇的功能在 Forth 中很容易實現,因為每個 Forth 字都執行一段 Forth 代碼。

包含 CODE 字的匯編代碼總是包含在一個 Forth 字的體中,代碼域必須包含有要執行的機器代碼的地址。所以機器代碼放在參數域中,代碼域包含了參數域的地址,如圖 6 所示。

圖 6 CODE 字

在直接或者子程序串線的 Forth 中,我們可以通過類推,把一個 JUMP 放到代碼域中。代碼域也可以用 NOP 或者相同的結果填充。更好的是,機器代碼可以直接從代碼域開始,然后進入參數域。從這一點看,代碼域和參數域就沒有區別了。這不應該有任何疑問,因為我們并不需要對一個 CODE 字做這樣的區分。但可能有一些反匯編器和一些聰明的編程技巧需要這一區分,我們在這里就不討論它們了。

CODE 字 -- 不論是怎么實現的 -- 都是不需要向它傳遞參數域地址的機器代碼動作。參數域不包含數據,只是需要執行的代碼。只有 NEXT 需要知道這個地址(或者代碼域地址),這樣它就可以直接跳到機器代碼。

使用 ;CODE

現在還有三個問題沒有回答:

?? 我們如何創建一個 Forth 字,使得能在它的參數域中含有一些任意的數據?

?? 我們如何改變一個字的代碼域,以指向可選擇的機器代碼?

?? 我們如何在代碼片段與一個使用它的字隔離的情況下編譯(匯編)這個代碼片段?

對于第一個問題的回答是:寫一個 Forth 字來做這一工作。在執行的時候,因為這個字將在 Forth 字典中定義一個新的字,所以它被稱為“定義字”。

CONSTANT 就是一個定義字。一個定義字的所有“硬工作”都是由一個內核字 CREATE 來完成的,它從輸入流中分析名字,為新字建立頭和代碼域,并把它鏈接到字典中。對程序員來說,剩下的工作就是構造參數域了。

第二個、第三個問題的答案包含在兩個費解的 Forth 字中,它們分別是 (;CODE) 和 ;CODE 。為了理解它們是如何工作的,我們來看看定義字 CONSTANT 實際上是如何用 Forth 高級定義來寫的。使用前面 MC6809 的例子:

: CONSTANT ( n -- )

CREATE / 創建一個新的字

, / 把 TOS 的值寫入字典,作為參數域的第 1 個單元

;CODE / 結束高級定義,開始匯編代碼

LDD 2,X / DOCON 的匯編代碼片斷

PSHU D

NEXT

END-CODE

這個 Forth 字包含了兩個部分:從 CONSTANT 到 ;CODE 的任何事情都是在 COSNTANT 被訪問時執行的高級 Forth 代碼。而從 ;CODE 到 END-CODE 的事情都是常數的“子女” -- 常數類字比如 UN 和 DEUX -- 執行時要執行的機器代碼。實際上也就是從字 ;CODE 到 END-CODE 的代碼片段為常量類字將指向的機器代碼片斷。 ;CODE 表示一個高級定義的結束(;)和一個機器代碼定義的開始 (CODE) 。但是,它并不在字典中建立兩個分離的字,從 CONSTANT 到 END-CODE 的全部內容都保存在 CONSTANT 的參數域中,如圖 7 所示。

圖 7 ITC 的 ;CODE

Derick 和 Baker [DER82] 使用三個“時間階段”來幫助理解定義字的行為:

時間階段 1

是在 CONSTANT 被定義時的行為。這需要同時引用高級編譯器(對于第一個部分)和 Forth 匯編器(對于第二個部分)。這就是定義 CONSTANT 被加入字典的過程,如圖 7 所示。我們可以看到, ;CODE 這個編譯指示器是在第一個階段被執行的。

時間階段 2

是字 CONSTANT 被執行時的行為,這時一些常數類字被定義,比如:

2 CONSTANT DEUX

這個階段就是字 CONSTANT 被執行、字 DEUX 被加入字典的時候。在這個階段 CONSTANT 的高級定義部分被執行,包括字 (;CODE).

時間階段 3

是常數類執行時的行為。在我們的例子中,這個階段就是 DEUX 被執行而把值 2 推入堆棧的時候。這時 CONSTANT 的機器代碼被執行(回憶 DEUX 的代碼域動作)

字 ;CODE 和 (;CODE) 的工作

;CODE 在時間階段 1 被執行,這是 CONSTANT 被編譯的時候。它是一個 Forth 立即字 -- IMMEDIATE 字 -- 這個字在 Forth 編譯時執行。

;CODE 做以下三件事情:

?? 它把 Forth 字 (;CODE) 編譯到 CONSTANT

?? 它關閉 Forth 編譯器,同時

?? 它打開 Forth 匯編器

而 (;CODE) 是字 CONSTANT 的一部分,它在 CONSTANT 執行的時候才被執行(時間階段 2 ),它執行以下動作:

?? 它得到緊隨其后的機器代碼的地址,這可以通過從 Forth 返回棧中彈出 IP 而實現;

?? 它把這個地址放到 CREATE 定義的字的代碼域中,通過 Forth 字 LAST (有時也稱為 LATEST )等到這個字的地址;

?? 它完成 EXIT 的動作(也稱為 ;S ),這樣 Forth 的內部解釋器就不會把后面的代碼作為 Forth 串線來執行,這是結束 Forth 串線的高級“子程序返回”。

F83[LAX84] 解釋了它們在 Forth 系統中的典型編碼:

: ;CODE

COMPILE (;CODE) / 編譯 (;CODE) 到定義中

?CSP [COMPILE] [ / 關閉 Forth 編譯器

REVEAL / ( 與 ";" 的行為類同 )

ASSEMBLER / 打開匯編器

; IMMEDIATE / 把這個字設為立即字

: (;CODE)

> / 彈出機器代碼地址

LAST @ NAME> / 得到最后一個字的 CA

! / 保存這個代碼地址到代碼域

; /

(;CODE) 字在兩個字當中更加微妙。因為它是一個高級 Forth 定義,在 CONSTANT 中后隨它的地址 -- 高級返回地址 -- 被壓入 Forth 返回棧中,所以在 (;CODE) 中彈出返回棧能夠得到后隨的機器代碼地址。同時,從返回棧中彈出這個值使得一級高級子程序被返回“旁路”,這樣在 (;CODE) 退出的時候,它可以退到 CONSTANT 的調用者。這等效于返回到 COSNTANT ,并使得 CONSTANT 立即返回。通過圖 7 并跟蹤字 CONSTANT 和 (;CODE) 的執行可以更加清楚地看到這是如何工作的。

直接和子程序串線

對于 DTC 和 STC ,;CODE 和 (;CODE) 的動作與 ITC 相同,但是也有一個重要的例外:它不再保存一個地址,而在代碼域中放有 JUMP 或者 CALL 指令。對于一個絕對 JUMP 或 CALL ,可能唯一要做的事情就是把地址保存在代碼域的最后,作為 JUMP 或者 CALL 指令的操作數。在 MC6809 的情況下,地址作為 3 字節 JSR 指令的最后 2 個字節保存。但是某些 Forth 系統比如 Intel 8086 的 Pygmy Forth ,它們在代碼域中使用相對轉移指令。在這種情況下,必須計算相對偏移量并把它們插入到分支指令中。

高級 Forth 行為

你已經看到了如何讓 Forth 字執行一個指定的匯編語言代碼片段,如何向這個片斷傳遞字的參數域地址,但是我們如何用高級 Forth 定義“寫出”子程序的行為呢?

每個 Forth 字必須 -- 通過 NEXT 的行為 -- 執行一些機器語言子程序。這就是代碼域的全部。因此,一個機器子程序、或者一系列子程序需要解決如何訪問高級行為的問題。我們稱這個子程序為 DODOES 。

這里有三個問題需要解決:

?? 我們如何找到與這個字相關聯的高級行為子程序的地址?

?? 我們如何從機器代碼中為調用一個高級行為子程序而訪問 Forth 解釋器?

?? 我們如何向那個子程序傳遞我們正在執行的字的參數域地址?

對于第三個問題的回答是:很容易,用我們為一個高級 Forth 子程序在參數棧上傳遞參數的方法。我們的機器語言子程序在訪問高級串線之前必須把參數域地址推到堆棧上(從我們以前的工作看,我們知道機器語言如何能夠得到 PFA )

第二個問題的答案有一點困難?;旧衔覀兛梢韵?Forth 字 EXECUTE 那樣做一些事情來訪問一個 Forth 字;或者也可能是 ENTER ,它訪問一個冒號定義。它們都是我們的“關鍵”核心字, DODOES 與此類似。

第一個問題好象有些難度。我們把高級子程序的地址放到哪里呢?記住:代碼域并不指向高級代碼,它必須指向機器代碼。在 Forth 的歷史上,人們曾經使用過以下兩種方法。

FIG-Forth 解決方案

FIG-Forth 用參數域的第一個單元來保存高級代碼的地址。 DODOES 子程序通過這個單元得到了參數域的地址,并把實際數據的地址(典型地 PFA+2 )推到棧上,取得高級子程序的地址,然后調用 EXECUTE 。這種方法存在兩個問題:

第一、參數域的結構因機器代碼行為和高級代碼行為而不同。例如一個使用機器代碼的 CONSTANT 可以把它的代碼保存到 PFA ,但是一個使用高級定義的 CONSTANT 行為卻必須把它的數據保存在(典型地) PFA+2 。

第二、每個高級行為類的實例都增加了一個單元的開銷。也就是說,如果 CONSTANT 用于一個高級行為,程序中的每個常數都要增大一個單元!幸運的是,聰明的 Forth 程序員很快就找到了解決這個問題的一種方法, FIG-Forth 方法就不再使用了。

現代的解決方案

大多數 Forth 程序員都為每個高級行為子程序配置了一個不同的機器語言代碼片段。于是,一個高級常數就會有它自己的代碼域,它指向一個機器語言片段,其核心功能就是訪問 CONSTANT 的高級行為;一個高級變量的代碼域將指向一個“ STARTUP ”子程序來實現高級的 VARIBLE 行為,等等。

這種方法會導致代碼的大量重復嗎?不會的。因為這些機器語言片斷只是對通常的啟動子程序 DODOES 的一個調用(不同于 FIG-Forth 的子程序),對 DODOES 高級代碼的地址作為一個“內嵌”子程序參數傳遞。這就意味著,高級代碼的地址被放到 JSR/CALL 指令之后。 DODOES 可以從 CPU 堆棧中彈出,然后通過一次讀取來得到這個地址。

實際上,我們還可以做得更簡單。高級代碼自身是放在 JSR/CALL 指令之后的, DODOES 彈出 CPU 堆棧,直接得到這個地址。因為我們知道這是高級 Forth 代碼,我們可以忽略代碼域,而只編譯高級串線……這就很方便地把 ENTER 的行為集成到了 DODOES 中。

現在每一個“定義”字都指向了一小部分機器代碼 – 沒有浪費任何的參數域空間。這一小部分機器代碼是 JSR 或者 CALL 指令,后隨一個高級行為子程序。在 MC6809 的例子中,我們已經使每個常數的兩個字節用一個 3 字節的 JSR 替代,它只出現一次。

使用這些策略使得在 Forth 內核中包含了許多費解的程序邏輯。所以,讓我們使用我們可信賴的 ITC MC6809 例子來看看實際上這是如何實現的:

圖 8 顯示了使用高級定義實現的 DEUX 常數。當 Forth 解釋器遇到 DEUX -- 也就是說當 Forth 的 IP 寄存器在 IP(1)時 -- 它做通常的事情:它讀取包含在 DEUX 代碼域中的地址,跳轉到那個地址。在那個地址上是一個 JSR DODOES 指令,于是立即發生第二個跳轉 -- 這次是一個子程序調用 。

?

圖 8 ITC DODOES

DODOES 接著必須執行下列動作:

?? 把 DEUX 的參數域地址推到參數棧上,以備將來高級行為子程序使用。因為 JSR 指令并不改變任何寄存器,我們希望 DEUX 的參數域地址(或者“鄰近”的地址)仍然保留在 W 寄存器中;

?? 通過彈出 CPU 堆棧得到高級行為子程序的地址(回憶:彈出 CPU 堆棧可以得到緊隨在 JSR 指令之后的不論什么的地址)。這是一個高級串線,冒號定義的參數域部分;

?? 保存舊的 Forth 指令指針 -- IP(2) -- 到 Forth 返回棧上,因為 IP 寄存器要被用于執行高級代碼。本質上,DODOES 必須“嵌套” IP ,就像 ENTER 一樣。記住 Forth 的返回棧也許不同于 CPU 的子程序堆棧;

?? 把高級串線的地址放到 IP 中,這是圖 8 中的 IP(3) ;

?? 在新的位置上執行 NEXT 以繼續高級解釋;

假設一個間接串線的 ITC MC6809 符合下列情況:

?? W 沒有被 NEXT 增量(也就是 W 將要包含 NEXT 進入字的 CFA )

?? MC6809 的 S 寄存器是 Forth 的 PSP,U 寄存器是 Forth 的 RSP (也就是 CPU 的堆棧不是 Forth 的返回棧)

?? MC6809 的 Y 寄存器是 Forth 的 IP,X 是 Forth 的 W

回憶在這些條件下的 NEXT 定義:

NEXT:

LDX ,Y++ ; (IP) -> W, and IP + 2 -> IP

JMP [,X] ; (W) -> temp, JMP (temp)

DODOES 可以這樣寫:

DODOES:

LEAX 2,X ; 使 W 指向參數域

PSHU Y ; 把舊的 IP 壓入返回棧

PULS Y ; 從 CPU 堆棧上彈了新的 IP

PSHS X ; 壓入參數域地址 W 到參數棧上

NEXT ; 訪問高級解釋器

這些操作并沒有嚴格按順序進行。當然,只要恰當的數據在恰當的時間內進入了恰當的堆棧(或者進入了恰當的寄存器),操作的順序并不要緊。在這里,我們實際上是利用了這樣一個事實:在新的 IP 從 CPU 堆棧中彈出之前,老的 IP 可以壓入 Forth 的返回棧。

在某些處理器上, CPU 的堆棧被用于 Forth 的返回棧。對于這種情況,就需要一個臨時存儲器訪問步驟。同樣是上面的例子,如果我們必須選擇 S=RSP和 U=PSP 則 DODOES 就成了:

DODOES:

LEAX 2,X ; 讓 W 指向參數域

PSHU X ; 把參數域地址 W 壓入參數棧

PULS X ; 從 CPU 堆棧中彈出串線的地址

PSHS Y ; 把舊的 IP 壓入返回棧

TFR X,Y ; 把串線的地址放入 IP

NEXT ; 訪問高級解釋器

因為我們本質上是在交換 IP 和返回棧/CPU 堆棧的內容,所以我們就必須用 X 作為臨時寄存器。于是,我們在重新使用 X 寄存器之前就必須把 PFA -- (A)壓入堆棧。

我們就是要這樣一步一步地研究這些 DODOES 例子,追蹤兩個堆棧和全部寄存器的內容。我自己就經常研究自己編寫的 DODOES 子程序,以確信任何一個寄存器都沒有在錯誤的時刻被亂用。

直接串線

DODOES 的邏輯在 DTC 中是一樣的。但是我的實現卻是不同的,這依賴于 DTC Forth 在一個字的代碼域中是使用 JMP 還是使用 CALL 。

在代碼域中使用 JMP。如果將要被執行的字的地址可以在寄存器中得到,則一個 DTC Forth 就可以在代碼域中使用 JMP,這就很像代碼域地址。從 DODOES 的觀點看,這與 ITC 是一樣的。

在我們的例子中, DODOES 知道 Forth 解釋器跳轉到了與 DEUX 相關的機器代碼,那個代碼是JSR 到 DODOES 。現在每個跳轉是使用直接跳轉還是使用間接跳轉并沒有什么關系,寄存器和堆棧的內容是相同的。所以,DODOES 的代碼與 ITC 是相同的(當然,NEXT 是不同的, W 也許要有不同的偏移量指向參數域)。

在 DTC 的 MC6809 中,我們從來就沒有顯式地讀取將要執行字的 CFA ,所以 Forth 字必須在它的代碼域中包含一個 JSR ,這樣我們就可以通過堆棧得到這個字的參數域地址,而不是從堆棧中得到。這種情況下的 DEUX 例子顯示在圖 9 中。

圖 9 DTC 的 DODOES

當 IP 在 IP(1)時, Forth 解釋器跳轉到 DEUX 的代碼域(同時增量 IP)。在代碼域中,是一個到 DEUX 機器代碼片斷的 JSR ,在那里是第二個 JSR ,到 DODOES 。于是兩個地址進入了 CPU 堆棧。

第一個 JSR 的返回地址是 DEUX 的參數域地址,第二個 JSR 的返回地址 -- 在 CPU 堆棧的最上面 -- 是將要執行的高級串線地址。 DODOES 必須確保舊的 IP 已經壓入到返回棧, DEUX 的 PFA 壓入了參數堆棧,高級串線的地址被裝入到 IP 中。這些對于堆棧分配是非常敏感的!對于 S=PSP(CPU 堆棧)和 U=RSP , NEXT 和 DODOES 的代碼變成了:

NEXT:

LDX [,Y++] ; (IP) -> temp, IP+2 -> IP, JMP (temp)

DODOES:

PSHU Y ; 把舊的 IP 壓入返回棧

PULS Y ; 從 CPU 堆棧中彈出新的 IP 。注意: CPU 堆棧是參數棧,最頂的元素現在正是我們需要的字的 PFA

NEXT ; 訪問高級解釋器

我們可以自己看一下 NEXT、DEUX、DODOES 壓入一項目 -- DEUX 的 PFA-- 到參棧的全過程。

子程序串線

圖 10 顯示了一個 MC6809 STC 的 DEUX 高級行為的例子。在進入 DODOES 的時候,三個數據被壓入了CPU/RETURN 的返回棧:“主串線”的返回地址、 DEUX 的 PFA、DEUX 的高級行為代碼的地址。DODOES 必須彈出最后兩個,把 PFA 壓入參數棧,跳轉到行為代碼:

?

圖 10 STC 的 DODOES

MC6809 的 DODOES 現在是一個 3 指令的子程序。它甚至可以通過“把 JSR DODOES 變成內嵌方法”來進一步簡化。也就是說用等效的機器代碼來代替 JSR DODOES 。由于簡化了一個 JSR ,也就簡化了堆棧的處理:

PULS X ; 從 CPU 堆棧中彈出 PFA

PSHU X ; 把它壓入參數棧

…… ; DEUX 的其它高級串線

這里使用了 4 字節的顯式代碼代替了 3 字節的 JSR 指令,從而相當有效地提高了執行的速度。對于 MC6809 這也許是一個很好的選擇,對于像 8051 這樣的處理器, DODEOS 則顯得太長了,大概應該還是作為一個子程序為好。

使用 DOES>

我們已經學習了使用 ;CODE 去創建一個 Forth 字,它的參數域中可以包含有任意的數據,以及如何使一個字的代碼域指向新的機器代碼片斷。那么我們如何編譯一個高級行為子程序并用一個新的字指向它呢?

答案依賴于兩個 Forth 字 DOES> 和 (DOES>) ,它們是 ;CODE 和 (;CODE) 的高級定義等效。為了理解它們,讓我們看一個使用它們的例子:

: CONSTANT ( n -- )

CREATE / 創建新的字

, / 把 TOS 值加入字典作為參數域的第 1 個單元

DOES> / 結束 " 創建部分 " 開始 " 行為 " 部分

@ / 給出 PFA ,得到它的內容

;

把這些與前面的 ;CODE 例子比較,可以看到 DOES> 執行的功能與 ;CODE 類似。從 : CONSTANT 到 DOES> 的每個行為都是在 CONSTANT 字執行時被訪問的。這是構建一個“定義”字的參數域和代碼。從 DOES> 到 ; 的代碼是 COSNTANT 的“孩子”(比如 DEUX )被訪問時執行的高級代碼,也就是代碼域將要指向的高級代碼片斷。(我們會看到 JSR DODOES 包含在這個高級代碼片斷之前)。

與 ;CODE 一樣,“CREATE”和“ACTION”子句都在 Forth 字 CONSTANT 體中,如圖 11 所示。

圖 11 ITC 的 DODOES

回憶時間序列 1 、 2 、 3 ,字 DOES> 和 (DOES>) 做下例事情:

?? 它把 Forth 字 (DOES>) 編譯到 CONSTANT 中;

?? 它把一個 JSR DODOES 編譯到 CONSTANT 中;

注意 DOES> 保持 Forth 編譯器一直運行,這樣可以保證后面的高級代碼片斷繼續得到編譯。同樣,盡管 JSR DODOES 本身不是 Forth 代碼,但是像 DOES> 這樣的立即字可以使它編譯到 Forth 代碼中。

(DOES>) 是字 CONSTANT 的一部分,所以在 CONSTANT 被執行的時候(時間序列 2 )執行,它做下例事情:

?? 它通過從 Forth 的返回棧中彈出 IP 得到緊隨其后的機器碼的地址( JSR DODOES );

?? 它把這個地址放到被 CREATE 剛剛定義的字的代碼域中。

?? 它執行 EXIT 行為,使得 CONSTANT 在這里中斷而不再執行后面的代碼片斷。

(DOES>) 的行為和 (;CODE) 是一樣的!所以 Forth 系統并不需要另外定義一個新的字。例如 F83 系統在 ;CODE 和 DOES> 中同時使用 (;CODE) 。我也從現在開始使用 (;CODE) 代替 (DOES>).

你已經看到了 (;CODE) 是如何工作的。 F83 是這樣定義 DOES> 的

: DOES>

COMPILE (;CODE) / 編譯 (;CODE) 到定義中

0E8 C, / CALL 指令的操作碼字節

DODOES HERE 2+ - , / 把相對轉移寫入 DODOES

; IMMEDIATE

這里 DODOES 是一個常數,它保存有 DODOES 子程序的地址(實際使用的 F83 源代碼和這里所說的有一點點兒不同,因為 F83 使用的 META 編譯器有不同的要求)。

DOES> 不需要改變 CSP 或者 SMUDGE 位,因為 Forth 編譯器的狀態是 'on.' 。在 Intel 8086 的情況下, CALL 指令使用相對地址,因此,需要對 DODOES 和 HERE 做一個算術運算。在 MC6809 中, DOES> 看起來像這樣的:

: DOES>

COMPILE (;CODE) / 把 (;CODE) 編譯進定義

0BD C, / JSR 擴展操作碼

DODOES , / 操作數: DODOES 的地址

; IMMEDIATE

你可以看到一個機器語言 JSR DODOES 是如何被編譯到高級 (;CODE) 之后和高級行為之前的。

直接和間接串線

DTC 和 STC 中的唯一區別是代碼域必須修改以指向新的子程序。這是由 (;CODE) 完成的,所要求的改變已經描述過了。 DOES> 沒有任何影響,除非你在 STC 中把 JSR DODOES 擴展成為顯式的機器代碼。在這種情況下, DOES> 被修改成匯編“內嵌”的機器代碼而不是 JSR DODOES 子程序。

思前想后

我們可能從來就沒有想到,這么幾行代碼會引出這么多的內容。這也是為什么我特別贊賞 ;CODE 和 DOES> ,說實在的,我從來也沒有見到過用這么經濟的方法就實現了這么復雜、強大和靈活的結構。

參考文獻

[DER82] Derick, Mitch and Baker, Linda, Forth Encyclopedia, Mountain View Press (1982). A word-by-word description of fig- Forth in minute detail. Still available from the Forth Interest Group, P.O. Box 2154, Oakland CA 94621.

[LAX84] Laxen, H. and Perry, M., F83 for the IBM PC, version 2.1.0 (1984). Distributed by the authors, available from the Forth Interest Group or GEnie.

第四部分 匯編器還是 META 編譯器

撰寫本文的過程貫穿著一個指導思想:“保持最短”。本著這個原則,我把源程序列表安排到另外的地方,對此我表示歉意?,F在,我們主要試圖討論以下話題:

你如何開始構造一個 Forth 系統?

你現在已經知道了,主要的 Forth 程序代碼是高級串線,它們通常被編譯成一系列地址。在 FIG-Forth 時代和早期的 Forth 實踐中,匯編語言是唯一可用的程序設計語言工具。匯編語言對于編寫 Forth 的 CODE 字是非常好的,但是高級串線必須用一系列的 DW 偽指令來編寫。例如, Forth 字:

: MAX ( n n - n) OVER OVER < IF SWAP THEN DROP ;

必須寫成

DW OVER,OVER,LESS,ZBRAN

DW MAX2-$

DW SWAP

MAX2: DW DROP,SEMIS

后來,由于可以實際工作的 Forth 系統越來越普及, Forth 編寫者開始把 Forth 編譯器修改成為交叉編譯器,通過運行在 CP/M (或者蘋果 II ,或者其它任何什么微機系統)的 Forth 系統,你就可以為其它 CPU 編寫 Forth 程序、更改 Forth 系統或者為那個 CPU 編寫一個全新的 Forth 系統。

由于是從 Forth 內部創建一個全新的 Forth 系統,這種編譯器被稱為“META 編譯器”。計算機科學的學究們反對這樣的稱謂,所以有些 Forth 編寫者仍然使用“交叉編譯”和“重新編譯”的術語,這兩個術語之間的差異是:“重新編譯”只能為相同的 CPU 產生新的 Forth 系統。

現在大多數 PC 機上的 Forth 都是通過 META 編譯產生的,但是,在嵌入式系統領域卻產生了意見分歧。

使用匯編器編寫 Forth 系統的觀點認為:

?? META 編譯器神秘難懂,你必須完全理解一個 META 編譯器然后才能使用它;

?? 一般的程序員都懂得匯編器;

?? 匯編器對于一個新的 CPU 總是可用的;

?? 匯編器處理許多優化(比如長短調用格式);

?? 匯編器處理前向引用和特殊的尋址模式,而許多 META 編譯器通常不能做到;

?? 匯編程序員可以使用熟悉的編輯器和調試工具;

?? 代碼的產生是完全可見的,沒有向程序員“隱藏”任何東西;

?? 改變 Forth 模型非常容易,而許多設計的考慮卻影響 META 編譯器的內部;

使用 META 編譯器的觀點認為:

?? 你是在編寫“正?!钡?Forth 代碼,它當然易于閱讀和調試;

?? 一但你理解了你的 META 編譯器,你就可以很容易地把它移植到新的 CPU 上 ;

?? 你需要的唯一工具就是你的計算機上的 Forth 系統;這一點對于沒有 PC 的人特別實用,因為現在的許多交叉匯編器要求 PC 機或者工作站。

我用各種方式編寫過幾個 Forth 系統,所以要作出選擇是很痛苦的。我傾向于使用 META 編譯器:我發現 Forth 的 MAX 代碼比等效的匯編代碼易讀、易懂。反對使用 META 編譯器的觀點許多已經被現代的“專業”編譯器所克服,如果你使用 Forth 工作,我強烈建議你考慮一個商業化產品。唉,公共的 META 編譯器(包括我自己的)依然落后于時代,笨拙同時神秘。

所以我準備為 Forth 程序員提供基本的材料,告訴你作出自己的選擇。我將給出 META 形式的MC6809 代碼,為 F83 ( IBM PC CP/M ST )提供 META 編譯器。 Z80 代碼將使用 CP/M 匯編器寫成。 8051 代碼使用公共的 PC 交叉匯編器編寫。

使用 C 語言編寫 Forth 系統?

如果不討論“用C語言編寫 Forth 系統”這個新的方法,本文就將是不完全的。 C 語言比匯編器具有更好的可移植性 -- 理論上,你所要作的全部工作就是為任何 CPU 重新編譯相同的代碼。

這種方法的缺點是:

?? 在設計決策選擇上更缺少靈活性;比如,不可能實現直接串線編碼,也不可能優化寄存器的分配;

?? 增加原語之后,你必須重新編譯 C 源代碼;

?? 某些 C 語言實現的 Forth 使用了效率很低的串線技術,比如多個 CASE 語句;

?? 大多數 C 編譯器產生的代碼比匯編語言程序員的代碼低效;

但是對于那些 UNIX 系統和不支持匯編語言編程的 RISC 工作站來說,這卻是 Forth 得以運行的唯一方法。最完全和廣泛使用的公共域 C 語言 Forth 系統是 TILE 。如果你沒有運行 UNIX 系統,你可以看一下文件 HENCE4TH_1.2.A 。

為了繼續以前的比較,還是先來看看 HENCE4TH 的 MAX 定義。為了清楚起見,我略去了字典頭:

_max()

{

OVER OVER LESS IF SWAP ENDIF DROP

}

不使用匯編器,用 C 語言編寫核心 CODE 定義,比如,這是 HENCE4TH 的 SWAP 定義:

_swap()

{

register cell i = *(dsp);

*(dsp) = *(dsp + 1);

*(dsp + 1) = i;

}

請注意:用 C 編寫 Forth 字有非常不同的技術,所以這些字在 CForth 和 TILE 中可能差異非常大。

在 MC68000 或者 SPARC 工作站上,這樣的編碼可以產生非常好的代碼。不過,一但你計劃用 C 來實現 Forth ,你也需要理解用匯編語言實現 Forth 是怎樣工作的。所以請繼續閱讀本文。

參考文獻

[CAS80] Cassady, John J., METAForth: A Metacompiler for Fig- Forth , Forth Interest Group (1980).

[MIS90] HenceForth in C , Version 1.2, distributed by The Missing Link, 975 East Ave. Suite 112, Chico, CA 95926, USA (1990). This is a shareware product available from the GEnie Forth Roundtable.

[ROD91] Rodriguez, B.J., letter to the editor, Forth Dimensions XIII:3 (Sep/Oct 1991), p.5.

[ROD92] Rodriguez, B.J., "Principles of Metacompilation," Forth Dimensions XIV:3 (Sep/Oct 1992), XIV:4 (Nov/Dec 1992), and XIV:5 (Jan/Feb 1993). Note that the published code is for a fig-Forth variant and not F83. The F83 version is on GEnie as CHROMIUM.ZIP

[SER91] Sergeant, Frank, "Metacompilation Made Easy," Forth Dimensions XII:6 (Mar/Apr 1991).

[TAL80] Talbot, R.J., fig-Forth for 6809 , Forth Interest Group, P.O. Box 2154, Oakland, CA 94621 (1980).

[TIN91] Ting, C.H., "How Metacompilation Stops the Growth Rate of Forth Programmers," Forth Dimensions XIII:1 (May/Jun 1991), p.17.

第五部分 Z80 原語

我提交的代碼

最后,我準備展示一個(我希望是) ANSI 兼容的編譯器 : CAMEL Forth ,包括它的全部源碼。作為一個很好的練習 -- 也為了版權的原因 -- 我重新編寫了全部的代碼(你知道不看優秀的代碼實例有多困難嗎?!)。當然,我在不同的 Forth 系統上的經驗無疑影響著所選擇的設計策略。

由于空間所限,源代碼分成四個部分安裝(如果你已經等不及了,可以去 GENIE 下載全部的文件)

?? Z80 Forth “原語”: 用匯編語言編寫

?? 8051 Forth “原語”: 也用匯編語言編寫

?? Z80/8051 高級內核 同樣

?? 完全的 6809 內核: 使用 META 編譯器的源文件

我計劃盡量使用公共軟件來實現 CAMEL Forth :對于 Z80 ,使用 CP/M 下的 Z80R 匯編工具;對于 8051 ,使用 IBM PC 上的 A51 交叉匯編器,對于MC6809 ,使用我自己的 F83 For CP/M IBM PC ATARI ST 工具。

我這里的“核心”是指組成一個基本 Forth 系統的一系列字,包括編譯和解釋字。對于 CAMEL Forth 來說,這些就是 ANS Forth 規定的核心字再加上為了實現這些核心字所需要的非 ANSI 字。 Forth 核心通常由兩部分組成:一部分是用機器代碼寫成的(即 CODE 字),另一部分是高級定義字,用機器代碼寫成的字稱為“原語”,因為在最后的分析中,全部的 Forth 系統就是由這些字組成的。

嚴格地說,哪些字應該用機器代碼來寫呢?選擇這些原語是一個有趣的任務。一個小原語集合可以簡化移植,但性能肯定很糟。我聽說過只用 13 個原語就能夠定義 Forth 的情況 - 當然這是一個很慢的 Forth 系統。 eForth 是一個以可移植性作為設計目標的 Forth 系統,它有 31 個原語。

而我的原則是這樣的:

?? 基本的算術、邏輯運算以及存儲器操作用 CODE 實現 ;

?? 如果一個 Forth 字不能簡單有效地用一系列的 Forth 字編寫,則它應該用 CODE 實現(如 U<, RSHIFT 等) ;

?? 如果一個簡單的字頻繁使用,則用 CODE 實現是值得的(如 NIP , TUCK );

?? 如果一個字用 CODE 編寫時并不需要多少字節,則用 CODE 實現;

?? 如果一個處理器包含有實現一個字所需要的功能,則用 CODE 編寫。比如,在 Z80 或者 8086 上,有 CMOVE 或者 SCAN 指令;

?? 如果一個字主要是搗弄堆棧上的參數,但是邏輯非常簡單,應該用 CODE 實現,這里參數可以放到寄存器中;

?? 如果一個字的控制和邏輯功能復雜,則它最好用高級定義實現;

對于 Z80 的 CAMELForth ,我使用了大約 70 個原語(見表 1 )。

序號

名稱

進入時 -- 時堆棧

描述

核心字:這些是 ANS Forth 文檔要求的核心定義

1

!

x a-addr --

把一個單元數存入存儲器

2

+

1/u1 n2/u2 -- n3/u3

加法 n1+n2

3

+!

n/u a-addr --

加一個單元到存儲器

4

-

n1/u1 n2/u2 -- n3/u3

減法 n1-n2

5

<

n1 n2 – flag

測試 n1<n2, 有符號數

6

=

x1 x2 – flag

測試 x1=x2

7

>

n1 n2 – flag

測試 n1>n2, 有符號數

8

>R

x -- R: -- x

壓入返回棧

9

?DUP

x -- 0 | x x

如果棧頂元素非0則復制

10

@

a-addr – x

從存儲器中讀取一個單元

11

0<

n – flag

如果 TOS 為負則為真

12

0=

n/u – flag

如果 TOS=0 則為真

13

1+

n1/u1 -- n2/u2

加 1 到 TOS

14

1-

n1/u1 -- n2/u2

從 TOS 中減 1

15

2*

x1 -- x2

算術左移

16

2/

x1 -- x2

算術右移

17

AND

x1 x2 -- x3

邏輯 AND

18

CONSTANT

n --

定義一個 Forth 常數

19

C!

c c-addr --

把字符存入存儲器

20

C@

c-addr – c

從存儲器讀取字符

21

DROP

x --

去除棧頂元素

22

DUP

x – x x

復制棧頂元素

23

EMIT

c --

向控制臺輸出字符

24

EXECUTE

Forth word 'xt'

執行棧頂的 Forth 字

25

EXIT

--

退出一個冒號字義

26

FILL

c-addr u c --

用字符填充存儲器

27

I

-- n R: y1 y2 -- y1 y2

得到內層的循環計數

28

INVERT

x1-- x2

位反轉

29

J

-- n R: 4*y -- 4*y

得到第二個循環計數

30

KEY

-- c

從鍵盤輸入一個字符

31

LSHIFT

x1 u -- x2

邏輯左移 u 位

32

NEGATE

x1 -- x2

2 的補碼

33

OR

x1 x2 -- x3

邏輯 OR

34

OVER

x1 x2 -- x1 x2 x1

復制次棧項

35

ROT

x1 x2 x3 --x2 x3 x1

棧頂三元素旋轉

36

RSHIFT

x1 u -- x2

邏輯右移 u 位

37

R>

-- x R: x --

從返回棧頂彈出

38

R@

-- x R: x – x

讀取返回棧

39

SWAP

x1 x2 -- x2 x1

交換棧頂的兩個項目

40

UM*

u1 u2 – ud

無符號 16x16->32 乘法

41

UM/MOD

ud u1 -- u2 u3

無符號 32/16->16 除法

42

UNLOOP

-- R: sys1 sys2 --

退出循環參數

43

U<

u1 u2 – flag

測試 u1<n2, 無符號

44

VARIABLE

--

定義一個 Forth 變量

45

XOR

x1 x2 -- x3

邏輯異或

擴展字:這些可選擇的字也是 ANS Forth 文檔定義的

46

<>

x1 x2 – flag

測試不相等

47

BYE

i*x --

返回到 CP/M 操作系統

48

CMOVE

c-addr1 c-addr2 u --

從底移動字節

49

CMOVE>

c-addr1 c-addr2 u --

從頂移動字節

50

KEY?

-- flag

如果在鍵盤上按了鍵則返回真

51

M+

d1 n -- d2

加無符號數到雙精度數

52

NIP

x1 x2 -- x2

去除次棧頂

53

TUCK

x1 x2 -- x2 x1 x2

見堆棧圖示

54

U>

u1 u2 – flag

測試 u1>u2, 無符號

個人擴展:這些字只屬于 CamelForth 實現

55

(do)

n1|u1 n2|u2 -- R:-- y1 y2 DO 的運行時間代碼

56

(loop)

R: y1 y2 -- | y1 y2

LOOP 的運行時間代碼

57

(+loop)

n -- R: y1 y2 -- | y1 y2 +LOOP 的運行時間代碼

58

><

x1 -- x2

交換字節

59

?branch

x --

如果 TOS 為 0 則跳轉

60

BDOS

DE C – A

調用 CP/M BDOS 功能

61

branch

--

無條件分支

62

Lit

-- x

內嵌文字常數放到堆棧上

63

PC!

c p-addr --

把字符輸出到口上

64

PC@

p-addr – c

從口輸入字符

65

RP!

a-addr --

設置返回棧指針

66

RP@

-- a-addr

得到返回棧指針

67

SCAN

ca1 u1 c -- ca2 u2

尋找匹配的字符

68

SKIP

ca1 u1 c -- ca2 u2

跳過匹配的字符

69

SP!

A-addr --

設置數據棧指針

70

SP@

-- a-addr

得到數據棧指針

71

S=

ca1 ca2 u – n

串比較 n<0: s1<s2, n=0: s1=s2, n>0: s1>s2

72

USER

n --

定義用戶變量 'n'

堆棧解釋

R: = 返回堆棧

c = 8位字符

flag = 布爾(0 或者 -1)

n = 有符號16位

u = 無符號16位

d = 無符號32位

ud = 無符號32位

+n = 無符號15位

x = 任何的單元值

i*x j*x = 任何的單元值

a-addr = 對齊的地址

ca = 字符地址

p-addr = I/O 口地址

y = 系統指定

在確定了 Forth 模型和它所使用的目標 CPU 之后,我依照下列過程進行開發:

?? 選擇一個 ANSI 核心字子集作為原語;

?? 按照 ANSI 的描述,編寫這些字的匯編定義,加入處理器的初始化代碼;

?? 運行匯編器,定位源程序的錯誤;

?? 測試產生的匯編代碼。我通常的做法是加上幾行匯編代碼,使得程序能夠在初始化完成之后輸出一個字符,這是一個非常關鍵的測試,它保證了你的硬件、匯編器、下載器( EPROM 編程器或者其它什么東西)、串行通訊口統統工作正常! (只對嵌入式系統)加入另外的匯編代碼段以讀取串口并回送,這樣就可以測試雙向通訊了;

?? 寫一個高級 Forth 代碼片斷以輸出一個字符,這個代碼段只使用 Forth 原語(通常是這樣的: LIT 33h EMIT BYE ),這就可以測試 Forth 寄存器的初始化、堆棧和串線機制。這個階段的問題可以通過追蹤 NEXT 、初始化、數據堆棧的邏輯錯誤來定位,比如把一個堆棧設置到了 ROM 中;

?? 寫一個冒號定義輸出一個字符,把這個定義包含在上面的高級定義片斷中,比如定義 : BLIP LIT 34 EMIT EXIT ; 然后測試代碼片段 LIT 33h EMIT BLIP BYE 。這個階段的問題通常與 DOCOLON 、 EXIT 、返回棧有關。

?? 現在可以編寫一些工具來輔助開發,比如顯示堆棧上的 16 進制數等等。列表 1 是一個簡單的測試子程序,它運行一個永不停止的存儲器 DUMP 動作(這個代碼片斷在用戶的輸入鍵盤不能工作時也可以使用)。這個程序測試原語 DUP 、 EMIT 、 EXIT 、 C@ 、 >< 、 LIT 、 1+ 和 BRANCH ,也測試了幾級嵌套。但它不使用 DO …… LOOP ,因為這個結構要正確工作通常比較難。當這些代碼可以運行后,你就會對自己的 Forth 模塊是否有效樹立一些信心。

接著測試其它的原語,其中 DO …… LOOP, UM/MOD, UM* 和 DODOES 必須嚴格,最后再加入高級定義。

閱讀源代碼!

如果你希望學習更多的 Forth 內核工作原理和它的編寫方法,學習列表 2 。這個列表遵循了下面的一些 Forth 文檔表示格式:

WORD-NAME stack in -- stack out description

其中 WORD-NAME 是 Forth 可以識別的字,由于這些字經常包含一些特殊的ASCII 字符,所以必須用一個近似的名字做為這個字的匯編語言標號,比如 OENPLUS 是字 1+ 的匯編語言標號。

stack in 是這個字希望輸入的棧上參數,最右邊總是棧頂元素, stack out 是這個字留在棧上的參數。

如果一個字影響返回棧,則會給出一個返回棧說明,用 R: 表示,比如:

stack in -- stack out R: stack in -- stack out

ANSI Forth 對數值堆棧參數給出了一個簡化的表示,通常 n 是長度為一個單元 CELL 的有符號數, u 是長度為一個單元的無符號數, c 是一個字符,等等,見表 1 。

參考文獻

[1] Definition of a camel: a horse designed by committee.

[2] Ting, C. H., eForth Implementation Guide , July 1990, available from Offete Enterprises, 1306 South B Stret, San Mateo, CA 94402 USA.

[3] Z80MR, a Z80 Macro Assembler by Mike Rubenstein, is public-domain, available on the GEnie CP/M Roundtable as file Z80MR-A.LBR. Warning: do not use the supplied Z1.COM program, use only Z80MR and LOAD. Z1 has a problem with conditional jumps.

[4] A51, PseudoCorp's freeware Level 1 cross-assembler for the 8051, is available from the Realtime and Control Forth Board, (303) 278-0364, or on the GEnie Forth Roundtable as file A51.ZIP. PseudoCorp's commercial products are advertised here in TCJ.

Z80 CamelForth 的源代碼在下列站點上可用 ftp://ftp.zetetics.com/pub/forth/camel/cam80-12.zip .

第六部分 Z80 高級內核

更正

在 TCJ#67 上發表的 CAMEL80.AZM 文件有兩個錯誤。一個主要的錯誤是 Forth 字 > 的宏定義名字頭長度誤為 2 ,實際上應該是 1 。另一個次要的錯誤是 CP/M 的控制臺 I/O 。 KEY 必須返回所打的字符,所以使用了 BDOS 功能 6 。 KEY?不能返回字符,使用 BDOS 功能 11 以測試當前是否有鍵按下。不幸的是, BDOS 功能6不清除功能 11 檢測是按下的鍵。我現在重新編寫了 KEY? 以使用 BDOS 功能 6 。因為這是一個“破壞性”的測試,我就必須保持已經“消耗”的鍵,并在下一次的 KEY 調用中返回。這個新的邏輯可以用于任何硬件只提供“破壞性”測試的場合。

高級定義

在上一次討論中,我沒有展開源代碼。每一個“原語”執行一個小的、明確定義的功能。它幾乎全部是 Z80 匯編代碼,就算是我沒有說清楚為什么原語中包含了一個特別的字,我也希望讀者明白每一個字是做什么的。在這一部分里,我可就不能這樣“奢華”了:我將要給出 Forth 語言的邏輯。許多書中描述了 Forth 內核,如果你希望完全掌握它,就請去買上一本。對于 TCJ 我將限制自己只給出編譯器和解釋器的關鍵字和給出清單 2.

文本解釋操作

文本解釋器或者稱為“外層解釋器”是一些從鍵盤上接收輸入并執行所要求 Forth 操作的 Forth 代碼(這與地址或者“內層解釋”器 NEXT 不同,后者執行編譯之后的串線代碼)。理解這些代碼的最好方式是看 Forth 系統的啟動。

CP/M 入口點(參看上一部分)檢測可用內存的頂部,設置堆棧指針( PSP 、 RSP )和用戶指針( UP ),建立如圖 1 所示的存儲器映象,然后設置“內層”解釋器指針( IP )以執行 Forth 字 COLD 。

圖 1 Z80 CP/M CAMELForth 儲器映象

?

COLD 通過啟動表初始化用戶變量,然后執行字 ABORT 。(COLD 也試圖從 CP/M 命令行執行 Forth 命令)。

ABORT 復位參數棧指針并執行 QUIT 。

QUIT 復位返回棧指針、 LOOP 棧指針、解釋狀態、然后開始執行命令(之所以要把名字這樣進行區別是因為 QUIT 可以用于退出應用程序,并返回到 Forth 的頂層。不像 ABORT, QUIT 保留了參數棧的內容)。

QUIT 是一個無限循環,它從鍵盤 ACCEPT 一行輸入,然后作為 Forth 命令調用 INTERPRET 。當沒有編譯錯誤時, QUIT 在每一行之后打印一個 Ok.

INTERPRET 幾乎是 ANS Forth 文檔 3.4 部分所給算法的逐字翻譯。它分析一個由空格分開的輸入串,試著用 FIND 把字串對應一個已經定義的 Forth 字。如果找到了一個字,則字或者被執行(如果這是一個 IMMEDIATE 立即字,或者是處于 STATE = 0 的“解釋狀態”)或者編譯到字典(如果在編譯狀態, STATE<>0 )。如果沒有找到, INTERPRET 就試著把字串編譯成數字。如果成功, LITERAL 或者把它放到參數棧(如果在“解釋狀態”)或者編譯成一個在線文字量(如果在編譯狀態)。如果這不是一個 Forth 字也不是一個合法的數,就顯示一個錯誤信息,解釋器執行 ABORT ,這個過程將一個字串一個字串地重復,直到輸入行的結尾。

Forth 字典

那么,解釋器如何通過名字“找到”一個 Forth 字呢?答案是: Forth 維護一個含有所有 Forth 名字的字典。每個名字都通過某種方式與它的可執行代碼相關聯。

有許多辦法可以保存用于查找的名字串:一個簡單的數組,一個鏈表,多重鏈表, HASH 表等。幾乎所有方法的都可以使用 -- Forth 的全部要求只是:如果你所查找的項目重名,那么最后定義的名字需要最先被找到。

我們也可以擁有幾個名字的集合(在新的 ANSI Forth 中,把這種集合稱為“詞匯表”)。這就允許你在不丟失一個名字原來意義的情況下再用這個名字。例如,你可以有一個整數的 + ,一個浮點的 + ,甚至是一個字符串的 + 。這在面向對象的系統中被稱為“運算符重載”。

每個字符串可以與它的可執行代碼通過鄰近的物理存儲器相聯系 -- 比如,名字在可執行代碼之前,這通常被稱為 Forth 字的首部。字符串也可以集中存放在不同的存儲器區域中,與可執行代碼通過指針連接(這種情況稱為“分離的首部”)。

你甚至可以有無名的 Forth 代碼片斷,只要你永遠不需要找到它們或者解釋它們。 ANSI 只要求 ANS Forth 字是可以找到的。

關于字典的設計策略可以寫成另一篇論文。 CAMEL Forth 使用的是最簡單的策略:一個簡單鏈表,定位在可執行代碼之前。沒有字匯表,也許我可以在以后的 TCJ 論文中加入這一能力。

字的首部結構

這里還有一個問題需要討論:在首部中需要什么數據?如何存儲它們?

最少的數據是名字、優先位、(顯式的或者隱式的)到可執行代碼的指針。為了簡單起見, CAMEL Forth 把名字作為一個“計數字符串”存儲(一個字節的長度,后面是 N 個字符)。早期的 Forth Inc. 產品只存儲名字串的長度和前 3 外字符。 FIG-Forth 使用不同的緊縮方法,用最后一個字符的 MSB 位置 1 來標識名字的最后一個字符,而不需要長度字節,其它的 Forth 系統也使用了緊縮字符串,我想甚至 C 風格的 NULL 字符串也是可以使用的。

“優先位”是一個標志,用于指示這個字是一個立即字( IMMEDIATE ),這種字在編譯時也被執行,以實現 Forth 的編譯指示和控制結構。也有其它方法實現編譯指示,例如,可以把它們放在一個單獨的字典中,等等。許多 Forth 系統直接把這個位保存在長度字節中。我使用了一個分離的字節,這樣可以把一個“通?!钡拇僮鞣糜谧执拿植僮?#xff08;比如在 FIND 中的 S= 和 WORDS 中的 TYPE )。

如果把名字保存在一個鏈表中,就需要有一個鏈。通常最后的字是在鏈表的前面,而鏈指向前一個字。這符合 ANSI (和大多數系統)對重定義字的要求。 Charles Curley 研究了 LINK 域的放置位置,發現如果把這個域放置在名字之前(而不是像 FIG-Forth 那樣在名字之后),由可以加快編譯的速度。

圖 2 是 CAMELForth 字首部結構,并與 FIG-Forth F83 Pygmy Forth 系統的首部做了比較。 F83 和 Pyhmy 的 "VIEW" 字段可以作為一個例子,它說明了如何把其它有用的信息保存在 Forth 首部中。

注意:把一個“頭”(首部)和“體”(可執行代碼部分)區別開來是非常重要的。它們并不需要存儲在一起。首部只是在編譯和解釋時才需要,一個“純的可執行”Forth 系統并不需要全部的首部。但是,對于一個合法的 ANSI Forth 系統來說,首部必須存在 -- 至少是 ANSI Forth 字集中的那些字必須有首部。

當從匯編代碼“編譯”一個 Forth 系統時,你可以定義宏來構建這個首部(參看 CAMELZ80.AZM 的 HEAD IMMED )。在 Forth 環境中,首部和代碼域都是由 CREATE 構建的。

編譯操作

我們已經具備了理解 Forth 編譯器的足夠知識。字 :開始一個新的高級字定義,首先為字創建一個頭(CREATE),改變它的代碼域到“DOCOLON”(!COLON),然后轉為編譯狀態。

回想一下,在編譯狀態下,文本解釋器遇到的每一個字都編譯進字典而不是立即執行。這個過程一直繼續,直到文本解釋器遇到了字“;”。這個字是一個立即字,它被立即執行,編譯一個 EXIT 到定義的結尾,然后切換到解釋狀態([) 。

同時,“:”隱藏這個新字,而“;”把這個新字顯示出來(通過清除首部或者名字中的 smudge 位),這就允許 Forth 字可以按“自我優先級”的方式重新定義。為了強制使這個被定義的字遞歸調用,需要使用字 RECURSE 。

我們可以看到, Forth“編譯器”與 C 或者 PASCAL 編譯器并沒有區別。 Forth 編譯器包含有不同Forth 字的動作,這就使得改變或者擴充編譯器變得很容易,但如果沒有一個“內建”的編譯器,則創建一個 Forth 應用就會特別困難。

相關的字集

還有許多其它的 Forth 字,它們是:

實現編譯器或者解釋器的需要,或者

提供編程的方便性

但是有一個字集應該引起特別的重視,那就是我放入文件 CAMEL80D.AZM 的那些字。

ANSI Forth 標準的一個目標就是向應用程序員隱藏 CPU 和相關的實現模型(直接或者間接串線、 16 位還是 32 位)。為了實現這個目的,需要向標準增加了幾個字。我把這個要求更向前推動了一步,努力把這些模型相關問題包裝到內核中。在理想的情況下,在文件 CAMEL80H.AZM 中的高級 FORH 代碼對于所有的 CAMEL Forth 目標應該是相同(盡管不同的匯編程序會有不同的語法)。

單元尺寸的差異和字的對齊要求由 ANS Forth 字 ALIGN ALIGNED CELL+ CELLS CHAR+ CHARS 和我自己附加的字 CELL ( 等效于 1 CELLS, 但編譯之后會更小 ) 來管理。

字 COMPILE 、 !CF 、 CF 、 !COLON 和 EXIT 隱藏了串線模型的特性,比如:串線是如何表示的、代碼域是如何實現的。

當你研究 Z80 直接串線和 8051 子程序串線時,這些字的值就變得非常明顯:

按同樣的風格,字 ,BRANCH 、 ,DEST 和 !DEST 隱藏了高級分支和循環操作符的實現。我試著發明 -- 不借用現有的 Forth 系統 -- 最少的操作符集合,它可以因子化實現的差異。只有時間、專家評判和許多 CAMEL Forth 才可以說明我在這方面取得了多少成功。

到目前為止,我并沒有成功地把首部結構中的不同因子化到相似的字集。FIND 和 CREATE 是和首部內容緊密相連的,我還沒有找到合適的子因子。我已經開始了這方面的努力,通過字 NFA>LFA NFA>CFA IMMED? HIDE REVEAL 和 ANS Forth 字 >BODY IMMEDIATE. ,我將繼續這一工作。值得欣慰的是,現在可以把同樣的首部結構用于所有的 CAMEL Forth 實現(因為它們都是字節方式尋址的16位 Forth 系統)

接下來我將要給出一個 8051 內核,并說明 Forth 編譯器和解釋器是如何用于 哈佛體系結構的(這種系統結構的計算機把存儲器邏輯地分成代碼和數據兩個部分,比如 8051)。對于 8051 ,我會給出文件 CAMEL51 和 CAMEL51D ,但是沒有 CAMEL51H ,因為除了匯編語言格式外,高級代碼不會與我這里討論的有什么差異,而本刊的編輯也需要發表其它的文章。好在所有的代碼都是可以下載的。

Link – 在 CamelForth 和 Fig-Forth 中,指向前一個字的長度字節。在 Pygmy Forth 和 F83, 中,指向前一個字的 LINK 。

P – 優先位,如果是 1 則為立即字,在 Pygmy 中沒有使用。

S - Smudge 位,阻止 FIND 找到這個字

1 – 在 Fig-Forth 和 F83 中,長度字節和名字和最后一個字符的最高有效位(位 7 )用 1 來標識

View – 在 Pygmy Forth 和 F83 中,是這個字所在源碼塊的編號

參考文獻

1. Derick, Mitch and Baker, Linda, Forth Encyclopedia , Mountain View Press, Route 2 Box 429, La Honda, CA 94020 USA (1982). Word-by-word description of Fig-Forth.

2. Ting, C. H., Systems Guide to fig-Forth , Offete Enterprises, 1306 South B Street, San Mateo, CA 94402 USA (1981).

3. Ting, C. H., Inside F83 , Offete Enterprises (1986).

4. Ewing, Martin S., The Caltech Forth Manual , a Technical Report of the Owens Valley Radio Observatory (1978). This PDP-11 Forth stored a length, four characters, and a link in two 16-bit words.

5. Sergeant, Frank, Pygmy Forth for the IBM PC , version 1.4 (1992). Distributed by the author, available from the Forth Interest Group (P.O. Box 2154, Oakland CA 94621 USA) or on GEnie.

6. J. E. Thomas examined this issue thoroughly when converting Pygmy Forth to an ANSI Forth. No matter what tricks you play with relinking words, strict ANSI compliance is violated. A regrettable decision on the part of the ANS Forth team.

7. In private communication.

The source code for Z80 CamelForth is now available on GEnie as CAMEL80.ARC in the CP/M and Forth Roundtables. Really. I just uploaded it. (Apologies to those who have been waiting.)

Z80 CamelForth 的源代碼可以從下列站點上得到 ftp://ftp.zetetics.com/pub/forth/camel/cam80-12.zip .

第七部分 8051 的 Camel Forth

在我們尊敬的編輯要求下,我給出了 8051 的 CAMEL Forth ,而用于 MC6809 的 Forth 也將很快完成。這個 8051 Forth 占用 6K 字節的程序存儲器。不過,全部的源代碼將占 TCJ 的 16 頁,所以這篇文章只給出了核心移植過程中的主要變化。我們將解釋高級代碼是如何按 8051 匯編器格式要求和子程序串線技術而修改的。

Z80 更正

在文件 CAMEL80H.AZM 中, DO 的定義是這樣給出的

['] xdo ,BRANCH . . .

它應該是

['] xdo ,XT . . .

這是由于在 Z80 上沒有 consequence ( 在那里, ,BRANCH 和 ,XT 是等價的 ), 但在 8051 上,它是明顯的。

8051 CAMEL Forth 模型

在 #60 論文中,我匯總了 8051 Forth 的設計方法。再次說明: 8051 反映遲鈍的存儲器尋址實際上要求使用子程序串線。這就意味著硬件堆棧(在 8051 的寄存器文件中)就是返回棧。參數棧(也就是數據棧)在 256 字節的外部 RAM 中,使用 R0 作為這個堆棧的指針。從那篇文章開始,我發現了把棧頂元素( TOS )放到 DPTR 中比放在 R3 : R2 中更好。于是就有了這樣的程序員模型

這其中也包含了 Charles Curley [CUR93] 的思想。在像 8051 這樣寄存器豐富的機器上,我們可以把內層循環索引放在寄存器中,以使得 LOOP 和 +LOOP 更快。 DO 必須向返回棧壓入兩個值:舊的循環索引和新的循環終值。 UNLOOP 當然需要從返回棧得到循環索引 -- ANSI 把 UNLLOP 做為一個單獨的詞。注意 R6:R7 不是返回棧的棧頂元素,它只是內層循環的索引。

P2 含有參數棧指針的高字節(允許 R0 尋址外部存儲器),它也是用戶指針的高字節 -- UP 的低字節假設為 00 。我費了很大的勁才明白當從外部 ROM 執行時, P2 是不能讀的,所以我保存了一份 P2 的拷貝在寄存器 8 中。

對 BRANCH 和 ?BRANCH 我有一個非常好的實現方法。由于 8051 模型是子程序串線,高級 Forth 作為真正的機器代碼來編譯,所以 BRANCH 可以用一個 SJMP (或者 AJMP 或者 LJMP )指令實現。 ?BRANCH 可以用一個 JZ 指令實現,只要 TOS 的零 / 非零標志已經放到了累加器中( A 寄存器)。用一個子程序 ZEROSENSE 做這個工作,所以 BRANCH 和 ?BRANCH 就變成了:

BRANCH:

SJMP dest

?BRANCH:

LCALL ZEROSENSE JZ dest

與此相似, LOOPSENSE 和 PLUSLOOPSENSE 允許 JZ 指令使用 LOOP 和 +LOOP 。對于這些情況,在 JZ 之后應該調用 UNLOOP 以清除程序“FALLS OUT”循環時的返回棧。

在匯編語言源文件中的許多地方,我手工把 LCALL word RET 用更短更快的字 LJMP word替換 ,只要“字”不是一個返回棧操作符(比如 R> 或者 >R )。而只要有可能,字 LCALL 和 LJMP 就用 ACALL 和 AJMP 替代。

我用 Intel 字節順序寫了 8051 內核(低字節在先),之后我發現編譯到 LJMP 和 LCALL 的地址是高字節在先。為了避免重寫整個內核,我為這些編譯 LCALLS 的字包含了一個字節交換的字: COMPILE, !CF 和 ,CF ( 它們都是Dependency 字匯集 ).

哈佛體系結構

8051 使用哈佛體系結構,程序和數據保存在分開的存儲器中。在嵌入式系統中,它們分別是 ROM 和 RAM 。 ANS Forth 是第一個能夠適應哈佛體系結構限制的標準。簡單地說, ANS Forth 規定:

?? 應用程序只能夠訪問數據存儲器,同時

?? 所有訪問存儲器和構造數據結構的操作符必須在數據空間操作。

( 參看 ANS 文檔 section 3.3.3 [ANS94].) 包括下列 Forth 字: @ ! C@ C! DP HERE ALLOT , C, COUNT TYPE WORD (S") S" CMOVE

然而, Forth 編譯器還需要訪問程序空間(也稱為代碼或者指令空間)。 Forth 需要為程序空間和數據空間維護一個字典指針。所以我增加以下這些新的字:

I@ I! IC@ IC! IDP IHERE IALLOT I, IC, ICOUNT ITYPE IWORD (IS") IS" D->I I->D

這里前綴“I”表示指令(因為 P 和 C 在 Forth 中已經有了其它的意義)。 ICOUNT 和 ITYPE 用于顯示已經被編譯到 ROM 中的串。 IWORD 從數據空間復制 WORD 留下的字到代碼空間 -- 用于構造 Forth 字頭和放到 ROM 中的串。 D->I 和 I->D 是與 CMOVE 等效的,它從 / 向代碼空間復制。

VARIABLE 必須定位到數據空間。所以它們不能使用傳統的辦法把數據緊接著代碼域存放。這里的方法是:在數據空間中數據的地址存放在代碼域之后?;旧?#xff0c;一個 VARIABLE 就是一個 CONSTANT ,它的值就是數據空間的地址。 ( 當然,傳統的 CONSTANT 依然有效 )

CREATE 字,以及使用 CREATE …… DOES> 創建的字,必須按同樣的方式工作。以下是它們在程序空間看起來的樣子:

CODE word: ...header... 8051 machine code

high-level: ...header... 8051 machine code

CONSTANT: ...header... LCALL-DOCON value

VARIABLE: ...header... LCALL-DOCON Data-adrs

CREATEd: ...header... LCALL-DOCON Data-adrs

注意 CONSTANT 必須替換 CREATE 存入的值,:必須 "un-allot" 所有這些值和 LCALL DOCON 。

S" 有特殊的問題。使用 S" 定義的串(“文本常數”)必須駐留在數據空間,在那里它們可以被 TYPE 和 EVALUATE 這樣一些字使用。但是我們希望這些字是定義的一部分,并且在 ROM Forth 環境中能夠駐留在 ROM 里。我們可以把字符串存儲在程序空間,引用的時候復制到 HERE ,但是 ANS 文檔不允許文本常數存在于這個“臨時”的存儲區域(參看 ANS 文檔 sections 3.3.3.4 和 3.3.3.6 [ANS93]) 。同時,如果 WORD 把它的串地址在 HERE中返回 -- 就像 CAMEL Forth -- 則文本常數不能改變這個臨時區域。

我的解決方案是 S" 存儲串到代碼空間,但是也在數據空間為它永久地保留位置,當引用時,把它從代碼空間復制到這個數據空間。 ANS Forth 并沒有解決 哈佛體系結構處理器的全部問題,有時像 C 一樣的“初始化數據區”可能也是需要的。

因為 ." 從來也不能被程序員使用,它們可以存儲在代碼空間中,方法是使用字 (IS") 和 IS" 。 ( 它們是 " 老 " 的 (S") 和 S".) 。雖然內核增加了兩個字,但是節省了許多數據空間。我計劃把有關串常數的字集中到 Dependency 字匯集,或者建立一個新的“HARDVARD”字匯集。

寫入程序空間

8051 并不能真正地寫入程序存儲器,沒有硬件信號,也沒有硬件指令。在這種環境下, CAMELForth 解釋器可以工作,但是不能編譯新的字。我們可以設法讓某些存儲器同時出現在程序和數據空間。許多 8031 應用說明給出了同時訪問數據和程序空間的方法,在硬件上通過組合一些信號實現。圖 1 給出了我對電路板的修改,這個電路板是 Blue Ridge Micros (2505 Plymouth Road, Johnson City, TN, 37601, USA, telephone 615-335-6696, fax 615-929-3164) 的 MCB8031. U1A 和 U1B 產生一個新的選通信號,只要程序或者數據讀一個有效時就可以 EPROM 在 A15 為低時被選擇 ( 低 32K), RAM 在 A15 為高時有效 ( 高 32K) 。 當然,你不能寫入 EPROM ,但是你可以從 RAM 中執行程序!有一個缺點: : 這使得 @ 和 I@ 等效,如果你在什么地方用錯了它們,則并不能馬上發現。

圖 1 修改的 8051 電路圖

這些高級定義字修改的目的是實現對 CAMEL Forth 在哈佛體系結構和馮諾曼 體系結構機器之間移植。對于后者,新的程序空間字可以簡單地對應到數據空間字,比如對于 Z80

IFETCH EQU FETCH

ISTORE EQU STORE

ITYPE EQU TYPE

等等

在下一篇文章中,我將要修改 8051 源代碼,使它能在 6809 上工作,這是一個通過不斷改進而得到的真正可移植模型。

參考文獻

[ANS93] dpANS-6 draft proposed American National Standard for Information Systems - Programming Languages - Forth , June 30, 1993. "It is distributed solely for the purpose of review and comment and should not be used as a design document. It is inappropriate to claim compatibility with this draft standard." Nevertheless, for the last 16 months it's all we've had to go by.

[CUR93] Curley, Charles, Optimization Considerations , Forth Dimensions XIV:5 (Jan/Feb 1993), pp. 6-12.

8051 CamelForth 的源代碼可以從下列站點上得到 ftp://ftp.zetetics.com/pub/forth/camel/cam51-15.zip

第八部分 MC6809 Camel Forth

現在,我們將給出本文的最后部分,也就是許諾已久的 MOTOROLA 6809 ANSI CAMEL Forth 。這個實現是專門為 Scroungmaster II 處理器板而設計的。

與 Z80 和 8051 的 CAMELForth 不同, MC6809 Forth 是用我的“Chromium 2 Forth MATA 編譯器”生成的。你可以看到兩件事:

?? 首先、 MATA 編譯器在一個老的 Forth 系統上運行(F83),所以源代碼中含有 16 x 64 的 Forth“SCREEN”。我試著把它轉為 ASCII 文件,但是原始的痕跡還是很明顯;

?? 第二、用于 META 編譯器的源代碼看起來很像一般的 Forth 代碼(我馬上就要討論,有一些小的變化),這樣,關于 1+ 的定義就變成了:

CODE 1+ 1 # ADDD, NEXT ;C

匯編器使用的是我以前討論過的 MC6809 匯編器。

我直接照著已經出版的列表打入高級源代碼(轉換到 Forth 語法)。不幸的是,由于這中間隔了很長時間,并且我有時參照 Z80 列表、有時又參照 8051 列表……結果是 HARVARD 體系結構構造字(比如 I@ IALLOC )沒有堅持用在 MC6809 中。這對于非 HARVARD 結構的 MC6809 并不重要,但是如果要把 Forth 代碼用于 HARVARD 結構,我就不得不再修改這些錯誤。

另外,由于我是在已經出版的列表基礎上工作的,我常常忘了給高級字定義寫上詳細的說明,不過,你可以從原來的列表中知道它們是如何工作的,當然,我并不強制你這樣做。

MC6809 CAMEL Forth 的源代碼說明

MC6809 CAMEL Forth 模型把 TOS 放到 D 寄存器中,把S 棧指針用于參數棧, U 指針用于返回棧, Y 是解釋指針。 X 是 W 寄存器的臨時寄存器。 MC6809 直接頁指針 DPR 保存用戶指針的高字節(低字節假設是 0 )。

Scroungemaster II 單板上的 8K RAM 和 8K EPROM 按以下地址映象:

6000-797Fh RAM 字典 ( 用于新定義 )

7980-79FFh 終端輸入緩沖區

7A00-7A7Fh User 區 (USER 變量 )

7A80-7AFFh 參數棧 ( 向下增長 )

7B00-7B27h HOLD 區 ( 向下增長 )

7B28-7B7Fh PAD 區 ( 通用緩沖區 )

7B80-7BFFh 返回棧 (grows downward)

E000-FFFFh EPROM 中的 Forth 內核

所有的 RAM 數據區通過用戶指針引用,它的開始地址是 UP-INIT :在我們這里是 7A00H (注意這個字的高字節和 UP-INIT-HI 的使用)。當 CAMEL Forth 開始的時候,它會把字典指針設置到 DP-INIT ,而且必須在 RAM 中,這樣你就可以向 Forth 字典中加入一個新的定義。這些都是由 META 編譯器的 EQU 指令指定的。這些 EQU 指令并不占用 MC6809 的核心空間,它們也不會出現在 MC6809 的 Forth 字典中。

DICTIONARY 告訴 MATA 編譯器在哪里編譯代碼,在我們的情況下是 E000-FFFFH 的 8K EPROM ,新的字典命名為 ROM ,然后 ROM 被指定到所選定的字典。(如果你熟悉 Forth 的詞匯表,你就會看到很強的相似性)。

字 AKA 定義一個 Forth 字的同義詞。因為 MC6809 不是一個哈佛體系結構計算機,我們應該把所有在源代碼中出現的 I@ 編譯成 @ ,其它的“帶 I 前綴”(指令空間)字也做同樣的處理。 AKA 將完成這個工作。這些同義詞像 EUQ 一樣,它們不出現在 MC6809 的字典中。

MATA 編譯器允許你使用前向引用,就是訪問那些還沒有定義的 Forth 字(你當然需要在全部完成之前定義它們!)。這通常是自動的,但是 AKA 要求你使用 PRESUME 明確地說明,比如:

PRESUME WORD AKA WORD IWORD

用于創建 IWORD 的同義詞。 @ ! HERE ALLOT 是 META 編譯器自動定義的,我們不需要對這些詞使用 PRESUME 。

CODE 定義非常方便。注意你可以使用:

HERE EQU labelname

在 META 編譯中產生一個標號,(這是 META 編譯器的一個功能而不是匯編器的功能)。另外, ASM: 開始一個匯編代碼片段(也就是說,這不是一個 CODE 字的一部分)。

下面的短語

HERE RESOLVES name

用于解決 META 編譯器使用的特定的前向引用(例如, MEATA 編譯器需要知道 DOCOLON 動作的代碼在哪里)。你應該使這些獨立。除此之外,你可以自由地在源代碼中加入 CODE 定義。

定義字和控制結構(IMMEDIATE 立即字)的代碼更加難懂,其中的原因是這些字在 META 編譯期間也要執行一些動作。例如: MC6809 Forth 包含有標準字 COSNTANT 用于定義一個新的常數。但是許多 COSNTANT 定義也出現在 MC6809 內核中。我們在 META 編譯中也許需要定義一個新的 CONSTANT 。 EMULATE: 短語用于指示不同的 CONSTANT 沖突時如何動作。這個短語是完全用 MEATA 編譯器字寫成的,所以看起來完全是含混不清。

與此類似, IF THEN 和其它同類的字包含 META 編譯短語用于構造和解決 MC6809 映象的分歧。一些 META 編譯器把這些字隱藏在編譯器之中,這可以產生漂亮的目標代碼,但是,如果你需要改變分支的方式,你就必須修改 META 編譯器。

我傾向于使這些動作易于修改,所以我選擇 Chromium 放到目標源代碼中。(最恐怖的例子是 TENDLOOP 和 TS" 的定義,它們實際是在目標源代碼當中擴展了 META 編譯器的詞匯表。

如果你是一個 Forth 和 META 編譯器的新手,最好的方法是接受這一切?!捌胀ā钡拿疤柖x是很容易加入的,只需要參照 MC6809 其它部分源代碼就可以了。你甚至可以寫 CREATE …… DOES> 字義,只要你不在 META 編譯器中使用它們。

在一個 1MHz 的 MC6809 上,一行文本輸入需要明顯長的時間去處理(粗略估計約為 1 秒鐘)。這其中的部分原因是由于解釋器的許多部分是使用高級 Forth 編碼的,另一部分原因是 CAMEL Forth 使用了一個單鏈表結構的字典。這些只影響編譯的速度而不會影響到執行速度。

不過,延遲總是煩人的,也許有一天我會寫出一篇有關“加速 Forth”的論文。

現在,用戶指針 UP 不會改變。我們擁有一個 UP 的目的是支持多任務 -- 每個任務有它自己的用戶區、堆棧等等。我將很快針對這個問題開展工作。我也許會研究 SM II 的存儲器管理,為每個任務提供 32K 的私有字典。當然,我會努力寫出一個真正的使用共享總線的多處理器 Forth 內核。如果我活得足夠長,理所當然地還應該寫一個使用串行口的分布式 Forth 內核。

MC6809 的 CAMEL Forth 版本 1.0 的源代碼在 GEnie 的 Forth Roundtable ,文件名稱為 CAM09-10.ZIP ,這個文件包含了 Chromium 2 meta 編譯器,是可以運行的。只要有 F83 ,你就可以輸入:

F83 CHROMIUM.SCR

1 LOAD

BYE

這樣就可以裝入 META 編譯器,編譯MC6809 CAMEL Forth ,把結果寫入 Intel 格式的6809.HEX 中。注意:如果你使用的是 CP/M 或者 Atari ST 版本的 F83 ,則必須編輯 LOAD 屏幕以刪除 HEX 文件實用程序,因為這個程序只是為 MS-DOS 機而編寫的。我沒有測試 Chromium 2 使用的 CP/M 或者 Atari ST ,如果需要幫助,請與我聯系。

參考文獻

[ROD91] Rodriguez, B. J., "B.Y.O. Assembler," The Computer Journal #52 (Sep/Oct 1991) and #54 (Jan/Feb 1992).

[ROD92] Rodriguez, B. J., "Principles of Metacompilation," Forth Dimensions XIV:3 (Sep/Oct 1992), XIV:4 (Nov/Dec 1992), and XIV:5 (Jan/Feb 1993). Describes the "Chromium 1" metacompiler.

MC6809 CamelForth的源代碼可以從下列站點上得到 ftp://ftp.zetetics.com/pub/ forth /camel/cam09-10.zip .

總結

以上是生活随笔為你收集整理的Forth 系统实现的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。

主站蜘蛛池模板: 国产成人久久婷婷精品流白浆 | www.色啪啪.com | 欧美日韩精品久久久 | 新香蕉视频 | 99mav| av在线免费播放网址 | 国产又大又黄又粗 | 亚洲熟妇av日韩熟妇在线 | 伊人久艹 | 欧美精品网站 | 日本丰满少妇裸体自慰 | 茄子av | 蜜臀国产AV天堂久久无码蜜臀 | 亚洲国产成人91porn | www.激情 | 精品无码久久久久久久 | 亚洲精品ww | 有机z中国电影免费观看 | 亚洲精品欧洲 | 天堂网在线观看 | 亚洲综合免费 | 亚洲情se | 永久av| 3d动漫啪啪精品一区二区中文字幕 | 国产原创视频在线 | 国产操视频 | 美女一二三区 | 亚洲综合一区中 | 国精产品一区二区 | xxxxⅹxxxhd日本8hd | 日韩中文在线观看 | 91精品国产乱码久久 | 日韩av中文字幕在线免费观看 | 操大爷影院 | 久久精品免费在线观看 | 午夜久久久久久噜噜噜噜 | 国产免费av网址 | 久久精品无码毛片 | 超鹏在线视频 | 日视频| 日韩在线视频免费播放 | 亚洲精品国产成人av在线 | 午夜九九| 亚洲美女在线观看 | 国产精品视频在线观看免费 | 国产日韩精品中文字无码 | 亚洲综合在线观看视频 | av日日操| 久久香蕉影院 | 日韩少妇一区二区三区 | 国产精品久久久久久久久岛 | 手机福利在线 | 中文字幕在线观看亚洲 | 久草加勒比 | 午夜亚洲福利在线老司机 | 伊人久久久久久久久久久久 | 日韩一级视频 | 99久久毛片 | 中文字幕最新在线 | 视频成人免费 | 91在线播| 青青国产在线视频 | 亚洲拍拍视频 | 成人免费看片入口 | 尤物网站在线播放 | 在哪里可以看毛片 | 黑人大群体交免费视频 | 色综合天天综合网国产成人网 | 一区二区高清在线观看 | 美女视频黄色 | 午夜tv影院 | 亚洲精品一区二区三 | 天天爽天天爱 | 国产精品日韩电影 | 日韩欧美高清在线视频 | 久久在线中文字幕 | 最好看的电影2019中文字幕 | 欧美福利视频一区 | 亚洲精品一区二区三区四区 | 波多野结衣在线免费观看视频 | 青青草偷拍视频 | 国产免费成人av | 国产极品在线观看 | 欧美有码在线观看 | 婷婷色av | 超碰不卡 | 欧美jizzhd精品欧美18 | 天堂网av2014| 欧美夜夜| 综合爱爱网 | 久久久久久伊人 | 毛片在线视频观看 | 91丨porny丨国产 | 91天天射 | 国产一国产二国产三 | 手机在线看片你懂的 | 色桃网| 天天狠天天透 | 国产 欧美 日韩 一区 |