2012-01-17 27 views
9

Para el siguiente código:¿Por qué este conjunto en línea no funciona?

long buf[64]; 

register long rrax asm ("rax"); 
register long rrbx asm ("rbx"); 
register long rrsi asm ("rsi"); 

rrax = 0x34; 
rrbx = 0x39; 

__asm__ __volatile__ ("movq $buf,%rsi"); 
__asm__ __volatile__ ("movq %rax, 0(%rsi);"); 
__asm__ __volatile__ ("movq %rbx, 8(%rsi);"); 

printf("buf[0] = %lx, buf[1] = %lx!\n", buf[0], buf[1]); 

me sale el siguiente resultado:

buf[0] = 0, buf[1] = 346161cbc0! 

mientras que debería haber sido:

buf[0] = 34, buf[1] = 39! 

¿Alguna idea de por qué no está funcionando correctamente, ¿Y como resolverlo?

+2

¿Por qué no paso por el código con gdb para que pueda * ver * qué está pasando? –

Respuesta

22

Guarda la memoria, pero no le cuentes a GCC al respecto, por lo que GCC puede almacenar valores en caché en buf en llamadas de ensamblaje. Si desea usar entradas y salidas, cuéntele a GCC todo.

__asm__ (
    "movq %1, 0(%0)\n\t" 
    "movq %2, 8(%0)" 
    :        /* Outputs (none) */ 
    : "r"(buf), "r"(rrax), "r"(rrbx) /* Inputs */ 
    : "memory");      /* Clobbered */ 

También generalmente quieren dejar GCC manejar la mayor parte del mov, regístrese selección, etc - incluso si la restringirá de manera explícita los registros (rrax es stil %rax) dejar que el flujo de información a través de GCC o se obtendrá inesperada resultados.

__volatile__ es incorrecto.

La razón por la cual __volatile__ existe es para que pueda garantizar que el compilador coloca su código exactamente donde está ... que es completamente innecesaria garantía para este código. Es necesario para implementar funciones avanzadas como barreras de memoria, pero casi inútil si solo está modificando la memoria y los registros.

CCG ya se sabe que no se puede mover esta asamblea después de printf porque la llamada printf accede buf, buf y podría ser una paliza por la asamblea. GCC ya sabe que no puede mover el ensamblaje antes de rrax=0x39; porque rax es una entrada al código de ensamblaje. Entonces, ¿qué te trae __volatile__? Nada.

Si el código no funciona sin __volatile__ entonces hay un error en el código que debe ser fijado en lugar de limitarse a añadir __volatile__ y esperando que hace que todo sea mejor. La palabra clave __volatile__ 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 le ayuda aquí:

rrax = 0x34; /* Dead code */ 

GCC está bien dentro de sus derechos para eliminar por completo la línea de arriba, 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 espera en -O0,

movl $5, %rax 
movq %rax, (global) 

Pero con la optimización de fuera, puede ser bastante descuidada sobre el montaje. Probemos -O2:

movq %rax, (global) 

¡Vaya! ¿A dónde se fue rax = 5;? Es código muerto, ya que %rax nunca se usa en la función, al menos en la medida en que lo sabe GCC. GCC no se asoma dentro del ensamblaje. ¿Qué sucede cuando eliminamos __volatile__?

; empty 

Bueno, se podría pensar __volatile__ que está haciendo un servicio al mantener GCC de descartar su precioso montaje, pero es sólo enmascarar el hecho de que GCC piensa que su montaje no está haciendo nada. GCC cree que su conjunto no toma entradas, no produce salidas y no deja memoria. Usted había enderezar mejor hacia fuera:

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 usted le dice CCG sobre las entradas, que se asegurará de que %rax se ha inicializado correctamente en primer lugar:

long global; 
void store_5(void) 
{ 
    register long rax asm ("rax"); 
    rax = 5; 
    __asm__ ("movq %%rax, (global)" : : "a"(rax) : "memory"); 
} 

La salida, con optimizaciones:

correcta! Y ni siquiera necesitamos usar __volatile__.

¿Por qué existe __volatile__?

El uso correcto principal para __volatile__ es si su código de ensamblaje hace algo más además de la entrada, la salida o la memoria de trituración. Tal vez se mete con registros especiales que GCC no conoce o que afectan a IO. Lo ves mucho en el kernel de Linux, pero se usa mal a menudo en el espacio de usuario.

La palabra clave __volatile__ es muy tentadora porque a los programadores de C a menudo les gusta pensar que estamos casi programando en lenguaje de ensamblado ya. No eran. Los compiladores C realizan una gran cantidad de análisis de flujo de datos, por lo que debe explicar el flujo de datos al compilador para su código ensamblador. De esta forma, el compilador puede manipular de forma segura su fragmento de ensamblaje tal como lo hace con el ensamblaje que genera.

Si se encuentra usando __volatile__ mucho, como alternativa podría escribir una función o módulo completo en un archivo de ensamblaje.

+1

__volatile__ in asm es decirle al compilador que coloque el código exactamente donde se coloca. No es como volátil para las variables. – MetallicPriest

+3

@MetallicPriest: Sí, eso es exactamente lo que es volátil, y es por eso que no es necesario aquí. Si no comprende eso, lea el CÓMO * de ensamblaje en línea de GCC desde el principio hasta el final * porque no ayuda a omitir los trozos. –

+0

Tendría tu respuesta si no fuera por el negrito "' __volatile__' es incorrecto "; eso se debe a que el ensamblaje _como lo indica el cartel original_ realmente lo necesita. Está mal por muchas otras razones (como también anotó, el clopber perdido). No obstante, las declaraciones _separate_ 'asm()', si uno insiste en usarlas (rara vez una buena idea), es necesario forzar el orden. Sí, llámame quisquilloso si lo deseas ;-) –

4

El compilador usa registros, y puede escribir sobre los valores que ha puesto en ellos.

En este caso, el compilador probablemente utiliza el registro rbx después de la asignación rrbx y antes de la sección de montaje en línea.

En general, no debe esperar que los registros mantengan sus valores después y entre las secuencias de códigos de ensamblaje en línea.

+0

Pero uso rsi como un puntero a buf antes de imprimirf. No importa, si printf lo usa o no. buf [0] y buf [1] deberían tener valores correctos de todos modos, ¿no? Incluso si elimino el rrsi de printf, todavía imprime los mismos valores erróneos. – MetallicPriest

+1

@ugoren: si haces que GCC asigne registros usando la palabra clave 'asm', los derramará correctamente y los volverá a cargar para que se guarden en las llamadas a funciones. –

+0

@MetallicPriest, tiene razón sobre el caso específico. Debería editar mi respuesta, pero estoy teniendo problemas técnicos. Pero la idea general es la que escribí. – ugoren

1

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:

  1. 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.
  2. 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:

  1. la declaración tiene un operando de salida, la variable bar, que después de la declaración se encuentra en un registro, "=r"(...)
  2. la declaración tiene un operando de entrada, la variable foo, que es para ser colocado en un registro, "r"(...)
  3. 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:

  1. bla (bostezo - igual que antes, bar tanto de entrada/salida)
  2. 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ó.

Cuestiones relacionadas