Hi there! If you’ve ever wanted to build your own operating system, you’ll quickly realise that your trusty Mac isn’t always on board with your grand plans. That’s where cross-compilers come in—they’re like translators for your code, helping it speak the language of your target platform. This guide is for developers who are new to OS development but already comfortable with coding.
I’m sharing my journey here so you can avoid the same pitfalls and maybe even save a few hours of frustration. My motivation? I’m working on a custom RTOS called IrisOS (more on that in future posts), and I wanted a reliable, freestanding toolchain to call my own. If you’re here, you probably want that too!
- Apple Silicon Mac
zsh
shell- Homebrew installed
Why Cross-Compilation?
When you’re writing code for a different platform than the one you’re developing on, you need a way to test it. Your system compiler won’t cut it—it only knows how to compile code for your current platform. That’s where the cross-compiler comes in. It lets you compile code for your target platform, even if it’s completely different from your Mac. In this tutorial, I’ll show you how to build a GCC cross-compiler for a custom OS (IrisOS, eventually).
Creating a GCC cross-compiler often comes with its fair share of errors, and OS X can be especially tricky. Part of this is due to the zsh
shell, which isn’t always supported for GCC builds. But don’t worry—we’ll make it work!
Project setup
First, let’s get organised. I like to build in a src
folder and assemble the compiler in its own dedicated folder. For this tutorial, that’ll be the aarch64-elf
folder (but you can choose any target you like).
Don’t put this folder on your iCloud drive—trust me, it leads to all sorts of build errors. I recommend building in a dedicated Code
folder in your home directory.
mkdir aarch64-elf aarch64-elf-src
cd aarch64-elf-src
Next, we should ascertain our desired versions of binutils, gdb and gcc. These are the building blocks for our cross-compiler. gdb
is optional, but I include it for device-agnostic debugging. We need the source files—not the brew
packages—so our cross-compiler can be truly freestanding.
Your folder structure should look something like this (with your downloaded versions):
├── aarch64-elf
└── aarch64-elf-src
├── binutils-2.44.tar
├── gcc-14.2.0.tar
└── gdb-16.2.tar
After extracting, I like to rename the folders to remove the version numbers (so everything works with the command we use later):
├── aarch64-elf
└── aarch64-elf-src
├── binutils
├── binutils-2.44.tar
├── gcc
├── gcc-14.2.0.tar
├── gdb
└── gdb-16.2.tar
Now, let’s set up our build variables. clang
isn’t enough to build gcc
, and I’ve had no luck with most things from xcode-select
, so we’ll use the brew
version of gcc
.
Environment Setup
Installing Prerequisites
NOTE: You will still need xcode-select
, to get it use in terminal:
xcode-select --install
You will likely have less issues if you download Xcode from the App Store, too!
If you don’t have Homebrew installed, get it with:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
To get gcc
, run:
brew install gcc
You’ll probably need to install some dependencies, so make sure you do that too.
# Modify this command to have your correct gcc compiler versions.
export CXX=$(brew --prefix gcc)/bin/g++-14
export CC=$(brew --prefix gcc)/bin/gcc-14
export CPP=$(brew --prefix gcc)/bin/cpp-14
export LD=$(brew --prefix gcc)/bin/gcc-14
This ensures our new cross-compiler uses the brew
version of gcc
, not the system clang
. These commands only affect your current terminal session. We’ll also need to tell the tools where our build path is:
export PREFIX="$HOME/path/to/aarch64-elf"
export TARGET=aarch64-elf
export PATH="$PREFIX/bin:$PATH"
Great job! Now we have a home for our new cross-compiler. (Have you thought of a name for it yet?)
Building
First, let’s make sure we have all the prerequisites installed. We’ll ask brew
to do the heavy lifting:
brew install cmake gmp libmpc mpfr isl libiconv
Now, let’s start building. We’ll begin with binutils
, which gives us the assembler, disassembler, and other tools for our $TARGET
. First, create a build
folder:
mkdir build-binutils
cd build-binutils
../binutils/configure --target=$TARGET --prefix="$PREFIX" --with-sysroot \
--enable-interwork --enable-multilib --disable-nls --disable-werror
Then, build and install:
gmake -j $(sysctl -n hw.logicalcpu)
gmake -j $(sysctl -n hw.logicalcpu) install
Next, build gdb
:
cd ..
mkdir build-gdb
cd build-gdb
../gdb/configure --target=$TARGET --prefix="$PREFIX" --disable-werror \
--with-gmp="$(brew --prefix gmp)" \
--with-mpfr="$(brew --prefix mpfr)" \
--with-libiconv-prefix="$(brew --prefix libiconv)"
gmake -j $(sysctl -n hw.logicalcpu) all-gdb
Finally, build gcc
:
mkdir build-gcc
cd ../build-gcc
../gcc/configure --target=$TARGET --prefix="$PREFIX" --disable-nls --enable-languages=c,c++ --without-headers --disable-hosted-libstdcxx --enable-interwork --enable-multilib --disable-gcov \
--with-gmp="$(brew --prefix gmp)" \
--with-mpc="$(brew --prefix libmpc)" \
--with-mpfr="$(brew --prefix mpfr)" \
--with-isl="$(brew --prefix isl)" \
--with-libiconv-prefix="$(brew --prefix libiconv)"
gmake all-gcc
gmake all-target-libgcc
gmake all-target-libstdc++-v3
gmake install-gcc
gmake install-target-libgcc
gmake install-target-libstdc++-v3
Troubleshooting Tips
If you’ve made it this far, congratulations! But if you’re like me, you’ve probably hit a few snags. Here are some common issues and how to tackle them:
-
Build Errors on iCloud:
Don’t build in an iCloud-synced folder. It’s a recipe for mysterious errors. -
Missing Dependencies:
If something fails, check if you’ve installed all the prerequisites. Sometimes, a quickbrew install
is all you need. The missing dependencies will be required by ourbrew
installs!
Summary & Reflection
Building a cross-compiler on macOS isn’t always straightforward, but it’s a great way to learn about toolchains, dependencies, and the quirks of different platforms. I’ve lost more hours than I’d like to admit to cryptic errors and conflicting instructions, but each time, I’ve come out a bit wiser. If you’re new to OS development, this process will teach you a lot about how compilers and toolchains work—skills that are valuable in many areas of software development.
What’s Next?
This is just the beginning! With your cross-compiler in hand, you’re ready to start building your own OS (or at least, the first few bytes of it). In future posts, I’ll dive into the development of IrisOS, my custom RTOS. I won’t be writing a tutorial for every step, but I’ll share the highlights, challenges, and lessons learned along the way.
If you found this guide helpful (or even if you didn’t!), I’d love to hear from you. Drop me a line at my contact page or LinkedIn—I’m always up for a chat about OS development, cross-compilers, or anything else tech-related!
Happy coding, and may your builds be ever successful! 🚀
As a software engineer with a strong foundation in C++, Python, and systems programming, I’m currently developing IrisOS (ARMv8), my own custom operating system. This project demonstrates my ability to build robust, low-level infrastructure and my commitment to learning by doing.
If you’re looking for someone who combines technical depth with practical leadership and a passion for systems, let’s connect.