;======================================================================
; 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
