The
Silent
Tower

gint: an embedded kernel/hypervisor on graphing calculators

I know what you're thinking — “how can a piece of software be both a kernel and a hypervisor at the same time?” Well, basically it's a kernel embedded in an application that somehow bypasses the OS to run bare-metal... and gets away with it. But it will make more sense with a bit of context.

A brief history of add-in programming #

CASIO graphing calculators used to provide a variant of BASIC as their main programming language. It's pretty garbage as a language: no local variables, no functions, non non-trivial data structures, and pretty slow on top of that. It's now been mostly replaced by Python (specifically a port of MicroPython), which is a way better language option, but still struggles with performance and a deep lack of system and I/O functions.

But then there's the rabbit-hole option. In the old days of the fx-9860G (2005), CASIO maintained an official SDK for writing native applications in C/C++ called add-ins. The SDK is actually a full IDE and pretty decent, complete with a high-quality emulator and a debugger. Its only big weakness was the C89-only Renesas compiler that shipped with it, and no we don't talk about that text “editor”.

[Image: Official fx-9860G SDK running under wine]
The official fx-9860G SDK now running under Wine. (Click to enlarge)

There were early successful attempts at developing add-ins with custom tool sets around 2011, motivated by the release of the Prizm fx-CG 20 for which there was no SDK. CASIO stopped maintaining SDKs because they were wary of counterfeit products, a stance that remains to this day. Various community alternatives were developed, and the GCC-based Prizm SDK came out on top, under Windows at the time. This work was then applied to build fx-9860G add-ins under Linux with libraries extracted from the official SDK.

And the libraries is where the fun begins, because it turns out that there is no userspace. Add-ins run in the CPU's privileged mode, the OS's syscall interface is a normal call through a table of function pointers, and many library functions just access hardware directly. The only userspace-like feature is that the binary file and RAM segment are mapped to memory at fixed addresses by the MMU. There is so much not a userspace that I strongly suspect the entire OS is a monolithic program statically linked with a RTOS kernel provided by the chip manufacturer, Renesas.

Which begs the question: how much can we abuse this?!

The answer is we can abuse everything. You're in for a treat.

World switching out of a half-hosted environment #

This kernel project was originally based on a self-debugger concept by Kristaba. The basic idea of the self-debugger is to take control of a single interrupt (the debugger interrupt) to implement breakpointing functionality, and leave all of the others to the original OS handler. This stunt, by itself, is already trickier than you might expect. These calculators run on SuperH SoCs, and SuperH's exception handling mechanism is all governed by a single register called VBR (Vector Base Register), defining three interrupt-handling routines:

Since add-ins run in privileged mode, overriding VBR only takes one line of assembly code; the problem is that suddenly every single exception and interrupt goes to the breakpoint handling function. Redirecting unrelated interrupts to the OS handler is possible but the OS actually has multiple handlers that all change VBR liberally to trick each other into believing they're “the one”, and it gets out of hand pretty much instantly.

So instead of dealing with this mess, I decided to go for it and just design an entire kernel that would drive itself, and stop using any OS code, official libraries or syscalls at all (because clearly that'd be easier). The project was named “gint” (pronounced /gɪ:n/) after “gestionnaire d'interruptions” (French for “interrupt handler”).

This is where the funny unikernel/hypervisor design comes into play. Taking over hardware and using it is one thing, but the add-in has to exit at some point, and the OS will not pick up hardware in any random state. For one, we don't just change VBR, we also stop OS timers, disable interrupts we don't handle and enable some new ones, change clock settings, stop the USB module, and more. Early programs that changed VBR (other than Kristaba's self-debugger) dealt with this by not dealing with it and rebooting the calculator when leaving the application. In fact they had the fairly unique feature of rebooting the calculator randomly, a baseline on which I was eager to improve.

To solve this problem, gint is equipped with a sort-of-hypervisor that can perform hardware context switches, by saving and restoring hardware modules' state. For instance, it saves timers' delays, counters and settings when starting the add-in, and restores them when leaving. This way, we can remove OS timers and use our own while the add-in runs, then later restore the originals and pretend that no low-level trickery ever happened, even though it definitely happened.

[Image: Driver ownership block diagram]
Block diagram for driver ownership around the hypervisor.

The process is shown on the above diagram. When add-ins are started the native OS owns VBR and handles all the hardware driving (“OS-owned”). In order to switch to gint, the mini-hypervisor saves each module's internal state, powers on modules that gint wants to drive, then gives gint control of VBR and lets it initialize the modules to their desired startup state (“gint-owned”). When the add-in needs to exit or run hardware-related OS code, gint's hardware state is saved and the hypervisor restores the previously-saved OS state.

Yatis coined the term “world switches” for these context switches, as another nod to virtualization techniques even though it's a lot simpler. There are some module-specific complications such as finishing DMA transfers, re-establishing USB communication which gets cut off during a switch, etc — but I'd be here all day if I got into these details.

What's really nice is that the OS is completely oblivious to the hardware takeover. I'm quite proud of how stable the whole process is; in addition to working on a few different MPUs, it's so stable that it can be used programmatically at any moment in a program:

#include <gint/gint.h>

static void run_in_OS_world(void)
{
    /* This is run in OS world */
}

int main(void)
{
    gint_world_switch(GINT_CALL(run_in_OS_world));
}

The result is that we now have a self-contained bare-metal environment running inside a half-hosted one. There are some cases in which we still rely on OS code (to setup the MMU, to access the filesystem and to invoke the calculator's main menu) but these are all handled by world switches. So now we get to write our own drivers, APIs, libraries, and toolchains; in layman's terms, it's party time!

Building up drivers and development tools #

I've said gint was a unikernel but haven't yet explained what that means. Broadly speaking, it's a kernel that's statically-linked to an application as a library, resulting in a free-standing executable running on a bare-metal or virtualized platform. Unikernels don't normally implement all kernel features such as userspace and complete privilege separation. In fact, it's quite the opposite: their intrication of kernel and application code is the objective as it allows for unique performance improvements.

The unikernel design in gint was the result of steady evolution starting from 2015, so it's pretty tame and monolithic, with each driver directly going from hardware control to high-level APIs for applications. The following diagram (discussed a bit later) sums up the different components that go into gint add-ins:

[Image: gint's structure block diagram]
Block diagram for add-in applications running with gint.

Nonetheless, these days gint has a number of traditional subsystems that will be familiar to kernel enthusiasts:

These components are all part of the “gint unikernel” block in the figure above and implemented in the gint repository. As previously hinted gint has no userspace so add-in code can still access hardware; nonetheless, there is a software/API contract that only drivers can use the hardware directly, and they must implement the hypervisor interface to be handled properly during world switches.

Going back to the diagram, where the userspace would usually sit, we find the application code along with the C/C++ runtime obtained by linking with a custom C library and the GNU C++ library provided with the cross-compiler.

Which leads me right back to where we started: the SDK. gint comes with a more modern SDK called the fxSDK. The job of the SDK is to tie together the development process from distribution to compilation to execution, and I shall be struck by lightning if I ever try to pretend that I knew what I was getting into on this front. Currently, the fxSDK includes:

... yeah, it's a lot... and I haven't even talked about distributing the whole thing, which I shall defer to another day!

Compatibility and showcase #

gint runs to some degree of compatibility on almost 20 years' worth of CASIO graphing calculator models; anything that remotely resembles an fx-9860G or an fx-CG is likely to work. The fxSDK itself runs on Linux (including WSL), Mac OS, and has been spotted running on OpenBSD on a sunny day.

Over the years, members of the Planète Casio community (in which the gint/fxSDK combo is the most popular C/C++ development option) and other users from around the world have created awesome programs with the SDK.