Inline Assembly Constraints in C Explained
Inline assembly allows developers to embed raw machine instructions directly within C code while letting the compiler manage register allocation and optimization. Inline assembly constraints serve as the communication bridge between C expressions and CPU registers, explicitly instructing the compiler on how to pass data into and out of assembly blocks. This article explains what constraints are, examines the syntax used in extended assembly, and demonstrates how constraints dictate register binding during compilation.
Understanding Extended Inline Assembly
In compilers like GCC and Clang, inline assembly uses the extended
asm syntax:
__asm__ (
"assembly code"
: output operands
: input operands
: clobbers
);While the assembly string contains raw CPU instructions, the operands define the relationship between C variables and the underlying hardware. Constraints are string literals attached to these operands that restrict where an operand can be placed—whether in a specific hardware register, any general-purpose register, or directly in memory.
How Constraints Direct Register Binding
During compilation, the register allocator determines which hardware
registers hold program variables. When it encounters an inline assembly
block, it reads the constraint strings to bind C variables to machine
registers. The compiler then inserts necessary instructions (such as
mov) to ensure values are loaded into those registers
before the assembly code runs and saved back to C variables
afterward.
1. General Register Constraints
The most flexible way to bind registers is using general constraints:
"r"constraint: Instructs the compiler to allocate any available general-purpose register.
int a = 10, b = 20, result;
__asm__ (
"add %2, %0"
: "=r" (result)
: "0" (a), "r" (b)
);In this example, "r" lets the compiler choose an
unoccupied register (such as %eax, %ebx, or
%r8 on x86) for variable b. The compiler
automatically substitutes %0, %1, and
%2 with the allocated register names.
2. Architecture-Specific Register Constraints
Certain assembly instructions require their operands to reside in specific hardware registers. To enforce this, compilers provide architecture-specific constraints.
On x86 and x86-64 architectures, specific letters bind operands directly to designated registers:
"a": Binds to the Accumulator register (%al,%ax,%eax,%rax)."b": Binds to the Base register (%bl,%bx,%ebx,%rbx)."c": Binds to the Counter register (%cl,%cx,%ecx,%rcx)."d": Binds to the Data register (%dl,%dx,%edx,%rdx)."S": Binds to the Source Index register (%si,%esi,%rsi)."D": Binds to the Destination Index register (%di,%edi,%rdi).
For example, the x86 cpuid instruction expects the
function code in eax:
unsigned int code = 1, eax, ebx, ecx, edx;
__asm__ (
"cpuid"
: "=a" (eax), "=b" (ebx), "=c" (ecx), "=d" (edx)
: "a" (code)
);The constraint "a" (code) forces the compiler to load
the variable code into %eax before executing
cpuid. The constraints "=a",
"=b", "=c", and "=d" instruct the
compiler to extract the resulting values directly from
%eax, %ebx, %ecx, and
%edx.
Constraint Modifiers and Data Flow
Modifiers are prefixed to constraint characters to define how the register is accessed:
=(Write-only): Indicates that the bound register is an output operand. The initial value in the register is discarded, and the compiler overwrites it with new data.+(Read-modify-write): Indicates the operand acts as both an input and an output. The compiler preloads the register with the variable's current value and reads the modified value back after execution.&(Earlyclobber): Informs the compiler that an output register is modified before all input registers are consumed. This prevents the register allocator from sharing the same physical register for an input and an output operand.
Memory and Constant Constraints
Not all constraints bind data to registers:
"m": Specifies a memory operand. The compiler passes an effective memory address rather than loading the value into a CPU register."i": Specifies an immediate integer constant known at compile-time, allowing instructions to embed hardcoded values directly without using a register.
By selecting appropriate constraints and modifiers, developers maintain strict control over machine-level execution while allowing the C compiler to optimize surrounding code, handle register spills, and prevent register allocation conflicts.