Skip to main content

Updated Sep 3, 2026

The Kernel Is Not C You Know

Freestanding C with no libc, no floating point, a tiny stack, GCC extensions in daily use, and the annotations sparse checks.

Kernel code is C. It is compiled by the same GCC or Clang that builds ordinary programs, and its syntax is unmistakably C. And yet the first unfamiliar file you open reads wrong — functions that look like they should exist don't, macros expand into things that look nothing like C, and every third declaration carries a word you've never seen attached to a type before. None of that is an accident or a house quirk to shrug off. The kernel is freestanding C — a C dialect that runs with no operating system underneath it, because it is the operating system — with a deliberate house style and a small, closed set of annotations layered on top. Once the five or six unfamiliar constructs are named, the strangeness disappears; it turns out to be a small vocabulary, reused constantly.

Freestanding, not hosted​

A hosted C implementation — the one taught in every C course — assumes an operating system underneath it: a C standard library to link against, a main() the OS calls to start the program, and a heap the OS manages for malloc. A freestanding implementation assumes none of that, because there is nothing underneath it to provide it. The kernel is the canonical freestanding C program, and it supplies its own version of everything a hosted program takes for granted:

  • No libc. There is no printf — there is printk, which formats into a ring buffer instead of a file descriptor and tags each line with a priority. There is no malloc/free — there is kmalloc/kfree (and vmalloc, and the per-CPU and slab allocators besides), each with different guarantees about physical contiguity and the context it's safe to call from.
  • No main(). Execution doesn't begin at a function the compiler and runtime conspire to call for you — it begins at an architecture-specific boot entry point, works through early setup with almost nothing initialized yet, and eventually reaches start_kernel() in init/main.c, which is as close to "the kernel's main" as anything gets.
  • No standard headers. <stdio.h>, <stdlib.h>, and friends don't exist in kernel context; the kernel's own include/linux/ and lib/ supply the equivalents it actually needs — memcpy, strlen, snprintf, and so on are real functions with real implementations in lib/string.c, not calls into a C library the kernel doesn't have.

The pattern to notice is not "the kernel renamed some functions" — it's that there is no runtime beneath kernel code to reach for. Every convenience a hosted program gets for free, the kernel had to build for itself, and each one has a kernel-specific name precisely so nobody mistakes it for the libc version with libc's assumptions attached.

No floating point​

This one surprises people early enough that it's worth stating before anything else: kernel code, by default, does not use floating-point or SIMD registers, and the reason is not a stylistic preference — it's that the kernel does not save and restore FPU/SSE/AVX register state across a context switch the way it saves general-purpose registers. Doing so on every switch would cost cycles that the overwhelming majority of kernel code, which never touches a float, shouldn't have to pay. So the kernel simply doesn't; ordinary kernel code that used float or double would be silently corrupting whatever user-space task last used those registers, or reading garbage left behind by one.

The small set of places that genuinely need vector instructions — some cryptography, some RAID implementations — bracket that use explicitly with kernel_fpu_begin() and kernel_fpu_end(), which save the current FPU state, make it safe to use SIMD for the duration, and restore it on the way out. Outside that bracket, floating point in kernel context is not merely unidiomatic, it's a bug.

The stack is 16 KB (and used to be 8), and that is all​

A user-space thread's stack grows on demand, typically up to several megabytes, and a stack overflow is something most programs never think about. A kernel stack is a small, fixed-size allocation carved out once per task, and there is nothing behind it that grows — overrun it and you corrupt whatever memory happens to sit past the end.

The size is architecture- and configuration-dependent. On x86-64 at v6.18, with a stock (non-KASAN) configuration, THREAD_SIZE works out to one page shifted left by THREAD_SIZE_ORDER, which is 2 — four 4 KB pages, i.e. 16 KB — and CONFIG_KASAN adds one more order of shift stack-instrumentation overhead needs, pushing it to 32 KB. The historical "8 or 16 KB" framing you'll see in older writing about the kernel reflects other architectures and older x86 configurations; on current x86-64 the floor is 16 KB. Either way, the number is small enough that the rules it implies are absolute, not stylistic advice:

  • No large local arrays or structs on the stack. A char buf[4096] local variable is a quarter of the entire budget.
  • No deep or unbounded recursion. A recursive algorithm with no hard depth bound is a latent stack overflow waiting for sufficiently adversarial input.
  • CONFIG_FRAME_WARN catches what review misses, at build time: it warns when any single function's stack frame exceeds a configured byte threshold, which is exactly the failure mode — one function silently eating an unreasonable share of a fixed 16 KB budget — that's easy to introduce by accident and hard to notice by reading.

GCC extensions in daily use​

The kernel's minimum-supported compilers accept a set of GNU C extensions beyond strict ISO C, and the kernel leans on several of them constantly enough that reading kernel code fluently means recognising them on sight.

Statement expressions — ({ ... }) — let a block of statements evaluate to a value, used everywhere a macro needs to do more than one thing and still act like an expression:

#define max(a, b) ({ \
typeof(a) _a = (a); \
typeof(b) _b = (b); \
_a > _b ? _a : _b; })

Simplified — the real max()/min() in include/linux/minmax.h wrap this in the more elaborate __careful_cmp machinery, which also catches signedness mismatches between a and b at build time.

typeof recovers a variable's type without naming it, which is what makes the macro above type-generic and is the same trick container_of (later on this page's sibling) depends on.

__attribute__((packed)), __attribute__((aligned(N))), __attribute__((always_inline)) control layout and inlining precisely where the compiler's defaults would be wrong — packed for a struct that mirrors an on-the-wire or on-disk format with no compiler-inserted padding, aligned for DMA buffers and cache-line separation, always_inline where a function must not become a real call (some lock-acquire fast paths).

Designated initialisers are used pervasively for the kernel's ubiquitous "ops struct" pattern — a struct of function pointers that a subsystem fills in to plug an implementation into a generic framework:

static const struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.read = my_read,
};

Only the fields that matter are named; the rest are zero-initialized, which for a function-pointer field means "this operation isn't supported" — the framework checks for NULL before calling it.

__builtin_* functions — __builtin_expect (branch-prediction hints, usually seen wrapped as likely() /unlikely()), __builtin_offsetof (which offsetof() itself expands to), __builtin_unreachable, and others — are the compiler-intrinsic layer the kernel's own macros are frequently built on top of.

The annotations​

A handful of __-prefixed tokens attached to types and declarations aren't decoration — some are compiler attributes with real enforcement, some are markers sparse (below) understands and the compiler otherwise ignores.

AnnotationMeaningEnforced by
__initPlaced in a discardable code section; the memory is freed after boot completesCompiler (section placement) — freeing after boot is the boot code's doing
__exitPlaced in a code section that's discarded entirely in a non-modular build (a function that only matters when unloading, which can't happen if the code was never a module)Compiler (section placement)
__initdataSame discardable-section treatment as __init, for data instead of codeCompiler (section placement)
__userPointer into user-space address space; must never be dereferenced directly in kernel contextsparse only — the compiler accepts a bare dereference silently
__percpuPointer to a per-CPU variable, not a plain address — must go through the per-CPU accessor APIsparse only
__iomemPointer to memory-mapped I/O; must go through readl/writel-style accessors, not ordinary loads/storessparse only
__rcuPointer protected by RCU; must be read and updated through the RCU accessor APIsparse only
__must_checkCaller must inspect the return valueCompiler (-Wunused-result, a real warning)

The split matters: __init/__exit/__initdata and __must_check change what the compiler actually does. __user/__percpu/__iomem/__rcu change nothing about the compiled output by themselves — the compiler treats the annotated pointer as an ordinary pointer of the same underlying type. They exist so a separate tool can catch the class of bug where kernel code treats a special address space as if it were an ordinary one.

sparse​

sparse is that separate tool: a static checker built for exactly this job, invoked with make C=1 (or C=2 to check even files that didn't need recompiling). It understands the address-space annotations above as real types with real rules — a __user pointer assigned directly to a plain pointer, or dereferenced without going through copy_from_user()/copy_to_user(), is a sparse error even though it's a perfectly legal, silently-accepted assignment as far as the C compiler is concerned. Without sparse, __user is a comment a reviewer might notice; with it, __user is enforced.

Optimisation barriers you will meet​

Two more names appear constantly in code that touches memory shared between CPUs or between a CPU and an interrupt handler: READ_ONCE()/WRITE_ONCE(), which force a single, non-elided, non-reordered-by-the- compiler memory access instead of one the compiler is free to cache in a register or split into pieces; and barrier(), a compiler-only ordering fence with no CPU instruction behind it. Both matter a great deal and neither is explained here — memory ordering is a large enough topic that it gets its own treatment in folder 09. For now, recognise the names and know they exist for correctness under concurrency, not performance.

References​

  • Linux kernel coding style — the house style, which explains as much of the kernel's strangeness as the language extensions do.
  • Sparse: a semantic parser for C — what the address-space annotations buy you, and how to run the checker over a subtree.
  • GCC: C extensions — the primary reference for the compiler extensions the kernel relies on daily: statement expressions, typeof, and the attributes.