Ch 11 EXE 可執行檔

組譯成 EXE 檔

在前面章節內的程式,小木偶都是以製作成 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 後面有許多選項可供選擇,當您寫副程式製作成程式庫,或是寫很大的程式把它切割成數個小程式再合併時,要注意這些選項。其語法是:

區段名  segment  排列型式   合併型式   類別名

排列型式 (align type) 是告訴連結程式該區段由那一種位址開始,或者說此區段排在前一區段後的那一種位址開始。可以選擇的位址種類有下面幾種:

  1. byte:由某一位元組開始,也就是任意位址開始,視載入 DOS 驅動程式多寡以及前面區段大小決定。
  2. word:由某一字組開始,也就是從偶數位址開始。
  3. dword:由某個雙字組開始,一個雙字組是 4 個位元組。
  4. para:由某一節開始,節的英文是 paragraph,一節的大小是 10H 個位元組,也就是十進位的 16 個位元組。
  5. page:由某一頁 (page) 開始,一頁是 100H 個位元組。

前面幾章的例子中,都省略排列形式,則連結器自動用 para 選項,也就是說沒有排列形式時,MASM 內定選項是 para。

合併型式 (combine type) 是告訴連結器該區段和程式庫內或其他目的檔內的那一個區段連結在一起成為一個區段。可以用的選項有:

  1. none:不與其他區段合併。
  2. public:具有相同名稱且相同類別名的區段合併成一個區段。
  3. common:具有相同名稱的區段合併成一個區段。
  4. at 位址:區段被置於特定位址。
  5. stack:表示此區段為堆疊區段。
  6. memory:使區段置於最高位址。

雖然 segment 的用法很複雜,但是最常用的還是『stack』、『public』。因為在主程式中要使用程式庫,或者要把某個副程式加入程式庫都必須宣告『public』及類別名,使連結程式 (LINK.EXE) 能順利把各區段正確連結。

DUP 修飾語

DUP 語法是

dw      數值 dup (數字)
db      數值 dup (數字或字串)

DUP 是接在 DW 或 DB 假指令後的一個修飾詞 ( 或陳述式 ),DUP 的目的是用來複製跟在 DUP 後面用括號刮起來的字,複製的次數在 DUP 前面的數值表示。IBM 個人電腦巨集組譯器手冊建議程式設計師把堆疊用 "stack " 字串填滿,要使這個字串重複多次,就可以用 DUP 使其重複。


用 DEBUG 觀察 EXE 檔在記憶體中的情形

現在我們用 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 

PSP 程式前置區

可以看到程式碼從 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 檔的格式做個總結

  1. 用 segment 及 ends 虛指令設定資料段、堆疊段、程式碼段。
  2. 用 segment 的選項 stack 設定堆疊段;並用 dw 或 db dup 設定適當堆疊段大小。
  3. 用 assume 將 CS、DS 暫存器與程式碼區段、資料段連起來。
  4. 程式碼區段一開始就要把 PSP 的內容保存在堆疊內,以便順利返回 DOS。
  5. 將 DS 暫存器確實指到資料段。

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