Un poco fuera de tema, pero me gustaría seguir un poco sobre el montaje en línea de gcc.
La (no) necesidad de __volatile__
proviene del hecho de que GCC optimiza el montaje en línea. GCC inspecciona el enunciado del ensamblaje en busca de efectos secundarios/requisitos previos, y si encuentra que no existen, puede elegir mover las instrucciones de ensamblaje o incluso decidir eliminarlo. Todo lo que __volatile__
hace es decirle al compilador "dejar de preocuparse y poner esto allí".
Que generalmente no es lo que realmente quieres.
Esto es donde la necesidad de limitaciones venir en el nombre está sobrecargado y, de hecho utiliza para diferentes cosas en el montaje del CCG en línea:.
- restricciones especifican operandos de entrada/salida utilizados en el bloque
asm()
- restricciones especifican la "lista de clobber", que detalla qué "estado" (registros, códigos de condición, memoria) se ven afectados por el
asm()
.
- restricciones especifican las clases de operandos (registros, direcciones, compensaciones, constantes, ...)
- limitaciones declaran asociaciones/entidades enlaces entre ensamblador y variables C/C++/expresiones
En muchos casos, los desarrolladores abuse__volatile__
porque notaron que su código se movía o incluso desaparecía sin él. Si esto sucede, generalmente es más bien una señal de que el desarrollador intentó 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 su parte, sólo se compila debido a un error de gcc. Normalmente, para escribir nombres de registro en el ensamblaje en línea, se necesita doble
%%
, pero en el ejemplo anterior si realmente los especifica, obtiene un error de 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, asume el compilador honra literalmente a
asm()
. Eso podría ser cierto para Microsoft Visual C++ pero es , no es el caso para gcc.
Si se compila sin optimización, se 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]>
[...]
usted puede encontrar su instrucción
add
, y las inicializaciones de los dos registros, y va a imprimir la espera. 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 los registros "usados" ya no están allí.El compilador las descartado porque no hay nada que pudiera ver los estaba utilizando, y si bien mantuvo las instrucciones de montaje se puso
antes de cualquier utilización de las dos variables. Es allí, pero no hace nada (en realidad ... Por suerte, si
rax
/
rbx
habían estado en uso que puede decir żqué han sucedido ...).
Y la razón de esto es que en realidad no se ha dijo a GCC que el conjunto está utilizando estos registros/estos valores de operando. Esto no tiene nada que ver con volatile
pero todos con el hecho de que estés usando un asm()
de expresión libre de restricción.
La manera de hacer esto correctamente es a través de restricciones, es decir, que tendría que utilizar:
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 la asamblea:
- tiene un argumento en un registro,
"+r"(...)
que tanto tiene que ser inicializado antes de la declaración de la Asamblea, y es modificado por la declaración de la Asamblea, y asociar la variable bar
con él.
- tiene un segundo argumento en un registro,
"r"(...)
que necesita ser inicializado antes de la declaración de la Asamblea y se trata como de sólo lectura/no modificado por el comunicado. Aquí, asocie foo
con eso.
Aviso que no se ha especificado ninguna asignación de registro - el compilador elige eso dependiendo de las variables/estado de la compilación.La salida (optimizada) de las anteriores:
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 ensamblado en línea de GCC son
casi siempre necesarias de alguna forma u otra, pero puede haber múltiples formas posibles de describir los mismos requisitos para el compilador; en lugar de lo anterior, también se puede escribir:
asm("add %1, %0" : "=r"(bar) : "r"(foo), "0"(bar));
Esto le dice a gcc:
- la declaración tiene un operando de salida, la variable
bar
, que después de la declaración se encuentra en un registro, "=r"(...)
- la declaración tiene un operando de entrada, la variable
foo
, que es para ser colocado en un registro, "r"(...)
- operando cero es también un operando de entrada y que se inicializa 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/salida)
- la declaración tiene un operando de entrada, la variable
foo
, que a la instrucción no le importa si está en un registro ter, en la memoria o una constante de tiempo de compilación (que es la "g"(...)
restricción)
El resultado es diferente de la 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
realidad ha descubierto
foo
es una constante de tiempo de compilación y simplemente incrustado el valor en la
add
instrucción! ¿No es eso limpio?
Es cierto que esto es complejo y requiere tiempo para acostumbrarse. La ventaja es que dejando que el compilador elija que registra para usar para qué operandos permite optimizar el código en general; si, por ejemplo, se utiliza una instrucción de ensamblaje en línea en una función de macro y/o static inline
, el compilador puede, dependiendo del contexto de llamada, elegir diferentes registros en diferentes instancias del código. O si un valor determinado es evaluable/constante en tiempo de compilación en un lugar pero no en otro, el compilador puede adaptar el ensamblaje creado para él.
Considere las restricciones de ensamblado en línea de GCC como una especie de "prototipos de funciones extendidas": le dicen al compilador qué tipos y ubicaciones tienen los argumentos/valores de retorno, más un poco más. Si no especifica estas restricciones, su ensamblaje en línea crea el análogo de funciones que operan solo en variables/estado globales, que, como probablemente todos estamos de acuerdo, casi nunca hacen exactamente lo que usted pensó.
¿Por qué no paso por el código con gdb para que pueda * ver * qué está pasando? –