If you’ve followed this free embedded Linux course so far, you already have a working cross toolchain and know how to invoke it. But every cross-compiler ships with a set of baked-in defaults — target architecture, floating-point ABI, CPU tuning, sysroot location — that were decided the moment the toolchain was built. This lecture is part of our free embedded systems course and shows you exactly how to inspect those defaults, override them safely on the command line, and understand why they matter before you ship a binary to real hardware.
gcc configuration
sysroot
binutils
free linux development course
What You Will Learn
- How to dump the exact configuration a cross-compiler was built with
- How to override baked-in CPU/ABI defaults from the command line
- What the toolchain sysroot actually contains and why the target needs part of it
- The full set of binutils and GNU dev tools that ship alongside gcc, and when to reach for each one
- The difference between building one toolchain per target (“Buildroot philosophy”) versus one generic toolchain (“Yocto philosophy”)
Prerequisites
You should already have a working cross toolchain on your PATH — built with crosstool-NG, downloaded prebuilt, or produced by Buildroot/Yocto — as covered in the earlier lectures of this free linux device drivers course track. If you don’t yet have one, go back and build one first; nothing here works without a real cross-compiler binary to inspect.
Every Toolchain Has Opinions Baked In
When someone builds a cross-compiler, they choose a target CPU family, an instruction set variant, a floating-point calling convention, and a default sysroot path. Those choices get compiled directly into the gcc binary as defaults. You never see this decision-making process — you just get a compiler that “does the right thing” for its intended target. The trouble starts when you assume those defaults match your actual board, or when you need to build for a slightly different CPU variant using the same toolchain.
The fix is simple: gcc can tell you exactly what it was configured with, and it lets you override almost every default from the command line without rebuilding anything.
Dumping the Full Configuration
Every gcc cross-compiler accepts -v, which prints its version, the exact configure invocation used to build it, and the thread model in use:
$ aarch64-none-linux-gnu-gcc -v
Using built-in specs.
COLLECT_GCC=aarch64-none-linux-gnu-gcc
Target: aarch64-none-linux-gnu
Configured with: ../configure --target=aarch64-none-linux-gnu \
--with-sysroot=/opt/ep-toolchain/aarch64-none-linux-gnu/libc \
--with-arch=armv8-a --enable-languages=c,c++ \
--enable-threads=posix --with-float=hard
Thread model: posix
gcc version 13.2.1 20231009 (EP Toolchain)
That output tells you the target triplet, the default sysroot path, which languages were enabled, and which ABI options are baked in. This is the first thing to check whenever a build behaves unexpectedly on a new machine — the compiler you think you’re running may not be configured the way you assume.
Overriding Defaults on the Command Line
Most of what -v reports as “configured with” can be overridden per invocation. Suppose the toolchain defaults to tuning for a Cortex-A53 core, but you’re compiling for a Cortex-A72 board this time:
$ aarch64-none-linux-gnu-gcc -mcpu=cortex-a72 hello.c -o hello
The -mcpu flag on the command line always wins over the compiled-in --with-cpu default. The same pattern applies to floating-point ABI (-mfloat-abi), instruction set (-march), and sysroot (--sysroot=). To see the complete list of architecture-specific flags a given cross-compiler supports, ask it directly:
$ aarch64-none-linux-gnu-gcc --target-help
This prints every machine-specific option the backend understands — invaluable when you’re porting to a new SoC and need to know exactly which tuning knobs exist.
–with-cpu=cortex-a53
–with-float=hard
–with-sysroot=/opt/ep-toolchain/…/libc|
| command-line flags override per invocation
vActual compile command
aarch64-none-linux-gnu-gcc -mcpu=cortex-a72 –sysroot=/custom/path app.c|
vResult: cortex-a72 tuning + custom sysroot, everything else
still inherited from the compiled-in defaults
One Toolchain Per Target, or One Generic Toolchain?
There are two common philosophies for how strict you make these defaults:
| Approach | Strategy | Trade-off |
|---|---|---|
| Per-target toolchain | Build (or download) a separate toolchain precisely configured for each board you support | Very low risk of ABI mismatches; more toolchains to manage and store |
| Generic toolchain | Build one broad toolchain and supply the correct -march/-mcpu/--sysroot flags per project |
One toolchain to maintain; you must get the override flags right every time |
Neither approach is universally correct — pick per-target toolchains when you have a handful of well-known boards and want zero ambiguity, and a generic toolchain when you’re supporting a broad, evolving hardware matrix and don’t want to regenerate toolchains constantly.
The Sysroot: What’s Actually Inside It
The sysroot is the directory the compiler and linker consult for target headers and libraries instead of your host’s own /usr/include and /usr/lib. You can print the active default with:
$ aarch64-none-linux-gnu-gcc -print-sysroot
/opt/ep-toolchain/aarch64-none-linux-gnu/libc
Inside a typical sysroot you’ll find:
| Path | Purpose | Needed where |
|---|---|---|
lib/ |
Shared objects for the C library and the dynamic loader (ld-linux) |
Target, at runtime |
usr/lib/ |
Static archives for the C library and other installed libraries | Host, at link time |
usr/include/ |
Headers for every library in the sysroot | Host, at compile time |
usr/bin/ |
Target-side utility programs, e.g. ldd |
Target, at runtime |
usr/share/ |
Locale and internationalization data | Target, at runtime |
sbin/ |
ldconfig, used to refresh the library cache |
Target, at runtime |
Notice the split: headers and static archives exist purely to help the compiler on your development host, while the shared libraries and loader in lib/ are what actually needs to be copied onto the target device’s root filesystem.
The Rest of the Toolchain: Binutils and GNU Dev Tools
gcc rarely works alone. A cross toolchain bundles a full set of GNU binutils and diagnostic tools, each with a distinct job:
| Tool | What it does |
|---|---|
as |
The GNU assembler — turns assembly output into object code |
ld |
The GNU linker — combines object files and libraries into an executable |
ar |
Creates and manages static library archives (.a files) |
ranlib |
Builds a symbol index inside a static archive so linking is faster |
nm |
Lists the symbols defined and referenced in an object file |
objdump |
Disassembles and dumps detailed information from object files |
objcopy |
Copies and translates between object file formats, e.g. ELF to raw binary |
readelf |
Displays structured information about ELF headers, sections, and segments |
strip |
Removes debug symbol tables to shrink a binary before deploying it |
size |
Reports section sizes and total image size — useful for flash-constrained targets |
strings |
Extracts printable character sequences from a binary file |
addr2line |
Maps a crashed program’s addresses back to source file and line, using debug symbols |
c++filt |
Demangles C++ symbol names into human-readable form |
gdb |
The GNU debugger, usually paired with gdbserver running on the target |
gprof |
Profiles where a program spends its execution time |
gcov |
Reports code coverage from instrumented builds |
Practical Example: Using addr2line After a Crash
One of the most common uses of these tools is decoding a crash address printed by a kernel oops or a userspace segfault handler. Build with debug symbols retained, then map an address back to source:
$ aarch64-none-linux-gnu-gcc -g -O0 ep_demo.c -o ep_demo
$ aarch64-none-linux-gnu-addr2line -e ep_demo -f -C 0x4006a4
main
/home/dev/ep_demo.c:12
Without -g at compile time, addr2line has nothing to map against — this is why you should keep an unstripped copy of every release binary even after shipping a strip-ped version to the target.
Common Mistakes and Troubleshooting
- Assuming the ABI matches the board: always confirm
--with-floatand the target triplet with-vbefore your first build for a new board. - Shipping the wrong sysroot library: mixing a target’s shared libraries built with one ABI against a host-linked binary built with another causes silent runtime crashes.
- Stripping before debugging: once you run
strip,addr2lineandgdblose all symbol information — always keep an unstripped build archived. - Forgetting
-print-sysrootcan lie: if you pass--sysroot=explicitly on the command line, the printed default no longer reflects what was actually used for that specific build.
Best Practices
- Run
-vonce per new toolchain install and keep the output alongside your project’s build documentation. - Prefer explicit
-march/-mcpu/--sysrootflags in your build system rather than relying on compiled-in defaults, especially in CI environments where the exact toolchain build may drift. - Archive unstripped binaries for every release build so crash addresses remain decodable months later.
Summary and Key Takeaways
-vreveals a cross-compiler’s exact build configuration, including its default sysroot and ABI.- Most compiled-in defaults can be overridden per-invocation with flags like
-mcpu,-march, and--sysroot=. - The sysroot splits cleanly into host-side compile/link material and target-side runtime material.
- Binutils tools like
nm,objdump,readelf, andaddr2lineare essential for inspecting and debugging cross-compiled binaries.
Conclusion
Understanding what your cross toolchain was configured with — and knowing which of those defaults you can safely override — turns a black-box compiler into a predictable, debuggable part of your build pipeline. Combined with the binutils suite, you now have everything needed to inspect, verify, and troubleshoot any binary this toolchain produces, a core skill for anyone following this free linux kernel development course track toward real embedded Linux work.
Frequently Asked Questions
How do I check which flags a cross-compiler was built with?
Run the compiler with -v on an empty or trivial input; it prints the full configure line it was built from, including its default architecture and sysroot.
Can I change the target CPU without rebuilding the toolchain?
Yes — pass -mcpu= or -march= on the compile command line. These override the compiled-in defaults for that invocation only.
What’s the difference between the sysroot’s lib and usr/lib directories?
lib/ holds shared objects needed on the target at runtime; usr/lib/ holds static archives used only by the compiler and linker on the host.
Why can’t gdb show source lines after I run strip?
strip removes the debug symbol table that gdb and addr2line rely on to map addresses back to source. Keep an unstripped copy before stripping a release build.
Should I build one toolchain per board or one generic toolchain?
Per-board toolchains reduce configuration risk but multiply what you maintain; a generic toolchain is easier to maintain but requires you to pass the correct override flags every build. Choose based on how many distinct boards you support.
What does –target-help actually show?
It lists every architecture-specific compiler flag the backend supports for that target, which is the fastest way to discover valid -m options for a new SoC.
Is objdump the same as readelf?
No — objdump focuses on disassembly and section content, while readelf focuses on structured ELF header, section, and segment metadata. They often overlap but aren’t interchangeable for every task.
Continue the Free Embedded Linux Course
More toolchain, driver, and kernel lectures are coming — all free, all original.
