The idea of testing a program by following the order of execution of statements was introduced in Chapter Seven. The use of the command TRACE ON was also described, as were the problems presented by the quantity of output this command can produce. Ideally what is needed is some way of watching the execution of a program line by line without cluttering up the screen. The object of the program developed in this chapter is to provide the BASIC programmer with a 'real time' indication of the order of execution. Roughly speaking it prints the line number of statement that is currently being obeyed at a fixed location at the top of the screen but, to allow the programmer time to read this number before it is overwritten, it also has to slow down the execution rate of the BASIC interpreter. This program is particularly attractive in that, as well as being a useful programming tool in its own right, it opens up the way to a very wide range of other utilities.
If a machine code program is going to print the line number of each line of BASIC as it is executed we obviously have to find some way of intercepting the BASIC interpreter as it either starts or finishes obeying a line. At first sight this seems like an impossible problem but the key to its solution lies in the way the BASIC interpreter communicates with the MOS. Whenever the interpreter needs to call the MOS to carry out some action it does so by calling one of the fixed addresses high in memory but these routines immediately 'indirect' through a number of 'jump vectors' stored in RAM. For example, if the interpreter wants to print a character on the screen it loads the A register with the character code and jumps to the subroutine OSWRCH at &FFEE. However, the routine at &FFEE immediately does an indirect jump using &20E (known as WRCHV) i.e. JMP (&20E). In other words, the routine at &FFEE transfers control to a routine whose address is stored in &20E and, as &20E is a location in RAM, it can be changed.
The principle behind intercepting the BASIC interpreter as it starts or finishes executing a line is to force it to make a jump to the MOS which could be diverted to a new machine code routine. (Notice that there is no way that this routine could be written in BASIC!) After examining the possible jump vectors that could be altered (given in section 43 of the User Guide) the first idea to present itself was to force an error at the start of each ine by including an illegal character. This would cause the BASIC interpreter to jump through BRKV at &202 but the problem with this method is that there is no obvious way of restarting the BASIC interpreter after an error of this sort. None of the other jump vectors seemed to be of my use and so it looked as though a completely different method would have to be employed. However, after leaving the problem for a few hours, inspiration struck! Suddenly the possibility of using the existing TRACE command became clear. Following a TRACE ON command the line number of each line that is executed is printed on the screen in a fixed format:
[line number] space
The printing is, of course, achieved by using OSWRCH and this routine can be intercepted quite easily. Thus the overall plan is to change the RAM vector used by OSWRCH to point to a new machine code routine that checks each character printed for '['. If it finds an opening square bracket then it can assume that what follows, up to the matching closing square bracket is a line number. The only problem with this method is that any programs that print square brackets as part of their normal output or that contain assembly language will behave strangely. This seems a small price to pay for such a useful utility so easily implemented!
To summarise the execution tracer should:
(1) Detect '[' and move the text cursor to a fixed location where the digits that follow '[' will be printed.
(2) Detect ']' and move the text cursor back to its original position and suppress the next blank that is printed.
If you are writing a lot of assembly language then it makes good sense to write a standard BASIC program that can be re-used with each assembly language program. For example:
10 DIM CODE% 500
20 PROCasm(CODE%)
30 CALL *****
40 STOP
1000 DEF PROCasm(START%)
1005 FOR PASS=0 TO 3 STEP 3
1010 P%=START%
1020 PROCprog
1030 NEXT PASS
1040 PRINT
1050 PRINT P%-START%;" Bytes"
1060 ENDROC
Line 10 sets up a byte array called CODE% to hold the machine code produced. (For large programs 500 bytes might prove insufficient.) Line 20 calls PROCasm which actually does the job of assembling the program. The parameter START% is used to indicate where assembly should begin. This is usually set to CODE%, but as explained later this is not always the case. Line 30 calls the assembled machine code for testing. Of course, ***** would be replaced by the name of the routine to be run. Lines 1005 to 1030 perform the two pass assembly as described in Chapter Four. Line 1050 prints the number of bytes that the assembled machine code occupies and it is worth keeping an eye on this figure to make sure that sufficient space has been allocated to the byte array CODE%. The actual assembly language to he assembled is written within PROCprog and this is the only procedure that ever needs to be significantly altered. If you want to write an assembly language program simply write it as part of PROCprog (enclosed in square brackets) and add it to the BASIC program given above.
Assembly language program development proceeds in exactly the same way as for a BASIC program that is, as a number of stages of refinement. There are two parts to the main program of the execution tracer, the part that 'installs' it by changing the jump vector and the part that checks for square brackets. There is an important difference between the two in that the part that installs the program is only run once to change the jump vector but the check for square brackets is executed each tune a character is printed out - in this sense the part that changes the jump vector isn't really an integrated part of the execution checker and it can be written later.
The main program has to do three things:
(1) Check for '[' and carry out the appropriate action.
(2) Check for ']' and carry out the appropriate action.
(3) Check for the 'trailing' space after ']' and suppress it.
The first two actions are obvious but you might wonder why the trailing space has to be suppressed. The reason for this is a matter of screen formatting and will become clear later. The main program can now be written quite easily:
3100 .MAIN% CMP #ASC("[")
3110 BEQ DOTRACE%
3120 CMP #ASC("]")
3130 BEQ FINTRACE%
3140 CMP #ASC(" ")
3150 BEQ SPACE%
3160 JMP (&70)
DOTRACE%, has to move the current printing position to the top of the screen so that the digits that follow the '[' are always printed in the same place. FINTRACE% has to restore the printing position back to where it was before DOTRACE% moved it! This implies that DOTRACE% also has to store the existing printing position before moving it elsewhere. SPACE% has the rather odd job of checking to see if each space printed follows a closing square bracket, but more of this later. If the character to be printed is anything other than a square bracket or a blank the main program does an indirect jump through location &70. The reason for this is that it is assumed that the part of the program that changes the jump vector WRCHV to point to the start of the execution tracer will store its original value in &70 and &71. Thus JMP (&70) will transfer control to the machine code in the MOS that implements OSWRCH.
The machine code routines that have to be written as part of the second stage of refinement are DOTRACE%, FINTRACE% and SPACE%. As described in the last section, DOTRACE%, has to save the current position of the text cursor and then move it to the position that the line number is to be printed at. Finding the current position of the text cursor is easy OSBYTE call 134 returns the x and y co-ordinates of the text cursor in the X and Y registers respectively. In practice, this is a little too easy in that there is a hidden trap inherent in using any of the MOS from within the execution tracer. The trouble is that whenever DOTRACE% is called, the BASIC interpreter will be in the middle of printing a '[' on the screen and if it is going to resume what it was doing before it was intercepted by the execution tracer the contents of all the registers and system variables have to be preserved. The easiest way to save and restore all or any of the registers is to use the system stack. However, pushing all the registers on the stack and then pulling them all off is a very long and boring list of assembly language statements. The obvious thing to do is to use a pair of macros:
9300 DEF PROCsave
9400 [OPT PASS
9410 PHA
9420 PHP
9430 TXA
9440 PHA
9450 TYA
9460 PHA
9470 ]
9480 ENDPROC
9500 DEF PROCrestore
9510 [OPT PASS
9520 PLA
9530 TAY
9540 PLA
9550 TAX
9560 PLP
9570 PLA
9580 ]
The instruction:
]:PROCsave:[OPT PASS
will generate the code to push each of the 6502% registers onto the system stack in the order A,P ,X, Y. Similarly:
]:PROCrestore:[OPT PASS
will pull the registers off the system stack in the reverse order i.e. Y,X,P,A thus restoring their original values. (The system stack is a LIFO stack, see Chapter Five.)
Now that the problem with preserving the registers has been solved DOTRACE% is easy to write:
3200 .DOTRACE% :]PROCsave:[OPT PASS
3210 LDA #134
3220 JSR OSBYTE%
3230 STX &72
3240 STY &73
3250 LDA #31
3260 JSR OSWRCH%
3270 LDA #20
3280 JSR OSWRCH%
3290 LDA #0
3300 JSR OSWRCH%
3310 :]PROCrestore:[OPT PASS
3320 LDA #0
3330 JMP (&70)
Lines 3210 to 3240 find the current location of the text cursor (using OSBYTE call 134) and store the X value in &72 and the Y value in &73. Lines 3250 to 3300 use the machine code equivalent of TAB(x,y) to move the text cursor. This is achieved by sending ASCII code 31 to the VDU driver (see Chapter Ten) followed by the desired x and y values (20 and 0 in this case). The last part of the routine restores the registers (line 3310) and then loads the A register with 0 the ASCII code for null and does an indirect jump through location &70 (line 3330). As already explained, this transfers control to machine code that implements OSWRCH in the MOS and thus completes the call to OSWRCH made by BASIC.
Notice that this simple routine, DOTRACE%, has a very subtle and potentially confusing action. It calls OSWRCH apparently directly using JSR OSWRCH%, but WRCHV (the jump vector for OSWRCH) has been changed to point to the start of the execution tracer. In other words, the instructions JSR OSWRCH% actually calls the execution tracer for a second time! This works because none of the routines within the execution tracer try to print any of the characters tested for by the main program and so all OSWRCH calls that originate from within the execution tracer are correctly passed on to OSWRCH (i.e. by line 3160). You might like to work out what happens if DOTRACE% attempts to print "[" or "]"!
FINTRACE% is quite easy:
3400 .FINTRACE% :]PROCsave:[OPT PASS
3405 JSR PBLANK%
3410 LDA #31
3420 JSR OSWRCH%
3430 LDA &72
3440 JSR OSWRCH%
3450 LDA &73
3460 JSR OSWRCH%
3470 LDA #&FF
3480 STA SPFLG%
3485 JSR DELAY%
3490 :]PROCrestore:[OPT PASS
3500 LDA #0
3510 JMP (&70)
After first saving the registers using PROCsave it prints two blanks using PBLANK% (a subroutine to be written at the next level of refinement). The reason for printing these blanks is to clear any trailing digits belonging to previous line numbers. Lines 3410 to 3460 restore the text cursor to its original position once again using the machine code equivalent of TAB(x,y). Line 3480 sets SPFLG% to &FF to indicate that the next space Ibm BASIC tries to print should be suppressed. Line 3485 then calls DELAY%. which, as its name suggests, serves to slow down the speed of execution of BASIC so that the fine numbers can be read. Finally a null is printed to complete the OSWRCH call originally made by BASIC.
Now that FINTRACE% has set SPFLG% to &FF when a space has to be suppressed, SPACE%, can be written as a simple test on SPFLG%.
3600 .SPACE% PHA
3610 LDA SPFLG%
3620 BEQ SPEXIT%
3630 PLA
3640 LDA #0
3650 STA SPFLG%
3660 JMP (&70)
3670 .SPEXIT% PLA
3680 JMP (&70)
After saving the contents of A (line 3600) SPFLG% is loaded and tested to see if it is zero. If it is, line 3260 transfers control to 3670 where the value in A is restored and printed (line 3680). If SPFLG% isn't zero then A is loaded with zero and this is used both to zero SPFLG%, and to print a null character in place of the space. Notice the way that FINTRACE%, and SPACE% use SPFLG% as a method of communication. FINTRACE% sets SPFLG% to &FF when the next space should be suppressed and SPACE% resets it to zero so that only a single space is supppressed. A variable or memory location used in this way is often referred to as a 'flag'.
At the third stage only PBLANK% and DELAY% remain to be implemented. These are both trivial:
3700 .DELAY% LDY #PAUSE
3710 .DLOOP% LDX #255
3720 .D1% DEX
3730 BNE D1%
3740 DEY
3750 BNE DLOOP%
3760 RTS
3800 .PBLANK% LDA #ASC(" ")
3810 JSR OSWRCH%
3820 LDA #ASC(" ")
3830 JSR OSWRCH%
3840 RTS
The only detail worth comment is the way that the X and Y registers are used as separate counters to provide a long enough delay. Lines 3710 to 3730 form an inner loop nested within an outer loop formed by lines 3700 to 3750. Each time through the outer loop the inner loop is executed 255 times. The total delay is set by the variable PAUSE% which for convenience can be defined as part of the BASIC program that assembles the execution tracer.
All that now remains to be implemented is a short program to change the jump vector WRCHV to point to the start of the execution tracer (i.e. MAIN%). This simply involves transferring the existing contents of &20E to &70 and &20F to &71 and then storing the low and high bytes of MAIN% in &20E and &20F respectively. That is:
3000 .SETUP% LDA &20E
3010 STA &70
3020 LDA &20F
3030 STA &71
3040 LDA #(MAIN% MOD 256)
3050 STA &20E
3060 LDA #(MAIN% DIV 256)
3070 STA &20F
3080 RTS
The complete program, including macros, data definitions and the procedures that assemble it, is given below:
10 DIM CODE% 500
15 PAUSE=255
20 PROCasm(CODE%)
30 CALL SETUP%
40 STOP
1000 DEF PROCasm(START%)
1005 FOR PASS=0 TO 3 STEP 3
1010 P%=START%
1020 PROCprog
1030 NEXT PASS
1040 PRINT
1050 PRINT P%-START%;" Bytes"
1060 ENDPROC
2000 DEF PROCprog
2010 OSBYTE%=&FFF4
2020 OSWRCH%=&FFEE
2030 OSWORD%=&FFF1
2990 [OPT PASS
3000 .SETUP% LDA &20E
\save WRCHV in &70
3010 STA &70
3020 LDA &20F
3030 STA &71
3040 LDA #(MAIN% MOD 256)
\store MAIN % in WRCHV
3050 STA &20E
3060 LDA #(MAIN% DIV 256)
3070 STA &20F
3080 RTS
3090 \
3100 .MAIN% CMP #ASC("[")
3110 BEQ DOTRACE%
3120 CMP #ASC("]")
3130 BEQ FINTRACE%
3140 CMP #ASC(" ")
3150 BEQ SPACE%
3160 JMP (&70)
\print the character in A
3170 \
3200 .DOTRACE% :]PROCsave:[OPT PASS
\save the registers
3210 LDA #134
\get the current cursor position
3220 JSR OSBYTE%
3230 STX &72
\and store it in &72 and &73
3240 STY &73
3250 LDA #31
\TAB(20,0)
3260 JSR OSWRCH%
3270 LDA #20
3280 JSR OSWRCH%
3290 LDA #0
3300 JSR OSWRCH%
3310 :]PROCrestore:[OPT PASS
\restore registers
3320 LDA #0
3330 JMP (&70)
\print a null
3340 \
3400 .FINTRACE% :]PROCsave:[OPT PASS
\save registers
3405 JSR PBLANK%
\print two blanks
3410 LDA #31
\TAB(X,Y) where x and y are
3420 JSR OSWRCH%
\old cursor position
3430 LDA &72
3440 JSR OSWRCH%
3450 LDA &73
3460 JSR OSWRCH%
3470 LDA #&FF
\set flag to supress next space
3480 STA SPFLG%
3485 JSR DELAY%
\make BASIC slower
3490 :]PROCrestore:[OPT PASS
\restore registers
3500 LDA #0
\print a null
3510 JMP (&70)
3520 \
3600 .SPACE% PHA
3610 LDA SPFLG%
\test flag
3620 BEQ SPEXIT%
\IF SPFLG=0 THEN PRINT A ELSE PRINT ""
3630 PLA
3640 LDA #0
3650 STA SPFLG%
3660 JMP (&70)
3670 .SPEXIT% PLA
3680 JMP (&70)
3690 \
3700 .DELAY% LDY #PAUSE
\PAUSE sets the length of delay
3710 .DLOOP% LDX #255
3720 .D1% DEX
3730 BNE D1%
3740 DEY
3750 BNE DLOOP%
3760 RTS
3770 \
3800 .PBLANK% LDA #ASC(" ")
3810 JSR OSWRCH%
3820 LDA #ASC(" ")
3830 JSR OSWRCH%
3840 RTS
9000 REM MACROS
9010 DEF FNequb(VA%)
9020 ?P%=(VA% MOD 256)
9025 IF PASS=3 THEN PRINT ~P%;"=";~?P%
9030 P%=P%+1
9040 =P%-1
9100 DEF FNequw(VA%)
9110 ?P%=(VA% MOD 256)
9115 IF PASS=3 THEN PRINT ~P%;"=";~?P%
9120 P%?1=(VA% DIV 256)
9125 IF PASS=3 THEN PRINT ~P%+1;"=";~(P%?1)
9130 P%=P%+2
9140 =P%-2
9200 DEF FNequs(S$)
9210 $P%=S$
9220 IF PASS=3 THEN PRINT ~P%;"=";S$
9230 P%=P%+LEN(S$)+1
9240 =P%-LEN(S$)-1
9300 DEF PROCsave
9400 [OPT PASS
9410 PHA
9420 PHP
9430 TXA
9440 PHA
9450 TYA
9460 PHA
9470 ]
9480 ENDPROC
9500 DEF PROCrestore
9510 [OPT PASS
9520 PLA
9530 TAY
9540 PLA
9550 TAX
9560 PLP
9570 PLA
9580 ]
9590 ENDPROC
If you run this program and then add to it the lines:
1 REM
2 GOTO 1
and then run it after typing TRACE ON you will see the line numbers '1' and '2' printed repeatedly at the top of the screen following the execution of the infinite loop. To disable the trace just type TRACE OFF. Notice that if you try to trace the execution of the program that assembles the execution trace program you are doomed to crash the machine. The reason for this is that the first pass of assembly overwrites the existing machine code with an incomplete version with obvious consequences.
If the execution tracer is going to be used to test BASIC programs it is clear that the machine code that it produces has to be stored somewhere out of harm's way so that another BASIC program can be loaded and run without destroying the machine code. There are locations where machine code can hide which do not need to be 'setup' and thus the user can be spared the problem of reserving memory. The trouble is that these readymade places are all used for something else by the BBC Micro. For example, the execution tracer could be stored in the RS423 buffers (transmit at &0900 to &09FF and receive at &0A00 to &0AFF) but this would cause a problem if the serial port was being used at the same time.
The alternative to using one of these existing areas is to reserve some memory by increasing the value stored in PAGE. However, for a utility such as the execution tracer this seems very inappropriate and likely to cause trouble if you load the machine code over an existing BASIC program by forgetting to set PAGE! After weighing up the alternatives, in this case it seems better to use the RS423 receive buffer at &0A00 to store the machine code. This still allows a serial printer if required without disturbing the machine code Of course, if the program that was being debugged using the tracer used the RS423 Input then it would have to be re-assembled In tun in the memory reserved using PAGE. (An example of the use of PAGE to reserve memory can be found in Chapter Ten in connection with the background clock program.)
To assemble the machine code into the RS432 receive buffer all you have to do is to change line 20 to read:
20 PROCasm(&A00)
The resulting machine code should then be saved on disk or tape by using:
*SAVE TRACER A00,AFF,A00
This will save the entire RS432 buffer from &A00 to &AFF. Following this you can install the execution tracer at any time by typing:
*RUN TRACER
and using TRACE ON/OFF to control it.
The execution tracer is not an easy program to write in that it 'tampers' with the normal working of the MOS. The program is already in a state where it is a useful tool but there are one or two simple extras that would be worth incorporating. It is very easy to install the execution tracer but currently the only way to remove it completely is to press BREAK. It would be better if another program was made available to remove the execution tracer by changing WRCHV back to its original value. It would also bean advantage to incorporate a method of allowing the user to alter the delay factor without having to re-assemble the program, but this is a more complicated addition.
The main worry with any program that modifies the workings of the MOS is that it will introduce unwanted and unpredictable side effects. Such side effects are always difficult to pin down because they are generally only observed after a great number of different things have happened and in this sense they are not repeatable. The execution tracer does have a serious problem that was first identified as a side effect- Occasionally while tracing the progress of a graphics program a line would be drawn to the wrong point. This happened very infrequently but often enough to cast doubt on the execution tracer. The cause of this erratic behaviour remained a mystery for some time until a simple program that consistently produced the misbehaviour was found. If you try:
10 VDU 23,224,0,91,0,91,0,91,0,91
20 PRINT "A"
30 GOTO 10
with the execution tracer installed and turned on you will find that the "A" is not pruned although the trace claims that line 20 is repeatedly executed. The explanation lies in the fact that ASC("[") is 91! A number of VDU codes are followed by a list of parameter bytes, all of which are sent to the VDU driver. For example, in the case of line 10 the VDU statement sends ASCII code 23, then 224, and then 0 and 91 four times. The execution tracer is thus fooled into thinking that a stream of opening square brackets are being sent to the screen when in fact they are all part of a character definition!
The solution to this problem is to detect those VDU codes that are followed by a sequence of parameter bytes and make sure that the execution tracer ignores them. This is quite easy in principle but in practice it takes quite a few extra lines of code. The modifications necessary to the execution tracer to enable it to ignore parameter bytes following certain VDU codes are given below:
3090 .MAIN% PHA
3091 LDA SUP_COUNT%
\IF SUP_COUNT<>0 THEN GOTO SKIP%
3092 BNE SKIP%
3093 PLA
3094 JSR VCODE%
\check for codes and set SUP_COUNT%
3109 CMP #ASC("[")
3110 BEQ DOTRACE%
3120 CMP #ASC("]")
3130 BEQ FINTRACE%
3140 CMP #ASC(" ")
3150 BEQ SPACE%
3160 JMP (&70)
3165 .SKIP% DEC SUP_COUNT%
\SUP_COUNT=SUP_COUNT-1
3166 PLA
\PRINT A
3167 JMP (&70)
4000 .VCODE% STX &74
\save X
4010 LDX #9
\load X with number of bytes that
follow each code
4020 CMP #23
\test for code
4030 BEQ COEX%
\IF A=code THEN GOTO COEX%
4040 LDX #8
4050 CMP #24
4060 BEQ COEX%
4070 LDX #5
4080 CMP #25
4090 BEQ COEX%
4100 LDX #4
4110 CMP #28
4120 BEQ COEX%
4130 LDX #4
4140 CMP #29
4150 BEQ COEX%
4155 LDX #2
4156 CMP #31
4157 BEQ COEX%
4160 LDX #0
4170 .COEX% STX SUP_COUNT%
4180 LDX &74
\restore X
4190 RTS
5000 ]
5010 REM DATA
5020 SPFLG%=FNequb(0)
5030 SUP_COUNT%=FNequb(0)
The basic idea behind this extension is to record in SUP_COUNT%, the number of bytes that should be ignored by the execution tracer. If SUP_COUNT% is zero then the program works as before apart from calling VCODE% to check to see if the code in the A register is followed by any parameter bytes. If SUP_COUNT% is non-zero then the code in the A register is not examined by the execution tracer and is simply passed to OSWRCH. Each code that is ignored in this way causes SUP_COUNT% to decrease by one. Thus the number of bytes that are ignored depends on the initial setting of SUP_COUNT%. Setting SUP_COUNT% is the responsibility of subroutine VCODE%. This repeatedly loads the X register with the number of bytes to be ignored following a particular code and then tests for that code. For example, line 4010 loads X with 9 and then line 4020 tests for code 23 and indeed nine parameter bytes always follow code 23. In this way the X register always contains the number of bytes to be ignored when the subroutine reaches COEX%. This value is stored in SUP_COUNT% and then control is returned to the main program. There are many other small but important details of how this extension works but you should be able to make sense of them one by one.
The execution tracer started life as a fairly simple idea for an assembly language program. As with many assembly language programs it quickly grew to be larger and more subtle than expected! The use of stepwise refinement and modular programming makes it much easier to cope with the unexpected as well as the planned! There is still a small problem with the execution tracer that results from the way that BBC BASIC handles the comma within a PRINT statement. This is because the 'zone' spacing is performed by BASIC, which uses the COUNT function to send the correct number of blanks to the screen, rather than by the MOS. There is nothing simple that can be done to correct this problem without using memory locations that might be changed in future versions of BASIC. Fortunately the trouble it causes doesn't detract loo much from the usefulness of the execution tracer.