Loading a program from tape - Part 4: The Absolute Loader
Updated: Jan 22, 2021
Recap of where we are
Let's summarise where we are. So far we have;
loaded the bootstrap loader
run the bootstrap loader to load the absolute loader
restored the bootstrap loader so it can be used again if needed
halted waiting for the operator to change the tape
If any of that doesn't make sense, you really need to read the previous parts of this series.
Now we want to use the absolute loader to read in a program from tape. The absolute loader expects paper tape data to be formatted in a particular way, as described in the next section. After that, we'll take a detailed look at how the absolute loader works.
Absolute loader tape format
The absolute loader tape format is extremely simple. The tape is made up of blocks of data, with each block being structured in the following way:
The first byte has the value 001
The second byte has the value 000
The next two bytes are the number of bytes in the block, including the header, stored little endian. This is referred to as the byte count.
The next two bytes are the memory address to load the block into, stored little endian. This is referred to as the load address.
The remainder of the block contains data bytes, with the exception of the final byte, which is a checksum to verify the content of the block.
If the block has a byte count of 6, in other words the block contains no data, the load address is interpreted as the address to jump to in order to start the program. This address should always be even. If it is odd the absolute loader will halt.
The absolute loader
Here is the full listing of the absolute loader. I've split it into a few sections so it's a bit less intimidating.
;the halt instruction 157476 000000 ;setup the absolute loader 157500 010706 157502 024646 157504 010705 157506 062705 157510 000112 157512 005001 157514 013716 157516 177570 157520 006016 157522 103402 157524 005016 157526 000403 157530 006316 157532 001001 157534 010116 ;seek to the beginning of a block then read block header 157536 005000 157540 004715 157542 105303 157544 001374 157546 004715 157550 004767 157552 000074 157554 010402 157556 162702 157560 000004 157562 022702 157564 000002 157566 001441 157570 004767 157572 000054 157574 061604 157576 010401 ;read in the rest of the block 157600 004715 157602 002004 157604 105700 157606 001753 157610 000000 157612 000751 157614 110321 157616 000770 ;subroutine to read a byte 157620 106703 157622 000152 157624 105213 157626 105713 157630 100376 157632 116303 157634 000002 157636 060300 157640 042703 157642 177400 157644 005302 157646 000207 ;subroutine to read a word 157650 012667 157652 000046 157654 004715 157656 010304 157660 004715 157662 000303 157664 050304 157666 016707 157670 000030 ;subroutine to check jump address and jump to the loaded program 157672 004767 157674 177752 157676 004715 157700 105700 157702 001342 157704 006204 157706 103002 157710 000000 157712 000700 157714 006304 157716 061604 157720 000114 157722 000000 ;this is the code that restores the bootstrap loader ;(discussed in the previous post) 157724 012767 157726 000352 157730 000020 157732 012767 157734 000765 157736 000034 157740 000167 157742 177532 ;this is the code that overwrites the bootstrap loader ;(discussed in the previous post) 157744 016701 157746 000026 157750 012702 373 353
In that form it is still obviously completely incomprehensible but actually as we work through it you'll see that it's not too bad, because it consists of a few subroutines and when you figure it out it all makes a lot of sense.
The HALT instruction
The first part of the absolute loader is the halt instruction:
;the halt instruction 157476 000000
After the bootstrap loader has been restored, control jumps to this instruction and the CPU is halted. The reason for this was that the operator of the PDP-11 would need to remove the absolute loader paper tape from the paper tape reader and put in the paper tape containing the program that was to be loaded.
When the paper tape had been changed over, the operator would press continue on the panel and that would step forward to the next instruction which began to execute the absolute loader and read in the tape.
Setting up the absolute loader
The next section of the code sets up the absolute loader.
;setup the absolute loader 157500 010706 157502 024646 157504 010705 157506 062705 157510 000112 157512 005001 157514 013716 157516 177570 157520 006016 157522 103402 157524 005016 157526 000403 157530 006316 157532 001001 157534 010116
Let's walk through this instruction-by-instruction.
157500 010706 157502 024646
The absolute loader uses subroutines, which requires storing return addresses on the stack. These first two instructions set up the stack pointer.
The first instruction is a MOV (octal "01"). The source operand is register 7, the program counter (octal "07") and the destination operand is register 6, the stack pointer (octal "06").
This assigns the stack pointer to be equal to the value of the program counter. Remember that the instruction is read and the progam counter is incremented before the instruction is executed. Therefore, at the time that this instruction is being executed, the value of the program counter is 157502.
There are two instructions in the program that precede this memory location; the MOV instruction just executed and the HALT instruction. Recall from my primer on the use of the stack pointer, that the stack pointer is decremented before it is used so we want the stack pointer to initially point to the memory address of the first instruction of the program 157476.
Therefore, we need to decrement the stack pointer twice to point at the address of the HALT instruction (157476). The second instruction (at address 157502) performs a very neat trick of decrementing the stack pointer twice with a single instruction.
The instruction is "CMP -(SP), -(SP)". This is a compare instruction (octal "02") but we don't care about the result of the compare, all we care about is the side-effect of gathering the two operands. The source operand (octal "46") means decrement by one word and then read the stack pointer. The destination is also octal 46, meaning decrement by one word and then read the stack pointer again. These two values will now be compared, but that doesn't matter. The upshot is that the stack point has been decremented twice and now has the value 157476. This was achieved with a single instruction, which is pretty cool.
157504 010705 157506 062705 157510 000112
The absolute loader's execution involves quite a lot of reading bytes from tape, so these next instructions set up the register R5 with the memory address of the read byte subroutine.
The first instruction MOVs (octal "01") the value from the source operand (octal "07"), which is the program counter (register 7) to the destination operand (octal "05"), which is register R5. When this instruction is executed the value 157506 will be moved into R5.
The next two words ADD (octal "06") the value of the source operand (octal "27"), which is the immediate value in the subsequent word (i.e. octal 112) to the destination operand (octal "05"), which is register R5. When this instruction completes, the value in R5 will be 157620, which is the address of the subroutine that loads a byte from paper tape. I'll describe how this subroutine works below.
This instruction clears the value of register R1.
The absolute loader will load blocks of instructions from the paper tape into locations specified by the load address in the header of each block. However, the operator can specify with the panel switches an offset address, or base address, to be added to the load address specified in each block. The remainder of the setup code is concerned with configuring this offset address. Ultimately, the offset address calculated by the code is stored in the memory address pointed to by the stack pointer.
157514 013716 157516 177570
Firstly, the value of the Control Switches and Display word is read and stored in the address pointed to by the stack pointer. This is achieved with a MOV instruction (octal "01") from the absolute address 177570 (the memory address of the Control Switches and Display word) into the address stored in R6, the stack pointer.
157520 006016 157522 103402
The next instruction rotates the content of the memory address pointed to by SP to the right (octal "0060" represents the ROR instruction). The lowest bit of the memory address pointed to by SP will be stored in the carry bit.
Since the offset address must be even, the lowest bit cannot form part of the address and it is therefore used as a flag to indicate whether or not to use an offset address. If the lowest bit is not set, then the offset address is not used. If the lowest bit is set, the offset address is used.
The second instruction tests the status of the carry bit (using a BCS instruction, represented by octal "1034") and if the carry bit was set it jumps ahead to address PC+2. If the carry bit is not set the branch does nothing.
157524 005016 157526 000403
If the carry bit is not set, we do not use an address offset and these two instructions are executed.
First we clear the value in the memory address pointed to by SP (octal "0050" represents CLR) and then branch ahead by three instructions to address 157536, which is the beginning of the instructions to read a block from tape.
157530 006316 157532 001001 157534 010116
Otherwise, the carry bit was set, in which case we are using an address offset and these three instructions are executed.
Remember we rotated the value pointed to by SP to the right to test whether the low bit was set? The first thing we need to do is restore the address offset, and this is done with an arithmetic shift left, or ASL instruction (represented by octal "0063"). This is performed on the content of the memory location pointed to by SP. A zero bit will be moved into the lowest order bit, ensuring that the address offset value is even.
Next there is a branch to determine whether the resulting value is zero. If the resulting value is not zero, control jumps ahead by one instruction (to address 157536) to the code that will begin reading a block from the tape. This is achieved with the BNE instruction, represented by octal 001.
Finally, in case the value in the memory address pointed to by SP is zero, the value of R1 is moved into this address and execution continues through to the code that reads a block from tape. The only situation I can find where this code may be executed is where a jump block has been read but it has an odd address in it. In that case the code halts and if the operator presses continue then control branches back up to this setup code again, to the instruction that reads the Control Switch and Display word again.
In that situation R1 will contain the next address into which a byte read from tape should have been loaded, so it seems that this branch is used to continue loading when an invalid jump block has been read in. I suspect this technique may have been used in situations where a single program was being loaded from multiple paper tapes.
The instructions to read a block from tape
The next section of the absolute loader reads a block from tape. The section is really the core of the program.
;seek to the beginning of a block then read block header 157536 005000 157540 004715 157542 105303 157544 001374 157546 004715 157550 004767 157552 000074 157554 010402 157556 162702 157560 000004 157562 022702 157564 000002 157566 001441 157570 004767 157572 000054 157574 061604 157576 010401 ;read in the rest of the block 157600 004715 157602 002004 157604 105700 157606 001753 157610 000000 157612 000751 157614 110321 157616 000770
The first instruction of this section clears the value in register R0:
Register R0 is used to hold the running checksum as the block is being read in.
157540 004715 157542 105303 157544 001374
It is possible that there may be leader bytes on the tape, for example some zero bytes before the first block starts. Therefore, these instructions reads bytes from the tape and loop until a byte containing 001 is identified, which is the first byte of the block header.
The first instruction reads a byte from the tape. This is performed by performing a JMP (octal "004") to the address contained in the register R5 (represented by octal "15), while storing the return address in register 7 (octal "7"). As mentioned earlier, the register R5 contains the address of the read byte subroutine. The resulting byte is stored in register R3.
The next instruction decrements the byte (octal "1053") contained in register R3. If the value in R3 was 1, then this decrement will result in a zero value in R3. For any other value, the result of the decrement will be non-zero.
The final instruction branches if the result of the decrement was not zero to address 157536 to repeat the process and read in another byte. When the value returned from the read was 1, the code continues:
This is another JMP PC, (R5) instruction, which reads another byte from the tape. This is the second byte of the block header, which is always zero, so this byte is read and then ignored.
017550 004767 017552 000074
The next two words are a another JMP instruction, storing the return address in register 7, as represented by octal 0047. The jump address is given as an offset of 74 from the current value of the program counter. This jumps to the subroutine to read and assemble a word from the paper tape. The result is stored in register R4.
This instruction moves the result reading the word, contained in R4, into R2.
157556 162702 157560 000004
The next instruction subtracts 4 from the value held in register R2. The SUB instruction is represented by the octal 16. The value to subtract is found in the next word, as reflected by bhe source operand of 27. This value is to be subtracted from the destination operand, which is the value in register R2.
The four bytes being subtracted from the byte count represent the first four bytes of the block; the 001 byte, the 000 byte and the word containing the byte count.
157562 022702 157564 000002
The next instruction checks whether the remaining byte count, as contained in R2, is 2.
The octal value "02" is the CMP instruction. The source operand is found in the next word, as reflected by the source operand of 27. This is compared to the destination operand, register R2.
The next instruction is a BEQ, branching if the CMP instruction determines that the two operands are equal (strictly speaking, if the difference between the two operands equals zero). If the operands are equal, this means that the block has a size of six and therefore that the load address represents the jump address to start the application.
If the block has a size of six, the code branches to PC+41. This jumps to the subroutine to jump to the code just read from the tape.
157570 004767 157572 000054
The next two words are a another JMP instruction, storing the return address in register 7, as represented by octal 0047. The jump address is given as an offset of 54 from the current value of the program counter. This jumps to the subroutine to read and assemble a word from the paper tape. The result is stored in register R4.
Next, any relocation offset from the setup phase of the absolute loader is added to the value in register R4. The ADD instruction is represented by the octal value "06". The source operand is represented by the value "16".
The "1" means register deferred addressing and the "6" means register R6. Register deferred addressing means that R6 does not contain the operand, rather it contains the memory address which contains the operand. R6 is, of course, the stack pointer. Therefore, the source operand is the value pointed to by the stack pointer.
The destination operand is represented by the value "04". The "0" means register addressing and the "4" means register R4. Register addressing means that the operand is the register itself, in this case R4.
This instruction moves the value from register R4 into register R1. So now, R1 contains the starting destination address for the bytes to be read from the remainder of the block.
This is another JMP PC, (R5) instruction, which reads another byte from the tape. The resulting byte is stored in register R3.
The last instruction of the subroutine that reads a byte from paper tape is to decrement R2, which contains the number of bytes remaining to be read in the block. This instruction tests whether the value in R2 has been decremented to zero. If it has not, in other words there are more bytes remaining, then code jumps forward to PC+4 (i.e. to address 157614).
Otherwise there are no bytes remaining, so the code moves on to validation of the block checksum.
157604 105700 157606 001753
The first of these two instructions tests the value in R0 with a TSTB instruction (represented by octal "1057"). This will set the flags in the PSW based on the value found in R0.
The second instruction is a BEQ instruction (given by an octal base value of "0014"), plus an offset. The offset in this case is 353, which is the 2's complement representation of -25 octal.
Together this means if the value in R0 is zero, which indicates that the checksum for the block was valid, then the code loops back to address 157536, which is the address of the beginning of the set of instructions to read a block from. This will begin reading in the next block from paper tape.
This is the instruction that actually stores the byte read from paper tape into its destination address in memory.
This instruction moves the byte contained in register R3 to the address specified in R1. The value in R1 will be auto-incremented after the value of R3 has been stored.
Note that this is a byte instruction (MOVB, given by octal "11") as opposed to a word instruction (MOV, which would be given by octal "01"). Therefore when R1 is being auto-incremented, it will be incremented by 1 representing one byte increase as opposed to 2 in the case of a MOV instruction, which would represent 1 word increase.
This auto-increment means that the register R1 now contains the value into which the next byte is to be stored when it is read from paper tape.
The final instruction in this section, having stored the byte at the relevant location in memory, branches back to read in another byte.
This is an unconditional branch instruction, given by the base octal value of "000400". The offset is 370, which is the 2's complement representation of the negative value -10 in octal. This will therefore loop back 10 word instructions to address 157600 to read and process another byte.
The remaining sections of the absolute loader are a set of supporting routines that help with different aspects of the code just described.
The instructions to read a byte from tape
This block of instructions is the subroutine to read a byte from the paper tape. Here it is:
;subroutine to read a byte 157620 106703 157622 000152 157624 105213 157626 105713 157630 100376 157632 116303 157634 000002 157636 060300 157640 042703 157642 177400 157644 005302 157646 000207
This code is quite similar to the bootstrap loader, so it should make a lot of sense as we go through it.
157620 016703 157622 000152
This instruction moves the address of the CSW of the paper tape reader into R3. The MOV instruction is represented by octal "01". The source operand, represented by octal 67, is the value at an address specified by the offset in the next byte (octal 152) added to the program counter, giving an address of 157776, which is the final address of the original bootstrap loader. This value is moved into R3.
This instruction increments the byte at the address pointed to by the value in R3. In other words, it sets the low bit of the CSW, which enables the paper tape reader.
157626 105713 157630 100376
These next two instructions loop until the paper tape reader indicates that there is a byte ready by setting the DONE bit.
The first instruction tests the low byte of the address contained in R3 using the TSTB instruction. The second instruction branches back to the TSTB instruction if the value is positive using a BPL instruction.
The DONE bit is bit 7 of the low byte of the CSW. When this value is set, this represents a negative value, so when the DONE bit is set, the value will not be positive and control will pass through the branch.
157632 116303 157634 000002
When the paper tape reader sets the DONE bit, these next instructions read the byte value from the paper tape reader buffer register (CSW + 2) and store it in register R3.
The value just read in and stored in R3 is added to R0 by this instruction. R0 is used to accumulate the checksum by adding each byte read to the running value in R0.
157640 042703 157642 177400
This next instruction clears all of the bits of R0 except those in the lowest 8 bits, representing the byte just read in from the paper tape reader.
This is achieved through the use of the BIC, or bit clear instruction with the mask value of 177400 (which in binary is 1111 1111 0000 0000).
Register R2, which represents the number of bytes remaining to be read in the current block, is decremented.
The last instruction returns from the subroutine, restoring the PC from the value stored on the stack.
The instructions to assemble a word
The next subroutine reads in two bytes and assembles them into a word. Here it is:
;subroutine to read a word 157650 012667 157652 000046 157654 004715 157656 010304 157660 004715 157662 000303 157664 050304 157666 016707 157670 000030
The first instruction saves the return address to a temporary location:
157650 012667 157652 000046
When a subroutine is invoked using JSR, the stack pointer will be decremented and the current program counter will be pushed onto the stack. Therefore, the stack pointer points at the return address, which is the value of the program counter to be returned to when this subroutine is finished.
This instruction (which is "MOV (SP)+ @46(PC)") reads the value at the stack pointer (which is the return address) and then increments the stack pointer. The value is then stored at a temporary location which is PC+46 (i.e. memory address 157722).
This is another JMP PC, (R5) instruction, which reads another byte from the tape. The resulting byte is stored in register R3. This will be the low order byte of the word that is being assembled by this subroutine.
This instruction moves the value in R3 into R4.
We then execute another JMP PC, (R5) instruction, which reads another byte from the tape. The resulting byte is stored in register R3. This will be the high order byte of the word that is being assembled by this subroutine.
This is a SWAB R3 instruction, meaning that the bytes in the register R3 will be swapped, making the low order byte the high order byte, and vice versa. This converts the byte just read, which would have been stored in the low order byte of R3 into a high order value.
This is a BIS R3, R4 instruction. This will set the bits in R4 that are set in R3. In other words, this has the effect of ORing R3 (the low order byte) and R4 (the high order byte). The resulting value is stored in R4.
157666 016707 157670 000030
Finally, this instruction moves the PC value that was stored in a temporary value at the beginning of this subroutine (located at PC+30) into the program counter. This returns from the subroutine.
The instructions to jump to the code just read in from tape
The last subroutine in the absolute loader is used to check, and then jump to, the address specified in a jump block (a block with a length of six). This will execute the code that has been read in from paper tape. The jump address must be even, otherwise the execution of the absolute loader will halt. Here is the code:
;subroutine to check jump address and jump to the loaded program 157672 004767 157674 177752 157676 004715 157700 105700 157702 001342 157704 006204 157706 103002 157710 000000 157712 000700 157714 006304 157716 061604 157720 000114 157722 000000
Recall that this code is only executed when the block loading code above determines that the block has a length of six. Four of the six bytes (the two header bytes and the byte count word) have already been read by the block loading code.
Therefore, the first thing this code does is read a word is read from the paper tape - this is the jump address.
157672 004767 157674 177752
This is achieved by JMPing to the adddress PC-26. The -26 is specified by the word value 177752, which is the 2's complement representation of that value. The result is stored in R4.
Next, one more byte is read. This is achieved using a JMP PC, (R5) instruction, which reads another byte from the tape. The resulting byte is stored in register R3. This is the checksum byte, which is not included in the byte count value.
As bytes are being read from tape, the running checksum value is stored in R0. When the checksum byte has been read in, if there are no checksum errors, the value in R0 should be zero. This instruction, therefore checks the value of R0 using a TSTB instruction. This will set the value of the flags in the PSW based on the value in R0.
If the value in R0 is non-zero this means there has been a checksum error, so this instruction is "BNE @-36PC". In other words, branch if the zero bit in the PSW is not equal to zero to address PC-36. This branches to the HALT instruction at address 157610, which represents the halt state arising from a checksum error.
157704 006204 157706 103002 157710 000000 157712 000700
At this point we have read the jump address and we have confirmed that there is not a checksum error. The next step is to test whether or not the jump address is even. If it is not, the absolute loader should halt. This is achieved by these next instructions.
The first instruction performs an ASR (octal "0062"), which means arithmetic shift right, on the value in register R4. This shifts all bits in the register one location to the right. The lowest bit in the register is moved into the carry flag.
If the number originally stored in register R4 was even, then the value in the carry flag should be zero. If the number originally stored in register R4 was odd, then the value in the carry flag should be one.
We therefore test the carry flag with the next instruction, "BCC 2(PC)". This instruction says branch if the carry bit is clear (i.e. if the value in R4 was even) to PC+2. In other words skip the next two instructions and carry on (remember PC already points at address 157710 as this instruction is executing).
If the carry flag was set, this branch will do nothing and the execution carries on to the next instruction with is HALT (octal "000000"). If the operator presses continue, control jumps back to the setup code (address 157536) by way of the branch instruction (octal "000700").
157714 006304 157716 061604 157720 000114
At this point we know that the jump address is valid because it passed the checksum test and the evenness test. Therefore all that remains is to jump to that address.
Remember, however that we shifted R4 to the right to test whether the value was odd or even. Therefore, we now need to shift R4 to the left to restore the value.
The first instruction (octal "0063") with the operand ("04") means "ASL R4", or arithmetic shift left of the value in R4. This will shift all bits in R4 one position to the left and add a zero at the lowest bit position. The adding a zero at the lowest bit position is fine because we know at this point that the jump address is even, which means a zero belongs in the lowest bit position anyway.
Next, we add the address in the location pointed to by the stack pointer to R4. This adds any address offset configured by the operator to R4. This is achieved by the octal "061604" which represents the instruction "ADD (SP), R4".
Lastly, we jump to the memory location contained in the register R4, which is achieved by the instruction "000114", or JMP ("0001") to the register deferred value in R4, in other words to the location pointed to by R4. This will begin executing the code that has been read in from the paper tape.
This last word is used as a temporary storage location by the subroutine that reads in and assembles a word. It is used to store the return address for use after the subroutine completes.
And that's it! The remaining code relates to the restoration of the bootstrap loader, and that was discussed in the previous post on loading the absolute loader.
A scanned printout of the absolute loader code from 1975, contains the assembly language equivalent of the machine code analysed in this post. If you prefer to follow along with the assembly language rather than the machine code, this would be a useful resource.