Article revision Version 5 of 5 Current

Build a GCC Cross-Compiler for Kernel Development

Edited by @nibblebits Sep 8, 2026 at 13:30
View current article
Published change

Missed a package install, clarified further that every command must be copy and pasted

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

Snapshot

Article at version 5

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 \
   libgmp-dev \
  libmpfr-dev \
 libmpc-dev

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.

Now in your terminal window paste the below code:

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"

You're terminal will need to be configured by pasting the above code before you continue. 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.

Now follow along pasting all the commands in this article

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 5

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