20210828

A new path: vm86-based venix emulator

 Venix Emulator Update

It's been a while since I've had time to work on the Venix emulator. When I set it aside over a year ago, I'd taken it as far as I could with the 8088 emulator I'd found online. It had no FP emulation and there were
a number of things misbehaving that I couldn't quite get right. And exec was proving hard to implement. Despite being written in C++, the original emulator resisted my efforts to make multiple instantiations.

So I set it aside last May, thinking I might get back to it when the qemu bsd-user changes FreeBSD has done have been upstreamed.

The first of August I took some time off from work and got the bsd-user changes in shape to upstream. Well, the first 10% of the changes that were the hardest since it was replacing what was there with something that minimally worked. This helped me learn qemu's x86 CPU much better and it got me thinking that qemu's user-mode stuff might be the way to go.

About this time I also found a vm86 test program in the FreeBSD tree. So I got to wondering, could I do a vm86 implementation of Venix?

So, I stole the bulk of my old 86sim-based Venix implementation, installed a i386 VM using bhyve on my FreeBSD/amd64 box and write a quick little test program. The test program worked, so in a fit of "why not give this a try" I ported the pcvenix.cc from 86sim to being driven from SIGSEGV in vm86 mode. Hello world quickly worked.

Enter VM86VENIX

So, I reworked fork and exec and the a.out loader a bit. I was able to get the C compiler going in this new setup. The 'cc' command is just a fancy script that strings together the pre-processor, compiler, optimizer, assembler and linker. Except on Venix it wasn't a shell script I could hack to run natively on FreeBSD. It was this weird binary that did all the forking, execing, redirecting, etc inline. More on that in a minute.

So vm86 mode is a special mode in 32-bit CPUs that lets you execute old 16-bit code in the context of a normal process. It's super easy to setup, but often of limited use.

Thankfully, the i386 ELF designers thought ahead. The starting address for binaries in ELF is this weird 0x00401430, which is just above 4MB. This means that one can map anything into low memory and it will work. FreeBSD has a security stop on mapping anything at location 0, however, but the rest of the first 4MB is available. The old 8086 could only see the first 1MiB of that, but since Venix binaries are at worst 'small-mode' the largest address space for a process was 128kiB. Plenty of room to find a place to map it.

So, I wrote a loader that would load the old Venix a.out binaries into this space. Or rather I hacked the loader I already had to do the mapping. I was able to reuse all the loader code from the 86sim-based emulator I had before.

I then shamelessly stole the setup code from the FreeBSD vm86 testing binary, which was little more than establishing signal handlers and zeroing the context and setting up a stack and IP as well as the segment registers. With that in hand, I was able to use sigreturn() to set the processor flags such that it would jump to where I wanted to go in the Venix binary. I'd been afraid of vm86 mode after reading through doscmd years ago, but there was no need for the fear: all the cruft in doscmd (I reread it after this) was the accumulation of cruft over the years for DOS, BIOS and other weirdness that evolved around the IBM PC, XT, AT and the plethora of clones which had nothing to do with vm86 mode, per se.

Every INT xx instruction would trap to the kernel. The kernel would note down the registers and send the process a SIGSEGV for these accesses. I was able to then look at CS:IP in the mcontext and decode the instruction that faulted. INT xx is encoded as the bytes 0xCD 0xXX for almost all values of X (INT3 has its own opcode 0xCC). In the signal handler, I could decode this opcode. I knew from past work that INT 0xf1 was the system call, so I hooked up the old venix system call handlers to this and I was back to where I was with 86sim. Further in fact because floating point worked.

Signal handlers have an implicit sigreturn with the context passed to the signal handler at the end. I needed to skip over the faulting instruction after performing the system call, and the process would then resume executing in 16-bit mode after the INT 0xf1. This was straight forward to implement.

I decided to implement fork as a real fork. It would copy the address space, all the open FDs, etc. This proved to be an easy way to cheat so I didn't have to create a context object and use threads to simulate processes.

Exec proved to be just a call to my loader that started all this off. The only thing I've not implemented is close on exec, but the rest was easy.

With these implemented, I could run the C compiler's cc command and generate trivial binaries. But if I needed to include anything, it would fail. I created a VENIX_ROOT env variable. For all opens, it would try VENIX_ROOT / name and then just the plain name if the name arg to open started with a '/'. This was enough for the preprocessor to include .h files and for the canonical hello world to build.

There was just one vexing problem: cc -o hello hello.c worked. However cc -O -o hello hello.c didn't.

Tracking down a silly bug

Well, there was another annoying thing: /bin/sh didn't work. I traced that to the fact I've not implemented passing an environment to the processes, and /bin/sh was choking on that. OK. Fine. I'll implement that later. /bin/csh worked, however, so I was happy. My happiness was short lived, alas, because I'd run a  command and I'd get weird output:
% ls
ls: Sig 44
%

 That's weird. So I added tracing. I tried the cc command, which in this version is a simple program that orchestrates all the different parts of the compiler using fork, exec, dup and the strategic close/open pair to setup stdin etc. All the tracing looked good as well, we'd see something like:

123: fork() 124
123: wait()
124: ... lots of stuff
124: exit 0
123: wait pid 124 status 0

 and then 123 would proceed to delete all the temp files and exit. It was like it was getting an error, despite its children exiting without an error. Every time it was like this, but only when I ran the optimizer. When I'd re-run the ls test, I'd get different Sig values as well.

Available 8086 compilers in 1985

So, I'd assumed that the compiler was derived from the V7 compiler. However, a number of hints in names suggested it wasn't. And the Venix manual had way more exceptions for 8086 than for pdp-11 when it described the options and operation for Intel, so I assumed it wasn't V7 derived. So I started hunting around for C compilers.

MIT produced one at the time for the PC. It could run on the VAX and generated a.out binaries that a conversion program would convert to .COM or .EXE files. I thought this might be where the Venix compiler came with. But after playing around with it for a few hours it was clear it wasn't. First, it had a shell script cc, not a program. Second, it had a number of VAX specific instructions sprinkled inline, and that wasn't going to run on the Rainbow :).

I took a look at the portable C compiler. This I think was the real genesis of what was shipped with Venix, but old versions that support 8086 are hard to come by, even in the successor portable C compiler project that's been going for 20-odd years now. I got the cc program from that compiling with Venix. It was a bit easier to fuss around with than the V7 one (but the V7 one would be close enough to hit the bug I found out later). Looking at the old System III sources that one can find on the internet, there's a copy of the portable C compiler there, rather than the C compiler from DMR as you'll find in the 7th edition. I used the cc program from there to try to build things. I hit the jackpot: it failed faster!

So I instrumented the pcc program and discovered that the status printed after wait() in the program didn't match the status that I'd returned from the kernel. Progress!

The wait(2) system call...

I'd implemented the wait(2) system call as part of getting fork/exec working. I did it from the VENIX manual that's available online. I looked at the first part of the manual:
SYNOPSIS
        wait(statusp)
        int *statusp;

which shows wait taking a pointer. So I assumed this was what the kernel received in the first arg that's passed into the kernel (DX). I assumed that DS:DX pointed to an integer where I'd return the status. Most of the time, DX was something that looked like a pointer on the stack, so I just did a copyout. The problem is that's not right.

So, I took another look at the manual. At the end of the man page I saw:

8086      BX=7
              int 0xf1
              AX = pid of process
              DX = status

and then it hit me.  DX isn't a pointer to anything. After int 0xf1 AX is the pid of the process (the normal return value) and DX is the status. Disassembling wait.o confirmed this:if statusp is not 0, dx is copied back to *statusp. Doh! The classic pointer vs value mistake. Fixing my implementation to take out the copyout and replace it with setting DX in the processor context made pcc work. And cc worked. And the silly test programs I wrote in the middle to debug things worked. Woo Hoo!

Once I fixed this, all weird combinations of compilations suddenly worked for me. I could optimize, strip, etc and there were no oddities.

Conclusion

It helps to read the manual carefully!

I need to try to build the system. There's shell scripts to do that that don't depend on environment variables working if run natively, so I'll see if they work and see how much of the system I can generate via this route. Stay tuned.

My TODO list still contains getting env working (I don't think it is hard, but I think I need to filter things because my default env is larger than the stack on these old x86 machines). I also need to look at rebasing my emulator as a *-user qemu emulator (even if they don't take it upstream). Maybe even add PC/IX and Xenix/86 support as well so that other researchers can play around with this.

No comments: