Sie belasten den Speicher, aber sagen GCC nichts davon, damit GCC Werte in buf
zwischenspeichern kann über Versammlungsaufrufe. Wenn Sie Ein- und Ausgänge verwenden möchten, teilen Sie GCC alles mit.
__asm__ (
"movq %1, 0(%0)\n\t"
"movq %2, 8(%0)"
: /* Outputs (none) */
: "r"(buf), "r"(rrax), "r"(rrbx) /* Inputs */
: "memory"); /* Clobbered */
Sie möchten im Allgemeinen auch den größten Teil von mov
von GCC erledigen lassen , Registerauswahl usw. -- auch wenn Sie die Register explizit einschränken (rrax ist immer noch %rax
) lassen Sie die Informationen durch GCC fließen oder Sie erhalten unerwartete Ergebnisse.
__volatile__
ist falsch.
Der Grund __volatile__
vorhanden ist, damit Sie garantieren können, dass der Compiler Ihren Code genau dort platziert, wo er ist ... was völlig unnötig ist Garantie für diesen Code. Es ist notwendig, um erweiterte Funktionen wie Speicherbarrieren zu implementieren, aber fast völlig wertlos, wenn Sie nur Speicher und Register ändern.
GCC weiß bereits, dass es diese Assembly nicht nach printf
verschieben kann weil die printf
Aufruf greift auf buf
zu , und buf
könnte von der Versammlung verprügelt werden. GCC weiß bereits, dass es die Assembly nicht vor rrax=0x39;
verschieben kann weil rax
ist eine Eingabe für den Assemblercode. Was bedeutet also __volatile__
bekommst du? Nichts.
Wenn Ihr Code ohne __volatile__
nicht funktioniert dann gibt es einen Fehler im Code, der behoben werden sollte anstatt einfach __volatile__
hinzuzufügen und hoffen, dass das alles besser macht. Die __volatile__
Schlüsselwort ist keine Zauberei und sollte nicht als solche behandelt werden.
Alternative Lösung:
Ist __volatile__
notwendig für Ihren ursprünglichen Code? Nein. Markieren Sie einfach die Eingaben und überschreiben Sie die Werte richtig.
/* 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");
Warum __volatile__
hilft dir hier nicht weiter:
rrax = 0x34; /* Dead code */
GCC hat durchaus das Recht, die obige Zeile vollständig zu löschen, da der Code in der obigen Frage behauptet, dass er niemals rrax
verwendet .
Ein deutlicheres Beispiel
long global;
void store_5(void)
{
register long rax asm ("rax");
rax = 5;
__asm__ __volatile__ ("movq %%rax, (global)");
}
Die Demontage ist mehr oder weniger so, wie Sie es bei -O0
erwarten ,
movl $5, %rax
movq %rax, (global)
Aber wenn die Optimierung ausgeschaltet ist, können Sie beim Zusammenbau ziemlich schlampig sein. Versuchen wir es mit -O2
:
movq %rax, (global)
Hoppla! Woher kam rax = 5;
gehen? Es ist toter Code, seit %rax
wird in der Funktion nie verwendet – zumindest soweit GCC weiß. GCC wirft keinen Blick in die Assembly. Was passiert, wenn wir __volatile__
entfernen ?
; empty
Nun, Sie denken vielleicht an __volatile__
tut Ihnen einen Dienst, indem es GCC davon abhält, Ihre wertvolle Assembly zu verwerfen, aber es verschleiert nur die Tatsache, dass GCC denkt, dass Ihre Assembly nicht tut irgendetwas. GCC geht davon aus, dass Ihre Assembly keine Eingaben entgegennimmt, keine Ausgaben erzeugt und keinen Speicher belegt. Du solltest es besser berichtigen:
long global;
void store_5(void)
{
register long rax asm ("rax");
rax = 5;
__asm__ __volatile__ ("movq %%rax, (global)" : : : "memory");
}
Nun erhalten wir folgende Ausgabe:
movq %rax, (global)
Besser. Aber wenn Sie GCC über die Eingaben informieren, wird es sicherstellen, dass %rax
zuerst richtig initialisiert wird:
long global;
void store_5(void)
{
register long rax asm ("rax");
rax = 5;
__asm__ ("movq %%rax, (global)" : : "a"(rax) : "memory");
}
Die Ausgabe mit Optimierungen:
movl $5, %eax
movq %rax, (global)
Richtig! Und wir müssen nicht einmal __volatile__
verwenden .
Warum funktioniert __volatile__
existieren?
Die primäre korrekte Verwendung für __volatile__
ist, wenn Ihr Assembler-Code neben Eingabe, Ausgabe oder Speicherüberlastung noch etwas anderes macht. Vielleicht bringt es spezielle Register durcheinander, von denen GCC nichts weiß, oder beeinflusst IO. Man sieht es oft im Linux-Kernel, aber es wird sehr oft im Userspace missbraucht.
Der __volatile__
Schlüsselwort ist sehr verlockend, weil wir C-Programmierer oft denken, dass wir fast sind Programmierung in Assemblersprache bereits. Waren nicht. C-Compiler führen viele Datenflussanalysen durch – daher müssen Sie dem Compiler den Datenfluss für Ihren Assemblercode erklären. Auf diese Weise kann der Compiler Ihren Assembly-Block sicher manipulieren, genau wie er die Assembly manipuliert, die er generiert.
Wenn Sie feststellen, dass Sie __volatile__
verwenden viel, alternativ könnten Sie eine ganze Funktion oder ein Modul in eine Assembly-Datei schreiben.
Der Compiler verwendet Register und kann die von Ihnen eingegebenen Werte überschreiben.
In diesem Fall verwendet der Compiler wahrscheinlich den rbx
registrieren Sie sich nach dem rrbx
Zuweisung und vor dem Inline-Assembly-Abschnitt.
Im Allgemeinen sollten Sie nicht erwarten, dass Register ihre Werte nach und zwischen Inline-Assembler-Codesequenzen beibehalten.
Etwas off-topic, aber ich würde gerne ein bisschen weiter auf die gcc-Inline-Assemblierung eingehen.
Die (Nicht-)Notwendigkeit von __volatile__
kommt daher, dass GCC optimiert Inline-Montage. GCC untersucht die Montageanleitung auf Nebenwirkungen/Voraussetzungen, und wenn sie feststellt, dass sie nicht vorhanden sind, kann es entscheiden, die Montageanleitung zu verschieben oder sogar zu entfernen es. Alle __volatile__
tut, ist, dem Compiler zu sagen:"Hör auf, dich darum zu kümmern, und lege das hier hin."
Das ist normalerweise nicht das, was Sie wirklich wollen.
Hier werden Einschränkungen benötigt come in. Der Name ist überladen und wird tatsächlich für verschiedene Dinge in der GCC-Inline-Assemblierung verwendet:
- Einschränkungen spezifizieren Eingabe-/Ausgabeoperanden, die in
asm()
verwendet werden blockieren - Einschränkungen geben die "Clobber-Liste" an, die angibt, welcher "Zustand" (Register, Bedingungscodes, Speicher) von
asm()
betroffen ist . - Einschränkungen spezifizieren Klassen von Operanden (Register, Adressen, Offsets, Konstanten, ...)
- Einschränkungen deklarieren Assoziationen/Bindungen zwischen Assembler-Entitäten und C/C++-Variablen/Ausdrücken
In vielen Fällen missbrauchen Entwickler __volatile__
weil sie bemerkten, dass ihr Code entweder verschoben wurde oder sogar ohne ihn verschwand. Wenn dies passiert, ist es normalerweise eher ein Zeichen dafür, dass der Entwickler es nicht versucht hat GCC über Nebenwirkungen / Voraussetzungen der Montage zu informieren. Zum Beispiel dieser fehlerhafte Code:
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);
Es hat mehrere Fehler:
- Zum einen wird es nur aufgrund eines gcc-Bugs (!) kompiliert. Um Registernamen in der Inline-Assembly zu schreiben, verdoppeln Sie normalerweise
%%
werden benötigt, aber wenn Sie sie oben tatsächlich angeben, erhalten Sie einen Compiler-/Assemblerfehler,/tmp/ccYPmr3g.s:22: Error: bad register name '%%rax'
. - Zweitens teilt es dem Compiler nicht mit, wann und wo Sie die Variablen benötigen/verwenden. Stattdessen wird angenommen der Compiler berücksichtigt
asm()
buchstäblich. Das mag für Microsoft Visual C++ zutreffen, ist aber nicht der Fall für gcc.
Wenn Sie es ohne kompilieren Optimierung erstellt es:
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]> [...]Sie können Ihren
add
finden Anweisung und die Initialisierungen der beiden Register, und es wird das Erwartete gedruckt. Wenn Sie dagegen die Optimierung aufdrehen, passiert etwas anderes: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]> [ ... ]Ihre Initialisierungen der beiden "benutzten" Register sind nicht mehr vorhanden. Der Compiler verwarf sie, weil nichts, was er sehen konnte, sie verwendete, und während er die Assembler-Anweisung beibehielt, stellte er sie vor jede Verwendung der beiden Variablen. Es ist da, aber es tut nichts (Zum Glück eigentlich ... wenn
rax
/ rbx
in Gebrauch gewesen wer weiß, was passiert wäre ...).
Und der Grund dafür ist, dass Sie es nicht wirklich erzählt haben GCC, dass die Assembly diese Register / diese Operandenwerte verwendet. Das hat überhaupt nichts mit volatile
zu tun aber alles mit der Tatsache, dass Sie einen uneingeschränkten asm()
verwenden Ausdruck.
So machen Sie das richtig ist via Constraints, d.h. Sie würden verwenden:
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);
Dies teilt dem Compiler mit, dass die Assembly:
- hat ein Argument in einem Register,
"+r"(...)
das sowohl vor der Assembly-Anweisung initialisiert werden muss als auch durch die Assembly-Anweisung modifiziert wird, und die Variablebar
zuordnen damit. - hat ein zweites Argument in einem Register,
"r"(...)
die vor der Assembly-Anweisung initialisiert werden muss und von der Anweisung als schreibgeschützt / nicht geändert behandelt wird. Verknüpfen Sie hierfoo
damit.
Beachten Sie, dass keine Registerzuweisung angegeben ist - der Compiler wählt dies abhängig von den Variablen / dem Zustand der Kompilierung. Die (optimierte) Ausgabe des Obigen:
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]> [ ... ]GCC Inline Assembly Constraints sind fast immer notwendig in der einen oder anderen Form, aber es kann mehrere Möglichkeiten geben, dem Compiler dieselben Anforderungen zu beschreiben; statt oben könnte man auch schreiben:
asm("add %1, %0" : "=r"(bar) : "r"(foo), "0"(bar));
Dies sagt gcc:
- die Anweisung hat einen Ausgabeoperanden, die Variable
bar
, dass nach der Anweisung in einem Register"=r"(...)
zu finden ist - die Anweisung hat einen Eingabeoperanden, die Variable
foo
, die in ein Register"r"(...)
eingetragen werden soll - Operand Null ist ebenfalls ein Eingabeoperand und mit
bar
zu initialisieren
Oder, wieder eine Alternative:
asm("add %1, %0" : "+r"(bar) : "g"(foo));
was gcc sagt:
- bla (gähn - wie zuvor,
bar
sowohl Eingang/Ausgang) - die Anweisung hat einen Eingabeoperanden, die Variable
foo
, wobei es der Anweisung egal ist, ob sie sich in einem Register, im Speicher oder in einer Konstante zur Kompilierzeit befindet (das ist der"g"(...)
Einschränkung)
Das Ergebnis unterscheidet sich vom vorherigen:
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]> [ ... ]denn jetzt hat GCC tatsächlich herausgefunden
foo
ist eine Kompilierzeitkonstante und bettet den Wert einfach in die ein add
Anweisung ! Ist das nicht ordentlich?
Zugegeben, das ist aufwendig und gewöhnungsbedürftig. Der Vorteil ist, dass man dem Compiler die Wahl lässt welche Register für welche Operanden zu verwenden sind, um den Code insgesamt zu optimieren; wenn zum Beispiel eine Inline-Assembler-Anweisung in einem Makro und/oder einem static inline
verwendet wird Funktion kann der Compiler je nach aufrufendem Kontext unterschiedliche Register bei unterschiedlichen Instanziierungen des Codes auswählen. Oder wenn ein bestimmter Wert an einer Stelle zur Kompilierzeit auswertbar / konstant ist, an einer anderen jedoch nicht, kann der Compiler die erstellte Assembly darauf zuschneiden.
Stellen Sie sich GCC-Inline-Assembly-Einschränkungen als eine Art "erweiterte Funktionsprototypen" vor - sie sagen dem Compiler, welche Typen und Orte für Argumente / Rückgabewerte sind, und noch ein bisschen mehr. Wenn Sie diese Einschränkungen nicht angeben, erstellt Ihre Inline-Assembly das Analogon von Funktionen, die nur auf globalen Variablen/Zuständen arbeiten - die, wie wir wahrscheinlich alle zustimmen, selten genau das tun, was Sie beabsichtigt haben.