This page looks best with JavaScript enabled

MercuryOS Cross-compiler and BinUtils

 ·  🎃 kr0m

Compiling 32-bit source code on a 64-bit system causes many problems. Until now, it has been indifferent to us since almost all the code was ASM, but continuing to develop like this is a source of problems in the future. If we use the compiler of our system, it will make many specific assumptions about our OS/Architecture, and it will also use its own libraries and headers. To avoid all this, we will install a native 32-bit cross-compiler, and we will also compile some 32-bit binutils.

Before starting, it is recommended that you read these previous articles:


When compiling a kernel instead of a user-space program, we must compile with certain options:

  • ffreestanding: Certain functions will be implemented by ourselves, such as memset, memcpy, memcmp, and memmove.
  • fno-exceptions, -fno-rtti (C++): We disable C++ options that do not work out-of-the-box in kernel-land.
  • nostdlib: It is equivalent to -nostartfiles -nodefaultlibs, we do not start files (crt0.o, crti.o, crtn.o), since they are only used in user-space programs, we also do not want the default libraries such as libc.
  • lgcc: By enabling nostdlib, we have also removed the gcc library, but this is necessary for the compiler, so we enable it, it must be the last compilation parameter.
    • GCC generates calls to routines in this library automatically, whenever it needs to perform some operation that is too complicated to emit inline code for.

We should never use the linker manually unless it is strictly necessary, and if we do, we must use the one we have compiled in our cross-tools. The same advice applies to the rest of the tools: readelf, objcopy, objdump.

The compiler will generate code and look for certain libraries based on the TargetTriplet, which describes the platform on which the code will run. We can see the TargetTriplet with the command:

gcc -dumpmachine

x86_64-portbld-freebsd12.1

The compiler that comes with FreeBSD will generate code for this TargetTriplet, but we need to generate it for yourOsTargetArch-yourChosenFormat-none, the none at the end indicates that it should not use any system library, we want our kernel to be as standalone as possible.

Later, when we have system libraries (/lib, /include), we will compile user-space programs with another compiler to dynamically link the binaries with those libraries, the compiler will be: yourOsTargetArch-yourChosenFormat-yourOs.

In this way, we will have two compilers:

  • yourOsTargetArch-yourChosenFormat-none: Kernel compiler.
  • yourOsTargetArch-yourChosenFormat-yourOs: User-space program compiler.

We install texinfo, a dependency necessary to compile our gcc.

pkg install texinfo

The rest of the steps can be performed as a regular user. We download the latest version of BinUtils and GCC :

mkdir -p crossCompiler/binutils
mkdir crossCompiler/gcc
cd crossCompiler/binutils
fetch https://ftp.gnu.org/gnu/binutils/binutils-2.34.tar.xz
tar -Jxvf binutils-2.34.tar.xz
cd ..
cd gcc
fetch https://ftp.gnu.org/gnu/gcc/gcc-10.1.0/gcc-10.1.0.tar.xz
tar -Jxvf gcc-10.1.0.tar.xz

The entire crossCompiler will be installed under $HOME/opt/cross so that we do not create conflicts with the compilation environment of our OS:

mkdir -p $HOME/opt/cross
export PREFIX="$HOME/opt/cross"
export TARGET=i686-elf
export PATH="$PREFIX/bin:$PATH"
cd $HOME/crossCompiler/binutils
mkdir build-binutils
cd build-binutils
../binutils-2.34/configure –target=$TARGET –prefix="$PREFIX" –with-sysroot –disable-nls –disable-werror
gmake -j8
gmake install

We can see some interesting configuration options:

  • --disable-nls: Tells binutils not to include native language support, it is optional but reduces compilation dependencies, we will only support English :)
  • --with-sysroot: Tells binutils to use an empty sysroot

With the following command, we check that the ASM compiler is available in the path:

which – $TARGET-as || echo $TARGET-as is not in the PATH
/home/kr0m/opt/cross/bin/i686-elf-as

We compile gcc:

cd $HOME/crossCompiler/gcc
mkdir build-gcc
cd build-gcc
../gcc-10.1.0/configure –target=$TARGET –prefix="$PREFIX" –disable-nls –enable-languages=c,c++ –without-headers
gmake -j8 all-gcc
gmake -j8 all-target-libgcc
gmake install-gcc
gmake install-target-libgcc

NOTE: gcc depends on libgcc, a low-level library that the compiler expects to be available when compiling code.

The options with which gcc has been compiled are:

  • --disable-nls: Tells binutils not to include native language support, it is optional but reduces compilation dependencies, we will only support English :)
  • --without-headers: We tell gcc not to rely on any C library in the system
  • --enable-languages: We indicate the programming languages that it should support

We can obtain the compiler version with:

/home/kr0m/opt/cross/bin/i686-elf-gcc -v

Using built-in specs.  
COLLECT_GCC=/home/kr0m/opt/cross/bin/i686-elf-gcc  
COLLECT_LTO_WRAPPER=/usr/home/kr0m/opt/cross/bin/../libexec/gcc/i686-elf/10.1.0/lto-wrapper  
Target: i686-elf  
Configured with: ../gcc-10.1.0/configure --target=i686-elf --prefix=/home/kr0m/opt/cross --disable-nls --enable-languages=c,c++ --without-headers  
Thread model: single  
Supported LTO compression algorithms: zlib zstd  
gcc version 10.1.0 (GCC) 

We check the TargetTriplet of the compiler:

/home/kr0m/opt/cross/bin/i686-elf-gcc -dumpmachine

i686-elf

We make the compiler and binutils available in the path:

export PATH="$HOME/opt/cross/bin:$PATH"
vi .bashrc

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

This compiler is freestanding, which means that it will not use libraries such as libc, only a very basic subset: float.h, iso646.h, limits.h, stdalign.h, stdarg.h, stdbool.h, stddef.h, stdint.h, and stdnoreturn.h, everything is just type definitions without .c files.

Our Makefile will also be affected:

vi Makefile

#$@ : Fichero a generar
#$^ : Todas las dependencias
#$< : Primera dependencia

# Automatically generate lists of sources using wildcards.
C_SOURCES = $(wildcard kernel/*.c drivers/*.c)
HEADERS = $(wildcard kernel/*.h drivers/*.h)

# TODO : Make sources dep on all header files .
# Convert the *.c filenames to *.o to give a list of object files to build
OBJ = ${C_SOURCES:.c=.o}

# Run Qemu to simulate booting of our code.
run : all
    #qemu-system-x86_64 os-image
    qemu-system-i386 os-image

# Run Qemu to simulate booting of our code with debugging session.
# gdb -ex "set architecture i386:x86-64" -ex "set disassembly-flavor intel" -ex "target remote localhost:1234" -ex "symbol-file kernel/kernel.elf"
# gdb -ex "set architecture i386" -ex "set disassembly-flavor intel" -ex "target remote localhost:1234" -ex "symbol-file kernel/kernel.elf"
debug : all kernel/kernel.elf
    #qemu-system-x86_64 os-image -s -S
    qemu-system-i386 os-image -s -S

# Used for debugging purposes
kernel/kernel.elf: boot/kernel_entry.o ${OBJ}
    i686-elf-ld -o $@ -Ttext 0x1000 $^

# Defaul build target
all: os-image

# This is the actual disk image that the computer loads
# which is the combination of our compiled bootsector and kernel
os-image : boot/boot_sect.bin kernel/kernel.bin
    cat $^ > os-image

boot/boot_sect.bin : boot/boot_sect.asm
    nasm -f bin $< -o $@

# This builds the binary of our kernel from two object files:
# - the kernel_entry, which jumps to main () in our kernel
# - the compiled C kernel and drivers
kernel/kernel.bin : kernel/kernel_entry.o ${OBJ}
    i686-elf-ld -o $@ -Ttext 0x1000 $^ --oformat binary

# Generic rule for compiling C code to an object file
# For simplicity , we C files depend on all header files.
%.o : %.c ${HEADERS}
    #cc -ggdb -m32 -ffreestanding -c $< -o $@
    i686-elf-gcc -ggdb -ffreestanding -fno-exceptions -nostdlib -c $< -o $@ -lgcc

# Assemble the kernel_entry.
%.o : %.asm
    nasm -f elf $< -o $@

%.bin : %.asm
    nasm -f bin $< -o $@

clean :
    rm -fr *.bin *.dis *.o os-image
    rm -fr kernel/*.o kernel/*.elf boot/*.bin drivers/*.o
If you liked the article, you can treat me to a RedBull here