From: tzs@stein2.u.washington.edu (Tim Smith) Subject: Re: Binary compatibility with other *nix Date: 9 Mar 1993 21:30:10 GMT
I think some of the techniques that ISC Unix used to provide 286 binary
compatibility could be adapted to work on binary compatibility for 386
programs. To give people ideas, this post will describe how the 286
binary compatibility worked on ISC Unix. (I sure hope I'm remembering
two things right: (1) the technical details, and (2) when my non-disclosure
agreement expired!) Then I'll talk about what might be involved in
doing 386 compatibility this way.
I won't be very Linux specific, because I don't have Linux. I just lurk
here occasionally. (Why don't I have Linux? Because I use a Macintosh.
Speaking of which, is it worth trying to port it, or are there too many
things that are tied to the 386 architecture?)
386 programs under ISC Unix are small model. CS, DS, and SS are set up
when the program starts, and are left alone. They are all set to some
selector low in the LDT. 286 Unix programs (in this post, 286 Unix means
both System V Release 2 and Xenix) can use multiple selectors, but, it turns
out, they don't use the first 8 selectors (these were reserved for some
Intel debugger or something like that, I believe). 386 Unix programs and
286 Unix programs make system calls through call gates. It turns out that
the call gate used by ISC Unix is not the same one used by 286 Unix, and
the one used by 286 Unix was unused in ISC Unix.
With that background, here's how the 286 compatibility works. I'll describe
this by explaining what happens in sequence when you try to run a 286
program.
First, the kernel reads the header and sees that it is not a 386 Unix binary.
It checks the format and determines that it is a 286 Unix program. The
kernel fudges things so that it looks like the user tried to exec either
/bin/i286emul or /bin/x286emul, depending on if the binary is for 286 System
V or for 286 Xenix. The original program name is appeneded to the argument
list, and the kernel then continues as if the user tried to run ?286emul.
Thus, if you have Xenix Word, and type "word spam" to the shell, it is
as if you typed "/bin/x286emul spam word". Recognizing 286 binaries
and directing them to the emulator is one of the few areas that the
kernel had to be changed to support 286 binaries.
286emul execs like any other 386 program. It looks at the end of its argument
list, and finds the 286 program name (word, in our example). It finds the
word binary by checking the PATH environment variable and looking for it.
It opens word as a file, and reads the header. From this, it determines
how many code and data segments the 286 program has. It mallocs memory
for these, and initializes them. Now it uses one of the other kernel
changes that had to be made to support 286 binaries. That is a system
call that allows an application to manipulate the LDT. Using this, x286emul
asks the kernel tto set up LDT entries to map the areas of memory that
x286emul has read segments into. This system call is fairly simple.
It basically lets a program say "here is part of my address space. Please
set up LDT entry so-and-so to refer to it with these permissions."
Another kernel change was a system call that says "please map the GDT
call gate used by 286 pprograms to this address." The emulator now uses that
to redirect that call gate to a handler in the emulator. A copy of this
is stored in the u area, and the call gate is update on task switch.
The emulator does some other random things somewhere in here, like trapping
all signals. Then it does a little assembly voodoo and jumps to the 286 code
in 16-bit mode. The 286 code starts running.
The 286 program now merrily rolls along. Eventually, it does a system call.
The emulator gets control via the call gate. The job of the emulator is
to emulate the system call.
For many calls this is trivial. For instance, to emulate getpid(), you just
do a getpid(). The only trick is you have to put the result where the
286 program expects to find it.
For some calls, the emulator will handle the call itself. For example,
sbrk() works this way. If you grow the data segment, the emulator might,
for example, malloc new memory, copy the contents of the old segment,
and then adjust the LDT to point to the new area. Another example
would be signal(). The emulator would for this call just update its
tables that keep track of what it is to do on signals, and return the
appropriate thing to the 286 program.
Some calls require pointer conversion. For example, if the 286 program
does a read(), it will be giving a buffer in 286 terms. The emulator takes
the selector:offset supplied by the program, and looks up in its tables
to find the 386 base address of the segment, and ads the offset. It can
then do the read call. Xenix was a lot uglier in this area, by the way.
The library code for System 5 converted small model pointers to large
model (e.g., it pushed DS onto the stack). The 286 kernel never got a small
model pointer from a user program, and so neither does the emulator. Xenix,
on the other hand, doesn't do this conversion. It's up to the kernel to
check to see if the user program is small model or large model. Ugly.
There are also some 16-bit int vs. 32-bit int conversions needed for some
system calls.
Anyway, our emulators actually handled all this in a nice table driven
fashion. There were tables that described each argument of each system
call. They told what size it was, and what conversion was needed.
For example, the table entry for read might look like this:
{ read, INT, INT, FPTR, INT, 0 }
which says that the 386 equivalent is read(), it returns a 16-bit int to the
286 code, and that the arguments from the 286 code are an int, a far pointer,
and an int. A call that required special handling, like sbrk, would have
a table entry like this:
{ emulSbrk, ...whatever... }
that would direct sbrk() calls to our handler.
There were a small number of random changes needed in the kernel that I've
not mentioned above. For example, when delivering a signal, the kernel
had to be aware that SS:SP could be kind of funny and deal with this.
This probably wouldn't be too hard to do under Linux for 286 programs
(our first emulator, i286emul, took me and one of our real wizards maybe
a couple of months or so to finish, and x286emul took me and a different
person a little longer, I believe). However, probably not too many people
want to run 286 Unix or Xenix binaries.
However, this sort of thing should be able to work for 386 programs, too.
As long as you can arrange so that the selectors needed by the binary you
want to emulate for and the selectors needed by your native application
do not overlap, and that the system call method used by the binary is
not imcompatible with your native system call method, you should be able
to use a scheme like ours to provide an emulator.
The nice thing about this approach is that most of your work is done in
your emulator program, which is just an application. The special system
calls to support it are fairly easy to get right. Most of your debugging
can use your normal application debugging tools (and your crashes won't
bring down the whole system). This helps development a lot!
If anyone has any questions, send me email -- the volume of this group
is too large relative to the frequency with which I read it for me to
be sure to notice any response to this.
--Tim Smith