四 基本的駐留程序
4.1 一個基本的COM程序
DOS之下有兩種形式的可執行文件,這兩種文件分別是COM文件和EXE文件.其中,COM文件可以迅速地加載和執行,但是其大小不能超過64K字節,只能有一個段,代碼段.而且起始地址為100H指令必須為程序的啟動指令.EXE文件可以加載到許多個段中,因此程序的大小沒有限制,但是程序加載的過程就比較慢,而且對於內存駐留程序來說還會造成更大的麻煩.
以下是一個可以正確執行的COM文件,但其內容是空的;只是一個COM文件的框架,可以把你寫的任何應用部分加在這個文件中,形成一個COM格式的內存駐留程序:
;Section 1
cseg segment
assume cs:cseg,ds:cseg
org 100h
;Section 2
start:
ret
;Section 3
cseg ends
end start
上面的程序可以分成三部分,第一部分定義了代碼段和數據段分別放在程序中的位置,以及執行代碼的起始地址.第二部分是可執行的程序,在這個例子只一個RET指令而已.第三部分是程序包段的終結,其中END敘述包含了程序開始執行地址.
若是把上面的程序經過匯編連接,你會發現所產生的COM文件只有一個字節長.這是因為所產生的COM文件沒有程序段前綴(Programsegmetn profix),因為在DOS下所有和COM文件都有相同的程序段前綴.當DOS加載一個COM文件到內存中時,就會自動地產生一份正確的程序段前綴.一個程序在執行的過程中,可以根據需要修改其程序段前綴,但是在一開始,所有COM文件的程序前綴都是相同的.下面是程序前綴的格式.
偏移位置 含義
0000H 程序終止處理子程序地址(INT 20H)
0002H 分配段的結束地址,段值
0004H 保留
0005H 調用DOS的服務
000AH 前一個父程序的IP和CS
000EH 前一個父程序的CONTROL_C處理子程序地址
0012H 前一個父程序包的硬件錯誤處理子程序地址
0016H 保留
002CH 環境段的地址值
005EH 保留
005CH FCB1
006CH ` FCB2
0080H 命令行的參數和磁盤轉移區域
4.2 一個最小的內存駐留程序
上面的程序只是一個一般的DOS程序而已.並不是內存駐留的.以下是一個基本的內存駐留程序結構:
;Section 1
cseg segment
assume cs:cseg;ds:cseg
org 100h
start: ;Section 2
nop
done: ;Section 3
mov dx,offset done
int 27h
;Section 4
cseg ends
end start
和前一個程序相比,這個程序只是增加了一個DONE部分.這個部分使用了INT 27H這個中斷調用,來終止並駐留在內存(Terminate and Stay Resident)中.使用INT 27H這個中斷調用時,必須設定好一個指針,讓這個指針指向內存中可以使用的部分,事實上,這就相當於設置一個COM文件可加載的位置.另外DOS還提供了INT 21H,AH=31H(駐留程序,Keep process),但是使用這個中斷調用時,我們必須設定所保留的內存大小,而不是設定一個指針;另外這個中斷調用會送出退出碼.
使用INT 27H時,必須設定一個指針指向可用存儲位置的開頭,以便讓DOS用來加載稍後執行的程序.DOS本身有一個指針,這個指針是加載COM文件或EXE文件時的基准地址值.INT尿27H 會改變這個指針或為新的數值.同時造成新指針和舊指針之間的存儲空間無法讓DOS使用因此這樣做會造成可用存儲位置愈來愈少.
調用INT 27H時所使用的指針是個FAR指針,其中DX存放的是位移指針(Offset pointer),它可以指到64K字節之內的范圍.而DOS是段指針(Segment pointer),它可以指到IBM PC中640K字節的任何一個段.在上面的例子中,DS的內容不必另外設定,因為當COM文件加載時,DS的內容就CS的內容相同了.
經常在編寫匯編程序時,常犯的一個錯誤就是:把assume ds:cseg這個敘述誤認為是,存放某一預設值到DS中,事實上,匯編語言程序中的Assume敘述不會產生任何的程序代碼,這個功能是告訴匯編器做某些必要的假設,以便正確地匯編程序.譬如以下的程序:
cseg segment
.............
assume ds:cseg
mov ah,radix
.............
radix db 16
.............
cseg ends
上面的程序匯編時,當匯編器看到mov ah,radix這個指令時,它就根據assume ds:cseg來產生一定形式的賦值指令.在面的Assume ds:cseg敘述是告訴匯編器,數據段就位於目前的代碼段中.這是內存駐留程序的一項重要關鍵.如果DS的內容和CS不相同時,無論是否有assume 敘述,程序執行時都會失敗.
4.3 改良的內存駐留程序
上面所介紹的內存駐留程序實際上沒有做任何事,只是駐留在內存中而已.事實上,在START和END之間放入任何程序代碼,都只會執行一次而已然後就永遠駐留在內存中,除非是使用轉移指令轉到START的地址去,否則將永遠無法被使用.還要注意一點,START的地址值並非固定不變,它會根據程序執行時計算機的狀態而改變.
下面的這個程序只是把需要駐留的程序代碼裝載好,但是並不會執行.
;Section 1
cseg segment
assume cs:cseg,ds:cseg
org 100h
;Section 2
start:
jmp initialize
;Section 3
app_start:
nop
initialize:
;Section 4
mov dx,offset initialize
int 27h
;Section 5
cseg ends
end start
上面的程序一開始執行時就傳到initialize標志的地方,裝置好駐留在內存的應用部分.原先的DONE已經改成initialize,而駐留在內存的程序代碼則放在App_Start 和Initialize之間.
另外,你也許注意到了,程序的起始地址並不是Initialize而是Start.這是因為所有COM程序的起始地址都是100H;而上面的程序中Start是放在100H的地方.如果把Initialize放在End之後,Initialize就變成起始地址,但是這樣的程序無法透過EXE2BIN轉換成COM文件了.如果無法產生COM文件時,那麼就必須直接處理段的內容.
4.4 減少內存的額外負擔
到目前為止,都沒有接觸到程序前綴,當使用INT 27H時,事實上是把指針以前的東西都保留在內存中,這也包括了COM的程序段前綴.因為COM文件執行完畢後,才可以把程序段前綴移掉.
從上面的事實可以看出:如果程序段前綴只能在COM裝置程序結束後才可以移去,那麼就可以由駐留在內存中的程序代碼完成.要做到這一點,可以把整個程序往下移動256個字節.但又如何做到這一點呢?我們可以設定一個標志(Flag),用來指示這個程序是否執行過.如果這個駐留程序或是第一次執行時,就把整個程序往下移動256個字節,以便把程序段前綴移去.但是如果駐留程序在裝置好之後,經過一段長時間仍然沒有被執行時,怎麼辦呢?如果同時載入了好幾個駐留程序時,雙該如何呢?這些重要的事情都需要使用不同的程序代碼來解決.如果說這些程序代碼超出了256字節時,那麼所占用的存儲位置就超出程序段前綴所浪費的空間.有些人用一些比較簡短的代碼來解決這個問題,但是還是比較麻煩.因此對於大部分的內存駐留程序而言,除非存儲空間太少,以至於256字節變得很重要,否則最好不要去處理程序段前綴,這樣子會讓你的程序簡潔而且容易閱讀.
第二個重要的指令是IRET,從中斷返回(Return from interrupt).IRET的功能和RET極相似,RET是用來從被調用 的子程序中返回,而IRET則是用來從中斷程序返回.但是使用IRET返回時,它會從堆棧中先取出返回的地址值,然後再取出CPU的狀態標志(State Flag).CPU的狀態標志在CPU接受中斷時,會自動地推入堆棧中.因此執行IRET指令後,CPU的狀態就恢復成未中斷前的狀態;也就是說CPU就可以繼續接受外界的中斷(CPU狀態標志中斷包括了中斷標志).嚴格地說,STI和IRET在這個例子中都是多余的,但是對於實際的中斷處理程序而言,這兩個指令都很重要.
另外,使用設置中斷矢量的中斷調用時,暫存器AL必須存入所要設置的中斷矢量,而中斷矢量指針則必須放到暫存器DS:DX中.
4.6 連接中斷處理程序
若是把前一節的程序拿來執行時,鍵盤是無法輸入的,事實上,處理鍵盤的硬件中斷處理程序會繼續地讀取敲入的字符,並且放到等待隊列中,直到隊列填滿為止;但是由於讀取等待隊列的軟件中斷INT 16H已經被改變了,因此隊列的內容就永遠取不出來.
現在寫一個中斷處理程序,這個中斷處理程序只是調用原先的鍵盤中斷處理程序,一旦做到這一點之後,接下來就可以根據鍵盤的輸入做修改.以下就是調用原先鍵盤處理程序的駐留程序:
cseg segment
assume cs:cseg,ds:cseg
org 100h
start:
jmp Initialize
Old_Keyboard_IO dd ?
;Section 1
new_keyboard_io proc far
sti
;Section 2
pushf
assume ds:nothing
call Old_Keyboard_IO
nop
iret
new_keyboard_io endp
;Section 3
Initialize:
assume cs:cseg,ds:cseg
mov bx,cs
mov ds,bx
mov al,16h
mov ah,35h
int 21h
mov word ptr Old_Keyboard_IO,bx
mov word ptr Old_Keyboard_IO[2],es
;End Section 3
mov dx,offset new_keyboard_io
mov al,16h
mov ah,25h
int 21h
mov dx,offset Initialize
int 27h
cseg ends
end start
上面的程序中,第一部分是兩個字(Double word),這是用來存放舊的鍵盤中斷矢量.因為COM的程序都只限制在一個段中,因此數據段和代碼段都在同一段中.而原先的中斷處理程序和我們所編寫的中斷處理程序未必會在同一段中,所以必須使用雙字來儲存地址值.
雙字Old_Keyboard_IO可以放在駐留程序中的任何地方;但是一般來說,放在Jmp Initialize 之後會比較方便;因為如果必須使用DEBUG來檢查程序的話,可以比較容易調試.
上面程序中的第二部分是駐留程序的主體,其中包括了一個調用原先鍵盤中斷處理程序的模擬中斷.因為原先的鍵盤中斷處理程序必須使用INT的方式調用,而不是使用CALL的指令調用;因此必須先使用PUSHF把CPU狀態標志壓入堆棧中,然後配合上CALL來模擬INT的動作.
注意一點,assume ds:nothing這一行是匯編指示,而不是程序代碼.它是用來告訴匯編器在產生下一行機器碼時,不要更會目前DS的內容;這樣做才可以讓匯編器為下一個指令產生雙字的地址值.
當Call Old_Keyboard_IO指令執行時,控制權就轉移到舊的鍵盤中斷處理程序.而當這個中斷調用執行完時,它就執行IRET指令,於是控制權又交還到目前的駐留程序.這樣做,不但可以讓原先的鍵盤中斷程序包為我們工作,同時也可以掌握控制權.如果只使用IMP指令,跳到舊的鍵盤中斷處理程序包去,而不把CPU狀態標志推入堆棧中,那麼一旦執行到IRET時,就真正返回到中斷的狀態.
上面程序中的第三部分是啟動代碼部分,在這一部分中,設定好新的中斷矢量,同時把舊的中斷矢量存放在駐留程序代碼中,以便讓駐留程序使用.
4.7 檢查駐留程序
到目前為止,已經成功地把駐留程序加在應用程序和DOS的鍵盤輸入之間;接下來可以修改輸入的字符.在這一節中,我們准備截獲鍵盤的輸入,並且把"Y"改成"y","y"改成"Y".