(ab)Using the Go compilers to build a Plan 9 kernel
Go ships with a tools suite that includes an assembler, a compiler, a linker, and a library packer. Being able to compile regular C code as used in the Plan 9 userland tools and kernel would be a big win because maintenance of the build tools would be the responsibility of the Go team as well as the Plan 9 developers. Also, Go's tools are derived from Inferno's tools, which in turn come from Plan 9's tools.
Although Go itself does not work properly with Plan 9, at the time of writing it is possible to compile the tools (8a, 8c, 8l, and pack, aka the assembler, compiler, linker, and achiver) on Plan 9 with the aid of a codereview patchset, CL #5695076. This sets up the build framework for compiling on Plan 9.
The Go C compiler, 8c, was nearly ready to produce object code as provided. It required only a simple modification in sgen.c, specifying that when compiling C on Plan 9 (i.e. when the -9 flag is given), functions are assumed to have the NOSPLIT flag set. The NOSPLIT flag specifies that a function cannot use a split stack; this is only of concern for variadic functions, and only when compiling for the Go runtime rather than Plan 9.
The linker, 8l, required slightly more work. The -l flag was re-enabled to prevent the automatic loading of referenced libraries if so desired. We also added the ability to link multiple object files at the same time; Go had removed this because of the way they use 8l, but Plan 9 needs it.
With multiple object files coming in to the linker, it is possible that a variable may be declared as an extern in one object file and then later be initialized in another; we had to modify the ADATA case of the ldobj1 function to allow "multiple initializations" by changing the symbol's associated file (s->file) if the symbol type was previously set to SXREF, and to skip to the next symbol if the symbol had been otherwise already declared. Similarly, changing "if(p->from.scale & DUPOK)" to "if(p->from.scale & DUPOK || debug['9'])" in the ATEXT case allowed functions to be overridden, for example the kernel re-implements several functions from libc, but includes libc.a near the end of its linker command. To accomodate that, we specified that the first definition of a function is the only one used and that later definitions should be ignored.
In 8l/pass.c, we had to modify dostkoff() to take into account the fact that Plan 9 C programs will not be linking in the Go runtime. We ended up wrapping the portion of the function which accesses a Go runtime stack function in an "if(!debug['9'])" statement.
Finally, our last modification to the linker was to set the default library path to /386/lib when linking for Plan 9.
Summary of changes to Go's tools:
- 8c/sgen.c: Assume NOSPLIT for all Plan 9 code
- 8l/obj.c: Re-enable the -l flag to prevent automatic library loading
- 8l/obj.c: Modify ADATA and ATEXT cases in ldobj1() to allow for "extern" and allow multiple declarations of functions
- 8l/pass.c: Modify dostkoff() to skip trying to find a function in the Go runtime
- ld/lib.c: Set the default library path to /386/lib when compiling Plan 9 code
With the changes listed above, it was possible to recompile the standard library (libc) and compile & link a simple Hello World program on Plan 9. In this simple case, the only difference in usage is that you will need "-9 -I /sys/include -I /386/include" in your CFLAGS and "-9 -E _main" in your LDFLAGS.
Compiling the kernel proved a bit more troublesome, but problems were generally the result of stupid mistakes rather than actual incompatibility between Go's tools and the Plan 9 kernel.
In the pc and port mkfiles, we had to include "-9 -I /sys/include -I /386/include" in the CFLAGS and "-9 -E <entry>" in LDFLAGS, where <entry> represents the appropriate entry point for that piece of code.
We removed realmode code from l.s because it caused compilation problems; having removed realmode, we also had to change the e820scan function to always return -1 and remove the portions of pci.c which used the realmode/bios32 code. We also had to duplicate some code from spllo and splhi in splx, because Go's assembler does not like you to JMP from one TEXT to another. The same quirk also meant we had to modify the interrupt handling assembly in _strayintr and _strayintrx. Assembly in the libraries and the rest of the kernel should also be checked in the future, because the assembler silently corrupts this sort of thing without warning.
To do the final kernel linking, we had to compile many of the system libraries with the Go tools and pack them with the archive tool "pack".