【汇编】TSR内存驻留程序实现与删除及热键设置——以实现时钟为例

原创 2015年06月06日 13:01:53

【汇编】TSR内存驻留程序实现与删除及热键设置——以实现时钟为例

最近完成了一个关于时钟中断驻留的汇编大作业,对于TSR很是感兴趣,查阅了不少资料,发现现在很少相关的内容资料。尤其是TSR删除这一块几乎找不到在低的水平完全正确的做法,下面我对于TSR的删除我会介绍一种几乎小程序可以完成的方法。当然,标题中的TSR实现,热键设置我也会一一介绍。

一、TSR的介绍(对TSR的基本概念已了解的朋友可以直接跳过第一部分):

先介绍下TSR内存驻留程序(或称中断驻留程序)即Terminate and Stay Resident Program,,缩写为TSR。这种程序加载进内存,,执行完成后,,就驻留在内存里,,当满足条件时(例如热键启动等),调到前台来执行。

程序常驻内存的目的有两个:第一,,对操作系统或BIOS 进行某些修改或补充, 比如转入用户编写的必须恒起作用的中断处理程序;第二,,解决PC DOS 不能实现多任务处理的问题,,例如在运行其他程序的过程中,定时地在屏幕上闪现一些日历、时间和其他提示信息。

 

二、TSR的基本格式

下面我先给出TSR程序的一般设计格式:

codes segment
			assume	cs:codes,ds:codes
			org		100h
start:
			jmp		install	   		
			;;此处定义需要用到的数据
			;;例:	flag	dw		0

subprogram:	
			;;此处为要驻留入内存的程序(称为驻留部分)
			
install:
			;;此处为将上面驻留部分驻留入内存的代码(称为暂驻部分)

codes ends
    		end	start

(1)我们需要注意到TSR的代码中只有一个段(code段),而“数据段”被搬到  jmp    install  之后,这是为了减少程序驻留时所需的内存——当在同一个段时,我们至多需要占用64K的内存;而多了一个数据段,为了保证驻留部分全部被驻留进内存,我们可能需要占用128K的内存,这明显是对空间的浪费。

(2)org    100g  的作用是强制使得 start 的偏移为0100h,其实这个语句会对我们后面的暂留部分计算驻留区大小有帮助,具体作用我们留到下面解释。

 

三、暂驻部分程序的设计

由于暂驻部分较为简单,下面我们先来解决程序的暂驻部分。暂驻程序主要完成两种工作:(1)如何使DOS再次执行常驻内存的驻留部分,这个我们会通过修改相关中断向量来完成,如int 1ch,int 09h等;(2)驻留程序的驻留及暂驻程序的结束,这个我们可以通过int 21h的31h子功能或int 27h来实现(下面我们只介绍int 21h的31h子功能)。因此,暂驻部分可以抽象为以下过程:

(1)取中断向量

(2)保存旧的中断向量

(3)设置新的中断向量

(4)调用中断(int 21h的31h子功能)使驻留程序驻留并结束暂驻程序

我们下面以时钟程序(显示系统时间)为例,我们将会用到以下DOS中断:

INT AH 功能 调用参数 返回参数
21H 25H 设置中断向量 DS:DX=中断向量,AL=中断类型
21H 31H 结束并驻留 AL=返回码,DX=驻留区大小
21H 35H 取中断向量 AL=中断类型 ES:BX=中断向量

我们再介绍一下时钟程序常用的一个中断接口(我们将用来设置,调用我们驻留程序的“窗口”)int 1ch——在系统定时器(中断类型为08h)的中断处理程序中,有一条中断指令int 1ch,时钟中断每发生一次(约每秒18.2次)都要嵌套调用一次中断类型1ch的处理程序。简而言之,就是int 1ch会以每秒18.2次的频率被系统自动调用,利用这个机制,我们可以通过在每次调用int 1ch时,取出系统时间并显示来完成时钟设计。以下我先给出暂驻部分的代码:

codes segment
			assume	cs:codes,ds:codes
			org		100h
start:
			jmp		install
			OLD1C		DW		2 DUP(?)

newint1c:	
			iret																										;;此处为要驻留入内存的程序(称为驻留部分)												
install:
			push		cs
			pop		ds
			
			mov		ah,35h
			mov		al,1ch
			int		21h					;;取旧的int1c向量并保存
			mov		OLD1C,bx
			mov		bx,es
			mov		OLD1C+2,bx
			
			mov		dx,offset newint1c
			mov		ah,25h					;;设置新的int1c向量																	mov		al,1ch
			int		21h
			
			mov		dX,offset install
			mov		cl,4
			shr		dx,cl					;;计算驻留区大小
			add		dx,11h
			
			mov		ah,31h
			xor		al,al					;;驻留并结束			               														int		21h

codes ends
    		end	start

我们定义了OLD1C来存放旧的int 1ch中断向量,并通过取旧向量并保存,设置新向量,计算驻留区大小,驻留并结束在暂留部分完成对中断向量的更改和驻留部分的载入内存。前面我们org    100g  的作用是强制使得 start 的偏移为0100h,这使得我们只需取得 install 的偏移地址就能大致计算驻留区的大小。

下面我用DOSBox运行这个程序,我们可以通过 mem、mem/c 命令查看内存占用情况,如下图(后面我们要介绍如何删除TSR,故此处检查其成功载入内存很重要):

我们可以看到可用内存减少1Kb,这表明我们的程序载入内存成功,只是当前驻留部分只有一个 iret 命令,不会完成任何功能。

 

四、驻留部分的设计

这一部分的设计我直接给出时钟程序的代码,因为这并不是我们的重点。我们的重点是在TSR的驻留实现(暂留部分),删除及最后优化的热键设置的介绍。我们需要关注是光标的保存设置和恢复,其他部分代码就是取出显示系统时间。代码及运行结果如下:

codes segment
			assume	cs:codes,ds:codes
			org		100h
start:
			jmp		install
			OLDCUR		DW		?			;;用来保存光标位置
			OLD1C		DW		2 DUP(?)

newint1c:																												mov		ah,03h
			xor		bh,bh
			int		10h					;;保存当前光标位置
			mov		OLDCUR,dx
			mov		ah,02h
			mov		dx,0046h
			xor		bh,bh					;;设置显示时钟的位置
			int		10h
			mov		ah,2ch
			int		21h					;;取系统时间
			mov		bx,dx
			
			mov		dl,ch					;;显示系统时间
			call		prtd
			mov		ah,02h
			mov		dl,3ah
			int		21h
			mov		dl,cl
			call		prtd
			mov		ah,02h
			mov		dl,3ah
			int		21h
			mov		dl,dh
			call		prtd
			
			mov		ah,02h					;;将光标返回原处
			mov		dx,OLDCUR
			xor		bh,bh
			int		10h
			iret
			
prtd		proc	near							;;十进制输出
			push		ds
			push		ax
			push		bx
			push		cx
			push		dx			
			mov		al,dl
			cbw
			mov		dh,10
			div		dh
			mov		bx,ax
			mov		ah,02h
			mov		dl,bl
			add		dl,30h
			int		21h
			mov		dl,bh
			add		dl,30h
			int		21h
			
			pop		dx
			pop		cx
			pop		bx
			pop		ax
			pop		ds
			ret			
prtd		endp
															
install:
			push		cs
			pop		ds
			
			mov		ah,35h
			mov		al,1ch
			int		21h					;;取旧的int1c向量并保存
			mov		OLD1C,bx
			mov		bx,es
			mov		OLD1C+2,bx
			
			mov		dx,offset newint1c
			mov		ah,25h					;;设置新的int1c向量			                               					 						mov		al,1ch
			int		21h
			
			mov		dX,offset install
			mov		cl,4
			shr		dx,cl					;;计算驻留区大小
			add		dx,11h
			
			mov		ah,31h
			xor		al,al					;;驻留并结束
			int		21h

codes ends
    		end	start



 五、TSR的热键设置

我们已经成功将时钟程序载入内存,然而我们并没有方式对时钟进行控制(修改,退出等),这显然是不够好的,因为我们没有方法让时钟按我们的想法进行更改。下面我们介绍TSR的热键设置,热键就是用户定义的一系列特殊键位(一般为组合键),来达到对驻留程序控制的目的。热键设置是基于中断int 09h,键盘每次被敲下时,键盘输入会达到60h端口,并调用int 09h(此时从60h端口取出的为键盘输入的扫描码)。因此,我们可以修改int 09h的向量,使得按下某些键位时可以执行某些操作对时钟进行控制。改修int 09h的方法与暂留程序相似。下面我们设置按下ESC使时钟停止——我们只需恢复int 1ch的向量地址便能使时钟停止,当然我也因同时恢复int 09h的向量地址(停止时钟后,我们可以通过再次运行程序来启动时钟)。代码如下:

codes segment
			assume	cs:codes,ds:codes
			org		100h
start:
			jmp		install
			OLDCUR		DW		?			;;用来保存光标位置
			OLD1C		DW		2 DUP(?)
			OLD09		DW		2 DUP(?)

newint1c:	
			mov		ah,03h
			xor		bh,bh
			int		10h					;;保存当前光标位置
			mov		cs:OLDCUR,dx
			mov		ah,02h
			mov		dx,0046h
			xor		bh,bh					;;设置显示时钟的位置
			int		10h
			mov		ah,2ch
			int		21h					;;取系统时间
			mov		bx,dx
			
			mov		dl,ch					;;显示系统时间
			call	prtd
			mov		ah,02h
			mov		dl,3ah
			int		21h
			mov		dl,cl
			call	prtd
			mov		ah,02h
			mov		dl,3ah
			int		21h
			mov		dl,dh
			call	prtd
			
			mov		ah,02h					;;将光标返回原处
			mov		dx,cs:OLDCUR
			xor		bh,bh
			int		10h
			iret
			
newint09:
			push	ax
			push	bx
			push	cx
			push	dx
			in		al,60h					;;从60h处取得扫描码
			pushf
			call	dword ptr cs:OLD09				;;模拟int调用处理其他键盘输入
			mov		ah,03h
			xor		bh,bh
			int		10h					;;保存当前光标位置
			mov		cs:OLDCUR,dx
			cmp		al,01h
			jz		esca					;;输入若为ESC,转入处理																	jmp		exit2		
exit1:		
			mov		ah,0ch					;;清除缓冲区
			mov		al,06h
			mov		dl,0ffh
			int		21h			
exit2:		
			pop		dx
			pop		cx
			pop		bx
			pop		ax
			iret
esca:
			mov		dx,cs:OLD09+2				;;将中断向量恢复
			mov		ds,dx
			mov		dx,cs:OLD09
			mov		ah,25h
			mov		al,09h
			int		21h
			mov		dx,cs:OLD1C+2
			mov		ds,dx
			mov		dx,cs:OLD1C
			mov		ah,25h
			mov		al,1ch
			int		21h
			mov		ah,2
			xor		bh,bh
			mov		dx,0046h
			int		10h
			mov		ah,0ah
			xor		bh,bh
			mov		al,20h
			mov		cx,8
			int		10h
			mov		ah,02h					;;将光标返回原处
			mov		dx,cs:OLDCUR
			xor		bh,bh
			int		10h
			jmp		exit1
			
prtd		proc	near							;;十进制输出
			push		ds
			push		ax
			push		bx
			push		cx
			push		dx			
			mov		al,dl
			cbw
			mov		dh,10
			div		dh
			mov		bx,ax
			mov		ah,02h
			mov		dl,bl
			add		dl,30h
			int		21h
			mov		dl,bh
			add		dl,30h
			int		21h
			
			pop		dx
			pop		cx
			pop		bx
			pop		ax
			pop		ds
			ret			
prtd		endp
															
install:
			push	cs
			pop		ds
			
			mov		ah,35h
			mov		al,1ch
			int		21h					;;取旧的int1c向量并保存
			mov		OLD1C,bx
			mov		bx,es
			mov		OLD1C+2,bx			
			mov		dx,offset newint1c
			mov		ah,25h					;;设置新的int1c向量			                                										mov		al,1ch
			int		21h
			
			mov		ah,35h
			mov		al,09h
			int		21h					;;取旧的int09向量并保存
			mov		OLD09,bx
			mov		bx,es
			mov		OLD09+2,bx
			mov		dx,offset newint09
			mov		ah,25h					;;设置新的int09向量
			int		21h
						
			mov		dX,offset install
			mov		cl,4
			shr		dx,cl					;;计算驻留区大小
			add		dx,11h
			
			mov		ah,31h
			xor		al,al					;;驻留并结束
			int		21h

codes ends
    		end	start

这里我们加入的 newint09 相比于 newint1c,多了pushf  ,call dword ptr cs:old09 这两个命令,这两个命令模拟了原来 int 09h 的调用(关于 int 09h 的机制,感兴趣的朋友可以看看王爽的汇编语言第15章)。由于 int 1ch 只有一个 iret 命令故不需要调用,但 int 09h 的要对键盘输入进行处理,故我们在更改了 int 09h 后在新的程序中应该重新模拟调用原来的 int 09h实际上大部分中断的更改都需要模拟调用,只是 int 1ch 比较特殊可以不需要
以上热键的设置只是一个小小的例子,大家可以根据需要设置自己需要的组合热键(一般为组合键,避免和常用键重叠)来完成自己的目的。

 

六、TSR的删除

上一部分中,我们设置了ESC作为停止,按下后就恢复 int 1ch 和 int 09h 的向量地址,使时钟停止。然而,时钟只是表面上退出,加入内存中的程序并没有被完全释放。那应该如何实现TSR的删除退出呢?TSR的删除应按以下两个步骤:

(1)恢复所有更改的中断向量地址

(2)释放驻留的程序占用的内存

 第一步的方法我们在第五部分已经有介绍,只需将向量地址更改回来即可;第二步,这里提供一种方法:int 21h的49h子功能可以对内存块进行释放,但要以"ES=内存起始段地址"为调用参数,因此,我们可以在暂驻部分利用int 21h的62h子功能将PSP段地址取出,并保存在某个文件(myint1c.txt)中。然后,利用另外一个小程序(clexit.exe),读取这个地址对内存块进行释放。代码如下(相关文件读写的代码请大家自己查阅):

;;=============================================================================================
;;				先是clock.exe的代码
;;=============================================================================================

codes segment
			assume	cs:codes,ds:codes
			org		100h
start:
			jmp		install
			OLDCUR		DW		?			;;用来保存光标位置
			OLD1C		DW		2 DUP(?)
			OLD68		DW		2 DUP(?)
			FNAME		DB		'C:\MYINT1C.TXT',0	;储存PSP地址的文件名
			HANDLE		DW		?
			PSP			DW		?

newint1c:	
			mov		ah,03h
			xor		bh,bh
			int		10h					;;保存当前光标位置
			mov		cs:OLDCUR,dx
			mov		ah,02h
			mov		dx,0046h
			xor		bh,bh					;;设置显示时钟的位置
			int		10h
			mov		ah,2ch
			int		21h					;;取系统时间
			mov		bx,dx
			
			mov		dl,ch					;;显示系统时间
			call	prtd
			mov		ah,02h
			mov		dl,3ah
			int		21h
			mov		dl,cl
			call	prtd
			mov		ah,02h
			mov		dl,3ah
			int		21h
			mov		dl,dh
			call	prtd
			
			mov		ah,02h					;;将光标返回原处
			mov		dx,cs:OLDCUR
			xor		bh,bh
			int		10h
			iret
			
newint68:									;;将恢复向量地址的程序载入68h中																push	ax
			push	bx
			push	cx
			push	dx
			
			mov		ah,03h
			xor		bh,bh
			int		10h					;;保存当前光标位置
			mov		cs:OLDCUR,dx
			
			mov		dx,cs:OLD68+2				;;将中断向量恢复
			mov		ds,dx
			mov		dx,cs:OLD68
			mov		ah,25h
			mov		al,68h
			int		21h
			mov		dx,cs:OLD1C+2
			mov		ds,dx
			mov		dx,cs:OLD1C
			mov		ah,25h
			mov		al,1ch
			int		21h
			mov		ah,2
			xor		bh,bh
			mov		dx,0046h
			int		10h
			mov		ah,0ah
			xor		bh,bh
			mov		al,20h
			mov		cx,8
			int		10h
			mov		ah,02h					;;将光标返回原处
			mov		dx,cs:OLDCUR
			xor		bh,bh
			int		10h
			
			pop		dx
			pop		cx
			pop		bx
			pop		ax
			iret
			
prtd		proc	near							;;十进制输出
			push	ds
			push	ax
			push	bx
			push	cx
			push	dx			
			mov		al,dl
			cbw
			mov		dh,10
			div		dh
			mov		bx,ax
			mov		ah,02h
			mov		dl,bl
			add		dl,30h
			int		21h
			mov		dl,bh
			add		dl,30h
			int		21h
			
			pop		dx
			pop		cx
			pop		bx
			pop		ax
			pop		ds
			ret			
prtd		endp
															
install:
			push	cs
			pop		ds
			
			mov		ah,35h
			mov		al,1ch
			int		21h					;;取旧的int1c向量并保存
			mov		OLD1C,bx
			mov		bx,es
			mov		OLD1C+2,bx			
			mov		dx,offset newint1c
			mov		ah,25h					;;设置新的int1c向量			                               				 							mov		al,1ch
			int		21h
			
			mov		ah,35h
			mov		al,68h
			int		21h					;;取旧的int68向量并保存
			mov		OLD68,bx
			mov		bx,es
			mov		OLD68+2,bx
			mov		dx,offset newint68
			mov		ah,25h					;;设置新的int68向量
			int		21h
			
			mov		ah,3dh
			mov		dx,offset FNAME
			mov		al,02h					;;打开文件																 		int		21h
			mov		HANDLE,ax
			mov		ah,51h
			int		21h
			mov		PSP,bx					;;取程序PSP地址并写入文件中																	mov		dx,offset PSP
			mov		ah,40h
			mov		cx,2
			mov		bx,HANDLE
			int		21h
			mov		ah,3eh					;;关闭文件																		mov		bx,HANDLE
			int		21h
						
			mov		dX,offset install
			mov		cl,4
			shr		dx,cl					;;计算驻留区大小
			add		dx,11h
			
			mov		ah,31h
			xor		al,al					;;驻留并结束
			int		21h

codes ends
    		end	start
;;============================================================================================
;;					下面是clexit.exe的代码
;;============================================================================================
codes	segment
    			assume	cs:codes,ds:codes
    	
start:
			jmp		init
			FNAME	DB		'C:\MYINT1C.TXT',0
			HANDLE	DW		?
			PSP	DW		?
			
init:
			int		68h					;;调用clock加入的68h中断,恢复向量地址															push		cs
			pop		ds
			
			mov		ah,3dh
			mov		al,02h					;;打开文件																		mov		dx,offset FNAME
			int		21h
			mov		HANDLE,ax
			mov		bx,HANDLE
			mov		cx,2
			mov		dx,offset PSP				;;读出文件中clock的PSP地址																	mov		ah,3fh
			int		21h
			mov		ah,3eh					;;关闭文件																		mov		bx,HANDLE
			int		21h
			
			mov		bx,PSP
			mov		es,bx					;;调用int21h的49h释放内存																	mov		ah,49h
			int		21h
						
			mov		ax,4c00h
			int		21h			

codes	ends
    		end		start

我们注意到 clock.exe 的代码中原本修改 int 09h 中断的代码变成了 int 68h。int 68h是一个用户保留中断,即它不完成任何功能,但用户可以根据自己的需要,对其进行设置。因此,我们把 int 68h 作为恢复向量地址的代码驻留后的接口,使得我们在clexit.exe中可以通过调用 int 68h 使得向量地址恢复。当然,由于原本的 int 68h不完成任何功能,我们无需在程序中模拟调用原本的 int 68h。

或许,有朋友会问,为什么不用原来的 int 09h 作为接口?对于这个问题我们来分析下,如果以 int 09h 作为接口,那么我们似乎有以下两种方式完成对TSR的释放:

(1)我们可以把删除TSR的代码插入在原来的 newint09 程序中。这样似乎不用再写一个子程序,只需按下ESC便能完成TSR的删除。但当你运行程序并按下ESC时,你会发现只有向量地址被恢复,内存并没有被释放。这是为什么呢?我们可以在调用 int 21h 的49h子功能后检查CF和AX的内容,发现CF=1、AX=0009,这说明释放内存出错,错误代码为0009,。错误代码0009表明ES中的内存卡起始地址是非法的,正常来说这PSP地址是没问题,但对于一个驻留程序来说,试图释放它所在的内存块是DOS所不允许的,故第一种方法受限于DOS的机制是失败的。

(2)我们可以保留 newint09, 通过按下ESC恢复向量地址,再在另一个小程序中只调用 int 21h 的49h子功能释放内存。有的朋友或许会发现一个问题,这样做是不是太“麻烦”了?首先,我们每次要删除TSR都必须先按下设定的热键,再运行退出程序(如clexit.exe)释放内存,这样两个步骤远不如我们直接运行退出程序(clexit.exe)就能完成两步操作来的简单。再者,假如某一次退出时,用户忘了按下热键恢复向量就释放掉内存。这是多么糟糕!你会发现,以之前含有 newint09 的程序为例,错误退出后我们的键盘输入全部无效了!原因是作为键盘输入处理中断的 int 09h 向量地址被更改了,而且我们把存放原来地址的内存释放了。好吧,后果我想不用再多说了。

下面是程序的运行结果(我们会检测内存情况来证明我们的程序是对的):


从运行结果可以看到,我们的程序对于将TSR从内存中删除时成功的,所以我们艰难的任务就这么成功了!

 

如果你能看到这里,我在这里是谢谢你,这是我的第一篇博客文,是我对最近汇编课程一个时钟中断大作业的探讨。对于汇编,我也还是一个小白,如果你对于这篇文章有什么看法,请留言,谢谢!

相关文章推荐

第八篇 TSR程序设计初探

这篇博文介绍一个简单的TSR程序设计的例子。   一、TSR是什么 TSR是内存驻留程序(Terminate and Stay Resident Program)的简称。内存驻留程序是指这样 ...

用汇编编写DOS下的内存驻留程序

绪言 0.1 内存驻留与中断 内存驻留程序英文叫Terminate and S 绪言 0.1 内存驻留与中断 内存驻留程序英文叫Terminate and Stay Resident...
  • pofante
  • pofante
  • 2011年08月01日 15:33
  • 3095

Java设置全局热键钩子——第三方包jintellitype实现

Java原生API并不支持为应用程序设置全局热键。要实现全局热键,需要用JNI方式实现,这就涉及到编写C/C++代码,这对于大多数不熟悉C/C++的javaer来说,有点困难。不过幸好,国外有人已经实...

一个关于内存驻留的汇编源代码

  • 2016年11月09日 18:27
  • 35KB
  • 下载

一个关于内存驻留的汇编源代码

  • 2002年08月14日 00:00
  • 35KB
  • 下载

Android开发——手机拨号程序实现

在上一篇文章中,我们实现了第一个程序:helloWorld,并成功测试完成。还给大家介绍了Android项目结构和说明。现在写一个手机拨号程序:          首先,我们新建一个项目:phone...

显示内存驻留程序的信息

  • 2007年06月03日 01:10
  • 17KB
  • 下载

TC 开发内存驻留程序

  • 2007年06月03日 01:09
  • 18KB
  • 下载
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:【汇编】TSR内存驻留程序实现与删除及热键设置——以实现时钟为例
举报原因:
原因补充:

(最多只允许输入30个字)