OS in Go? Why Not?
This article was written by an external contributor.
Operating systems are the heart and soul of a computer system. The operating system manages the computer’s hardware as well as software resources and provides the user with a means to interact with the computer. Traditionally, languages like C and Assembly have been used to write operating systems because of their low overhead and “close to the machine” features. But the rise of high-level languages such as Go has introduced features that should make developing complex software such as an OS much easier. For example, features like type safety, error handling, and concurrency can be advantageous when writing an OS. These features suggest that a high-level language like Go would be a natural choice for OS development. So, why isn’t it?
In this article, you’ll learn why languages like C have a stronghold over OS development and whether writing an OS using Go is possible.
Why are OSs or kernels written in C and assembly?
Determining which language was used to write any particular operating system can be challenging because every OS is written in a combination of multiple languages.
An operating system comprises different components with different responsibilities and can be written in different languages. The heart of an OS is the kernel – the component responsible for interacting with the hardware—which is almost always written in C or assembly. The user-facing components, such as the GUI apps, can be written in any language. For example, Android uses Java for the userland components, such as the GUI framework and system apps like Camera, Phone, etc. In contrast, the kernel is written in C and assembly, and the low-level system components, such as the libraries, are written in C++.
This article focuses explicitly on whether Go is a good candidate for writing the kernel of an OS. Almost all major kernels are written in C, with bits of assembly in between. There are a few reasons why C rules the kernel world:
- Direct memory management: The selling point of C is that it allows the programmer to manipulate memory directly. While manual memory management can be daunting in general when you’re writing a kernel, you want as much control over memory as possible, and C gives you precisely that. Concepts like memory-mapped I/O, DMA controllers, page tables, swapping, and more all require memory manipulation, which is possible with C.
- Lack of abstraction: C has no complex data structures like hashes, trees, or linked lists, and should the programmer require them, they are expected to implement their own. This provides more granular control over the code, as the programmer can tweak the details of the implementation as needed for efficiency.
- No runtime needed: Unlike Java and Python, C requires no runtime. You only need a mechanism to call the main() function, and C will happily run. This means you can run C programs directly on the hardware without struggling with memory management, process management, etc.
- Portability: C has been ported to many different CPU architectures, making it an excellent choice for writing kernels that support multiple architectures.
However, C itself is usually insufficient for writing an entire kernel, as there are cases where you need to write assembly code:
- When manually written, assembly can be better than what the compiler produces. Some operations can be hard to implement in C, and the compiler-produced machine code can be convoluted or inefficient. In that case, writing assembly manually is a better choice.
- Some code is impossible to write in C. For example, when task switching in C, it’s impossible to save the registers onto the stack or save the stack pointer into the task-control block because C provides no direct access to the stack pointer.
Why can Go be an alternative language for OS development?
High-level languages like Go offer some desirable features that, on the surface, make them seem like an excellent choice for OS development:
- Certain types of bugs are much less likely in a high-level language. Buffer overruns and use-after-free bugs are virtually impossible in languages like Go. Even C code written very carefully by expert programmers inadvertently includes bugs like this. The use-after-free bug is so common that the Linux kernel consists of a memory checker to detect some use-after-free and buffer overrun bugs at runtime.
- Handling concurrency is easier in high-level languages, as almost every high-level language comes with the mechanisms necessary to handle concurrency built in.
- The type safety of a language like Go can protect against C’s relaxed type enforcements.
Why is Go not used in OS/Kernel development?
Though Go offers desirable features that can make the life of an OS developer easier, it also has a few limitations.
As a language with garbage collection, Go isn’t really suited for OS development. Writing a kernel in Go means tiptoeing around Go’s garbage collection by writing code carefully in order to minimize heap usage. As explained in this Reddit thread, mouse lag is likely because the interrupt handler allocates memory that triggers garbage collection.
Go also requires an extensive runtime to execute, which means it can’t be run directly on the hardware. Although TinyGo can compile Go to run on bare metal, it supports only a tiny amount of architecture compared to C, which can be run on virtually any architecture.
Another related issue is that syscalls make up a large number of the operations in a typical Go runtime. The runtime communicates with the OS for various operations, such as writing a file or starting a thread. However, when you’re writing an OS, you have to implement these operations on bare metal and hack the Go runtime to call your implementations. The question then boils down to whether you really want to spend so much time and effort on hacking the runtime, whereas other languages like C allow you to start writing the OS immediately.
As you’ll see, it isn’t impossible to write an OS in Go. However, writing a non-toy OS that can be used by general users is nearly impossible. It’s easy to write an OS that boots on a single architecture and drops into a shell, but using Go to write an OS that runs on multiple architectures, supports different devices such as video cards or network cards, and is maybe POSIX-compliant would be very challenging. You can definitely omit some of those features, but that would limit the viability of the OS.
Operating systems written in Go
Although Go isn’t the most suitable choice for OS development, this doesn’t mean that writing an OS in Go is impossible, and many research projects are exploring how this can be done.
Biscuit is an OS written in Go that runs on the 64-bit X86 architecture. It uses a modified implementation of the Go 1.10 runtime, where more assembly code has been added to handle boot and entry/exit for syscalls and interrupt handlers. The boot block, written in assembly, loads the Go runtime and a “shim” layer. The Go runtime expects to communicate with an underlying kernel for various functions. Since there’s no underlying kernel, the “shim” layer provides these functions.
Biscuit provides user processes with a POSIX interface, with support for fork, exec, etc. It implements a file system supporting the core POSIX file system calls. Biscuit implements a TCP/IP stack and a driver for Intel PCI-Express Ethernet NICs written in Go. Using POSIX interfaces, Biscuit can run many Linux C programs without modifying the source code.
Biscuit, however, lacks many features such as scheduling priority, swapping out to page or disk, and security features like users, access control lists, and address space randomization.
More details about Biscuit can be found in the white paper.
gopher-os is another proof-of-concept kernel written in Go. Like Biscuit, it uses assembly to set up a Go runtime and load the kernel. However, it’s still in the early stages of development and hasn’t received any updates since 2018. It’s also broken on the latest versions of Go.
Clive is another unikernel written in Go, but it doesn’t run on bare metal, and a modified Go compiler is used to compile Clive software.
gVisor is an application kernel written in Go that implements the Linux system API in sandbox containers.
Even though C is the dominant language when it comes to OS development, Go offers features like type safety, automatic memory management, and concurrency, giving it the potential to be an excellent choice for OS development. However, a lack of fine-tuned control over the runtime and different aspects of the language, coupled with the popularity of C, make it challenging for Go to gain a foothold in the OS development world. However, some research OSs have been written in Go, and we can expect consumer-friendly OSs to be written in Go in the near future.
Subscribe to Blog updates
Thanks, we've got you!