¿Por qué Windows64 usa una convención de llamadas diferente de todos los demás sistemas operativos en x86-64?

AMD tiene una especificación ABI que describe la convención de llamadas para usar en x86-64. Todos los sistemas operativos lo siguen, excepto Windows, que tiene su propia convención de llamadas x86-64. ¿Por qué?

¿Alguien conoce los motivos técnicos, históricos o políticos de esta diferencia, o es puramente una cuestión de NIHsyndrome?

Entiendo que diferentes sistemas operativos pueden tener diferentes necesidades para cosas de nivel superior, pero eso no explica por qué el orden de paso de parámetros de registro en Windows es rcx - rdx - r8 - r9 - rest on stack mientras todos los demás usan rdi - rsi - rdx - rcx - r8 - r9 - rest on stack .

PD. Soy consciente de cómo estas convenciones de llamadas difieren en general y sé dónde encontrar detalles si es necesario. Lo que quiero saber es por qué .

Editar: para ver cómo, ver, por ejemplo, la entrada de la wikipedia y enlaces desde allí.

Elegir cuatro registros de argumento en x64 – común para UN * X / Win64

Una de las cosas a tener en cuenta acerca de x86 es que el nombre del registro para la encoding del “número de registro” no es obvio; en términos de encoding de instrucciones (el byte MOD R / M , consulte http://www.c-jump.com/CIS77/CPU/x86/X77_0060_mod_reg_r_m_byte.htm ), los números de registro 0 … 7 son, en ese orden, ?AX ?CX ?DX ?BX ?SP ?BP ?SI ?DI .

Por lo tanto, elegir A / C / D (regs 0..2) para el valor de retorno y los primeros dos argumentos (que es la convención “clásica” de 32 bits __fastcall ) es una elección lógica. En lo que se refiere a ir a 64 bits, los registros “superiores” están ordenados, y tanto Microsoft como UN * X / Linux eligieron R8 / R9 como los primeros.

Teniendo esto en cuenta, la elección de Microsoft de RAX (valor de retorno) y RCX , RDX , R8 , R9 (arg [0..3]) es una selección comprensible si elige cuatro registros para los argumentos.

No sé por qué el AMD64 UN * X ABI eligió RDX antes que RCX .

Elección de seis registros de argumentos en x64 – UN * X específico

UN * X, en architectures RISC, tradicionalmente ha hecho pasar argumentos en registros, específicamente, para los primeros seis argumentos (eso es así en PPC, SPARC, MIPS al menos). Lo cual podría ser una de las razones principales por las que los diseñadores de AMD64 (UN * X) ABI eligieron usar seis registros en esa architecture también.

Entonces, si desea que seis registros transmitan argumentos, y es lógico elegir RCX , RDX , R8 y R9 para cuatro de ellos, ¿qué otros dos debería elegir?

Los registros “superiores” requieren un byte de prefijo de instrucción adicional para seleccionarlos y, por lo tanto, tienen una huella de tamaño de instrucción mayor, por lo que no le conviene elegir ninguno de ellos si tiene opciones. De los registros clásicos, debido al significado implícito de RBP y RSP estos no están disponibles, y RBX tradicionalmente tiene un uso especial en UN * X (tabla de compensación global) que aparentemente los diseñadores AMD64 ABI no querían volverse innecesariamente incompatibles. con.
Ergo, la única opción era RSI / RDI .

Entonces, si tiene que tomar RSI / RDI como registros de argumentos, ¿qué argumentos deberían ser?

Hacerlos arg[0] y arg[1] tiene algunas ventajas. Ver el comentario de cHao.
?SI y ?DI son operandos de fuente / destino de instrucción de cadena, y como cHao mencionó, su uso como registros de argumento significa que con las convenciones de llamada AMD64 UN * X, la función strcpy() más simple posible, por ejemplo, solo consiste en los dos Instrucciones de la CPU repz movsb; ret repz movsb; ret porque las direcciones fuente / destino han sido puestas en los registros correctos por la persona que llama. Hay, particularmente en código de “pegamento” de bajo nivel y generado por comstackdor (piense, por ejemplo, que algunos asignadores de montón de C ++ rellenan objetos en construcción, o las páginas de sbrk() relleno de kernel en sbrk() , o copy-on -write pagefaults) una cantidad enorme de bloque copiar / llenar, por lo tanto, será útil para el código utilizado con tanta frecuencia para guardar las dos o tres instrucciones de la CPU que de lo contrario cargarían tales argumentos fuente / dirección de destino en los registros “correctos”.

En cierto modo, UN * X y Win64 solo son diferentes en que UN * X “prepone” dos argumentos adicionales, en registros RSI / RDI elegidos a propósito, a la elección natural de cuatro argumentos en RCX , RDX , R8 y R9 .

Más allá de eso …

Hay más diferencias entre los ABI de UN * X y Windows x64 que solo el mapeo de argumentos para registros específicos. Para obtener información general sobre Win64, verifique:

http://msdn.microsoft.com/en-us/library/7kcdt6fy.aspx

Win64 y AMD64 UN * X también difieren notablemente en la forma en que se usa stackspace; en Win64, por ejemplo, la persona que llama debe asignar stackspace para los argumentos de la función aunque los argumentos 0 … 3 se pasen en los registros. En UN * X, por otro lado, ni siquiera se requiere una función de hoja (es decir, una que no llame a otras funciones) para asignar espacio astackdo si no necesita más de 128 Bytes (sí, es propietario y puede usar una cierta cantidad de stack sin asignarlo … bueno, a menos que seas el código del kernel, una fuente de errores ingeniosos). Todas estas son opciones de optimización particulares, la mayoría de las razones para ello se explican en las referencias completas de ABI a las que apunta la referencia de la wikipedia del póster original.

IDK por qué Windows hizo lo que hizo. Vea el final de esta respuesta para adivinar. Tenía curiosidad sobre cómo se decidió la convención de llamadas SysV, así que busqué en el archivo de la lista de correo y encontré algunas cosas interesantes.

Es interesante leer algunos de esos viejos temas en la lista de correo de AMD64, ya que los arquitectos de AMD estaban activos en él. Por ejemplo, la elección de los nombres de registro fue una de las partes difíciles: AMD consideró cambiar el nombre de los 8 registros originales r0-r7, o llamar a los nuevos registros cosas como UAX .

Además, los comentarios de los desarrolladores del kernel identificaron cosas que hicieron inutilizable el diseño original de syscall y swapgs . Así es como AMD actualizó las instrucciones para resolver esto antes de lanzar las fichas. También es interesante que a fines del 2000, la suposición era que Intel probablemente no adoptaría AMD64.


La convención de llamadas SysV (Linux) y la decisión sobre cuántos registros deberían conservarse en función del llamante frente a la reserva de llamada, se realizó inicialmente en noviembre de 2000, por Jan Hubicka (un desarrollador de gcc). Compiló SPEC2000 y miró el tamaño del código y el número de instrucciones. Ese hilo de discusión rebota en torno a algunas de las mismas ideas que las respuestas y comentarios sobre esta pregunta SO. En un segundo hilo, propuso la secuencia actual como óptima y, con suerte, final, generando un código más pequeño que algunas alternativas .

Él está usando el término “global” para referirse a los registros conservados en la llamada, que deben ser empujados / reventados si se usan.

La elección de rsi , rsi , rdx como los primeros tres argumentos estuvo motivada por:

  • ahorro de tamaño de código menor en funciones que llaman a memset u otra función de cadena C en sus argumentos (donde gcc enlista una operación de cadena de repetición)
  • rbx se conserva en la llamada porque es posible ganar dos rbx conservadas de llamada sin prefijos REX (rbx y rbp). Presumiblemente elegido porque es el único otro registro que no se usa implícitamente en ninguna instrucción. (cadena de caracteres, número de cambios, y salidas / entradas de mul / div tocan todo lo demás).
  • Ninguno de los registros con propósitos especiales se conserva en la llamada (ver el punto anterior), por lo que una función que quiera usar instrucciones de cadena de repetición o un cambio de conteo variable podría tener que mover argumentos de función a otro lugar, pero no tiene que guardar / restablecer el valor de la persona que llama.
  • Estamos tratando de evitar RCX al principio de la secuencia, ya que se usa comúnmente para fines especiales, como EAX, por lo que tiene el mismo propósito que falta en la secuencia. Tampoco se puede usar para syscalls y nos gustaría hacer que la secuencia de syscall coincida con la secuencia de llamadas de función tanto como sea posible.

    (fondo: syscall / sysret inevitablemente destruye rcx (con rip ) y r11 (con RFLAGS ), por lo que el kernel no puede ver lo que originalmente estaba en rcx cuando se ejecutó syscall ).

El kernel-call call ABI fue elegido para coincidir con la llamada de función ABI, excepto r10 lugar de rcx , por lo que una envoltura de libc funciona como mmap(2) puede mov %rcx, %r10 / mov $0x9, %eax / syscall .


Tenga en cuenta que la convención de llamadas SysV utilizada por i386 Linux es una mierda en comparación con el __vectorcall de 32 bits de Windows. Pasa todo en la stack, y solo regresa en edx:eax para int64, no para estructuras pequeñas . No es de extrañar que se haya hecho un pequeño esfuerzo para mantener la compatibilidad con él. Cuando no hay ninguna razón para no hacerlo, hacían cosas como mantener el call-preservado de rbx , ya que decidieron que tener otro en el 8 original (que no necesita un prefijo REX) era bueno.

Hacer que el ABI sea óptimo es mucho más importante a largo plazo que cualquier otra consideración. Creo que hicieron un muy buen trabajo. No estoy totalmente seguro de devolver las estructuras empaquetadas en los registros, en lugar de diferentes campos en diferentes regs. Supongo que el código que los transfiere por valor sin realmente operar en los campos gana de esta manera, pero el trabajo adicional de desempaquetado parece tonto. Podrían haber tenido más registros enteros de retorno, más que solo rdx:rax , por lo que devolver una estructura con 4 miembros podría devolverlos en rdi, rsi, rdx, rax o algo así.

Consideraron pasar enteros en vectores regs, porque SSE2 puede operar en enteros. Afortunadamente ellos no hicieron eso. Los enteros se utilizan como desplazamientos de puntero con mucha frecuencia, y un viaje de ida y vuelta a la memoria de stack es bastante barato . Además, las instrucciones SSE2 toman más bytes de código que las instrucciones enteras.


Sospecho que los diseñadores de Windows ABI podrían haber tenido como objective minimizar las diferencias entre 32 y 64 bits en beneficio de las personas que tienen que portar asm de una a la otra, o que pueden usar un par de #ifdef en alguna ASM para que la misma fuente pueda construye fácilmente una versión de 32 o 64 bits de una función.

Minimizar los cambios en la cadena de herramientas parece poco probable. Un comstackdor x86-64 necesita una tabla separada cuyo registro se utiliza para qué, y cuál es la convención de llamada. Tener una pequeña superposición con 32bit es poco probable que produzca ahorros significativos en el tamaño / complejidad del código de la cadena de herramientas.

Win32 tiene sus propios usos para ESI y EDI, y requiere que no se modifiquen (o al menos que se restauren antes de llamar a la API). Me imagino que el código de 64 bits hace lo mismo con RSI y RDI, lo que explicaría por qué no se usan para pasar argumentos de funciones.

Sin embargo, no podría decirte por qué RCX y RDX están conmutadas.

Recuerde que Microsoft inicialmente fue “oficialmente ajeno al esfuerzo inicial de AMD64” (de “A History of Modern 64-bit Computing” por Matthew Kerner y Neil Padgett) porque eran socios fuertes de Intel en la architecture IA64. Creo que esto significaba que incluso si hubieran estado dispuestos a trabajar con los ingenieros de GCC en un ABI para usar tanto en Unix como en Windows, no lo hubieran hecho, ya que significaría apoyar públicamente el esfuerzo de AMD64 cuando no lo hubieran hecho. Todavía oficialmente lo hizo (y probablemente hubiera molestado a Intel).

Además de eso, en aquellos tiempos, Microsoft no tenía absolutamente ninguna inclinación a ser amigable con los proyectos de código abierto. Ciertamente no Linux o GCC.

Entonces, ¿por qué habrían cooperado en un ABI? Supongo que los ABI son diferentes simplemente porque fueron diseñados más o menos al mismo tiempo y de forma aislada.

Otra cita de “A History of Modern 64-bit Computing”:

En paralelo con la colaboración de Microsoft, AMD también se involucró con la comunidad de código abierto para prepararse para el chip. AMD contrató tanto a Code Sorcery como a SuSE para el trabajo de cadena de herramientas (Red Hat ya estaba contratada por Intel en el puerto de la cadena de herramientas IA64). Russell explicó que SuSE produjo comstackdores C y FORTRAN, y Code Sorcery produjo un comstackdor Pascal. Weber explicó que la compañía también se comprometió con la comunidad Linux para preparar un puerto Linux. Este esfuerzo fue muy importante: actuó como un incentivo para que Microsoft continuara invirtiendo en el esfuerzo de AMD64 para Windows, y también se aseguró de que Linux, que se estaba convirtiendo en un sistema operativo importante en ese momento, estuviera disponible una vez que se lanzaran los chips.

Weber llega al extremo de decir que el trabajo de Linux fue absolutamente crucial para el éxito de AMD64, porque permitió a AMD producir un sistema integral sin la ayuda de ninguna otra compañía si fuese necesario. Esta posibilidad aseguró que AMD tuviera una estrategia de supervivencia en el peor de los casos, incluso si otros socios se retiraban, lo que a su vez mantenía a los otros socios comprometidos por temor a que los dejaran atrás.

Esto indica que incluso AMD no sentía que la cooperación fuera necesariamente lo más importante entre MS y Unix, pero que tener soporte para Unix / Linux era muy importante. Tal vez incluso tratar de convencer a una o ambas partes de comprometerse o cooperar no valía la pena el esfuerzo o el riesgo (?) De irritar a ninguno de ellos. Tal vez AMD pensó que incluso sugerir un ABI común podría retrasar o descarrilar el objective más importante de simplemente tener soporte de software listo cuando el chip estuviera listo.

Las especulaciones por mi parte, pero creo que la principal razón por la que los ABI son diferentes fue la razón política por la que MS y los lados de Unix / Linux simplemente no trabajaron juntos en eso, y AMD no lo vio como un problema.

Intereting Posts