name spyprime

rt		equ	0dh	; return
lf		equ	0ah	; linefeed
eof		equ	1ah	; end of file
ok_seg		equ	1	; flag for segment decode check
ok_off		equ	2	; flag for offset decode check
max_size	equ	4095	; maximum size for SPY' search region
beep_delay	equ	4	; number of clock ticks between beeps
timer_count	equ	597	; frequency of beep = 1,193,180 / timer_count

; These constants are used by the newint16h routine in this, the SPY' program.
; They are not needed for the SPY program.
ckey1		equ	20cdh	; value of first key (a "backwords" int 20)
ckey2		equ	1234h	; value of second key
ckey3		equ	5678h	; value of third key

code		segment
		assume cs:code, ds:code

		org	0
key1		dw	?	; offset of int 20h instruction
				;   used to verify a PSP location

		org	100h

begin:		jmp	start	; jump to program start location

;-------------------------------------------------------------------------------
; This copyright message appears when SPY'.COM is "typed" to the screen.
;-------------------------------------------------------------------------------

		db	8,8,8,'   '
copyright	db	rt,lf
		db	"SPY' Copyright (C) 1987 by Charles Lazo III, v1.0"
		db	eof,8,' ',rt,lf,'$'

key2		dw	ckey2	; keys used to determine prior load of SPY'
key3		dw	ckey3

oldint8	     	dd	?	; double word storage for old int 8 vector
oldint16h	dd	?	; double word storage for old int 16 vector
beeps		dw	0	; number of beeps (notices) to send to user
delay		dw	1	; number of clock ticks 'til next beep
beep_on		db	0	; beep on/off status
spy_size	dw	?	; number of bytes to spy on
resident	db	0	; indicates prior load of SPY' (0 = none)

spy_location	label	dword
spy_off		dw	?	; storage for value of offset for SPYing
spy_seg		dw	?	; storage for value of segment for SPYing

;-------------------------------------------------------------------------------
; Comparisons of the spied region and the spy buffer (set up initially as a copy
; of the spied region) are made in this addition to the clock interrupt.
; Changes to the spied region are counted and the spy buffer is updated.  The
; speaker is turned on and off here, but frequency is set during program
; initialization.
;-------------------------------------------------------------------------------

newint8		proc	near
		push	ax		; save interrupted program's registers
		push	bx
		push	cx
		push	si
		push	di
		push	ds
		push	es

		mov	ax,cs		; our data lies here
		mov	ds,ax

		lea	si,spy_buffer	; point to our buffer
		les	di,spy_location	; point to spied region
		mov	cx,spy_size	; compare whole region
		cld			; forward!
cmp_more:	repz	cmpsb		; compare until no match or cx = 0
		jz	cmp_done	; if zero, then cx = 0 and we're done
		inc	beeps		; account for a change
		mov	al,es:[di-1]	; change accounted; update spy_buffer
		mov	[si-1],al
		or	cx,cx		; set zero flag by cx (avoid inf loop)
		jmp	short cmp_more	; continue 'til done

cmp_done:	cmp	beep_on,0	; is the beep on?
		jz	do_beep?	; no, shall we do a beep?
		dec	beep_on		; yes, turn it off
		in	al,97		; get speaker control bits
		and	al,0fch		; set them off
		out	97,al		; turn off speaker
		jmp	short exit	; job done; get out
do_beep?:	cmp	beeps,0		; are there beeps to be done?
		jz	delay1		; no, get out
		dec	delay		; reduce delay 'til next beep
		jnz	exit		; not zero, then exit
		in	al,97		; get speaker control bits
		or	al,3		; set them on
		out	97,al		; turn on speaker
		inc	beep_on		; signal beep is on
		mov	delay,beep_delay; reinitialize delay counter
		dec	beeps		; one less beep to do
		jmp	short exit	; leave now
delay1:		mov	delay,1		; don't wait for first beep of a series
exit:		pop	es		; restore registers
		pop	ds
		pop	di
		pop	si
		pop	cx
		pop	bx
		pop	ax
		jmp	dword ptr cs:oldint8	; continue with interrupt
newint8		endp

;-------------------------------------------------------------------------------
; Keyboard interrupt, int 16h, is issued with function number ah = 77h in the
; set_es routine.  If there has been no prior load of SPY', then the interrupt
; returns without changing anything.  However, if SPY' has been run previously,
; then this routine has been chained into the interrupt 16h handler and the
; issue of "keyboard" function number 77h causes the returned value of es to be
; what cs was for the original load of SPY'.
;-------------------------------------------------------------------------------

newint16h	proc	near
		cmp	ah,77h		; our cue?
		jne	not_us		; nah, not us
		push	ax		; save this
		mov	ax,ckey1	; have we a match for the first key?
		cmp	ax,cs:key1
		je	test_2nd	; yes, test the 2nd key
		jmp	short onward	; no previous load found
test_2nd:	mov	ax,ckey2	; have we a match for the second key?
		cmp	ax,cs:key2
		je	test_3rd	; yes, test the 3rd key
		jmp	short onward	; no previous load found
test_3rd:	mov	ax,ckey3	; have we a match for the third key?
		cmp	ax,cs:key3
		jne	onward		; no match => no previous load
		pop	ax		; unstack saved value
		mov	ax,cs		; pass this cs in es
		mov	es,ax
		mov	ax,ckey1	; pass these to inform set_es
		mov	bx,ckey2
		iret
onward:		pop	ax		; recover this
not_us:		jmp	dword ptr cs:oldint16h	; continue with interrupt
newint16h	endp

;-------------------------------------------------------------------------------
; Here the region spied upon is copied to an area of the TSR that remains in
; memory after termination, the spy_buffer.  The es register is equal either to
; cs if SPY' has not yet been loaded or to the value of cs when SPY' was loaded
; previously (accomplished by a call to the set_es routine).
;-------------------------------------------------------------------------------

copy_spied:	lea	di,spy_buffer	; destination of copy
		mov	si,spy_off	; the offset is source of copy
		mov	cx,spy_size	; number of bytes to copy
		mov	ax,spy_seg	; load segment of SPY' region to ds
		push	ds		; SOD (save our data)
		mov	ds,ax
		cld			; forward copy
		rep	movsb		; copy the SPY' region to spy_buffer
		pop	ds		; RestoreOD

		cmp	resident,0	; is SPY' currently resident in memory?
		je	tsr		; no, make it so
		mov	ax,4c00h	; yes, end with error code 0
		int	21h

;-------------------------------------------------------------------------------
; SPY' has not yet been loaded into memory, so we do it now.  First the 8253-5
; chip that generates the frequency of the beeps is initialized with the value
; given by the constant timer_count.  Then the new interrupt 8 routine is
; spliced in and finally the constant max_size is used to reserve enough memory
; for the largest permissible spy buffer.
;-------------------------------------------------------------------------------

tsr:		mov	delay,beep_delay; initialize delay counter

		; set 8253-5 programmable timer to chosen frequency
		mov	al,182		; byte to initialize 8253 timer
		out	67,al		; tell timer next two bytes are count
		mov	ax,timer_count	; get timer count
		out	66,al		; output low byte
		mov	al,ah
		out	66,al		; output high byte

		mov	ax,3508h	; get the interrupt 8 (clock) vector
		int	21h		;   with DOS function 35h call

; Retain offset and segment of interrupt 8 vector:

		mov	word ptr cs:oldint8,bx
		mov	word ptr cs:oldint8+2,es

		lea	dx,newint8	; place offset in dx
		mov	ax,2508h	; set its pointer into interrupt table
		int	21h		;   with DOS function 25h call

		mov	ax,3516h	; get the int 16h (keyboard) vector
		int	21h		;   with DOS function 35h call

; Retain offset and segment of interrupt 16h vector:

		mov	word ptr cs:oldint16h,bx
		mov	word ptr cs:oldint16h+2,es

		lea	dx,newint16h	; place offset in dx
		mov	ax,2516h	; set its pointer into interrupt table
		int	21h		;   with DOS function 25h call

		lea	dx,spy_buffer	; where SPY' buffer begins
		mov	cx,max_size	; add the maximum size of the buffer:
		add	dx,cx
		mov	cl,4		; compute number of paragraphs to save:
		shr	dx,cl		;   bytes to paragraphs in dx
		inc	dx		;   insure sufficient size for buffer
		mov	ax,3100h	; terminate but stay resident code = 0
		int	21h

;-------------------------------------------------------------------------------
; The following is initialization code and data that is overwritten by the data
; copied to the spy buffer when this buffer is initialized with a copy of the
; spied region.
;-------------------------------------------------------------------------------

spy_buffer:	; this is where SPY' will store a copy of the SPY' region
		;   to later check for any changes to the region

syntax		db	rt,lf,"SPY' syntax:  SPY' xxxx:yyyy L zzz",rt,lf,rt,lf
		db	'Where xxxx, and yyyy are hexadecimal numbers up to '
		db	'four digits in length and',rt,lf
		db	"zzz is a one to three digit hexadecimal number.  SPY' "
		db	'will monitor the segment-',rt,lf
		db	'offset region xxxx:yyyy for a length of zzz bytes and '
		db	'report to the user with a',rt,lf
		db	'number of beeps equal to the number of bytes changed '
		db	'in that region if and when',rt,lf
		db	'any are changed.',rt,lf,'$'

progress	db	0	; flags for progress of command line conversion

;-------------------------------------------------------------------------------
; This routine will convert an ASCII hex digit in al (with either upper or lower
; case alpha hex) into a 1-of-16 binary value in al.
;-------------------------------------------------------------------------------

make_binary	proc	near		; hex ASCII digit in al to binary in al
		cmp	al,'0'		; less than "0", then not hex digit
		jb	not_digit
		cmp	al,'f'		; greater than "f", then not hex digit
		ja	not_digit
		cmp	al,'9'		; test for numeric digit
		ja	not_numeric	; not a numeric digit
		sub	al,'0'		; convert to binary
		ret
not_numeric:	cmp	al,'F'		; over "F"?
		ja	low_case	; yes, test for lower case
		cmp	al,'A'		; less than "A"?
		jb	not_digit	; yes, not hex digit
		sub	al,37h		; convert to binary
		ret
low_case:	cmp	al,'a'		; less than "a"?
		jb	not_digit	; yes, not hex digit
		sub	al,57h		; convert to binary
		ret
not_digit:	stc			; set carry flag if not hex digit
		ret
make_binary	endp

;-------------------------------------------------------------------------------
; This routine is called to allow the command line parse routines to ignore any
; space and tab characters on the command line that do not lie in hex numbers.
; If the return character at the end of the command line is encountered, then
; the carry flag is set; otherwise it is reset.
;-------------------------------------------------------------------------------

skip		proc	near		; skip over spaces and tabs; signal
					;   a RETURN with the carry flag
more_skip:	cmp	al,' '		; is it a space?
		je	another		; if so, get another
		cmp	al,9		; is it a tab?
		je	another		; if so, get another
		cmp	al,rt		; is it a RETURN?
		je	a_rt		; yes, set carry and return
		clc			; not one of these three so return
		ret			;   with carry clear
another:	lodsb			; get another character
		jmp	short more_skip	; and try again
a_rt:		stc			; return with carry set
		ret
skip		endp

;-------------------------------------------------------------------------------
; Here we parse the command line, which by the conditions of proper syntax has
; the three hexadecimal values for segment, offset and byte size for the region
; which is to be spied.  The routine between  cnv_more:  and  cnv_error:  is run
; once each to decode segment, offset and size which are stored respectively in
; the variables spy_seg, spy_off, and spy_size unless a syntax error is found.
; The variable progress is used to keep track of which of these three numbers
; has just been decoded.  A semicolon can be used to preface comments at the end
; of the command line.
;-------------------------------------------------------------------------------

convert		proc	near		; convert hex ASCII to binary in ax
		mov	cx,16		; for hex to binary conversion
cnv_more:	xor	bx,bx		; accumulate result here
		xor	ah,ah		; need this zero too
		dec	si		; move back to last character
cycle:		lodsb			; get possible ASCII hex digit
		call	make_binary	; if ASCII hex, then make binary in al
		jc	test_seg	; if carry set, then char not hex digit
		xchg	ax,bx		; accumulation in ax, last digit in bx
		mul	cx		; bump to next power
		or	dx,dx		; test for overflow
		jnz	cnv_error	; result can't be larger than fff0h
		add	ax,bx		; add in most recent digit
		xchg	ax,bx		; accumulation in bx, ah = 0
		jmp	short cycle	; continue conversion
cnv_error:	pop	ax		; remove return location (of call)
		jmp	stax		; display syntax and end
test_seg:	test	progress,ok_seg	; have we decoded the segment value?
		jnz	test_off	; yes, test offset
		call	skip		; skip over spaces and tabs
		jc	cnv_error	; carry flag set if RETURN was found
		cmp	al,':'		; syntax says must have colon here
		jne	cnv_error
		lodsb			; get another
		call	skip		; skip over spaces and tabs
		jc	cnv_error	; carry flag set if RETURN was found
		mov	spy_seg,bx	; store value of segment for SPYing
		or	progress,ok_seg	; segment value is now decoded
		jmp	short cnv_more	; continue to convert offset and size
test_off:	test	progress,ok_off	; have we decoded the offset value?
		jnz	test_size	; yes, test size
		call	skip		; skip over spaces and tabs
		jc	cnv_error	; carry flag set if RETURN was found
		and	al,0dfh		; convert "L" or "l" to "L"
		cmp	al,'L'		; syntax says must have "l" or "L" here
		jne	cnv_error
		lodsb			; get another
		call	skip		; skip over spaces and tabs
		jc	cnv_error	; carry flag set if return was found
		mov	spy_off,bx	; store value of offset for SPYing
		or	progress,ok_off	; offset value is now decoded
		jmp	short cnv_more	; continue to convert size
test_size:	cmp	bx,max_size	; size must not be over maximum
		ja	cnv_error
		call	skip		; skip over spaces and tabs
        jnc comnt     ; shall allow comments at cmd line end
cnv_finished:	mov	spy_size,bx	; store away size
		ret
comnt:    cmp al,';'      ; test for comment symbol
		jne	cnv_error	; not comment symbol, then error
		jmp	short cnv_finished	; all done
convert		endp

;-------------------------------------------------------------------------------
; This is the routine that contains the code that this program was designed to
; demonstrate.  It serves to detect if SPY' has been loaded previously as a TSR.
; If it has, then the code segment at the time of the previous load will be put
; into es; if not, then es will return with the current value of cs.  The method
; used attempts to find the set of key values key1, key2, and key3 in the memory
; above the current PSP.  key1 is word cd20h which is the int 20h instruction
; found at offset 0 of every PSP.  If key1 matches, then key2 is tried and if it
; matches key3 is tried.  A triple match gives fair confidence that SPY' has
; been run as a TSR once before.  It is even possible to gain further confidence
; or detect the presence of a different version of the same program by comparing
; a string in the returned es to a string in cs, say the copyright notice.  Note
; that the keys are compared in the newint16h routine that handles the function
; 77h if SPY' has been previously loaded.
;-------------------------------------------------------------------------------

set_es		proc	near		; es becomes cs of original SPY' load
		mov	ah,77h		; function to find previous load address
		int	16h
		cmp	ax,ckey1	; if ax = ckey1
		jne	not_loaded
		cmp	bx,ckey2	; and bx = ckey2
		jne	not_loaded
		mov	resident,1	; then there's been a prior load
		ret
not_loaded:	mov	ax,cs		; else, return with es = cs
		mov	es,ax
		ret
set_es		endp

;-------------------------------------------------------------------------------
; Execution starts here.  The command line is decoded, new parameters are passed
; to the prior load if it exists, and the beeps variable is set to zero for a
; fresh start.  Execution is then passed to  copy_spied:  and other termination
; code that can not be overwritten by the copy process.
;-------------------------------------------------------------------------------

start:		lea	dx,copyright	; display copyright
		mov	ah,9
		int	21h

		mov	bx,80h		; get command line byte count
		mov	al,[bx]
		cmp	al,1
		ja	parse		; if command given, parse it
stax:		lea	dx,syntax	; display command syntax
		mov	ah,9
		int	21h
		mov	ax,4c01h	; and exit with error code 1
		int	21h

parse:		mov	si,82h		; address first usable character
		lodsb
		call	skip		; skip over spaces and tabs
		jc	stax		; carry is set if RETURN was found
		call	convert		; convert and store hex values
		call	set_es		; set es to spy_buffer segment

		mov	ax,spy_seg	; set SPY' region to newly given values
		mov	es:spy_seg,ax	;   (this isn't necessary for first
		mov	ax,spy_off	;    time through, but it doesn't
		mov	es:spy_off,ax	;    hurt either.)
		mov	ax,spy_size
		mov	es:spy_size,ax

		xor	ax,ax		; initialize beeps count
		mov	es:beeps,ax

		jmp	copy_spied	; copy that which is to be spied

code		ends
		end	begin
