A cross-compiler is a compiler that runs on one architecture or operating system (the host) and produces executable code for a different architecture or operating system (the target). In embedded development, the host is almost always a desktop or server machine (x86/x86-64 running Linux, macOS, or Windows), while the target is a microcontroller or embedded processor such as an ARM Cortex-M, RISC-V, AVR, or MIPS core.
In practice
Cross-compilation is the standard build workflow for most embedded systems because the target hardware typically lacks the resources (storage, RAM, OS) needed to run a compiler natively, though higher-end embedded Linux targets sometimes run native toolchains. Toolchains such as arm-none-eabi-gcc, avr-gcc, riscv32-unknown-elf-gcc, and xtensa-esp32-elf-gcc are all cross-compilers packaged with matching binutils and, optionally, a C runtime library (newlib, picolibc, or a vendor-specific libc). The triple in the toolchain name (e.g., arm-none-eabi) encodes the target CPU architecture, vendor, and OS/ABI fields (where fields may be placeholders such as 'none'), and selecting the wrong toolchain is a common early mistake.
Setting up a cross-compilation environment involves more than just the compiler binary. You also need a linker script that describes the target's memory map, a startup file that initializes the stack and .data/.bss sections before calling main, and the correct set of compiler flags for the target CPU (e.g., -mcpu=cortex-m4 -mfpu=fpv4-sp-d16 -mfloat-abi=hard for an STM32F4). Getting these flags wrong can produce code that assembles and links cleanly but faults at runtime, because the generated instructions or ABI do not match what the hardware or bootloader expects.
Because the cross-compiler runs on the host, developers cannot simply run the compiled binary to test it. Verification strategies include hardware-in-the-loop testing via a JTAG/SWD debug probe, instruction-set simulation (QEMU supports several ARM and RISC-V profiles), and unit-testing host-portable logic with a native compiler. Examining compiler output -- disassembly, map files, and generated assembly -- is a valuable practice for catching ABI mismatches, unexpected code size, and missed optimizations, as discussed in "Trust, but Verify: Examining the Output of an Embedded Compiler".
Optimization flags interact with cross-compilation in ways that matter for embedded targets. Choices between size (-Os) and speed (-O2/-O3), and features like link-time optimization (LTO) and function inlining, affect both code size and performance on memory-constrained devices. "Small or fast?" and "C Programming Techniques: Function Call Inlining" cover these trade-offs in detail. Understanding what the cross-compiler actually does with your source -- rather than assuming it does what you intend -- is a recurring theme in embedded debugging, explored in "My friend, the compiler" and "Jaywalking Around the Compiler".
Discussed on EmbeddedRelated
Frequently asked
Why can't I just use the gcc or clang already installed on my Linux or macOS machine?
The system compiler is configured to target the host architecture (x86-64 on most desktop and server machines, though ARM on Apple Silicon or ARM Linux hosts) and links against the host OS's C library (glibc, libSystem, etc.). Code compiled with it will not run on a Cortex-M or AVR target. You need a
toolchain built specifically for the target triple, such as arm-none-eabi-gcc, which targets bare-metal ARM with no OS and the EABI calling convention.
What does the 'none-eabi' part of arm-none-eabi-gcc mean?
The
toolchain name follows the GNU target triple format: architecture-vendor-os/ABI. For arm-none-eabi, 'arm' is the target architecture, 'none' means no vendor or OS (bare-metal), and 'eabi' is the Embedded Application Binary Interface, which defines calling conventions, register usage, and data layout for ARM embedded targets. Using the wrong ABI (e.g., mixing eabi and eabihf objects) causes link errors or runtime crashes.
How do I know which -mcpu and -mfloat-abi flags to use?
These must match the physical CPU on your target board. For example, an STM32F407 (Cortex-M4 with
FPU) typically uses -mcpu=cortex-m4 -mfpu=fpv4-sp-d16 -mfloat-abi=hard. An STM32F103 (Cortex-M3, no FPU) uses -mcpu=cortex-m3 -mfloat-abi=soft. Vendor SDKs, board support packages, and reference
Makefiles for a given device are the most reliable starting point. Mismatched flags can produce code that links but then fails at runtime -- through hard faults, subtle misbehavior, or wrong instruction generation.
Can I run unit tests on my host machine if I'm cross-compiling for a microcontroller?
Yes, for logic that does not depend on hardware peripherals. A common pattern is to write hardware-independent modules that can be compiled with the host's native compiler (gcc or clang targeting x86-64) and tested with a standard test framework. Hardware-dependent code is typically tested on the target via a debug probe, or inside a simulator like QEMU for supported architectures.
Is Clang/LLVM a viable cross-compiler for embedded targets?
Increasingly, yes. Clang with the right --target triple (e.g., --target=thumbv7em-none-eabi) and appropriate sysroot can cross-compile for ARM Cortex-M and several other embedded architectures. LLVM's LLD can replace GNU ld for many targets. However, GNU arm-none-eabi-gcc remains the dominant
toolchain for most Cortex-M work as of the mid-2020s, and some vendor SDKs (especially for AVR, PIC, and proprietary cores) only officially support their own or GNU-based toolchains.
Differentiators vs similar concepts
A cross-compiler is sometimes confused with a native compiler used with an emulator or simulator. A native compiler targets the host machine; one common workflow then runs the resulting binary inside an emulator that mimics the target, though native compilation is also used for host-side testing without any emulator. A cross-compiler targets the embedded architecture directly, producing native machine code for that target; no emulation layer is involved on the build machine. The two approaches can be combined (cross-compile for ARM, then run the output in QEMU), but they are distinct steps. Cross-compilation is also distinct from transpilation, which converts source code in one language to source code in another rather than producing target machine code or object files.