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.