Your program doesn't run by itself.
You write code, compile it, ship a binary. Something on the other side — the OS, the kernel, the thing you usually don't think about — reads that binary and decides what to do with it. The two sides have a contract. That contract is the ABI.
Most engineers have encountered the term. Few can define it clearly. Here's what it actually means, and why you have to get every detail right before anything works.
It's not the API
API is the code-level contract: call this function with these arguments, get this result back. You interact with it through source code. Your compiler enforces it.
ABI is the binary contract. It's what the compiled output of your code actually looks like, and what the system that runs it expects to see. There's no compiler holding your hand here — you either match the spec or nothing works.
For Linux, the ABI is three things:
The binary format. Linux uses ELF — Executable and Linkable Format. An ELF binary has a header: a magic number (\x7fELF), a field saying whether it's 32-bit or 64-bit, a field for the target architecture, the address to start executing at. The kernel reads this header before it runs a single instruction of your program. If it's wrong or missing, the kernel doesn't try — it just refuses the file.
The calling convention. When you call a function, arguments don't just float magically into the callee. They go in specific places — specific registers, in a specific order. On x86_64 Linux: first argument in rdi, second in rsi, third in rdx, fourth in rcx, and so on. The return value comes back in rax. If your compiler and the system don't agree on this, calls return garbage. This is why you can't freely mix code compiled with different conventions — the ABI mismatch corrupts everything silently.
The syscall interface. Your program can't talk to hardware directly. It asks the kernel. On x86_64 Linux, you put a number in rax — the syscall number — arguments in registers, then execute the syscall instruction. The kernel takes it from there. write is syscall 1. exit_group is syscall 231. mmap is 9. These numbers are stable across kernel versions — part of the ABI. They don't change.
Hello world at the bottom
The hello world in the repo is a #![no_std] Rust binary — no C runtime, no libc, no allocator. Direct syscalls via the x86_64 ABI, using Rust's inline asm:
fn sys_write(fd: u64, buf: *const u8, len: usize) -> i64 {
let r: i64;
unsafe {
asm!(
"syscall",
in("rax") 1u64, // syscall 1: write
in("rdi") fd,
in("rsi") buf,
in("rdx") len,
lateout("rax") r,
out("rcx") _,
out("r11") _,
options(nostack),
);
}
r
}
fn sys_exit(code: i32) -> ! {
unsafe {
asm!(
"syscall",
in("rax") 60u64, // syscall 60: exit
in("rdi") code as u64,
options(noreturn, nostack),
);
}
}
That's the whole program. No function calls to a library. No runtime initialization. Just: load the syscall number, load the arguments into the right registers, execute syscall. When the kernel runs this, it reads the ELF header to find the entry point, loads the program into a fresh address space, drops to ring 3, and gets a write syscall followed by exit.
The ABI is the agreement that made every one of those steps work.
Why it matters for a kernel
If you're building a kernel that claims Linux ABI compatibility, the syscall interface is the spec. All of it. Every syscall number, the register convention for arguments, the format of the ELF binary it loads. There's no "mostly right" here. Get it wrong and every compiled binary on the planet refuses to run on your kernel.
Get it right and every compiled binary works — without recompilation, without modification, without knowing you're not on Linux.
That's the goal for the Cyphera Kernel. Not "runs Linux." Speaks the same language as Linux, so your existing software runs without changes.
The hello world proof
The repo has a minimal hello world: the smallest thing that proves the ABI is working. Static binary, two syscalls, no dependencies. If this runs, the ELF loader is correct, the syscall dispatch is correct, and the calling convention is wired up right.
This is the foundation. Before PostgreSQL can run, before Alpine boots, before any of the workloads in the next post — this works. Everything else is just syscall coverage from here.