Golpeas la memoria pero no le dices a GCC al respecto, por lo que GCC puede almacenar en caché los valores en buf
a través de convocatorias de asamblea. Si quieres usar entradas y salidas, cuéntale todo a GCC.
__asm__ (
"movq %1, 0(%0)\n\t"
"movq %2, 8(%0)"
: /* Outputs (none) */
: "r"(buf), "r"(rrax), "r"(rrbx) /* Inputs */
: "memory"); /* Clobbered */
Por lo general, también desea dejar que GCC maneje la mayoría de los mov
, selección de registros, etc., incluso si restringe explícitamente los registros (rrax sigue siendo %rax
) deje que la información fluya a través de GCC o obtendrá resultados inesperados.
__volatile__
está mal.
La razón __volatile__
existe es para que pueda garantizar que el compilador coloque su código exactamente donde está... lo cual es completamente innecesario garantía para este código. Es necesario para implementar funciones avanzadas como barreras de memoria, pero casi completamente inútil si solo modifica la memoria y los registros.
GCC ya sabe que no puede mover este ensamblaje después de printf
porque el printf
accesos a llamadas buf
y buf
podría ser golpeado por la asamblea. GCC ya sabe que no puede mover el ensamblaje antes de rrax=0x39;
porque rax
es una entrada al código ensamblador. Entonces, ¿qué hace __volatile__
¿conseguirle? Nada.
Si su código no funciona sin __volatile__
entonces hay un error en el código que debe ser corregido en lugar de simplemente agregar __volatile__
y esperando que eso haga todo mejor. El __volatile__
La palabra clave no es mágica y no debe tratarse como tal.
Solución alternativa:
es __volatile__
necesario para su código original? No. Simplemente marque las entradas y los valores de clobber correctamente.
/* The "S" constraint means %rsi, "b" means %rbx, and "a" means %rax
The inputs and clobbered values are specified. There is no output
so that section is blank. */
rsi = (long) buf;
__asm__ ("movq %%rax, 0(%%rsi)" : : "a"(rrax), "S"(rssi) : "memory");
__asm__ ("movq %%rbx, 0(%%rsi)" : : "b"(rrbx), "S"(rrsi) : "memory");
Por qué __volatile__
no te ayuda aquí:
rrax = 0x34; /* Dead code */
GCC está en su derecho de eliminar por completo la línea anterior, ya que el código en la pregunta anterior afirma que nunca usa rrax
.
Un ejemplo más claro
long global;
void store_5(void)
{
register long rax asm ("rax");
rax = 5;
__asm__ __volatile__ ("movq %%rax, (global)");
}
El desmontaje es más o menos como lo esperas en -O0
,
movl $5, %rax
movq %rax, (global)
Pero con la optimización desactivada, puede ser bastante descuidado con el ensamblaje. Probemos -O2
:
movq %rax, (global)
¡Vaya! ¿De dónde salió rax = 5;
? ¿Vamos? Es código muerto, desde %rax
nunca se usa en la función, al menos hasta donde sabe GCC. GCC no mira dentro del ensamblaje. Qué sucede cuando eliminamos __volatile__
?
; empty
Bueno, podrías pensar __volatile__
le está haciendo un servicio al evitar que GCC deseche su preciado ensamblaje, pero solo enmascara el hecho de que GCC cree que su ensamblaje no está haciendo cualquier cosa. GCC piensa que su ensamblaje no toma entradas, no produce salidas y no golpea la memoria. Será mejor que lo arregles:
long global;
void store_5(void)
{
register long rax asm ("rax");
rax = 5;
__asm__ __volatile__ ("movq %%rax, (global)" : : : "memory");
}
Ahora obtenemos el siguiente resultado:
movq %rax, (global)
Mejor. Pero si le informa a GCC sobre las entradas, se asegurará de que %rax
se inicializa correctamente primero:
long global;
void store_5(void)
{
register long rax asm ("rax");
rax = 5;
__asm__ ("movq %%rax, (global)" : : "a"(rax) : "memory");
}
El resultado, con optimizaciones:
movl $5, %eax
movq %rax, (global)
¡Correcto! Y ni siquiera necesitamos usar __volatile__
.
¿Por qué __volatile__
existe?
El principal uso correcto de __volatile__
es si su código ensamblador hace algo más además de la entrada, la salida o la memoria. Quizás interfiere con registros especiales que GCC no conoce o afecta a IO. Lo ves mucho en el kernel de Linux, pero se usa mal con mucha frecuencia en el espacio del usuario.
El __volatile__
palabra clave es muy tentador porque a los programadores de C a menudo nos gusta pensar que estamos casi programación en lenguaje ensamblador ya. No eran. Los compiladores de C realizan muchos análisis de flujo de datos, por lo que debe explicar el flujo de datos al compilador para su código ensamblador. De esa forma, el compilador puede manipular de forma segura su parte del ensamblaje tal como manipula el ensamblaje que genera.
Si te encuentras usando __volatile__
mucho, como alternativa, podría escribir una función o módulo completo en un archivo de ensamblaje.
El compilador usa registros y puede escribir sobre los valores que ha ingresado en ellos.
En este caso, el compilador probablemente use el rbx
registrarse después del rrbx
asignación y antes de la sección de ensamblaje en línea.
En general, no debe esperar que los registros mantengan sus valores después y entre secuencias de código ensamblador en línea.
Ligeramente fuera de tema, pero me gustaría hacer un seguimiento del ensamblaje en línea de gcc.
La (no) necesidad de __volatile__
proviene del hecho de que GCC optimiza montaje en línea. GCC inspecciona la declaración de ensamblado en busca de efectos secundarios/requisitos previos, y si descubre que no existen, puede optar por mover la instrucción de ensamblado o incluso decidir eliminar eso. Todo __volatile__
lo que hace es decirle al compilador que "deje de preocuparse y ponga esto ahí".
Que normalmente no es lo que realmente quieres.
Aquí es donde la necesidad de restricciones entra. El nombre está sobrecargado y en realidad se usa para diferentes cosas en el ensamblaje en línea de GCC:
- las restricciones especifican los operandos de entrada/salida utilizados en el
asm()
bloquear - las restricciones especifican la "lista de clobber", que detalla qué "estado" (registros, códigos de condición, memoria) se ven afectados por el
asm()
. - las restricciones especifican clases de operandos (registros, direcciones, compensaciones, constantes, ...)
- las restricciones declaran asociaciones/enlaces entre entidades ensambladoras y variables/expresiones C/C++
En muchos casos, los desarrolladores abusan __volatile__
porque notaron que su código se movía o incluso desaparecía sin él. Si esto sucede, generalmente es una señal de que el desarrollador ha intentado no para informar a GCC sobre los efectos secundarios / requisitos previos del ensamblaje. Por ejemplo, este código con errores:
register int foo __asm__("rax") = 1234;
register int bar __adm__("rbx") = 4321;
asm("add %rax, %rbx");
printf("I'm expecting 'bar' to be 5555 it is: %d\n", bar);
Tiene varios errores:
- por un lado, solo se compila debido a un error de gcc (!). Normalmente, para escribir nombres de registro en ensamblador en línea, doble
%%
son necesarios, pero en lo anterior, si realmente los especifica, obtiene un error del compilador/ensamblador,/tmp/ccYPmr3g.s:22: Error: bad register name '%%rax'
. - segundo, no le dice al compilador cuándo y dónde necesita/usa las variables. En su lugar, supone el compilador respeta
asm()
literalmente. Eso podría ser cierto para Microsoft Visual C++ pero no es el caso para gcc.
Si lo compilas sin optimización, crea:
0000000000400524 <main>: [ ... ] 400534: b8 d2 04 00 00 mov $0x4d2,%eax 400539: bb e1 10 00 00 mov $0x10e1,%ebx 40053e: 48 01 c3 add %rax,%rbx 400541: 48 89 da mov %rbx,%rdx 400544: b8 5c 06 40 00 mov $0x40065c,%eax 400549: 48 89 d6 mov %rdx,%rsi 40054c: 48 89 c7 mov %rax,%rdi 40054f: b8 00 00 00 00 mov $0x0,%eax 400554: e8 d7 fe ff ff callq 400430 <[email protected]> [...]Puedes encontrar tu
add
instrucción y las inicializaciones de los dos registros, e imprimirá lo esperado. Si, por otro lado, aumenta la optimización, sucede algo más:0000000000400530 <main>: 400530: 48 83 ec 08 sub $0x8,%rsp 400534: 48 01 c3 add %rax,%rbx 400537: be e1 10 00 00 mov $0x10e1,%esi 40053c: bf 3c 06 40 00 mov $0x40063c,%edi 400541: 31 c0 xor %eax,%eax 400543: e8 e8 fe ff ff callq 400430 <[email protected]> [ ... ]Sus inicializaciones de ambos registros "usados" ya no están allí. El compilador los descartó porque nada que pudiera ver los estaba usando, y mientras mantuvo la instrucción de ensamblaje, la puso before cualquier uso de las dos variables. Está ahí pero no hace nada (Afortunadamente en realidad... si
rax
/ rbx
había estado en uso quién puede decir lo que hubiera pasado...).
Y la razón de esto es que en realidad no dijiste GCC que el ensamblado está usando estos registros/estos valores de operandos. Esto no tiene nada que ver con volatile
pero todo con el hecho de que estás usando un asm()
sin restricciones expresión.
La forma de hacer esto correctamente es a través de restricciones, es decir, usaría:
int foo = 1234;
int bar = 4321;
asm("add %1, %0" : "+r"(bar) : "r"(foo));
printf("I'm expecting 'bar' to be 5555 it is: %d\n", bar);
Esto le dice al compilador que el ensamblado:
- tiene un argumento en un registro,
"+r"(...)
que ambos deben inicializarse antes de la declaración de ensamblaje, y es modificado por la declaración de ensamblaje, y asociar la variablebar
con eso. - tiene un segundo argumento en un registro,
"r"(...)
que debe inicializarse antes de la declaración de ensamblaje y se trata como de solo lectura/no modificado por la declaración. Aquí, asociafoo
con eso.
Tenga en cuenta que no se especifica ninguna asignación de registro:el compilador elige eso según las variables / estado de la compilación. El resultado (optimizado) de lo anterior:
0000000000400530 <main>: 400530: 48 83 ec 08 sub $0x8,%rsp 400534: b8 d2 04 00 00 mov $0x4d2,%eax 400539: be e1 10 00 00 mov $0x10e1,%esi 40053e: bf 4c 06 40 00 mov $0x40064c,%edi 400543: 01 c6 add %eax,%esi 400545: 31 c0 xor %eax,%eax 400547: e8 e4 fe ff ff callq 400430 <[email protected]> [ ... ]Las restricciones de ensamblaje en línea de GCC son casi siempre necesarias de una forma u otra, pero puede haber varias formas posibles de describir los mismos requisitos al compilador; en lugar de lo anterior, también podría escribir:
asm("add %1, %0" : "=r"(bar) : "r"(foo), "0"(bar));
Esto le dice a gcc:
- la sentencia tiene un operando de salida, la variable
bar
, que después de la declaración se encontrará en un registro,"=r"(...)
- la declaración tiene un operando de entrada, la variable
foo
, que se colocará en un registro,"r"(...)
- el operando cero también es un operando de entrada y debe inicializarse con
bar
O, de nuevo una alternativa:
asm("add %1, %0" : "+r"(bar) : "g"(foo));
que le dice a gcc:
- bla (bostezo - igual que antes,
bar
tanto de entrada como de salida) - la declaración tiene un operando de entrada, la variable
foo
, que a la declaración no le importa si está en un registro, en la memoria o en una constante de tiempo de compilación (esa es la"g"(...)
restricción)
El resultado es diferente al anterior:
0000000000400530 <main>: 400530: 48 83 ec 08 sub $0x8,%rsp 400534: bf 4c 06 40 00 mov $0x40064c,%edi 400539: 31 c0 xor %eax,%eax 40053b: be e1 10 00 00 mov $0x10e1,%esi 400540: 81 c6 d2 04 00 00 add $0x4d2,%esi 400546: e8 e5 fe ff ff callq 400430 <[email protected]> [ ... ]porque ahora, GCC realmente se ha dado cuenta
foo
es una constante de tiempo de compilación y simplemente incrusta el valor en add
instrucción ! ¿No es genial?
Es cierto que esto es complejo y cuesta acostumbrarse. La ventaja es que dejar que el compilador elija qué registros usar para qué operandos permite optimizar el código en general; si, por ejemplo, se usa una declaración de ensamblaje en línea en una macro y/o un static inline
función, el compilador puede, según el contexto de llamada, elegir diferentes registros en diferentes instancias del código. O si un determinado valor es evaluable/constante en tiempo de compilación en un lugar pero no en otro, el compilador puede adaptar el ensamblaje creado para él.
Piense en las restricciones de ensamblaje en línea de GCC como una especie de "prototipos de funciones extendidas":le dicen al compilador qué tipos y ubicaciones son para los argumentos/valores devueltos, y un poco más. Si no especifica estas restricciones, su ensamblaje en línea está creando el análogo de funciones que operan solo en variables globales/estado, que, como probablemente todos estamos de acuerdo, rara vez hacen exactamente lo que pretendía.