GNU/Linux >> LINUX-Kenntnisse >  >> Linux

Warum funktioniert diese Inline-Assembly nicht mit einer separaten asm volatile-Anweisung für jede Anweisung?

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:

  1. 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 Variable bar zuordnen damit.
  2. 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 hier foo 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:

  1. die Anweisung hat einen Ausgabeoperanden, die Variable bar , dass nach der Anweisung in einem Register "=r"(...) zu finden ist
  2. die Anweisung hat einen Eingabeoperanden, die Variable foo , die in ein Register "r"(...) eingetragen werden soll
  3. 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:

  1. bla (gähn - wie zuvor, bar sowohl Eingang/Ausgang)
  2. 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.


Linux
  1. Warum funktioniert die Bash-Prozesssubstitution bei einigen Befehlen nicht?

  2. PYTHONPATH funktioniert nicht für sudo unter GNU/Linux (funktioniert für root)

  3. Warum funktioniert Tomcat mit Port 8080, aber nicht mit 80?

  4. ssh-Konfigurationsnamen-Alias ​​funktioniert nicht für scp

  5. Nohup für Python-Skript funktioniert nicht, wenn es im Hintergrund mit &ausgeführt wird

Arbeiten mit dem Echtzeit-Kernel für Red Hat Enterprise Linux

Warum erfordert „ls“ einen separaten Prozess zur Ausführung?

Warum sehe ich MSG_EOR für SOCK_SEQPACKET unter Linux nicht?

root-Passwort funktioniert nicht für su im Terminal

Warum sollte ich eine separate Partition für /tmp erstellen?

Warum funktioniert diese Regex nicht unter Linux?