Cómo arrancar el kernel de Linux en una nueva plataforma

Fuentes: How to bring up the Linux Kernel on a new platform, github.com

Arrancar el kernel de Linux en una plataforma nueva, especialmente en sistemas embebidos o hardware personalizado, es un proceso fundamental que requiere comprender los requisitos mínimos del sistema operativo. Aunque Linux es un software complejo, sus necesidades de hardware para iniciar son sorprendentemente modestas: una CPU con una Unidad de Gestión de Memoria (MMU) para traducir direcciones virtuales a físicas y aislar procesos, suficiente RAM para cargar el kernel, el Device Tree y un initramfs, y un temporizador de reloj del sistema que se active periódicamente. Este artículo explora el proceso de llevar Linux a una plataforma RISC-V mínima y emulada, un ejercicio que ilustra los pasos necesarios para cualquier nuevo hardware.

El primer paso crucial es la preparación del entorno de desarrollo. Para compilar software que se ejecutará en la plataforma destino, se necesita una cadena de herramientas (toolchain) adecuada. Compilarla manualmente es tedioso, por lo que proyectos como crosstool-ng facilitan enormemente la tarea. En el caso de un sistema RISC-V de 32 bits, se configura una toolchain para riscv32-unknown-linux-gnu, que incluye el compilador GCC y la biblioteca C estándar glibc. La configuración requiere habilitar el uso de la MMU, establecer Linux como sistema operativo objetivo y seleccionar glibc como biblioteca C. Una vez configurada, la compilación puede tomar desde unos minutos hasta más de una hora, dependiendo del hardware de la máquina de desarrollo.

Con la toolchain lista, el siguiente paso es compilar el kernel de Linux. Se clona el repositorio oficial de Linux y se crea una configuración específica para la plataforma. Dado que la plataforma de ejemplo es minimalista, se parte de la configuración predeterminada de RISC-V y se modifica para adaptarse a las limitaciones del emulador. Es esencial cambiar la arquitectura base a RV32I y deshabilitar las extensiones de soporte que no están implementadas en el emulador, como el conjunto de instrucciones comprimidas. Además, se desactiva el soporte UEFI y se configura el kernel para que sea reubicable, lo que permite cargarlo en cualquier dirección de memoria. Para optimizar el tiempo de arranque y reducir el tamaño, se desactivan funciones innecesarias como la virtualización, el soporte de módulos cargables, la red y la API criptográfica. El resultado de este proceso es un archivo Image en la carpeta arch/riscv/boot/, que contiene el kernel compilado listo para ejecutarse.

Además del kernel, se necesita un programa init que se ejecutará en el espacio de usuario una vez que el kernel haya terminado de arrancar. En un sistema embebido real, esto podría ser un shell como sh o herramientas de busybox. Para simplificar la creación del initramfs, es recomendable compilar este programa como un binario estáticamente enlazado, evitando así la necesidad de incluir bibliotecas compartidas adicionales. Este programa puede ser tan simple como una aplicación en C que imprime texto en la consola, sirviendo como prueba de que el sistema operativo ha iniciado correctamente.

En un sistema real, en lugar de escribir un emulador de CPU desde cero (como se hizo en el ejemplo con unas 2000 líneas de código C++ para implementar el conjunto de instrucciones RV32IMA y periféricos básicos como la RAM y el chip WD8250), se leería la hoja de datos del SoC para mapear los periféricos en el espacio de direcciones de la CPU. También se utilizaría un cargador de arranque (bootloader) como U-Boot para cargar el kernel, el Device Tree y el initramfs desde una memoria no volátil, como NOR Flash o eMMC. Este enfoque demuestra que, aunque el hardware varíe, los pasos fundamentales para arrancar Linux—configurar la toolchain, compilar el kernel con las opciones adecuadas y preparar el entorno de usuario inicial—permanecen consistentes. Este proceso es vital para desarrolladores que trabajan con hardware personalizado, asegurando que el sistema operativo base funcione antes de añadir funcionalidades más complejas.