EmbeddedRelated.com

Assembly Language

Category: Tools | Also known as: ASM, assembler

Assembly language (ASM) is a low-level programming language in which each statement typically corresponds directly to one machine instruction for a specific processor architecture, though assemblers also support pseudo-instructions, macros, and data directives that may expand to multiple instructions, no instructions, or assembler-controlled sequences. An assembler translates the human-readable mnemonics and operands into the binary opcodes the CPU executes.

In practice

Assembly language appears in embedded work wherever the developer needs precise, deterministic control over what the processor does at the instruction level. Typical use cases include startup code (setting up the stack pointer, initializing the vector table, copying initialized data from flash to RAM before jumping to main), interrupt entry/exit sequences, and extremely tight timing loops where the cycle count of every instruction matters.

Inline assembly, supported by GCC, Clang, and most embedded toolchains (though with differing syntax and constraint models -- GCC/Clang use extended asm with clobber lists, while toolchains such as MSVC or some vendor-specific compilers use different syntax or impose additional restrictions), lets developers insert ASM statements inside a C source file. This is common for operations the C language cannot express cleanly: enabling or disabling interrupts with a single instruction, inserting a specific barrier or NOP, or reading a processor register directly. The blog post "Jaywalking Around the Compiler" explores how mixing C and ASM at this boundary requires careful attention to clobber lists and compiler assumptions.

The most frequent pitfall is writing assembly when the compiler would generate equivalent or better code on its own. Modern optimizing compilers can vectorize, pipeline, and schedule instructions in ways that are difficult to match by hand. The blog post "Assembly language is best - except when it isn't" covers this trade-off honestly: ASM is genuinely useful in specific narrow cases but counterproductive when applied broadly. Before resorting to ASM for performance, tools like Compiler Explorer (discussed in the LFSR series) let you inspect the compiler's output and verify whether hand-tuning is actually necessary. Note that the compiler's assembly output (e.g., via GCC's -S flag) shows what the compiler would assemble from your source, while true disassembly of the final linked binary (e.g., objdump -d) reflects the actual executable code after linking, inlining, and any linker-level transformations; both views are useful but they are not always identical.

Portability is the other major cost. Assembly is architecture-specific and often core-revision-specific. Code written for a Cortex-M4 with DSP instructions will not assemble for a Cortex-M0. Any project that carries hand-written ASM accumulates a maintenance burden every time the target changes, which is why confining ASM to small, well-documented, well-tested modules is strongly preferred over scattering it throughout the codebase.

Discussed on EmbeddedRelated

Frequently asked

When is it actually justified to write assembly in an embedded project?
The clearest justifications are: startup/reset code that must run before the C runtime is initialized, ISR prologue/epilogue sequences requiring exact register control, operations that map to a single instruction with no C equivalent (e.g., CLZ, BKPT, WFI, specific barrier instructions), and hard real-time loops where every cycle has been accounted for and the compiler cannot match the required schedule. Outside these cases, a well-written C function with appropriate compiler hints usually wins.
What is inline assembly and when should I use it instead of a separate .s file?
Inline assembly (asm() or __asm() blocks inside a C file) is appropriate for short, isolated instruction sequences that need to interact tightly with surrounding C variables, such as enabling interrupts or reading a status register. Separate .s files are better for larger routines, startup code, or anything that must remain readable and testable in isolation. Inline assembly clobber lists are error-prone; use them only when the interaction with the C context genuinely requires it.
How do I know whether the compiler already generates good enough code without writing ASM?
Inspect the compiler output. Most toolchains support a flag to emit compiler-generated assembly (e.g., -S for GCC produces the assembly the compiler would feed to the assembler) and a separate disassembly path for the final linked binary (e.g., objdump -d), which reflects actual executable code after linking and any post-link transformations. These two views are complementary but not always identical. Online tools like Compiler Explorer let you compare output across compilers and optimization levels in real time. As discussed in the LFSR series blog post, looking at what the compiler actually produces is often more productive than assuming it needs help.
Does writing assembly always produce faster code than C?
No. Optimizing compilers perform instruction scheduling, register allocation, and sometimes auto-vectorization that is difficult to replicate by hand. For anything beyond a few dozen instructions, hand-written ASM frequently performs the same as or worse than optimized C, and it introduces portability and maintenance costs. The blog post "Assembly language is best - except when it isn't" addresses this directly.
Is assembly language the same across all microcontrollers?
No. Assembly is specific to the processor's instruction set architecture (ISA). ARM Cortex-M, AVR, MSP430, RISC-V, and PIC each have distinct mnemonics, register names, calling conventions, and addressing modes. Even within a family there can be differences: Cortex-M0 lacks many instructions present in Cortex-M4 (DSP extensions, hardware divide). Any ASM written for one architecture must be rewritten from scratch for another.

Differentiators vs similar concepts

Assembly language is sometimes conflated with machine code. Machine code is the raw binary encoding the CPU executes directly; assembly language is the human-readable textual representation of those same instructions, translated by an assembler. The terms are conceptually close but not identical. Assembly is also sometimes confused with the assembler itself, which is the tool (program) that performs the translation; in casual usage "write it in assembler" means "write it in assembly language."