進階的組譯器功能

為了更進一步說明組譯器的設計原理,並且更詳細的考量各種組譯器設計的進階功能。在本節當中,我們將介紹有關定址格式、常數、定義、程式區塊、控制區塊等主題後,接著使用一個較為複雜的組合語言範例,說明這些進階功能的用途。

定址範圍的問題

由於單一指令的 CPU 定址範圍通常無法涵蓋所有的位址空間,因此,組譯器有時必須自行決定定址格式,甚至在無法正確定址時提示錯誤訊息,以告知程式設計師,讓程式設計師適時的採用特定的定址方式。

舉例而言,在 CPU0 當中,變數的載入與儲存指令 LD 與 ST,通常會直接採用如下寫法。

LD Rx, 變數名稱

例如,範例 4.9當中的 LD R1, B 與 LD R1, A 即是採用此種撰寫方式。

此時,CPU0的組譯器通常會採用相對於程式計數器的方式,以進行定址。因為,指令與變數之間的距離,通常不會超過 LD、ST 指令的極限,也就是 -32768 到 32767 個 bytes。但是,對於某些 CPU而言,其記憶體存取指令的定址範圍,可能會相當的小。例如 ARM7 CPU 的載入儲存指令 LDR與 STR,其位移 (offset)範圍只有 -2048 到 2047。因此,當宣告了一個較大的陣列之後,就可能造成造成定址範圍不足的問題,此時,組譯器就有必要告知程式設計師,採用其他的相對定址方式 (例如,以基底暫存器定址的方式,就能解決此類問題)。

當然,對於 CPU0,雖然記憶體存取指令的定址範圍較大,但是仍有可能會不足,假如,我們將範例 4.2的程式稍微修改一下,成為範例 4.9當中的錯誤版。那麼,這個程式表面上看起來並沒有甚麼問題,但實際上已經發生錯誤了。

由於我們在範例 4.9的行號 3 的指令之後的 3-1 行當中,加入一個巨大的陣列宣告 X RESW 100000,由於該宣告占用了 100000個words,也就是 400000 bytes的記憶體位址空間。因此,LD R1, B 與 B WORD 29 兩個指令間的距離已經超過了 CPU0 的極限 32767,此時,組譯器應該提示錯誤訊息,例如,『Line 3: LD R1, B <— Error, instruction B address out of range.』以便告知程式設計師應該修改程式。

此時,一個有經驗的組合語言程式設計師,就應該會發現這個問題,然後,更改定址模式,不要再用預設相對於程式計數器的方式,改用某個暫存器當作基底,以避免定址範圍超出的問題。舉例而言,程式設計師可以將範例 4.9的錯誤版改寫,改成右邊的修正後版本,該修正後版本利用基底定址法,先將基底值 Base (也就是 A 的位址),載入到暫存器 R9 當中,接著所有的定址都強制相對於 R9 進行,如此,就能避開指令與變數距離過遠的問題了。

範例 4.9存取位址超過可定址範圍的組合語言程式碼

(a) 程式碼 (錯誤版)            ; (b) 程式碼 (修正後版本)
        LD      R1, B          ;           LD     R9, Base   
        ST      R1, A          ;           LD     R1, R9+B-A 
X       RESW    100000         ;           ST     R1, R9+A-A 
A       RESW    1              ; X        RESW    100000    
B       WORD    29             ; A        RESW    1         
                               ; B        WORD    29       
                               ; Base     WORD    &A

常數值的表示法

雖然 CPU0的組譯器採用類似 C 語言的語法,以定義十六進位與字串等常數,但是,大部分的組譯器為求方便起見,會利用特定的字首代表常數。例如,許多組譯器使用了等號作為常數的字首,像是範例 4.10的程式當中,EOF的語法 =C’EOF’ 就是一個字串定義的例子,而 =X’FFFFFF00’ 則定義了 16 進位的常數。

範例 4.10常數值表示法的組合語言程式範例

...
        LD R1, EOF
WLOOP:    ST R1, oDev
...
EOF        BYTE =C’EOF’
oDev    WORD =X’FFFFFF00’

但是,為了能與 C 語言盡可能一致,在本書當中,我們採用了 0xFFFFFF00 這樣的寫法,而沒有用 =X’FFFFFF00’ 這種寫法。其實,組合語言的語法本來就是相當多樣的,組合語言程式設計師必須隨時準備適應新的語法,只要掌握住基本的原則,就能很快的適應新的寫法。

實字 (Literal) – 直接在指令當中使用常數值

有時,程式設計師甚至會希望能直接將常數值寫在指令當中,這可以透過實字 (literal) 來完成,範例 4.11 (a) 當中的 =C’EOF’ 與 =X’FFFFFF00’ 被內嵌於指令當中,就是使用實字的兩個範例。

正式的說,所謂的 literal 就是直接在指令當中寫入常數值,但是,支援實字的組譯器必須自行為這些常數值命名,以便正確的分配與引用記憶體,範例 4.11(b) 當中就顯示了實字被展開後的狀況,組譯器仍然必須建立出每一個實字所對應的變數,因此,實字其實只是提供給程式設計師的一種方便寫法而已。

範例 4.11實字 (Literal) 的組合語言程式範例

(a) 具有實字的組合語言         ; (b) 將實字展開後的結果
        LD R1, =C'EOF'         ;       ...                           
WLOOP:  ST R1, =X'FFFFFF00'    ;               LD R1, $LITERAL1           
                               ; WLOOP:        ST R1, $LITERAL2      
                               ; ...                           
                               ; $LITERAL1:    BYTE =C'EOF'       
                               ; $LITERAL2:    WORD =X'FFFFFF00'

使用實字的時候,通常組譯器通常會將所有實字常數放到程式的最後,這就有可能造成定址時超過範圍。為了避免此種情況,程式師可以用 LTORG 這種指令,要求組譯器提早展開實字常數,範例 4.12就顯示了一個利用 LTORG 的範例,這個範例在巨大陣列 BUFFER 前加上 LTORG 指令,強迫組譯器提早輸出實字常數,以避免 LD, ST 等指令超過定址範圍。

範例 4.12以 LTORG 提早展開實字的範例

(a) 具有實字的組合語言         ; (b) 將實字展開後的結果
        LD R1, =C'EOF'         ;         ...                          
WLOOP:  ST R1, =X'FFFFFF00'    ;            LD R1, $LITERAL1          
        LTORG                  ; WLOOP:     ST R1, $LITERAL2     
BUFFER: RESW 65536             ;            LTORG                     
...                             ; $LITERAL1: BYTE =C'EOF'      
                               ; $LITERAL2: WORD =X'FFFFFF00'
                               ; BUFFER:    RESW 65536

重新定義 – EQU 指令

在 C 語言當中,我們常透過 #define 指令定義常數,以免未來修改該常數時需要將所有程式都修改一次,在進階的組合語言語法當中,通常也會提供類似的常數定義功能,例如,範例 4.13中的第一行,就利用 EQU 這個假指令,定義了 MAXLEN 這個常數為 4096。

範例 4.13使用EQU假指令的組合語言程式片段

MAXLEN    EQU 4096
PC        EQU R15
LR        EQU R14
SP        EQU R13
...
LD R1, #MAXLEN
MOV LR, PC
MOV SP, R1
...

EQU 這個假指令,除了可以定義常數之外,還有其他用途,例如,假使 CPU0 的組譯器並沒有提供 PC、SP、LR 等暫存器的寫法,那麼,就可以利用 EQU 為這些暫存器重新命名,以方便程式設計師記憶與撰寫。範例 4.13中就進行了這樣的重新定義。利用 EQU 指令將 PC 定義為R15,同時也對 R14 與 R13 定義為 LR 與SP。

EQU 除了這些使用方式,也可以用來進行位址上的定義,例如,範例 4.14就利用 EQU 指令,定義出person 記錄當中,欄位 name 與 age 的位址。name EQU person 這個指令定義了 name 欄位的位址,該位址位於 person 記錄的一開頭。而 age EQU person 則定義了 age 這個欄位的位址,該位址位於 person 記錄開始後的第 20 個 byte。如此,就能在組合語言當中,模擬出C 語言的複雜結構 struct 關鍵字的類似效果。

範例 4.14使用EQU進行相對位址模擬 C語言的 struct 結構

(a) 組合語言                    ; (b) C 語言
person:  RESB 24                ; struct person {      
name:    EQU person             ;      char name[20];     
age:     EQU person + 20        ;      int age;           
...                             ; }

另外,還有一個符號可以與 EQU 指令進行搭配,同樣可以達成模擬 struct 結構的功能,這個符號在組合語言當中常用星號 * 或錢字號 $ 表示。這個符號可以用來向組譯器取得目前的記憶體位址。

例如,範例 4.15當中的 person EQU * 指令,就會將目前的記憶體為址記錄在 person 這個符號當中,接著,再利用 name RESB 20 與 age RESW 1 分別保留 person 記錄所需要的欄位空間。這種寫法比起範例 4.14更為精簡易讀,這也是組合語言中常用的一種程式撰寫手法。

範例 4.15使用EQU與星號 * 模擬 C語言的 struct 結構

(a) 組合語言                    ; (b) C 語言
person:  EQU *                  ; struct person { 
name:    RESB 20                ;      char name[20];
age:     RESW 1                 ;      int age;      
...                             ; }

除了使用 EQU 之外,ORG 指令也可以達成類似的功能,但是其用法與意義卻完全不同。

ORG 的功能是用來重新設定組譯器當中的程式計數器 PC 值,強制設定該行的程式計數器為特定數值。舉例而言,範例 4.16就利用 ORG 指令,達成了類似範例 4.14的效果。因為第一個 ORG person 指令強制程式計數器回到 person 的起頭,使得 name 欄位與 person 重疊在一起,而後續的 age 欄位,其位址也就落在 person + 20 上,相當於範例 4.14當中指令 age EQU person + 20 的效果。

範例 4.16使用ORG假指令的組合語言程式片段

(a) 組合語言                   ; (b) C 語言
person  RESB 240               ; struct {        
        ORG person             ;                       
name:   RESB 20                ;  char name[10];
age:    RESW 1                 ;  int age;      
size:   EQU *-person           ;                       
        ORG                    ; } person[10];   
sum:    WORD 0                 ; int sum = 0;    
...

在範例 4.16當中,第二個 ORG 指令,其效果則是讓程式計數器回到原來的位址。如此,後續的 sum 變數才不會與 person 記錄重疊在一起,而造成程式空間分配上的問題。

必須注意的是,範例 4.16當中的第1行 person RESB 240 保留了 240 個 byte 的空間,但是欄位內容 name 與 age 總共只占用了 24 個 byte,此時,我們相當於宣告了 10 個 person 記錄的空間,也就相當於右半部的C 語言中所宣告的 person[10] 之效果,這也是組合語言當中相當常見的技巧之一。

當然,我們也可以直接將第二個 ORG 指令改為ORG person + 240,但是這樣的寫法必須前後一致,否則就會很容易產生錯誤,因此,使用不具參數的 ORG 指令回復到前一個 ORG 之後的位址是比較方便且不容易出錯的寫法。

運算式

在範例 4.16當中,第5行的size EQU *-person是一個可以計算出 person 記錄大小的運算式。因為 * 所代表的是目前的記憶體位址,由於前面有 name RESB 20 與 age RESW 1 這兩個指令讓組譯器前進了 24 個 byte,因此,size 的內容將會是 24。採用這種寫法,可以讓我們輕鬆的在組合語言當中計算出 person結構的大小。

運算式是組合語言當中強有力的程式設計語法,這種語法為組合語言的程式師提供了相當大的方便性,有效的使用 EQU 與運算式式組合語言程式設計師的必備能力。為此,我們再舉一個範例,以便說明運算式與 EQU 的搭配組合方式。

範例 4.17使用運算式計算陣列大小的組合語言程式片段

BUFFER RESB 4096
BUFEND EQU *
BUFLEN EQU BUFEND-BUFFER

範例 4.17使用了並用 BUFEND 記住了BUFFER 緩衝區的結尾,然後用BUFEND-BUFFER 這個運算式,計算出 BUFFER 的大小。

從上面幾個範例可以看出,程式設計師若能善用 ORG, EQU 與運算式,對組合語言的撰寫會有相當大的幫助,因此,一個好的組合語言設計師必須懂得善用這些語法。

程式區塊

在前文介紹實字的時候,我們曾經利用 LTORG 要求組譯器提早輸出實字常數,以避免巨大陣列所造成的定址問題。然而,這往往仍無法完全避開巨大陣列的困擾,在許多組合語言程式當中,巨大陣列仍然經常造成問題。舉例而言,在範例 4.18 (a) 當中,由於巨大陣列 BUFFER 組檔在 LD, ST 指令與 EOF, oDev 之間,這使得 LD, ST 指令無法透過『相對於程式計數器 PC』 的方式定址。這是因為 LD, ST等指令在 CPU0 當中屬於 B 型的格式,其位址欄只有 16 個位元,表示範圍只有 -32768 ~ 32767 而已,但是 BUFFER 的大小是 65536,這使得 LD 指令與 EOF 標記間的距離超過 65536,因此,無法正確定址。

範例 4.18以USE區塊解決巨大陣列所造成的定址問題

(a) 巨大陣列造成定址錯誤       ; (b) 使用 USE 指令處理巨大陣列
        LD R1, EOF             ;           LD R1, EOF           
WLOOP:  ST R1, oDev            ; WLOOP:    ST R1, oDev     
        RET                    ;           RET                  
BUFFER: RESB 65536             ;           USE CBLKS            
EOF:    RESW =C'EOF'           ; BUFFER:   RESB 65536      
oDev:   WORD =X'FFFFFF00'      ;           USE                  
                               ; EOF       RESW =C'EOF'      
                               ; oDev      WORD =X'FFFFFF00'

為了解決範例 4.18 (a)的問題,程式設計師可以自行將巨大陣列移動到程式的最後,但有時這樣做會造成程式閱讀上的困難,甚至在有多個巨大陣列時會難以解決。此時,我們可以使用 USE 指令,將程式分成數個區塊,然後再妥善的管理這些區塊,以避開巨大陣列所造成的定址問題。

在範例 4.18 (b) 當中,我們在 BUFFER 前加上 USE CBLKS 這個指令,然後在 BUFFER 後在加上 USE 指令,恢復到預設的區塊當中。這使得BUFFER在組譯時會形成一個單獨區塊,不會與其他程式混在一起,如此就避開了 BUFFER 過於巨大所造成的問題,這式程式區塊常見的用途之一。

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License