| |

Rust 124 🦀 Compiler Toolchain Targets, Cross-Compilation, and Linker Flags

⏱️ Estimated read: 10 min | |

Rust’s default build path assumes the host machine and the target machine are the same. A standard cargo build compiles for the host architecture, links against the host’s C library, and produces a binary that runs on the machine that produced it. Cross-compilation breaks this assumption. The toolchain must be told which target to build for, the correct standard library for that target must be available, and the linker must be configured to produce output the target architecture can execute. For bare-metal targets—kernels, embedded firmware, eBPF programs—the standard library does not exist in a precompiled form, and the linker is not the system’s default.

Key point: Cross-compilation requires three things: the target specification (a triple or a JSON file), the target’s standard library (installed via rustup or compiled via build-std), and a linker that can produce the target’s object format. Missing any one of these produces a build failure, and the error message usually identifies which piece is missing.


Why targets and linker flags exist

The host versus target problem. The Rust compiler is a cross-compiler by design. A single rustc binary installed on Linux can emit code for Windows, macOS, ARM, RISC-V, and WebAssembly. But rustc does not bundle the standard library for every target. By default, it ships only the host’s rust-std. Cross-compiling requires installing the target’s standard library separately, which is what rustup target add does .

The linker problem. rustc does not produce the final binary. It emits object files and delegates linking to a system linker (cc, ld, lld, or a custom linker). The default linker is configured for the host. When compiling for a different architecture, the linker must either understand that architecture’s object format or be replaced by a cross-linker. Cargo’s target.<triple>.linker setting overrides the default for a specific target .

The bare-metal problem. Targets like x86_64-unknown-none or thumbv7em-none-eabi do not have a precompiled standard library because they do not have an operating system. The core and compiler_builtins crates must be compiled from source using Cargo’s -Z build-std flag. This requires a nightly toolchain and the rust-src component .

The linker flag problem. Even with the correct linker, additional flags may be required: a custom linker script for bare-metal targets, --sysroot for cross-compilation with a target-specific C library, or architecture-specific flags like -C link-arg=-Tlinker.ld. These are configured in .cargo/config.toml under [target.<triple>].rustflags .

The trade-off. Cross-compilation adds configuration complexity. A correct .cargo/config.toml and a pre-installed cross-linker are prerequisites. The benefit is that a single development machine can produce binaries for every deployment target without requiring a build server for each architecture.


a. Managing targets with rustup

The rustup target subcommands manage the standard library for a given toolchain. The list subcommand shows available targets; add installs the standard library for one or more targets; remove uninstalls them .

# List all available targets for the active toolchain
rustup target list

# List only installed targets
rustup target list --installed

# Install the standard library for a target
rustup target add aarch64-unknown-linux-gnu

# Install multiple targets at once
rustup target add x86_64-pc-windows-gnu wasm32-unknown-unknown

After installing the target’s standard library, cargo build --target <triple> can compile for that target. However, rustup target add installs only the Rust standard library. The linker and any platform-specific C libraries must be installed separately .

For targets that do not have a precompiled standard library—custom JSON targets or bare-metal targets—rustup target add does not apply. These require build-std.


b. Building the standard library with build-std

The build-std feature compiles the Rust standard library from source for a custom or unsupported target. It requires a nightly toolchain and the rust-src component. The flag is configured in .cargo/config.toml or passed on the command line .

# .cargo/config.toml
[unstable]
build-std = ["core", "compiler_builtins"]
build-std-features = ["compiler-builtins-mem"]

[build]
target = "x86_64-blog_os.json"

The compiler_builtins crate provides functions that the compiler assumes exist on every platform: memcpy, memset, memcmp, and others. By default, these are disabled because the C library normally provides them. In a no_std environment, the compiler-builtins-mem feature enables the crate’s own implementations, which are marked #[no_mangle] and become available to the linker .

For eBPF targets like bpfel-unknown-none, bpf-linker is the linker and build-std = ["core"] is sufficient because compiler_builtins is not needed for the restricted eBPF environment .


c. Configuring linkers and linker flags

The linker for a target is configured in .cargo/config.toml under the [target.<triple>] table. The linker key sets the executable path, and rustflags passes additional flags to rustc .

[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"

[target.x86_64-pc-windows-gnu]
linker = "x86_64-w64-mingw32-gcc"

For bare-metal targets that use a custom linker script, the script is passed via link-arg:

[target.x86_64-blog_os]
rustflags = ["-C", "link-arg=-Tlinker.ld"]

The link-arg flag passes the argument directly to the linker. The linker script defines the memory layout of the target: where the kernel or firmware is loaded, what symbols are defined, and how sections are arranged. Without it, the linker produces a binary with the host’s default layout, which the bootloader will not load correctly.

For bpf-linker, the configuration is similar but the linker is the bpf-linker binary itself:

[target.bpfel-unknown-none]
linker = "bpf-linker"

The bpf-linker requires LLVM 19 and is installed via cargo install bpf-linker. It operates on LLVM bitcode and produces BPF object files that the kernel verifier can load .


Complete Example Session

# ============================================
# PART 1: CROSS-COMPILE FOR A SUPPORTED TARGET
# ============================================

# Install the target's standard library
rustup target add aarch64-unknown-linux-gnu

# Build for the target
cargo build --target aarch64-unknown-linux-gnu
# Fails if the aarch64 cross-linker is not installed
# ============================================
# PART 2: CONFIGURE THE CROSS-LINKER
# ============================================

# Install the cross-linker (Debian/Ubuntu)
sudo apt install gcc-aarch64-linux-gnu

# .cargo/config.toml
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
# ============================================
# PART 3: REBUILD WITH LINKER CONFIGURED
# ============================================

cargo build --target aarch64-unknown-linux-gnu
# Produces a binary that runs on ARM64 Linux
file target/aarch64-unknown-linux-gnu/debug/myapp
# ELF 64-bit LSB shared object, ARM aarch64
# ============================================
# PART 4: BARE-METAL TARGET SETUP
# ============================================

# Install nightly and rust-src for build-std
rustup toolchain install nightly --component rust-src

# .cargo/config.toml
[build]
target = "x86_64-blog_os.json"

[unstable]
build-std = ["core", "compiler_builtins"]
build-std-features = ["compiler-builtins-mem"]
# ============================================
# PART 5: LINKER SCRIPT FOR BARE-METAL
# ============================================

# linker.ld defines the memory layout
# .cargo/config.toml
[target.x86_64-blog_os]
rustflags = ["-C", "link-arg=-Tlinker.ld"]
# ============================================
# PART 6: BUILD THE BARE-METAL TARGET
# ============================================

cargo +nightly build
# Compiles core and compiler_builtins from source
# Links with the custom linker script
# ============================================
# PART 7: eBPF TARGET CONFIGURATION
# ============================================

# Install bpf-linker
cargo install bpf-linker

# .cargo/config.toml (in the eBPF crate)
[target.bpfel-unknown-none]
linker = "bpf-linker"

[unstable]
build-std = ["core"]
# ============================================
# PART 8: eBPF BUILD
# ============================================

cargo +nightly build --target bpfel-unknown-none
# Produces a BPF object file
# ============================================
# PART 9: DEFAULT TARGET OVERRIDE
# ============================================

# .cargo/config.toml
[build]
target = "aarch64-unknown-linux-gnu"

# Now a bare "cargo build" targets aarch64
cargo build

The nine parts trace the cross-compilation workflow: installing a target, configuring a cross-linker, building with the linker, setting up a bare-metal target with build-std, adding a linker script, building the bare-metal target, configuring the eBPF linker, building eBPF, and setting a default target to avoid repeating --target on every invocation.


Quick Reference

rustup target Commands

CommandPurpose
rustup target listList available targets
rustup target add <triple>Install standard library for target
rustup target remove <triple>Uninstall target standard library
rustup target list --installedList installed targets

Cargo Configuration Keys

KeyPurpose
build.targetDefault target when --target is omitted
target.<triple>.linkerLinker executable for target
target.<triple>.rustflagsAdditional flags for rustc
unstable.build-stdCrates to compile from source
unstable.build-std-featuresFeatures for build-std crates

Common Cross-Compilation Targets

TargetPlatform
aarch64-unknown-linux-gnuLinux ARM64
x86_64-pc-windows-gnuWindows x86_64 (GNU ABI)
wasm32-unknown-unknownWebAssembly (no OS)
bpfel-unknown-noneeBPF (little-endian)
x86_64-unknown-noneBare-metal x86_64

Best Practices

✅ Do This:

[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
  • ✅ Configure the cross-linker explicitly for each target
[unstable]
build-std = ["core", "compiler_builtins"]
build-std-features = ["compiler-builtins-mem"]
  • ✅ Use build-std for bare-metal targets
[build]
target = "aarch64-unknown-linux-gnu"
  • ✅ Set a default target to avoid repeating --target

❌ Don’t Do This:

cargo build --target aarch64-unknown-linux-gnu
# Without the cross-linker installed
  • ❌ Fails at link time with a cryptic error
# Use build-std on stable Rust
  • ❌ build-std requires nightly
# Omit the linker script for bare-metal
  • ❌ The binary will not load at the correct address

Common Pitfalls

PitfallWhy It HappensFix
linker not foundCross-linker not installedInstall the target’s toolchain (e.g., gcc-aarch64-linux-gnu)
can't find crate for coreStandard library not installed for targetrustup target add <triple>
eh_personality errorUnwinding enabled for bare-metalSet panic = "abort" in Cargo.toml
memcpy undefinedcompiler_builtins memory feature disabledSet build-std-features = ["compiler-builtins-mem"]
eBPF build failsMissing bpf-linker or nightlycargo install bpf-linker and use nightly

Real-World Examples

1. Install ARM64 Linux Target

rustup target add aarch64-unknown-linux-gnu

2. Configure Cross-Linker

[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"

3. Build for Target

cargo build --target aarch64-unknown-linux-gnu

4. Bare-Metal build-std

[unstable]
build-std = ["core", "compiler_builtins"]

5. Memory Intrinsics

[unstable]
build-std-features = ["compiler-builtins-mem"]

6. Linker Script

[target.x86_64-blog_os]
rustflags = ["-C", "link-arg=-Tlinker.ld"]

7. eBPF Linker

[target.bpfel-unknown-none]
linker = "bpf-linker"

8. Default Target

[build]
target = "x86_64-blog_os.json"

9. List Installed Targets

rustup target list --installed

10. Verify Binary Architecture

file target/aarch64-unknown-linux-gnu/debug/myapp

Visual

Cross-Compilation Pipeline

┌──────────────────────────────────────────────┐
│  Rust source                                 │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  rustc compiles for target triple            │
│  (needs target's standard library)           │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  Standard library source (build-std)         │
│  or precompiled (rustup target add)          │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  Object files emitted                        │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  Linker (cross-linker or bpf-linker)         │
│  + linker script (bare-metal)                │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  Executable for target architecture          │
└──────────────────────────────────────────────┘

Each stage removes a dependency on the host platform.

build-std vs rustup target add

┌──────────────────────────────────────────────┐
│  rustup target add <triple>                  │
│  Precompiled standard library                │
│  Available for supported targets             │
│  No nightly required                         │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  build-std = ["core", ...]                   │
│  Compile standard library from source        │
│  Required for custom/bare-metal targets      │
│  Requires nightly + rust-src                 │
└──────────────────────────────────────────────┘

Use rustup target add for standard targets. Use build-std for targets without precompiled libraries.


Summary

ItemValue
List targetsrustup target list
Install target stdrustup target add <triple>
Cross-linker config[target.<triple>].linker
Additional flags[target.<triple>].rustflags
Bare-metal std buildbuild-std + nightly
Memory intrinsicsbuild-std-features = ["compiler-builtins-mem"]
Default target[build].target
eBPF linkerbpf-linker

Key takeaways:

  • The target triple identifies the platform. rustup target add installs the standard library for that triple, but the linker must be installed separately .
  • build-std replaces precompiled libraries. Custom targets and bare-metal targets have no precompiled standard library. Nightly and rust-src are required .
  • The linker is not optional. A cross-compiler without a cross-linker produces object files that the default linker cannot combine into a target-architecture binary .
  • compiler-builtins-mem provides memory intrinsics. memcpy, memset, and memcmp are disabled by default because the C library normally provides them. In no_std, they must be enabled explicitly .
  • Linker scripts define bare-metal layout. Without a linker script, the binary is linked at the wrong address and the bootloader cannot execute it.

Remember: Cross-compilation is the process of removing the host from the build pipeline. The target triple tells the compiler what to emit; rustup target add or build-std provides the standard library; the linker configuration tells the system how to produce the final binary. Each piece is independent, and each failure mode has a distinct error message. Read the error, identify the missing piece, and add it.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!