用匯編編寫(xiě)DOS下的內(nèi)存駐留程序(4)
Posted on 2007-09-30 08:19 David.Ko 閱讀(249) 評(píng)論(0) 編輯 收藏 所屬分類: 病毒四 基本的駐留程序
4.1 一個(gè)基本的COM程序
DOS之下有兩種形式的可執(zhí)行文件,這兩種文件分別是COM文件和EXE文件.其中,COM文件可以迅速地加載和執(zhí)行,但是其大小不能超過(guò)64K字節(jié),只能有一個(gè)段,代碼段.而且起始地址為100H指令必須為程序的啟動(dòng)指令.EXE文件可以加載到許多個(gè)段中,因此程序的大小沒(méi)有限制,但是程序加載的過(guò)程就比較慢,而且對(duì)于內(nèi)存駐留程序來(lái)說(shuō)還會(huì)造成更大的麻煩.
以下是一個(gè)可以正確執(zhí)行的COM文件,但其內(nèi)容是空的;只是一個(gè)COM文件的框架,可以把你寫(xiě)的任何應(yīng)用部分加在這個(gè)文件中,形成一個(gè)COM格式的內(nèi)存駐留程序:
;Section 1
cseg segment
assume cs:cseg,ds:cseg
org 100h
;Section 2
start:
ret
;Section 3
cseg ends
end start
上面的程序可以分成三部分,第一部分定義了代碼段和數(shù)據(jù)段分別放在程序中的位置,以及執(zhí)行代碼的起始地址.第二部分是可執(zhí)行的程序,在這個(gè)例子只一個(gè)RET指令而已.第三部分是程序包段的終結(jié),其中END敘述包含了程序開(kāi)始執(zhí)行地址.
若是把上面的程序經(jīng)過(guò)匯編連接,你會(huì)發(fā)現(xiàn)所產(chǎn)生的COM文件只有一個(gè)字節(jié)長(zhǎng).這是因?yàn)樗a(chǎn)生的COM文件沒(méi)有程序段前綴(Programsegmetn profix),因?yàn)樵贒OS下所有和COM文件都有相同的程序段前綴.當(dāng)DOS加載一個(gè)COM文件到內(nèi)存中時(shí),就會(huì)自動(dòng)地產(chǎn)生一份正確的程序段前綴.一個(gè)程序在執(zhí)行的過(guò)程中,可以根據(jù)需要修改其程序段前綴,但是在一開(kāi)始,所有COM文件的程序前綴都是相同的.下面是程序前綴的格式.
偏移位置 含義
0000H 程序終止處理子程序地址(INT 20H)
0002H 分配段的結(jié)束地址,段值
0004H 保留
0005H 調(diào)用DOS的服務(wù)
000AH 前一個(gè)父程序的IP和CS
000EH 前一個(gè)父程序的CONTROL_C處理子程序地址
0012H 前一個(gè)父程序包的硬件錯(cuò)誤處理子程序地址
0016H 保留
002CH 環(huán)境段的地址值
005EH 保留
005CH FCB1
006CH ` FCB2
0080H 命令行的參數(shù)和磁盤轉(zhuǎn)移區(qū)域
4.2 一個(gè)最小的內(nèi)存駐留程序
上面的程序只是一個(gè)一般的DOS程序而已.并不是內(nèi)存駐留的.以下是一個(gè)基本的內(nèi)存駐留程序結(jié)構(gòu):
;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
和前一個(gè)程序相比,這個(gè)程序只是增加了一個(gè)DONE部分.這個(gè)部分使用了INT 27H這個(gè)中斷調(diào)用,來(lái)終止并駐留在內(nèi)存(Terminate and Stay Resident)中.使用INT 27H這個(gè)中斷調(diào)用時(shí),必須設(shè)定好一個(gè)指針,讓這個(gè)指針指向內(nèi)存中可以使用的部分,事實(shí)上,這就相當(dāng)于設(shè)置一個(gè)COM文件可加載的位置.另外DOS還提供了INT 21H,AH=31H(駐留程序,Keep process),但是使用這個(gè)中斷調(diào)用時(shí),我們必須設(shè)定所保留的內(nèi)存大小,而不是設(shè)定一個(gè)指針;另外這個(gè)中斷調(diào)用會(huì)送出退出碼.
使用INT 27H時(shí),必須設(shè)定一個(gè)指針指向可用存儲(chǔ)位置的開(kāi)頭,以便讓DOS用來(lái)加載稍后執(zhí)行的程序.DOS本身有一個(gè)指針,這個(gè)指針是加載COM文件或EXE文件時(shí)的基準(zhǔn)地址值.INT尿27H 會(huì)改變這個(gè)指針或?yàn)樾碌臄?shù)值.同時(shí)造成新指針和舊指針之間的存儲(chǔ)空間無(wú)法讓DOS使用因此這樣做會(huì)造成可用存儲(chǔ)位置愈來(lái)愈少.
調(diào)用INT 27H時(shí)所使用的指針是個(gè)FAR指針,其中DX存放的是位移指針(Offset pointer),它可以指到64K字節(jié)之內(nèi)的范圍.而DOS是段指針(Segment pointer),它可以指到IBM PC中640K字節(jié)的任何一個(gè)段.在上面的例子中,DS的內(nèi)容不必另外設(shè)定,因?yàn)楫?dāng)COM文件加載時(shí),DS的內(nèi)容就CS的內(nèi)容相同了.
經(jīng)常在編寫(xiě)匯編程序時(shí),常犯的一個(gè)錯(cuò)誤就是:把a(bǔ)ssume ds:cseg這個(gè)敘述誤認(rèn)為是,存放某一預(yù)設(shè)值到DS中,事實(shí)上,匯編語(yǔ)言程序中的Assume敘述不會(huì)產(chǎn)生任何的程序代碼,這個(gè)功能是告訴匯編器做某些必要的假設(shè),以便正確地匯編程序.譬如以下的程序:
cseg segment
.............
assume ds:cseg
mov ah,radix
.............
radix db 16
.............
cseg ends
上面的程序匯編時(shí),當(dāng)匯編器看到mov ah,radix這個(gè)指令時(shí),它就根據(jù)assume ds:cseg來(lái)產(chǎn)生一定形式的賦值指令.在面的Assume ds:cseg敘述是告訴匯編器,數(shù)據(jù)段就位于目前的代碼段中.這是內(nèi)存駐留程序的一項(xiàng)重要關(guān)鍵.如果DS的內(nèi)容和CS不相同時(shí),無(wú)論是否有assume 敘述,程序執(zhí)行時(shí)都會(huì)失敗.
4.3 改良的內(nèi)存駐留程序
上面所介紹的內(nèi)存駐留程序?qū)嶋H上沒(méi)有做任何事,只是駐留在內(nèi)存中而已.事實(shí)上,在START和END之間放入任何程序代碼,都只會(huì)執(zhí)行一次而已然后就永遠(yuǎn)駐留在內(nèi)存中,除非是使用轉(zhuǎn)移指令轉(zhuǎn)到START的地址去,否則將永遠(yuǎn)無(wú)法被使用.還要注意一點(diǎn),START的地址值并非固定不變,它會(huì)根據(jù)程序執(zhí)行時(shí)計(jì)算機(jī)的狀態(tài)而改變.
下面的這個(gè)程序只是把需要駐留的程序代碼裝載好,但是并不會(huì)執(zhí)行.
;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
上面的程序一開(kāi)始執(zhí)行時(shí)就傳到initialize標(biāo)志的地方,裝置好駐留在內(nèi)存的應(yīng)用部分.原先的DONE已經(jīng)改成initialize,而駐留在內(nèi)存的程序代碼則放在App_Start 和Initialize之間.
另外,你也許注意到了,程序的起始地址并不是Initialize而是Start.這是因?yàn)樗蠧OM程序的起始地址都是100H;而上面的程序中Start是放在100H的地方.如果把Initialize放在End之后,Initialize就變成起始地址,但是這樣的程序無(wú)法透過(guò)EXE2BIN轉(zhuǎn)換成COM文件了.如果無(wú)法產(chǎn)生COM文件時(shí),那么就必須直接處理段的內(nèi)容.
4.4 減少內(nèi)存的額外負(fù)擔(dān)
到目前為止,都沒(méi)有接觸到程序前綴,當(dāng)使用INT 27H時(shí),事實(shí)上是把指針以前的東西都保留在內(nèi)存中,這也包括了COM的程序段前綴.因?yàn)镃OM文件執(zhí)行完畢后,才可以把程序段前綴移掉.
從上面的事實(shí)可以看出:如果程序段前綴只能在COM裝置程序結(jié)束后才可以移去,那么就可以由駐留在內(nèi)存中的程序代碼完成.要做到這一點(diǎn),可以把整個(gè)程序往下移動(dòng)256個(gè)字節(jié).但又如何做到這一點(diǎn)呢?我們可以設(shè)定一個(gè)標(biāo)志(Flag),用來(lái)指示這個(gè)程序是否執(zhí)行過(guò).如果這個(gè)駐留程序或是第一次執(zhí)行時(shí),就把整個(gè)程序往下移動(dòng)256個(gè)字節(jié),以便把程序段前綴移去.但是如果駐留程序在裝置好之后,經(jīng)過(guò)一段長(zhǎng)時(shí)間仍然沒(méi)有被執(zhí)行時(shí),怎么辦呢?如果同時(shí)載入了好幾個(gè)駐留程序時(shí),雙該如何呢?這些重要的事情都需要使用不同的程序代碼來(lái)解決.如果說(shuō)這些程序代碼超出了256字節(jié)時(shí),那么所占用的存儲(chǔ)位置就超出程序段前綴所浪費(fèi)的空間.有些人用一些比較簡(jiǎn)短的代碼來(lái)解決這個(gè)問(wèn)題,但是還是比較麻煩.因此對(duì)于大部分的內(nèi)存駐留程序而言,除非存儲(chǔ)空間太少,以至于256字節(jié)變得很重要,否則最好不要去處理程序段前綴,這樣子會(huì)讓你的程序簡(jiǎn)潔而且容易閱讀.
4.5 使用駐留程序
上面介紹了如何把程序加載到內(nèi)存,并且讓它永遠(yuǎn)留在內(nèi)存中,接下來(lái),介紹如何來(lái)使用駐留在內(nèi)存中的程序.
內(nèi)存駐留程序的使用方法和它原先的設(shè)計(jì)有密切的關(guān)系.譬如,截獲鍵盤輸入的程序就必須通過(guò)鍵盤輸入的軟件中斷,或是敲鍵盤所產(chǎn)生的硬件中斷來(lái)使用.其它的駐留程序可能就必須靠:系統(tǒng)時(shí)鐘,系統(tǒng)調(diào)用,或是其它的中斷才有辦法使用.這些駐留程序必須要和以上的使用方法連結(jié);而且在駐留程序安裝好之后,至少必須建立一種使用的管道,否則駐留程序?qū)o(wú)法使用.
IBM PC必須經(jīng)由事件來(lái)驅(qū)動(dòng),譬如:鍵盤,系統(tǒng)時(shí)鐘,或是軟件中斷.這些事件可以被截獲,然后根據(jù)所發(fā)生的事件來(lái)執(zhí)行一定的動(dòng)作.因此必須讓中斷事件發(fā)生時(shí),先執(zhí)行我們的程序,而非系統(tǒng)的程序.
譬如,當(dāng)我們?cè)O(shè)計(jì)一個(gè)截獲鍵盤輸入的駐留程序時(shí),就必須把駐留程序和執(zhí)行鍵盤輸入的系統(tǒng)調(diào)用連結(jié)起來(lái).當(dāng)DOS或是應(yīng)用程序希望從鍵盤讀取一個(gè)字符時(shí),它就必須執(zhí)行INT 16H調(diào)用.因此如果我們能夠在調(diào)用INT 16H時(shí),先執(zhí)行我們的駐留程序,那么駐留程序就可能變成應(yīng)用程序和操作系統(tǒng)間的橋梁.
可以使用INT 21H中斷調(diào)用中AH=25H來(lái)完成以上的要求.設(shè)置中斷矢量可以更改INT 16H原先的中斷矢量?jī)?nèi)容,讓它改為指向我們的程序.譬如以下的例子所示:
cseg segment
assume cs:cseg,ds:cseg
org 100h
start:
jmp Initialize
;Section 1
new_keyboard_io proc far
sti
nop
iret
new_keyboard_io endp
;Section 2
Initialize:
mov dx,offset new_keyboard_io
mov al,16h
mov ah,25h
int 21h
;Section 3
mov dx,offset Initialize
int 27h
cseg ends
end start
上面的程序和4.3的程序結(jié)構(gòu)是一樣的,但是仍然有一些重要的改變.在Section 1和Section 2.在Section 1把駐留部分修改成子程序形式(Procedure),這樣做是為了增加程序的可讀性.另外,駐留部分多加了兩個(gè)指令,STI和IRET.其中STI是設(shè)置中斷標(biāo)志(Set Interrupt Flag)和起始中斷(Enable interrupts).
當(dāng)CPU發(fā)生中斷時(shí),它就關(guān)閉中斷標(biāo)志,因此CPU就不再接受中斷.事實(shí)上,CPU會(huì)專心地為目前發(fā)生的中斷服務(wù).當(dāng)CPU停止接受中斷時(shí),任何硬件中斷的信號(hào)都會(huì)被忽略,譬如:鍵盤,時(shí)鐘脈沖,磁盤機(jī)信號(hào),調(diào)制解調(diào)器的中斷.如果CPU一直不接受中斷,那么就會(huì)漏掉一些重要的信息,計(jì)算機(jī)系統(tǒng)也可能因此而死機(jī).因此雖然CPU可以停止接受中斷一段時(shí)間,但是卻不能夠久.
第二個(gè)重要的指令是IRET,從中斷返回(Return from interrupt).IRET的功能和RET極相似,RET是用來(lái)從被調(diào)用 的子程序中返回,而IRET則是用來(lái)從中斷程序返回.但是使用IRET返回時(shí),它會(huì)從堆棧中先取出返回的地址值,然后再取出CPU的狀態(tài)標(biāo)志(State Flag).CPU的狀態(tài)標(biāo)志在CPU接受中斷時(shí),會(huì)自動(dòng)地推入堆棧中.因此執(zhí)行IRET指令后,CPU的狀態(tài)就恢復(fù)成未中斷前的狀態(tài);也就是說(shuō)CPU就可以繼續(xù)接受外界的中斷(CPU狀態(tài)標(biāo)志中斷包括了中斷標(biāo)志).嚴(yán)格地說(shuō),STI和IRET在這個(gè)例子中都是多余的,但是對(duì)于實(shí)際的中斷處理程序而言,這兩個(gè)指令都很重要.
另外,使用設(shè)置中斷矢量的中斷調(diào)用時(shí),暫存器AL必須存入所要設(shè)置的中斷矢量,而中斷矢量指針則必須放到暫存器DS:DX中.
4.6 連接中斷處理程序
若是把前一節(jié)的程序拿來(lái)執(zhí)行時(shí),鍵盤是無(wú)法輸入的,事實(shí)上,處理鍵盤的硬件中斷處理程序會(huì)繼續(xù)地讀取敲入的字符,并且放到等待隊(duì)列中,直到隊(duì)列填滿為止;但是由于讀取等待隊(duì)列的軟件中斷INT 16H已經(jīng)被改變了,因此隊(duì)列的內(nèi)容就永遠(yuǎn)取不出來(lái).
現(xiàn)在寫(xiě)一個(gè)中斷處理程序,這個(gè)中斷處理程序只是調(diào)用原先的鍵盤中斷處理程序,一旦做到這一點(diǎn)之后,接下來(lái)就可以根據(jù)鍵盤的輸入做修改.以下就是調(diào)用原先鍵盤處理程序的駐留程序:
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
上面的程序中,第一部分是兩個(gè)字(Double word),這是用來(lái)存放舊的鍵盤中斷矢量.因?yàn)镃OM的程序都只限制在一個(gè)段中,因此數(shù)據(jù)段和代碼段都在同一段中.而原先的中斷處理程序和我們所編寫(xiě)的中斷處理程序未必會(huì)在同一段中,所以必須使用雙字來(lái)儲(chǔ)存地址值.
雙字Old_Keyboard_IO可以放在駐留程序中的任何地方;但是一般來(lái)說(shuō),放在Jmp Initialize 之后會(huì)比較方便;因?yàn)槿绻仨毷褂肈EBUG來(lái)檢查程序的話,可以比較容易調(diào)試.
上面程序中的第二部分是駐留程序的主體,其中包括了一個(gè)調(diào)用原先鍵盤中斷處理程序的模擬中斷.因?yàn)樵鹊逆I盤中斷處理程序必須使用INT的方式調(diào)用,而不是使用CALL的指令調(diào)用;因此必須先使用PUSHF把CPU狀態(tài)標(biāo)志壓入堆棧中,然后配合上CALL來(lái)模擬INT的動(dòng)作.
注意一點(diǎn),assume ds:nothing這一行是匯編指示,而不是程序代碼.它是用來(lái)告訴匯編器在產(chǎn)生下一行機(jī)器碼時(shí),不要更會(huì)目前DS的內(nèi)容;這樣做才可以讓匯編器為下一個(gè)指令產(chǎn)生雙字的地址值.
當(dāng)Call Old_Keyboard_IO指令執(zhí)行時(shí),控制權(quán)就轉(zhuǎn)移到舊的鍵盤中斷處理程序.而當(dāng)這個(gè)中斷調(diào)用執(zhí)行完時(shí),它就執(zhí)行IRET指令,于是控制權(quán)又交還到目前的駐留程序.這樣做,不但可以讓原先的鍵盤中斷程序包為我們工作,同時(shí)也可以掌握控制權(quán).如果只使用IMP指令,跳到舊的鍵盤中斷處理程序包去,而不把CPU狀態(tài)標(biāo)志推入堆棧中,那么一旦執(zhí)行到IRET時(shí),就真正返回到中斷的狀態(tài).
上面程序中的第三部分是啟動(dòng)代碼部分,在這一部分中,設(shè)定好新的中斷矢量,同時(shí)把舊的中斷矢量存放在駐留程序代碼中,以便讓駐留程序使用.
4.7 檢查駐留程序
到目前為止,已經(jīng)成功地把駐留程序加在應(yīng)用程序和DOS的鍵盤輸入之間;接下來(lái)可以修改輸入的字符.在這一節(jié)中,我們準(zhǔn)備截獲鍵盤的輸入,并且把"Y"改成"y","y"改成"Y".
以下是程序代碼:
cseg segment
assume cs:cseg,ds:cseg
org 100h
start:
jmp Initialize
Old_Keyboard_IO dd ?
new_keyboard_io proc far
assume cs:cseg,ds:cseg
sti
;Section 1
cmp ah,0
je ki0
assume ds:nothing
jmp Old_Keyboard_IO
;Section 2
ki0:
pushf
assume ds:nothing
call Old_Keyboard_IO
cmp al,'y'
jne ki1
mov al,'y'
jmp kidone
ki1:
cmp al,'Y'
jne kidone
mov al,'y'
kidone:
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
在面的程序第一部分主要是檢查AH是否等于0(讀取字符).如果AH不等于0,就用舊的中斷處理程序來(lái)處理其它的功能:1H(讀取鍵盤狀態(tài)),2H(讀取鍵盤標(biāo)志).在這里,使用JMP指令,而非使用CALL來(lái)模擬軟件中斷;因此原先的中斷處理程序結(jié)束后,就直接返回到中斷前的狀態(tài).
程序的第二部分是處理AH=0H時(shí)的情形.首先程序中斷模擬一個(gè)軟件中斷來(lái)調(diào)用舊的鍵盤處理程序,是為了在讀完字符之后,控制權(quán)能交還到我們的駐留程序,接下來(lái)的幾行程序是檢查讀到的字符是不是"Y"和"y",如果是的話就修改它.
可以借執(zhí)行這個(gè)程序,來(lái)驗(yàn)證其是否正確.除此之外,也可以證明,在操作系統(tǒng)和應(yīng)用程序之間可以加入一層控制碼.這一層控制碼可以先選擇性地加強(qiáng)或取代某些DOS的功能,修改結(jié)果以滿足我們的要求.
4.1 一個(gè)基本的COM程序
DOS之下有兩種形式的可執(zhí)行文件,這兩種文件分別是COM文件和EXE文件.其中,COM文件可以迅速地加載和執(zhí)行,但是其大小不能超過(guò)64K字節(jié),只能有一個(gè)段,代碼段.而且起始地址為100H指令必須為程序的啟動(dòng)指令.EXE文件可以加載到許多個(gè)段中,因此程序的大小沒(méi)有限制,但是程序加載的過(guò)程就比較慢,而且對(duì)于內(nèi)存駐留程序來(lái)說(shuō)還會(huì)造成更大的麻煩.
以下是一個(gè)可以正確執(zhí)行的COM文件,但其內(nèi)容是空的;只是一個(gè)COM文件的框架,可以把你寫(xiě)的任何應(yīng)用部分加在這個(gè)文件中,形成一個(gè)COM格式的內(nèi)存駐留程序:
;Section 1
cseg segment
assume cs:cseg,ds:cseg
org 100h
;Section 2
start:
ret
;Section 3
cseg ends
end start
上面的程序可以分成三部分,第一部分定義了代碼段和數(shù)據(jù)段分別放在程序中的位置,以及執(zhí)行代碼的起始地址.第二部分是可執(zhí)行的程序,在這個(gè)例子只一個(gè)RET指令而已.第三部分是程序包段的終結(jié),其中END敘述包含了程序開(kāi)始執(zhí)行地址.
若是把上面的程序經(jīng)過(guò)匯編連接,你會(huì)發(fā)現(xiàn)所產(chǎn)生的COM文件只有一個(gè)字節(jié)長(zhǎng).這是因?yàn)樗a(chǎn)生的COM文件沒(méi)有程序段前綴(Programsegmetn profix),因?yàn)樵贒OS下所有和COM文件都有相同的程序段前綴.當(dāng)DOS加載一個(gè)COM文件到內(nèi)存中時(shí),就會(huì)自動(dòng)地產(chǎn)生一份正確的程序段前綴.一個(gè)程序在執(zhí)行的過(guò)程中,可以根據(jù)需要修改其程序段前綴,但是在一開(kāi)始,所有COM文件的程序前綴都是相同的.下面是程序前綴的格式.
偏移位置 含義
0000H 程序終止處理子程序地址(INT 20H)
0002H 分配段的結(jié)束地址,段值
0004H 保留
0005H 調(diào)用DOS的服務(wù)
000AH 前一個(gè)父程序的IP和CS
000EH 前一個(gè)父程序的CONTROL_C處理子程序地址
0012H 前一個(gè)父程序包的硬件錯(cuò)誤處理子程序地址
0016H 保留
002CH 環(huán)境段的地址值
005EH 保留
005CH FCB1
006CH ` FCB2
0080H 命令行的參數(shù)和磁盤轉(zhuǎn)移區(qū)域
4.2 一個(gè)最小的內(nèi)存駐留程序
上面的程序只是一個(gè)一般的DOS程序而已.并不是內(nèi)存駐留的.以下是一個(gè)基本的內(nèi)存駐留程序結(jié)構(gòu):
;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
和前一個(gè)程序相比,這個(gè)程序只是增加了一個(gè)DONE部分.這個(gè)部分使用了INT 27H這個(gè)中斷調(diào)用,來(lái)終止并駐留在內(nèi)存(Terminate and Stay Resident)中.使用INT 27H這個(gè)中斷調(diào)用時(shí),必須設(shè)定好一個(gè)指針,讓這個(gè)指針指向內(nèi)存中可以使用的部分,事實(shí)上,這就相當(dāng)于設(shè)置一個(gè)COM文件可加載的位置.另外DOS還提供了INT 21H,AH=31H(駐留程序,Keep process),但是使用這個(gè)中斷調(diào)用時(shí),我們必須設(shè)定所保留的內(nèi)存大小,而不是設(shè)定一個(gè)指針;另外這個(gè)中斷調(diào)用會(huì)送出退出碼.
使用INT 27H時(shí),必須設(shè)定一個(gè)指針指向可用存儲(chǔ)位置的開(kāi)頭,以便讓DOS用來(lái)加載稍后執(zhí)行的程序.DOS本身有一個(gè)指針,這個(gè)指針是加載COM文件或EXE文件時(shí)的基準(zhǔn)地址值.INT尿27H 會(huì)改變這個(gè)指針或?yàn)樾碌臄?shù)值.同時(shí)造成新指針和舊指針之間的存儲(chǔ)空間無(wú)法讓DOS使用因此這樣做會(huì)造成可用存儲(chǔ)位置愈來(lái)愈少.
調(diào)用INT 27H時(shí)所使用的指針是個(gè)FAR指針,其中DX存放的是位移指針(Offset pointer),它可以指到64K字節(jié)之內(nèi)的范圍.而DOS是段指針(Segment pointer),它可以指到IBM PC中640K字節(jié)的任何一個(gè)段.在上面的例子中,DS的內(nèi)容不必另外設(shè)定,因?yàn)楫?dāng)COM文件加載時(shí),DS的內(nèi)容就CS的內(nèi)容相同了.
經(jīng)常在編寫(xiě)匯編程序時(shí),常犯的一個(gè)錯(cuò)誤就是:把a(bǔ)ssume ds:cseg這個(gè)敘述誤認(rèn)為是,存放某一預(yù)設(shè)值到DS中,事實(shí)上,匯編語(yǔ)言程序中的Assume敘述不會(huì)產(chǎn)生任何的程序代碼,這個(gè)功能是告訴匯編器做某些必要的假設(shè),以便正確地匯編程序.譬如以下的程序:
cseg segment
.............
assume ds:cseg
mov ah,radix
.............
radix db 16
.............
cseg ends
上面的程序匯編時(shí),當(dāng)匯編器看到mov ah,radix這個(gè)指令時(shí),它就根據(jù)assume ds:cseg來(lái)產(chǎn)生一定形式的賦值指令.在面的Assume ds:cseg敘述是告訴匯編器,數(shù)據(jù)段就位于目前的代碼段中.這是內(nèi)存駐留程序的一項(xiàng)重要關(guān)鍵.如果DS的內(nèi)容和CS不相同時(shí),無(wú)論是否有assume 敘述,程序執(zhí)行時(shí)都會(huì)失敗.
4.3 改良的內(nèi)存駐留程序
上面所介紹的內(nèi)存駐留程序?qū)嶋H上沒(méi)有做任何事,只是駐留在內(nèi)存中而已.事實(shí)上,在START和END之間放入任何程序代碼,都只會(huì)執(zhí)行一次而已然后就永遠(yuǎn)駐留在內(nèi)存中,除非是使用轉(zhuǎn)移指令轉(zhuǎn)到START的地址去,否則將永遠(yuǎn)無(wú)法被使用.還要注意一點(diǎn),START的地址值并非固定不變,它會(huì)根據(jù)程序執(zhí)行時(shí)計(jì)算機(jī)的狀態(tài)而改變.
下面的這個(gè)程序只是把需要駐留的程序代碼裝載好,但是并不會(huì)執(zhí)行.
;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
上面的程序一開(kāi)始執(zhí)行時(shí)就傳到initialize標(biāo)志的地方,裝置好駐留在內(nèi)存的應(yīng)用部分.原先的DONE已經(jīng)改成initialize,而駐留在內(nèi)存的程序代碼則放在App_Start 和Initialize之間.
另外,你也許注意到了,程序的起始地址并不是Initialize而是Start.這是因?yàn)樗蠧OM程序的起始地址都是100H;而上面的程序中Start是放在100H的地方.如果把Initialize放在End之后,Initialize就變成起始地址,但是這樣的程序無(wú)法透過(guò)EXE2BIN轉(zhuǎn)換成COM文件了.如果無(wú)法產(chǎn)生COM文件時(shí),那么就必須直接處理段的內(nèi)容.
4.4 減少內(nèi)存的額外負(fù)擔(dān)
到目前為止,都沒(méi)有接觸到程序前綴,當(dāng)使用INT 27H時(shí),事實(shí)上是把指針以前的東西都保留在內(nèi)存中,這也包括了COM的程序段前綴.因?yàn)镃OM文件執(zhí)行完畢后,才可以把程序段前綴移掉.
從上面的事實(shí)可以看出:如果程序段前綴只能在COM裝置程序結(jié)束后才可以移去,那么就可以由駐留在內(nèi)存中的程序代碼完成.要做到這一點(diǎn),可以把整個(gè)程序往下移動(dòng)256個(gè)字節(jié).但又如何做到這一點(diǎn)呢?我們可以設(shè)定一個(gè)標(biāo)志(Flag),用來(lái)指示這個(gè)程序是否執(zhí)行過(guò).如果這個(gè)駐留程序或是第一次執(zhí)行時(shí),就把整個(gè)程序往下移動(dòng)256個(gè)字節(jié),以便把程序段前綴移去.但是如果駐留程序在裝置好之后,經(jīng)過(guò)一段長(zhǎng)時(shí)間仍然沒(méi)有被執(zhí)行時(shí),怎么辦呢?如果同時(shí)載入了好幾個(gè)駐留程序時(shí),雙該如何呢?這些重要的事情都需要使用不同的程序代碼來(lái)解決.如果說(shuō)這些程序代碼超出了256字節(jié)時(shí),那么所占用的存儲(chǔ)位置就超出程序段前綴所浪費(fèi)的空間.有些人用一些比較簡(jiǎn)短的代碼來(lái)解決這個(gè)問(wèn)題,但是還是比較麻煩.因此對(duì)于大部分的內(nèi)存駐留程序而言,除非存儲(chǔ)空間太少,以至于256字節(jié)變得很重要,否則最好不要去處理程序段前綴,這樣子會(huì)讓你的程序簡(jiǎn)潔而且容易閱讀.
4.5 使用駐留程序
上面介紹了如何把程序加載到內(nèi)存,并且讓它永遠(yuǎn)留在內(nèi)存中,接下來(lái),介紹如何來(lái)使用駐留在內(nèi)存中的程序.
內(nèi)存駐留程序的使用方法和它原先的設(shè)計(jì)有密切的關(guān)系.譬如,截獲鍵盤輸入的程序就必須通過(guò)鍵盤輸入的軟件中斷,或是敲鍵盤所產(chǎn)生的硬件中斷來(lái)使用.其它的駐留程序可能就必須靠:系統(tǒng)時(shí)鐘,系統(tǒng)調(diào)用,或是其它的中斷才有辦法使用.這些駐留程序必須要和以上的使用方法連結(jié);而且在駐留程序安裝好之后,至少必須建立一種使用的管道,否則駐留程序?qū)o(wú)法使用.
IBM PC必須經(jīng)由事件來(lái)驅(qū)動(dòng),譬如:鍵盤,系統(tǒng)時(shí)鐘,或是軟件中斷.這些事件可以被截獲,然后根據(jù)所發(fā)生的事件來(lái)執(zhí)行一定的動(dòng)作.因此必須讓中斷事件發(fā)生時(shí),先執(zhí)行我們的程序,而非系統(tǒng)的程序.
譬如,當(dāng)我們?cè)O(shè)計(jì)一個(gè)截獲鍵盤輸入的駐留程序時(shí),就必須把駐留程序和執(zhí)行鍵盤輸入的系統(tǒng)調(diào)用連結(jié)起來(lái).當(dāng)DOS或是應(yīng)用程序希望從鍵盤讀取一個(gè)字符時(shí),它就必須執(zhí)行INT 16H調(diào)用.因此如果我們能夠在調(diào)用INT 16H時(shí),先執(zhí)行我們的駐留程序,那么駐留程序就可能變成應(yīng)用程序和操作系統(tǒng)間的橋梁.
可以使用INT 21H中斷調(diào)用中AH=25H來(lái)完成以上的要求.設(shè)置中斷矢量可以更改INT 16H原先的中斷矢量?jī)?nèi)容,讓它改為指向我們的程序.譬如以下的例子所示:
cseg segment
assume cs:cseg,ds:cseg
org 100h
start:
jmp Initialize
;Section 1
new_keyboard_io proc far
sti
nop
iret
new_keyboard_io endp
;Section 2
Initialize:
mov dx,offset new_keyboard_io
mov al,16h
mov ah,25h
int 21h
;Section 3
mov dx,offset Initialize
int 27h
cseg ends
end start
上面的程序和4.3的程序結(jié)構(gòu)是一樣的,但是仍然有一些重要的改變.在Section 1和Section 2.在Section 1把駐留部分修改成子程序形式(Procedure),這樣做是為了增加程序的可讀性.另外,駐留部分多加了兩個(gè)指令,STI和IRET.其中STI是設(shè)置中斷標(biāo)志(Set Interrupt Flag)和起始中斷(Enable interrupts).
當(dāng)CPU發(fā)生中斷時(shí),它就關(guān)閉中斷標(biāo)志,因此CPU就不再接受中斷.事實(shí)上,CPU會(huì)專心地為目前發(fā)生的中斷服務(wù).當(dāng)CPU停止接受中斷時(shí),任何硬件中斷的信號(hào)都會(huì)被忽略,譬如:鍵盤,時(shí)鐘脈沖,磁盤機(jī)信號(hào),調(diào)制解調(diào)器的中斷.如果CPU一直不接受中斷,那么就會(huì)漏掉一些重要的信息,計(jì)算機(jī)系統(tǒng)也可能因此而死機(jī).因此雖然CPU可以停止接受中斷一段時(shí)間,但是卻不能夠久.
第二個(gè)重要的指令是IRET,從中斷返回(Return from interrupt).IRET的功能和RET極相似,RET是用來(lái)從被調(diào)用 的子程序中返回,而IRET則是用來(lái)從中斷程序返回.但是使用IRET返回時(shí),它會(huì)從堆棧中先取出返回的地址值,然后再取出CPU的狀態(tài)標(biāo)志(State Flag).CPU的狀態(tài)標(biāo)志在CPU接受中斷時(shí),會(huì)自動(dòng)地推入堆棧中.因此執(zhí)行IRET指令后,CPU的狀態(tài)就恢復(fù)成未中斷前的狀態(tài);也就是說(shuō)CPU就可以繼續(xù)接受外界的中斷(CPU狀態(tài)標(biāo)志中斷包括了中斷標(biāo)志).嚴(yán)格地說(shuō),STI和IRET在這個(gè)例子中都是多余的,但是對(duì)于實(shí)際的中斷處理程序而言,這兩個(gè)指令都很重要.
另外,使用設(shè)置中斷矢量的中斷調(diào)用時(shí),暫存器AL必須存入所要設(shè)置的中斷矢量,而中斷矢量指針則必須放到暫存器DS:DX中.
4.6 連接中斷處理程序
若是把前一節(jié)的程序拿來(lái)執(zhí)行時(shí),鍵盤是無(wú)法輸入的,事實(shí)上,處理鍵盤的硬件中斷處理程序會(huì)繼續(xù)地讀取敲入的字符,并且放到等待隊(duì)列中,直到隊(duì)列填滿為止;但是由于讀取等待隊(duì)列的軟件中斷INT 16H已經(jīng)被改變了,因此隊(duì)列的內(nèi)容就永遠(yuǎn)取不出來(lái).
現(xiàn)在寫(xiě)一個(gè)中斷處理程序,這個(gè)中斷處理程序只是調(diào)用原先的鍵盤中斷處理程序,一旦做到這一點(diǎn)之后,接下來(lái)就可以根據(jù)鍵盤的輸入做修改.以下就是調(diào)用原先鍵盤處理程序的駐留程序:
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
上面的程序中,第一部分是兩個(gè)字(Double word),這是用來(lái)存放舊的鍵盤中斷矢量.因?yàn)镃OM的程序都只限制在一個(gè)段中,因此數(shù)據(jù)段和代碼段都在同一段中.而原先的中斷處理程序和我們所編寫(xiě)的中斷處理程序未必會(huì)在同一段中,所以必須使用雙字來(lái)儲(chǔ)存地址值.
雙字Old_Keyboard_IO可以放在駐留程序中的任何地方;但是一般來(lái)說(shuō),放在Jmp Initialize 之后會(huì)比較方便;因?yàn)槿绻仨毷褂肈EBUG來(lái)檢查程序的話,可以比較容易調(diào)試.
上面程序中的第二部分是駐留程序的主體,其中包括了一個(gè)調(diào)用原先鍵盤中斷處理程序的模擬中斷.因?yàn)樵鹊逆I盤中斷處理程序必須使用INT的方式調(diào)用,而不是使用CALL的指令調(diào)用;因此必須先使用PUSHF把CPU狀態(tài)標(biāo)志壓入堆棧中,然后配合上CALL來(lái)模擬INT的動(dòng)作.
注意一點(diǎn),assume ds:nothing這一行是匯編指示,而不是程序代碼.它是用來(lái)告訴匯編器在產(chǎn)生下一行機(jī)器碼時(shí),不要更會(huì)目前DS的內(nèi)容;這樣做才可以讓匯編器為下一個(gè)指令產(chǎn)生雙字的地址值.
當(dāng)Call Old_Keyboard_IO指令執(zhí)行時(shí),控制權(quán)就轉(zhuǎn)移到舊的鍵盤中斷處理程序.而當(dāng)這個(gè)中斷調(diào)用執(zhí)行完時(shí),它就執(zhí)行IRET指令,于是控制權(quán)又交還到目前的駐留程序.這樣做,不但可以讓原先的鍵盤中斷程序包為我們工作,同時(shí)也可以掌握控制權(quán).如果只使用IMP指令,跳到舊的鍵盤中斷處理程序包去,而不把CPU狀態(tài)標(biāo)志推入堆棧中,那么一旦執(zhí)行到IRET時(shí),就真正返回到中斷的狀態(tài).
上面程序中的第三部分是啟動(dòng)代碼部分,在這一部分中,設(shè)定好新的中斷矢量,同時(shí)把舊的中斷矢量存放在駐留程序代碼中,以便讓駐留程序使用.
4.7 檢查駐留程序
到目前為止,已經(jīng)成功地把駐留程序加在應(yīng)用程序和DOS的鍵盤輸入之間;接下來(lái)可以修改輸入的字符.在這一節(jié)中,我們準(zhǔn)備截獲鍵盤的輸入,并且把"Y"改成"y","y"改成"Y".
以下是程序代碼:
cseg segment
assume cs:cseg,ds:cseg
org 100h
start:
jmp Initialize
Old_Keyboard_IO dd ?
new_keyboard_io proc far
assume cs:cseg,ds:cseg
sti
;Section 1
cmp ah,0
je ki0
assume ds:nothing
jmp Old_Keyboard_IO
;Section 2
ki0:
pushf
assume ds:nothing
call Old_Keyboard_IO
cmp al,'y'
jne ki1
mov al,'y'
jmp kidone
ki1:
cmp al,'Y'
jne kidone
mov al,'y'
kidone:
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
在面的程序第一部分主要是檢查AH是否等于0(讀取字符).如果AH不等于0,就用舊的中斷處理程序來(lái)處理其它的功能:1H(讀取鍵盤狀態(tài)),2H(讀取鍵盤標(biāo)志).在這里,使用JMP指令,而非使用CALL來(lái)模擬軟件中斷;因此原先的中斷處理程序結(jié)束后,就直接返回到中斷前的狀態(tài).
程序的第二部分是處理AH=0H時(shí)的情形.首先程序中斷模擬一個(gè)軟件中斷來(lái)調(diào)用舊的鍵盤處理程序,是為了在讀完字符之后,控制權(quán)能交還到我們的駐留程序,接下來(lái)的幾行程序是檢查讀到的字符是不是"Y"和"y",如果是的話就修改它.
可以借執(zhí)行這個(gè)程序,來(lái)驗(yàn)證其是否正確.除此之外,也可以證明,在操作系統(tǒng)和應(yīng)用程序之間可以加入一層控制碼.這一層控制碼可以先選擇性地加強(qiáng)或取代某些DOS的功能,修改結(jié)果以滿足我們的要求.