Building the i686-elf-gcc Cross-Compiler

Post Stastics

  • This post has 2155 words.
  • Estimated read time is 10.26 minute(s).

In the first chapter of our “Write Your Own Linux-style Operating System” article, we introduced the tools we will use to build our small Linux-like operating system. One of those tools was listed almost casually:

i686-elf-gcc

That one tool deserves its own article. Setting up a cross compiler on a system can be a trying experience. So here we will walk the reader through setting up the i686-elf-gcc cross compiler.

For normal application programming, you usually use the compiler that came with your operating system. On Linux, that might be gcc. On macOS, that might be Apple Clang. On Windows, that might be MSVC, MinGW, or a compiler inside WSL.

For operating system development, that is not good enough.

We are not compiling a Linux program. We are not compiling a macOS program. We are not compiling a Windows program. We are compiling code for a tiny operating system that does not exist yet.

That means we need a compiler that targets a bare machine environment, not the host operating system.

For this series, our first target will be:

i686-elf

That means:

i686  = 32-bit Intel x86
elf   = Executable and Linkable Format

More importantly, it means:

not Linux
not macOS
not Windows
not glibc
not musl
not the host system's C runtime

We are building a freestanding compiler for our own kernel.


1 Why We Need a Cross-Compiler

Your normal system compiler is designed to build programs for the operating system you are already running.

For example, on Linux, a normal gcc command usually produces a Linux executable. It expects Linux headers, Linux system calls, Linux linker behavior, Linux startup files, and the Linux C runtime.

That is exactly what we do not want.

A kernel is different. A kernel does not start inside main() with an operating system already running beneath it. A kernel is the operating system.

So when we compile kernel code, we must avoid accidentally pulling in:

host operating system headers
host startup files
host libraries
host ABI assumptions
host linker defaults

This is why we build a dedicated compiler named:

i686-elf-gcc

When we use i686-elf-gcc, we are telling the toolchain:

Build 32-bit x86 ELF code, but do not assume Linux, macOS, or Windows.

That is exactly what we need for a small 32-bit hobby kernel.


2 What We Are Going To Build

We will build two major pieces:

binutils
gcc

GNU binutils gives us low-level binary tools such as:

i686-elf-as
i686-elf-ld
i686-elf-objdump
i686-elf-objcopy
i686-elf-readelf

GCC gives us:

i686-elf-gcc

Together, these tools allow us to assemble, compile, link, inspect, and transform kernel binaries without using the host operating systemโ€™s normal compiler target.

We will install everything under:

$HOME/opt/cross

That keeps the cross-compiler separate from the system compiler.

Do not install this over your system GCC.

Do not replace /usr/bin/gcc.

Do not use sudo make install for this chapter.

Our cross-compiler belongs in our home directory.


3 Versions Used In This Series

For this series, we will use these versions:

export BINUTILS_VERSION=2.46.1
export GCC_VERSION=15.3.0

We are intentionally using GCC 15.3.0 instead of chasing the newest possible major release. Newest is not always the best choice for a tutorial. A recent maintenance release is usually a better teaching target because it is current, supported, and less likely to surprise readers with brand-new behavior.

Set the common variables now:

export TARGET=i686-elf
export PREFIX="$HOME/opt/cross"
export PATH="$PREFIX/bin:$PATH"
export BINUTILS_VERSION=2.46.1
export GCC_VERSION=15.3.0

You may place these in your shell startup file later, but for now we will define them manually while building.


4 Linux Setup

These commands are for normal Linux distributions.

On Debian, Ubuntu, Linux Mint, Pop!_OS, and similar systems:

sudo apt update

sudo apt install -y \
    build-essential \
    bison \
    flex \
    libgmp3-dev \
    libmpfr-dev \
    libmpc-dev \
    texinfo \
    wget \
    curl \
    xz-utils \
    m4 \
    gawk \
    make

On Fedora:

sudo dnf groupinstall -y "Development Tools"

sudo dnf install -y \
    gcc \
    gcc-c++ \
    make \
    bison \
    flex \
    gmp-devel \
    mpfr-devel \
    libmpc-devel \
    texinfo \
    wget \
    curl \
    xz \
    m4 \
    gawk

On Arch Linux or Manjaro:

sudo pacman -Syu

sudo pacman -S --needed \
    base-devel \
    bison \
    flex \
    gmp \
    mpfr \
    libmpc \
    texinfo \
    wget \
    curl \
    xz \
    m4 \
    gawk

Then set the build variables:

export TARGET=i686-elf
export PREFIX="$HOME/opt/cross"
export PATH="$PREFIX/bin:$PATH"
export BINUTILS_VERSION=2.46.1
export GCC_VERSION=15.3.0

export JOBS="$(nproc)"
export MAKE=make

5 macOS Setup

On macOS, we need Appleโ€™s command-line tools and Homebrew.

First install Appleโ€™s command-line tools:

xcode-select --install

Then install Homebrew if you do not already have it. Homebrew provides the build tools and libraries we need.

Once Homebrew is installed, run:

brew update

brew install \
    gmp \
    mpfr \
    libmpc \
    isl \
    texinfo \
    wget \
    xz \
    make

Homebrew installs GNU Make as gmake, not always as make, so we will use gmake explicitly on macOS.

Set the build variables:

export TARGET=i686-elf
export PREFIX="$HOME/opt/cross"
export PATH="$PREFIX/bin:$PATH"
export BINUTILS_VERSION=2.46.1
export GCC_VERSION=15.3.0

export JOBS="$(sysctl -n hw.ncpu)"
export MAKE=gmake

On some macOS systems, Homebrewโ€™s texinfo is not automatically placed first in the shell path. If the build later complains about makeinfo, add this:

export PATH="$(brew --prefix texinfo)/bin:$PATH"

Then continue with the common build steps below.


6 WSL Setup

WSL means Windows Subsystem for Linux. It lets you run a real Linux userland on Windows.

For this series, WSL is much better than trying to build the toolchain directly with native Windows tools. The commands are almost identical to normal Ubuntu Linux.

From an Administrator PowerShell window, install Ubuntu if WSL is not already installed:

wsl --install -d Ubuntu

Restart if Windows asks you to.

Then open Ubuntu from the Start Menu and update the package list:

sudo apt update
sudo apt upgrade -y

Install the build dependencies:

sudo apt install -y \
    build-essential \
    bison \
    flex \
    libgmp3-dev \
    libmpfr-dev \
    libmpc-dev \
    texinfo \
    wget \
    curl \
    xz-utils \
    m4 \
    gawk \
    make

Set the build variables:

export TARGET=i686-elf
export PREFIX="$HOME/opt/cross"
export PATH="$PREFIX/bin:$PATH"
export BINUTILS_VERSION=2.46.1
export GCC_VERSION=15.3.0

export JOBS="$(nproc)"
export MAKE=make

Important WSL note:

Keep your OS project inside the Linux filesystem, not under /mnt/c.

Good:

/home/yourname/projects/toyix

Avoid:

/mnt/c/Users/yourname/projects/toyix

The Windows-mounted filesystem is slower and can cause annoying permission, path, and performance issues during builds.


7 Create The Source And Build Directories

The rest of this chapter is the same for Linux, macOS, and WSL.

Create a working directory:

mkdir -p "$HOME/src"
cd "$HOME/src"

Download binutils and GCC:

wget "https://ftp.gnu.org/gnu/binutils/binutils-${BINUTILS_VERSION}.tar.xz"
wget "https://ftp.gnu.org/gnu/gcc/gcc-${GCC_VERSION}/gcc-${GCC_VERSION}.tar.xz"

Extract them:

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

Enter the GCC source directory and download GCCโ€™s in-tree prerequisites:

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

Then return to the source directory:

cd "$HOME/src"

The download_prerequisites step helps avoid problems with missing or incompatible GMP, MPFR, MPC, and ISL libraries.

Even though we installed many of those libraries through the package manager, using GCCโ€™s bundled prerequisite setup usually makes the build more predictable.


8 Build binutils

Never build binutils directly inside the source directory.

Create a separate build directory:

mkdir -p "$HOME/src/build-binutils"
cd "$HOME/src/build-binutils"

Configure binutils for our target:

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

Build and install:

$MAKE -j"$JOBS"
$MAKE install

After this finishes, check that the target assembler and linker exist:

which i686-elf-as
which i686-elf-ld

You should see paths similar to:

/home/yourname/opt/cross/bin/i686-elf-as
/home/yourname/opt/cross/bin/i686-elf-ld

If which cannot find them, your PATH is probably missing:

export PATH="$HOME/opt/cross/bin:$PATH"

9 Build GCC

Now build the cross-compiler.

Again, do not build inside the GCC source directory.

Create a separate build directory:

mkdir -p "$HOME/src/build-gcc"
cd "$HOME/src/build-gcc"

Configure GCC:

"../gcc-${GCC_VERSION}/configure" \
    --target="$TARGET" \
    --prefix="$PREFIX" \
    --disable-nls \
    --enable-languages=c \
    --without-headers \
    --disable-multilib

The important options are:

--target=i686-elf

Build a compiler that targets 32-bit x86 ELF.

--prefix=$HOME/opt/cross

Install the cross-compiler into our home directory.

--enable-languages=c

Build the C compiler only.

--without-headers

Tell GCC that the target has no operating system headers yet.

--disable-multilib

Avoid building extra library variants that we do not need for this series.

Now build GCC itself:

$MAKE -j"$JOBS" all-gcc
$MAKE -j"$JOBS" all-target-libgcc

Install it:

$MAKE install-gcc
$MAKE install-target-libgcc

When this completes, the cross-compiler should be available:

which i686-elf-gcc

Expected output:

/home/yourname/opt/cross/bin/i686-elf-gcc

Check the compiler version:

i686-elf-gcc --version

Check the target:

i686-elf-gcc -dumpmachine

Expected output:

i686-elf

That output matters. If it says something like this:

x86_64-linux-gnu

then you are using the wrong compiler.


10 Make The PATH Permanent

Right now, the cross-compiler is available only in the current terminal session.

Add it permanently to your shell startup file.

For Bash:

echo 'export PATH="$HOME/opt/cross/bin:$PATH"' >> "$HOME/.bashrc"

Then reload Bash:

source "$HOME/.bashrc"

For Zsh:

echo 'export PATH="$HOME/opt/cross/bin:$PATH"' >> "$HOME/.zshrc"

Then reload Zsh:

source "$HOME/.zshrc"

On macOS, Zsh is usually the default shell.

On most Linux and WSL systems, Bash is usually the default shell.

You can check your shell with:

echo "$SHELL"

11 Smoke Test The Cross-Compiler

Now we will compile a tiny freestanding C file.

Create a temporary test directory:

mkdir -p "$HOME/src/cross-test"
cd "$HOME/src/cross-test"

Create test.c:

cat > test.c <<'EOF'
int kernel_add(int a, int b) {
    return a + b;
}
EOF

Compile it:

i686-elf-gcc -ffreestanding -m32 -c test.c -o test.o

Inspect the object file:

i686-elf-objdump -d test.o

You should see disassembly for a small function.

Now check the object format:

i686-elf-readelf -h test.o

Look for lines similar to:

Class:                             ELF32
Machine:                           Intel 80386

This confirms that we are producing 32-bit x86 ELF output.

That is exactly what we need for the first version of our kernel.


12 Common Problems

Problem: i686-elf-gcc: command not found

Your cross-compiler is probably installed, but your shell cannot find it.

Run:

export PATH="$HOME/opt/cross/bin:$PATH"

Then try again:

i686-elf-gcc --version

If that works, add the path to .bashrc or .zshrc.


Problem: configure: error: C compiler cannot create executables

This usually means your host build tools are missing or broken.

On Ubuntu or WSL, run:

sudo apt install -y build-essential

On Fedora, run:

sudo dnf groupinstall -y "Development Tools"

On Arch, run:

sudo pacman -S --needed base-devel

On macOS, make sure the command-line tools are installed:

xcode-select --install

Problem: GCC cannot find GMP, MPFR, or MPC

Make sure you ran:

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

Then delete the failed GCC build directory and configure again:

rm -rf "$HOME/src/build-gcc"
mkdir -p "$HOME/src/build-gcc"
cd "$HOME/src/build-gcc"

Then rerun the GCC configure and build steps.


Problem: makeinfo is missing

Install Texinfo.

On Ubuntu or WSL:

sudo apt install -y texinfo

On Fedora:

sudo dnf install -y texinfo

On Arch:

sudo pacman -S --needed texinfo

On macOS:

brew install texinfo
export PATH="$(brew --prefix texinfo)/bin:$PATH"

Then retry the failed build step.


Problem: The build fails after changing versions

Do not reuse old build directories after changing binutils or GCC versions.

Remove the old build directories:

rm -rf "$HOME/src/build-binutils"
rm -rf "$HOME/src/build-gcc"

Then repeat the configure and build steps.

The source directories may remain, but the build directories should be recreated cleanly.


13 Do Not Use The Host Compiler For Kernel Code

From this point onward, kernel code should be compiled with:

i686-elf-gcc

not:

gcc
clang
cc
x86_64-linux-gnu-gcc

This distinction is important enough that we will encode it into our Makefiles.

A later Makefile might contain something like:

TARGET := i686-elf

CC := $(TARGET)-gcc
AS := $(TARGET)-as
LD := $(TARGET)-ld
OBJCOPY := $(TARGET)-objcopy
OBJDUMP := $(TARGET)-objdump
READELF := $(TARGET)-readelf

That way, the project itself remembers which compiler it is supposed to use.


14 Where This Fits In Our OS Project

At this point, we have not built a kernel yet.

That is fine.

This chapter was about preparing the foundation.

We now have:

i686-elf-as
i686-elf-ld
i686-elf-gcc
i686-elf-objcopy
i686-elf-objdump
i686-elf-readelf

These are the tools we will use to build the first real kernel image.

In the next chapter, we will create the initial project structure and write the smallest possible kernel entry point. We will still use GRUB to load the kernel, but the kernel itself will be compiled with our new freestanding cross-compiler.

Before continuing, verify these commands work:

i686-elf-gcc --version
i686-elf-gcc -dumpmachine
i686-elf-as --version
i686-elf-ld --version

The most important one is:

i686-elf-gcc -dumpmachine

It must print:

i686-elf

Once that works, our toolchain is ready.

Leave a Reply

Your email address will not be published. Required fields are marked *