Código abiertoProgramación

Compatibilidad binaria entre sistemas *nix

La compatibilidad de los binarios, o ejecutables, depende de varios factores como bien sabes. En este artículo intentaré aclarar una duda que me preguntan mucho sobre la compatibilidad binaria entre sistemas. La verdad es que genera gran confusión entre los usuarios menos experimentados y merece la pena hacer una introducción.

Aquí me centraré en los sistemas operativos *nix que, a pesar de ser de la misma familia, presentan incompatibilidades entre sí. Pero como verás, se pueden saltar estas barreras en algunos casos para eludir la incompatibilidad…

¿De qué depende la compatibilidad binaria?

ELF Linux formato

Más información Introducción a la programación.

Comenzamos por los códigos fuente (incluidos los interpretados), éstos códigos escritos en lenguaje de alto nivel son compatibles (en principio) con todo sistema operativo y máquina. Aunque habría alguna excepción, y es cuando se usan syscalls (llamadas al sistema) específicas de un sistema operativo. En ese caso, un programa escrito para Windows no sería compatible con Linux o viceversa.

En cuanto al apartado de la máquina o ISA para la que ha sido escrito, un código fuente escrito en lenguajes de alto nivel no debería presentar ningún inconveniente para ejecutarse en diversas ISAs diferentes. Por ejemplo, un mismo código fuente se puede compilar para generar binarios para la arquitectura propia o para ajenas (compilación cruzada). Por tanto, un programa escrito, por ejemplo en C, podría compilarse para ARM, AMD64, SPARC, POWER, etc.

Si el código estuviese escrito en lenguaje ensamblador, entonces la cosa cambia, ya que la sintaxis, instrucciones y tipos de datos usados son específicos para una ISA. Por ejemplo, no es igual un código ASM para RISC-V que para ARM, a pesar de que ambas ISAs tienen semejanzas y son de tipo RISC.

Si seguimos bajando de nivel, los siguiente sería el binario, es decir, el ejecutable ya compilado. En este caso, independientemente del lenguaje usado y del contenido del código, solo sería compatible para un sistema operativo y arquitectura para el que se haya creado.

En este caso, va a depender mucho de las bibliotecas, APIs, ABI del sistema operativo, y del formato binario para la ISA específica para la que haya sido compilado. Es decir, un binario para una máquina x86 Linux solo sería compatible con dicha máquina, ni siquiera serviría en Linux en otra máquina de diferente como puede ser ARM. Por eso hay que recompilar los paquetes para, por ejemplo, la Raspberry Pi…

La ISA o arquitectura es una interfaz abstracta entre el hardware y el software de bajo nivel y que abarca toda la información necesaria en una computadora para escribir un programa en lenguaje máquina que se ejecute sobre una CPU. Para eso, necesita definir el repertorio de las instrucciones, su formato, los registros, los modos de acceso a memoria, E/S, tipos de datos aceptados, etc.

Como ABI se entiende a la parte de la ISA junto con las interfaces del sistema operativo usados por los desarrolladores de los programas. Define un estándar para la portabilidad binaria entre computadoras.

Si hubiese cambios importantes en la API, kernel del sistema (System Call Interface), ABI, etc., entonces es probable que incluso binarios para una versión determinada de un sistema operativo no sean compatibles con las versiones anteriores (retrocompatibilidad). Lo mismo ocurre con los cambios de las ISAs si se quitan instrucciones.

Siguiente paso en la escala… ¿hay algo más bajo que el binario? Realmente no, pero sí que se podría optimizar el código para sets específicos de extensiones. Por ejemplo, imagina un mismo paquete binario compilado para Linux y para máquinas x86-64. Ya sabes que existen varias CPUs que pertenecen a esta familia de ISA, como las de Intel, AMD, VIA, etc.

Se podría dar el caso de que use instrucciones que solo sean compatibles con una de estas marcas e incluso que ni siquiera pudiese ser aprovechado por todos los modelos de una misma marca. Imagina que se usan instrucciones AVX-512, que solo son compatibles con algunos modelos de Intel (próximamente también en AMD).

En estos casos, hay muchos códigos donde se agregan estas instrucciones específicas, aunque no se usan en ciertos casos. Solo si se activa la opción del compilador correspondiente a dicho repertorio o extensión de instrucciones, entonces se cambiarán algunas instrucciones de la ISA base por otras del nuevo repertorio. Esto hace que el binario solo funcione con CPUs compatibles. Una CPU sin soporte no podría ejecutar el código (ni las partes no dependientes del set), ya que el conjunto del programa no funcionaría.

Por eso, es probable que encuentres paquetes compilados para hacer uso de un set específico y otro que no las usa para maximizar la compatibilidad con todas las CPUs del mercado de es familia.

Las distros Linux agregan otro problema adicional, y es la fragmentación existente entre estas distribuciones, los diferentes gestores de paquetes y tipos de paquetes existentes (.rpm, .deb,…), etc. Eso genera que los binarios para Linux y una misma arquitectura no funcionen en todas las distros. Por eso han surgido algunos proyectos para paliar este problema como los paquetes universales:

Incluso están surgiendo algunos proyectos de convergencia que buscan ir un paso más allá y adaptar apps multiplataforma/dispositivo. Un ejemplo de ello es MAUI.

¿Se pueden saltar esas restricciones?

Sí, lo cierto es que se puede. Todos conoceréis emuladores como DOSBox (que permite ejecutar software nativo de MS-DOS en otros sistemas operativos), o los típicos proyectos de retro gaming que usan emuladores para consolas como las de Sega, Nintendo, Atari, etc. Incluso también están las máquinas virtuales, que puede recrear una máquina de la misma arquitectura para ejecutar otros sistemas operativos.

Además de eso, existe lo que se conoce compilación en tiempo de ejecución, que puede borrar estas barreras. Las técnicas JIT (Just-In-Time) permiten una traducción dinámica del código de un programa para traducir un bytecode en código máquina nativo al vuelo. Es lo que se usa, por ejemplo, en la máquina virtual Java.

También existen las capas de compatibilidad, que no son exactamente emuladores. Es el caso del proyecto Wine, que pretende hacer compatible todos aquellos binarios nativos de Windows en sistemas *nix. O Darling, para hacer que los binarios Mach-O de macOS puedan ejecutarse sobre Linux (EFL/a.out).

Por otro lado, existen también la posibilidad de generar compatibilidad binaria para paquetes para una misma arquitectura, pero que pertenecen a otros sistema operativo similar. Es el caso de los Unix/Unix-like, que pueden implementar compatibilidad binaria con los paquetes de Linux. De esa forma aprovechan los paquetes nativos para Linux, ya que son más numerosos que los compilados específicamente para sistemas como *BSD, Solaris, etc.

A pesar de sus semejanzas, y compatibilidad con POSIX, para que eso sea posible se necesita también implementar una ABI compatible con el sistema del que se quieren usar programas. Es el caso de, por ejemplo, FreeBSD, que tiene compatibilidad binaria para los paquetes Linux, generando una especie de traducción entre las llamadas al sistema Linux a la correspondiente de BSD. Además de usar bibliotecas y otras dependencias… También recuerdo lxrun de Solaris para paquetes Linux, o el propio iBCS2 de Linux para ejecutar paquetes de otros Unix.

En FreeBSD no viene activado por defecto, debes cargar el objeto del kernel (KLD) correspondiente con kldload.

En algunos casos, se podría incluso hacer correr un programa de otro *nix, pero con un esfuerzo bastante considerable para que funcione.

Isaac

Apasionado de la computación y la tecnología en general. Siempre intentando desaprender para apreHender.

Deja una respuesta

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