Article revision Version 2 of 4

Build a GCC Cross-Compiler for Kernel Development

Edited by @nibblebits Aug 25, 2026 at 11:43
View current article
Published change

Stricter explanation

This permanent snapshot records exactly what @nibblebits published in version 2.

Snapshot

Article at version 2

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.

We will now begin installing necessary tools and compiling a cross compiler from source which can then be used for kernel development.

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
Change set

Changes in version 2

+3−1
article.md
1 1 <!--
2 2 Community title: Build a GCC Cross-Compiler for Kernel Development
3 3 Suggested tags: cross-compiler, gcc, kernel, os-development, binutils
4 4 -->
5 5
6 6 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.
7 7
8 8 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.
9 9
10 10 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.
11 11
12 12 By the end, you will have:
13 13
14 14 - `${TARGET}-gcc` and `${TARGET}-g++` for compiling freestanding C and C++
15 15 - `${TARGET}-as` for assembling target code
16 16 - `${TARGET}-ld` for linking target objects
17 17 - `${TARGET}-ar`, `${TARGET}-objcopy`, `${TARGET}-objdump`, `${TARGET}-readelf`, and other target-aware utilities
18 18 - `libgcc`, GCC's low-level runtime support library
19 19 - the freestanding subset of `libstdc++` for useful C++ headers that do not require a hosted operating system
20 20
21 21 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.
22 22
23 23 ## Why the host compiler is the wrong tool
24 24
25 25 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.
26 26
27 27 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.
28 28
29 29 A target such as `i686-elf` makes your intent explicit:
30 30
31 31 - `i686` selects a 32-bit x86 processor family.
32 32 - `elf` selects the Executable and Linkable Format without naming a hosted operating system.
33 33 - The installed programs are prefixed with `i686-elf-`, so accidentally invoking the host linker becomes much less likely.
34 34
35 35 That separation is the real advantage. Your kernel build no longer depends on whatever defaults your desktop distribution chose this year.
36 36
37 37 ## What build, host, and target mean
38 38
39 39 Toolchain documentation uses three similar terms:
40 40
41 41 - **Build** is the machine on which you compile the toolchain.
42 42 - **Host** is the machine on which the resulting compiler will run.
43 43 - **Target** is the machine for which that compiler will generate code.
44 44
45 45 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.
46 46
47 47 ## Before you begin
48 48
49 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.
49 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.
50
51 **We will now begin installing necessary tools and compiling a cross compiler from source which can then be used for kernel development.**
50 52
51 53 Install the basic host tools:
52 54
53 55 ```bash
54 56 sudo apt update
55 57 sudo apt install -y \
56 58 build-essential \
57 59 bison \
58 60 flex \
59 61 texinfo \
60 62 xz-utils \
61 63 curl \
62 64 ca-certificates
63 65 ```
64 66
65 67 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.
66 68
67 69 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.
68 70
69 71 ## 1. Choose versions, target, and directories
70 72
71 73 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.
72 74
73 75 ```bash
74 76 export GCC_VERSION=16.2.0
75 77 export BINUTILS_VERSION=2.47
76 78 export GDB_VERSION=17.2
77 79 export TARGET=i686-elf
78 80
79 81 export PREFIX="$HOME/opt/cross"
80 82 export SRC="$HOME/src/cross"
81 83 export BUILD="$HOME/build/cross"
82 84
83 85 mkdir -p "$PREFIX" "$SRC" "$BUILD"
84 86 export PATH="$PREFIX/bin:$PATH"
85 87 ```
86 88
87 89 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.
88 90
89 91 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.
90 92
91 93 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.
92 94
93 95 ## 2. Download the official source releases
94 96
95 97 Download GCC and Binutils from Sourceware's official release directories:
96 98
97 99 ```bash
98 100 cd "$SRC"
99 101
100 102 curl -LO \
101 103 "https://sourceware.org/pub/binutils/releases/binutils-${BINUTILS_VERSION}.tar.xz"
102 104
103 105 curl -LO \
104 106 "https://sourceware.org/pub/gcc/releases/gcc-${GCC_VERSION}/gcc-${GCC_VERSION}.tar.xz"
105 107
106 108 tar -xf "binutils-${BINUTILS_VERSION}.tar.xz"
107 109 tar -xf "gcc-${GCC_VERSION}.tar.xz"
108 110 ```
109 111
110 112 For a security-sensitive or reproducible environment, also download the published signature or checksum files and verify the archives before extracting them.
111 113
112 114 Now fetch GCC's supported prerequisites:
113 115
114 116 ```bash
115 117 cd "$SRC/gcc-${GCC_VERSION}"
116 118 ./contrib/download_prerequisites
117 119 ```
118 120
119 121 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.
120 122
121 123 ## 3. Build and install GNU Binutils
122 124
123 125 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.
124 126
125 127 Create an empty out-of-tree build directory:
126 128
127 129 ```bash
128 130 mkdir -p "$BUILD/binutils"
129 131 cd "$BUILD/binutils"
130 132 ```
131 133
132 134 Configure Binutils for the target:
133 135
134 136 ```bash
135 137 "$SRC/binutils-${BINUTILS_VERSION}/configure" \
136 138 --target="$TARGET" \
137 139 --prefix="$PREFIX" \
138 140 --with-sysroot \
139 141 --disable-nls \
140 142 --disable-werror
141 143 ```
142 144
143 145 The important options are:
144 146
145 147 - `--target="$TARGET"` selects the object format and architecture the tools will handle.
146 148 - `--prefix="$PREFIX"` installs everything into the isolated cross-toolchain directory.
147 149 - `--with-sysroot` gives the tools a target-root concept that can grow with your kernel project.
148 150 - `--disable-nls` omits translated diagnostic catalogs, reducing build complexity.
149 151 - `--disable-werror` prevents a warning in Binutils itself from becoming a fatal build error on a newer host compiler.
150 152
151 153 Build in parallel and install:
152 154
153 155 ```bash
154 156 make -j"$(nproc)"
155 157 make install
156 158 ```
157 159
158 160 Do not use `sudo make install`. Your prefix is inside your home directory and should be writable by your normal account.
159 161
160 162 Confirm that the cross-linker is available before continuing:
161 163
162 164 ```bash
163 165 command -v "${TARGET}-ld"
164 166 "${TARGET}-ld" --version
165 167 ```
166 168
167 169 If `command -v` prints nothing, verify that `$PREFIX/bin` exists and appears near the beginning of `PATH`.
168 170
169 171 ## 4. Build the freestanding GCC compiler
170 172
171 173 Create a second empty build directory. GCC's own installation instructions strongly recommend building outside the source tree.
172 174
173 175 ```bash
174 176 mkdir -p "$BUILD/gcc"
175 177 cd "$BUILD/gcc"
176 178 ```
177 179
178 180 Configure GCC:
179 181
180 182 ```bash
181 183 "$SRC/gcc-${GCC_VERSION}/configure" \
182 184 --target="$TARGET" \
183 185 --prefix="$PREFIX" \
184 186 --disable-nls \
185 187 --enable-languages=c,c++ \
186 188 --without-headers \
187 189 --disable-hosted-libstdcxx \
188 190 --disable-threads \
189 191 --disable-multilib
190 192 ```
191 193
192 194 Here is what those choices mean:
193 195
194 196 - `--enable-languages=c,c++` builds the C and C++ front ends and skips languages you do not need for a typical kernel.
195 197 - `--without-headers` tells GCC that the target has no C library headers. That is the correct starting point for a new freestanding system.
196 198 - `--disable-hosted-libstdcxx` builds only the part of GNU's C++ library intended for freestanding environments.
197 199 - `--disable-threads` selects the single-threaded target model until your operating system supplies a threading runtime.
198 200 - `--disable-multilib` builds one default target runtime variant. This avoids failures caused by missing secondary-ABI support and keeps the first toolchain small.
199 201
200 202 Now build the compiler programs themselves:
201 203
202 204 ```bash
203 205 make -j"$(nproc)" all-gcc
204 206 ```
205 207
206 208 Next, build `libgcc` for the target:
207 209
208 210 ```bash
209 211 make -j"$(nproc)" all-target-libgcc
210 212 ```
211 213
212 214 Build the freestanding subset of `libstdc++` as well:
213 215
214 216 ```bash
215 217 make -j"$(nproc)" all-target-libstdc++-v3
216 218 ```
217 219
218 220 Finally, install the compiler and both target libraries:
219 221
220 222 ```bash
221 223 make install-gcc
222 224 make install-target-libgcc
223 225 make install-target-libstdc++-v3
224 226 ```
225 227
226 228 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.
227 229
228 230 ## 5. Verify the toolchain
229 231
230 232 First, ask GCC which target it was built for:
231 233
232 234 ```bash
233 235 "${TARGET}-gcc" -dumpmachine
234 236 ```
235 237
236 238 The output should be:
237 239
238 240 ```text
239 241 i686-elf
240 242 ```
241 243
242 244 Check the installed compiler and `libgcc` path:
243 245
244 246 ```bash
245 247 "${TARGET}-gcc" --version
246 248 "${TARGET}-gcc" -print-libgcc-file-name
247 249 ```
248 250
249 251 The second command should print a file inside your cross-toolchain prefix, not a library from `/usr/lib`.
250 252
251 253 Now compile a tiny freestanding translation unit:
252 254
253 255 ```bash
254 256 cd "$BUILD"
255 257
256 258 cat > sanity.c <<'EOF'
257 259 void kernel_entry(void)
258 260 {
259 261 for (;;) {
260 262 __asm__ volatile ("hlt");
261 263 }
262 264 }
263 265 EOF
264 266
265 267 "${TARGET}-gcc" \
266 268 -std=gnu23 \
267 269 -ffreestanding \
268 270 -O2 \
269 271 -Wall \
270 272 -Wextra \
271 273 -c sanity.c \
272 274 -o sanity.o
273 275
274 276 "${TARGET}-readelf" -h sanity.o
275 277 ```
276 278
277 279 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.
278 280
279 281 ## Which standard headers are available?
280 282
281 283 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:
282 284
283 285 - `float.h`
284 286 - `iso646.h`
285 287 - `limits.h`
286 288 - `stdalign.h`
287 289 - `stdarg.h`
288 290 - `stdbool.h`
289 291 - `stddef.h`
290 292 - `stdint.h`
291 293 - `stdnoreturn.h`
292 294
293 295 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.
294 296
295 297 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.
296 298
297 299 ## 6. Use it in a kernel build
298 300
299 301 Your build system should name the cross tools explicitly. A small Makefile might start like this:
300 302
301 303 ```make
302 304 TARGET := i686-elf
303 305 CC := $(TARGET)-gcc
304 306 CXX := $(TARGET)-g++
305 307 AS := $(TARGET)-as
306 308 LD := $(TARGET)-ld
307 309 AR := $(TARGET)-ar
308 310 OBJCOPY := $(TARGET)-objcopy
309 311
310 312 CFLAGS := -std=gnu23 -ffreestanding -O2 -Wall -Wextra
311 313 CFLAGS += -fno-stack-protector -fno-pie
312 314 ```
313 315
314 316 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.
315 317
316 318 For final linking, many kernels use the compiler driver rather than invoking `ld` directly. The driver knows where its target `libgcc` lives:
317 319
318 320 ```bash
319 321 "${TARGET}-gcc" \
320 322 -T linker.ld \
321 323 -ffreestanding \
322 324 -O2 \
323 325 -nostdlib \
324 326 boot.o kernel.o \
325 327 -lgcc \
326 328 -o kernel.elf
327 329 ```
328 330
329 331 `-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.
330 332
331 333 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.
332 334
333 335 ## What C++ support does—and does not—include
334 336
335 337 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.
336 338
337 339 Until you implement or deliberately port the required runtime pieces, kernel C++ code usually avoids exceptions, RTTI, threads, and the hosted standard library:
338 340
339 341 ```make
340 342 CXXFLAGS := $(CFLAGS) -fno-exceptions -fno-rtti
341 343 ```
342 344
343 345 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.
344 346
345 347 You can confirm that a freestanding C++ header is installed with a compile-only test:
346 348
347 349 ```bash
348 350 cat > sanity.cc <<'EOF'
349 351 #include <type_traits>
350 352
351 353 static_assert(std::is_unsigned_v<unsigned int>);
352 354 EOF
353 355
354 356 "${TARGET}-g++" \
355 357 -std=gnu++23 \
356 358 -ffreestanding \
357 359 -fno-exceptions \
358 360 -fno-rtti \
359 361 -c sanity.cc \
360 362 -o sanity-cxx.o
361 363 ```
362 364
363 365 ## Changing the target
364 366
365 367 For a 64-bit x86 kernel, you can rebuild with:
366 368
367 369 ```bash
368 370 export TARGET=x86_64-elf
369 371 ```
370 372
371 373 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.
372 374
373 375 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.
374 376
375 377 ## Optional: build a target-aware GDB
376 378
377 379 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.
378 380
379 381 Download and unpack the current official release:
380 382
381 383 ```bash
382 384 cd "$SRC"
383 385
384 386 curl -LO \
385 387 "https://sourceware.org/pub/gdb/releases/gdb-${GDB_VERSION}.tar.xz"
386 388
387 389 tar -xf "gdb-${GDB_VERSION}.tar.xz"
388 390 ```
389 391
390 392 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:
391 393
392 394 ```bash
393 395 mkdir -p "$BUILD/gdb"
394 396 cd "$BUILD/gdb"
395 397
396 398 "$SRC/gdb-${GDB_VERSION}/configure" \
397 399 --target="$TARGET" \
398 400 --prefix="$PREFIX" \
399 401 --disable-werror
400 402
401 403 make -j"$(nproc)" all-gdb
402 404 make install-gdb
403 405 ```
404 406
405 407 Verify it with:
406 408
407 409 ```bash
408 410 "${TARGET}-gdb" --version
409 411 ```
410 412
411 413 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.
412 414
413 415 ## Common failures and what they mean
414 416
415 417 ### `${TARGET}-as: command not found`
416 418
417 419 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.
418 420
419 421 ### `cannot compute suffix of object files`
420 422
421 423 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.
422 424
423 425 ### Missing `gnu/stubs-32.h` or another secondary-ABI header
424 426
425 427 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.
426 428
427 429 ### `fatal error: stdio.h: No such file or directory`
428 430
429 431 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.
430 432
431 433 ### Undefined references to `memcpy`, `memset`, or `memcmp`
432 434
433 435 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.
434 436
435 437 ### `cannot find -lgcc`
436 438
437 439 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`.
438 440
439 441 ### The final ELF file has the wrong architecture
440 442
441 443 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.
442 444
443 445 ## Make the setup reproducible
444 446
445 447 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.
446 448
447 449 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.
448 450
449 451 ## Official references
450 452
451 453 - [GCC releases](https://gcc.gnu.org/releases.html)
452 454 - [GCC installation prerequisites](https://gcc.gnu.org/install/prerequisites.html)
453 455 - [GCC configuration options](https://gcc.gnu.org/install/configure.html)
454 456 - [Building GCC and cross-compilers](https://gcc.gnu.org/install/build.html)
455 457 - [GCC on hosted and freestanding environments](https://gcc.gnu.org/onlinedocs/gcc/Standards.html)
456 458 - [GNU libstdc++ configuration](https://gcc.gnu.org/onlinedocs/libstdc%2B%2B/manual/configure.html)
457 459 - [GNU Binutils releases and documentation](https://sourceware.org/binutils/)
458 460 - [GDB downloads and documentation](https://sourceware.org/gdb/download/)
459 461
460 462 ## Go from a toolchain to a working kernel
461 463
462 464 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.
463 465
464 466 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.
465 467
466 468 [Get the 69-hour Kernel Development From Scratch Bundle](https://dragonzap.com/offer/kernel-development-from-scratch-69-hours?tracking=community)