The C Path — learn C, visually

⚙️ Compiler & Toolchain Mastery

The GNU dialect: __attribute__ and friends

⏱ 16 min · free interactive lesson · quizzes, visualizations & a real compiler

▶ Open the interactive lesson — free, no signup
Why you're learning this

Linux — 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:

layout.c
#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;
}
terminal
$ 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:

attribs.c
#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;
}
terminal
$ 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)

🧠 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:

maxmacro.c
#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):

vm.c — a 3-opcode virtual machine
#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;
}
terminal
$ 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

builtins.c
#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;
}
terminal
$ 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

grabbag.c
/* 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);
}

🧠 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)