Article revision Version 4 of 4 Current

Build a GCC Cross-Compiler for Kernel Development

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

more clarification

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

Snapshot

Article at version 4

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

This is just an example of how you're kernels makefile might look to target the cross compiler you built and not something to copy and paste

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 4

+2−0
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 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 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 **This is just an example of how you're kernels makefile might look to target the cross compiler you built and not something to copy and paste**
302
301 303 Your build system should name the cross tools explicitly. A small Makefile might start like this:
302 304
303 305 ```make
304 306 TARGET := i686-elf
305 307 CC := $(TARGET)-gcc
306 308 CXX := $(TARGET)-g++
307 309 AS := $(TARGET)-as
308 310 LD := $(TARGET)-ld
309 311 AR := $(TARGET)-ar
310 312 OBJCOPY := $(TARGET)-objcopy
311 313
312 314 CFLAGS := -std=gnu23 -ffreestanding -O2 -Wall -Wextra
313 315 CFLAGS += -fno-stack-protector -fno-pie
314 316 ```
315 317
316 318 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 319
318 320 For final linking, many kernels use the compiler driver rather than invoking `ld` directly. The driver knows where its target `libgcc` lives:
319 321
320 322 ```bash
321 323 "${TARGET}-gcc" \
322 324 -T linker.ld \
323 325 -ffreestanding \
324 326 -O2 \
325 327 -nostdlib \
326 328 boot.o kernel.o \
327 329 -lgcc \
328 330 -o kernel.elf
329 331 ```
330 332
331 333 `-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 334
333 335 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 336
335 337 ## What C++ support does—and does not—include
336 338
337 339 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 340
339 341 Until you implement or deliberately port the required runtime pieces, kernel C++ code usually avoids exceptions, RTTI, threads, and the hosted standard library:
340 342
341 343 ```make
342 344 CXXFLAGS := $(CFLAGS) -fno-exceptions -fno-rtti
343 345 ```
344 346
345 347 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 348
347 349 You can confirm that a freestanding C++ header is installed with a compile-only test:
348 350
349 351 ```bash
350 352 cat > sanity.cc <<'EOF'
351 353 #include <type_traits>
352 354
353 355 static_assert(std::is_unsigned_v<unsigned int>);
354 356 EOF
355 357
356 358 "${TARGET}-g++" \
357 359 -std=gnu++23 \
358 360 -ffreestanding \
359 361 -fno-exceptions \
360 362 -fno-rtti \
361 363 -c sanity.cc \
362 364 -o sanity-cxx.o
363 365 ```
364 366
365 367 ## Changing the target
366 368
367 369 For a 64-bit x86 kernel, you can rebuild with:
368 370
369 371 ```bash
370 372 export TARGET=x86_64-elf
371 373 ```
372 374
373 375 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 376
375 377 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 378
377 379 ## Optional: build a target-aware GDB
378 380
379 381 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 382
381 383 Download and unpack the current official release:
382 384
383 385 ```bash
384 386 cd "$SRC"
385 387
386 388 curl -LO \
387 389 "https://sourceware.org/pub/gdb/releases/gdb-${GDB_VERSION}.tar.xz"
388 390
389 391 tar -xf "gdb-${GDB_VERSION}.tar.xz"
390 392 ```
391 393
392 394 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 395
394 396 ```bash
395 397 mkdir -p "$BUILD/gdb"
396 398 cd "$BUILD/gdb"
397 399
398 400 "$SRC/gdb-${GDB_VERSION}/configure" \
399 401 --target="$TARGET" \
400 402 --prefix="$PREFIX" \
401 403 --disable-werror
402 404
403 405 make -j"$(nproc)" all-gdb
404 406 make install-gdb
405 407 ```
406 408
407 409 Verify it with:
408 410
409 411 ```bash
410 412 "${TARGET}-gdb" --version
411 413 ```
412 414
413 415 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 416
415 417 ## Common failures and what they mean
416 418
417 419 ### `${TARGET}-as: command not found`
418 420
419 421 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 422
421 423 ### `cannot compute suffix of object files`
422 424
423 425 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 426
425 427 ### Missing `gnu/stubs-32.h` or another secondary-ABI header
426 428
427 429 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 430
429 431 ### `fatal error: stdio.h: No such file or directory`
430 432
431 433 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 434
433 435 ### Undefined references to `memcpy`, `memset`, or `memcmp`
434 436
435 437 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 438
437 439 ### `cannot find -lgcc`
438 440
439 441 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 442
441 443 ### The final ELF file has the wrong architecture
442 444
443 445 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 446
445 447 ## Make the setup reproducible
446 448
447 449 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 450
449 451 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 452
451 453 ## Official references
452 454
453 455 - [GCC releases](https://gcc.gnu.org/releases.html)
454 456 - [GCC installation prerequisites](https://gcc.gnu.org/install/prerequisites.html)
455 457 - [GCC configuration options](https://gcc.gnu.org/install/configure.html)
456 458 - [Building GCC and cross-compilers](https://gcc.gnu.org/install/build.html)
457 459 - [GCC on hosted and freestanding environments](https://gcc.gnu.org/onlinedocs/gcc/Standards.html)
458 460 - [GNU libstdc++ configuration](https://gcc.gnu.org/onlinedocs/libstdc%2B%2B/manual/configure.html)
459 461 - [GNU Binutils releases and documentation](https://sourceware.org/binutils/)
460 462 - [GDB downloads and documentation](https://sourceware.org/gdb/download/)
461 463
462 464 ## Go from a toolchain to a working kernel
463 465
464 466 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 467
466 468 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 469
468 470 [Get the 69-hour Kernel Development From Scratch Bundle](https://dragonzap.com/offer/kernel-development-from-scratch-69-hours?tracking=community)