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

110

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 las razones técnicas, históricas o políticas de esta diferencia, o es una cuestión puramente del síndrome de los NIH?

Entiendo que diferentes sistemas operativos pueden tener diferentes necesidades para cosas de nivel superior, pero eso no explica por qué, por ejemplo, el orden de paso del parámetro de registro en Windows es rcx - rdx - r8 - r9 - rest on stackmientras todos los demás lo 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 los detalles si es necesario. Lo que quiero saber es por qué .

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

JanKanis
fuente
2
Bueno, solo para el primer registro: rcx: ecx era el parámetro "this" para la convención msvc __thiscall x86. Así que probablemente solo para facilitar la migración de su compilador a x64, comenzaron con rcx como el primero. El hecho de que todo lo demás también sería diferente fue solo una consecuencia de esa decisión inicial.
Chris Becke
@Chris: Agregué una referencia al documento complementario AMD64 ABI (y algunas explicaciones sobre lo que realmente es) a continuación.
FrankH.
1
No he encontrado una justificación de la EM, pero encontré algo de discusión aquí
phuclv

Respuestas:

81

Elegir cuatro registros de argumentos en x64 - común a UN * X / Win64

Una de las cosas a tener en cuenta acerca de x86 es que el nombre del registro para la codificación del "número de registro" no es obvio; en términos de codificación 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 (reglas 0..2) como valor de retorno y los dos primeros argumentos (que es la __fastcallconvención "clásica" de 32 bits ) es una elección lógica. En lo que respecta a ir a 64 bits, las reglas "superiores" están ordenadas, y tanto Microsoft como UN * X / Linux eligieron R8/ R9como las primeras.

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

No sé por qué el AMD64 UN * X ABI eligió RDXantes RCX.

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

UN * X, en arquitecturas 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 principales razones por las que los diseñadores de ABI AMD64 (UN * X) eligieron usar seis registros en esa arquitectura también.

Así que si quieres seis registros para pasar argumentos en, y es lógico que elegir RCX, RDX, R8y R9para cuatro de ellos, que otros dos debe escoger?

Las reglas "superiores" requieren un byte de prefijo de instrucción adicional para seleccionarlas y, por lo tanto, tienen un tamaño de instrucción más grande, por lo que no querrá elegir ninguna de ellas si tiene opciones. De los registros clásicos, debido al significado implícito de RBPy RSPestos no están disponibles, y RBXtradicionalmente tiene un uso especial en UN * X (tabla de compensación global) con el que aparentemente los diseñadores de ABI AMD64 no querían volverse innecesariamente incompatibles.
Ergo, la única opción era RSI/ RDI.

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

Hacerlos arg[0]y arg[1]tiene algunas ventajas. Vea el comentario de cHao.
?SIy ?DIson operandos de origen / destino de instrucciones de cadena, y como mencionó cHao, su uso como registros de argumentos significa que con las convenciones de llamada AMD64 UN * X, la strcpy()función más simple posible , por ejemplo, solo consta de las dos instrucciones de CPU repz movsb; retporque el origen / destino La persona que llama ha introducido las direcciones en los registros correctos. Existe, particularmente en el código de "pegamento" de bajo nivel y generado por el compilador (piense, por ejemplo, en algunos asignadores de montón de C ++ objetos de relleno cero en la construcción, o las páginas de montón de relleno cero del kernel ensbrk(), o fallas de página de copia en escritura) una enorme cantidad de copia / relleno de bloque, por lo tanto, será útil para el código que se usa con tanta frecuencia para guardar las dos o tres instrucciones de la CPU que de otra manera cargarían dichos argumentos de dirección de origen / destino en el registros "correctos".

Así pues, en cierto modo, UN * X y Win64 sólo son diferentes en que la ONU * X "antepone" dos argumentos adicionales, a propósito de elegidos RSI/ RDIregistros, a la elección natural de cuatro argumentos en RCX, RDX, R8y R9.

Más allá de eso ...

Hay más diferencias entre las ABI de UN * X y Windows x64 que solo la asignación de argumentos a registros específicos. Para obtener una descripción general de Win64, consulte:

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 el espacio de pila; en Win64, por ejemplo, la persona que llama debe asignar espacio de pila para los argumentos de la función aunque los argumentos 0 ... 3 se pasen en los registros. En UN * X, por otro lado, una función de hoja (es decir, una que no llama a otras funciones) ni siquiera se requiere para asignar espacio de pila en absoluto si no necesita más de 128 bytes (sí, usted posee y puede usar una cierta cantidad de pila sin asignarla ... bueno, a menos que sea el código del kernel, una fuente de errores ingeniosos). Todas estas son opciones de optimización particulares, la mayor parte de la justificación para ellas se explica en las referencias ABI completas a las que apunta la referencia de wikipedia del póster original.

FrankH.
fuente
1
Acerca de los nombres de registro: ese byte de prefijo puede ser un factor. Pero entonces sería más lógico que MS elija rcx - rdx - rdi - rsi como registros de argumentos. Pero el valor numérico de los primeros ocho podría guiarlo si está diseñando un ABI desde cero, pero no hay razón para cambiarlos si ya existe un ABI perfectamente bueno, eso solo conduce a más confusión.
JanKanis
2
En RSI / RDI: estas instrucciones generalmente estarán en línea, en cuyo caso la convención de llamada no importa. De lo contrario, solo hay una copia (o quizás algunas) de esa función en todo el sistema, por lo que solo ahorra un puñado de bytes en total . No vale la pena. Sobre otras diferencias / pila de llamadas: la utilidad de opciones específicas se explica en las referencias de ABI, pero no hacen una comparación. No dicen por qué no se eligieron otras optimizaciones, por ejemplo, ¿por qué Windows no tiene la zona roja de 128 bytes y por qué AMD ABI no tiene las ranuras de pila adicionales para los argumentos?
JanKanis
1
@cHao: no. Pero lo cambiaron de todos modos. La ABI de Win64 es diferente de la de Win32 (y no es compatible), y también es diferente de la ABI de AMD.
JanKanis
7
@Somejan: Win64 y Win32 __fastcallson 100% idénticos para el caso de no tener más de dos argumentos no mayores de 32 bits y devolver un valor no mayor de 32 bits. Esa no es una pequeña clase de funciones. No es posible tal compatibilidad con versiones anteriores entre las ABI UN * X para i386 / amd64.
FrankH.
2
@szx: Acabo de encontrar el hilo de la lista de correo relevante de noviembre de 2000 y publiqué una respuesta que resume el razonamiento. Tenga en cuenta que es lo memcpyque podría implementarse de esa manera, no strcpy.
Peter Cordes
42

IDK por qué Windows hizo lo que hizo. Vea el final de esta respuesta para una suposición. Tenía curiosidad acerca de 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 hilos en la lista de correo de AMD64, ya que los arquitectos de AMD estaban activos en ellos. Por ejemplo, elegir los nombres de los registros 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 comoUAX .

Además, la retroalimentación de los proyectos de desarrollo del kernel cosas identificadas que hicieron el diseño original de syscalle swapgsinutilizable . Así es como AMD actualizó las instrucciones para solucionar esto antes de lanzar cualquier chip real. También es interesante que a finales de 2000, se suponía que Intel probablemente no adoptaría AMD64.


La convención de llamadas de SysV (Linux) y la decisión sobre cuántos registros deben conservarse en lugar de guardar llamadas se realizó inicialmente en noviembre de 2000 por Jan Hubicka (un desarrollador de gcc). Se compiló SPEC2000 y miró tamaño del código y número de instrucciones. Ese hilo de discusión rebota en 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 .

Está usando el término "global" para referirse a registros de llamadas preservadas, que deben ser push / popped si se usan.

La elección de rdi, rsi, rdxcomo los tres primeros argumentos fue motivada por:

  • ahorro menor del tamaño del código en funciones que llaman memsetu otra función de cadena C en sus argumentos (¿donde gcc inserta una operación de cadena de repetición?)
  • rbxse conserva en llamadas porque tener dos registros de llamadas conservadas accesibles sin prefijos REX (rbx y rbp) es una victoria. Es de suponer que se eligió porque es el único otro registro que no se utiliza implícitamente en ninguna instrucción. (la cadena de repeticiones, el recuento de turnos y las salidas / entradas mul / div tocan todo lo demás).
  • Ninguno de los registros con propósitos especiales se conserva en llamadas (consulte el punto anterior), por lo que una función que quiera usar instrucciones de cadena de repetición o un cambio de recuento de variables podría tener que mover los argumentos de función a otro lugar, pero no tiene que guardar / restaurar el valor de la persona que llama.
  • Estamos tratando de evitar RCX al principio de la secuencia, ya que se usa un registro comúnmente para propósitos especiales, como EAX, por lo que tiene el mismo propósito que falte en la secuencia. Además, no se puede usar para llamadas al sistema y nos gustaría hacer que la secuencia de llamada al sistema coincida con la secuencia de llamada de función tanto como sea posible.

    (fondo: syscall/ sysretinevitablemente destruir rcx(con rip) y r11(con RFLAGS), por lo que el kernel no puede ver lo que estaba originalmente rcxcuando se syscallejecutó).

La llamada al sistema del kernel ABI se eligió para que coincida con la llamada a la función ABI, excepto en r10lugar de rcx, por lo que un contenedor de libc funciona como mmap(2)can just mov %rcx, %r10/ mov $0x9, %eax/ syscall.


Tenga en cuenta que la convención de llamadas SysV utilizada por i386 Linux apesta en comparación con __vectorcall de 32 bits de Windows. Pasa todo en la pila y solo regresa edx:eaxpara 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 razón para no hacerlo, hicieron cosas como mantener la rbxllamada preservada, 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 buen trabajo. No estoy totalmente seguro de devolver estructuras empaquetadas en registros, en lugar de diferentes campos en diferentes registros. Supongo que el código que los transmite por valor sin operar realmente en los campos gana de esta manera, pero el trabajo adicional de desempaquetar parece una tontería. Podrían haber tenido más registros de retorno de enteros, 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 registros vectoriales, porque SSE2 puede operar con enteros. Afortunadamente no hicieron eso. Los enteros se utilizan muy a menudo como compensaciones de puntero, y un viaje de ida y vuelta a la memoria de pila 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 objetivo minimizar las diferencias entre 32 y 64 bits para el beneficio de las personas que tienen que portar asm de uno a otro, o que pueden usar un par de #ifdefs en algunos ASM para que la misma fuente pueda compilar más 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 compilador x86-64 necesita una tabla separada de qué registro se usa para qué y cuál es la convención de llamada. Es poco probable que tener una pequeña superposición con 32 bits produzca ahorros significativos en el tamaño / complejidad del código de la cadena de herramientas.

Peter Cordes
fuente
1
Creo que he leído en algún lugar del blog de Raymond Chen sobre la justificación para elegir esos registros después de la evaluación comparativa desde el lado de la EM, pero ya no puedo encontrarlos. Sin embargo, aquí se explicaron algunas razones con respecto a la zona de inicio blogs.msdn.microsoft.com/oldnewthing/20160623-00/?p=93735 blogs.msdn.microsoft.com/freik/2006/03/06/…
phuclv
@phuclv: Ver también ¿Es válido escribir debajo de ESP? . Los comentarios de Raymond sobre mi respuesta allí señalaron algunos detalles de SEH que no sabía que explican por qué x86 32/64 Windows no tiene actualmente una zona roja de facto. Su publicación de blog tiene algunos casos plausibles para la misma posibilidad de controlador de entrada de página de código que mencioné en esa respuesta :) Así que sí, Raymond hizo un mejor trabajo al explicarlo que yo (como era de esperar porque comencé a saber muy poco sobre Windows), y la tabla de tamaños de zona roja para no x86 es realmente ordenada.
Peter Cordes
13

Recuerde que Microsoft inicialmente "no se comprometió oficialmente con el esfuerzo inicial de AMD64" (de "Una historia de la informática moderna de 64 bits" por Matthew Kerner y Neil Padgett) porque eran socios fuertes de Intel en la arquitectura IA64. Creo que esto significaba que incluso si hubieran estado dispuestos a trabajar con ingenieros de GCC en una ABI para usar tanto en Unix como en Windows, no lo habrían hecho, ya que significaría respaldar públicamente el esfuerzo de AMD64 cuando no lo habían hecho. Aún no lo he hecho oficialmente (y probablemente habría molestado a Intel).

Además de eso, en aquellos días Microsoft no tenía ninguna inclinación hacia 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 se diseñaron más o menos al mismo tiempo y de forma aislada.

Otra cita de "Una historia de la informática moderna de 64 bits":

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

Weber llega a decir que el trabajo de Linux fue absolutamente crucial para el éxito de AMD64, porque permitió a AMD producir un sistema de extremo a extremo sin la ayuda de ninguna otra empresa si fuera 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 mantuvo a los demás socios comprometidos por temor a quedarse atrás.

Esto indica que incluso AMD no sintió que la cooperación fuera necesariamente lo más importante entre MS y Unix, pero que tener soporte para Unix / Linux era muy importante. ¿Quizás incluso tratar de convencer a una o ambas partes de que se comprometieran o cooperaran no valía la pena el esfuerzo o el riesgo (?) De irritar a cualquiera de ellos. Quizás AMD pensó que incluso sugerir una ABI común podría retrasar o descarrilar el objetivo más importante de simplemente tener listo el soporte de software cuando el chip esté listo.

Especulación de mi parte, pero creo que la razón principal por la que las ABI son diferentes fue la razón política por la que MS y las partes Unix / Linux simplemente no funcionaron juntas, y AMD no vio eso como un problema.

Michael Burr
fuente
Bonita perspectiva de la política. Estoy de acuerdo en que no es culpa ni responsabilidad de AMD. Culpo a Microsoft por elegir una convención de llamadas peor. Si su convención de llamadas hubiera resultado ser mejor, tendría algo de simpatía, pero tuvieron que cambiar de su ABI inicial a __vectorcallporque pasar __m128la pila apestaba. Tener semántica preservada de llamadas para el bajo 128b de algunas de las reglas vectoriales también es extraño (en parte es culpa de Intel por no diseñar un mecanismo extensible de guardar / restaurar con SSE originalmente, y aún no con AVX).
Peter Cordes
1
Realmente no tengo ninguna experiencia o conocimiento de lo buenos que son los ABI. De vez en cuando necesito saber cuáles son para poder entender / depurar a nivel de ensamblaje.
Michael Burr
1
Una buena ABI minimiza el tamaño del código y el número de instrucciones, y mantiene las cadenas de dependencia con baja latencia al evitar viajes de ida y vuelta adicionales a través de la memoria. (para argumentos, o para locales que necesitan ser derramados / recargados). Hay compensaciones. La zona roja de SysV toma un par de instrucciones adicionales en un solo lugar (el despachador del manejador de señales del kernel), para un beneficio relativamente grande para las funciones hoja de no tener que ajustar el puntero de la pila para obtener algo de espacio de borrador. Así que esa es una clara victoria con una desventaja cercana a cero. Fue adoptado prácticamente sin discusión después de que se propuso para SysV.
Peter Cordes
1
@dgnuff: Correcto, esa es la respuesta a ¿Por qué el código del kernel no puede usar una Zona Roja ? Las interrupciones usan la pila del kernel, no la pila de espacio de usuario, incluso si llegan cuando la CPU está ejecutando código de espacio de usuario. El kernel no confía en las pilas de espacio de usuario porque otro hilo en el mismo proceso de espacio de usuario podría modificarlo, ¡asumiendo el control del kernel!
Peter Cordes
1
@ DavidA.Gray: sí, la ABI no dice que tenga que usar RBP como un puntero de cuadro, por lo que el código optimizado generalmente no lo hace (excepto en las funciones que usan allocao en algunos otros casos). Esto es normal si está acostumbrado a gcc -fomit-frame-pointerser el predeterminado en Linux. La ABI define metadatos de desenrollado de pila que permiten que el manejo de excepciones aún funcione. (Supongo que funciona algo así como las cosas CFI de GNU / Linux x86-64 System V .eh_frame). gcc -fomit-frame-pointerha sido el predeterminado (con la optimización habilitada) desde siempre en x86-64, y otros compiladores (como MSVC) hacen lo mismo.
Peter Cordes
12

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 utilizan para transmitir argumentos de función.

Sin embargo, no podría decirte por qué se cambian RCX y RDX.

cHao
fuente
1
Todas las convenciones de llamadas tienen algunos registros designados como scratch y algunos como conservados, como ESI / EDI y RSI / RDI en Win64. Pero esos son registros de propósito general, Microsoft podría haber elegido sin problema usarlos de manera diferente.
JanKanis
1
@Somejan: Claro, si quisieran reescribir toda la API y tener dos sistemas operativos diferentes. Sin embargo, yo no llamaría a eso "sin problemas". Durante decenas de años, MS ha hecho ciertas promesas sobre lo que hará y lo que no hará con los registros x86, y han sido más o menos consistentes y compatibles todo ese tiempo. No van a tirar todo eso por la ventana solo por algún edicto de AMD, especialmente uno tan arbitrario y fuera del ámbito de "construir un procesador".
cHao
5
@Somejan: La ABI AMD64 UN * X siempre fue exactamente eso: una pieza específica de UNIX . El documento, x86-64.org/documentation/abi.pdf , se titula Interfaz binaria de la aplicación System V, Suplemento del procesador de arquitectura AMD64 por una razón. Las ABI (comunes) de UNIX (una colección de varios volúmenes, sco.com/developers/devspecs ) dejan una sección para el capítulo 3 específico del procesador, el Suplemento , que son las convenciones de llamada de funciones y las reglas de disposición de datos para un procesador específico.
FrankH.
7
@Somejan: Microsoft Windows nunca ha intentado estar particularmente cerca de UN * X, y cuando se trató de portar Windows a x64 / AMD64, simplemente eligieron extender su propia __fastcall convención de llamadas. Usted afirma que Win32 / Win64 no son compatibles, pero luego, mire de cerca: para una función que toma dos argumentos de 32 bits y devuelve 32 bits, Win64 y Win32 en __fastcallrealidad son 100% compatibles (las mismas reglas para pasar dos argumentos de 32 bits, el mismo valor de retorno). Incluso algunos códigos binarios (!) Pueden funcionar en ambos modos de funcionamiento. El lado de UNIX rompió por completo con las "viejas formas". Por buenas razones, pero un descanso es un descanso.
FrankH.
2
@Olof: Es algo más que un compilador. Tuve problemas con ESI y EDI cuando hice cosas independientes en NASM. Windows definitivamente se preocupa por esos registros. Pero sí, puede usarlos si los guarda antes de hacerlo y los restaura antes de que Windows los necesite.
cHao