C Mastery / Inline Assembly, Intrinsics, and Barriers
Part 7 — CPU Architecture and Assembly

Inline Assembly, Intrinsics, and Barriers

This chapter covers three ways to reach below C to the machine: inline assembly, compiler intrinsics, and compiler/memory barriers. These are non-portable, but essential for systems, embedded, and performance work.

Why This Matters

C does not expose every CPU instruction or every hardware operation. When you need a specific instruction (SIMD, atomics, special registers), a precise memory ordering, or a hand-tuned sequence, you use these tools. Using them correctly requires knowing exactly what they guarantee.

Prerequisites

Core Concept

Inline assembly

GCC/Clang support asm (or __asm__) to embed assembly inside C:

static inline unsigned long read_sp(void)
{
    unsigned long sp;
    __asm__ volatile("mov %%rsp, %0" : "=r"(sp));
    return sp;
}

The syntax has three parts: the instruction template, output operands, and input operands, with clobbers for registers it modifies. volatile prevents the compiler from optimizing the asm away.

Intrinsics

Intrinsics are compiler-provided functions that map to specific instructions or operations. Examples:

#include <x86intrin.h>
__m256 a = _mm256_set1_ps(2.0f);

Intrinsics are portable *within* a compiler/architecture, unlike raw asm.

Barriers

operations across it, but emits no hardware instruction: asm volatile("" ::: "memory").

that orders memory operations at the CPU level.

#include <stdatomic.h>
atomic_thread_fence(memory_order_seq_cst);   /* C11 memory fence */

Examples

Reading a control register (x86)

static inline unsigned long read_cr3(void)
{
    unsigned long v;
    __asm__ volatile("mov %%cr3, %0" : "=r"(v));
    return v;
}

Count leading zeros (intrinsic)

#include <stdint.h>
int clz32(uint32_t x)
{
    return __builtin_clz(x);   /* GCC/Clang */
}

Compiler barrier for MMIO

#define COMPILER_BARRIER() __asm__ volatile("" ::: "memory")

void write_reg(volatile uint32_t *reg, uint32_t v)
{
    COMPILER_BARRIER();
    *reg = v;
    COMPILER_BARRIER();
}

This orders the volatile MMIO access relative to surrounding code in the generated output.

How It Works

Inline asm is passed through to the assembler with operand substitution. Intrinsics are lowered by the compiler into the target instruction or a small code sequence. Barriers constrain the compiler's reordering (compiler barrier) or the CPU's memory reordering (fence instruction).

Variations

Extended asm constraints

GCC extended asm uses constraint letters (r for register, m for memory, i for immediate) and clobber lists. This is powerful but non-portable and error-prone.

C11 atomics vs. compiler intrinsics

Prefer C11 _Atomic and atomic_thread_fence over __sync/__atomic intrinsics for portability, reserving intrinsics for operations the standard does not expose.

Common Mistakes

reordering).

Undefined Behavior

behavior or corrupt state. The compiler cannot verify the asm.

Portability

only behind feature detection.

Under the Hood

Inline asm is emitted verbatim into the assembly. Intrinsics lower to instructions (or call a compiler helper). Barriers become either nothing (compiler barrier) or a fence instruction (memory barrier).

Practical Usage

Exercises

1. Write a read_sp function using inline asm and verify it prints a stack address. 2. Use __builtin_clz to implement a fast log2. 3. Write a compiler barrier and inspect its effect on generated code.

Deep Challenge

Implement a small spinlock using atomic_flag (C11) and explain why a plain volatile flag is insufficient. Then add the appropriate memory order and explain what hardware fence (if any) is emitted on x86-64 vs. ARM64.

References

Verification

COMPILER-SPECIFIC

verified.`

Progress

Concept checkboxes

Mastery levels

ConceptCurrent level (0–8)Target level
c.cpu.inline-asm06
c.cpu.intrinsics06
c.cpu.barrier06