IRIX Binary Compatibility, Part 1
Pages: 1, 2
Implementation Plans
IRIX 6.5 is known to be a System V Release 4 (SVR4) derived Operating
System,
and thanks to Christos Zoulas, NetBSD already contains a SVR4 binary
compatibility option. The code for this SVR4 emulation can be found
in sys/compat/svr4 in the NetBSD kernel sources.
NetBSD already has a binary compatibility with major OSes such as Solaris 2 or SCO OpenServer through this SVR4 compatibility option. The first problem was to decide if the IRIX compatibility would be implemented by improving the SVR4 compatibility, or by introducing an IRIX specific compatibility option.
The answer to this first question is obvious once you compare the
system
call tables for plain SVR4 and for IRIX 6.5. The table for SVR4 can be
found in NetBSD kernel source in sys/compat/svr4/syscalls.master. The
table for IRIX can be found on an IRIX system in /usr/include/sys.s,
and in
NetBSD kernel sources, it can be found in
sys/compat/irix/syscalls.master.
In the IRIX 6.5 system table, only the first 88 system calls are plain
SVR4. Following are 147 system calls that are either IRIX specific, or
are just SVR4 system calls with different system call numbers. This
strongly
suggests that IRIX binary compatibility in NetBSD should have its own
syscalls.master, since it would be a pain to add dozens of #ifdef's in
SVR4's syscalls.master.
|
Related Reading
UNIX in a Nutshell |
Thus, we needed a sys/compat/irix directory in NetBSD kernel sources,
with
an IRIX-specific syscalls.master file. However, there are a lot of
plain
SVR4 system calls in IRIX, therefore a lot of code in sys/compat/svr4
is
used by the IRIX binary compatibility. This code is built when the
kernel
is built with the IRIX binary compatibility option (COMPAT_IRIX)
is set, even if the SVR4 binary compatibility option (COMPAT_SVR4), is
not.
Setting Up the New Compatibility Option
Overview
In order to create a new compatibility option in NetBSD, we need
- A
syscall.masterfile for the emulated OS. - The implementation for the system calls described in the
syscall.masterfile. - A function for sending signals to the emulated processes.
- Some startup routines, to recognize and launch IRIX binaries.
- Finally, we need to make all of this visible to the rest of the kernel.
Most of the work can be done in an incremental way, starting from code
which
is duplicated from the NetBSD native version, and modifying it until
IRIX
binaries work. The only field where a NetBSD version is not very
relevant
is the syscalls.master file, because we know that everything will be
changed
later. A null syscalls.master file, which defines no system calls at
all is a
good start.
Registering Our New Emulation
Now let us see how an emulation option is made visible to the NetBSD
kernel. Everything is done in the sys/kern/exec_conf.c. In this file, an array
called execsw_builtin is defined. Each entry in this array is a struct
execsw, as defined in sys/sys/exec.h
The struct execsw describe a particular execution environment. This
includes foreign OSes emulation, and natives situations as well : There are
entries in execsw_builtin for shell scripts, a.out native binaries, ELF native binaries, and 32-bit ELF binaries running on 64-bit NetBSD systems.
Informations held by the struct execsw include pointers to a function
responsible for identifying the executable format (a.out, ELF, ECOFF, Mach-O...), a probe function that should be able to tell if this exec
switch is able to handle a particular binary, and functions for setting up the
program's initial stack, CPU registers, and for writing a core dump to
the disk.
The struct execsw also holds a pointer to a struct emul, which is
defined in sys/sys/proc.h. Whereas the fields of struct execsw are used
at program creation and termination, the struct emul is used during the
program normal operation. It contains pointers to the system call
table, and to various functions used to handle traps and signals.
The distinction between struct execsw and struct emul is there because
some
OSes supports several executable formats. For instance, NetBSD itself
supports native a.out or ELF binaries. Both kind of binaries share the
same system table and signal handlers, and therefore they have the same
struct emul. But the binary loading is different, hence they have two
distinct
struct execsw in execsw_builtin.
The first job is to create the struct emul for IRIX binary
compatibility. This uses the IRIX system call table (which is empty so far), and all
other fields are copied from the NetBSD native struct emul, which is found in
sys/kern/kern_exec.c. It is named emul_netbsd. The struct emul for IRIX is naturally named emul_irix.
Then we can add the entry for IRIX in the exec_builtin array. IRIX uses
ELF binaries, so this entails not much more than copying NetBSD ELF
native's entry, and replacing the struct emul emul_netbsd by our emul_irix.
Matching the Binaries We Can Run
Now we have registered a new execution environment with the kernel. The next step is to have it actually run something. The struct execsw includes a probe function whose purpose is to tell the kernel if the execution environment described by this entry is able to handle a given binary.
In order to use our new execution environment, we must therefore write
a probe function. Usually, this kind of function tries to find a
signature specific to an OS in an ELF section, or a magic number in a a.out
header that would identify the binary.
For IRIX, things are a bit complicated, since IRIX uses no less than three different kind of ELF executables. Theses correspond to the three Application Binary Interface (ABI) supported in IRIX: o32, n32 and n64. The ABI is the set of conventions that explains how the stacks and registers should be used when calling a function, or doing a system call.
o32 is the traditional 32-bit SVR4 ABI for MIPS processors. Here is a pdf document extensively describing o32 from the SVR4 ABI MIPS processor supplement. n64 is the 64-bit ABI, used for 64-bit ELF binaries. Finally, n32 is a hybrid ABI used to increase performance of applications using a 32-bit address space on 64-bit processors. The difference between o32 and n32 is that 64-bit registers are used instead of 32-bit, where relevant, and more function arguments are transmitted through registers instead of the stack. The goal behind n32 is to improve performance on 32-bit applications that are not necessarily able to build as 64-bit, because some assumption were made on pointer size, for instance.
At the time this paper was written, the NetBSD/mips kernel is only able to run o32 binaries. The goal is hence to match IRIX o32 binaries. o32 binaries are themselves divided into two families: static o32 and dynamic o32. Dynamic o32 binaries are the easy part of the job; therefore we will start with them.
ELF binaries are divided into ELF sections. The sections can be
inspected using the objdump(1) command:
objdump -h file will list the ELF sections of file, and
objdump -j .section -s file will dump .section from file.
All dynamic ELF executables have an .interp section that contains the
name of the ELF interpreter, which is also known as the dynamic linker. On
NetBSD, this is /usr/libexec/ld.elf_so. See ld.elf_so(1) for more
information about ELF dynamic linking.
On program startup, the kernel loads the executable and the interpreter, and then transfers control to the interpreter. The interpreter loads the shared objects into memory, and then transfers control to the dynamically linked program.
On IRIX, things are a bit strange: the interpreter is libc itself.
Libc loads the dynamic linker (/usr/lib/rld), which in turn loads
all the shared objects and then executes the program by calling it's
main()
function.
This IRIX particularity is good for matching IRIX binaries: the
interpreter
name is /lib/libc.so.1, which is quite unusual. Another good point
to examine is that because it maintains three different ABIs, IRIX has
three
different sets of libraries, and therefore three different interpreter:
/lib/libc.so.1 for o32 binaries, /lib32/libc.so.1 for n32 and /lib64/libc.so.1 for n64. It is therefore quite easy to check whether a dynamic executable is an IRIX o32 binary : we just have to peek at the .interp section and see if the interpreter is /lib/libc.so.1
|
In This Series
IRIX Binary Compatibility, Part 6
IRIX Binary Compatibility, Part 5
IRIX Binary Compatibility, Part 4
IRIX Binary Compatibility, Part 3
IRIX Binary Compatibility, Part 2 |
Static binaries are more tricky. At a glance, the only difference
between o32 and n32 static binaries is the presence of a .MIPS.options section in o32 static binaries. This could have been a good test, but
unfortunately, it has some false negatives. One can find a few static
o32
binaries in IRIX 5 that do not have this ELF section (the expr(1)
command for
instance).
But since IRIX's file(1) command is able to distinguish o32 and n32,
there must
be a reliable difference. The answer is in an IRIX header file:
/usr/include/sys/elf.h. This file defines the ELF header, in which we
can find an e_flags field. Two bits in this field are used for IRIX binaries
to distinguish between the three ABIs. In order to tell if an IRIX
binary is o32, n32, or n64, we just have to check for theses two bits in the ELF header.
This is quite a robust test to distinguish between o32, n32, and n64 IRIX binaries; however, it can still have some trouble when it is used to distinguish between IRIX and non-IRIX static binaries. Fortunately, there are not a lot of static binaries on an IRIX 6.5 system, and it is not trivial to build static o32 programs. (In fact, I was not able to find how to do it.) Hence, we can afford to use a weak matching scheme; as soon as we can match the few static binaries from IRIX 6.5 correctly, we are safe.
Emmanuel Dreyfus is a system and network administrator in Paris, France, and is currently a developer for NetBSD.
Return to the BSD DevCenter.
