Compilar código fuente de 32bits en un sistema de 64bits accarea muchos problemas, hasta ahora nos ha sido indiferente ya que casi todo el código era ASM, pero seguir desarrollando así es una fuente de problemas en el futuro. Si utilizamos el compilador de nuestro sistema, este hará muchas suposiciones específicas a nuestro SO/Arquitectura, además utilizará las librerias y cabeceras propias. Para evitar todo esto vamos a instalar un cross-compiler de 32bits nativo, se acabaron las opciones como -m32 en nuestro gcc, además compilaremos también unas binutils de 32bits.
Antes de comenzar es recomendable que leas estos artículos anteriores:
- Boot Sector
- Interrupciones
- Memoria
- Pila
- IF-ELSE
- Funciones
- Segmentación de memoria
- Lectura de datos desde disco
- Entrando a modo protegido 32bits
- Compilación, linkado, gestión de la pila y variables en C
- Punteros
- Kernel
- EntryPoint
- Gmake
- Gmake wildcards
- I/O Hardware
Al compilar un kernel y no un programa user-space debemos compilar con ciertas opciones:
- ffreestanding: Ciertas funciones serán implementadas por nosotros mismo, memset, memcpy, memcmp y memmove
- fno-exceptions, -fno-rtti (C++): Deshabilitamos opciones de C++ que no funcionan out-of-the-box en kernel-land
- nostdlib: Es el equivalente a -nostartfiles -nodefaultlibs, no iniciamos ficheros(crt0.o, crti.o, crtn.o), ya que solo se utilizan en programas user-space, tampoco queremos las librerías por defecto como libc
- lgcc: Al habilitar nostdlib hemos eliminado también la librería gcc pero esta es necesaria para el compilador así que la habilitamos, debe ser el último parámetro de compilación
- 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
No debemos utilizar nunca el linker manualmente a no ser que sea estrictamente necesario y si lo hacemos debemos utilizar el que hemos compilado en nuestras cross-tools. El mismo consejo es aplicable para el resto de tools: readelf, objcopy, objdump.
El compilador generará código y buscará ciertas librerías basándose en el TargetTriplet, este describe la plataforma sobre la que correrá el código. Podemos ver el TargetTriplet con el comando:
x86_64-portbld-freebsd12.1
El compilador que trae FreeBSD generará código para este TargetTriplet, pero nosotros necesitamos generarlo para yourOsTargetArch-yourChosenFormat-none, el none del final indica que no debe utilizar ninguna librería del sistema, queremos que nuestro kernel sea lo mas standalone posible.
Mas tarde cuando tengamos librerías del sistema(/lib, /include) compilaremos los programas de user-space con otro compilador para que enlace de forma dinámica los binarios con dichas librerías, el compilador será: yourOsTargetArch-yourChosenFormat-yourOs
De este modo tendremos dos compiladores:
- yourOsTargetArch-yourChosenFormat-none: Compilador del kernel
- yourOsTargetArch-yourChosenFormat-yourOs: Compilador de los programas userspace
Instalamos texinfo, una dependencia necesaria para compilar nuestro gcc.
El resto de pasos se pueden realizar como usuario regular, nos bajamos la última versión de
BinUtils
y
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
Todo el crossCompiler se va instalar bajo $HOME/opt/cross de este modo no crearemos conflictos con el entorno de compilación de nuestro SO:
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
Podemos ver algunas opciones de configuración interesantes:
- --disable-nls: Le indica a binutils que no incluya soporte nativo de lenguajes, es opcional pero reduce las dependencias de compilación, solo soportaremos Inglés :)
- --with-sysroot: Le indica a binutils que utilice un sysroot vacío
Con el comando siguiente comprobamos que el compilador de ASM esté disponible en el path:
/home/kr0m/opt/cross/bin/i686-elf-as
Compilamos 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
NOTA: gcc depende de libgcc una librería de bajo nivel que el compilador espera que esté disponible cuando compila código.
Las opciones con las que se ha compilado gcc son:
- --disable-nls: Le indica a binutils que no incluya soporte nativo de lenguajes, es opcional pero reduce las dependencias de compilación, solo soportaremos Inglés :)
- --without-headers: Le indicamos a gcc que no debe confiar en que haya ninguna librería de C en el sistema
- --enable-languages: Le indicamos los lenguajes de programación que debe soportar
Podemos obtener la versión del compilador con:
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)
Consultamos el TargetTriplet del compilador:
i686-elf
Hacemos que el compilador y las binutils estén disponibles en el path:
vi .bashrc
export PATH="$HOME/opt/cross/bin:$PATH"
Este compilador es freestanding lo que implica que no utilizará librerías como libc tan solo un subset muy básico:float.h, iso646.h, limits.h, stdalign.h, stdarg.h, stdbool.h, stddef.h, stdint.h y stdnoreturn.h, todo son simplemente definiciones de tipos sin ficheros .c
Nuestro fichero
Makefile
también se verá afectado:
#$@ : 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