Ch 31 MASM 6.x 新增指令

雖然 MASM 6.0 已經出廠販售很久了,但是小木偶一直使用 MASM 5.0 版,主要原因是 MASM 5.0 有較多的自由 ( 也有可能是我才疏學淺,不知如何使用 ),但是卻也不可忽略 MASM 6.x 所帶來的好處。

這一章將介紹 MASM 6.0 以後的版本所新增的假指令,這些假指令對組合語言的『高階化』( 就是能把判斷、迴圈、副程式等做更有效的安排 ) 小有幫助。這一章的內容是參考微軟的 MASM 6.1 手冊及 Glow Glove 大大多年前在九○網所發佈的『MASM 6.x 新增指令列表』,特別感謝 Glow Glove 大大。


呼叫副程式

在組合語言中,一般呼叫副程式是用暫存器傳遞參數,但是暫存器就那麼幾個而且又要負責計算,常常不夠用,實在很不方便。於是 MASM 6.0 以後加入了幾個特殊的假指令,使得程式可以使用堆疊傳遞參數。其實在高階語言或早期的 MASM 版本,就已經可以利用堆疊傳遞參數,只是到了 MASM 6.0 以後,又更加便利。我們先來看看早期利用堆疊傳遞參數給副程式的方法。

用 BP 暫存器存取參數與 RET n 指令返回

要知道堆疊如何傳遞參數,當然得先了解堆疊,有關堆疊的原理請參考第五章,此處不細說,直接進入主題。

當我們想利用堆疊傳遞參數時,一般是先把參數推入堆疊,副程式要用幾個參數就推入幾個參數到堆疊堙A然後再用 CALL 指令呼叫副程式。此時堆疊堛熙怬C位址,便存有主程式的返回位址,而 SP 暫存器指向此返回位址,傳入副程式的參數便在此返回位址較高的幾個字組處。

好了,進入副程式後,副程式如何取得這些參數呢?一般是利用 BP 暫存器當成基底 ( 或者想成參考點 ),不過為了不破壞 BP 原先的內容,所以一般會先把 BP 保存在堆疊堙A再使 BP 指向堆疊最低位址,也就是和 SP 指到一樣的地方,因此副程式一開始都是下面這兩個指令:

        PUSH    BP
        MOV     BP,SP

這樣如果要存取參數時,就利用 BP+n 所指的位址存取參數,例如最後推入堆疊的參數便在 BP+4 的位址處,倒數第二推入堆疊的參數,便在 BP+6 的位址處。

舉一個例子說明吧!假設有一個副程式稱為 day_of_week,它是計算西元某年某月某日是星期幾的副程式,那麼顯然它需要三個參數,年、月、日,而計算後,AX 暫存器傳回該日是星期幾。部份程式碼與每步執行後,堆疊變化如下:

;-------------------------------------------------
;day_of_week 副程式由此開始
day_of_week     proc
        push    bp          ;保存 BP
        mov     bp,sp       ;使 BP 當成參考點,各參數變成 BP+n
        ……    ……
        mov     ax,[bp+4]   ;AX=年
        ……    ……
        mov     bx,[bp+6]   ;BX=月
        ……    ……
        pop     bp
        ret     6
day_of_week     endp
;-------------------------------------------------
;主程式由此進入
        ……    ……
        push    day         ;把『日』推入堆疊
        push    month       ;把『月』推入堆疊
        push    year        ;把『年』推入堆疊
        call    day_of_week
        ……    ……

堆疊傳遞參數
上圖一是參數尚未推入堆疊時的情形,亦即 push day 還沒有被 CPU 執行,此時 SP 指向已使用堆疊的最低位址 ( SP 所指的位址是已經被使用過的了,下一較低字組的位址 SP-2 便是空的堆疊 )。當要執行『push day』指令時,SP 會減二以指向尚未使用最高堆疊位址,此時 SP-2 便是空的堆疊,然後再把 day 推入這個位址,如圖二。在連續推入 month、year 後,堆疊內的情形如圖四。而後下一指令是 call day_of_week 進入副程式,此時 CPU 會把 call day_of_week 的下一條指令位址,也就是返回位址推入堆疊,如圖五。進入副程式後的第一條指令是保存原來的 BP,做法就是把 BP 推入堆疊,如圖六。接下來是使 BP 等於 SP,所以執行完這條指令後,BP 就指向存放原 BP 的堆疊位址,圖六右邊淡藍色的位址,BP、BP+2、BP+3……便是,換句話說,就是以 BP 參考點。

可能有人會問為何還要另選 BP 當做參考點,直接使用 SP 不是更簡便嗎?其實這樣子的做法並不恰當,因為 SP 一般都是指向已使用的堆疊最低位址,隨時可能改變,例如硬體中斷時也可能改變 SP,所以得另選 BP 做為參考點。基於上述理由,使 BP 等於 SP 以後,BP 就當做參考點,我們可以用這個參考點為基準去存取各個參數。例如要取得 day,就可用 [BP+8] 取得,要取得 year 就可用 [BP+4] 取得。

當結束副程式時,第一步便是由堆疊取回原來的 BP 值,因此在副程式尾端得加上一行 pop bp。接下來就要執行 RET 指令返回原來的主程式,這個指令會自 SP 所指的堆疊位址處取回主程式位址,然後把執行權交還主程式;但是僅僅這樣還不夠,還得把原先推入堆疊堛滌捊き丳鞳A這時可以用 ret n 指令。

RET 這個指令的完整格式是 RET n,n 可有可無,如果沒有 n 的話,CPU 就只從堆疊取出返回位址,且讓 SP 再加 2,恢復呼叫副程式之前的數值;如果 n 沒有省略,那麼 CPU 除了使 SP 再加 2 之外並從堆疊取出返回位址之外,還會再使 SP 加上 n 這個數,亦即捨棄堆疊堛滬Y干資料。

MASM 6.x 的做法

以上是舊版 MASM 利用堆疊傳入參數來呼叫副程式的用法。由上面,您應當可以了解如果您算錯了參數個數,或者多一個步驟又或少一個步驟,那麼極可能造成無法真正返回到主程式位址,而造成當機。所以在 MASM 6.x 新增加一些假指令,幫助我們於撰寫原始碼時簡化這些步驟,避免一些不必要的錯誤。首先要談的是 PROC/ENDP 這對假指令,MASM 6.x 對它們做了一些修改。

PROC/ENDP 假指令:定義副程式

所提,PROC/ENDP 假指令是用來定義副程式,在 MASM 6.x 還新增了許多功能,較完整的語法是;

副程式名稱 proc [位移][程式語言][可視區域][USES 暫存器列表][,
                參數一[:類型][,參數二:[類型]... ]]
           ;副程式內容
副程式名稱 endp

說明如下:

副程式名稱: 以 CALL 呼叫時,其後所接的名稱,其實是一個標記。
位移: 可用 near 或 far。
 near:表示副程式與主程式在同一區段內。
 far:表示副程式與主程式在不同區段內。
程式語言: 是用來約定呼叫方式及堆疊存放方式,可以用 BASIC、C、FORTRAN、PASCAL、STDCALL、SYSCALL,其中 BASIC、FORTRAN、PASCAL 所代表的意義相同,因此總共可分為四種:
C: 呼叫時,由最右邊的參數開始推入堆疊;返回時,由主程式清除堆疊堛滌捊ヾC
PASCAL: 呼叫時,由最左邊的參數開始推入堆疊;返回時,由副程式清除堆疊堛滌捊ヾC
SYSCALL: 呼叫時,由最右邊的參數開始推入堆疊;返回時,由副程式清除堆疊堛滌捊ヾC
STDCALL: 呼叫時,由最右邊的參數開始推入堆疊;一般返回時,由副程式清除堆疊堛滌捊ヾA但是當資料型態是 VARARG 時,必須由主程式清除堆疊堛滌捊ヾC
程式語言可以省略,如果省略時,則由 .MODEL 中指定 ( 見稍後說明 )。
USES 暫存器列表: 組譯器自動的在進入副程式時,加上 PUSH 指令,把在 USES 之後的暫存器推入堆疊保存,並在 RET 指令返回主程式之前,POP 這些暫存器。
可視區域: 可視區域可以是 PRIVATE、PUBLIC、EXPORT 三種,因編寫大程式時,可能是由許多 OBJ、LIB 連結而成,因此有這個選項。
 PRIVATE:表示只有這個模組可用,亦即只有此 OBJ 內的程式可呼叫。
 PUBLIC:表示所有模組均可呼叫此副程式。
參數與資料型態: 參數必須和前面的位移、程式語言…等隔一個『,』,如果參數不只一個,每個參數之間也要以『,』分開。如果參數太多,也可以換行,這時候,『,』必須在上一行的最後面,否則會無法組譯成功。參數是指在進入副程式之後,所使用的變數名稱,也就是說,在副程式中須以參數名稱來表示。參數之後得接上這個參數的資料形態,常用的資料型態有 BYTE、WORD、DWORD、QWORD、TBYTE 等,分別代表一個位元組、一個字組、雙字組、四字組 ( 8 個位元組 )、10 個位元組。參數與資料型態之間以『:』相連,資料形態用來表示這個參數佔用多少個位元組,雖然在 DOS 程式中,每次 PUSH 都是一個字組,但是我們不須操心會不會出問題,因為組譯器會自動地把不是一個字組的參數改成分次推入堆疊,或加長成一字組再推入堆疊。另外,資料型態也可以用 VARARG,表示此副程式的參數不固定,必須由主程式呼叫的當時來決定。

副程式結束之處以 ENDP 表示,MASM 6.x 在 ENDP 假指令之前,會自動的依據 PROC 之後的位移決定用近程或遠程返回主程式,同時當我們用 PASCAL、SYSCALL、STDCALL 也會自動的加上 POP BP,恢復原來的 BP,以及 RET N,捨棄在堆疊堛滌捊ヾC

PROTO 假指令:宣告副程式原型

PROTO 是用來宣告副程式原型的指令,所謂宣告原型的意思是告訴組譯器這個副程式的性質,例如要用那些參數,這些參數的資料型態是什麼,堆疊存入方式等等。一般如果在原始碼的前面用 PROC/ENDP 已定義副程式,而主程式在原始碼後面才呼叫副程式,那麼可以不須用 PROTO 宣告副程式原型。如果原始碼在呼叫副程式之前還沒有用 PROC/ENDP 定義副程式或是呼叫外部程式庫,那麼就要用 PROTO 須告副程式原型。當我們修改副程式時,如果曾經宣告,那麼 PROTO 和 PROC 都必須同時修改,否則組譯器就會無所適從。所以建議,如果能把副程式寫在前面,盡量省略 PROTO。

PROTO 的語法如下:

副程式名稱 proto [位移][程式語言][可視區域][,
                參數一[:類型][,參數二:[類型]... ]]

您應該可看得出來, proto 的語法和 proc 幾乎一樣,只刪除了暫存器列表等小部份。

INVOKE 假指令:呼叫副程式

INVOKE 英文是召喚之意,在組合語言中便是呼叫副程式。其語法是:

        invoke  副程式名稱[,參數一[,參數二]……]

INVOKE 是 MASM 6.x 中新的假指令,它會被組譯器『翻譯』成數個 80x86 指令

        push    參數 n
        push    參數 n-1
        push    …………
        push    參數二
        push    參數一
        call    副程式名稱

上式中,有多少個參數,就會被翻譯成多少個 push 指令,最後再用 call 呼叫副程式。不過是最右邊的參數先推入堆疊,還是參數一先推入堆疊,得依據 PROC 或 PROTO 的程式語言選項決定。

一個副程式可能會被主程式呼叫好幾次,每次呼叫時傳入的參數都可能 不一樣,因此這些接在 INVOKE 之後參數的名稱和在 PROC 之後定義的參數名稱不能同名,否則會使得組譯器弄不清楚。

LOCAL:定義區域變數

副程式中常常會做大量計算,如果暫存器不夠就得用到一些變數,假如除了這個副程式使用這些變數以外,其餘程式都用不著這些變數,那麼在資料區段堜w義這個變數,不是最好的做法。因為這樣會造成維護上的麻煩,當您想把這個副程式變成程式庫,在資料區段的變數也是一大問題。最好的方法是把這種只有在副程式中才存取的變數 ( 稱為區域變數 ) 定義在堆疊段堙A這樣就能把副程式封裝起來,以達結構化的目的。

在 MASM 6.x 的版本,提供了一個簡單定義區域變數的方法,那就是使用 LOCAL 假指令,LOCAL 的語法是

        LOCAL   變數名[[重複次數]]:資料型態 [,變數名[[重複次數]]:資料型態……]

一個 LOCAL 假指令可以定義數個區域變數,當然也可以分開來定義,例如下面兩個例子:

        LOCAL   a:WORD
        LOCAL   key_buffer[16]:BYTE

第一個例子是定義一個字組長度的區域變數,a。第二個例子是定義一個長 16 位元組的區域變數,key_buffer,這個區域變數是一個陣列,這個陣列有 16 個元素,每個元素長一個位元組。當組譯器遇到 LOCAL 時,組譯器會在堆疊中留下一塊空間,容納所有的區域變數。要在堆疊中留下一塊空間,只需要使 SP 減掉所有區域變數的長度之和。而程式設計師所要做的,就是和使用變數一樣的方式使用區域變數,也就是以區域變數名稱當成位址存取其內容,例如:

        mov     a,ax

原始碼中如果要存取區域變數也不需要在區域變數之前加上 SS:,因為組譯器會把區域變數變成堆疊上的位址,此位址也是以 BP 為基準,而以 BP 為基準的位址,在 80x86 指令中的內定區段暫存器就是 SS 段暫存器。可能您會問,這樣會造成堆疊改變,那麼在返回主程式會不會有問題?這您大可放心,因為組譯器會自動的在副程式結束和捨棄參數之前,加上下面這條指令

        mov     sp,bp

使 SP 恢復成剛進入副程式時候的樣子 ( 還記得吧?剛進入副程式時,會有一條 mov bp,sp,亦即使 BP 保存 SP ),然後返回主程式及捨棄參數。小木偶實際舉一個例子來說明堆疊如何存放區域變數。如同上面的 day_of_week 副程式,假設其內有兩個區域變數,a、century,其型態都是 WORD。那麼把部份原始碼改寫成適合 MASM 6.x 的新功能:


;-------------------------------------------------
day_of_week     proc    y:WORD,m:WORD,d:WORD
        local   a:WORD
        local   century:WORD
        ……    ……
        sub     bx,a
        mov     y,bx
        mov     century,ax
        ……    ……
        ret
day_of_week     endp
;-------------------------------------------------
        ……    ……
        ;主程式
        ……    ……
        invoke  day_of_week,year,month,day

這段程式,會被組譯器變成:

day_of_week:
        push    bp
        mov     bp,sp       ;*
        add     sp,-04
        ……  ……
        sub     bx,[bp-02]  ;BX 減去區域變數 a
        mov     [bp+04],bx  ;參數 year 等於 BX
        mov     [bp-04],ax
        ……    ……
        mov     sp,bp       ;開始離開副程式
        pop     bp
        ret     0006
        ……    ……
        push    day
        push    month
        push    year
        call    day_of_week

上面淡藍色的部份是 day_of_week 副程式,前面我們已經討論過參數如何存放在堆疊堙A讓我們繼續看下去:

堆疊中的區域變數
當進入副程式,並且設定 BP 為基準後 ( 亦即已執行完有『*』註記的那條指令後 ),因為副程式定義了兩個字組的區域變數,兩個字組共佔用 4 個位元組,故使 SP 減少 4,空下兩個字組,分別作為 a、century 的存放空間,如圖七、八,到此進入副程式的一切手續都已完備。如果在副程式中,想要存取區域變數 a,那麼就用 [BP-2] 表示,要存取參數 year,就用 [BP+4] 表示,這可以很清楚的從圖上及組譯後的情形對照就得到。

圖九是要離開副程式返回主程式時的情形,首先是執行 mov sp,bp,此時 BP 值指向原 BP 值的堆疊位址,參考上圖最左邊淡藍色的 BP,BP-2 等,可得知 BP 之值。當執行完 mov sp,bp 後,SP 便指向原 BP 值的堆疊位址,如圖十,此時便已捨棄了區域變數。接下來執行 pop bp,由堆疊取回原 BP 值,如圖十一。這兩步可以用一條 LEAVE 指令代替。

雖然使用區域變數的方法和一般變數幾乎沒什麼兩樣,但是如果要取得區域變數的位址時,就不能直接用

        mov     si,offset key_buffer

了。因為我們在撰寫原始碼時,無法確知進入副程式後,堆疊到底使用了多少 ( 因為有可能在副程式中呼叫副程式 ),所以無法用 OFFSET 來取得區域變數的位址。但是底下有個變通的辦法。

LEA 指令

LEA 是 80x86 CPU 指令集的一員,在 8086 CPU 就已經有這個指令,這個指令是用來取得變數的位址,其語法是

        LEA     暫存器,變數名

這個指令和 OFFSET 假指令的功能幾乎一樣,但是它是在 CPU 執行時,才取得變數的位址;而 OFFSET 則是在組譯時就取得位址。因此 LEA 可以用在變數位址可能會改變的情形,例如取得區域變數的位址。

ADDR 假指令:取得區域變數位址

ADDR 假指令的語法是

        ADDR 變數名

ADDR 假指令可以取得變數的位址,但一般只配合 INVOKE 假指令中使用。如果是要取得區域變數位址,組譯器會把 ADDR『翻譯』成

        lea     ax,變數名
        push    ax

如果變數的位址在組譯時已確定,組譯器會把他看成 OFFSET 變數名。

LEAVE 指令

LEAVE 指令是 80X86 CPU 指令集的一個指令,它先使 SP 暫存器設定為 BP 之值,然後再彈出一個堆疊數值存於 BP。常用於返回主程式時把 SP 之值設定為正確值。在 32 位元的作業系統中,例如 Win32 ,也可以使用,這時使 ESP 暫存器設定為 EBP 之值,然後再彈出一個堆疊數值存於 EBP,所不同的是堆疊一個數值長度為雙字組 ( 32 個位元 )。


高階程式控制流程

MASM 6.x 除了更方便的呼叫副程式、使用區域變數之外,還添加了更方便的程式流程。在低階版本的 MASM 要控制流程,不外乎一大堆 cmp、jz、ja、jb 等,不但可讀性低,也難以維護。但是現在不同了,MASM 加上了好幾種控制流程,在 Win32 的組合語言第四章也介紹過一點,可供參考。

@@ 標記與跳躍 @F 或 @B

除了高階的流程控制之外,MASM 6.x 還接受一種特殊標記,@@:。這種標記是做為跳躍指令的目的地,要用 @@: 做為目的地的跳躍指令,必須配合 @f 或 @b 使用,前者是指往前跳躍,後者是往後跳躍,它們都只向前或向後跳躍到第一個 @@: 標記處。

例如有一個程式片段,是用來使一個佔用 16 個位元組的 key_buffer 陣列所有元素填上 0,一般會這樣寫:

        mov     cx,16
        lea     si,key_buffer
next:   mov     byte ptr ss:[si],0
        inc     si
        loop    next

如果 next: 標記只有此處用到,那麼便可以用 @@: 代替,變成下面的程式:

        mov     cx,16
        lea     si,key_buffer
@@:     mov     byte ptr ss:[si],0
        inc     si
        loop    @b

高階假指令︰程式流程控制

在第 26 章巨集與第 27 章條件組譯兩章堙A曾經提到 IF/ELSE/ENDIF 假指令,這些指令前沒有小數點,而且在意義上和底下小木偶將介紹的三種高階流程控制完全不同,故在撰寫程式時,千萬不可搞混。事實上,實際編寫程式時,底下要介紹的高階流程控制比條件組譯更常用到。

.WHILE/.ENDW 假指令

先來看看它的語法︰

.WHILE  判斷式
        程式
.ENDW

這個 .WHILE/.ENDW 迴圈的執行過程如下︰當程式執行到 .WHILE 時,會檢查判斷式是否為真,假如為真,則執行 .WHILE 與 .ENDW 之間的程式,直到遇到 .ENDW 時,再回到開頭 .WHILE 處檢查判斷式是否為真,若為真時,再度執行 .WHILE 與 .ENDW 之間的程式,若為假,則跳到 .ENDW 下一行程式執行。所以看起來,.WHILE/.ENDW 就像是 C/C++ 的 WHILE {} 迴圈一樣。

至於判斷式的模樣,大致可分為兩種︰

  暫存器或變數 邏輯運算子 暫存器或數值
  暫存器或變數或數值

上面那一種情形時,運算子左右兩邊可以同時為暫存器,但不能同時為變數,這是因為在 80X86 指令堙A可以有 cmp ax,bx,但沒有 cmp a,b 這樣的指令。假如為下面那一種情形時,暫存器、變數、數值為非零時,會被組譯器認為『真』;為零時,被認為『偽』。

常用的邏輯運算子如下表︰

運算子 描述 運算子 描述
==等於 !=不等於
>大於 >=大於等於
<小於 <=小於等於
& ¦
!

除了這些邏輯運算子之外,兩個判斷式之間也可以做『或』與『且』的邏輯運算,這時『或』要用¦¦,『且』要用&&,來連接兩個判斷式,並且這兩個判斷式應該用兩對小括號括起來。例如,底下的例子是當 AX 大於 SI 且 BX 大於 DI 時,使 CX 增加一,並使 BX 減一,直到上述條件不成立時︰

.WHILE  (ax>si)&&(bx>di)
        inc     cx
        dec     bx
.ENDW

底下的例子是當 AX 不等於零時,把 BX 所指位址之數值移入 AX,直到 AX 等於零時停止︰

.WHILE  AX
        mov     ax,[bx]
        inc     bx
        inc     bx
.ENDW

.IF/.ESLEIF/.ELSE/.ENDIF 假指令

最簡單的情形是︰

.IF     判斷式
        程式一
.ELSE
        程式二
.ENDIF

這應該很容易了解,當判斷式為『真』時執行程式一堛澈令;為『偽』時就執行程式二堛澈令,假如為『偽』時不用執行任何指令,則 .ELSE 以及程式二可以刪除。底下是一個比較複雜的例子,

.IF     判斷式一
        程式一
.ELSEIF 判斷式二
        程式二
.ELSEIF 判斷式三
        程式三
.ELSE
        程式四
.ENDIF

這是分支指令,假如判斷式一為真,則執行程式一的指令;假如判斷式二為真,則執行程式二的指令;假如判斷式三為真,則執行程式三的指令……,假如都不是上面的情形,則執行 .ELSE 之後的程式。換句話說,.IF/.ESLEIF/.ELSE/.ENDIF 很有彈性,您可以依需要,省卻 .ELSEIF 或省卻 .ELSE,也可以視情況增加 .ELSEIF 條件分支,不過不論那一種情形,.IF 和 .ENDIF 必定是搭配在一起,並且在條件跳躍的最前與最後。

最後還有一個問題。.IF/.ESLEIF/.ELSE/.ENDIF 是 80x86 指令集的一員嗎?答案當然不是,他們只是假指令,組譯器會自動地把它變成適當的指令。例如下面這個程式:

.if cx==7
        mov     bx,0ch
.else
        mov     bx,0ah
.endif

會被組譯器組譯成底下這段程式:

        CMP    CX,+07
        JNZ    N1
        MOV    BX,000C
        JMP    N2
N1:     MOV    BX,000A
N2:

.IF/ENDIF 判斷式也可以用旗標表示。例如 ZERO?、CARRY?、OVERFLOW?、SIGN? 或 PARITY?,分別表示零旗標、進位旗標、溢位旗標、符號旗標或同位旗標被設定時為真,例如下面的例子是零旗標被設定時,則使 AX 增加一:

.IF ZERO?
        inc     ax
.ENDIF

用旗標作為判斷式,不僅 .IF/.ENDIF 可以,.WHILE/.ENDW 或底下將要介紹的 .REPEAT/.UNTIL 也都可以如此。

.REPEAT/.UNTIL

這一對假指令和 C/C++ 的 DO 迴圈一樣,在 MASM 堙A.REPEAT/.UNTIL 的語法是:

.REPEAT
        程式
.UNTIL  判斷式

這段程式碼會一直重複在 .REPEAT 到 .UNTIL 之間的程式,直到判斷式為真為止。.REPEAT/.UNTIL 與 .WHILE/.ENDW 很像,但稍有不同。.REPEAT/.UNTIL 會先執行其中的程式一次,然後再判斷 .UNIT 後的真偽,以決定是否重複再執行一次;而 .WHILE/.ENDW 則會先判斷 .WHILE 後的真偽,決定是否執行其內的程式。

判斷式中的有號數

上述三種條件跳躍或迴圈,都需要判斷式,在內定情形下,MASM 都會把在判斷式中的變數或暫存器當成正數 ( 無號數 ) 來作判斷。但是如果要使變數或暫存器當成有號數來判斷時,可以在前面加上 SBYTE PTR、SWORD PTR、SDWORD PTR 強迫使其變成有號數。例如下面的例子是判斷 AX 是否大於零,如果不是那麼每次加上 7,直到 AX 大於零為止:

        mov     bx,7
.while  sword ptr ax<0  ;若 AX<0,加上 7 直到 AX>0
        add     ax,bx
.endw

很明顯的,SBYTE PTR、SWORD PTR、SDWORD PTR 這些資料形態之前冠以『S』,這『S』表示符號 ( sign ) 之意。

.BREAK .IF 與 .CONTINUE .IF

在 .WHILE/.ENDW 或 .REPEAT/.UNTIL 迴圈堙A如果有其他情形也要退出迴圈時,也可以加上 .BREAK .IF 假指令,強制退出迴圈,表示只有在 .IF 之後的條件為『真』時,才退出迴圈。例如︰

.WHILE 1
        lodsw
.BREAK  .IF     !AX
        add     ax,bx
.ENDW

上面這個例子是把 SI 所指的記憶體數值移入 AX 中,同時 SI 自動指向下一位址,並使 BX 之數值與 AX 相加,再存入 AX 堙A直到 AX 等於零時退出 .WHILE/.ENDW 迴圈。

.CONTINUE .IF 則是在 .IF 之後的條件為真時,會跳到 .WHILE/.ENDW 或 .REPEAT/.UNTIL 迴圈開始處執行。例如下面是一個輸入數字字串的程式,當使用者不是按 0∼9 的阿拉伯數字時,會跳回迴圈起始處,繼續等待使用者輸入數字;如果使用者按下 Enter 鍵 ( ASCII 碼為 0DH ) 或 Esc 鍵 ( 掃描碼是 01 ) 時,跳出迴圈。

.REPEAT
        mov     ah,0
        int     16h
.BREAK .IF al==0dh
.CONTINUE .IF (al<='0')||(al>='9')
        stosb
.UNTIL ah==1

範例:CALENDAR.ASM

照例,小木偶寫個程式,當做是這章的範例。這個程式稱為 CALENDAR.ASM,它是顯示西元某年某月的月曆程式。欲達此目的,最有趣的一個問題是計算這個月的第一天是星期幾,有關這個問題的原理,請參考 Calvin Shing 的天文曆法網站,Calvin Shing 的網站有詳細的說明,小木偶就不再描述了。

CALENDAR.ASM 會用到四個副程式,分別是用來在螢幕某處印出字串的 print_string 副程式、在螢幕某處印出一個字元的 print_char 副程式、可以輸入 64 位元整數的 input 副程式與印出 AL 暫存器的內容。這四個副程式很有用,因此小木偶把它獨立出來,變成 STR_NUM.ASM,然後再加入我們原有的程式庫 MYASMLIB.LIB 堙C另外在螢幕上顯示的文字顏色,像是藍字黑底用 09H ( 請參考附錄七 ),這些數值不易一目了然,因此可以用一常數表示,而常數名則取為 blue_on_black,就可望文生義了,小木偶把這一部份變成一個 MYASMINC.INC 含入檔。此外各外部副程式的宣告,也可放入含入檔中,這樣也可避免副程式庫修改時,凡是用到該副程式的原始檔也跟著相改。

原始碼

底下是含入檔 MYASMINC.INC 的原始碼:

black_on_black   equ    000h    ;黑底黑字
navy_on_black    equ    001h    ;黑底深藍字
green_on_black   equ    002h    ;黑底深綠字
cyan_on_black    equ    003h    ;黑底青字
maroon_on_black  equ    004h    ;黑底紅字
purple_on_black  equ    005h    ;黑底紫字
brown_on_black   equ    006h    ;黑底棕字
silver_on_black  equ    007h    ;黑底銀字
gray_on_black    equ    008h    ;黑底灰字
blue_on_black    equ    009h    ;黑底藍字
lime_on_black    equ    00ah    ;黑底亮綠字
aqua_on_black    equ    00bh    ;黑底亮青字
red_on_black     equ    00ch    ;黑底亮紅字
fushisa_on_black equ    00dh    ;黑底粉紅字
yellow_on_black  equ    00eh    ;黑底亮黃字
white_on_black   equ    00fh    ;黑底白字
black_on_navy    equ    010h    ;深藍底黑字
navy_on_navy     equ    011h    ;深藍底深藍字
green_on_navy    equ    012h    ;深藍底深綠字
cyan_on_navy     equ    013h    ;深藍底青字
maroon_on_navy   equ    014h    ;深藍底紅字
purple_on_navy   equ    015h    ;深藍底紫字
brown_on_navy    equ    016h    ;深藍底棕字
silver_on_navy   equ    017h    ;深藍底銀字
gray_on_navy     equ    018h    ;深藍底灰字
blue_on_navy     equ    019h    ;深藍底藍字
lime_on_navy     equ    01ah    ;深藍底亮綠字
aqua_on_navy     equ    01bh    ;深藍底亮青字
red_on_navy      equ    01ch    ;深藍底亮紅字
fushisa_on_navy  equ    01dh    ;深藍底粉紅字
yellow_on_navy   equ    01eh    ;深藍底亮黃字
white_on_navy    equ    01fh    ;深藍底白字

cr               equ    0dh
lf               equ    0ah
carriage_return  equ    cr
line_feed        equ    lf

print_char      proto   near :byte,:word,:word
print_string    proto   near :word,:word,:word
input           proto   near :word,:word,:word,:word
print_al        proto   near :word,:word
print_ax        proto   near :word,:word

底下是 STR_NUM.ASM 的原始碼:

;作為程式庫的原始檔

        .model  tiny,stdcall
        .386
        public  print_char,print_string,input
        public  print_al,print_ax

include myasminc.inc

;***********************************************************
code    segment byte    public  'code'  use16
        assume  cs:code
;-----------------------------------------------------------
;印出字元
;
;用法: invoke  print_char,字元,顏色,位置
;
;       字元:一個位元組長,ASCII 字元
;       顏色:一個字組長度,較高 8 位元必須為零,較低 8 位元的色碼如下
;             00-黑色;01-藍色;02-綠色;03-青色;04-紅色
;             05-紫色;06-棕色;07-銀色;08-灰色;09-亮藍
;             0A-亮綠;0B-亮青;0C-亮紅;0D-亮紫;0E-黃色
;             0F-白色
;       位置:較高 8 位元表示列 ( y ),較低 8 位元表示行 ( x )
;輸出:DX 指向下一個位置
print_char      proc    near uses ax bx cx,
                        char:BYTE,color:WORD,p_xy:WORD
        mov     ah,2
        mov     dx,p_xy
        mov     bx,color
        int     10h
        mov     al,char
        mov     ah,9
        mov     cx,1
        int     10h
        inc     p_xy
        mov     ah,2
        mov     dx,p_xy
        int     10h
        ret
print_char      endp
;-----------------------------------------------------------
;印出字串,並清除字串後的螢幕內容
;
;用法: invoke  print_string,字串位址,顏色,位置
;
;       字串位址:要列印字串的存放偏移位址,區段位址固定為 DS
;                 ,且字串以'$'結束。
;       顏色:一個字組長度,較高 8 位元必須為零,較低 8 位元的色碼如下
;             00-黑色;01-藍色;02-綠色;03-青色;04-紅色
;             05-紫色;06-棕色;07-銀色;08-灰色;09-亮藍
;             0A-亮綠;0B-亮青;0C-亮紅;0D-亮紫;0E-黃色
;             0F-白色
;       位置:較高 8 位元表示列 ( y ),較低 8 位元表示行 ( x )
;輸出:DX-指向下一個位置
print_string    proc    near,
                        addr_str:WORD,attrib:WORD,posi:WORD
        pusha
        mov     si,addr_str
        mov     dx,posi
        mov     cx,1
        mov     bx,attrib
@@:     lodsb
        mov     ah,2
        int     10h     ;設定游標位置
        cmp     al,'$'  ;若為 '$' 則字串已印完
        je      @f
        invoke  print_char,al,bx,dx
        jmp     @b
@@:     mov     cx,79   ;清除字串後的螢幕
        sub     cl,dl
        mov     ax,920h
        mov     bx,attrib
        mov     posi,dx
        int     10h
        popa
        mov     dx,posi
        ret
print_string    endp
;-----------------------------------------------------------
;印出 AL 內之十六進位數值
;
;用法: invoke  print_al,顏色,位置
;
;       顏色、位置:同 print_string
print_al        proc    near uses ax,
                        al_color:word,al_posi:word
        mov     ah,al
        shr     al,4
        add     al,'0'
        cmp     al,'9'
        jbe     @f
        add     al,7
@@:     invoke  print_char,al,al_color,al_posi
        mov     al,ah
        and     al,0fh
        add     al,'0'
        cmp     al,'9'
        jbe     @f
        add     al,7
@@:     invoke  print_char,al,al_color,dx
        ret
print_al        endp
;-----------------------------------------------------------
;印出 AX 內之十六進位數值
;
;用法: invoke  print_ax,顏色,位置
;
;       顏色、位置:同 print_string
print_ax        proc    near uses ax,
                        ax_color:word,ax_posi:word
        push    ax
        mov     al,ah
        invoke  print_al,ax_color,ax_posi
        pop     ax
        invoke  print_al,ax_color,dx
        ret
print_ax        endp
;-----------------------------------------------------------
;接收輸入的數字
;
;用法: invoke  input,最大輸入的位數,進位制,顏色,位置
;
;       最大位數:使用者最多只能輸入的位數,最大為 16
;       進位制:10D 表十進位制,10H 表十六進位制
;       位置與顏色:使用者每輸入一個數字,會顯示在螢幕上的位置與顏色
;輸出:NC-使用者輸入正確,此時 EDX:EAX 為輸入的十六進位數目
;      CY-使用者按 Esc 鍵取消輸入
input   proc    near uses bx cx si di,
                n:WORD,rdx:WORD,char_color:WORD,position:WORD
        local   key_buffer[16]:BYTE
        mov     cx,16
        cmp     n,cx
        ja      ipt8

        lea     si,key_buffer
@@:     mov     byte ptr ss:[si],0      ;設定 key_buffer 初始值為 0
        inc     si
        loop    @b

        mov     dx,position     ;CX=使用者已經輸入的位數
                                ;DX=螢幕位置
;DI=使用者輸入的數字存入位址,例如使用者輸入 『1』、『F』、『8』、
;『Enter』,則 key_buffer 變為 01、0F、08、00、00、00……
        lea     si,key_buffer
ipt1:   mov     ah,0
        int     16h
        cmp     ah,01   ;使用者是否按下 Esc 鍵
        je      ipt8
        cmp     al,cr   ;使用者是否按下 Enter 鍵
        je      ipt4
        cmp     al,8    ;使用者是否按下倒退鍵
        je      ipt6
        mov     bl,'0'
        cmp     al,bl   ;檢查使用按下的鍵是否小於
        jb      ipt1    ;30H,若小於則重新輸入
        cmp     al,'9'  ;檢查使用按下的鍵是否在 30H∼39H 之間
        jbe     ipt2    ;若是,則跳到 ipt2:
        cmp     rdx,10d ;檢查十進位制還是十六進位制,若為十進位制
        je      ipt1    ;使用者只能輸入 0∼9,Esc、Enter、BS 鍵
        cmp     rdx,10h ;若為十六進位制,則使用者還可輸入 A∼F 鍵
        jne     ipt8
        and     al,0dfh ;使用者按下 39H 以後的鍵,且用十六進位制輸入
        cmp     al,'A'  ;檢查是否在 A∼F 之間
        jb      ipt1
        cmp     al,'F'
        ja      ipt1
        mov     bl,37h
ipt2:   invoke  print_char,al,char_color,dx     ;使用者輸入 A∼F 鍵
        sub     al,bl
ipt3:   mov     ss:[si],al
        inc     si
        inc     cx      ;使用者輸入位數增一,之後和最大輸入的位數比較
        cmp     cx,n
        jne     ipt1

;使用者按下 Enter 鍵,完成輸入,接下來得把輸入所得的字串,轉換成數值,
;並存於 EDX:EAX 堙C若 CX=0,表示使用者沒有輸入任何數字,就按下 Enter 鍵
ipt4:   jcxz    ipt8
        lea     si,key_buffer
        sub     eax,eax
        mov     edi,10
        mov     edx,eax ;EDX:EAX 將存放使用者輸入的數值
ipt5:   movzx   ebx,byte ptr ss:[si]
        inc     si
.if rdx==10d            ;使用者輸入十進位
        mul     edi
        add     eax,ebx
        adc     edx,0
.elseif rdx==10h
        shld    edx,eax,4
        shl     eax,4
        add     eax,ebx
.else
        jmp     short ipt8
.endif
        loop    ipt5
        clc             ;使用者輸入正常數字離開,令進位旗標為 NC
        jmp     short ipt9

;使用者按 BackSpace 鍵
ipt6:   jcxz    ipt7    ;檢查是否還沒輸入,就按 BackSpace 鍵
        dec     dx
        invoke  print_char,' ',char_color,dx
        dec     dx
        mov     ah,2
        mov     bh,0
        int     10h
        dec     si
        dec     cx
ipt7:   jmp     ipt1

ipt8:   stc             ;使用者取消輸入,令進位旗標為 CY
ipt9:   ret
input   endp
;-----------------------------------------------------------
code    ends
;***********************************************************
        end     print_char

底下是 CALENDAR.ASM 的原始碼:

;顯示某年某月的月曆
;必須以 MASM 6.0 以上版本組譯
        .model  tiny,stdcall
        .386

include         myasminc.inc
includelib      myasmlib.lib

input_color     equ     yellow_on_black
normal_color    equ     lime_on_black
title_color     equ     white_on_black
sunday_color    equ     red_on_black

;***********************************************************
code    segment para    public  'code'  use16
        assume  cs:code,ds:code
        org     100h
start:  jmp     begin
msg0    db      '輸入公元年:$'
msg1    db      '輸入月份:$'
msg2    db      '日 一 二 三 四 五 六$'
year    dw      ?
month   dw      ?
day     dw      ?       ;使用者所輸入月份的天數
cursor  dw      100h    ;游標位置 ( 較高 8 位元表列,較低 8 位元表行 )
;day_m 是一月到 12 月,每個月的天數 ( 以 BCD 數表示 )
day_m   db      31h,28h,31h,30h,31h,30h,31h,31h,30h,31h,30h,31h
;-----------------------------------------------------------
;計算所給的格勒哥里曆,求出該日是星期幾
;用法: invoke  day_of_week,year,month,date
;       year 是西元年份,也就是格勒哥里曆法的年份
;       month 是月份,1∼12
;       date 是日,1∼31
;輸出:AX-0∼7 分別表示星期日∼六
;公式:Zeller's 公式
;      W={C/4–2*C + Y+ Y/4 + [(13*M–1)/5]+D} Mod7
;        a=int((14-month)/12)
;        Y=(year - a)之末兩位數
;        C=世紀=(year -a)之千位數與百位數,例如 year=2006,則 C=20
;        M=month+12a-2
;        D=day
;        計算上面的所有除法運算時,只求商,無條件捨去小數
day_of_week     proc    uses bx ,y:WORD,m:WORD,d:WORD
        local   a:WORD
        local   century:WORD
        mov     ax,14
        sub     ax,m
        mov     bl,12
        div     bl
        cbw
        mov     a,ax    ;a=int((14-month)/12)
        sub     y,ax
        mov     ax,y
        mov     bl,100
        div     bl
        movzx   bx,ah   ;BX=year 之末兩位數
        and     ax,0ffh ;AX=世紀
        mov     y,bx
        mov     century,ax
        mov     bl,12
        mov     ax,a
        mul     bl
        add     ax,m
        inc     bl      ;BL=13
        sub     ax,2    ;AX=M=month+12a-2
        mul     bl
        dec     ax
        mov     bl,5
        div     bl
        mov     ah,0    ;AX=int((13M-1)/5)
        add     ax,y
        add     ax,d
        shr     y,2     ;y=int(y/4)
        add     ax,y
        mov     bx,century
        shl     century,1
        shr     bx,2
        add     ax,bx
        sub     ax,century
        mov     bx,7
.while  sword ptr ax<0  ;若 AX<0,加上 7 直到 AX>0
        add     ax,bx
.endw
        div     bl
        shr     ax,8    ;AL=W
        ret
day_of_week     endp
;-----------------------------------------------------------
;計算某年是否為閏年
;
;用法: invoke  leap_year,西元年
;
;輸出:閏年,AL=29H;平年:AL=28H
leap_year       proc    near stdcall uses bx dx ,year1:word
        sub     dx,dx
        mov     ax,year1
        mov     bx,400
        div     bx
        or      dx,dx
        jz      leap
        mov     ax,year1
        sub     dx,dx
        mov     bx,100
        div     bx
        or      dx,dx
        jz      common
        sub     dx,dx
        mov     bx,4
        mov     ax,year1
        div     bx
        or      dx,dx
        jz      leap
common: mov     al,28h
        jmp     short ly_qut
leap:   mov     al,29h
ly_qut: ret
leap_year       endp
;-----------------------------------------------------------
begin:  mov     ax,600h
        sub     cx,cx
        mov     bh,normal_color
        mov     dx,184fh
        int     10h             ;清除螢幕

        invoke  print_string,offset msg0,input_color,cursor
        invoke  input,4,10,input_color,dx
        mov     year,ax
        invoke  leap_year,ax    ;檢查是否閏年
        mov     bx,1            ;若為閏年,把二月設為 29 天
        mov     day_m[bx],al

        add     cursor,20
        invoke  print_string,offset msg1,input_color,cursor
        invoke  input,2,10,input_color,dx
        mov     bx,ax
        mov     month,ax
        dec     bx
        movzx   ax,day_m[bx]    ;AX=使用者所輸入月份的天數
        mov     day,ax

        add     cursor,100h
        and     cursor,0ff00h   ;使游標移到下一列
        invoke  print_string,offset msg2,title_color,cursor
        invoke  day_of_week,year,month,1

;計算該月一日開始印出的行位置 ( x )。星期日在每列的第 0 行,星期一在第 3 行
;星期:日 一 二 三 四 五 六
;行:   0  3  6  9 12 15 18
        mov     dx,ax
        mov     bx,ax
        shl     dx,1
        add     dx,bx   ;DX=該月一日開始印出的行位置 ( x )
        mov     ax,1    ;AX=日期,由 1 遞增到 day
nxt_rw: mov     cx,7    ;CX=每一列要印出幾個日子
        sub     cx,bx   ;CX=該月的第一個星期,還有幾天要印出來
        add     cursor,100h
        and     cursor,0ff00h   ;使游標移到下一列
        add     cursor,dx
nxt_d:  
.if cx==7
        mov     dx,sunday_color ;以紅色印出星期天
.else
        mov     dx,normal_color ;以綠色印出其他日子
.endif
        push    ax
        invoke  print_al,dx,cursor      ;印出日期的十位數
        pop     ax
        inc     dx
        add     al,1
        daa
        mov     cursor,dx       ;指向下一個位置
        cmp     ax,day          ;檢查是否超過 day
        ja      exit
        loop    nxt_d
        sub     bx,bx   ;只有在該月的第一個星期,以 day_of_week 算出螢幕
        mov     dx,bx   ;起始位置;否則 BX、DX 均為零,都是從第 0 行開始
        cmp     ax,day
        jbe     nxt_rw

exit:   mov     ax,4c00h;返回 DOS
        int     21h
;-----------------------------------------------------------
code    ends
;***********************************************************
        end     start

組譯與連結

這個程式無法用 MASM 5.x 或以下的版本組譯,必須用 MASM 6.0 或以後的版本。MASM 6.0 及其以後版本的組譯器稱為 ML.EXE,它能在組譯之後自動載入連結器 ( 即 LINK.EXE ) 連結,為了防止 ML.EXE 找不到 LIN.EXE,所以得先設好路徑。因為小木偶把 ML.EXE 與 LINK.EXE 放在 I:\MASM611\BIN 子目錄中,所以先設好 PATH:

E:\HomePage\SOURCE>set path=i:\masm611\bin;path% [Enter]

E:\HomePage\SOURCE>ml /c str_num.asm [Enter]  →此步驟加上『/c』,表示只組譯不連結
Microsoft (R) Macro Assembler Version 6.11
Copyright (C) Microsoft Corp 1981-1993.  All rights reserved.

 Assembling: str_num.asm

E:\HomePage\SOURCE>lib myasmlib [Enter]  →把 STR_NUM.OBJ 模組加入 MYASMLIB.LIB 

Microsoft (R) Library Manager  Version 3.20.010
Copyright (C) Microsoft Corp 1983-1992.  All rights reserved.

Operations: +str_num [Enter]
List file: myasmlib.lst [Enter]
Output library: myasmlib [Enter]  →到此,已把 STR_NUM 模組加入程式庫了

E:\HomePage\SOURCE>ml calendar.asm [Enter]
Microsoft (R) Macro Assembler Version 6.11
Copyright (C) Microsoft Corp 1981-1993.  All rights reserved.

 Assembling: calendar.asm

Microsoft (R) Segmented Executable Linker  Version 5.31.009 Jul 13 1992
Copyright (C) Microsoft Corp 1984-1992.  All rights reserved.

Object Modules [.obj]: calendar.obj/t
Run File [what_day.com]: "calendar.com"  →見下面說明
List File [nul.map]: NUL
Libraries [.lib]: →因加上 includelib,故不須指定程式庫
Definitions File [nul.def]:

E:\HomePage\SOURCE>

底下來看看這個程式包含了那些新的指令。

.MODEL 假指令

.MODEL 的功用較雜亂,其語法是:

.MODEL  記憶體模式 [,程式語言] [,堆疊選項]

記憶體模式可以是 TINY、SMALL、COMPACT、MEDIUM、LARGE、HUGE 或 FLAT 等七種堶悸漱@種:

TINY: 程式碼區段、資料區段、堆疊區段等所有區段都重疊,也就是在擠在同一個區段堙C故全部大小不能超過 64KB,但是程式可以在執行時配置超過 64KB 的記憶體。也因為所有區段都在同一區段內,故呼叫副程式均為近程呼叫。
指定 .model tiny 後,ML.EXE 會自動送 /TINY 參數給 LINK.EXE,故不須在組譯時,輸入 ml /AT 參數。
SMALL: 一個程式碼區段與一個資料區段,且這兩個區段是在不同區段堙C故呼叫副程式時,仍為近程呼叫。
MEDIUM: 可以有多個程式碼區段與一個資料區段。故呼叫副程式可以是遠程呼叫。
COMPACT:只有一個程式碼區段與多個資料區段。故呼叫副程式只能是近程呼叫。
LARGE: 可以有多個程式碼區段與多個資料區段。故呼叫副程式可以是遠程呼叫。
HUGE:多個程式碼區段與多個資料區段。故呼叫副程式可以是遠程呼叫。
FLAT: 程式碼區段、資料區段、堆疊區段等所有區段都重疊,也就是在擠在同一個區段堙A但是於此模式時,是以 32 位元的定址模式,所以這個區段長 4GB,一般在 Win32 系統中的應用程式屬於此模式。
在指定 .model flat 前,得先指定 .386 或比其較高等級的 CPU 才可使用 FLAT 模式。

.MODEL 的程式語言意義與 PROC 相同,此處不多說了。如果 PROC 之後的程式語言省略,那麼組譯器便會以這兒所定義的作為組譯的準繩。

小木偶偏好 *.COM 執行檔,故也把 CALENDAR.ASM 寫成 *.COM 檔,因此組譯時有些細節在此說明。由上面『.model tiny』的說明,可知並不是所有原始碼都可以組譯成 *.COM 執行檔,只有程式碼區段、資料區段、堆疊區段等所有區段都在同一區段的原始碼才可組譯成 *.COM 執行檔。如果在原始碼中,有指定『.model tiny』,那麼在組譯時,ML.EXE 會自動製作出 *.COM 來;如果原始碼中沒有指定『.model tiny』,那麼也可以在命令提示符號輸入

E:\HomePage\SOURCE>ml /AT 原始檔名.ASM {Enter]

來製作 *.COM,參數『/AT』指的就是『Enable tiny model 』之意。當然,不論那一種方法,所有的區段都必須重疊在同一區段才能產生 *.COM。

INCLUDE 假指令

當我們製作程式庫時,有些常數或副程式的宣告會同時出現在副程式原始檔與主程式原始檔堙A像這個例子的顏色。在這種情形下,如果要修改程式,就必須同時修改副程式與主程式的原始檔,這樣很不方便,於是含入檔就孕育而生了。

一般含入檔的副檔名是『*.INC』,在 MASM 6.x 的版本堙A也支援含入檔。其語法是:

        include     含入檔名

含入檔名可以包含路徑,可以用一對『<』、『>』括起來,也可以不用。含入檔名必須是完整的,組譯器沒有內定的副檔名。例如,如果您的 include 假指令是

        include     color

表示您是要把 color 檔,而不是 color.inc 或其他 color.txt ……檔囊括進來。如果您沒有在含入檔名指定路徑,那麼 ML.EXE 會先在目前的目錄中尋找含入檔,如果沒找著,會到您在命令提示符號所輸入的『/I』參數後的路徑去找,如果也沒找著的話,最後會到環境變數 INCLUDE 所指定的目錄奡M找。

INCLUDELIB 假指令

INCLUDELIB 是告訴連結器將要與那一個程式庫連結,其語法是:

        INCLUDE     程式庫檔名

程式庫檔名可以不加副檔名,不加副檔名時,ML.EXE 會自行加上 .LIB 當做副檔名。程式庫檔名也可以加上完整的路徑名,也可以用一對『<』、『>』括起來,也可以不用。假如在原始程式堙A加上這一道假指令,那麼 ML.EXE 就會自動地把程式庫檔名傳給 LINK.EXE,因此就不須指定程式庫了。

MOVZX 與 MOVSX 指令

吾人撰寫程式時,如果要把一個長 8 位元的變數移進 16 位元的暫存器中,一般要用 ptr 假指令,例如:

centimeter      db      0c8h    ;0c8h=200d
cm_per_meter    db      64h     ;64h=100d
        mov     ax,ptr word centimeter

但這樣做還不夠,因為 ptr 只是忽略 db 的定義,而把它向高位址擴展成字組,如果高它一個位址的記憶體不是零,那麼也會照樣地搬到 AX 中,所以最後 AX 變成 64c8h。如果真要只搬移 centimeter,而使高位元組為零,在 80386 之前的 CPU 得再加上『mov ah,0』,但 80386 及其以後的 CPU 有兩條新的指令,MOVZX 和 MOVSX,可以達到目的。MOVZX 和 MOVSX 的語法是:

        movzx   r32,r8/m8
        movzx   r32,r16/m16
        movzx   r16,r8/m8

        movsx   r32,r8/m8
        movsx   r32,r16/m16
        movsx   r16,r8/m8

r8、r16、r32 分別表示 8、16、32 位元的暫存器,m8、m16 分別表示 8、16 位元的變數 ( m 表示記憶體,因為變數都存在記憶體中 )。MSOZX 指令的目的是把後面的較短的暫存器或變數之值移到前面較長的暫存器中,且使較長暫存器中較高的所有位元都為零;而 MOVSX 則是使有號數在搬移後結果仍為有號數,亦即視第二個運算元的最高位元是否為 1,決定是否把較長暫存器的較高位元都填入 1。例如有段程式:

        mov     bl,88h
        mov     bh,70h
        movzx   eax,bl
        movsx   ecx,bl
        movsx   edx,bh

執行完後,EAX=00000088H,ECX 為 0FFFFFF88H,EDX 為 00000070H。MOVZX、MOVSX 在把較短位元的暫存器擴展成較長位元的暫存器很有用,很明顯的『Z』表示 ZERO 之意,MOVZX 是做無號數的擴展;『S』是 SIGN 之意,是做有號數的擴展。

SHLD 與 SHRD 指令

SHLD 指令的語法是:

        SHLD    r/m16,r16,imm8
        SHLD    r/m16,r16,CL
        SHLD    r/m32,r32,imm8
        SHLD    r/m32,r32,CL

上式 r16、r32 分別是 16、32 位元的暫存器,m16、m32 分別是 16、32 位元的記憶體變數,imm8 是 8 位元的立即值 ( 常數 )。SHLD 大概是少數有三個運算元的 80x86 指令,其作用是使第二個運算元 ( 稱為來源運算元 ) 向左移數個位元到第一個運算元 ( 稱為目的運算元 ) 堙C要左移的位元數在第三個運算元表示,可以是常數也可以放在 CL 暫存器中,但不論是那一種,其數值只能在 0∼31 範圍內。這個指令運算完後,來源運算元的數值不改變。例如底下的程式片段:

        mov     edx,12345678h
        mov     eax,9abcdef0h
        shld    edx,eax,4

因為左移 4 個位元,相當於 16 進位制的一位數,所以很容易可得執行完後 EDX 變為 23456789H,EAX 仍為 9abcdef0。亦即 EAX 向左移一位數 ( 即 4 位元 ),這一位數被移進了 EDX 的個位數了。

SHRD 的語法與作用和 SHLD 一樣,只是把左移改成右移。例如:

        mov     edx,12345678h
        mov     eax,9abcdef0h
        shrd    edx,eax,4

執行完 SHRD 後,EDX 變為 01234567H,EAX 仍為 9abcdef0h。EAX 向右移 4 位元,移出的 4 位元被移進了 EDX 最高的 4 位元堣F。


後記

在撰寫這一章時,曾為一個問題困擾甚久,到現在也不能說完全解決。那就是在一個主程式堙A似乎無法同時以堆疊傳遞參數和以暫存器傳遞參數呼叫在程式庫堛漱ㄕP副程式,這樣做會造成 LINK 編號 L2029 的錯誤,unresolved external,此訊息是程式庫堥S這個副程式。最後小木偶只好把這章所有在程式庫堛滌け{式改成以堆疊傳遞。

另一個常犯的錯誤,是執行檔的優先次序。想來也是好笑,這個錯誤以前也犯過,但在這章堣S再重蹈覆轍,耗費一天除錯,卻找不到錯誤。前面曾提過,DOS 的可執行檔有 *.COM 與 *.EXE 兩種,當一個目錄堶Y有主檔名相同,但副檔名不同的可執行檔,DOS 優先執行 COM 檔,其次才是 EXE 檔。例如同一子目錄埵 CALENDAR.COM 與 CALENDAR.EXE,當在 MS-DOS 模式的提示符號下輸入 CALENDAR 時,是執行 CALENDAR.COM 而不是執行 CALENDAR.EXE,而小木偶先前有一個錯誤的 CALENDAR.COM,後來想簡化手續,把原始碼改成 *.EXE 格式。因執行時只要輸入主檔名,但除錯卻要輸入 DEBUG CALENDAR.EXE,完整檔名,所以結果執行起來老是錯誤,但除錯卻總是找不到錯誤。


回到首頁到第三十章到第三十二章