Ch 26 巨集

小木偶最常用的組譯器是微軟出品的 MASM,其中的『ASM』當然是指『assembly』之簡寫,但是您可知道第一個『M』代表什麼意義?可能有許多人以為是微軟 ( Microsoft ) 的第一個字母,不過這是不對的,因為一般對微軟的簡稱是『MS』,所以小木偶以為這個『M』與其代表微軟不如代表巨集 ( macro ) 來得正確。由命名就可以知道使用巨集應該也是相當重要的,所以在這一章堙A小木偶打算介紹如何使用巨集。

何謂巨集?簡單的說是用來簡化撰寫原始程式的一種語法。組合語言比其他高階語言最大的好處是效率與所佔體積小,但是撰寫組合語言程式所花的精力卻遠比高階語言多,而其維護成本也高。原因是組合語言每執行一個動作要數行,甚至數十行、數百行指令才能完成。假如您用 BASIC 寫過程式,要在螢幕上印出『Hellow, world!』這個字串的話,只需寫

PRINT "Hellow, world!"

即可,但用組合語言的話必須這樣寫

message db      'Hellow, world!$'
        mov     ah,9
        mov     dx,offset message
        int     21h

至少需要四行指令才能完成。微軟有鑑於此,於是把他發行的組譯器加入巨集的寫法,使其能簡化原始程式之撰寫,甚至可以和高階語言的寫法很類似。


用巨集寫組合語言程式

底下小木偶將介紹如何簡化?以第一章的 EXAM01.ASM 為例吧!原來的程式碼是:

;***************************************    ;01
code    segment                             ;02.code 段開始位址
        assume  cs:code,ds:code             ;03.假設程式段及資料段
        org     100h                        ;04.可改成 *.COM 檔
;---------------------------------------    ;05.
start:  jmp     begin                       ;06.程式進入點
mes     db      'Hi, I learn assembly.$'    ;07.要印出的訊息
begin:  mov     dx,offset mes               ;08.指向 mes 的位址
        mov     ah,9                        ;09.指定要呼叫的服務號碼
        int     21h                         ;10.呼叫 DOS 服務程式
        mov     ax,4c00h                    ;11.指定要呼叫的服務號碼
        int     21h                         ;12.呼叫 DOS 服務程式
;---------------------------------------    ;13.
code    ends                                ;14.code 段結束
;***************************************    ;15.
        end     start                       ;16.使組譯器知道程式進入點

分析程式碼 (白色部分),可分為兩個步驟,第一步是印出字串,用去三行程式碼,第二步是結束程式,用去兩行。小木偶分別以 display、exit 這兩個巨集代替,程式變成

        page	,132                  ;01 設定頁長與頁寬 ( 註一 )
display macro   string                  ;02 定義 display 巨集
        local   st_addr,dsp_str         ;03 設定標號 dsp_str 為局部標號
        jmp     short dsp_str           ;04 短程跳躍
st_addr db      string,'$'              ;05 由巨集輸入引數決定所定義之字串
dsp_str:                                ;06 定義 dsp_str 標號
        mov     ah,9
        mov     dx,offset st_addr       ;08 取得 st_addr 之位址
        int     21h                     ;09 於螢幕上印出 st_addr 字串
        endm                            ;10 display 巨集結束

exit    macro   exit_code               ;12 定義 exit 巨集
        mov     ah,4ch
        mov     al,exit_code            ;14 使 AL 等於返回碼
        int     21h                     ;15 呼叫 DOS 服務中斷以結束本程式
        endm                            ;16 exit 巨集結束

;***************************************
code    segment
        assume  cs:code,ds:code
        org     100h
;---------------------------------------
start:  display 'Hi, I learn assembly.' ;23 印出字串
        exit    0                       ;24 結束程式
;---------------------------------------
code    ends
;***************************************
        end     start

這樣程式碼 ( 白色部分 ) 只變成兩行,使用 display 及 exit 這兩個巨集,在程式小時不易看出其優點,但是程式大時就會使得原始程式很容易閱讀與修改。要使用巨集,必須先定義巨集,一般巨集可以寫在原始程式堙A也可以寫在巨集程式庫 ( 含入檔 ) 堙A如果直接寫在原始程式堙A一般是在程式最前面先定義其內容。

定義巨集:MACRO 與 ENDM

巨集的內容必須定義在 macro 和 endm 這兩個字之間,其形式像

name    macro   x,y,z……
        ………
        程式碼
        ………
        endm

name 是該巨集的名稱,也就是給程式使用時『呼叫』之用的。x、y、z……等是該巨集的輸入引數,輸入引數可以有一個、或兩個、或三個……直到 MASM 組譯器的符號區被填滿為止,巨集也可以沒有輸入引數,全視需要而定。巨集名稱可以任意取名,但不可用到 MASM 的保留字或 CPU 指令。巨集結束時用 endm 表示,這個『m』即表示巨集之意,要注意的是 endm 之前不可加巨集名稱,組譯器會自動配合。假如您有需要,也可以在巨集中使用另外一個巨集。

巨集展開

原始程式中使用巨集時,直接寫出巨集名稱及其輸入引數即可,就像上述程式的第 23、24 行。當組譯器組譯原始檔時,組譯器會自動以巨集內容取代該行巨集名稱,以巨集名稱後之數值取代引數。像上面的例子,當組譯器看到原始程式的第 24 行,exit 0,由前面定義知道這是一個巨集,於是 exit 0,這一行自動變成

        mov     ah,4ch
        mov     al,quit_code
        int     21h

而 quit_code 被輸入引數,0,取代,於是進一步變成

        mov     ah,4ch
        mov     al,0
        int     21h

像這種取代工作稱之為巨集代換 ( macro subtitution ) 或巨集展開 ( macro expansion )。巨集展開與呼叫副程式並不相同,每一次展開巨集時,組譯器都是把巨集程式碼照抄一遍,只有輸入引數不同而已,所以如果您的程式展開 10 次相同的巨集,其執行檔案增加的大小大約是巨集的 10 倍,當執行到展開的巨集時,程式照著展開的程式碼執行,不轉移流程。而呼叫副程式是使程式流程轉移到副程式處執行,執行完副程式後又返回主程式,故即使您呼叫 10 次相同的副程式,而該副程式只有一段,而且呼叫副程式時必須先使下個指令位址推入堆疊,再移轉至副程式處。

假指令:LOCAL ( 局部 )

好了,exit 巨集已經介紹過了,那 display 巨集裡面多了一個奇怪的假指令 LOCAL 是什麼作用呢?先不管 LOCAL 這個假指令,我們先把 dispaly 巨集展開,看到第 23 行有 display 的輸入引數,string,為一個字串,『Hi, I learn assembly.』,所以該巨集所有的 string 都變成『Hi, I learn assembly.』,再看第 5 行,就變成:

st_addr db      'Hi, I learn assembly.','$'

所以第 23 行展開後變成第 4 行到第 9 行,只是 st_addr 字串變成上述內容了。這樣似乎一切都很完美,應該不需要 local 這個假指令吧?的確,在這個程式堙A有沒有 local 是一樣的,但是如果在這個程式埵A使用一次 display 巨集時,MASM 就會無法組譯,它會告訴您重複定義變數名及標記名,原因是 display 展開時,會產生兩個 st_addr 字串變數及兩個 dip_str 標記。

事實上,您知道,這個字串與標記用什麼名稱其實並不重要,為了解決這個問題,MASM 提供一個 local 假指令,local 表示st_addr、dsp_str 是一個局部變數與局部標記,它的名稱只在本次使用的巨集埵陵纂A當下次再使用這個巨集時,區域變數與局部標記的名稱改變。事實上,MASM 區域變數會依『??NNNN』的順序更換,此處的 NNNN 是由 0000 開始的十六進位整數依次遞增。所以第一次使用 display 時,st_addr 會變為『??0000』,dsp_str 變成『??0001』,第二次使用 display 時,st_addr 變成『??0002』,dsp_str 變成『??0003』……,這樣 display 就能重複使用了。

此外,使用 local 假指令時,還有一點要注意,否則必定會出錯。那就是 local 一定要放在巨集內容的第一行。好吧!底下我們來看看 EXAM03.ASM 組譯時所建立的 EXAM03.LST 檔,巨集展開的樣子。

H:\HomePage\SOURCE>masm exam03 [Enter]
Microsoft (R) Macro Assembler Version 5.00
Copyright (C) Microsoft Corp 1981-1985, 1987.  All rights reserved.

Object filename [exam03.OBJ]: [Enter]
Source listing  [NUL.LST]: exam03 [Enter]  →輸入 LST 檔以觀察巨集
Cross-reference [NUL.CRF]: [Enter]

  50964 + 365948 Bytes symbol space free

      0 Warning Errors
      0 Severe  Errors

H:\HomePage\SOURCE>link exam03; [Enter]

Microsoft (R) Personal Computer Linker  Version 2.40
Copyright (C) Microsoft Corp 1983, 1984, 1985.  All rights reserved.

Warning: no stack segment

H:\HomePage\SOURCE>exe2bin exam03 exam03.com [Enter]

H:\HomePage\SOURCE>dir exam03.* [Enter]

 Volume in drive H is DATA_1
 Volume Serial Number is 0330-08F6
 Directory of H:\HomePage\SOURCE

EXAM03   LST         3,236  02-08-03   0:19 EXAM03.LST
EXAM03   OBJ           107  02-08-03   0:19 EXAM03.OBJ
EXAM03   EXE           805  02-08-03   0:19 EXAM03.EXE
EXAM03   COM            37  02-08-03   0:20 EXAM03.COM
EXAM03   ASM         1,279  02-08-03   0:00 EXAM03.ASM
         5 file(s)          5,464 bytes
         0 dir(s)        3,689.07 MB free

H:\HomePage\SOURCE>

LST 是一個列表檔,屬於純文字檔,您可以用任何一個文書處理器開啟它,並觀察它,會看見下面的程式碼。

   start:  display 'Hi, I learn assembly.'
1          jmp     short ??0001
1  ??0000 db      'Hi, I learn assembly.',
1  ??0001:
1          mov     ah,9
1          mov     dx,offset ??0000
1          int     21h
           exit    0
1          mov     ah,4ch
1          mov     al,0
1          int     21h

黃色的『1』表示巨集展開,注意到紅色部分的變數名與標記名都被更換了。


巨集程式庫

假如您寫了許多巨集程式,那麼您也可以把這些巨集集合起來變成一個純文字檔,這樣就建立起巨集程式庫,當您撰寫新的程式時可以把這個巨集程式庫包含進來。如此一來,您只要寫一次巨集,就可以在許多程式堥洏峞A節省許多煩瑣的工作,除此之外,您的程式看起來會很簡潔,就會像高階語言一樣比較好維護。

事實上,MASM 安裝好後,INCLUDE 子目錄堶情A就提供許多巨集程式庫。巨集程式庫和第 10 章所提到的程式庫是不一樣的,巨集程式庫是由文書處理器編輯而成的純文字檔,提供 MASM 組譯用的,一般副檔名是 *.H、*.INC 或 *.MAC ( H、INC、MAC 分別是 head、include、macro 之意 );而程式庫是副程式的目的檔經由 LIB 編碼並集合起來所製作的二進位檔,是提供給 LINK 連結用的,副檔名是 *.LIB。

假指令:INCLUDE

INCLUDE 假指令就是為了在原始檔塈t括巨集程式檔的,語法是

include 巨集程式檔檔名

這個檔名可以包含磁碟機名、路徑。

假指令:PURGE

假如您的巨集程式檔太大,以致包含了許多巨集,但您可能只用了其中的一部份,而沒用到的部分雖然不會被組譯,但是組譯時會消耗記憶體,以致使 MASM 符號空間不夠用,這時候可以用 purge 假指令使 MASM 忽略它。其語法為

purge   巨集名1, 巨集名2, 巨集名3……

接在 purge 之後的就是要使其不作用的巨集。

修正程式碼與資料交叉安置

EXAM03.ASM 這個程式堙A程式碼與資料互相交叉,為了改善這個缺點,小木偶把 display 巨集加以改良。底下是改良後的巨集程式庫:

display macro   string
        local   str
data    segment para    public  'data'
str     db      string,'$'
data    ends
code    segment para    public  'code'
        mov     ah,9
        mov     dx,offset str
        int     21h
code    ends
endm

exit    macro   quit_code
code    segment para    public  'code'
        mov     al,quit_code
        mov     ah,4ch
        int     21h
code    ends
endm

initial macro
stack   segment stack
        db      128 dup ('stack   ')
stack   ends
data    segment para    public  'data'
data    ends
code    segment para    public  'code'
        assume  cs:code,ds:data
start:  push    ds
        sub     ax,ax
        push    ax
        mov     ax,data
        mov     ds,ax
code    ends
endm

把上述程式存成 MYMAC.INC 巨集程式庫,而原始程式變成:

        include mymac.inc

        initial
        display 'I learn Macro Assembly.'   ;印出『I learn Macro Assembly.』字串
        display <0dh,0ah>                   ;換行
        display 'It is very fun!'           ;印出『It is very fun!』字串
        exit    0

        end     start

小木偶把這個程式取名為 EXAM04.ASM,您看這個程式一點也不像組合語言,倒像是一種高階語言,但是它組譯後可執行檔大卻很小,用 DEBUG 觀察,完完全全是組合語言,這就是小木偶在本章一開始說,巨集簡化了組合語言,使組合語言語法趨向高階語言。不過呢,您得記得,這只是程度上的問題,事實上要用巨集使組合語言能完全像高階語言一樣,還有好大一段距離,還得配合『條件組譯』才能接近這個目的。注意!小木偶只說『接近』而已,並沒有說能完全像高階語言一樣。好吧!底下我們看看這個程式與巨集庫。

在這個巨集堙A小木偶用了底下的一個觀念。當我們在程式中定義一個區段時,可以把這個區段分散開來,只要在合併形式類別名加以適當的定義,連結器就會自動地把這個分散於原始程式各處的區段片段,合成一個連續的區段,請參考第 11 章有關 segment 假指令的說明,以這個程式而言,小木偶把所有分散各處的 data 區段之類別名取名為『data』,code 區段取名為『code』,並且合併形式指定為『public』,表示相同之類別名與區段名合成一連續區段。如下圖,左邊是展開後的原始程式,而右邊是連結器合併後的可執行檔內的程式碼。

stack   segment stack
        db      128 dup ('stack   ')
stack   ends
data    segment para    public  'data'
data    ends
code    segment para    public  'code'
        assume  cs:code,ds:data
start:  push    ds
        sub     ax,ax
        push    ax
        mov     ax,data
        mov     ds,ax
code    ends
data    segment para    public  'data'
??0000  db      'I learn Macro Assembly.','$'
data    ends
code    segment para    public  'code'
        mov     ah,9
        mov     dx,offset ??0000
        int     21h
code    ends
data    segment para    public  'data'
??0001  db      0dh,0ah,'$'
data    ends
code    segment para    public  'code'
        mov     ah,9
        mov     dx,offset ??0001
        int     21h
code    ends
data    segment para    public  'data'
??0002  db      'It is very fun!','$'
data    ends
code    segment para    public  'code'
        mov     ah,9
        mov     dx,offset ??0002
        int     21h
code    ends
code    segment para    public  'code'
        mov     al,0
        mov     ah,4ch
        int     21h
code    ends
stack   segment stack
        db      128 dup ('stack   ')
stack   ends
data    segment para    public  'data'
??0000  db      'I learn Macro Assembly.','$'
??0001  db      0dh,0ah,'$'
??0002  db      'It is very fun!','$'
data    ends
code    segment para    public  'code'
start:  push    ds
        sub     ax,ax
        push    ax
        mov     ax,data
        mov     ds,ax
        mov     ah,9
        mov     dx,offset ??0000
        int     21h
        mov     ah,9
        mov     dx,offset ??0001
        int     21h
        mov     ah,9
        mov     dx,offset ??0002
        int     21h
        mov     al,0
        mov     ah,4ch
        int     21h
code    ends

堆疊段只有一個,不與其他區段合併;程式碼區段分散 5 處,小木偶以黃色表示;資料區段分散 4 處,以藍色表示。連結後,5 處程式碼區段會合併成一個連續的區段,4 處資料區段也會合併成一個連續的區段。

< 與 > 字串運算子

小木偶想,您可能已經猜得著,這對角括號的意思是使在角括號內的所有運算元變成一個字串變數,%、&、;;、! 等特殊字都會失去效力,變成字元,和角括號的其他字變成一個字串。在這個例子堙A假如您用

display 0dh,0ah

的話,結果只有 0dh 被顯示於螢光幕上,這是因為 display 巨集只有一個引數,所以結果並不是我們想要的。底下再介紹幾種在巨集堭`用的文字運算子。

& 取代運算子

& 是強迫使輸入引數取代『&』後面的運算元,而且不管 & 出現在那堙C例如有個巨集是

error_code      macro   x,y
err&x   db      'Error &x : &y'
        endm

若是用下面敘述使用此巨集

error_code      5,<磁碟機無法讀取>

展開後變成

err5    db      'Error 5 : 磁碟機無法讀取'

;; 註解運算子

雙分號之後的文字會被組譯器忽略,並且在 *.LST 檔中展開巨集時不會出現,只有在定義巨集時才出現,因此常在巨集中被用來做為註解。當然,在巨集中您也可以使用『;』做為註解,但是『;』在定義巨集以及展開巨集時都會出現註解。

% 運算式運算子

這個運算子是使其後所接的運算式算出來,使它帶進巨集的引數堙A也就是說,『%』運算子只能用在巨集引數列堙C

! 字元運算子

這個運算子使其後接的一個字元不管是不是特殊字元 ( 例如 <、>、%、&、;、! ) 都看成是一個普通的字元。例如使用上例巨集

error_code      103,<運算元 !> 255 >

展開後變成

err103  db      'Error 103 : 運算元 > 255 '

重複區塊

在撰寫程式時,常常會要定義資料,例如定義英文字母,最直接的方法是:

alpha   db      'ABCDEFGHIJKLMNOPQRSTUVWXYZ'

但是,如果資料太長時,就很不方便,這時可以用『重複區塊』來定義。MASM 提供的重複區塊有三種,不管是那一種重複區塊,都沒有名字,因此不能被程式『呼叫』,一般而言,重複區塊都是直接內嵌於程式中,或以另一個巨集包含起來。

REPT

這是用來建立有規則的重複區塊,有規則的意思是指每次的變化量都是相同的。其語法是:

REPT    數學式
        ……
        敘述
        ……
ENDM

數學式的運算結果必須是一個 16 位元的正數,它代表由 REPT 到 ENDM 之間的重複次數。REPT 實際上在使用時,每一次的情形常常是和上一次不同,因此常常要設一個變數來記錄這個值,而這個變數並不會在記憶體埵據一塊空間,所以稱為『虛擬變數』,事實上巨集引數也是一種虛擬變數。好吧!回過頭來繼續說明 REPT 的使用方法,這個虛擬變數的起始值必須要在 REPT…ENDM 區塊之前先設定,然後在區塊中加以改變這個虛擬變數之值。

= 假指令

用來設定這個虛擬變數的指令是『=』,他和 EQU 假指令不同的地方是,EQU 所設定之直是常數,無法再更改﹔而用『=』所指定的變數是還可以再更改的,這兩個假指令相同處便是該變數都不會佔據記憶體。= 的語法是

變數名  =        數學式

底下這個巨集是用來在記憶體中定義 41H、42H、43H、44H……5AH,也就是

        db      'ABCDEFGHIJKLMNOPQRSTUVWXYZ'

當然上面是最直接的方法,但也可以用下面的方法

x       =       0
        REPT    26
        db      'A'+x
x       =       x+1
        ENDM

小木偶把上述重複區塊寫成一個完整的程式:

        include mymac.inc

alpha   MACRO
data    segment para    public  'data'
x       =       0               ;05 設定虛擬變數起始值
letters EQU     THIS BYTE       ;06 定義'ABC…Z'字串之位址
        REPT    26              ;07 共有 26 個英文字母
        db      'A'+x           ;08 定義每一個英文字母
x       =       x+1             ;09 設定下一次之虛擬變數
        ENDM                    ;10 重複區塊結束
        db      '$'             ;11 'ABC…Z'字串結束
data    ends
        ENDM                    ;13 alpha 巨集結束

print   MACRO   string
code    segment para    public  'code'
        mov     ah,9
        mov     dx,offset string
        int     21h
code    ends
        ENDM

        initial
        alpha
        print   letters
        exit    0
        end     start

這個程式在組譯與連結後,執行時會在螢幕上印出『ABCDEFGHIJKLMNOPQRSTUVWXYZ』26 個英文字母。

IRP 假指令

IRP 被稱為不定重複 ( indefinite repeat ),代表每次重複的虛擬變數並不規則,其語法是

IRP     參數,<引數1, 引數2, 引數3……>
        ………
        敘述
        ………
ENDM

當 MASM 遇到 IRP 假指令時,首先會以『引數1』之值代入參數堬桫間A一直遇到 ENDM 時,再以『引數2』代入參數組譯……如此重複直到所有的引數結束為止。如此一來,每一次組譯情形因為輸入引數可以由設計者自由決定,所以可以是沒有規則的。

IRPC 假指令

語法為

IRPC    參數,字串
        ………
        敘述
        ………
ENDM

當 MASM 遇見 IRPC 區塊時,首先會以字串的第一個字元代入參數中組譯其後的敘述,直到遇見 ENDM,然後再以字串的第二個字元代入參數中組譯……,直到字串中的所有字元都處理過。它常常用來檢查字串堿O否含有某些字元。例如底下的程式片段:

IRPC    letter,ABCDE
        db      '&letter'
        db      '&letter'+20h
ENDM

其實就是

        db      'ABCDE'
        db      'abcde'

有關 IRP、IRPC 還有更有用的例子,請看下一章的通用推入堆疊巨集


註一:

PAGE 假指令

PAGE 是一個假指令,其作用控制列表檔的長度以及寬度。組合語言的列表檔每一行長度常常超過 80 個字母,但早期螢幕的文字模式每一行最多只能有 80 個英文字母,因此多出來的常常會到下一行,不易觀察。而列表機可以達 132 個字母,比較方便,因此用 page 來控制列表檔的長度以及寬度,語法如下:

page    [長度][,寬度]

長度必須在 10 到 255 十進位整數範圍內,如果沒設定長度,內定值是 50。寬度要在 80 到 132 十進位整數範圍內,如果沒設寬度,內定值是 80。假如只設定寬度而不設定長度,必須在前面加上『,』否則 MASM 會誤以為是長度。假如長度與寬度都省略,則會強迫跳頁。


回到首頁到第二十五章到第二十七章