Ir al contenido

Usuario:Nirei/Convenciones de llamadas X86

De Wikipedia, la enciclopedia libre

Este artículo describe las convenciones de llamada utilizadas al programar microprocesadores de la arquitectura x86.

Las convenciones de llamada describen la interfaz mediante la cuál se invoca una función o subrutina, en particular:

  • El orden en el que se asignan los parámetros atómicos (escalares), o cada una de las partes individuales de un parámetro complejo.
  • La forma en que se pasan los parámetros (empujándolos a la pila, situándolos en registros del procesador o una combinación de ambas técnicas).
  • Los registros que la función llamada debe preservar intactos (también conocidos como registros no volátiles).
  • El reparto de responsabilidades a la hora de preparar y restaurar la pila después de una llamada a función, entre el código que llama y el que es llamado.

Estas reglas están íntimamente ligadas a los tamaños y formatos de los tipos de datos de los lenguajes de programación. Otro tema estrechamente relacionado es la codificación de nombres (name mangling), que determina cómo se asignan los nombres de los símbolos en código a los nombres de los símbolos utilizados por el enlazador. Las convenciones de llamada, las representaciones de tipos y la manipulación de nombres son parte de lo que se conoce como interfaz binaria de aplicación (ABI, por sus siglas en inglés).

Existen sutiles diferencias en la forma en que los distintos compiladores implementan estas convenciones, por lo que a menudo es difícil integrar el código compilado mediante diferentes compiladores. Por otro lado, las convenciones utilizadas como interfaz estándar, como stdcall, se implementan de manera lo más uniforme posible para permitir la interoperabilidad entre programas y lenguajes.

Trasfondo histórico[editar]

Antes de las microcomputadoras, el fabricante de cada máquina solía proporcionar un sistema operativo y compiladores para varios lenguajes de programación. Las convenciones de llamada para cada plataforma eran las definidas por las herramientas de programación del fabricante.

Por normal general, las primeras microcomputadoras anteriores a Commodore Pet y Apple II no iban acompañadas ni de un sistema operativo ni de compiladores. El ordenador personal IBM incluía el producto de Microsoft precursor de Windows: el Sistema Operativo en Disco (DOS), pero no un compilador. El único estándar de hardware para máquinas compatibles con IBM PC fue definido por los procesadores Intel (8086, 80386) y por el propio hardware producido por IBM. Las extensiones de hardware y todos los estándares de <i>software</i> (salvo una convención de llamada de BIOS) quedaron abiertos a la competencia del mercado.

Una gran variedad de empresas de software independientes ofrecían sistemas operativos, compiladores para multitud de lenguajes de programación y aplicaciones. Esto provocó que se implementasen muchos esquemas de llamada diferentes, a menudo mutuamente excluyentes, basados en diferentes requisitos, prácticas históricas y en la creatividad de los programadores.

Tras la explosión del mercado de compatibles con IBM PC, los sistemas operativos y las herramientas de programación de Microsoft (con diferentes convenciones) se volvieron predominantes, mientras que empresas de menor calibre como Borland y Novell, y proyectos de software libre y código abierto como GNU Compiler Collection (GCC), mantenían sus propios estándares. Finalmente se adoptaron disposiciones para la interoperabilidad entre proveedores y productos, simplificando el problema de elegir una convención viable.

Limpieza realizada por el llamador[editar]

En este tipo de convenciones de llamada, es el código que realiza la llamada el que limpia los argumentos de la pila, restableciendo el estado de la pila al punto en el que estaba antes de que se invocase la función llamada.

cdecl[editar]

cdecl (abreviatura de «declaración C») es una convención de llamada para el lenguaje de programación C. Es utilizada por muchos compiladores de este lenguaje para la arquitectura x86. En cdecl, los argumentos de la subrutina se incorporan a la pila. Si los valores de retorno son valores enteros o direcciones de memoria, el destinatario los guarda en el registro EAX, mientras que los valores de punto flotante se colocan en el registro ST0 x87. Los registros EAX, ECX y EDX son responsabilidad del código que realiza la llamada, mientras que el resto los gestiona el código invocado. Los registros de punto flotante x87 ST0 a ST7 deben vaciarse (desapilándolos o liberándolos) al llamar a una nueva función, y ST1 a ST7 deben estar vacíos al terminar la ejecución de una función. ST0 también debe estar vacío si no se utiliza para retornar un valor.

En el contexto del lenguaje C, los argumentos de la función se insertan en la pila de derecha a izquierda, es decir, el último argumento de la función se inserta primero.

Considere el siguiente fragmento de código fuente en C:

int llamado(int, int, int);

int lamador(void)
{
    return callee(1, 2, 3) + 5;
}

{{syntaxhighlight}}En x86, podría producir el siguiente código ensamblador (sintaxis de Intel):

caller:
    ; crear un nuevo marco de llamada
    ; (algunos compiladores pueden producir una instrucción 'enter' en su lugar)
    push    ebp       ; guardar el antiguo marco de llamada
    mov     ebp, esp  ; inicializar el nuevo marco de llamada
    ; empujar los argumentos de la llamada, en orden inverso
    ; (algunos compiladores pueden restar el espacio requerido del puntero de pila,
    ; luego escribir cada argumento directamente, ver abajo.
    ; La instrucción 'enter' también puede hacer algo similar)
    ; sub esp, 12      ; la instrucción 'enter' podría hacer esto por nosotros
    ; mov [ebp-4], 3   ; o mov [esp+8], 3
    ; mov [ebp-8], 2   ; o mov [esp+4], 2
    ; mov [ebp-12], 1  ; o mov [esp], 1
    push    3
    push    2
    push    1
    call    callee    ; llamar a la subrutina 'callee'
    add     esp, 12   ; eliminar los argumentos de la llamada del marco
    add     eax, 5    ; modificar el resultado de la subrutina
                      ; (eax es el valor de retorno de nuestro 'callee',
                      ; así que no tenemos que moverlo a una variable local)
    ; restaurar el antiguo marco de llamada
    ; (algunos compiladores pueden producir una instrucción 'leave' en su lugar)
    mov     esp, ebp  ; la mayoría de las convenciones de llamada dictan que ebp debe ser preservado por el callee,
                      ; es decir, se conserva después de llamar al 'callee'.
                      ; por lo tanto, todavía apunta al inicio de nuestro marco de pila.
                      ; debemos asegurarnos de que el 'callee' no modifique (o restaure) ebp,
                      ; así que debemos asegurarnos de que
                      ; use una convención de llamada que haga esto
    pop     ebp       ; restaurar el antiguo marco de llamada
    ret               ; retornar

{{syntaxhighlight}}El código llamador limpia la pila después de que regresa la llamada a la función. La convención de llamada cdecl suele ser la convención de llamada predeterminada en compiladores de C para la arquitectura x86, aunque muchos compiladores ofrecen opciones para seleccionar otras convenciones de llamada. Para definir manualmente una función como cdecl, algunos compiladores admiten la siguiente sintaxis:

return_type __cdecl nombre_funcion();

{{syntaxhighlight}}

Variaciones[editar]

Existen algunas variaciones en la interpretación de cdecl. Esto implica que los programas para arquitectura x86 compilados para diferentes plataformas de sistemas operativos o por diferentes compiladores pueden resultar incompatibles, aún cuando usen la convención cdecl y no interactúen con el entorno subyacente.

Con respecto a la forma de transmitir el valor de retorno, algunos compiladores devuelven estructuras de datos simples con una longitud inferior a dos registros en el par de registros EAX:EDX. Las estructuras y clases más grandes que requieren un trato especial por parte de la gestión de excepciones, como por ejemplo, un constructor definido, destructor o asignación; se retornan a través de la memoria. Para retornar un valor a través de la memoria, el código que realiza la llamada reserva la memoria necesaria y pasa un puntero como primer parámetro oculto. El destinatario escribe el valor de retorno en esa sección de memoria, y el código que realizó la llamada recupera el puntero oculto desde la pila a su regreso.[1]

En Linux, GCC establece el estándar de facto sobre convenciones de llamada. De la versión 4.5 de GCC en adelante, la pila debe estar alineada a un segmento de 16 bytes al llamar a una función (las versiones anteriores solo requerían una alineación de 4 bytes).[2]

La ABI de System V para sistemas i386 describe una versión de cdecl. [3]

syscall[editar]

Esta convención es similar a cdecl en que los argumentos se apilan de derecha a izquierda. EAX, ECX y EDX no se conservan. El tamaño de la lista de parámetros en palabras dobles se pasa en el registro AL.

syscall es la convención de llamada estándar para la API OS/2 de 32 bits.

optlink[editar]

Los argumentos también se desplazan de derecha a izquierda. Los tres primeros argumentos (los situados más a la izquierda) se pasan en EAX, EDX y ECX y hasta cuatro argumentos de punto flotante se pasan de ST0 a ST3, aunque se reserva espacio para ellos en la lista de argumentos de la pila. Los resultados se devuelven en EAX o ST0. Se conservan los registros EBP, EBX, ESI y EDI.

optlink es utilizado por los compiladores de IBM VisualAge. [[Categoría:Subrutinas]] [[Categoría:Arquitectura x86]]

  1. Pollard, Jonathan de Boyne (2010). «The gen on function calling conventions». Frequently Given Answers. 
  2. «GCC Bugzilla – Bug 40838 - gcc shouldn't assume that the stack is aligned». 2009. 
  3. «System V Application Binary Interface: Intel 386 Architecture Processor Supplement» (4th edición).