diff --git a/modules/pcjs/lib/debugger.js b/modules/pcjs/lib/debugger.js index 1638cfe8d..890bd6299 100644 --- a/modules/pcjs/lib/debugger.js +++ b/modules/pcjs/lib/debugger.js @@ -1662,7 +1662,7 @@ if (DEBUGGER) { { if (dbgAddr.sel != null) { var seg = this.getSegment(dbgAddr.sel); - if (!seg || dbgAddr.off > seg.limit) { + if (!seg || (dbgAddr.off >>> 0) >= seg.offMax) { /* * TODO: This automatic wrap-to-zero is OK for normal segments, but for expand-down segments, not so much. */ @@ -1777,7 +1777,7 @@ if (DEBUGGER) { } }; - Debugger.aTSSFields = { + Debugger.TSS286 = { "PREV_TSS": 0x00, "CPL0_SP": 0x02, "CPL0_SS": 0x04, @@ -1802,6 +1802,35 @@ if (DEBUGGER) { "TASK_LDT": 0x2a }; + Debugger.TSS386 = { + "PREV_TSS": 0x00, + "CPL0_ESP": 0x04, + "CPL0_SS": 0x08, + "CPL1_ESP": 0x0c, + "CPL1_SS": 0x10, + "CPL2_ESP": 0x14, + "CPL2_SS": 0x18, + "TASK_CR3": 0x1C, + "TASK_EIP": 0x20, + "TASK_PS": 0x24, + "TASK_EAX": 0x28, + "TASK_ECX": 0x2C, + "TASK_EDX": 0x30, + "TASK_EBX": 0x34, + "TASK_ESP": 0x38, + "TASK_EBP": 0x3C, + "TASK_ESI": 0x40, + "TASK_EDI": 0x44, + "TASK_ES": 0x48, + "TASK_CS": 0x4C, + "TASK_SS": 0x50, + "TASK_DS": 0x54, + "TASK_FS": 0x58, + "TASK_GS": 0x5C, + "TASK_LDT": 0x60, + "TASK_IOPM": 0x64 + }; + /** * dumpBus(s) * @@ -2011,15 +2040,34 @@ if (DEBUGGER) { if (!seg) return; var sDump = ""; - for (var sField in Debugger.aTSSFields) { - var off = Debugger.aTSSFields[sField]; - var ch = (sField.length < 8? ' ' : ''); - var addr = seg.base + off; - var w = this.cpu.probeAddr(addr) | (this.cpu.probeAddr(addr + 1) << 8); + var type = seg.type & ~X86.DESC.ACC.TSS_BUSY; + var cch = (type == X86.DESC.ACC.TYPE.TSS286? 4 : 8); + var aTSSFields = (type == X86.DESC.ACC.TYPE.TSS286? Debugger.TSS286 : Debugger.TSS386); + var off, addr, v; + for (var sField in aTSSFields) { + off = aTSSFields[sField]; + addr = seg.base + off; + v = this.cpu.probeAddr(addr) | (this.cpu.probeAddr(addr + 1) << 8); + if (type == X86.DESC.ACC.TYPE.TSS386) { + v |= (this.cpu.probeAddr(addr + 2) << 16) | (this.cpu.probeAddr(addr + 3) << 24); + } if (sDump) sDump += '\n'; - sDump += str.toHexWord(off) + " " + sField + ": " + ch + str.toHexWord(w); + sDump += str.toHexWord(off) + ' ' + str.pad(sField + ':', 11) + str.toHex(v, cch); + } + if (type == X86.DESC.ACC.TYPE.TSS386) { + var iPort = 0; + off = (v >>> 16); + /* + * We arbitrarily cut the IOPM dump off at port 0x3FF, because we're not interested in anything above that. + */ + while (off < seg.offMax && iPort < 0x3ff) { + addr = seg.base + off; + v = this.cpu.probeAddr(addr) | (this.cpu.probeAddr(addr + 1) << 8); + sDump += "\nports " + str.toHexWord(iPort) + '-' + str.toHexWord(iPort+15) + ": " + str.toBinBytes(v, 2); + iPort += 16; + off += 2; + } } - this.println(sDump); }; @@ -3841,7 +3889,7 @@ if (DEBUGGER) { }; /** - * parseAddr(sAddr, type) + * parseAddr(sAddr, type, fNoChecks) * * As discussed above, dbgAddr variables contain one or more of: off, sel, and addr. They represent * a segmented address (sel:off) when sel is defined or a linear address (addr) when sel is undefined @@ -3865,9 +3913,10 @@ if (DEBUGGER) { * @this {Debugger} * @param {string|undefined} sAddr * @param {number|undefined} [type] is the address segment type, in case sAddr doesn't specify a segment + * @param {boolean} [fNoChecks] (eg, true when setting breakpoints that may not be valid now, but will be later) * @return {{DbgAddr}} */ - Debugger.prototype.parseAddr = function(sAddr, type) + Debugger.prototype.parseAddr = function(sAddr, type, fNoChecks) { var dbgAddr; var dbgAddrNext = (type === Debugger.ADDR_CODE? this.dbgAddrNextCode : this.dbgAddrNextData); @@ -3902,7 +3951,7 @@ if (DEBUGGER) { } dbgAddr = this.newAddr(off, sel, addr); - this.checkLimit(dbgAddr); + if (!fNoChecks) this.checkLimit(dbgAddr); return dbgAddr; }; @@ -3998,7 +4047,7 @@ if (DEBUGGER) { var fError = false; var sExpOrig = sExp; var aVals = [], aOps = []; - var asValues = sExp.split(/[|^&+%\/*-]/); // RegExp of "binops" only (unary and others saved for a rainy day) + var asValues = sExp.split(/[|^&+%\/*-]/); // RegExp of "binops" only (unary and other "ops" saved for a rainy day) for (var i = 0; i < asValues.length; i++) { var sValue = asValues[i]; var s = str.trim(asValues[i]); @@ -4026,7 +4075,7 @@ if (DEBUGGER) { } if (!fError) { value = aVals.pop(); - if (fPrint) this.println(sExpOrig + "=" + str.toHex(value) + "h bin=" + str.toBinBytes(value) + " dec=" + value + '.'); + if (fPrint) this.println(sExpOrig + '=' + str.toHex(value) + "h (" + value + ". " + str.toBinBytes(value) + ')'); } else { if (fPrint) this.println("error parsing '" + sExpOrig + "' at character " + (sExpOrig.length - sExp.length)); } @@ -4405,6 +4454,10 @@ if (DEBUGGER) { * These two new commands operate as toggles so that if "*" is used to trap all input (or output), * you can also use these commands to NOT trap specific ports. * + * TODO: Update the "bl" command to include any/all I/O breakpoints, and the "bc" command to + * clear them. Because "bi" and "bo" commands are piggy-backing on Bus functions, those breakpoints + * are outside the realm of what "bl" and "bc" are aware of. + * * @this {Debugger} * @param {string} sCmd * @param {string} [sAddr] @@ -4437,7 +4490,7 @@ if (DEBUGGER) { } var dbgAddr = {}; if (sAddr != "*") { - dbgAddr = this.parseAddr(sAddr, Debugger.ADDR_CODE); + dbgAddr = this.parseAddr(sAddr, Debugger.ADDR_CODE, true); if (dbgAddr.off == null) return; } sAddr = (dbgAddr.off == null? sAddr : str.toHexWord(dbgAddr.off)); @@ -5747,7 +5800,11 @@ if (DEBUGGER) { } } else { - this.println(">> " + sCmd); + var sPrompt = ">> "; + if (this.cpu.regCR0 & X86.CR0.MSW.PE) { + sPrompt = (this.cpu.regPS & X86.PS.VM)? "-- " : "## "; + } + this.println(sPrompt + sCmd); } sCmd = sCmd.toLowerCase(); diff --git a/modules/pcjs/lib/x86cpu.js b/modules/pcjs/lib/x86cpu.js index e228cfde4..a983c200d 100644 --- a/modules/pcjs/lib/x86cpu.js +++ b/modules/pcjs/lib/x86cpu.js @@ -1070,6 +1070,7 @@ X86CPU.prototype.initProcessor = function() * nested-task weirdness in real-mode. */ this.PS_CLEAR_RM = X86.PS.NT; + this.PS_DIRECT |= X86.PS.RF | X86.PS.VM; this.aOps[X86.OPCODE.FS] = X86.opFS; // 0x64 this.aOps[X86.OPCODE.GS] = X86.opGS; // 0x65 this.aOps[X86.OPCODE.OS] = X86.opOS; // 0x66 @@ -1596,7 +1597,7 @@ X86CPU.prototype.checkIntReturn = function(addr) }; /** - * setProtMode(fProt) + * setProtMode(fProt, fV86) * * Update any opcode handlers that operate significantly differently in real-mode vs. protected-mode, and * notify all the segment registers about the mode change as well -- but only those that are "bi-modal"; internal @@ -1609,23 +1610,27 @@ X86CPU.prototype.checkIntReturn = function(addr) * * @this {X86CPU} * @param {boolean} [fProt] (use the current MSW PE bit if not specified) + * @param {boolean} [fV86] true if the X86.PS.VM (V86-mode) flag is set (or is about to be) */ -X86CPU.prototype.setProtMode = function(fProt) +X86CPU.prototype.setProtMode = function(fProt, fV86) { if (fProt === undefined) { fProt = !!(this.regCR0 & X86.CR0.MSW.PE); } - if (!fProt != !(this.regCR0 & X86.CR0.MSW.PE) && this.messageEnabled()) { - this.printMessage("CPU switching to " + (fProt? "protected" : "real") + "-mode", this.bitsMessage, true); + if (fV86 === undefined) { + fV86 = !!(this.regPS & X86.PS.VM); } - this.aOpGrp6 = (fProt? X86.aOpGrp6Prot : X86.aOpGrp6Real); - this.segCS.updateMode(); - this.segDS.updateMode(); - this.segSS.updateMode(); - this.segES.updateMode(); + if (!fProt != !(this.regCR0 & X86.CR0.MSW.PE) && this.messageEnabled()) { + this.printMessage("CPU switching to " + (fProt? (fV86? "v86" : "protected") : "real") + "-mode", this.bitsMessage, true); + } + this.aOpGrp6 = (fProt && !fV86? X86.aOpGrp6Prot : X86.aOpGrp6Real); + this.segCS.updateMode(false, fProt, fV86); + this.segDS.updateMode(false, fProt, fV86); + this.segSS.updateMode(false, fProt, fV86); + this.segES.updateMode(false, fProt, fV86); if (I386 && this.model >= X86.MODEL_80386) { - this.segFS.updateMode(); - this.segGS.updateMode(); + this.segFS.updateMode(false, fProt, fV86); + this.segGS.updateMode(false, fProt, fV86); this.resetSizes(); } }; @@ -2740,6 +2745,31 @@ X86CPU.prototype.setPS = function(regPS, cpl) } }; +/** + * checkIOPM(port, nPorts) + * + * @this {X86CPU} + * @param {number} port (0x0000 to 0xffff) + * @param {number} nPorts (1 to 4) + * @return {boolean} true if allowed, false if not + */ +X86CPU.prototype.checkIOPM = function(port, nPorts) +{ + var bitsPorts = 0; + if (I386 && (this.regCR0 & X86.CR0.MSW.PE) && this.segCS.cpl > this.nIOPL && this.segTSS.addrIOPM) { + var offIOPM = port >>> 3; + var addrIOPM = this.segTSS.addrIOPM + offIOPM; + bitsPorts = ((1 << nPorts) - 1) << (port & 0x7); + while (bitsPorts && addrIOPM <= this.segTSS.addrIOPMLimit) { + var bits = this.getByte(addrIOPM); + if (bits & bitsPorts) break; + bitsPorts >>>= 8; + addrIOPM++; + } + } + return !bitsPorts; +}; + /** * traceLog(prop, dst, src, flagsIn, flagsOut, resultLo, resultHi) * diff --git a/modules/pcjs/lib/x86func.js b/modules/pcjs/lib/x86func.js index f5db355fc..234b7564c 100644 --- a/modules/pcjs/lib/x86func.js +++ b/modules/pcjs/lib/x86func.js @@ -142,6 +142,28 @@ X86.fnANDw = function ANDw(dst, src) */ X86.fnARPL = function ARPL(dst, src) { + /* + * ARPL is one of several protected-mode instructions that are meaningless and not allowed in either real-mode + * or V86-mode; others include LAR, LSL, VERR and VERW. More meaningful but potentially harmful protected-mode + * instructions that ARE allowed in real-mode but NOT in V86-mode include LIDT, LGDT, LMSW, CLTS, HLT, and + * control register MOV instructions. + * + * ARPL is somewhat more noteworthy because enhanced-mode Windows (going back to at least Windows 3.00, and + * possibly even the earliest versions of Windows/386) selected the ARPL opcode as a controlled means of exiting + * V86-mode via its UD_FAULT exception. Windows would use the same ARPL for all controlled exits, using different + * segment:offset pointers to the ARPL to differentiate them. ARPL was probably chosen because it could trigger + * a UD_FAULT with a single byte (0x63); any subsequent address bytes would be irrelevant. + * + * TODO: You may have noticed that setProtMode() already swaps out a 0x0F opcode dispatch table for another based + * on the mode, because none of the "GRP6" 0x0F opcodes (eg, SLDT, STR, LLDT, LTR, VERR and VERW) are allowed in + * real-mode, and it was easy to swap all those handlers in/out with a single update. We've extended that particular + * swap to include V86-mode as well, but we might want to consider swapping out more opcode handlers in a similar + * fashion, instead of using these in-line mode tests. + */ + if (!(this.regCR0 & X86.CR0.MSW.PE) || I386 && (this.regPS & X86.PS.VM)) { + X86.opInvalid.call(this); + return dst; + } this.nStepCycles -= (10 + (this.regEA === X86.ADDR_INVALID? 0 : 1)); if ((dst & X86.SEL.RPL) < (src & X86.SEL.RPL)) { dst = (dst & ~X86.SEL.RPL) | (src & X86.SEL.RPL); @@ -1247,7 +1269,7 @@ X86.fnIRET = function IRET() if (this.regPS & X86.PS.NT) { var addrNew = this.segTSS.base; /* - * Fortunately, X86.TSS286.PREV_TSS and X86.TSS386.PREV_TSS are at the same TSS offset. + * Fortunately, X86.TSS286.PREV_TSS and X86.TSS386.PREV_TSS refer to the same TSS offset. */ var sel = this.getShort(addrNew + X86.TSS286.PREV_TSS); this.segCS.switchTSS(sel, false); @@ -1259,6 +1281,51 @@ X86.fnIRET = function IRET() var newCS = this.popWord(); var newPS = this.popWord(); + if (I386) { + if (this.regPS & X86.PS.VM) { + /* + * On the 80386, in V86-mode, RF is the only defined EFLAGS bit above bit 15 that may be changed by IRETD. + * This is less restrictive than POPFD, which cannot change ANY bits above bit 15; see opPOPF() for details. + */ + newPS = (newPS & (0xffff | X86.PS.RF)) | (this.regPS & ~(0xffff | X86.PS.RF)); + } + else { + if (newPS & X86.PS.VM) { + this.assert(!!(this.regCR0 & X86.CR0.MSW.PE)); + /* + * We have to assume that a full V86-mode interrupt frame was on the protected-mode stack; namely: + * + * GS + * FS + * DS + * ES + * SS + * ESP + * EFLAGS + * CS + * EIP + * + * We've already popped EIP, CS, and EFLAGS into newIP, newCS and newPS, respectively, so we must now + * pop the rest, while we're still in protected-mode, before the switch to V86-mode alters the current + * operand size (among other things). + */ + var newSP = this.popWord(); + var newSS = this.popWord(); + var newES = this.popWord(); + var newDS = this.popWord(); + var newFS = this.popWord(); + var newGS = this.popWord(); + this.setProtMode(true, true); // flip the switch to V86-mode now + this.setSS(newSS); + this.setSP(newSP); + this.setES(newES); + this.setDS(newDS); + this.setFS(newFS); + this.setGS(newGS); + } + } + } + // if (DEBUG) this.printMessage(" returning to " + str.toHex(newCS, 4) + ':' + str.toHex(newIP, this.dataSize << 1), this.bitsMessage, true); if (this.setCSIP(newIP, newCS, false) != null) { @@ -1478,7 +1545,10 @@ X86.fnLFS = function LFS(dst, src) */ X86.fnLGDT = function LGDT(dst, src) { - if (this.regEA === X86.ADDR_INVALID) { + /* + * TODO: Consider swapping out this function whenever setProtMode() changes the mode to V86-mode. + */ + if (this.regEA === X86.ADDR_INVALID || I386 && (this.regPS & X86.PS.VM)) { X86.opInvalid.call(this); } else { /* @@ -1537,7 +1607,10 @@ X86.fnLGS = function LGS(dst, src) */ X86.fnLIDT = function LIDT(dst, src) { - if (this.regEA === X86.ADDR_INVALID) { + /* + * TODO: Consider swapping out this function whenever setProtMode() changes the mode to V86-mode. + */ + if (this.regEA === X86.ADDR_INVALID || I386 && (this.regPS & X86.PS.VM)) { X86.opInvalid.call(this); } else { /* @@ -1586,9 +1659,16 @@ X86.fnLLDT = function LLDT(dst, src) { */ X86.fnLMSW = function LMSW(dst, src) { - this.setMSW(dst); - this.nStepCycles -= (this.regEA === X86.ADDR_INVALID? 3 : 6); - this.opFlags |= X86.OPFLAG.NOWRITE; + /* + * TODO: Consider swapping out this function whenever setProtMode() changes the mode to V86-mode. + */ + if (I386 && (this.regPS & X86.PS.VM)) { + X86.opInvalid.call(this); + } else { + this.setMSW(dst); + this.nStepCycles -= (this.regEA === X86.ADDR_INVALID? 3 : 6); + this.opFlags |= X86.OPFLAG.NOWRITE; + } return dst; }; @@ -3640,7 +3720,10 @@ X86.fnFault = function(nFault, nError, fHalt, nCycles) fDispatch = false; } - if (fDispatch) X86.fnINT.call(this, this.nFault = nFault, nError, nCycles || 0); + if (fDispatch) { + this.nFault = nFault; + X86.fnINT.call(this, nFault, nError, nCycles || 0); + } /* * Since this fault is likely being issued in the context of an instruction that hasn't finished @@ -3652,7 +3735,7 @@ X86.fnFault = function(nFault, nError, fHalt, nCycles) * opPUSHA(): if a GP fault occurs on any PUSH other than the last, a subsequent PUSH is likely to * cause another fault, which we will misinterpret as a double-fault. * - * TODO: Throw a special JavaScript exception that cpu.js must intercept and quietly ignore. + * TODO: Throw a special JavaScript exception that cpu.js must intercept and quietly redirect. */ this.opFlags |= (X86.OPFLAG.NOREAD | X86.OPFLAG.NOWRITE); }; @@ -3718,6 +3801,15 @@ X86.fnFaultMessage = function(nFault, nError, fHalt) bitsMessage |= Messages.CPU; } + /* + * Windows 3.00 (and other versions of enhanced-mode Windows) use an ARPL in V86-mode to switch out + * of V86-mode; we don't need to report the UD_FAULT by default. + */ + if (bOpcode == X86.OPCODE.ARPL && (this.regPS & X86.PS.VM)) { + fHalt = false; + bitsMessage |= Messages.CPU; + } + /* * Similarly, the PC AT ROM BIOS deliberately generates a couple of GP faults as part of the POST * (Power-On Self Test); we don't want to ignore those, but we don't want to halt on them either. We diff --git a/modules/pcjs/lib/x86op0f.js b/modules/pcjs/lib/x86op0f.js index b183ec1b0..7c5c5691a 100644 --- a/modules/pcjs/lib/x86op0f.js +++ b/modules/pcjs/lib/x86op0f.js @@ -73,6 +73,13 @@ X86.opGRP7 = function GRP7() */ X86.opLAR = function LAR() { + /* + * TODO: Consider swapping out this function whenever setProtMode() changes the mode to real-mode or V86-mode. + */ + if (!(this.regCR0 & X86.CR0.MSW.PE) || I386 && (this.regPS & X86.PS.VM)) { + X86.opInvalid.call(this); + return; + } this.aOpModRegWord[this.getIPByte()].call(this, X86.fnLAR); }; @@ -85,6 +92,13 @@ X86.opLAR = function LAR() */ X86.opLSL = function LSL() { + /* + * TODO: Consider swapping out this function whenever setProtMode() changes the mode to real-mode or V86-mode. + */ + if (!(this.regCR0 & X86.CR0.MSW.PE) || I386 && (this.regPS & X86.PS.VM)) { + X86.opInvalid.call(this); + return; + } this.aOpModRegWord[this.getIPByte()].call(this, X86.fnLSL); }; @@ -187,6 +201,9 @@ X86.opLOADALL = function LOADALL() */ X86.opCLTS = function CLTS() { + /* + * NOTE: The following code shouldn't need to test for X86.PS.VM because V86-mode is CPL 3. + */ if (this.segCS.cpl) { X86.fnFault.call(this, X86.EXCEPTION.GP_FAULT, 0); return; @@ -216,18 +233,19 @@ X86.opCLTS = function CLTS() */ X86.opMOVrc = function MOVrc() { - var bModRM = this.getIPByte(); - + /* + * NOTE: The following code shouldn't need to test for X86.PS.VM because V86-mode is CPL 3. + */ if (this.segCS.cpl) { /* * You're not allowed to read control registers if the current privilege level is not zero - * (TODO: I'm issuing this AFTER fetching the ModRM byte, but I assume it makes no difference). */ X86.fnFault.call(this, X86.EXCEPTION.GP_FAULT, 0); return; } var reg; + var bModRM = this.getIPByte(); switch((bModRM & 0x38) >> 3) { case 0x0: reg = this.regCR0; @@ -298,18 +316,19 @@ X86.opMOVrc = function MOVrc() */ X86.opMOVcr = function MOVcr() { - var bModRM = this.getIPByte(); - + /* + * NOTE: The following code shouldn't need to test for X86.PS.VM because V86-mode is CPL 3. + */ if (this.segCS.cpl) { /* * You're not allowed to write control registers if the current privilege level is not zero - * (TODO: I'm issuing this AFTER fetching the ModRM byte, but I assume it makes no difference). */ X86.fnFault.call(this, X86.EXCEPTION.GP_FAULT, 0); return; } var reg; + var bModRM = this.getIPByte(); switch(bModRM & 0x7) { case 0x0: reg = this.regEAX; diff --git a/modules/pcjs/lib/x86ops.js b/modules/pcjs/lib/x86ops.js index 127800ad8..495bb2f87 100644 --- a/modules/pcjs/lib/x86ops.js +++ b/modules/pcjs/lib/x86ops.js @@ -1502,12 +1502,17 @@ X86.opINSb = function INSb() } if (nReps--) { - var b = this.bus.checkPortInputNotify(this.regEDX & 0xffff, this.regLIP - nDelta - 1); + var port = this.regEDX & 0xffff; + if (!this.checkIOPM(port, 1)) { + X86.fnFault.call(this, X86.EXCEPTION.GP_FAULT, 0); + return; + } + var b = this.bus.checkPortInputNotify(port, this.regLIP - nDelta - 1); if (BACKTRACK) this.backTrack.btiMemLo = this.backTrack.btiIO; this.setSOByte(this.segES, this.regEDI & this.addrMask, b); this.regEDI = (this.regEDI & ~this.addrMask) | ((this.regEDI + ((this.regPS & X86.PS.DF)? -1 : 1)) & this.addrMask); - this.nStepCycles -= nCycles; this.regECX = (this.regECX & ~this.addrMask) | ((this.regECX - nDelta) & this.addrMask); + this.nStepCycles -= nCycles; if (nReps) { if (BUGS_8086) { this.rewindIP(-2); // this instruction does not support multiple overrides @@ -1550,8 +1555,13 @@ X86.opINSw = function INSw() if (nReps--) { var addrFrom = this.regLIP - nDelta - 1; var w = 0, shift = 0; + var port = this.regEDX & 0xffff; + if (!this.checkIOPM(port, 1)) { + X86.fnFault.call(this, X86.EXCEPTION.GP_FAULT, 0); + return; + } for (var n = 0; n < this.dataSize; n++) { - w |= this.bus.checkPortInputNotify(this.regEDX & 0xffff, addrFrom) << shift; + w |= this.bus.checkPortInputNotify(port, addrFrom) << shift; shift += 8; if (BACKTRACK) { if (!n) { @@ -1563,8 +1573,8 @@ X86.opINSw = function INSw() } this.setSOWord(this.segES, this.regEDI & this.addrMask, w); this.regEDI = (this.regEDI & ~this.addrMask) | ((this.regEDI + ((this.regPS & X86.PS.DF)? -this.dataSize : this.dataSize)) & this.addrMask); - this.nStepCycles -= nCycles; this.regECX = (this.regECX & ~this.addrMask) | ((this.regECX - nDelta) & this.addrMask); + this.nStepCycles -= nCycles; if (nReps) { if (BUGS_8086) { this.rewindIP(-2); // this instruction does not support multiple overrides @@ -1604,12 +1614,17 @@ X86.opOUTSb = function OUTSb() if (this.opPrefixes & X86.OPFLAG.REPEAT) nCycles = 4; } if (nReps--) { + var port = this.regEDX & 0xffff; + if (!this.checkIOPM(port, 1)) { + X86.fnFault.call(this, X86.EXCEPTION.GP_FAULT, 0); + return; + } var b = this.getSOByte(this.segDS, this.regESI & this.addrMask); - this.regESI = (this.regESI & ~this.addrMask) | ((this.regESI + ((this.regPS & X86.PS.DF)? -1 : 1)) & this.addrMask); - this.nStepCycles -= nCycles; if (BACKTRACK) this.backTrack.btiIO = this.backTrack.btiMemLo; - this.bus.checkPortOutputNotify(this.regEDX & 0xffff, b, this.regLIP - nDelta - 1); + this.bus.checkPortOutputNotify(port, b, this.regLIP - nDelta - 1); + this.regESI = (this.regESI & ~this.addrMask) | ((this.regESI + ((this.regPS & X86.PS.DF)? -1 : 1)) & this.addrMask); this.regECX = (this.regECX & ~this.addrMask) | ((this.regECX - nDelta) & this.addrMask); + this.nStepCycles -= nCycles; if (nReps) { if (BUGS_8086) { this.rewindIP(-2); // this instruction does not support multiple overrides @@ -1650,9 +1665,12 @@ X86.opOUTSw = function OUTSw() } if (nReps--) { var w = this.getSOWord(this.segDS, this.regESI & this.addrMask); - this.regESI = (this.regESI & ~this.addrMask) | ((this.regESI + ((this.regPS & X86.PS.DF)? -this.dataSize : this.dataSize)) & this.addrMask); - this.nStepCycles -= nCycles; var addrFrom = this.regLIP - nDelta - 1, shift = 0; + var port = this.regEDX & 0xffff; + if (!this.checkIOPM(port, 1)) { + X86.fnFault.call(this, X86.EXCEPTION.GP_FAULT, 0); + return; + } for (var n = 0; n < this.dataSize; n++) { if (BACKTRACK) { if (!n) { @@ -1661,10 +1679,12 @@ X86.opOUTSw = function OUTSw() this.backTrack.btiIO = this.backTrack.btiMemHi; } } - this.bus.checkPortOutputNotify(this.regEDX & 0xffff, (w >> shift) & 0xff, addrFrom); + this.bus.checkPortOutputNotify(port, (w >> shift) & 0xff, addrFrom); shift += 8; } + this.regESI = (this.regESI & ~this.addrMask) | ((this.regESI + ((this.regPS & X86.PS.DF)? -this.dataSize : this.dataSize)) & this.addrMask); this.regECX = (this.regECX & ~this.addrMask) | ((this.regECX - nDelta) & this.addrMask); + this.nStepCycles -= nCycles; if (nReps) { if (BUGS_8086) { this.rewindIP(-2); // this instruction does not support multiple overrides @@ -2458,7 +2478,19 @@ X86.opPUSHF = function PUSHF() */ X86.opPOPF = function POPF() { - this.setPS(this.popWord()); + /* + * TODO: Consider swapping out this function whenever setProtMode() changes the mode to V86-mode. + */ + if (I386 && (this.regPS & X86.PS.VM) && this.nIOPL < 3) { + X86.fnFault.call(this, X86.EXCEPTION.GP_FAULT, 0); + return; + } + /* + * On the 80386, regardless of mode, VM and RF (the only defined EFLAGS bit above bit 15) are never changed by POPFD. + */ + var newPS = this.popWord(); + if (I386) newPS = (newPS & 0xffff) | (this.regPS & ~0xffff); + this.setPS(newPS); /* * NOTE: I'm assuming that neither POPF nor IRET are required to set NOINTR like STI does. */ @@ -3353,6 +3385,13 @@ X86.opRETF = function RETF() */ X86.opINT3 = function INT3() { + /* + * TODO: Consider swapping out this function whenever setProtMode() changes the mode to V86-mode. + */ + if (I386 && (this.regPS & X86.PS.VM) && this.nIOPL < 3) { + X86.fnFault.call(this, X86.EXCEPTION.GP_FAULT, 0); + return; + } /* * To give our own Debugger the ability to stop execution on INT3, I thought about treating this as * a fault rather than an interrupt, in order to leverage the existing Debugger logic inside fnFault() @@ -3389,6 +3428,13 @@ X86.opINT3 = function INT3() */ X86.opINTn = function INTn() { + /* + * TODO: Consider swapping out this function whenever setProtMode() changes the mode to V86-mode. + */ + if (I386 && (this.regPS & X86.PS.VM) && this.nIOPL < 3) { + X86.fnFault.call(this, X86.EXCEPTION.GP_FAULT, 0); + return; + } var nInt = this.getIPByte(); if (this.checkIntNotify(nInt)) { X86.fnINT.call(this, nInt, null, 0); @@ -3405,6 +3451,13 @@ X86.opINTn = function INTn() X86.opINTO = function INTO() { if (this.getOF()) { + /* + * TODO: Consider swapping out this function whenever setProtMode() changes the mode to V86-mode. + */ + if (I386 && (this.regPS & X86.PS.VM) && this.nIOPL < 3) { + X86.fnFault.call(this, X86.EXCEPTION.GP_FAULT, 0); + return; + } X86.fnINT.call(this, X86.EXCEPTION.OVERFLOW, null, this.cycleCounts.nOpCyclesIntOD); return; } @@ -3639,6 +3692,10 @@ X86.opJCXZ = function JCXZ() X86.opINb = function INb() { var port = this.getIPByte(); + if (!this.checkIOPM(port, 1)) { + X86.fnFault.call(this, X86.EXCEPTION.GP_FAULT, 0); + return; + } this.regEAX = (this.regEAX & ~0xff) | this.bus.checkPortInputNotify(port, this.regLIP - 2); if (BACKTRACK) this.backTrack.btiAL = this.backTrack.btiIO; this.nStepCycles -= this.cycleCounts.nOpCyclesInP; @@ -3652,6 +3709,10 @@ X86.opINb = function INb() X86.opINw = function INw() { var port = this.getIPByte(); + if (!this.checkIOPM(port, 2)) { + X86.fnFault.call(this, X86.EXCEPTION.GP_FAULT, 0); + return; + } this.regEAX = this.bus.checkPortInputNotify(port, this.regLIP - 2); if (BACKTRACK) this.backTrack.btiAL = this.backTrack.btiIO; /* @@ -3672,6 +3733,10 @@ X86.opINw = function INw() X86.opOUTb = function OUTb() { var port = this.getIPByte(); + if (!this.checkIOPM(port, 1)) { + X86.fnFault.call(this, X86.EXCEPTION.GP_FAULT, 0); + return; + } this.bus.checkPortOutputNotify(port, this.regEAX & 0xff, this.regLIP - 2); this.nStepCycles -= this.cycleCounts.nOpCyclesOutP; }; @@ -3684,6 +3749,10 @@ X86.opOUTb = function OUTb() X86.opOUTw = function OUTw() { var port = this.getIPByte(); + if (!this.checkIOPM(port, 2)) { + X86.fnFault.call(this, X86.EXCEPTION.GP_FAULT, 0); + return; + } this.bus.checkPortOutputNotify(port, this.regEAX & 0xff, this.regLIP - 2); /* * TODO: Specs are clear that bits 8-15 of the port address for the FIRST byte of I/O will be zero, but @@ -3754,7 +3823,12 @@ X86.opJMPs = function JMPs() */ X86.opINDXb = function INDXb() { - this.regEAX = (this.regEAX & ~0xff) | this.bus.checkPortInputNotify(this.regEDX & 0xffff, this.regLIP - 1); + var port = this.regEDX & 0xffff; + if (!this.checkIOPM(port, 1)) { + X86.fnFault.call(this, X86.EXCEPTION.GP_FAULT, 0); + return; + } + this.regEAX = (this.regEAX & ~0xff) | this.bus.checkPortInputNotify(port, this.regLIP - 1); if (BACKTRACK) this.backTrack.btiAL = this.backTrack.btiIO; this.nStepCycles -= this.cycleCounts.nOpCyclesInDX; }; @@ -3766,9 +3840,14 @@ X86.opINDXb = function INDXb() */ X86.opINDXw = function INDXw() { - this.regEAX = this.bus.checkPortInputNotify(this.regEDX & 0xffff, this.regLIP - 1); + var port = this.regEDX & 0xffff; + if (!this.checkIOPM(port, 2)) { + X86.fnFault.call(this, X86.EXCEPTION.GP_FAULT, 0); + return; + } + this.regEAX = this.bus.checkPortInputNotify(port, this.regLIP - 1); if (BACKTRACK) this.backTrack.btiAL = this.backTrack.btiIO; - this.regEAX |= (this.bus.checkPortInputNotify((this.regEDX + 1) & 0xffff, this.regLIP - 1) << 8); + this.regEAX |= (this.bus.checkPortInputNotify((port + 1) & 0xffff, this.regLIP - 1) << 8); if (BACKTRACK) this.backTrack.btiAH = this.backTrack.btiIO; this.nStepCycles -= this.cycleCounts.nOpCyclesInDX; }; @@ -3780,8 +3859,13 @@ X86.opINDXw = function INDXw() */ X86.opOUTDXb = function OUTDXb() { + var port = this.regEDX & 0xffff; + if (!this.checkIOPM(port, 1)) { + X86.fnFault.call(this, X86.EXCEPTION.GP_FAULT, 0); + return; + } if (BACKTRACK) this.backTrack.btiIO = this.backTrack.btiAL; - this.bus.checkPortOutputNotify(this.regEDX & 0xffff, this.regEAX & 0xff, this.regLIP - 1); + this.bus.checkPortOutputNotify(port, this.regEAX & 0xff, this.regLIP - 1); this.nStepCycles -= this.cycleCounts.nOpCyclesOutDX; }; @@ -3792,10 +3876,15 @@ X86.opOUTDXb = function OUTDXb() */ X86.opOUTDXw = function OUTDXw() { + var port = this.regEDX & 0xffff; + if (!this.checkIOPM(port, 2)) { + X86.fnFault.call(this, X86.EXCEPTION.GP_FAULT, 0); + return; + } if (BACKTRACK) this.backTrack.btiIO = this.backTrack.btiAL; - this.bus.checkPortOutputNotify(this.regEDX & 0xffff, this.regEAX & 0xff, this.regLIP - 1); + this.bus.checkPortOutputNotify(port, this.regEAX & 0xff, this.regLIP - 1); if (BACKTRACK) this.backTrack.btiIO = this.backTrack.btiAH; - this.bus.checkPortOutputNotify((this.regEDX + 1) & 0xffff, this.regEAX >> 8, this.regLIP - 1); + this.bus.checkPortOutputNotify((port + 1) & 0xffff, this.regEAX >> 8, this.regLIP - 1); this.nStepCycles -= this.cycleCounts.nOpCyclesOutDX; }; @@ -3863,6 +3952,13 @@ X86.opREPZ = function REPZ() */ X86.opHLT = function HLT() { + /* + * TODO: Consider swapping out this function whenever setProtMode() changes the mode to V86-mode. + */ + if (I386 && (this.regPS & X86.PS.VM)) { + X86.fnFault.call(this, X86.EXCEPTION.GP_FAULT, 0); + return; + } /* * The CPU is never REALLY halted by a HLT instruction; instead, by setting X86.INTFLAG.HALT, * we are signalling to stepCPU() that it's free to end the current burst AND that it should not diff --git a/modules/pcjs/lib/x86seg.js b/modules/pcjs/lib/x86seg.js index 6784b9c8d..c732408be 100644 --- a/modules/pcjs/lib/x86seg.js +++ b/modules/pcjs/lib/x86seg.js @@ -91,6 +91,11 @@ function X86Seg(cpu, id, sName, fProt) this.addrDesc = X86.ADDR_INVALID; this.dataSize = this.addrSize = 2; this.dataMask = this.addrMask = 0xffff; + + this.loadV86 = this.loadReal; + this.checkReadV86 = this.checkReadReal; + this.checkWriteV86 = this.checkWriteReal; + /* * The following properties are used for CODE segments only (ie, segCS); if the process of loading * CS also requires a stack switch, then fStackSwitch will be set to true; additionally, if the stack @@ -242,7 +247,7 @@ X86Seg.prototype.loadIDTReal = function loadIDTReal(nIDT) * * "[T]he 80286 will shut down if the SP = 1, 3, or 5 before executing the INT or INTO instruction--due to lack of stack space" * - * TODO: Verify that 80286 real-mode actually enforces the above. See http://localhost:8088/pubs/pc/reference/intel/80286/progref/#page-260 + * TODO: Verify that 80286 real-mode actually enforces the above. See http://www.pcjs.org/pubs/pc/reference/intel/80286/progref/#page-260 */ var addrIDT = cpu.addrIDT + (nIDT << 2); var off = cpu.getShort(addrIDT); @@ -536,7 +541,7 @@ X86Seg.prototype.loadDesc8 = function(addrDesc, sel, fSuppress) if (this.id == X86Seg.ID.CODE) { this.fStackSwitch = false; var fCall = this.fCall; - var regPSMask, nFaultError, regSP; + var regPSClear, nFaultError, regSP; var rpl = sel & X86.SEL.RPL; var dpl = (acc & X86.DESC.ACC.DPL.MASK) >> X86.DESC.ACC.DPL.SHIFT; @@ -579,19 +584,19 @@ X86Seg.prototype.loadDesc8 = function(addrDesc, sel, fSuppress) } else if (type == X86.DESC.ACC.TYPE.GATE_CALL || type == X86.DESC.ACC.TYPE.GATE386_CALL) { fGate = true; - regPSMask = ~0; + regPSClear = 0; nFaultError = sel; if (rpl < this.cpl) rpl = this.cpl; // set RPL to max(RPL,CPL) for call gates } else if (type == X86.DESC.ACC.TYPE.GATE286_INT || type == X86.DESC.ACC.TYPE.GATE386_INT) { fGate = true; - regPSMask = ~(X86.PS.NT | X86.PS.TF | X86.PS.IF); + regPSClear = (X86.PS.NT | X86.PS.TF | X86.PS.IF); nFaultError = sel | X86.ERRCODE.EXT; cpu.assert(!(acc & 0x1f)); } else if (type == X86.DESC.ACC.TYPE.GATE286_TRAP || type == X86.DESC.ACC.TYPE.GATE386_TRAP) { fGate = true; - regPSMask = ~(X86.PS.NT | X86.PS.TF); + regPSClear = (X86.PS.NT | X86.PS.TF); nFaultError = sel | X86.ERRCODE.EXT; cpu.assert(!(acc & 0x1f)); } @@ -614,6 +619,7 @@ X86Seg.prototype.loadDesc8 = function(addrDesc, sel, fSuppress) * TODO: Verify the PRESENT bit of the gate descriptor, and issue NP_FAULT as appropriate. */ cplPrev = this.cpl; + /* * For gates, there is no "base" and "limit", but rather "selector" and "offset"; the selector * is located where the first 16 bits of base are normally stored, and the offset comes from the @@ -623,11 +629,24 @@ X86Seg.prototype.loadDesc8 = function(addrDesc, sel, fSuppress) if (I386 && (type & X86.DESC.ACC.NONSEG_386)) { limit = limitOrig | (ext << 16); } + + /* + * At a minimum, we need to clear X86.PS.VM now, so that the following load will initialize the + * new code segment properly. + */ + var regPS = cpu.regPS; + cpu.regPS &= ~regPSClear; + if (cpu.regPS & X86.PS.VM) { + cpu.regPS &= ~X86.PS.VM; + cpu.setProtMode(true, false); + } + if (this.load(selCode, true) === X86.ADDR_INVALID) { cpu.assert(false); base = addrDesc = X86.ADDR_INVALID; break; } + cpu.regEIP = limit; if (this.cpl < cplPrev) { if (fCall !== true) { @@ -655,13 +674,22 @@ X86Seg.prototype.loadDesc8 = function(addrDesc, sel, fSuppress) offSS = offSP + 4; cpu.setSS(cpu.getShort(addrTSS + offSS), true); cpu.setSP(cpu.getLong(addrTSS + offSP)); + if (regPS & X86.PS.VM) { + cpu.pushWord(cpu.segGS.sel); + cpu.setGS(0); + cpu.pushWord(cpu.segFS.sel); + cpu.setFS(0); + cpu.pushWord(cpu.segDS.sel); + cpu.setDS(0); + cpu.pushWord(cpu.segES.sel); + cpu.setES(0); + } } cpu.pushWord(regSSPrev); cpu.pushWord(regSPPrev); while (i) cpu.pushWord(this.awParms[--i]); this.fStackSwitch = true; } - cpu.regPS &= regPSMask; return this.base; } cpu.assert(false); @@ -730,6 +758,14 @@ X86Seg.prototype.loadDesc8 = function(addrDesc, sel, fSuppress) base = addrDesc = X86.ADDR_INVALID; break; } + /* + * For more efficient IOPM lookups, we cache the starting linear address in segTSS.addrIOPM, and the + * last valid address in segTSS.addrIOPMLimit. + */ + if (typeTSS == X86.DESC.ACC.TYPE.TSS386) { + this.addrIOPM = (base + cpu.getShort(base + X86.TSS386.TASK_IOPM))|0; + this.addrIOPMLimit = (base + this.limit)|0; + } } else if (this.id == X86Seg.ID.OTHER) { /* @@ -935,7 +971,7 @@ X86Seg.prototype.switchTSS = function switchTSS(selNew, fNest) } /* - * Fortunately, X86.TSS286.PREV_TSS and X86.TSS386.PREV_TSS are at the same TSS offset. + * Fortunately, X86.TSS286.PREV_TSS and X86.TSS386.PREV_TSS refer to the same TSS offset. */ if (fNest) cpu.setShort(addrNew + X86.TSS286.PREV_TSS, selOld); @@ -1032,7 +1068,7 @@ X86Seg.prototype.restore = function(a) }; /** - * updateMode(fLoad, fProt) + * updateMode(fLoad, fProt, fV86) * * Ensures that the segment register's access (ie, load and check methods) matches the specified (or current) * operating mode (real or protected). @@ -1040,9 +1076,9 @@ X86Seg.prototype.restore = function(a) * @this {X86Seg} * @param {boolean} [fLoad] true if the segment was just (re)loaded, false if not * @param {boolean} [fProt] true for protected-mode access, false for real-mode access, undefined for current mode - * @return {boolean} + * @param {boolean} [fV86] true for V86-mode access, false for protected-mode access, undefined for current mode */ -X86Seg.prototype.updateMode = function(fLoad, fProt) +X86Seg.prototype.updateMode = function(fLoad, fProt, fV86) { if (fProt === undefined) { fProt = !!(this.cpu.regCR0 & X86.CR0.MSW.PE); @@ -1061,6 +1097,30 @@ X86Seg.prototype.updateMode = function(fLoad, fProt) this.checkRead = this.checkReadProt; this.checkWrite = this.checkWriteProt; + if (fV86 === undefined) { + fV86 = !!(this.cpu.regPS & X86.PS.VM); + } + + if (fV86) { + this.load = this.loadV86; + this.checkRead = this.checkReadV86; + this.checkWrite = this.checkWriteV86; + /* + * One important feature of V86-mode (as compared to real-mode) are that other segment attributes + * (eg, limit, operand size, address size, etc) ARE updated, whereas in real-mode, segment attributes + * remain set to whatever was in effect in protected-mode. + */ + this.cpl = this.dpl = 3; + this.dataSize = this.addrSize = 2; + this.dataMask = this.addrMask = 0xffff; + this.limit = 0xffff; + this.offMax = this.limit + 1; + this.addrSize = this.dataSize; + this.addrDesc = X86.ADDR_INVALID; + this.fStackSwitch = false; + return; + } + /* * TODO: For null GDT selectors, should we rely on the descriptor being invalid, or should we assume that * the null descriptor might contain uninitialized (or other) data? I'm assuming the latter, hence the @@ -1134,6 +1194,12 @@ X86Seg.prototype.updateMode = function(fLoad, fProt) this.addrMask = this.dataMask; } } else { + /* + * One important feature of real-mode (as compared to V86-mode) are that other segment attributes + * (eg, limit, operand size, address size, etc) are NOT updated, enabling features like "big real-mode" + * (aka "unreal mode"), which is used by system software like HIMEM.SYS to access extended memory from + * real-mode. + */ this.load = this.loadReal; this.loadIDT = this.loadIDTReal; this.checkRead = this.checkReadReal; @@ -1142,7 +1208,6 @@ X86Seg.prototype.updateMode = function(fLoad, fProt) this.addrDesc = X86.ADDR_INVALID; this.fStackSwitch = false; } - return fProt; }; /** diff --git a/modules/shared/lib/strlib.js b/modules/shared/lib/strlib.js index caeb04f4f..7da5d1d6c 100644 --- a/modules/shared/lib/strlib.js +++ b/modules/shared/lib/strlib.js @@ -42,13 +42,14 @@ var str = {}; * So use this function to validate the entire string. * * @param {string} s is the string representation of some number - * @param {number} [base] is the radix of the number represented above (only 10 and 16 are supported) + * @param {number} [base] is the radix of the number represented above (only 2, 10 and 16 are supported) * @return {boolean} true if valid, false if invalid (or the specified base isn't supported) */ str.isValidInt = function(s, base) { if (!base || base == 10) return s.match(/^[0-9]+$/) !== null; if (base == 16) return s.match(/^[0-9a-f]+$/i) !== null; + if (base == 2) return s.match(/^[01]+$/i) !== null; return false; }; @@ -56,8 +57,11 @@ str.isValidInt = function(s, base) * parseInt(s, base) * * This is a wrapper around the built-in parseInt() function, which recognizes certain prefixes (eg, - * '$' or "0x" for hex) and suffixes (eg, 'h' for hex or '.' for decimal), and then calls isValidInt() - * to ensure we don't get partial values (see isValidInt() for details). + * '$' or "0x" for hex) and suffixes (eg, 'h' for hex, or '.' for decimal), and then calls isValidInt() + * to ensure we don't convert strings that contain partial values (see isValidInt() for details). + * + * We don't support multiple prefix/suffix combinations, nor do we support the "0b" prefix (or "b" suffix) + * for binary, because 1) it's not commonly used, and 2) it conflicts with valid hex sequences. * * @param {string} s is the string representation of some number * @param {number} [base] is the default radix to use (default is 16); can be overridden by prefixes/suffixes @@ -84,7 +88,7 @@ str.parseInt = function(s, base) base = 10; chSuffix = null; } - if (chSuffix === null) s = s.substr(0, s.length-1); + if (chSuffix == null) s = s.substr(0, s.length-1); } var v; if (str.isValidInt(s, base) && !isNaN(v = parseInt(s, base))) { @@ -108,13 +112,16 @@ str.toBin = function(n, cch) var s = ""; if (cch === undefined) { cch = 32; - s = "b"; } else { if (cch > 32) cch = 32; } /* * An initial "falsey" check for null takes care of both null and undefined; * we can't rely entirely on isNaN(), because isNaN(null) returns false, oddly enough. + * + * Alternatively, we could mask and shift n regardless of whether it's null/undefined/NaN, + * since JavaScript coerces such operands to zero, but I think there's "value" in seeing those + * values displayed differently. */ if (n == null || isNaN(n)) { while (cch-- > 0) s = '?' + s; @@ -155,8 +162,8 @@ str.toBinBytes = function(n, cb) * * You might be tempted to use the built-in n.toString(16) instead, but it doesn't zero-pad and it * doesn't properly convert negative values; for example, if n is -2147483647, then n.toString(16) - * will return "-7fffffff" instead of "80000001". Moreover, if n is undefined, n.toString() will throw - * an exception, whereas toHex() will simply return '?' characters. + * will return "-7fffffff" instead of "80000001". Moreover, if n is undefined, n.toString() will + * throw an exception, whereas this function will return '?' characters. * * NOTE: The following work-around (adapted from code found on StackOverflow) would be another solution, * taking care of negative values, zero-padding, and upper-casing, but not null/undefined/NaN values: @@ -180,6 +187,10 @@ str.toHex = function(n, cch) /* * An initial "falsey" check for null takes care of both null and undefined; * we can't rely entirely on isNaN(), because isNaN(null) returns false, oddly enough. + * + * Alternatively, we could mask and shift n regardless of whether it's null/undefined/NaN, + * since JavaScript coerces such operands to zero, but I think there's "value" in seeing those + * values displayed differently. */ if (n == null || isNaN(n)) { while (cch-- > 0) s = '?' + s;