Minor Debugger improvements, notes, etc, while debugging the PDP-11 BASIC

This commit is contained in:
Jeff Parsons 2016-10-20 22:56:29 -07:00 committed by Jeff Parsons
commit aa2a25c5ec
7 changed files with 163 additions and 94 deletions

View file

@ -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)

View file

@ -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.

View file

@ -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);
};

View file

@ -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
}
}
}

View file

@ -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.<string>} asArgs
* @param {string} sCmd
* @return {Array.<string>}
*/
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':

View file

@ -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)

View file

@ -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;