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:
- Boot Sector
- Interrupts
- Memory
- Stack
- IF-ELSE
- Functions
- Memory Segmentation
- Reading Data from Disk
- Entering Protected Mode 32bits
- Compilation, Linking, Stack Management, and Variables in C
- Pointers
- Kernel
- EntryPoint
- Gmake
- Gmake wildcards
- I/O Hardware
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:
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.
The rest of the steps can be performed as a regular user. We download the latest version of
BinUtils
and
GCC
:
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:
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:
/home/kr0m/opt/cross/bin/i686-elf-as
We compile 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:
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:
i686-elf
We make the compiler and binutils available in the 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:
#$@ : 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