Article revision Version 3 of 4

Build a GCC Cross-Compiler for Kernel Development

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

fixed mistake

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

Snapshot

Article at version 3

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 in the Ubuntu or Linux terminal:

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. These export commands are also run in the Ubuntu terminal.

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 3

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