在前面章節內的程式,小木偶都是以製作成 COM 檔的格式寫成。事實上,在 DOS 的可執行檔有兩種,COM 及 EXE;但是在 Windows 3.1/9x/Me 下的可執行檔是 EXE,而 COM 檔只能在 DOS 模式下執行,無法在 Windows 的圖形模式下執行。在古早以前的 8 位元時代裡,受限於硬體,其執行檔大小不能超過 64K,而且其程式碼、資料以及堆疊都放在同一區段,這就是 COM 檔的由來。後來到了 16 位元時代,為了執行更大的程式,處理更多的資料,無可避免的是執行檔的大小勢必得增加了,EXE 檔因而誕生了。
EXE 檔將程式碼、堆疊、與資料分開來了,分別放在不同的區段內,雖然每個區段的長度還是無法超過 64K,但是總長度可以超過 64K。如果您的程式太大,那組譯器也允許您設一個以上的程式段或其他區段,需要時只要將區段暫存器指向該處,就可以超越區段的限制了。
這樣的處理方式最大的好處是可以處理很大的資料,程式碼也可以很大,程式功能也就日益強大了。另一個好處是程式碼、堆疊、與資料分開了,就不怕程式碼及堆疊不小心被覆蓋而造成當機。
為了達到程式碼、資料、堆疊在不同區段,在寫原始程式時,就要先分好。一般而言,EXE 檔原始程式的格式如下:
;*************************************** data segment ;資料區段 ;資料放置處 data ends ;*************************************** code segment ;程式碼區段 assume cs:code,ds:data ;--------------------------------------- main proc far ;主程式開始處 start: push ds sub ax,ax push ax mov ax,data mov ds,ax ;程式碼放置 ret ;返回 DOS main endp ;主程式結束處 ;--------------------------------------- subrout proc near ;副程式開始處 ;程式碼放置處 subrout endp ;--------------------------------------- code ends ;*************************************** stack segment stack ;堆疊區段 dw xxx dup (?) stack ends ;*************************************** end start
在寫 EXE 原始檔時,大致的格式就跟上面一樣,必須在上面的『資料放置處』和『程式碼放置處』加進資料和程式而已。在上面的格式中,區段名稱,如 data、code、stack 都是可以任意取的,但是要以英文字母、底線為開頭,並避免組合語言保留字,至於那個區段在前,那一個在後並不重要。每個區段都是以 segment 開始,到 ends 結束,要注意的是,雖然在原始程式中,編譯時可以避免程式段及堆疊段被覆蓋的問題,但是如果因為設計者邏輯上的錯誤而造成覆蓋,在執行時系統並不會提出警告。
原始程式的堆疊段還要加上一個 stack,表示這個程式是堆疊段,至於堆疊段的大小可以任意設,但是設得稍微大一點比較好,堆疊段的大小是以 xxx 字組表示。至於程式碼,應該很容易就可以用『進入點』來區別。程式的進入點都是在原始程式的最後一行,以
end 標號
的方式表示,這個進入點的位址就是 end 後面的標號所在位址,它是可以在程式碼區段的任何位址,我們要做的就是將進入點前冠上 end 假指令。當連結器製作出 EXE 檔時,會把這個位址寫入 EXE 檔內,當系統載入程式時,會找到這個位址並將控制權交給它,開始執行程式。
此外,在程式碼區段 (就是 code 區段) 應該再分出主程式和副程式來,主程式主要負責主要流程或跳躍,副程式主要負責較瑣碎的雜事或常常用到的程式片段。主程式名後面接『far 來表示由 DOS 將控制權交給程式時是遠程呼叫,所謂遠程呼叫就是所要呼叫的程式是在不同區段,而在主程式中,第一步必須將 DS 和 0H 推入堆疊,小木偶稍後在說明理由。
好吧,現在小木偶就將第一章的 exam01.asm 改寫成 EXE 檔,並重新命名為 exam02.asm,改寫後的程式如下:
;*************************************** ;01 message segment ;02 mes db 'Hi, I learn assembly.$' ;03 message ends ;04 ;*************************************** stack segment stack ;06 db 20 dup ('stack123') ;07 stack ends ;08 ;*************************************** pnt_msg segment ;10 assume cs:pnt_msg,ds:message ;11 ;--------------------------------------- main proc far ;13 start: push ds ;14 sub ax,ax ;15 push ax ;16 mov ax,message ;17 mov ds,ax ;18 mov dx,offset mes ;20 mov ah,9 int 21h mov ax,4c00h int 21h ;24 main endp ;--------------------------------------- pnt_msg ends ;27 ;*************************************** end start ;29
將上述原始程式寫好後,存成 exam02.asm,再組譯、連結即可,不必再用 EXE2BIN 轉換成 COM 檔 ( 即使您想轉換成 COM 檔,也因為已經定義了三個區段,所以根本做不到。質言之,並不是所有原始碼都可以組譯成 *.COM 執行檔,只有程式碼區段、資料區段、堆疊區段等所有區段都在同一區段的原始碼才可組譯成 *.COM 執行檔。 )。
H:\HomePage\SOURCE>path h:\homepage\masm50 [Enter] H:\HomePage\SOURCE>masm exam02; [Enter] Microsoft (R) Macro Assembler Version 5.00 Copyright (C) Microsoft Corp 1981-1985, 1987. All rights reserved. 51554 + 381838 Bytes symbol space free 0 Warning Errors 0 Severe Errors H:\HomePage\SOURCE>link exam02; [Enter] Microsoft (R) Personal Computer Linker Version 2.40 Copyright (C) Microsoft Corp 1983, 1984, 1985. All rights reserved. H:\HomePage\SOURCE>exam02 [Enter] Hi, I learn assembly. H:\HomePage\SOURCE>dir exam02*.* [Enter] Volume in drive H is DATA_1 Volume Serial Number is 0330-08F6 Directory of H:\HomePage\SOURCE EXAM02 ASM 1,128 07-11-02 5:11 EXAM02.ASM EXAM02 LST 2,110 07-11-02 5:08 EXAM02.LST EXAM02 OBJ 186 07-14-02 23:37 EXAM02.OBJ EXAM02 MAP 233 07-11-02 5:08 EXAM02.MAP EXAM02 EXE 725 07-14-02 23:38 EXAM02.EXE 5 file(s) 4,382 bytes 0 dir(s) 6,893.41 MB free
執行後,並沒有問題,現在先來看看新的指令。
之前已經提過 SEGMENT 假指令了,事實上 SEGMENT 後面有許多選項可供選擇,當您寫副程式製作成程式庫,或是寫很大的程式把它切割成數個小程式再合併時,要注意這些選項。其語法是:
區段名 segment 排列型式 合併型式 類別名
排列型式 (align type) 是告訴連結程式該區段由那一種位址開始,或者說此區段排在前一區段後的那一種位址開始。可以選擇的位址種類有下面幾種:
前面幾章的例子中,都省略排列形式,則連結器自動用 para 選項,也就是說沒有排列形式時,MASM 內定選項是 para。
合併型式 (combine type) 是告訴連結器該區段和程式庫內或其他目的檔內的那一個區段連結在一起成為一個區段。可以用的選項有:
雖然 segment 的用法很複雜,但是最常用的還是『stack』、『public』。因為在主程式中要使用程式庫,或者要把某個副程式加入程式庫都必須宣告『public』及類別名,使連結程式 (LINK.EXE) 能順利把各區段正確連結。
DUP 語法是
dw 數值 dup (數字) db 數值 dup (數字或字串)
DUP 是接在 DW 或 DB 假指令後的一個修飾詞 ( 或陳述式 ),DUP 的目的是用來複製跟在 DUP 後面用括號刮起來的字,複製的次數在 DUP 前面的數值表示。IBM 個人電腦巨集組譯器手冊建議程式設計師把堆疊用 "stack " 字串填滿,要使這個字串重複多次,就可以用 DUP 使其重複。
現在我們用 DEBUG 載入這個程式,看看記憶體內發生了什麼事。這個程式雖然不用下參數,但是為了觀察方便,小木偶還是隨便下一個參數,『XXyyZz』,它不會影響程式執行。
H:\HomePage\SOURCE>debug exam02.exe XXyyZz [Enter] -r [Enter] AX=0000 BX=0000 CX=00D5 DX=0000 SP=00A0 BP=0000 SI=0000 DI=0000 DS=1C8C ES=1C8C SS=1C9E CS=1CA8 IP=0000 NV UP EI PL NZ NA PO NC 1CA8:0000 1E PUSH DS -d DS:0 L120 [Enter] 1C8C:0000 CD 20 00 A0 00 9A F0 FE-1D F0 4F 03 F1 15 8A 03 . ........O..... 1C8C:0010 F1 15 17 03 F1 15 E0 15-01 01 01 00 02 FF FF FF ................ 1C8C:0020 FF FF FF FF FF FF FF FF-FF FF FF FF 7D 1C 4C 01 ............}.L. 1C8C:0030 F8 19 14 00 18 00 8C 1C-FF FF FF FF 00 00 00 00 ................ 1C8C:0040 07 0A 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 1C8C:0050 CD 21 CB 00 00 00 00 00-00 00 00 00 00 58 58 59 .!...........XXY 1C8C:0060 59 5A 5A 20 20 20 20 20-00 00 00 00 00 20 20 20 YZZ ..... 1C8C:0070 20 20 20 20 20 20 20 20-00 00 00 00 00 00 00 00 ........ 1C8C:0080 07 20 58 58 79 79 5A 7A-0D 65 78 65 20 58 58 79 . XXyyZz.exe XXy 1C8C:0090 79 5A 7A 0D 00 00 00 00-00 00 00 00 00 00 00 00 yZz............. 1C8C:00A0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 1C8C:00B0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 1C8C:00C0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 1C8C:00D0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 1C8C:00E0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 1C8C:00F0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 1C8C:0100 48 69 2C 20 49 20 6C 65-61 72 6E 20 61 73 73 65 Hi, I learn asse 1C8C:0110 6D 62 6C 79 2E 24 00 00-00 00 00 00 00 00 00 00 mbly.$.......... -d [Enter] 1C8C:0120 73 74 61 63 6B 31 32 33-73 74 61 63 6B 31 32 33 stack123stack123 1C8C:0130 73 74 61 63 6B 31 32 33-73 74 61 63 6B 31 32 33 stack123stack123 1C8C:0140 73 74 61 63 6B 31 32 33-73 74 61 63 6B 31 32 33 stack123stack123 1C8C:0150 73 74 61 63 6B 31 32 33-73 74 61 63 6B 31 32 33 stack123stack123 1C8C:0160 73 74 61 63 6B 31 32 33-73 74 61 63 6B 31 32 33 stack123stack123 1C8C:0170 73 74 61 63 6B 31 32 33-73 74 61 63 6B 31 32 33 stack123stack123 1C8C:0180 73 74 61 63 6B 31 32 33-73 74 61 63 6B 31 32 33 stack123stack123 1C8C:0190 73 74 61 63 6B 31 32 33-73 74 61 63 6B 31 32 33 stack123stack123 -d [Enter] 1C8C:01A0 73 74 61 63 6B 31 32 33-73 74 61 63 6B 31 32 33 stack123stack123 1C8C:01B0 73 74 61 63 6B 31 32 33-73 74 61 63 6B 31 00 00 stack123stack1.. 1C8C:01C0 1E 2B C0 50 B8 9C 1C 8E-D8 BA 00 00 B4 09 CD 21 .+.P...........! 1C8C:01D0 B8 00 4C CD 21 86 74 FF-50 8D 86 76 FF 50 8D 46 ..L.!.t.P..v.P.F 1C8C:01E0 FA 50 FF 76 04 E8 6E FF-83 C4 08 8D 86 78 FF 50 .P.v..n......x.P 1C8C:01F0 8D 46 FC 50 8D 46 FE 50-FF 76 06 E8 58 FF 83 C4 .F.P.F.P.v..X... 1C8C:0200 08 2B F6 8B 7E FE 4F 0B-FF 74 16 8D 86 7B FF 50 .+..~.O..t...{.P 1C8C:0210 8B 46 06 40 50 57 E8 49-0F 83 C4 06 8B 76 FE EB .F.@PW.I.....v.. -u cs:0 [Enter] 1CA8:0000 1E PUSH DS 1CA8:0001 2BC0 SUB AX,AX 1CA8:0003 50 PUSH AX 1CA8:0004 B89C1C MOV AX,1C9C 1CA8:0007 8ED8 MOV DS,AX 1CA8:0009 BA0000 MOV DX,0000 1CA8:000C B409 MOV AH,09 1CA8:000E CD21 INT 21 1CA8:0010 B8004C MOV AX,4C00 1CA8:0013 CD21 INT 21 1CA8:0015 8674FF XCHG DH,[SI-01] 1CA8:0018 50 PUSH AX 1CA8:0019 8D8676FF LEA AX,[BP+FF76] 1CA8:001D 50 PUSH AX 1CA8:001E 8D46FA LEA AX,[BP-06]
用 DEBUG 載入後,先觀看各暫存器,發現 DS 區段暫存器之值為 1C8CH,再看看其內容,發現 1C8C:0000 這個位址後的 100H 才是我們所定義的資料區段 (白色處),但是在原始程式中,我們根本沒有定義 mes 字串要從偏移位址 100H 開始(見原始程式前三行),照理講 mes 之偏移位址應該為 0000H 才對。原來 DOS 載入 EXE 可執行檔時,一開始 DS 暫存器並非指向原始程式的資料區段,而是指向 PSP(稍後說明)。所以在原始程式的第 17 行,才會有將 message 區段移入 AX 暫存器,然後再存到 DS 暫存器中,這樣才能確保 DS 指向小木偶所指定的區段。當我們在 DEBUG 反組譯時,看到
1CA8:0004 B89C1C MOV AX,1C9C 1CA8:0007 8ED8 MOV DS,AX
這一行,發現事實上當 DOS 載入 exam02.exe 時,DOS 將 message 區段設為 1C9CH,而 1C9C:0000 這個位址事實上就是 1C8C:0100 位址,也就是資料區段 message 的位址。
讓我們來看看 stack 區段吧。在原始碼的第七行,小木偶定義了堆疊段的大小是
db 20 dup ('stack123')
表示堆疊內容是以『s』、『t』、『a』、『c』、『k』、『1』、『2』、『3』這八個 ASCII 字元共 8 個位元組為一組,總共 20 組,所以總長度是 160 個位元組,換算成十六進位就是 0A0H 個位元組。而堆疊段是在資料段的後面,所以是從 1C8C:0120 這個位址開始,到 1C8C:01BF 為止。而 1C8C:0120 這個位址就相當於 1C9E:0000 (1C8C+12=1C9E)。所以當我們用 DEBUG 的 r 指令來觀看時,SS 段暫存器的數值就是 1C9E,而 SP 暫存器是指向堆疊段的最高位址。這種情形就像 COM 一樣,只是 COM 檔 SP 最高位址是 FFFE,而 EXE 因為設有堆疊段,所以最高位址會依堆疊段之大小而不同。
-r [Enter] AX=0000 BX=0000 CX=00D5 DX=0000 SP=00A0 BP=0000 SI=0000 DI=0000 DS=1C8C ES=1C8C SS=1C9E CS=1CA8 IP=0000 NV UP EI PL NZ NA PO NC 1CA8:0000 1E PUSH DS -t [Enter] AX=0000 BX=0000 CX=00D5 DX=0000 SP=009E BP=0000 SI=0000 DI=0000 DS=1C8C ES=1C8C SS=1C9E CS=1CA8 IP=0001 NV UP EI PL NZ NA PO NC 1CA8:0001 2BC0 SUB AX,AX -t [Enter] AX=0000 BX=0000 CX=00D5 DX=0000 SP=009E BP=0000 SI=0000 DI=0000 DS=1C8C ES=1C8C SS=1C9E CS=1CA8 IP=0003 NV UP EI PL ZR NA PE NC 1CA8:0003 50 PUSH AX -t [Enter] AX=0000 BX=0000 CX=00D5 DX=0000 SP=009C BP=0000 SI=0000 DI=0000 DS=1C8C ES=1C8C SS=1C9E CS=1CA8 IP=0004 NV UP EI PL ZR NA PE NC 1CA8:0004 B89C1C MOV AX,1C9C -d SS:80 L20 [Enter] 1C9E:0080 73 74 61 63 6B 31 32 33-73 74 61 63 6B 31 32 33 stack123stack123 1C9E:0090 73 74 00 00 00 00 04 00-A8 1C F1 15 00 00 8C 1C st.............. -
現在,讓我們來看看程式碼區段吧 (也就是 pnt_msg 區段)。在 DEBUG 中,輸入
-u cs:0
可以看到程式碼從 1CA8:0000 開始,而 CS:IP 這兩個暫存器也是指向1CA8:0000,前三個指令 (也就是原始程式的第 14 行到第 16 行) 是把 DS 及 0000H 推入堆疊,為何要這樣做呢?原因是當我們結束 exam02.exe 後,必須把控制權交回給 DOS 的某處,然後 DOS 等待使用者命令才能繼續執行下一道應用軟體。exam02.exe 交還控制權時,當然不是將控制權交到任何位址都可以,所以 DOS 必須把一些資料傳給 exam02.exe,告訴 exam02.exe 要把控制權交到那裡。而這些資料,就放在 PSP 內。PSP 稱為『程式前置區』(program segment prefix,簡稱 PSP ) 是 DOS 與應用程式傳遞資訊的一塊區域,DOS 載入應用程式時,DS 所指的區段位址開始共 100H 的區域就是 PSP。
事實上,您可以把 DOS 看成是主程式,而應用軟體或我們所寫的程式是副程式。當應用軟體 (exam02.exe) 執行時,好像 DOS 是呼叫一個遠程的副程式(所謂遠程是指不同區段之間的呼叫)。當呼叫副程式時,不管遠程或近程都會把主程式下一個要執行的位址推入堆疊,如果近程呼叫,因主、副程式都在同一區段,只需把下一個要執行的偏移位址推入堆疊即可,如果是遠程呼叫還要把區段位址也推入堆疊。待結束應用程式返回 DOS 時,系統可以由堆疊取回 DOS 接收控制權的位址。而這個位址其實就是 PSP 的最前端。因此在程式一開始,就應該把 DS 及 0000 推入堆疊。
最後補充一點,其實在程式的第 23、24 行,小木偶以 AH=4CH/INT 21H 結束程式,但是也可以用 ret 來結束程式,結果是一樣的。
最後,小木偶把 EXE 檔的格式做個總結