Chapter F.4: The C You Need§

Authors: Shrey Patel and Jay Patel, Coconut Labs
Book: Think Like an OS. See book/00-INDEX.md

This is not a C tutorial. You already know a language well: Python, TypeScript, Java, Go, Swift. What you lack is the specific working subset kernel code is written in: pointers used aggressively, structs as an object system, one strange macro (container_of) that appears on nearly every page of kernel source, bit manipulation as a first-class skill, goto used on purpose, and a compiler legally allowed to ruin your day if you break its rules. Two kinds of reader end up here. If you are heading for kernel work, this is the literacy every later chapter assumes. If you are heading for low-latency trading, it is the same subset for the same reasons: code that must not pause, must not allocate behind your back, and must put its bytes exactly where the next reader expects them. By the end you can open a random file in the Linux 6.12 tree and recognize every idiom on the screen, and you will have built two of those idioms yourself in a Docker container.

The problem§

Every later chapter eventually lands in kernel source, and kernel source is C in a particular dialect and a particular house style. A course teaches you the language. It does not teach the idioms, and the idioms are where first-time kernel readers get stuck.

Concretely: when we read how a driver plugs into the kernel, you will meet a struct of thirty function pointers and must recognize it instantly as an interface. When we walk the scheduler's task lists, you will meet container_of, a macro that appears to do arithmetic on the number zero. Without it every linked structure in the kernel is unreadable. In syscall error paths you will meet goto, used systematically and correctly. And to understand kernel security bugs you need undefined behavior: the reason a compiler once silently deleted a safety check from Linux and turned a small bug into a root exploit. None of this is hard. All of it is specific.

None of it is kernel-only either. If you are headed for a trading engine rather than a driver, the three properties below are the same three properties that put C on the hot path there: nothing runs underneath you, the byte layout of a struct is a contract you can rely on, and an address is a number you can compute with.

Unfolded§

Why C still owns this layer§

Three properties keep C in charge of kernels, and each is something C lacks.

No runtime. Most languages ship a runtime, meaning support machinery that runs underneath your code. Python has an interpreter, Java a virtual machine and garbage collector, Go a goroutine scheduler. That machinery needs somewhere to stand: memory allocation, threads, a loaded library. The kernel is what provides those to everyone else. It runs below every runtime, so it can depend on none. C compiles to bare machine instructions with nothing underneath: no collector to pause you mid-interrupt, no allocator you did not ask for. A pause you did not schedule is a problem in an interrupt handler for the same reason it is a problem on an order-entry path. Neither one gets to choose when it runs.

Predictable layout. In Java you neither know nor care how an object is arranged in memory. In C, a struct is a contract: fields at knowable byte offsets, in declaration order. That matters when the thing reading your memory is not your code but a device doing DMA, another CPU, or userspace across a syscall boundary. It matters the same way when a network card writes a packet straight into a buffer you preallocated: the struct declaration is the only agreement about where each field landed.

Direct memory access. A C pointer is an address you can do arithmetic on. You can conjure a pointer from an integer because a device's control registers live at that integer. Higher-level languages forbid this; a kernel cannot exist without it.

The kernel does not use ISO C. It uses GNU C: the language plus the GCC extensions it depends on. Since the 5.18 cycle (a 2022 patch series from Arnd Bergmann, "Kbuild: move to -std=gnu11," replacing the ancient gnu89) the dialect is gnu11: C11 plus GNU extensions. In the 6.12 Makefile, verbatim:

make
KBUILD_CFLAGS += -std=gnu11

Three extensions matter enough to name now, and all three appear inside one macro later:

ExtensionWhat it allowsWhy the kernel wants it
typeof(x)Declare a variable with the type of an expressionType-generic macros
Statement expressions ({ ... })A block used as an expression; per the GCC manual, "the value of this subexpression serves as the value of the entire construct"Macros with local variables that still return a value
Arithmetic on void *GCC treats "the size of a void ... as 1"Byte-granular address math

Pointers in anger§

Baseline first. A pointer is a variable whose value is a memory address. int *p declares a pointer to an int; *p reads what it points at ("dereferencing"); &x yields the address of x. The new idea versus Java references is only that the address is a number you can see and compute with.

Arithmetic is scaled. p + 1 means one element later, not one byte: for an int * with 4-byte ints, 4 bytes higher. p[i] is defined as *(p + i).

void * is "an address, type forgotten." It is C's generic-code escape hatch. Anything converts to void * and back; the price is the compiler can no longer check what you do with it. ISO C forbids arithmetic on void * (no element size); GNU C permits it at byte granularity. Kernel code uses that freely; portable code casts to char *. You will see both below.

Pointers to pointers (struct agent **out) encode one boring idea: an out-parameter. Kernel functions conventionally return an int error code, so a function that produces an object is handed the address of your pointer and writes the object's address through it.

Function pointers are where C grows an object system. Code has addresses too:

c
ssize_t (*read)(struct file *, char *, size_t, loff_t *);

Read inside-out: read points to a function taking those arguments, returning ssize_t. Collected into a struct, this becomes the kernel's central structural idiom, the ops table. The real thing is struct file_operations, from include/linux/fs.h in 6.12, trimmed:

c
struct file_operations {
	struct module *owner;
	fop_flags_t fop_flags;
	loff_t (*llseek) (struct file *, loff_t, int);
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
	int (*mmap) (struct file *, struct vm_area_struct *);
	int (*open) (struct inode *, struct file *);
	/* ... roughly two dozen more ... */
};

A driver that wants to behave like a file fills one of these with its own functions and hands it to the kernel. When userspace calls read(), the kernel calls f->f_op->read(...), whatever the driver installed.

You knowThe kernel's version
Interface (Java/Go/TS)Struct of function pointers
Class implementing itDriver's functions + one filled-in ops struct
Compiler-hidden vtableThe same table, built by hand, visible
Dynamic dispatchf->f_op->read(...), an explicit indirect call

C has no interface keyword, so the kernel builds interfaces from structs and function pointers, in plain sight.

Structs: the kernel's object system§

A struct is named fields laid out consecutively, in declaration order. Two refinements matter.

Padding. The compiler may insert unused bytes so each field lands on an address its type prefers (8-byte pointers on 8-byte boundaries). Never guess an offset by summing sizes. The offsetof man page warns that "compilers may insert different numbers of padding bytes between fields." The lab has a trap waiting on exactly this.

Nesting is by value. A Java field holds a reference to an object elsewhere; a C struct field of struct type embeds the whole inner struct contiguously inside the outer. That is why a struct cannot contain itself (it would be infinitely large) but can contain a pointer to its own type:

c
struct my_node {
	struct my_node *next, *prev;
};

Every linked structure in C is built from that self-referential shape. The kernel's struct list_head (declared in include/linux/types.h; the operations live in include/linux/list.h) is exactly this: two pointers, and no data field at all. A Java LinkedList<Mission> owns the structure and holds references to your objects. The kernel inverts it: you embed the node inside your struct, and generic list code links the embedded nodes. This is an intrusive list, and it forces the question: if the list code only sees a two-pointer node buried in the middle of your struct, how do you get your struct back?

container_of, unfolded slowly§

The single most important kernel-C idiom. Walking pace.

Step 1: the situation. Your struct embeds a node mid-struct:

c
struct mission {
	char name[16];
	int priority;
	struct my_node node;   /* embedded, by value */
};

The list hands you struct my_node *p, a pointer to the node inside some mission. You want the mission.

Step 2: the key fact. Because the node is embedded by value, it sits at a fixed, compile-time-knowable byte offset in every struct mission that will ever exist. offsetof(type, member), from <stddef.h> (C89, kept through C11), yields that offset in bytes as a size_t.

Step 3: the arithmetic. If the node lives 24 bytes in, the mission starts 24 bytes before the node:

struct mission m (x86-64)         offset    address if m is at 0x1000
┌────────────────────────────┐
│ char name[16]              │       0      0x1000   ← what we want
│ int priority               │      16      0x1010
│ (4 bytes padding)          │      20      0x1014
│ struct my_node node        │      24      0x1018   ← p, what we have
│    .next   (8 bytes)       │      24      0x1018
│    .prev   (8 bytes)       │      32      0x1020
└────────────────────────────┘  total 40

recovered = (struct mission *)((char *)p - offsetof(struct mission, node))
          =                     0x1018  -  24     =  0x1000  =  &m

(The padding is on the page: 16 + 4 = 20, but the pointers in node want 8-byte alignment, so the offset is 24. That is why you ask offsetof instead of adding sizes.)

Step 4: the real macro, verbatim from include/linux/container_of.h in 6.12:

c
#define container_of(ptr, type, member) ({				\
	void *__mptr = (void *)(ptr);					\
	static_assert(__same_type(*(ptr), ((type *)0)->member) ||	\
		      __same_type(*(ptr), void),			\
		      "pointer type mismatch in container_of()");	\
	((type *)(__mptr - offsetof(type, member))); })

Line by line. All three GNU extensions from earlier appear:

Step 5: it is everywhere. From include/linux/list.h in 6.12:

c
#define list_entry(ptr, type, member) \
	container_of(ptr, type, member)

list_entry, which you see in every list walk in the kernel, is container_of renamed. Once this macro is in your head, intrusive lists, red-black trees, work queues, and half the kernel's data structures read the same way.

This is the section people quietly reread, and rereading it is the normal shape of the material rather than a sign you are behind. If the subtraction in Step 3 sits right with you, then list_for_each_entry in F.6 and every task-list walk in Chapter 9 are ordinary loops from here on instead of type-punning magic.

Bits: the other half of fluency§

Hardware registers, page-table entries, and flags words pack many booleans into one integer, so kernel code manipulates bits constantly.

OperatorNamePer-bit ruleTypical job
a & bAND1 only if both 1test / extract with a mask
`a \b`OR1 if either 1set bits
a ^ bXOR1 if differenttoggle bits
~aNOTinvert every bit"everything except" masks
a << nshift leftmove bits up nbuild masks: (1u << n)
a >> nshift rightmove bits down nextract a field

A mask is an integer whose 1-bits mark positions you care about. The four everyday patterns:

c
flags |=  READY;         /* set    */
flags &= ~READY;         /* clear  */
if (flags & READY) ...   /* test   */
flags ^=  READY;         /* toggle */

Worked example: Unix permission bits. A file mode packs nine booleans into one integer: three groups (owner, group, others) of three bits (read=4, write=2, execute=1). C's octal literals fit because each octal digit is exactly three bits, and a leading 0 means base 8:

mode 0644  =  110 100 100 binary
              ─┬─ ─┬─ ─┬─
           owner group others
           rw-   r--  r--

"Can the owner write?" Shift the owner group down, mask three bits, test:

c
unsigned int owner = (mode >> 6) & 07;   /* 0644 -> binary 110 */
if (owner & 02)                          /* write bit? yes */

Every flags word in the kernel is this pattern at various widths, including the ones in the Coconut agent_* ABI.

Macros: textual, unscoped, booby-trapped§

The preprocessor runs before the compiler and does pure text substitution, with no types and no scopes. Object-like macros are named constants (#define BUF_SZ 4096); function-like macros take arguments (#define BIT(n) (1u << (n))). Two traps define kernel style.

Trap 1: multi-statement macros.

c
#define poke_hw(dev)    set_bit(dev); log_poke(dev)

if (ready)
	poke_hw(dev);
else
	bail();

After substitution only set_bit is governed by the if. The if ends at the first semicolon, log_poke becomes unconditional, and the dangling else is now a syntax error. Without an else it is worse: it compiles, and log_poke silently runs every time. The kernel coding-style document (chapter 12, "Macros, Enums and RTL") mandates the fix, and its wording is "macros with multiple statements should be enclosed in a do - while block":

c
#define poke_hw(dev)			\
	do {				\
		set_bit(dev);		\
		log_poke(dev);		\
	} while (0)

A do { } while (0) is one statement that expects a trailing semicolon, so the macro behaves like a function call in every position, including one-armed ifs.

Trap 2: side effects. An argument appearing twice is evaluated twice: #define SQUARE(x) ((x) * (x)) given SQUARE(v++) increments v twice. Serious kernel macros use typeof plus statement expressions to evaluate each argument exactly once, the same machinery as container_of.

goto, rehabilitated§

You were taught goto is evil. Kernel C uses it for one narrow job: error-path cleanup. C has no exceptions, destructors, defer, or finally. Yet a function acquiring three resources, able to fail after each, must release exactly what it acquired so far. The pattern: a ladder of labels at the bottom, jumped to from failure points, cleanups in reverse order of acquisition:

c
static int agent_register(struct agent_conf *conf)
{
	struct agent *ag;
	int ret;

	ag = kmalloc(sizeof(*ag), GFP_KERNEL);
	if (!ag)
		return -ENOMEM;

	ag->buf = kmalloc(BUF_SZ, GFP_KERNEL);
	if (!ag->buf) {
		ret = -ENOMEM;
		goto err_free_ag;
	}

	ret = registry_add(ag);
	if (ret)
		goto err_free_buf;

	return 0;

err_free_buf:
	kfree(ag->buf);
err_free_ag:
	kfree(ag);
	return ret;
}

Fail at step 2: enter the ladder at err_free_ag, free only ag. Fail at step 3: enter one rung higher and fall through both cleanups. Coding-style chapter 7 ("Centralized exiting of functions") codifies this, with rationale worth keeping: unconditional statements are easier to follow, nesting is reduced, and you avoid forgetting to update one of several duplicated exit paths. In kernel source, goto out; is not spaghetti. It is the language's substitute for finally.

Undefined behavior: the compiler may do anything§

The concept, more than any syntax, that separates C from your languages. The C standard defines undefined behavior (UB) as behavior, upon use of an erroneous construct, "for which this International Standard imposes no requirements." Its own note says the permitted range starts at "ignoring the situation completely with unpredictable results." No requirements means anything.

Designing a language this way buys speed, and the mechanism is a contract. You promise certain things never happen: signed integers never overflow (CERT's INT32-C states flatly that "signed integer overflow is undefined behavior"), indexes stay in bounds, freed memory is never touched again (use-after-free). The optimizer generates fast code by assuming your promises hold. Break one, and optimization-by-assumption turns surreal: since signed overflow "cannot happen," a compiler may treat x + 1 > x as always true for signed x and delete your overflow check as dead code.

The case that made this concrete. In 2009, the Linux 2.6.30 tun driver's tun_chr_poll() contained (simplified):

c
struct sock *sk = tun->sk;   /* dereferences tun ... */
if (!tun)                    /* ... then checks it for NULL */
	return POLLERR;

The dereference comes first. GCC reasoned as follows. If tun were NULL, line one is already UB. So on any execution reaching line two, tun cannot be NULL. So the check is dead code, and GCC deleted it from the compiled kernel. Combined with a way to map memory at address zero, the deleted check became a local privilege escalation: CVE-2009-1897, dissected in LWN's "Fun with NULL pointers." The source contained the safety check; the binary did not. The compiler did nothing wrong.

The kernel's response shows how seriously to take this. The 6.12 Makefile disables the UB-assumption optimizations the kernel cannot live with. Verbatim:

make
KBUILD_CFLAGS	+= -fno-strict-overflow
KBUILD_CFLAGS	+= -fno-delete-null-pointer-checks
KBUILD_CFLAGS += -fno-strict-aliasing

The first makes signed overflow predictable instead of assumed-away (GCC's related -fwrapv documents the alternative: "assume that signed arithmetic overflow ... wraps around using twos-complement representation"). The second forbids exactly the CVE-2009-1897 deletion. The posture to internalize: in C, certain errors do not produce wrong answers. They void the warranty on the whole program, at the compiler's discretion.

Undefined behavior is the idea on this page that experienced C programmers still get wrong, which is exactly why the kernel's answer was to switch the assumptions off rather than to promise to be careful.

What kernel C does not have§

MissingWhyReplacement
The C standard librarylibc is userspace and makes syscalls into the kernelprintk for printf, kmalloc/kfree for malloc/free; the build passes -nostdinc so standard headers cannot leak in
Floating point (generally)6.12 kernel-hacking guide: "The FPU context is not saved; even in user context the FPU state probably won't correspond with the current process"Integer and fixed-point math; rare, explicitly bracketed FPU regions
int/long for layout-sensitive dataSizes vary by architectureFixed-size u8/u16/u32/u64 (and s8...s64), typedef'd in include/asm-generic/int-ll64.h
A big stackSame guide: about 14K on most 64-bit archs, partly shared with interruptsSmall locals; heap for anything sizable
A safety net"If you corrupt memory ... the whole machine will crash" (same guide)Discipline, review, and these idioms

The real thing in Linux§

Everything above is anchored in specific 6.12-tree files, readable on elixir.bootlin.com or GitHub:

WhatWhere in the 6.12 tree
-std=gnu11, -nostdinc, -fno-strict-overflow, -fno-delete-null-pointer-checks, -fno-strict-aliasingTop-level Makefile (KBUILD_CFLAGS)
container_of, container_of_const, typeof_memberinclude/linux/container_of.h
struct list_head, LIST_HEAD_INIT, list_entry, list_for_each_entryinclude/linux/list.h
The canonical ops table, struct file_operationsinclude/linux/fs.h
u8/u16/u32/u64 typedefsinclude/asm-generic/int-ll64.h
goto ladders (ch. 7), do-while macros (ch. 12) as policyDocumentation/process/coding-style.rst
Scene of the deleted NULL checkdrivers/net/tun.c (2.6.30-era bug, long fixed)

Open any driver at random and the full set is on one screen: an ops struct, list_for_each_entry walks expanding to container_of, flags tested with &, and an error ladder at the bottom of every nontrivial function.

Coconut tie-in§

Coconut OS is a Linux 6.12 fork, so the planned kernel/agent/, security/coconut/, and kernel/audit/coconut/ subsystems (04-HLD, 05-LLD) will be written in exactly this dialect: the agent registry is intrusive kernel lists walked via container_of, the LSM hook tables are ops structs, and the agent_spawn/agent_attest ABI (syscalls 472-473) is specified in fixed-width u32/u64 fields and flags words precisely because struct layout is a userspace-visible contract. This chapter is the literacy layer for reading Coconut patches.

Lab§

Runs inside a Docker Linux container on your macOS (or any) host; no QEMU, no kernel build. Three short programs, the last one optional. Type them rather than paste them, because the padding surprise in Part 1 lands harder when the numbers are yours. Start a throwaway container with a toolchain, current directory mounted:

sh
mkdir -p f4lab && cd f4lab
docker run --rm -it -v "$PWD":/work -w /work gcc:14 bash

(Any recent official gcc image works.)

Part 1: your own container_of and intrusive list§

Create mylist.h, written as portable ISO C, hence char * rather than void *:

c
#ifndef MYLIST_H
#define MYLIST_H
#include <stddef.h>

#define my_container_of(ptr, type, member) \
	((type *)((char *)(ptr) - offsetof(type, member)))

struct my_node {
	struct my_node *next, *prev;
};

static inline void my_list_init(struct my_node *h)
{
	h->next = h->prev = h;
}

static inline void my_list_add_tail(struct my_node *n, struct my_node *h)
{
	n->prev = h->prev;
	n->next = h;
	h->prev->next = n;
	h->prev = n;
}
#endif

Create missions.c:

c
#include <stdio.h>
#include "mylist.h"

struct mission {
	char name[16];
	int priority;
	struct my_node node;
};

int main(void)
{
	struct my_node head;
	struct mission a = { "boot",  1 }, b = { "mount", 2 }, c = { "spawn", 3 };

	my_list_init(&head);
	my_list_add_tail(&a.node, &head);
	my_list_add_tail(&b.node, &head);
	my_list_add_tail(&c.node, &head);

	printf("offsetof(node) = %zu\n", offsetof(struct mission, node));
	printf("&b      = %p\n", (void *)&b);
	printf("&b.node = %p\n", (void *)&b.node);

	for (struct my_node *p = head.next; p != &head; p = p->next) {
		struct mission *m = my_container_of(p, struct mission, node);
		printf("%s (prio %d)\n", m->name, m->priority);
	}
	return 0;
}

Predict before running: (1) what number does offsetof(node) print? Sum the fields: char name[16] is 16, int is 4. (2) What is &b.node - &b? (3) In what order do missions print?

sh
gcc -Wall -Wextra -o missions missions.c && ./missions

Expected:

offsetof(node) = 24
&b      = 0x7ffd... (some address)
&b.node = 0x7ffd... (that address + 24)
boot (prio 1)
mount (prio 2)
spawn (prio 3)

If you predicted 20, you just met padding in person: 16 + 4 = 20, but the pointers in node demand 8-byte alignment, so the compiler inserted 4 dead bytes. This is the trap offsetof defuses, and it is why my_container_of worked even though your size arithmetic was wrong.

Now diff your macro against the kernel's:

sh
curl -sO https://raw.githubusercontent.com/torvalds/linux/v6.12/include/linux/container_of.h
cat container_of.h

Three differences to find: the kernel wraps the body in ({ ... }) to declare __mptr; it subtracts on a void * (GNU byte arithmetic, where you used ISO char *); and it adds a static_assert so a mismatched pointer type is a compile error. Try it: change one loop line to my_container_of(p, struct my_node, next). Yours compiles and prints nonsense. That missing tripwire is the entire value of the kernel's extra lines.

Part 2: an ops-table plugin system in about 40 lines§

Create plug.c:

c
#include <stdio.h>
#include <string.h>
#include <ctype.h>

struct codec_ops {
	const char *name;
	void (*encode)(const char *in, char *out);
	void (*decode)(const char *in, char *out);
};

static void shout_enc(const char *in, char *out)
{
	while (*in) *out++ = (char)toupper((unsigned char)*in++);
	*out = '\0';
}
static void shout_dec(const char *in, char *out)
{
	while (*in) *out++ = (char)tolower((unsigned char)*in++);
	*out = '\0';
}
static void rev_xf(const char *in, char *out)
{
	size_t n = strlen(in);
	for (size_t i = 0; i < n; i++) out[i] = in[n - 1 - i];
	out[n] = '\0';
}

static const struct codec_ops shout = { "shout", shout_enc, shout_dec };
static const struct codec_ops rev   = { "rev",   rev_xf,    rev_xf   };

static const struct codec_ops *codecs[] = { &shout, &rev };

int main(void)
{
	char enc[64], dec[64];

	for (size_t i = 0; i < sizeof(codecs) / sizeof(codecs[0]); i++) {
		const struct codec_ops *c = codecs[i];
		c->encode("Coconut", enc);
		c->decode(enc, dec);
		printf("%-5s : %s -> %s -> %s\n", c->name, "Coconut", enc, dec);
	}
	return 0;
}

Predict: what does each line print? rev installs the same function in both slots, so why does that round-trip?

sh
gcc -Wall -Wextra -o plug plug.c && ./plug

Expected:

shout : Coconut -> COCONUT -> coconut
rev   : Coconut -> tunocoC -> Coconut

You have written a file_operations in miniature: an interface as a struct of function pointers, two "drivers," a registry array, dispatch through c->encode(...). (Did shout surprise you? Encode-then-decode is not the identity, and nothing in an ops table promises round-trips.)

Part 3 (optional): watch undefined behavior move§

Create ub.c:

c
#include <stdio.h>
static int will_wrap(int x) { return x + 1 < x; }
int main(void)
{
	printf("%d\n", will_wrap(2147483647));   /* INT_MAX */
	return 0;
}

Predict each output, then:

sh
gcc -O0 ub.c -o ub0 && ./ub0
gcc -O2 ub.c -o ub2 && ./ub2
gcc -O2 -fwrapv ub.c -o ubw && ./ubw

Expected: commonly 1, then 0, then 1. At -O2 the optimizer may assume signed overflow never happens, fold x + 1 < x to false, and delete your check; -fwrapv restores wrapping. Your compiler may print a different combination, and that is the lesson: the standard permits every outcome for the same source line. Re-read the kernel's -fno-strict-overflow with new eyes.

Three things you can do now that you could not before Part 1. You can read a list_for_each_entry loop and name the struct it walks without looking anything up. You can recognize an ops table as an interface on sight, in a driver you have never opened. And you can read a -fno- flag in a build file as a statement about which undefined behavior that project refuses to bet on. All three arrive together in F.6, where the kernel's own list code stops being the example and becomes the subject.

Bridge notes§

With compiler and microprocessor coursework, skim the pointer, struct, and bit sections. That is notation you own. Four things are genuinely kernel-specific. First, intrusive containers invert the data structure you were taught: the node lives inside the payload, container_of recovers the payload, list_entry is its alias. Internalize the 6.12 macro including the static_assert. Second, do { } while (0) and the goto ladder are codified policy (coding-style chapters 12 and 7), not folklore; reviewers hold you to them. Third, the kernel's UB posture is unusual: it disables the assumption machinery wholesale (-fno-strict-overflow, -fno-delete-null-pointer-checks, -fno-strict-aliasing). CVE-2009-1897 is the case study, better than your compilers-course examples because the optimizer was correct. Fourth, the dialect is gnu11, not C11: statement expressions, typeof, and void * arithmetic are load-bearing, so ISO instincts about "not legal C" will misfire in the tree.

Sources§