怎么样使用汇编编写DOS下的内存驻留程序?
五键盘输入扩充程序
有了前一节的基本驻留程序为基础,就可以建立起不同的应用程序.接下来,就写一个驻留程序,把用户敲入的字符,用一系列的字符来取代.这样可以减少用户的击键次数. 首先,先复习一下前一节的驻留程序的格式,如下所示: csegsegment assumecs:cseg,ds:cseg org 100h start: jmpInitialize Old_Keyboard_IOdd? ;Section 1 new_keyboard_ioprocfar sti ;Section 2 pushf assumeds:nothing callOld_Keyboard_IO nop iret new_keyboard_ioendp ;Section 3 Initialize: assumecs:cseg,ds:cseg movbx,cs movds,bx moval,16h movah,35h int21h movword ptr Old_Keyboard_IO,bx movword ptr Old_Keyboard_IO[2],es ;End Section 3 movdx,offset new_keyboard_io moval,16h movah,25h int21h movdx,offset Initialize int27h csegends endstart 只要New_keyboard_IO这个程序,就可以把以上的程序变成许多不同的键盘应用程序.在开始设计之前,必须解决一些问题. 首先,必须决定哪些键可以用来加以扩充.如果把一般的英文字母或是数目字做为扩充字符的话可能会出现一些问题.如果是对控制字符做扩充,应该不会有什么问题,但是DOS把某些控制字符视为特殊的功能.譬如Control_H,IBM PC本身有一组自己独有和增加字符(extended character),譬如:功能键(F1到F10),以及ALT键和其它组合所产生的字符等.这些增加字符通常都是使用在文书编辑程序中,这些字符比较适合用来作为扩充字符用.这组字符是由两个码组成,前面一个码永远是0,因此DOS可以很容易加以分辨.而且使用这些字符作为扩充字符对DOS的使用也不会产生太大的影响.下面是扩充字符组的第二个码大小: 12Paoudo_NULL345 678910 1112131415 Shift_Tab16Alt_Q17Alt_W18Alt_E19Alt_R20Alt_T21Alt_Y22Alt_U23Alt_I24Alt_O25Alt_P2627282930Alt_A 31Alt_S32Alt_D33Alt_F34Alt_G35Alt_H 36Alt_J37Alt_K38Alt_L3940 41424344Alt_Z45Alt_X 46Alt_C47Alt_V484950 5152535455 56575859F160F2 61F362F463F564F665F7 66F867F968F106970 71HOME72UpArrow73PgUp7475LeftArrow 7677RightArrow7879End80DownArrow 81PgDn82Insert83Delete84Shift_F185Shift_F2 86Shift_F387Shift_F488Shift_F589Shift_F690Shift_F7 91Shift_F892Shift_F993Shift_F1094Control_F195Control_F2 96Control_F397Control_F498Control_F599Control_F6100Control_F7 101Control_F8102Control_F9103Control_F10104Alt_F1105Alt_F2 106Alt_F3107Alt_F4108Alt_F5109Alt_F6110Alt_F7 111Alt_F8112Alt_F9113Alt_F10114Control_PrtSc115 Control_LArrow 116Control_RArrow117Control_End118Control_PgDn119Control_Home120Alt_1 121Alt_2122Alt_3123Alt_4124Alt_5125Alt_6 126Alt_7127Alt_8128Alt_9129Alt_0130Alt_Hyphan 131Alt_Space132Control_PgUp 接下来,需要决定把扩充字符扩充成什么样的字符串.譬如,所扩充的字符串以什么作结尾?有一个可能的选择是:回车键(Carriage Return,ASCII码0DH).这种选择很合乎逻辑,因为一般的指令都能是以回车键做结尾.但是,如果选择回车键名做扩充字符串的结尾,那么就很难表示许多行的扩充字符串.另外一个选择是使用$作为扩充字符串的结尾.但是,因为有些DOS的系统调用使用$作为字符结尾;因此如果采用$时,那么扩充字符串中就不能有$出现. C语言中都是采用ASCII码的0做为字符串的结尾,这种形式的字符串称为ASCII字符串(ASCII零结尾).使用ASCII字符串格式,就可以表示所有的可见字符和不可见字符,因为从键盘不可能输入ASCII码为0的字符. 下面的例子中,把F1这个键(扩充码59)定义为DIR指令.也可以把F1定义成以下的指令: MASM MACRO; LINK MACRO; EXE2BINMACRO.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_IOprocfar sti cmpah,0;A read request? jeksread cmpah,1;A status request? jeksstat assumeds:nothing;Let original routine jmpOld_Keyboard_IO;Do remaining subfunction ksRead: callkeyRead;Get next char to return iret ksstat: callkeyStat;GetStatus ret2;It's important!! New_Keyboard_IOendp 上面的New_Keyboard_IO程序中,把0H(读取字符)和1H(取得键盘状态)这两项功能自行处理.这个程序很简单,但是其中有一个关键点.当我们处理取得键盘状态的功能时,因为原先的键盘中断处理程序是利用ZF返回键盘状态,因此程序包中也必须保有这种特性,如果使用IRET返回的话,那么设定好ZF就会因为CPU状态标志从堆栈中取出,而恢复成未中断前的状态. 为了解决这个问题可以使用RET的参数来设置.这个参数是用来指示从堆栈中取出多少个字节.通常这是用在高级语言的子程序返回时,用来从堆栈中除去一些参数或是变数.在这里我们希望用来移去原先中断时堆栈的CPU状态,这样才有办法把改变的ZF传回,因此在这里使用了RET 2这个指令. 上面的程序码中调用到Keyread和KeyStat这两个子程序,其内容如下所示: assumeds: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 KeyStatproc cmpcs:current,0 jneFakeStat pushf;Let original routine callOld_Keyboard_IO;get keyboard status ret FakeStat: movbx,1;Fake a "char ready" cmpbx,0;by clearing ZF KeyStatendp ;Read a character from the keyboard input queue, ;if not expanding or the expansion string. ;if expansion is in progress KeyReadproc cmpcs:current,0 jneExpandChar ReadChar: movcs:current,0;Slightly peculiar pushf;Let original routine callOld_Keyboard_IO;Get keyboard status cmpal,0 jeExtended ReadDone: ret Expanded: cmpah,59;Is this character to expand? jneReadDone;If not,then return it normally ;If so,then start expanding movcs:current,offset string ExpandChar: pushsi movsi,cs:current moval,cs:[si] inccs:current popsi cmpal,0;Is this end of string? jeReadChar;If so,then read a real char? ret KeyReadendp ;Pointer to where we are in the expansion string currentdw0 ;String we will return when an F1 is typed ;0DH is ASCII carriage return stringdb'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 KeyReadproc cmpcs:current,0 jneExpandChar ReadChar: movcs:current,0;Slightly peculiar pushf;Let original routine callOld_Keyboard_IO;Get keyboard status cmpal,0 jeExtended jmpReadDone Extended: cmpbyte ptr cs:[si],0;Is this end of table? jeReadDone cmpah,cs:[si] jeStartExpand addsi,3 jmpNextExt StartExtend: push bx addsi,1 movbx,cs:[si] movcs:current,bx;If so,start expanding ExpandChar: movsi,cs:current moval,cs:[si] inccs:current cmpal,0;Is this end of string? jeReadChar;If so,then read a real char? ReadDone: popsi ret KeyReadendp currentdw0 KeyTabdb59 dwdir_cmd db60 dwdir_wide db61 dwdir_asm db62 dwdir_com db63 dwdir_exe db50 dwmake_macro db0;This must be last in key table dir_cmpdb'DIR',0dh,0 dir_widedb'DIR/W',0dh,0 dir_asmdb'DIR *.ASM',0dh,0 dir_comdb'DIR *.COM',0dh,0 dir_exedb'DIR *.EXE',0dh,0 make_macrodb'MASM MACRO;',0dh,0 db'LINK MACRO;',0dh,0 db'EXE2BIN MACRO.EXE MACRO.COM',0dh,0 上面的程序是节省了一点的时间,但是对于和用户界面而言则变得比较不方便,因为把功能键的定义移到汇编语言的程序中.但是可以高法改写这个程序,让它在初次执行时从一个文件装载所定义的字符患上 .这样做并不会改变驻留程序代码的大小,因为装载文件的起始码可以在执行完后抛弃,因此不必占用驻留程序代码的位置. 5.3单键扩充程序 以下是单键扩充成命令字符串的程序内容: csegsegment assumecs:cseg,ds:cseg org100h Start: jmpInitialize Old_Keyboard_IOdd? assumeds:nothing New_Keyboard_IOprocfar sti cmpah,0;Is this call a read request? jeksRead cmpah,1;Is it a status request? jeksStat;Let original routine jmpOld_Keyboard_IO;handle remianing subfunction ksRead: callKeyRead;Get next character to return iret ksStat: callKeyStat;Return appropriate status ret2;Important!!! New_Keyboard_IOendp KeyReadProcnear cmpcs:current,0 jneExpandChar ReadChar: movcs:current,0;Slightly peculiar pushf;Let original routine callOld_Keyboard_IO;Determine keyboard status cmpal,0 jeExtended ReadDone: ret Extended: cmpah,59;Is this character to expand? jneReadDone;If not,return it normally ;If so,start expanding movcs:current,offset String ExpandChar: pushsi movsi,cs:current movsi,cs:[si] inccs:current popsi cmpal,0;Is this end of string? jeReadChar;If so,then read a real char? ret KeyReadendp KeyStatprocnear cmpcs:current,0 jneFakeStat pushf;Let original routine callOld_Keyboard_IO;Determine keyboard ret FakeStat: movbx,1;Fake a "Character ready" by clearing ZF cmpbx,0 ret KeyStatendp currentdw0 stringdb'masm macro;',0dh db'link macro;',0dh db'exe2bin macro.exe macro.com',0dh,0 Initialize: assumecs:cseg,ds:cseg movbx,cs movds,bx moval,16h movah,35h int21h movword ptr Old_Keyboard_IO,bx movword ptr Old_Keyboard_IO,es movdx,offset New_Keyboard_IO moval,16h movah,25h int21h movdx,offset Initialize int27h csegends endStart 5.4一般的键盘扩充程序Mactab.asm 以下和程序可以把由表的查询,将任意娄的扩充键扩充成命令字符串: csegsegment assumecs:cseg,ds:cseg org100h Start: jmpInitialize Old_Keyboard_IOdd? assumeds:nothing cmpbyte ptr cs:[si],0;end of table jeReadDone cmpah,cs:[si] jeStartExpand addsi,3 jmpNextExt StartExpand: addsi,1 pushbx movbx,cs:[si] movcs:current,bx popbx ExpandChar: movsi,cs:current moval,cs:[si] inccs:current cmpal,0;end of string 2 jeReadChar;then read real char ReadDone: popsi ret3 KeyReadendp currentdw0 KeyTabdb59 dwdir_cmd db60 dwdir_wide db61 dwdir_asm db62 dwdir_com db63 dwdir_exe db50 dwmake_macro db0;This must be last in key table dir_cmpdb'DIR',0dh,0 dir_widedb'DIR/W',0dh,0 dir_asmdb'DIR *.ASM',0dh,0 dir_comdb'DIR *.COM',0dh,0 dir_exedb'DIR *.EXE',0dh,0 make_macrodb'MASM MACRO;',0dh,0 db'LINK MACRO;',0dh,0 db'EXE2BIN MACRO.EXE MACRO.COM',0dh,0 New_Keyboard_IOprocfar sti cmpah,0;Is this call a read request? jeksRead cmpah,1;Is it a status request? jeksStat;Let original routine jmpOld_Keyboard_IO;handle remianing subfunction ksRead: callKeyRead;Get next character to return iret ksStat: callKeyStat;Return appropriate status ret2;Important!!! New_Keyboard_IOendp KeyStatprocnear cmpcs:current,0 jneFakeStat pushf;Let original routine callOld_Keyboard_IO;Determine keyboard ret FakeStat: movbx,1;Fake a "Character ready" by clearing ZF cmpbx,0 ret KeyStatendp ;Read a character from the keyboard input queue, ;if not expanding or the expansion string. ;if expansion is in progress KeyReadproc cmpcs:current,0 jneExpandChar ReadChar: movcs:current,0;Slightly peculiar pushf;Let original routine callOld_Keyboard_IO;Get keyboard status cmpal,0 jeExtended ReadDone: ret Expanded: cmpah,59;Is this character to expand? jneReadDone;If not,then return it normally ;If so,then start expanding movcs:current,offset string ExpandChar: pushsi movsi,cs:current moval,cs:[si] inccs:current popsi cmpal,0;Is this end of string? jeReadChar;If so,then read a real char? ret KeyReadendp Initialize: assumecs:cseg,ds:cseg movbx,cs movds,bx moval,16h movah,35h int21h movword ptr Old_Keyboard_IO,bx movword ptr Old_Keyboard_IO,es movdx,offset New_Keyboard_IO moval,16h movah,25h int21h movdx,offset Initialize int27h csegends endStart 未完待续......