GCC Assembly Optimizations - Warum sind diese gleichwertig?

8

Ich versuche zu lernen, wie die Montage auf einer elementaren Ebene funktioniert und so habe ich mit der -S-Ausgabe von gcc-Kompilationen gespielt. Ich schrieb ein einfaches Programm, das zwei Bytes definiert und ihre Summe zurückgibt. Das gesamte Programm folgt:

%Vor%

Wenn ich dies ohne Optimierungen kompiliere, benutze:

%Vor%

Ich bekomme test.s, das wie folgt aussieht:

%Vor%

Nun, da ich erkenne, dass dieses Programm sehr einfach vereinfacht werden kann, um einfach eine Konstante zurückzugeben (15), konnte ich die Baugruppe von Hand verkleinern, um dieselbe Funktion mit diesem Code auszuführen:

%Vor%

Dies scheint mir die geringste Menge an Code zu sein, die möglich ist (aber ich realisiere, dass es ganz falsch sein könnte), diese zugegebenermaßen triviale Aufgabe auszuführen. Ist diese Form die "optimierte" Version meines C-Programms?

Warum ist die ursprüngliche Ausgabe von GCC so viel ausführlicher? Was machen die Zeilen von .cfi_startproc zu call__main? Was macht __main? Ich kann nicht herausfinden, wofür die beiden Subtraktionsoperationen sind.

Auch wenn die Optimierungen in GCC auf -O3 gesetzt sind, bekomme ich folgendes:

%Vor%

Das scheint eine Reihe von Operationen entfernt zu haben, lässt aber alle Zeilen, die zum Aufruf von __main führen, überflüssig erscheinen. Wozu dienen alle .cfi_XXX-Zeilen? Warum werden so viele Labels hinzugefügt? Was tun .section, .ident, .def .p2align, usw. usw.?

Ich verstehe, dass viele der Labels und Symbole zum Debuggen enthalten sind, aber sollten diese nicht entfernt oder weggelassen werden, wenn ich nicht mit -g aktiviere?

AKTUALISIEREN

Um es zu verdeutlichen, indem Sie

sagen
  

Dies scheint mir der geringste mögliche Code zu sein (aber ich   zu realisieren, könnte völlig falsch sein), um diese zugegebenermaßen triviale Aufgabe auszuführen.   Ist diese Form die "optimierte" Version meines C-Programms?

Ich behaupte nicht, dass ich eine optimierte Version dieses Programms versuche oder erreicht habe. Ich verstehe, dass das Programm nutzlos und trivial ist. Ich benutze es nur als ein Werkzeug, um Assembly zu lernen und wie der Compiler funktioniert.

Der Kern, warum ich dieses Bit hinzugefügt habe, soll veranschaulichen, warum ich verwirrt bin, dass die 4-zeilige Version dieses Assembler-Codes den gleichen Effekt wie die anderen erzielen kann. Es scheint mir, dass der GCC eine Menge "Zeug" hinzugefügt hat, dessen Zweck ich nicht erkennen kann.

    
Kin3TiX 01.07.2015, 16:21
quelle

5 Antworten

6

Danke, Kin3TiX, dass du eine asm-newbie-Frage gestellt hast, die nicht nur ein Code-Dump von etwas bösartigem Code ohne Kommentare und ein wirklich einfaches Problem war. :)

Um Ihre Füße mit ASM nass zu machen, würde ich vorschlagen, mit Funktionen anders als main zu arbeiten. z.B. nur eine Funktion, die zwei ganzzahlige Argumente akzeptiert und sie hinzufügt. Dann kann der Compiler es nicht optimieren. Sie können es immer noch mit Konstanten als Argumente aufrufen, und wenn es sich in einer anderen Datei als main befindet, wird es nicht inline angezeigt, sodass Sie es sogar in einem Schritt durchgehen können.

Es gibt einige Vorteile zu verstehen, was auf der asm-Ebene passiert, wenn Sie main kompilieren, aber anders als bei eingebetteten Systemen werden Sie immer nur optimierte innere Schleifen in asm schreiben. IMO, es gibt wenig Sinn Asm zu verwenden, wenn Sie nicht die Hölle daraus optimieren wollen. Sonst wirst du wahrscheinlich die Compiler-Ausgabe der Quelle nicht übertreffen, die viel einfacher zu lesen ist.

Weitere Tipps zum Verständnis der Compiler-Ausgabe: Kompilieren mit
gcc -S -fno-stack-check -fverbose-asm . Die Kommentare nach jeder Anweisung erinnern oft daran, wofür diese Ladung verwendet wurde. Ziemlich bald degeneriert es in ein Durcheinander von Provisorien mit Namen wie D.2983 , aber sowas wie movq 8(%rdi), %rcx # a_1(D)->elements, a_1(D)->elements wird dir einen Hin- und Rückweg zur ABI-Referenz sichern, um zu sehen, welche Funktion arg in %rdi kommt und welche Strukturelement ist bei Offset 8.

  

Was tun die Zeilen von .cfi_startproc bis call__main?

%Vor%

Wie andere bereits gesagt haben, ist .cfi stuff Debugging-Info. Es ist das Zeug, das strip aus deiner Binärdatei entfernt, oder das nicht an erster Stelle steht, wenn du -g nicht benutzt hast. IDK, warum sie dort in der -S -Ausgabe sind, ohne -g . Oft sehe ich asm von objdump -d output, anstatt von gcc -S . Normalerweise, weil ich die ausführbare Datei vergleichen und auf ihre Asm schauen kann, ohne gcc mehrmals aufrufen zu müssen.

Wenn Sie %ebp drücken und dann auf den Wert des Stackpointers für den Funktionseintrag setzen, wird ein so genannter "Stack-Frame" eingerichtet. Deshalb wird %ebp als Basiszeiger bezeichnet. Diese Insns sind nicht vorhanden, wenn Sie mit -fomit-frame-pointer kompilieren, was dem Code ein zusätzliches Register zum Arbeiten gibt. (Dies ist riesig für 32bit x86, da das von 6 bis 7 regs dauert. (% Co_de% ist immer noch gebunden als Stapelzeiger; es wird vorübergehend in einem xmm oder mmx reg gespeichert und dann als ein anderes GP reg ist möglich , aber Ihr Code wird schwer zu debuggen sein!)

Der Befehl %esp vor dem leave ist ebenfalls Teil dieses Stack-Frames.

Ich bin mir nicht ganz klar über den Zweck von Frame-Zeigern. Mit Debug-Symbolen können Sie den Aufruf-Stack auch mit ret gut zurückverfolgen, und dies ist die Standardeinstellung für amd64. (Der amd64 ABI hat Ausrichtungsanforderungen für den Stapel, ist auf andere Weise auch viel besser. Er übergibt beispielsweise Args in Regs anstatt auf dem Stapel.)

%Vor%

Der -fomit-frame-pointer richtet den Stapel an eine 16-Byte-Grenze aus, unabhängig davon, was er zuvor war. Der and reserviert 16 Bytes auf dem Stack für diese Funktion. (Beachten Sie, dass es in der optimierten Version fehlt, da es die Notwendigkeit für die Speicherung beliebiger Variablen beseitigt.)

%Vor%

sub (asm name = _main ) ist wahrscheinlich eine gcc run-time-Bibliotheksfunktion, die Konstruktoren für Dinge aufruft, die sie brauchen. Vielleicht ein bißchen Bibliothekseinrichtungskram, und es könnte der Ort sein, von dem Konstruktoren für Ihre eigenen globalen / statischen Variablen aufgerufen werden. (Diese alte Mailinglisten-Nachricht zeigt __main ist für Konstruktoren, aber das es main sollte es nicht auf Plattformen aufrufen müssen, die es unterstützen, den Startup-Code aufzurufen, es aufzurufen. Vielleicht hat i386 das nicht, nur amd64?) edit: Sie sagten in einem Kommentar, dass dies von Cygwin kam. Das würde es erklären, da Cygwin nicht-ELF .exes machen muss.

%Vor%
  

Warum ist die ursprüngliche Ausgabe von GCC so viel ausführlicher?

Ohne aktivierte Optimierungen ordnet gcc C-Anweisungen so wörtlich wie möglich in asm zu. Etwas anderes zu tun würde mehr Zeit für die Kompilierung erfordern. Daher stammt _main von den Initialisierern für Ihre beiden Variablen. Der Rückgabewert wird durch Ausführen von zwei Ladevorgängen berechnet (mit Vorzeichenerweiterung, da wir VOR der Addition auf int hochkonvertieren müssen, um der Semantik des C-Codes wie geschrieben zu entsprechen, soweit ein Überlauf vorliegt).

  

Ich kann nicht herausfinden, wofür die beiden Subtraktionsoperationen sind.

Es gibt nur eine movb Anweisung. Es reserviert Speicherplatz auf dem Stack für die Variablen der Funktion vor dem Aufruf von sub . Über welches andere Sub sprechen Sie?

  

Was tun .section, .ident, .def .p2align usw. usw.?

Siehe das Handbuch für den GNU Assembler . Auch lokal als Infoseiten verfügbar: run __main .

info gas und .ident : Sieht so aus, als ob gcc der Objektdatei den Stempel aufdrückt, damit Sie sehen können, welcher Compiler / Assembler es erzeugt hat. Nicht relevant, ignoriere diese.

.def : bestimmt, in welchen Abschnitt der ELF-Objektdatei die Bytes aller folgenden Anweisungen oder Datenanweisungen (z. B. .section ) eingehen, bis zur nächsten .byte 0x00 Assembler-Direktive. Entweder .section (schreibgeschützt, gemeinsam nutzbar), code (initialisierte Lese- / Schreibdaten, privat) oder data (Blockspeichersegment. Null initialisiert, nimmt in der Objektdatei keinen Platz ein).

bss : Potenz von 2 Align. Pad mit NOP-Anweisungen bis zur gewünschten Ausrichtung. .p2align ist identisch mit .align 16 . Der Sprungbefehl ist schneller, wenn das Ziel ausgerichtet ist, weil der Befehl in Blöcken von 16B abgerufen wird, eine Seitengrenze nicht überquert wird oder einfach eine Cache-Zeilengrenze nicht überquert wird. (Die 32B-Ausrichtung ist relevant, wenn sich der Code bereits im UOP-Cache einer Intel Sandybridge und später befindet.) Siehe beispielsweise Agner Fogs Dokumente .

  

Der Grund warum ich dieses Bit hinzugefügt habe, ist zu veranschaulichen, warum ich verwirrt bin   dass die 4-Linien-Version dieses Assembler-Codes effektiv erreichen kann   der gleiche Effekt wie die anderen. Es scheint mir, dass der GCC viel hinzugefügt hat   von Dingen, deren Zweck ich nicht erkennen kann.

Fügen Sie den Code von Interesse in eine Funktion selbst ein. Viele Dinge sind etwas Besonderes an .p2align 4 .

Sie haben Recht, dass ein main -immediate und ein mov alles sind, was benötigt wird, um die Funktion zu implementieren, aber gcc hat offensichtlich keine Abkürzungen, um triviale ganze Programme zu erkennen und den Stack-Frame von ret wegzulassen oder der Aufruf von main . & gt;. & lt;

Gute Frage allerdings. Wie gesagt, ignorieren Sie einfach diesen ganzen Mist und sorgen Sie sich nur um den kleinen Teil, den Sie optimieren möchten.

    
Peter Cordes 02.07.2015, 19:54
quelle
5

.cfi (Call-Rahmeninformationen) Richtlinien verwendet werden, in gas (Gnu Assembler), vor allem für die Fehlersuche. Sie ermöglichen es dem Debugger, den Stapel aufzuwickeln. Um sie zu deaktivieren, können Sie den folgenden Parameter verwenden, wenn Sie den Kompilierungstreiber -fno-asynchronous-unwind-tables aufrufen.

Wenn Sie mit dem Compiler im Allgemeinen spielen möchten, können Sie die folgende Zusammenstellung Treiber Aufruf Befehl -o <filename.S> -S -masm=intel -fno-asynchronous-unwind-tables <filename.C> oder benutzen Sie einfach Godbolt interaktiven Compiler

    
NlightNFotis 01.07.2015 17:27
quelle
1

Zunächst ist das CFI-Zeug für Debugging-Zwecke (und in C ++, Exception-Handling). Es teilt dem Debugger mit, wie der Stapelrahmen bei jeder Anweisung aussieht, so dass der Debugger den Status der Programmvariablen rekonstruieren kann. Diese führen nicht zu ausführbaren Anweisungen und haben keine Auswirkungen auf die Laufzeitleistung eines Programms.

Ich weiß nicht, was der Aufruf von __main dort macht - mein GCC tut das nicht. Tatsächlich gibt mir mein GCC (4.9.2) folgendes für gcc test.c -S -O1 :

%Vor%

und würdest du dir das ansehen, _main ist genau die Zwei-Befehlsfolge, die du erwartet hast. (Der __eh_frame stuff ist mehr Debugging-Informationen in einem anderen Format).

    
nneonneo 01.07.2015 16:53
quelle
1

Die Option -o0 leitet die Ausgabe an eine Datei namens 0 . Vielleicht meinst du das Optimierungslevel (das ist Kapital O ): das deaktiviert Optimierungen.

Ich verstehe nicht, warum es einen Aufruf von ____main geben würde, es sei denn, dies wurde für eine emulierte oder angehakte Umgebung erzeugt. Wenn ich mit gcc -O0 -c -S t.c kompiliere, bekomme ich:

%Vor%

Vielleicht haben Sie ein hohes Optimierungsniveau erwartet? Das bekomme ich mit gcc -O3 -c -S t.c :

%Vor%

Abgesehen von den Debug-Informationen ist es so kurz wie möglich. Derselbe Code wird für gcc -O2 -c -S t.c und gcc -O1 -c -S t.c erstellt. Das heißt, die geringste Optimierung wertet alle Konstanten zum Zeitpunkt der Kompilierung aus.

    
wallyk 01.07.2015 17:44
quelle
0

Ich denke, dass dieser Teil nur ein festes Muster ist, das einen 16-Byte ausgerichteten Stapel aufbaut und der CFI ist Ausnahmerahmen Handhabung.

Es ist schwierig, festzustellen, dass diese für main () nicht benötigt werden, da dies eine globale Optimierung ist, da main Funktionen in anderen Kompilierungseinheiten aufrufen kann.

Und es lohnt sich wahrscheinlich nicht, die Zeit zu nutzen, um diesen trivialen und ziemlich nutzlosen Fall zu optimieren.

Wenn Sie sich anders fühlen, können Sie immer an einer solchen Optimierung arbeiten und sie an gcc senden.

    
Marco van de Voort 01.07.2015 16:27
quelle

Tags und Links