diff --git a/apps/pdp11/tapes/absloader/README.md b/apps/pdp11/tapes/absloader/README.md index b0517778d..cedbefa90 100644 --- a/apps/pdp11/tapes/absloader/README.md +++ b/apps/pdp11/tapes/absloader/README.md @@ -16,4 +16,4 @@ PCjs has archived the following DEC resources: - [Absolute Loader](DEC-11-L2PC-PO.json) - [LISTING OF PDP-11 ABSOLUTE LOADER (June 1975)](http://archive.pcjs.org/pubs/dec/pdp11/other/DEC-11-UABLA-A-LA_PDP-11AbsoluteLoaderListing_Jun75.pdf) -- "APPENDIX D: THE BOOTSTRAP AND ABSOLUTE LOADERS" from the [PDP-11 BASIC PROGRAMMING MANUAL (December 1970)](http://archive.pcjs.org/pubs/dec/pdp11/other/BASIC_Programming_Manual_Dec70.pdf) +- "APPENDIX D: THE BOOTSTRAP AND ABSOLUTE LOADERS" from the [PDP-11 BASIC PROGRAMMING MANUAL (December 1970)](http://archive.pcjs.org/pubs/dec/pdp11/basic/DEC-11-AJPB-D_PDP-11_BASIC_Programming_Manual_Dec70.pdf) diff --git a/apps/pdp11/tapes/basic/README.md b/apps/pdp11/tapes/basic/README.md index bed855f63..fb2d877e2 100644 --- a/apps/pdp11/tapes/basic/README.md +++ b/apps/pdp11/tapes/basic/README.md @@ -12,9 +12,55 @@ Try our [DEC BASIC Demo](/devices/pdp11/machine/1120/basic/). PCjs has archived the following DEC resources: - [BASIC (Single User)](DEC-11-AJPB-PB.json) -- [PDP-11 BASIC PROGRAMMING MANUAL (December 1970)](http://archive.pcjs.org/pubs/dec/pdp11/other/BASIC_Programming_Manual_Dec70.pdf) +- [PDP-11 BASIC PROGRAMMING MANUAL (December 1970)](http://archive.pcjs.org/pubs/dec/pdp11/basic/DEC-11-AJPB-D_PDP-11_BASIC_Programming_Manual_Dec70.pdf) Third-party resources include: - "[PDP-11 Paper Tape BASIC](http://www.avitech.com.au/ptb/ptb.html)", written March 24, 2013 +--- + +Debugging Notes +--------------- + +One of the first things I noticed when debugging PDP-11 BASIC was its reliance on TRAP instructions. +For example, `TRAP 000` used to output the character in R2 to the terminal. Let's take a closer look at how its +TRAP handler works. + +First, if you check the table of PDP-11 trap vectors, you'll see that the vector for TRAP instructions is 000034. +So let's dump the contents of the two-word vector at 000034: + + >> dw 034 l2 + 000034 000100 000000 + +The BASIC TRAP handler is at 000100, and here's that code: + + >> u 000100 + 000100: 011666 000002 MOV @SP,2(SP) + 000104: 162716 000002 SUB #2,@SP + 000110: 013646 MOV @(SP)+,-(SP) + 000112: 006216 ASR @SP + 000114: 103404 BCS 000126 + 000116: 006316 ASL @SP + 000120: 062716 073654 ADD #73654,@SP + 000124: 013607 MOV @(SP)+,PC + +When the code starts, the TRAP instruction has already pushed two words onto the stack: + + 0(SP): previous PC + 2(SP): previous PSW + +The first instruction, `MOV @SP,2(SP)`, copies the *previous PC* onto the *previous PSW*, which is where we'll eventually want +*previous PC*, so that the handler can eventually return with a simple `RTS PC`. + +The next instruction, `SUB #2,@SP`, subtracts 2 from the original *previous PC*, so that it now points to the TRAP instruction. + +Then `@(SP)+,-(SP)` fetches the TRAP instruction while also "popping" the original *previous PC* and then "pushing" TRAP +instruction onto the stack, overwriting the original *previous PC*. + +The next few instructions shift the TRAP right to see if bit 0 is set, and if it is not, then the TRAP instruction is restored +by shifting it left again, and then a large offset is added to it, transforming the TRAP instruction (which is now known to be +an *even* value) into a jump table index. + +The final instruction, `MOV @(SP)+,PC`, moves the address at the jump table index into PC, while also removing the TRAP +instruction from the stack, leaving only the *previous PC* on the stack. diff --git a/modules/pdp11/lib/cpuops.js b/modules/pdp11/lib/cpuops.js index 14cfb8f48..a4981ddc8 100644 --- a/modules/pdp11/lib/cpuops.js +++ b/modules/pdp11/lib/cpuops.js @@ -827,7 +827,7 @@ PDP11.opBPL = function(opCode) */ PDP11.opBPT = function(opCode) { - this.trap(PDP11.TRAP.BREAKPOINT, PDP11.REASON.BPT); + this.trap(PDP11.TRAP.BPT, PDP11.REASON.BPT); this.nStepCycles -= (4 + 1); }; @@ -1101,7 +1101,7 @@ PDP11.opDIV = function(opCode) */ PDP11.opEMT = function(opCode) { - this.trap(PDP11.TRAP.EMULATOR, PDP11.REASON.EMT); + this.trap(PDP11.TRAP.EMT, PDP11.REASON.EMT); this.nStepCycles -= (22 + 3); }; @@ -1496,8 +1496,15 @@ PDP11.opRTS = function(opCode) } var src = this.popWord(); var reg = opCode & PDP11.OPREG.MASK; - this.setPC(this.regsGen[reg]); - this.regsGen[reg] = src; + /* + * When the popular "RTS PC" form is used, we might as well eliminate the useless setting of PC to itself. + */ + if (reg == PDP11.REG.PC) { + this.setPC(src); + } else { + this.setPC(this.regsGen[reg]); + this.regsGen[reg] = src; + } this.nStepCycles -= (7 + 2); }; diff --git a/modules/pdp11/lib/cpustate.js b/modules/pdp11/lib/cpustate.js index 2663912cd..ccf1608c9 100644 --- a/modules/pdp11/lib/cpustate.js +++ b/modules/pdp11/lib/cpustate.js @@ -547,31 +547,6 @@ CPUStatePDP11.prototype.setNF = function() this.flagN = 0x8000; }; -/** - * getPC() - * - * @this {CPUStatePDP11} - * @return {number} - */ -CPUStatePDP11.prototype.getPC = function() -{ - return this.regsGen[PDP11.REG.PC]; -}; - -/** - * getLastPC() - * - * @this {CPUStatePDP11} - * @return {number} - */ -CPUStatePDP11.prototype.getLastPC = function() -{ - /* - * As long as we're always snapping the PC before every opcode, we might as well use it. - */ - return this.regMMR2; -}; - /** * getOpcode() * @@ -595,6 +570,8 @@ CPUStatePDP11.prototype.getOpcode = function() /** * advancePC(off) * + * NOTE: This function is nothing more than a convenience, and we fully expect it to be inlined at runtime. + * * @this {CPUStatePDP11} * @param {number} off * @return {number} (original PC) @@ -606,9 +583,40 @@ CPUStatePDP11.prototype.advancePC = function(off) return pc; }; +/** + * getPC() + * + * NOTE: This function is nothing more than a convenience, and we fully expect it to be inlined at runtime. + * + * @this {CPUStatePDP11} + * @return {number} + */ +CPUStatePDP11.prototype.getPC = function() +{ + return this.regsGen[PDP11.REG.PC]; +}; + +/** + * getLastPC() + * + * @this {CPUStatePDP11} + * @return {number} + */ +CPUStatePDP11.prototype.getLastPC = function() +{ + /* + * As long as we're always snapping the PC into regMMR2 before every opcode, we might as well use it. + */ + return this.regMMR2; +}; + /** * setPC() * + * NOTE: Unlike other PCjs emulators, such as PCx86, where all PC updates MUST go through the setPC() + * function, this function is nothing more than a convenience, because in the PDP-11, the PC can be loaded + * like any other general register. We fully expect this function to be inlined at runtime. + * * @this {CPUStatePDP11} * @param {number} addr */ @@ -620,6 +628,8 @@ CPUStatePDP11.prototype.setPC = function(addr) /** * getSP() * + * NOTE: This function is nothing more than a convenience, and we fully expect it to be inlined at runtime. + * * @this {CPUStatePDP11} * @return {number} */ @@ -631,6 +641,10 @@ CPUStatePDP11.prototype.getSP = function() /** * setSP() * + * NOTE: Unlike other PCjs emulators, such as PCx86, where all SP updates MUST go through the setSP() + * function, this function is nothing more than a convenience, because in the PDP-11, the PC can be loaded + * like any other general register. We fully expect this function to be inlined at runtime. + * * @this {CPUStatePDP11} * @param {number} addr */ @@ -1326,7 +1340,7 @@ CPUStatePDP11.prototype.mapVirtualToPhysical = function(virtualAddress, accessFl if (!(this.regMMR0 & 0xe000)) { this.regMMR0 |= errorMask | (this.mmuLastMode << 5) | (this.mmuLastPage << 1); } - this.trap(PDP11.TRAP.MMU_FAULT, PDP11.REASON.MAPERROR); + this.trap(PDP11.TRAP.MMU, PDP11.REASON.MAPERROR); } if (!(this.regMMR0 & 0xf000)) { //if (physicalAddress < 017772200 || physicalAddress > 017777677) { @@ -2083,13 +2097,13 @@ CPUStatePDP11.prototype.stepCPU = function(nMinCycles) */ if (this.opFlags & PDP11.OPFLAG.TRAP_MASK) { if (this.opFlags & PDP11.OPFLAG.TRAP_MMU) { - this.trap(PDP11.TRAP.MMU_FAULT, PDP11.REASON.TRAPMMU); // MMU trap has priority + this.trap(PDP11.TRAP.MMU, PDP11.REASON.TRAPMMU); // MMU trap has priority } else { if (this.opFlags & PDP11.OPFLAG.TRAP_SP) { - this.trap(PDP11.TRAP.BUS_ERROR, PDP11.REASON.TRAPSP); // then SP trap + this.trap(PDP11.TRAP.BUS_ERROR, PDP11.REASON.TRAPSP); // then SP trap } else { if (this.opFlags & PDP11.OPFLAG.TRAP_TF) { - this.trap(PDP11.TRAP.BREAKPOINT, PDP11.REASON.TRAPTF); // and finally a TF trap + this.trap(PDP11.TRAP.BPT, PDP11.REASON.TRAPTF); // and finally a TF trap } } } diff --git a/modules/pdp11/lib/debugger.js b/modules/pdp11/lib/debugger.js index b417ece44..3abed8bf5 100644 --- a/modules/pdp11/lib/debugger.js +++ b/modules/pdp11/lib/debugger.js @@ -140,7 +140,7 @@ function DebuggerPDP11(parmsDbg) * The new "bn" command allows you to specify a number of instructions to execute and then stop; * "bn 0" disables any outstanding count. */ - this.nBreakIns = 0; + this.nBreakInstructions = 0; /* * Execution history is allocated by historyInit() whenever checksEnabled() conditions change. @@ -280,6 +280,7 @@ if (DEBUGGER) { DebuggerPDP11.OP_DSTOFF = 0x2000; DebuggerPDP11.OP_DSTNUM3 = 0x3000; // DST 3-bit number (ie, just the DSTREG field) DebuggerPDP11.OP_DSTNUM6 = 0x6000; // DST 6-bit number (ie, both the DSTREG and DSTMODE fields) + DebuggerPDP11.OP_DSTNUM8 = 0x8000; // DST 8-bit number DebuggerPDP11.OP_OTHER = 0xF000; /* @@ -339,8 +340,8 @@ if (DEBUGGER) { 0x8500: [DebuggerPDP11.OPS.BVS, DebuggerPDP11.OP_BRANCH], 0x8600: [DebuggerPDP11.OPS.BCC, DebuggerPDP11.OP_BRANCH], 0x8700: [DebuggerPDP11.OPS.BCS, DebuggerPDP11.OP_BRANCH], - 0x8800: [DebuggerPDP11.OPS.EMT], // 104000..104377 - 0x8900: [DebuggerPDP11.OPS.TRAP] // 104400..104777 + 0x8800: [DebuggerPDP11.OPS.EMT, DebuggerPDP11.OP_DSTNUM8], // 104000..104377 + 0x8900: [DebuggerPDP11.OPS.TRAP, DebuggerPDP11.OP_DSTNUM8] // 104400..104777 }, 0xFFC0: { 0x0040: [DebuggerPDP11.OPS.JMP, DebuggerPDP11.OP_DST], // 0001DD @@ -932,8 +933,8 @@ if (DEBUGGER) { { var sMore = ""; var cHistory = 0; - var iHistory = this.iOpcodeHistory; - var aHistory = this.aOpcodeHistory; + var iHistory = this.iInstructionHistory; + var aHistory = this.aInstructionHistory; if (aHistory.length) { var nPrev = +sPrev || this.nextHistory; @@ -985,7 +986,7 @@ if (DEBUGGER) { * * If you re-enable this protection, be sure to re-enable the decrement below, too. */ - while (nLines > 0 && iHistory != this.iOpcodeHistory) { + while (nLines > 0 && iHistory != this.iInstructionHistory) { var dbgAddr = aHistory[iHistory++]; if (dbgAddr.addr == null) break; @@ -1245,34 +1246,27 @@ if (DEBUGGER) { { var i; if (!this.checksEnabled()) { - if (this.aOpcodeHistory && this.aOpcodeHistory.length && !fQuiet) { + if (this.aInstructionHistory && this.aInstructionHistory.length && !fQuiet) { this.println("instruction history buffer freed"); } - this.iOpcodeHistory = 0; - this.aOpcodeHistory = []; - this.aaOpcodeCounts = []; + this.iInstructionHistory = 0; + this.aInstructionHistory = []; return; } - if (!this.aOpcodeHistory || !this.aOpcodeHistory.length) { - this.aOpcodeHistory = new Array(DebuggerPDP11.HISTORY_LIMIT); - for (i = 0; i < this.aOpcodeHistory.length; i++) { + if (!this.aInstructionHistory || !this.aInstructionHistory.length) { + this.aInstructionHistory = new Array(DebuggerPDP11.HISTORY_LIMIT); + for (i = 0; i < this.aInstructionHistory.length; i++) { /* * Preallocate dummy Addr (Array) objects in every history slot, so that * checkInstruction() doesn't need to call newAddr() on every slot update. */ - this.aOpcodeHistory[i] = this.newAddr(); + this.aInstructionHistory[i] = this.newAddr(); } - this.iOpcodeHistory = 0; + this.iInstructionHistory = 0; if (!fQuiet) { this.println("instruction history buffer allocated"); } } - if (!this.aaOpcodeCounts || !this.aaOpcodeCounts.length) { - this.aaOpcodeCounts = new Array(256); - for (i = 0; i < this.aaOpcodeCounts.length; i++) { - this.aaOpcodeCounts[i] = [i, 0]; - } - } }; /** @@ -1324,7 +1318,7 @@ if (DEBUGGER) { this.nCycles += nCyclesStep; this.cpu.addCycles(nCyclesStep, true); this.cpu.updateChecksum(nCyclesStep); - this.cOpcodes++; + this.cInstructions++; } } catch(exception) { @@ -1456,7 +1450,7 @@ if (DEBUGGER) { DebuggerPDP11.prototype.reset = function(fQuiet) { this.historyInit(); - this.cOpcodes = this.cOpcodesStart = 0; + this.cInstructions = this.cInstructionsStart = 0; this.sMessagePrev = null; this.nCycles = 0; this.dbgAddrNextCode = this.newAddr(this.cpu.getPC()); @@ -1550,14 +1544,14 @@ if (DEBUGGER) { var nCyclesPerSecond = (msTotal > 0? Math.round(this.nCycles * 1000 / msTotal) : 0); sStopped += " ("; if (this.checksEnabled()) { - sStopped += this.cOpcodes + " opcodes, "; + sStopped += this.cInstructions + " instructions, "; /* * $ops displays progress by calculating cOpcodes - cOpcodesStart, so before * zeroing cOpcodes, we should subtract cOpcodes from cOpcodesStart (since we're * effectively subtracting cOpcodes from cOpcodes as well). */ - this.cOpcodesStart -= this.cOpcodes; - this.cOpcodes = 0; + this.cInstructionsStart -= this.cInstructions; + this.cInstructions = 0; } sStopped += this.nCycles + " cycles, " + msTotal + " ms, " + nCyclesPerSecond + " hz)"; } else { @@ -1594,7 +1588,7 @@ if (DEBUGGER) { */ DebuggerPDP11.prototype.checksEnabled = function(fRelease) { - return ((DEBUG && !fRelease)? true : (this.aBreakExec.length > 1 || !!this.nBreakIns)); + return ((DEBUG && !fRelease)? true : (this.aBreakExec.length > 1 || !!this.nBreakInstructions)); }; /** @@ -1610,7 +1604,7 @@ if (DEBUGGER) { */ DebuggerPDP11.prototype.checkInstruction = function(addr, nState) { - var opCode = -1 + var opCode = -1; var cpu = this.cpu; /* @@ -1620,13 +1614,13 @@ if (DEBUGGER) { if (nState == 0) { opCode = this.cpu.getWordDirect(addr); if (opCode == PDP11.OPCODE.HALT) { - this.cpu.advancePC(2); + addr = this.cpu.advancePC(2); } } if (nState > 0) { - if (this.nBreakIns && !--this.nBreakIns) { - return true; + if (this.nBreakInstructions) { + if (!--this.nBreakInstructions) return true; } if (this.checkBreakpoint(addr, 1, this.aBreakExec)) { return true; @@ -1639,14 +1633,16 @@ if (DEBUGGER) { * adding/removing breakpoints, simply because it's breakpoints that trigger the call to checkInstruction(); * well, OK, and a few other things now, like enabling MessagesPDP11.INT messages. */ - if (nState >= 0 && this.aaOpcodeCounts.length) { - this.cOpcodes++; - if (opCode < 0) opCode = this.cpu.getWordDirect(addr); - if (opCode != null) { - var dbgAddr = this.aOpcodeHistory[this.iOpcodeHistory]; - this.setAddr(dbgAddr, cpu.getPC()); + if (nState >= 0 && this.aInstructionHistory.length) { + this.cInstructions++; + if (opCode < 0) { + opCode = this.cpu.getWordDirect(addr); + } + if ((opCode & 0xffff) != PDP11.OPCODE.INVALID) { + var dbgAddr = this.aInstructionHistory[this.iInstructionHistory]; + this.setAddr(dbgAddr, addr); // if (DEBUG) dbgAddr.cycleCount = cpu.getCycles(); - if (++this.iOpcodeHistory == this.aOpcodeHistory.length) this.iOpcodeHistory = 0; + if (++this.iInstructionHistory == this.aInstructionHistory.length) this.iInstructionHistory = 0; } } return false; @@ -2161,13 +2157,17 @@ if (DEBUGGER) { sOperand = this.toStrBase(addr); } else if (opTypeOther == DebuggerPDP11.OP_DSTNUM3) { - disp = (opCode & 0x7); + disp = (opCode & 0x07); sOperand = this.toStrBase(disp, 1); } else if (opTypeOther == DebuggerPDP11.OP_DSTNUM6) { disp = (opCode & 0x3f); sOperand = this.toStrBase(disp, 1); } + else if (opTypeOther == DebuggerPDP11.OP_DSTNUM8) { + disp = (opCode & 0xff); + sOperand = this.toStrBase(disp, 1); + } else { /* * Isolate all OP_SRC or OP_DST bits from opcode in the opMode variable. @@ -2724,8 +2724,8 @@ if (DEBUGGER) { } if (sParm == 'n') { - this.nBreakIns = this.parseValue(sAddr); - this.println("break after " + this.nBreakIns + " instruction(s)"); + this.nBreakInstructions = this.parseValue(sAddr); + this.println("break after " + this.nBreakInstructions + " instruction(s)"); return; } @@ -2914,7 +2914,7 @@ if (DEBUGGER) { data = shift = 0; } sChars += (v >= 32 && v < 128? String.fromCharCode(v) : '.'); - nBytes--; + nBytes -= n; } if (sDump) sDump += '\n'; sDump += sAddr + " " + sData + ((i == 0)? (' ' + sChars) : ""); @@ -3680,16 +3680,16 @@ if (DEBUGGER) { }; /** - * shiftArgs(asArgs) - * - * Used with any command (eg, "r") that allows but doesn't require whitespace between command and first argument. + * splitArgs(sCmd) * * @this {DebuggerPDP11} - * @param {Array.} asArgs + * @param {string} sCmd * @return {Array.} */ - DebuggerPDP11.prototype.shiftArgs = function(asArgs) + DebuggerPDP11.prototype.splitArgs = function(sCmd) { + var asArgs = sCmd.replace(/ +/g, ' ').split(' '); + asArgs[0] = asArgs[0].toLowerCase(); if (asArgs && asArgs.length) { var s0 = asArgs[0]; var ch0 = s0.charAt(0); @@ -3749,8 +3749,7 @@ if (DEBUGGER) { } var fError = false; - var asArgs = this.shiftArgs(sCmd.replace(/ +/g, ' ').split(' ')); - asArgs[0] = asArgs[0].toLowerCase(); + var asArgs = this.splitArgs(sCmd); switch (asArgs[0].charAt(0)) { case 'a': diff --git a/modules/pdp11/lib/defines.js b/modules/pdp11/lib/defines.js index 425001751..294f02885 100644 --- a/modules/pdp11/lib/defines.js +++ b/modules/pdp11/lib/defines.js @@ -189,7 +189,8 @@ var PDP11 = { * Assorted common opcodes */ OPCODE: { - HALT: 0x0000 + HALT: 0x0000, + INVALID: 0xFFFF // far from the only invalid opcode, just a KNOWN invalid opcode }, /* * Internal operation state flags @@ -249,13 +250,13 @@ var PDP11 = { UNDEFINED: 0x00, // 000 (reserved) BUS_ERROR: 0x04, // 004 illegal instructions, bus errors, stack limit, illegal internal address, microbreak RESERVED: 0x08, // 010 reserved instructions - BREAKPOINT: 0x0C, // 014 BPT, breakpoint trap (trace) - IOT: 0x10, // 020 IOT, input/output trap - POWER_FAIL: 0x14, // 024 power fail - EMULATOR: 0x18, // 030 EMT, emulator trap + BPT: 0x0C, // 014 BPT: breakpoint trap (trace) + IOT: 0x10, // 020 IOT: input/output trap + PF: 0x14, // 024 power fail + EMT: 0x18, // 030 EMT: emulator trap TRAP: 0x1C, // 034 TRAP instruction - PIRQ: 0xA0, // 240 PIRQ, program interrupt request - MMU_FAULT: 0xA8 // 250 MMU aborts and traps + PIRQ: 0xA0, // 240 PIRQ: program interrupt request + MMU: 0xA8 // 250 MMU: aborts and traps }, /* * PDP-11 trap reasons (for diagnostic purposes only) diff --git a/modules/pdp11/lib/ram.js b/modules/pdp11/lib/ram.js index a88c2c740..7555f963d 100644 --- a/modules/pdp11/lib/ram.js +++ b/modules/pdp11/lib/ram.js @@ -258,8 +258,8 @@ RAMPDP11.prototype.loadImage = function(aBytes, addrLoad, addrExec, addrInit) * unless the address is odd (usually 1). DEC's "Absolute Loader" jumps to the exec address * in former case, halts in the latter. * - * All values are stored "little endian" (low byte first, followed by high byte), just like - * the PDP-11 does. + * All values are stored "little endian" (low byte followed by high byte), just like the + * PDP-11's memory architecture. * * After the data bytes, there is a single checksum byte. The 8-bit sum of all the bytes in * the block (including the header bytes and checksum byte) should be zero. @@ -267,7 +267,9 @@ RAMPDP11.prototype.loadImage = function(aBytes, addrLoad, addrExec, addrInit) * ANOMALIES: Tape files don't always begin with a signature word, so I allow any number of * leading zeros before the first signature. Tape files don't always end cleanly either, so as * soon as I see an invalid signature, I break out of the loop without signalling an error, as - * long as at least ONE block was successfully processed. + * long as at least ONE block was successfully processed. In fact, it's possible that as + * soon as a block with ZERO data bytes is encountered, processing is supposed to stop, but + * I haven't examined enough tapes (or the Absolute Loader code) to know for sure. */ if (addrLoad == null) { var off = 0, fError = false;