GNU/Linux >> Tutoriels Linux >  >> Linux

Pourquoi cet assembly en ligne ne fonctionne-t-il pas avec une instruction asm volatile distincte pour chaque instruction ?

Vous encombrez la mémoire mais n'en parlez pas à GCC, donc GCC peut mettre en cache les valeurs dans buf à travers les appels d'assemblage. Si vous souhaitez utiliser des entrées et des sorties, informez GCC de tout.

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

Vous voulez aussi généralement laisser GCC gérer la plupart des mov , sélection de registre, etc -- même si vous contraignez explicitement les registres (rrax vaut toujours %rax ) laissez les informations passer par GCC ou vous obtiendrez des résultats inattendus.

__volatile__ est faux.

La raison __volatile__ existe pour que vous puissiez garantir que le compilateur place votre code exactement là où il se trouve... ce qui est complètement inutile garantie pour ce code. Il est nécessaire pour implémenter des fonctionnalités avancées telles que les barrières de mémoire, mais presque complètement sans valeur si vous ne modifiez que la mémoire et les registres.

GCC sait déjà qu'il ne peut pas déplacer cet assembly après printf parce que le printf appel accède buf , et buf pourrait être bousculé par l'assemblée. GCC sait déjà qu'il ne peut pas déplacer l'assembly avant rrax=0x39; car rax est une entrée du code assembleur. Alors qu'est-ce que __volatile__ vous obtenez? Rien.

Si votre code ne fonctionne pas sans __volatile__ alors il y a une erreur dans le code qui devrait être corrigée au lieu d'ajouter simplement __volatile__ et en espérant que tout s'arrange. Le __volatile__ mot-clé n'est pas magique et ne doit pas être traité comme tel.

Correction alternative :

Est __volatile__ nécessaire pour votre code d'origine? Non. Marquez simplement les entrées et les valeurs de clobber correctement.

/* 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");

Pourquoi __volatile__ ne vous aide pas ici :

rrax = 0x34; /* Dead code */

GCC est bien dans son droit de supprimer complètement la ligne ci-dessus, puisque le code dans la question ci-dessus affirme qu'il n'utilise jamais rrax .

Un exemple plus clair

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

Le démontage est plus ou moins comme prévu à -O0 ,

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

Mais avec l'optimisation désactivée, vous pouvez être assez bâclé sur l'assemblage. Essayons -O2 :

movq %rax, (global)

Oups ! Où est passé rax = 5; aller? C'est du code mort, depuis %rax n'est jamais utilisé dans la fonction - du moins pour autant que GCC le sache. GCC ne regarde pas à l'intérieur de l'assemblage. Que se passe-t-il lorsque nous supprimons __volatile__ ?

; empty

Eh bien, vous pourriez penser __volatile__ vous rend service en empêchant GCC de jeter votre précieux assemblage, mais cela masque simplement le fait que GCC pense que votre assemblage ne fait n'importe quoi. GCC pense que votre assemblage ne prend aucune entrée, ne produit aucune sortie et n'encombre aucune mémoire. Vous feriez mieux de le redresser :

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

Nous obtenons maintenant la sortie suivante :

movq %rax, (global)

Meilleur. Mais si vous parlez à GCC des entrées, il s'assurera que %rax est correctement initialisé en premier :

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

Le résultat, avec les optimisations :

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

Corriger! Et nous n'avons même pas besoin d'utiliser __volatile__ .

Pourquoi __volatile__ existe ?

L'utilisation principale correcte pour __volatile__ c'est si votre code assembleur fait autre chose que l'entrée, la sortie ou l'encombrement de la mémoire. Peut-être que cela perturbe les registres spéciaux que GCC ne connaît pas ou affecte IO. Vous le voyez souvent dans le noyau Linux, mais il est très souvent utilisé à mauvais escient dans l'espace utilisateur.

Le __volatile__ mot clé est très tentant car nous, les programmeurs C, aimons souvent penser que nous sommes presque programmation en langage assembleur déjà. N'étaient pas. Les compilateurs C effectuent de nombreuses analyses de flux de données. Vous devez donc expliquer le flux de données au compilateur pour votre code assembleur. De cette façon, le compilateur peut manipuler en toute sécurité votre morceau d'assembly tout comme il manipule l'assembly qu'il génère.

Si vous utilisez __volatile__ beaucoup, comme alternative, vous pouvez écrire une fonction ou un module entier dans un fichier d'assemblage.


Le compilateur utilise des registres, et il peut écraser les valeurs que vous y avez mises.

Dans ce cas, le compilateur utilise probablement le rbx s'inscrire après le rrbx affectation et avant la section d'assemblage en ligne.

En général, vous ne devriez pas vous attendre à ce que les registres conservent leurs valeurs après et entre les séquences de code assembleur en ligne.


Un peu hors sujet, mais j'aimerais revenir un peu sur l'assemblage en ligne de gcc.

Le (non-)besoin de __volatile__ vient du fait que GCC optimise assemblage en ligne. GCC inspecte la déclaration d'assemblage à la recherche d'effets secondaires / prérequis, et s'il constate qu'ils n'existent pas, il peut choisir de déplacer l'instruction d'assemblage ou même décider de supprimer ce. Tous __volatile__ fait est de dire au compilateur "arrête de t'en soucier et mets ça juste là".

Ce qui n'est généralement pas ce que vous voulez vraiment.

C'est là que le besoin de contraintes entrez. Le nom est surchargé et en fait utilisé pour différentes choses dans l'assemblage en ligne de GCC :

  • les contraintes spécifient les opérandes d'entrée/sortie utilisés dans le asm() bloquer
  • les contraintes spécifient la "liste de clobber", qui détaille quel "état" (registres, codes de condition, mémoire) est affecté par le asm() .
  • les contraintes spécifient les classes d'opérandes (registres, adresses, décalages, constantes, ...)
  • les contraintes déclarent des associations/liaisons entre les entités assembleur et les variables/expressions C/C++

Dans de nombreux cas, les développeurs abusent __volatile__ parce qu'ils ont remarqué que leur code était déplacé ou même disparaissait sans lui. Si cela se produit, c'est généralement plutôt un signe que le développeur a tenté pas pour informer GCC des effets secondaires / prérequis de l'assemblage. Par exemple, ce code bogué :

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);

Il y a plusieurs bogues :

  • d'une part, il ne compile qu'à cause d'un bogue gcc (!). Normalement, pour écrire des noms de registre en assemblage en ligne, doublez %% sont nécessaires, mais dans ce qui précède, si vous les spécifiez réellement, vous obtenez une erreur du compilateur/assembleur, /tmp/ccYPmr3g.s:22: Error: bad register name '%%rax' .
  • deuxièmement, il ne dit pas au compilateur quand et où vous avez besoin/utilisez les variables. Au lieu de cela, il suppose le compilateur respecte asm() au sens propre. Cela peut être vrai pour Microsoft Visual C++ mais ce n'est pas le cas pour gcc.

Si vous le compilez sans optimisation, il crée :

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]>
[...]
Vous pouvez trouver votre add instruction, et les initialisations des deux registres, et il imprimera l'attendu. Si, d'un autre côté, vous augmentez l'optimisation, quelque chose d'autre se produit :
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]>
[ ... ]
Vos initialisations des deux registres "utilisés" ne sont plus là. Le compilateur les a rejetés car rien de ce qu'il pouvait voir ne les utilisait, et bien qu'il ait conservé l'instruction d'assemblage, il l'a placée avant toute utilisation des deux variables. C'est là mais ça ne fait rien (Heureusement en fait... si rax / rbx avait été utilisé qui peut dire ce qui se serait passé ...).

Et la raison en est que vous n'avez pas réellement dit GCC que l'assembly utilise ces registres/ces valeurs d'opérandes. Cela n'a rien à voir avec volatile mais tout cela avec le fait que vous utilisez un asm() sans contrainte expression.

La façon de le faire correctement est via des contraintes, c'est-à-dire que vous utiliseriez :

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);

Cela indique au compilateur que l'assembly :

  1. a un argument dans un registre, "+r"(...) que les deux doivent être initialisés avant l'instruction d'assemblage, et sont modifiés par l'instruction d'assemblage, et associez la variable bar avec elle.
  2. a un deuxième argument dans un registre, "r"(...) qui doit être initialisé avant l'instruction d'assemblage et est traité comme en lecture seule/non modifié par l'instruction. Ici, associez foo avec ça.

Notez qu'aucune affectation de registre n'est spécifiée - le compilateur choisit cela en fonction des variables / de l'état de la compilation. La sortie (optimisée) de ce qui précède :

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]>
[ ... ]
Les contraintes d'assemblage en ligne GCC sont presque toujours nécessaires sous une forme ou une autre, mais il peut y avoir plusieurs façons possibles de décrire les mêmes exigences au compilateur ; au lieu de ce qui précède, vous pouvez également écrire:

asm("add %1, %0" : "=r"(bar) : "r"(foo), "0"(bar));

Cela indique à gcc :

  1. l'instruction a un opérande de sortie, la variable bar , qu'après l'instruction se retrouvera dans un registre, "=r"(...)
  2. l'instruction a un opérande d'entrée, la variable foo , qui doit être placé dans un registre, "r"(...)
  3. l'opérande zéro est aussi un opérande d'entrée et à initialiser avec bar

Ou encore une alternative :

asm("add %1, %0" : "+r"(bar) : "g"(foo));

qui indique à gcc :

  1. bla (bâillement - comme avant, bar entrée/sortie)
  2. l'instruction a un opérande d'entrée, la variable foo , dont l'instruction se fiche qu'elle soit dans un registre, en mémoire ou dans une constante de compilation (c'est le "g"(...) contrainte)

Le résultat est différent du précédent :

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]>
[ ... ]
parce que maintenant, GCC a en fait compris foo est une constante de compilation et intègre simplement la valeur dans le add instruction ! N'est-ce pas chouette ?

C'est vrai que c'est complexe et qu'il faut s'y habituer. L'avantage est que laisser le compilateur choisir quels registres utiliser pour quels opérandes permet d'optimiser le code dans son ensemble ; si, par exemple, une instruction d'assemblage en ligne est utilisée dans une macro et/ou un static inline fonction, le compilateur peut, selon le contexte d'appel, choisir différents registres à différentes instanciations du code. Ou si une certaine valeur est évaluable/constante au moment de la compilation à un endroit mais pas à un autre, le compilateur peut adapter l'assembly créé pour cela.

Considérez les contraintes d'assemblage en ligne de GCC comme une sorte de "prototypes de fonctions étendues" - elles indiquent au compilateur quels sont les types et les emplacements des arguments/valeurs de retour, et un peu plus. Si vous ne spécifiez pas ces contraintes, votre assemblage en ligne crée l'analogue de fonctions qui fonctionnent uniquement sur des variables/états globaux - qui, comme nous en convenons probablement tous, font rarement exactement ce que vous vouliez.


Linux
  1. Pourquoi la substitution de processus Bash ne fonctionne-t-elle pas avec certaines commandes ?

  2. Pourquoi ne vois-je pas MSG_EOR pour SOCK_SEQPACKET sous Linux ?

  3. L'alias de nom de configuration ssh ne fonctionne pas pour scp

  4. Nohup pour le script Python ne fonctionne pas lors de l'exécution en arrière-plan avec &

  5. Pourquoi 'dd' ne fonctionne-t-il pas pour créer une clé USB amorçable ?

Travailler avec le noyau en temps réel pour Red Hat Enterprise Linux

le mot de passe root ne fonctionne pas pour su dans le terminal

Pourquoi devrais-je créer une partition séparée pour /tmp ?

Pourquoi poll n'est pas remplacé par epoll ?

Pourquoi l'USB ne fonctionne-t-il pas sous Linux alors qu'il fonctionne dans UEFI/BIOS ?

Pourquoi ma crontab ne fonctionne-t-elle pas et comment puis-je la dépanner ?