GNU/Linux >> Tutoriales Linux >  >> Linux

¿Por qué este ensamblaje en línea no funciona con una declaración volátil asm separada para cada instrucción?

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:

  1. 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 variable bar con eso.
  2. 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í, asocia foo 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:

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

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


Linux
  1. ¿Por qué Bash no almacena comandos que comienzan con espacios?

  2. Linux:¿por qué el USB no funciona en Linux cuando funciona en Uefi/bios?

  3. ¿Por qué "ls" requiere un proceso separado para su ejecución?

  4. ¿Por qué este código falla con la aleatorización de direcciones activada?

  5. ¿Por qué 'dd' no funciona para crear un USB de arranque?

Trabajar con el kernel en tiempo real para Red Hat Enterprise Linux

la contraseña de root no funciona para su en la terminal

¿Por qué debo hacer una partición separada para /tmp?

¿Por qué no funciona sftp rmdir?

¿Por qué poll no se reemplaza con epoll?

¿Por qué USB no funciona en Linux cuando funciona en UEFI/BIOS?