第五章 巨集(一)

微軟所販售的組合語言開發工具稱為「Microsoft Macro Assembler」,其縮寫為「MASM」,最後的三個字母,顯然是組譯器 ( assembler ) 的縮寫;而第一個「M」是 Macro 的意思 ( 註一 ),Macro 中文翻譯成巨集 ( 大陸翻譯成「宏」)。總之就是,微軟的組合語言開發工具,「Microsoft Macro Assembler」可翻譯為「巨集組譯器」。微軟都以「巨集」稱呼自身的組譯器了,其重要性可見一斑。但是一般教授組合語言的書或網站,很少提 及巨集,非常可惜。小木偶不自量力,想介紹這方面的主題。

在第零章曾提及,微軟包含在許多開發工具的 64 位元組譯器,ML64.EXE,並不提供原本在第 6.x 版就支援的高階指令,例如像 INVOKE、.IF/.ENDIF……等假指令。不過,在許多前輩、先進的努力之下,利用巨集重現了這些高階指令。小木偶私下揣測,要增進組合語言功力,當然要能模仿這些大神級人物,因此不能不懂巨集。即使無法像他們一樣登峰造極,但也心嚮往之。這也是讓小木偶想介紹巨集的原因。

停止廢話,言歸正傳。何謂巨集?巨集是一系列文字敘述或一些指令的集合。一旦宣告巨集之後,就可以在原始程式其他地方引用此巨集。組譯器會在組譯原始程式時期,如果遇見引用巨集時,會預先處理巨集,「展開」成原來的假指令或 x86 指令,安插在引用巨集的地方。這樣只需宣告一次巨集,就能重複多次引用,以達到精簡原始程式的目的。如果巨集再配合條件組譯與巨集運算子,甚至會有意想不到的效果。

巨集分成下面幾種,在這一章裡因為篇幅的關係,只會介紹前三種巨集,最後兩種巨集在第六章介紹:

  1. 巨集程序 ( macro procedures ):可以接受參數,在引用的地方展開為一些指令。
  2. 巨集函式 ( macro functions ):類似巨集程序,不同的是巨集函式結束時有回傳值。
  3. 文字巨集 ( text macro ):可在原始程式中展開為原來的文字。
  4. 預先定義的巨集函式和字串假指令 ( predefined macro functions and string directives ):用於處理字串,見下一章
  5. 重複區塊 ( repeat blocks ):可以根據指定的次數或條件重複多次,見下一章

乍看之下,光是看巨集的種類,就覺得很複雜的樣子,但實際上應用時並不太區分它們之間的不同。本章底下的內容,就依上面的順序逐一介紹這些巨集,先來看看巨集程序。


巨集程序

不管哪一種巨集,都必須先宣告,才能使用,否則便會產生錯誤。宣告巨集的地方可以在檔案開始不遠處、也可以在「.CONST」區段內,甚至在「.CODE」區段內的第一行指令之後都行;也可以放在包含檔 ( 副檔名為 INC 或 MAC ) 中,然後在原始程式中以「INCLUDE」假指令載入。不論哪一種方式,都只有一個條件,就是要放在引用巨集之前。有些文獻把使用巨集,說成呼叫巨集,這也不算什麼錯誤;但是巨集與副程式不同,因此本文以「引用」代替「呼叫」。

MACRO/ENDM 假指令

宣告巨集程序的方法是使用 MACRO/ENDM 假指令,語法如下:

巨集名稱    MACRO   參數列表
           敘述
ENDM

上面的敘述可以是 x86 指令或是符合組合語言語法的假指令,必須夾在 MACRO 與 ENDM 之間。在 MACRO 之後的是參數列表,如果巨集有許多參數的話,每個參數之間以「,」隔開,仍寫在參數列表中。巨集的參數只能在巨集內容使用,其他地方無效。要引用巨集時,只需要寫上巨集名稱,其後 接上引數即可 ( 什麼是引數,見第一章註二 )。

底下,小木偶用 GetStdH 巨集程序作為例子,藉著這個例子說明如何宣告及引用巨集程序。

在第三、四章的程式中,都曾呼叫 GetStdHandle、WriteConsoleA、ReadConsoleA 等 Win64 API 數次,每次都需要傳遞許多參數,很是麻煩。如果把呼叫它們的過程製作成巨集,並且在呼叫它們的地方改成引用巨集,這樣原始程式看起來就會精簡許多。

底下是把呼叫 GetStdHandle 的過程改成 GetStdH 巨集程序:

GetStdH MACRO   nStd,handle
        mov     rcx,nStd                ;;要取得標準輸入裝置還是標準輸出裝置
        call    GetStdHandle            ;;取得標準裝置代碼
        cmp     rax,INVALID_HANDLE_VALUE;;檢查是否呼叫失敗
        je      exit                    ;;若呼叫失敗,跳至exit:
        mov     handle,rax              ;;若呼叫成功,將代碼存起來
ENDM

GetStdH 巨集有兩個參數,nStd 是指定要取得標準輸出裝置代碼,還是標準輸入裝置代碼;handle 則是把取得的代碼,存放在哪一個變數裡。要引用 GetStdH 巨集時,只需要寫上巨集名稱,其後接上適當的引數即可,如下:

        GetStdH STD_OUTPUT_HANDLE,hOutput

當組譯器組譯時,遇見像上一行引用 GetStdH 巨集時,就會用 STD_OUTPUT_HANDLE 代入進 nStd 裡面,用 hOutput 代入進 handle 裡面,然後把 GetStdH 內的 nStd、handle 全換成這兩個引數,然後再引入巨集之處,換上已取代的巨集內容。像這樣將巨集內容的參數以引數取代之後,把引用巨集之處,寫入取代後的巨集內容,稱為展開 ( expansion )。因此,原來引用巨集僅一行指令,展開之後會變許多行。展開後的結果,可以在列表檔中看到。

下面的表格就是 GetStdH 展開後的對照,左邊是原始程式引用 GetStdH 巨集,右邊是列表檔展開的結果。展開巨集前,先將引用的巨集抄寫一遍,而後再展開。在機械碼與指令之間會添加「1」,它表示是第一層巨集展開的結果。有第一層,當然有第二層,假如在巨集中引用另一個巨集,組譯器生成的列表檔會標上「2」。

原始程式,引用巨集列表檔,展開的結果
GetStdH STD_OUTPUT_HANDLE,hOutput                                     GetStdH STD_OUTPUT_HANDLE,hOutput
00000004  48/ C7 C1 FFFFFFF5   1    mov     rcx,STD_OUTPUT_HANDLE
0000000B  E8 00000000 E        1    call    GetStdHandle
00000010  48/ 83 F8 FF         1    cmp     rax,INVALID_HANDLE_VALUE
00000014  0F 84 000000D6       1    je      exit
0000001A  48/ 89 05            1    mov     hOutput,rax

在巨集內也能加上註解,巨集的註解可以用「;」或「;;」。組譯器都會忽略掉在「;」或「;;」之後的文字,因此可以用它們作為註解的起頭。「;」與「;;」的差別在於展開巨集時,以「;」為起頭的註解會列在列表檔裡面;而以「;;」為起頭的不會,就顯得不那麼囉嗦。

另外有一點對於撰寫巨集非常重要,就是要區別字串與數值。一般文字,像英文字母、中文等組成的一個句子,必定是字串,這沒有什麼問題,但是如果是像下面的例子:

x       DB      "12"
y       DB      12

x 是字串,因為組譯後會變成「31H、32H」兩個位元組,31H 代表 ASCII 字元的「1」,在較低位址;32H 代表 ASCII 字元的「2」,在較高位址。x 是字串變數,但卻由阿拉伯數字組成,可稱其為數值字串 ( 或數值字串變數 )。y 是數值,組譯後變成「0CH」,就是十進位的 12,可稱 y 為數值變數。假設有一段程式是

        mov     al,x
        mov     cl,y

執行結果是:AL 為 31H,CL 為 0CH。看起來「mov al,x」似乎是錯誤的,的確如此,但 ML64.EXE 卻能正常組譯不發生錯誤訊息,因此要小心謹慎。

在引用巨集時所用的引數都必須是字串,引數不能用數值。即使是像下面那樣引用巨集都能成功:

        GetStdH STD_OUTPUT_HANDLE,hOutput
        GetStdH -11,hOutput

但實際上,組譯器把「STD_OUTPUT_HANDLE」、「-11」都轉換成字串形式,然後才代入到巨集內容裡面。

如果已經對巨集有了粗略的瞭解之後,小木偶打算先介紹撰寫巨集時,必定會用得上的觀念,例如巨集參數的屬性、巨集內部的變數、巨集運算子;最後是 ECHO 假指令,這個指令在撰寫巨集階段除錯很重要。等這一切都介紹完畢,再依序介紹巨集函式、文字巨集……。

巨集參數的屬性:必要的、有預設值的、可變數量的參數

在 MACRO 之後的參數列表中,會有一系列的參數,這些參數以「,」隔開,而且在整個巨集中都無法改變其值,否則組譯器就會告知發生錯誤。除此之外,參數還可以有以下屬性:①必要的、②有預設值的、③可變數量的,底下一一介紹。

組譯器組譯時,並不會檢查參數個數與引數個數是否相等,即使不等,只要展開時沒有語法上的錯誤,就不會產生錯誤訊息。例如把 GetStdH 改成底下的樣子:

GetStdH MACRO   nStd,handle
        mov     rcx,nStd                ;;要取得標準輸入裝置還是標準輸出裝置
        call    GetStdHandle            ;;取得標準裝置代碼
        cmp     rax,INVALID_HANDLE_VALUE;;檢查是否呼叫失敗
        je      exit                    ;;若呼叫失敗,跳至exit:
        mov     hOutput,rax             ;;若呼叫成功,將代碼存起來,但都存於hOutput變數裡
ENDM

以下面方式引用:

        GetStdH STD_INPUT_HANDLE

組譯時,組譯器並不會發生錯誤。為了避免這種情形,可以在參數後面加上「:REQ」,代表這是必要的參數不可省略,若省略組譯器會發生錯誤。例如底下的 GetStdH 把兩個參數都設為必要的:

GetStdH MACRO   nStd:REQ,handle:REQ
        ⁝
ENDM

參數除了可設為必要的之外,也可以設定預設值,設定預設值的方式是在參數後面加上「:=文字」。假如是預設的參數是數值的話,必須以一對 < > 括起來。例如底下的 Quit 巨集,把 exit_code 參數的預設值設為 0:

Quit    MACRO   exit_code:=<0>
        mov     rcx,exit_code
        call    ExitProcess
ENDM

如果像下面的方式引用 Quit 巨集時,也就是沒有設定 exit_code 時,exit_code 會自動以零代入:

        Quit

最後一種參數屬性,是參數的數量是不固定的。這種參數必須放在參數列表的最後面,並且以「:VARARG」表示。通常會可變數量的參數會搭配 FOR/ENDM,請看下一章重複區塊 FOR/ENDM

巨集的變數

在巨集內部,免不了要對數值或是字串做運算,因此有時必須在巨集內宣告變數。這些變數區分成兩類:數值變數與字串變數。數值變數可以用 EQU 或 = 假指令宣告,差別在於以前者宣告之後無法更改其數值;後者可以。所以嚴格來說,EQU 宣告常數,= 宣告變數,它們的語法是:

符號    EQU     算術運算式
符號    =       算術運算式

其中的符號就是變數名稱或常數名稱;算術運算式是指可以是用算術計算的數學式子,其運算元應該都是數值或已宣告過的變數。以這兩種方式宣告的符號,只有在組譯時期有效,而不是在執行時期。例如底下的 Arithmetic 巨集能列出等差數列 ( arithmetic sequence ),此數列首項是 a1,公差是 d,列出的項數是 terms,其內容是:

Arithmetic MACRO a1,d,terms
  ;;Arithmetic能在資料區段中定義一等差數列,首項為a1、公差為d,共有terms項
  a_s     DD     a1     ;;首項
          n=terms-1     ;;n為項數,但首項已定義好,故項數再少一
          an=a1+d       ;;第二項為首項加公差
  WHILE n
          DD     an     ;;定義第n項
          an=an+d       ;;下一項為前一項加公差
          n=n-1         ;;項數少一
  ENDM
ENDM

如果在資料區段,以下面方式引用 Arithmetic 巨集,就能產生首項是 2,每項比前項增加 10,共八項的等差數列:

Arithmetic  2,10,8

就會產生「a_s DD 2,12,22,32,42,52,62,72」的資料。你可以看見在 Arithmetic 內,a1、d、terms 都是參數,而 n、an 則是變數。變數能依需求進行運算,「=」右邊的運算式可以直接指定一個參數、變數或常數,也可以是它們之間進行加、減……等運算。在 Arithmetic 巨集中的 n、an 都只是在 ML64.EXE 組譯原始程式時,因處理此巨集需要,ML64.EXE 才在記憶體內設立這些變數,等處理完 Arithmetic 巨集後就被回收了。當 ML64.EXE 完成組譯原始程式後,所有在巨集內的變數、甚至參數,都會被回收而消失。像這種只有在組譯時期才存在的變數,就稱為組譯時期的變數。

有關 WHILE/ENDM 在第六章會介紹,現在只要知道當 n 不為零時,會一直重複執行 WHILE 與 ENDM 之間的程式碼。

字串變數的宣告方法是用 EQU 或 TEXTEQU,語法如下:

符號    EQU     <字串>
符號    TEXTEQU <字串>

不管用 EQU 還是 TEXTEQU 宣告的字串,都能更改變成另一個字串,差別是 TEXTEQU 能搭配後面要提到的 ECHO 假指令及巨集運算子做運算,而 EQU 不能。有關 TEXTEQU 的功能還不只宣告字串,還有許多,請參考後面的文字巨集。另外,在巨集內也可以對字串進行求出字串長度、搜尋字串中的文字、擷取字串內部分文字、連接數個字串等運算,這些都會在第六章預先定義的巨集函式和字串假指令說明。

LOCAL 假指令

假如某個符號 ( 包含變數、標記……) 只有在某個巨集中使用到,那麼可以將此符號設為區域的。把符號設為區域的方法是使用 LOCAL 假指令,它的語法是

LOCAL  符號名稱列表

要注意的是,LOCAL 必須緊接著在 MACRO 下一行,除非兩行之間是註解行。另外在巨集中宣告某些符號是區域性的,與在副程式中宣告區域變數,雖然都是使用 LOCAL,但是兩者意義完全不同。區分方式很簡單,前者是緊接著 MACRO 之後;後者緊接著 PROC 之後。有關在副程式中的區域變數,請參閱前一章

在巨集中,宣告某個符號是區域性後,每次引用此巨集時,組譯器會自動為已宣告過區域性的符號產生新的名稱。這個新的符號名稱格式是「??XXXX」,其中的 XXXX 是從零開始的十六進位數。因為這樣的緣故,所以表面上看起來,能達到使符號變成區域性的目的。但是知道的這樣的命名規則之後,就可以由巨集以外的地方存取這樣的變數,或是跳躍至這樣的標記處。

下表左欄是 TST.ASM 完整的原始程式,也能組譯並連結成功,組譯產生的列表檔在右欄。因為這個段落介紹 LOCAL,所以著重白色字的 GetStd 巨集程序,引用 GetStd 巨集的地方有兩處,均以黃色表示;而右欄黃色字下方數行的橙色字,則是 GetStd 展開後的結果。

GetStd 的第一行宣告 handle 為 LOCAL,而第三行表明了 handle 是在資料區段內、長度四字組的變數。所以第一次引用 GetStd 時,會在資料區段內定義一個四字組的變數,名稱為「??0000」;第二次引用時,也跟第一次引用時一樣,但變數名稱改為「??0001」。

原始程式列表檔




OPTION     CASEMAP:NONE
EXTRN      GetStdHandle:PROC
EXTRN      ExitProcess:PROC
INCLUDELIB e:\masm32\lib64\kernel32.lib

STD_INPUT_HANDLE        EQU     -10
STD_OUTPUT_HANDLE       EQU     -11

Quit    MACRO   exit_code:=<0>
        mov     rcx,exit_code
        call    ExitProcess
ENDM

GetStd  MACRO   nStd
        LOCAL   handle
        .DATA
        handle  DQ      ?
        .CODE
        mov     rcx,nStd
        call    GetStdHandle
        mov     handle,rax 
ENDM
;***************************************
.CODE
;---------------------------------------
main    PROC
        GetStd  STD_INPUT_HANDLE
        GetStd  STD_OUTPUT_HANDLE
        mov     rax,??0000
        mov     rcx,??0001
exit:   Quit
main    ENDP
;***************************************
END
Microsoft (R) Macro Assembler (x64) Version 14.25.28614.0   07/10/23 13:36:19
tst.asm                                                      Page 1 - 1


                                   OPTION     CASEMAP:NONE
                                   EXTRN      GetStdHandle:PROC
                                   EXTRN      ExitProcess:PROC
                                   INCLUDELIB e:\masm32\lib64\kernel32.lib

 =-0000000A                        STD_INPUT_HANDLE        EQU     -10
 =-0000000B                        STD_OUTPUT_HANDLE       EQU     -11

                                   Quit    MACRO   exit_code:=<0>
                                           mov     rcx,exit_code
                                           call    ExitProcess
                                   ENDM

                                   GetStd  MACRO   nStd
                                           LOCAL   handle
                                           .DATA
                                           handle  DQ      ?
                                           .CODE
                                           mov     rcx,nStd
                                           call    GetStdHandle
                                           mov     handle,rax
                                   ENDM
                                   ;***************************************
 00000000                          .CODE
                                   ;---------------------------------------
 00000000                          main    PROC
                                           GetStd  STD_INPUT_HANDLE
 00000000                       1          .DATA
 00000000  0000000000000000     1          ??0000  DQ      ?
 00000000                       1          .CODE
 00000000  48/ C7 C1 FFFFFFF6   1          mov     rcx,STD_INPUT_HANDLE
 00000007  E8 00000000 E        1          call    GetStdHandle
 0000000C  48/ 89 05 00000000 R 1          mov     ??0000,rax
                                           GetStd  STD_OUTPUT_HANDLE
 00000008                       1          .DATA
 00000008  0000000000000000     1          ??0001  DQ      ?
 00000013                       1          .CODE
 00000013  48/ C7 C1 FFFFFFF5   1          mov     rcx,STD_OUTPUT_HANDLE
 0000001A  E8 00000000 E        1          call    GetStdHandle
 0000001F  48/ 89 05 00000008 R 1          mov     ??0001,rax
 00000026  48/ 8B 05 00000000 R            mov     rax,??0000
 0000002D  48/ 8B 0D 00000008 R            mov     rcx,??0001
 00000034                          exit:   Quit
 00000034  48/ C7 C1 00000000   1          mov     rcx,0
 0000003B  E8 00000000 E        1          call    ExitProcess
 00000040                          main    ENDP
                                   ;***************************************
                                   END

Microsoft (R) Macro Assembler (x64) Version 14.25.28614.0   07/10/23 13:36:19
tst.asm                                                     Symbols 2 - 1

Macros:

                N a m e                 Type

GetStd . . . . . . . . . . . . .        Proc
Quit . . . . . . . . . . . . . .        Proc


Procedures, parameters, and locals:

                N a m e                 Type     Value    Attr

main . . . . . . . . . . . . . .        P        00000000 _TEXT Length= 00000040 Public
  exit . . . . . . . . . . . . .        L        00000034 _TEXT


Symbols:

                N a m e                 Type     Value    Attr

??0000 . . . . . . . . . . . . .        QWord    00000000 _DATA
??0001 . . . . . . . . . . . . .        QWord    00000008 _DATA
ExitProcess  . . . . . . . . . .        L        00000000 External
GetStdHandle . . . . . . . . . .        L        00000000 External
STD_INPUT_HANDLE . . . . . . . .        Number   -0000000Ah  
STD_OUTPUT_HANDLE  . . . . . . .        Number   -0000000Bh  

           0 Warnings
           0 Errors

這裡有個很有趣的寫法,看到宣告 GetStd 巨集時,是像底下的方式宣告的:

GetStd  MACRO   nStd
        LOCAL   handle
        .DATA
        handle  DQ      ?
        .CODE
        ⁝
ENDM

看到沒?在 GetStd 裡面,先在資料區段中定義一個變數、然後又在程式碼區段寫入一些 x86 指令,如果僅是這樣應該沒有問題。問題是,假設 GetStd 被引用了兩次甚至更多次,那麼展開之後,資料區段與程式碼區段豈不是交錯在一起了嗎?事實上,並非如此。組譯器會把名稱相同的區段都集合在一起,爾後合併成為完整的區段,即使它們原來是散落在原始程式各處。以 .DATA 假指令開始的資料區段,其名稱均為「_DATA」;而以 .CODE 假指令開始的程式碼區段,名稱均為「_TEXT」,因此組譯器能將它們都合併起來。

ECHO 假指令

ECHO 會使組譯器在組譯原始程式期間,把接在 ECHO 之後的資料印在螢幕上。其主要目的,是對巨集除錯。它有兩種用法:

ECHO 字串
%ECHO 字串變數

第一種用法很簡單,就是單純的把字串印在螢光幕上。第二種用法能把字串變數的內容印出來,如果在 ECHO 之前不加上「%」運算子,就只能印出變數的名稱,就變成第一種用法。加了「%」之後,「%」有求得其值的功用,「%」與 ECHO 之間可以有空白,也可以不加空白。例如底下的例子:

Love    EQU    <I love assembly.>  ;;宣告Love字串
ECHO    Love                       ;;組譯時只會印出Love
%ECHO   Love                       ;;組譯時會印出「I love assembly.」

其實,ECHO 的第二種用法很有彈性。它不僅可以印出字串變數的內容來,也可以在字串變數前後加上其他字串,甚至也可以把巨集函式的回傳值印出來。例如底下的 regp 巨集:

regp    MACRO   param,reg
        disp    TEXTEQU %param*8+8
        mov     [rbp+disp],reg
ENDM

regp 巨集中的 TEXTEQU 假指令在後面的「文字巨集」章節中再詳細介紹,這裡只要知道在它後面的是一個字串,TEXTEQU 會讓 disp 等於此字串。而此字串是由「%」運算子運算而得,「%」運算子會指示組譯器把 param*8+8 之數值計算出來並轉換為字串。「%」運算子就在後面,接著要介紹的巨集運算子中。如果以下面方式引用這個巨集:

        regp    1,rcx

會產生下面的程式碼並寫入目的檔,連結後寫入可執行檔:

        mov     [rbp+16],rcx

這是因為傳入的引數,1,使得 param 為 1,在「param*8+8」之前的「%」運算子會去計算「param*8+8」之數值,並將它轉換成字串,最後結果是字串,"16"。然後 TEXTEQU 讓 disp 等於這個字串,所以 disp 是字串變數,其內容為"16"。

有時候,在撰寫程式時,並不確定像 param*8+8 的運算式是否寫對,尤其是較複雜的運算式。這時候可以在「disp TEXTEQU %param*8+8」底下加上一行:

        %ECHO   mov [rbp+disp],reg

讓組譯器把 disp 及其他剩餘的部分印在螢幕上,好讓程式設計師去檢查,是否按照設想的去運算,藉以除錯。這是撰寫巨集時,常用的除錯技巧。另外,這個巨集可用於 Win64 程式呼叫副程式的時候,當進入副程式後,把前四個存於暫存器的參數存入堆疊裡。param 是第幾個參數,reg 是所相對應的暫存器。

巨集運算子

常用的巨集運算子如下表,很明顯前四個運算子是一類,是專門用來對文字進行運算的。這四個巨集運算子,有自己的名稱,並以一個特殊字元作為運算符號。就像數學四則運算的加、減、乘、除一樣,像加法的符號就是+。同樣的道理,文字分隔運算子的符號是「< >」。第五個自成一類,是求得參數屬性的運算子,並沒有特別的名稱。

運算子符號名稱簡要說明
< >文字分隔運算子 ( text delimiters )將 < > 內的文字變為字串
!字元運算子 ( literal-character operator )使其後的符號變為文字
&取代運算子 ( substitution operator )使在兩個 & 之間的文字變為參數
%展開運算子 ( expansion operator )計算使其後的運算式變為文字
OPATTR求得符號的屬性

①:文字分隔運算子的符號是一對 <、>,在這對符號之間的文字,即使有分隔符號,像空白、逗號 ( , )、引號 ( " )、分號 ( ; )……等,都會被文字分隔運算子忽略,而把這些分隔符號視為一般字元,使整段文字變為一個字串。

例如有個 Print 巨集 ( 事實上,Print 巨集全部內容在註二 ),只有一個參數,此參數為一字串或字串的位址,引用 Print 巨集時,就能把字串印在螢幕上。如果以下面兩種方式引用 Print:

        Print   "天若無光海無水,山若無峰雲無軌;"
        Print   0dh,0ah,"人卻有情死生不悔,何懼隔世再夢迴。"

第一行能順利組譯並成功印出來;但第二行組譯時,組譯器就會提出警告,說第二行太多參數了,但還是能組譯成功;執行卻會發生問題,無法印出「人卻有情死生不悔,何懼隔世再夢迴。」。應該改成下面的樣子就行了:

        Print   <0dh,0ah,"人卻有情死生不悔,何懼隔世再夢迴。">

如果不加上「< >」,組譯器會認為有三個參數,因為「,」是用來分隔參數的,所以前兩個字元 0dh、0ah 各自成一個參數,其餘又成一個參數。加了「< >」之後,會迫使在它們之間的文字,當成是一個字串,逗號是字元而不是分隔符號了,這樣就只有一個參數了。

②:字元運算子的符號是「!」,其作用是把在「!」後面的特殊符號當做一般字元。例如底下的例子就會發生錯誤,因為「!>」中的「!」是字元運算子,會把後面的 > 當做是一般字元,於是缺少了結尾的 >,就發生錯誤:

Love    TEXTEQU <I love assembly!>
%ECHO   Love

如果要印出「I love assembly!」,要像下面:

Love    TEXTEQU <I love assembly!!>
%ECHO   Love

③:取代運算子的符號是「&」。在兩個 & 之間的文字會被組譯器視為參數或變數,而以實際字串代入,這種情形多用於參數或變數位於字串內或者為某個單字的一部分。例如:

ErrMsg     MACRO   num,message
Error&num& DB      "Error &num&: &message&"
ENDM

如果在資料區段內,以下面方式引用 ErrMsg 巨集:

ErrMsg  1,<Invalid standard input device.>
ErrMsg  2,<Invalid standard output device.>

就會產生下面的程式碼:

Error1  DB      "Error 1: Invalid standard input device."
Error2  DB      "Error 2: Invalid standard output device."

如果參數之後是具有分隔意味的符號,例如空白、冒號、引號、逗號……等,那麼後面的 & 可以省略,所以上面的 ErrMsg 巨集也可以寫成下面的樣子:

ErrMsg     MACRO   num,message
Error&num  DB      "Error &num: &message"
ENDM

甚至是具有分隔意味的符號在參數之前,那麼省略前面的 &,僅寫出後面的 & 也是可以的。所以上面的 ErrMsg 巨集也可以寫成下面的樣子:

ErrMsg     MACRO   num,message
Error&num  DB      "Error num&: message&"
ENDM

④:展開運算子的符號是「%」,它用有兩種用法:

⑴:「%」不是在一行的最前面,並且後面接著數學運算式,這裡的數學運算式是由常數組成的運算式。在這種情形,「%」會去計算由數學運算式的結果,並且將其轉換為字串形式。這可以搭配文字巨集 ( TEXTEQU ) 使用,也可以在引用巨集時先計算引數。例如底下的第一個例子是搭配 TEXTEQU 使用,第二個例子是搭配 work 巨集的引數使用:

numstr  TEXTEQU %3+10   ;;%先把常數 3 加上 10,得到 13,再轉換成字串,故numstr=<13>
work    MACRO   arg
        mov     rax,arg*4
ENDM
        ⁝
        work    3+4     ;;「mov rax,3+4*4」即「mov rax,19」
        work    %3+4    ;;「mov rax,(3+4)*4」即「mov rax,28」

第一次引用 work 巨集時,必須把引數,「3+4」看成是字串代入後變成「mov rax,3+4*4」,因此變成「mov rax,19」。還記得吧?組譯器將引數代入巨集時,是以字串形式代入,非數值形式。第二次引用 work 巨集時,% 運算子會先把常數組成的數學運算式,就是「3+4」,計算出答案來,結果是「7」,轉換成字串後代入,所以得到「mov rax,28」。

⑵:「%」在一行的最前面,與 ECHO、TITLE 及 SUBTITLE 搭配使用。「%」指示組譯器展開 ECHO、TITLE 及 SUBTITLE 之後的文字巨集或是巨集函式以及其餘文字。底下的 StkArg 巨集是在呼叫 Win64 副程式時,把第五個或是以後的參數存入堆疊的,其內容及引用方式如下:

StkArg  MACRO   param,arg
        disp    TEXTEQU %20h+(param-5)*8
        %ECHO   mov QWORD PTR [rsp+disp],arg
        mov     QWORD PTR [rsp+disp],arg
ENDM
        ⁝
        StkArg  5,rax   ;;把第五個參數,RAX,存入堆疊

若第五個參數是 RAX,引用時的指令是「StkArg 5,rax」。這會產生「mov QWORD PTR [rsp+32],rax」程式碼,而第三行的「%ECHO mov QWORD PTR [rsp+disp],arg」其實沒有必要,可以刪去,但是對於巨集的除錯很有幫助。巨集只在組譯時展開,無法用除錯程式除錯,可以用這種方式將關鍵結果印在螢幕上,以檢查是否設計錯誤。

⑤:OPATTR 運算子的語法是

OPATTR 運算元

OPATTR 是用來求得運算元的屬性,這裡的運算元是某個符號,如果是未定義的符號,結果為 0;如果是已定義的符號,其運算結果為一個十六位元的數值,每一個位元代表一種屬性。第 11∼15 位元未使用,第 0∼10 位元的意義如下表:

位元說 明
0運算元在程式碼區段內時,位元零為一;否則為零
1運算元是記憶體變數時,位元一為一;否則為零
2運算元是常數時,位元二為一;否則為零
3運算元使用直接定址時,位元三為一;否則為零
4運算元是暫存器,位元四為一;否則為零
5運算元為已定義的符號且沒有錯誤時,位元五為一;否則為零
6運算元在堆疊區段時,位元六為一;否則為零
7運算元為外部符號時,位元七為一;否則為零
8∼10與語言類型有關,但 Win64 不使用語言類型,故省略

底下的 TSTOPA.ASM 是用來檢視各類符號的屬性,其中有個 TSTOPA 巨集程序,它會把符號的屬性印出來。先來看看原始程式,TSTOPA.ASM 就只是呼叫 ExitProcess,然後多次引用 TSTOPA 巨集,就是讓 TSTOPA 巨集程序在組譯時期印出像 num 變數、subr 副程式這些符號的屬性。在 TSTOPA 之後的註解,列出了十進位、十六進位和二進位的結果,以方便對照理解。

;TEST OPATTR:測試OPATTR運算子,並印出其結果
OPTION          CASEMAP:NONE
EXTRN           ExitProcess:PROC,extvar:WORD
INCLUDELIB      e:\masm32\lib64\kernel32.lib
TRUE    EQU     1

TSTOPA  MACRO   arg
  temp  TEXTEQU %OPATTR arg
  %ECHO arg=temp
ENDM
;*********************************************************************
.DATA
num     DQ      112233h
;*********************************************************************
.CODE
;---------------------------------------------------------------------
subr    PROC
        ret     ;subr副程式
subr    ENDP
;---------------------------------------------------------------------
main    PROC
        LOCAL   locvar:QWORD
exit:   xor     rcx,rcx
        call    ExitProcess
        TSTOPA  exit        ;標記   = 37d=25h=0010 0101b
        TSTOPA  subr        ;副程式名稱= 37d=25h=0010 0101b
        TSTOPA  num         ;記憶體變數= 42d=2Ah=0010 1010b
        TSTOPA  locvar      ;區域變數 = 98d=62h=0110 0010b
        TSTOPA  rax         ;暫存器  = 48d=30h=0011 0000b
        TSTOPA  TRUE        ;常數   = 36d=30h=0011 0000b
        TSTOPA  [rcx]       ;間接定址 = 34d=22h=0010 0010b
        TSTOPA  [rcx+8]     ;間接定址 = 34d=22h=0010 0010b
        TSTOPA  extvar      ;外部變數 =170d=AAh=1010 1010b
        TSTOPA  undefn      ;未定義符號=  0d= 0h=0000 0000b
main    ENDP
;*********************************************************************
END

這個程式不製作成可執行檔,僅組譯而已。有兩個理由:①因為 TSTOPA.ASM 第二行定義了外部變數,extvar,如要連結還必須在其他檔案定義 extvar,再包含進來,很麻煩;②這個程式只是用來檢驗 OPATTR 的結果,在組譯時期就可達到目的了。要只組譯不連結,只需在組譯時輸入「-c」或「/c」選項即可 ( 注意!「-c」或「/c」中的 c 必須要小寫 ),如下圖:
底下做個小小的結論:

  1. 如果是未定義的符號,其 OPATTR 結果的第五位元為零,結果亦為零。
  2. 如果是已定義的符號,不論是變數、暫存器、副程式名稱……等,第五位元為一。
  3. 如果是暫存器,除了第五位元為一外,表示為暫存器的第四位元也是一,所以結果為 30H。
  4. 如果是區域變數,除了第五位元為一外,表示在堆疊區段的第六位元及在記憶體內的第一位元均為一,故結果是 62H。
  5. 如果是常數,除了第五位元為一外,表示常數的第二位元亦為一,故結果是 24H。
  6. 如果是在資料區段的變數,除了第五位元為一外,表示在記憶體內的第一位元及以直接定址的第三位元均為一,故結果是 42H。
  7. 如果是標記或副程式名稱,除了第五位元為一外,表示在程式碼區段的第零位元及常數的第二位元均為一,故結果是 25H。( 因為在組譯時,標記及副程式之位址就已經確定,故視為常數,第二位元為一 )
  8. 如果是間接定址,除了第五位元為一外,表示在記憶體內的第一位元亦為一,故結果是 22H。
  9. 如果是外部變數,除了第五位元為一外,表示在記憶體內的第一位元、以直接定址的第三位元、外部符號的第七位元均為一,故結果是 AAH。

在巨集中常用的運算子、參數的使用均已說明完畢,這些不僅是巨集程序使用,其他巨集也會應用到。底下來說明巨集函式。


巨集函式

巨集函式的宣告方式與巨集程序相同,都是使用 MACRO/ENDM,差別只在於前者有回傳值而後者沒有,以及引用方式不太相同,除此之外它們之間差別並不大。引用巨集函式的方式是:

巨集函式名稱(引數列表)

如果巨集函式有多個參數,那麼引用時,引數列表中,引數與引數之間以「,」隔開。如果巨集函式沒有參數,在引用時,仍然要在巨集函式名字之後加上「()」,只是括號內沒有任何引數。如果在原始程式的程式碼區段或資料區段中,引用巨集函式,那麼組譯時,巨集函式的回傳值會取代引用的巨集函式。假如巨集函式內,有 x86 指令或其他指令,也跟巨集程序一樣,會把這些程式碼寫入目的檔及可執行檔中。

EXITM 假指令

在巨集中,可以利用 EXITM 把回傳值傳回,EXITM 的語法是:

EXITM   [textitem]

當組譯器由上而下組譯原始程式的巨集時,如果遇到 EXITM,就會忽略從 EXITM 到 ENDM 之間的巨集程式碼,跳出巨集,並傳回回傳值。

回傳值緊接著 EXITM 之後。上面的語法表明,回傳值以一對「[」、「]」括起來,代表回傳值可以省略。如果省略的話,就沒有回傳值。如果沒有省略的話,textitem 就是巨集函式的回傳值。textitem 必須是字串、或是代表字串的符號名稱,也可以是另一個巨集函式的回傳值。巨集函式可以使用文字分隔運算子 ( 就是一對「<」、「>」),其括起來的內容會轉變為字串。也可以使用展開運算子 ( % ),將數值轉變成字串。

例如底下的 Learn 巨集函式,其內容為:

Learn   MACRO   language
        EXITM   <"&language language is wonderful.">
ENDM
% ECHO  Learn(Assembly)     ;;引用Learn巨集,並把回傳值印在螢幕上

上面的程式碼,會在組譯時期,讓組譯器印出在螢幕上一行字,「Assembly language is wonderful.」。會顯示這一行字串,是因為 Learn 巨集函式的回傳值本身就是字串,所以最後一行的「%ECHO」才會把 Learn 的回傳值印在螢幕上。

因為巨集函式能傳回回傳值,且為一個字串,因此可以將它與 x64 指令或假指令搭配。例如在資料區段中,下面的程式碼:

.DATA
asm     DB      Learn(Assembly)
bas     DB      Learn(BASIC)

結果產生下面的程式碼

.DATA
asm     DB      "Assembly language is wonderful."
bas     DB      "BASIC language is wonderful."

底下還有個巨集函式的例子,可以見底下的 paramx 巨集函式。


文字巨集

組譯器允許使用者將一段文字,或者也可以說是字串,設定為一個符號,而後在原始程式中如果要用到這段文字,就可以用這符號代替。這樣也能精簡原始程式,所以也算是一種巨集,稱為文字巨集。文字巨集只有一種,那就是 TEXTEQU,所以提到文字巨集,其實就是指 TEXTEQU。

TEXTEQU 假指令

TEXTEQU 假指令使一個符號代替一段文字。TEXTEQU 有三種用法,其語法如下:

name TEXTEQU <text>
name TEXTEQU macroId | textmacro
name TEXTEQU %constExpr
  1. 第一種用法是指用一個稱為 name 的符號去代替「<」、「>」括號括起來的字串,text。這個符號,可以把它看成是字串變數。這種用法跟 EQU 假指令,把符號宣告為字串的方法 ( 也就是「符號 EQU <字串>」),有點類似,但還是有點不同:
      ①EQU 無法搭配 % 的運算工作、
      ②以 EQU 宣告的字串不能包含像「!」等運算子。
    如果「name TEXTEQU <text>」不加 <、> 的話,TEXTEQU 就會把 text 當作是另一個符號來進行相等運算,這其實是第二種用法,但是如果右邊的符號未定義,就會出錯。
  2. 第二種用法又可分為兩類:
      ①macroId:macroId 是已經宣告過的巨集函式,這種用法是指把 name 設為 macroID 巨集函式的回傳值。
      ②textmacro:這類用法是把 name 設為另一個文字巨集,textmacro,而此文字巨集必須是先前宣告過的文字巨集,或是
       將已宣告的文字巨集再做運算。
  3. 第三種用法是把 name 設為 %constExpr 所計算出來的數值轉換後的文字。

底下的例子是第一種用法的演示:

param5  TEXTEQU <QWORD PTR [rsp+20h]> ;param5即為「QWORD PTR [rsp+20h]」這個字串

然後我們可以在呼叫 WriteConsoleA API 時,用下面的程式碼,把第五個參數傳給 WriteConsoleA API:

        mov     param5,0

底下的 paramx 巨集函式是第二及第三種用法的演示。它可以用在呼叫像 WriteConsoleA 這種超過四個參數的 Win64 API,把第五個參數,甚至更後面的參數傳給 Win64 API。paramx 內容如下:

paramx  MACRO   x,arg
        disp    TEXTEQU %(x-5)*8+32
        command TEXTEQU <mov QWORD PTR [rsp+disp],arg>
        EXITM   command
ENDM

其中 x 是第幾個參數,從 5 開始,arg 是其引數。例如 WriteConsoleA 的第五個參數是零,那麼在原始程式的程式碼區段裡,要呼叫 WriteConsoleA 時,第五個參數填上零的寫法,可以像下面的方式寫出來:

        paramx(5,0)

在 paramx 中的字串變數 disp 就是第三種用法,配合 % 運算子,把其後的數學運算式計算出來的數值轉換成字串,然後把 disp 之值指定為此字串。例如當 x=5,(5-5)*8+32 為 32,disp 就是「"32"」字串。paramx 中的 command 變數則是第二種用法,將已宣告過的文字巨集再做運算,也就是把剛才的 disp 安插在「"mov QWORD PTR ...arg"」字串中間。最後得到 command 其實就是字串「mov QWORD PTR [rsp+32],0」。

paramx 的最後一行是 EXITM 假指令,它會傳回 command 字串,而此字串會寫在目的檔中,最後會寫入可執行檔中。也就是說,在 paramx(5,0) 巨集函式的這一行,會被「mov QWORD PTR [rsp+32],0」所取代。

巢狀巨集 ( Nesting Macros )

在宣告巨集的地方,再宣告另一個巨集,這種狀況稱為巢狀巨集。一般而言,巢狀巨集的形式如下:

甲   MACRO   甲巨集參數列表
    ⁝
 乙  MACRO   乙巨集參數列表
    ⁝
 ENDM
    ⁝
ENDM

巢狀巨集最常見的情形,就是在巨集內宣告文字巨集。像上面的 paramx 巨集中,又宣告了 disp 與 command 兩個文字巨集。


註一:MASM 名稱

微軟 ( Microsoft ) 的縮寫是 MS,這可以從它二十世紀所販售的作業系統 MS-DOS 就可以知道。因此 MASM 的第一個「M」並不是指微軟。

註二:GREETING1.ASM ( 以巨集改寫後的 GREETING.ASM )

小木偶以第三章的 GREETING.ASM 當作例子,把呼叫 Win64 API 的過程全都製作成巨集,而把呼叫它們的地方變成引用巨集。整個原始程式變成下面的樣子,把它命名為 GREETING1.ASM:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
;GREETING1.ASM把GREETING.ASM中呼叫GetStdHandle與WriteConsoleA的過程改寫成巨集
;組譯方式:
;path e:\masm32\bin64;%path%
;set link=/subsystem:console /entry:main
;ml64 greeting1.asm (或 ml64 /Fl greeting1.asm 製作greeting1.lst列表檔)
INCLUDE GREETING1.INC
;*******************************************************************************
.DATA
hOutput         DQ      ?               ;標準輸出裝置代碼
hInput          DQ      ?               ;標準輸入裝置代碼
;*******************************************************************************
.CODE
;-------------------------------------------------------------------------------
main    PROC
        sub     rsp,28h

        GetStdH STD_OUTPUT_HANDLE,hOutput
        GetStdH STD_INPUT_HANDLE,hInput

;在標準輸出裝置上印出提示字串,當做提示讓使用者明白該輸入什麼
        Print   "請輸入您的姓名(最多四個中文字):"

        Input   YourName                ;請使用者輸入自己的姓名
        Print   <ADDR YourName>,rcx     ;印出使用者姓名
        Print   ",您好嗎?"

exit:   Quit
main    ENDP
;*******************************************************************************
END

如果只看上面的 GREETING1.ASM,似乎與前幾章的組合語言程式碼非常不同,看起來跟高階語言很像,一點都不像是組合語言。的確,這就是巨集令人驚奇的地方。當然,僅有 GREETING1.ASM 是無法組譯成功的,還必須要有底下的 GREETING1.INC 才行。

但是看了 GREETING1.INC 之後,就知道其實骨子裡還是組合語言。不過這些巨集,如果寫得好,其實可以重複利用,所以還是能節省時間。

底下的 GREETING1.INC 中的巨集,有些部分在第六章才會說明。本來照順序,GREETING1.ASM 與 GREETING1.INC 應該放在第六章,但是本章說了太多指令,卻沒有一個完整的應用,會讓人不知如何應用;再者超出本章範圍的部分也不多,僅 IFIDNI/ELSE/ENDIF 與 SUBSTR。為了讓大家能明瞭巨集到底是幹嘛的,就把這個例子放在這裡了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
OPTION          CASEMAP:NONE
EXTRN           GetStdHandle:PROC,WriteConsoleA:PROC,ReadConsoleA:PROC,ExitProcess:PROC
INCLUDELIB      e:\masm32\lib64\kernel32.lib
MAX_NAME        EQU     4*2+2   ;中文姓名最多四個中文字,每個中文字佔兩個位元組,再加上0dH、0aH

STD_INPUT_HANDLE        EQU     -10
STD_OUTPUT_HANDLE       EQU     -11
INVALID_HANDLE_VALUE    EQU     -1
;-------------------------------------------------------------------------------
GetStdH MACRO   nStd,handle
        mov     rcx,nStd                ;;要取得標準輸入裝置還是標準輸出裝置
        call    GetStdHandle            ;;取得標準裝置代碼
        cmp     rax,INVALID_HANDLE_VALUE;;檢查是否呼叫失敗
        je      exit                    ;;若呼叫失敗,跳至exit:
        mov     handle,rax              ;;若呼叫成功,將代碼存起來
ENDM
;-------------------------------------------------------------------------------
;印出字串,sStr可以是字串,也可以是字串的位址:
;1.若為字串,引用時應以兩個「"」括住
;2.若為字串位址,sStr必須是像「ADDR 字串變數名稱」的格式,且slen為字串長度
Print   MACRO   sStr,slen
        LOCAL   string,chrWrt
        lead    SUBSTR <sStr>,1,5       ;;lead為sStr的前五個字元
        .DATA
        chrWrt  DQ      ?               ;;實際寫入多少字元
;;如果sStr是字串的位址
IFIDNI lead,<ADDR >
        .CODE
        address SUBSTR <sStr>,6         ;;address=字串之位址
        lea     rdx,address             ;;使RDX=字串之位址
        mov     r8,slen                 ;;R8=字串長度
ELSE
;;如果sStr是字串
        string  DB      sStr            ;;在資料區段中定義sStr字串
        .CODE
        lea     rdx,string              ;;使RDX=sStr之位址
        mov     r8,SIZEOF string        ;;使R8=sStr之長度
ENDIF
        lea     r9,chrWrt
        mov     rcx,hOutput
        mov     QWORD PTR [rsp+20h],0
        call    WriteConsoleA
ENDM
;-------------------------------------------------------------------------------
;讓使用者輸入一字串。返回時,字串存於string變數之中,且RCX為字串長度
;(單位:位元組),此長度不含0DH、0AH
Input   MACRO   string
        LOCAL   ChrRd
        .DATA
string  DB      MAX_NAME DUP (0)
chrRd   DQ      ?
        .CODE
        mov     rcx,hInput
        lea     rdx,string
        mov     r8,MAX_NAME
        lea     r9,chrRd
        mov     QWORD PTR [rsp+20h],0
        call    ReadConsoleA
        mov     rcx,chrRd               ;;輸入的位元組個數,包含0DH、0AH
        sub     rcx,2                   ;;輸入的位元組個數,不含0DH、0AH
ENDM
;-------------------------------------------------------------------------------
Quit    MACRO   exit_code:=<0>
        mov     rcx,exit_code
        call    ExitProcess
ENDM

INCLUDE 假指令

如果每次使用巨集前,都要在原始程式中先宣告,就很麻煩,因此才有了 INCLUDE 假指令。

我們可以把已製作好的巨集,以及某些常數,都寫保存在一種稱為包含檔或含入檔的純文字檔案中,這種檔案的副檔名通常是「.INC」或「.MAC」,就是 include file 或 macro file 的意思。然後可以在原始程式中以 INCLUDE 假指令將其含括進來,INCLUDE 的語法如下:

INCLUDE 檔案名稱
檔案名稱可以是包含磁碟機、路徑的完整檔名,也可以僅僅是檔案名稱,也可以是相對路徑。這三種情形說明如下:
①、如果檔案名稱是包含磁碟機、路徑……一直到包含檔的主檔名與副檔名,那麼組譯器就只會開啟指定的包含檔,如果無法找到,就會產生錯誤。
②、如果僅僅是檔案名稱,不包含路徑。那麼組譯器會依下面順序搜尋該檔案:
  1. 先到「/I」選項所指定的目錄去尋找。「/I」選項是「ML64.EXE」的一個選項,例如在命令提示字元下組譯時,下達「ml64 /Ie:\homepage\source greeting1.asm」,組譯器就會到「e:\homepage\source」子目錄去搜尋包含檔。
  2. 當前目錄。
  3. 如果使用者有事先定義「INCLUDE」環境變數,那麼就會到此目錄下去搜尋。設定 INCLUDE 環境變數,必須用命令提示字元的「SET INCLUDE=」指令,同時 INCLUDE 可以設定在多個子目錄中搜尋,中間以「;」隔開。
③、如果檔案名稱包含相對路徑,那麼組譯器會把 INCLUDE 後的路徑接在②所提到的三種目錄之後,也就是說 INCLUDE 後的路徑當成是這三個目錄的子目錄。例如原始程式中是「INCLUDE XC\GREETING1.INC」,而組譯時輸入「ml64 /IE:\HomePage GREETING1.ASM」,那麼組譯器會開啟「E:\HomePage\XC\GREETING1.INC」包含檔。

註三:列表檔

列表檔是組譯器將原始程式組譯後,照實列出的每一行指令的機械碼及位址的純文字檔案,其副檔名為「.LST」。可以用印表機列印出來,對於除錯很有幫助。「展開巨集」的結果,也可以在列表檔看出來。

要產生列表檔,可以在組譯原始程式時,輸入「/Fl」選項即可,如下:

E:\HomePage\SOURCE\Win64\CONSOLE>ml64 /Fl greeting1.asm [Enter] →組譯greeting1.asm,同時製作列表檔
Microsoft (R) Macro Assembler (x64) Version 14.25.28614.0
Copyright (C) Microsoft Corporation.  All rights reserved.

 Assembling: greeting1.asm
Microsoft (R) Incremental Linker Version 14.25.28614.0
Copyright (C) Microsoft Corporation.  All rights reserved.

/SUBSYSTEM:CONSOLE /ENTRY:main
/OUT:greeting1.exe
greeting1.obj
E:\HomePage\SOURCE\Win64\CONSOLE>

選項「/Fl」也可以用「-Fl」,其中的 F 一定要大寫,l 一定要小寫 ( 此為英文字母「L」的小寫並非阿拉伯數字的「1」)。在「/Fl」之後也可以加上檔案名稱,這檔案名稱與「/Fl」之間不能有空格。如果加上檔案名稱,列表檔檔名就是「/Fl」之後的檔案名稱。如果不加,就用預設的列表檔;預設的列表檔檔案名稱,其主檔名是組合語言原始程式的主檔名,副檔名是「.LST」。

列表檔的最後面,會有原始程式所有符號列表。這些符號包含巨集、副程式、變數等等,以及這些符號的性質。在除錯時,這些性質也能提供些許幫助。

其中,比較特別的是區段名稱。在第一章時,只提過程式碼與資料在不同的區段,而資料區段又分三種。如今可以由列表檔看出來,這些區段其實都有自己的名稱,如下表:

宣告方式類型名稱
.CODE程式碼區段_TEXT
.DATA資料區段_DATA
.CONST常數資料區段CONST
.DATA?沒有初始值的資料區段_BSS