From f0a848910b75dbf371ed27567b78705439c7bc5c Mon Sep 17 00:00:00 2001 From: Neil Webber Date: Sat, 16 Sep 2023 14:58:36 -0500 Subject: [PATCH] all-assembler mmu test works, also verified in SIMH --- pdptests.py | 499 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 302 insertions(+), 197 deletions(-) diff --git a/pdptests.py b/pdptests.py index bb651a5..15f20e4 100644 --- a/pdptests.py +++ b/pdptests.py @@ -661,28 +661,31 @@ class TestMethods(unittest.TestCase): trapexpected = i self.assertEqual(p.r[1], trapexpected) - def test_mmu_updown(self): - # test the page length field support in both up and down directions - - # XXX whether it was wise to code this test as a magnum opus - # of assembler prowess is well open to debate. On the plus - # side, it certainly exercises a bunch of features besides - # just testing the MMU page length functionality. + def _make_updown(self, taddr, uaddr, kaddr, uphysdata=0o200000): + # Makes the instruction blocks required for the mmu_updown tests. + # This is separated out so (as described below) it becomes possible + # to execute this in isolation just to generate the instructions + # and use them in other simulators (e.g., SIMH) + # + # Returns a tuple (t, u, k) each being an InstructionBlock cn = self.usefulconstants() - p = self.make_pdp() # Two tests - up and down. # # In both tests, KERNEL I space page 0 is mapped to physical 0 # and KERNEL I space page 7 is mapped to the I/O page. # I/D separation is NOT enabled for KERNEL. + # taddr and kaddr MUST be in this first 8K of memory (if only + # because the mapping setup doesn't map anything else) + # + # USER I space is mapped to uaddr which can be any 8K boundary + # 0o20000 and beyond. # - # USER I space is mapped to 0o20000. # All 64K of USER D space is mapped to 64K of physical memory - # from 0o200000 (not a typo) to 0o400000 (not a typo), but with - # a bizarre page length scheme according to UP or DOWN phase of - # the test as below. I/D separation is (obviously) enabled for USER. + # from uphysdata to uphysdata + 64K, but with a bizarre page + # length scheme according to UP or DOWN phase of the test as + # below. I/D separation is (obviously) enabled for USER. # All 64K of that memory is filled with sequential words such # that (vaddr) + vaddr = 0o123456 (where vaddr is a user D space # virtual address 0 .. 65534). This gives the test two ways to verify @@ -709,11 +712,7 @@ class TestMethods(unittest.TestCase): # same 0, 16, 32 .. progression (of valid "blocks") but they # are at the end of the segments. - # these instructions do initialization common to both up/down cases - kernel_addr = 0o6000 # arbitrary start for all this - traps_addr = 0o4000 # make sure enough room before kernel_addr - - # this code will go at traps_addr + # this code will go at taddr with ASM() as tr: # The trap handler for MMU faults and the trap 0 / trap 1 from # the user code (when there is no MMU fault). It is integrated @@ -732,22 +731,26 @@ class TestMethods(unittest.TestCase): tr.mov('(sp)+', 'r3') # r3 is now the trap instruction tr.bic(0o177400, 'r3') tr.cmp(1, 'r3') - tr.beq(1) + tr.beq(2) # skip the HALT and the MMU entry point # this was not a "good" trap, the user code failed tr.halt() - tr.br(1) # skip over the MMU entry point - tr.label('TrapMMU') tr.clr('r3') # indicate MMU fault case + # both Utrap and TrapMMU join in common here on out # see if the access was good/bad as expected tr.cmp('r2', 'r3') tr.beq(1) # jump over the HALT tr.halt() # NOPE, something wrong! + # the user mode code specifically avoids '(r0)+' + # to avoid ambiguity in machine types for when the + # autoincrement happens in the face of MMU aborts. + # Bump r0 for the user code here accordingly: + tr.add(2, 'r0') + # see if it is time to switch to next table entry - tr.add(2, 'r0') # didn't rely on (r0)+ vs MMU semantic tr.cmp('2(r5)', 'r0') tr.bne(7) # skip over the "time to switch" stanza @@ -762,172 +765,15 @@ class TestMethods(unittest.TestCase): tr.clr('(sp)') # put user PC back to zero tr.rtt() - # this code goes at kernel_addr - with ASM() as a: - a.mov(0o20000, 'sp') # start system stack at 8k - # KERNEL I SPACE - # PAR 0 to physical 0 - # PAR 7 to physical 760000 and 22bit not turned on - # - # PDR 77406 = read/write, full length - a.clr(a.ptr(cn.KISA0)) - a.mov(0o760000 >> 6, a.ptr(cn.KISA7)) - a.mov(0o077406, a.ptr(cn.KISD0)) - a.mov(0o077406, a.ptr(cn.KISD7)) + # this trap handler is only used during the startup phase + # See where the kernel code invokes the user setup code + tr.label('trap_usersetup') + # the kernel put a resume address onto the stack, just go there + tr.add(4, 'sp') # get rid of user trap frame, don't care + tr.mov('(sp)+', 'pc') - # USER I SPACE - a.mov(0o20000 >> 6, a.ptr(cn.UISA0)) - a.mov(0o077406, a.ptr(cn.UISD0)) - - # USER D SPACE going UP... - a.mov(cn.UDSD0, 'r3') # will walk through D0 .. D7 - # NOTE: A0 .. A7 is 040(r3) - a.clr('r0') # r0: segno*2 = (0, 2, 4, .., 14) - a.mov(0o2000, 'r4') # phys addr base (0o200000>>6) - - a.label('PARloop') - - a.mov('r4', '040(r3)') # set U PAR; don't bump r3 yet - a.add(0o200, 'r4') # 0o200 = 8192>>6 - - a.mov('r0', 'r2') # r2 = segno*2 - a.ash(3, 'r2') # r2 = segno*16 - a.swab('r2') # really (segno*16)<<8 - a.add(0o06, 'r2') # ACF r/w segment - a.mov('r2', '(r3)+') # set U PDR - a.inc('r0') # bump r0 by two - a.inc('r0') - a.cmp('r0', 16) # and loop until done all 8 segments - a.blt('PARloop') - - a.bis(1, a.ptr(cn.MMR3)) # enable I/D sep just for USER - a.mov(1, a.ptr(cn.MMR0)) # turn on MMU - - # create the test table, just push it onto the stack (yeehah!) - a.mov(0, '-(sp)') # this is a PAD (not really needed) - a.mov(0o666, '-(sp)') # this is a sentinel - a.mov(0o176100, '-(sp)') - a.mov(1, '-(sp)') - a.mov(0o160000, '-(sp)') - a.mov(0, '-(sp)') - a.mov(0o154100, '-(sp)') - a.mov(1, '-(sp)') - a.mov(0o140000, '-(sp)') - a.mov(0, '-(sp)') - a.mov(0o132100, '-(sp)') - a.mov(1, '-(sp)') - a.mov(0o120000, '-(sp)') - a.mov(0, '-(sp)') - a.mov(0o110100, '-(sp)') - a.mov(1, '-(sp)') - a.mov(0o100000, '-(sp)') - a.mov(0, '-(sp)') - a.mov(0o66100, '-(sp)') - a.mov(1, '-(sp)') - a.mov(0o60000, '-(sp)') - a.mov(0, '-(sp)') - a.mov(0o44100, '-(sp)') - a.mov(1, '-(sp)') - a.mov(0o40000, '-(sp)') - a.mov(0, '-(sp)') - a.mov(0o22100, '-(sp)') - a.mov(1, '-(sp)') - a.mov(0o20000, '-(sp)') - a.mov(0, '-(sp)') - a.mov(0o100, '-(sp)') - a.mov(1, '-(sp)') - - # the test table for the trap handler is now here: - a.mov('sp', 'r5') - # test starts in the region at the start of the table - a.mov('(r5)', 'r2') - - # ok, now ready to start the user program - a.mov(0o140340, '-(sp)') # push user-ish PSW to K stack - a.clr('-(sp)') # new user PC = 0 - a.clr('r0') # user test expects r0 to start zero - a.rtt() - - # these instructions will be used to switch over - # to the DOWN phase of the test. Similar to the UP but - # don't have to do the PARs (they stay the same) and the - # pln calculations are different. - - a.label('DOWN') - a.mov(cn.UDSD0, 'r3') - a.clr('r0') - a.label('PARloopDOWN') - # compute segno * 8 in r2 (r0 starts as segno*2) - a.mov('r0', 'r2') - a.ash(3, 'r2') - # pln = 0o177 - (segno * 16) - a.mov(0o177, 'r1') - a.sub('r2', 'r1') - a.mov('r1', 'r2') - a.swab('r2') - a.add(0o16, 'r2') # the downward growing case - a.mov('r2', '(r3)+') # set U PDR - a.inc('r0') - a.inc('r0') - a.cmp('r0', 16) - a.blt('PARloopDOWN') - - # this halt will be right before the first run of user mode test - a.halt() - - a.clr('r0') # initial loop condition - a.clr('(sp)') # just knows the user loop starts at zero - a.rtt() - - # Now for something extra frosty... relocate just segment 4 - # (arbitrarily chosen) of the user memory to a different - # physical page and run the test again to ensure it still works. - # This will make use of KERNEL A1 and A2 segments to map the - # relocation (note: I space because no sep I/D for kernel here) - a.label('BONUS') - - # copy UDSA4 into KISA1 - mapping old segment into kernel space - a.mov(a.ptr(cn.UDSA0 + 4*2), a.ptr(cn.KISA0 + 2)) # i.e., A1 - - # the new location for this data will be physical 0o600000 - # (not a typo) which becomes 0o6000 in the PAR - a.mov(0o6000, a.ptr(cn.KISA0 + 4)) # i.e., A2 - - # the standard PDR access/full-length/etc bits - a.mov(0o077406, a.ptr(cn.KISD0 + 2)) - a.mov(0o077406, a.ptr(cn.KISD0 + 4)) - - # count r0, source address r1, destination r2 - a.mov(4096, 'r0') - a.mov(8192, 'r1') - a.mov(8192*2, 'r2') - a.mov('(r1)+', '(r2)+') - a.literal(0o077002) # SOB to the copy - - # switch the user page to the new mapping - a.mov(0o6000, a.ptr(cn.UDSA0 + 4*2)) - - # and the standard initialization/resume dance - a.halt() - a.clr('r0') - a.clr('(sp)') # just knows the user loop starts at zero - a.rtt() - - # poke the trap handler vector (250) - pcps = [traps_addr + (tr.labels['TrapMMU'] * 2), 0o340] - self.loadphysmem(p, pcps, 0o250) - - # same for the "trap N" handler - pcps = [traps_addr + (tr.labels['UTrap'] * 2), 0o340] - self.loadphysmem(p, pcps, 0o34) - - # all those trap instructions - self.loadphysmem(p, tr.instructions(), traps_addr) - - # all those kernel instructions - self.loadphysmem(p, a.instructions(), kernel_addr) - - # user mode program: + # user mode program: there are two parts to this + # Starting at (user) location ZERO: the test program: # read the given address: mov (r0),r1 # If this causes an MMU fault, it goes to the MMU trap handler # If it succeeds, it then hits the trap(1) and goes to that @@ -937,29 +783,288 @@ class TestMethods(unittest.TestCase): # the PC to zero and bumps r0 then returns to user mode for the # next iteration. (Yes, all this could have been done with mfpd # but that feels like a different test than this) + # + # Start at (user) location labelled 'setup' + # user-mode code executed before the test begins to put + # the test pattern into memory - user_phys_ISPACEaddr = 0o20000 with ASM() as u: # this subtract combines the access check with part1 of chksum u.mov(0o123456, 'r1') u.sub('(r0)', 'r1') u.cmp('r0', 'r1') u.beq(1) - u.trap(0o77) + u.trap(0o77) # trap 77 indicates miscompare u.trap(1) # indicate good status + # the kernel puts the PC back to zero after the good trap + # and also bumps r0. This is how the loop loops. u.halt() # never get here, this is illegal - self.loadphysmem(p, u.instructions(), user_phys_ISPACEaddr) - # set the physical memory that will be mapped to user D - # space to this pattern so the test can verify the mapping - checksum = 0o123456 # arbitrary - user_phys_DSPACEbase = 0o200000 - words = (checksum - (user_phys_DSPACEbase + o) & 0o177777 - for o in range(0, 65536, 2)) - self.loadphysmem(p, words, user_phys_DSPACEbase) + # this code is executed one time at startup (see kernel code) + u.label('setup') + + # Initialize the user D space pattern that will be checked + # by the user code. In python this was: + # + # checksum = 0o123456 # arbitrary + # user_phys_DSPACEbase = 0o200000 + # words = (checksum - (user_phys_DSPACEbase + o) & 0o177777 + # for o in range(0, 65536, 2)) + # self.loadphysmem(p, words, user_phys_DSPACEbase) + u.clr('r0') + u.mov(0o123456, 'r1') + + u.label('pattern') + u.mov('r1', '(r0)+') + u.sub(2, 'r1') + u.tst('r0') + u.bne('pattern') + # the kernel code looks for this in r1 as success flag + u.mov(0o3333, 'r1') + u.trap(0) + + # The kernel-mode code that drives the whole test + with ASM() as k: + k.mov(0o20000, 'sp') # start system stack at 8k + # KERNEL I SPACE + # PAR 0 to physical 0 + # PAR 7 to physical 760000 and 22bit not turned on + # + # PDR 77406 = read/write, full length + k.clr(k.ptr(cn.KISA0)) + k.mov(0o760000 >> 6, k.ptr(cn.KISA7)) + k.mov(0o077406, k.ptr(cn.KISD0)) + k.mov(0o077406, k.ptr(cn.KISD7)) + + # USER I SPACE + k.mov(uaddr >> 6, k.ptr(cn.UISA0)) + k.mov(0o077406, k.ptr(cn.UISD0)) + + # USER D SPACE... first set it up to be simply/fully + # accessible at its physical home, so that the pattern + # can be set. Then come back and limit the page lengths later. + + k.mov(cn.UDSD0, 'r3') # will walk through D0 .. D7 + # NOTE: A0 .. A7 is 040(r3) + k.mov(uphysdata >> 6, 'r4') # phys addr base + k.mov(8, 'r0') + k.label('utmp') + k.mov('r4', '040(r3)') # set U PAR; don't bump r3 yet + k.add(0o200, 'r4') # 0o200 = 8192>>6 + k.mov(0o77406, '(r3)+') # set U PDR and bump to next + k.sob(0, 'utmp') + + k.bis(1, k.ptr(cn.MMR3)) # enable I/D sep just for USER + k.mov(1, k.ptr(cn.MMR0)) # turn on MMU + + # certainly could have used mtpd to set up the user mode + # pattern but this is just another excuse to test more things + # jump to the user 'setup' code, but first establish a handler + # for the trap it will execute when done. + k.mov(taddr + (2 * tr.labels['trap_usersetup']), '*$34') + k.mov(0o340, '*$36') + + # compute where the trap handler should resume + # XXX NEED BETTER FORWARD LABEL SUPPORT IN asmhelper + k.mov('pc', '-(sp)') + k.add(14, '(sp)') # hand calculated to be just after rtt + k.mov(0o140340, '-(sp)') # push user-ish PSW to K stack + k.mov(u.labels['setup'] * 2, '-(sp)') # PC for setup code + k.rtt() + + # user code dropped this magic value into r1 on success + k.cmp(0o3333, 'r1') + k.beq(1) + k.halt() + + # and now set the length limits on the user D space + k.mov(cn.UDSD0, 'r3') # will walk through D0 .. D7 + k.clr('r0') # r0: segno*2 = (0, 2, 4, .., 14) + + k.label('PDRloop') + k.mov('r0', 'r2') # r2 = segno*2 + k.ash(3, 'r2') # r2 = segno*16 + k.swab('r2') # really (segno*16)<<8 + k.add(0o06, 'r2') # ACF r/w segment + k.mov('r2', '(r3)+') # set U PDR + k.inc('r0') # bump r0 by two + k.inc('r0') + k.cmp('r0', 16) # and loop until done all 8 segments + k.blt('PDRloop') + + # create the test table, just push it onto the stack (yeehah!) + k.mov(0, '-(sp)') # this is a PAD (not really needed) + k.mov(0o666, '-(sp)') # this is a sentinel + k.mov(0o176100, '-(sp)') + k.mov(1, '-(sp)') + k.mov(0o160000, '-(sp)') + k.mov(0, '-(sp)') + k.mov(0o154100, '-(sp)') + k.mov(1, '-(sp)') + k.mov(0o140000, '-(sp)') + k.mov(0, '-(sp)') + k.mov(0o132100, '-(sp)') + k.mov(1, '-(sp)') + k.mov(0o120000, '-(sp)') + k.mov(0, '-(sp)') + k.mov(0o110100, '-(sp)') + k.mov(1, '-(sp)') + k.mov(0o100000, '-(sp)') + k.mov(0, '-(sp)') + k.mov(0o66100, '-(sp)') + k.mov(1, '-(sp)') + k.mov(0o60000, '-(sp)') + k.mov(0, '-(sp)') + k.mov(0o44100, '-(sp)') + k.mov(1, '-(sp)') + k.mov(0o40000, '-(sp)') + k.mov(0, '-(sp)') + k.mov(0o22100, '-(sp)') + k.mov(1, '-(sp)') + k.mov(0o20000, '-(sp)') + k.mov(0, '-(sp)') + k.mov(0o100, '-(sp)') + k.mov(1, '-(sp)') + + # the test table for the trap handler is now here: + k.mov('sp', 'r5') + # test starts in the region at the start of the table + k.mov('(r5)', 'r2') + + # poke the MMU trap handler vector (250) + k.mov(taddr + (tr.labels['TrapMMU'] * 2), '*$250') + k.mov(0o340, '*$252') + + # same for the "trap N" handler + k.mov(taddr + (tr.labels['UTrap'] * 2), '*$34') + k.mov(0o340, '*$36') + + # ok, now ready to start the user program + k.mov(0o140340, '-(sp)') # push user-ish PSW to K stack + k.clr('-(sp)') # new user PC = 0 + k.clr('r0') # user test expects r0 to start zero + k.rtt() + + # these instructions will be used to switch over + # to the DOWN phase of the test. Similar to the UP but + # don't have to do the PARs (they stay the same) and the + # pln calculations are different. + + k.label('DOWN') + k.mov(cn.UDSD0, 'r3') + k.clr('r0') + k.label('PARloopDOWN') + # compute segno * 8 in r2 (r0 starts as segno*2) + k.mov('r0', 'r2') + k.ash(3, 'r2') + # pln = 0o177 - (segno * 16) + k.mov(0o177, 'r1') + k.sub('r2', 'r1') + k.mov('r1', 'r2') + k.swab('r2') + k.add(0o16, 'r2') # the downward growing case + k.mov('r2', '(r3)+') # set U PDR + k.inc('r0') + k.inc('r0') + k.cmp('r0', 16) + k.blt('PARloopDOWN') + + # this halt will be right before the first run of user mode test + k.halt() + + k.clr('r0') # initial loop condition + k.clr('(sp)') # just knows the user loop starts at zero + k.rtt() + + # Now for something extra frosty... relocate just segment 4 + # (arbitrarily chosen) of the user memory to a different + # physical page and run the test again to ensure it still works. + # This will make use of KERNEL A1 and A2 segments to map the + # relocation (note: I space because no sep I/D for kernel here) + k.label('BONUS') + + # copy UDSA4 into KISA1 - mapping old segment into kernel space + k.mov(k.ptr(cn.UDSA0 + 4*2), k.ptr(cn.KISA0 + 2)) # i.e., A1 + + # the new location for this data will be physical 0o600000 + # (not a typo) which becomes 0o6000 in the PAR + k.mov(0o6000, k.ptr(cn.KISA0 + 4)) # i.e., A2 + + # the standard PDR access/full-length/etc bits + k.mov(0o077406, k.ptr(cn.KISD0 + 2)) + k.mov(0o077406, k.ptr(cn.KISD0 + 4)) + + # count r0, source address r1, destination r2 + k.mov(4096, 'r0') + k.mov(8192, 'r1') + k.mov(8192*2, 'r2') + k.mov('(r1)+', '(r2)+') + k.literal(0o077002) # SOB to the copy + + # switch the user page to the new mapping + k.mov(0o6000, k.ptr(cn.UDSA0 + 4*2)) + + # and the standard initialization/resume dance + k.halt() + k.clr('r0') + k.clr('(sp)') # just knows the user loop starts at zero + k.rtt() + + return (taddr, tr), (uaddr, u), (kaddr, k) + + def test_mmu_updown(self): + # test the page length field support in both up and down directions + + # This somewhat-irresponsible magnum opus of assembly code would + # have been much easier to write, understand, and maintain if it + # used the PDP1170/mmu/etc methods directly and was mostly + # written at the python level rather than elaborate machine code. + # For example, it could have looped over calls to mmu.v2p() + # instead of performing an elaborate dance of user mode code + # and kernel trap handlers to analyze the same thing. + # + # HOWEVER, doing it as machine code allowed the same instructions + # to be run through SIMH for cross-verification of the test itself + # and the machine behavior. Was the juice worth the squeeze? + # Someone else will have to decide; the deed is done. + # + # Note that the assembly of three code segments + # (the trap handlers, the user code, and the "kernel") are + # in a separate method, which is helpful for getting at the + # instructions in the "import to SIMH" usage scenario. + # + + cn = self.usefulconstants() + p = self.make_pdp() + + # On these addresses: the code isn't fully general (mostly in + # how the MMU is set up). The kernel stack will start at 8K + # (0o20000 physical) and work downwards. The traps and kernel + # code can be "anywhere" so long as it is in that first 8K + # of memory and leaves room for trap vectors at the bottom and + # stack at the top. + # + # The user code can be on any 64-byte (!) physical boundary. + # The kernel knows the virtual address for the start will be zero. + # + + # this are all PHYSICAL addresses. The code is not fully general, + # there are constraints: taddr and kaddr must be in the first + # 8K of physical memory (if only because that's how the trivial + # kernel mapping is set up). The kernel stack, which also + # requires room for the test tables, must start at the tail end + # of the first 8K. + # + # The uaddr must be on an 8K boundary + taddr = 0o4000 + kaddr = 0o6000 # make sure enough room for the traps code + uaddr = 0o20000 + + for addr, b in self._make_updown(taddr, uaddr, kaddr): + self.loadphysmem(p, b.instructions(), addr) # finally ready to run the whole shebang! - p.run(pc=kernel_addr) + p.run(pc=kaddr) # a halt was encountered, verify r2 is the end sentinel self.assertEqual(p.r[2], 0o666)