En un artículo reciente comenté que quería construir herramientas para un Linux reducido que corre en una impresora 3D con una CPU MIPS. Tenía dos caminos: armar una cadena de herramientas para compilación cruzada, o usar Zig, que en teoría trae toolchains integradas para MIPS. Me costó bastante hacer funcionar Zig, y como ya había mencionado Crosstool-Ng, quizás se preguntan por qué no empecé por ahí. Resulta que también tenía lo suyo.

¿Qué es Crosstool-Ng?

Crosstool-NG es un sistema de build para crear cadenas de compilación cruzada: compiladores, ensambladores, linkers, librerías C, cabeceras del kernel y todas las piezas necesarias para construir en una máquina software que correrá en otra distinta. En vez de emparejar a mano una versión concreta de GCC con el binutils, glibc o musl adecuado, las cabeceras de Linux, los parches y las opciones de configuración, uno elige la arquitectura destino y deja que Crosstool-NG descargue, parche, configure y construya todo el stack. El resultado es una cadena autocontenida con comandos como mipsel-linux-musl-gcc o arm-none-eabi-gcc, lista para producir binarios para el sistema destino.

No hay forma de saber si es Zig o Crosstool-Ng con solo mirar esta foto.
No hay forma de saber si es Zig o Crosstool-Ng con solo mirar esta foto.

El nombre de cuatro partes sigue un formato habitual en el mundo de la compilación cruzada. Tomemos arm-none-eabi-gcc. La herramienta acá es gcc y, como es de esperar, también existirán arm-none-eabi-as y arm-none-eabi-ld, entre otras. La primera parte, arm en este caso, es la arquitectura destino.

La segunda parte puede significar varias cosas. En teoría es el nombre del fabricante, pero a veces es "none", que suele querer decir "genérico", o "linux" en el caso de un destino Linux, que técnicamente no es un fabricante.

La tercera parte es la convención de llamada y, con frecuencia, una idea de la librería. Por ejemplo, arm-linux-gnueabihf-gcc indicaría la librería GNU con la EABI de ARM y punto flotante por hardware. A estos nombres se les llama a veces target triples porque, históricamente, eran CPU-FABRICANTE-SO, aunque hoy suelen tener cuatro o incluso cinco partes cuando la convención de llamada incluye el sistema operativo, como linux-musl.

Suena simple, pero las cadenas cruzadas son inusualmente sensibles a las combinaciones de versiones y a los detalles de la ABI. El endianness, las convenciones de punto flotante, las variantes del set de instrucciones, el soporte de hilos y la elección de librería C tienen que coincidir. Decir "ARM" o "MIPS" no dice mucho: hay que contemplar todas las variaciones posibles de la CPU y las librerías. Crosstool-NG no elimina esas decisiones, pero las convierte en una configuración reproducible en lugar de una larga secuencia de componentes armados a mano. Tuve dos problemas que terminé resolviendo.

Problema uno: las versiones

Algo bueno de Crosstool-Ng es que descarga las versiones correctas de todo por ti. El problema es que, cuando lo instalas desde los repositorios de tu sistema, probablemente obtengas una versión antiquísima de la propia herramienta. No encontraba las entradas correctas en la configuración al hacerlo así, de modo que terminé desinstalándolo y tomando la última versión directo del código fuente. Si ese hubiera sido el único problema, habría tenido suerte.

Problema dos: combinaciones infinitas

La CPU de la impresora es rara. Como señalé la vez pasada, los ejecutables usan el set de instrucciones r2 pero también la convención nan2008, que suele encontrarse en r6. Crosstool-Ng permite especificar exactamente lo que quieres, pero no siempre deja claro cómo detallar cada opción. Para ser justos, como con Zig, parte de eso puede ser culpa mía: no uso ninguna de las dos a diario. La mala noticia es que me tomó tres o cuatro intentos dar con la cadena correcta. La buena, que fue mucho más fácil que descargar todo a mano, intentar arreglarlo y construirlo tres o cuatro veces.

¿Cómo se configura?

La mayoría de los cambios necesarios estaban acá, aunque no todos.
La mayoría de los cambios necesarios estaban acá, aunque no todos.

Igual que busybox o la construcción de un kernel a medida, la configuración de Crosstool-Ng usa el comando ct-ng menuconfig. Eso abre un menú donde defines qué quieres y dónde guardarlo. El detalle es que el ajuste nan2008 que necesitaba no forma parte de un mips32r2 estándar. En los ajustes del destino tuve que agregar -mnan=2008 tanto a CFLAGS como a LDFLAGS. Pero eso solo no basta: el compilador C también necesitaba --with-nan2008 (en las opciones del compilador C, bajo extra target CFLAGS) y la misma opción debía ir en la pantalla de la librería C. Es como una sopa de letras: una vez que ves las respuestas parecen obvias, pero mientras buscas entre páginas de opciones es fácil pasar una por alto.

La prueba está en el binario

Con todo bien configurado, pude producir una cadena (ct-ng build) capaz de compilar busybox e incluso un pequeño editor de texto. Todo corrió sin problemas en la impresora. Para compilar busybox usé:

Código
make V=1 CC="mipsel-unknown-linux-musl-gcc -march=mips32r2 -msoft-float -static -Os" STRIP='mipsel-unknown-linux-musl-strip' -j6

A diferencia de Zig, no hizo falta ningún parche. La versión de Zig pesaba unos 9 kB más que esta, así que la diferencia fue mínima: ambas superaban apenas el megabyte. También quería compilar un editor de texto, pero la mayoría dependen de cosas como ncurses, engorrosas de empaquetar. Así que tomé el editor tutorial kilo y lo extendí para que se pareciera un poco a emacs. Funciona muy bien y es un buen punto de partida si necesitas un editor estático que ocupe poco espacio.

Lección aprendida

Si la CPU de la impresora hubiera sido más convencional, cualquiera de los dos enfoques habría funcionado. En este caso prefiero la solución con Crosstool, porque no estoy mintiendo al parchear la cabecera ELF. Acá esa mentira no hace daño, pero un programa con mucho cálculo de punto flotante podría no funcionar bien, mientras que el binario producido por Crosstool debería estar bien incluso para un programa de punto flotante. Como en casi todo Unix y Linux, siempre hay más de una forma de resolver el problema: para incrustar ejecutables en una máquina Linux ajena, estas dos son perfectamente válidas.

En el ecosistema de microcontroladores y SBC esta técnica es cotidiana. Compilar en un PC x86 para un destino ARM Cortex-M (STM32, RP2040), RISC-V o MIPS es la norma cuando el chip no tiene recursos para compilar por sí mismo. Herramientas como arm-none-eabi-gcc se usan a diario para programar microcontroladores, y Crosstool-Ng permite reproducir esa misma cadena para arquitecturas menos habituales sin depender de la versión que traiga cada distribución.