In the previous lecture of this free embedded systems course we broke a toolchain down into its four components. Now we tackle a decision every embedded developer faces on day one: should your code be compiled on the same type of machine it will run on, or on a completely different one? This is the native-vs-cross question, and getting it right shapes your entire development workflow. This lecture continues our free linux development course with a hands-on cross-compilation example you can run today.
cross toolchain
cross-compilation
host vs target
embedded Linux build
What You Will Learn
- The precise difference between a native and a cross toolchain
- Why cross-compilation is the default choice for embedded Linux, not just a convenience
- The real risks of “cheating” by natively compiling on a host and copying binaries to the target
- How to install and use a real cross toolchain to build and inspect an ARM64 binary from an x86_64 machine
- When native compilation on the target actually makes sense
Prerequisites
- Completion of the previous lecture, “What Is a Linux Toolchain?”
- A Debian/Ubuntu-based Linux machine or VM for the hands-on demo
Two Kinds of Toolchain
Every toolchain falls into one of two categories, defined entirely by the relationship between the machine that runs the compiler (the host) and the machine that will run the resulting binary (the target).
Native Toolchain
Host and target are the same type of system — often literally the same machine. You compile a program on your laptop and run it on that same laptop. This is how desktop and server development normally works, and it’s increasingly common on more capable embedded boards, like a Raspberry Pi running a full Debian image with a self-hosted compiler installed directly on it.
Cross Toolchain
Host and target are different systems — typically a powerful x86_64 desktop building code for an ARM, MIPS, or RISC-V embedded board. The compiler itself runs on the host but emits machine code for the target’s instruction set. The resulting binary cannot run on the host at all; it can only run on the target.
Why Cross-Compilation Wins in Embedded Linux
Almost all embedded Linux development uses a cross toolchain, and the reasons go well beyond “your board is slow.”
- Hardware constraints. Most embedded targets simply don’t have the CPU speed, RAM, or storage to run a compiler comfortably — some don’t even have enough storage to hold a toolchain at all.
- Environment separation. Keeping host and target build environments distinct avoids a subtle trap: if your host and target happen to share an architecture, it’s tempting to compile natively and just copy the binary over. This works only as long as host and target stay in lockstep on every shared library version — and in practice, host distributions get updated far more often than embedded targets, so this approach quietly rots over time.
- Team consistency. When every engineer builds with the exact same cross toolchain, you eliminate an entire class of “works on my machine” bugs that come from slightly different host library versions leaking into target binaries.
The Honest Trade-offs
Cross-compilation isn’t free of downsides, and pretending otherwise does students a disservice.
| Factor | Native Compilation | Cross Compilation |
|---|---|---|
| Build speed | Limited by target hardware — often very slow | Fast, uses full host desktop/server power |
| Setup complexity | Low — just install a compiler on the target | Higher — must build or obtain a matching cross toolchain |
| Building third-party packages | Usually straightforward, package assumes it’s building for itself | Many open-source packages assume host == target and need patching or a build-system wrapper |
| Producing a full distro (e.g., Debian for a board) | Often required for some packages — practically unavoidable | Impossible for certain packages without native compilation somewhere in the chain |
| Host/target consistency | Risk of drift if host and target share an architecture | Clean separation, no accidental host contamination |
This is exactly why integrated build systems like Buildroot and the Yocto Project exist — they encapsulate the considerable bureaucracy of cross-compiling dozens of packages correctly, so you don’t hand-roll that logic yourself. We’ll cover both build systems in a dedicated later lecture.
Hands-On: Cross-Compiling for ARM64 From an x86_64 Host
Let’s make this concrete. The example below uses only standard, freely available Ubuntu packages — no vendor SDKs, no book code. You’ll build a small original program natively first, then cross-compile the exact same source for an ARM64 target and prove the two binaries are fundamentally different.
|
———————–
| |
v v
gcc (native) aarch64-linux-gnu-gcc (cross)
| |
v v
x86_64 binary ARM64 binary
| |
v v
runs on THIS host runs only on ARM64 target
# Install a real ARM64 cross toolchain (Ubuntu/Debian)
$ sudo apt update
$ sudo apt install -y gcc-aarch64-linux-gnu
# Confirm it installed correctly
$ aarch64-linux-gnu-gcc --version
aarch64-linux-gnu-gcc (Ubuntu 13.2.0-23ubuntu4) 13.2.0
Now write one small original source file and build it twice — once natively, once cross-compiled:
$ cat > ep_arch_check.c <<'EOF'
#include <stdio.h>
int main(void)
{
#if defined(__x86_64__)
printf("Built for x86_64 (native host)\n");
#elif defined(__aarch64__)
printf("Built for ARM64 (cross target)\n");
#else
printf("Built for an unrecognized architecture\n");
#endif
return 0;
}
EOF
# Native build — runs right here
$ gcc -O2 -o ep_arch_check_native ep_arch_check.c
$ ./ep_arch_check_native
Built for x86_64 (native host)
# Cross build — same source, different toolchain
$ aarch64-linux-gnu-gcc -O2 -o ep_arch_check_arm64 ep_arch_check.c
# Try to run the ARM64 binary directly on the x86_64 host
$ ./ep_arch_check_arm64
bash: ./ep_arch_check_arm64: cannot execute binary file: Exec format error
That “Exec format error” is the whole lesson in one line: the ARM64 binary is fully valid, correctly built machine code — it just isn’t machine code your x86_64 CPU understands. Let’s confirm the difference with binutils tools instead of guessing:
$ file ep_arch_check_native
ep_arch_check_native: ELF 64-bit LSB pie executable, x86-64, ...
$ file ep_arch_check_arm64
ep_arch_check_arm64: ELF 64-bit LSB pie executable, ARM aarch64, ...
Two different file outputs from one source file — that’s cross-compilation working exactly as intended. On a real embedded project, the ARM64 binary would be copied to your target board (over SCP, a shared NFS mount, or flashed into a root filesystem image) and executed there, while the native binary stays useless clutter on your host.
When Native Compilation Still Makes Sense
Cross-compilation is the default, not a universal rule. Native compilation is the right call when:
- Your target is powerful enough to comfortably self-host a compiler (many single-board computers running full desktop-class distros qualify).
- You’re building a full Linux distribution image for the target — some packages’ build systems assume they’re compiling for themselves and resist cross-compilation entirely.
- You need to bootstrap a native build environment from scratch: build a cross toolchain first, use it to construct a minimal native environment on the target (or under QEMU emulation), then compile natively from there onward.
Common Mistakes and Troubleshooting
- “Exec format error” confusion. Beginners often think this means a corrupted build. It almost always means an architecture mismatch — you ran a binary built for one CPU architecture on a different one.
- Assuming a shared architecture means you can skip cross-compilation. Even if host and target are both x86_64, differing library versions between the two will eventually cause runtime failures on the target that never show up on the host.
- Forgetting to install the matching cross C library. A cross compiler alone isn’t enough for anything beyond the simplest static programs — you’ll need a matching cross-built C library for real applications, which we’ll cover when we discuss C library choices.
- Trying to cross-compile a package that assumes native builds. Some legacy build scripts run compiled helper tools mid-build; a cross toolchain can’t execute those on the host. This is precisely the class of problem Buildroot and Yocto abstract away for you.
Best Practices
- Default to cross-compilation for embedded targets unless you have a specific, understood reason not to.
- Always verify architecture with
filebefore shipping a binary to a target — it takes five seconds and catches an entire category of deployment mistakes. - Never assume a shared host/target architecture is a shortcut around a proper cross toolchain for production work.
- Standardize your team’s cross toolchain (exact version, exact target triplet) and document it alongside your project, exactly as emphasized in the previous lecture.
Summary and Key Takeaways
- Native toolchains build and run on the same type of system; cross toolchains build on a host for a different target.
- Cross-compilation is the embedded Linux default because of hardware limits and the need for clean host/target separation.
- Building natively and copying binaries across a shared architecture is a tempting shortcut that degrades over time as host and target libraries drift apart.
- Full distro builds for a target sometimes require native compilation for specific packages — cross and native aren’t mutually exclusive across an entire project.
Conclusion
The native-vs-cross decision isn’t a one-time checkbox — it’s a mindset that shapes your whole build pipeline. For the overwhelming majority of embedded Linux work, a well-maintained cross toolchain, kept consistent across your team, is the right foundation. In the next lecture of this free linux device drivers course, we’ll go one level deeper and look at how CPU architecture, endianness, floating point support, and ABI choices determine exactly which cross toolchain you need for a given target.
FAQ
What does “Exec format error” actually mean?
It means you tried to run a binary built for a different CPU architecture than the machine you’re running it on — a classic sign of a cross-compiled binary run on the wrong host.
Can I just natively compile on my x86_64 host and copy the binary if my target is also x86_64?
You can, but it’s risky for anything beyond quick experiments — host and target library versions tend to drift apart over time, causing failures that only appear on the target.
Do I need a different cross toolchain for every target board?
You need a toolchain matching the target’s CPU architecture, endianness, floating point support, and ABI — boards sharing all of those can often share one toolchain.
Why can’t some packages be cross-compiled at all?
Some build systems run compiled helper tools as part of the build itself; a cross toolchain produces binaries the host can’t execute, breaking that workflow unless the build is restructured.
Is Buildroot or Yocto required for cross-compilation?
No, you can cross-compile manually as shown in this lecture. Build systems become valuable once you need to cross-compile dozens of interdependent packages reliably.
Is a self-hosted Raspberry Pi compiler a native or cross toolchain?
Native — the Pi is compiling code to run on itself, using its own onboard compiler.
What’s the fastest way to check what architecture a binary was built for?
Run the file command on it — it reports the target architecture directly, as shown in this lecture’s demo.
Keep Building Your Toolchain Knowledge
Continue this free embedded Linux course with the next lecture on CPU architecture and toolchain selection.
