程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 更多編程語言 >> 匯編語言 >> 匯編語言編寫DOS下的內存駐留程序(4)

匯編語言編寫DOS下的內存駐留程序(4)

編輯:匯編語言

四 基本的駐留程序
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字節變得很重要,否則最好不要去處理程序段前綴,這樣子會讓你的程序簡潔而且容易閱讀.


4.5 使用駐留程序
上面介紹了如何把程序加載到內存,並且讓它永遠留在內存中,接下來,介紹如何來使用駐留在內存中的程序.
內存駐留程序的使用方法和它原先的設計有密切的關系.譬如,截獲鍵盤輸入的程序就必須通過鍵盤輸入的軟件中斷,或是敲鍵盤所產生的硬件中斷來使用.其它的駐留程序可能就必須靠:系統時鐘,系統調用,或是其它的中斷才有辦法使用.這些駐留程序必須要和以上的使用方法連結;而且在駐留程序安裝好之後,至少必須建立一種使用的管道,否則駐留程序將無法使用.
IBM PC必須經由事件來驅動,譬如:鍵盤,系統時鐘,或是軟件中斷.這些事件可以被截獲,然後根據所發生的事件來執行一定的動作.因此必須讓中斷事件發生時,先執行我們的程序,而非系統的程序.
譬如,當我們設計一個截獲鍵盤輸入的駐留程序時,就必須把駐留程序和執行鍵盤輸入的系統調用連結起來.當DOS或是應用程序希望從鍵盤讀取一個字符時,它就必須執行INT 16H調用.因此如果我們能夠在調用INT 16H時,先執行我們的駐留程序,那麼駐留程序就可能變成應用程序和操作系統間的橋梁.
可以使用INT 21H中斷調用中AH=25H來完成以上的要求.設置中斷矢量可以更改INT 16H原先的中斷矢量內容,讓它改為指向我們的程序.譬如以下的例子所示:
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的程序結構是一樣的,但是仍然有一些重要的改變.在Section 1和Section 2.在Section 1把駐留部分修改成子程序形式(Procedure),這樣做是為了增加程序的可讀性.另外,駐留部分多加了兩個指令,STI和IRET.其中STI是設置中斷標志(Set Interrupt Flag)和起始中斷(Enable interrupts).
當CPU發生中斷時,它就關閉中斷標志,因此CPU就不再接受中斷.事實上,CPU會專心地為目前發生的中斷服務.當CPU停止接受中斷時,任何硬件中斷的信號都會被忽略,譬如:鍵盤,時鐘脈沖,磁盤機信號,調制解調器的中斷.如果CPU一直不接受中斷,那麼就會漏掉一些重要的信息,計算機系統也可能因此而死機.因此雖然CPU可以停止接受中斷一段時間,但是卻不能夠久.

第二個重要的指令是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".


以下是程序代碼:
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,就用舊的中斷處理程序來處理其它的功能:1H(讀取鍵盤狀態),2H(讀取鍵盤標志).在這裡,使用JMP指令,而非使用CALL來模擬軟件中斷;因此原先的中斷處理程序結束後,就直接返回到中斷前的狀態.
程序的第二部分是處理AH=0H時的情形.首先程序中斷模擬一個軟件中斷來調用舊的鍵盤處理程序,是為了在讀完字符之後,控制權能交還到我們的駐留程序,接下來的幾行程序是檢查讀到的字符是不是"Y"和"y",如果是的話就修改它.
可以借執行這個程序,來驗證其是否正確.

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved