Linux Compatibility on BSD for the PPC Platform: Part 3

In the previous articles, we explained what was necessary in order to run
both statically and dynamically linked Linux binaries on the NetBSD/PowerPC platforms, but we have only worked on program launching and system calls.

Because most real-life programs cannot live without signals, it's time to focus on the way signals are handled in Linux emulation. At the end of this article, we will also have a look at a few system-call-specific bug fixes.

Signals

Now that dynamic executables work and get their arguments, the next important step is setting up signal handling. Signals are the interactions from the kernel to the user program. The kernel will have signals pending for a process for various reasons: the process made a memory fault, it has data ready for asynchronous I/O, or the user decided to suspend the process.

A process may choose to trap a signal or not. If a signal is delivered to a process that does not trap for that signal, the kernel will apply a default behavior that halts the program and eventually dumps the core. This is the easy part because there is no actual interaction between the kernel and the process.

The process can choose to trap a given signal. It does this using the signal() or sigaction() system call to install a signal handler for a given set of signals. When the kernel has a signal to deliver to the process, it will go to user space to run the signal handler. This is slightly more complicated, because the kernel has to trick the process so that it runs the signal handler on its return to user space.

Here is how the job is done: To make a system call, the program uses a software exception instruction. This instruction transfers control to the kernel and a trap frame is built on the kernel stack (this is done by locore.S or by the hardware, depending on the architecture). The trap frame holds the register's values so it is possible to return control to the user process.

When the kernel has to invoke a signal handler, it has to modify the context stored in the trap frame so that on return to user space, the process will jump to a "signal trampoline" set up on the user stack. This signal trampoline invokes the signal handler, and also sets up the signal trampoline code and a signal context expected by the signal handler on the user stack. All of this is done by the sendsig() function in the kernel.

After the signal handler returns, the trampoline code calls the sys_sigreturn() system call. This system call must undo everything that was done on the user stack to invoke the signal handler.

This is the global picture for NetBSD signal handling. Let us now look at what differs between invoking a signal handler in a Linux process and in a NetBSD process. The only thing that the user process will see is the user stack, and even in the user stack, there is no reason why it should assume a particular layout beyond the signal context. We can use NetBSD code to set up the user stack, except for the signal context, which must be defined in a Linux fashion.

The job can therefore easily be done by picking up the NetBSD signal trampoline from sys/arch/powerpc/powerpc/sigcode.S, and the NetBSD sendsig() and sys_sigreturn() implementations, which can be found in sys/arch/powerpc/powerpc/sig_machdep.c for the PowerPC port of NetBSD.

Of course, this code needs to be adjusted so that a few necessary translations are done between the Linux and NetBSD structures, and we must set up a struct linux_sigcontext instead of a native NetBSD struct sigcontext on the user stack. The modified code fits in sys/compat/linux/arch/powerpc/linux_machdep.c.

After a few mistakes in the way the stack is set up, the emulated program should be able to catch signals. Here is a sample program that tests signal-catching: