Inline Assembly in C
GCC extended inline assembly is the precise contract language between C code and the CPU, and reading it fluently unlocks the kernel's most performance-critical and architecture-specific internals.
Overview
Inline assembly is the art of embedding raw machine instructions directly inside C source code. At first glance this seems like a hack — why would you reach past the compiler? The answer is that some things simply cannot be expressed in standard C: atomically swapping a register with memory, reading a CPU-specific control register, issuing a fence instruction that the C memory model doesn't expose, or squeezing every last nanosecond out of a hot path. The Linux kernel does all of these thousands of times. Understanding inline assembly is the key that unlocks the kernel's most architecture-specific, performance-critical, and security-sensitive code.
GCC's extended inline assembly syntax is the language the kernel uses almost exclusively. It is notoriously terse — a string of constraints and operands that looks like line noise until you understand the grammar. But the grammar is precise and elegant. Once you see it, you will be able to read arch/x86/include/asm/atomic.h, arch/arm64/include/asm/barrier.h, and arch/riscv/include/asm/csr.h as fluently as ordinary C.
This module builds the mental model from first principles. We start with the simplest possible asm statement, explain exactly what the compiler does with it, add output operands, input operands, clobbers, and memory constraints — each time grounding the theory in a real, compilable, testable example. We finish by reading actual kernel inline asm, demystifying the patterns you will encounter in the wild.
The goal is not memorization; it is comprehension deep enough that you can write correct inline assembly for a new architecture you have never seen before, and spot subtle bugs when reviewing someone else's.
The Linux kernel is a C program with islands of assembly. Those islands are not decorative — they are load-bearing. Every atomic operation, every memory barrier, every system-call entry point, every CPU feature detection, every context switch touches inline assembly or pure assembly files. When you contribute a driver, a scheduler improvement, or a security fix, you will encounter these islands. If they look opaque, you will treat them as black boxes and accidentally break invariants. If you can read them fluently, you understand the real contract between the kernel and the hardware.
Beyond the kernel itself, inline assembly teaches you how the compiler-to-hardware abstraction actually works. You learn what the compiler cannot prove on its own (aliasing across a call, register state after a context switch, the existence of hardware side-effects), and why the kernel goes to such lengths to tell the compiler the truth about what a piece of asm does. This understanding is directly transferable to writing safer, faster systems code in any language, and to code review: the ability to look at a constraint string and immediately see whether it is wrong is a skill that has caught real security vulnerabilities.
GCC Extended Asm: Compiler Integration Flow
When GCC encounters an extended asm statement, it is not a passive text substitution. The register allocator actively assigns registers to operands, the clobber list updates the liveness map, and volatile/memory annotations change how the code generator schedules the surrounding code.
The concepts
01Why Inline Assembly Exists: The Abstraction Gap
C is a portable abstraction over a generic machine. That abstraction is intentional — it lets the same source file compile on x86, ARM, RISC-V, and PowerPC. But hardware is not generic. CPUs expose capabilities that have no C equivalent: rotate instructions, carry-flag arithmetic, hardware prefetch hints, control register access, atomic compare-and-swap with a specific memory ordering, or a pause instruction that reduces power in a spin loop.
The compiler is also not omniscient. It cannot know that a piece of memory is memory-mapped I/O and must not be cached or reordered. It cannot know that two apparently unrelated writes must appear in a specific order to a device that is watching the bus.
Inline assembly is the escape hatch. It says: 'I know something about the machine that you cannot infer from the C source. Here is the raw instruction. Please integrate it into the surrounding code correctly.' The challenge is that 'correctly' is a rich contract: the compiler needs to know which C variables are inputs, which are outputs, which registers the asm destroys, and whether the asm has side effects the compiler should not reorder.
GCC's extended asm syntax is a formal language for expressing exactly that contract. It is not optional boilerplate — every part of it is load-bearing.
02Basic Inline Assembly: The `asm` Statement
The simplest form of inline assembly in GCC is the basic asm statement:
asm("nop");
This inserts a single nop instruction. The compiler passes the string literally to the assembler. Multiple instructions are separated by \n\t (newline + tab) to produce correct assembly syntax:
asm("nop\n\t"
"nop\n\t"
"nop");
The \n ends the current line in the assembly output; the \t indents the next instruction so it looks like normal assembly. Without them, the assembler sees nopnopnop on one line and errors out.
Basic asm has a critical limitation: it cannot refer to C variables, and the compiler does not know what it modifies. Because of this, GCC treats basic asm (outside of a function) as if it has no inputs and no outputs, but inside a function, basic asm can interfere with register allocation in unpredictable ways. The kernel style guide and most serious inline-assembly use almost exclusively the extended form.
The keyword is asm in C99/C11 and __asm__ as an alias that works even when compiling with -std=c11 (which would otherwise treat asm as an error). The kernel always uses __asm__ or asm depending on context, often with the __volatile__ qualifier (explained later).
#include <stdio.h>
int main(void) {
/* Basic asm: just emit a nop */
asm("nop");
printf("Still here after nop\n");
return 0;
}
// Compile: gcc -O2 -o basic_asm basic_asm.c
// Inspect: objdump -d basic_asm | grep -A5 '<main>'
03Extended Asm: The Full Syntax
Extended asm gives the compiler a complete contract. The syntax is:
asm [volatile] ( AssemblerTemplate
: OutputOperands
: InputOperands
: Clobbers
: GotoLabels );
Each section is separated by a colon. Sections after the last needed one can be omitted. Let's break down each part:
AssemblerTemplate: A C string (or concatenated strings) containing the assembly instructions. C variable operands are referenced by position (%0, %1, ...) or by name (%[name]).
OutputOperands: Comma-separated list of "constraint" (c_expression) pairs. These are C lvalues that the asm writes to. The constraint tells the compiler what kind of storage is acceptable (a register, a memory location, etc.).
InputOperands: Same format, but these are C rvalues that the asm reads from.
Clobbers: A comma-separated list of strings naming registers (e.g. "eax", "r0") or the special tokens "cc" (condition codes/flags register) and "memory" (the asm may read or write arbitrary memory). This tells the compiler which registers are destroyed by the asm so it does not keep live values there.
GotoLabels: Used only with asm goto (covered separately); names C labels the asm may jump to.
The most important insight: the compiler is not a passive bystander. It reads your constraints and allocates real registers to your operands. It may put your variable in %eax for one asm statement and %ecx for another, depending on what is live. The %0 in your template is replaced with whatever register the compiler chose. This is what makes extended asm powerful and dangerous: you must describe the contract precisely, or the compiler will generate subtly wrong code.
/* Add two integers using inline asm (x86-64) */
#include <stdio.h>
int add(int a, int b) {
int result;
asm("addl %2, %0"
: "=r" (result) /* output: result in any register */
: "0" (a), /* input: same reg as output 0 (i.e. result) */
"r" (b) /* input: b in any register */
: /* no clobbers */ );
return result;
}
int main(void) {
printf("%d\n", add(3, 4)); /* prints 7 */
return 0;
}
// gcc -O2 -o add add.c && ./add
// gcc -O2 -S add.c && cat add.s # see generated asm
04Output and Input Constraints: The Heart of Extended Asm
Constraints are single-character (or multi-character) codes that specify where operands may live. The most common:
| Constraint | Meaning |
|---|---|
r | Any general-purpose register |
m | Any memory operand |
i | An immediate integer constant |
n | An immediate integer known at compile time |
g | Any register, memory, or immediate |
a | The %eax/%rax register specifically |
b | %ebx/%rbx |
c | %ecx/%rcx |
d | %edx/%rdx |
D | %edi/%rdi |
S | %esi/%rsi |
q | Any of a, b, c, d (x86) |
0..9 | Same register as operand N (matching constraint) |
Output constraints are prefixed with = (write-only) or + (read-write). Input constraints have no prefix.
The matching constraint ("0", "1", etc.) is critical for instructions like add that use the same register as both input and output. In the example above, "0" (a) means 'put a in the same register that was chosen for output operand 0'. This tells the compiler that a is consumed and replaced by result.
Named operands improve readability enormously and are used extensively in the kernel:
asm("addl %[b], %[a]"
: [a] "+r" (a)
: [b] "r" (b));
The m constraint is essential for memory-mapped I/O and for passing structs. When you write "m" (x), the compiler emits the correct memory address expression for x, including any base-register + offset encoding the architecture uses.
Architecture-specific constraints: ARM has l (low registers r0-r7), h (high registers), w (VFP/NEON registers). RISC-V has f (floating-point). Always consult the GCC manual section for your target architecture.
/* Demonstrate output, input, matching, and named constraints */
#include <stdio.h>
#include <stdint.h>
/* x86-64: read the timestamp counter */
static inline uint64_t rdtsc(void) {
uint32_t lo, hi;
asm volatile("rdtsc"
: "=a" (lo), /* output: low 32 bits in eax */
"=d" (hi) /* output: high 32 bits in edx */
: /* no inputs */
: /* no clobbers beyond =a, =d */ );
return ((uint64_t)hi << 32) | lo;
}
/* x86-64: rotate left (no C operator for this) */
static inline uint32_t rotl32(uint32_t val, int n) {
asm("roll %b[n], %[val]"
: [val] "+r" (val) /* read-write operand */
: [n] "c" (n) /* n must be in %cl for variable rotate */
: "cc"); /* rotl sets carry/overflow */
return val;
}
int main(void) {
printf("TSC: %llu\n", (unsigned long long)rdtsc());
printf("rotl32(1, 3) = %u\n", rotl32(1, 3)); /* expect 8 */
return 0;
}
05Clobbers: Telling the Compiler What Gets Destroyed
The compiler's register allocator works by tracking which registers hold live values. If your inline asm destroys a register that the compiler is using to store a variable, and you do not declare that register as a clobber, the compiler will happily continue using that register as if nothing happened — and your program will produce wrong answers silently.
Register clobbers: List any register your asm writes to that is not already declared as an output operand. Example: x86's imul instruction that writes a 64-bit product into edx:eax — if you only capture eax, you must declare "edx" as a clobber.
The "cc" clobber: Most arithmetic and comparison instructions on x86, ARM, etc. set the condition codes (flags register: zero, carry, overflow, sign). If your asm does this, declare "cc". On some architectures (like x86) GCC already knows that "cc" is always clobbered by arithmetic, but it is good practice to be explicit. On ARM, "cc" is more important.
The "memory" clobber: This is the most powerful and most misunderstood clobber. It tells the compiler: 'this asm may read or write any memory location, not just the explicitly named operands.' The compiler responds by flushing all cached variable values from registers to memory before the asm, and re-loading them after. This is a memory barrier from the compiler's perspective (a compiler barrier, not a hardware memory barrier — those require the appropriate fence instruction too).
The kernel uses "memory" clobber extensively in barrier macros:
/* From arch/x86/include/asm/barrier.h */
#define barrier() asm volatile("" ::: "memory")
This produces no instructions at runtime but prevents the compiler from reordering loads and stores across the barrier. It is a compiler fence, not a CPU fence. For a CPU fence on x86, you need mfence or lock; addl $0, 0(%%rsp).
/* Demonstrating why clobbers matter */
#include <stdio.h>
int broken_mul(int a, int b) {
int lo;
/* BUG: imull writes high bits to edx but we don't clobber it.
If the compiler stored something in edx, it gets silently trashed.
This is undefined behavior in the constraint spec. */
asm("imull %2"
: "=a" (lo)
: "a" (a), "r" (b)
/* MISSING: : "edx" */
);
return lo;
}
int correct_mul(int a, int b) {
int lo;
asm("imull %2"
: "=a" (lo)
: "a" (a), "r" (b)
: "edx"); /* correct: declare that edx is destroyed */
return lo;
}
/* Compiler barrier: zero instructions, full compiler fence */
static inline void compiler_barrier(void) {
asm volatile("" ::: "memory");
}
int main(void) {
printf("%d\n", correct_mul(6, 7)); /* 42 */
volatile int x = 1;
compiler_barrier();
volatile int y = x + 1; /* compiler cannot hoist this above barrier */
printf("%d\n", y);
return 0;
}
06The `volatile` Qualifier: Preventing Asm Elimination
GCC's optimizer is aggressive. If it decides an asm statement's outputs are never used, it may delete the entire asm statement. This is correct for a pure computational asm like addl, but catastrophically wrong for an asm that has side effects: writing to a port, reading a hardware register, issuing a fence, incrementing a counter in a special register.
asm volatile(...) tells the compiler: 'this statement has side effects you cannot see. Do not delete it, do not move it relative to other volatile operations.'
The precise semantics of asm volatile:
- The asm will not be deleted even if its output is unused.
- The asm will not be moved relative to other volatile asm statements or volatile accesses. It can still be moved relative to non-volatile code if the compiler thinks that is safe (unless you also use the
"memory"clobber). - The asm will not be CSE'd (common subexpression eliminated) — each occurrence is emitted separately.
When to use volatile:
- Any asm that reads hardware state (
rdtsc,rdmsr, reading a CSR) - Any asm that writes hardware state (writing MSRs, flushing TLBs)
- Memory barriers (
mfence,dmb,fence) - Serialization instructions (
cpuid,lfence) - Anything that should not be optimized away
When NOT to use volatile: Pure computational asm (implementing a missing instruction like bswap or clz) — let the optimizer CSE and eliminate it freely. Incorrect volatile use is a pessimization.
The kernel default is to use __asm__ __volatile__ for nearly everything, erring on the side of safety. When you see it in the kernel, it is not superstition — it is a deliberate statement that this instruction has architectural significance.
/* Contrast: with and without volatile */
#include <stdint.h>
/* WITHOUT volatile: compiler may CSE or delete if result unused */
static inline uint32_t bswap_pure(uint32_t x) {
asm("bswapl %0" : "+r" (x)); /* pure: no volatile needed */
return x;
}
/* WITH volatile: reads hardware, must not be deleted or reordered */
static inline uint64_t read_tsc(void) {
uint32_t lo, hi;
__asm__ __volatile__("rdtsc" : "=a"(lo), "=d"(hi));
return ((uint64_t)hi << 32) | lo;
}
/* Kernel-style compiler barrier (from include/linux/compiler.h) */
#define barrier() __asm__ __volatile__("" ::: "memory")
/* Kernel-style x86 CPU serialization fence */
static inline void serialize(void) {
/* lfence: load fence, also serializes instruction fetch */
__asm__ __volatile__("lfence" ::: "memory");
}
void example(void) {
uint64_t t1 = read_tsc(); /* will NOT be deleted */
barrier(); /* compiler fence */
uint64_t t2 = read_tsc(); /* separate call guaranteed */
(void)(t2 - t1);
}
07Operand Modifiers and Size Suffixes
Inside the assembly template string, %0, %1, etc. are replaced by the register or memory expression the compiler chose. But sometimes you need to control exactly which part of that register is used — for example, addl needs a 32-bit register name like %eax, but addb needs the 8-bit name %al.
GCC provides operand modifiers using the format %Xn where X is a single-character modifier and n is the operand number:
| Modifier | x86-64 effect | Example with %rax chosen |
|---|---|---|
| (none) | Default size for the constraint | %rax (64-bit) |
b | Low 8-bit name | %al |
h | High 8-bit name | %ah |
w | 16-bit name | %ax |
k | 32-bit name | %eax |
q | 64-bit name | %rax |
This matters when you write a single asm template that needs to operate on a specific width. The kernel uses %b0, %w0, %k0, %q0 patterns constantly.
For ARM/AArch64, the modifier for the 32-bit sub-register of x0 is %w0. For RISC-V, registers are always the full width and there are no sub-register modifiers.
Another important modifier: %c0 emits the operand as a bare constant (no $ prefix on x86), useful when you need an immediate in a context where the $ would be wrong — for example, inside a .macro that expects a plain number.
The %P0 modifier (x86) emits the raw offset for use with position-independent code, and %a0 emits the operand as an address (adds parentheses for indirect addressing). These are used in the kernel's CALL_NOSPEC and retpoline machinery.
/* Operand size modifiers on x86-64 */
#include <stdint.h>
#include <stdio.h>
static inline uint8_t add8(uint8_t a, uint8_t b) {
uint32_t result;
/* We pass 32-bit values to get a full register,
but use %b modifier to emit 8-bit register names */
asm("addb %b[b], %b[a]"
: [a] "=q" (result) /* =q: a,b,c,d regs (have 8-bit names) */
: "0" (a),
[b] "q" (b)
: "cc");
return (uint8_t)result;
}
/* AArch64 example: use %w0 for 32-bit sub-register */
#if defined(__aarch64__)
static inline uint32_t rev32(uint32_t x) {
uint32_t out;
asm("rev %w[out], %w[in]"
: [out] "=r" (out)
: [in] "r" (x));
return out;
}
#endif
int main(void) {
printf("add8(200, 100) = %u (wraps to %u)\n",
(unsigned)add8(200, 100), (unsigned)(uint8_t)(200+100));
return 0;
}
08The `asm goto` Extension: Branching Out of Inline Asm
asm goto (added in GCC 4.5, C extension) allows inline assembly to jump to C labels. This is used in the kernel for static keys (jump labels), which are an optimization technique that eliminates branch mispredictions in hot paths.
The syntax adds a fifth section to the asm template — the GotoLabels section:
asm goto ("testl %0, %0; jnz %l[positive]"
: /* no outputs */
: "r" (x)
: "cc"
: positive); /* C label name */
If the condition is true, execution jumps to the positive: label in the surrounding C function. If not, execution falls through to the next C statement.
Static keys in the kernel: The kernel uses asm goto to implement static_branch_likely() and static_branch_unlikely(). A static key is a global boolean. In the hot path, the asm is a single unconditional jump (jmp) or a 5-byte NOP. At runtime, if the key changes state, the kernel uses text_poke() to rewrite the instruction in-place — changing the NOP to a JMP or vice versa. This means a disabled branch has zero branch prediction cost: it is just a NOP or an unconditional jump, never a conditional branch. The %l[label] operand modifier formats the label as a branch target address.
This technique is used for things like tracepoints (disabled by default = NOP, enabled = JMP to tracing code), perf events, and KASAN checks. Reading include/linux/jump_label.h and arch/x86/include/asm/jump_label.h becomes approachable after understanding this.
/* asm goto example: jump to C label from asm */
#include <stdio.h>
void check_positive(int x) {
asm goto ("testl %0, %0\n\t"
"jg %l[positive]" /* jg: jump if greater (signed) */
: /* no outputs */
: "r" (x)
: "cc"
: positive); /* goto label list */
/* Fall-through: x <= 0 */
printf("%d is not positive\n", x);
return;
positive:
printf("%d is positive\n", x);
}
int main(void) {
check_positive(5); /* positive */
check_positive(-3); /* not positive */
check_positive(0); /* not positive */
return 0;
}
// gcc -O2 -o asm_goto asm_goto.c && ./asm_goto
09Architecture Portability: Macros and Alternative Constraints
Real kernel code must compile on multiple architectures. The way the kernel handles this is layered:
-
Per-arch headers:
arch/x86/include/asm/,arch/arm64/include/asm/,arch/riscv/include/asm/contain architecture-specific implementations of the same conceptual operation. The generic header atinclude/asm-generic/provides fallback C implementations. -
Preprocessor macros wrapping asm: Architecture-specific asm is always wrapped in
static inlinefunctions or macros, never written inline at the call site. This way the caller uses a clean C API. -
Alternative constraint sets: GCC allows multiple alternative constraint sets separated by commas. The compiler picks the one that best fits the available operands:
asm("..." : "=r,m" (out) : "r,r" (in));This says: either put
outin a register andinin a register, OR putoutin memory andinin a register. -
The
__builtin_*escape: For some operations, GCC provides builtins (__builtin_clz,__builtin_bswap32,__builtin_expect) that compile to the right instruction on each architecture without any inline asm. Prefer builtins when they exist; they are portable and let the compiler optimize further. -
RISC-V CSR access: RISC-V control and status registers use specific asm instructions (
csrr,csrw,csrrs) that have no C equivalent. The kernel'sarch/riscv/include/asm/csr.hdefines macros likecsr_read(csr)using inline asm with the immediate CSR number substituted by%[csr]with aniconstraint (immediate).
/* Portable clz (count leading zeros) using builtin first, asm fallback */
#include <stdint.h>
static inline int clz32_portable(uint32_t x) {
if (x == 0) return 32;
return __builtin_clz(x); /* compiler picks bsr/lzcnt/clz/cntlz */
}
/* RISC-V CSR read (from arch/riscv/include/asm/csr.h pattern) */
#if defined(__riscv)
#define csr_read(csr) ({ \
unsigned long __val; \
__asm__ __volatile__("csrr %0, %1" \
: "=r" (__val) \
: "i" (csr) /* immediate */ \
: /* no clobbers */); \
__val; \
})
/* Usage: unsigned long cycle = csr_read(0xC00); // CSR_CYCLE */
#endif
/* x86-64 specific: read cr3 (page table base) - kernel only */
#if defined(__x86_64__) && defined(__KERNEL__)
static inline unsigned long read_cr3(void) {
unsigned long val;
__asm__ __volatile__("mov %%cr3, %0" : "=r" (val));
return val;
}
#endif
10Reading Kernel Inline Asm: Real Examples Decoded
With the above knowledge, let's read actual kernel inline assembly.
Example 1: atomic_add on x86 (arch/x86/include/asm/atomic.h)
static __always_inline void arch_atomic_add(int i, atomic_t *v)
{
asm volatile(LOCK_PREFIX "addl %1,%0"
: "+m" (v->counter)
: "ir" (i)
: "cc");
}
Decoding: LOCK_PREFIX expands to "lock;" on SMP builds (empty on UP). addl %1,%0 adds %1 (the integer i) to %0 (the memory location v->counter). Output: "+m" means v->counter is read-write and in memory. Input: "ir" means i may be an immediate or a register. Clobber: "cc" because addl sets flags.
Example 2: Memory barrier on AArch64 (arch/arm64/include/asm/barrier.h)
#define dmb(opt) asm volatile("dmb " #opt ::: "memory")
#define smp_mb() dmb(ish)
Decoding: dmb ish is the ARM64 'Data Memory Barrier, Inner Shareable domain' instruction. #opt stringifies the macro argument. No operands. "memory" clobber: this is a full compiler + CPU barrier. volatile ensures it is not deleted.
Example 3: copy_user_generic_string entry (x86, arch/x86/lib/copy_user_64.S) uses pure .S assembly, not inline asm. Inline asm in the kernel is almost always in headers (.h) for inlining into callers, not in .c files for standalone functions.
Example 4: native_read_msr (arch/x86/include/asm/msr.h)
static inline unsigned long long native_read_msr(unsigned int msr)
{
DECLARE_ARGS(val, low, high);
asm volatile("rdmsr"
: EAX_EDX_RET(val, low, high)
: "c" (msr));
return EAX_EDX_VAL(val, low, high);
}
The DECLARE_ARGS, EAX_EDX_RET, EAX_EDX_VAL macros handle the 32-bit vs 64-bit difference in how to combine edx:eax. "c" (msr) puts msr in %ecx (the required input for rdmsr). volatile because reading an MSR is a hardware side-effect.
/* Minimal re-implementation of atomic add for study */
#include <stdint.h>
typedef struct { volatile int counter; } atomic_t;
/* Study version of arch_atomic_add (user-space, no LOCK_PREFIX) */
static inline void atomic_add_study(int i, atomic_t *v)
{
__asm__ __volatile__("addl %1, %0"
: "+m" (v->counter) /* read-write memory */
: "ir" (i) /* immediate or register */
: "cc"); /* sets flags */
}
/* Study version of smp_mb for x86 */
static inline void smp_mb_study(void)
{
/* Full memory barrier: mfence + compiler barrier */
__asm__ __volatile__("mfence" ::: "memory");
}
/* Compiler-only barrier (no CPU instruction) */
static inline void compiler_barrier_study(void)
{
__asm__ __volatile__("" ::: "memory");
}
11Common Pitfalls and How to Debug Inline Asm
Inline asm bugs are nasty because they are often silent and intermittent (they depend on what the compiler chose for register allocation, which changes with optimization level and surrounding code).
Debugging workflow:
- Compile with
-O0first (less optimization, more predictable register assignment), then-O2. - Use
gcc -Sto see the generated assembly. Find your asm template in the output and check that the substituted registers make sense. - Use Compiler Explorer (godbolt.org) — paste your code, select GCC x86-64, set flags. You see the generated asm instantly with color-coded correspondence to source lines.
- Add
__builtin_trap()or__asm__ __volatile__("int3")around the asm for GDB breakpointing. - Use
-fno-inlineduring debugging to prevent inlining from changing register assignments.
Using Compiler Explorer for constraint verification: The most effective technique. Write your asm, compile at O2, and verify that the generated code does exactly what you intended. If the compiler emits a spurious mov before your asm, you likely have a constraint mismatch (you asked for a register but the compiler needed to shuffle values). If it emits warnings like 'asm operand has impossible constraints', your constraint is genuinely impossible on this architecture.
The info clobber: On some architectures, certain instructions implicitly read registers you did not list as inputs. Check the instruction set manual. For example, div on x86 implicitly reads edx:eax and writes eax (quotient) and edx (remainder) — you must account for all of these.
GCC's -fasm-blocks and Clang compatibility: The kernel is now being compiled with Clang/LLVM as well as GCC. Clang's inline asm is mostly compatible but has subtle differences in constraint handling. When writing kernel inline asm, test with both compilers if possible, or check arch/*/Kconfig for existing CC_HAS_* tests.
/* Template for debugging inline asm */
#include <stdio.h>
/* Annotated debug wrapper: print operand values around asm */
static inline int debug_add(int a, int b) {
int result;
printf("[before asm] a=%d, b=%d\n", a, b);
__asm__ __volatile__(
"/* DEBUG: operands will be substituted below */\n\t"
"addl %2, %0"
: "=r" (result)
: "0" (a), "r" (b)
: "cc"
);
printf("[after asm] result=%d\n", result);
return result;
}
/* Compile with: gcc -O2 -S debug_asm.c
Then look for '## DEBUG' comment in the .s file
and see what registers %0, %1, %2 resolved to.
Or: paste into godbolt.org with -O2 */
int main(void) {
int x = debug_add(10, 32);
printf("10 + 32 = %d\n", x);
return 0;
}
Key insights
- ✦The constraint string is a contract between you and the compiler's register allocator — every character is load-bearing, not decoration.
- ✦The `"memory"` clobber is a *compiler* barrier; it prevents instruction reordering in the generated code but does NOT emit a CPU memory fence instruction. You need both for a real SMP barrier.
- ✦`asm volatile` prevents deletion and CSE, but does NOT prevent reordering relative to non-volatile code unless you also clobber `"memory"`.
- ✦The `%0`, `%1` placeholders are replaced by whatever register the compiler chose — the compiler is an active participant, not a passive transcriber.
- ✦Matching constraints (`"0"`, `"1"`) are how you model read-modify-write instructions where the input and output share the same register.
- ✦Real kernel inline asm is almost always wrapped in `static inline` functions in `.h` files — never write raw inline asm at a call site.
- ✦`asm goto` with jump labels is the foundation of static keys, the kernel's zero-overhead tracepoint and feature-toggle mechanism.
- ✦When in doubt, compile with `gcc -S` or use Compiler Explorer to see exactly what assembly your inline asm produces — the generated code is the ground truth.
Common pitfalls
- ⚠Forgetting to clobber `"cc"` after instructions that set flags (add, sub, test, cmp). On x86 the compiler may not always catch this, but on ARM it is critical.
- ⚠Using basic asm inside a function body — the compiler has no idea what registers it touches and may silently corrupt live variables. Always use extended asm inside functions.
- ⚠Confusing the `"memory"` clobber (compiler barrier only) with a CPU memory fence instruction. To get a real SMP barrier you need the fence instruction AND the memory clobber.
- ⚠Writing `asm volatile` on a pure computational asm (like `bswap`) where volatile is unnecessary — this prevents CSE and loop hoisting that would make the code faster.
- ⚠Using `=r` instead of `+r` for a read-modify-write operand. With `=r` the compiler treats the operand as write-only and will not initialize it from the C variable — reading garbage.
- ⚠Not accounting for implicit register uses: `div` trashes `edx`, `imul` writes `edx:eax`, `push`/`pop` modify `rsp`. If you use these instructions, declare all affected registers.
- ⚠Assuming the constraint letter has the same meaning across architectures. `r` means 'any general register' everywhere, but `q` means 'a, b, c, or d' on x86 and something completely different on ARM.
- ⚠Forgetting that `asm goto` outputs must be declared as `=r` outputs or as `asm goto` outputs — you cannot mix data outputs and goto labels in early GCC versions; check your kernel's minimum GCC version.
Practice
- 01easy
Implement `uint64_t read_tsc(void)` using inline asm on x86-64. Then write a small benchmark that reads the TSC before and after a `memset` of 1 MB and prints the elapsed cycles. Verify by compiling with `-O2` and inspecting the output with `objdump -d` to confirm the `rdtsc` instruction appears and is not hoisted out of position.
- 02medium
Write a `uint32_t crc32_byte(uint32_t crc, uint8_t byte)` function using the x86 `crc32` instruction (SSE4.2: `crc32b %1, %0`). Handle the constraint carefully: the `crc32b` instruction writes a 32-bit result. Test it against a software CRC32 implementation on the same inputs.
- 03medium
Port the kernel's `arch_atomic_add` and `arch_atomic_inc` to a user-space test program (without the LOCK_PREFIX). Then add the LOCK_PREFIX back and use two threads hammering a shared counter to verify that the locked version gives correct results and the unlocked version does not.
- 04hard
Study `arch/x86/include/asm/barrier.h` in the Linux kernel source. Identify three different barrier macros, decode every constraint and clobber in their inline asm, and write a one-paragraph explanation of what each one guarantees and why the implementation is correct.
- 05medium
Write a user-space demonstration of `asm goto`. Implement a fast integer absolute value: if the input is negative, jump to a label that negates it; otherwise fall through. Compile with -O2, inspect the assembly, and verify there are no conditional branches — only an unconditional jump or no jump at all.
- 06hard
Implement `uint32_t bit_reverse32(uint32_t x)` WITHOUT using any existing bswap or bit-reversal instructions — use only shifts and ORs in inline asm, deliberately writing a multi-instruction asm block. Then compare the generated code size to the equivalent C implementation and to `__builtin_bitreverse32` if your GCC version has it.
Resources
The authoritative reference for every constraint, modifier, and qualifier. Read this after the module to fill gaps; use it as a reference while writing asm.
Complete listing of machine-independent and architecture-specific constraints — bookmark this page.
The single most useful tool for understanding inline asm. Paste C with inline asm, select compiler and flags, see real generated assembly instantly with source-line highlighting.
Bootlin's cross-referenced kernel source. Browse atomic.h, barrier.h, msr.h with clickable symbol definitions — invaluable for reading real kernel inline asm in context.
Classic LWN article walking through kernel inline asm patterns with explanation — written by a kernel developer for kernel developers.
GCC uses AT&T syntax by default (source before destination, % register prefix, $ immediate prefix). This article clarifies the differences from Intel syntax used in CPU manuals.
The ground truth for what every x86 instruction does to registers, flags, and memory. Essential when you need to know exactly what to clobber.
The ARM equivalent of the Intel SDM. Required reading when writing AArch64 inline asm; explains dmb/dsb/isb semantics.
Free online; covers atomic operations and barriers from the kernel developer perspective, grounding inline asm in real usage patterns.
For understanding RISC-V CSR instructions (csrr, csrw, fence) used in arch/riscv inline asm — clean, readable spec compared to x86.
Checkpoints — you can now…
- ✓You can write a correct extended asm statement with output operands, input operands, and clobbers for a simple x86 arithmetic instruction, and verify the output with gcc -S.
- ✓You can explain the difference between the `=r` (write-only), `+r` (read-write), and matching `"0"` constraints, and choose the correct one for any given instruction.
- ✓You can explain why `asm volatile` does not, by itself, prevent reordering relative to C memory accesses, and what you must add (the `"memory"` clobber) to get a compiler barrier.
- ✓You can open `arch/x86/include/asm/atomic.h` in the Linux kernel and decode every part of an `arch_atomic_add` implementation including the LOCK_PREFIX, the `+m` constraint, and the `cc` clobber.
- ✓You can use Compiler Explorer to inspect the assembly generated from your inline asm at -O2, and identify cases where the compiler emitted unexpected register moves indicating a constraint problem.
- ✓You can describe what `asm goto` is, how the `%l[label]` operand works, and give a concrete example of how the kernel uses it for static keys / jump labels.
- ✓You can explain the difference between a compiler barrier (`asm volatile("" ::: "memory")`) and a CPU memory barrier (`mfence` / `dmb`), and when each is sufficient.