5.3.10 · D4Build Systems & Toolchain

Exercises — Cross-compilation — toolchains, sysroot

2,126 words10 min readBack to topic
Figure — Cross-compilation — toolchains, sysroot

Level 1 — Recognition

Goal: name the parts. No computation, just "what is this thing?"

L1.1 — Split the triplet

Given the triplet arm-linux-gnueabihf, name what each of the four conceptual fields means (arch / vendor / os / abi). Note: the vendor field is absent here — say why that is legal.

Recall Solution

The general shape is <arch>-<vendor>-<os>-<abi/libc>. This triplet only has three dash-separated tokens, so one field is dropped:

  • armarch: 32-bit ARM CPU.
  • (vendor missing) → the vendor field (unknown/pc/none) is decorative and is routinely omitted. Tools treat a 3-token triplet as arch-os-abi.
  • linuxos: runs under a Linux kernel (so printf, malloc, syscalls all exist).
  • gnueabihfabi/libc: gnu (glibc) + eabi (embedded ABI) + hf (hard-float — floats passed in FPU registers).

See ABI and calling conventions for why the hf matters at the register level.

L1.2 — Which machine is which?

You compile on your x86-64 laptop a program destined for a Raspberry Pi (ARM). Label build, host, and target.

Recall Solution
  • build = x86-64 laptop (where the compiler binary itself was assembled — same as host here).
  • host = x86-64 laptop (where gcc executes).
  • target = Raspberry Pi ARM (where the output runs).

Because build = host ≠ target, this is an ordinary cross compile (not a Canadian cross).


Level 2 — Application

Goal: use the rules to produce a concrete answer.

L2.1 — Rewrite the search paths under a sysroot

A native compiler searches includes in /usr/include and /usr/local/include, and libraries in /lib and /usr/lib. Apply sysroot /opt/pi. Write out the four transformed paths.

Recall Solution

The sysroot rule: every absolute default path that begins at root / becomes (relocate the root, don't replace the logic — exactly like chroot). Note there is no double slash: /opt/pi + /usr/include = /opt/pi/usr/include.

L2.2 — Count the tool invocations

For arm-linux-gnueabihf-, name which tool from the toolchain does each job in order for compiling one .c into a runnable ELF: (a) source→assembly, (b) assembly→object, (c) object+libs→executable.

Recall Solution
  • (a) source → assembly: arm-linux-gnueabihf-gcc -S (the compiler proper).
  • (b) assembly → object: arm-linux-gnueabihf-as (the assembler).
  • (c) objects + libs → executable: arm-linux-gnueabihf-ld (the linker).

In practice gcc orchestrates all three, but each stage is a distinct prefixed tool. The object files are in ELF object file format.

L2.3 — Predict the file output

You run:

arm-linux-gnueabihf-gcc --sysroot=/opt/pi -o hello hello.c
file hello

What ISA word do you expect file to print, and what would it mean if it printed x86-64 instead?

Recall Solution

Expect: ELF 32-bit LSB executable, ARM, EABI5. The prefix's arch (arm) determines the emitted ISA. If it printed x86-64, the prefix did not take effect — you accidentally invoked the native gcc (wrong binary in $PATH). Fix by giving the full prefixed path or checking which arm-linux-gnueabihf-gcc.


Level 3 — Analysis

Goal: explain WHY, diagnose a broken setup.

L3.1 — Diagnose "fatal error: stdio.h: No such file or directory"

Your cross gcc reports it cannot find stdio.h, even though your native gcc always finds it. Explain the mechanism and give the one-flag fix.

Recall Solution

Mechanism: the native gcc has host include paths (/usr/include) baked in at its build time. The cross gcc's baked-in defaults point at a sysroot that is empty or unspecified — it deliberately refuses to fall back to the host's /usr/include, because those are x86 headers. So no stdio.h is visible. Fix: --sysroot=/opt/pi (or use a toolchain that bundles a populated sysroot). This makes <sysroot>/usr/include/stdio.h the file it finds.

L3.2 — Why --host ≠ --build is the cross signal

In autotools you run ./configure --host=arm-linux-gnueabihf --build=x86_64-linux-gnu .... Explain precisely why the inequality itself triggers cross-mode, and one feature-test that configure must now disable.

Recall Solution

configure compares the --host and --build triplets. If they differ, it concludes: "the binaries I produce (for host) will not run on the build machine." Therefore any test of the form "compile a tiny program, run it, read its output" is impossible — the ARM test binary cannot execute on x86. Disabled feature: run-time probes such as "does this system's malloc(0) return non-null?" or "endianness by executing a test" — configure switches to cached / assumed answers (or requires you to pre-seed a config.cache). See Build systems CMake and autotools.

Two .o files link cleanly, but the program crashes on the first floating-point call. The triplets were gnueabi (soft-float) and gnueabihf (hard-float). Explain why linking succeeded yet execution failed.

Recall Solution

The linker matches by symbol name, not by calling convention. float add(float,float) has the same mangled name in both ABIs, so the symbol resolves — link succeeds. At run time the mismatch bites: soft-float code passes the float in an integer register (r0), hard-float code reads it from an FPU register (s0). One side writes r0, the other reads s0 → garbage / trap. This is an ABI mismatch that no linker check catches. Fix: keep the entire triplet — including float ABI — identical across your code, libc, and every dependency.


Level 4 — Synthesis

Goal: assemble a whole correct invocation from scratch.

L4.1 — Bare-metal build line

You target aarch64-none-elf (64-bit ARM, no OS). Write the compile command for start.s + main.cfirmware.elf, and justify each of the three special flags: -nostdlib, -ffreestanding, -T link.ld.

Recall Solution
aarch64-none-elf-gcc -nostdlib -ffreestanding \
    -T link.ld -o firmware.elf start.s main.c
  • -nostdlib: the triplet's none means no libc and no crt0 startup exist — don't try to link them, or you get undefined-symbol errors for things that were never provided.
  • -ffreestanding: tells the compiler this is not a hosted environment — main is not special, printf/malloc are not assumed, and it won't insert calls to library helpers.
  • -T link.ld: with no OS loader, you decide where code and data sit in physical memory; the linker script places sections at the chip's real addresses. This ties into Static vs dynamic linking — bare metal is fully static, there is no dynamic loader on the chip.

L4.2 — Path arithmetic check

Sysroot /opt/pi. The compiler wants header sys/types.h. Compute the full absolute path it will open (using the primary include dir), and confirm it contains no //.

Recall Solution

Primary include dir is = /opt/pi/usr/include. Append the requested relative header sys/types.h: Single slash between every component — no //. That is the exact file open() receives.


Level 5 — Mastery

Goal: reason about a multi-machine scenario and defend it.

L5.1 — Classify the Canadian cross

A CI server (x86-64) builds a compiler whose binary must run on an ARM build-farm node, and that compiler emits code for a MIPS router. Fill build / host / target and explain in one line why this is not an ordinary cross.

Recall Solution
  • build = x86-64 CI server (where the compiler is assembled).
  • host = ARM build-farm node (where the compiler will execute).
  • target = MIPS router (what the compiler's output runs on).

All three differ (x86 ≠ arm ≠ mips) → this is a Canadian cross. An ordinary cross has build = host ≠ target; here even build ≠ host, so you need a cross toolchain to build the cross compiler — two toolchains deep.

L5.2 — Design and audit a sysroot

You must cross-compile a Linux ARM app that uses OpenSSL. List the minimum contents your sysroot at /opt/pi must contain, and state which single mistake would let it build but produce a binary that fails to load on the Pi.

Recall Solution

Minimum contents (all target/ARM builds):

  • /opt/pi/usr/include/openssl/*.h and /opt/pi/usr/include/stdio.h (headers → compiler knows prototypes/struct layouts).
  • /opt/pi/usr/lib/libssl.so, /opt/pi/usr/lib/libcrypto.so, /opt/pi/lib/libc.so.6 (libraries → linker resolves symbols against target code).
  • Startup files /opt/pi/usr/lib/crt1.o, crti.o, crtn.o.

The fatal-but-buildable mistake: placing host x86 libssl.so inside the sysroot. Symbol names match, so it links, but the shared object holds x86 instructions. On the Pi the dynamic loader tries to map ARM ELF that references an x86 .so → the loader either rejects the ELF class or the app segfaults on the first OpenSSL call. Every file under the sysroot must be target-ISA.


Recall Self-test recap

What makes build ≠ host ≠ host a Canadian cross? ::: All three roles differ, so you need a cross toolchain even to build the cross compiler. Why does a soft/hard-float mismatch link but crash? ::: Linker matches symbol names only; float register conventions differ at run time. What does --host ≠ --build tell configure? ::: That it is a cross build, so disable run-a-test-binary feature probes. Sysroot S=/opt/pi, header usr/include: full path of sys/types.h? ::: /opt/pi/usr/include/sys/types.h