Inline Assembly in C++ on Raspberry Pi¶
Introduction¶
While writing pure assembly files (.s or .S) is useful for stand-alone routines, there are times when you want to execute a few low-level CPU instructions directly inside your C++ code. Writing a full assembly function for simple actions (like reading a hardware register or invoking a specific instruction) adds unnecessary function call overhead.
This is where inline assembly comes in. GCC and Clang (the compilers used on Raspberry Pi OS) support extended assembly syntax that allows you to embed Arm64 assembly instructions directly within your C++ functions.
In this tutorial, you will learn the syntax of inline assembly, how to bind C++ variables to assembly registers, and how to write safe, optimized inline assembly code.
Basic vs. Extended Syntax¶
The C++ keyword for inline assembly is asm (or __asm__). There are two types of inline assembly: Basic and Extended.
Basic Inline Assembly¶
Basic inline assembly is simple but limited. It only executes instructions and cannot interact with C++ variables safely:
Extended Inline Assembly¶
To read from or write to C++ variables, you must use Extended asm. Extended assembly uses the following structure:
__volatile__(orvolatile): Prevents the compiler from optimizing out, moving, or deleting the assembly block. Always use it if your assembly has side effects (like reading hardware states or modifying system memory).
Operands and Constraints¶
To pass values between C++ and assembly, you specify operands. The compiler automatically maps C++ variables to available CPU registers based on constraints.
Syntax Example¶
Let's look at a function that adds two integers using inline assembly:
How to read this:¶
- Instruction Template:
"add %w0, %w1, %w2"*%0,%1,%2refer to the operands listed below the code. * Thewprefix (e.g.,%w0) tells the compiler to use 32-bit registers (w0-w30) instead of 64-bit ones (x0-x30), matching the size ofintvariables. - Output Operands:
: "=r" (result)*=indicates that this variable is write-only. *rindicates that the variable must be placed in a general-purpose register. *(result)binds the C++ variableresultto%0. - Input Operands:
: "r" (a), "r" (b)*rindicates that inputsaandbmust be loaded into general-purpose registers. *ais bound to%1, andbis bound to%2.
Common Constraints for Arm64¶
| Constraint | Type | Description |
|---|---|---|
"r" |
Register | General-purpose register (x0-x30 or w0-w30) |
"w" |
Register | SIMD/Vector/Floating-point register (v0-v31 or s0/d0 variants) |
"I" |
Immediate | Integer constant suitable for arithmetic instructions |
"m" |
Memory | Memory operand (allows direct memory referencing) |
"+" |
Modifier | Operand is both read from and written to |
"=" |
Modifier | Operand is write-only |
Practical Example 1: High-Resolution Timer¶
Raspberry Pi's Arm64 cores have a built-in virtual counter register (CNTVCT_EL0) that counts clock cycles at a fixed system frequency. We can read this 64-bit register using a single mrs instruction.
Practical Example 2: Vector Math (SIMD Inline)¶
You can also use NEON registers in inline assembly. Let's write a function that multiplies four floats simultaneously using the vector registers.
The Clobber List Explained¶
At the end of the vector_multiply block, we specified "v0", "v1", "v2", "memory".
This is the Clobber List. It tells the compiler:
1. We manually changed v0, v1, and v2. If the compiler was holding important variables in those registers, it must save them elsewhere before our block executes.
2. We modified "memory". This forces the compiler to write cached variables back to RAM before our assembly block runs and reload them afterward, preventing pointer optimization issues.
Best Practices and Pitfalls¶
- Don't Overuse It: Compilers are excellent at optimizing code. Write inline assembly only when standard C++ cannot generate the specific CPU instruction you need (like
mrs,clz, or cache control instructions). - Mark Clobbers Accurately: Forgetting to list a modified register in the clobber list is one of the hardest bugs to trace. The compiler will assume the register was untouched and may use corrupted values.
- Understand Register Prefixes: In Arm64, using
%0defaults to 64-bitxregisters. For 32-bit types (likeint), use thewmodifier:%w0. For floating-point/vector registers, specify size suffixes or elements. - Use
__volatile__: If your block is reading status registers or memory-mapped hardware,__volatile__is critical. Otherwise, the compiler might think the code is redundant and optimize it away.
Conclusion¶
Inline assembly is a bridge between C++ and low-level hardware control. It allows you to write clean C++ while retaining the ability to execute performance-critical CPU operations natively on your Raspberry Pi.
This completes our core series on Arm64 Assembly Programming on Raspberry Pi! You now have the knowledge to write standalone assembly programs, interface with C++, leverage NEON SIMD vector optimization, and write inline assembly blocks.