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

Resuelto JanKanis asked hace 13 años • 0 respuestas

AMD tiene una especificación ABI que describe la convención de llamadas que se utilizará 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 puramente una cuestión de síndrome NIH?

Entiendo que diferentes sistemas operativos pueden tener diferentes necesidades de 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 usan rdi - rsi - rdx - rcx - r8 - r9 - rest on stack.

PD: Soy consciente de cómo difieren estas convenciones de llamadas en general y sé dónde encontrar 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 avatar Dec 13 '10 20:12 JanKanis
Aceptado

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 la codificación del nombre del registro al "número de registro" no es obvia; 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 (regs 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 pasar a 64 bits, las regulaciones "superiores" están ordenadas, y tanto Microsoft como UN*X/Linux optaron por R8/ R9como las primeras.

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é se eligió AMD64 UN*X ABI RDXantes RCX.

Elegir seis registros de argumentos en x64: específico de UN*X

UN*X, en arquitecturas RISC, tradicionalmente ha realizado el paso de argumentos en registros, específicamente, para los primeros seis argumentos (eso es así en PPC, SPARC, MIPS al menos). Esta podría ser una de las principales razones por las que los diseñadores de ABI AMD64 (UN*X) eligieron utilizar también seis registros en esa arquitectura.

Entonces, si desea que seis registros pasen argumentos y es lógico elegir RCX, y para RDXcuatro de ellos, ¿cuáles otros dos debería elegir?R8R9

Las regulaciones "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ás elegir ninguna de ellas si tienes opciones. De los registros clásicos, debido al significado implícito de RBPy RSP, estos no están disponibles y RBXtradicionalmente tienen un uso especial en UN*X (tabla de compensación global) con el que aparentemente los diseñadores de AMD64 ABI no querían volverse incompatibles innecesariamente.
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 la CPU repz movsb; retporque el origen/destino La persona que llama ha puesto las direcciones en los registros correctos. Lo hay, particularmente en el código "pegamento" de bajo nivel y generado por el compilador (piense, por ejemplo, en algunos asignadores de montón de C++ que llenan cero los objetos en la construcción, o las páginas del montón de relleno cero del núcleo en sbrk(), o fallas de página de copia en escritura ) una enorme cantidad de copia/relleno de bloques, por lo que será útil para el código utilizado con tanta frecuencia para guardar las dos o tres instrucciones de la CPU que, de otro modo, cargarían dichos argumentos de dirección de origen/destino en los registros "correctos".

Entonces, en cierto modo, UN*X y Win64 solo se diferencian en que UN*X "antepone" dos argumentos adicionales, en registros / elegidos intencionalmente , RSIa RDIla elección natural de cuatro argumentos en RCX, y .RDXR8R9

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 sorprendentemente en la forma en que se utiliza 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. Por otro lado, en UN*X, una función hoja (es decir, una que no llama a otras funciones) ni siquiera es necesaria para asignar espacio de pila 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 código del kernel, una fuente de errores ingeniosos). Todas estas son opciones de optimización particulares, la mayor parte del fundamento de ellas se explica en las referencias completas de ABI a las que apunta la referencia de Wikipedia del cartel original.

FrankH. avatar Dec 14 '2010 11:12 FrankH.

IDK por qué Windows hizo lo que hizo. Vea el final de esta respuesta para hacer una suposición. 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 hilos antiguos en la lista de correo AMD64, ya que los arquitectos de AMD estuvieron 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, UAXetc.

Además, los comentarios de los desarrolladores del kernel identificaron cosas que hacían que el diseño original syscallfuera swapgsinutilizable . Así es como AMD actualizó las instrucciones para solucionar este problema 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 SysV (Linux) y la decisión sobre cuántos registros deberían conservarse para el destinatario de la llamada o guardarse para el destinatario de la llamada, se tomó inicialmente en noviembre de 2000 por Jan Hubicka (un desarrollador de gcc). Compiló SPEC2000 y analizó el tamaño del código y la cantidad de instrucciones. Ese hilo de discusión gira 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 .

He's using the term "global" to mean call-preserved registers, that have to be push/popped if used.

The choice of rdi, rsi, rdx as the first three args was motivated by:

  • minor code-size saving in functions that call memset or other C string function on their args (where gcc inlines a rep string operation?)
  • rbx is call-preserved because having two call-preserved regs accessible without REX prefixes (rbx and rbp) is a win. Presumably chosen because they're the only "legacy" registers that aren't implicitly used by any common instruction. (rep string, shift count, and mul/div outputs/inputs touch everything else).
  • None of the registers that common instructions force you to use are call-preserved (see prev point), so a function that wants to use a variable-count shift or division might have to move function args somewhere else, but doesn't have to save/restore the caller's value. cmpxchg16b and cpuid need RBX, but are rarely used so not a big factor. (cmpxchg16b wasn't part of original AMD64, but RBX would still have been the obvious choice. cmpxchg8b exists but was obsoleted by qword cmpxchg)
  • We are trying to avoid RCX early in the sequence, since it is register used commonly for special purposes, like EAX, so it has same purpose to be missing in the sequence. Also it can't be used for syscalls and we would like to make syscall sequence to match function call sequence as much as possible.

(background: syscall / sysret unavoidably destroy rcx(with rip) and r11(with RFLAGS), so the kernel can't see what was originally in rcx when syscall ran.)

The kernel system-call ABI was chosen to match the function call ABI, except for r10 instead of rcx, so a libc wrapper functions like mmap(2) can just mov %rcx, %r10 / mov $0x9, %eax / syscall.


Note that the SysV calling convention used by i386 Linux sucks compared to Window's 32bit __vectorcall. It passes everything on the stack, and only returns in edx:eax for int64, not for small structs. It's no surprise little effort was made to maintain compatibility with it. When there's no reason not to, they did things like keeping rbx call-preserved, since they decided that having another in the original 8 (that don't need a REX prefix) was good.

Making the ABI optimal is much more important long-term than any other consideration. I think they did a pretty good job. I'm not totally sure about returning structs packed into registers, instead of different fields in different regs. I guess code that passes them around by value without actually operating on the fields wins this way, but the extra work of unpacking seems silly. They could have had more integer return registers, more than just rdx:rax, so returning a struct with 4 members could return them in rdi, rsi, rdx, rax or something.

They considered passing integers in vector regs, because SSE2 can operate on integers. Fortunately they didn't do that. Integers are used as pointer offsets very often, and a round-trip to stack memory is pretty cheap. Also SSE2 instructions take more code bytes than integer instructions.


I suspect Windows ABI designers might have been aiming to minimize differences between 32 and 64bit for the benefit of people that have to port asm from one to the other, or that can use a couple #ifdefs in some ASM so the same source can more easily build a 32 or 64bit version of a function.

Minimizing changes in the toolchain seems unlikely. An x86-64 compiler needs a separate table of which register is used for what, and what the calling convention is. Having a small overlap with 32bit is unlikely to produce significant savings in toolchain code size / complexity.

Peter Cordes avatar Feb 25 '2016 06:02 Peter Cordes