BSD DevCenter
oreilly.comSafari Books Online.Conferences.


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
By Arnold Robbins

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


In order to create a new compatibility option in NetBSD, we need

  • A syscall.master file for the emulated OS.
  • The implementation for the system calls described in the syscall.master file.
  • 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/, 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/ for o32 binaries, /lib32/ for n32 and /lib64/ 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/

In This Series

IRIX Binary Compatibility, Part 6
With IRIX threads emulated, it's time to emulate share groups, a building block of parallel processing. Emmanuel Dreyfus digs deep into his bag of reverse engineering tricks to demonstrate how headers, documentation, a debugger, and a lot of luck are helping NetBSD build a binary compatibility layer for IRIX.

IRIX Binary Compatibility, Part 5
How do you emulate a thread model on an operating system that doesn't support native threads (in user space, anyway)? Emmanuel Dreyfus returns with the fifth article of his series on reverse engineering and kernel programming. This time, he explains thread models and demonstrates how NetBSD emulates IRIX threads.

IRIX Binary Compatibility, Part 4
Emmanuel Dreyfus tackles the chore of emulating IRIX signal handling on NetBSD.

IRIX Binary Compatibility, Part 3
Emmanuel Dreyfus shows us some of the IRIX oddities, the system calls that you will not see anywhere else.

IRIX Binary Compatibility, Part 2
Emmanual Dreyfus shows us how he implemented the things necessary to start an IRIX binary. These things include the program's arguments, environment, and for dynamic binaries, the ELF auxiliary table, which is used by the dynamic linker to learn how to link the program.

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.

Sponsored by: