Wow, amazing job, thanks a lot. The PC-D was my first PC which I used for years, so I have some strong memories of it. But while I only remember seeing SINIX booted up once (on what must have been a PC-X, then), I got really intrigued about that custom external MMU, and did some of my own research.
First, I tried to find pictures, manuals, references, anything about the hardware side of it, with almost no result at all. There is a PC-D picture gallery that claims to show the MMU
, but that's only the memory controller, no remapping support or anything like that. The only piece of information I found much later, while well into reverse engineering, in the form of this slashdot comment
which claims that the MMU had an 8086 and a ROM of its own, which is really interesting. Obviously, an 8086 alone sitting on the bus won't cut it, but if it's true that it was part of the board it could mean that the MMU could be understood in much greater detail if we got hold of the ROM.
It also seems like nothing else I could find from that era, e.g. the fairly well documented MMU in the Altos 486
. This was another 80186-based machine from around the same vintage as well (and obviously had nothing to do with the not yet invented Intel 486), that also run XENIX, which SINIX is at least based on.
So, with nothing else to go on, I went the software route, and looked at how both the MMU self test in the PC-X ROM and SINIX (mostly 1.0) handled it, just like crazyc must have done. I found out a few more things than apparent from the current MAME implementation, about MMU and NMIs. There are still many open questions.0x8400: page table entry/mapping registers, protection and fault status bits
There are two more bits that are also to be considered part of the "page table entry" (selected by 0x84xx with xx being the page): Bit 4 (0x10) and bit 5 (0x20).Bit 4
seems to be the write allow bit, i.e. the MMU will fault on any write access to the page if this bit is not set. SINIX seems to work well if I make this bit fault the write access always, not just in "user mode" (which is a fuzzy proposition anyway, see later), as long as the MMU is enabled (bit 5 in 0x8000), as it never seems to try to write to a page without that bit even in kernel mode (or "system mode", as SINIX calls it).Bit 5
is either the opposite "read allow" bit, or maybe a more general "present" bit. The difference would be that a "present" bit would fault any access, read or write, as long as this bit is not set, whereas "read allow" would still allow writes as long as "write allow" is set. The distinction seems moot for SINIX, as I haven't observed it ever trying to write to a page that did not also have this bit set. I mentally treat it as "present".
Implementing the two bits as above seems to make SINIX behave well in terms of memory protection: Faulty programs that read or write outside memory they are supposed to are killed with a "Speicherzugriffsfehler" (memory access error), and their core is dumped. Since this early SINIX is a simple UNIX that merely swaps whole processes in or out and not individual pages, any such access aught to be treated as terminally fatal to the task and this behaves as expected.
Then there are, somewhat awkwardly, two more bits that seem to not be part of the page table entry, but global flags that are read from the page table entry register as well: Bit 6 (0x40) and bit 7 (0x80). crazyc's code already handles one case of them being used (system calls).Bit 6
seems to always be set when checked, which is generally in the MMU fault handler (INT 0x20) path. If the bit is not set on such a fault, SINIX panics. Rather than being an always-on bit, I speculate that this bit actually indicates that an MMU fault occurred, and that it should be cleared e.g. after reading it (I haven't tried that detail out yet). That way, a spurious INT 0x20 without this bit being set correctly informs SINIX that something fatally unexpected happened.Bit 7
is more obvious: If this bit is set, the fault was not due to a memory access with the wrong protection (i.e. "present" or "write allowed" not set), but because of something else, e.g. a system call (by writing to port 0x8800). See the discussion on 0x8a00 for what else this bit could potentially indicate, but overall it seems like this bit being not
set indicates a page protection fault.
So, I would collectively call bit 6 and 7 the "fault status" bits. I don't know why they are crammed into the same register as the page table entries. Maybe they wanted to keep 0x8000 free from status bits (I only know it to contain control bits so far, so possibly it also always reads as last written by the system), and the other registers 0x8800 and 0x8a00 already seem to cause side effects both when reading or writing.
those two bits has any meaning, then not an obvious one. SINIX never seems to set them on purpose (it sometimes does copy page table entries by just copying all the bits--but depending on how things work in detail, it could expect them to be clear when doing so).0x8a00: disable I/O port access?
Port 0x8a00 is an interesting one. According to a test in the MMU self test routines, it's plausible that reading
it disallows all further I/O port access, causing an NMI on any attempt.
If true, this would be a great feature for full (or at least better?) protection. Without that, memory is still protected by the page tables: A user mode task's page table simply only allow full access to any of its pages, as well as read-only access to only the kernel pages that are needed while the task is running (mostly the interrupt vector table and interrupt handlers themselves; because the 80186 does not actually have any privilege levels, some concessions on what code is visible had to be made, but at least it seems protected from writes).
But access to I/O ports is separate, and a task can still mess with the hardware directly, make the system crash, or worse, just circumvent the memory protection by reprogramming the MMU registers itself.
So, by disabling any I/O port access before handling control to the user mode task, any such violation can be prevented. Any attempt by the task to do I/O would raise an NMI instead. The 80186 reference manual mentions that the NMI vector sequence starts at the next instruction edge, which I think works out well. Just as with memory accesses, the MMU prevents the transaction itself from appearing on the actual bus (presumably at the cost of some latency). The MMU would then re-enable I/O access, since we vectored into kernel code now, which will handle the violation.
Read access to port 0x8800 could be exempt, because those are the system calls already handled separately. The MMU however would also need to re-enable I/O port access when the CPU vectored into the (non-NMI) INT 0x20 MMU fault handler (and only when that certainly happened), for the kernel to be able to do its job. From just glancing over the 80186 manual, it's not entirely clear to me how complicated this would be.
I think the only problematic thing then left for a user task to attempt is issuing a CLI instruction to mask all interrupts. But the MMU could maybe detect that, for example, non-NMI faults and timer interrupts are not served by the CPU in the expected time frame, and raise an NMI, which, as the name implies, is never masked. So a truly rogue task could stall the machine only for a short time before being killed.
But, here's the thing: SINIX, at least version 1.0B, does not seem to ever read or write 0x8a00. So it seems that feature, or whatever else is behind that I/O port, is unused. I later even found a German Usenet message from 1990 lamenting the fact that I/O ports weren't protected
, claiming that application development would require frequent reboots.
I don't know why that is. It's all the more curious that there actually are the files /dev/inout and /dev/inoutb for proper I/O port access through the kernel. Maybe actually implementing it in the XENIX-based SINIX turned out to be too hard for at least the first few versions. Maybe the feature could not be made to work well at all. Or maybe the MMU did not actually support I/O port protection in the first place (even though the Altos 486's MMU did, as mentioned in its System Reference Manual
), and I/O port 0x8a00 does something else entirely.
Because in the end, all this is pure speculation from looking at a single, tiny ROM MMU self-test accessing 0x8a00 and then expecting an NMI on the next I/O instruction. Moreover, that next I/O instruction is not to some random I/O port, but to 0x8000, the MMU control register, which might still be an arbitrary choice, but might also have a deeper meaning. Generally, it's very hard to draw conclusions just from that one test, since SINIX does not appear to have any relevant code.NMI status bits
Speaking of NMIs, inspection of the SINIX NMI handler gives a relatively good picture of at least a subset of NMI types the machine can generate. The NMI type is read as a byte from port 0xf840, and it seems that the bits are all inverted, as they are all tested for being clear instead of set to match against NMI types. To avoid confusing language, I use the term "indicated" instead of "clear" in the following list:Bit 6+Bit 5
: Bit 6 alone does nothing, but if indicated together with bit 5, SINIX shuts down cleanly (by just sending SIGTERM to init). It's conceivable that this happens when flipping the power switch: Despite the deceiving flip switch that PC-Ds/PC-Xs have on their front, they actually have some sort of soft power. I remember once being very surprised when I flipped the quite substantial switch and the machine would just stay on. It would also be interesting to find out how the power switch is disabled.Bit 3
: If indicated, the NMI is a "DEBUG TRAP", and SINIX dumps some machine state and exits to the ROM debugger. The machines have a "debug" button next to the reset button, which is known to cause a distinguishable NMI (effective under DOS as well).Bit 2
: Bit 2 is interesting. If this bit is indicated, and if SINIX is currently in its initialization routines during startup, the NMI is completely ignored. I suspect that bit 2 could indicate "non fatal" faults, and that e.g. the power down fault from above would have bit 2 indicated (as it does not make sense to send a signal to init when the kernel isn't even initialized yet), whereas the DEBUG one probably does not.Bit 1
: This indicates a power issue. As long as the bit is indicated, the kernel will print out "nmitrap: narrow power-fail spike!" every few seconds.
All other bits are unknown, and will print out "NMI TRAP" and exit to the ROM debugger when not indicated with any of the other bits above (the DEBUG TRAP is similar, but prints some more state).How is the number of tasks/segments per task/task selector width in 0x8000 selected?
This is the biggest mystery to me. crazyc already mentioned it, and despite explicitly trying to figure this out, I was not able to do so either. This is the reason why there is this hack of selecting between SINIX 1.0 and SINIX 1.2. But as crazyc already mentioned, it's much more likely that there is a way to configure this task selector width by software.
I had many attempts with many dead ends. Early on, I realized that when SINIX writes to 0x8000 to select a task+segment, which can be done entirely in 8 bits, the upper bits of the 16bit word would sometimes be set to something. I thought maybe this implements some sort of buddy system, where those bits indicate the width of the address space. But it seems that those bits are only what's left in the AH register, with no real meaning ascertained by the code.
I also could not find any other write to any of the MMU ports that seemed to indicate any such configuration taking place. If it happens through writing to any other I/O port, it's apparently not through a port that is not already known in the MAME code. I have not fully ruled out that writing to the status (0xf840) or LED (0xf841) ports configures the MMU, but nothing seems obvious either. Maybe finding out how the power switch is disabled can give a hint, as presumably that's another unaccounted for configuration.
At this point, it's not fully unlikely that the width is configured by hardware, e.g. by some DIP switch, possibly directly on the MMU board. But then that means that the MMU self test always had to be deactivated, since the self test expects a task selector width that's different from both SINIX 1.0 or 1.2.