From 73086a7ac8b5ac976db29b58e6fa7741a8e248c6 Mon Sep 17 00:00:00 2001 From: Jeff Parsons Date: Tue, 5 May 2015 10:32:04 -0700 Subject: [PATCH] Moved paging functions from Bus to CPU --- modules/pcjs/lib/bus.js | 231 +++++--------------------------- modules/pcjs/lib/memory.js | 8 +- modules/pcjs/lib/x86cpu.js | 258 +++++++++++++++++++++++++++++++----- modules/pcjs/lib/x86func.js | 6 +- modules/pcjs/lib/x86op0f.js | 2 +- tests/pc/80386/tests.nasm | 32 +++-- 6 files changed, 281 insertions(+), 256 deletions(-) diff --git a/modules/pcjs/lib/bus.js b/modules/pcjs/lib/bus.js index 0ab637253..1599a7458 100644 --- a/modules/pcjs/lib/bus.js +++ b/modules/pcjs/lib/bus.js @@ -53,8 +53,8 @@ if (typeof module !== 'undefined') { * which then calls the initBus() method of all the other components. * * When initMemory() initializes the entire address space, it also passes aMemBlocks - * to the CPU object, so that the CPU can perform all its own address-to-block and memory - * block accesses directly. + * to the CPU object, so that the CPU can perform its own address-to-block calculations + * (essential, for example, when the CPU enables paging). * * For memory beyond the simple needs of the ROM and RAM components (ie, memory-mapped * devices), the address space must still be allocated through the Bus component via @@ -141,7 +141,7 @@ function Bus(parmsBus, cpu, dbg) * [1]: registered function to call for every I/O access * * The registered function is called with the port address, and if the access was triggered by the CPU, - * the linear address (LIP) that the access occurred from. + * the linear instruction pointer (LIP) at the point of access. * * WARNING: Unlike the (old) read and write memory notification functions, these support only one * pair of input/output functions per port. A more sophisticated architecture could support a list @@ -172,11 +172,6 @@ function Bus(parmsBus, cpu, dbg) this.ibtLastDelete = 0; } - if (PAGEBLOCKS) { - this.addrPD = null; - this.aPhysBlocks = null; - } - this.setReady(); } @@ -297,7 +292,7 @@ Bus.prototype.initMemory = function() for (var iBlock = 0; iBlock < this.blockTotal; iBlock++) { this.aMemBlocks[iBlock] = block; } - this.cpu.initMemory(this.aMemBlocks, this.blockShift, this.blockLimit, this.blockMask); + this.cpu.initMemory(this.aMemBlocks, this.blockShift); this.cpu.setAddressMask(this.busMask); }; @@ -518,9 +513,6 @@ Bus.prototype.setA20 = function(fEnable) var addrMask = (this.busMask & ~0x100000) | (fEnable? 0x100000 : 0); if (addrMask != this.busMask) { this.busMask = addrMask; - /* - * This callback is required only because the CPU "insists" on using its own memory access functions. - */ if (this.cpu) this.cpu.setAddressMask(addrMask); } } @@ -652,155 +644,13 @@ Bus.prototype.setMemoryBlocks = function(addr, size, aBlocks, type) } }; -/** - * enablePageBlocks(addrPD) - * - * Whenever the CPU turns on paging and/or updates CR3, this function is called to leverage the Bus's - * memory-mapping abilities and simulate the effects of the CPU's page directory and page table entries. - * Whenever the CPU turns paging off, disablePageBlocks() must be called to restore the original physical - * memory mapping. - * - * 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, 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 (ie, from regCR3) - */ -Bus.prototype.enablePageBlocks = function(addrPD) -{ - if (!PAGEBLOCKS) { - Component.error("PAGEBLOCK support missing"); - return; - } - this.addrPD = addrPD; - if (!this.aPhysBlocks) { - this.aPhysBlocks = this.aMemBlocks; - 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] = 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, 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 - * @param {boolean} fWrite (true if called for a write, false if for a read) - * @return {Memory|null} - */ -Bus.prototype.mapPageBlock = function(addr, fWrite) -{ - var offPDE = (addr & X86.LADDR.PDE.MASK) >>> X86.LADDR.PDE.SHIFT; - 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); - - if (!(pde & X86.PTE.PRESENT)) { - X86.fnPageFault.call(this.cpu, addr, false, fWrite); - return null; - } - - if (!(pde & X86.PTE.USER) && this.cpu.segCS.cpl == 3) { - X86.fnPageFault.call(this.cpu, addr, true, fWrite); - return null; - } - - var offPTE = (addr & X86.LADDR.PTE.MASK) >>> X86.LADDR.PTE.SHIFT; - 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); - - if (!(pte & X86.PTE.PRESENT)) { - X86.fnPageFault.call(this.cpu, addr, false, fWrite); - return null; - } - - if (!(pte & X86.PTE.USER) && this.cpu.segCS.cpl == 3) { - X86.fnPageFault.call(this.cpu, addr, true, fWrite); - return null; - } - - 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 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(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) * - * The CPU could use this, but the CPU also needs to update BACKTRACK states. There may also be a slight - * performance advantage calling its own getByte() method vs. calling through another object (ie, the Bus object). + * For physical addresses only; for linear addresses, use cpu.getByte(). * * @this {Bus} - * @param {number} addr is a physical (non-segmented) address + * @param {number} addr is a physical address * @return {number} byte (8-bit) value at that address */ Bus.prototype.getByte = function(addr) @@ -814,7 +664,7 @@ Bus.prototype.getByte = function(addr) * This is useful for the Debugger and other components that want to bypass getByte() breakpoint detection. * * @this {Bus} - * @param {number} addr is a physical (non-segmented) address + * @param {number} addr is a physical address * @return {number} byte (8-bit) value at that address */ Bus.prototype.getByteDirect = function(addr) @@ -825,12 +675,10 @@ Bus.prototype.getByteDirect = function(addr) /** * getShort(addr) * - * The CPU could use this, but the CPU also needs to update cycle counts, along with BACKTRACK states. - * There may also be a slight performance advantage calling its own getShort() method vs. calling through another - * object (ie, the Bus object). + * For physical addresses only; for linear addresses, use cpu.getShort(). * * @this {Bus} - * @param {number} addr is a physical (non-segmented) address + * @param {number} addr is a physical address * @return {number} word (16-bit) value at that address */ Bus.prototype.getShort = function(addr) @@ -849,7 +697,7 @@ Bus.prototype.getShort = function(addr) * This is useful for the Debugger and other components that want to bypass getShort() breakpoint detection. * * @this {Bus} - * @param {number} addr is a physical (non-segmented) address + * @param {number} addr is a physical address * @return {number} word (16-bit) value at that address */ Bus.prototype.getShortDirect = function(addr) @@ -865,12 +713,10 @@ Bus.prototype.getShortDirect = function(addr) /** * getLong(addr) * - * The CPU could use this, but the CPU also needs to update cycle counts, along with BACKTRACK states. - * There may also be a slight performance advantage calling its own getLong() method vs. calling through another - * object (ie, the Bus object). + * For physical addresses only; for linear addresses, use cpu.getLong(). * * @this {Bus} - * @param {number} addr is a physical (non-segmented) address + * @param {number} addr is a physical address * @return {number} long (32-bit) value at that address */ Bus.prototype.getLong = function(addr) @@ -890,7 +736,7 @@ Bus.prototype.getLong = function(addr) * This is useful for the Debugger and other components that want to bypass getLong() breakpoint detection. * * @this {Bus} - * @param {number} addr is a physical (non-segmented) address + * @param {number} addr is a physical address * @return {number} long (32-bit) value at that address */ Bus.prototype.getLongDirect = function(addr) @@ -907,11 +753,10 @@ Bus.prototype.getLongDirect = function(addr) /** * setByte(addr, b) * - * The CPU could use this, but the CPU also needs to update BACKTRACK states. There may also be a slight - * performance advantage calling its own setByte() method vs. calling through another object (ie, the Bus object). + * For physical addresses only; for linear addresses, use cpu.setByte(). * * @this {Bus} - * @param {number} addr is a physical (non-segmented) address + * @param {number} addr is a physical address * @param {number} b is the byte (8-bit) value to write (we truncate it to 8 bits to be safe) */ Bus.prototype.setByte = function(addr, b) @@ -926,7 +771,7 @@ Bus.prototype.setByte = function(addr, b) * memory protection (for example, this is an interface the ROM component could use to initialize ROM contents). * * @this {Bus} - * @param {number} addr is a physical (non-segmented) address + * @param {number} addr is a physical address * @param {number} b is the byte (8-bit) value to write (we truncate it to 8 bits to be safe) */ Bus.prototype.setByteDirect = function(addr, b) @@ -937,12 +782,10 @@ Bus.prototype.setByteDirect = function(addr, b) /** * setShort(addr, w) * - * The CPU could use this, but the CPU also needs to update cycle counts, along with BACKTRACK states. - * There may also be a slight performance advantage calling its own setShort() method vs. calling through another - * object (ie, the Bus object). + * For physical addresses only; for linear addresses, use cpu.setShort(). * * @this {Bus} - * @param {number} addr is a physical (non-segmented) address + * @param {number} addr is a physical address * @param {number} w is the word (16-bit) value to write (we truncate it to 16 bits to be safe) */ Bus.prototype.setShort = function(addr, w) @@ -964,7 +807,7 @@ Bus.prototype.setShort = function(addr, w) * memory protection (for example, this is an interface the ROM component could use to initialize ROM contents). * * @this {Bus} - * @param {number} addr is a physical (non-segmented) address + * @param {number} addr is a physical address * @param {number} w is the word (16-bit) value to write (we truncate it to 16 bits to be safe) */ Bus.prototype.setShortDirect = function(addr, w) @@ -982,12 +825,10 @@ Bus.prototype.setShortDirect = function(addr, w) /** * setLong(addr, l) * - * The CPU could use this, but the CPU also needs to update cycle counts, along with BACKTRACK states. - * There may also be a slight performance advantage calling its own setLong() method vs. calling through another - * object (ie, the Bus object). + * For physical addresses only; for linear addresses, use cpu.setLong(). * * @this {Bus} - * @param {number} addr is a physical (non-segmented) address + * @param {number} addr is a physical address * @param {number} l is the long (32-bit) value to write */ Bus.prototype.setLong = function(addr, l) @@ -1015,7 +856,7 @@ Bus.prototype.setLong = function(addr, l) * memory protection (for example, this is an interface the ROM component could use to initialize ROM contents). * * @this {Bus} - * @param {number} addr is a physical (non-segmented) address + * @param {number} addr is a physical address * @param {number} l is the long (32-bit) value to write */ Bus.prototype.setLongDirect = function(addr, l) @@ -1119,7 +960,7 @@ Bus.prototype.getBackTrackIndex = function(bto, off) * writeBackTrackObject(addr, bto, off) * * @this {Bus} - * @param {number} addr is a physical (non-segmented) address + * @param {number} addr is a physical address * @param {BackTrack|null} bto * @param {number} off */ @@ -1136,7 +977,7 @@ Bus.prototype.writeBackTrackObject = function(addr, bto, off) * readBackTrack(addr) * * @this {Bus} - * @param {number} addr is a physical (non-segmented) address + * @param {number} addr is a physical address * @return {number} */ Bus.prototype.readBackTrack = function(addr) @@ -1151,7 +992,7 @@ Bus.prototype.readBackTrack = function(addr) * writeBackTrack(addr, bti) * * @this {Bus} - * @param {number} addr is a physical (non-segmented) address + * @param {number} addr is a physical address * @param {number} bti */ Bus.prototype.writeBackTrack = function(addr, bti) @@ -1247,7 +1088,7 @@ Bus.prototype.isBackTrackWeak = function(bti) * updateBackTrackCode(addr, bti) * * @this {Bus} - * @param {number} addr is a physical (non-segmented) address + * @param {number} addr is a physical address * @param {number} bti */ Bus.prototype.updateBackTrackCode = function(addr, bti) @@ -1514,17 +1355,17 @@ Bus.prototype.addPortInputTable = function(component, table, offset) }; /** - * checkPortInputNotify(port, addrFrom) + * checkPortInputNotify(port, addrLIP) * * @this {Bus} * @param {number} port - * @param {number} [addrFrom] is the LIP value at the time of the input + * @param {number} [addrLIP] is the LIP value at the time of the input * @return {number} simulated port value (0xff if none) * * NOTE: It seems that at least parts of the ROM BIOS (like the RS-232 probes around F000:E5D7 in the 5150 BIOS) * assume that ports for non-existent hardware return 0xff rather than 0x00, hence my new default (0xff) below. */ -Bus.prototype.checkPortInputNotify = function(port, addrFrom) +Bus.prototype.checkPortInputNotify = function(port, addrLIP) { var bIn = 0xff; var aNotify = this.aPortInputNotify[port]; @@ -1534,7 +1375,7 @@ Bus.prototype.checkPortInputNotify = function(port, addrFrom) } if (aNotify !== undefined) { if (aNotify[1]) { - bIn = aNotify[1].call(aNotify[0], port, addrFrom); + bIn = aNotify[1].call(aNotify[0], port, addrLIP); } if (DEBUGGER && this.dbg && this.fPortInputBreakAll != aNotify[2]) { this.dbg.checkPortInput(port, bIn); @@ -1542,7 +1383,7 @@ Bus.prototype.checkPortInputNotify = function(port, addrFrom) } else { if (DEBUGGER && this.dbg) { - this.dbg.messageIO(this, port, null, addrFrom); + this.dbg.messageIO(this, port, null, addrLIP); if (this.fPortInputBreakAll) this.dbg.checkPortInput(port, bIn); } } @@ -1634,19 +1475,19 @@ Bus.prototype.addPortOutputTable = function(component, table, offset) }; /** - * checkPortOutputNotify(port, bOut, addrFrom) + * checkPortOutputNotify(port, bOut, addrLIP) * * @this {Bus} * @param {number} port * @param {number} bOut - * @param {number} [addrFrom] is the LIP value at the time of the output + * @param {number} [addrLIP] is the LIP value at the time of the output */ -Bus.prototype.checkPortOutputNotify = function(port, bOut, addrFrom) +Bus.prototype.checkPortOutputNotify = function(port, bOut, addrLIP) { var aNotify = this.aPortOutputNotify[port]; if (aNotify !== undefined) { if (aNotify[1]) { - aNotify[1].call(aNotify[0], port, bOut, addrFrom); + aNotify[1].call(aNotify[0], port, bOut, addrLIP); } if (DEBUGGER && this.dbg && this.fPortOutputBreakAll != aNotify[2]) { this.dbg.checkPortOutput(port, bOut); @@ -1654,7 +1495,7 @@ Bus.prototype.checkPortOutputNotify = function(port, bOut, addrFrom) } else { if (DEBUGGER && this.dbg) { - this.dbg.messageIO(this, port, bOut, addrFrom); + this.dbg.messageIO(this, port, bOut, addrLIP); if (this.fPortOutputBreakAll) this.dbg.checkPortOutput(port, bOut); } } diff --git a/modules/pcjs/lib/memory.js b/modules/pcjs/lib/memory.js index b724a09a2..c62c51137 100644 --- a/modules/pcjs/lib/memory.js +++ b/modules/pcjs/lib/memory.js @@ -94,9 +94,9 @@ var littleEndian = (TYPEDARRAYS? (function() { * @param {number} [size] of block's buffer in bytes (0 for none); must be a multiple of 4 * @param {number} [type] is one of the Memory.TYPE constants (default is Memory.TYPE.NONE) * @param {Object} [controller] is an optional memory controller component - * @param {Bus} [bus] + * @param {X86CPU} [cpu] is required for UNPAGED memory blocks, so that the CPU can map it to a PAGED block */ -function Memory(addr, used, size, type, controller, bus) +function Memory(addr, used, size, type, controller, cpu) { var i; this.id = (Memory.idBlock += 2); @@ -108,7 +108,7 @@ function Memory(addr, used, size, type, controller, bus) this.type = type || Memory.TYPE.NONE; this.fReadOnly = (type == Memory.TYPE.ROM); this.controller = null; - this.bus = bus; + this.cpu = cpu; this.fDirty = this.fDirtyEver = false; if (BACKTRACK) { @@ -470,7 +470,7 @@ Memory.prototype = { * @return {Memory} */ getPageBlock: function(addr, fWrite) { - var block = this.bus.mapPageBlock(addr, fWrite); + var block = this.cpu.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, diff --git a/modules/pcjs/lib/x86cpu.js b/modules/pcjs/lib/x86cpu.js index 3e6915f7c..88a958d07 100644 --- a/modules/pcjs/lib/x86cpu.js +++ b/modules/pcjs/lib/x86cpu.js @@ -38,6 +38,7 @@ if (typeof module !== 'undefined') { var Component = require("../../shared/lib/component"); var Messages = require("./messages"); var Bus = require("./bus"); + var Memory = require("./memory"); var State = require("./state"); var CPU = require("./cpu"); var X86 = require("./x86"); @@ -164,8 +165,9 @@ function X86CPU(parmsCPU) * We're just declaring aMemBlocks and associated Bus parameters here; they'll be initialized by initMemory() * when the Bus is initialized. */ - this.aMemBlocks = []; - this.busMask = this.blockShift = this.blockLimit = this.blockMask = 0; + this.aBusBlocks = this.aMemBlocks = []; + this.busMask = this.memMask = 0; + this.blockShift = this.blockSize = this.blockLimit = this.blockTotal = this.blockMask = 0; if (SAMPLER) { /* @@ -574,7 +576,7 @@ X86CPU.PREFETCH = { }; /** - * initMemory(aMemBlocks, busMask, blockShift, blockLimit, blockMask) + * initMemory(aMemBlocks, blockShift) * * Notification from Bus.initMemory(), giving us direct access to the entire memory space * (aMemBlocks). @@ -588,7 +590,7 @@ X86CPU.PREFETCH = { * ... * 7: [ -1, 0] * - * where tag is the physical address of the byte that's been prefetched, and b is the + * where tag is the linear address of the byte that's been prefetched, and b is the * value of the byte. N is currently 8 (PREFETCH.ARRAY), but it can be any power-of-two * that is equal to or greater than (PREFETCH.QUEUE), the effective size of the prefetch * queue (6 on an 8086, 4 on an 8088; currently hard-coded to the latter). All slots @@ -618,15 +620,21 @@ X86CPU.PREFETCH = { * @this {X86CPU} * @param {Array} aMemBlocks * @param {number} blockShift - * @param {number} blockLimit - * @param {number} blockMask */ -X86CPU.prototype.initMemory = function(aMemBlocks, blockShift, blockLimit, blockMask) +X86CPU.prototype.initMemory = function(aMemBlocks, blockShift) { + /* + * aBusBlocks preserves the Bus block array for the life of the machine, whereas aMemBlocks + * will be altered if/when the CPU enables paging. PAGEBLOCKS must be true when using Memory + * blocks to simulate paging, ensuring that physical blocks and pages have the same size (4Kb). + */ + this.aBusBlocks = aMemBlocks; this.aMemBlocks = aMemBlocks; this.blockShift = blockShift; - this.blockLimit = blockLimit; - this.blockMask = blockMask; + this.blockSize = 1 << this.blockShift; + this.blockLimit = this.blockSize - 1; + this.blockTotal = aMemBlocks.length; + this.blockMask = this.blockTotal - 1; if (PREFETCH) { this.nBusCycles = 0; this.aPrefetch = new Array(X86CPU.PREFETCH.ARRAY); @@ -640,14 +648,174 @@ X86CPU.prototype.initMemory = function(aMemBlocks, blockShift, blockLimit, block /** * setAddressMask(busMask) * - * Notification from Bus.setA20(), called whenever the physical A20 line changes + * Notification from Bus.initMemory() and Bus.setA20(); the latter calls us whenever the physical + * A20 line changes (note that on a 20-bit bus machine, address lines A20 and higher are always zero). + * + * For 32-bit bus machines (eg, 80386), busMask is never changed after the initial call, because A20 + * wrap-around is simulated by changing the physical memory map rather than altering the A20 bit in busMask. + * + * We maintain memMask separate from busMask, because when paging is enabled on the 80386, the CPU memory + * functions are now dealing with linear addresses rather than physical addresses, so it would be incorrect + * to apply busMask to those addresses; memMask must remain 0xffffffff (-1) for the duration. If we change + * how A20 is simulated on the 80386, then enablePageBlocks() and disablePageBlocks() will need to override + * memMask appropriately. + * + * TODO: Ideally, we would eliminate masking altogether of 32-bit addresses, but that would require different + * sets of memory access functions for different machines. * * @this {X86CPU} * @param {number} busMask */ X86CPU.prototype.setAddressMask = function(busMask) { - this.busMask = busMask; + this.busMask = this.memMask = busMask; +}; + +/** + * enablePageBlocks() + * + * Whenever the CPU turns on paging and/or updates CR3, this function is called to update our copy + * of the Bus block array, to simulate paging. Whenever the CPU turns paging off, disablePageBlocks() + * must be called to restore our copy of the Bus block array to its original (physical) mapping. + * + * This also requires PAGEBLOCKS be enabled, ensuring that the Bus is configured with a 4Kb block size. + * + * The first time this function is called, aMemBlocks and aBusBlocks are identical, so aMemBlocks is + * 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 aBusBlocks. A parallel array, aBlocksPaged, keeps track (by block number) of + * which blocks have been PAGED, so that whenever CR3 is updated, those blocks can be UNPAGED again. + * + * @this {X86CPU} + */ +X86CPU.prototype.enablePageBlocks = function() +{ + if (!PAGEBLOCKS) { + this.setError("PAGEBLOCK support required"); + return; + } + if (this.aMemBlocks === this.aBusBlocks) { + this.aMemBlocks = new Array(this.blockTotal); + this.blockUnpaged = new Memory(null, 0, 0, Memory.TYPE.UNPAGED, null, this); + for (var iBlock = 0; iBlock < this.blockTotal; iBlock++) { + this.aMemBlocks[iBlock] = this.blockUnpaged; + } + } else { + for (var i = 0; i < this.aBlocksPaged.length; i++) { + this.aMemBlocks[this.aBlocksPaged[i]] = this.blockUnpaged; + } + } + this.aBlocksPaged = []; +}; + +/** + * mapPageBlock(addr, fWrite) + * + * 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. 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 {X86CPU} + * @param {number} addr is a linear address + * @param {boolean} fWrite (true if called for a write, false if for a read) + * @return {Memory|null} + */ +X86CPU.prototype.mapPageBlock = function(addr, fWrite) +{ + var offPDE = (addr & X86.LADDR.PDE.MASK) >>> X86.LADDR.PDE.SHIFT; + var addrPDE = this.regCR3 + offPDE; + + /* + * bus.getLong(addrPDE) would be simpler, but setPhysBlock() needs to know blockPDE and offPDE, too. + * TODO: Since we're immediately shifting addrPDE by blockShift, then we could also skip adding offPDE. + */ + var blockPDE = this.aBusBlocks[(addrPDE & this.busMask) >>> this.blockShift]; + var pde = blockPDE.readLong(offPDE); + + if (!(pde & X86.PTE.PRESENT)) { + X86.fnPageFault.call(this, addr, false, fWrite); + return null; + } + + if (!(pde & X86.PTE.USER) && this.segCS.cpl == 3) { + X86.fnPageFault.call(this, addr, true, fWrite); + return null; + } + + var offPTE = (addr & X86.LADDR.PTE.MASK) >>> X86.LADDR.PTE.SHIFT; + var addrPTE = (pde & X86.PTE.FRAME) + offPTE; + + /* + * bus.getLong(addrPTE) would be simpler, but setPhysBlock() needs to know blockPTE and offPTE, too. + * TODO: Since we're immediately shifting addrPDE by blockShift, then we could also skip adding offPTE. + */ + var blockPTE = this.aBusBlocks[(addrPTE & this.busMask) >>> this.blockShift]; + var pte = blockPTE.readLong(offPTE); + + if (!(pte & X86.PTE.PRESENT)) { + X86.fnPageFault.call(this, addr, false, fWrite); + return null; + } + + if (!(pte & X86.PTE.USER) && this.segCS.cpl == 3) { + X86.fnPageFault.call(this, addr, true, fWrite); + return null; + } + + var addrPhys = (pte & X86.PTE.FRAME) + (addr & X86.LADDR.OFFSET); + /* + * TODO: Since we're immediately shifting addrPhys by blockShift, we could also skip adding the addr's offset. + */ + var blockPhys = this.aBusBlocks[(addrPhys & this.busMask) >>> this.blockShift]; + + /* + * So we have the block containing the physical memory corresponding to the given linear address. + * + * 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(blockPhys, blockPDE, offPDE, blockPTE, offPTE); + + var iBlock = addr >>> this.blockShift; + this.aMemBlocks[iBlock] = blockPage; + this.aBlocksPaged.push(iBlock); + return blockPage; +}; + +/** + * disablePageBlocks() + * + * Whenever the CPU turns off paging, this function restores the CPU's original aMemBlocks. + * + * @this {X86CPU} + */ +X86CPU.prototype.disablePageBlocks = function() +{ + if (this.aMemBlocks != this.aBusBlocks) { + this.aMemBlocks = this.aBusBlocks; + this.blockUnpaged = null; + this.aBlocksPaged = null; + } }; /** @@ -1037,7 +1205,7 @@ X86CPU.prototype.resetRegs = function() /* * Segment registers used to be defined as separate variables (eg, regCS and regCS0 stored the segment - * number and base physical address, respectively), but segment registers are now defined as X86Seg objects. + * number and base linear address, respectively), but segment registers are now defined as X86Seg objects. */ this.segCS = new X86Seg(this, X86Seg.ID.CODE, "CS"); this.segDS = new X86Seg(this, X86Seg.ID.DATA, "DS"); @@ -1337,7 +1505,7 @@ X86CPU.prototype.checkIntNotify = function(nInt) * another interrupt notification function is intercepting, so use it as an advisory value only. * * @this {X86CPU} - * @param {number} addr is a physical (non-segmented) address + * @param {number} addr is a linear address * @param {function(number)} fn is an interrupt-return notification function */ X86CPU.prototype.addIntReturn = function(addr, fn) @@ -1363,7 +1531,7 @@ X86CPU.prototype.addIntReturn = function(addr, fn) * if the count is zero, for maximum performance. * * @this {X86CPU} - * @param {number} addr is a physical (non-segmented) address + * @param {number} addr is a linear address */ X86CPU.prototype.checkIntReturn = function(addr) { @@ -1552,7 +1720,7 @@ X86CPU.prototype.getSeg = function(sName) return this.segNULL; default: /* - * HACK: We return a fake segment register object in which only the base physical address is valid, + * HACK: We return a fake segment register object in which only the base linear address is valid, * because that's all the caller provided (ie, we must be restoring from an older state). */ this.assert(typeof sName == "number"); @@ -2518,27 +2686,33 @@ X86CPU.prototype.setBinding = function(sHTMLType, sBinding, control) /** * getByte(addr) * + * Use bus.getByte() for physical addresses, and cpu.getByte() for linear addresses; the latter takes care + * of paging, cycle counts, and BACKTRACK states, if any. + * * @this {X86CPU} - * @param {number} addr is a physical (non-segmented) address + * @param {number} addr is a linear address * @return {number} byte (8-bit) value at that address */ X86CPU.prototype.getByte = function getByte(addr) { if (BACKTRACK) this.backTrack.btiMemLo = this.bus.readBackTrack(addr); - return this.aMemBlocks[(addr & this.busMask) >>> this.blockShift].readByte(addr & this.blockLimit, addr); + return this.aMemBlocks[(addr & this.memMask) >>> this.blockShift].readByte(addr & this.blockLimit, addr); }; /** * getShort(addr) * + * Use bus.getShort() for physical addresses, and cpu.getShort() for linear addresses; the latter takes care + * of paging, cycle counts, and BACKTRACK states, if any. + * * @this {X86CPU} - * @param {number} addr is a physical (non-segmented) address + * @param {number} addr is a linear address * @return {number} word (16-bit) value at that address */ X86CPU.prototype.getShort = function getShort(addr) { var off = addr & this.blockLimit; - var iBlock = (addr & this.busMask) >>> this.blockShift; + var iBlock = (addr & this.memMask) >>> this.blockShift; /* * On the 8088, it takes 4 cycles to read the additional byte REGARDLESS whether the address is odd or even. * TODO: For the 8086, the penalty is actually "(addr & 0x1) << 2" (4 additional cycles only when the address is odd). @@ -2558,14 +2732,17 @@ X86CPU.prototype.getShort = function getShort(addr) /** * getLong(addr) * + * Use bus.getLong() for physical addresses, and cpu.getLong() for linear addresses; the latter takes care + * of paging, cycle counts, and BACKTRACK states, if any. + * * @this {X86CPU} - * @param {number} addr is a physical (non-segmented) address + * @param {number} addr is a linear address * @return {number} long (32-bit) value at that address */ X86CPU.prototype.getLong = function getLong(addr) { var off = addr & this.blockLimit; - var iBlock = (addr & this.busMask) >>> this.blockShift; + var iBlock = (addr & this.memMask) >>> this.blockShift; if (BACKTRACK) { this.backTrack.btiMemLo = this.bus.readBackTrack(addr); this.backTrack.btiMemHi = this.bus.readBackTrack(addr + 1); @@ -2580,27 +2757,33 @@ X86CPU.prototype.getLong = function getLong(addr) /** * setByte(addr, b) * + * Use bus.setByte() for physical addresses, and cpu.setByte() for linear addresses; the latter takes care + * of paging, cycle counts, and BACKTRACK states, if any. + * * @this {X86CPU} - * @param {number} addr is a physical (non-segmented) address + * @param {number} addr is a linear address * @param {number} b is the byte (8-bit) value to write (which we truncate to 8 bits; required by opSTOSb) */ X86CPU.prototype.setByte = function setByte(addr, b) { if (BACKTRACK) this.bus.writeBackTrack(addr, this.backTrack.btiMemLo); - this.aMemBlocks[(addr & this.busMask) >>> this.blockShift].writeByte(addr & this.blockLimit, b & 0xff, addr); + this.aMemBlocks[(addr & this.memMask) >>> this.blockShift].writeByte(addr & this.blockLimit, b & 0xff, addr); }; /** * setShort(addr, w) * + * Use bus.setShort() for physical addresses, and cpu.setShort() for linear addresses; the latter takes care + * of paging, cycle counts, and BACKTRACK states, if any. + * * @this {X86CPU} - * @param {number} addr is a physical (non-segmented) address + * @param {number} addr is a linear address * @param {number} w is the word (16-bit) value to write (which we truncate to 16 bits to be safe) */ X86CPU.prototype.setShort = function setShort(addr, w) { var off = addr & this.blockLimit; - var iBlock = (addr & this.busMask) >>> this.blockShift; + var iBlock = (addr & this.memMask) >>> this.blockShift; /* * On the 8088, it takes 4 cycles to write the additional byte REGARDLESS whether the address is odd or even. * TODO: For the 8086, the penalty is actually "(addr & 0x1) << 2" (4 additional cycles only when the address is odd). @@ -2622,14 +2805,17 @@ X86CPU.prototype.setShort = function setShort(addr, w) /** * setLong(addr, l) * + * Use bus.setLong() for physical addresses, and cpu.setLong() for linear addresses; the latter takes care + * of paging, cycle counts, and BACKTRACK states, if any. + * * @this {X86CPU} - * @param {number} addr is a physical (non-segmented) address + * @param {number} addr is a linear address * @param {number} l is the long (32-bit) value to write */ X86CPU.prototype.setLong = function setLong(addr, l) { var off = addr & this.blockLimit; - var iBlock = (addr & this.busMask) >>> this.blockShift; + var iBlock = (addr & this.memMask) >>> this.blockShift; this.nStepCycles -= this.cycleCounts.nWordCyclePenalty; if (BACKTRACK) { @@ -2931,7 +3117,7 @@ X86CPU.prototype.setSOWord = function(seg, off, w) * Return the next byte from the prefetch queue, prefetching it now if necessary. * * @this {X86CPU} - * @param {number} addr is a physical (non-segmented) address + * @param {number} addr is a linear address * @return {number} byte (8-bit) value at that address */ X86CPU.prototype.getBytePrefetch = function(addr) @@ -2951,10 +3137,10 @@ X86CPU.prototype.getBytePrefetch = function(addr) * with side-effects we may not want, and in any case, while it seemed to improve Safari's performance slightly, * it did nothing for the oddball Chrome performance I'm seeing with PREFETCH enabled. * - * b = this.aMemBlocks[(addr & this.busMask) >>> this.blockShift].readByte(addr & this.blockLimit, addr); + * b = this.aMemBlocks[(addr & this.memMask) >>> this.blockShift].readByte(addr & this.blockLimit, addr); * this.nBusCycles += 4; * this.cbPrefetchValid = 0; - * this.addrPrefetchHead = (addr + 1) & this.busMask; + * this.addrPrefetchHead = (addr + 1) & this.memMask; * return b; */ } @@ -2981,7 +3167,7 @@ X86CPU.prototype.getBytePrefetch = function(addr) * the prefetch queue, we're taking the easy way out and simply calling getBytePrefetch() twice. * * @this {X86CPU} - * @param {number} addr is a physical (non-segmented) address + * @param {number} addr is a linear address * @return {number} short (16-bit) value at that address */ X86CPU.prototype.getShortPrefetch = function(addr) @@ -2996,7 +3182,7 @@ X86CPU.prototype.getShortPrefetch = function(addr) * easy way out and call getShortPrefetch() twice. * * @this {X86CPU} - * @param {number} addr is a physical (non-segmented) address + * @param {number} addr is a linear address * @return {number} long (32-bit) value at that address */ X86CPU.prototype.getLongPrefetch = function(addr) @@ -3008,7 +3194,7 @@ X86CPU.prototype.getLongPrefetch = function(addr) * getWordPrefetch(addr) * * @this {X86CPU} - * @param {number} addr is a physical (non-segmented) address + * @param {number} addr is a linear address * @return {number} short (16-bit) or long (32-bit value as appropriate */ X86CPU.prototype.getWordPrefetch = function(addr) @@ -3028,10 +3214,10 @@ X86CPU.prototype.fillPrefetch = function(n) { while (n-- > 0 && this.cbPrefetchQueued < X86CPU.PREFETCH.QUEUE) { var addr = this.addrPrefetchHead; - var b = this.aMemBlocks[(addr & this.busMask) >>> this.blockShift].readByte(addr & this.blockLimit, addr); + var b = this.aMemBlocks[(addr & this.memMask) >>> this.blockShift].readByte(addr & this.blockLimit, addr); this.aPrefetch[this.iPrefetchHead] = b | (addr << 8); if (MAXDEBUG) this.printMessage(" fillPrefetch[" + this.iPrefetchHead + "]: " + str.toHex(addr) + ":" + str.toHexByte(b)); - this.addrPrefetchHead = (addr + 1) & this.busMask; + this.addrPrefetchHead = (addr + 1) & this.memMask; this.iPrefetchHead = (this.iPrefetchHead + 1) & X86CPU.PREFETCH.MASK; this.cbPrefetchQueued++; /* @@ -3049,7 +3235,7 @@ X86CPU.prototype.fillPrefetch = function(n) * Empty the prefetch queue. * * @this {X86CPU} - * @param {number} addr is a physical (non-segmented) address of the current program counter (regLIP) + * @param {number} addr is a linear address of the current program counter (regLIP) */ X86CPU.prototype.flushPrefetch = function(addr) { diff --git a/modules/pcjs/lib/x86func.js b/modules/pcjs/lib/x86func.js index 4f920e842..41418d98f 100644 --- a/modules/pcjs/lib/x86func.js +++ b/modules/pcjs/lib/x86func.js @@ -1331,9 +1331,9 @@ X86.fnLCR0 = function LCR0(l) this.regCR0 = l; this.setProtMode(); if (this.regCR0 & X86.CR0.PG) { - this.bus.enablePageBlocks(this.regCR3); + this.enablePageBlocks(); } else { - this.bus.disablePageBlocks(); + this.disablePageBlocks(); } }; @@ -1353,7 +1353,7 @@ X86.fnLCR3 = function LCR3(l) * 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); + if (this.regCR0 & X86.CR0.PG) this.enablePageBlocks(); }; /** diff --git a/modules/pcjs/lib/x86op0f.js b/modules/pcjs/lib/x86op0f.js index d2e258755..d8ca41f67 100644 --- a/modules/pcjs/lib/x86op0f.js +++ b/modules/pcjs/lib/x86op0f.js @@ -351,7 +351,7 @@ X86.opMOVcr = function MOVcr() * Also, in 16-bit code, even though a signed rel16 value would seem to imply a range of -32768 * to +32767, any location within a 64Kb code segment outside that range can be reached by choosing * a displacement in the opposite direction, causing the 16-bit value in EIP to underflow or overflow; - * any underflow or overflow doesn't matter, because only the low 16 bits of EIP are used when a + * any underflow or overflow doesn't matter, because only the low 16 bits of EIP are updated when a * 16-bit OPERAND size is in effect. * * In fact, for 16-bit jumps, it's simpler to always think of rel16 as an UNSIGNED value added to diff --git a/tests/pc/80386/tests.nasm b/tests/pc/80386/tests.nasm index b2d1107a4..0a376d30b 100644 --- a/tests/pc/80386/tests.nasm +++ b/tests/pc/80386/tests.nasm @@ -1,8 +1,8 @@ - cpu 386 ; ; This file is designed to run as a ROM replacement, but it has a .COM extension because it's ; also designed to run as a COM file under DOS (hence the "org 0x100"). ; + cpu 386 org 0x100 section .text @@ -22,14 +22,14 @@ ACC_TYPE_DATA_WRITABLE equ 0x1200 EXT_BIG equ 0x0040 -SEG_CODE_REAL equ 0xf000 -SEG_CODE_PROT equ 0x0008 -SEG_DATA_PROT equ 0x0010 +CSEG_REAL equ 0xf000 +CSEG_PROT equ 0x0008 +DSEG_PROT equ 0x0010 CR0_MSW_PE equ 0x0001 ; -; descDT defines a descriptor, given a base (%1), limit (%2), type (%3), dpl (%4), and bigness (%5) +; descDT defines a descriptor, given a base (%1), limit (%2), type (%3), dpl (%4), and ext (%5) ; %macro defDesc 1-5 0,0,0,0 dw (%2 & 0x0000ffff) @@ -43,21 +43,20 @@ start: mov eax,0x44332211 mov ecx,0x88776655 mul ecx div ecx - jnz near goProt + jnz near goProt ; apparently we have to tell NASM "near" because this is a forward reference + times 32768 nop ; lots of NOPs to force a 16-bit conditional jump - times 32768 nop - -romGDT: defDesc 0 - defDesc 0x000f0000,0x0000ffff,ACC_TYPE_CODE_READABLE,0 - defDesc 0x00000000,0x000fffff,ACC_TYPE_DATA_WRITABLE,0 +romGDT: defDesc 0 ; the first descriptor in any descriptor table is always a dud (it corresponds to the null descriptor) + defDesc 0x000f0000,0x0000ffff,ACC_TYPE_CODE_READABLE + defDesc 0x00000000,0x000fffff,ACC_TYPE_DATA_WRITABLE goProt: lgdt [cs:romGDT] mov eax,cr0 or eax,CR0_MSW_PE mov cr0,eax - jmp dword SEG_CODE_PROT:inProt + jmp dword CSEG_PROT:inProt -inProt: mov ax,SEG_DATA_PROT +inProt: mov ax,DSEG_PROT mov ds,ax ; ; Do some protected-mode tests... @@ -66,20 +65,19 @@ inProt: mov ax,SEG_DATA_PROT goReal: mov eax,cr0 and eax,~CR0_MSW_PE mov cr0,eax - jmp dword SEG_CODE_REAL:inReal + jmp dword CSEG_REAL:inReal inReal: or eax,1 - jnz start + jnz start ; apparently we do NOT have to say "near" here since this is a backward reference ; ; Fill the remaining space with NOPs until we get to target offset 0xFFF0. - ; ; Note that we subtract 0x100 from the target offset because we're ORG'ed at 0x100. ; times 0xfff0-0x100-($-$$) nop bits 16 - jmp SEG_CODE_REAL:start + jmp CSEG_REAL:start db 0x20 db '04/04/15'