五 鍵盤輸入擴充程序
有了前一節的基本駐留程序為基礎,就可以建立起不同的應用程序.接下來,就寫一個駐留程序,把用戶敲入的字符,用一系列的字符來取代.這樣可以減少用戶的擊鍵次數.
首先,先復習一下前一節的駐留程序的格式,如下所示:
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
只要New_keyboard_IO這個程序,就可以把以上的程序變成許多不同的鍵盤應用程序.在開始設計之前,必須解決一些問題.
首先,必須決定哪些鍵可以用來加以擴充.如果把一般的英文字母或是數目字做為擴充字符的話可能會出現一些問題.如果是對控制字符做擴充,應該不會有什麼問題,但是DOS把某些控制字符視為特殊的功能.譬如Control_H,IBM PC本身有一組自己獨有和增加字符(extended character),譬如:功能鍵(F1到F10),以及ALT鍵和其它組合所產生的字符等.這些增加字符通常都是使用在文書編輯程序中,這些字符比較適合用來作為擴充字符用.這組字符是由兩個碼組成,前面一個碼永遠是0,因此DOS可以很容易加以分辨.而且使用這些字符作為擴充字符對DOS的使用也不會產生太大的影響.下面是擴充字符組的第二個碼大小:
1 2 Paoudo_NULL 3 4 5
6 7 8 9 10
11 12 13 14 15 Shift_Tab 16 Alt_Q 17 Alt_W 18 Alt_E 19 Alt_R 20 Alt_T 21 Alt_Y 22 Alt_U 23 Alt_I 24 Alt_O 25 Alt_P 26 27 28 29 30 Alt_A
31 Alt_S 32 Alt_D 33 Alt_F 34 Alt_G 35 Alt_H
36 Alt_J 37 Alt_K 38 Alt_L 39 40
41 42 43 44 Alt_Z 45 Alt_X
46 Alt_C 47 Alt_V 48 49 50
51 52 53 54 55
56 57 58 59 F1 60 F2
61 F3 62 F4 63 F5 64 F6 65 F7
66 F8 67 F9 68 F10 69 70
71 HOME 72 UpArrow 73 PgUp 74 75 LeftArrow
76 77 RightArrow 78 79 End 80 DownArrow
81 PgDn 82 Insert 83 Delete 84 Shift_F1 85 Shift_F2
86 Shift_F3 87 Shift_F4 88 Shift_F5 89 Shift_F6 90 Shift_F7
91 Shift_F8 92 Shift_F9 93 Shift_F10 94 Control_F1 95 Control_F2
96 Control_F3 97 Control_F4 98 Control_F5 99 Control_F6 100 Control_F7
101 Control_F8 102 Control_F9 103 Control_F10 104 Alt_F1 105 Alt_F2
106 Alt_F3 107 Alt_F4 108 Alt_F5 109 Alt_F6 110 Alt_F7
111 Alt_F8 112 Alt_F9 113 Alt_F10 114 Control_PrtSc 115 Control_LArrow
116 Control_RArrow 117 Control_End 118 Control_PgDn 119 Control_Home 120 Alt_1
121 Alt_2 122 Alt_3 123 Alt_4 124 Alt_5 125 Alt_6
126 Alt_7 127 Alt_8 128 Alt_9 129 Alt_0 130 Alt_Hyphan
131 Alt_Space 132 Control_PgUp
接下來,需要決定把擴充字符擴充成什麼樣的字符串.譬如,所擴充的字符串以什麼作結尾?有一個可能的選擇是:回車鍵(Carriage Return,ASCII碼0DH).這種選擇很合乎邏輯,因為一般的指令都能是以回車鍵做結尾.但是,如果選擇回車鍵名做擴充字符串的結尾,那麼就很難表示許多行的擴充字符串.另外一個選擇是使用$作為擴充字符串的結尾.但是,因為有些DOS的系統調用使用$作為字符結尾;因此如果采用$時,那麼擴充字符串中就不能有$出現.
C語言中都是采用ASCII碼的0做為字符串的結尾,這種形式的字符串稱為ASCII字符串(ASCII零結尾).使用ASCII字符串格式,就可以表示所有的可見字符和不可見字符,因為從鍵盤不可能輸入ASCII碼為0的字符.
下面的例子中,把F1這個鍵(擴充碼59)定義為DIR指令.也可以把F1定義成以下的指令:
MASM MACRO;
LINK MACRO;
EXE2BIN MACRO.EXE MACRO.COM;
上面的指令中,每一行都是以回車鍵作結尾的.
最後要做的是,解決將擴充的字符返回給DOS的問題.通常每當在鍵盤敲入一個鍵時,DOS就會從鍵盤輸入隊列取得一個字符.因此必須設法欺騙DOS,讓它接受一連串的字符.
DOS借檢查鍵盤的狀態來判斷,是否有字符輸入,ROM BIOS上的鍵盤輸入功能在沒有輸入字符時就把ZF(Zero Flag)設定為1,否則就把ZF設定為0.如果可以控制這個功能,反復地欺騙DOS目前有字符要輸入,然後把預的字符串傳回給DOS,那麼就可以讓DOS接受任何數量的字符.
5.1 基本的擴充程序
可以把上面的空的New_Keyboard_IO程序,改用以下的程序來代替.
New_Keyboard_IO proc far
sti
cmp ah,0 ;A read request?
je ksread
cmp ah,1 ;A status request?
je ksstat
assume ds:nothing ;Let original routine
jmp Old_Keyboard_IO ;Do remaining subfunction
ksRead:
call keyRead ;Get next char to return
iret
ksstat:
call keyStat ;GetStatus
ret 2 ;It's important!!
New_Keyboard_IO endp
上面的New_Keyboard_IO程序中,把0H(讀取字符)和1H(取得鍵盤狀態)這兩項功能自行處理.這個程序很簡單,但是其中有一個關鍵點.當我們處理取得鍵盤狀態的功能時,因為原先的鍵盤中斷處理程序是利用ZF返回鍵盤狀態,因此程序包中也必須保有這種特性,如果使用IRET返回的話,那麼設定好ZF就會因為CPU狀態標志從堆棧中取出,而恢復成未中斷前的狀態.
為了解決這個問題可以使用RET的參數來設置.這個參數是用來指示從堆棧中取出多少個字節.通常這是用在高級語言的子程序返回時,用來從堆棧中除去一些參數或是變數.在這裡我們希望用來移去原先中斷時堆棧的CPU狀態,這樣才有辦法把改變的ZF傳回,因此在這裡使用了RET 2這個指令.
上面的程序碼中調用到Keyread和KeyStat這兩個子程序,其內容如下所示:
assume ds:nothing
;If expansion is in progress,return a fake status
;of ZF=0,indicatin gthat a character is ready to be
;read,If expansion is not in progress,then return
;the actual status from the keyboard
KeyStat proc
cmp cs:current,0
jne FakeStat
pushf ;Let original routine
call Old_Keyboard_IO ;get keyboard status
ret
FakeStat:
mov bx,1 ;Fake a "char ready"
cmp bx,0 ;by clearing ZF
KeyStat endp
;Read a character from the keyboard input queue,
;if not expanding or the expansion string.
;if expansion is in progress
KeyRead proc
cmp cs:current,0
jne ExpandChar
ReadChar:
mov cs:current,0 ;Slightly peculiar
pushf ;Let original routine
call Old_Keyboard_IO ;Get keyboard status
cmp al,0
je Extended
ReadDone:
ret
Expanded:
cmp ah,59 ;Is this character to expand?
jne ReadDone ;If not,then return it normally
;If so,then start expanding
mov cs:current,offset string
ExpandChar:
push si
mov si,cs:current
mov al,cs:[si]
inc cs:current
pop si
cmp al,0 ;Is this end of string?
je ReadChar ;If so,then read a real char?
ret
KeyRead endp
;Pointer to where we are in the expansion string
current dw 0
;String we will return when an F1 is typed
;0DH is ASCII carriage return
string db 'DIR',0dh,0
上面的程序中,使用了一個指針current,這個指針指向傳給DOS的下一個字符.如果current等於0時,就表示擴充字符沒了.如果current不等於0,那麼current所指的字符就會被傳回,除非所指到的字符是ASCII 0,如果current所指到的字符是ASCII 0,那麼就必須把current設定成0.
狀態檢查程序KeyStat和字符輸入程序KeyRead都各有兩個部分,一部分是當current等於0,另一部分則是當current等於0.
如果current等於0,也就是沒有擴充字符時,那麼狀態檢查程序就需調用舊的鍵盤輸入程序,來檢查目前鍵盤輸入隊列的狀態.如果current不等於0,ZF就必須設定成0,以表示目前有字符輸入.ZF要設定成0或1,可以先執行某一運算讓結果為0或非0即可.
鍵盤輸入程序是整個程序最復雜的部分.這個程序決定了下個送給DOS的字符是什麼.如果擴充字符送完時,就調用舊的鍵盤輸入程序取得下一個輸入的字符.無論從鍵盤輸入的字符是什麼,都必須檢查是否是希望擴充的字符.鍵盤輸入程序是把輸入的結果放在寄存器AL中.如果輸入的字符是增加字符時(如F1),那麼AL的內容是0,增加的字符碼則放在AH中.
如果讀到的字符是希望擴充的字符F1,那麼就必須開始進行擴充工作.這時候就必須把指針current指到擴充字符串的開頭.大多數人常犯的一個錯誤是:使用mov cs:current,string而不是mov cs:current,offset string.這兩者的差別在於前者是錯誤的,因為它的意思是把一個字節的內容移到一個字節之中,匯編器會強迫兩者的形式吻合.後者則是正確的,因為 我們希望做的是把式string指向的地址值移到current之中.
當我們在進行擴充時,就把指針current所指的字節內容移到AL中,只要AL的內容不是0,就不必管AH的內容是什麼.如果AL是0的話,就表示已經到了擴充字符的結尾了.這表示不應該傳回0,而必須重新調用Old_Keyboard_IO ,以便從鍵盤取得輸入字符.
在程序包KeyRead中有一行指令比較特殊,你也許注意到了,在進入KeyRead,當確定current為0時,接下來又把current設定成0.這樣做雖然有些奇怪,卻沒有任何傷害;但是對於擴充字符串到達結尾時,卻很有用.當我們到達擴充字符串的結尾時,current的內容將指到字符串結尾的下一個位置,而不是0.因此必把current設定為0,可以先跳到某一位置把current設定為0,然後再跳到ReadChar.而采取前面程序的做法時,只是浪費一行毫無傷害的指令,卻可以使程序變得簡明.
在這個程序中,每次使用到內存的內容時,都必須牽涉到段值,這一點相當重要.當計算機的控制權轉移到我們的程序中時,我們對於DS的內容是不知道的.但是有兩件事可以確定:第一,DS的內容對我們的程序幾乎沒有任何用;第二,DS的內容對於被中斷的程序可能很重要.因此我們必須保證每次使用到內存位置時,都是使用目前的段,亦即以目前的CS值為標准.必須要確定:如果使用到任何寄存器的話那麼在程序結束前,必須恢復其值.
5.2 多鍵擴充程序
上面的程序是把某一個特殊鍵擴充成一個字符串.如果要把一組特殊鍵擴充成其個別的擴充字符串,該如何做呢?
一個比較常見的做法是,修改上面的程序,讓它接受被擴充字符以被擴充字符串為參數.譬如,如果這個程序名為MACRO,那麼可以在AUTOEXEC.BAT中定義以下的指令:
........
MACRO F1 DIR
MACRO F2 DIR/W
MACRO F3 DIR *.ASM
MACRO F4 DIR *.COM
MACRO F5 DIR *.EXE
........
這種做法是把MACRO這程序一個個留在內存中,至於每一個所做的擴充字符串則分別定義在AUTOEXEC.BAT中,因此可以AUTOEXEC.BAT以的內容.來改變擴充字符的意思.每當執行AUTOEXEC.BAT的MACRO時,就把一個新的鍵盤程序和BIOS中的鍵盤處理程序連結起來.第二次執行MACRO則是在新的鍵盤處理程序上加上第二層的鍵盤處理程序,以後依次類推.每一個輸入字符都必須經過一層一層的鍵盤處理程序,以過濾出被擴充字符.
這種鍵盤程序一層一層加上去的做法只能使用在希望被擴充字符不多時,因為 每一個希望被擴充字符需要將近一百個字節的駐留程序代碼,如果要為128個功能鍵產生個別的擴充字符時,那麼就要耗費13K字節的內存,顯然可以采納別的比較節省內存的方法.
如果可以在一個小程序中辨認出一個字符,那麼也應該可以辨認出一個以上的字符.然後使用所辨認出的字符轉換成索引值.再從一個由字符串所組成的表格中,找出所擴充的字符串.
一個字符串本身占用一個字節,而指到字符串的指針則占用兩個字節,如果有128個字符需要擴充時,則總共需要284個字節.另外原先的程序大約需要增加50個字節.因此整個程序的大小就變成大約半K字節.假設每一個擴充字符串占用20個字節,那麼128個擴充鍵就需2.5K字節,這和程序代碼的0.5K字節加起來,總共也不過3K字節,還比前一種方法少10K字節.
上面的單鍵擴充程序轉換成多鍵擴充程序時,只要修改其中的KeyRead這個程序以及數據區的內容即可.以下就是修改後的內容:
;Read a character from the keyboard input queue,
;if not expanding or the expansion string.
;if expansion is in progress
KeyRead proc
cmp cs:current,0
jne ExpandChar
ReadChar:
mov cs:current,0 ;Slightly peculiar
pushf ;Let original routine
call Old_Keyboard_IO ;Get keyboard status
cmp al,0
je Extended
jmp ReadDone
Extended:
cmp byte ptr cs:[si],0 ;Is this end of table?
je ReadDone
cmp ah,cs:[si]
je StartExpand
add si,3
jmp NextExt
StartExtend:
push bx
add si,1
mov bx,cs:[si]
mov cs:current,bx ;If so,start expanding
ExpandChar:
mov si,cs:current
mov al,cs:[si]
inc cs:current
cmp al,0 ;Is this end of string?
je ReadChar ;If so,then read a real char?
ReadDone:
pop si
ret
KeyRead endp
current dw 0
KeyTab db 59
dw dir_cmd
db 60
dw dir_wide
db 61
dw dir_asm
db 62
dw dir_com
db 63
dw dir_exe
db 50
dw make_macro
db 0 ;This must be last in key table
dir_cmp db 'DIR',0dh,0
dir_wide db 'DIR/W',0dh,0
dir_asm db 'DIR *.ASM',0dh,0
dir_com db 'DIR *.COM',0dh,0
dir_exe db 'DIR *.EXE',0dh,0
make_macro db 'MASM MACRO;',0dh,0
db 'LINK MACRO;',0dh,0
db 'EXE2BIN MACRO.EXE MACRO.COM',0dh,0
上面的程序是節省了一點的時間,但是對於和用戶界面而言則變得比較不方便,因為把功能鍵的定義移到匯編語言的程序中.但是可以高法改寫這個程序,讓它在初次執行時從一個文件裝載所定義的字符患上 .這樣做並不會改變駐留程序代碼的大小,因為裝載文件的起始碼可以在執行完後拋棄,因此不必占用駐留程序代碼的位置.
5.3 單鍵擴充程序
以下是單鍵擴充成命令字符串的程序內容:
cseg segment
assume cs:cseg,ds:cseg
org 100h
Start:
jmp Initialize
Old_Keyboard_IO dd ?
assume ds:nothing
New_Keyboard_IO proc far
sti
cmp ah,0 ;Is this call a read request?
je ksRead
cmp ah,1 ;Is it a status request?
je ksStat ;Let original routine
jmp Old_Keyboard_IO ;handle remianing subfunction
ksRead:
call KeyRead ;Get next character to return
iret
ksStat:
call KeyStat ;Return appropriate status
ret 2 ;Important!!!
New_Keyboard_IO endp
KeyRead Proc near
cmp cs:current,0
jne ExpandChar
ReadChar:
mov cs:current,0 ;Slightly peculiar
pushf ;Let original routine
call Old_Keyboard_IO ;Determine keyboard status
cmp al,0
je Extended
ReadDone:
ret
Extended:
cmp ah,59 ;Is this character to expand?
jne ReadDone ;If not,return it normally
;If so,start expanding
mov cs:current,offset String
ExpandChar:
push si
mov si,cs:current
mov si,cs:[si]
inc cs:current
pop si
cmp al,0 ;Is this end of string?
je ReadChar ;If so,then read a real char?
ret
KeyRead endp
KeyStat proc near
cmp cs:current,0
jne FakeStat
pushf ;Let original routine
call Old_Keyboard_IO ;Determine keyboard
ret
FakeStat:
mov bx,1 ;Fake a "Character ready" by clearing ZF
cmp bx,0
ret
KeyStat endp
current dw 0
string db 'masm macro;',0dh
db 'link macro;',0dh
db 'exe2bin macro.exe macro.com',0dh,0
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,es
mov dx,offset New_Keyboard_IO
mov al,16h
mov ah,25h
int 21h
mov dx,offset Initialize
int 27h
cseg ends
end Start
5.4 一般的鍵盤擴充程序Mactab.asm
以下和程序可以把由表的查詢,將任意婁的擴充鍵擴充成命令字符串:
cseg segment
assume cs:cseg,ds:cseg
org 100h
Start:
jmp Initialize
Old_Keyboard_IO dd ?
assume ds:nothing
cmp byte ptr cs:[si],0 ;end of table
je ReadDone
cmp ah,cs:[si]
je StartExpand
add si,3
jmp NextExt
StartExpand:
add si,1
push bx
mov bx,cs:[si]
mov cs:current,bx
pop bx
ExpandChar:
mov si,cs:current
mov al,cs:[si]
inc cs:current
cmp al,0 ;end of string 2
je ReadChar ;then read real char
ReadDone:
pop si
ret 3
KeyRead endp
current dw 0
KeyTab db 59
dw dir_cmd
db 60
dw dir_wide
db 61
dw dir_asm
db 62
dw dir_com
db 63
dw dir_exe
db 50
dw make_macro
db 0 ;This must be last in key table
dir_cmp db 'DIR',0dh,0
dir_wide db 'DIR/W',0dh,0
dir_asm db 'DIR *.ASM',0dh,0
dir_com db 'DIR *.COM',0dh,0
dir_exe db 'DIR *.EXE',0dh,0
make_macro db 'MASM MACRO;',0dh,0
db 'LINK MACRO;',0dh,0
db 'EXE2BIN MACRO.EXE MACRO.COM',0dh,0
New_Keyboard_IO proc far
sti
cmp ah,0 ;Is this call a read request?
je ksRead
cmp ah,1 ;Is it a status request?
je ksStat ;Let original routine
jmp Old_Keyboard_IO ;handle remianing subfunction
ksRead:
call KeyRead ;Get next character to return
iret
ksStat:
call KeyStat ;Return appropriate status
ret 2 ;Important!!!
New_Keyboard_IO endp
KeyStat proc near
cmp cs:current,0
jne FakeStat
pushf ;Let original routine
call Old_Keyboard_IO ;Determine keyboard
ret
FakeStat:
mov bx,1 ;Fake a "Character ready" by clearing ZF
cmp bx,0
ret
KeyStat endp
;Read a character from the keyboard input queue,
;if not expanding or the expansion string.
;if expansion is in progress
KeyRead proc
cmp cs:current,0
jne ExpandChar
ReadChar:
mov cs:current,0 ;Slightly peculiar
pushf ;Let original routine
call Old_Keyboard_IO ;Get keyboard status
cmp al,0
je Extended
ReadDone:
ret
Expanded:
cmp ah,59 ;Is this character to expand?
jne ReadDone ;If not,then return it normally
;If so,then start expanding
mov cs:current,offset string
ExpandChar:
push si
mov si,cs:current
mov al,cs:[si]
inc cs:current
pop si
cmp al,0 ;Is this end of string?
je ReadChar ;If so,then read a real char?
ret
KeyRead endp
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,es
mov dx,offset New_Keyboard_IO
mov al,16h
mov ah,25h
int 21h
mov dx,offset Initialize
int 27h
cseg ends
end Start