Search
  • daveor

Reverse engineering PDP-11 BASIC: Part 2

Updated: Jan 25

This is the second in my series of posts reverse-engineering the PDP-11 BASIC code. In this instalment I'll be breaking down a few more of the TRAP calls; TRAP 100, TRAP 72 and TRAP 152.


TRAP 100

The TRAP 100 routine is used for handling input. It's long but as we work through it you'll see that it's really not that complicated. Here's the code:

; This is part of the trap 100 routine but not the entry point
; it is invoked when the user presses DEL
000616 005767 TST 13674
000622 001012 BNE 650
000624 022703 CMP #13540, R3
000630 103005 BCC 644
000632 012702 MOV #134, R2
000636 104400 TRAP 0
000640 005303 DEC R3
000642 000402 BR 650

; This is the TRAP 100 entry point
000644 012703 MOV #13540, R3
000650 005767 TST 13674
000654 001437 BEQ 754
000656 005067 CLR 13700
000662 016702 MOV 13704, R2
000666 020227 CMP R2, #177560
000672 001002 BNE 700
000674 005067 CLR 177560
000700 105212 INCB (R2)
000702 005267 INC 13710
000706 005712 TST (R2)
000710 100502 BMI 1116
000712 105712 TSTB (R2)
000714 100372 BPL 702
000716 016202 MOV 2(R2), R2
000722 001752 BEQ 650
000724 042702 BIC #177600, R2
000730 120227 CMPB R2, #20
000734 001457 BEQ 1074
000736 122702 CMPB #15, R2
000742 001011 BNE 766
000744 104402 TRAP 2
000746 012702 MOV #12, R2
000752 000417 BR 1012
000754 005067 CLR 177560
000760 012702 MOV #177560, R2
000764 000746 BR 702
000766 122702 CMPB #177, R2
000772 001711 BEQ 616
000774 122702 CMPB #140, R2
001000 003445 BLE 1114
001002 122702 CMPB #25, R2
001006 001422 BEQ 1054
001010 104400 TRAP 0
001012 110223 MOVB R2, (R3)+
001014 120227 CMPB R2, #12
001020 001404 BEQ 1032
001022 020327 CMP R3, #13657
001026 103710 BCS 650
001030 104431 TRAP 31
001032 012701 MOV #13540, R1
001036 012767 MOV #1, 13700
001044 052767 BIS #100, 177560
001052 000207 RTS PC

; Code for displaying a NAK
001054 112702 MOVB #136, R2
001060 104400 TRAP 0
001062 112702 MOVB #125, R2
001066 104400 TRAP 0
001070 104402 TRAP 2
001072 000664 BR 644

; Code for handling Ctrl-P
001074 012767 MOV #100, 177560
001102 005046 CLR -(SP)
001104 012746 MOV #3056, -(SP)
001110 010246 MOV R2, -(SP)
001112 000423 BR 1162

;code for an invalid character (e.g. lower case character)
001114 104407 TRAP 7

001116 005067 CLR 13674
001122 012767 MOV #1, 13700
001130 005726 TST (SP)+
001132 000167 JMP 3112

So, let's walk through the code line-by-line, starting at the TRAP 100 entry point:

000644 012703 MOV #13540, R3

The value 13540 is stored in register R3. This is the input buffer for the code, so whatever data is input by the user (or read from paper tape) will be stored here.

000650 005767 TST 13674
000654 001437 BEQ 754

Next a check is carried out to see whether an input device has been configured by the user, in which case memory address 13674 will have the value 1, otherwise the default input device (TTY) is used if the address 13674 contains the value 0. If the value at 13674 is zero then the execution jumps to 754.

000656 005067 CLR 13700

This code is executed in the case where 13674 was non-zero. In other words where the user has selected a specific input device, which could either by the TTY or the paper tape reader/punch. Firstly the value at 13700 is cleared (i.e. set to zero). This will prevent simultaneous execution of the reading and writing code. As far as I can tell this is a scenario that needs to be prevented in the case of the paper tape reader/punch.

000662 016702 MOV 13704, R2

Next, the value from address 13704 is moved into R2. 13704 will contain the CSW (Control and Status Word) of the configured input device (either the TTY or the punch tape reader).

000666 020227 CMP R2, #177560
000672 001002 BNE 700

The value in R2 is compared to 177560, which is the CSW of the TTY device. If R2 is not equal to this value, in other words the code is configured to read from the paper tape reader, control branches to address 700.

000674 005067 CLR 177560

If the value in R2 equals 177560, this code is executed, which clears the value in 177560. This will disable interrupts from the TTY device.

000700 105212 INCB (R2)

Next the byte value at address R2 is incremented. The previous instruction was skipped in the case of the paper tape reader, but execution continues from here in any case. This will enable the device whose CSW is contained in R2.

000702 005267 INC 13710

Next the value in address 13710 is incremented. I'm not sure what this is used for yet.

000706 005712 TST (R2)
000710 100502 BMI 1116
000712 105712 TSTB (R2)
000714 100372 BPL 702

This is the code that loops waiting for data to become available from the input device.

Firstly, the value at the CSW stored in R2 is tested. If this value is negative (i.e. bit 15 is set) then the code jumps to address 1116. Bit 15 of the receive CSW of the TTY is not used so this test will never succeed in the case of the TTY. However, bit 15 of the paper tape reader CSW is used to represent an error condition. Therefore, this branch to 1116 will only take place in the case of an error on the paper tape reader.


Presuming such an error has not occured, or is not relevent (as in the case of the TTY device), then the low byte of the CSW stored in R2 is tested. If it is positive (i.e. bit 7 is not set) then we branch back to 702 and loop, waiting for input. Bit 7 represents a DONE condition, indicating that there is a byte ready to be read in the input buffer.

000716 016202 MOV 2(R2), R2
000722 001752 BEQ 650

Next a value is moved from the value of CSW+2, which is the input buffer address, into R2. If the value read in equals zero, the code branches back up to the top and reads in another byte. Otherwise, the code continues below.

000724 042702 BIC #177600, R2

All of the high bits of the register R2 are set to zero, except for the lowest seven bits. This is achieved with the bit clear (BIC) instruction that clears all bits in the second operand corresponding to bits that are set in the first operand. 177600 is octal is, in binary, 1111 1111 1000 0000, meaning that all of the bits are cleared except the lowest seven.


What follows next are a series of tests that handle certain keypresses in particular ways. I'll discuss not only the test in each case, but also what is done if the test passes.

000730 120227 CMPB R2, #20
000734 001457 BEQ 1074

;code for handling CTRL-P
001074 012767 MOV #100, 177560
001102 005046 CLR -(SP)
001104 012746 MOV #3056, -(SP)
001110 010246 MOV R2, -(SP)
001112 000423 BR 1162

The first test compares the value in R2 to 20. This value in ASCII represents DLE (Data Link Escape) which means Ctrl-P. If a Ctrl-P was pressed, execution jumps to 1074, which is also shown above.


If Ctrl-P is pressed, firstly the value 100 is moved to register 177560 (the receive CSW for the TTY). This will enable interrupts. Next the stack pointer is decremented and the value is cleared. This pushes a zero word onto the stack. Then the value 3056 is pushed onto the stack. Finally, the value in R2 is moved into the stack and the code branches to 1162. The code at 1162 displays "^P" on the screen, pops R2 off the stack and returns using the RTI command. RTI will firstly pop the program counter off the stack, which will be loaded with the value 3056. Then the zero word will be loaded off the stack into the PSW.

;code for handling CR (Carriage Return)
000736 122702 CMPB #15, R2
000742 001011 BNE 766
000744 104402 TRAP 2
000746 012702 MOV #12, R2
000752 000417 BR 1012

The next piece of code checks whether the value in R2 equals 15, which is CR (Carriage Return). If it is not equal, execution jumps to 766. Otherwise, in the case where a CR has been entered, the remainder of this block of code is executed.


Firstly a TRAP 2 is generated, which echoes a CR-LF combination to the user. Next the CR is replaced with a LF in R2 and control jumps to address 1012.

000754 005067 CLR 177560
000760 012702 MOV #177560, R2
000764 000746 BR 702

These next three instructions are slightly out of place in the code flow. They are executed when the test right at the beginning of the TRAP 100 code (TST 13674) determines that 13674 has a zero value, meaning that the default input device (i.e. the TTY should be used).


In that case the address 17756o, the receive CSW of the TTY device is cleared, which will disable interrupts. Then the value 177560 is moved into R2 and code branches back up to the top of the code, to location 702, which is the beginning of the loop to wait for a byte of data to become available.

000766 122702 CMPB #177, R2
000772 001711 BEQ 616
   
;code for handling DEL
000616 005767 TST 13674
000622 001012 BNE 650
000624 022703 CMP #13540, R3
000630 103005 BCC 644
000632 012702 MOV #134, R2
000636 104400 TRAP 0
000640 005303 DEC R3
000642 000402 BR 650

The next test compares the value in R2 to 177. This value in ASCII represents DEL (Delete). If DEL was pressed, execution jumps to 616, which is also shown above.


Firstly, the value in 13674 is tested. If the memory address does not contain a zero, in other words if there is a specifically configured input device (TTY or tape reader) code will jump to address 650 which, in effect, means ignore the DEL character and read in another byte.


The value in R3 is compared to 13540. This tests whether the string that has been read is zero length. If it is, jump to 644, which will again, in effect, ignore the DEL character and read in another byte.


Otherwise, the ASCII code 134 (representing "\") is placed into R2 and a TRAP 0 is executed to echo a "\" back to the user.


R3 is decremented, removing the most recently entered character from the string buffer, and control branches to 650. This will cause the code to loop back and wait for the next input character.

000774 122702 CMPB #140, R2
001000 003445 BLE 1114
   
;code for an invalid character (e.g. lower case character)
001114 104407 TRAP 7   

The next test compares the value in R2 to 140. This is confusing because the 140 is first and the register is second. What this basically tests is whether the value in R2 is greater than 140. If R2 is greater than 140, then control branches to address 1114.


In effect, what this does is test whether the user entered a lower case character (represented, generally speaking, by ASCII codes greater than 140). If lower case characters are entered these are errors, so an uneven trap (representing an error condition) is generated.

001002 122702 CMPB #25, R2
001006 001422 BEQ 1054
   
; Code for displaying a NAK
001054 112702 MOVB #136, R2
001060 104400 TRAP 0
001062 112702 MOVB #125, R2
001066 104400 TRAP 0
001070 104402 TRAP 2
001072 000664 BR 644

The next test compares the value in R2 to 25. This value in ASCII represents NAK. If NAK was received, execution jumps to 1054, which is also shown above.


When a NAK is pressed, firstly 136 (ASCII "^") is moved into R2 and TRAP 0 is invoked to echo this character to the screen. Next 125 (ASCII "U") is moved into R2 and TRAP 0 is invoked again to echo this character to the screen. Finally a TRAP 2 is invoked to echo a CR-LF pair to the screen, and execution loops back to address 644 to read in the next character.

001010 104400 TRAP 0

Once all of the special cases have been dealt with, this code now handled the "default" case, of all other possible characters. Firstly, a TRAP 0 is invoked to echo the character back to the user.

001012 110223 MOVB R2, (R3)+

Next, the value that has been read in is added to the string that is being built. The value of the memory address in R3 points to the next character location in the string buffer. After adding R2 to the string, R3 is incremented, ready to accept the next character.

001014 120227 CMPB R2, #12
001020 001404 BEQ 1032

This code tests whether the the user entered an LF (Line Feed) or a CR (which is converted into an LF character by the code earlier). If yes, the code branches to 1032.

001022 020327 CMP R3, #13657
001026 103710 BCS 650
001030 104431 TRAP 31

If the user didn't enter an LF, the value of R3 is compared to the value 13657. This is a test for the max length of string. If the value held in R3 is less than 13657 the code will branch back to 650, allowing the reading of another byte, otherwise an error condition, TRAP 31, will be generated.

001032 012701 MOV #13540, R1
   ; Move the string buffer address into R1
001036 012767 MOV #1, 13700
   ; set 13700 to 1 (allows reading from paper tape, prevented when 13700 is zero)
001044 052767 BIS #100, 177560
   ; Mask bit 6 of TTY read status register
   ; bit 6 is the RCRV INT ENB bit.
   ; When set, allows an interrupt sequence to start when RCVR DONE (bit 7) sets.
   ; i.e. this will enable TTY interrupts
001052 000207 RTS PC

Final arrangements to return from the TRAP are being made by this code. Firstly, the address of the string buffer, 13540, is moved into R1.


Next, the value 1 is moved into 13700, which will allow writing to paper tape, prevented when 13700 is set to zero. Then bit 6 of 177560 is set, this will enable TTY interrupts when there is a byte ready to be read from the TTY.


Finally, control returns from the subroutine.

001116 005067 CLR 13674
001122 012767 MOV #1, 13700
001130 005726 TST (SP)+
001132 000167 JMP 3112

These last few instructions handle the situation where there is a paper tape read error.


Firstly the value 13674 is cleared. A non-zero value of this address indicates the selection of a non-default input device. Setting this to zero presumably will prevent the tape reader being used again.


Next, the value 1 is moved into 13700, which will allow writing to paper tape, prevented when 13700 is set to zero.


The PSW flags are then set based on the current value on the top of the stack, which is then popped off the stack and control jumps to address 3112.


TRAP 72

The TRAP 72 handler is very simple. It just gets the next non-whitespace character in the string R1 and puts it in R2.

001224 112102 MOVB (R1)+, R2
001226 122702 CMPB #40, R2
   ; Compare R2 to the ascii code for space
001232 001774 BEQ 1224
001234 000207 RTS PC

It's only a few lines but worth talking through line-by-line for completeness.

001224 112102 MOVB (R1)+, R2

First move the byte pointed to by the address in R1 into R2, then increment the address in R1.

001226 122702 CMPB #40, R2

Compare the value in R2 to 40 (the ascii code for space).

001232 001774 BEQ 1224

If it's equal, jump back to 1224 and get the next character.

001234 000207 RTS PC

Otherwise return from the subroutine. That's it!


TRAP 152

The last TRAP I want to talk about in this post is TRAP 152. Before using TRAP 152, R0 should be pre-loaded with the address of a string to be displayed.


; TRAP 152 handling
; Return state:
; R2 contains the character that the user typed
; R3 is positive (1) if the user pressed "N"
; R3 is negative (-1) if the user pressed "Y"
; otherwise R3 is zero
017012 104466 TRAP 66
017014 104500 TRAP 100
017016 104472 TRAP 72
017020 005003 CLR R3
017022 120227 CMPB R2, #131
   ; Compare R2 to the ascii code for "Y"
017026 001002 BNE 17034
017030 005303 DEC R3
017032 000207 RTS PC
017034 120227 CMPB R2, #116
   ; Compare R2 to the ascii code for "N"
017040 001374 BNE 17032
017042 005203 INC R3
017044 000207 RTS PC

Let's take a look at how this works.

017012 104466 TRAP 66

Firstly, TRAP 66 is invoked to display the string pointed to by the address contained in R0.

017014 104500 TRAP 100

Next a TRAP 100 is used to read a string into the address pointed to by R0.

017016 104472 TRAP 72
   ; gets the next non-whitespace character in the string pointed to by R0 and puts it in R2

TRAP 72 is then used to get the first non-whitespace character in the string pointed to by R0, with the result being placed in R2.

017020 005003 CLR R3

The value in R3 is cleared, so R3 now contains zero.

017022 120227 CMPB R2, #131
   ; Compare R2 to the ascii code for "Y"
017026 001002 BNE 17034
017030 005303 DEC R3
017032 000207 RTS PC

The byte value in R2 is compared to the value 131 (ASCII "Y"). If it is not equal, control branches to address 17034. If the value in R2 equals "Y" then R3 is decremented, so it will now have a value of -1. After this, control returns from the subroutine.

017034 120227 CMPB R2, #116
   ; Compare R2 to the ascii code for "N"
017040 001374 BNE 17032
017042 005203 INC R3
017044 000207 RTS PC

If the value in R2 does not equal "Y" then it is compared to the value 116 (ASCII "N"). If it is not equal control branches to address 17032. Otherwise R2 is incremented, so it will have the value 1. After this, control returns from the subroutine.


In summary, TRAP 152 does the following;

  1. Display the string starting at the memory address contained in R0.

  2. Read input from the user and store the first non-whitespace character in R2.

  3. If the user enters "Y", set R3 to -1

  4. If the user enters "N", set R3 to 1

  5. If the user enters anything else, set R3 to zero.

Summary

In conclusion, these are the main input routines that I have identified so far.

26 views0 comments

Recent Posts

See All

Reverse engineering PDP-11 BASIC: Part 26

This short post describes TRAP 116 and the BASIC RUN, STOP, END and RANDOMIZE commands. For context and a list of other posts on this topic, see the PDP-11 BASIC reverse engineering project page. TRAP

Reverse engineering PDP-11 BASIC: Part 25

This post describes the BASIC FOR and NEXT loop commands. For context and a list of other posts on this topic, see the PDP-11 BASIC reverse engineering project page. Introduction Loops are implemented

Reverse engineering PDP-11 BASIC: Part 24

This post describes the BASIC READ, INPUT and RESTORE commands. For context and a list of other posts on this topic, see the PDP-11 BASIC reverse engineering project page. Introduction The INPUT comma