Search
  • daveor

Reverse engineering PDP-11 BASIC: Part 15

This post will examine some of the floating point operations. We'll be looking at TRAP 24 (negate a floating point number), TRAP 20 (add two floating point numbers) and TRAP 22 (subtract two floating point numbers).


For context and a list of other posts on this topic, see the PDP-11 BASIC reverse engineering project page.


TRAP 24 (NEGF)

TRAP 24 is used to negate a floating point number. A floating point number pointed to by the memory address in R1 is negated and the result is stored in the memory address pointed to by R0.


Here's the code:

012500 012102 MOV (R1)+, R2
012502 012103 MOV (R1)+, R3
012504 005403 NEG R3
012506 005402 NEG R2
012510 005603 SBC R3
012512 010220 MOV R2, (R0)+
012514 010320 MOV R3, (R0)+
012516 012120 MOV (R1)+, (R0)+
012520 000207 RTS PC

It's quite a simple TRAP, but let's see how it works:

012500 012102 MOV (R1)+, R2
012502 012103 MOV (R1)+, R3

Firstly the significand of the floating point number (the first two words) are moved into R2 (low) and R3 (high).

012504 005403 NEG R3
012506 005402 NEG R2
012510 005603 SBC R3

The value in R3 is negated. Then the value in R2 is negated and if the negation of R2 causes a carry, the carry is subtracted from R3.

012512 010220 MOV R2, (R0)+
012514 010320 MOV R3, (R0)+
012516 012120 MOV (R1)+, (R0)+
012520 000207 RTS PC

Finally, the two words of the significand are moved into consecutive memory addresses starting at the location in R0. The exponent is copied directly from the original floating point number (pointed to by R1) into the result (pointed to by R0). Control then returns to the calling code.


TRAP 20 (ADDF)

TRAP 20 is used to add two floating point numbers together. The two numbers to be added are pointed to by the memory locations specified in R0 and R1. The result is stored at the memory locations pointed to by R0.


For background, the general approach to addition of floating point numbers is:

  1. Adjust the smaller number to match the exponent of the bigger number (unless the exponents are already equal!).

  2. Add the two significands together.

  3. Normalise the number (which means, for example turning 123.4E5 into 1.234E7).

Here's the code:

012216 010546 MOV R5, -(SP)
012220 010046 MOV R0, -(SP)
012222 012146 MOV (R1)+, -(SP)
012224 012146 MOV (R1)+, -(SP)
012226 011146 MOV (R1), -(SP)
012230 012002 MOV (R0)+, R2
012232 012001 MOV (R0)+, R1
012234 011000 MOV (R0), R0
012236 020016 CMP R0, (SP) 
012240 101412 BLOS 12266
012242 010604 MOV SP, R4
012244 010003 MOV R0, R3
012246 011400 MOV (R4), R0
012250 010324 MOV R3, (R4)+
012252 010103 MOV R1, R3
012254 011401 MOV (R4), R1
012256 010324 MOV R3, (R4)+
012260 010203 MOV R2, R3
012262 011402 MOV (R4), R2
012264 010324 MOV R3, (R4)+
012266 161600 SUB (SP), R0
012270 001431 BEQ 12354
012272 100003 BPL 12302
012274 020027 CMP R0, #177741
012300 002005 BGE 12314
012302 016605 MOV 2(SP), R5
012306 016603 MOV 4(SP), R3
012312 000434 BR 12404
012314 020027 CMP R0, #177760
012320 003007 BGT 12340
012322 062700 ADD #20, R0
012326 010102 MOV R1, R2
012330 005001 CLR R1
012332 005702 TST R2
012334 100001 BPL 12340
012336 005101 COM R1
012340 005700 TST R0
012342 001404 BEQ 12354
012344 006201 ASR R1
012346 006002 ROR R2
012350 005200 INC R0
012352 001374 BNE 12344
012354 016605 MOV 2(SP), R5
012360 016603 MOV 4(SP), R3
012364 060203 ADD R2, R3
012366 005505 ADC R5
012370 102421 BVS 12434
012372 060105 ADD R1, R5
012374 102003 BVC 12404
012376 006005 ROR R5
012400 006003 ROR R3
012402 005216 INC (SP)
012404 016600 MOV 6(SP), R0
012410 010001 MOV R0, R1
012412 010320 MOV R3, (R0)+
012414 010520 MOV R5, (R0)+
012416 011620 MOV (SP), (R0)+
012420 010100 MOV R1, R0
012422 062706 ADD #10, SP
012426 012605 MOV (SP)+, R5
012430 000167 JMP 13202
012434 060105 ADD R1, R5
012436 103762 BCS 12404
012440 000756 BR 12376

Let's figure out how this works.

012216 010546 MOV R5, -(SP)
012220 010046 MOV R0, -(SP)

First R5 and R0 are pushed onto the stack.

012222 012146 MOV (R1)+, -(SP)
012224 012146 MOV (R1)+, -(SP)
012226 011146 MOV (R1), -(SP)

Then, the first floating point number is pushed onto the stack.

012230 012002 MOV (R0)+, R2
012232 012001 MOV (R0)+, R1
012234 011000 MOV (R0), R0

The second floating point number is copied into R2, R1 and R0.

012236 020016 CMP R0, (SP) 
012240 101412 BLOS 12266

The exponents of the two floating point numbers are compared (unsigned). The branch will occur if R0 is equal to or lower than (SP). Otherwise, R0 is greater than (SP), in which case the two floating point numbers are swapped so that the smallest number is in R0:

012242 010604 MOV SP, R4
012244 010003 MOV R0, R3
012246 011400 MOV (R4), R0
012250 010324 MOV R3, (R4)+
012252 010103 MOV R1, R3
012254 011401 MOV (R4), R1
012256 010324 MOV R3, (R4)+
012260 010203 MOV R2, R3
012262 011402 MOV (R4), R2
012264 010324 MOV R3, (R4)+

The stack pointer is backed up into R4.


R0 (the exponent from one float) is moved into R3. The value from the memory location pointed to by R4 (the exponent of the other float) is stored in R0. R3, the exponent from the first float, is then moved to the location pointed to by R4 and then R4 is incremented.


R1 (the second word of the significand of one float) is moved into R3. The value from the memory location pointed to by R4 (the second word of the significand of the other float) is stored in R1. R3, the second word of the significand from the first float, is then moved to the location pointed to by R4 and then R4 is incremented.


R2 (the first word of the significand of one float) is moved into R3. The value from the memory location pointed to by R4 (the first word of the significand of the other float) is stored in R2. R3, the first word of the significand from the first float, is then moved to the location pointed to by R4 and then R4 is incremented.


After this code, the floating point number with the biggest exponent is on the stack, and the floating point number with the smallest exponent is in R2, R1 and R0. In other words, R0 is the one with the lower of the two exponent values.

012266 161600 SUB (SP), R0
012270 001431 BEQ 12354
012272 100003 BPL 12302

The exponent value on the stack is now subtracted from R0. If, when subtracted, the result is zero, that means that the two floating point numbers had the same exponent. In this case, control jumps to 12354.


If, when subtracted, the result is positive, control jumps to 12302. I don't understand how this situation can arise. We have specifically arranged the numbers such that we are subtracting a larger number from a smaller number, which will lead to a negative result or zero (if both values are equal).

012274 020027 CMP R0, #177741
012300 002005 BGE 12314

Otherwise, the result is negative, in which case it is compared to the value 177741 (which is -31 in decimal). Since this is a signed comparison, any value that is less negative that this will be greater than 177741, in which case control jumps to 12314.

012302 016605 MOV 2(SP), R5
012306 016603 MOV 4(SP), R3
012312 000434 BR 12404

This code is executed if the subtraction of the exponents results in a positive value, or a value more negative than -31. It seems that in these cases, the larger of the two floating point numbers (stored on the stack) is returned from the TRAP. This is achieved by moving the two words of the significand into R5 and R3 and then branching to 12404, which is the code that assembles the return value before the normalisation step takes place.

012314 020027 CMP R0, #177760
012320 003007 BGT 12340
012322 062700 ADD #20, R0
012326 010102 MOV R1, R2
012330 005001 CLR R1
012332 005702 TST R2
012334 100001 BPL 12340
012336 005101 COM R1

So, we now know that we have a difference between significands that is in the valid range (between -1 and -30 in decimal). We next begin the process of adjusting the significand of the smaller floating point number to match the significand of the larger number. This is achieved by rotating the significand to the right by the number of bits calculated from the difference between the exponents of the two numbers (i.e. the value contained in R0).


First the value in R0 is compared to 177760 (-16). If the value is greater (i.e. less negative) than -16 we jump to 12340. Otherwise, the remainder of the block of code is executed.


We begin by adding 20 (decimal 16) to R0 and then moving R1 into R2, which has the effect of shifting R1 to the right by 16 bits. R1 is then cleared. The value in R2 is tested and if it is negative then R1 is complemented (i.e. set to all 1's).


In summary, therefore, this code tests whether the significand needs to be shifted right by more than 16 bits and if so performs a word swap to achieve 16 bit shift with one set of instructions. If there are any remaining bit shifts required, they are handled by the next piece of code, that handles the situation where there is a shift of less than 16 bits required.

012340 005700 TST R0
012342 001404 BEQ 12354
012344 006201 ASR R1
012346 006002 ROR R2
012350 005200 INC R0
012352 001374 BNE 12344

First the value in R0 is tested and if it is equal to zero that means no more bit shifting is required, so we jump to 12354. Otherwise we shift R1 to the right. The lowest bit of R1 is shifted into the carry flag. Then, R2 is rotated right and the carry flag (i.e. the lowest bit from R1) will be shifted into the uppermost bit position in R2.


R0 is incremented to reduce the number of bit shifts remaining and if it is not equal to zero control jumps to 12344 to shift R1 and R2 right by another bit. This will continue until R0 equals zero, at which point both numbers are configured with the same exponent.


Once that is done, we can move on to the code that actually performs the addition.

012354 016605 MOV 2(SP), R5
012360 016603 MOV 4(SP), R3

The significand of the floating point number is moved from the stack into R5 and R3. The corresponding words of the other floating point number are stored in R1 and R2 respectively.

012364 060203 ADD R2, R3
012366 005505 ADC R5
012370 102421 BVS 12434

R2 is added to R3. If the addition causes the carry flag to be set, this is added to R5. It is possible that the addition of the carry flag to R5 may cause an overflow, in which case control jumps to 12434. The overflow bit will be set by the ADC instruction if the value in R5 was 077777 and the carry flag was 1. This addition will cause overflow into negative numbers.


Let's jump ahead to 12434 and see how that situation is handled:

012434 060105 ADD R1, R5
012436 103762 BCS 12404
012440 000756 BR 12376

The value in R1 is added to the value in R5. If the carry flag is set, which will occur if there is a carry from the most significant bit, control will jump to 12404, otherwise control will jump to 12376.

012372 060105 ADD R1, R5
012374 102003 BVC 12404
012376 006005 ROR R5
012400 006003 ROR R3
012402 005216 INC (SP)

Otherwise (i.e. the original addition of R2 to R3 and adding the carry to R5) did not cause an overflow, in which case R1 is added to R5 and if there is no overflow, control jumps to 12404. If there was an overflow, then the value in R5 is rotated right (lowest bit will be rotated into the carry flag) and R3 is rotated right (carry flag will be rotated into the highest bit position). The exponent, stored at the top of the stack, is also incremented.


The addition is now complete and we prepare to move on to the next step, which is normalising the result.

012404 016600 MOV 6(SP), R0
012410 010001 MOV R0, R1
012412 010320 MOV R3, (R0)+
012414 010520 MOV R5, (R0)+
012416 011620 MOV (SP), (R0)+
012420 010100 MOV R1, R0
012422 062706 ADD #10, SP
012426 012605 MOV (SP)+, R5
012430 000167 JMP 13202

First the destination for the result is copied from the stack (at SP+6) into R0, and then R0 is also copied to R1, so both R0 and R1 will contain the location where the result should be stored.


R3 is copied to the memory location at R0, and R0 is incremented. R5 is then copied to the location at R0, and R0 is incremented again. Finally, the exponent is copied from the top of the stack into R0, and R0 is incremented again. R1, the location of the result is once again copied into R0.


Ten (octal) is added to the stack pointer, which effectively pops four words off the top of the stack and discards them, and then R5 is restored from the stack.


Finally, control jumps to 13202, which is part of TRAP 36. I described this code in detail in Part 14 of this series. Briefly, this is the code that will strip leading zeros or ones from the significand and adjust the exponent accordingly. This has the effect of normalising the number and if you are interested in understanding exactly how it is done, you can find the walkthrough in the previous post. The TRAP 36 code works on a floating point number stored at the memory address in register R1, and that is why the result memory location is assigned to both R0 and R1 above.


TRAP 22 (SUBF)

Having discussed the negation and addition of floating point numbers, we now have all the tools we need to implement subtraction because subtraction is the addition of the negation of a number. Put another way, A - B is the same as A + (-B). This means that the subtraction code is relatively simple, because all of the heavy lifting is done by the addition code above.


TRAP 22 subtracts the floating point value pointed to by R1 from the floating point value pointed to by R0.


Here's the code:

012442 010004 MOV R0, R4
012444 162706 SUB #6, SP
012450 010600 MOV SP, R0
012452 104424 TRAP 24
012454 010400 MOV R4, R0
012456 010601 MOV SP, R1
012460 104420 TRAP 20
012462 062706 ADD #6, SP
012466 000207 RTS PC

Let's see how this works.

012442 010004 MOV R0, R4
012444 162706 SUB #6, SP
012450 010600 MOV SP, R0
012452 104424 TRAP 24

Firstly, the location of the floating point number in R0 is backed up to R4. Then, six is subtracted from the stack pointer, allocating three words on the stack. The location of the stack pointer is then moved into R0, so R0 now points at three words on the stack.


TRAP 24 is then used to negate a floating point number. As discussed above, TRAP 24 negates a floating point number pointed to by the memory address in R1 and the result is stored in the memory address pointed to by R0.


So, the negation of the value in R1 is now stored on the stack.

012454 010400 MOV R4, R0
012456 010601 MOV SP, R1
012460 104420 TRAP 20

Now, the location of the first floating point number is restored from R4 into R0 and the location of the second floating point number (i.e. the stack pointer) is moved into R1.


TRAP 20 is then used to add two floating point numbers together. The two numbers to be added are pointed to by the memory locations specified in R0 and R1. The result is stored at the memory locations pointed to by R0.

012462 062706 ADD #6, SP
012466 000207 RTS PC

Finally, six is added to the stack pointer to discard the negation of the floating point number and then control is returned.


Conclusion

We took a look in this post and the code for negating, adding and subtracting floating point numbers, with most of the heavy lifting being done by the addition code.


10 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