diff --git a/modules/pcjs/lib/bus.js b/modules/pcjs/lib/bus.js index bc71bd1ad..0ab637253 100644 --- a/modules/pcjs/lib/bus.js +++ b/modules/pcjs/lib/bus.js @@ -660,17 +660,17 @@ Bus.prototype.setMemoryBlocks = function(addr, size, aBlocks, type) * Whenever the CPU turns paging off, disablePageBlocks() must be called to restore the original physical * memory mapping. * - * This also requires that PAGEBLOCKS be true, ensuring that the Bus is preconfigured with 4Kb memory + * This also requires that PAGEBLOCKS be enabled, to ensure that the Bus is preconfigured with 4Kb memory * mapping granularity. * * The first time this function is called, aMemBlocks is stashed in aPhysBlocks, and aMemBlocks is then * reinitialized with special "unpaged" Memory blocks that know how to perform page directory/page table * lookup and replace themselves with special "paged" Memory blocks that reference memory from the - * appropriate block in aPhysBlocks. A parallel array, aMemPaged, keeps track of which blocks have been - * "paged", so that whenever CR3 is updated, just those blocks can be "unpaged" again. + * appropriate block in aPhysBlocks. A parallel array, aPageBlockNums, keeps track of which block numbers + * have been "paged", so that whenever CR3 is updated, just those blocks can be "unpaged" again. * * @this {Bus} - * @param {number} addrPD is the starting physical address of the CPU's page directory + * @param {number} addrPD is the starting physical address of the CPU's page directory (ie, from regCR3) */ Bus.prototype.enablePageBlocks = function(addrPD) { @@ -681,18 +681,44 @@ Bus.prototype.enablePageBlocks = function(addrPD) this.addrPD = addrPD; if (!this.aPhysBlocks) { this.aPhysBlocks = this.aMemBlocks; - var block = new Memory(null, 0, 0, Memory.TYPE.UNPAGED, null, this); + this.blockUnpaged = new Memory(null, 0, 0, Memory.TYPE.UNPAGED, null, this); this.aMemBlocks = new Array(this.blockTotal); for (var iBlock = 0; iBlock < this.blockTotal; iBlock++) { - this.aMemBlocks[iBlock] = block; + this.aMemBlocks[iBlock] = this.blockUnpaged; + } + } else { + for (var i = 0; i < this.aPageBlockNums.length; i++) { + this.aMemBlocks[this.aPageBlockNums[i]] = this.blockUnpaged; } } + this.aPageBlockNums = []; }; /** * mapPageBlock(addr, fWrite) * - * Locate the corresponding physical PDE, PTE and memory blocks for the given linear address. + * Locate the corresponding physical PDE, PTE and memory blocks for the given linear address, and then + * upgrade the block from an "unpaged" Memory block to a new "paged" Memory block; all future accesses to + * the current page will go directly to that block, instead of coming here through the "unpaged" block + * handlers. + * + * Note that since the incoming address (addr) is a linear address, we never need to mask it with busMask, + * but all the intermediate (PDE, PTE) and final physical addresses we calculate should still be masked. + * + * Granted, busMask on a 32-bit bus is generally going to be 0xffffffff (-1), so making might seem like + * a waste of time; however, if we decide to once again rely on busMask for emulating A20 wrap-around + * (instead of changing the physical memory map to alias the 2nd Mb to the 1st Mb), then performing + * consistent masking will be important. + * + * Also, addrPDE, addrPTE and addrPhys do not need any offsets added to them, because we immediately shift + * the offset portion of those addresses out (see TODOs below). But for now, at least for debugging and + * documentation purposes, my preference is to perform full address calculations. + * + * Besides, this should not be a performance-critical function; it's normally called only once per "unpaged" + * page. Obviously, if CR3 is constantly being updated, that will trigger repeated calls to enablePageBlocks(), + * which will perform our equivalent of a TLB flush (ie, resetting all "paged" blocks back to "unpaged" blocks). + * That would hurt our performance, but it would hurt performance on a real machine as well, so let's see + * what real-world scenarios we run into. * * @this {Bus} * @param {number} addr is a linear address @@ -702,7 +728,7 @@ Bus.prototype.enablePageBlocks = function(addrPD) Bus.prototype.mapPageBlock = function(addr, fWrite) { var offPDE = (addr & X86.LADDR.PDE.MASK) >>> X86.LADDR.PDE.SHIFT; - var addrPDE = this.cpu.regCR3 + offPDE; // TODO: adding offPDE could be eliminated, along with the busMask mask + var addrPDE = this.addrPD + offPDE; // TODO: adding offPDE could be eliminated var blockPDE = this.aPhysBlocks[(addrPDE & this.busMask) >>> this.blockShift]; var pde = blockPDE.readLong(offPDE); @@ -717,7 +743,7 @@ Bus.prototype.mapPageBlock = function(addr, fWrite) } var offPTE = (addr & X86.LADDR.PTE.MASK) >>> X86.LADDR.PTE.SHIFT; - var addrPTE = (pde & X86.PTE.FRAME) + offPTE; // TODO: adding offPTE could be eliminated, along with the busMask mask + var addrPTE = (pde & X86.PTE.FRAME) + offPTE; // TODO: adding offPTE could be eliminated var blockPTE = this.aPhysBlocks[(addrPTE & this.busMask) >>> this.blockShift]; var pte = blockPTE.readLong(offPTE); @@ -731,20 +757,42 @@ Bus.prototype.mapPageBlock = function(addr, fWrite) return null; } - var addrPhys = (pte & X86.PTE.FRAME) + (addr & X86.LADDR.OFFSET); // TODO: Adding OFFSET could be eliminated, along with the busMask mask + var addrPhys = (pte & X86.PTE.FRAME) + (addr & X86.LADDR.OFFSET); // TODO: Adding OFFSET could be eliminated var blockPhys = this.aPhysBlocks[(addrPhys & this.busMask) >>> this.blockShift]; /* * So we have the block containing the physical memory corresponding to the given linear address. * - * Now we create a new "paged" Memory block and record the physical block info using setPhysBlock(). + * Now we can create a new "paged" Memory block and record the physical block info using setPhysBlock(). */ var addrPage = addr & ~X86.LADDR.OFFSET; var blockPage = new Memory(addrPage, 0, this.blockSize, Memory.TYPE.PAGED); - blockPage.setPhysBlock(blockPage, blockPDE, offPDE, blockPTE, offPTE); + blockPage.setPhysBlock(blockPhys, blockPDE, offPDE, blockPTE, offPTE); + + var iBlock = addr >>> this.blockShift; + this.aMemBlocks[iBlock] = blockPage; + this.aPageBlockNums.push(iBlock); return blockPage; }; +/** + * disablePageBlocks() + * + * Whenever the CPU turns off paging, this function restores the original aMemBlocks. + * + * @this {Bus} + */ +Bus.prototype.disablePageBlocks = function() +{ + if (this.aPhysBlocks) { + this.aMemBlocks = this.aPhysBlocks; + this.aPhysBlocks = null; + this.blockUnpaged = null; + this.aPageBlockNums = null; + } + this.addrPD = X86.ADDR_INVALID; +}; + /** * getByte(addr) * diff --git a/modules/pcjs/lib/memory.js b/modules/pcjs/lib/memory.js index 0233fee8c..b724a09a2 100644 --- a/modules/pcjs/lib/memory.js +++ b/modules/pcjs/lib/memory.js @@ -471,6 +471,11 @@ Memory.prototype = { */ getPageBlock: function(addr, fWrite) { var block = this.bus.mapPageBlock(addr, fWrite); + /* + * If mapPageBlock() fails -- which can easily happen if the page is not present or has insufficient + * privileges -- then a fault will be triggered and block will be null. We still have to return a block, + * but it will be our old "unpaged" self. + */ return block || this; }, /** @@ -486,9 +491,9 @@ Memory.prototype = { setPhysBlock: function(blockPhys, blockPDE, offPDE, blockPTE, offPTE) { this.blockPhys = blockPhys; this.blockPDE = blockPDE; - this.iPDE = offPDE >> 2; // convert offPDE into an adw index (iPDE) + this.iPDE = offPDE >> 2; // convert offPDE into iPDE (an adw index) this.blockPTE = blockPTE; - this.iPTE = offPTE >> 2; // convert offPTE into an adw index (iPTE) + this.iPTE = offPTE >> 2; // convert offPTE into iPTE (an adw index) this.bitPTEDirty = this.adjustEndian(X86.PTE.ACCESSED | X86.PTE.DIRTY); this.bitPTEAccessed = this.adjustEndian(X86.PTE.ACCESSED); }, diff --git a/modules/pcjs/lib/x86cpu.js b/modules/pcjs/lib/x86cpu.js index 72403fc0e..66ebe93a4 100644 --- a/modules/pcjs/lib/x86cpu.js +++ b/modules/pcjs/lib/x86cpu.js @@ -2379,10 +2379,10 @@ X86CPU.prototype.setMSW = function(w) w |= (this.regCR0 & X86.CR0.MSW.PE) | X86.CR0.MSW.ON; this.regCR0 = (this.regCR0 & ~X86.CR0.MSW.MASK) | (w & X86.CR0.MSW.MASK); /* - * Since the 80286 cannot return to real-mode via this instruction, the only transition - * we must worry about is to protected-mode. And there's no harm calling setProtMode() - * if the CPU is already in protected-mode (we could certainly optimize the call out in that - * case, but this instruction isn't used frequently enough to warrant it). + * Since the 80286 cannot return to real-mode via this instruction, the only transition we + * must worry about is to protected-mode. And there's no harm calling setProtMode() if the + * CPU is already in protected-mode; we could certainly optimize out the call in that case, + * but the instruction isn't used frequently enough to warrant it. */ if (this.regCR0 & X86.CR0.MSW.PE) this.setProtMode(true); }; diff --git a/modules/pcjs/lib/x86func.js b/modules/pcjs/lib/x86func.js index a169f4702..4f920e842 100644 --- a/modules/pcjs/lib/x86func.js +++ b/modules/pcjs/lib/x86func.js @@ -1319,7 +1319,7 @@ X86.fnLAR = function LAR(dst, src) /** * fnLCR0(l) * - * This called on behalf of 80386 opcodes only (ie, MOV CR0,reg). + * This is called by an 80386 control instruction (ie, MOV CR0,reg). * * TODO: Determine which CR0 bits, if any, cannot be modified by MOV CR0,reg. * @@ -1330,6 +1330,30 @@ X86.fnLCR0 = function LCR0(l) { this.regCR0 = l; this.setProtMode(); + if (this.regCR0 & X86.CR0.PG) { + this.bus.enablePageBlocks(this.regCR3); + } else { + this.bus.disablePageBlocks(); + } +}; + +/** + * fnLCR3(l) + * + * This is called by an 80386 control instruction (ie, MOV CR3,reg) or an 80386 task switch. + * + * @this {X86CPU} + * @param {number} l + */ +X86.fnLCR3 = function LCR3(l) +{ + this.regCR3 = l; + /* + * Normal use of regCR3 involves adding a 0-4K (12-bit) offset to obtain a page directory entry, + * so let's ensure that the low 12 bits of regCR3 are always zero. + */ + this.assert(!(this.regCR3 & X86.LADDR.OFFSET)); + if (this.regCR0 & X86.CR0.PG) this.bus.enablePageBlocks(this.regCR3); }; /** @@ -3521,7 +3545,7 @@ X86.fnSrcNone = function SrcNone() * * @this {X86CPU} * @param {number} nFault - * @param {number} [nError] + * @param {number} [nError] (if omitted, no error code will be pushed) * @param {boolean} [fHalt] will halt the CPU if true *and* a Debugger is loaded */ X86.fnFault = function(nFault, nError, fHalt) @@ -3612,7 +3636,7 @@ X86.fnPageFault = function(addr, fPresent, fWrite) * * @this {X86CPU} * @param {number} nFault - * @param {number} [nError] + * @param {number} [nError] (if omitted, no error code will be reported) * @param {boolean} [fHalt] true if the CPU should always be halted, false if "it depends" * @return {boolean|undefined} true to block the fault (often desirable when fHalt is true), otherwise dispatch it */ diff --git a/modules/pcjs/lib/x86op0f.js b/modules/pcjs/lib/x86op0f.js index 9b8462c05..0c9c810b8 100644 --- a/modules/pcjs/lib/x86op0f.js +++ b/modules/pcjs/lib/x86op0f.js @@ -133,7 +133,7 @@ X86.opLOADALL = function LOADALL() { if (this.segCS.cpl) { /* - * You're not allowed to use LOADALL if the current privilege level is something other than zero + * You're not allowed to use LOADALL if the current privilege level is something other than zero. */ X86.fnFault.call(this, X86.EXCEPTION.GP_FAULT, 0, true); return; @@ -188,7 +188,7 @@ X86.opLOADALL = function LOADALL() X86.opCLTS = function CLTS() { if (this.segCS.cpl) { - X86.fnFault.call(this, X86.EXCEPTION.GP_FAULT, 0, true); + X86.fnFault.call(this, X86.EXCEPTION.GP_FAULT, 0); return; } this.regCR0 &= ~X86.CR0.MSW.TS; @@ -204,20 +204,35 @@ X86.opCLTS = function CLTS() * the appropriate control register into a special variable (regXX), which our helper function * (fnMOVxx) will use to replace the decoder's src operand. * + * From PCMag_Prog_TechRef, p.476: "The 80386 executes the MOV to/from control registers (CRn) + * regardless of the setting of the MOD field. The MOD field should be set to 0b11, but an early + * 80386 documentation error indicated that the MOD field value was a don't care. Early versions + * of the 80486 detect a MOD != 0b11 as an illegal opcode. This was changed in later versions to + * ignore the value of MOD. Assemblers that generate MOD != 0b11 for these instructions will fail + * on some 80486s." + * * @this {X86CPU} */ X86.opMOVrc = function MOVrc() { - var bModRM = this.getIPByte() | 0xc0; /* - * Unlike, say, opcode 0x8C (MOV word,sr), this opcode supports only registers, not memory; - * however, the 80386 apparently ignores the mod bits, treating any combination as if it was 0xc0. + * We address the MOD field problem (see above) by coercing it to 0b11 (0xc0), regardless. * - if ((bModRM & 0xc0) != 0xc0) { - X86.opInvalid.call(this); + * TODO: One issue not clearly addressed is if, when an assembler/compiler generated a bogus MOD value, + * it also generated the additional displacement bytes, if any, that would typically accompany such a MOD + * value. I assume not. + */ + var bModRM = this.getIPByte() | 0xc0; + + 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 = (bModRM & 0x38) >> 3; switch(reg) { case 0x0: @@ -250,26 +265,40 @@ X86.opMOVrc = function MOVrc() * * op=0x0F,0x22 (MOV creg,reg) * - * NOTE: Since the ModRM decoders deal only with general-purpose registers, we have to - * make a note of which general-purpose register will be overwritten, so that we can restore it - * after moving the modified value to the correct control register. + * NOTE: Since the ModRM decoders deal only with general-purpose registers, we have to make a note + * of which general-purpose register will be overwritten, so that we can restore it after moving the + * modified value to the correct control register. + * + * From PCMag_Prog_TechRef, p.476: "The 80386 executes the MOV to/from control registers (CRn) + * regardless of the setting of the MOD field. The MOD field should be set to 0b11, but an early + * 80386 documentation error indicated that the MOD field value was a don't care. Early versions + * of the 80486 detect a MOD != 0b11 as an illegal opcode. This was changed in later versions to + * ignore the value of MOD. Assemblers that generate MOD != 0b11 for these instructions will fail + * on some 80486s." * * @this {X86CPU} */ X86.opMOVcr = function MOVcr() { var temp; - var bModRM = this.getIPByte() | 0xc0; /* - * Unlike, say, opcode 0x8E (MOV sreg,word), this opcode supports only registers, not memory; - * however, the 80386 apparently ignores the mod bits, treating any combination as if it was 0xc0. - * TODO: Verify. + * We address the MOD field problem (see above) by coercing it to 0b11 (0xc0), regardless. * - if ((bModRM & 0xc0) != 0xc0) { - X86.opInvalid.call(this); + * TODO: One issue not clearly addressed is if, when an assembler/compiler generated a bogus MOD value, + * it also generated the additional displacement bytes, if any, that would typically accompany such a MOD + * value. I assume not. + */ + var bModRM = this.getIPByte() | 0xc0; + + 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 = (bModRM & 0x38) >> 3; switch(reg) { case 0x0: @@ -306,13 +335,9 @@ X86.opMOVcr = function MOVcr() this.regEDX = temp; break; case 0x3: - this.regCR3 = this.regEBX; - /* - * Normal use of regCR3 involves adding a 0-4K (12-bit) offset to obtain a page directory entry, so - * let's ensure that the low 12 bits of regCR3 are always zero. - */ - this.assert(!(this.regCR3 & X86.LADDR.OFFSET)); + reg = this.regEBX; this.regEBX = temp; + X86.fnLCR3.call(this, reg); break; } }; diff --git a/modules/pcjs/lib/x86seg.js b/modules/pcjs/lib/x86seg.js index 0f0696dfd..00f7596ce 100644 --- a/modules/pcjs/lib/x86seg.js +++ b/modules/pcjs/lib/x86seg.js @@ -414,6 +414,8 @@ X86Seg.prototype.checkWriteProtDisallowed = function checkWriteProtDisallowed(of * Of course, that all could have been avoided if IBM had heeded Intel's advice and not used Intel-reserved IDT * entries for PC interrupts. * + * TODO: Add 80386 TSS support (including CR3 support). + * * @this {X86Seg} * @param {number} selNew * @param {boolean} fNest is true if nesting, false if un-nesting