2016-06-02 24 views
2

CSApp 3. Auflage gesagt:Ist das RBP/EBP-Register wirklich notwendig, um Stack-Frames variabler Größe zu unterstützen?

Um ein variabler Größe Stapelrahmens zu verwalten, verwendet x86-64 Code% RBP zum Server als Rahmenzeigerregister.

Allerdings bin ich gespannt, ob dieses %rbp Register wirklich notwendig ist. Obwohl der Compiler nicht weiß, wie viel Platz er für den Stack-Frame der Funktion reservieren muss, kann er immer die aktuell zugewiesene Größe des Stack-Frames in einem beliebigen Register speichern, nachdem subq xxxx, %rsp aufgerufen wurde. Daher muss er nicht auf %rbp zurückgesetzt werden der Wert von %rsp .. Ist das wahr? Wenn ja, heißt das, %rbp ist überhaupt nicht notwendig, aber nur eine Konvention?

+5

Es ist eine weit verbreitete Konvention, die von einigen ABIs verlangt wird. Aber wenn Sie Ihr eigenes ABI erfinden, dann müssen Sie '% rbp' nicht verwenden. (Das heißt, '% rbp' ist eine gute Wahl, da es für' (% rbp) 'keinen Adressierungsmodus gibt, Sie müssen' 0 (% rbp) 'verwenden. Dies macht'% rbp' zu einer schlechten Wahl für einen General Zweck-Zeiger, aber es ist okay als ein Rahmenzeiger, weil Sie niemals auf '(% rbp)' zugreifen müssen, da alles, was es enthält, der vorherige Rahmenzeiger ist.) –

+0

Es gibt auch eine 'Leave'-Anweisung, die' mov% rbp,% rsp '/' pop% rbp'. Es ist 3 Ups auf Intel, vs 2 Ups für die gleiche Sache "manuell", aber es ist nur 1 Byte. –

+2

Es verwendet auch implizit 'ss' als Selektor/Segment und war eines der wenigen im Realmodus verfügbaren Basisregister. –

Antwort

3

Sie haben Recht. Wenn Sie die Größe, die Sie in variabler Größe verwenden halten sub xxx, %rsp, können Sie es umgekehrt mit einem add am Ende (oder mit einem lea fixed_size(%rsp,%rdi,4), %rsp auch ausplanen keiner festen Größe Stack-Platz reserviert.

Wie @Ross weist darauf hin, , dies skaliert nicht gut zu mehreren Zuweisungen mit variabler Länge in der gleichen Funktion. Selbst mit einem einzelnen VLA ist es nicht schneller als ein mov %rbp, %rsp (oder leave) am Ende der Funktion. Es würde den Compiler die Größe und verschütten lassen haben 15 freie Register anstelle von 14 für Teile der Funktion, die es nie mit %rbp macht, wenn es als Rahmenzeiger verwendet wird. Wie auch immer, dies bedeutet, dass gcc immer noch auf einen Rahmenzeiger für komplexe Fälle zurückgreifen würde. Der Standardwert ist , aber machen Sie sich keine Sorgen dass es gcc nicht zwingt, niemals einen zu benutzen).

Mit %rbp als ein Rahmenzeiger einige kleinere Vorteile, vor allem in Code-Größe hat: Ein addressing mode mit %rsp als das Basisregister immer ein SIB-Byte (Skala/Index/Basis) benötigt, da die Mod/RM-Codierung das würde bedeuten, (%rsp) ist eigentlich eine Escape-Sequenz, um anzuzeigen, dass es ein SIB-Byte gibt. In ähnlicher Weise bedeutet die Kodierung, die (%rbp) ohne Verschiebung bedeutet, dass es überhaupt kein Basisregister gibt, so dass Sie immer ein disp8 Byte wie 0(%rbp) benötigen.

Zum Beispiel ist mov %eax, 16(%rsp) 1B länger als mov %eax, -8(%rbp). Jan Hubicka suggested, dass es gut wäre, wenn gcc eine Heuristik hätte, um Rahmenzeiger in Funktionen zu aktivieren, in denen die Code-Größe gespeichert wurde, ohne Leistungsregressionen zu verursachen, und denkt, dass dies häufig der Fall ist. Es kann auch einige Stack-Sync-Ups speichern, um die Verwendung von %e/rsp direkt (nach Push/Pop oder Call) auf Intel-CPUs mit einem Stack-Engine zu vermeiden.

GCC verwendet immer %rbp als Rahmenzeiger in jeder Funktion mit C99 Arrays variabler Größe. Wahrscheinlich fanden gcc-Entwickler, dass es sich nicht lohnte, herauszufinden, wann eine solche Funktion ohne Frame-Pointer noch genauso effizient sein könnte, und in diesen seltenen Sonderfällen viel Code in gcc haben.


Aber was, wenn wir einen Rahmen mit Zeiger in einer Funktion mit einem VLA wirklich vermeiden wollten?

Das siebte und spätere Integer-Argument (in der SysV ABI, siehe das -Tag-Wiki) befindet sich auf dem Stapel über der Rücksendeadresse. Ein Zugriff über disp(%rsp) ist nicht möglich, da die Verschiebung zur Kompilierzeit nicht bekannt ist.

disp(%rsp, %rcx, 1) wäre möglich, wobei %rcx die Variable Länge Array-Größe enthält. (Oder die Gesamtgröße aller VLAs). Dies kostet keine zusätzliche Codegröße über disp(%rsp), da Adressierungsmodi mit %rsp als Basisregister bereits ein SIB-Byte verwenden müssen. Aber das bedeutet, dass die VLA-Größe in einem Register in Vollzeit bleiben muss, was uns nichts bringt, wenn ein Rahmenzeiger verwendet wird. (Und verlieren auf Code-Größe).

Die Alternative besteht darin, Skalare/Fixed-size Locals unterhalb von Zuordnungen mit variabler Länge zu halten, sodass wir immer auf sie mit einer festen Verschiebung relativ zu %rsp zugreifen können. Das ist gut für die Code-Größe, da wir disp8 (1B) anstelle von disp32 (4B) verwenden können, um innerhalb von [-128, + 127] Bytes von %rsp zuzugreifen.

Aber es funktioniert nur, wenn Sie die VLA Größe (n) früh bestimmen können, bevor Sie etwas an die Einheimischen verschütten müssen. Also haben Sie wieder einen komplexen Sonderfall, nach dem der Compiler sucht, und er benötigt in diesem Fall eine Menge Code-Generierungscode in gcc.

Wenn Sie die VLA-Größe verschütten und sie vor ret urn erneut verwenden, machen Sie den Wert %rsp abhängig von einem Neuladen aus dem Speicher. Out-of-Order-Ausführung kann diese zusätzliche Latenz wahrscheinlich verbergen, aber es gibt Fälle, in denen diese zusätzliche Latenz alles andere verzögert, was %rsp verwendet, einschließlich der Wiederherstellung der Register des Aufrufers.

Diese Art von Code-Gen hätte wahrscheinlich auch einige Eckfälle für gcc, um damit korrekten und effizienten Code zu erstellen. Da es wenig benutzt wird, wird der "effiziente" Teil davon möglicherweise nicht viel Aufmerksamkeit bekommen.

Es ist ziemlich leicht zu sehen, warum gcc einfach in den Frame-Pointer-Modus zurückfällt, wenn es nicht einfach ist, es wegzulassen. Normalerweise erhält man fast kostenlos ein zusätzliches Register, daher lohnt es sich, den Code-Größenvorteil aufzugeben, auch wenn man viele Einheimische anspricht. Dies gilt insbesondere für 32-Bit-Code, wo Sie von 6 bis 7 allgemeine Register gehen (ohne esp). Dieser Unterschied ist normalerweise bei 64-Bit-Code geringer, wobei 14 gegenüber 15 ein viel kleinerer Unterschied ist. Es speichert immer noch die Push/mov/pop Anweisungen in Funktionen, die sie nicht benötigen, was ein separater Vorteil ist. (Die Verwendung von %rbp als Allzweckregister erfordert immer noch das Drücken/Öffnen.)

+1

Nur in relativ seltenen Fällen würde es sich lohnen, die Gesamtgröße aller Zuweisungen mit variabler Länge in einem Register zu speichern, anstatt die alte Stapelzeigervariable in einem Register zu speichern. –

+0

@RossRidge: einverstanden. Vielleicht in einer trivialen Funktion, bei der Sie nicht einmal aus Scratch-Registern kamen, so dass Sie den Wert einfach dort in dem Register belassen konnten, das ihn bereits enthielt. –

+0

In den schlechten alten Zeiten war es üblich asm zu stehlen bp als Ersatzregister, da es normalerweise nicht für die "zugewiesene" Verwendung benötigt wurde –