Der Versuch, die gcc-Option -fomit-frame-pointer zu verstehen

79

Ich habe Google gebeten, mir die Bedeutung der gccOption anzugeben -fomit-frame-pointer, wodurch ich zur folgenden Anweisung weitergeleitet werde.

-fomit-frame-pointer

Bewahren Sie den Frame-Zeiger nicht in einem Register für Funktionen auf, die keinen benötigen. Dadurch werden die Anweisungen zum Speichern, Einrichten und Wiederherstellen von Frame-Zeigern vermieden. Es stellt auch ein zusätzliches Register in vielen Funktionen zur Verfügung. Es macht auch das Debuggen auf einigen Computern unmöglich.

Nach meinem Wissen über jede Funktion wird ein Aktivierungsdatensatz im Stapel des Prozessspeichers erstellt, um alle lokalen Variablen und einige weitere Informationen zu speichern. Ich hoffe, dieser Frame-Zeiger bedeutet die Adresse des Aktivierungsdatensatzes einer Funktion.

Was sind in diesem Fall die Arten von Funktionen, für die der Frame-Zeiger nicht in einem Register gespeichert werden muss? Wenn ich diese Informationen erhalte, werde ich versuchen, die neue Funktion basierend darauf zu entwerfen (falls möglich), da einige Anweisungen in Binärform weggelassen werden, wenn der Rahmenzeiger nicht in Registern gespeichert ist. Dies wird die Leistung in einer Anwendung mit vielen Funktionen spürbar verbessern.

Rashok
quelle
4
Das Debuggen nur eines Crash-Dumps von Code, der mit dieser Option kompiliert wurde, reicht aus, um Sie dazu zu bringen, diese Option aus Ihren Makefiles zu entfernen. Es entfernt übrigens keine Anweisungen, sondern gibt dem Optimierer nur ein weiteres Register, mit dem er für die Speicherung arbeiten kann.
Hans Passant
1
@HansPassant Eigentlich ist es ziemlich nützlich für Release-Builds. Nehmen Sie diese Option als Beispiel, wenn Sie zwei Ziele in einem Makefile haben - Releaseund das Debugist tatsächlich sehr nützlich.
Kotauskas
2
@VladislavToncharov Ich denke, Sie mussten noch nie einen Crash-Dump von einem Kunden debuggen, der Ihr Release-build ausführt?
Andreas Magnusson

Antworten:

58

Die meisten kleineren Funktionen benötigen keinen Frame-Zeiger - größere Funktionen benötigen möglicherweise einen.

Es geht wirklich darum, wie gut der Compiler es schafft, zu verfolgen, wie der Stapel verwendet wird und wo sich die Dinge auf dem Stapel befinden (lokale Variablen, an die aktuelle Funktion übergebene Argumente und Argumente, die für eine Funktion vorbereitet werden, die aufgerufen werden soll). Ich denke nicht, dass es einfach ist, die Funktionen zu charakterisieren, die einen Frame-Zeiger benötigen oder nicht benötigen (technisch gesehen muss KEINE Funktion einen Frame-Zeiger haben - es ist eher ein Fall von "wenn der Compiler es für notwendig hält, die Komplexität von zu reduzieren." anderer Code ").

Ich denke nicht, dass Sie "versuchen sollten, Funktionen so zu gestalten, dass sie keinen Frame-Zeiger haben" als Teil Ihrer Codierungsstrategie - wie gesagt, einfache Funktionen benötigen sie nicht. Verwenden -fomit-frame-pointerSie sie also , und Sie erhalten ein weiteres Register zur Verfügung für den Registerzuordner und speichern Sie 1-3 Anweisungen beim Ein- / Ausstieg in Funktionen. Wenn Ihre Funktion einen Frame-Zeiger benötigt, ist der Compiler der Meinung, dass dies eine bessere Option ist, als keinen Frame-Zeiger zu verwenden. Es ist kein Ziel, Funktionen ohne Frame-Zeiger zu haben, sondern ein Code, der sowohl korrekt als auch schnell funktioniert.

Beachten Sie, dass "kein Frame-Zeiger" eine bessere Leistung bringen sollte, aber es ist kein Wundermittel, das enorme Verbesserungen bringt - insbesondere nicht bei x86-64, das bereits 16 Register hat. Bei 32-Bit-x86 bedeutet dies, dass 25% des Registerplatzes belegt sind, da nur 8 Register vorhanden sind, von denen eines der Stapelzeiger ist und ein anderes als Rahmenzeiger verwendet wird. Dies auf 12,5% zu ändern, ist eine ziemliche Verbesserung. Natürlich hilft auch das Kompilieren für 64-Bit sehr.

Mats Petersson
quelle
24
Normalerweise kann der Compiler die Stapeltiefe selbst verfolgen und benötigt keinen Frame-Zeiger. Die Ausnahme ist, wenn die Funktion verwendet, allocadie den Stapelzeiger um einen variablen Betrag bewegt. Das Auslassen von Frame-Zeigern erschwert das Debuggen erheblich. Lokale Variablen sind schwieriger zu lokalisieren und Stapelspuren sind viel schwieriger zu rekonstruieren, ohne dass ein Frame-Zeiger hilft. Außerdem kann der Zugriff auf Parameter teurer werden, da sie weit vom oberen Rand des Stapels entfernt sind und möglicherweise teurere Adressierungsmodi erfordern.
Raymond Chen
3
Ja, vorausgesetzt, wir verwenden nicht alloca[wer tut das? - Ich bin mir zu 99% sicher, dass ich noch nie Code geschrieben habe, der alloca] oder variable size local arrays[was eine moderne Form von alloca] ist, dann kann der Compiler immer noch entscheiden, dass die Verwendung eines Frame-Zeigers eine bessere Option ist - da Compiler so geschrieben sind, dass sie dem nicht blind folgen Optionen gegeben, aber geben Sie die besten Entscheidungen.
Mats Petersson
6
@MatsPetersson VLA unterscheidet sich von alloca: Sie werden weggeworfen, sobald Sie den Bereich verlassen, in dem sie deklariert sind, während allocaSpeicherplatz nur freigegeben wird, wenn Sie die Funktion verlassen. Dies macht es viel einfacher, VLA zu folgen, als allocaich denke.
Jens Gustedt
34
Es ist vielleicht erwähnenswert, dass gcc -fomit-frame-pointerfür x86-64 standardmäßig aktiviert ist.
zwol
5
@JensGustedt, das Problem ist nicht, wenn sie weggeworfen werden, das Problem ist, dass ihre Größe (wie alloca'ed space) zum Zeitpunkt der Kompilierung unbekannt ist . Normalerweise verwendet der Compiler den Frame-Zeiger, um die Adresse lokaler Variablen abzurufen. Wenn sich die Größe des Stack-Frames nicht ändert, kann er sie an einem festen Versatz zum Stack-Zeiger lokalisieren.
vonbrand
15

Hier geht es um das BP / EBP / RBP-Register auf Intel-Plattformen. Dieses Register ist standardmäßig auf das Stapelsegment eingestellt (für den Zugriff auf das Stapelsegment ist kein spezielles Präfix erforderlich).

Das EBP ist die beste Wahl für den Zugriff auf Datenstrukturen, Variablen und dynamisch zugewiesenen Arbeitsbereich innerhalb des Stapels. EBP wird häufig verwendet, um auf Elemente auf dem Stapel relativ zu einem festen Punkt auf dem Stapel und nicht relativ zu den aktuellen Nutzungsbedingungen zuzugreifen. Es identifiziert typischerweise die Basisadresse des aktuellen Stapelrahmens, der für die aktuelle Prozedur eingerichtet wurde. Wenn EBP als Basisregister in einer Versatzberechnung verwendet wird, wird der Versatz automatisch im aktuellen Stapelsegment (dh dem aktuell von SS ausgewählten Segment) berechnet. Da SS nicht explizit angegeben werden muss, ist die Befehlskodierung in solchen Fällen effizienter. EBP kann auch verwendet werden, um in Segmente zu indizieren, die über andere Segmentregister adressierbar sind.

(Quelle - http://css.csail.mit.edu/6.858/2017/readings/i386/s02_03.htm )

Da auf den meisten 32-Bit-Plattformen Datensegment und Stapelsegment identisch sind, ist diese Zuordnung von EBP / RBP zum Stapel kein Problem mehr. Dies gilt auch für 64-Bit-Plattformen: Die von AMD 2003 eingeführte x86-64-Architektur hat die Unterstützung für die Segmentierung im 64-Bit-Modus weitgehend eingestellt: Vier der Segmentregister: CS, SS, DS und ES werden auf 0 gesetzt Diese Umstände von x86 32-Bit- und 64-Bit-Plattformen bedeuten im Wesentlichen, dass das EBP / RBP-Register ohne Präfix in den Prozessoranweisungen verwendet werden kann, die auf den Speicher zugreifen.

Die Compiler-Option, über die Sie geschrieben haben, ermöglicht es also, BP / EBP / RBP für andere Zwecke zu verwenden, z. B. um eine lokale Variable zu speichern.

Mit "Dies vermeidet die Anweisungen zum Speichern, Einrichten und Wiederherstellen von Frame-Zeigern" ist das Vermeiden des folgenden Codes bei der Eingabe jeder Funktion gemeint:

push ebp
mov ebp, esp

oder die enterAnweisung, die auf Intel 80286- und 80386-Prozessoren sehr nützlich war.

Vor der Rückkehr der Funktion wird außerdem der folgende Code verwendet:

mov esp, ebp
pop ebp 

oder die leaveAnweisung.

Debugging-Tools können die Stack-Daten scannen und diese Push-EBP-Registerdaten beim Auffinden verwenden call sites, dh um die Namen der Funktion und die Argumente in der Reihenfolge anzuzeigen, in der sie hierarchisch aufgerufen wurden.

Programmierer haben möglicherweise Fragen zu Stapelrahmen nicht in einem weiten Sinne (dass es sich um eine einzelne Entität im Stapel handelt, die nur einen Funktionsaufruf bedient und die Rücksprungadresse, Argumente und lokalen Variablen beibehält), sondern im engeren Sinne - wenn der Begriff stack framesin erwähnt wird den Kontext der Compileroptionen. Aus Sicht des Compilers ist ein Stapelrahmen nur der Eingangs- und Ausgangscode für die Routine , der einen Anker auf den Stapel schiebt - der auch zum Debuggen und zur Ausnahmebehandlung verwendet werden kann. Debugging-Tools können die Stapeldaten scannen und diese Anker für die Rückverfolgung verwenden, während sie sich call sitesim Stapel befinden, dh um die Namen der Funktion in der Reihenfolge anzuzeigen, in der sie hierarchisch aufgerufen wurden.

Aus diesem Grund ist es für einen Programmierer sehr wichtig zu verstehen, was ein Stapelrahmen in Bezug auf Compileroptionen ist - da der Compiler steuern kann, ob dieser Code generiert werden soll oder nicht.

In einigen Fällen kann der Compiler auf den Stapelrahmen (Eingangs- und Ausgangscode für die Routine) verzichten, und auf die Variablen wird direkt über den Stapelzeiger (SP / ESP / RSP) und nicht über den praktischen Basiszeiger (BP /) zugegriffen. ESP / RSP). Die Bedingungen, unter denen ein Compiler die Stapelrahmen für einige Funktionen weglässt, können unterschiedlich sein, zum Beispiel: (1) Die Funktion ist eine Blattfunktion (dh eine Endeinheit, die keine anderen Funktionen aufruft). (2) es werden keine Ausnahmen verwendet; (3) Es werden keine Routinen mit ausgehenden Parametern auf dem Stapel aufgerufen. (4) Die Funktion hat keine Parameter.

Das Weglassen von Stapelrahmen (Eingabe- und Ausstiegscode für die Routine) kann den Code kleiner und schneller machen, kann jedoch auch die Fähigkeit der Debugger beeinträchtigen, die Daten im Stapel zurückzuverfolgen und dem Programmierer anzuzeigen. Dies sind die Compileroptionen, die bestimmen, unter welchen Bedingungen eine Funktion erfüllt sein soll, damit der Compiler ihr den Stack-Frame-Ein- und Ausstiegscode zuweist. Beispielsweise kann ein Compiler in den folgenden Fällen Optionen zum Hinzufügen eines solchen Eingangs- und Ausgangscodes zu Funktionen haben: (a) immer, (b) nie, (c) bei Bedarf (Angabe der Bedingungen).

Zurück von den Allgemeinheiten zu den Besonderheiten: Wenn Sie die -fomit-frame-pointerGCC-Compileroption verwenden, können Sie sowohl beim Eingabe- als auch beim Ausstiegscode für die Routine und beim Vorhandensein eines zusätzlichen Registers gewinnen (es sei denn, es ist standardmäßig entweder selbst oder implizit von anderen aktiviert Optionen, in diesem Fall profitieren Sie bereits vom Gewinn der Verwendung des EBP / RBP-Registers, und durch explizite Angabe dieser Option wird kein zusätzlicher Gewinn erzielt, wenn sie bereits implizit aktiviert ist. Beachten Sie jedoch, dass das BP-Register im 16-Bit- und 32-Bit-Modus nicht wie AX (AL und AH) auf 8-Bit-Teile zugreifen kann.

Da diese Option nicht nur dem Compiler ermöglicht, EBP als Allzweckregister für Optimierungen zu verwenden, sondern auch das Generieren von Exit- und Entry-Code für den Stack-Frame verhindert, was das Debuggen erschwert, wird in der GCC-Dokumentation ausdrücklich angegeben (ungewöhnlich fett hervorgehoben) Stil), dass das Aktivieren dieser Option das Debuggen auf einigen Computern unmöglich macht

Beachten Sie auch, dass andere Compileroptionen, die sich auf das Debuggen oder Optimieren beziehen, die -fomit-frame-pointerOption möglicherweise implizit ein- oder ausschalten.

Ich habe auf gcc.gnu.org keine offiziellen Informationen darüber gefunden, wie sich andere Optionen -fomit-frame-pointer auf x86-Plattformen auswirken , https://gcc.gnu.org/onlinedocs/gcc-3.4.4/gcc/Optimize-Options.html gibt nur folgendes an:

-O aktiviert auch -fomit-frame-pointer auf Computern, auf denen dies das Debuggen nicht beeinträchtigt.

Aus der Dokumentation an sich geht also nicht hervor, ob sie aktiviert-fomit-frame-pointer wird, wenn Sie nur mit einer einzigen -OOption auf der x86-Plattform kompilieren . Es kann empirisch getestet werden, aber in diesem Fall sind die GCC-Entwickler nicht verpflichtet, das Verhalten dieser Option in Zukunft nicht ohne vorherige Ankündigung zu ändern.

Doch Peter Cordes hat in den Kommentaren darauf hingewiesen , dass es einen Unterschied für die Standardeinstellungen der -fomit-frame-pointerzwischen x86-16 - Plattformen und x86-32 / 64 - Plattformen.

Diese Option - -fomit-frame-pointer- ist auch für den Intel C ++ Compiler 15.0 relevant , nicht nur für den GCC:

Für den Intel Compiler hat diese Option einen Alias /Oy.

Folgendes hat Intel darüber geschrieben:

Diese Optionen bestimmen, ob EBP bei Optimierungen als Allzweckregister verwendet wird. Die Optionen -fomit-frame-pointer und / Oy ermöglichen diese Verwendung. Optionen -fno-omit-frame-pointer und / Oy- verbieten es.

Einige Debugger erwarten, dass EBP als Stapelrahmenzeiger verwendet wird, und können keine Stapelrückverfolgung erzeugen, wenn dies nicht der Fall ist. Die Optionen -fno-omit-frame-pointer und / Oy- weisen den Compiler an, Code zu generieren, der EBP als Stack-Frame-Zeiger für alle Funktionen verwaltet und verwendet, sodass ein Debugger weiterhin einen Stack-Backtrace erstellen kann, ohne Folgendes zu tun:

Für -fno-omit-frame-pointer: Deaktivieren von Optimierungen mit -O0 Für / Oy-: Deaktivieren von / O1-, / O2- oder / O3-Optimierungen Die Option -fno-omit-frame-pointer wird festgelegt, wenn Sie die Option - angeben. O0 oder die Option -g. Die Option -fomit-frame-pointer wird festgelegt, wenn Sie die Option -O1, -O2 oder -O3 angeben.

Die Option / Oy wird festgelegt, wenn Sie die Option / O1, / O2 oder / O3 angeben. Option / Oy- wird festgelegt, wenn Sie die Option / Od angeben.

Die Verwendung des Option -fno-omit-frame-pointer oder der Option / Oy- reduziert die Anzahl der verfügbaren Universalregister um 1 und kann zu etwas weniger effizientem Code führen.

HINWEIS Für Linux * -Systeme: Derzeit liegt ein Problem mit der Behandlung von GCC 3.2-Ausnahmen vor. Daher ignoriert der Intel-Compiler diese Option, wenn GCC 3.2 für C ++ installiert und die Ausnahmebehandlung aktiviert ist (Standardeinstellung).

Bitte beachten Sie, dass das obige Zitat nur für den Intel C ++ 15-Compiler relevant ist, nicht für GCC.

Maxim Masiutin
quelle
1
16-Bit-Code und BP standardmäßig SS anstelle von DS sind für gcc nicht wirklich relevant. gcc -m16existiert, aber das ist ein seltsamer Sonderfall, bei dem im Grunde genommen 32-Bit-Code erstellt wird, der im 16-Bit-Modus mit Präfixen überall ausgeführt wird. Beachten Sie auch, dass dies -fomit-frame-pointerunter x86 seit Jahren standardmäßig aktiviert -m32ist und länger als unter x86-64 ( -m64).
Peter Cordes
@PeterCordes - Vielen Dank, ich habe die Änderungen entsprechend den von Ihnen angesprochenen Problemen aktualisiert.
Maxim Masiutin