visual studio para funcion ensamblador configurar assembler asm c gcc assembly visual-c++ inline-assembly

studio - ¿Cuál es la diferencia entre ''asm'', ''__asm'' y ''__asm__''?



visual studio asm (4)

Por lo que puedo decir, la única diferencia entre __asm { ... }; y __asm__("..."); es que el primero usa mov eax, var y el segundo usa movl %0, %%eax con :"=r" (var) al final. ¿Qué otras diferencias hay? ¿Y qué tal solo asm ?


Con el compilador gcc, no es una gran diferencia. asm o __asm o __asm__ son las mismas, solo se usan para evitar el propósito del espacio de nombres de conflictos (hay funciones definidas por el usuario que asm de nombres, etc.)


Cuál de ustedes usa depende de su compilador. Esto no es estándar como el lenguaje C.


Hay una enorme diferencia entre el ASM en línea de MSVC y el asm en línea C de GNU. La sintaxis de GCC está diseñada para una salida óptima sin necesidad de instrucciones desperdiciadas, para envolver una sola instrucción o algo así. La sintaxis de MSVC está diseñada para ser bastante simple, pero AFAICT es imposible de usar sin la latencia y las instrucciones adicionales de un viaje de ida y vuelta a través de la memoria para sus entradas y salidas.

Si está utilizando un ASM en línea por motivos de rendimiento, esto hace que MSVC in-line asm solo sea viable si escribe un ciclo completo en asm, no para envolver secuencias cortas en una función en línea. El siguiente ejemplo (envolviendo idiv con una función) es el tipo de cosas en las que MSVC es malo: ~ 8 instrucciones adicionales de almacenamiento / carga.

MSVC inline asm (utilizado por MSVC y probablemente icc, quizás también disponible en algunos compiladores comerciales):

  • mira tu ASM para descubrir qué registra tus pasos de código.
  • solo puede transferir datos a través de la memoria. Los datos que estaban en vivo en los registros son almacenados por el compilador para prepararse para su mov ecx, shift_count , por ejemplo. Entonces, usar una instrucción de un solo asm que el compilador no generará para usted implica un viaje de ida y vuelta por la memoria en el camino de entrada y salida.
  • más fácil para principiantes, pero a menudo es imposible evitar la sobrecarga de datos entrando y saliendo . Incluso además de las limitaciones de sintaxis, el optimizador en las versiones actuales de MSVC tampoco es bueno para optimizar los bloques de ASM en línea.

GNU C inline asm no es una buena forma de aprender asm . Tienes que entender asm muy bien para poder decirle al compilador sobre tu código. Y debes entender lo que los compiladores necesitan saber. Esa respuesta también tiene enlaces a otras guías en línea y preguntas y respuestas. La wiki de la etiqueta x86 tiene muchas cosas buenas para asm en general, pero solo enlaces a la de asm en línea GNU. (El contenido de esa respuesta también se aplica al asm en línea de GNU en plataformas que no sean x86).

La sintaxis inm del GNU C inline es utilizada por gcc, clang, icc, y tal vez algunos compiladores comerciales que implementan GNU C:

  • Tienes que decirle al compilador lo que tocas. De lo contrario, se producirá la rotura del código circundante en formas no obvias y difíciles de eliminar.
  • Potente pero difícil de leer, aprender y usar sintaxis para decirle al compilador cómo suministrar entradas y dónde encontrar las salidas. Por ejemplo, "c" (shift_count) hará que el compilador ponga la variable shift_count en ecx antes de que se ejecute su asm en línea.
  • extra torpe para grandes bloques de código, porque el asm tiene que estar dentro de una cadena constante. Entonces usted típicamente necesita

    "insn %[inputvar], %%reg/n/t" // comment "insn2 %%reg, %[outputvar]/n/t"

  • muy implacable / más difícil, pero permite una menor sobrecarga esp. para envolver instrucciones individuales . (El único propósito original del diseño fue envolver las instrucciones, por lo que tiene que indicarle al compilador especialmente sobre los números primitivos para evitar que use el mismo registro para una entrada y salida si eso es un problema).

Ejemplo: división entera de ancho completo ( div )

En una CPU de 32 bits, dividir un entero de 64 bits por un entero de 32 bits, o hacer una multiplicación completa (32x32-> 64), puede beneficiarse del asm en línea. gcc y clang no aprovechan idiv para (int64_t)a / (int32_t)b , probablemente porque la instrucción falla si el resultado no cabe en un registro de 32 bits. Así que a diferencia de este Q & A sobre cómo obtener el cociente y el resto de un div , este es un caso de uso para el asm en línea. (A menos que haya una manera de informar al compilador que el resultado se ajustará, por lo que idiv no tendrá fallas).

Usaremos convenciones de llamadas que colocan algunos argumentos en los registros (con hi incluso en el registro correcto ), para mostrar una situación que está más cerca de lo que verías al crear una función tan pequeña como esta.

MSVC

Tenga cuidado con las convenciones de llamadas register-arg cuando use inline-asm. Aparentemente, el soporte de los asedios en línea está tan mal diseñado / implementado que el compilador podría no guardar / restaurar registros arg alrededor del asm en línea, si esos argumentos no se usan en el asm en línea . Gracias @RossRidge por señalar esto.

// MSVC. Be careful with _vectorcall & inline-asm: see above // we could return a struct, but that would complicate things int _vectorcall div64(int hi, int lo, int divisor, int *premainder) { int quotient, tmp; __asm { mov edx, hi; mov eax, lo; idiv divisor mov quotient, eax mov tmp, edx; // mov ecx, premainder // Or this I guess? // mov [ecx], edx } *premainder = tmp; return quotient; // or omit the return with a value in eax }

Actualización: aparentemente deja un valor en eax o edx:eax y luego cae al final de una función no nula (sin return ) , incluso cuando está en línea . Supongo que esto funciona solo si no hay un código después de la declaración asm . Esto evita el almacenamiento / recargas para la salida (al menos para el quotient ), pero no podemos hacer nada con las entradas. En una función no en línea con stack args, ya estarán en la memoria, pero en este caso de uso estamos escribiendo una pequeña función que podría ser útil en línea.

Compilado con MSVC 19.00.23026 /O2 en rextester (con un main() que encuentra el directorio del exe y descarga la salida del asm del compilador a stdout ).

## My added comments use. ## ; ... define some symbolic constants for stack offsets of parameters ; 48 : int ABI div64(int hi, int lo, int divisor, int *premainder) { sub esp, 16 ; 00000010H mov DWORD PTR _lo$[esp+16], edx ## these symbolic constants match up with the names of the stack args and locals mov DWORD PTR _hi$[esp+16], ecx ## start of __asm { mov edx, DWORD PTR _hi$[esp+16] mov eax, DWORD PTR _lo$[esp+16] idiv DWORD PTR _divisor$[esp+12] mov DWORD PTR _quotient$[esp+16], eax ## store to a local temporary, not *premainder mov DWORD PTR _tmp$[esp+16], edx ## end of __asm block mov ecx, DWORD PTR _premainder$[esp+12] mov eax, DWORD PTR _tmp$[esp+16] mov DWORD PTR [ecx], eax ## I guess we should have done this inside the inline asm so this would suck slightly less mov eax, DWORD PTR _quotient$[esp+16] ## but this one is unavoidable add esp, 16 ; 00000010H ret 8

Hay un montón de instrucciones mov adicionales, y el compilador ni siquiera se acerca a optimizar nada de eso. Pensé que tal vez vería y entendería el mov tmp, edx dentro del asm en línea, y lo convertiría en una tienda para premainder . Pero eso requeriría cargar premainder de la pila en un registro antes del bloque asm en línea, supongo.

Esta función es en realidad peor con _vectorcall que con el ABI normal de todo en la pila. Con dos entradas en los registros, las almacena en la memoria para que el asm en línea pueda cargarlas desde variables nombradas. Si esto estuviera en línea, incluso más de los parámetros podrían estar en los regs, y tendría que almacenarlos todos, ¡así que el asm tendría operandos de memoria! Entonces, a diferencia de gcc, no ganamos mucho al subrayar esto.

Hacer *premainder = tmp dentro del bloque asm significa más código escrito en asm, pero evita la ruta de almacenamiento / carga / almacenamiento de braindead total para el resto. Esto reduce el recuento de instrucciones en un total de 2, hasta 11 (sin incluir el ret ).

Estoy tratando de obtener el mejor código posible de MSVC, no "usarlo mal" y crear un argumento de hombre de paja. Pero AFAICT es horrible para envolver secuencias muy cortas. Es de suponer que hay una función intrínseca para la división 64/32 -> 32 que permite al compilador generar un buen código para este caso en particular, por lo que la premisa de usar asm en línea para esto en MSVC podría ser un argumento de hombre de paja . Pero te muestra que los intrínsecos son mucho mejores que los asm en línea para MSVC.

GNU C (gcc / clang / icc)

Gcc es incluso mejor que la salida que se muestra aquí al incluir div64, porque normalmente puede organizar que el código anterior genere el entero de 64 bits en edx: eax en primer lugar.

No puedo hacer que gcc compile para el vector de 32 bits ABI. Clang puede, pero apesta a asm en línea con restricciones "rm" (pruébelo en el enlace godbolt: rebota la función arg a través de la memoria en lugar de usar la opción de registro en la restricción). La convención de llamadas MS de 64 bits está cerca del vectorcall de 32 bits, con los dos primeros parametros en edx, ecx. La diferencia es que 2 params más entran en regs antes de usar la pila (y que el callee no saca los argumentos de la pila, que es de lo que se trataba el ret 8 en la salida de MSVC).

// GNU C // change everything to int64_t to do 128b/64b -> 64b division // MSVC doesn''t do x86-64 inline asm, so we''ll use 32bit to be comparable int div64(int lo, int hi, int *premainder, int divisor) { int quotient, rem; asm ("idivl %[divsrc]" : "=a" (quotient), "=d" (rem) // a means eax, d means edx : "d" (hi), "a" (lo), [divsrc] "rm" (divisor) // Could have just used %0 instead of naming divsrc // note the "rm" to allow the src to be in a register or not, whatever gcc chooses. // "rmi" would also allow an immediate, but unlike adc, idiv doesn''t have an immediate form : // no clobbers ); *premainder = rem; return quotient; }

compilado con gcc -m64 -O3 -mabi=ms -fverbose-asm . Con -m32 solo obtienes 3 cargas, idiv y una tienda, como puedes ver al cambiar cosas en ese enlace de godbolt.

mov eax, ecx # lo, lo idivl r9d # divisor mov DWORD PTR [r8], edx # *premainder_7(D), rem ret

Para vectorcall de 32 bits, gcc haría algo así como

## Not real compiler output, but probably similar to what you''d get mov eax, ecx # lo, lo mov ecx, [esp+12] # premainder idivl [esp+16] # divisor mov DWORD PTR [ecx], edx # *premainder_7(D), rem ret 8

MSVC usa 13 instrucciones (sin incluir el ret), en comparación con las 4 de gcc. Con la alineación, como ya he dicho, compila potencialmente a solo una, mientras que MSVC todavía usaría probablemente 9. (No será necesario reservar espacio en la pila o cargar premainder ; supongo que todavía tiene que almacenar aproximadamente 2 de las 3 entradas. Luego las recarga dentro del asm, ejecuta idiv , almacena dos salidas y las recarga fuera del asm. Así que eso es 4 cargas / tiendas para entrada, y otros 4 para salida.)


asm vs __asm__ en GCC

-std=c99 no funciona con -std=c99 , tienes dos alternativas:

  • usa __asm__
  • use -std=gnu99

Más detalles: error: ''asm'' no declarado (primer uso en esta función)

__asm vs __asm__ en GCC

No pude encontrar dónde está documentado __asm (especialmente no mencionado en https://gcc.gnu.org/onlinedocs/gcc-7.2.0/gcc/Alternate-Keywords.html#Alternate-Keywords ), pero de la fuente de GCC 8.1 Son exactamente lo mismo:

{ "__asm", RID_ASM, 0 }, { "__asm__", RID_ASM, 0 },

así que solo usaría __asm__ que está documentado.