2026年3月16日 星期一

如何設計高低階混用碼指令?

如何設計高低階混用碼指令?


曾慶潭 Ching-Tang Tseng
ilikeforth@gmail.com
Hamilton, New Zealand
16 March 2026


在某些特殊情況,當我們使用 FORTH 系統提供的所有現成指令設計程式時,會希望高低階指令可以任意混合使用,這種特殊編寫程式的功能稱之為混碼程式(Mixing code)技術。程式語言可以有許許多多的古怪技術,但對 FORTH 而言,只要是你描述得出來的技術, FORTH 沒有設計不出來的,而且強調僅由個人私下就可以解決,這是 FORTH 具有延伸性性能使然。不像其他程式語言,特殊功能通常要等原販售公司將其添加於系統,發行新版後才有可能,因為大多數程式語言均屬封閉性,不容使用者隨意修改系統既定功能。本文介紹如何為 FORTH 系統添加混碼程式功能?它就是一個 FORTH 程式語言與其他程式語言比較時,在眾多性能差異上,特別成為鮮明對比的例子。

1 預備知識

初學 FORTH 之人,可能較難看懂這種指令的設計原理,因為初學者通常不會一開始就直接接觸理論性的『FORTH系統結構』與『FORTH指令基本結構』,更進一步的『FORTH執行原理』,則要等學過 FORTH 後,還有興趣深入探討系統時才會去研究它,而欲設計這種指令時,必須先具備這些基本知識,否則不易透徹了解它們的設計方法。另外,因為要混用高低階指令編寫程式,系統當然要具備 FORTH Assembler 的功能,在使用 FORTH 組合語言時,所有的規矩也都得遵循,要有『CPU內有那些暫存器不可亂用?』的限制觀念。上述的預備知識就成了能看懂本文的基本要素了。屬於系統部份的預備知識,你可以從 1987 年 4 月我們出版過的『FORTH期刊』中獲得。組合語言的部份要自行下功夫實作後才能真正體會了解,要先行標出 CPU 內那些暫存器被 FORTH 系統規劃掉了?必須避開不用,或用前搬開內容,用完再合理恢復,所寫的程式執行時才不會出錯。

這些指令不屬於標準指令,在歷來的標準指令表中找不到他們的蹤跡,甚至於最新的 ANSI 標準中也絕口不提,可是它在 FORTH 發展的早期就已經被經常使用了,很久以前,我們在使用這種性能已經多年之後,才在後來新發行的 C 類程式語言系統中見到它的仿照設計。 FORTH 有許多性能領先別人許久,一直到今天依然如此,我們還可以舉出多項性能是一般程式語言所沒有的,這也是為何學會 FORTH 後便愛用 FORTH 的原因。學會設計混碼程式需用指令的設計方法,將更加強對於 FORTH 系統的深入認識,也將終身受用。看懂本文,將來就能輕易的為任何新出現的 FORTH 系統添加此一性能,這是作者為傳揚 FORTH 的最主要目的。因此,特別挑選了一個原本欠缺此一性能的大眾化系統來驗證它,就以 eForth32 做為設計基礎,為其添加此一性能。如果你手頭上使用中的系統已經有此性能,或者只有一半性能,例如:只能在高階定義指令中混入低階指令編寫程式,而於編寫低階定義指令時,還不能混入高階指令。那麼,你就可以根據本文自行添加此性能,自己磨練系統指令的程式設計能力,並與原已有功能比較,將是毫不遜色,因為不同的設計都將殊途同歸,對於程式執行速度或執行效果毫無影響。

如果你尚未準備好上述背景知識,建議你暫時不用細看本文,留供日後參考便可。通常一般商售的 FORTH 系統都提供了此一性能,它們較常用的指令名稱為 : [C 、 C] 、 C: 、 ;C 之類的名稱。前二者成一對,使用在高階定義中轉入低階再轉回高階。後二者成另一對,使用在低階定義中轉入高階再轉回低階。不用讀通本文,你也可以直接使用這些指令,使用手冊的說明文獻也只能做到這個地步。

留供日後參考的建議係作者個人的贈言,四十幾年前,我們在中山科學研究院內的週五研討會上,討論過此項性能。我記得當時藍瑞賢個人獨衷 Zen-Forth 商售系統,其中只提供了這項功能的半套指令,而當時大家公認使用的推廣標準是無版權問題的 F83-FORTH 系統,已有完整的這項功能。為此問題我們另有 Poly-FORTH 系統,也有完整的這項功能,可以比照參考。陳爽的 FORTH 功力當時最好,他帶頭講解的結果,讓與會者此後便有能力自建此功能於任何系統,我則仔細的做了筆記。三十年前我們移民到了紐西蘭,我隨身攜帶來此的書籍、簿本,只有三樣,一本中英對照的『聖經』,一本精裝大字足本的『四書讀本』,另外就是所有的 FORTH 歷年筆記,這些都是精神糧食,好東西就應該留供日後參考,單純只留在腦袋中,老了是會忘記的。我在 eForth32 中重建此功能時,只花了半天時間,而且一次完成,為什麼?靠的就是筆記。

2 使用時機

我們可以舉出許多例子來強調這項功能非常方便,尤其是對一個想玩進 FORTH 系統內部的人更具有吸引力,因為它涉及了低階程式的可以無所不做能力,以及執行速度可以發揮到系統極限的先天優點,另外配上高階程式的現成與複雜功能,二者組合起來,在許多場合就相當方便。因此,若輕易就能擁有的功能,為何不用?它不是標準指令又有甚麼關係? 舉例來說:如果我有一個程式想測系統性能,必須斤斤計較執行速度到系統的基本時脈週期程度。我曾寫過這種程式,就以讀取電腦中的標準時基計數器內容當標準,要讀得又快又穩又準,當然得用低階組合語言來寫程式,想像中幾乎是一瞬間就得到了參考時基,而且只用幾個低階指令,自己耗了幾個時脈數很容易清算,易於隨後扣除。接下來要處理計算與顯示時,若還只能用低階程式,那就非常痛苦了,這項高低階可以混用的功能就很有用處,讓問題極容易解決。

另外,我們在設計中斷處理程式最後,免不了要用低階程式來安放執行位址(Entry point)於中斷執行向量位置,中斷瞬間想以低階最快速度做一件事,隨後想做的事通常就很複雜,不用高階就不方便,做到關鍵處還有可能又要求用低階設計比較有效,如此又高又低的翻來覆去,這項功能更能提供方便。

還有一些平日設計的高階程式,在某些過程中想穿透系統去做一些沒有指令可用的工作,例如:像除錯工具指令一樣可以取得當時 FORTH 系統高階定義指令的程式計數器(Program counter) 內容,高階指令設計中便可混入幾個低階指令來完成。等同於 FORTH 系統高階指令程式計數器的專有名詞,在 FORTH 領域,則稱之為指令指標暫存器(Interpreter Pointer or Instruction Pointer,IP ),系統一般均不提供操作它的指令,古時候的系統則有此 IP 指令可用,它會直接取得系統執行當時的 IP 內容,有幾個結構化跳躍指令,可以藉助於此指令之存在而容易完成設計,後來有新觀念被發展出來才不用它了。

另外,一個接近終了的程式設計,也許會有強調必須改善執行速度的要求,大家都知道,此時非用低階指令取代設計不可了,如果可修正的指令不想搬離原設計位置,那麼,這項功能理所當然的可以提供服務。

還有無數的例子可以舉出,那怕僅只是玩一玩,當基本教材訓練學生,都是有用的,越用 FORTH 設計程式,就越能發現那些場合?可以讓這項功能盡量發揮。我常用,可是很不喜歡用這種程式來當作寫 FORTH 文章的題材,因為要用組合語言,文章只能在限用時間內有效,我擅長用卻不擅長描述這種程式,痛苦之至,最好不寫。很不幸本文的程式設計就不可或免的得用到一些組合語言,我只好將就一點,勉為其難的在該部份進行簡單說明。

3 基本原理

在解說問題時,必須將問題簡化才方便解釋,我們就以這個方式開始。 FORTH 系統中,強調所有的東西都盡可能為統一的格式,因此,無論是高、低階指令,或變數、常數、或字彙,它們的基本結構都是一致的,至少有名稱欄、連結欄,解碼欄、參數欄,某些系統還多了觀察欄或其他欄,我們暫時別管它們的存在與否,只考慮基本上標準的前四欄。

無論你如何寫程式,能被 FORTH 系統接受的最後結果,其格式仍必須符合標準結構的要求,否則不能被正常的執行。因此,我們現在探討的主題,也不能脫離這個範疇。也就是說,高階定義指令中混用低階的指令,或低階指令中混用了高階指令,這個基本結構規矩仍得遵循,不可以在基本結構中另出一基本結構,我們想解說的問題因此被簡化了一半。

四欄的結構有時又被分成兩個部份,一是頭部,也就是名稱欄與連結欄,另一個就是體部,也就是解碼欄與參數欄。分成兩部份,有時有利,有時有害,利害分析這部份,超出本文探討範圍,擱置不予置評,有許多系統都這樣做, eForth32 、Win32Forth 都是, F83 則不是。高階定義指令中使用低階,其中的低階既然不能也是一個原來的標準結構樣子,那麼,必須是怎樣的結構才能被系統接受呢?根據『FORTH執行原理』來考慮,如果我們能夠去除這個下屬於高階指令的低階指令其頭部,系統的執行機制就可以符合正常的 FORTH 系統執行原理。換句話說,如果我們將這樣的程式設計,搞成包括進來的低階指令沒了頭部,只有體部,那麼整個系統的執行機理就能維持而未被破壞。反之,低階指令中使用了高階,其理亦然,只能留住下屬者的體部,而不能有其頭部。根據這樣的原則,我們就能按照FORTH的執行原理,在邊界位置設計程式,來切換系統執行高階或低階指令時的相關程式計數器內容,以達到目的。

所謂的程式計數器,我們暫時可以將情況比擬想像成, Forth 系統中高階定義指令的程式計數器是 IP 。而低階定義指令的程式計數器是 W ,也就是 Forth 系統中所謂的工作暫存器(Current Word Pointer or Working Register)。一般 CPU 所擁有的程式計數器則仍然稱之為 PC ,如果還想再延伸推理到 FORTH 系統中是否還有其他類似意義的程式計數器?那麼我們也可以說負責多工任務(Multi-task)的另一個指標,使用者系統變數指標暫存器 UP(User Pointer),可以算是多工(Tasks)運轉時的程式計數器,接受這一段描述,很可以加強你了解所謂『FORTH虛電腦』這個專有名詞的意義。

如果你很能融會貫通新學的東西,也很有創意,用心想一想,有一天也許能在 FORTH 領域創作出另一個程式計數器,例如:一種中階定義指令的指標暫存器,名叫 MP(Middle Word Pointer ),或者是比 UP 更高的超級程式計數器,用它供作多人使用時、許多系統相連時、甚或全世界網域互通時的指標暫存器之用,它名叫 Super-P ,因為管得太大了,所以名字比較長。讀者千萬不要認為這些推理不切實際,它蘊涵了 FORTH 一貫的哲理。

將指令結構分成兩部份的系統,常強調最終的應用程式被執行時只有體部才被用到,頭部可以說是多餘的,因此分成兩部份放置後,最後可以一次就丟光系統所有的頭部,只留體部,應用程式仍然照舊執行,省了記憶體,還讓有心人無法以逆行追蹤的方式還原源程式的結構。混用碼的程式技術也就是根據這樣的原則,來將其所下屬的高低不平等指令埋進系統的,我們須要的指令就根據此理而設計,這一部份簡化了剩下來另外一半的問題。

4 程式設計

下列是一套設計完成後經過測試的指令組:


 

ONLY FORTH ALSO ASSEMBLER ALSO DEFINITIONS
: [C HERE 2+ , ASSEMBLER [COMPILE] [ ; IMMEDIATE
: C] [ ASSEMBLER ] HERE SPLIT DROP 6 + # SI MOV NEXT FORTH ] ;
CODE DOCOLON
      BP SP XCHG
      DS PUSH
      SI PUSH
      BP SP XCHG
      SI POP
      NEXT 
      END-CODE
CODE (;C)
      BP SP XCHG
      SI POP
      DS POP
      BP SP XCHG
      NEXT 
      END-CODE
: C: [’] DOCOLON CALL, FORTH ] ;
: ;C [ ASSEMBLER ] COMPILE (;C) ASSEMBLER [COMPILE] [ ; IMMEDIATE
FORTH DEFINITIONS



事實上,這個程式可以精簡到只有高階定義的四列程式,上列兩個低階定義的指令 : DOCOLON 與 (;C) 根本就是從原 eForth32 系統中抄來的。我不喜講解組合語言的程式,此處係為了方便而借來使用而已。這個 DOCOLON 在 eForth32 系統源程式中被命名為 doList ,而 (;C) 為 semi ,但系統經蛻變程式(Meta-compiler)長成後,將名稱埋掉了,使用者見不到指令名,因此也就無從直接叫用,指令根本不長,我們就重新以低階指令方式定義一遍又有何妨?也因此讓上列程式多了幾列,而實質重點確實只須四列。

隨便寫兩個測試程式試驗指令功能如下:


 

:  DOUBLE ( n - - )         \  高階混入低階舉例
   DUP                      \ 以高階指令設計,模擬慢速輸入數據
   [C   BX  AX  MOV         \ 以低階指令設計,模擬快速處理數據
   AX  AX  ADD    
   AX  BX  MOV   C]
   CR  .                    \ 恢復方便但慢速的顯示處理
   .” is the double of ”  .  ;

CODE LMH ( - - )            \ 低階混入高階舉例            
     DX  PUSH               \ 以低階指令設計,模擬高速輸入數據
     BX  PUSH
     5  #  BX  MOV
     0  #  DX  MOV
     C:                     \ 轉入高階指令環境
     CR                     \ 以高階指令設計,模擬方便但慢速的數據處理
     .”  The double of 5 is ”  
     DUP  +  . 
     ;C                     \ 恢復低階指令環境
     NEXT	
     END-CODE



它們均執行無誤,可是我得提醒你,在你並不清楚 eForth32 系統中組合語言的使用限制前,千萬不要隨便將一般組合語言的用法,直接設計程式來測試這一組指令,因為系統將 CPU 內的一些暫存器分配給 eForth32 虛電腦使用了,不得破壞,換句話說,你得遵循 FORTH Assembler 的規矩設計低階部份的程式才行。

5 程式說明

前一節中的四列高階設計指令,就是本文的主要重點,不詳加說明誠難理解。現在我們才開始正式的逐個說明程式設計的內容。首先是由高階中進入低階的第一個指令 [C ,為說明方便,它的定義重抄重排如下:


 

: [C   HERE   2+   ,
  ASSEMBLER  
  [COMPILE]   [   ;   IMMEDIATE 
  


我們希望這個『 [C 』指令執行之後,可以開始自由的書寫 FORTH Assembler 程式。說明前可以先行想像,如果不作此額外要求,而是正常高階定義的指令,系統編譯到此處時,應該要編進系統另一個高階指令,或一個低階指令解碼欄所在之所謂的執行位址,現在有此額外要求了,因此必須編進系統一個低階指令格式所須的解碼欄執行位址,就如同前述第 3 節基本原理中已經提到,此後插進來的低階指令,必須是一種沒有頭部,只有體部的格式,因此,我們必須為系統在此處先造好一個低階指令格式的解碼欄,然後接著的內容形同一個低階指令的參數欄,才能自由的書寫 FORTH Assembler 。因此,第一列程式就是這個參數欄。如果你對傳統組合語言很熟,一定看得懂組合語言程式中出現了『 * + 2 』時所代表的意義,它也是以前傳統 FORTH 系統(例如: F83 )源程式中,低階指令解碼欄內所設計的內容,它用來告訴實際上的電腦 CPU ,去執行後續 2 個 bytes 偏移量的組合語言指令。在 FORTH 領域的意義,則是指揮低階程式計數器 W 指到此處,以便將低階指令轉交給真實電腦的程式計數器 PC 執行程式, W 在 PC 處理完後續所有低階指令前則不再變化。因此,『 HERE 2+ , 』 就是安放了一個低階格式所須要的解碼欄,至此明焉!

看似簡單的程式仍有另一個變化,由於 eFroth32 是一個直接線串碼(DTC)式的系統,它將低階指令程式計數器 W 的操作機理,設計成指令的解碼欄中,直接就得開始安放組合語言式的程式,因此,此處可以用『 HERE 2+ , 』達到目的。相對於此的另一種系統,稱之為間接線串碼式的系統,如 F83 ,其低階指令解碼欄中放的就是 * + 2 ,我們反而要設計使用兩次同套指令,也就是『 HERE 2+ , HERE 2+ , 』才能達到目的,係因為其系統中 W 的操作機理有所不同的關係,才必須這樣設計。

接下來的部份就很容易看懂了,可以開始使用組合語言寫程式前,當然要宣告系統找字最優先秩序轉到 ASSEMBLER 字彙之下。然後,要特別注意了,在 FORTH 系統觀念中, FORTH 的組合語言指令被編譯時,是以被執行的方式編寫機器碼入記憶體,而非以編譯狀態來編譯。因此,這個程式中要設計許多次狀態的切換,第三列程式中的『 [COMPILE] [ 』就是第一個。『 [C 』當然不能在高階程式格式,冒號定義的外面使用,所以宣告 IMMEDIATE 。

第二個由低階狀況返回高階狀況的指令 C] 重抄重新排列如下:


 
:  C]  [  ASSEMBLER  ]
   HERE  SPLIT  DROP  6  +  #  SI  MOV
   NEXT  FORTH  ]  ;


第一列程式要求系統進入執譯狀態,宣告找字最優先秩序轉到 ASSEMBLER 字彙之下,然後立刻恢復成編譯狀態,這一列是僅為這個 C] 指令自身設計須要而設計的,因為第二列程式就要開始使用組合語言寫程式了。請注意,暫時摒除程式說明不談,轉看另一個狀況,這裡已經出現了高階內混用低階的情況,有一個 MOV 組合語言用指令,但是它卻出現在系統的編譯狀態下,系統此時不是將組合語言指令的機器碼編譯入記憶體,而是將它編譯成 [C 指令的執行內容,此後使用 [C 指令時,系統才會去執行 [C 指令的內容,包括這個 MOV 。正式的高階內用低階的情況不能像此處的用法,否則,每次都得在這個切換位置擺好這三列程式,整個系統也才能維持正常運作,把三列程式縮減成一個 [C 指令是設計它的目的。

回到此指令的程式說明,我們必須先行解釋第三列程式。這個指令的主要工作,是在用完低階設計程式後,要設法回到高階狀態。前一個 [C 指令造好了體部上方的解碼欄,這一個 C] 指令也要造好一個標準低階指令格式參數欄下方最後的尾綴結構,它們是 NEXT 與 END-CODE ,然後才能回到 FORTH 字彙及高階定義指令所須的編譯狀態,第三列程式即完成這項工作。

回頭看第二列程式所為何在?由低階回到高階狀態時,我們必須先調整好高階指令程式計數器 IP 的內容才能回去, CPU 中的 SI 被指定當 IP 使用,必須讓它指到用完低階,接著出現在 C] 後面那一個指令所在的記憶體位址,如此回到高階狀態時,系統才不會出差錯。這個位址隨著系統的不同,要經過核算才能決定, eForth32 是與當時系統的 HERE 相差了 6 個位元組的記憶體偏移量,它指的是各個使用到 C] 的處所,編譯 HERE 前到編譯完 NEXT 後之位址偏移量為 6 ,如果不能確定此量,可以使用 DUMP 指令看編譯後結果來加以確定。

eForth32 中的 NEXT 內容以普通組合語言表示如下:

LODS WORD \ 1 byte
JMP AX \ 2 bytes

而第二列程式中的 HERE SPLIT DROP 6 + # SI MOV 一長串 FORTH 程式,最後會被編譯成單一個低階指令,可用普通組合語言表示如下:

MOV SI, # value \ 3 bytes

這個總共為 6 的偏移量就是這樣核算出來的。 F83 系 統只要設計成 HERE 6 + 便可得到 value 之值。 eForth32 系統在執行程式時,使用 32 位元的執行位址,因此使用 HERE 後會得到 32 位元值,但是此系統在編譯程式的執行碼時,仍然只用 16 位元的執行位址,如此才能放進 16 位元的 CPU 之 SI 內,因此,我們使用了 HERE SPLIT DROP 6 + 來求得上列 value 之值,完成這個指令的設計。

如果你讀完了上述兩個指令的程式說明完全沒有問題,那麼該恭喜你,接下來的兩個指令的程式說明可以直接了解了。我們一次重抄重排兩個指令的程式設計內容如下:



:  C:   [’]  DOCOLON  CALL,
        FORTH  ]  ;
:  ;C   [  ASSEMBLER  ]
        COMPILE  (;C)
        ASSEMBLER  [COMPILER]  [  ;  IMMEDIATE


設計低階指令程式要轉入可用高階指令狀況前,執行 C: ,此指令首先就安排好去執行一個高階指令該有的體部解碼欄,由第一列程式『 [’] DOCOLON CALL, 』完成,高階定義指令的程式環境,需宣告成第一優先找字的字彙為 FORTH ,且系統須為編譯狀態,由第二列程式『 FORTH ] 』完成,此後在這裡使用任何高階定義方式設計程式就沒問題了。

用完了高階定義格式,一定得再回到低階定義格式的環境去,否則系統就亂了,因此執行過 C: ,就一定得再執行 ;C ,讓系統恢復。這個指令只做了類似前述 C] 指令的工作,將一個編完高階指令後該有的尾綴結構要求 (;C) ,也就是 eForth32 中的半冒號指令『 ; 』之核心結構 semi 編進系統。同時,再次強調,別忘了! FORTH 中的 Assembler 被編譯時是以被執行的方式編寫機器碼入系統,而非以編譯狀態來編譯,這就是最後一列程式的說明。

沒有留言: