jmcc: the Portable C Compiler on RISC-V 64
For a few months there has been a compiler in my tree that I have said nothing about, and it has now done the one thing that made me want to write about it at last: it compiles the whole of QSOE/N — the Skimmer microkernel, taskman, the C library, the dynamic loader, the shell, and every driver and userspace program — and the operating system it produces boots on real hardware and logs in as root, with no GCC anywhere in the toolchain that built it.
The compiler is jmcc, and it is the Portable C Compiler. If that name means nothing to you, the next part matters more than any of the engineering that follows; and if it does mean something to you, then you already understand why I have wanted to write this for months.
What pcc is, and what Anders Magnusson did to it
The Portable C Compiler — pcc — was written by Stephen C. Johnson at Bell Labs in the mid-1970s, the same Johnson who gave us yacc and lint, and it was one of the first compilers built deliberately to be moved from one machine to another: only a small part of it knew anything about the hardware underneath, and everything else was portable. That decision is a large part of why C and Unix travelled as far as they did. pcc was the C compiler that came with Seventh Edition Unix, it stayed the C compiler of Berkeley Unix through to 4.4BSD in 1994, and over the years it was retargeted to something like 200 architectures; by the early 1980s, as Dennis Ritchie told it, nearly every C compiler in the world descended from Johnson's.
Then GCC arrived, and pcc was retired from the systems it had carried. It did not disappear, though, and this is the part I want to give its proper weight, because the compiler I am writing about owes far more to this second chapter than to the first. In the mid-2000s Anders Magnusson took the old Johnson code and did not merely dust it off — he rewrote well over half of it, brought it up to real modern C, kept it under a BSD licence, and, most important for me, restructured it so that porting it to a new architecture, and to the 64-bit architectures in particular, became a genuinely tractable job rather than a heroic one. The amd64 back end was already there when I began, and it was good; that was his doing, not mine. The plain fact is that a RISC-V port was thinkable for one person only because of the shape Magnusson had left the compiler in.
So the name. jmcc is the Johnson–Magnusson C Compiler: Johnson, who wrote it; Magnusson, who rebuilt it and kept it alive. My own part is the newest and by far the smallest layer — a RISC-V 64-bit back end, and the work of getting it good enough to build a real operating system, which is a target pcc had not had before.
How I came to this
I met C later than Pascal. The first book I ever saw it mentioned in was a half-pirated thing with a title along the lines of "system software for the ZX Spectrum," which described the HISOFT C 3.0 compiler; and then I borrowed Kernighan and Pike from a library, and that book may well have changed my life. So from the mid-1990s I have carried a fond memory of those strange identifiers that begin with "yy," and I know the small joke the GNU people made when they named their own versions of those two tools bison and flex — and that, more or less, is where my deep knowledge of parsers and lexical analysers ends.
The knowledge is shallow; the interest is not. I have always been drawn to compilers, and drawn more still to system software that is not bloated — programs small enough that one person can hold the whole of them in his head. So when I learned, in the mid-2000s, that Anders Magnusson was actively building a new incarnation of pcc, I was delighted; I followed it, I remember learning that pcc had been taken into OpenBSD, and I remember, some years later, learning that Anders had stopped working on it.
It was around that same time that RISC-V began to interest me, and somewhere around 2020 the thought arrived and would not leave: what if one were to try to port pcc to this architecture? It stayed only a thought, because on my own I could not have done it. It became possible only now, with Claude Code. In March of this year I gave it the first task, and qrvcc — as it then was, aimed at my QRV system — took its first shape: the classic pcc back-end files, 32 integer and 32 floating-point registers, the LP64 ABI, a RISC-V target copied from pcc's mips64 one. Two days later it emitted its first RISC-V assembly. Then it sat untouched for four months, while I gave my time to QRV, to QSOE, and to the HFI BIOS instead.
The July sprint
In July I came back to it with a different question — not whether it could compile toy programs, but why not point it at a real system and see what happened. QSOE/N was the obvious candidate, because it is my own operating system and I know it well enough to tell when a compiler is lying to me. I renamed the compiler jmcc, retargeted it from QRV to QSOE, and gave Claude Code a week.
The RISC-V back end had begun as a copy of pcc's mips64 target, and it turned out that it had never really run. Almost every early failure was a MIPS-ism that had survived the copy: branches emitted in a two-operand form RISC-V does not use; a subi instruction that does not exist on RV64; int and long treated as a single width, so that a 32-bit field was loaded with a 64-bit access and the slot beside it was quietly trampled; 12-bit immediates accepted with no range check at all; function prologues that broke the moment a frame grew past 2 KB. Measured against the kernel, jmcc moved over that week from 26 of 32 files compiling — and none of them assembling — to all 32 doing both.
Where an old compiler meets a new machine
The character of the whole effort sits in a single observation: pcc was built around an older idea of what a machine is, and RISC-V is not that machine, and the old code was never wrong when it was written, only wrong here. pcc's optimiser, to take one example, will happily fold a load from a constant address — the shape that every memory-mapped register access and every assert() ultimately takes — into a direct memory reference with no register involved, which is exactly right on a machine with absolute addressing and simply impossible on RISC-V, which has none, so that on RISC-V the address has first to be placed in a register. Atomics were a larger instance of the same kind of work: the load-reserved/store-conditional sequences, the atomic read-modify-writes, the compare-and-swap loops and the fences all had to become real operations that the compiler understands and will neither hoist nor reorder, carrying acquire and release ordering and never anything weaker, and it was when those landed that the Skimmer kernel compiled and assembled in full for the first time. There were subtler cases as well — the 64-bit ABI keeps a value narrowed to 32 bits in a particular sign-extended form, and a missing canonicalisation there is the kind of defect that lets an entire OS build and boot and still compare a credential wrongly — but the shape of them is always the same, and none of it is worth dwelling on at length, least of all in an era in which finding and fixing such things is no longer the slow handwork it once was.
The whole OS, and no GCC
By the end of July jmcc reached the point I had been aiming at: it builds the entire QSOE/N with no build failures, and the operating system it produces boots to a login prompt and logs in as root to a working shell, at parity with the GCC-built system — no libgcc, and no GCC headers. That last part is the one I care about most, because it means jmcc is not a front end bolted onto someone else's machinery but a complete and independent toolchain: its own preprocessor, its own freestanding headers, and its own runtime library, libjmcc_s.a, which carries what a freestanding C program needs, including a full software binary128 implementation, since on RV64 long double is real IEEE quad and I wanted its constants to be bit-for-bit identical to what GCC produces, which they are. On the portable-C conformance suite it stands at 212 of 220, and the few it misses are known and unremarkable.
rvasm, and asm359
jmcc can also emit a second assembly dialect, rvasm — a NASM-flavoured syntax for RISC-V, with Intel-style [base+disp] operands, proc and endp around functions, and db/dw/dd/dq for data. This is not a whim, and NASM is not foreign ground for me: I was active in the NASM project between 1999 and 2004, sent a fair number of patches, and maintained its RDOFF subsystem. When I built asm359, the assembler for my System/359 micro-mainframe, I deliberately separated the two Siamese brothers that NASM keeps joined — the assembler proper and its powerful preprocessor — and rvasm is that separation paying off: it reuses that very same preprocessor, very nearly verbatim from asm359, and puts behind it an assembler whose syntax resembles NASM's, because I like that syntax very much. QSOE/N builds and boots under jmcc and rvasm together as well.
rvasm, the preprocessor it inherits, and the whole asm359 lineage behind it deserve a post of their own, and they will have one.
And x86-64
Lest this read as a RISC-V-only exercise, jmcc targets x86-64 as well — though I can take little credit for that, since Magnusson's amd64 back end was already there and already good. It lives in my tree as a secondary target, its hosted programs compile, link against the system C library and run, and it needed only small adjustments from me to fit the rest of the toolchain around it. The primary target, the one that builds an entire operating system and then boots it, is and remains RISC-V 64.
In closing
I will not make this sound larger than it is. A compiler about half a century old, already rewritten once by someone else, has been given a new back end and now builds a small and unbloated operating system for a new architecture, and that system runs. What pleases me is not novelty for its own sake but the particular shape of the thing: a toolchain I can read in full, a kernel I wrote myself, a runtime that owes nothing to anyone, and the fact that all of it became possible only because a tool I could not have imagined in 2020 now does the parts I never had the years to do by hand. This is the first time I have written about jmcc anywhere, and there will be more.