How to Use Inline Assembly in C Programming

Inline assembly allows developers to embed architecture-specific assembly instructions directly into C source code without creating separate assembly files. This article explains how C compilers integrate inline assembly using the asm keyword, details the structure of basic and extended assembly blocks, and outlines how the compiler manages registers, memory, and code optimization during the compilation process.

The asm Keyword

In standard C and compiler extensions like GCC, Clang, and MSVC, inline assembly is invoked using the asm or __asm__ keyword. The compiler treats the content inside the assembly block as raw instructions that bypass the high-level language translation, placing them directly into the instruction stream emitted by the compiler.

Depending on the compiler and platform, inline assembly is implemented in two main styles:

  • MSVC (x86 only): Uses block format (__asm { ... }) where instructions are written naturally and the compiler resolves C variable names automatically.
  • GCC / Clang: Uses a string-based format (asm("...")) supporting both Basic Inline Assembly and Extended Inline Assembly.

Basic vs. Extended Inline Assembly

Basic inline assembly simply passes instruction strings to the assembler without interacting with C variables:

__asm__("nop");

Extended inline assembly allows data exchange between C expressions and assembly instructions using a standardized four-part syntax:

__asm__ (
    "assembly template"
    : output operands
    : input operands
    : clobbered registers
);
  1. Assembly Template: A string containing the assembly instructions. The compiler substitutes tokens like %0, %1 with the physical registers or memory locations mapped to the operands.
  2. Output Operands: Expressions where the results of the assembly instructions are stored, prefixed by constraint modifiers (e.g., "=r" for write-only general-purpose register).
  3. Input Operands: C variables or constants passed into the instructions, specified with constraint letters (e.g., "r" for general register, "m" for memory).
  4. Clobber List: A list of registers, condition codes ("cc"), or memory ("memory") altered by the assembly instructions. This tells the compiler's register allocator not to assume these values are preserved.

How the Compiler Processes Inline Assembly

The C compiler does not validate whether the assembly instructions are semantically correct. Instead, it follows these integration steps:

  1. Register Allocation: The compiler inspects the input and output constraints and maps C variables to appropriate hardware registers or stack locations.
  2. Template Substitution: It replaces operand placeholders (such as %0) with the assigned hardware registers.
  3. Instruction Emission: The processed assembly string is inserted into the compiler’s intermediate assembly output (.s file).
  4. Assembler Execution: The native assembler (e.g., GNU as) parses and translates the final assembly text into machine code within the object file (.o).

Preserving Assembly from Optimization

Optimizing compilers routinely remove dead code or reorder instructions. To prevent the compiler from moving, altering, or eliminating an assembly block that performs critical tasks (like hardware I/O or processor state changes), the volatile qualifier is used:

__asm__ __volatile__ (
    "cli" ::: "memory"
);

The volatile keyword guarantees that the compiler preserves the assembly block exactly where it appears in the control flow.