用匯編編寫DOS下的記憶體駐留程式(4) (轉)

amyz發表於2007-08-15
用匯編編寫DOS下的記憶體駐留程式(4) (轉)[@more@] 四 基本的駐留
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這個中斷呼叫,來終止並駐留在記憶體(Tenate and Stay Resnt)中.使用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  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可以停止接受中斷一段時間,但是卻不能夠久.
 第二個重要的指令是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  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 之後會比較方便;因為如果必須使用DE來檢查程式的話,可以比較容易.
 上面程式中的第二部分是駐留程式的主體,其中包括了一個呼叫原先鍵盤中斷處理程式的模擬中斷.因為原先的鍵盤中斷處理程式必須使用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",如果是的話就修改它.
 可以借執行這個程式,來驗證其是否正確.除此之外,也可以證明,在作業系統和應用程式之間可以加入一層控制碼.這一層控制碼可以先選擇性地加強或取代某些DOS的功能,修改結果以滿足我們的要求.
 

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752019/viewspace-958996/,如需轉載,請註明出處,否則將追究法律責任。

相關文章