Article revision Version 1 of 4

Build a GCC Cross-Compiler for Kernel Development

Originally published by @nibblebits Aug 25, 2026 at 11:40
View current article
Initial version

Original article publication

This is the article as it appeared before any published revisions.

Snapshot

Article at version 1

Compilers & Toolchains

Your everyday compiler is built to create programs for your everyday operating system. It expects that system's ABI, headers, libraries, startup files, executable format, and linker conventions. That is exactly what you do not want when you are writing a kernel.

A kernel runs in a freestanding environment. There may be no C library, no process loader, no main, and no operating system underneath your code. You control the entry point, memory layout, runtime support, and every byte that enters the final image.

The clean solution is a cross-compiler: a compiler that runs on your development machine but emits code for a deliberately separate target. In this guide, we will build an i686-elf toolchain suitable for a 32-bit x86 kernel. The same structure also works for targets such as x86_64-elf, although architecture-specific kernel flags and startup code will differ.

By the end, you will have:

  • ${TARGET}-gcc and ${TARGET}-g++ for compiling freestanding C and C++
  • ${TARGET}-as for assembling target code
  • ${TARGET}-ld for linking target objects
  • ${TARGET}-ar, ${TARGET}-objcopy, ${TARGET}-objdump, ${TARGET}-readelf, and other target-aware utilities
  • libgcc, GCC's low-level runtime support library
  • the freestanding subset of libstdc++ for useful C++ headers that do not require a hosted operating system

This guide uses GCC 16.2 and GNU Binutils 2.47, the current releases at the time of writing. The optional debugger section uses GDB 17.2. If newer releases are available when you read this, update the version variables and review their installation notes before building.

Why the host compiler is the wrong tool

Suppose your development machine runs 64-bit Linux. Its normal gcc is probably configured for a target similar to x86_64-pc-linux-gnu. Even when it can emit 32-bit instructions, its defaults still belong to a hosted Linux environment. Depending on the distribution, it may assume position-independent executables, Linux startup objects, glibc, a dynamic loader, and the host's linker behavior.

The -ffreestanding option is necessary for kernel code, but it does not transform a host toolchain into a purpose-built kernel toolchain. It changes the compiler's language assumptions: GCC sets __STDC_HOSTED__ to 0 and stops assuming most standard-library functions have their hosted meanings. You still need to control assembly, linking, startup, runtime support, and the target format.

A target such as i686-elf makes your intent explicit:

  • i686 selects a 32-bit x86 processor family.
  • elf selects the Executable and Linkable Format without naming a hosted operating system.
  • The installed programs are prefixed with i686-elf-, so accidentally invoking the host linker becomes much less likely.

That separation is the real advantage. Your kernel build no longer depends on whatever defaults your desktop distribution chose this year.

What build, host, and target mean

Toolchain documentation uses three similar terms:

  • Build is the machine on which you compile the toolchain.
  • Host is the machine on which the resulting compiler will run.
  • Target is the machine for which that compiler will generate code.

For this guide, build and host are your current GNU/Linux machine, while target is i686-elf. Because host and target differ, the result is a cross-compiler.

Before you begin

The commands below assume a recent Debian or Ubuntu installation, including Ubuntu running in WSL 2. Native Windows users will have a much easier time performing this build inside WSL 2 than trying to translate a POSIX-oriented build system into PowerShell.

Install the basic host tools:

sudo apt update
sudo apt install -y \
  build-essential \
  bison \
  flex \
  texinfo \
  xz-utils \
  curl \
  ca-certificates

GCC needs a working C++14 compiler, GNU Make, and several mathematical support libraries. We will use GCC's contrib/download_prerequisites helper to download supported copies of GMP, MPFR, MPC, ISL, and other required source packages into the GCC tree.

Plan for several gigabytes of free disk space and a build that may take from minutes to well over an hour, depending on your CPU, storage, and available memory.

1. Choose versions, target, and directories

Keep the source, build, and installation trees separate. This makes failed builds easier to diagnose and lets you replace the toolchain without touching the system compiler.

export GCC_VERSION=16.2.0
export BINUTILS_VERSION=2.47
export GDB_VERSION=17.2
export TARGET=i686-elf

export PREFIX="$HOME/opt/cross"
export SRC="$HOME/src/cross"
export BUILD="$HOME/build/cross"

mkdir -p "$PREFIX" "$SRC" "$BUILD"
export PATH="$PREFIX/bin:$PATH"

The PREFIX is intentionally outside /usr. GCC does not provide a reliable uninstall target, so an isolated installation directory is safer to upgrade or remove later.

The PATH change matters twice: it lets your shell find the finished tools, and it lets the GCC build find the cross-assembler and cross-linker that we install first.

If you open a new terminal later, export PREFIX, TARGET, and the updated PATH again. After the build succeeds, you can place the relevant exports in your shell profile.

2. Download the official source releases

Download GCC and Binutils from Sourceware's official release directories:

cd "$SRC"

curl -LO \
  "https://sourceware.org/pub/binutils/releases/binutils-${BINUTILS_VERSION}.tar.xz"

curl -LO \
  "https://sourceware.org/pub/gcc/releases/gcc-${GCC_VERSION}/gcc-${GCC_VERSION}.tar.xz"

tar -xf "binutils-${BINUTILS_VERSION}.tar.xz"
tar -xf "gcc-${GCC_VERSION}.tar.xz"

For a security-sensitive or reproducible environment, also download the published signature or checksum files and verify the archives before extracting them.

Now fetch GCC's supported prerequisites:

cd "$SRC/gcc-${GCC_VERSION}"
./contrib/download_prerequisites

Run that helper from the root of the GCC source tree. It places the prerequisite sources where GCC's build system expects to find them.

3. Build and install GNU Binutils

GCC generates assembly and object code, but it still needs target-aware programs to assemble, archive, inspect, and link those files. That is why Binutils comes first.

Create an empty out-of-tree build directory:

mkdir -p "$BUILD/binutils"
cd "$BUILD/binutils"

Configure Binutils for the target:

"$SRC/binutils-${BINUTILS_VERSION}/configure" \
  --target="$TARGET" \
  --prefix="$PREFIX" \
  --with-sysroot \
  --disable-nls \
  --disable-werror

The important options are:

  • --target="$TARGET" selects the object format and architecture the tools will handle.
  • --prefix="$PREFIX" installs everything into the isolated cross-toolchain directory.
  • --with-sysroot gives the tools a target-root concept that can grow with your kernel project.
  • --disable-nls omits translated diagnostic catalogs, reducing build complexity.
  • --disable-werror prevents a warning in Binutils itself from becoming a fatal build error on a newer host compiler.

Build in parallel and install:

make -j"$(nproc)"
make install

Do not use sudo make install. Your prefix is inside your home directory and should be writable by your normal account.

Confirm that the cross-linker is available before continuing:

command -v "${TARGET}-ld"
"${TARGET}-ld" --version

If command -v prints nothing, verify that $PREFIX/bin exists and appears near the beginning of PATH.

4. Build the freestanding GCC compiler

Create a second empty build directory. GCC's own installation instructions strongly recommend building outside the source tree.

mkdir -p "$BUILD/gcc"
cd "$BUILD/gcc"

Configure GCC:

"$SRC/gcc-${GCC_VERSION}/configure" \
  --target="$TARGET" \
  --prefix="$PREFIX" \
  --disable-nls \
  --enable-languages=c,c++ \
  --without-headers \
  --disable-hosted-libstdcxx \
  --disable-threads \
  --disable-multilib

Here is what those choices mean:

  • --enable-languages=c,c++ builds the C and C++ front ends and skips languages you do not need for a typical kernel.
  • --without-headers tells GCC that the target has no C library headers. That is the correct starting point for a new freestanding system.
  • --disable-hosted-libstdcxx builds only the part of GNU's C++ library intended for freestanding environments.
  • --disable-threads selects the single-threaded target model until your operating system supplies a threading runtime.
  • --disable-multilib builds one default target runtime variant. This avoids failures caused by missing secondary-ABI support and keeps the first toolchain small.

Now build the compiler programs themselves:

make -j"$(nproc)" all-gcc

Next, build libgcc for the target:

make -j"$(nproc)" all-target-libgcc

Build the freestanding subset of libstdc++ as well:

make -j"$(nproc)" all-target-libstdc++-v3

Finally, install the compiler and both target libraries:

make install-gcc
make install-target-libgcc
make install-target-libstdc++-v3

This is intentionally not a complete hosted GCC installation. There is no target libc and only the freestanding subset of the C++ standard library. You are building the layer needed to compile freestanding kernel code, not a compiler for normal user-space applications.

5. Verify the toolchain

First, ask GCC which target it was built for:

"${TARGET}-gcc" -dumpmachine

The output should be:

i686-elf

Check the installed compiler and libgcc path:

"${TARGET}-gcc" --version
"${TARGET}-gcc" -print-libgcc-file-name

The second command should print a file inside your cross-toolchain prefix, not a library from /usr/lib.

Now compile a tiny freestanding translation unit:

cd "$BUILD"

cat > sanity.c <<'EOF'
void kernel_entry(void)
{
    for (;;) {
        __asm__ volatile ("hlt");
    }
}
EOF

"${TARGET}-gcc" \
  -std=gnu23 \
  -ffreestanding \
  -O2 \
  -Wall \
  -Wextra \
  -c sanity.c \
  -o sanity.o

"${TARGET}-readelf" -h sanity.o

For i686-elf, the ELF header should identify a 32-bit object for Intel 80386. This test proves that the compiler, assembler, and binary-inspection tools agree on the target.

Which standard headers are available?

A naked cross-compiler does not have a target C library, so hosted headers such as stdio.h are intentionally absent. GCC does provide the C headers required for a basic freestanding implementation. For C11, that set includes:

  • float.h
  • iso646.h
  • limits.h
  • stdalign.h
  • stdarg.h
  • stdbool.h
  • stddef.h
  • stdint.h
  • stdnoreturn.h

These headers primarily define types, constants, and macros rather than depending on operating-system services. Later language standards adjust the exact required set, so treat the GCC manual for your selected language mode as authoritative.

The reduced libstdc++ installation similarly exposes the C++ facilities that can work without a hosted C library. In modern GCC releases that is more than a token handful of headers, but it is still not the full desktop C++ library. Unsupported hosted headers or portions of headers will be unavailable in freestanding mode.

6. Use it in a kernel build

Your build system should name the cross tools explicitly. A small Makefile might start like this:

TARGET  := i686-elf
CC      := $(TARGET)-gcc
CXX     := $(TARGET)-g++
AS      := $(TARGET)-as
LD      := $(TARGET)-ld
AR      := $(TARGET)-ar
OBJCOPY := $(TARGET)-objcopy

CFLAGS := -std=gnu23 -ffreestanding -O2 -Wall -Wextra
CFLAGS += -fno-stack-protector -fno-pie

The right flags depend on your architecture, boot protocol, memory model, and runtime. A 64-bit x86 kernel commonly adds -mno-red-zone, for example, because interrupt handlers can invalidate the assumptions behind the System V red zone.

For final linking, many kernels use the compiler driver rather than invoking ld directly. The driver knows where its target libgcc lives:

"${TARGET}-gcc" \
  -T linker.ld \
  -ffreestanding \
  -O2 \
  -nostdlib \
  boot.o kernel.o \
  -lgcc \
  -o kernel.elf

-nostdlib prevents hosted startup files and standard libraries from entering the link. It also suppresses libgcc, so adding -lgcc explicitly is usually the right choice. GCC may emit calls to helper routines for arithmetic or other operations that the processor cannot express in one instruction.

Your kernel must provide anything else it uses. Even freestanding compiler output can require routines such as memcpy, memmove, memset, or memcmp, depending on the code and optimization decisions. Implement those routines inside the kernel instead of trying to link the host's libc.

What C++ support does—and does not—include

Enabling the C++ front end gives you ${TARGET}-g++, the C++ language parser, and the freestanding subset of libstdc++. It does not give your new kernel a complete hosted C++ runtime.

Until you implement or deliberately port the required runtime pieces, kernel C++ code usually avoids exceptions, RTTI, threads, and the hosted standard library:

CXXFLAGS := $(CFLAGS) -fno-exceptions -fno-rtti

You may also need to provide global new and delete, constructor initialization, destructor registration policy, guard functions for local statics, and other ABI support. C++ can be an excellent kernel language, but the kernel—not the compiler build—must define its runtime environment.

You can confirm that a freestanding C++ header is installed with a compile-only test:

cat > sanity.cc <<'EOF'
#include <type_traits>

static_assert(std::is_unsigned_v<unsigned int>);
EOF

"${TARGET}-g++" \
  -std=gnu++23 \
  -ffreestanding \
  -fno-exceptions \
  -fno-rtti \
  -c sanity.cc \
  -o sanity-cxx.o

Changing the target

For a 64-bit x86 kernel, you can rebuild with:

export TARGET=x86_64-elf

Use fresh Binutils and GCC build directories when changing the target. A build directory caches configuration decisions; reusing one across targets produces confusing and sometimes subtly incorrect results.

Do not assume that changing the target triple is the only architecture work required. Your boot path, assembly, linker script, ABI, compiler flags, interrupt rules, and emulator configuration must all match the new architecture.

Optional: build a target-aware GDB

Your host's GDB may already understand the architecture you are developing for. If the host and target architectures differ, or if you want a consistently prefixed debugger alongside the rest of the toolchain, build GDB separately.

Download and unpack the current official release:

cd "$SRC"

curl -LO \
  "https://sourceware.org/pub/gdb/releases/gdb-${GDB_VERSION}.tar.xz"

tar -xf "gdb-${GDB_VERSION}.tar.xz"

Depending on your distribution, GDB may require additional host development packages such as Expat, ncurses, and Python headers. Configure only the debugger for your target in a fresh build directory:

mkdir -p "$BUILD/gdb"
cd "$BUILD/gdb"

"$SRC/gdb-${GDB_VERSION}/configure" \
  --target="$TARGET" \
  --prefix="$PREFIX" \
  --disable-werror

make -j"$(nproc)" all-gdb
make install-gdb

Verify it with:

"${TARGET}-gdb" --version

For emulator-based kernel debugging, GDB commonly connects to a remote debugging stub. For example, after starting QEMU with its GDB server enabled, load your symbol-bearing kernel.elf and use target remote localhost:1234 from GDB. Keep the unstripped ELF file for debugging even if your boot image uses a stripped or converted copy.

Common failures and what they mean

${TARGET}-as: command not found

Binutils was not installed successfully, or $PREFIX/bin was not in PATH when GCC was configured. Fix the path, confirm ${TARGET}-as and ${TARGET}-ld work, then configure GCC in a fresh build directory.

cannot compute suffix of object files

This is a summary error, not usually the root cause. Look earlier in config.log for the first failed compiler or linker command. Common causes include an unavailable cross-assembler, stale configuration files, missing host prerequisites, or an incorrect path.

Missing gnu/stubs-32.h or another secondary-ABI header

This often means the build attempted a multilib variant unsupported by your host setup. Confirm that GCC was configured with --disable-multilib, then rebuild from a clean GCC build directory.

fatal error: stdio.h: No such file or directory

That is expected in this toolchain. You deliberately built it without target libc headers. Kernel code should not include hosted headers such as stdio.h. Create freestanding kernel headers and implement the services you need.

Undefined references to memcpy, memset, or memcmp

The compiler may generate calls to these functions even when your source does not call them directly. Supply kernel implementations with the correct signatures and semantics.

cannot find -lgcc

You probably installed the compiler without building and installing the target runtime. Run the all-target-libgcc and install-target-libgcc steps, then verify the result with ${TARGET}-gcc -print-libgcc-file-name.

The final ELF file has the wrong architecture

Run ${TARGET}-gcc -dumpmachine and ${TARGET}-readelf -h kernel.elf. Also inspect the build log for an accidental invocation of plain gcc, as, or ld without the target prefix.

Make the setup reproducible

A manually entered build is useful for learning, but a checked-in script is better for a real project. Pin the GCC and Binutils versions, verify source checksums, keep the installation prefix versioned, and record the configure flags. That lets teammates and CI build the same toolchain instead of depending on a developer's workstation state.

Also keep the toolchain separate from the kernel source tree. The compiler is a build dependency; your kernel repository should be able to detect it, report a useful error when it is missing, and build cleanly when it is present.

Official references

Go from a toolchain to a working kernel

Building the cross-compiler is the foundation. The exciting work comes next: bootstrapping the machine, entering the right CPU mode, designing memory management, handling interrupts, writing drivers, building filesystems, adding processes, and debugging the whole stack when there is no operating system underneath you.

If you want a structured path through that work, the DragonZap Kernel Development From Scratch Bundle gives you 69 hours of hands-on kernel development training in one focused package. Stop piecing the journey together from disconnected snippets and start building with a complete roadmap.

Get the 69-hour Kernel Development From Scratch Bundle

kernelcross-compilergccos-developmentbinutils