中国DOS联盟论坛

中国DOS联盟

-- 联合DOS 推动DOS 发展DOS --

联盟域名:www.cn-dos.net  论坛域名:www.cn-dos.net/forum
DOS,代表着自由开放与发展,我们努力起来,学习FreeDOS和Linux的自由开放与GNU精神,共同创造和发展美好的自由与GNU GPL世界吧!

游客:  注册 | 登录 | 命令行 | 会员 | 搜索 | 上传 | 帮助 »
中国DOS联盟论坛 » DOS开发编程 & 发展交流 (开发室) » [zt]用汇编编写DOS下的内存驻留程序
作者:
标题: [zt]用汇编编写DOS下的内存驻留程序 上一主题 | 下一主题
070
高级用户

苏醒的沉睡者


积分 659
发帖 217
注册 2003-2-15
来自 福建
状态 离线
『楼 主』:  [zt]用汇编编写DOS下的内存驻留程序

这东西可是无价之宝阿,看来声卡驱动有戏,不过需要高深的汇编基础,不知道那位大人有这能力啊,我的汇编只学了个皮毛:-<

绪言
0.1 内存驻留与中断
内存驻留程序英文叫Terminate and Stay Resident Program,缩写为TSR.这些程序加载进内存,执行完后,就驻留在内存里,当满足条件时,调到前台来执行。
内存驻留程序的常用形式有:
  >诸如Borland 的SideKick弹出式实用程序
  >日历系统
  >网络服务器
  >通讯程序
  >本地的DOS扩展(如CCDOS,UCDOS等中文系统都属于这个范畴)
  >一些可恶的人利用TSR技术制作很多可恶的病毒程序,几乎所有的病毒程序都是TSR程序.
就象多任务系统调度一个进程有一个调度程序一样,在PC中从前台程序进入到一个TSR,也要有一个调度者,只是PC操作系统的调度不称为调度程序,而只称为触发机制.触发机制调度TSR执行在PC机上党称为激活一个TSR.触发机制主要有以下几种:
  >硬件中断:党用的是键盘中断INT 9H,时钟中断INT 8H,通讯中断INT 14H,磁盘中断INT 13H等等.
  >软件中断:党用的是键盘中断INT 16H,时钟中断INT 1CH,DOS中断INT 21H,等等.
  >以上各种的结合.
  从以上的触发机制可以看出,TSR和PC机的中断系统有着密切的关系.每种激活方式实际上都是与中断有关的.常用特殊的击键序列的识别码是通过截获INT 9H和INT 16H来实现.实际上不管TSR程序的哪一个环节,都与中断有着密切的关系.因此在具体进行TSR和程序设计之前,先介绍PC中断系统.在此只作简单说明.
在PC机内存的最低端(0000H开始)的1K字节中,存放着256个指针即常说的中为向量或中断矢量(Interrupt vertor),每个中断向量都指向一个子程序,该程序称为中断处理程序(Interrup handler).一个中断向量由四个字节组成,有一个字是中断处理程序的偏移量值,后一个字是中断处理程序的段值.256中断向量一起称为中断向量表.
手式计算中断向量的首址,可通过以下的公式来求得:
  X号中断向量的首址=0000H:X*4
当产生一个中断时,处理器都按顺序执行以下步骤:
  >在堆栈上压入处理器的标志(相当于指令PUSHF).
  >在堆栈上压入当前CS和IP值(相当于指令PUSH CS和PUSH IP).
  >关闭中断(CLI)
  >从中断向量加载的CS和IP,执行中断处理程序.
当执行完中断处理程序后,一般用IRET返回,它的作用是:
  >从堆栈上取出保存的IP和CS(相当于指令POP CS和PUSH CS).
  >同时恢复中断前的处理器标志(相当于指令POPF).
中断有多种分类,由触发的原因和实现的性质来分,可分为硬件中断和软件中断,从操作系统分层实现来说,可以分成BIOS中断,BOS中断和用户中断.
  一方面,BIOS和DOS通过中断系统向用户提供一个操作系统功能界面.也就是说用户(一般来说是前台程序)的功能主要是通过调用DOS和BIOS的中断服务来实现的,具体来说就是通过INT指令来实现的.另一方面,BIOS和DOS由中断系统所构成,BIOS对硬件成为高层的功能,并通过中断的形式向用户提供.
如果在当前程序执行的同时,能将一块代码放在内存,把中断向量指向代码中的子程序,那么在当前程序执行中产生中断时,就有可能执行不属于当前程序和操作系统的代码,产生的中断可能是当前程序产生的软件中断,也可能是由硬件产生的硬件中断.这就是单任务的PC操作系统可能执行多于一个进程的简单说明.
在PC中断系统中有几个中断具有周期性,即INT 8H,INT 1CH和INT 28H.它们或者周期性被执行用于时间计时,或者周期性产生用于等待.它们是在实现TSR时进行轮询触发的基础.键盘中断(INT 9H和INT 16H)当用户击键时发生,利用它们是进行热键处理的基础.串行口通讯也是触发的一个重要机制.此外众多的软件中断也是触发的媒介.

0.2 DOS的可重入性分析
一个多任务操作系统之所以能使多个进行并存,是因为操作系统的大部分代码是可以了重的,对于临界资源有相应的PV操作,使得当调度一个新的进程时,能完整地保存前一个里程的现场,当再一次调度被挂起的进程时能象没有被中断一样继续执行.
  对于PC机来说,代码的重入性比较弱,对临界资源没有PC操作.当我们用中断程序启动用户的TSR时,如果只保存标志和寄存器,以及当前进程一些信息,那么只保存了当前程序的一部分现场,DOS的临界资源不会自动保存.在进行TSR设计时,一定要了解PC操作系统的重入性和临界资源.
重入性总是体现在代码上,所谓可重入代码的指这样的代码,即该代码被执行时还没有从中退出,由于某种原因又一次或者多次进入相同的代码,该代码每次的执行结果都是正确的,就说该代码是可重入的.相反,如果结果不正确,那么就就该代码是不可重入的.下面是一个可重入的子程序的例子:
  Add proc near
   cmp DS:word ptr [si],0
   je DonotAddTheValue
   add ax,DS:word ptr [si]
  DonotAddTheValue:
   ret
  Add endp
上面的例子不管在其中任何一处再一次执行该子程序,执行结果不变.为了说明,只举多种可能性中的一种.
  mov ds,0100h    ;ds=0100h
  mov si,0010h    ;si=0010h
  mov ax,0001h    ;ax,=0001h
  call Add
  cmp 0100h:word ptr [0010h],0  ;Call Add subroutine
  push ds     ;Interrupted
  push si
  push ax
  mov ds,0200h    ;ds=0200h
  mov si,0200h    ;si=0020h
  mov ax,0003h    ;ax=0003h
  call Add
  cmp 0200h:word ptr [0020h],0  ;0200:0020h=0004h
  jne
  add ax,0200h:word ptr [0020h]  ;ax=0007h
  ret      ;Return
  pop ax     ;ax=0001h
  pop si     ;si=0010h
  pop ds     ;ds=0100h
  iret      ;Return to Add subroutine
  jne
  add ax,0100h:word ptr [0100h]  ;ax=   0001h
        ;0100h:0010h= 0002h
        ;----------------------------------------
        ;ax  = 0003h
  ret
  mov bx,ax
而下面的子程序是不可重入的:
  Add proc near
   mov Temp,ax
   mov ax,DS:word ptr [si]
   cmp ax,0
   je DonotTheValue
   add ax,Temp
  DonotTheValue:
   ret
  Temp:
   dw  0
  Add endp
可以利用检查可重入子程序的方法检查这个子程序的不可重入性,尝试一下在" mov ax,DS:word ptr [si]"指令后再次执行该子程序,那么就会出第一次调用返回的结果不对.
  mov ds,0100h    ;ds=0100h
  mov si,0010h    ;si=0010h
  mov ax,0001h    ;ax,=0001h
  call Add
  mov Temp,ax    ;Call Add subroutine
        ;Temp=0001h
  mov ax,0100h:word ptr [0010h]  ;0100h:0010h=0002h
        ;ax=2
  push ds     ;Interrupted
  push si
  push ax
  mov ds,0200h    ;ds=0200h
  mov si,0020h    ;si=0020h
  mov ax,0003h    ;ax=0003h
  call Add
  mov Temp,ax    ;Temp=0003h
  mov ax,0200h:word ptr [0020h]  ;0200h:0020h=0004h
  cmp ax,0     ;ax=0004h
  jne      ;Not equal ,add
  add ax,Temp    ;ax=0007h
  ret      ;Return to the interrupted point
  pop ax     ;ax=0002h
  pop si     ;si=0010h
  pop ds     ;ds=0100h
  iret      ;Return to Add subroutine
  cmp ax,0     ;ax=2
  jne      ;No equal,add
  add ax,Temp    ;ax   =0002h
        ;0100h:0010h =0003h
        ;----------------------------------------
        ;ax   =0005h
  ret     
  mov bx,ax
上面执行的结果是AX=5,实上正确的结果应该是AX=3,这是由于当Add子程序从中断子程序再一次被调用时,修改了Temp的值,当从中断返回时不能正确恢复其值.
解决的方法是把Temp放在堆栈中,当每次Add子程序被调用时Temp的地址都不一样,因此原调用的Temp值不会被第二次在中断中调用的Add所破坏.
  Add proc near
   push bp    ;Store BP
   sub sp,2    ;distribute a byte space in the stack
   mov bp,sp    ;SS:BP point to the stack head
  temp equ SS:word ptr [BP+0]  ;Explain the pointer to SS:BP
   mov Temp,ax
   mov ax,DS:word ptr [si]
   cmp ax,0
   je DonotAddTheValue
   add ax,Temp
  DonotAddTheValue:
   add sp,2    ;Release the dsitributed space in the stack
   pop bp    ;Restore BP
   ret
  Add endp
  对于DOS来说,DOS的内存数据就象Temp变量,它被分配在数据区,而不在堆栈上,因此DOS从总体上是不可重入的.从最后的一个例子看来.重入性跟堆栈有很大的关系.可重入代码允许在任何时候被中断,其所有的变量都存放在该代码的私有堆栈中.DOS是一个单任务的操作系统,在执行INT 21H的代码时是不允许中断DOS,并再次调用INT 21H的.每个时该最多有一个进程在调用DOS的代码.
对DOS的重入性,以及相应所作的处理总结如下:
  >当通过INT 21H调用DOS时,DOS会使三个内部栈之一:I/O栈,磁盘栈和辅助栈.功能00H到处0CH使用I/O栈,除了不致命错误处理程序以外使用磁盘栈,致命错误处理程序使用辅助栈.在这种栈切换模式下,如果前台处在INT 22H中,而TSR调用了使用相同栈的DOS功能, 就会使前台程序保存栈中的数据被TSR的数据覆盖掉;但如果调用不同栈的DOS功能,那将是安全的.INT 21H中的几个功能调即33H,50H,  51H,62H,和64H由于非常简单,使用用户栈,因此在任何情况下都是可重入的.避免这种不可重入的简单方法是当前台程序正处在INT 21H 中时,不要调用INT 21H.或者如果前台程序正在处理INT 21H时,只允许调用不同栈的INT 21H功能.
  >DOS数据区中有一个InDOS标志,也探源为DOS安全标志,表示当前访问DOS功能是来否安全.由于DOS不可重入,它指示当前是 否处于DOS中,激活TSR和代码可检查该标志(34H),如果DOS忙,则不能激活使用INT 21H 调用的TSR.
  >当前台程序执行能设置错误状态的DOS功能时,DOS会把扩展错误信息存放起来,正常情况下,前台程序可以读取扩展错误信息; 如果在前台程序读取信息之前激活TSR,且TSR也执行能报告错误信息的DOS功能,则后来的错误信息会覆盖原来的错误信息,前台程序就会得不到正确的错误信息.因此必须在激活TSR之前保存(59H)这些错误信息,并在退出以前把它们恢复(5D0AH)成原来的值.
  >大多硬件中断如INT 13H,INT 0BH和INT 0CH等都是不可重往返.如果设置一引起寄存器,而在此时被TSR打断,执行类似的设置 ,就会出现非常情况,端口是不会自动保持值的.在进入这些中断时设置一个进入的标志,如果TSR检查到标志已置,则不调用相应的中断.
  >最好也不要重入INT 10H,INT 25H,和INT 26H中断.在进入这些中断时设置一个进入的标志,如果TSR检查到标志已置,则不调用 相应的中断.
  >最好能接管INT 1BH,INT 23H和INT 24H中断.
  >保存DOS的数据交换区(SDA)可以安全地使用的DOS的功能.SDA保存了DOS几乎所有内部数据,如果保存(5D06H)和恢复 (5D0BH)SDA ,DOS就变成在任何时候都可以重入的了.当DOS处在关键区中时,调用INT 2AH.一旦处在关键区中,就不能改变SDA.在关键区的结束处会 调用INT21H的81H和82H功能.
0.3 内存驻留程序设计一般过程
驻留程序分成两个部分,即暂驻部分和驻留部分.驻留程序要完成安装检测,激活和删除等过程.
基本上可抽象成以下几个过程:
  >取中断向量
  >保存旧的中断向量
  >设置或恢复中断向量
  >中断处理程序的链接
  >检测是来呀已驻留
  >执行终止并驻留
  >TSR的删除
删除TSR比较复杂,必须按下列步骤进行:
  >检查中断向量是否已经被替换.如果没有替换,就恢复所有的中断向量;如果某个中断向量被替换,则跳过下面各步,不能删除该 TSR.
  >TSR的PSP中偏移量16H存放着父进程的PSP.把这个值改为当前进程的地址.
  >把当前PSP设为TSR的PSP
  >执行INT 21H的4CH功能,释放TSR占用的内存,关闭所有文件,并使用PSP中存放的父进程地址和终止地址.
  >这里控制返回到初始进程中,当前PSP也指向初始进程,所有寄存器值包括SS和SP都不确定.
在执行完上述步骤后,要恢复寄存器.
如果要无条件地删除TSR,必须监控每个TSR对中断向量表,内存控制块和设备驱动程序链的修改.
0.5 缩写语表
ASCIZ: 以零结束的ASCII字符串.
BPD:  "BIOS Parameter Block (BIOS 参数块)"的缩写.含有对驱动器的低级参数的说明.
CDS:  "Current Directory Structure(当前目录结构)"的缩写,含有某个逻辑驱动器的当前目录,类型和其它信息.
DPB:  "DOS Drive Parameter Block(DOS驱动器参数块)"的缩写,含有某个逻辑驱动器的介质说明及一些内部信息.
DPL:  "DOS Parameter List (DOS参数表)"的缩写,该数据结构用来传递参数给SHARE和网络功能调用.
DTA:  "Disk Transfer Address(磁盘传输地址)"的缩写,指示对磁盘进行数据读写的功能调用不必显式地给出缓冲区地址.
FAT:  "File Allocation Table(文件分配表)"的缩写,磁盘的文件分配表记录了所使用的簇信息.
FCB:  "File Control Block(文件控制块)"的缩写,在DOS的1.X版本中,用FCB来记录文件打开的状态..
IFS:  "Installable File System(可安装的文件系统)"的缩写,它允许一个非DOS格式的介质被DOS所使用. 大多数情况下IFS     与网络驱动器非常相似,尽管IFS最典型的情况是一个本地驱动器而不是一个远程驱动器.
JFT:  "Job File Table(工作文件表)或Open File Table(打开文件表)"的缩写,程序PSP中的JFT可用来将文件句柄转换成SFT值.
NCB:  "Network control Block(网络控制块)"的缩写.NCB可用传递对NETBIOS的请求和接受来自NETBIOS处理程序的状态信息.
PSP:  "Porgram Segment Prefix(程序段前缀)"的缩写.当程序被装入时,PSP为一个预留的256字节的数据区它包含了程序调用时   的命令行内容和一些DOS的内部信息.
SDA:  "DOS Swappable Data Area (DOS对换数据区)"的缩写.SDA中包含有DOS内部使用的记录某个正在处理的功能调用状态的   所有变量.
SFT:  "System File Table(系统文件表)"的缩写,SFT是一个DOS内部数据结构,在DOS 2+版本的句柄功能调用中用于管理某个已打   开文件的状态,这就和在DOS1.X中,FCB管理已打开文件状态一样.



好久没碰Dos,手都生了,赶紧回来练练.嘿嘿
2006-6-19 03:38
查看资料  发送邮件  发短消息 网志  OICQ (181315400)  编辑帖子  回复  引用回复
070
高级用户

苏醒的沉睡者


积分 659
发帖 217
注册 2003-2-15
来自 福建
状态 离线
『第 2 楼』:  

基本原理
2.1 8086/8088
IBM PC中央处理单元(Central Processing Unit)是微处理器Inter 8088,8088是8086是小的版本.对于编写程序而言,两者几乎完全相同.两者之间的差别是在于:它们对外的沟通.8086和外界沟通时是经由16 位的输入输出通道,内存存取也是每次以16位为单位,8088和8086极为相似,但是它和外界沟通时就必须经由16位的通道.
2.1.1 寄存器
  8086/8088的结构简单,其中包含了一组一般用途的16位寄存器.AX,BX,CX,DX,BP,SI,DI.其中AX,BX,CX,DX还可以分成8位的寄存器,譬如:AX可分为AH,AL;BX可分为BH,BL;CX可分为CH,CL;DX可分为DH,DL.寄存器BP,SI,DI的用途也没有特别的限制, 但是却不能分成两个字节.另外寄存器SP主要是用来当做堆栈指针.除此之外,还有四个非常重要的段寄存器(Segment Register):CS,DS,SS,ES.指令指针(Instru -ction pointer)IP是用来控制目前CPU执行到哪一个指令.
  8086设计时考虑到要和8位的CPU8080兼容.8位的计算机是使用两个字节(亦即16位)来定址,因此其定址空间可以达64K字节.16位的CPU 在地址设定上选择了完全不同的方法.CPU以段(Segment)为单位,每一段范围内包括64K字节,而内存中则可以包含许多段.所以,操作系统可以在一个段内执行.而使用者的程序则可以在另一个段内执行.在一个段内,程序包可以把计算机视为只有64K字节内存空间.因此原先8位计算机上执行的程序就可以很容易地移植到16位计算机上.除此之外,内存段也可以彼此重叠,因而两个不同的程序就可以共用某一块内存.段值是以寄存器来设定的,而实际的地址值则是把段值(16位)往左移4位,然后再加上16位的位移(Offset),因此构成20位的地址值.所以8086可以直接做20位的地址,也就是可能存取到一兆字节的内存.在这一兆字节的内存中,IBM PC保留了最前面的320K字节给系统的ROM BIOS和显示内存,因此使用者最多也就能使用640K字节.
2.1.2 寻址方式
  寻址方式(Addressing mode)是一台计算机上许多复杂操作的关键所在.8086提供了以下几种寻址方法:立即寻址,内存间接寻址, 寄存器间接寻址等.
  立即寻址,直接使用数字.
  内存间接寻址,数值存放在数据段中的某个位置.
    mov bx,foo
    foo dw 5
  寄存器间接寻址.有两种寄存器可以使用在这种寻址方式下:基址寄存器(Base Register)和索引寄存器(Index Register).基址寄存 器分别是BX和BP,索引寄存器则是SI和DI.在这种寻址方式下,寄存器存放了数据段中的地址值.
    mov ax,0F000h
    mov es,ax
    mov si,0FFFEh
    mov dl,byte ptr es:[si]
  上面的程序使用间接寻址方式,由寄存器SI读出位于F000:FFFE位置的数据.寄存器间接存取时,最多只能使用玛个基址寄存器各 一个索引寄存器.
  以上的寻址方式可以做不同的结合,因此组合后的结果很多.
2.1.3 标志
  8086有9个一位的标志(Flag),它们可以用指示CPU的各种状态.以下是9个标志的简介:
   CF(Carry Flag):CF为1时就表示算术运算的结果超出正确的长度.
   PF(Parity Flag):PF为1就表示使用偶校验,PF为0就表示使用奇校验.
   AF(Auxiliary Carry Flag):和CF相同,只是它使用在低4位的结果.AF通常都使用在20位的地址计算上.
   ZF(Zero Flag):ZF为1就表示运算结果是0,否则ZF就为0.
   SF(Sign Flag):SF为1就表示运算结果的最高位是1,否则SF就为0.
   TF(Trap Flag):TF为1,CPU就单步地执行,在这种模式下每完成一个指令就发生一个特殊的中断.
   IF(Interrupt Enable Flag):IF为1,允许CPU接收外界的中断,否则IF就为0.
   DF(Direction Flag):这个标志使用在循环指令,譬如:MOVS,MOVSB,MOVSW,CMPS,CMPSB和CMPSW.如果DF为1,循环运行时就使地  址值往前增加.如果DF为0,则使地址往后减少.
   OF(Over Flag):OF为1,表示一个考虑正负号的运算超出了正确的字节的长度.
2.1.4 循环
  所有的循环指令都是以CX作为计数器.一个循环会反复地执行直到CX等于某一特定值为止.以下的程序就是利用反复地相加,完成 两个数的相乘.
    mov ax,0
    mov cx,4
   next: add ax,6
    loop next
  在上面的程序中,LOOP指令执行时会把CX减1,并且检查CX的内容;如果CX等于0,就转移到下一条指令,否则就跳到NEXT标示的地方 执行.
  也可以用下面的程序完成相同的功能:
    mov ax,0
    mov cx,4
   next:
    add ax,6
    dec cx
    cmp cx,0
    jne next
2.1.5 内存的数据结构
  8088是以字节为存取数据的基本单位.计算机的存储结构是8位的字节,但是CPU本身处理数据则是以16位为单位.在内存中,都遵 循一个原则,即:高高低低的存储方式.高字节对应高地址,低字节对应低地址.
  下面是一个简单程序,在AX中放入一个字节的内容并显示:
   cseg segment
    org 100h
    assume cs:cseg,ds:cseg
   start:
    mov bx,cs
    mov ds,bx
    mov ah,'H'
    mov al,'L'
    mov test,ax
    mov al,[si]   ;First byte of test
    call dchar
    mov al,[si+1]  ;Second byte of test
    call dchar
    ret
   ;Display the character contained in AL
   dchar  proc
    push ax
    push bx
    mov bh,1
    mov ah,0eh
    int 10h
    pop bx
    pop ax
    ret
   dchar  endp
   test dw ?
   cseg ends
    end start



好久没碰Dos,手都生了,赶紧回来练练.嘿嘿
2006-6-19 03:38
查看资料  发送邮件  发短消息 网志  OICQ (181315400)  编辑帖子  回复  引用回复
070
高级用户

苏醒的沉睡者


积分 659
发帖 217
注册 2003-2-15
来自 福建
状态 离线
『第 3 楼』:  

三 中断矢量
3.1 IBM PC提供的中断
IBM PC有两种基本形态的中断.如果是由外围设备所产生的中断就叫做硬件中断(Hardware interrupt),譬如:键盘,磁盘机和时钟等外围设备都可以产生硬件中断.外围设备所产生的中断信号都连接到中断控制器,中断控制器可以根据它们之间的重要性来安排优先顺序,以便使CPU有效地处理这些硬件信号.另一种中断是软件中断(Software interrupt),软件中断也叫做陷井(Trap),它是由执行中的软件所产生.虽然软件包中断的处理方式和硬件中断完全相同,但是通常软件中断是希望执行操作系统所提供的服务.
表3.1是IBM PC所提供的中断,这些中断是根据中断号码和中断矢量(Interrupt vector)排列.
IBM PC的用户或是编写应用程序的程序人员很少会直接接触到硬件中断,除非是使用某些特殊的硬件,或是需要较严格的要求时,最常被修改的硬件中断是敲键盘所产生的中断(9H),尤其是文本编辑的程序.大体而言,只有硬件设计者基是系统程序人员才会注意到所有在硬件中断;编写内存驻留程序的设计人员则只使用到部分硬件中断而已,尤其是:键盘中断和计时器(Timer)的中断.
反之,软件中断对于任何编写汇编程序的人,甚至对编写高级语言程序的人都相当的重要.软件中断是应用程序进入到IBM PC操作系统的接口,经由这些接口应用程序才可以执行所要求的系统服务.
其中软件中断中最重要,同时也是最常被汇编语言程序设计师所用到是DOS INT 21H.这个中断是执行DOS系统调用的软件中断,它可以让应用程序执行任何DOS的操作.
接下来最有用的软件中断是ROM-BIOS(基本输入输出系统)所提供的中断.这些软件中断是IBM PC所提供的的低层次服务,譬如:键盘输入,显示器输出和磁盘机的输入与输出等.
3.2 键盘输入的方法
  以下就以IBM PC从键盘读取字符为例子,来说明中断的工作方式.IBM PC从键盘读取字符时,使用了两种不同形式中断,亦即:硬件中断和软件中断.当使用者从键盘敲下一个键时,键盘的线路就会送出一个信号.这个信号会造成硬件中断发生,从而触发低层次的键盘中断处理程序开始执行.这个中断处理程序马上从键盘的硬件读取使用者所敲入的字符,然后把它放到一个队列中,如果这个队列填满时,键盘中断处理程序会使IBM PC发出一声响.键盘中断处理程序做完这些事情之后,它就把控制权交还给原先被中断的程序.如果有一个程序希望从键盘读取一个字符时,它就发出适当的软件中断信号,这时候就由相对应的中断处理程序去检查键盘队列,并且传回队列中的第一个字符.
上面所介绍的键盘输入工作方式,在中断驱动系统中很普遍地采用.这和做法可以把实际上需要输入的应用程序和实际上执行输入的处理部分分开来.这种做法也可以用在其它不同形式的输入和输出外围设备.
3.3 改变输入矢量
中断矢量储存在IBM PC最前面的400H个字节中.每一个矢量的长度是四个字节组成,这四个字节内所存放的是中断处理程序执行的地址值.其中前两个字节包含地址值的位移(Offset)部分,后面的两个字节则包含了段(Segment)部分.
中断矢量有两种修改方法.可以直接地设置中断矢量的地址值,或是使用DOS所提供的系统调用设置中断矢量的地址值.
3.3.1 直接设置中断矢量
  因为中断矢量只是存放地址值的存储位置,因此我们可以直接地把地址存放到存储位置中.以下是一个小例子:
    mov ax,0
    mov es,ax
    mov word ptr es:24,offset Keyboard
    mov word ptr es:26,seg Keyboard
  在许多情况下,上面的程序都可以正确地执行.但是如果上面的程序正在执行时突然敲下一个键的话,就可能会问题;而最糟的情况是发生:第三个MOV已经执行完毕,而第四个MOV尚未执行时.如果在此时敲下任何键的话,键盘中断矢量都没有任何意义,而造成整个系统死机.因此我们可以在设置中断矢量时,让中断无效,譬如:
    mov ax,0
    mov es,ax
    cli
    mov word ptr es:24,offset Keyboard
    mov word ptr es:26,seg Keyboard
  上面的做法在大部分的情况下都可以正确地执行.但是CLI这个指令无法停止NMI中断(不可屏蔽中断),因此如果发生NMI中断时就 没用办法.下面的这一种做法虽然比较复杂,但是对于所有的中断都有效,这包括了NMI中断在内:
    mov word ptr kbd-ptr[0],offset Keyboard
    mov word ptr kbd-ptr[2],seg Keyboard
    mov di,0      ;Use Di to Set ES to zero
    mov es,di      ;Set ES to destination segment
    mov di,24      ;Set DI to destination offset
    mov si,offset kbdptr    ;set SI to source offset
    mov cx,2      ;Set word count to 2
    cld       ;Set direction to forward   
    cli        ;Disable interrupts
    rep movsw     ;Copy the new vector
    sti       ;Enable interrupts
    kbdptr  dd  ?
  上面的程序中,kbdptr是两个字节(WORD)的指针(Pointer),其中包含了键盘 中断处理程序的起始志趣值.REP这个指令将根据寄存器CX所设置的次数来重复执行MOVSW,而整个指令就如同单一的指令一样.NMI中断不能够发生在一个完整的指令中.因为地址值搬移的操作都能包含在一个单一指令中,因此可以免除任何中断的干扰.
3.3.2 使用DOS来设置中断矢量
  因为要想安全地设置中断矢量需要一些技巧,因此DOS提供了一项特殊的服务,以帮助程序人员安全地设置中断矢量,如果只使用 DOS所提供的这项服务来设定中断矢量的话,那么就不必担心会发生前面所叙述的差错.DOS同时也提供了:读取中断矢量的服务.因为读取中断矢量的内容不会修改系统的状态;因此若直接写程序读取,也很安全.但是如果你要自己直接读取中断矢量的内容时,就必须计算出中断矢量的位置.而DOS已经提供了这项服务.
  使用DOS所提供的系统调用,来读取中断矢量的内容时,必须利用INT 21H中的函数35H(读取中断矢量),这个函数热气矢量号码来 计算中断矢量的地址,然后返回其中的内容.以下就是一个例子:
    Old_Keyboard_IO  dd  ?
    mov al,16h
    mov ah,35h
    int 21h
    mov word ptr Old_Keyboard_IO,bx  ;Offset of interrupt handler
    mov word ptr Old_Keyboard_IO,es  ;Segment of interrupt handler
  用DOS来设置中断矢量例子:
    New_Keyboard_IO  dd  ?
    mov word ptr New_Keyboard_IO,bx  ;Offset of interrupt handler
    mov word ptr New_Keyboard_IO,es  ;Segment of interrupt handler
    mov al,16h
    mov ah,25h
    int 21h
3.4 检查中断矢量
这里都是采用COM格式编程,可以建立一个BAT文件来处理写好的程序,以减少击键次数.设BAT文件名为MAKE.BAT:
    MASM %1
    LINK  %1
    EXE2BIN %1.EXE %1.COM
如果写好的程序名为MACRO.ASM,则可敲入:
    C:\MAKE MACRO.ASM
即可.
3.5 显示中断矢量
下面这个例子可以列出所有的重要的中断矢量内容,在刚刚打开PC时,并且没有执行任何驻留程序时,可以发现所有的中断矢量段值都相同,这些地址值所存放的是ROM的程序.当你修改中断矢量之后,就可以利用这个程序观察到中断矢量的变化.以下就是IVEC.ASM的内容:
   cseg    segment para public 'CODE'
    org     100h
    jmp start
    assume  cs:cseg,ds:cseg
   start:
    mov     bx,cs                   ;Make data seg be the same as
    mov     ds,bx                   ;the code seg
    call    vectors
   waitIn:
    mov ah,0bh
    int 21h
    cmp al,0ffh
    jne waitIn
    mov     ah,4ch
    int     21h
   ;****************************************************************************
   ;Scan through display table,prinying two vectors per line
   ;If any record has an interrupt #=zero,this indicates
   ;end of the table.
   ;****************************************************************************
    mov     di,offset disptab       ;Pointer to start of table
    mov     dh,0                    ;Zero out top half of DX
   vloop:
    mov     dl,[di]                 ;Get the interrupt number
    cmp     dl,0                    ;If it's zero,we are done
    je      vdone                   ;so exit loop
    add     di,1                    ;Advance pointer 1 byte
    mov     si,[di]                 ;Get pointer to description
    call    dvector                 ;Call the display routine
    add     di,2                    ;Get the interrupt number
    mov     dl,[di]                 ;Advance to the next record      
    cmp     dl,0                    ;If it's zero,we are done
    je      vdone                   ;so exit loop
    add     di,1                    ;Advance pointer 1 byte
    mov     si,[di]                 ;get pointer to description
    call    dvector                 ;Call the display routine
    add     di,2                    ;Advance to the next record
    jmp     vloop                  
   vdone:                   ;Print final CRLF
    ret
   vectors endp
   ;----------------------------------------------------------------------------
   ;Displays an interrupt vector.Display is in the form of
   ;<banner>,<interrupt#>,<seg>:<offset>
   ;where <interrupt #>,<seg>and<offset>
   ;are all dexadecimal numbers
   ;Call with
   ;DX     -interrupt number
   ;DS:SI  -pointer to banner string
   ;----------------------------------------------------------------------------
   dvector proc    near
    call    dstring                 ;Display the string in DS:SI
    call    dbyte                   ;Display the byte in DL
    call    dspace                  ;Display a space
    call dspace
    ;
    mov     al,dl                   ;move the interrupt number to AL
    mov     ah,35h                  ;Function is Get interrupt vector
    int     21h
    mov     dx,bx                   ;Move BX to DX so we can display
    call    ddword                  ;double-word in ES:DX
    call dEndFra
    call    dcrlf                   ;Display a newline
    ret
   dvector endp  
   ;----------------------------------------------------------------------------
   ;DS:SI points to ASCII string to be printed
   ;----------------------------------------------------------------------------
   dstring proc    near
    push    si
    push    ax
   dis:    mov     al,[si]                 ;Fetch the next character
    cmp     al,0                    ;If it's zero,we are done
    je      disdone        
    call    dchar                   ;If not,point it
    inc     si                      ;Advance pointer to nest char
    jmp     dis
   disdone:pop     ax
    pop     si
    ret
   dstring endp
   ;----------------------------------------------------------------------------  
   ;ES:DX contains double word to be displayed
   ;----------------------------------------------------------------------------
   ddword  proc    near
    push    dx                      ;Save offset temporarily
    mov     dx,es                   ;Move segment to DX
    call    dsword                  ;Display segment
    call    dcolon                  ;Print a ";"
;          call    dcrlf
    pop     dx                      ;Restore offset to DX
    call    dsword                  ;Display offset
    ret
   ddword  endp
   ;----------------------------------------------------------------------------
   ;DX containes single word to be displayed
   ;----------------------------------------------------------------------------
   dsword  proc    near
    push    dx                      ;Save low byte temporarily
    mov     dl,dh                   ;Move high byte to low byte
    call    dbyte                   ;Display high byte
    pop     dx                      ;Restore low byte to DL
    call    dbyte                   ;Display low byte
    ret
   dsword  endp
   ;----------------------------------------------------------------------------
   ;DL contains byte to be displayed
   ;----------------------------------------------------------------------------
   dbyte   proc    near
    push    ax                      ;Save any registers used
    push    dx                     
    push    si
    push    dx                      ;Save low nybble temporarily
    push    cx                      ;Save CX
    mov     cl,4                    ;Set shift count to 4
    shr     dx,cl                   ;Shift high nybble into low nybble
    and     dx,0fh                  ;Mask out all but low nybble
    mov     si,dx                   ;Use low nybble as index into
    mov     al,hextab[si]           ;hexadecimal character table
    call    dchar                   ;Display character
    pop     cx                      ;Restore CX
    pop     dx                      ;Restore low nybble
    and     dx,0fh                  ;Mask out all but low nybble
    mov     si,dx                   ;Use low nybble as an index into
    mov     al,hextab[si]           ;hexadecimal character table
    call    dchar                   ;Display character
    pop     si                      ;Restore registers
    pop     dx
    pop     ax
    ret
   dbyte   endp
   ;----------------------------------------------------------------------------
   ;Display a ":"
   ;----------------------------------------------------------------------------
   dcolon  proc    near
    mov     al,':'
    call    dchar
    ret
   dcolon  endp
   ;----------------------------------------------------------------------------
   ;Display a " "
   ;----------------------------------------------------------------------------
   dspace  proc    near
    mov     al,' '
    call    dchar
    ret
   dspace  endp
   ;----------------------------------------------------------------------------
   ;Display a Carriage Return/Line Feed
   ;----------------------------------------------------------------------------
   dcrlf   proc    near
    mov     al,0dh
    call    dchar
    mov     al,0ah
    call    dchar
    ret
   dcrlf   endp
   ;----------------------------------------------------------------------------
   ;Display the character contained in AL
   ;----------------------------------------------------------------------------
   dchar   proc    near
    push    ax
    push    bx
    mov     bh,1
    mov     ah,0eh
    int     10h
    pop     bx
    pop     ax
    ret
   dchar   endp
   ;----------------------------------------------------------------------------
   ;Data define
   ;----------------------------------------------------------------------------
   hextab  db      '0123456789ABCDEF',0
   disptab db      05h                     ;Print screen
    dw      v05
    db      19h                     ;Bootstrap loader
    dw      v19                    
    db      08h                     ;Timer tick
    dw      v08
    db      1ah                     ;Real_time clock
    dw      v1a
    db      09h                     ;Keyboard input
    dw      v09   
    db      1bh                     ;CTRL_Break handler
    dw      v1b   
    db      0bh                     ;Comm.port 1
    dw      v0b                    
    db      1ch                     ;Timer control
    dw      v1c
    db      0ch                     ;Comm.port 0
    dw      v0c
    db      1dh                     ;Pointer to video parameter table
    dw      v1d
    db      0dh                     ;Hard disk controller
    dw      v0d
    db      1eh                     ;Pointer to disk parameter table
    dw      v1e
    db      0eh                     ;Floppy disk controller
    dw      v0e                    
    db      1fh                     ;Pointer graphics character table
    dw      v1f
    db      0fh                     ;Printer controller
    dw      v0f            
    db      20h                     ;Program terminate
    dw      v20
    db      10h                     ;Video driver
    dw      v10
    db      21h                     ;DOS universal function
    dw      v21
    db      11h                     ;Equipment check
    dw      v11            
    db      22h                     ;Pointer to termination handler
    dw      v22
    db      12h                     ;Memorey size check
    dw      v12
    db      23h                     ;Pointer to Ctrl_C handler
    dw      v23
    db      13h                     ;Disk driver
    dw      v13
    db      24h                     ;Pointer to critical error handler
    dw      v24
    db      14h                     ;Communications driver
    dw      v14
    db      25h                     ;Absolute disk read
    dw      v25
    db      15h                     ;Cassette driver
    dw      v15
    db      26h                     ;Absolute disk write
    dw      v26   
    db      16h                     ;Keyboard driver
    dw      v16
    db      27h                     ;Terminate and stay resident
    dw      v27
    db      17h                     ;Printer driver
    dw      v17
    db      2fh                     ;Print spooler
    dw      v2f
    db      18h                     ;Rom basic
    dw      v18
    db      0
    dw      0
   v05     db      186,5 dup (20h),'Print screen:',26 dup (20h),0
   v08     db      186,5 dup (20h),'Timer tick controller:',17 dup (20h),0
   v09     db      186,5 dup (20h),'Keyboard input:',24 dup (20h),0                     
   v0b     db      186,5 dup (20h),'Communication port 1:',18 dup (20h),0
   v0c     db      186,5 dup (20h),'Communication port 0:',18 dup (20h),0
   v0d     db      186,5 dup (20h),'Hard disk controller:',18 dup (20h),0
   v0e     db      186,5 dup (20h),'Floppy disk controller:',16 dup (20h),0
   v0f     db     186,5 dup (20h),'Printer controller:',20 dup (20h),0
   v10     db      186,5 dup (20h),'Video driver:',26 dup (20h),0
   v11     db      186,5 dup (20h),'Equipment check:',23 dup (20h),0     
   v12     db      186,5 dup (20h),'Memory size check:',21 dup (20h),0
   v13     db      186,5 dup (20h),'Disk driver:',27 dup (20h),0
   v14     db      186,5 dup (20h),'Communication driver:',18 dup (20h),0
   v15     db      186,5 dup (20h),'Cassette driver:',23 dup (20h),0
   v16     db      186,5 dup (20h),'Keyboard driver:',23 dup (20h),0
   v17     db      186,5 dup (20h),'Printer driver:',24 dup (20h),0
   v18     db      186,5 dup (20h),'ROM BASIC:',29 dup (20h),0
   v19     db      186,5 dup (20h),'Bootstrap loader:',22 dup (20h),0
   v1a     db      186,5 dup (20h),'Real_time clock:',23 dup (20h),0
   v1b     db      186,5 dup (20h),'Ctrl_break handler:',20 dup (20h),0
   v1c     db      186,5 dup (20h),'Timer control:',25 dup (20h),0
   v1d     db      186,5 dup (20h),'Video parameter table:',17 dup (20h),0
   v1e     db      186,5 dup (20h),'Disk parameter:',24 dup (20h),0
   v1f     db     186,5 dup (20h),'Graphic character table:',15 dup (20h),0
   v20     db      186,5 dup (20h),'Programe terminate:',20 dup (20h),0
   v21     db      186,5 dup (20h),'DOS universal function:',16 dup (20h),0
   v22     db     186,5 dup (20h),'Terminate vector:',22 dup (20h),0
   v23     db      186,5 dup (20h),'Ctrl_C vector:',25 dup (20h),0
   v24     db      186,5 dup (20h),'Critical error vector:',17 dup (20h),0
   v25     db      186,5 dup (20h),'Absolute disk read:',20 dup (20h),0
   v26     db      186,5 dup (20h),'Absolute disk write:',19 dup (20h),0
   v27     db      186,5 dup (20h),'Terminate and stay resident:',11 dup (20h),0
   v2f     db      186,5 dup (20h),'Print spooler:',25 dup (20h),0
   cseg    ends
    end     start



好久没碰Dos,手都生了,赶紧回来练练.嘿嘿
2006-6-19 03:39
查看资料  发送邮件  发短消息 网志  OICQ (181315400)  编辑帖子  回复  引用回复
070
高级用户

苏醒的沉睡者


积分 659
发帖 217
注册 2003-2-15
来自 福建
状态 离线
『第 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",如果是的话就修改它.
可以借执行这个程序,来验证其是否正确.除此之外,也可以证明,在操作系统和应用程



好久没碰Dos,手都生了,赶紧回来练练.嘿嘿
2006-6-19 03:40
查看资料  发送邮件  发短消息 网志  OICQ (181315400)  编辑帖子  回复  引用回复
070
高级用户

苏醒的沉睡者


积分 659
发帖 217
注册 2003-2-15
来自 福建
状态 离线
『第 5 楼』:  

五 键盘输入扩充程序
有了前一节的基本驻留程序为基础,就可以建立起不同的应用程序.接下来,就写一个驻留程序,把用户敲入的字符,用一系列的字符来取代.这样可以减少用户的击键次数.
首先,先复习一下前一节的驻留程序的格式,如下所示:
     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
                 

原文就到这里结束,觉得好像未完成。虽然汇编比较繁琐,但编写驻留程序好像就汇编最好,高级语言顶多用C+汇编连接.

[ Last edited by 070 on 2006-6-19 at 03:49 ]



好久没碰Dos,手都生了,赶紧回来练练.嘿嘿
2006-6-19 03:40
查看资料  发送邮件  发短消息 网志  OICQ (181315400)  编辑帖子  回复  引用回复

请注意:您目前尚未注册或登录,请您注册登录以使用论坛的各项功能,例如发表和回复帖子等。


可打印版本 | 推荐给朋友 | 订阅主题 | 收藏主题



论坛跳转: