Compilador cruzado GCC

He retomado mis estudios e investigación casera sobre los sistemas operativos después de como 10 años de haberlo dejado. Creo que por fin con mi actual empleo va a ser posible ir más allá que la teoría y los temas introductorios. En esta ocasión escribo sobre como generar un compilador cruzado gcc.

A fin de escribir un kernel desde cero, es prerrequisito construir un “toolchain”, que es el conjunto de utilidades necesarias para compilar el kernel. Desafortunadamente, cuando se desarrolla un kernel, no es posible utilizar un compilador para programar aplicaciones de usuario. El problema es que este compilador genera código que contiene llamadas al sistema operativo anfitrión. Cuando se construye un kernel desde cero, no existen las llamadas al sistema. El compilador debe ser capaz de generar únicamente código directamente para el hardware objetivo (target).

Nunca antes había compilado un compilador cruzado. En días pasados compré unos cursos sobre sistemas operativos en Udemy. Los instructores tocan temas que durante muchos años fueron verdaderos socavones en mi entendimiento de los sistemas operativos. Aun con esa asistencia, me costó trabajo lograr una compilación exitosa. En este artículo voy a mostrar lo que hice.

Un compilador cruzado es un compilador que se ejecuta en la plataforma A, el sistema anfitrión (host), pero produce código para la plataforma B, el sistema objetivo (target). Se pueden construir compiladores cruzados para plataformas con procesadores diferentes, o sistemas operativos diferentes o formatos de archivos ejecutables diferentes. Para una explicación más detallada, se puede consultar aquí y aquí.

Inicié el proceso instalando Ubuntu 20.04.2 LTS Server en VirtualBox. Aquí la idea es que si el proceso de compilación se descarrila siempre será posible empezar de nuevo sin comprometer información valiosa.

Para compilar GCC a mano hay que cubrir una serie de requisitos. Los dos más importantes son descargar el código fuente de GCC y el código fuente de Binutils. Y la pregunta del millón es ¿Qué versiones? Yo no me quebré mucho la cabeza. Decidí utilizar las mismas versiones que vienen instaladas con Ubuntu 20.04.2 LTS.

  • GCC 9.3.0
  • Binutils 2.34

Otras dependencias a cubrir se muestran en la siguiente tabla. En mi caso particular la columna Versión muestra las versiones que se instalaron usando el comando sudo apt install <Dependencia>. Sin embargo, hay algunos detalles que tuve que tomar en consideración y que comentaré más adelante.

DependenciaVersiónComentarios
build-essential12.8ubuntu1.1
bison2:3.5.1+dfsg-1
flex2.6.4-6.2
libgmp3-dev2:6.2.0+dfsg-4Necesario para GCC
libmpc-dev1.1.0-1Necesario para GCC
libmpfr-dev4.0.2-1Necesario para GCC
texinfo6.7.0.dfsg.2-5Necesario para Binutils
libcloog-isl-dev0.18.4-2Opcional
libisl-dev0.22.1-1Opcional
Requisitos para compilar GCC

Los siguientes comandos instalaran por nosotros las dependencias mencionadas en la tabla de arriba.

sudo apt install build-essential
sudo apt install bison
sudo apt install flex
sudo apt install libgmp3-dev
sudo apt install libmpc-dev
sudo apt install libmpfr-dev
sudo apt install texinfo
sudo apt install libcloog-isl-dev
sudo apt install libisl-dev

La librería libfbloog-isl-dev no está presente en los repositorios de Ubuntu en México. Esta librería es opcional y considero que es seguro saltarse este paso, sin embargo decidí instalarla de todos modos. Para ello, se requiere agregar la siguiente línea al archivo en /etc/apt/sources.list.

deb http://cz.archive.ubuntu.com/ubuntu bionic main universe

Para trabajar en nuestro proceso de compilación de manera ordenada y metódica, vamos a construir un árbol de directorios.

mkdir src
cd src
mkdir binutils
mkdir binutils/build-binutils
mkdir gcc
mkdir gcc/build-gcc

A continuación creamos unas variables de entorno. Al final de la compilación encontraremos los ejecutables generados en el directorio $HOME/opt/cross/bin.

export PREFIX="$HOME/opt/cross"
export TARGET=i686-elf
export PATH="$PREFIX/bin:$PATH"

Bajamos el codigo fuente de Binutils.

cd $HOME/src/binutils
wget https://ftp.gnu.org/gnu/binutils/binutils-2.34.tar.xz
tar -xvf binutils-2.34.tar.xz
# Este proceso crea el directorio binutils-2.34

Procedemos a bajar el código fuente de GCC.

cd $HOME/sources/gcc
wget https://ftp.gnu.org/gnu/gcc/gcc-9.3.0/gcc-9.3.0.tar.xz
tar -xvf gcc-9.3.0.tar.xz
# Este proceso crea el directorio gcc-9.3.0

Compilando Binutils.

cd $HOME/sources/binutils/build-binutils

../binutils-2.34/configure --target=$TARGET --prefix="$PREFIX" --with-sysroot --disable-nls --disable-werror
make
make install

Y aquí viene la parte más complicada, compilar GCC. Primero intenté ejecutar la siguiente secuencia de comandos.

1) cd $HOME/src/gcc/build-gcc

2) ../gcc-9.3.0/configure --target=$TARGET --prefix="$PREFIX" --disable-nls --enable-languages=c,c++ --without-headers
3) make all-gcc
4) make all-target-libgcc
5) make install-gcc
6) make install-target-libgcc

En la línea del inciso 2 falla la configuración. Sale un mensaje de error que se refiere a las dependencias libgmp3-dev, libmpfr-dev y libmpc-dev y solicita la instalación de las siguientes versiones mínimas para funcionar.

  • gmp 4.2
  • mpfr 3.1.0
  • mpc 0.8.0

Después de investigar un poco decidí irme por la solución que a continuación describo. En el directorio que contiene el código fuente de GCC, es decir, en $HOME/src/gcc/gcc9.3.0, se crean los directorios /mpfr y /mpc, para ello, se ejecuta la siguiente secuencia de comandos. La librería gmp no tuvo ningún problema.

cd $HOME/src/gcc/gcc-9.3.0
mkdir mpfr
mkdir mpc
cd mpfr
wget https://gcc.gnu.org/pub/gcc/infrastructure/mpfr-3.1.4.tar.bz2
# Este último comando genera el directorio mpfr-3.1.4
cd mpfr-3.1.4
cp -rf * ../
cd ..
rm -rf mpfr-3.1.4
touch aclocal.m4 configure Makefile.am Makefile.in
cd $HOME/src/gcc/gcc-9.3.0/mpc
wget https://gcc.gnu.org/pub/gcc/infrastructure/mpc-1.0.3.tar.bz2
# Este último comando genera el directorio mpc-1.0.3
cd mpc-1.0.3
cp -rf * ../
cd ..
rm -rf mpc-1.0.3
touch aclocal.m4 configure Makefile.am Makefile.in

Lo que acaba de pasar es que bajamos las librerías mpfr y mpc que por cierto ya habíamos instalado con el comando sudo apt install pero que por alguna razón el comando configure no está detectando y utilizando. Al parecer la razón es que podrían faltar los archivos de cabecera .h de estos paquetes aunque aún no he investigado del todo la causa.

Queremos que las fuentes de ambas librerías queden directamente sobre los directorios /mpfr y /mpc respectivamente, para que configure y make las encuentren. Este proceso de mover el código fuente de un directorio a otro modifica las estampas de tiempo de algunos archivos importantes (aclocal.m4, configure, Makefile.am, Makefile.in) y se debe utilizar el comando touch para reestablecer dichas estampas.

Procedemos ahora si, si es que no cometí ningún error, a ejecutar el proceso de compilación de GCC.

cd $HOME/src/gcc/build-gcc

../gcc-9.3.0/configure --target=$TARGET --prefix="$PREFIX" --disable-nls --enable-languages=c,c++ --without-headers
make all-gcc
make all-target-libgcc
make install-gcc
make install-target-libgcc

No necesité hacer make distclean para limpiar el proceso fallido. Al parecer el proceso de compilación de GCC es tan robusto que reinicia donde se quedó. Aunque si lo prefieren pueden limpiar los restos de la compilación pasada.

Por último, para probar la instalación se ejecuta el siguiente comando.

$HOME/opt/cross/bin/$TARGET-gcc --version

Si sale un mensaje como el de la figura de abajo, ya la hicimos.

Definitivamente es un proceso largo y tedioso. Algunos posts y parece ser que también el manual de gcc, que no he leído, se refieren a este proceso como la forma difícil. Estoy seguro que se pueden optimizar y automatizar varias partes. Por cierto, les recomiendo mucho leer el artículo de stack overflow sobre como instalar GCC pedazo a pedazo.

Este compilador cruzado, que recién nos construimos, no sirve para compilar programas de aplicación. Este es un compilador para desarrollo de sistemas operativos. Si intentamos incluir archivos .h que pertenecen a la librería estándar, el compilador arrojará errores. Sin embargo, al parecer hay algunas librerías independientes de la plataforma que pueden ser usadas. Ya me daré cuenta de las ventajas y desventajas de este compilador con el uso.

Y ya para concluir a continuación se muestra el proceso de instalar el ensamblador nasm y el emulador qemu para completar nuestra caja de herramientas. En otro artículo mostraremos como se usan.

sudo apt update
sudo apt install nasm

sudo apt install qemu-system-x86
nasm -v
Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *