2016-06-05 23 views
2

Ich habe keine Erfahrung in der Montage, aber das ist, woran ich gearbeitet habe. Ich würde gerne eine Eingabe machen, wenn ich grundlegende Aspekte für das Übergeben von Parametern und das Aufrufen einer Funktion über den Zeiger in Assembly vermisse.Ist diese Assembly-Funktion sicher/vollständig?

Zum Beispiel frage ich mich, ob ich wiederherstellen sollte ecx, edx, esi, edi. Ich habe gelesen, dass es sich um Allzweckregister handelt, aber ich konnte nicht herausfinden, ob sie wiederhergestellt werden müssen. Gibt es irgendeine Art von Aufräumarbeiten, die ich nach einem Anruf durchführen soll?

Dies ist der Code, den ich jetzt habe, und es funktioniert:

#include "stdio.h" 

void foo(int a, int b, int c, int d) 
{ 
    printf("values = %d and %d and %d and %d\r\n", a, b, c, d); 
} 

int main() 
{ 

    int a=3,b=6,c=9,d=12; 
    __asm__(
      "mov %3, %%ecx;" 
      "mov %2, %%edx;" 
      "mov %1, %%esi;" 
      "mov %0, %%edi;" 
      "call %4;" 
      : 
      : "g"(a), "g"(b), "g"(c), "g"(d), "a"(foo) 
     ); 

} 
+1

Sie könnten versuchen: http://codereview.stackexchange.com/ – 4386427

+0

Kurz gesagt: Dieser Code ist unsicher. Sie ändern Register in der Inline-Asm, ohne den Compiler zu benachrichtigen. Es gibt Dinge, die Sie tun können, um es besser zu machen (verwenden Sie [x86 machine constraints] (https://gcc.gnu.org/onlinedocs/gcc/Machine-Constraints.html), fügen Sie den Speicher-Clobber, Clobber-Register hinzu, die der ABI erlaubt foo zu ändern (eax?), etc.), aber vielleicht würden Sie eine bessere Antwort bekommen, wenn Sie sagen würden, was Sie erreichen wollten. –

+1

Ich nehme an, das ist 64-Bit-Code? (Zumindest scheint es so von der Aufrufkonvention zu sein). Das Aufrufen einer Funktion in 64-Bit-Code ist wesentlich komplizierter als Sie vielleicht wissen. Ich habe vor kurzem eine Antwort geschrieben, die für Sie von Nutzen sein kann (bezüglich 64-Bit-Code, Inline-Assembler und Aufruffunktionen). In diesem Fall rufe ich 'printf' an, aber es gilt ziemlich genau für den Aufruf einer beliebigen Funktion in 64-Bit-Code von Inline Assembler: http://Stackoverflow.com/a/37503773/3857942 –

Antwort

6

Die ursprüngliche Frage war Is this assembly function call safe/complete?. Die Antwort darauf lautet: Nein. Auch wenn es in diesem einfachen Beispiel so aussieht (insbesondere wenn Optimierungen deaktiviert sind), verstoßen Sie gegen Regeln, die schließlich zu Fehlern führen (solche, die schwer zu finden sind).

Ich möchte die (offensichtliche) Follow-up-Frage ansprechen, wie man es sicher macht, aber ohne Rückmeldung vom OP auf die eigentliche Absicht, kann ich das nicht wirklich tun.

Also, ich werde das Beste tun, was ich kann mit dem, was wir haben, und versuchen, die Dinge zu beschreiben, die es unsicher machen und einige der Dinge, die man dagegen tun kann.

Beginnen wir damit, dass die asm vereinfacht:

__asm__(
      "mov %0, %%edi;" 
      : 
      : "g"(a) 
     ); 

Auch mit dieser einzigen Aussage, dieser Code ist bereits unsicher. Warum? Weil wir den Wert eines Registers (edi) ändern, ohne dass der Compiler davon erfährt.

Wie kann der Compiler nicht wissen, dass Sie fragen? Schließlich ist es genau dort in der asm! Die Antwort kommt von dieser Linie in den gcc docs:

GCC nicht den Assembler-Anweisungen selbst nicht analysieren und nicht wissen, was sie bedeuten, oder sogar, ob sie gültig Assembler eingegeben werden.

In diesem Fall, wie lässt du gcc wissen, was vor sich geht? Die Antwort liegt in der Verwendung der Constraints (der Dinge nach den Doppelpunkten), um den Einfluss der ASM zu beschreiben.

vielleicht einfachste Weg, um diesen Code zu beheben würde so aussehen:

__asm__(
      "mov %0, %%edi;" 
      : 
      : "g"(a) 
      : edi 
     ); 

Dies fügt edi zum clobber list. Kurz gesagt, dies teilt gcc mit, dass der Wert von edi durch den Code geändert wird, und dass gcc nicht davon ausgehen sollte, dass ein bestimmter Wert darin enthalten ist, wenn asm beendet wird.

Nun, während das das einfachste ist, ist es nicht unbedingt der beste Weg. Betrachten Sie diesen Code:

__asm__(
      "" 
      : 
      : "D"(a) 
     ); 

Dieses eine machine constraint verwendet gcc sagen, damit Sie den Wert der Variablen a in das edi-Register zu setzen. Auf diese Weise lädt gcc das Register für Sie zu einem "passenden" Zeitpunkt, vielleicht indem Sie immer a in edi behalten.

Es gibt eine (signifikante) Einschränkung für diesen Code: Indem wir den Parameter hinter den zweiten Doppelpunkt setzen, deklarieren wir ihn als Eingabe. Eingabeparameter müssen schreibgeschützt sein (dh sie müssen den gleichen Wert beim Verlassen der asm haben).

In Ihrem Fall bedeutet die call Anweisung, dass wir nicht garantieren können, dass edi nicht geändert wird, also funktioniert das nicht ganz. Es gibt ein paar Möglichkeiten, damit umzugehen. Am einfachsten ist es, die Einschränkung nach dem ersten Doppelpunkt nach oben zu verschieben, um sie zu einer Ausgabe zu machen, und "+D" anzugeben, um anzugeben, dass der Wert read + write lautet. Aber dann wird der Inhalt von a nach dem asm ziemlich undefiniert sein (printf könnte irgendetwas einstellen). Wenn a zerstören nicht akzeptabel ist, es gibt immer etwas wie folgt aus:

int junk; 
    __asm__ volatile (
      "" 
      : "=D" (junk) 
      : "0"(a) 
     ); 

Dieser gcc erzählt, der die asm auf starten, sollten sie den Wert der Variablen a in der gleichen Stelle wie Ausgabebedingung # 0 (dh edi) setzen . Es sagt auch, dass edi bei der Ausgabe nicht mehr a sein wird, sondern die Variable junk enthalten wird.

Edit: Da die Variable 'junk' nicht verwendet wird, müssen wir den Qualifier volatile hinzufügen. Volatile war implizit, wenn keine Ausgabeparameter vorhanden waren.

Ein weiterer Punkt in dieser Zeile: Sie beenden es mit einem Semikolon. Dies ist legal und wird wie erwartet funktionieren. Wenn Sie jedoch jemals die Befehlszeilenoption -S verwenden möchten, um genau zu sehen, welcher Code generiert wurde (und wenn Sie mit inline asm arbeiten wollen), werden Sie feststellen, dass schwer lesbarer Code erzeugt wird. Ich würde empfehlen, \n\t anstelle eines Semikolons zu verwenden.

Alles, und wir sind immer noch in der ersten Zeile ...

Offensichtlich ist das gleiche mit den anderen beiden mov Aussagen gelten würde.

Das bringt uns zur call Aussage.

Sowohl Michael und ich haben eine Reihe von Gründen aufgelistet Aufruf in Inline-Asm ist schwierig.

  • Behandlung aller Register, die durch den ABI des Funktionsaufrufs verfälscht werden können.
  • Umgang mit der roten Zone.
  • Handhabung Ausrichtung.
  • Speicher Clobber.

Wenn das Ziel hier ist "Lernen", dann fühlen Sie sich frei zu experimentieren. Aber ich weiß nicht, dass ich mich im Produktionscode jemals wohl fühlen würde. Selbst wenn es so aussieht, als würde es funktionieren, würde ich nie sicher sein, dass es keinen seltsamen Fall gab, den ich vermisst hätte. Das ist abgesehen von meinen normalen Bedenken über using inline asm at all.

Ich weiß, das ist eine Menge Informationen. Wahrscheinlich mehr, als Sie als Einführung in den Befehl asm von gcc gesucht haben, aber Sie haben sich einen herausfordernden Platz zum Starten ausgesucht.

Wenn Sie dies noch nicht getan haben, verbringen Sie Ihre Zeit damit, alle Dokumente in gcc's Assembly Language interface zu betrachten. Es gibt viele gute Informationen und Beispiele, die versuchen zu erklären, wie alles funktioniert.

+0

re: nicht inline-asm verwenden, wenn Sie es vermeiden können: Ich [erweitert, dass mehr in einer aktuellen Antwort] (http://stackoverflow.com/questions/37620659/merit-of-inline-asm-rounding-via-putting -float-in-int-Variable). Völlig zustimmen, es ist eine schreckliche Idee für den praktischen Einsatz, und wird viele Optimierungen (wie Inlining und ständige Verbreitung) zu besiegen, und auch * wirklich * schwer zu vertrauen, dass es sicher ist. Danke auch für den Link zum gcc wiki. –

+1

@PeterCordes - Nachdem ich die ganze Zeit damit verbracht habe, darüber zu lernen, wie gcc's inline asm funktioniert, damit ich endlich diesen schrecklichen Doktoren das Neuschreiben geben kann, das sie so dringend brauchen, habe ich gemerkt, dass es zwar eine coole Idee ist. Deshalb habe ich diese Wiki-Seite geschrieben. Für eine Zugabe versuche ich gerade, gcc's basic asm (die Art ohne Doppelpunkte) zu erhalten, wenn es innerhalb einer Funktion verwendet wird. Das erklärt meinen [anderen Wiki-Eintrag] (https://gcc.gnu.org/wiki/ConvertBasicAsmToExtended). So schlecht wie die Verwendung von Extended Asm ist, ist Basic viel schlechter. –

+0

BTW, '" + D "(tmp)' ist viel einfacher als separate Eingangs- und Ausgangsargumente. Es macht die C-Variable unpassend, so dass der separate Eingabe- und Ausgabe-Arg-Stil einfacher ist als die Verwendung von tmp vars in C, wenn Sie die Eingaben nach der Inline-Asm benötigen. (Der einzige Fall, an den ich mich erinnere, wo das nicht funktioniert, ist x87: Ich denke, "" + t "' ist nicht erlaubt.) –

1

Ich lese sie Universalregister sind, aber ich konnte nicht finden, wenn sie wiederhergestellt werden müssen?

Ich bin nicht der Experte auf dem Gebiet, aber aus meiner Lektüre der x86-64 ABI (Abbildung 3.4) die folgenden Register: %rdi, %rsi, %rdx und %rcx nicht zwischen Funktionsaufrufe erhalten, tun also offenbar nicht müssen wiederhergestellt werden.

Wie von David Wohlferd kommentiert, sollten Sie vorsichtig sein, weil der Compiler den "custom" -Funktionsaufruf auf keinen Fall bemerkt und Sie daher in den Weg kommen können, insbesondere weil er Register nicht kennt Änderung.

+0

Während der ABI sagt, dass sie nicht wiederhergestellt werden müssen, hat gcc keine Ahnung, dass ein Funktionsaufruf stattfindet. Es analysiert das Asm überhaupt nicht. Es gibt also keinen Grund zu der Annahme, dass sich die Werte von rdi, rsi, rdx oder rcx geändert haben. –

+3

Besser. Aber es gibt auch andere Überlegungen, wie die rote Zone für 64-Bit-Code. Dies bedeutet, dass Push/Pop (die traditionelle Methode der "Wiederherstellung" von Registern) komplizierter ist als gewöhnlich. Und obwohl Rax in diesem Code nicht explizit erwähnt wird, kann es von printf oder einem seiner untergeordneten Elemente geändert werden, daher muss es auch "durchgestrichen" werden. Zusätzlich zu R8, R10, usw. (Sicher) Aufruf von Funktionen von Inline-Asm ist * schwer *, und ist in der Regel eine schlechte Idee. –

+1

Ich stimme mit @DavidWohlferd überein: das Aufrufen von Funktionen aus Inline-Assembler erfordert einiges an Wissen. Ich [schrieb eine Antwort] (http://stackoverflow.com/a/37503773/3857942), die in letzter Zeit nicht sehr trivial war, die 64-Bit-Code/Inline-Assembler/Aufrufen einer Funktion beinhaltete.Zusätzlich zu dem, was David sagte, benötigt _GCC_ selbst, dass der Stapel an einer 16-Byte-Grenze an dem Punkt ausgerichtet wird, an dem ein "CALL" gemacht wird. Sie müssen also nicht nur mit der Redzone und den Clobbern umgehen, sondern müssen sich vor dem Aufruf mit der Stack-Ausrichtung befassen. –