⚙️ Compiler & Toolchain Mastery
The GNU dialect: __attribute__ and friends
▶ Open the interactive lesson — free, no signupLinux — the system inside every Android phone and most of the internet's servers — isn't written in quite the C you've been learning. It's written in GCC's souped-up version of the language, and since that's what gcc compiles by default, you've been using it without knowing. Learn its extra powers and you can read real operating-system source, run code before main() even starts, and pack a struct with no wasted bytes between its fields.
Compile with -std=gnu17 (GCC's default!) and you're not writing standard C — you're writing GNU C, a dialect stacked with decades of extensions: extra powers the official standard never adopted. The Linux kernel is written in it. Some extensions were so good they became standard C; others remain gloriously nonportable (use them and your code only builds with GCC or Clang). Let's tour the greatest hits.
__attribute__((...)): annotations with teeth
Attributes bolt extra semantics onto declarations. The layout-changing ones first:
#include <stdio.h>
struct normal { char tag; int value; }; /* padded */
struct __attribute__((packed)) tight { char tag; int value; };/* crammed */
struct vec { float x, y, z, w; } __attribute__((aligned(16)));
int main(void) {
printf("normal : %zu bytes\n", sizeof(struct normal));
printf("packed : %zu bytes\n", sizeof(struct tight));
printf("aligned: %zu, _Alignof = %zu\n",
sizeof(struct vec), _Alignof(struct vec));
return 0;
}$ gcc -std=gnu17 layout.c -o layout && ./layout normal : 8 bytes packed : 5 bytes aligned: 16, _Alignof = 16
packed deletes the padding you learned about in the structs lesson — 5 bytes instead of 8. The price: value is now misaligned, so access is slower on x86 and can outright fault on some ARM cores. Use it for wire formats and file headers, not general data. aligned(16) goes the other way — over-aligning for SIMD or cache-line purposes.
Then the behavioral ones — these run code or move warnings around:
#include <stdio.h>
__attribute__((constructor))
static void before_main(void) {
puts("constructor: I run before main!");
}
__attribute__((deprecated("use greet_v2() instead")))
static void greet(void) { puts("hello (old api)"); }
/* teach -Wall to type-check OUR format function */
__attribute__((format(printf, 1, 2)))
static void logf_(const char *fmt, ...);
int main(void) {
greet(); /* warning arrives at compile time */
return 0;
}$ gcc -std=gnu17 -Wall attribs.c -o attribs
attribs.c: In function 'main':
attribs.c:16:5: warning: 'greet' is deprecated: use greet_v2() instead [-Wdeprecated-declarations]
16 | greet();
| ^~~~~
$ ./attribs
constructor: I run before main!
hello (old api)constructor/destructor— run beforemain/ afterexit. Libraries use these for self-registration.deprecated("msg")— every caller gets a compile-time warning with your message.unused— suppresses the unused-variable warning on purpose-built spares.format(printf, 1, 2)— teaches GCC that your function takes printf-style formats, so-Walltype-checks its arguments. Any logging function you write deserves this.cleanup(fn)— callsfn(&var)automatically when the variable leaves scope. Poor man's destructors: systemd builds all its resource management on this.
🧠 Checkpoint: What is the cost of __attribute__((packed))?
- The struct becomes read-only
- Members may be misaligned — slower access, and faults on some CPUs
- It only works on global structs
- sizeof stops working
Show answer
Members may be misaligned — slower access, and faults on some CPUs — Packing removes padding, so multi-byte members land on odd addresses. x86 tolerates that (slower); some ARM/embedded cores trap. Reserve packed for matching external byte layouts like network packets and file headers.
typeof and statement expressions: macros grow up
Two extensions that team up to make macros hygienic:
#include <stdio.h>
/* typeof + statement expression = a MAX that is type-generic
AND evaluates each argument exactly once */
#define MAX(a, b) ({ \
typeof(a) _a = (a); \
typeof(b) _b = (b); \
_a > _b ? _a : _b; \
})
int main(void) {
int i = 3;
printf("%d\n", MAX(i++, 2)); /* i++ happens ONCE */
printf("i is now %d\n", i);
printf("%.2f\n", MAX(2.5, 1.0/3)); /* works for doubles */
return 0;
}typeof(x) gives you the type of an expression — so the macro works for int, double, pointers, anything. The ({ ... }) statement expression lets a block produce a value (its last expression), giving the macro local variables so a and b are evaluated exactly once — MAX(x++, y) is finally safe. This duo was so useful that C23 standardized typeof; statement expressions remain GNU-only.
🧠 Checkpoint: Which GNU extension was adopted into standard C23?
- statement expressions
({ ... }) - computed goto
&&label typeof- nested functions
Show answer
typeof — C23 standardized typeof (and typeof_unqual). Statement expressions, computed goto, and nested functions remain compiler extensions — the first two widely supported by GCC and Clang, the last GCC-only.
Computed goto: &&label
GNU C lets you take the address of a label with &&label, store it in a void *, and jump to it with goto *ptr;. That sounds unhinged until you write an interpreter — it's the classic "threaded dispatch" technique used by real VMs (CPython used a giant switch until it adopted computed goto for a measurable speedup):
#include <stdio.h>
int run(const int *prog) {
static void *ops[] = { &&op_halt, &&op_inc, &&op_dbl };
int acc = 0, pc = 0;
goto *ops[prog[pc]]; /* dispatch! */
op_inc: acc += 1; goto *ops[prog[++pc]];
op_dbl: acc *= 2; goto *ops[prog[++pc]];
op_halt: return acc;
}
int main(void) {
int prog[] = {1, 1, 2, 2, 1, 0}; /* inc inc dbl dbl inc halt */
printf("result: %d\n", run(prog));
return 0;
}$ gcc -std=gnu17 vm.c -o vm && ./vm result: 9 # ((0+1+1) * 2 * 2) + 1 = 9 — each opcode jumps STRAIGHT to the next, # no central switch, no bounds re-check: that's threaded dispatch.
🤔 What is the type of &&op_halt, and why can a plain switch not be compiled this efficiently?
Think first
&&label has type void *. A switch funnels every iteration through one central jump, so the CPU’s branch predictor sees a single chaotic indirect branch. With computed goto, each opcode’s handler ends in its own jump, giving the predictor per-opcode history — measurably faster in interpreter loops (this is why CPython adopted it).
Builtins: talking to the optimizer
#include <stdio.h>
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)
int main(void) {
unsigned v = 0x00FF00F0u;
printf("popcount(v) = %d\n", __builtin_popcount(v));
printf("clz(v) = %d\n", __builtin_clz(v)); /* leading zeros */
printf("ctz(v) = %d\n", __builtin_ctz(v)); /* trailing zeros */
int fd = 3; /* imagine: result of open() */
if (unlikely(fd < 0)) {
fprintf(stderr, "open failed\n");
return 1;
}
puts("hot path laid out fall-through-first");
return 0;
}$ gcc -std=gnu17 -O2 builtins.c -o builtins && ./builtins popcount(v) = 12 clz(v) = 8 ctz(v) = 4 hot path laid out fall-through-first # with -O2 -march=native, popcount compiles to ONE popcnt instruction.
__builtin_expect tells the compiler which branch is the common case so it lays out the hot path fall-through-first — you've seen the kernel's likely()/unlikely() macros; this is all they are. The bit-counting builtins compile to single instructions (popcnt, lzcnt, tzcnt) where the CPU has them. C23 finally standardized this family as <stdbit.h> (stdc_count_ones and friends).
Odds and ends worth recognizing
/* flexible array member — STANDARD since C99 */
struct message {
size_t len;
char data[]; /* sized at malloc time */
};
/* struct message *m = malloc(sizeof *m + len); */
/* case ranges — GNU only (mind the spaces around ...) */
const char *classify(char c) {
switch (c) {
case '0' ... '9': return "digit";
case 'a' ... 'z': case 'A' ... 'Z': return "letter";
default: return "other";
}
}
/* nested function — GCC only; pointer to it needs a stack
trampoline (historically: executable stack). Avoid. */
int sum3(int a, int b, int c) {
int add(int x, int y) { return x + y; } /* sees a,b,c too */
return add(add(a, b), c);
}- Flexible array members — a struct ending in
char data[];sized atmalloctime. This one is fully standard since C99 (GCC's older zero-lengthdata[0]spelling is the extension). The single best way to allocate a header plus payload in one block. - Case ranges —
case '0' ... '9':in a switch. Pure GNU sugar; note the spaces around...are required. - Nested functions — a function inside a function, able to see the enclosing locals. GCC-only and controversial: taking a pointer to one forces GCC to generate a trampoline on the stack, historically requiring an executable stack — a security hole. Clang refuses to implement them. Know they exist; don't use them.
🧠 Checkpoint: Why do nested functions have a bad reputation even among GNU-extension fans?
- They cannot access enclosing variables
- Taking their address needs a stack trampoline, historically forcing an executable stack
- They only work at -O0
- They are slower than macros
Show answer
Taking their address needs a stack trampoline, historically forcing an executable stack — A pointer to a nested function must carry the enclosing frame, so GCC writes a tiny code stub (“trampoline”) onto the stack — which then must be executable, weakening a key exploit mitigation. Clang never implemented them.
Portability discipline: every use of a GNU extension is a promise that your code only builds with GCC/Clang. Guard the optional ones: #ifdef __GNUC__ … #else provide a plain-C fallback #endif. And know which dialect you're compiling: -std=c17 disables some extensions and defines __STRICT_ANSI__, while -std=gnu17 keeps them all on.
Practice
▶ This spot has an interactive editor widget — open the interactive lesson to play with it.
One extension we skipped deserves an entire lesson: embedding raw assembly in your C — let's go there now.
▶ Practice this lesson interactively (with live gcc)