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 stack
mientras 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í.
fuente
Respuestas:
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
__fastcall
convenció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 eligieronR8
/R9
como las primeras.Teniendo esto en mente, la elección de de Microsoft
RAX
(valor de retorno) yRCX
,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ó
RDX
antesRCX
.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
,R8
yR9
para 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
RBP
yRSP
estos no están disponibles, yRBX
tradicionalmente 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
/RDI
como registros de argumentos, ¿qué argumentos deberían ser?Hacerlos
arg[0]
yarg[1]
tiene algunas ventajas. Vea el comentario de cHao.?SI
y?DI
son 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, lastrcpy()
función más simple posible , por ejemplo, solo consta de las dos instrucciones de CPUrepz movsb; ret
porque 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
/RDI
registros, a la elección natural de cuatro argumentos enRCX
,RDX
,R8
yR9
.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.
fuente
__fastcall
son 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.memcpy
que podría implementarse de esa manera, nostrcpy
.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 como
UAX
.Además, la retroalimentación de los proyectos de desarrollo del kernel cosas identificadas que hicieron el diseño original de
syscall
eswapgs
inutilizable . 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
,rdx
como los tres primeros argumentos fue motivada por:memset
u otra función de cadena C en sus argumentos (¿donde gcc inserta una operación de cadena de repetición?)rbx
se 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).(fondo:
syscall
/sysret
inevitablemente destruirrcx
(conrip
) yr11
(conRFLAGS
), por lo que el kernel no puede ver lo que estaba originalmentercx
cuando sesyscall
ejecutó).La llamada al sistema del kernel ABI se eligió para que coincida con la llamada a la función ABI, excepto en
r10
lugar dercx
, por lo que un contenedor de libc funciona comommap(2)
can justmov %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: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 razón para no hacerlo, hicieron cosas como mantener larbx
llamada 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
#ifdef
s 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.
fuente
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":
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.
fuente
__vectorcall
porque pasar__m128
la 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).alloca
o en algunos otros casos). Esto es normal si está acostumbrado agcc -fomit-frame-pointer
ser 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-pointer
ha sido el predeterminado (con la optimización habilitada) desde siempre en x86-64, y otros compiladores (como MSVC) hacen lo mismo.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.
fuente
__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__fastcall
realidad 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.