Forth 系统实现
Forth 系統(tǒng)實(shí)現(xiàn)
原作者 Brad Rodriguez
編譯者 趙宇 張文翠
本文編譯自 Brad Rodriguez 《 Moving Forth 》,原文首次在 The Computer Journal #59 (January/February 1993) 上發(fā)表,現(xiàn)在可在下面網(wǎng)站上取得
http://www.zetetics.com/bj/papers/index.html
本文對(duì) Forth 語言在各種處理器上的各種實(shí)現(xiàn)方式進(jìn)行了深入的探討,盡管其中作為例子的處理器非常古老,但對(duì)于理解 Forth 系統(tǒng)仍有著很大的參考價(jià)值。譯文按原文結(jié)構(gòu)翻譯,省略了各部分的參考文獻(xiàn),其內(nèi)容和本文中所列出的源程序代碼都可以在上述網(wǎng)站上得到。
目錄
第一部分 Forth 內(nèi)核的設(shè)計(jì)決策
第二部分 內(nèi)核基準(zhǔn)測(cè)試和個(gè)案研究
第三部分 解密 DOES>
第四部分 匯編器還是 META 編譯器
第五部分 Z80 原語
第六部分 Z80 高級(jí)內(nèi)核
第七部分 8051 的 CamelForth
第八部分 MC6809 CamelForth
第一部分 Forth 內(nèi)核的設(shè)計(jì)決策
前言
每一個(gè)進(jìn)入 Forth 圈里的人都說或者聽說“把 Forth 移植到一個(gè)新 CPU 上是一件易如反掌的事情”。不過,就像其它許多“易如反掌”的事情一樣,卻沒有多少書面的資料告訴我們?nèi)绾稳プ?#xff01;所以,當(dāng) Bill Kibler 建議這個(gè)論文題目時(shí),我決定打破 Forth 編寫者只說不練的傳統(tǒng),給出一個(gè)白紙黑字的 Forth 實(shí)現(xiàn) , 包括為 MC6809 、 Intel 8051 和 Z80 實(shí)現(xiàn)的 Forth 系統(tǒng)。
在整個(gè)文檔中,我準(zhǔn)備為 MC6809 、 Intel 8051 和 Zilog Z80 實(shí)現(xiàn) Forth 系統(tǒng)。我會(huì)用 MC6809 來解釋一個(gè)簡(jiǎn)單的和傳統(tǒng)的 Forth 模型,此外,我還將公布一個(gè) MC6809 匯編器,把 6809 Forth 用于未來的 TCJ 計(jì)劃,把 8051 作為一個(gè)大學(xué)項(xiàng)目,其中也解釋了一些非常不同的決策。 Z80 Forth 是為所有的 TCJ CP/M 讀者和許多 TRS-80 的老朋友而編寫的。
有效的硬件
首先,我們必須選擇一個(gè) CPU 。不過,我不想陷入“Forth 運(yùn)行在這種 CPU 上比運(yùn)行在那種 CPU 上更有效”的爭(zhēng)論中,因?yàn)?CPU 的選擇通常還需要考慮其它因素,并且這篇論文的目標(biāo)之一就是想說明如何把 Forth 搬到任何一個(gè) CPU 上。
通常, 16 位 Forth 內(nèi)核需要 8K 字節(jié)的程序空間。對(duì)于一個(gè)能夠真正用來編譯 Forth 語言應(yīng)用的完整內(nèi)核來說,應(yīng)該至少有 1K 字節(jié)的 RAM 。如果想使用 Forth 的磁盤存儲(chǔ)器塊管理功能,還應(yīng)該再增加 3K 字節(jié)以上的 RAM 用于緩沖區(qū)。對(duì)于 32 位系統(tǒng),這些數(shù)值都需要加倍。
以上這些是一個(gè) Forth 內(nèi)核能夠運(yùn)行的最小要求。為了在硬件上運(yùn)行應(yīng)用程序,你還得按實(shí)際需要另外增加 PROM 和 RAM 的大小。
使用 16 位還是 32 位系統(tǒng)
實(shí)際的系統(tǒng)并不要求 Forth 的字長(zhǎng)度與 CPU 的字長(zhǎng)度一致。最小的、實(shí)際可用的 Forth 系統(tǒng)都使用 16 位模型,也就是說使用 16 位的整數(shù)和 16 位的地址。 Forth 的術(shù)語把這種尺寸稱為單元(CELL)而不是我們常說的“字”,因?yàn)椤白帧痹?Forth 中是指一個(gè) Forth 定義(可以簡(jiǎn)單地理解成其它高級(jí)語言的子程序名)。
所有的 8 位 CPU 幾乎都不加改變地支持 16 位的 Forth ,為此,要求編碼進(jìn)行雙字節(jié)算術(shù)運(yùn)算,盡管某些 8 位 CPU 也能夠直接支持其中的一些操作。
有一些技術(shù)可以在一個(gè)16位的機(jī)器上寫出 32 位的 Forth ,但是 16 位的 CPU 通常運(yùn)行 16 位的 Forth ,雖然我們也看到 32 位的 Forth 可以運(yùn)行在 Intel 8086/8088 上。
32 位的 CPU 通常運(yùn)行 32 位的 Forth 。實(shí)際應(yīng)用中,一個(gè)更小的模型幾乎不能節(jié)省代碼空間和處理器時(shí)間。但我也見到過為 MC68000 編寫的 16 位 Forth 。這個(gè)系統(tǒng)的代碼長(zhǎng)度縮小了 2 倍,因?yàn)楦呒?jí) Forth 定義變成了 16 位的地址串而不再使用 32 位的地址串。不過大多數(shù)的 MC68000 系統(tǒng)都有很多 RAM ,好象沒有必要進(jìn)行這樣的努力。
本文描述的所有例子都是運(yùn)行在 8 位 CPU 上的 16 位 Forth 系統(tǒng)。
串線編碼技術(shù)
“串線編碼”是 Forth 的標(biāo)志。一個(gè) Forth “串線”就是被執(zhí)行的子程序地址的列表。你可以把它們想象成一連串省略了 CALL 指令的子程序調(diào)用表。長(zhǎng)期以來,人們發(fā)明了多種串線形式,為了作出選擇,你必須理解所有這些串線形式是如何工作的,以及它們各自的優(yōu)缺點(diǎn)。
間接串線編碼( ITC )
間接串線編碼(ITC)技術(shù)是一種經(jīng)典的 Forth 串線編碼技術(shù),最早出現(xiàn)在 FIG-Forth 和 F83 系統(tǒng)中,并且在許多關(guān)于 Forth 的書中都有描述。后來的串線方式都是直接串線編碼方式的“發(fā)展”,所以,你需要先理解這個(gè)技術(shù)。
讓我們看一個(gè) Forth 字 SQUARE 的定義 :
: SQUARE DUP * ;
在一個(gè)典型的 ITC Forth 中,該定義在存儲(chǔ)器中的情況如圖 1 所示(首部將在以后討論,它保存編譯信息,但并不在串線中訪問)
圖 1 ITC Forth 定義的存儲(chǔ)器
假設(shè)在執(zhí)行某個(gè) Forth 字的時(shí)候遇到了字 SQUARE , Forth 的解釋指針 IP 將指向存儲(chǔ)器的一個(gè)單元,其中包含字 SQUARE 的地址,當(dāng)然更嚴(yán)格地應(yīng)該說,這個(gè)單元包含著 SQUARE 的“代碼域地址”。解釋器讀出這個(gè)地址并用這個(gè)地址讀出 SQUARE 的代碼域內(nèi)容。它還是一個(gè)地址 -- 這個(gè)地址是一個(gè)機(jī)器語言子程序,由這個(gè)子程序來執(zhí)行定義 SQUARE 。
我們可以把上面的描述通過偽碼表示如下:
(IP)-> W 讀取 IP 指向的存儲(chǔ)器內(nèi)容到 W 寄存器, W 現(xiàn)在有代碼域的地址;
IP+2->IP 增量 IP, 它就像一個(gè)程序計(jì)數(shù)器,而且假設(shè)串線中的地址是 2 個(gè)字節(jié)長(zhǎng);
(W) -> X 讀取由 W 指向的存儲(chǔ)器內(nèi)容到 X 寄存器, X 現(xiàn)在指向機(jī)器碼地址;
JP (X) 跳到 X 寄存器指向的地址執(zhí)行;
這里解釋了一個(gè)重要的、但卻很少有人說明的原理:進(jìn)入的 Forth 字的當(dāng)前地址保存在 W 寄存器中。 CODE 字不需要這個(gè)信息,但是其它類型的 Forth 字確實(shí)需要其中的信息。
如果 SQUARE 是由機(jī)器代碼寫成的,事情也就結(jié)束了:這些機(jī)器代碼被執(zhí)行,然后跳回到 Forth 解釋器 -- 由于 IP 已經(jīng)增量,它將指向下一個(gè)將被執(zhí)行的字。所以 Forth 解釋器通常被稱為 NEXT 。
可是, SQUARE 是一個(gè)高級(jí)的“冒號(hào)”定義 -- 它保持一個(gè)“串線”,或者說一個(gè)地址列表。為了執(zhí)行這個(gè)定義, Forth 解釋器必須在一個(gè)新的位置上重新啟動(dòng),這個(gè)位置就是 SQUARE 的參數(shù)域。當(dāng)然,解釋器必須保存舊的位置,以便 SQUARE 結(jié)束之后能夠恢復(fù)“另一個(gè)” Forth 字。這實(shí)際上與一個(gè)子程序調(diào)用沒有任何區(qū)別!
SQUARE 機(jī)器語言的動(dòng)作就是簡(jiǎn)單地把舊的 IP 值保存到堆棧上,并把 IP 指向新的位置,執(zhí)行解釋器,當(dāng) SQUARE 完成后彈出恢復(fù) IP 。(正如你看到的, IP 就是 Forth 高級(jí)定義的“程序計(jì)數(shù)器”),這個(gè)過程在不同的 Forth 版本中可能被稱為 DOCOLON 或者 ENTER :
PUSH IP 壓入“返回地址?!?;
W+2 -> IP W 已經(jīng)指向代碼域,所以 W+2 就是定義體的地址! ( 假設(shè)是 2 字節(jié)的地址,不同的 Forth 可能不同 )
JUMP NEXT 到解釋器 ( “ NEXT ” )
這樣的一段代碼被用于所有的高級(jí)(串線) Forth 定義!于是我們回答了兩個(gè)問題:
?? 為什么在 Forth 定義中用一個(gè)指針指向代碼段,而不是把代碼段本身直接嵌入到定義中。因?yàn)槿绻袛?shù)以百計(jì)的定義,就可以節(jié)省大量的空間;
?? 為什么這種方式被稱為“間接串線編碼”;
“從子程序返回”動(dòng)作由字 EXIT 完成,它被 Forth 的分號(hào)“;”編譯進(jìn)定義中(有些 Forth 系統(tǒng)使用 ;S 替代 EXIT)。 EXIT 執(zhí)行下列的機(jī)器語言:
POP IP 從“返回地址棧”彈出指針
JUMP interpreter 跳轉(zhuǎn)到解釋器
注意 ITC 的特點(diǎn):每個(gè) Forth 字都有一個(gè)單元的代碼域,冒號(hào)定義給定義中的每一個(gè)字編譯一個(gè)單元。 Forth 解釋器為了執(zhí)行機(jī)器代碼,必須實(shí)際執(zhí)行兩次間接才能取得下一個(gè)機(jī)器碼的地址(首先通過 IP ,然后通過 W)。
ITC 既不是代碼尺寸最小的、也不是執(zhí)行速度最快的串線技術(shù)。它可能只是最簡(jiǎn)單的技術(shù),盡管下面討論的另一種技術(shù)DTC 實(shí)際上也不是特別復(fù)雜。那么為什么有這么多的 Forth 系統(tǒng)都使用間接串線技術(shù)呢?主要是由于以前作為原始模型的 Forth 系統(tǒng)都使用間接串線技術(shù),而現(xiàn)在, DTC 技術(shù)卻用得最多。
那么什么時(shí)候應(yīng)該使用 ITC 技術(shù)呢?很明顯, ITC 形式能夠產(chǎn)生最純凈和最一致的定義:其中只有一種類型,這種類型就是地址。如果你正好就有這樣的需要,那 ITC 技術(shù)就是適合的。如果你的代碼關(guān)注定義的內(nèi)部, ITC 技術(shù)的簡(jiǎn)單性和單一性還能夠增加可移植性。
此外, ITC 是經(jīng)典的 Forth 模型,它可以非常好地用于教學(xué)。
最后,在某些缺少子程序調(diào)用指令的早期 CPU 上 -- 比如 1802 -- ITC 常常比 DTC 更有效。
直接串線編碼( DTC )
直接串線編碼(DTC)技術(shù)與 ITC 技術(shù)的差別只有一點(diǎn):不像 ITC 在代碼域中包含機(jī)器碼的地址, DTC 的代碼域中包含有實(shí)際的機(jī)器代碼本身。
注意,我并不是說在每個(gè)冒號(hào)定義中都包含全部的 ENTER 代碼。我的意思是:在“高級(jí)” Forth 字中,如圖 2 所示,代碼字段有一個(gè)子程序調(diào)用指令。例如,冒號(hào)定義中將包含一個(gè)對(duì) ENTER 子程序的調(diào)用。
圖 2 DTC Forth 定義的存儲(chǔ)
直接串線的 NEXT 偽代碼非常簡(jiǎn)單:
(IP) -> W 取 IP 指針指向的存儲(chǔ)器內(nèi)容到 W 寄存器中
IP+2 -> IP 增量 IP ( 假設(shè) 2 字節(jié)的地址 )
JP (W) 跳轉(zhuǎn)到 W 寄存器指向的地址執(zhí)行
DTC 的收益就是速度:解釋程序現(xiàn)在只需要執(zhí)行一次間接。在 Z80 上,這實(shí)際是把 NEXT 子程序 --Forth 內(nèi)核中最常用的代碼段 -- 從 11 個(gè)指令減少到了 7 個(gè)指令。
DTC 的成本是空間:在一個(gè) Z80 Forth 中,每個(gè)高級(jí)定義都將增加一個(gè)字節(jié)的長(zhǎng)度,因?yàn)?ITC 中 2 個(gè)字節(jié)的地址現(xiàn)在被 3 個(gè)字節(jié)的 CALL 調(diào)用指令所取代。當(dāng)然這個(gè)結(jié)論也不是廣泛適用的,在 32 位的 MC68000 Forth 中,可以用 4 字節(jié)的 BSR 指令代替 4 字節(jié)的地址,其中沒有任何差異。而在 Zilog 的 SUPER8 中,有一個(gè)直接用于 Forth DTC 的指令,它用一個(gè)字節(jié)的 ENTER 指令代替 2 字節(jié)的地址,使得在 SUPER8 上, DTC Forth 比 ITC Forth 的代碼還要小。
當(dāng)然 DTC 的 CODE 定義也縮短了 2 個(gè)字節(jié),因?yàn)樗鼈儾辉傩枰羔槨?
我一直以為 DTC Forth 的高級(jí)定義字必須在代碼域中使用子程序調(diào)用指令, Frank Sergeant的 Pygmy Forth [SER90] 提出可以使用更簡(jiǎn)單的跳轉(zhuǎn)指令,這樣更容易實(shí)現(xiàn),通常也更快。
Guy Kelly 對(duì) IBM PC 上實(shí)現(xiàn)的 Forth 系統(tǒng)進(jìn)行了很好的總結(jié),這也是我對(duì)所有 Forth 編寫者的建議。
在他研究的 19 個(gè) Forth 實(shí)現(xiàn)中,有 10 個(gè)使用了 DTC 技術(shù), 7 個(gè)使用了 ITC 技術(shù), 2 個(gè)使用了子程序串線技術(shù)(這種技術(shù)我們將在下面討論)。所以,我認(rèn)為所有新實(shí)現(xiàn)的 Forth 內(nèi)核都應(yīng)該使用直接串線技術(shù),而不要再使用間接串線技術(shù)了。
跳轉(zhuǎn)到 NEXT 還是對(duì) NEXT 使用內(nèi)嵌編碼?
Forth 的內(nèi)層解釋器 NEXT 是一個(gè)用于所有 CODE 定義的通用子程序。你可以編寫一個(gè)子程序,然后讓所有的 CODE 字跳轉(zhuǎn)到這個(gè)子程序上執(zhí)行(注意:跳轉(zhuǎn)到 NEXT 而不必通過子程序調(diào)用到 NEXT )。
然而, NEXT 的速度對(duì)于整個(gè) Forth 系統(tǒng)的速度來說是至關(guān)重要的,從這個(gè)角度考慮, NEXT 最好是內(nèi)嵌代碼,于是 NEXT 也可以被定義成一個(gè)匯編的宏。
這就是一個(gè)常見的速度 / 空間折衷問題:內(nèi)嵌的 NEXT 總是更快,但也總是更大。全部增加的尺寸數(shù)量是內(nèi)嵌擴(kuò)展需要的字節(jié)數(shù)乘以系統(tǒng)中 CODE 字的數(shù)量。當(dāng)然有時(shí)也根本不需要考慮折衷:在 MC6809 中,內(nèi)嵌的 NEXT 總是比一個(gè) JUMP 指令還要短!
子程序串線( STC )
一個(gè)高級(jí)的 Forth 定義字只不過是“要執(zhí)行的子程序的列表”,并不一定要通過解釋才能實(shí)現(xiàn)它們,你也可以通過簡(jiǎn)單地調(diào)用一系列子程序而得到同樣的效果:
SQUARE:
CALL DUP
CALL * ; 或者是一個(gè)合適的名字,因?yàn)橛行﹨R編器不支持把 * 作為子程序名
RET
圖 3 為匯編程序員解釋了 Forth 的 STC 串線技術(shù)。 [KOG82].
圖 3 DTC Forth 定義的存儲(chǔ)
STC 有一個(gè)統(tǒng)一的表示方式,冒號(hào)定義和 CODE 字沒有區(qū)別,“定義字”(這是 Forth 的專用術(shù)語,像 VARIABLE 、 CONSTANT 這樣一些可以用來定義新字的字被稱為定義字)像 DTC一樣處理 -- 代碼域用一個(gè)跳轉(zhuǎn)或者調(diào)用指令轉(zhuǎn)到其它地方的機(jī)器碼。
STC 的一個(gè)主要缺點(diǎn)是:子程序調(diào)用指令通常比簡(jiǎn)單的地址列表址大。比如在 Z80 上,冒號(hào)定義的尺寸將增大 50% -- 而你的應(yīng)用中大部分都是冒號(hào)定義。相比在32位的 MC68000 上,如果使用 4 字節(jié)的 BSR 代替 4 字節(jié)地址,代碼尺寸沒有任何增加,不過,如果你的代碼超過了64K ,一些地址就必須用 6 字節(jié)的 JSR 代替。
子程序串線可能比直接串線更快。在 STC 中節(jié)省了解釋器執(zhí)行的時(shí)間,但必須花費(fèi) Forth 字用于返回的 PUSH 、 POP 時(shí)間。而在 DTC Forth 中,只有高級(jí)定義才引起返回棧動(dòng)作,在 MC6809 或者 Zilog SUPER8 中, DTC 比 STC 更快。
STC 還有一個(gè)優(yōu)點(diǎn):它不需要 IP 寄存器。有些處理器 -- 像 Intel8051 -- 缺少地址寄存器,沒有虛擬機(jī) IP 寄存器可以真正地簡(jiǎn)化內(nèi)核并提高速度。
STC 的內(nèi)嵌擴(kuò)展、優(yōu)化、直接編譯
在一些古老的 8 位 CPU 上,幾乎每個(gè) Forth 原語都需要用幾個(gè)機(jī)器指令才能實(shí)現(xiàn),但是在更強(qiáng)大的 CPU 上,有時(shí) Forth 原語只需要一個(gè)機(jī)器指令。例如,在一個(gè) 32 位的 MC68000 上, DROP 可以簡(jiǎn)化為:
ADDQ #4,An 這里 An 是 Forth 的 PSP 參數(shù)棧寄存器
在一個(gè)子程序串線的 Forth 中,冒號(hào)定義中使用 DROP 將產(chǎn)生這樣的序列:
BSR ...
BSR DROP ...
DROP:
ADDQ #4,An
BSR ...
RTS
ADDQ 本來是一個(gè) 2 字節(jié)指令,我們?yōu)槭裁匆獙懸粋€(gè)對(duì)這個(gè) 2 字節(jié)指令的 4 字節(jié)子程序調(diào)用呢?在這種情況下,不論有多少個(gè) DROP ,通過子程序調(diào)用都不會(huì)產(chǎn)生任何的節(jié)省。而如果把 ADDQ 直接編碼到 BSR 流中,產(chǎn)生的代碼都會(huì)更小,運(yùn)行得更快。有些 Forth 編譯程序已經(jīng)實(shí)現(xiàn)了這樣的 CODE 字“內(nèi)嵌擴(kuò)展” [CUR93a] 。
內(nèi)嵌擴(kuò)展的缺點(diǎn)是:如果要把代碼反編譯回原始的代碼就會(huì)非常困難。如果僅僅是使用子程序串線,我們依然可以得到指向 Forth 字的指針(子程序的地址)。通過字指針,就可以得到它們的名字。但是如果指令字?jǐn)U展到內(nèi)嵌編碼中,所有的關(guān)于字來源的信息就全部丟失了。
除了速度和空間之外,內(nèi)嵌擴(kuò)展還有個(gè)優(yōu)點(diǎn):潛在的代碼優(yōu)化。例如: Forth 序列:
3 +
在 68000 STC 被編譯成:
BSR LIT
.DW 3
BSR PLUS
但是,使用內(nèi)嵌代碼,就可以把它優(yōu)化成一個(gè)機(jī)器指令。
Forth 編譯器優(yōu)化是一個(gè)廣闊的領(lǐng)域,也是 Forth 語言研究中一個(gè)非常活躍的領(lǐng)域,這里不能完全討論,可參見 [SCO89] 和 [CUR93b] 。優(yōu)化 STC 的最終結(jié)果是能夠產(chǎn)生“純”機(jī)器代碼的 Forth 編譯器,就像 C 或者 Fortran 編譯器一樣。
標(biāo)記串線編碼( TTC )
DTC 和 STC 技術(shù)的目標(biāo)是用一定的存儲(chǔ)器消耗為代價(jià)來增加 Forth 程序的執(zhí)行速度?,F(xiàn)在讓我們轉(zhuǎn)向 ITC 的另一個(gè)方向:運(yùn)行速度更慢、但代碼尺寸更小。
Forth 串線的目的是指定一系列將要執(zhí)行的 Forth 字(子程序)的地址。假設(shè)一個(gè) 16 位的 Forth 字最大只有 256 個(gè) Forth 字,那么每個(gè) Forth 字都可以用一個(gè) 8 位數(shù)來標(biāo)識(shí),我們就可以不使用 16 位的地址列表,而是用一系列的 8 位標(biāo)識(shí)或者稱為“標(biāo)記( TOKEN )”來代替地址,這樣冒號(hào)定義的代碼尺寸就減少了一半。
在一個(gè)標(biāo)記串線編碼的 Forth 系統(tǒng)中,需要有一個(gè)記錄所有 Forth 字的表格,如圖 4 所示。標(biāo)記值就是這個(gè)表項(xiàng)的索引,通過它來尋找一個(gè)指定標(biāo)記對(duì)應(yīng)的 Forth 字。這種方法為 Forth 解釋器增加了一次間接訪問,所以它比“地址串線”的 Forth 執(zhí)行速度更慢。
圖 4 DTC Forth 定義的存儲(chǔ)
標(biāo)記串線的基本優(yōu)點(diǎn)是尺寸很小。 TTC 技術(shù)在手持計(jì)算機(jī)和其它對(duì)尺寸要求嚴(yán)格的應(yīng)用中極為常見。同時(shí),使用統(tǒng)一的 Forth 字“入口”表也簡(jiǎn)化了分開編譯模塊的鏈接。
TTC 的缺點(diǎn)是:速度慢。 TTC 的 Forth 系統(tǒng)速度是所有技術(shù)中最慢的, TTC 編譯器也比其它技術(shù)的編譯器更復(fù)雜一些。如果你的應(yīng)用有多于 255 個(gè) Forth 字定義,則還需要一些其它的編碼方式來混合 8 位和更大的標(biāo)記。
說到 TOKEN 串線,也許會(huì)想到的情況是 32 位的 Forth 系統(tǒng)通過 TOKEN 串線而使用 16 位的 Forth 代碼,不過,實(shí)際上又有多少 32 位系統(tǒng)是存儲(chǔ)器尺寸受限的呢?
段串線編碼
由于曾經(jīng)有許多的 Intel 8086 派生系統(tǒng),我們也簡(jiǎn)單地提一下段串線技術(shù)。這種技術(shù)不再使用一個(gè) 64K 段內(nèi)的“一般”字節(jié)地址,而是使用節(jié)地址(在 Intel 8086 中,一個(gè)節(jié)的大小是 16 個(gè)字節(jié))。這樣,解釋器可以把這些地址裝入段寄存器,而不是通常的地址寄存器。這就允許 16 位的 Forth 模型可以有效地訪問 8086 的 1M 字節(jié)存儲(chǔ)器。
段串線模型的基本缺點(diǎn)是 16 字節(jié)大小的存儲(chǔ)器“粒度”,因?yàn)檫@種技術(shù)要求每個(gè) Forth 字必須在 16 字節(jié)的邊界上對(duì)齊,而一個(gè) Forth 字又具有隨機(jī)的長(zhǎng)度,所以平均每個(gè)字要浪費(fèi) 8 個(gè)字節(jié)。
寄存器分配
在討論了各種串線技術(shù)之后, CPU 寄存器的分配和使用就是至關(guān)重要的設(shè)計(jì)考慮了。這可能也是最困難的。 CPU 寄存器的可用性又會(huì)反過來決定我們使用哪種串線技術(shù),甚至決定我們使用哪種方式的存儲(chǔ)器映射。
經(jīng)典的 Forth 寄存器
經(jīng)典的 Forth 虛擬機(jī)模型有 5 個(gè)“虛擬寄存器”。它們是 Forth 原語的抽象實(shí)體。 NEXT 、 ENTER 、 EXIT 就是用這些抽象寄存器定義的。
每個(gè)寄存器的寬度都是一個(gè)單元,也就是說,在 16 位 Forth 系統(tǒng)中,它們都是 16 位寄存器。(以后你會(huì)看到,也有一些特例)。它們不一定全部都是 CPU 寄存器,如果你的 CPU 沒有足夠的寄存器,其中一些可以保存在存儲(chǔ)器中。本文將按照這些寄存器的重要性來描述,也就是說,在沒有足夠 CPU 物理寄存器的情況下,最后描述的寄存器應(yīng)該最先考慮被放置到存儲(chǔ)器中。
W 是工作寄存器 它可以被用來做很多事情。首先, W 寄存器應(yīng)該是一個(gè)地址寄存器,應(yīng)該能用 W 寄存器作為地址來讀取和寫入存儲(chǔ)器;也需要用 W 寄存器做算術(shù)運(yùn)算。在 DTC Forth 中,還要求能用 W 實(shí)現(xiàn)間接跳轉(zhuǎn)。W 寄存器在每個(gè) Forth 字中被解釋器使用,如果 CPU 只有一個(gè)寄存器,那你也必須把這個(gè)唯一的寄存器用于W 寄存器 ,而把其它的寄存器放到存儲(chǔ)器中,當(dāng)然,這種實(shí)現(xiàn)會(huì)使整個(gè)系統(tǒng)慢得令人難以置信。
IP 是解釋指針 它被每個(gè) Forth 字使用(通過 NEXT 、 ENTER 、 EXIT )。 IP 必須是一個(gè)地址寄存器,你也需要增量 IP。子程序串線的 Forth 系統(tǒng)不需要這個(gè)寄存器。
PSP 是參數(shù)棧指針(或者叫數(shù)據(jù)棧指針) 有時(shí)也簡(jiǎn)稱作 SP 。我使用 PSP 是由于“SP”通常都是 CPU 硬件寄存器的名字,而它們彼此是不能混淆的。大多數(shù) CODE 字需要使用這個(gè)寄存器。 PSP 必須是一個(gè)堆棧指針,或者是能夠增量和減量的地址寄存器。如果可以通過 PSP 進(jìn)行索引尋址則會(huì)為系統(tǒng)帶來有一些附加的好處。
RSP 是返回棧指針 有時(shí)也簡(jiǎn)稱RP。在 ITC 和 DTC 的 Forth 系統(tǒng)中, RSP被冒號(hào)定義使用,在 STC 的 Forth 系統(tǒng)中,它被所有的字使用。 RSP 必須是一個(gè)堆棧指針,或者是能夠增量和減量的地址寄存器。
如果可能,應(yīng)該把 W 、 IP 、 PSP 、 RSP 都放到實(shí)際的 CPU 物理寄存器中,其它的虛擬寄存器可以保存在存儲(chǔ)器中,當(dāng)然,如果所有的寄存器都保持在 CPU 硬件寄存器中,將帶來速度方面的好處。
X 寄存器是一個(gè)工作寄存器 不過這里并沒有把它作為一個(gè)經(jīng)典的 Forth 寄存器考慮,甚至在使用它作為二次間接的經(jīng)典 ITC 實(shí)現(xiàn)中也沒有被當(dāng)做經(jīng)典寄存器。在 ITC 中,必須能夠使用 X 寄存器實(shí)現(xiàn)間接跳轉(zhuǎn)。 X 寄存器也被幾個(gè) CODE 字作為算術(shù)運(yùn)算操作的目的地址,在不能使用存儲(chǔ)器作為操作數(shù)的處理器上是特別重要的。比如在 Z80 上,需要通過下面的方式來實(shí)現(xiàn)加法運(yùn)算(用偽碼表示):
POP W
POP X
X+W -> W
PUSH W
有時(shí)也定義另外一個(gè)寄存器 Y 。
UP 是用戶指針 它保持當(dāng)前任務(wù)的用戶區(qū)基地址。 UP 通常的用法是加上一個(gè)偏移量后在高級(jí) Forth 定義中使用它。如果 CPU 可以通過 UP 寄存器索引尋址, CODE 字就可以更簡(jiǎn)單和更快速地訪問用戶變量。如果你有多余的寄存器,可以用其中一個(gè)作為 UP 。單任務(wù)的 Forth 不需要 UP 。
如果需要 X ,則 X 應(yīng)該優(yōu)先于 UP 放入 CPU 物理寄存器。 UP 是 Forth 虛擬寄存器中最適合放入存儲(chǔ)器的。
硬件堆棧的使用
許多 CPU 把堆棧指針作為硬件的一部分用于中斷和子程序調(diào)用。如果把堆棧指針作為 Forth 的一個(gè)虛擬寄存器將會(huì)怎么樣呢?它應(yīng)該是 PSP 還是 RSP 呢?
這要根據(jù)具體情況來考慮。一般認(rèn)為在 ITC 和 DTC 的 Forth 中, PSP 的使用比 RSP 更加頻繁,如果你的 CPU 只有不多的寄存器, PUSH 和 POP 就會(huì)比顯式地引用存儲(chǔ)器速度更快,所以我們可以使用硬件堆棧作為參數(shù)棧。
另一方面,如果你的 CPU 有豐富的尋址方式,特別是允許進(jìn)行索引尋址,就應(yīng)該為 PSP 分配一個(gè)通用的地址寄存器,在這種情況下,應(yīng)該使用硬件堆棧作為返回棧。
這里的結(jié)論對(duì)下面的情況不合適。比如在 TMS320C25 中,硬件堆棧的深度只有 8 個(gè)單元,這對(duì)于 Forth 系統(tǒng)來說基本上沒有什么用途,所以它的硬件堆棧只能用于中斷, PSP 和 RSP 都必須是通用的地址寄存器。注意 ANS Forth 規(guī)范中指定最小的參數(shù)棧是 32 個(gè)單元,返回棧是 24 個(gè)單元,而我選擇的數(shù)據(jù)棧和返回棧都是 64 個(gè)單元。
有時(shí)你可能會(huì)遇到教條的說法,比如硬件堆?!氨仨毷菂?shù)?!被蛘摺氨仨毷欠祷貤!?。在這種情況下,你可以編寫幾個(gè) Forth 原語比如: SWAP 、 OVER 、 @ 、 ! 、 + 、 0= 來看看哪種情況代碼更小、速度更快。
順便說一下,如果要做這種測(cè)試,字 DUP 和 DROP 價(jià)值不高。
偶爾你也會(huì)得到有趣的結(jié)論! Gary Bergstrom 指出在 MC6809 的 DTC 實(shí)現(xiàn)中,用 MC6809 的用戶堆棧指針作為 IP 可以快幾個(gè)周期,這里 NEXT 變成了 POP 。他使用索引指針作為 Forth 的堆棧指針。
把棧頂元素( TOS )放入寄存器
如果能把參數(shù)棧棧頂元素 TOS 放到寄存器中,則 Forth 的性能會(huì)得到明顯改善。許多 Forth 字(比如 0= )將不再訪問堆棧,其它的 Forth 字做同樣的 PUSH 和 POP ,只不過在代碼中的位置不同。只有不多的 Forth 字(比如 DROP 和 2DROP )變得比較復(fù)雜 -- 你必須同時(shí)更新 TOS 的內(nèi)容。
把棧頂元素放到寄存器中之后,編寫 CODE 字時(shí)需要遵循這樣幾個(gè)規(guī)則:
?? 一個(gè)字從堆棧上移出一個(gè)項(xiàng)目時(shí),必須彈出“新”的 TOS 到寄存器中;
?? 一個(gè)字加入一個(gè)新的項(xiàng)目到堆棧上,必須把“舊”的 TOS 壓入棧上 ( 當(dāng)然,除非它被消耗掉 )
如果你的 CPU 至少有 6 個(gè)物理寄存器,我建議你保存 TOS 到其中一個(gè)寄存器中。我認(rèn)為 TOS 比 UP 更重要,但它的重要性又次于 W 、 IP 、 PSP 、 和 RSP 寄存器。 TOS 寄存器執(zhí)行了許多 X 寄存器的功能,如果這個(gè)寄存器可以實(shí)現(xiàn)存儲(chǔ)器尋址就更加有用。 PDP-11 、 Z8 、 MC68000 處理器都是很好的例子。
Guy Kelly [KEL92] 研究了 19 個(gè) IBM PC 上的 Forth 系統(tǒng),其中有 9 個(gè)使用了 TOS 寄存器。
我認(rèn)為, TOS 的想法沒有廣泛被接受的原因首先是下面一些錯(cuò)誤的見解:
?? 增加了指令;
?? 棧頂元素必須通過存儲(chǔ)器訪問。
?? 過分強(qiáng)調(diào)了PICK、ROLL 這些價(jià)值不高的字,說它們?cè)?TOS 情況下必須進(jìn)行重新編碼。
如果把兩個(gè)棧頂元素都放到寄存器中,結(jié)果會(huì)怎么樣呢?當(dāng)你這樣做的時(shí)候,操作效率是相同的。一個(gè) PUSH 仍然是一個(gè) PUSH ,不論你在此前和以后進(jìn)行了什么操作。另一方面,緩沖兩個(gè)堆棧元素卻增加了大量的代碼:一個(gè) PUSH 現(xiàn)在變成了一個(gè) PUSH 后隨一個(gè) MOVE 。把兩個(gè)元素緩沖到寄存器中,只有在 RTX2000 一類的 Forth 芯片上才有意義,其它的都是一些假想的、聽起來似乎非常聰明、但在實(shí)際應(yīng)用中沒有什么意義的優(yōu)化。
實(shí)際分配的一些例子
這里是一些不同的 CPU 寄存器分配實(shí)例,通過這個(gè)表,我們可以看出每個(gè) Forth 系統(tǒng)作者的寄存器分配考慮。
[1] F83. [2] Pygmy Forth.
圖 5 寄存器分配
“SP”指硬件堆棧指針?!癦page”是指保存在 6502 存儲(chǔ)器零頁的值,零頁幾乎和寄存器一樣有用,有時(shí)比寄存器更有用。比如,它們可以被用于存儲(chǔ)器尋址?!癋ixed” 指 Payne's 8051 Forth 有一個(gè)單一的、不可移動(dòng)的用戶區(qū), UP 是硬編碼的常數(shù)。
寄存器變窄
我們?cè)谏厦娴谋砀裰凶⒁獾搅耸裁雌婀值氖虑閱?#xff1f; 6502 Forth 是一個(gè) 16 位的模型,但是卻使用了 8 位的棧指針。
在實(shí)際情況下,使 PSP 、 RSP 和 UP 的尺寸小于 Forth 的單元尺寸是可能的。這是因?yàn)槎褩:陀脩魠^(qū)相對(duì)于整個(gè) CPU 可尋址存儲(chǔ)器來說比較小。每個(gè)堆??梢孕〉?64 個(gè)單元,而用戶區(qū)很少超過 128 個(gè)單元。你只需要簡(jiǎn)單地相信:
?? 這些數(shù)據(jù)區(qū)被限制在存儲(chǔ)器的一個(gè)小的區(qū)域中,可以使用短的地址訪問;
?? 高地址位用其它的方式提供,比如,通過頁面選擇的方式來提供;
在 6502 CPU 中,硬件堆棧被 CPU 的設(shè)計(jì)者限定在 RAM 的一個(gè)頁中(地址為 0x1xx )。8 位堆棧指針可以用作返回棧。參數(shù)棧保存在 RAM 的零頁中,通過一個(gè) 8 位索引寄存器間接訪問。
在 8051 中,你可以使用 8 位的寄存器 R0 和 R2 訪問外部 RAM ,并顯式地提供地址的高 8 位輸出到 PORT 2 。這就允許對(duì)兩個(gè)堆棧進(jìn)行“頁選擇”。
UP 與 PSP 的 RSP 是有明顯區(qū)別的:它只是簡(jiǎn)單地提供一個(gè)基地址,從來都不增量和減量。所以,它實(shí)際上只是提供這個(gè)虛擬寄存器的高位。低位必須借助某種索引技術(shù)來實(shí)現(xiàn)。例如,在 MC6809 中,你可以使用 DP 寄存器作為 UP 的高 8 位,然后使用直接頁面尋址模式去訪問這個(gè)頁面中的 256 個(gè)位置。這就強(qiáng)制用戶區(qū)域從 0x??00 開始,同時(shí)限制用戶區(qū)域長(zhǎng)度為 128 個(gè)單元, 這些都不是什么大問題。而在 Intel 8086 上,你還可以使用一個(gè)段寄存器作為用戶區(qū)的基地址。
參考文獻(xiàn)
[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 實(shí)現(xiàn)
[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).
第二部分 內(nèi)核基準(zhǔn)測(cè)試和個(gè)案研究
基準(zhǔn)測(cè)試
我們已經(jīng)回答了每個(gè)與 Forth 實(shí)現(xiàn)決策有關(guān)的問題,現(xiàn)在應(yīng)該是“編碼并查看結(jié)果”的時(shí)候了。不過,你肯定不想僅僅為了測(cè)試不同的方法就編寫許多個(gè)完整的 Forth 內(nèi)核。幸運(yùn)的是,僅僅編寫 Forth 內(nèi)核的小子集就可以得到一些相當(dāng)好的“感覺”。
Guy Kelly [KEL92] 研究了 19 個(gè)不同的 IBM PC 的下列一些代碼樣例:
?? NEXT …… 是鏈接“串線”中一個(gè)字到另一個(gè)字的“內(nèi)層解釋器”。用于每一個(gè) CODE 定義的結(jié)尾,是決定 Forth 執(zhí)行速度的一個(gè)最重要的因素。你已經(jīng)看到了它的 ITC 和 DTC 偽碼;在 STC 中,它就是 CALL/RET 指令。
?? ENTER …… 也稱為 DOCOL 或者 DOCOLON ,高級(jí)“冒號(hào)”定義代碼域動(dòng)作。它對(duì)于速度也是至關(guān)重要的;用于每個(gè)冒號(hào)定義的開始,在 STC 中不需要。
?? EXIT …… 在 FIG-Forth 中稱為 S; 。結(jié)束一個(gè)冒號(hào)定義執(zhí)行的代碼。它在每個(gè)冒號(hào)定義的結(jié)束處出現(xiàn),決定高級(jí)子程序的返回效率。在 STC 中它就是一個(gè) RET 機(jī)器指令。
NEXT 、 ENTER 和 EXIT 表現(xiàn)了串線機(jī)制的性能。它們都應(yīng)該通過實(shí)際的編碼來評(píng)估實(shí)現(xiàn)性能。它們也反映了實(shí)現(xiàn)時(shí) IP 、 W 和 RSP 寄存器分配策略是否正確。
?? DOVAR …… “變量”,對(duì)于所有 Forth 變量 VARIABLE 的代碼域動(dòng)作。
?? DOCON …… “常量”,對(duì)于所有 Forth 常量 CONSTANT 的代碼域動(dòng)作。
DOCON 、 DOVAR 和 ENTER 一起顯示了你可以得到一個(gè)正在執(zhí)行的字的參數(shù)域地址的效率。這反映了你對(duì) W 寄存器的選擇,在 DTC Forth 中,也指出應(yīng)該在代碼域中放一個(gè) JUMP 指令還是一個(gè) CALL 指令。
?? LIT …… “文字量”。這個(gè)字從 Forth 的高級(jí)串線中取一個(gè)單元值。有幾個(gè)字需要使用這樣的內(nèi)嵌參數(shù),這很好地顯示了它們的性能。它反映了你對(duì) IP 寄存器的選擇。
?? @ …… Forth 的存儲(chǔ)器讀取操作,顯示了從高級(jí) Forth 中訪問存儲(chǔ)器可以有多快。這個(gè)字常常從堆棧的 TOS 中受益。
?? ! …… Forth 的存儲(chǔ)器存操作,從另一方面反映了存儲(chǔ)器訪問的能力。它消耗堆棧的兩個(gè)項(xiàng)目,所以能反映參數(shù)棧的訪問效率。它也很好地說明了我們把 TOS 放在存儲(chǔ)器還是放在寄存器中的決策。
?? + …… 加法操作,是所有 Forth 算術(shù)和邏輯操作的典型代表。
以上是一個(gè)非常好的代碼樣例。我還增加了幾個(gè)附屬的測(cè)試:
?? DODEOS …… 是用 DOES> 構(gòu)建字的代碼域動(dòng)作,盡管它沒有反映 W 、 IP 和 RSP 的使用。我包含這個(gè)字是因?yàn)樗?Forth 內(nèi)核中最費(fèi)解的代碼,如果你可以編碼 DODOES 的邏輯,其它的任何東西就都不在話下了。 DODOES 的復(fù)雜性將在本文的后面描述。
?? SWAP …… 是一個(gè)簡(jiǎn)單的堆棧操作符,但能說明問題。
?? ROT …… 是一個(gè)更加復(fù)雜的堆棧操作符。它為你能簡(jiǎn)單地訪問參數(shù)棧給出一個(gè)好主意。 ROT 好像需要一個(gè)外加的臨時(shí)寄存器才能完成。如果你能夠在不使用 X 寄存器的情況下實(shí)現(xiàn) ROT ,則其它情況下也不會(huì)需要 X 寄存器。
?? 0= …… 是不多的幾個(gè)單目算術(shù)操作之一,是最有可能從“TOS 在寄存器中 ” 獲益的字之一。
?? +! …… 是最多被說明的操作,組合了堆棧訪問、算術(shù)、存儲(chǔ)器取和存儲(chǔ)器存。這是一個(gè)非常理想的用于標(biāo)準(zhǔn)測(cè)試的字,盡管比上面所列出的其它字使用頻率低。
以上所列的都是最常用的 Forth 字,努力優(yōu)化它們是值得的。我將給出一個(gè) MC6809 的偽碼例子。對(duì)于其它的處理器,我將解釋特別選擇的代碼片段。
個(gè)案研究 1 : MC6809
在 8 位 CPU 世界中, MC6809 是 Forth 程序員的甜蜜之夢(mèng)。它支持 2 個(gè)堆棧!還有另外 2 個(gè)地址寄存器和大量的只有 PDP-11 才有的正交尋址方式。正交的意思是指所有的地址寄存器有相同的選項(xiàng)和相同的工作方式,而兩個(gè) 8 位累加器可以作為一個(gè)單一的 16 位累加器使用,并具有許多 16 位操作指令。
MC6809 的程序員模型是
A - 8 bit 累加器
B - 8 bit 累加器 大多數(shù)算術(shù)操作以累加作為目的寄存器。它們也可以連接在一起作為一個(gè) 16 位的累加器 D ( A 是高 8 位, B 是低 8 位)。
X - 16 位索引寄存器
Y - 16 位索寄存器
S - 16 位堆棧指針
U - 16 位堆棧指針 所有用于 X 和 Y 寄存器的尋址模式也可以用于 S 和 U 寄存器。
PC - 16 位程序計(jì)數(shù)器
CC - 8 位條件標(biāo)志寄存器
DP - 8 位直接頁訪問寄存器
MC6800 系列的直接尋址模式可以使用一個(gè) 8 位寄存器訪問零頁存儲(chǔ)器的任何位置。 MC6809 允許對(duì)任何頁進(jìn)行直接尋址。DP 寄存器提供高 8 位地址(頁地址)。
有 2 個(gè)堆棧指針可供 Forth 使用,它們是等效的,但 CPU 設(shè)計(jì)者把 S 用于子程序調(diào)用和中斷。為一致起見,我們把 S 作為返回棧, U 作為參數(shù)棧。
W 和 IP 都要求使用地址寄存器,它們邏輯上用于 X 和 Y 寄存器,我們可以任意指定:
X => W 而 Y => IP 。
現(xiàn)在來選擇一個(gè)串線模型。我簡(jiǎn)單地舍棄 STC 和 TTC ,構(gòu)造一個(gè)“傳統(tǒng)”的 Forth 。性能上的限制因素是 NEXT 子程序。讓我們先看看它的 ITC 和 DTC 實(shí)現(xiàn):
ITC-NEXT:
LDX ,Y ++ (8) (IP) -> W, 增量 IP
JMP [,X] (6) (W) -> temp, 跳轉(zhuǎn)到臨時(shí)單元的地址
DTC-NEXT:
JMP [,Y++] (9) (IP)->temp, 增量 IP, 跳轉(zhuǎn)到臨時(shí)單元地址,臨時(shí)單元在 MC6809 的內(nèi)部。
NEXT 在 DTC 的 MC6809 中只有一條指令!這就意味著你可以用 2 個(gè)字節(jié)的內(nèi)嵌編碼,比 JMP NEXT 又快又好。作為比較,子程序串線是這樣的:
RTS (5) ... 在 CODE 字的結(jié)尾
JSR nextword (8) ... 在串線中下一個(gè) CODE 字的開始
STC 花費(fèi) 13 個(gè)周期用于串線下的一個(gè)字,而 DTC 只需要 9 個(gè)周期。這是由于子程序串線需要將返回地址彈出和壓棧,而 CODE 字卻不需要。
決定了使用 DTC 之后,你還有兩個(gè)選擇:高級(jí)定義字在它的代碼域中使用 JMP 還是 CALL ?決定的因素是我們?nèi)绾文芨斓氐玫胶竺娴膮?shù)域地址。讓我們注意一個(gè)冒號(hào)定義的 ENTER 編碼:
如果使用 JSR (Call):
JSR ENTER (8)
...
ENTER:
PULS W (7) 得到 JSR 之后的地址到 W 中
PSHS IP (7) 保存舊的 IP 到返回棧
TFR W,IP (6) 參數(shù)域地址 -> IP
NEXT (9) JMP [,Y++] 的匯編語言智能
以上總計(jì) 37 個(gè)周期
如果使用 JMP:
JMP ENTER (4)
...
ENTER:
PSHS IP (7) 保舊的 IP 到返回棧上
LDX -2,IP (6) 重新得到代碼域地址
LEAY 3,X (5) 加 3 存入 IP ( Y )寄存器中
NEXT (9)
以上總計(jì) 31 個(gè)周期
因?yàn)?MC6809 的尋址模式允許另外一級(jí)的間接,所以 6809 的 NEXT 不使用 W 寄存器。 ENTER 的 JMP 版本必須再次讀取代碼域的地址 -- NEXT 沒有在任何寄存器中留下這個(gè)地址。 JSR 可以通過彈出返回棧直接得到參數(shù)域地址。所以, JMP 版本更快。
不論哪一種方式, EXIT 都是一樣的:
EXIT:
PULS IP 從返回棧中彈出“保存的”IP
NEXT 繼續(xù) Forth 解釋
有些寄存器尚未分配。你可以把用戶指針放到存儲(chǔ)器中,這樣的 Forth 也運(yùn)行得很好。不過 DP 寄存器就浪費(fèi)了,而 DP 也沒有什么其它的用處。讓我們使用一個(gè)“技巧”來實(shí)現(xiàn),我們把 UP 的高位搬到 DP 中(它的低字節(jié)是 0 )。
還有一個(gè)沒有使用的寄存器 D 寄存器,許多算術(shù)操作需要這個(gè)寄存器。它應(yīng)該自由地作為一個(gè)可隨意使用的寄存器呢?還是應(yīng)該作為棧頂元素呢? MC6809 使用存儲(chǔ)器作為一個(gè)操作數(shù),所以并不需要第二個(gè)工作寄存器。如果臨時(shí)需要寄存器,把 D 壓入和彈出也很容易。
所以我們只能對(duì)兩種方式編寫測(cè)試程序,看看哪個(gè)更快。
NEXT 、 ENTER 和 EXIT 不使用堆棧,在各種情況下的代碼都是一樣的。
DOVAR 、 DOCON 和 LIT 在兩種情況下所用的時(shí)鐘周期數(shù)相同。這就解釋了我們以前談到的把 TOS 放到寄存器中僅僅改變 PUSH 或者 POP 的位置:
SWAP 、 ROT 、 0= 、 @ 特別是 + 通過把 TOS 放到寄存器中而加快 :
但是, ! 和 +! 卻由于 TOS 放到寄存器中而變慢 :
這些字變慢的原因是許多訪問存儲(chǔ)器的 Forth 字希望地址在棧頂,所以需要一個(gè)額外的 FTR 指令。這就是為什么 TOS 寄存器必須是一個(gè)地址寄存器。不幸的是, MC6809 的地址寄存器都用于更重要的 W 、 IP 、 PSP 和 RSP 了。不過,把 TOS 放到寄存器中對(duì)于 ! 和 !+ 的損失可以通過許多算術(shù)和堆棧操作運(yùn)行速度的提高而得到彌補(bǔ)。
個(gè)案研究 2 : 8051
如果說 MC6809 是 Forth 系統(tǒng)實(shí)現(xiàn)者的美夢(mèng),那 Intel 8051 簡(jiǎn)直就是 Forth 實(shí)現(xiàn)者的惡夢(mèng)了。它只有一個(gè)通用的地址寄存器,一種尋址模式,總是使用一個(gè) 8 位累加器。
所有的算術(shù)操作、許多的邏輯操作都必須使用累加器。一個(gè)唯一的 16 位操作是 INC DPTR 。硬件堆棧必須使用 128 字節(jié)的片內(nèi)寄存器文件,這樣的 CPU 簡(jiǎn)直就是一堆破銅爛鐵!
有些 8051 Forth 實(shí)現(xiàn)了一個(gè) 16 位的 Forth ,但是它們太慢而不能滿足我們的要求。讓我們進(jìn)行某些權(quán)衡,以產(chǎn)生一個(gè)更快的 8051 Forth 系統(tǒng)。
我們最初的想法是利用那個(gè)唯一的地址寄存器。所以我們用 8051 的程序計(jì)數(shù)器作為 IP -- 也就是說,我們構(gòu)造一個(gè)子程序串線的 Forth 系統(tǒng)。如果編譯器在所有可能的情況下都使用 2 字節(jié)的 ACALL 代替 3 字節(jié)的 LCALL ,多數(shù)的 STC 代碼將和 ITC/STC 一樣小。
子程序串線意味著返回棧指針就是硬件堆棧指針。片上寄存器文件共有 64 個(gè)單元空間,但是這些空間并不足以支持多任務(wù)堆棧。面對(duì)這種情況下你可以考慮以下幾個(gè)策略:
?? 限制這個(gè) Forth 系統(tǒng)為單任務(wù)系統(tǒng);
?? 在所有的 Forth 定義入口處把返回地址保存到一個(gè)外部 RAM 軟件堆棧中;
?? 在任務(wù)切換的時(shí)候把全部返回棧的內(nèi)容保存到外部 RAM 中。
第二種方法是最慢的!在每個(gè)任務(wù)切換的時(shí)候移動(dòng) 128 個(gè)字節(jié)比在每個(gè) Forth 字中移動(dòng)兩個(gè)字節(jié)要快得多?,F(xiàn)在我選擇 1 ,而將選擇 3 留作以后擴(kuò)充。
唯一一個(gè)真正的地址寄存器 DPTR 將要擔(dān)負(fù)多種使命。它就是 W ,多用途的工作寄存器。
實(shí)際上,還有兩個(gè)寄存器可以尋址外部存儲(chǔ)器: R0 和 R1 。它們僅僅提供 8 位地址,高 8 位將顯式地輸出到口 2 上。但是對(duì)于堆棧,這是一個(gè)可以容忍的限制,因?yàn)槲覀兛梢园讯褩O拗圃?256 字節(jié)空間。所以我們使用 R0 作為 PSP 。
同樣的 256 字節(jié)可以用于用戶數(shù)據(jù)區(qū),這使得 P2 (口 2 )成為用戶指針的高字節(jié),像 MC6809 一樣,而低字節(jié)隱含是 0.
于是 8051 的程序員模型就變成了:
寄存器地址 8051 名字 Forth 使用
0 R0 PSP 的低字節(jié)
1 R1
2 R2
3 R3
4 R4
5 R5
6 R6
7 R7
8-7Fh 120 字節(jié)的返回棧
81h SP RSP 的低字節(jié)(高位字節(jié) = 0 )
82-83h DPTR W 寄存器
A0h P2 UP 和 PSP 的高字節(jié)
E0h A
F0h B
注意我們僅僅使用了 BANK0 , 另外的 3 個(gè)寄存器 BANK 從 08H 到 1FH , 從 20H 到 2FH 的位尋址寄存器都沒有被 Forth 使用。使用 BANK0 可以為返回棧得到最大的連續(xù)空間。如果需要,返回棧還可以縮小。
在子程序串線的 Forth 系統(tǒng)中,不需要 NEXT、ENTER 和 EXIT 。
如何處理?xiàng)m斣啬?#xff1f;在 8051 中,有許多的寄存器,而存儲(chǔ)器操作卻非常昂貴。我們把 TOS 放到 R3:R2 中(按 INTEL 格式,R3 是高字節(jié))。注意,我們不能使用 B:A 寄存器對(duì) -- A 寄存器是一個(gè)漏斗,所有的寄存器引用都要通過它進(jìn)行。
8051 采用了“哈佛”體系結(jié)構(gòu):程序和數(shù)據(jù)在分開的存儲(chǔ)器中存放。(Z8 和 TMS320 是哈佛體系結(jié)構(gòu)的另外兩個(gè)例子)。但 8051 使用的是一種“野蠻”的退化形式:軟件沒有辦法從物理上向程序存儲(chǔ)器寫,這就意味著 Forth 的開發(fā)者只能使用下述兩個(gè)方式:
?? 交叉編譯全部程序,包括應(yīng)用程序,放棄實(shí)現(xiàn)一個(gè) 8051 交互式 Forth 的努力;
?? 使一部分或者全部的程序存儲(chǔ)器在數(shù)據(jù)空間可見,最簡(jiǎn)單的辦法就是使這兩個(gè)空間完全覆蓋。
相比 Z8 和 TMS320 就比較文明,它們?cè)试S向程序存儲(chǔ)器寫入。Forth 內(nèi)核的具體實(shí)現(xiàn)將在以后討論。
個(gè)案研究 3 : Z80
選擇討論 Z80 是因?yàn)樗欠钦?CPU 的一個(gè)極端例子,它有 4 個(gè)不同種類的地址寄存器,有些操作使用寄存器 A 作為目的寄存器,有些則可以是任意的8位寄存器,有些是 HL 寄存器對(duì),有些則可以是任意的16位寄存器,等等。有些操作(比如 EX DE, HL )卻只允許一種寄存器組合。
在 Z80 這類的 CPU 中(或者同樣在 8086 中), Forth 功能的指定必須仔細(xì)匹配 CPU 寄存器的能力。許多方案需要評(píng)估,而唯一的辦法常常就是對(duì)不同的決策方案編寫各種代碼進(jìn)行測(cè)試。為了避免本文變成為一堆“代碼列表”,我選擇了基于許多 Z80 編碼經(jīng)驗(yàn)的一種寄存器指定,它說明了這些選擇可以合理地解釋早期討論的一般原理。
我希望得到一個(gè)傳統(tǒng)的 Forth ,盡管我使用了直接串線技術(shù)。我需要全部的“經(jīng)典”虛擬寄存器。
忽略其它的寄存器集, Z80 的6個(gè)地址寄存器具有下列能力:
?? 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 也可以作為位寄存器對(duì)來處理。
8 位寄存器 A 必須留作臨時(shí)寄存器,因?yàn)樵S多 ALU 操作和存儲(chǔ)器引用操作都使用它作為目的。
HL 無疑是最通用的寄存器,可以逐個(gè)試著用它作為每個(gè)虛擬寄存器。然而,由于它的通用性 -- 它是唯一可以讀取字格式和支持間接跳轉(zhuǎn)的寄存器 -- HL 應(yīng)該作為 Forth 的通用工作寄存器 W 。
由于 IX 、 IY 都有索引尋址模式并可用 ALU 操作,所以可以考慮用它們作為堆棧指針 。但是它們有兩個(gè)主要的問題:通用的堆棧指針 SP 寄存器沒有用,而 IX/IY 卻特別慢!
在 Forth 的兩個(gè)棧上都有許多 16 位的 PUSH / POP 類操作,對(duì)于 SP 來說,這些操作只需要一條指令,而 IX 或者 IY 操作卻需要 4 條指令。所以兩個(gè)堆棧之一應(yīng)該用 SP 實(shí)現(xiàn),這應(yīng)該是參數(shù)棧,因?yàn)樗氖褂妙l率比返回棧要高。
如何考慮 Forth 的 IP 寄存器呢?在大多數(shù)情況下, IP 都是從存儲(chǔ)器讀取并且自動(dòng)增量的,使用 IX/IY 作為 IP 不會(huì)比使用 BC/DE 有任何編程上的好處,考慮 IP 的速度,使用 BC/DE 對(duì)卻更快。讓我們把 IP 放到 DE 中:它可以與 HL 的內(nèi)容交換,而后者是通用的。
需要第二個(gè) Z80 寄存器對(duì)(不是 W )進(jìn)行 16 位的算術(shù)運(yùn)算。現(xiàn)在只有 BC 了,它可以用于尋址或者與 A 進(jìn)行 ALU 操作。但是,我們是用 BC 作為第二個(gè)工作寄存器“X”、還是作為棧頂元素呢?只有編碼才能得到結(jié)論。現(xiàn)在,讓我們樂觀地假定 BC = TOS 。
只剩下 RSP 和 UP 了,還有 IX 和 IY 寄存器沒有分配。 IX 和 IY 是等效的,我們?cè)O(shè) IX = RSP , IY = UP 。
于是, Z80 Forth 系統(tǒng)的寄存器分配如下
BC = TOS IX = RSP
DE = IP IY = UP
HL = W SP = PSP
現(xiàn)在讓我們看看 DTC 的 Forth 系統(tǒng) 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) 跳轉(zhuǎn)到 W 中的地址
還可以有其它的版本(具有同樣的時(shí)鐘周期)
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) 轉(zhuǎn)到 W 中的地址
注意單元是以低位字節(jié)優(yōu)先的方式存放在存儲(chǔ)器中的。同樣,盡管看起來把 IP 保存在 HL 寄存器中有許多好處,但實(shí)際上卻沒有。這是由于 Z80 不能進(jìn)行 JP (DE) 。 NEXT-HL 進(jìn)入將更短一些。
僅僅用于比較,讓我們看一下 ITC NEXT 。以前給出的偽代碼需要另一個(gè)臨時(shí)寄存器“X”,它的內(nèi)容用于間接跳轉(zhuǎn)。令 DE = X, BC = IP, TOS 保存在存儲(chǔ)器中。
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) 跳轉(zhuǎn)到 X 中的地址
JP (HL) (4)
這就把“W”加 1 并放到在 DE 寄存器中了。只要這是一致的,就不會(huì)有任何問題 -- 代碼知道在需要 W 的內(nèi)容時(shí)如何去找到它,以及如何調(diào)整它。
ITC 的 NEXT 是 11 個(gè)同期, DTC 是 7 個(gè)同期。 ITC 沒有將 TOS 保存在寄存器中的能力,所以我選擇 DTC 。
如果使用內(nèi)嵌編碼, DTC NEXT 在每個(gè) CODE 字中需要 7 個(gè)字節(jié)。一個(gè)直接跳轉(zhuǎn)到 NEXT 的子程序只需要 3 個(gè)字節(jié),但需要附加 10 個(gè)時(shí)鐘周期,這是一個(gè)特別的例子,我們選擇的是內(nèi)嵌方式的 NEXT 。但有時(shí) NEXT 特別大,或者存儲(chǔ)器很小,更謹(jǐn)慎的決策可能是使用 JMP 到 NEXT 。
現(xiàn)在讓我們來看 ENTER 的代碼。使用一個(gè) CALL ,可以彈出硬件堆棧以得到參數(shù)域地址:
CALL ENTER (17)
...
ENTER:
DEC IX (10) 把老的 IP 放到返回棧上
LD (IX+0),D (19)
DEC IX (10)
LD (IX+0),E (19)
POP DE (10) 參數(shù)域地址 -> IP
NEXT (38) 7 個(gè)機(jī)器指令的匯編語言宏
實(shí)際上這比 POP HL 快,然而使用最后的 6 個(gè)指令(不用 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) 參數(shù)域地址 -> HL
NEXT-HL (34) 看上面的 DTC 的 NEXT 代碼
總計(jì) 119 個(gè)周期
當(dāng)使用 JP 時(shí), W 寄存器( HL )依然指向代碼域。代碼域是其后的 3 個(gè)字節(jié):
JP ENTER (10)
...
ENTER:
DEC IX (10) 把老的 IP 放到返回棧上 LD (IX+0),D (19)
DEC IX (10)
LD (IX+0),E (19)
INC HL ( 6) 參數(shù)域地址 -> IP
INC HL ( 6)
INC HL ( 6)
NEXT-HL (34)
總計(jì) 120 個(gè)周期
由于改變了 NEXT 的入口, IP 的新值就不必放入 DE 寄存器對(duì)了。
CALL 版本快了 1 個(gè)周期。在嵌入式系統(tǒng)應(yīng)用 Z80 時(shí),我們還可以使用單字節(jié)的 RST 指令來得到速度和空間的雙重收益,但是在基于 Z80 的個(gè)人計(jì)算機(jī)上,這個(gè)策略并不可用(操作系統(tǒng)使用了這個(gè)特性,即操作系統(tǒng)的系統(tǒng)調(diào)用是通過這個(gè)接口進(jìn)入的)。
個(gè)案研究 4 : INTEL 8086
Intel 的 8086 是另一個(gè)有教育意義的 CPU 。我們不再詳細(xì)討論設(shè)計(jì)過程,只是看一個(gè)新的用于 PC 的共享軟件: Pygmy Forth [SER90].
Pygmy 是一個(gè)直接串線的 Forth 系統(tǒng),棧頂元素保存在寄存器中。 8086 寄存器是這樣安排的:
AX = W DI = scratch
BX = TOS SI = IP
CX = scratch BP = RSP
DX = scratch SP = PSP
許多 8086 Forth 系統(tǒng)的實(shí)現(xiàn)使用 SI 寄存器作為 IP ,所以 NEXT 可以通過 LODSW 指令實(shí)現(xiàn)。在 Pygmy 的 DTC 實(shí)現(xiàn)中, NEXT 是這樣的:
NEXT:
LODSW
JMP AX
這已經(jīng)小得足以嵌入到每個(gè) CODE 字中了 。
高級(jí)“定義” Forth 字使用一個(gè) JMP (相對(duì))指令轉(zhuǎn)向它們的機(jī)器碼。 ENTER 子程序(在 Pygmy 中稱為 'docol' )因此需要從 W 中得到參數(shù)域地址。
ENTER:
XCHG SP,BP
PUSH SI
XCHG SP,BP
ADD AX,3 參數(shù)域地址 -> IP
MOV SI,AX
NEXT
注意交換兩個(gè)堆棧指針的 XCHG 用法,這允許對(duì)兩個(gè)堆棧都使用 PUSH 和 POP 指令,這比使用基于 BP 的直接尋址指令要快。
EXIT:
XCHG SP,BP
POP SI
XCHG SP,BP
NEXT
段模型
Pygmy Forth 是一個(gè)單段的 Forth 系統(tǒng),所有的代碼和數(shù)據(jù)都在一個(gè) 64K 字節(jié)的段中, 這相當(dāng)于 Turbo C 的緊縮模式。到目前為止,我們討論的 Forth 標(biāo)準(zhǔn)都假設(shè)所有的東西全部包含在單一的存儲(chǔ)器地址空間,使用同樣的讀寫操作符。然而, IMP PC Forth 開始使用多個(gè)段來處理 5 種不同的數(shù)據(jù),它們是:
CODE …… 機(jī)器代碼
LIST …… 高級(jí) Forth 串線 ( 所以這個(gè)段也稱為 THREADS)
HEAD …… Forth 字的首部
STACK …… 參數(shù)和返回棧
DATA …… 變量和用戶定義數(shù)據(jù)
這就允許 PC 機(jī)上的 Forth 突破 64K 字節(jié)的段限制,而又不需要在一個(gè) 16 位的 CPU 上實(shí)現(xiàn)一個(gè) 32 位的 Forth 系統(tǒng)。但是,實(shí)現(xiàn)一個(gè)多段的模型、分支到 Forth 核心等等內(nèi)容已經(jīng)遠(yuǎn)遠(yuǎn)超出了本文的討論范圍。
參考文獻(xiàn)
[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 實(shí)現(xiàn)
[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 設(shè)計(jì)決策中存在一個(gè)很大的錯(cuò)誤,在我編碼 Forth 字 EXECUTE 的時(shí)候,它變得非常明顯。
EXECUTE 引起一個(gè) Forth 字的執(zhí)行,它的地址在參數(shù)棧上。更精確地說:編譯地址、或者說代碼域地址在參數(shù)棧上給出。這可以是任何類型的 Forth 字: CODE 定義、冒號(hào)定義、 CONSTANT 、 VARIBLE 或者是定義字。與通常的 Forth 解釋過程不同的是,執(zhí)行字的地址在棧上給出,而不是通過“串線”給出(通過 IP 指定)。
在我們的直接串線 MC6809 中,這可以很容易地編碼 :
EXECUTE:
TFR TOS,W 把字的地址放到 W 中
PULU TOS 彈出新的 TOS
JMP ,W 跳到 W 給定的地址
注意:應(yīng)該是 JMP ,W 而不是 JMP [,W], 因?yàn)槲覀円呀?jīng)有了這個(gè)字的代碼地址,不是從高級(jí)線程中讀取的。如果 TOS 不在寄存器中, EXECUTE 可以更簡(jiǎn)單地實(shí)現(xiàn) JMP [,PSP++] ?,F(xiàn)在假設(shè)這個(gè)被執(zhí)行的字是一個(gè)冒號(hào)定義, W 將要指向它的代碼域,其中包含有 JMP ENTER 。 如下所示:
JMP ENTER
...
ENTER:
PSHS IP
LDX -2,IP 重新取得代碼域地址
LEAY 3,X
NEXT
這就是錯(cuò)誤所在!因?yàn)槲覀儾皇菑拇€中執(zhí)行這個(gè)字,所以 IP 并沒有指向代碼域地址的一個(gè)拷貝。記住: EXECUTE 字的地址來自于堆棧。這種方式的 ENTER 不能與 EXECUTE 一同工作,因?yàn)闆]有辦法得到將要執(zhí)行的字的地址。
這也同時(shí)提出了 DTC Forth 的一個(gè)新規(guī)則:如果 NEXT 沒有把將要執(zhí)行的字的地址放到一個(gè)寄存器中,你就必須在代碼域中使用 CALL 。
于是, MC6809 Forth 只好倒退回在代碼域中使用 JSR 的方法。但是, ENTER 是 Forth 中使用最多的代碼片斷,為了避免速度的損失,我完成了上一章中的“學(xué)生練習(xí)”。注意當(dāng)你交換 RSP 和 PSP 時(shí)發(fā)生了什么:
執(zhí)行新版本需要 31 個(gè)周期,這與我前面使用的 JMP 版本的時(shí)間一樣。其中的改進(jìn)是由于 JSR 版本的 ENTER 同時(shí)使用 Forth 的返回棧和 MC6809 子程序返回棧( JSR 棧)。使用兩個(gè)不同的堆棧指針意味著我們不必與IP“交換” TOS ,也就不需要任何的臨時(shí)寄存器了。
這也解釋了一個(gè)新 Forth 內(nèi)核通常的開發(fā)過程:先做出一些設(shè)計(jì)決策,然后寫出一些簡(jiǎn)單的代碼,再找出一個(gè) BUG 或者一個(gè)更好的方法做這件事情,改變某些設(shè)計(jì)策略,重新編寫示例代碼,重復(fù)這個(gè)過程直到滿意為止。
這給了我們一個(gè)教訓(xùn):把 EXECUTE 做為一個(gè)基準(zhǔn)測(cè)試字。
Carey Bloodworth of Van Buren, AR 指出了上一版本 MC6809 中的一個(gè)小的、但是讓我不好意思的錯(cuò)誤:
對(duì)于 0= 的“ TOS 在存儲(chǔ)器”版本,我應(yīng)該這樣編寫代碼:
LDD ,PSP
CMPD #0
這是為了測(cè)試 TOS 是否為 0 ??墒窃谶@種情況下, CMPD 指令完全是多余的,因?yàn)?LDD 指令在 D 寄存器為 0 時(shí)將設(shè)置 Zero 標(biāo)志。 TOS 在 D 寄存器的版本還是需要 CMPD 指令的,但是比 TOS 在存儲(chǔ)器版本執(zhí)行速度更快。
現(xiàn)在讓我們開始討論主題
什么是代碼域?
DOES 的概念看起來是 Forth 中最難懂和最神秘的一部分,不過 DOES 也是使 Forth 具有強(qiáng)大能力的一個(gè)原因 -- 在許多方面,它是先天面向?qū)ο蟮摹?DOES 的行為和能力也與 Forth 最閃亮的方面有著聯(lián)系:代碼域。
回憶第一部分, Forth 的定義體由兩個(gè)部分組成:代碼域和參數(shù)域。你可以從不同的方面來考察這兩個(gè)域:
?? 代碼域是這個(gè) Forth 字的動(dòng)作,參數(shù)域是與動(dòng)作有關(guān)的數(shù)據(jù);
?? 代碼域是一個(gè)子程序調(diào)用,參數(shù)域是調(diào)用后面的“內(nèi)嵌”參數(shù)(匯編程序員觀點(diǎn));
?? 代碼域是字類的單個(gè)“方法”,參數(shù)域是某個(gè)特別字的“實(shí)例變量”(面向?qū)ο蟪绦騿T的觀點(diǎn));
所有這些觀點(diǎn)都有著共同點(diǎn):
?? 代碼域子程序在調(diào)用時(shí)至少有一個(gè)參數(shù),它就是這個(gè)要執(zhí)行的 Forth 字的參數(shù)域地址,參數(shù)域可以包含有任何數(shù)目的參數(shù) ;
?? 只有幾個(gè)相對(duì)不多的特殊動(dòng)作,或者說,代碼域只引用為數(shù)不多的幾個(gè)特殊子程序(我們后面將會(huì)看到,這對(duì)于 CODE 例外)。我們可以回憶一下第 2 部分的 ENTER 子程序:這個(gè)通用的子程序被所有的 Forth 冒號(hào)定義引用 ;
?? 對(duì)參數(shù)域的解釋隱含地由代碼域的內(nèi)容去解釋?;蛘哒f,每個(gè)代碼域子程序希望參數(shù)域包含一定類型的數(shù)據(jù) ;
一個(gè)典型的 Forth 內(nèi)核有以下幾個(gè)預(yù)定義的代碼域子程序 .
Forth 之所以強(qiáng)大的原因在于 Forth 程序并不限于只能使用這些代碼域子程序(或者只能使用你的 Forth 系統(tǒng)內(nèi)核所提供的其它子程序集)。程序員可以定義新的代碼域子程序,可以定義一個(gè)新的參數(shù)域類型與之匹配。用面向?qū)ο蟪绦蛟O(shè)計(jì)方法的“行話”來說,可以創(chuàng)建新的“類”和“方法”(盡管每個(gè)類只有一個(gè)方法)。同時(shí),就像其它的 Forth 字一樣 -- 代碼域可以用匯編語言定義,也可以用高級(jí) Forth 字來定義。
為了理解代碼域的機(jī)制和參數(shù)是如何傳遞的,我們首先看看匯編語言(機(jī)器代碼)的情況。我們先考察間接串線(ITC)的情況,它是最容易理解的,然后再看看如何修改這些邏輯到直接串線(DTC)和子程序串線的(STC)上。最后,再看如何使用高級(jí) Forth 定義來描述代碼域的動(dòng)作。
Forth 的編寫者在使用術(shù)語時(shí)有些混亂,所以,我使用我自己的術(shù)語來解釋,如圖 1 所示。首部包含有字典信息,與一個(gè) Forth 字的執(zhí)行沒有關(guān)系。體是這個(gè)字的“工作”部分,包含有固定長(zhǎng)度的代碼域和可變長(zhǎng)度的參數(shù)域。對(duì)于任何一個(gè)給定的字,這兩個(gè)域在存儲(chǔ)器中的位置分別被稱為代碼域地址(CFA)和參數(shù)域地址(PFA)。一個(gè)字的代碼域地址就是這個(gè)字在存儲(chǔ)器中的位置。不要把這個(gè)與代碼域的內(nèi)容相混淆,在 ITC 中,內(nèi)容是另一個(gè)不同的地址。
需要明確的是:代碼域的內(nèi)容是另外一片存儲(chǔ)器的地址,在那一片存儲(chǔ)器中是機(jī)器代碼。我把這個(gè)地址稱為代碼地址。最后,當(dāng)討論 DTC 和 STC Forth 時(shí),我也引用“代碼域內(nèi)容”,它的含義比代碼域地址更多。
圖 1 一個(gè) ITC Forth 字
機(jī)器代碼動(dòng)作
Forth 的 CONSTANT 可能是最簡(jiǎn)單的機(jī)器代碼例子。讓我們考察一個(gè)法語的例子:
1 CONSTANT UN
2 CONSTANT DEUX
3 CONSTANT TROIS
執(zhí)行 UN 會(huì)把值 1 壓入堆棧,執(zhí)行 DEUX 把 2 壓入堆棧等等。(不要把參數(shù)棧和參數(shù)域混淆,它們是完全獨(dú)立的)
在 Forth 內(nèi)核中有一個(gè)字稱為 CONSTANT 。這并不是一個(gè)常數(shù)類的字本身,它是一個(gè)高級(jí) Forth 定義。 CONSTANT 是一個(gè)“定義字”:它在 Forth 字典中創(chuàng)建一個(gè)新字,通過它我們能夠創(chuàng)建新的“常數(shù)類”字 UN 、 DEUX 和 TROIS 。你也可以把它們理解成常數(shù)“類”的一個(gè)個(gè)“實(shí)例”。這三個(gè)字都有自己的代碼域,都指向同樣的 COSNTANT 動(dòng)作的機(jī)器代碼片斷。
這個(gè)代碼片斷應(yīng)該執(zhí)行什么動(dòng)作呢?圖 2 給出了這三個(gè)常數(shù)的存儲(chǔ)器表示。所有這三個(gè)字都指向共同的動(dòng)作子程序。這些字的區(qū)別在于它們的參數(shù)域,這里簡(jiǎn)單地包含有常數(shù)的值,或者用面向?qū)ο蟮恼f法是“實(shí)例變量”。所以,這三個(gè)字的動(dòng)作都應(yīng)該是讀取參數(shù)域的內(nèi)容,并把它們放到棧頂。這段代碼也隱含地知道參數(shù)域包含一個(gè)單元大小的值。
圖 2 三個(gè)常數(shù)
為了寫出做這件事情的機(jī)器代碼片斷,我們需要知道怎樣才能找到參數(shù)域的地址,之后 Forth 的解釋器就可以跳轉(zhuǎn)到機(jī)器代碼。那么,PFA 是如何傳遞給機(jī)器代碼子程序的呢?并且, Forth 解釋器的 NEXT 是如何編碼的呢?這依賴于不同的實(shí)現(xiàn)。為了寫出機(jī)器代碼動(dòng)作,我們首先需要理解 NEXT 。
ITC 的 NEXT 在第一部分已經(jīng)用偽碼描述了,以下是 MC6809 的實(shí)現(xiàn),使用 Y=IP,X=W:
NEXT: LDX ,Y++ ; (IP) -> W, IP+2 -> IP
JMP [,X] ; (W) -> temp, JMP (temp)
假設(shè)我們的高級(jí)串線中有這樣的代碼:
... SWAP DEUX + ...
當(dāng) NEXT 被執(zhí)行時(shí),使用 IP 解釋指針指向 DEUX “指令”(緊接在 SWAP 之后),圖 3 解釋了發(fā)生的事情。 IP (寄存器 Y )指向高級(jí)串線內(nèi)部的一個(gè)存儲(chǔ)器單元,它包含有 Forth 字 DEUX 的地址。更精確地說,這個(gè)單元包含有字 DEUX 的代碼域地址。于是,當(dāng)我們使用 Y 讀取一個(gè)單元時(shí),自動(dòng)增量 Y ,我們就得到了 DEUX 的代碼域地址。把它寫入 W (寄存器 X ), W 現(xiàn)在已經(jīng)指向了代碼域,是一個(gè)機(jī)器代碼片斷的地址。我們可以讀取這個(gè)單元的內(nèi)容,然后使用一條MC6809 指令跳轉(zhuǎn)到相應(yīng)的機(jī)器代碼處執(zhí)行。這個(gè)過程并沒有改變寄存器 X ,所以 W 仍然指向 DEUX 的 CFA ,我們就可以得到參數(shù)域地址,它在代碼域之后兩個(gè)字節(jié)的位置。
圖 3 ITC 在 NEXT 之前和之后的情況
所以,機(jī)器代碼片斷只需要把 W 加 2 ,讀取這個(gè)地址的單元內(nèi)容,把它壓到棧上。這個(gè)代碼片斷通常被稱為 DOCON
DOCON:
LDD 2,X ; 讀取 W+2 處的單元
PSHU D ; 把它放到參數(shù)棧是
NEXT ; ( 宏 ) 跳轉(zhuǎn)到下一個(gè)高級(jí)字
這個(gè)例子中, TOS 在存儲(chǔ)器中。注意前面的 NEXT 已經(jīng)把 IP 增加了 2 ,所以當(dāng) DOCON 做 NEXT 時(shí),它已經(jīng)指向了串線的下一個(gè)單元(“+” 的 CFA )。
通常, ITC Forth 會(huì)在 W 寄存器中留下參數(shù)域地址或者一些“鄰近”的地址。在這種情況下, W 包含有 CFA ,它在這個(gè) Forth 實(shí)現(xiàn)中總是 PFA - 2 。由于除了 CODE 之外的每類 Forth 字都需要使用參數(shù)域地址,許多 NEXT 實(shí)現(xiàn)方法都是增量 W 使它指向 PFA 。我們可以在 MC6809 上做一些小的改變:
NEXT:
LDX ,Y++ ; (IP) -> W, IP + 2 -> IP
JMP [,X++] ; (W) -> temp, JMP (temp), W+2 -> W
這使 NEXT 增加了 3 個(gè)周期,但是把參數(shù)域地址放入了 W 寄存器。對(duì)于代碼域子程序它做了些什么呢?
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 個(gè)周期的代價(jià)中我們得到了什么收益呢? DOCON 減少了 1 個(gè)周期, DOVAR 減少了 5 個(gè)周期, ENTER 減少了 1 個(gè)周期。 CODE 字不使用 W 中的值,所以它們沒有從自動(dòng)增量中受益。速度的增加或者損失要通過 Forth 字的混合執(zhí)行來考察。通常的規(guī)則是執(zhí)行最多的字是 CODE 字,這樣,在 NEXT 中增量 W 會(huì)有一點(diǎn)點(diǎn)速度上的損失 -- 當(dāng)然也節(jié)省了存儲(chǔ)器 -- 不過 DOCON , DOVAR 和 ENTER 只出現(xiàn)一次,得到的收益并不明顯。
說來說去,最好的結(jié)論還是依賴于具體的處理器。比如像 Z80 這樣的處理器只能通過字節(jié)訪問存儲(chǔ)器,它沒有自動(dòng)增量指令,所以通常的情況下,最好是保留 W 指向 IP+1 (從代碼域讀取的最后一個(gè)字節(jié))。而在有些機(jī)器上,自動(dòng)增量是“免費(fèi)的”,這時(shí)讓 W 指向參數(shù)域就是最方便的。
注意:在一個(gè)系統(tǒng)中決策必須一致。如果 NEXT 讓 W 在執(zhí)行時(shí)指向 PFA ,則 EXECUTE 也必須這樣做(這就是為什么我在本文的開頭拼命更正的原因)。
直接串線
直接串線和間接串線差不多,除了代碼域的內(nèi)容:它不再是一些機(jī)器代碼的地址,而是 JUMP 或者 CALL 。這樣做可能會(huì)使得代碼域更大 -- 比如在 MC6809 上要大 1 個(gè)字節(jié),但是,它省去了 NEXT 子程序中的一級(jí)間接。
在代碼域中選擇 JUMP 還是 CALL 指令依賴于機(jī)器碼子程序如何得到參數(shù)域地址。為了跳轉(zhuǎn)到代碼域,許多 CPU 要求把它的地址放在一個(gè)寄存器中。例如, Intel 8086 的間接跳轉(zhuǎn)指令是 JMP AX (或者其它的寄存器),在 Z80 上是 JP ( HL 或者 IX 或者 IY)。在這些處理器上, DTC 的 NEXT 包括兩個(gè)操作,在 MC6809 上將變成:
NEXT:
LDX ,Y++ ; (IP) -> W, IP + 2 -> IP
JMP ,X ; JMP (W)
在 Intel 8086 上,這兩條指令可以是 LODSW 和 JMP AX ,其中的影響可以通過圖 4 的 CASE1 說明。 DEUX 的代碼域地址是從高級(jí)串線中讀取的, IP 被增量。然后,不再進(jìn)行讀取操作,而是用一個(gè) JUMP 指令跳轉(zhuǎn)到代碼域。也就是說, CPU 直接跳轉(zhuǎn)到代碼域。 CFA 被留在 W 寄存器中,就像上面 ITC 的第一個(gè)例子。由于這個(gè)地址已經(jīng)在寄存器中了,我們可以簡(jiǎn)單地把 JUMP 放到 DOCON 的代碼域中, DOCON 的代碼片斷將和上面描述一樣地工作。
圖 4 DTC 中 NEXT 之前和之后的情況
不過,我們也許會(huì)注意到:在有些處理器上,比如 MC6809 和 PDP-11上,可以用一個(gè)指令來實(shí)現(xiàn)這個(gè) DTC NEXT
NEXT:
JMP [,Y++] ; (IP) -> temp, IP+2 -> IP, JMP (temp)
這也能使 CPU 跳轉(zhuǎn)到 DEUX 的代碼域。但其中有一個(gè)巨大的差異:任何寄存器中都沒有留下 CFA !那么機(jī)器代碼片斷如何得到參數(shù)域的地址呢?答案是:通過使用 CALL (或者 JSR )指令來替代 JUMP 。在許多 CPU 上, CALL 指令會(huì)把返回地址放到返回棧上 -- 這就是緊隨在 CALL 指令之后的地址 。
如圖 4 所示的 CASE2 ,這個(gè)地址就是我們所需要的參數(shù)域地址!所以 DOCON 要做的就是從返回棧得到地址 -- 滿足代碼域放置 JSR 的要求 -- 然后使用這個(gè)地址來讀取常量,于是:
DOCON:
PULS X ; 從返回棧彈出 PFA
LDD ,X ; 讀取參數(shù)域的單元
PSHU D ; 壓入?yún)?shù)棧
NEXT ; ( 宏 ) 轉(zhuǎn)到下一個(gè)高級(jí)字
把這個(gè)同 ITC 版本相比較。 DOCON 多了 1 個(gè)指令,但是 NEXT 少了 1 個(gè)指令。 DOVAR 和 NEXT 也多了 1 個(gè)指令:
DOVAR:
PULS X ; 彈出這個(gè)字的 PFA
PSHU X ; 把那個(gè)地址放到參數(shù)棧上
NEXT
ENTER:
PULS X ; 彈出這個(gè)字的 PFA
PSHS Y ; 壓入老的 IP
TFR X,Y ; PFA 變成了新的 IP
NEXT
現(xiàn)在回到本文的開頭,重新讀一下我的“更正”,看一看為什么我們不能通過 IP 來重新讀 CFA 。同時(shí)也要注意,把 Forth 的堆棧指針給 MC6809 的 U 寄存器而 S 保留的情況與這里討論的不同。
子程序串線
子程序串線(STC)和 DTC 非常相似,都是 CPU 直接跳轉(zhuǎn)到一個(gè) Forth 字的代碼域。但是現(xiàn)在不再有 NEXT 代碼,不再有 IP 寄存器,也沒有 W 寄存器。所以,只能在代碼域中使用 JSR 而不可能有其它的選擇,這是可以得到參數(shù)域地址的唯一辦法。這個(gè)過程如圖 5 所示。
圖 5 STC 的串線編碼
高級(jí)串線是被 CPU 執(zhí)行的一系列子程序調(diào)用。當(dāng)一個(gè) JSR DEUX 被執(zhí)行的時(shí)候,串線中下一個(gè)指令的地址被推進(jìn)返回棧。接著,在字 DEUX 中的 JSR DOCON 被執(zhí)行,它使得另一個(gè)返回地址 -- DEUX 的 PFA 被推入堆棧。 DOCON 可以彈出這個(gè)地址,使用它來讀取常數(shù),把常數(shù)保存在堆棧上,然后用一個(gè) RTS 指令返回到串線:
DOCON:
PULS X ; 從返回棧彈出 PFA
LDD ,X ; 讀取參數(shù)域單元
PSHU D ; 把它壓入?yún)?shù)棧
RTS ; 執(zhí)行下一個(gè)高級(jí)字
在子程序串線代碼中,我們?nèi)匀豢梢匝赜么a域和參數(shù)域這樣的術(shù)語。除了 CODE 和冒號(hào)定義之外的每一個(gè) Forth 字的類中,代碼域是被 JSR 或者 CALL 占用的空間(就像 DTC )一樣,而參數(shù)域就是它后面的空間。所以,在 MC6809 上, PFA 等于 CFA+3 。于是, CODE 和冒號(hào)定義的“參數(shù)域”含意變得有點(diǎn)兒模糊,在本文的后面可以看到這一點(diǎn)。
特例: CODE 字
在以上所有的一般性討論中,有一個(gè)明顯的例外,這就是 CODE 定義 -- 用匯編碼子程序定義的 Forth 字。用“匯編語言來定義一個(gè)字” -- 這個(gè)神奇的功能在 Forth 中很容易實(shí)現(xiàn),因?yàn)槊總€(gè) Forth 字都執(zhí)行一段 Forth 代碼。
包含 CODE 字的匯編代碼總是包含在一個(gè) Forth 字的體中,代碼域必須包含有要執(zhí)行的機(jī)器代碼的地址。所以機(jī)器代碼放在參數(shù)域中,代碼域包含了參數(shù)域的地址,如圖 6 所示。
圖 6 CODE 字
在直接或者子程序串線的 Forth 中,我們可以通過類推,把一個(gè) JUMP 放到代碼域中。代碼域也可以用 NOP 或者相同的結(jié)果填充。更好的是,機(jī)器代碼可以直接從代碼域開始,然后進(jìn)入?yún)?shù)域。從這一點(diǎn)看,代碼域和參數(shù)域就沒有區(qū)別了。這不應(yīng)該有任何疑問,因?yàn)槲覀儾⒉恍枰獙?duì)一個(gè) CODE 字做這樣的區(qū)分。但可能有一些反匯編器和一些聰明的編程技巧需要這一區(qū)分,我們?cè)谶@里就不討論它們了。
CODE 字 -- 不論是怎么實(shí)現(xiàn)的 -- 都是不需要向它傳遞參數(shù)域地址的機(jī)器代碼動(dòng)作。參數(shù)域不包含數(shù)據(jù),只是需要執(zhí)行的代碼。只有 NEXT 需要知道這個(gè)地址(或者代碼域地址),這樣它就可以直接跳到機(jī)器代碼。
使用 ;CODE
現(xiàn)在還有三個(gè)問題沒有回答:
?? 我們?nèi)绾蝿?chuàng)建一個(gè) Forth 字,使得能在它的參數(shù)域中含有一些任意的數(shù)據(jù)?
?? 我們?nèi)绾胃淖円粋€(gè)字的代碼域,以指向可選擇的機(jī)器代碼?
?? 我們?nèi)绾卧诖a片段與一個(gè)使用它的字隔離的情況下編譯(匯編)這個(gè)代碼片段?
對(duì)于第一個(gè)問題的回答是:寫一個(gè) Forth 字來做這一工作。在執(zhí)行的時(shí)候,因?yàn)檫@個(gè)字將在 Forth 字典中定義一個(gè)新的字,所以它被稱為“定義字”。
CONSTANT 就是一個(gè)定義字。一個(gè)定義字的所有“硬工作”都是由一個(gè)內(nèi)核字 CREATE 來完成的,它從輸入流中分析名字,為新字建立頭和代碼域,并把它鏈接到字典中。對(duì)程序員來說,剩下的工作就是構(gòu)造參數(shù)域了。
第二個(gè)、第三個(gè)問題的答案包含在兩個(gè)費(fèi)解的 Forth 字中,它們分別是 (;CODE) 和 ;CODE 。為了理解它們是如何工作的,我們來看看定義字 CONSTANT 實(shí)際上是如何用 Forth 高級(jí)定義來寫的。使用前面 MC6809 的例子:
: CONSTANT ( n -- )
CREATE / 創(chuàng)建一個(gè)新的字
, / 把 TOS 的值寫入字典,作為參數(shù)域的第 1 個(gè)單元
;CODE / 結(jié)束高級(jí)定義,開始匯編代碼
LDD 2,X / DOCON 的匯編代碼片斷
PSHU D
NEXT
END-CODE
這個(gè) Forth 字包含了兩個(gè)部分:從 CONSTANT 到 ;CODE 的任何事情都是在 COSNTANT 被訪問時(shí)執(zhí)行的高級(jí) Forth 代碼。而從 ;CODE 到 END-CODE 的事情都是常數(shù)的“子女” -- 常數(shù)類字比如 UN 和 DEUX -- 執(zhí)行時(shí)要執(zhí)行的機(jī)器代碼。實(shí)際上也就是從字 ;CODE 到 END-CODE 的代碼片段為常量類字將指向的機(jī)器代碼片斷。 ;CODE 表示一個(gè)高級(jí)定義的結(jié)束(;)和一個(gè)機(jī)器代碼定義的開始 (CODE) 。但是,它并不在字典中建立兩個(gè)分離的字,從 CONSTANT 到 END-CODE 的全部?jī)?nèi)容都保存在 CONSTANT 的參數(shù)域中,如圖 7 所示。
圖 7 ITC 的 ;CODE
Derick 和 Baker [DER82] 使用三個(gè)“時(shí)間階段”來幫助理解定義字的行為:
時(shí)間階段 1
是在 CONSTANT 被定義時(shí)的行為。這需要同時(shí)引用高級(jí)編譯器(對(duì)于第一個(gè)部分)和 Forth 匯編器(對(duì)于第二個(gè)部分)。這就是定義 CONSTANT 被加入字典的過程,如圖 7 所示。我們可以看到, ;CODE 這個(gè)編譯指示器是在第一個(gè)階段被執(zhí)行的。
時(shí)間階段 2
是字 CONSTANT 被執(zhí)行時(shí)的行為,這時(shí)一些常數(shù)類字被定義,比如:
2 CONSTANT DEUX
這個(gè)階段就是字 CONSTANT 被執(zhí)行、字 DEUX 被加入字典的時(shí)候。在這個(gè)階段 CONSTANT 的高級(jí)定義部分被執(zhí)行,包括字 (;CODE).
時(shí)間階段 3
是常數(shù)類執(zhí)行時(shí)的行為。在我們的例子中,這個(gè)階段就是 DEUX 被執(zhí)行而把值 2 推入堆棧的時(shí)候。這時(shí) CONSTANT 的機(jī)器代碼被執(zhí)行(回憶 DEUX 的代碼域動(dòng)作)
字 ;CODE 和 (;CODE) 的工作
;CODE 在時(shí)間階段 1 被執(zhí)行,這是 CONSTANT 被編譯的時(shí)候。它是一個(gè) Forth 立即字 -- IMMEDIATE 字 -- 這個(gè)字在 Forth 編譯時(shí)執(zhí)行。
;CODE 做以下三件事情:
?? 它把 Forth 字 (;CODE) 編譯到 CONSTANT
?? 它關(guān)閉 Forth 編譯器,同時(shí)
?? 它打開 Forth 匯編器
而 (;CODE) 是字 CONSTANT 的一部分,它在 CONSTANT 執(zhí)行的時(shí)候才被執(zhí)行(時(shí)間階段 2 ),它執(zhí)行以下動(dòng)作:
?? 它得到緊隨其后的機(jī)器代碼的地址,這可以通過從 Forth 返回棧中彈出 IP 而實(shí)現(xiàn);
?? 它把這個(gè)地址放到 CREATE 定義的字的代碼域中,通過 Forth 字 LAST (有時(shí)也稱為 LATEST )等到這個(gè)字的地址;
?? 它完成 EXIT 的動(dòng)作(也稱為 ;S ),這樣 Forth 的內(nèi)部解釋器就不會(huì)把后面的代碼作為 Forth 串線來執(zhí)行,這是結(jié)束 Forth 串線的高級(jí)“子程序返回”。
F83[LAX84] 解釋了它們?cè)?Forth 系統(tǒng)中的典型編碼:
: ;CODE
COMPILE (;CODE) / 編譯 (;CODE) 到定義中
?CSP [COMPILE] [ / 關(guān)閉 Forth 編譯器
REVEAL / ( 與 ";" 的行為類同 )
ASSEMBLER / 打開匯編器
; IMMEDIATE / 把這個(gè)字設(shè)為立即字
: (;CODE)
> / 彈出機(jī)器代碼地址
LAST @ NAME> / 得到最后一個(gè)字的 CA
! / 保存這個(gè)代碼地址到代碼域
; /
(;CODE) 字在兩個(gè)字當(dāng)中更加微妙。因?yàn)樗且粋€(gè)高級(jí) Forth 定義,在 CONSTANT 中后隨它的地址 -- 高級(jí)返回地址 -- 被壓入 Forth 返回棧中,所以在 (;CODE) 中彈出返回棧能夠得到后隨的機(jī)器代碼地址。同時(shí),從返回棧中彈出這個(gè)值使得一級(jí)高級(jí)子程序被返回“旁路”,這樣在 (;CODE) 退出的時(shí)候,它可以退到 CONSTANT 的調(diào)用者。這等效于返回到 COSNTANT ,并使得 CONSTANT 立即返回。通過圖 7 并跟蹤字 CONSTANT 和 (;CODE) 的執(zhí)行可以更加清楚地看到這是如何工作的。
直接和子程序串線
對(duì)于 DTC 和 STC ,;CODE 和 (;CODE) 的動(dòng)作與 ITC 相同,但是也有一個(gè)重要的例外:它不再保存一個(gè)地址,而在代碼域中放有 JUMP 或者 CALL 指令。對(duì)于一個(gè)絕對(duì) JUMP 或 CALL ,可能唯一要做的事情就是把地址保存在代碼域的最后,作為 JUMP 或者 CALL 指令的操作數(shù)。在 MC6809 的情況下,地址作為 3 字節(jié) JSR 指令的最后 2 個(gè)字節(jié)保存。但是某些 Forth 系統(tǒng)比如 Intel 8086 的 Pygmy Forth ,它們?cè)诖a域中使用相對(duì)轉(zhuǎn)移指令。在這種情況下,必須計(jì)算相對(duì)偏移量并把它們插入到分支指令中。
高級(jí) Forth 行為
你已經(jīng)看到了如何讓 Forth 字執(zhí)行一個(gè)指定的匯編語言代碼片段,如何向這個(gè)片斷傳遞字的參數(shù)域地址,但是我們?nèi)绾斡酶呒?jí) Forth 定義“寫出”子程序的行為呢?
每個(gè) Forth 字必須 -- 通過 NEXT 的行為 -- 執(zhí)行一些機(jī)器語言子程序。這就是代碼域的全部。因此,一個(gè)機(jī)器子程序、或者一系列子程序需要解決如何訪問高級(jí)行為的問題。我們稱這個(gè)子程序?yàn)?DODOES 。
這里有三個(gè)問題需要解決:
?? 我們?nèi)绾握业脚c這個(gè)字相關(guān)聯(lián)的高級(jí)行為子程序的地址?
?? 我們?nèi)绾螐臋C(jī)器代碼中為調(diào)用一個(gè)高級(jí)行為子程序而訪問 Forth 解釋器?
?? 我們?nèi)绾蜗蚰莻€(gè)子程序傳遞我們正在執(zhí)行的字的參數(shù)域地址?
對(duì)于第三個(gè)問題的回答是:很容易,用我們?yōu)橐粋€(gè)高級(jí) Forth 子程序在參數(shù)棧上傳遞參數(shù)的方法。我們的機(jī)器語言子程序在訪問高級(jí)串線之前必須把參數(shù)域地址推到堆棧上(從我們以前的工作看,我們知道機(jī)器語言如何能夠得到 PFA )
第二個(gè)問題的答案有一點(diǎn)困難?;旧衔覀兛梢韵?Forth 字 EXECUTE 那樣做一些事情來訪問一個(gè) Forth 字;或者也可能是 ENTER ,它訪問一個(gè)冒號(hào)定義。它們都是我們的“關(guān)鍵”核心字, DODOES 與此類似。
第一個(gè)問題好象有些難度。我們把高級(jí)子程序的地址放到哪里呢?記住:代碼域并不指向高級(jí)代碼,它必須指向機(jī)器代碼。在 Forth 的歷史上,人們?cè)?jīng)使用過以下兩種方法。
FIG-Forth 解決方案
FIG-Forth 用參數(shù)域的第一個(gè)單元來保存高級(jí)代碼的地址。 DODOES 子程序通過這個(gè)單元得到了參數(shù)域的地址,并把實(shí)際數(shù)據(jù)的地址(典型地 PFA+2 )推到棧上,取得高級(jí)子程序的地址,然后調(diào)用 EXECUTE 。這種方法存在兩個(gè)問題:
第一、參數(shù)域的結(jié)構(gòu)因機(jī)器代碼行為和高級(jí)代碼行為而不同。例如一個(gè)使用機(jī)器代碼的 CONSTANT 可以把它的代碼保存到 PFA ,但是一個(gè)使用高級(jí)定義的 CONSTANT 行為卻必須把它的數(shù)據(jù)保存在(典型地) PFA+2 。
第二、每個(gè)高級(jí)行為類的實(shí)例都增加了一個(gè)單元的開銷。也就是說,如果 CONSTANT 用于一個(gè)高級(jí)行為,程序中的每個(gè)常數(shù)都要增大一個(gè)單元!幸運(yùn)的是,聰明的 Forth 程序員很快就找到了解決這個(gè)問題的一種方法, FIG-Forth 方法就不再使用了。
現(xiàn)代的解決方案
大多數(shù) Forth 程序員都為每個(gè)高級(jí)行為子程序配置了一個(gè)不同的機(jī)器語言代碼片段。于是,一個(gè)高級(jí)常數(shù)就會(huì)有它自己的代碼域,它指向一個(gè)機(jī)器語言片段,其核心功能就是訪問 CONSTANT 的高級(jí)行為;一個(gè)高級(jí)變量的代碼域?qū)⒅赶蛞粋€(gè)“ STARTUP ”子程序來實(shí)現(xiàn)高級(jí)的 VARIBLE 行為,等等。
這種方法會(huì)導(dǎo)致代碼的大量重復(fù)嗎?不會(huì)的。因?yàn)檫@些機(jī)器語言片斷只是對(duì)通常的啟動(dòng)子程序 DODOES 的一個(gè)調(diào)用(不同于 FIG-Forth 的子程序),對(duì) DODOES 高級(jí)代碼的地址作為一個(gè)“內(nèi)嵌”子程序參數(shù)傳遞。這就意味著,高級(jí)代碼的地址被放到 JSR/CALL 指令之后。 DODOES 可以從 CPU 堆棧中彈出,然后通過一次讀取來得到這個(gè)地址。
實(shí)際上,我們還可以做得更簡(jiǎn)單。高級(jí)代碼自身是放在 JSR/CALL 指令之后的, DODOES 彈出 CPU 堆棧,直接得到這個(gè)地址。因?yàn)槲覀冎肋@是高級(jí) Forth 代碼,我們可以忽略代碼域,而只編譯高級(jí)串線……這就很方便地把 ENTER 的行為集成到了 DODOES 中。
現(xiàn)在每一個(gè)“定義”字都指向了一小部分機(jī)器代碼 – 沒有浪費(fèi)任何的參數(shù)域空間。這一小部分機(jī)器代碼是 JSR 或者 CALL 指令,后隨一個(gè)高級(jí)行為子程序。在 MC6809 的例子中,我們已經(jīng)使每個(gè)常數(shù)的兩個(gè)字節(jié)用一個(gè) 3 字節(jié)的 JSR 替代,它只出現(xiàn)一次。
使用這些策略使得在 Forth 內(nèi)核中包含了許多費(fèi)解的程序邏輯。所以,讓我們使用我們可信賴的 ITC MC6809 例子來看看實(shí)際上這是如何實(shí)現(xiàn)的:
圖 8 顯示了使用高級(jí)定義實(shí)現(xiàn)的 DEUX 常數(shù)。當(dāng) Forth 解釋器遇到 DEUX -- 也就是說當(dāng) Forth 的 IP 寄存器在 IP(1)時(shí) -- 它做通常的事情:它讀取包含在 DEUX 代碼域中的地址,跳轉(zhuǎn)到那個(gè)地址。在那個(gè)地址上是一個(gè) JSR DODOES 指令,于是立即發(fā)生第二個(gè)跳轉(zhuǎn) -- 這次是一個(gè)子程序調(diào)用 。
?
圖 8 ITC DODOES
DODOES 接著必須執(zhí)行下列動(dòng)作:
?? 把 DEUX 的參數(shù)域地址推到參數(shù)棧上,以備將來高級(jí)行為子程序使用。因?yàn)?JSR 指令并不改變?nèi)魏渭拇嫫?#xff0c;我們希望 DEUX 的參數(shù)域地址(或者“鄰近”的地址)仍然保留在 W 寄存器中;
?? 通過彈出 CPU 堆棧得到高級(jí)行為子程序的地址(回憶:彈出 CPU 堆??梢缘玫骄o隨在 JSR 指令之后的不論什么的地址)。這是一個(gè)高級(jí)串線,冒號(hào)定義的參數(shù)域部分;
?? 保存舊的 Forth 指令指針 -- IP(2) -- 到 Forth 返回棧上,因?yàn)?IP 寄存器要被用于執(zhí)行高級(jí)代碼。本質(zhì)上,DODOES 必須“嵌套” IP ,就像 ENTER 一樣。記住 Forth 的返回棧也許不同于 CPU 的子程序堆棧;
?? 把高級(jí)串線的地址放到 IP 中,這是圖 8 中的 IP(3) ;
?? 在新的位置上執(zhí)行 NEXT 以繼續(xù)高級(jí)解釋;
假設(shè)一個(gè)間接串線的 ITC MC6809 符合下列情況:
?? W 沒有被 NEXT 增量(也就是 W 將要包含 NEXT 進(jìn)入字的 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 指向參數(shù)域
PSHU Y ; 把舊的 IP 壓入返回棧
PULS Y ; 從 CPU 堆棧上彈了新的 IP
PSHS X ; 壓入?yún)?shù)域地址 W 到參數(shù)棧上
NEXT ; 訪問高級(jí)解釋器
這些操作并沒有嚴(yán)格按順序進(jìn)行。當(dāng)然,只要恰當(dāng)?shù)臄?shù)據(jù)在恰當(dāng)?shù)臅r(shí)間內(nèi)進(jìn)入了恰當(dāng)?shù)亩褩?#xff08;或者進(jìn)入了恰當(dāng)?shù)募拇嫫?#xff09;,操作的順序并不要緊。在這里,我們實(shí)際上是利用了這樣一個(gè)事實(shí):在新的 IP 從 CPU 堆棧中彈出之前,老的 IP 可以壓入 Forth 的返回棧。
在某些處理器上, CPU 的堆棧被用于 Forth 的返回棧。對(duì)于這種情況,就需要一個(gè)臨時(shí)存儲(chǔ)器訪問步驟。同樣是上面的例子,如果我們必須選擇 S=RSP和 U=PSP 則 DODOES 就成了:
DODOES:
LEAX 2,X ; 讓 W 指向參數(shù)域
PSHU X ; 把參數(shù)域地址 W 壓入?yún)?shù)棧
PULS X ; 從 CPU 堆棧中彈出串線的地址
PSHS Y ; 把舊的 IP 壓入返回棧
TFR X,Y ; 把串線的地址放入 IP
NEXT ; 訪問高級(jí)解釋器
因?yàn)槲覀儽举|(zhì)上是在交換 IP 和返回棧/CPU 堆棧的內(nèi)容,所以我們就必須用 X 作為臨時(shí)寄存器。于是,我們?cè)谥匦率褂?X 寄存器之前就必須把 PFA -- (A)壓入堆棧。
我們就是要這樣一步一步地研究這些 DODOES 例子,追蹤兩個(gè)堆棧和全部寄存器的內(nèi)容。我自己就經(jīng)常研究自己編寫的 DODOES 子程序,以確信任何一個(gè)寄存器都沒有在錯(cuò)誤的時(shí)刻被亂用。
直接串線
DODOES 的邏輯在 DTC 中是一樣的。但是我的實(shí)現(xiàn)卻是不同的,這依賴于 DTC Forth 在一個(gè)字的代碼域中是使用 JMP 還是使用 CALL 。
在代碼域中使用 JMP。如果將要被執(zhí)行的字的地址可以在寄存器中得到,則一個(gè) DTC Forth 就可以在代碼域中使用 JMP,這就很像代碼域地址。從 DODOES 的觀點(diǎn)看,這與 ITC 是一樣的。
在我們的例子中, DODOES 知道 Forth 解釋器跳轉(zhuǎn)到了與 DEUX 相關(guān)的機(jī)器代碼,那個(gè)代碼是JSR 到 DODOES 。現(xiàn)在每個(gè)跳轉(zhuǎn)是使用直接跳轉(zhuǎn)還是使用間接跳轉(zhuǎn)并沒有什么關(guān)系,寄存器和堆棧的內(nèi)容是相同的。所以,DODOES 的代碼與 ITC 是相同的(當(dāng)然,NEXT 是不同的, W 也許要有不同的偏移量指向參數(shù)域)。
在 DTC 的 MC6809 中,我們從來就沒有顯式地讀取將要執(zhí)行字的 CFA ,所以 Forth 字必須在它的代碼域中包含一個(gè) JSR ,這樣我們就可以通過堆棧得到這個(gè)字的參數(shù)域地址,而不是從堆棧中得到。這種情況下的 DEUX 例子顯示在圖 9 中。
圖 9 DTC 的 DODOES
當(dāng) IP 在 IP(1)時(shí), Forth 解釋器跳轉(zhuǎn)到 DEUX 的代碼域(同時(shí)增量 IP)。在代碼域中,是一個(gè)到 DEUX 機(jī)器代碼片斷的 JSR ,在那里是第二個(gè) JSR ,到 DODOES 。于是兩個(gè)地址進(jìn)入了 CPU 堆棧。
第一個(gè) JSR 的返回地址是 DEUX 的參數(shù)域地址,第二個(gè) JSR 的返回地址 -- 在 CPU 堆棧的最上面 -- 是將要執(zhí)行的高級(jí)串線地址。 DODOES 必須確保舊的 IP 已經(jīng)壓入到返回棧, DEUX 的 PFA 壓入了參數(shù)堆棧,高級(jí)串線的地址被裝入到 IP 中。這些對(duì)于堆棧分配是非常敏感的!對(duì)于 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 堆棧是參數(shù)棧,最頂?shù)脑噩F(xiàn)在正是我們需要的字的 PFA
NEXT ; 訪問高級(jí)解釋器
我們可以自己看一下 NEXT、DEUX、DODOES 壓入一項(xiàng)目 -- DEUX 的 PFA-- 到參棧的全過程。
子程序串線
圖 10 顯示了一個(gè) MC6809 STC 的 DEUX 高級(jí)行為的例子。在進(jìn)入 DODOES 的時(shí)候,三個(gè)數(shù)據(jù)被壓入了CPU/RETURN 的返回棧:“主串線”的返回地址、 DEUX 的 PFA、DEUX 的高級(jí)行為代碼的地址。DODOES 必須彈出最后兩個(gè),把 PFA 壓入?yún)?shù)棧,跳轉(zhuǎn)到行為代碼:
?
圖 10 STC 的 DODOES
MC6809 的 DODOES 現(xiàn)在是一個(gè) 3 指令的子程序。它甚至可以通過“把 JSR DODOES 變成內(nèi)嵌方法”來進(jìn)一步簡(jiǎn)化。也就是說用等效的機(jī)器代碼來代替 JSR DODOES 。由于簡(jiǎn)化了一個(gè) JSR ,也就簡(jiǎn)化了堆棧的處理:
PULS X ; 從 CPU 堆棧中彈出 PFA
PSHU X ; 把它壓入?yún)?shù)棧
…… ; DEUX 的其它高級(jí)串線
這里使用了 4 字節(jié)的顯式代碼代替了 3 字節(jié)的 JSR 指令,從而相當(dāng)有效地提高了執(zhí)行的速度。對(duì)于 MC6809 這也許是一個(gè)很好的選擇,對(duì)于像 8051 這樣的處理器, DODEOS 則顯得太長(zhǎng)了,大概應(yīng)該還是作為一個(gè)子程序?yàn)楹谩?
使用 DOES>
我們已經(jīng)學(xué)習(xí)了使用 ;CODE 去創(chuàng)建一個(gè) Forth 字,它的參數(shù)域中可以包含有任意的數(shù)據(jù),以及如何使一個(gè)字的代碼域指向新的機(jī)器代碼片斷。那么我們?nèi)绾尉幾g一個(gè)高級(jí)行為子程序并用一個(gè)新的字指向它呢?
答案依賴于兩個(gè) Forth 字 DOES> 和 (DOES>) ,它們是 ;CODE 和 (;CODE) 的高級(jí)定義等效。為了理解它們,讓我們看一個(gè)使用它們的例子:
: CONSTANT ( n -- )
CREATE / 創(chuàng)建新的字
, / 把 TOS 值加入字典作為參數(shù)域的第 1 個(gè)單元
DOES> / 結(jié)束 " 創(chuàng)建部分 " 開始 " 行為 " 部分
@ / 給出 PFA ,得到它的內(nèi)容
;
把這些與前面的 ;CODE 例子比較,可以看到 DOES> 執(zhí)行的功能與 ;CODE 類似。從 : CONSTANT 到 DOES> 的每個(gè)行為都是在 CONSTANT 字執(zhí)行時(shí)被訪問的。這是構(gòu)建一個(gè)“定義”字的參數(shù)域和代碼。從 DOES> 到 ; 的代碼是 COSNTANT 的“孩子”(比如 DEUX )被訪問時(shí)執(zhí)行的高級(jí)代碼,也就是代碼域?qū)⒁赶虻母呒?jí)代碼片斷。(我們會(huì)看到 JSR DODOES 包含在這個(gè)高級(jí)代碼片斷之前)。
與 ;CODE 一樣,“CREATE”和“ACTION”子句都在 Forth 字 CONSTANT 體中,如圖 11 所示。
圖 11 ITC 的 DODOES
回憶時(shí)間序列 1 、 2 、 3 ,字 DOES> 和 (DOES>) 做下例事情:
?? 它把 Forth 字 (DOES>) 編譯到 CONSTANT 中;
?? 它把一個(gè) JSR DODOES 編譯到 CONSTANT 中;
注意 DOES> 保持 Forth 編譯器一直運(yùn)行,這樣可以保證后面的高級(jí)代碼片斷繼續(xù)得到編譯。同樣,盡管 JSR DODOES 本身不是 Forth 代碼,但是像 DOES> 這樣的立即字可以使它編譯到 Forth 代碼中。
(DOES>) 是字 CONSTANT 的一部分,所以在 CONSTANT 被執(zhí)行的時(shí)候(時(shí)間序列 2 )執(zhí)行,它做下例事情:
?? 它通過從 Forth 的返回棧中彈出 IP 得到緊隨其后的機(jī)器碼的地址( JSR DODOES );
?? 它把這個(gè)地址放到被 CREATE 剛剛定義的字的代碼域中。
?? 它執(zhí)行 EXIT 行為,使得 CONSTANT 在這里中斷而不再執(zhí)行后面的代碼片斷。
(DOES>) 的行為和 (;CODE) 是一樣的!所以 Forth 系統(tǒng)并不需要另外定義一個(gè)新的字。例如 F83 系統(tǒng)在 ;CODE 和 DOES> 中同時(shí)使用 (;CODE) 。我也從現(xiàn)在開始使用 (;CODE) 代替 (DOES>).
你已經(jīng)看到了 (;CODE) 是如何工作的。 F83 是這樣定義 DOES> 的
: DOES>
COMPILE (;CODE) / 編譯 (;CODE) 到定義中
0E8 C, / CALL 指令的操作碼字節(jié)
DODOES HERE 2+ - , / 把相對(duì)轉(zhuǎn)移寫入 DODOES
; IMMEDIATE
這里 DODOES 是一個(gè)常數(shù),它保存有 DODOES 子程序的地址(實(shí)際使用的 F83 源代碼和這里所說的有一點(diǎn)點(diǎn)兒不同,因?yàn)?F83 使用的 META 編譯器有不同的要求)。
DOES> 不需要改變 CSP 或者 SMUDGE 位,因?yàn)?Forth 編譯器的狀態(tài)是 'on.' 。在 Intel 8086 的情況下, CALL 指令使用相對(duì)地址,因此,需要對(duì) DODOES 和 HERE 做一個(gè)算術(shù)運(yùn)算。在 MC6809 中, DOES> 看起來像這樣的:
: DOES>
COMPILE (;CODE) / 把 (;CODE) 編譯進(jìn)定義
0BD C, / JSR 擴(kuò)展操作碼
DODOES , / 操作數(shù): DODOES 的地址
; IMMEDIATE
你可以看到一個(gè)機(jī)器語言 JSR DODOES 是如何被編譯到高級(jí) (;CODE) 之后和高級(jí)行為之前的。
直接和間接串線
DTC 和 STC 中的唯一區(qū)別是代碼域必須修改以指向新的子程序。這是由 (;CODE) 完成的,所要求的改變已經(jīng)描述過了。 DOES> 沒有任何影響,除非你在 STC 中把 JSR DODOES 擴(kuò)展成為顯式的機(jī)器代碼。在這種情況下, DOES> 被修改成匯編“內(nèi)嵌”的機(jī)器代碼而不是 JSR DODOES 子程序。
思前想后
我們可能從來就沒有想到,這么幾行代碼會(huì)引出這么多的內(nèi)容。這也是為什么我特別贊賞 ;CODE 和 DOES> ,說實(shí)在的,我從來也沒有見到過用這么經(jīng)濟(jì)的方法就實(shí)現(xiàn)了這么復(fù)雜、強(qiáng)大和靈活的結(jié)構(gòu)。
參考文獻(xiàn)
[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 編譯器
撰寫本文的過程貫穿著一個(gè)指導(dǎo)思想:“保持最短”。本著這個(gè)原則,我把源程序列表安排到另外的地方,對(duì)此我表示歉意。現(xiàn)在,我們主要試圖討論以下話題:
你如何開始構(gòu)造一個(gè) Forth 系統(tǒng)?
你現(xiàn)在已經(jīng)知道了,主要的 Forth 程序代碼是高級(jí)串線,它們通常被編譯成一系列地址。在 FIG-Forth 時(shí)代和早期的 Forth 實(shí)踐中,匯編語言是唯一可用的程序設(shè)計(jì)語言工具。匯編語言對(duì)于編寫 Forth 的 CODE 字是非常好的,但是高級(jí)串線必須用一系列的 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
后來,由于可以實(shí)際工作的 Forth 系統(tǒng)越來越普及, Forth 編寫者開始把 Forth 編譯器修改成為交叉編譯器,通過運(yùn)行在 CP/M (或者蘋果 II ,或者其它任何什么微機(jī)系統(tǒng))的 Forth 系統(tǒng),你就可以為其它 CPU 編寫 Forth 程序、更改 Forth 系統(tǒng)或者為那個(gè) CPU 編寫一個(gè)全新的 Forth 系統(tǒng)。
由于是從 Forth 內(nèi)部創(chuàng)建一個(gè)全新的 Forth 系統(tǒng),這種編譯器被稱為“META 編譯器”。計(jì)算機(jī)科學(xué)的學(xué)究們反對(duì)這樣的稱謂,所以有些 Forth 編寫者仍然使用“交叉編譯”和“重新編譯”的術(shù)語,這兩個(gè)術(shù)語之間的差異是:“重新編譯”只能為相同的 CPU 產(chǎn)生新的 Forth 系統(tǒng)。
現(xiàn)在大多數(shù) PC 機(jī)上的 Forth 都是通過 META 編譯產(chǎn)生的,但是,在嵌入式系統(tǒng)領(lǐng)域卻產(chǎn)生了意見分歧。
使用匯編器編寫 Forth 系統(tǒng)的觀點(diǎn)認(rèn)為:
?? META 編譯器神秘難懂,你必須完全理解一個(gè) META 編譯器然后才能使用它;
?? 一般的程序員都懂得匯編器;
?? 匯編器對(duì)于一個(gè)新的 CPU 總是可用的;
?? 匯編器處理許多優(yōu)化(比如長(zhǎng)短調(diào)用格式);
?? 匯編器處理前向引用和特殊的尋址模式,而許多 META 編譯器通常不能做到;
?? 匯編程序員可以使用熟悉的編輯器和調(diào)試工具;
?? 代碼的產(chǎn)生是完全可見的,沒有向程序員“隱藏”任何東西;
?? 改變 Forth 模型非常容易,而許多設(shè)計(jì)的考慮卻影響 META 編譯器的內(nèi)部;
使用 META 編譯器的觀點(diǎn)認(rèn)為:
?? 你是在編寫“正?!钡?Forth 代碼,它當(dāng)然易于閱讀和調(diào)試;
?? 一但你理解了你的 META 編譯器,你就可以很容易地把它移植到新的 CPU 上 ;
?? 你需要的唯一工具就是你的計(jì)算機(jī)上的 Forth 系統(tǒng);這一點(diǎn)對(duì)于沒有 PC 的人特別實(shí)用,因?yàn)楝F(xiàn)在的許多交叉匯編器要求 PC 機(jī)或者工作站。
我用各種方式編寫過幾個(gè) Forth 系統(tǒng),所以要作出選擇是很痛苦的。我傾向于使用 META 編譯器:我發(fā)現(xiàn) Forth 的 MAX 代碼比等效的匯編代碼易讀、易懂。反對(duì)使用 META 編譯器的觀點(diǎn)許多已經(jīng)被現(xiàn)代的“專業(yè)”編譯器所克服,如果你使用 Forth 工作,我強(qiáng)烈建議你考慮一個(gè)商業(yè)化產(chǎn)品。唉,公共的 META 編譯器(包括我自己的)依然落后于時(shí)代,笨拙同時(shí)神秘。
所以我準(zhǔn)備為 Forth 程序員提供基本的材料,告訴你作出自己的選擇。我將給出 META 形式的MC6809 代碼,為 F83 ( IBM PC CP/M ST )提供 META 編譯器。 Z80 代碼將使用 CP/M 匯編器寫成。 8051 代碼使用公共的 PC 交叉匯編器編寫。
使用 C 語言編寫 Forth 系統(tǒng)?
如果不討論“用C語言編寫 Forth 系統(tǒng)”這個(gè)新的方法,本文就將是不完全的。 C 語言比匯編器具有更好的可移植性 -- 理論上,你所要作的全部工作就是為任何 CPU 重新編譯相同的代碼。
這種方法的缺點(diǎn)是:
?? 在設(shè)計(jì)決策選擇上更缺少靈活性;比如,不可能實(shí)現(xiàn)直接串線編碼,也不可能優(yōu)化寄存器的分配;
?? 增加原語之后,你必須重新編譯 C 源代碼;
?? 某些 C 語言實(shí)現(xiàn)的 Forth 使用了效率很低的串線技術(shù),比如多個(gè) CASE 語句;
?? 大多數(shù) C 編譯器產(chǎn)生的代碼比匯編語言程序員的代碼低效;
但是對(duì)于那些 UNIX 系統(tǒng)和不支持匯編語言編程的 RISC 工作站來說,這卻是 Forth 得以運(yùn)行的唯一方法。最完全和廣泛使用的公共域 C 語言 Forth 系統(tǒng)是 TILE 。如果你沒有運(yùn)行 UNIX 系統(tǒng),你可以看一下文件 HENCE4TH_1.2.A 。
為了繼續(xù)以前的比較,還是先來看看 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;
}
請(qǐng)注意:用 C 編寫 Forth 字有非常不同的技術(shù),所以這些字在 CForth 和 TILE 中可能差異非常大。
在 MC68000 或者 SPARC 工作站上,這樣的編碼可以產(chǎn)生非常好的代碼。不過,一但你計(jì)劃用 C 來實(shí)現(xiàn) Forth ,你也需要理解用匯編語言實(shí)現(xiàn) Forth 是怎樣工作的。所以請(qǐng)繼續(xù)閱讀本文。
參考文獻(xiàn)
[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 原語
我提交的代碼
最后,我準(zhǔn)備展示一個(gè)(我希望是) ANSI 兼容的編譯器 : CAMEL Forth ,包括它的全部源碼。作為一個(gè)很好的練習(xí) -- 也為了版權(quán)的原因 -- 我重新編寫了全部的代碼(你知道不看優(yōu)秀的代碼實(shí)例有多困難嗎?!)。當(dāng)然,我在不同的 Forth 系統(tǒng)上的經(jīng)驗(yàn)無疑影響著所選擇的設(shè)計(jì)策略。
由于空間所限,源代碼分成四個(gè)部分安裝(如果你已經(jīng)等不及了,可以去 GENIE 下載全部的文件)
?? Z80 Forth “原語”: 用匯編語言編寫
?? 8051 Forth “原語”: 也用匯編語言編寫
?? Z80/8051 高級(jí)內(nèi)核 同樣
?? 完全的 6809 內(nèi)核: 使用 META 編譯器的源文件
我計(jì)劃盡量使用公共軟件來實(shí)現(xiàn) CAMEL Forth :對(duì)于 Z80 ,使用 CP/M 下的 Z80R 匯編工具;對(duì)于 8051 ,使用 IBM PC 上的 A51 交叉匯編器,對(duì)于MC6809 ,使用我自己的 F83 For CP/M IBM PC ATARI ST 工具。
我這里的“核心”是指組成一個(gè)基本 Forth 系統(tǒng)的一系列字,包括編譯和解釋字。對(duì)于 CAMEL Forth 來說,這些就是 ANS Forth 規(guī)定的核心字再加上為了實(shí)現(xiàn)這些核心字所需要的非 ANSI 字。 Forth 核心通常由兩部分組成:一部分是用機(jī)器代碼寫成的(即 CODE 字),另一部分是高級(jí)定義字,用機(jī)器代碼寫成的字稱為“原語”,因?yàn)樵谧詈蟮姆治鲋?#xff0c;全部的 Forth 系統(tǒng)就是由這些字組成的。
嚴(yán)格地說,哪些字應(yīng)該用機(jī)器代碼來寫呢?選擇這些原語是一個(gè)有趣的任務(wù)。一個(gè)小原語集合可以簡(jiǎn)化移植,但性能肯定很糟。我聽說過只用 13 個(gè)原語就能夠定義 Forth 的情況 - 當(dāng)然這是一個(gè)很慢的 Forth 系統(tǒng)。 eForth 是一個(gè)以可移植性作為設(shè)計(jì)目標(biāo)的 Forth 系統(tǒng),它有 31 個(gè)原語。
而我的原則是這樣的:
?? 基本的算術(shù)、邏輯運(yùn)算以及存儲(chǔ)器操作用 CODE 實(shí)現(xiàn) ;
?? 如果一個(gè) Forth 字不能簡(jiǎn)單有效地用一系列的 Forth 字編寫,則它應(yīng)該用 CODE 實(shí)現(xiàn)(如 U<, RSHIFT 等) ;
?? 如果一個(gè)簡(jiǎn)單的字頻繁使用,則用 CODE 實(shí)現(xiàn)是值得的(如 NIP , TUCK );
?? 如果一個(gè)字用 CODE 編寫時(shí)并不需要多少字節(jié),則用 CODE 實(shí)現(xiàn);
?? 如果一個(gè)處理器包含有實(shí)現(xiàn)一個(gè)字所需要的功能,則用 CODE 編寫。比如,在 Z80 或者 8086 上,有 CMOVE 或者 SCAN 指令;
?? 如果一個(gè)字主要是搗弄堆棧上的參數(shù),但是邏輯非常簡(jiǎn)單,應(yīng)該用 CODE 實(shí)現(xiàn),這里參數(shù)可以放到寄存器中;
?? 如果一個(gè)字的控制和邏輯功能復(fù)雜,則它最好用高級(jí)定義實(shí)現(xiàn);
對(duì)于 Z80 的 CAMELForth ,我使用了大約 70 個(gè)原語(見表 1 )。
| 序號(hào) | 名稱 | 進(jìn)入時(shí) -- 時(shí)堆棧 | 描述 | |
核心字:這些是 ANS Forth 文檔要求的核心定義 | ||||
1 | ! | x a-addr -- | 把一個(gè)單元數(shù)存入存儲(chǔ)器 | |
2 | + | 1/u1 n2/u2 -- n3/u3 | 加法 n1+n2 | |
3 | +! | n/u a-addr -- | 加一個(gè)單元到存儲(chǔ)器 | |
4 | - | n1/u1 n2/u2 -- n3/u3 | 減法 n1-n2 | |
5 | < | n1 n2 – flag | 測(cè)試 n1<n2, 有符號(hào)數(shù) | |
6 | = | x1 x2 – flag | 測(cè)試 x1=x2 | |
7 | > | n1 n2 – flag | 測(cè)試 n1>n2, 有符號(hào)數(shù) | |
8 | >R | x -- R: -- x | 壓入返回棧 | |
9 | ?DUP | x -- 0 | x x | 如果棧頂元素非0則復(fù)制 | |
10 | @ | a-addr – x | 從存儲(chǔ)器中讀取一個(gè)單元 | |
11 | 0< | n – flag | 如果 TOS 為負(fù)則為真 | |
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 | 算術(shù)左移 | |
16 | 2/ | x1 -- x2 | 算術(shù)右移 | |
17 | AND | x1 x2 -- x3 | 邏輯 AND | |
18 | CONSTANT | n -- | 定義一個(gè) Forth 常數(shù) | |
19 | C! | c c-addr -- | 把字符存入存儲(chǔ)器 | |
20 | C@ | c-addr – c | 從存儲(chǔ)器讀取字符 | |
21 | DROP | x -- | 去除棧頂元素 | |
22 | DUP | x – x x | 復(fù)制棧頂元素 | |
23 | EMIT | c -- | 向控制臺(tái)輸出字符 | |
24 | EXECUTE | Forth word 'xt' | 執(zhí)行棧頂?shù)?Forth 字 | |
25 | EXIT | -- | 退出一個(gè)冒號(hào)字義 | |
26 | FILL | c-addr u c -- | 用字符填充存儲(chǔ)器 | |
27 | I | -- n R: y1 y2 -- y1 y2 | 得到內(nèi)層的循環(huán)計(jì)數(shù) | |
28 | INVERT | x1-- x2 | 位反轉(zhuǎn) | |
29 | J | -- n R: 4*y -- 4*y | 得到第二個(gè)循環(huán)計(jì)數(shù) | |
30 | KEY | -- c | 從鍵盤輸入一個(gè)字符 | |
31 | LSHIFT | x1 u -- x2 | 邏輯左移 u 位 | |
32 | NEGATE | x1 -- x2 | 2 的補(bǔ)碼 | |
33 | OR | x1 x2 -- x3 | 邏輯 OR | |
34 | OVER | x1 x2 -- x1 x2 x1 | 復(fù)制次棧項(xiàng) | |
35 | ROT | x1 x2 x3 --x2 x3 x1 | 棧頂三元素旋轉(zhuǎn) | |
36 | RSHIFT | x1 u -- x2 | 邏輯右移 u 位 | |
37 | R> | -- x R: x -- | 從返回棧頂彈出 | |
38 | R@ | -- x R: x – x | 讀取返回棧 | |
39 | SWAP | x1 x2 -- x2 x1 | 交換棧頂?shù)膬蓚€(gè)項(xiàng)目 | |
40 | UM* | u1 u2 – ud | 無符號(hào) 16x16->32 乘法 | |
41 | UM/MOD | ud u1 -- u2 u3 | 無符號(hào) 32/16->16 除法 | |
42 | UNLOOP | -- R: sys1 sys2 -- | 退出循環(huán)參數(shù) | |
43 | U< | u1 u2 – flag | 測(cè)試 u1<n2, 無符號(hào) | |
44 | VARIABLE | -- | 定義一個(gè) Forth 變量 | |
45 | XOR | x1 x2 -- x3 | 邏輯異或 | |
擴(kuò)展字:這些可選擇的字也是 ANS Forth 文檔定義的 | ||||
46 | <> | x1 x2 – flag | 測(cè)試不相等 | |
47 | BYE | i*x -- | 返回到 CP/M 操作系統(tǒng) | |
48 | CMOVE | c-addr1 c-addr2 u -- | 從底移動(dòng)字節(jié) | |
49 | CMOVE> | c-addr1 c-addr2 u -- | 從頂移動(dòng)字節(jié) | |
50 | KEY? | -- flag | 如果在鍵盤上按了鍵則返回真 | |
51 | M+ | d1 n -- d2 | 加無符號(hào)數(shù)到雙精度數(shù) | |
52 | NIP | x1 x2 -- x2 | 去除次棧頂 | |
53 | TUCK | x1 x2 -- x2 x1 x2 | 見堆棧圖示 | |
54 | U> | u1 u2 – flag | 測(cè)試 u1>u2, 無符號(hào) | |
個(gè)人擴(kuò)展:這些字只屬于 CamelForth 實(shí)現(xiàn) | ||||
55 | (do) | n1|u1 n2|u2 -- R:-- y1 y2 | DO 的運(yùn)行時(shí)間代碼 | |
56 | (loop) | R: y1 y2 -- | y1 y2 | LOOP 的運(yùn)行時(shí)間代碼 | |
57 | (+loop) | n -- R: y1 y2 -- | y1 y2 | +LOOP 的運(yùn)行時(shí)間代碼 | |
58 | >< | x1 -- x2 | 交換字節(jié) | |
59 | ?branch | x -- | 如果 TOS 為 0 則跳轉(zhuǎn) | |
60 | BDOS | DE C – A | 調(diào)用 CP/M BDOS 功能 | |
61 | branch | -- | 無條件分支 | |
62 | Lit | -- x | 內(nèi)嵌文字常數(shù)放到堆棧上 | |
63 | PC! | c p-addr -- | 把字符輸出到口上 | |
64 | PC@ | p-addr – c | 從口輸入字符 | |
65 | RP! | a-addr -- | 設(shè)置返回棧指針 | |
66 | RP@ | -- a-addr | 得到返回棧指針 | |
67 | SCAN | ca1 u1 c -- ca2 u2 | 尋找匹配的字符 | |
68 | SKIP | ca1 u1 c -- ca2 u2 | 跳過匹配的字符 | |
69 | SP! | A-addr -- | 設(shè)置數(shù)據(jù)棧指針 | |
70 | SP@ | -- a-addr | 得到數(shù)據(jù)棧指針 | |
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 = 有符號(hào)16位
u = 無符號(hào)16位
d = 無符號(hào)32位
ud = 無符號(hào)32位
+n = 無符號(hào)15位
x = 任何的單元值
i*x j*x = 任何的單元值
a-addr = 對(duì)齊的地址
ca = 字符地址
p-addr = I/O 口地址
y = 系統(tǒng)指定
在確定了 Forth 模型和它所使用的目標(biāo) CPU 之后,我依照下列過程進(jìn)行開發(fā):
?? 選擇一個(gè) ANSI 核心字子集作為原語;
?? 按照 ANSI 的描述,編寫這些字的匯編定義,加入處理器的初始化代碼;
?? 運(yùn)行匯編器,定位源程序的錯(cuò)誤;
?? 測(cè)試產(chǎn)生的匯編代碼。我通常的做法是加上幾行匯編代碼,使得程序能夠在初始化完成之后輸出一個(gè)字符,這是一個(gè)非常關(guān)鍵的測(cè)試,它保證了你的硬件、匯編器、下載器( EPROM 編程器或者其它什么東西)、串行通訊口統(tǒng)統(tǒng)工作正常! (只對(duì)嵌入式系統(tǒng))加入另外的匯編代碼段以讀取串口并回送,這樣就可以測(cè)試雙向通訊了;
?? 寫一個(gè)高級(jí) Forth 代碼片斷以輸出一個(gè)字符,這個(gè)代碼段只使用 Forth 原語(通常是這樣的: LIT 33h EMIT BYE ),這就可以測(cè)試 Forth 寄存器的初始化、堆棧和串線機(jī)制。這個(gè)階段的問題可以通過追蹤 NEXT 、初始化、數(shù)據(jù)堆棧的邏輯錯(cuò)誤來定位,比如把一個(gè)堆棧設(shè)置到了 ROM 中;
?? 寫一個(gè)冒號(hào)定義輸出一個(gè)字符,把這個(gè)定義包含在上面的高級(jí)定義片斷中,比如定義 : BLIP LIT 34 EMIT EXIT ; 然后測(cè)試代碼片段 LIT 33h EMIT BLIP BYE 。這個(gè)階段的問題通常與 DOCOLON 、 EXIT 、返回棧有關(guān)。
?? 現(xiàn)在可以編寫一些工具來輔助開發(fā),比如顯示堆棧上的 16 進(jìn)制數(shù)等等。列表 1 是一個(gè)簡(jiǎn)單的測(cè)試子程序,它運(yùn)行一個(gè)永不停止的存儲(chǔ)器 DUMP 動(dòng)作(這個(gè)代碼片斷在用戶的輸入鍵盤不能工作時(shí)也可以使用)。這個(gè)程序測(cè)試原語 DUP 、 EMIT 、 EXIT 、 C@ 、 >< 、 LIT 、 1+ 和 BRANCH ,也測(cè)試了幾級(jí)嵌套。但它不使用 DO …… LOOP ,因?yàn)檫@個(gè)結(jié)構(gòu)要正確工作通常比較難。當(dāng)這些代碼可以運(yùn)行后,你就會(huì)對(duì)自己的 Forth 模塊是否有效樹立一些信心。
接著測(cè)試其它的原語,其中 DO …… LOOP, UM/MOD, UM* 和 DODOES 必須嚴(yán)格,最后再加入高級(jí)定義。
閱讀源代碼!
如果你希望學(xué)習(xí)更多的 Forth 內(nèi)核工作原理和它的編寫方法,學(xué)習(xí)列表 2 。這個(gè)列表遵循了下面的一些 Forth 文檔表示格式:
WORD-NAME stack in -- stack out description
其中 WORD-NAME 是 Forth 可以識(shí)別的字,由于這些字經(jīng)常包含一些特殊的ASCII 字符,所以必須用一個(gè)近似的名字做為這個(gè)字的匯編語言標(biāo)號(hào),比如 OENPLUS 是字 1+ 的匯編語言標(biāo)號(hào)。
stack in 是這個(gè)字希望輸入的棧上參數(shù),最右邊總是棧頂元素, stack out 是這個(gè)字留在棧上的參數(shù)。
如果一個(gè)字影響返回棧,則會(huì)給出一個(gè)返回棧說明,用 R: 表示,比如:
stack in -- stack out R: stack in -- stack out
ANSI Forth 對(duì)數(shù)值堆棧參數(shù)給出了一個(gè)簡(jiǎn)化的表示,通常 n 是長(zhǎng)度為一個(gè)單元 CELL 的有符號(hào)數(shù), u 是長(zhǎng)度為一個(gè)單元的無符號(hào)數(shù), c 是一個(gè)字符,等等,見表 1 。
參考文獻(xiàn)
[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 的源代碼在下列站點(diǎn)上可用 ftp://ftp.zetetics.com/pub/forth/camel/cam80-12.zip .
第六部分 Z80 高級(jí)內(nèi)核
更正
在 TCJ#67 上發(fā)表的 CAMEL80.AZM 文件有兩個(gè)錯(cuò)誤。一個(gè)主要的錯(cuò)誤是 Forth 字 > 的宏定義名字頭長(zhǎng)度誤為 2 ,實(shí)際上應(yīng)該是 1 。另一個(gè)次要的錯(cuò)誤是 CP/M 的控制臺(tái) I/O 。 KEY 必須返回所打的字符,所以使用了 BDOS 功能 6 。 KEY?不能返回字符,使用 BDOS 功能 11 以測(cè)試當(dāng)前是否有鍵按下。不幸的是, BDOS 功能6不清除功能 11 檢測(cè)是按下的鍵。我現(xiàn)在重新編寫了 KEY? 以使用 BDOS 功能 6 。因?yàn)檫@是一個(gè)“破壞性”的測(cè)試,我就必須保持已經(jīng)“消耗”的鍵,并在下一次的 KEY 調(diào)用中返回。這個(gè)新的邏輯可以用于任何硬件只提供“破壞性”測(cè)試的場(chǎng)合。
高級(jí)定義
在上一次討論中,我沒有展開源代碼。每一個(gè)“原語”執(zhí)行一個(gè)小的、明確定義的功能。它幾乎全部是 Z80 匯編代碼,就算是我沒有說清楚為什么原語中包含了一個(gè)特別的字,我也希望讀者明白每一個(gè)字是做什么的。在這一部分里,我可就不能這樣“奢華”了:我將要給出 Forth 語言的邏輯。許多書中描述了 Forth 內(nèi)核,如果你希望完全掌握它,就請(qǐng)去買上一本。對(duì)于 TCJ 我將限制自己只給出編譯器和解釋器的關(guān)鍵字和給出清單 2.
文本解釋操作
文本解釋器或者稱為“外層解釋器”是一些從鍵盤上接收輸入并執(zhí)行所要求 Forth 操作的 Forth 代碼(這與地址或者“內(nèi)層解釋”器 NEXT 不同,后者執(zhí)行編譯之后的串線代碼)。理解這些代碼的最好方式是看 Forth 系統(tǒng)的啟動(dòng)。
CP/M 入口點(diǎn)(參看上一部分)檢測(cè)可用內(nèi)存的頂部,設(shè)置堆棧指針( PSP 、 RSP )和用戶指針( UP ),建立如圖 1 所示的存儲(chǔ)器映象,然后設(shè)置“內(nèi)層”解釋器指針( IP )以執(zhí)行 Forth 字 COLD 。
圖 1 Z80 CP/M CAMELForth 儲(chǔ)器映象
?
COLD 通過啟動(dòng)表初始化用戶變量,然后執(zhí)行字 ABORT 。(COLD 也試圖從 CP/M 命令行執(zhí)行 Forth 命令)。
ABORT 復(fù)位參數(shù)棧指針并執(zhí)行 QUIT 。
QUIT 復(fù)位返回棧指針、 LOOP 棧指針、解釋狀態(tài)、然后開始執(zhí)行命令(之所以要把名字這樣進(jìn)行區(qū)別是因?yàn)?QUIT 可以用于退出應(yīng)用程序,并返回到 Forth 的頂層。不像 ABORT, QUIT 保留了參數(shù)棧的內(nèi)容)。
QUIT 是一個(gè)無限循環(huán),它從鍵盤 ACCEPT 一行輸入,然后作為 Forth 命令調(diào)用 INTERPRET 。當(dāng)沒有編譯錯(cuò)誤時(shí), QUIT 在每一行之后打印一個(gè) Ok.
INTERPRET 幾乎是 ANS Forth 文檔 3.4 部分所給算法的逐字翻譯。它分析一個(gè)由空格分開的輸入串,試著用 FIND 把字串對(duì)應(yīng)一個(gè)已經(jīng)定義的 Forth 字。如果找到了一個(gè)字,則字或者被執(zhí)行(如果這是一個(gè) IMMEDIATE 立即字,或者是處于 STATE = 0 的“解釋狀態(tài)”)或者編譯到字典(如果在編譯狀態(tài), STATE<>0 )。如果沒有找到, INTERPRET 就試著把字串編譯成數(shù)字。如果成功, LITERAL 或者把它放到參數(shù)棧(如果在“解釋狀態(tài)”)或者編譯成一個(gè)在線文字量(如果在編譯狀態(tài))。如果這不是一個(gè) Forth 字也不是一個(gè)合法的數(shù),就顯示一個(gè)錯(cuò)誤信息,解釋器執(zhí)行 ABORT ,這個(gè)過程將一個(gè)字串一個(gè)字串地重復(fù),直到輸入行的結(jié)尾。
Forth 字典
那么,解釋器如何通過名字“找到”一個(gè) Forth 字呢?答案是: Forth 維護(hù)一個(gè)含有所有 Forth 名字的字典。每個(gè)名字都通過某種方式與它的可執(zhí)行代碼相關(guān)聯(lián)。
有許多辦法可以保存用于查找的名字串:一個(gè)簡(jiǎn)單的數(shù)組,一個(gè)鏈表,多重鏈表, HASH 表等。幾乎所有方法的都可以使用 -- Forth 的全部要求只是:如果你所查找的項(xiàng)目重名,那么最后定義的名字需要最先被找到。
我們也可以擁有幾個(gè)名字的集合(在新的 ANSI Forth 中,把這種集合稱為“詞匯表”)。這就允許你在不丟失一個(gè)名字原來意義的情況下再用這個(gè)名字。例如,你可以有一個(gè)整數(shù)的 + ,一個(gè)浮點(diǎn)的 + ,甚至是一個(gè)字符串的 + 。這在面向?qū)ο蟮南到y(tǒng)中被稱為“運(yùn)算符重載”。
每個(gè)字符串可以與它的可執(zhí)行代碼通過鄰近的物理存儲(chǔ)器相聯(lián)系 -- 比如,名字在可執(zhí)行代碼之前,這通常被稱為 Forth 字的首部。字符串也可以集中存放在不同的存儲(chǔ)器區(qū)域中,與可執(zhí)行代碼通過指針連接(這種情況稱為“分離的首部”)。
你甚至可以有無名的 Forth 代碼片斷,只要你永遠(yuǎn)不需要找到它們或者解釋它們。 ANSI 只要求 ANS Forth 字是可以找到的。
關(guān)于字典的設(shè)計(jì)策略可以寫成另一篇論文。 CAMEL Forth 使用的是最簡(jiǎn)單的策略:一個(gè)簡(jiǎn)單鏈表,定位在可執(zhí)行代碼之前。沒有字匯表,也許我可以在以后的 TCJ 論文中加入這一能力。
字的首部結(jié)構(gòu)
這里還有一個(gè)問題需要討論:在首部中需要什么數(shù)據(jù)?如何存儲(chǔ)它們?
最少的數(shù)據(jù)是名字、優(yōu)先位、(顯式的或者隱式的)到可執(zhí)行代碼的指針。為了簡(jiǎn)單起見, CAMEL Forth 把名字作為一個(gè)“計(jì)數(shù)字符串”存儲(chǔ)(一個(gè)字節(jié)的長(zhǎng)度,后面是 N 個(gè)字符)。早期的 Forth Inc. 產(chǎn)品只存儲(chǔ)名字串的長(zhǎng)度和前 3 外字符。 FIG-Forth 使用不同的緊縮方法,用最后一個(gè)字符的 MSB 位置 1 來標(biāo)識(shí)名字的最后一個(gè)字符,而不需要長(zhǎng)度字節(jié),其它的 Forth 系統(tǒng)也使用了緊縮字符串,我想甚至 C 風(fēng)格的 NULL 字符串也是可以使用的。
“優(yōu)先位”是一個(gè)標(biāo)志,用于指示這個(gè)字是一個(gè)立即字( IMMEDIATE ),這種字在編譯時(shí)也被執(zhí)行,以實(shí)現(xiàn) Forth 的編譯指示和控制結(jié)構(gòu)。也有其它方法實(shí)現(xiàn)編譯指示,例如,可以把它們放在一個(gè)單獨(dú)的字典中,等等。許多 Forth 系統(tǒng)直接把這個(gè)位保存在長(zhǎng)度字節(jié)中。我使用了一個(gè)分離的字節(jié),這樣可以把一個(gè)“通?!钡拇僮鞣糜谧执拿植僮?#xff08;比如在 FIND 中的 S= 和 WORDS 中的 TYPE )。
如果把名字保存在一個(gè)鏈表中,就需要有一個(gè)鏈。通常最后的字是在鏈表的前面,而鏈指向前一個(gè)字。這符合 ANSI (和大多數(shù)系統(tǒng))對(duì)重定義字的要求。 Charles Curley 研究了 LINK 域的放置位置,發(fā)現(xiàn)如果把這個(gè)域放置在名字之前(而不是像 FIG-Forth 那樣在名字之后),由可以加快編譯的速度。
圖 2 是 CAMELForth 字首部結(jié)構(gòu),并與 FIG-Forth F83 Pygmy Forth 系統(tǒng)的首部做了比較。 F83 和 Pyhmy 的 "VIEW" 字段可以作為一個(gè)例子,它說明了如何把其它有用的信息保存在 Forth 首部中。
注意:把一個(gè)“頭”(首部)和“體”(可執(zhí)行代碼部分)區(qū)別開來是非常重要的。它們并不需要存儲(chǔ)在一起。首部只是在編譯和解釋時(shí)才需要,一個(gè)“純的可執(zhí)行”Forth 系統(tǒng)并不需要全部的首部。但是,對(duì)于一個(gè)合法的 ANSI Forth 系統(tǒng)來說,首部必須存在 -- 至少是 ANSI Forth 字集中的那些字必須有首部。
當(dāng)從匯編代碼“編譯”一個(gè) Forth 系統(tǒng)時(shí),你可以定義宏來構(gòu)建這個(gè)首部(參看 CAMELZ80.AZM 的 HEAD IMMED )。在 Forth 環(huán)境中,首部和代碼域都是由 CREATE 構(gòu)建的。
編譯操作
我們已經(jīng)具備了理解 Forth 編譯器的足夠知識(shí)。字 :開始一個(gè)新的高級(jí)字定義,首先為字創(chuàng)建一個(gè)頭(CREATE),改變它的代碼域到“DOCOLON”(!COLON),然后轉(zhuǎn)為編譯狀態(tài)。
回想一下,在編譯狀態(tài)下,文本解釋器遇到的每一個(gè)字都編譯進(jìn)字典而不是立即執(zhí)行。這個(gè)過程一直繼續(xù),直到文本解釋器遇到了字“;”。這個(gè)字是一個(gè)立即字,它被立即執(zhí)行,編譯一個(gè) EXIT 到定義的結(jié)尾,然后切換到解釋狀態(tài)([) 。
同時(shí),“:”隱藏這個(gè)新字,而“;”把這個(gè)新字顯示出來(通過清除首部或者名字中的 smudge 位),這就允許 Forth 字可以按“自我優(yōu)先級(jí)”的方式重新定義。為了強(qiáng)制使這個(gè)被定義的字遞歸調(diào)用,需要使用字 RECURSE 。
我們可以看到, Forth“編譯器”與 C 或者 PASCAL 編譯器并沒有區(qū)別。 Forth 編譯器包含有不同F(xiàn)orth 字的動(dòng)作,這就使得改變或者擴(kuò)充編譯器變得很容易,但如果沒有一個(gè)“內(nèi)建”的編譯器,則創(chuàng)建一個(gè) Forth 應(yīng)用就會(huì)特別困難。
相關(guān)的字集
還有許多其它的 Forth 字,它們是:
實(shí)現(xiàn)編譯器或者解釋器的需要,或者
提供編程的方便性
但是有一個(gè)字集應(yīng)該引起特別的重視,那就是我放入文件 CAMEL80D.AZM 的那些字。
ANSI Forth 標(biāo)準(zhǔn)的一個(gè)目標(biāo)就是向應(yīng)用程序員隱藏 CPU 和相關(guān)的實(shí)現(xiàn)模型(直接或者間接串線、 16 位還是 32 位)。為了實(shí)現(xiàn)這個(gè)目的,需要向標(biāo)準(zhǔn)增加了幾個(gè)字。我把這個(gè)要求更向前推動(dòng)了一步,努力把這些模型相關(guān)問題包裝到內(nèi)核中。在理想的情況下,在文件 CAMEL80H.AZM 中的高級(jí) FORH 代碼對(duì)于所有的 CAMEL Forth 目標(biāo)應(yīng)該是相同(盡管不同的匯編程序會(huì)有不同的語法)。
單元尺寸的差異和字的對(duì)齊要求由 ANS Forth 字 ALIGN ALIGNED CELL+ CELLS CHAR+ CHARS 和我自己附加的字 CELL ( 等效于 1 CELLS, 但編譯之后會(huì)更小 ) 來管理。
字 COMPILE 、 !CF 、 CF 、 !COLON 和 EXIT 隱藏了串線模型的特性,比如:串線是如何表示的、代碼域是如何實(shí)現(xiàn)的。
當(dāng)你研究 Z80 直接串線和 8051 子程序串線時(shí),這些字的值就變得非常明顯:
按同樣的風(fēng)格,字 ,BRANCH 、 ,DEST 和 !DEST 隱藏了高級(jí)分支和循環(huán)操作符的實(shí)現(xiàn)。我試著發(fā)明 -- 不借用現(xiàn)有的 Forth 系統(tǒng) -- 最少的操作符集合,它可以因子化實(shí)現(xiàn)的差異。只有時(shí)間、專家評(píng)判和許多 CAMEL Forth 才可以說明我在這方面取得了多少成功。
到目前為止,我并沒有成功地把首部結(jié)構(gòu)中的不同因子化到相似的字集。FIND 和 CREATE 是和首部?jī)?nèi)容緊密相連的,我還沒有找到合適的子因子。我已經(jīng)開始了這方面的努力,通過字 NFA>LFA NFA>CFA IMMED? HIDE REVEAL 和 ANS Forth 字 >BODY IMMEDIATE. ,我將繼續(xù)這一工作。值得欣慰的是,現(xiàn)在可以把同樣的首部結(jié)構(gòu)用于所有的 CAMEL Forth 實(shí)現(xiàn)(因?yàn)樗鼈兌际亲止?jié)方式尋址的16位 Forth 系統(tǒng))
接下來我將要給出一個(gè) 8051 內(nèi)核,并說明 Forth 編譯器和解釋器是如何用于 哈佛體系結(jié)構(gòu)的(這種系統(tǒng)結(jié)構(gòu)的計(jì)算機(jī)把存儲(chǔ)器邏輯地分成代碼和數(shù)據(jù)兩個(gè)部分,比如 8051)。對(duì)于 8051 ,我會(huì)給出文件 CAMEL51 和 CAMEL51D ,但是沒有 CAMEL51H ,因?yàn)槌藚R編語言格式外,高級(jí)代碼不會(huì)與我這里討論的有什么差異,而本刊的編輯也需要發(fā)表其它的文章。好在所有的代碼都是可以下載的。
Link – 在 CamelForth 和 Fig-Forth 中,指向前一個(gè)字的長(zhǎng)度字節(jié)。在 Pygmy Forth 和 F83, 中,指向前一個(gè)字的 LINK 。
P – 優(yōu)先位,如果是 1 則為立即字,在 Pygmy 中沒有使用。
S - Smudge 位,阻止 FIND 找到這個(gè)字
1 – 在 Fig-Forth 和 F83 中,長(zhǎng)度字節(jié)和名字和最后一個(gè)字符的最高有效位(位 7 )用 1 來標(biāo)識(shí)
View – 在 Pygmy Forth 和 F83 中,是這個(gè)字所在源碼塊的編號(hào)
參考文獻(xiàn)
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 的源代碼可以從下列站點(diǎn)上得到 ftp://ftp.zetetics.com/pub/forth/camel/cam80-12.zip .
第七部分 8051 的 Camel Forth
在我們尊敬的編輯要求下,我給出了 8051 的 CAMEL Forth ,而用于 MC6809 的 Forth 也將很快完成。這個(gè) 8051 Forth 占用 6K 字節(jié)的程序存儲(chǔ)器。不過,全部的源代碼將占 TCJ 的 16 頁,所以這篇文章只給出了核心移植過程中的主要變化。我們將解釋高級(jí)代碼是如何按 8051 匯編器格式要求和子程序串線技術(shù)而修改的。
Z80 更正
在文件 CAMEL80H.AZM 中, DO 的定義是這樣給出的
['] xdo ,BRANCH . . .
它應(yīng)該是
['] xdo ,XT . . .
這是由于在 Z80 上沒有 consequence ( 在那里, ,BRANCH 和 ,XT 是等價(jià)的 ), 但在 8051 上,它是明顯的。
8051 CAMEL Forth 模型
在 #60 論文中,我匯總了 8051 Forth 的設(shè)計(jì)方法。再次說明: 8051 反映遲鈍的存儲(chǔ)器尋址實(shí)際上要求使用子程序串線。這就意味著硬件堆棧(在 8051 的寄存器文件中)就是返回棧。參數(shù)棧(也就是數(shù)據(jù)棧)在 256 字節(jié)的外部 RAM 中,使用 R0 作為這個(gè)堆棧的指針。從那篇文章開始,我發(fā)現(xiàn)了把棧頂元素( TOS )放到 DPTR 中比放在 R3 : R2 中更好。于是就有了這樣的程序員模型
這其中也包含了 Charles Curley [CUR93] 的思想。在像 8051 這樣寄存器豐富的機(jī)器上,我們可以把內(nèi)層循環(huán)索引放在寄存器中,以使得 LOOP 和 +LOOP 更快。 DO 必須向返回棧壓入兩個(gè)值:舊的循環(huán)索引和新的循環(huán)終值。 UNLOOP 當(dāng)然需要從返回棧得到循環(huán)索引 -- ANSI 把 UNLLOP 做為一個(gè)單獨(dú)的詞。注意 R6:R7 不是返回棧的棧頂元素,它只是內(nèi)層循環(huán)的索引。
P2 含有參數(shù)棧指針的高字節(jié)(允許 R0 尋址外部存儲(chǔ)器),它也是用戶指針的高字節(jié) -- UP 的低字節(jié)假設(shè)為 00 。我費(fèi)了很大的勁才明白當(dāng)從外部 ROM 執(zhí)行時(shí), P2 是不能讀的,所以我保存了一份 P2 的拷貝在寄存器 8 中。
對(duì) BRANCH 和 ?BRANCH 我有一個(gè)非常好的實(shí)現(xiàn)方法。由于 8051 模型是子程序串線,高級(jí) Forth 作為真正的機(jī)器代碼來編譯,所以 BRANCH 可以用一個(gè) SJMP (或者 AJMP 或者 LJMP )指令實(shí)現(xiàn)。 ?BRANCH 可以用一個(gè) JZ 指令實(shí)現(xiàn),只要 TOS 的零 / 非零標(biāo)志已經(jīng)放到了累加器中( A 寄存器)。用一個(gè)子程序 ZEROSENSE 做這個(gè)工作,所以 BRANCH 和 ?BRANCH 就變成了:
BRANCH:
SJMP dest
?BRANCH:
LCALL ZEROSENSE JZ dest
與此相似, LOOPSENSE 和 PLUSLOOPSENSE 允許 JZ 指令使用 LOOP 和 +LOOP 。對(duì)于這些情況,在 JZ 之后應(yīng)該調(diào)用 UNLOOP 以清除程序“FALLS OUT”循環(huán)時(shí)的返回棧。
在匯編語言源文件中的許多地方,我手工把 LCALL word RET 用更短更快的字 LJMP word替換 ,只要“字”不是一個(gè)返回棧操作符(比如 R> 或者 >R )。而只要有可能,字 LCALL 和 LJMP 就用 ACALL 和 AJMP 替代。
我用 Intel 字節(jié)順序?qū)懥?8051 內(nèi)核(低字節(jié)在先),之后我發(fā)現(xiàn)編譯到 LJMP 和 LCALL 的地址是高字節(jié)在先。為了避免重寫整個(gè)內(nèi)核,我為這些編譯 LCALLS 的字包含了一個(gè)字節(jié)交換的字: COMPILE, !CF 和 ,CF ( 它們都是Dependency 字匯集 ).
哈佛體系結(jié)構(gòu)
8051 使用哈佛體系結(jié)構(gòu),程序和數(shù)據(jù)保存在分開的存儲(chǔ)器中。在嵌入式系統(tǒng)中,它們分別是 ROM 和 RAM 。 ANS Forth 是第一個(gè)能夠適應(yīng)哈佛體系結(jié)構(gòu)限制的標(biāo)準(zhǔn)。簡(jiǎn)單地說, ANS Forth 規(guī)定:
?? 應(yīng)用程序只能夠訪問數(shù)據(jù)存儲(chǔ)器,同時(shí)
?? 所有訪問存儲(chǔ)器和構(gòu)造數(shù)據(jù)結(jié)構(gòu)的操作符必須在數(shù)據(jù)空間操作。
( 參看 ANS 文檔 section 3.3.3 [ANS94].) 包括下列 Forth 字: @ ! C@ C! DP HERE ALLOT , C, COUNT TYPE WORD (S") S" CMOVE
然而, Forth 編譯器還需要訪問程序空間(也稱為代碼或者指令空間)。 Forth 需要為程序空間和數(shù)據(jù)空間維護(hù)一個(gè)字典指針。所以我增加以下這些新的字:
I@ I! IC@ IC! IDP IHERE IALLOT I, IC, ICOUNT ITYPE IWORD (IS") IS" D->I I->D
這里前綴“I”表示指令(因?yàn)?P 和 C 在 Forth 中已經(jīng)有了其它的意義)。 ICOUNT 和 ITYPE 用于顯示已經(jīng)被編譯到 ROM 中的串。 IWORD 從數(shù)據(jù)空間復(fù)制 WORD 留下的字到代碼空間 -- 用于構(gòu)造 Forth 字頭和放到 ROM 中的串。 D->I 和 I->D 是與 CMOVE 等效的,它從 / 向代碼空間復(fù)制。
VARIABLE 必須定位到數(shù)據(jù)空間。所以它們不能使用傳統(tǒng)的辦法把數(shù)據(jù)緊接著代碼域存放。這里的方法是:在數(shù)據(jù)空間中數(shù)據(jù)的地址存放在代碼域之后?;旧?#xff0c;一個(gè) VARIABLE 就是一個(gè) CONSTANT ,它的值就是數(shù)據(jù)空間的地址。 ( 當(dāng)然,傳統(tǒng)的 CONSTANT 依然有效 )
CREATE 字,以及使用 CREATE …… DOES> 創(chuàng)建的字,必須按同樣的方式工作。以下是它們?cè)诔绦蚩臻g看起來的樣子:
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" 定義的串(“文本常數(shù)”)必須駐留在數(shù)據(jù)空間,在那里它們可以被 TYPE 和 EVALUATE 這樣一些字使用。但是我們希望這些字是定義的一部分,并且在 ROM Forth 環(huán)境中能夠駐留在 ROM 里。我們可以把字符串存儲(chǔ)在程序空間,引用的時(shí)候復(fù)制到 HERE ,但是 ANS 文檔不允許文本常數(shù)存在于這個(gè)“臨時(shí)”的存儲(chǔ)區(qū)域(參看 ANS 文檔 sections 3.3.3.4 和 3.3.3.6 [ANS93]) 。同時(shí),如果 WORD 把它的串地址在 HERE中返回 -- 就像 CAMEL Forth -- 則文本常數(shù)不能改變這個(gè)臨時(shí)區(qū)域。
我的解決方案是 S" 存儲(chǔ)串到代碼空間,但是也在數(shù)據(jù)空間為它永久地保留位置,當(dāng)引用時(shí),把它從代碼空間復(fù)制到這個(gè)數(shù)據(jù)空間。 ANS Forth 并沒有解決 哈佛體系結(jié)構(gòu)處理器的全部問題,有時(shí)像 C 一樣的“初始化數(shù)據(jù)區(qū)”可能也是需要的。
因?yàn)?." 從來也不能被程序員使用,它們可以存儲(chǔ)在代碼空間中,方法是使用字 (IS") 和 IS" 。 ( 它們是 " 老 " 的 (S") 和 S".) 。雖然內(nèi)核增加了兩個(gè)字,但是節(jié)省了許多數(shù)據(jù)空間。我計(jì)劃把有關(guān)串常數(shù)的字集中到 Dependency 字匯集,或者建立一個(gè)新的“HARDVARD”字匯集。
寫入程序空間
8051 并不能真正地寫入程序存儲(chǔ)器,沒有硬件信號(hào),也沒有硬件指令。在這種環(huán)境下, CAMELForth 解釋器可以工作,但是不能編譯新的字。我們可以設(shè)法讓某些存儲(chǔ)器同時(shí)出現(xiàn)在程序和數(shù)據(jù)空間。許多 8031 應(yīng)用說明給出了同時(shí)訪問數(shù)據(jù)和程序空間的方法,在硬件上通過組合一些信號(hào)實(shí)現(xiàn)。圖 1 給出了我對(duì)電路板的修改,這個(gè)電路板是 Blue Ridge Micros (2505 Plymouth Road, Johnson City, TN, 37601, USA, telephone 615-335-6696, fax 615-929-3164) 的 MCB8031. U1A 和 U1B 產(chǎn)生一個(gè)新的選通信號(hào),只要程序或者數(shù)據(jù)讀一個(gè)有效時(shí)就可以 EPROM 在 A15 為低時(shí)被選擇 ( 低 32K), RAM 在 A15 為高時(shí)有效 ( 高 32K) 。 當(dāng)然,你不能寫入 EPROM ,但是你可以從 RAM 中執(zhí)行程序!有一個(gè)缺點(diǎn): : 這使得 @ 和 I@ 等效,如果你在什么地方用錯(cuò)了它們,則并不能馬上發(fā)現(xiàn)。
圖 1 修改的 8051 電路圖
這些高級(jí)定義字修改的目的是實(shí)現(xiàn)對(duì) CAMEL Forth 在哈佛體系結(jié)構(gòu)和馮諾曼 體系結(jié)構(gòu)機(jī)器之間移植。對(duì)于后者,新的程序空間字可以簡(jiǎn)單地對(duì)應(yīng)到數(shù)據(jù)空間字,比如對(duì)于 Z80
IFETCH EQU FETCH
ISTORE EQU STORE
ITYPE EQU TYPE
等等
在下一篇文章中,我將要修改 8051 源代碼,使它能在 6809 上工作,這是一個(gè)通過不斷改進(jìn)而得到的真正可移植模型。
參考文獻(xiàn)
[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 的源代碼可以從下列站點(diǎn)上得到 ftp://ftp.zetetics.com/pub/forth/camel/cam51-15.zip
第八部分 MC6809 Camel Forth
現(xiàn)在,我們將給出本文的最后部分,也就是許諾已久的 MOTOROLA 6809 ANSI CAMEL Forth 。這個(gè)實(shí)現(xiàn)是專門為 Scroungmaster II 處理器板而設(shè)計(jì)的。
與 Z80 和 8051 的 CAMELForth 不同, MC6809 Forth 是用我的“Chromium 2 Forth MATA 編譯器”生成的。你可以看到兩件事:
?? 首先、 MATA 編譯器在一個(gè)老的 Forth 系統(tǒng)上運(yùn)行(F83),所以源代碼中含有 16 x 64 的 Forth“SCREEN”。我試著把它轉(zhuǎn)為 ASCII 文件,但是原始的痕跡還是很明顯;
?? 第二、用于 META 編譯器的源代碼看起來很像一般的 Forth 代碼(我馬上就要討論,有一些小的變化),這樣,關(guān)于 1+ 的定義就變成了:
CODE 1+ 1 # ADDD, NEXT ;C
匯編器使用的是我以前討論過的 MC6809 匯編器。
我直接照著已經(jīng)出版的列表打入高級(jí)源代碼(轉(zhuǎn)換到 Forth 語法)。不幸的是,由于這中間隔了很長(zhǎng)時(shí)間,并且我有時(shí)參照 Z80 列表、有時(shí)又參照 8051 列表……結(jié)果是 HARVARD 體系結(jié)構(gòu)構(gòu)造字(比如 I@ IALLOC )沒有堅(jiān)持用在 MC6809 中。這對(duì)于非 HARVARD 結(jié)構(gòu)的 MC6809 并不重要,但是如果要把 Forth 代碼用于 HARVARD 結(jié)構(gòu),我就不得不再修改這些錯(cuò)誤。
另外,由于我是在已經(jīng)出版的列表基礎(chǔ)上工作的,我常常忘了給高級(jí)字定義寫上詳細(xì)的說明,不過,你可以從原來的列表中知道它們是如何工作的,當(dāng)然,我并不強(qiáng)制你這樣做。
MC6809 CAMEL Forth 的源代碼說明
MC6809 CAMEL Forth 模型把 TOS 放到 D 寄存器中,把S 棧指針用于參數(shù)棧, U 指針用于返回棧, Y 是解釋指針。 X 是 W 寄存器的臨時(shí)寄存器。 MC6809 直接頁指針 DPR 保存用戶指針的高字節(jié)(低字節(jié)假設(shè)是 0 )。
Scroungemaster II 單板上的 8K RAM 和 8K EPROM 按以下地址映象:
6000-797Fh RAM 字典 ( 用于新定義 )
7980-79FFh 終端輸入緩沖區(qū)
7A00-7A7Fh User 區(qū) (USER 變量 )
7A80-7AFFh 參數(shù)棧 ( 向下增長(zhǎng) )
7B00-7B27h HOLD 區(qū) ( 向下增長(zhǎng) )
7B28-7B7Fh PAD 區(qū) ( 通用緩沖區(qū) )
7B80-7BFFh 返回棧 (grows downward)
E000-FFFFh EPROM 中的 Forth 內(nèi)核
所有的 RAM 數(shù)據(jù)區(qū)通過用戶指針引用,它的開始地址是 UP-INIT :在我們這里是 7A00H (注意這個(gè)字的高字節(jié)和 UP-INIT-HI 的使用)。當(dāng) CAMEL Forth 開始的時(shí)候,它會(huì)把字典指針設(shè)置到 DP-INIT ,而且必須在 RAM 中,這樣你就可以向 Forth 字典中加入一個(gè)新的定義。這些都是由 META 編譯器的 EQU 指令指定的。這些 EQU 指令并不占用 MC6809 的核心空間,它們也不會(huì)出現(xiàn)在 MC6809 的 Forth 字典中。
DICTIONARY 告訴 MATA 編譯器在哪里編譯代碼,在我們的情況下是 E000-FFFFH 的 8K EPROM ,新的字典命名為 ROM ,然后 ROM 被指定到所選定的字典。(如果你熟悉 Forth 的詞匯表,你就會(huì)看到很強(qiáng)的相似性)。
字 AKA 定義一個(gè) Forth 字的同義詞。因?yàn)?MC6809 不是一個(gè)哈佛體系結(jié)構(gòu)計(jì)算機(jī),我們應(yīng)該把所有在源代碼中出現(xiàn)的 I@ 編譯成 @ ,其它的“帶 I 前綴”(指令空間)字也做同樣的處理。 AKA 將完成這個(gè)工作。這些同義詞像 EUQ 一樣,它們不出現(xiàn)在 MC6809 的字典中。
MATA 編譯器允許你使用前向引用,就是訪問那些還沒有定義的 Forth 字(你當(dāng)然需要在全部完成之前定義它們!)。這通常是自動(dòng)的,但是 AKA 要求你使用 PRESUME 明確地說明,比如:
PRESUME WORD AKA WORD IWORD
用于創(chuàng)建 IWORD 的同義詞。 @ ! HERE ALLOT 是 META 編譯器自動(dòng)定義的,我們不需要對(duì)這些詞使用 PRESUME 。
CODE 定義非常方便。注意你可以使用:
HERE EQU labelname
在 META 編譯中產(chǎn)生一個(gè)標(biāo)號(hào),(這是 META 編譯器的一個(gè)功能而不是匯編器的功能)。另外, ASM: 開始一個(gè)匯編代碼片段(也就是說,這不是一個(gè) CODE 字的一部分)。
下面的短語
HERE RESOLVES name
用于解決 META 編譯器使用的特定的前向引用(例如, MEATA 編譯器需要知道 DOCOLON 動(dòng)作的代碼在哪里)。你應(yīng)該使這些獨(dú)立。除此之外,你可以自由地在源代碼中加入 CODE 定義。
定義字和控制結(jié)構(gòu)(IMMEDIATE 立即字)的代碼更加難懂,其中的原因是這些字在 META 編譯期間也要執(zhí)行一些動(dòng)作。例如: MC6809 Forth 包含有標(biāo)準(zhǔn)字 COSNTANT 用于定義一個(gè)新的常數(shù)。但是許多 COSNTANT 定義也出現(xiàn)在 MC6809 內(nèi)核中。我們?cè)?META 編譯中也許需要定義一個(gè)新的 CONSTANT 。 EMULATE: 短語用于指示不同的 CONSTANT 沖突時(shí)如何動(dòng)作。這個(gè)短語是完全用 MEATA 編譯器字寫成的,所以看起來完全是含混不清。
與此類似, IF THEN 和其它同類的字包含 META 編譯短語用于構(gòu)造和解決 MC6809 映象的分歧。一些 META 編譯器把這些字隱藏在編譯器之中,這可以產(chǎn)生漂亮的目標(biāo)代碼,但是,如果你需要改變分支的方式,你就必須修改 META 編譯器。
我傾向于使這些動(dòng)作易于修改,所以我選擇 Chromium 放到目標(biāo)源代碼中。(最恐怖的例子是 TENDLOOP 和 TS" 的定義,它們實(shí)際是在目標(biāo)源代碼當(dāng)中擴(kuò)展了 META 編譯器的詞匯表。
如果你是一個(gè) Forth 和 META 編譯器的新手,最好的方法是接受這一切?!捌胀ā钡拿疤?hào)定義是很容易加入的,只需要參照 MC6809 其它部分源代碼就可以了。你甚至可以寫 CREATE …… DOES> 字義,只要你不在 META 編譯器中使用它們。
在一個(gè) 1MHz 的 MC6809 上,一行文本輸入需要明顯長(zhǎng)的時(shí)間去處理(粗略估計(jì)約為 1 秒鐘)。這其中的部分原因是由于解釋器的許多部分是使用高級(jí) Forth 編碼的,另一部分原因是 CAMEL Forth 使用了一個(gè)單鏈表結(jié)構(gòu)的字典。這些只影響編譯的速度而不會(huì)影響到執(zhí)行速度。
不過,延遲總是煩人的,也許有一天我會(huì)寫出一篇有關(guān)“加速 Forth”的論文。
現(xiàn)在,用戶指針 UP 不會(huì)改變。我們擁有一個(gè) UP 的目的是支持多任務(wù) -- 每個(gè)任務(wù)有它自己的用戶區(qū)、堆棧等等。我將很快針對(duì)這個(gè)問題開展工作。我也許會(huì)研究 SM II 的存儲(chǔ)器管理,為每個(gè)任務(wù)提供 32K 的私有字典。當(dāng)然,我會(huì)努力寫出一個(gè)真正的使用共享總線的多處理器 Forth 內(nèi)核。如果我活得足夠長(zhǎng),理所當(dāng)然地還應(yīng)該寫一個(gè)使用串行口的分布式 Forth 內(nèi)核。
MC6809 的 CAMEL Forth 版本 1.0 的源代碼在 GEnie 的 Forth Roundtable ,文件名稱為 CAM09-10.ZIP ,這個(gè)文件包含了 Chromium 2 meta 編譯器,是可以運(yùn)行的。只要有 F83 ,你就可以輸入:
F83 CHROMIUM.SCR
1 LOAD
BYE
這樣就可以裝入 META 編譯器,編譯MC6809 CAMEL Forth ,把結(jié)果寫入 Intel 格式的6809.HEX 中。注意:如果你使用的是 CP/M 或者 Atari ST 版本的 F83 ,則必須編輯 LOAD 屏幕以刪除 HEX 文件實(shí)用程序,因?yàn)檫@個(gè)程序只是為 MS-DOS 機(jī)而編寫的。我沒有測(cè)試 Chromium 2 使用的 CP/M 或者 Atari ST ,如果需要幫助,請(qǐng)與我聯(lián)系。
參考文獻(xiàn)
[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的源代碼可以從下列站點(diǎn)上得到 ftp://ftp.zetetics.com/pub/ forth /camel/cam09-10.zip .
總結(jié)
以上是生活随笔為你收集整理的Forth 系统实现的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 北京交通大学计算机学院复试名单,北京交通
- 下一篇: 使用c++实现各种进制之间的转换