Was ist der Zweck des EBP-Rahmenzeigerregisters?


94

Ich bin ein Anfänger in Assemblersprache und habe festgestellt, dass der von Compilern ausgegebene x86-Code den Frame-Zeiger normalerweise auch im Release- / optimierten Modus herumhält, wenn er das EBPRegister für etwas anderes verwenden könnte.

Ich verstehe, warum der Frame-Zeiger das Debuggen von Code erleichtert und möglicherweise erforderlich ist, wenn alloca()er innerhalb einer Funktion aufgerufen wird. X86 hat jedoch nur sehr wenige Register, und zwei davon zu verwenden, um die Position des Stapelrahmens zu halten, wenn eines ausreichen würde, macht für mich einfach keinen Sinn. Warum wird das Weglassen des Frame-Zeigers selbst in optimierten / Release-Builds als schlechte Idee angesehen?


19
Wenn Sie denken, dass x86 nur sehr wenige Register hat, sollten Sie 6502 überprüfen :)
Sedat Kapanoglu


1
Davon kann auch C99 VLA profitieren.
Ciro Santilli 法轮功 冠状 病 六四 事件 1


1
Macht der Rahmenzeiger den Stapelzeiger nicht überflüssig? . TL; DR: 1. Nicht triviale Stapelausrichtung 2. Stapelzuweisung ( alloca) 3. Einfache Laufzeitimplementierung: Ausnahmebehandlung, Sandbox, GC
Alexander Malakhov

Antworten:


102

Der Frame-Zeiger ist ein Referenzzeiger, mit dem ein Debugger mit einem einzigen konstanten Offset erkennen kann, wo sich die lokale Variable oder ein Argument befindet. Obwohl sich der Wert von ESP im Laufe der Ausführung ändert, bleibt EBP derselbe, sodass dieselbe Variable bei demselben Offset erreicht werden kann (z. B. der erste Parameter liegt immer bei EBP + 8, während sich die ESP-Offsets erheblich ändern können, da Sie Druck ausüben / knallende Dinge)

Warum werfen Compiler den Frame-Zeiger nicht weg? Denn mit dem Frame-Zeiger kann der Debugger herausfinden, wo lokale Variablen und Argumente die Symboltabelle verwenden, da sie garantiert einen konstanten Versatz zu EBP aufweisen. Andernfalls gibt es keine einfache Möglichkeit, herauszufinden, wo sich eine lokale Variable an einem beliebigen Punkt im Code befindet.

Wie Greg erwähnt hat, hilft es auch beim Abwickeln des Stapels für einen Debugger, da EBP eine umgekehrt verknüpfte Liste von Stapelrahmen bereitstellt, sodass der Debugger die Größe des Stapelrahmens (lokale Variablen + Argumente) der Funktion ermitteln kann.

Die meisten Compiler bieten die Möglichkeit, Frame-Zeiger wegzulassen, obwohl dies das Debuggen sehr erschwert. Diese Option sollte niemals global verwendet werden, auch nicht im Release-Code. Sie wissen nicht, wann Sie den Absturz eines Benutzers debuggen müssen.


10
Der Compiler weiß wahrscheinlich, was er mit ESP macht. Die anderen Punkte sind jedoch gültig, +1
erikkallen

8
Moderne Debugger können Stack-Backtraces auch in Code ausführen, mit dem kompiliert wurde -fomit-frame-pointer. Diese Einstellung ist die Standardeinstellung in der letzten gcc.
Peter Cordes

2
@SedatKapanoglu: Ein Datenabschnitt zeichnet die notwendigen Informationen auf: yosefk.com/blog/…
Peter Cordes

3
@SedatKapanoglu: Der .eh_frame_hdrAbschnitt wird auch für Laufzeitausnahmen verwendet. Sie finden es (mit objdump -h) in den meisten Binärdateien auf einem Linux-System. Es ist ungefähr 16k für /bin/bash, gegenüber 572B für GNU /bin/true, 108k für ffmpeg. Es gibt eine gcc-Option, um das Generieren zu deaktivieren, aber es ist ein "normaler" Datenabschnitt, kein Debug-Abschnitt, stripder standardmäßig entfernt wird. Andernfalls könnten Sie nicht durch eine Bibliotheksfunktion zurückverfolgen, die keine Debug-Symbole hatte. Dieser Abschnitt ist möglicherweise größer als die push/mov/popAnweisungen, die er ersetzt, hat jedoch Laufzeitkosten nahe Null (z. B. UOP-Cache).
Peter Cordes

3
Zu "wie der erste Parameter immer bei EBP-4 sein wird": Ist der erste Parameter nicht bei EBP + 8 (bei x86)?
Aydin K.

31

Ich addiere nur meine zwei Cent zu bereits guten Antworten.

Es ist Teil einer guten Spracharchitektur, eine Kette von Stapelrahmen zu haben. Der BP zeigt auf den aktuellen Frame, in dem subroutinenlokale Variablen gespeichert sind. (Einheimische haben negative Offsets und Argumente positive Offsets.)

Die Idee, dass verhindert wird, dass ein perfektes Register für die Optimierung verwendet wird, wirft die Frage auf: Wann und wo lohnt sich die Optimierung tatsächlich?

Die Optimierung lohnt sich nur in engen Schleifen, in denen 1) keine Funktionen aufgerufen werden, 2) der Programmzähler einen erheblichen Teil seiner Zeit verbringt und 3) der Code, den der Compiler tatsächlich jemals sehen wird (dh Funktionen außerhalb der Bibliothek). Dies ist normalerweise ein sehr kleiner Teil des gesamten Codes, insbesondere in großen Systemen.

Anderer Code kann verdreht und zusammengedrückt werden, um Zyklen loszuwerden, und es spielt einfach keine Rolle, da der Programmzähler praktisch nie da ist.

Ich weiß, dass Sie dies nicht gefragt haben, aber meiner Erfahrung nach haben 99% der Leistungsprobleme überhaupt nichts mit der Compileroptimierung zu tun. Sie haben alles mit Überdesign zu tun.


Danke @Mike, ich fand deine Antwort sehr hilfreich.
Sixtyfootersdude

2
Wenn Sie den Frame-Zeiger weglassen, sparen Sie außerdem bei jedem Funktionsaufruf ein paar Anweisungen. Dies ist eine kleine Optimierung für sich. Übrigens ist Ihre Verwendung von "wirft die Frage auf" falsch; du meinst "wirft die Frage auf".
August

@augurar: Behoben. Vielen Dank. Ich bin selbst ein bisschen ein Grammatik-Grummel :)
Mike Dunlavey

3
@augurar Sprache entwickelt sich: "Bittet um die Frage" bedeutet jetzt nur noch "wirft die Frage auf". Ein verschreibungspflichtiger Nitpicker für veraltete Verwendung zu sein, bringt nichts.
user3364825

9

Das hängt natürlich vom Compiler ab. Ich habe optimierten Code gesehen, der von x86-Compilern ausgegeben wird, die das EBP-Register frei als Allzweckregister verwenden. (Ich erinnere mich jedoch nicht, mit welchem ​​Compiler ich das bemerkt habe.)

Compiler können auch das EBP-Register verwalten, um das Abwickeln des Stapels während der Ausnahmebehandlung zu unterstützen. Dies hängt jedoch wiederum von der genauen Implementierung des Compilers ab.


Die meisten Compiler verwenden standardmäßig, -fomit-frame-pointerwenn die Optimierung aktiviert ist. (wenn der ABI es erlaubt). GCC, Clang, ICC und MSVC machen es alle, IIRC, selbst wenn sie auf 32-Bit-Windows abzielen. Ja, meine Antwort auf Warum ist es besser, das ebp als das esp-Register zu verwenden, um Parameter auf dem Stapel zu lokalisieren? zeigt, dass auch 32-Bit-Windows den Frame-Zeiger weglassen kann. 32-Bit x86 Linux kann und tut es definitiv. Und natürlich haben 64-Bit-ABIs von Anfang an das Weglassen von Frame-Zeigern ermöglicht.
Peter Cordes

4

X86 hat jedoch nur sehr wenige Register

Dies gilt nur in dem Sinne, dass Opcodes nur 8 Register adressieren können. Der Prozessor selbst wird tatsächlich viel mehr Register haben und Registerumbenennung, Pipelining, spekulative Ausführung und andere Prozessor-Schlagworte verwenden, um diese Grenze zu umgehen. Wikipedia hat einen guten einleitenden Abschnitt darüber, was ein x86-Prozessor tun kann, um das Registerlimit zu überwinden: http://en.wikipedia.org/wiki/X86#Current_implementations .


1
Die ursprüngliche Frage bezieht sich auf generierten Code, der streng auf die Register beschränkt ist, auf die durch Opcodes verwiesen werden kann.
Darron

1
Ja, aber aus diesem Grund ist es heutzutage nicht mehr so ​​wichtig, den Frame-Zeiger in optimierten Builds wegzulassen.
Michael

1
Das Umbenennen von Registern ist jedoch nicht ganz das Gleiche wie das Vorhandensein einer größeren Anzahl von Registern. Es gibt immer noch viele Situationen, in denen das Umbenennen von Registern nicht hilft, aber "regulärere" Register.
Jalf

1

Die Verwendung von Stack-Frames ist in jeder Hardware, auch aus der Ferne, unglaublich billig geworden. Wenn Sie billige Stapelrahmen haben, ist das Speichern einiger Register nicht so wichtig. Ich bin sicher, dass schnelle Stapelrahmen im Vergleich zu mehr Registern ein technischer Kompromiss waren und schnelle Stapelrahmen gewonnen haben.

Wie viel sparen Sie bei der reinen Registrierung? Lohnt es sich?


Mehr Register werden durch Befehlscodierung begrenzt. x86-64 verwendet Bits im REX-Präfixbyte, um den registerspezifischen Teil der Anweisungen von 3 auf 4 Bits für src- und dest-Register zu erweitern. Wenn Platz vorhanden wäre, wäre x86-64 wahrscheinlich in 32 Architekturregister gegangen, obwohl sich das Speichern / Wiederherstellen so vieler Kontextschalter summiert. 15 ist ein großer Fortschritt gegenüber 7, aber 31 ist in den meisten Fällen eine viel geringere Verbesserung. (Der Stapelzeiger wird nicht als Allzweck gezählt.) Push / Pop schnell zu machen ist großartig für mehr als nur Stapelrahmen. Es ist jedoch kein Kompromiss mit der Anzahl der Regs.
Peter Cordes
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.