Saltar al contenido

¿Qué es una interfaz binaria de aplicación (ABI)?

Hola usuario de nuestra página web, hemos encontrado la solución a lo que andabas buscando, has scroll y la encontrarás un poco más abajo.

Solución:

Una forma fácil de entender “ABI” es compararla con “API”.

Ya está familiarizado con el concepto de API. Si desea utilizar las funciones de, digamos, alguna biblioteca o su sistema operativo, programará contra una API. La API consta de tipos / estructuras de datos, constantes, funciones, etc. que puede usar en su código para acceder a la funcionalidad de ese componente externo.

Un ABI es muy similar. Piense en ello como la versión compilada de una API (o como una API a nivel de lenguaje de máquina). Cuando escribe código fuente, accede a la biblioteca a través de una API. Una vez que se compila el código, su aplicación accede a los datos binarios en la biblioteca a través de la ABI. La ABI define las estructuras y métodos que su aplicación compilada usará para acceder a la biblioteca externa (tal como lo hizo la API), solo en un nivel inferior. Su API define el orden en el que pasa argumentos a una función. Tu ABI define la mecánica de cómo estos argumentos se pasan (registros, pila, etc.). Tu API define qué funciones son parte de tu biblioteca. Su ABI define cómo se almacena su código dentro del archivo de la biblioteca, de modo que cualquier programa que use su biblioteca pueda ubicar la función deseada y ejecutarla.

Las ABI son importantes cuando se trata de aplicaciones que utilizan bibliotecas externas. Las bibliotecas están llenas de código y otros recursos, pero su programa debe saber cómo ubicar lo que necesita dentro del archivo de la biblioteca. Su ABI define cómo se almacenan los contenidos de una biblioteca dentro del archivo, y su programa usa el ABI para buscar en el archivo y encontrar lo que necesita. Si todo en su sistema se ajusta a la misma ABI, entonces cualquier programa puede trabajar con cualquier archivo de biblioteca, sin importar quién lo haya creado. Linux y Windows usan ABI diferentes, por lo que un programa de Windows no sabrá cómo acceder a una biblioteca compilada para Linux.

A veces, los cambios de ABI son inevitables. Cuando esto sucede, cualquier programa que use esa biblioteca no funcionará a menos que se vuelva a compilar para usar la nueva versión de la biblioteca. Si la ABI cambia pero la API no, las versiones antiguas y nuevas de la biblioteca a veces se denominan “compatibles con la fuente”. Esto implica que, si bien un programa compilado para una versión de biblioteca no funcionará con la otra, el código fuente escrito para una funcionará para la otra si se vuelve a compilar.

Por esta razón, los desarrolladores tienden a tratar de mantener estable su ABI (para minimizar las interrupciones). Mantener una ABI estable significa no cambiar las interfaces de funciones (tipo y número de retorno, tipos y orden de argumentos), definiciones de tipos de datos o estructuras de datos, constantes definidas, etc. Se pueden agregar nuevas funciones y tipos de datos, pero los existentes deben permanecer lo mismo. Si, por ejemplo, su biblioteca usa enteros de 32 bits para indicar el desplazamiento de una función y cambia a enteros de 64 bits, entonces el código ya compilado que usa esa biblioteca no accederá a ese campo (ni a ninguno de los siguientes) correctamente. . El acceso a los miembros de la estructura de datos se convierte en direcciones de memoria y compensaciones durante la compilación y si la estructura de datos cambia, estas compensaciones no apuntarán a lo que el código espera que apunten y los resultados son, en el mejor de los casos, impredecibles.

Una ABI no es necesariamente algo que proporcionará explícitamente a menos que esté realizando un trabajo de diseño de sistemas de muy bajo nivel. Tampoco es específico del idioma, ya que (por ejemplo) una aplicación C y una aplicación Pascal pueden usar la misma ABI después de compilarse.

Editar: Con respecto a su pregunta sobre los capítulos sobre el formato de archivo ELF en los documentos SysV ABI: La razón por la que se incluye esta información es porque el formato ELF define la interfaz entre el sistema operativo y la aplicación. Cuando le dice al sistema operativo que ejecute un programa, espera que el programa se formatee de cierta manera y (por ejemplo) espera que la primera sección del binario sea un encabezado ELF que contenga cierta información en compensaciones de memoria específicas. Así es como la aplicación comunica información importante sobre sí misma al sistema operativo. Si crea un programa en un formato binario que no es ELF (como a.out o PE), un sistema operativo que espera aplicaciones con formato ELF no podrá interpretar el archivo binario ni ejecutar la aplicación. Esta es una gran razón por la que las aplicaciones de Windows no se pueden ejecutar directamente en una máquina Linux (o viceversa) sin volver a compilarse o ejecutarse dentro de algún tipo de capa de emulación que pueda traducirse de un formato binario a otro.

IIRC, Windows utiliza actualmente el formato Portable Executable (o PE). Hay enlaces en la sección “enlaces externos” de esa página de Wikipedia con más información sobre el formato PE.

Además, con respecto a su nota sobre la alteración de nombres de C ++: al ubicar una función en un archivo de biblioteca, la función generalmente se busca por su nombre. C ++ le permite sobrecargar los nombres de las funciones, por lo que el nombre por sí solo no es suficiente para identificar una función. Los compiladores de C ++ tienen sus propias formas de lidiar con esto internamente, llamadas nombre destrozando. Una ABI puede definir una forma estándar de codificar el nombre de una función para que los programas creados con un lenguaje o compilador diferente puedan localizar lo que necesitan. Cuando usas extern "c" en un programa C ++, está indicando al compilador que utilice una forma estandarizada de registrar nombres que sea comprensible para otro software.

Si conoce el ensamblaje y cómo funcionan las cosas a nivel de sistema operativo, se está ajustando a una determinada ABI. La ABI gobierna cosas como cómo se pasan los parámetros, dónde se colocan los valores de retorno. Para muchas plataformas, solo hay una ABI para elegir, y en esos casos la ABI es simplemente “cómo funcionan las cosas”.

Sin embargo, la ABI también rige cosas como cómo se distribuyen las clases / objetos en C ++. Esto es necesario si desea poder pasar referencias de objetos a través de los límites del módulo o si desea mezclar código compilado con diferentes compiladores.

Además, si tiene un sistema operativo de 64 bits que puede ejecutar binarios de 32 bits, tendrá diferentes ABI para código de 32 y 64 bits.

En general, cualquier código que vincule al mismo ejecutable debe cumplir con la misma ABI. Si desea comunicarse entre códigos utilizando diferentes ABI, debe usar algún tipo de RPC o protocolos de serialización.

Creo que se está esforzando demasiado por incluir diferentes tipos de interfaces en un conjunto fijo de características. Por ejemplo, una interfaz no tiene que dividirse necesariamente en consumidores y productores. Una interfaz es solo una convención mediante la cual interactúan dos entidades.

Los ABI pueden ser (parcialmente) independientes de ISA. Algunos aspectos (como las convenciones de llamadas) dependen de la ISA, mientras que otros aspectos (como el diseño de la clase C ++) no.

Un ABI bien definido es muy importante para las personas que escriben compiladores. Sin una ABI bien definida, sería imposible generar código interoperable.

EDITAR: Algunas notas para aclarar:

  • “Binario” en ABI no excluye el uso de cadenas o texto. Si desea vincular una DLL que exporta una clase C ++, en algún lugar de ella se deben codificar los métodos y las firmas de tipo. Ahí es donde entra en juego la alteración de nombres de C ++.
  • La razón por la que nunca proporcionó un ABI es que la gran mayoría de los programadores nunca lo harán. Las ABI son proporcionadas por las mismas personas que diseñan la plataforma (es decir, el sistema operativo), y muy pocos programadores tendrán el privilegio de diseñar una ABI ampliamente utilizada.

Tu realmente no necesita una ABI en absoluto si …

  • Tu programa no tiene funciones y …
  • Su programa es un solo ejecutable que se ejecuta solo (es decir, un sistema integrado) donde es literalmente lo único que se ejecuta y no necesita hablar con nadie más.

Un resumen simplificado:

API:“Aquí están todas las funciones que puede llamar”.

ABI:“Este es cómo para llamar a una función “.

La ABI es un conjunto de reglas que los compiladores y enlazadores cumplen para compilar su programa para que funcione correctamente. Las ABI cubren varios temas:

  • Podría decirse que la parte más grande e importante de una ABI es el estándar de llamada a procedimiento conocido a veces como la “convención de llamada”. Las convenciones de llamadas estandarizan cómo se traducen las “funciones” al código ensamblador.
  • Los ABI también dictan cómo nombres de las funciones expuestas en las bibliotecas deben representarse para que otro código pueda llamar a esas bibliotecas y saber qué argumentos deben pasarse. Esto se llama “alteración de nombres”.
  • Las ABI también dictan qué tipo de tipos de datos se pueden usar, cómo deben alinearse y otros detalles de bajo nivel.

Echando un vistazo más profundo a la convención de llamadas, que considero el núcleo de una ABI:

La máquina en sí no tiene un concepto de “funciones”. Cuando escribe una función en un lenguaje de alto nivel como c, el compilador genera una línea de código ensamblador como _MyFunction1:. Esto es un etiqueta, que eventualmente será resuelto en una dirección por el ensamblador. Esta etiqueta marca el “inicio” de su “función” en el código ensamblador. En el código de alto nivel, cuando “llamas” a esa función, lo que realmente estás haciendo es hacer que la CPU salto a la dirección de esa etiqueta y continuar ejecutándose allí.

En preparación para el salto, el compilador debe hacer un montón de cosas importantes. La convención de llamada es como una lista de verificación que sigue el compilador para hacer todas estas cosas:

  • Primero, el compilador inserta un poco de código ensamblador para guardar la dirección actual, de modo que cuando termine su “función”, la CPU pueda volver al lugar correcto y continuar ejecutándose.
  • A continuación, el compilador genera código ensamblador para pasar los argumentos.
    • Algunas convenciones de llamada dictan que los argumentos deben colocarse en la pila (en un orden particular por supuesto).
    • Otras convenciones dictan que los argumentos deben colocarse en registros particulares (dependiendo de sus tipos de datos por supuesto).
    • Aún otras convenciones dictan que se debe usar una combinación específica de pila y registros.
  • Por supuesto, si antes había algo importante en esos registros, esos valores ahora se sobrescriben y se pierden para siempre, por lo que algunas convenciones de llamada pueden dictar que el compilador debe guardar algunos de esos registros antes de poner los argumentos en ellos.
  • Ahora el compilador inserta una instrucción de salto que le dice a la CPU que vaya a la etiqueta que hizo anteriormente (_MyFunction1:). En este punto, puede considerar que la CPU está “en” su “función”.
  • Al final de la función, el compilador coloca algunos ensamblados código que hará que la CPU escriba el valor de retorno en el lugar correcto. La convención de llamada dictará si el valor de retorno debe colocarse en un registro particular (dependiendo de su tipo) o en la pila.
  • Ahora es el momento de la limpieza. La convención de llamada dictará dónde el compilador coloca el código de ensamblaje de limpieza.
    • Algunas convenciones dicen que la persona que llama debe limpiar la pila. Esto significa que después de que se realiza la “función” y la CPU vuelve a donde estaba antes, el siguiente código que se ejecutará debería ser un código de limpieza muy específico.
    • Otras convenciones dicen que algunas partes particulares del código de limpieza deben estar al final de la “función” antes de el salto hacia atrás.

Hay muchas ABI / convenciones de llamada diferentes. Algunas de las principales son:

  • Para la CPU x86 o x86-64 (entorno de 32 bits):
    • CDECL
    • STDCALL
    • LLAMADA RÁPIDA
    • VECTORCALL
    • ESTA LLAMADA
  • Para la CPU x86-64 (entorno de 64 bits):
    • SYSTEMV
    • MSNATIVE
    • VECTORCALL
  • Para la CPU ARM (32 bits)
    • AAPCS
  • Para la CPU ARM (64 bits)
    • AAPCS64

Aquí hay una gran página que realmente muestra las diferencias en el ensamblado generado al compilar para diferentes ABI.

Otra cosa a mencionar es que un ABI no solo es relevante dentro el módulo ejecutable de su programa. Es además utilizado por el enlazador para asegurarse de que su programa llama a la biblioteca funciona correctamente. Tiene varias bibliotecas compartidas ejecutándose en su computadora, y siempre que su compilador sepa qué ABI usa cada uno, puede llamar a funciones de ellas correctamente sin hacer explotar la pila.

Su compilador comprende cómo llamar a las funciones de la biblioteca es extremadamente importante. En una plataforma alojada (es decir, una en la que un sistema operativo carga programas), su programa ni siquiera puede parpadear sin realizar una llamada al kernel.

No se te olvide difundir este tutorial si te valió la pena.

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

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