;======================================================================
; LEVEL 1.00 Copyright (c) 1992, Robert L. Hummel
; PC Magazine Assembly Language Lab Notes
;
; LEVEL is a TSR management tool that provides a reliable way to
; remove TSRs from memory.
;----------------------------------------------------------------------
CSEG		SEGMENT	PARA	PUBLIC	'CODE'
	ASSUME	CS:CSEG, DS:CSEG, ES:CSEG, SS:CSEG

		ORG	100H			;COM file format
ENTPT:		JMP	MAIN

;======================================================================
; Program data area.
;----------------------------------------------------------------------
CR		EQU	13		;ASCII carriage return
LF		EQU	10		;ASCII line feed
BLANK		EQU	32		;ASCII blank (space)
JMPFAR		EQU	0EAH		;Opcode for JMP FAR

;----------------------------------------------------------------------
; This copyright notice remains resident along with the data structure.
;----------------------------------------------------------------------
		DB	"LEVEL 1.00 ",254," Copyright (c) 1992,"
		DB	" Robert L. Hummel",26

;----------------------------------------------------------------------
; This data structure controls this layer:
;
; _NEXTLEVEL	SEG:OFF of next data area
; _PREVLEVEL	SEG:OFF of previous data area
; _NPSPS	(Word) Number of PSP entries in the _PSP_TABLE
; _PSP_TABLE	Array of up to MAX_PSPS words holding resident PSPs
; _INTVECTS	Array of DWORD vectors for interrupts 00h-7Fh
; _OVERFLOW	(Word) Non-zero if table has overflowed
;
; The "O_" entries specify the offset of the data elements to the
; beginning of the structure. These are later used to address elements
; when examining other structures.
;----------------------------------------------------------------------
DATA_STRUCTURE	LABEL	BYTE

O_NEXTLEVEL	EQU	$-DATA_STRUCTURE
_NEXTLEVEL	DD	-1

O_PREVLEVEL	EQU	$-DATA_STRUCTURE
_PREVLEVEL	DD	-1

O_NPSPS		EQU	$-DATA_STRUCTURE
_NPSPS		DW	0

MAX_PSPS	EQU	16
_PSP_TABLE	DW	MAX_PSPS DUP (0)

O_INTVECTS	EQU	$-DATA_STRUCTURE
_INTVECTS	DD	128 DUP (0)

_OVERFLOW	DW	0

;======================================================================
; INT_21 (ISR)
;
; When resident, this routine intercepts calls to Int 21h. If the INT
; request is for the Keep (Int 21h, AH=31h) function and if this is the
; top level, the PSP of the terminating process is recorded.
;----------------------------------------------------------------------
INT_21		PROC	FAR
	ASSUME	CS:CSEG, DS:NOTHING, ES:NOTHING, SS:NOTHING

		CMP	AH,31H			;Keep function?
		JNE	I21_EXIT

		CMP	WORD PTR CS:[_NEXTLEVEL],-1	;Our level?
		JNE	I21_EXIT
;----------------------------------------------------------------------
; Save the PSP of the terminating process in our table.
;----------------------------------------------------------------------
		STI				;Allow interrupts
		PUSH	AX			;Save used registers
		PUSH	BX

		MOV	AH,51H			;Get active PSP in BX
		INT	21H			; thru DOS

		MOV	AX,BX			;Save PSP
		MOV	BX,CS:[_NPSPS]		;Get index
		CMP	BX,MAX_PSPS		;Too many saved?
		JB	I21_1A
;----------------------------------------------------------------------
; The table is full, there are too many TSRs at this level. (This is
; very unlikely.) Place a non-zero value in the trouble flag so that
; we'll signal an error when removing. (We know BX is non-zero.)
;----------------------------------------------------------------------
		MOV	CS:[_OVERFLOW],BX	;Set flag non-zero
		JMP	SHORT I21_1B
;----------------------------------------------------------------------
; List is not full. Add this PSP.
;----------------------------------------------------------------------
I21_1A:
		ADD	BX,BX			;Index to words
		MOV	CS:[_PSP_TABLE][BX],AX	;Save PSP in table
		INC	CS:[_NPSPS]		;Advance count
I21_1B:
		POP	BX			;Restore used registers
		POP	AX
		CLI				;Disable interrupts
;----------------------------------------------------------------------
; Exit by jumping to the original Int 21h vector as stored in this
; level's _INTVECTS table.
;----------------------------------------------------------------------
I21_EXIT:
		JMP	DWORD PTR CS:[_INTVECTS][21H*4]	;Far jump

INT_21		ENDP

;======================================================================
; INT_27 (ISR)
;
; When resident, this routine intercepts calls to Int 27h (terminate
; and stay resident). If this is the top level, the PSP of the
; terminating process is recorded.
;----------------------------------------------------------------------
INT_27		PROC	FAR
	ASSUME	CS:CSEG, DS:NOTHING, ES:NOTHING, SS:NOTHING

		CMP	WORD PTR CS:[_NEXTLEVEL],-1	;Our level?
		JNE	I27_EXIT
;----------------------------------------------------------------------
; Save the PSP of the terminating process in our table.
;----------------------------------------------------------------------
		STI				;Allow interrupts
		PUSH	AX			;Save used registers
		PUSH	BX

		MOV	AH,51H			;Get active PSP in BX
		INT	21H			; thru DOS

		MOV	AX,BX			;Save PSP
		MOV	BX,CS:[_NPSPS]		;Get index
		CMP	BX,MAX_PSPS		;Too many saved?
		JB	I27_1A
;----------------------------------------------------------------------
; The table is full, there are too many TSRs at this level. (This is
; very unlikely.) Place a non-zero value in the trouble flag so that
; we'll signal an error when removing. (We know BX is non-zero.)
;----------------------------------------------------------------------
		MOV	CS:[_OVERFLOW],BX	;Set flag non-zero
		JMP	SHORT I27_1B
;----------------------------------------------------------------------
; Add this PSP to our list.
;----------------------------------------------------------------------
I27_1A:
		ADD	BX,BX			;Create word index
		MOV	CS:[_PSP_TABLE][BX],AX	;Save PSP in table
		INC	CS:[_NPSPS]		;Advance count
I27_1B:
		POP	BX			;Restore used registers
		POP	AX
		CLI				;Disable interrupts
;----------------------------------------------------------------------
; Exit by jumping to the original Int 21h vector as stored in this
; level's _INTVECTS table.
;----------------------------------------------------------------------
I27_EXIT:
		JMP	DWORD PTR CS:[_INTVECTS][27H*4]	;Far jump

INT_27		ENDP

;----------------------------------------------------------------------
; This is the cutoff point for all but the first installation.
;----------------------------------------------------------------------
CUTOFF		EQU	$

;======================================================================
; INT_66 (ISR)
;
; This routine chains into a "user-defined" interrupt to signal its
; presence to other copies. Only the lowest installed level hooks this
; interrupt.
;----------------------------------------------------------------------
;   If, on entry, the following conditions are met:
;	AX = "RH"
;	BX = 0
;
;   Then, on exit, the registers will be set as follows:
;	AX="HR"
;	ES:BX -> data structure of first copy (defined above)
;----------------------------------------------------------------------
INT_66		PROC	FAR
	ASSUME	CS:CSEG, DS:NOTHING, ES:NOTHING, SS:NOTHING

		CMP	AX,"RH"			;Our special ID?
		JE	I66_1
I66_EXIT:

I66_OPCODE	DB	JMPFAR			;FAR jump to
OLD66		DD	-1			; saved vector
;----------------------------------------------------------------------
; Code to check BX is here to allow more functions to be supported.
;----------------------------------------------------------------------
I66_1:
		OR	BX,BX			;Function 0 = get ptr
		JNZ	I66_EXIT

		MOV	BX,OFFSET DATA_STRUCTURE ;Return pointer

		PUSH	CS			;Point ES
		POP	ES			; to this segment
	ASSUME	ES:NOTHING

		XCHG	AH,AL			;Reverse signature
		IRET				;Return to caller

INT_66		ENDP

;----------------------------------------------------------------------
; This is the cutoff point for the first installation only.
;----------------------------------------------------------------------
FIRST_CUTOFF	EQU	$

;======================================================================
; MAIN procedure.
;----------------------------------------------------------------------
COPYRIGHT$	DB	LF,"LEVEL 1.00 ",254," Copyright (c) 1992,"
		DB	" Robert L. Hummel",CR,LF
		DB	"PC Magazine Assembly Language Lab Notes",CR,LF
		DB	"Usage: LEVEL [IN | OUT]",CR,LF,LF
		DB	"Current Status:",CR,LF,"$"

LEVEL$		DB	"-> LEVEL # "
LEVEL_NUM	DB	"00: "
NUM_TSRS	DB	"00 TSRs",CR,LF,"$"

NOLEVELS$	DB	"-> No LEVELs In Memory",CR,LF,"$"
IN_OKAY$	DB	LF,"New Level Successfully Installed",CR,LF,"$"
OUT_OKAY$	DB	LF,"Level Successfully Removed",CR,LF,"$"

OVERFLOW$	DB	"Too many TSRs were installed at this level",CR
		DB	LF,"Some memory will not be released",CR,LF,"$"

COPTION		DB	0			;Save cmd line option
UMB_LINK	DB	-1			;Holds UMB link status

;----------------------------------------------------------------------
MAIN		PROC	NEAR
	ASSUME	CS:CSEG,DS:CSEG,ES:CSEG,SS:CSEG

		CLD				;String moves forward
;----------------------------------------------------------------------
; Display the program title.
;----------------------------------------------------------------------
		MOV	AH,9			;Display string fn
		MOV	DX,OFFSET COPYRIGHT$	; located here
		INT	21H			; thru DOS
;----------------------------------------------------------------------
; Search the command tail for options.
;----------------------------------------------------------------------
		MOV	SI,81H			;Point to cmd tail
		SUB	CH,CH			;Set CX to
		MOV	CL,[SI-1]		; # chars in tail
		JCXZ	M_1D
M_1A:
		LODSB				;Get char
		CMP	AL,BLANK		;Skip blanks
		JNE	M_1B
		LOOP	M_1A
		JMP	SHORT M_1D
M_1B:
;----------------------------------------------------------------------
; We found a non-blank character in the command tail. If not IN or OUT,
; just ignore it and print the usage message.
;----------------------------------------------------------------------
		OR	AL,20H			;Make lower case
		CMP	AL,"i"			;Was it "IN"?
		JE	M_1C

		CMP	AL,"o"			;Was it "OUT"?
		JNE	M_1D
M_1C:
		MOV	[COPTION],AL		;Save the option
M_1D:
;----------------------------------------------------------------------
; Get the current vector for Int 66h in ES:BX.
;----------------------------------------------------------------------
		MOV	AX,3566H		;Get vector for Int 66h
		INT	21H			; thru DOS
	ASSUME	ES:NOTHING
;----------------------------------------------------------------------
; If ES=BX=0, there are no previous levels in memory.
;----------------------------------------------------------------------
		MOV	WORD PTR [OLD66][0],BX	;Save vector in case
		MOV	WORD PTR [OLD66][2],ES	; we need it later

		MOV	AX,ES			;Get the segment
		OR	AX,BX			; and the offset
		JNZ	M_3
;----------------------------------------------------------------------
; There's no previous int 66h handler to transfer control to, so just
; patch our Int 66 handler so that it does an IRET.
;----------------------------------------------------------------------
		MOV	BYTE PTR [I66_OPCODE],0CFH ;Opcode for IRET
;----------------------------------------------------------------------
; There are no levels. Display a message saying so.
;----------------------------------------------------------------------
	ASSUME	ES:NOTHING
M_2:
		MOV	AH,9			;Display string
		MOV	DX,OFFSET NOLEVELS$	;Say no levels
		INT	21H			; thru DOS
;----------------------------------------------------------------------
; Because there are no previous levels, the only permissible option is
; "IN". If "OUT" or nothing was specified, just terminate.
;----------------------------------------------------------------------
		CMP	[COPTION],"i"		;Was it "IN"?
		JE	M_5A
;----------------------------------------------------------------------
; Terminate this iteration of the program.
;----------------------------------------------------------------------
	ASSUME	ES:NOTHING
M_EXIT:
		MOV	AH,4CH			;Terminate program
		INT	21H			; thru DOS
;----------------------------------------------------------------------
; The 66h vector is non-zero, so we'll assume it's a valid vector.
; Request a pointer to the Level 1 data structure.
;----------------------------------------------------------------------
M_3:
		MOV	AX,"RH"			;Pass this code
		SUB	BX,BX			;0=return address
		INT	66H			; to previous levels
	ASSUME	ES:NOTHING

		CMP	AX,"HR"			;Should return this
		JNE	M_2
;----------------------------------------------------------------------
; Trace the structures and display the info.
;----------------------------------------------------------------------
		MOV	BP,"00"			;ASCII digit mask
		SUB	CX,CX			;CX=current level
M_4A:
		INC	CX			;Move to next level
		MOV	AX,CX			; and put in AX
		AAM				;Separate digits
		OR	AX,BP			;Make ASCII
		XCHG	AH,AL			;Put in string order
		MOV	WORD PTR [LEVEL_NUM],AX	;Save as level #

		MOV	AX,ES:[BX][O_NPSPS]	;Get # TSRs this level
		DEC	AX			;Remove ourselves
		AAM				;Separate digits
		OR	AX,BP			;Make ASCII
		XCHG	AH,AL			;Put in string order
		MOV	WORD PTR [NUM_TSRS],AX	;Save in message

		MOV	AH,9			;Display string
		MOV	DX,OFFSET LEVEL$	;Describe level
		INT	21H			; thru DOS

		CMP	WORD PTR ES:[BX][O_NEXTLEVEL],-1 ;Last one?
		JE	M_4B

		LES	BX,DWORD PTR ES:[BX][O_NEXTLEVEL] ;Get next
	ASSUME	ES:NOTHING
		JMP	M_4A
M_4B:
;----------------------------------------------------------------------
; ES:BX is left pointing to the last structure in memory. Save this
; backpointer in the current structure.
;----------------------------------------------------------------------
		MOV	WORD PTR [_PREVLEVEL][0],BX	;Offset and
		MOV	WORD PTR [_PREVLEVEL][2],ES	; segment
;----------------------------------------------------------------------
; See if this is an IN or OUT remove request.
;----------------------------------------------------------------------
M_4C:
		CMP	[COPTION],"o"		;Level OUT?
		JE	M_7A

		CMP	[COPTION],"i"		;Level IN?
		JNE	M_EXIT
;----------------------------------------------------------------------
; Insert a new level. If there were previous levels, change the last
; pointer to point to us. Otherwise, hook INT 66h.
;----------------------------------------------------------------------
	ASSUME	ES:NOTHING
M_5A:
		CMP	WORD PTR [LEVEL_NUM],"00"	;No previous?
		JE	M_5B

	MOV	WORD PTR ES:[BX][O_NEXTLEVEL][0],OFFSET DATA_STRUCTURE
	MOV	WORD PTR ES:[BX][O_NEXTLEVEL][2],CS
M_5B:
;----------------------------------------------------------------------
; Capture the interrupt vectors for INT 0 - INT 7F.
;----------------------------------------------------------------------
		SUB	SI,SI			;Point DS:SI to 0:0
		MOV	DS,SI
	ASSUME	DS:NOTHING


		MOV	DI,OFFSET _INTVECTS	;Point ES:DI to
		PUSH	CS			; vector save area
		POP	ES
	ASSUME	ES:CSEG

		MOV	CX,128*2		;Copy this many words
		REP	MOVSW

		PUSH	CS			;Restore segment
		POP	DS
	ASSUME	DS:CSEG
;----------------------------------------------------------------------
; Leave the INT_66 proc in memory only for the first install.
;----------------------------------------------------------------------
		MOV	CX,(OFFSET CUTOFF - CSEG + 15) / 16 ;Not 1st

		CMP	WORD PTR [LEVEL_NUM],"00"	;No previous?
		JNE	M_6

		MOV	CX,(OFFSET FIRST_CUTOFF - CSEG + 15) / 16
;----------------------------------------------------------------------
; Hook in our interrupt service routines.
;----------------------------------------------------------------------
		MOV	AX,2566H		;Set Int 66h vector
		MOV	DX,OFFSET INT_66	; to point here
		INT	21H			; thru DOS
M_6:
		MOV	AX,2521H		;Set Int 21h vector
		MOV	DX,OFFSET INT_21	; to point here
		INT	21H			; thru DOS

		MOV	AX,2527H		;Set Int 27h vector
		MOV	DX,OFFSET INT_27	; to point here
		INT	21H			; thru DOS
;----------------------------------------------------------------------
; Now terminate and stay resident.
;----------------------------------------------------------------------
		MOV	AX,DS:[2CH]		;Get environment seg
		MOV	ES,AX
	ASSUME	ES:NOTHING
		MOV	AH,49H			;Release seg in ES
		INT	21H			; thru DOS

		MOV	AH,9			;Display string
		MOV	DX,OFFSET IN_OKAY$	;Say it worked
		INT	21H			; thru DOS

		MOV	AH,31H			;End as TSR
		MOV	DX,CX			;Save DX paragraphs
		INT	21H			; thru DOS
;----------------------------------------------------------------------
; We come here to remove the top level. If, however, the PSP table
; overflowed, we can't free all the memory. Disable the TSRs by
; restoring the IVT and free as much memory as possible.
;----------------------------------------------------------------------
	ASSUME	DS:CSEG, ES:NOTHING
M_7A:
		CMP	ES:[_OVERFLOW],0	;Top layer overflow?
		JE	M_7B

		MOV	AH,9			;Display string
		MOV	DX,OFFSET OVERFLOW$	;Relate problem
		INT	21H			; thru DOS
;----------------------------------------------------------------------
; Check our LEVEL message to determine if the level we're removing is
; the only one in memory.
;----------------------------------------------------------------------
M_7B:
		CMP	WORD PTR [LEVEL_NUM],"10" ;Reversed 01
		JE	M_7C
;----------------------------------------------------------------------
; There is at least one level below the one we're removing. Point DS:SI
; to the next lower copy's data structure and erase the forward link.
;----------------------------------------------------------------------
		LDS	SI,DWORD PTR ES:[BX][O_PREVLEVEL] ;Get link
	ASSUME	DS:NOTHING

		MOV	AX,-1				    ;Negate
		MOV	WORD PTR DS:[SI][O_NEXTLEVEL][0],AX ; forward
		MOV	WORD PTR DS:[SI][O_NEXTLEVEL][2],AX ; link
M_7C:
;----------------------------------------------------------------------
; Restore the interrupt vector table. This will disable any of the TSRs
; we're about to remove.
;----------------------------------------------------------------------
		LEA	SI,[BX][O_INTVECTS]	;Point to vector table
		PUSH	ES			; in segment of
		POP	DS			; top level
	ASSUME	DS:NOTHING			;DS -> top level

		SUB	DI,DI			;Point ES:DI to 0:0
		MOV	ES,DI			; destination
	ASSUME	ES:NOTHING			;ES -> 0000

		MOV	CX,128*2		;# of words to move

		CLI				;No interrupts
		REP	MOVSW			;Restore vectors
		STI				;Interrupts on
;----------------------------------------------------------------------
; Get the DOS version. If ver 5 or later, save the current UMB state,
; then link them.
;----------------------------------------------------------------------
		MOV	AH,30H			;Get DOS version in AX
		INT	21H			; thru DOS

		CMP	AL,5			;DOS 5 or later
		JB	M_8

		MOV	AX,5802H		;Get current UMB link
		INT	21H			; thru DOS
		JC	M_8

		MOV	CS:[UMB_LINK],AL	;Save it

		MOV	AX,5803H		;Set UMBs to be
		MOV	BX,1			; linked in chain
		INT	21H			; thru DOS
M_8:
;----------------------------------------------------------------------
; Deallocate memory belonging to the PSPs recorded in the top level.
;----------------------------------------------------------------------
		MOV	AH,52H			;Get IVARS pointer
		INT	21H			; thru DOS
	ASSUME	ES:NOTHING
		MOV	ES,ES:[BX-2]		;Get first MCB
	ASSUME	ES:NOTHING
;----------------------------------------------------------------------
; Point DS:SI to the table of PSPs in the top level. Compare each MCB
; segment to the entries in the table. If any match, release them.
; (Humming `Born Free' as you do so is optional.)
;----------------------------------------------------------------------
M_9A:
		MOV	SI,OFFSET _PSP_TABLE	;DS:SI -> PSP table
		MOV	CX,DS:[_NPSPS]		;Number PSPS in table

		MOV	BX,ES:[1]		;Get owner of block
M_9B:
		LODSW				;Get first PSP
		CMP	AX,BX			;Does it match?
		JNE	M_9C
;----------------------------------------------------------------------
; The owner of this block is in our list. To release the memory, we
; have to point ES to the segment -- not to the MCB.
;----------------------------------------------------------------------
		PUSH	ES			;Save MCB segment

		MOV	BX,ES			;Change MCB seg
		INC	BX			; to block seg
		MOV	ES,BX			; and reload
	ASSUME	ES:NOTHING

		MOV	AH,49H			;Release seg in ES
		INT	21H			; thru DOS

		POP	ES			;Restore MCB
	ASSUME	ES:NOTHING
		JMP	SHORT M_9D
;----------------------------------------------------------------------
; Try the next entry in the PSP table.
;----------------------------------------------------------------------
M_9C:
		LOOP	M_9B
;----------------------------------------------------------------------
; We either found a match and removed it or went through the entire
; list without a match. Move to the next MCB.
;----------------------------------------------------------------------
M_9D:
		MOV	BX,ES			;MCB address
		INC	BX			;+1=segment address
		ADD	BX,ES:[3]		;+block length

		CMP	BYTE PTR ES:[0],"Z"	;This block the last?
		MOV	ES,BX			;(Load it meanwhile)
	ASSUME	ES:NOTHING
		JNE	M_9A
;----------------------------------------------------------------------
; Restore the UMB link to its previous state.
;----------------------------------------------------------------------
		MOV	BL,CS:[UMB_LINK]	;Original link state
		CMP	BL,-1			;Was it recorded?
		JE	M_10

		SUB	BH,BH			;Link in BX
		MOV	AX,5803H		;Set UBM link
		INT	21H			; thru DOS
M_10:
;----------------------------------------------------------------------
; All memory has been released. Exit the program.
;----------------------------------------------------------------------
		PUSH	CS			;Address data
		POP	DS			; in this segment
	ASSUME	DS:CSEG

		MOV	AH,9			;Display string
		MOV	DX,OFFSET OUT_OKAY$	;Say removal is done
		INT	21H			; thru DOS

		JMP	M_EXIT

MAIN		ENDP

CSEG		ENDS
		END	ENTPT
