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 :
- 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 variablebar
avec elle. - 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, associezfoo
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 :
- l'instruction a un opérande de sortie, la variable
bar
, qu'après l'instruction se retrouvera dans un registre,"=r"(...)
- l'instruction a un opérande d'entrée, la variable
foo
, qui doit être placé dans un registre,"r"(...)
- 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 :
- bla (bâillement - comme avant,
bar
entrée/sortie) - 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.