Before there was a kernel, there was a question: what does a computer actually execute?
Not “what language do I write in.” What bytes does the CPU fetch from memory, decode, and run? The assembly-toy-language directory on my USB drive — 128 files — is the answer to that question, worked out by hand.
The Minimal ELF Binary
The first program I wrote for this project wasn’t in any language. It was a hex dump:
7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
02 00 3e 00 01 00 00 00 78 00 40 00 00 00 00 00
40 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 40 00 38 00 01 00 00 00 00 00 00 00
01 00 00 00 05 00 00 00 00 00 00 00 00 00 00 00
00 00 40 00 00 00 00 00 00 00 40 00 00 00 00 00
84 00 00 00 00 00 00 00 84 00 00 00 00 00 00 00
00 00 20 00 00 00 00 00 b8 3c 00 00 00 bf 00 00
00 00 0f 05 That’s a complete Linux program. An ELF header (the 7f 45 4c 46 magic bytes), a program header that tells the kernel where to load the code, and two instructions: mov eax, 60 (the exit syscall number) and syscall. When you run it, it exits cleanly with status 0. No compiler. No assembler. Just bytes that the kernel loader and CPU understand.
Writing this taught me what an ELF binary actually is — not a mystery file format, but a structured header followed by code. Every compiled C program, every Rust binary, every Go executable starts with these same magic bytes. The linker generates them. I wrote them by hand to understand what it generates.
From Hex to NASM
Once you understand the raw bytes, you earn the right to abstract. NASM (Netwide Assembler) translates human-readable assembly into those bytes:
section .text
global _start
_start:
mov rax, 1 ; sys_write
mov rdi, 1 ; stdout
lea rsi, [rel msg] ; buffer
mov rdx, 6 ; length
syscall
mov rax, 60 ; sys_exit
xor edi, edi ; status 0
syscall
section .rodata
msg: db "Hello", 10 Seven instructions. Two syscalls — write and exit. No libc, no printf, no runtime. The program asks the kernel directly to write 6 bytes to file descriptor 1 (stdout), then to terminate. This is what printf("Hello\n") actually does, 47 abstraction layers down.
The Bootstrap Chain
The most ambitious part of the assembly experiments was the bcompiler chain — a series of programs where each stage compiles the next, adding one feature at a time:
hex1 → hex2a → hex3 → hex4 → hex5 → bcc hex1 is written in raw hex. It reads hex digits from stdin and outputs bytes — the simplest possible assembler. hex2a is written in hex1’s input format and adds support for comments and labels. hex3 adds macros. Each stage is written in the language that the previous stage understands.
This is the same principle behind GCC bootstrapping itself — you need a compiler to compile a compiler. The chain starts at the absolute bottom (raw hex) and works up. By bcc, you have something that resembles a C compiler, built from nothing but hex bytes and the programs that processed them.
I didn’t complete the full chain to a working C compiler. The point wasn’t the destination — it was understanding that there’s no magic. Every tool you use was built by a tool that was built by a simpler tool, all the way down to someone typing hex into a file.
What This Taught Me
There is no bottom turtle. Software developers work at abstraction layers — high-level language on top of compiler on top of assembler on top of CPU microcode. The assembly experiments removed every layer and showed what’s left: bytes that the CPU fetches, decodes, and executes. Once you’ve seen the bottom, every layer above it makes more sense.
ELF is a format, not a mystery. Before this project, compiled binaries were opaque files. After writing an ELF header by hand, I could read hex dumps of any binary and understand the structure. This directly fed into TerranoxOS’s ELF loader — parsing the format I’d once written manually.
Syscalls are an API. The kernel exposes a numbered interface — write is 1, exit is 60 on x86_64 Linux. User programs communicate with the kernel by putting a number in rax, arguments in rdi/rsi/rdx, and executing syscall. Everything else — function signatures, error codes, errno — is wrapping by libc. The raw interface is simpler than it looks.
Bootstrapping is a real engineering problem. You can’t use tools that don’t exist yet. The bootstrap chain is how you get from nothing to something — each stage creates the tool that the next stage needs. GCC does this. LLVM does this. I did it in miniature.
This is Part 2 of 8 in the OS Kernel Museum series.