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”.
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:
VBR+0x100
for exceptions;VBR+0x400
for TLB misses (page faults);VBR+0x600
for interrupts.
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.
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:
Nonetheless, these days gint has a number of traditional subsystems that will be familiar to kernel enthusiasts:
- An exception/interrupt handling system (extensible with custom interrupt handlers and crash handlers);
- Low-level drivers: clock control, timers, RTC, DMA, keyboard, display,and others;
- A built-in heap allocator (a 16-class segregated best-fit);
- Asynchronous USB communication;
- A Unix-like filesystem interface (extensible with custom file types);
- A basic double-buffered (sometimes triple-buffered) rendering interface.
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:
- A cross-compiled binutils and GCC (with the C++ library) along with a build automation system.
- A custom C standard library together with a ported OpenLibm.
- The fxSDK tools themselves also include:
- a CMake-based build system and tools to generate binary files in the calculator-specific format (flat binary with some metadata in a header);
fxconv
, an extensible asset converter to encode images, fonts and other resources in fast-to-use binary formats and embed them in programs;fxlink
, an interactive communication tool which can send files across USB and dynamically interact with add-ins to send data, receive screenshots, record videos, and then some.
... 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.
- Of the many many games, I can mention Terrario, a Terraria clone on the fx-9860G series by KBD2 (below, left). Terrario uses gint's gray engine to produce 4-color images on a black-and-white screen by flipping two buffers quickly. The other photo is of OutRun, an original interpretation of the famous arcade game on the fx-CG 50 by SlyVTT (below, right).
- Yatis has pushed the kernel concept quite a bit further. He's prototyped a number of systems within the Vhex project, one of them including an ELF loader with PIC and shared library support. Some versions have used gint as a boot mechanism.
- In 2021, Manawyrm and TobleMiner used a prototype serial driver to connect an fx-9750GII to a PC and host a web server on it (!!) using a port of uIP, a TCP/IP stack. The calculator then worked as a fully-functional IP network node, with a static web server and an IRC client. (repository, article).
- A work in progress at the time of writing, Redoste has prototyped a remote GDB interface for the fx-CG emulator that is being extended for live debugging on the calculator using the USB driver and support for the hardware debugging module. Coming full circle from Kristaba's self-debugger!
Related links #
- gint : un environnement de développement d'add-ins (planet-casio.com, in French)
- Reverse-engineering documentation related to the project (bible.planet-casio.com)
- Source code at repository
Lephenixnoir/gint