.Z80 .COMMENT \ SPY Date/time in title Copyright (c) 1986 J L Washington 33 Turnoak Ave, Woking, Surrey, England GU22 0AJ SPY.COM investigates the use of disk by a program. SPY relocates itself to the high end of the TPA, then loads and runs another program, reporting on calls of the bdos and (optionally) the bios. It is intended to run on most (all?) CP/M-80 2.2 systems with a Z80 processor. To use type something like SPY M80 =FRED in other words simply preface the command you would otherwise type to run a program without spying on it. Warning: this program could be much improved, and some of the label names and comments could be a good deal clearer. It's one of those programs slung together in a hurry to solve a specific problem, that have lain in the toolbox and been quickly cobbled when needed for the next specific problem. Perhaps some generous soul will do the job properly and start afresh? You'll quickly find another lack. When you start it running you'll want to know what all that wondrous gibberish on your screen means! Yes, I know I should be using my time to write a manual for you, instead of composing an apology. You'll just have to study the assembler code to figure it out in detail, though some of it should be obvious enough (e.g. the name of the function, and the name of the file). RESTRICTIONS: Some programs look in 80h onwards to find their command tail, in which case SPY sets up what they'd expect. But some programs rely on the CCP decoding the command tail into a File Control Block at 5Ch onwards. This is not yet implemented by SPY. STOP PRESS... Just implemented. But not tried! Tread carefully! It is always possible to write (by accident or design) a program which behaves differently when run under the control of a software monitor. As you would guess, SPY works by tampering with the pointers to (and within) the bdos and bios. Of course, there are also other programs which do this, sometimes for very good reasons, and it may be difficult for SPY to coexist with such programs successfully. For example, VDE and VDO do some sneaky modifications for the excellent reason of capturing i/o errors without that unfriendly "BDOS ERR" message. Perhaps this is why I've not succeeded in running either of these editors under SPY. \ false equ 0 true equ not false ;*** Alter the following line to choose whether to have bios tracing ;*** possible as well as bdos tracing. The advantage of setting it ;*** false would be to reduce the size of SPY.COM, in order to monitor ;*** a program that needs a lot of TPA memory. ;*** However, I havn't used this feature recently, so you might have ;*** to remove a few assembly errors, or other problems. In which case ;*** I'd appreciate it if you'd save me some time by letting me know. fullspy equ true ; Addresses in low memory: BDOS EQU 5 TBASE EQU 100H TBUFF EQU 80H TFCB EQU 5Ch cr equ 0dh lf equ 0ah Start: LD DE,WarnPrinterUsed LD C,9 CALL BDOS CALL OutStr DB 'Spy BDOS' if fullspy DB '+BIOS' endif DB ' 861102.1715',cr,lf DB 'Relocated to ',0 LD HL,Start CALL OutH4 CALL OutStr DB '..',0 LD HL,END CALL OutH4 ; (Might be better to delay these questions until program has been loaded?) CALL OutStr DB CR,LF,CR,LF DB 'Bios trace? (y/n) ',0 CALL GetYN LD (BiosTrace),A CALL OutStr DB CR,LF,'Use printer? (y/n) ',0 CALL GetYN JR Z,NoLst LD HL,0018h ; HL := "JR $+2" LD (PatchLst),HL NoLst: CALL OutStr DB CR,LF,'Display slowly? (y/n) ',0 CALL GetYN JR Z,NoSlow LD HL,3000 ; Set higher delay counter in o/p routine LD (PatchSlow),HL NoSlow: CALL OutStr DB CR,LF,'Wait for keystrike each bdos call? (y/n) ',0 CALL GetYN JR Z,NoPause LD HL,0018h ; HL := "JR $+2" LD (PatchPause),HL NoPause: ; Load the program requested in the command tail CALL OutStr DB CR,LF DB 'Load-file ',0 LD HL,TFCB LD DE,FCB LD BC,9 LDIR CALL OPENFCB JP Z,0 ; problem -> boot CALL OutStr DB 'opened',cr,lf,0 LD HL,TBASE UNK3: PUSH HL EX DE,HL CALL DMASET LD DE,FCB CALL RDREC JP NZ,UNK4 ; end of file or read err -> CALL OutStr DB CR DB 'Reading ',0 POP HL CALL OutH4 LD DE,80H ADD HL,DE LD DE,Start-80h XOR A PUSH HL SBC HL,DE ; fits? POP HL JP NC,0 ; no -> boot (OUGHT TO HAVE ERROR MSG HERE) JR UNK3 UNK4: CALL OutStr DB cr,lf,'Program loaded',0 POP HL DEC A ; Normal eof? JP NZ,0 ; no -> boot CALL OutStr DB ' ok',cr,lf,0 ; Now create the appropriate command tail CALL OutTail LD DE,TBUFF CALL DMASET LD HL,TBUFF LD DE,TBUFF+2 JJ1: LD A,(TBUFF) INC HL OR A JP Z,JJ9 DEC A LD (TBUFF),A LD A,(STATE) PUSH HL PUSH DE LD HL,JTBL LD D,0 LD E,A ADD HL,DE ADD HL,DE LD E,(HL) INC HL LD D,(HL) EX DE,HL POP DE EX (SP),HL LD A,(HL) RET ; BdosStack is not used until prog is entered and spying starts BdosStack: ; Sorry about some of the meaningless label names in this section. ; It seems to work ok, and I haven't touched it in a long time! STATE: DB 0 JTBL: DW JL0,JL1,JL2,JL3 ; JL entries: HL = source buffer ptr, DE = dest buffer ptr, A = (HL) JL0: ; Initial state, search for first space JL2: ; Name of prog to load and spy on found, search for first space CP ' ' JR NZ,JJ8 JR JJ6 JL1: ; First space found, skip any more CP ' ' JR Z,JJ8 JR JJ6 JL3: ; First space found after spyee prog name, copy the rest LD (DE),A INC DE CALL OUTVCH LD A,(BCOUNT) INC A LD (BCOUNT),A JR JJ8 JJ6: LD A,(STATE) INC A LD (STATE),A CALL OutStr DB CR,LF,0 JJ8: JP JJ1 JJ9: LD A,(BCOUNT) LD (TBUFF),A CALL OutTail JP LOADED BCOUNT: DB 1 ; Byte count for params OUTTAIL: LD A,(TBUFF) LD B,A LD HL,TBUFF+1 JG1: LD A,B OR A JR Z,JG2 DEC A LD B,A LD A,(HL) INC HL JR JG1 JG2: RET DMASET: LD C,26 JP BDOS OPENFCB: XOR A LD (FCB+32),A LD DE,FCB LD C,15 JR ENTRY1 ENTRY1: CALL BDOS LD (RTNCODE),A INC A RET RDREC: LD C,20 ; JR ENTRY2 ENTRY2: CALL BDOS OR A RET WarnPrinterUsed: DB 'Uses printer' DB CR,LF DB '$' FCB: DB 0 DB '12345678' DB 'COM' DB 0,0,0 DB 0,0,0,0,0,0,0,0 DB 0,0,0,0,0,0,0,0 DB 0,0,0,0,0,0,0,0 DB 0,0,0,0,0,0,0,0 RTNCODE: DB 0 INPOINT: DW 0 NAMEPOINT: DW 0 CDRIVE: DB 0 ; Ought to be currently logged drive *** ; *** not yet implemented SYNERR: CALL OutStr DB 'Syntax err',0 JP 0 ; Following routines based on some code in the CCP CHECK: LD A,(DE) OR A RET Z CP ' ' ; Control char? JR C,SYNERR ; yes -> RET Z CP '=' RET Z CP '_' RET Z CP '.' RET Z CP ':' RET Z CP ';' RET Z CP '<' RET Z CP '>' RET NONBLANK: ; Preserves HL LD A,(DE) OR A RET Z CP ' ' RET NZ INC DE JR NONBLANK Convert: ; Enter A = offset within TFCB (5C) ; INPOINT points to i/p buffer to parse ; Exit Filename parsed into fcb, and INPOINT updated LD HL,TFCB ; FCB in low memory LD E,A LD D,0 ADD HL,DE LD DE,(INPOINT) CALL NONBLANK ; Get next non-blank character ; (Routine preserves HL) LD (NAMEPOINT),DE ; Save ptr for an error msg ; (not yet used) LD A,(DE) ; A := First char OR A ; Is it null? JR Z,CONVRT1 ; no -> SBC A,'A'-1 ; Convert possible drive name to binary LD B,A INC DE LD A,(DE) ; A := 2nd char CP ':' ; Was first char a drive name? JR Z,CONVRT2 ; yes -> DEC DE ; Point to start again CONVRT1: LD A,(CDRIVE) ; A := Currently logged drive ; (not yet implemented, currently drive A regardless) LD (HL),A JR CONVRT3 CONVRT2: LD A,B LD (HL),B INC DE CONVRT3: ; Convert the basic filename LD B,8 CONVRT4: CALL CHECK ; Punctuation? JR Z,CONVRT8 ; yes -> INC HL CP '*' ; Ambig filename? JR NZ,CONVRT5 ; no -> LD (HL),'?' ; Expand with question marks JR CONVRT6 CONVRT5: LD (HL),A ; Simply copy char into fcb INC DE CONVRT6: DJNZ CONVRT4 CONVRT7: CALL CHECK ; Get next delimiter. Found? JR Z,GETEXT ; yes -> INC DE JR CONVRT7 CONVRT8: INC HL ; Fill with trailing blanks LD (HL),' ' DJNZ CONVRT8 GETEXT: ; Get the extension and convert it LD B,3 CP '.' ; Extension follows? JR NZ,GETEXT5 ; no -> INC DE GETEXT1: CALL CHECK JR Z,GETEXT5 INC HL CP '*' JR NZ,GETEXT2 LD (HL),'?' JR GETEXT3 GETEXT2: LD (HL),A INC DE GETEXT3: DJNZ GETEXT1 GETEXT4: CALL CHECK JR Z,GETEXT6 INC DE JR GETEXT4 GETEXT5: INC HL LD (HL),' ' ; Pad extension with trailing blanks DJNZ GETEXT5 GETEXT6: ; LD B,3 LD B,5 ; Kluge to ensure TFCB+33h is cleared GETEXT7: INC HL LD (HL),0 DJNZ GETEXT7 EX DE,HL LD (INPOINT),HL ; Save input line pointer RET TailConvert: ; *** NOT FINISHED, AND ONLY RECENTLY INTRODUCED CALL OutStr DB CR,LF DB 'New code, TailConvert',cr,lf,0 LD HL,TBUFF+1 LD (INPOINT),HL XOR A CALL CONVERT ; Convert 1st name into fcb LD A,16 CALL CONVERT ; Convert 2nd name into fcb 16 bytes later XOR A LD (TFCB+16),A ; (Ought really to be drive) LD (TFCB+32),A ; Make record number zero ; Dump new fcbs for checking LD B,2 LD HL,TFCB TAIL2: PUSH BC LD B,16 TAIL3: LD A,(HL) CALL OUTVCH INC HL DJNZ TAIL3 CALL OutStr DB CR,LF,0 POP BC DJNZ TAIL2 CALL OutStr DB 'TailConvert complete',cr,lf,0 RET ; End of code based on CCP LOADED: LD HL,(6) ; HL := Entry to BDOS LD (BdosFin+1),HL ; Patch the JP/CALL to BDOS after the report ; Adjust BDOS jump to come thru this monitor. ; But create a double jump, so that the application will think ; the bdos gateway is completely normal, being located at xx06. LD DE,BdosSpy ; DE := address to be planted in JP at xx06 LD HL,BdosSpy DEC H LD A,6 LD L,A ; HL := xx06 below BdosSpy LD HL,Start+6 ; Unnecessarily conservative ; change when the local ; stacks are better PUSH HL LD A,0C3H LD (HL),A INC HL LD (HL),E INC HL LD (HL),D ; xx06.. := JP BdosSpy POP HL LD (6),HL ; 0005.. := JP xx06 ;Bdos jump now fudged ok if fullspy LD A,(BiosTrace) OR A JP Z,BT1 LD HL,(1) LD L,0 ; HL := Start of bios jumps LD (BiosAddr),HL LD DE,BiosTbl LD BC,BiosTblX-BiosTbl LDIR ; Copy bios jumps into BiosTbl LD DE,4 LD HL,(BiosAddr) ADD HL,DE LD DE,Booting LD (HL),E INC HL LD (HL),D ; Redirect warm boot to 'Booting' LD DE,18h ; xx18 is Home (first bios disk routine) LD HL,(BiosAddr) ADD HL,DE ; HL := xx18 LD B,7 LD A,0CDh ; A := CALL opcode LD DE,BiosSpy BI1: LD (HL),A INC HL LD (HL),E INC HL LD (HL),D INC HL DJNZ BI1 ; BiosStack is not used until prog is entered and spying starts BiosStack: BT1: endif ; DECODE OF COMMAND TAIL INTO FCB AT 5Ch.. CALL TailConvert CALL TBASE ; enter loaded program JP 0 DB 0,0,0,0,0,0 DB 'BdosSpy:' BdosSpy: ; monitor whatever LD (SAVEHL),HL LD HL,0 ADD HL,SP LD (SAVESP),HL LD SP,BdosStack PUSH DE PUSH BC PUSH AF LD A,0C3h LD (BdosFin),A ; Make it a JP instruction LD A,C CP 13 JP C,SKIP CALL OutStr DB 'bdos ',0 LD L,C LD H,0 ADD HL,HL ADD HL,HL ADD HL,HL LD DE,BdosTbl2-104 ; 104 = 8*13 ; *** Write more cleanly ADD HL,DE LD DE,DM1 LD BC,6 LDIR LD DE,DFLAGS LD BC,2 LDIR CALL OutStr DB ' ' DM1: DB '123456 ',0 POP AF POP BC POP DE PUSH DE PUSH BC PUSH AF LD A,(FLAG1) OR A JR Z,F10 CP 1 JR NZ,F12 CALL OutStr DB ' ',0 LD A,E CALL OutH2 ; bdos param E JR F19 F12: LD H,D LD L,E CALL OutH4 ; bdos param DE JR F19 F10: CALL OutStr DB ' ',0 F19: CALL OutSpace LD A,(FLAG2) OR A JP Z,F20 PUSH DE LD B,16 L20: LD A,(DE) CALL OUTVCH ; First 16 bytes of FCB INC DE DJNZ L20 CALL OutSpace POP DE LD HL,33 ADD HL,DE LD E,(HL) INC HL LD D,(HL) EX DE,HL CALL OutH4 ; Rndm rec no. JR F29 F20: CALL OutStr DB ' ',0 F29: CALL OutStr DB CR,LF,0 LD A,(InCount) OR A JR Z,F40 DEC A LD (InCount),A JR F49 F40: PatchPause: JR F40A ; %% CALL CONIN ; Pause at each Bdos call F40A: CP ' ' JR C,F52 AND 0Fh JR F54 F52: XOR A F54: LD (InCount),A F49: LD A,0CDh LD (BdosFin),A ; Make it a CALL SKIP: POP AF POP BC POP DE LD SP,(SAVESP) LD HL,(SAVEHL) BdosFin: JP $-$ ; %% JP/CALL BDOS to do whatever was asked for ; %% may be JP or CALL CALL OutStr DB 'bdos exit',CR,LF,0 RET SAVESP: DW 0 SAVEHL: DW 0 InCount: DB 0 BdosTbl2: DB 'Reset ',0,0 DB 'Select',1,0 db 'Open ',2,1 db 'Close ',2,1 db 'Srch1 ',2,1 db 'SrchNx',2,1 db 'Erase ',2,1 db 'RdSeq ',2,1 db 'WrSeq ',2,1 db 'Create',2,1 db 'Rename',2,1 db 'GetAct',0,0 db 'GetCur',0,0 db 'SetDma',2,0 db 'GetAlV',0,0 db 'SetR/O',0,0 db 'GetR/O',0,0 db 'SetAtt',2,1 db 'GetDpb',0,0 db 'User ',1,0 db 'RdRan ',2,1 db 'WrRan ',2,1 db 'GetSiz',2,1 db 'SetR# ',2,1 db 'ResDrv',2,0 db '38 ',0,0 db '39 ',0,0 db 'Fill0 ',2,1 DFLAGS: FLAG1: DB 0 FLAG2: DB 0 if fullspy DB 'BiosSpy:' BiosSpy: ; monitor whatever in bios call LD (BiosHL),HL POP HL LD (BiosLink),HL POP HL PUSH HL LD (BiosRet),HL LD HL,0 ADD HL,SP LD (BiosSP),HL LD SP,BiosStack PUSH AF PUSH DE PUSH BC CALL OutStr DB 'bios ',0 LD HL,(BiosLink) LD H,0 LD DE,BiosTbl2-9 EX DE,HL ADD HL,DE ADD HL,DE ADD HL,DE LD DE,BM1 LD BC,6 LDIR LD DE,FLAGS LD BC,3 LDIR CALL OutStr DB ' ' BM1: DB '123456',0 CALL OutSpace POP HL POP DE PUSH DE PUSH HL LD A,(FLAGBC) OR A JR Z,FBC0 CP 1 JR NZ,FBC2 CALL OutStr DB ' ',0 LD A,L CALL OutH2 ; bios param C JR FBC9 FBC2: CALL OutH4 ; bios param BC JR FBC9 FBC0: CALL OutStr DB ' ',0 FBC9: CALL OutSpace LD A,(FLAGDE) OR A JR Z,FDE0 LD A,E CALL OutH2 ; bios param E JR FDE9 FDE0: CALL OutStr DB ' ',0 FDE9: CALL OutSpace LD HL,(BiosRet) CALL OutH4 ; bios caller LD HL,(BiosLink) LD H,0 LD DE,BiosTbl-3 ADD HL,DE LD (BiosJ),HL POP BC POP DE POP AF LD HL,(BiosHL) CALL $ ; jump to Bios to do whatever was asked for BiosJ EQU $-2 CALL OutStr DB '.',CR,LF,0 LD SP,(BiosSP) RET BiosSP: DW 0 BiosHL: DW 0 BiosLink: DW 0 BiosRet: DW 0 BiosTbl: DS 33h BiosTblX: BiosTbl2: DB 'Boot ',0,0,0 DB 'WmBoot',0,0,0 DB 'ConSt ',0,0,0 DB 'ConIn ',0,0,0 DB 'ConOut',0,0,0 DB 'List ',0,0,0 DB 'Punch ',0,0,0 DB 'Reader',0,0,0 DB 'Home ',0,0,0 DB 'SelDsk',1,1,0 DB 'SetTrk',2,0,0 DB 'SetSec',1,0,0 DB 'SetDma',2,0,0 DB 'Read ',0,0,0 DB 'Write ',1,0,0 DB 'ListSt',0,0,0 DB 'SecTrn',0,0,0 FLAGS: ; Next 3 bytes are grouped together FLAGBC: DB 0 FLAGDE: DB 0 DB 0 Booting: LD HL,BiosTbl LD DE,(BiosAddr) LD BC,BiosTblX-BiosTbl LDIR ; Restore bios jumps LD HL,(BiosAddr) INC HL INC HL INC HL JP (HL) ; Continue warm boot BiosAddr: DW 0 endif BiosTrace: DB 0 ; 0 = Trace Bdos calls only ; FF = Trace Bdos & Bios calls ; Character i/o routines GetYN: ; Exit A = result ; also Z flag indicates result CALL OutStr DB 8,' ',8,0 CALL CONIN CALL OutCh RES 5,A ; Force upper case SUB 'N' RET Z CP 'Y'-'N' JR NZ,GetYN LD A,0FFh OR A RET OUTVCH: ; O/p visible character or hex between angle brackets ; Regs preserved CP ' ' JR C,OV2 CP 7EH ; 7E or higher? JR C,OutCh ; no -> treat as normal char and o/p it ; (Stop at 7E rather than 7F, since some ; terminals, notably Hazeltine, use this ; as a screen control byte). OV2: PUSH AF LD A,'<' CALL OutCh POP AF PUSH AF CALL OutH2 LD A,'>' CALL OutCh POP AF RET OutH4: ; Regs preserved PUSH AF LD A,H CALL OutH2 LD A,L CALL OutH2 POP AF RET OutH2: ; Regs preserved PUSH AF RRA RRA RRA RRA CALL OutH1 POP AF CALL OutH1 RET OutH1: ; Regs preserved PUSH AF AND 0FH CP 10 JR C,OH1A ADD A,'A'-'0'-10 OH1A: ADD A,'0' CALL OutCh POP AF RET OutStr: EX (SP),HL PUSH AF LD A,(HL) INC HL OR A JR Z,OutStr1 CALL OutCh POP AF EX (SP),HL JR OutStr OutStr1: POP AF EX (SP),HL RET OutSpace: PUSH AF LD A,' ' CALL OutCh POP AF RET OutCh: ; Regs preserved (assuming BIOS doesn't change IX, IY) PUSH HL PUSH DE PUSH BC PUSH AF LD C,A CALL ConOut ;Bios+0Ch ; O/p to screen LD HL,1 ; %% Optional delay PatchSlow EQU $-2 OutCh1: dec hl ld a,h or l jr nz,OutCh1 POP AF PatchLst: JR SkipLst ; %% PUSH AF LD C,A CALL LstOut ; Bios+0Fh O/p to printer POP AF SkipLst: POP BC POP DE POP HL RET ConOut: LD HL,(1) LD L,0Ch JP (HL) LstOut: LD HL,(1) LD L,0Fh JP (HL) CONIN: LD HL,(1) LD L,9 JP (HL) END EQU $ END Start