Ruby VM Stack und Frame Layout
Dieses Dokument erklärt die Architektur des Ruby VM Stacks, einschließlich der Art und Weise, wie der Wertestack (SP) und die Kontrollframes (CFP) einen einzigen zusammenhängenden Speicherbereich teilen und wie einzelne Frames strukturiert sind.
VM Stack Architektur
Die Ruby VM verwendet einen einzigen zusammenhängenden Stack (ec->vm_stack) mit zwei verschiedenen Regionen, die aufeinander zu wachsen. Das Verständnis hierfür erfordert die Unterscheidung zwischen der Gesamtarchitektur (wie CFPs und Werte einen Stack teilen) und den Interna einzelner Frames (wie Werte für einen einzelnen Frame organisiert sind).
High addresses (ec->vm_stack + ec->vm_stack_size)
↓
[CFP region starts here] ← RUBY_VM_END_CONTROL_FRAME(ec)
[CFP - 1] New frame pushed here (grows downward)
[CFP - 2] Another frame
...
(Unused space - stack overflow when they meet)
... Value stack grows UP toward higher addresses
[SP + n] Values pushed here
[ec->cfp->sp] Current executing frame's stack pointer
↑
Low addresses (ec->vm_stack)
Der "unbenutzte Speicher" stellt freien Speicher dar, der für neue Frames und Werte verfügbar ist. Wenn diese Lücke schließt (CFP trifft SP), tritt ein Stack-Überlauf auf.
Stack Wachstumsrichtungen
Kontrollframes (CFP)
-
Beginnen bei
ec->vm_stack + ec->vm_stack_size(hohe Adressen) -
Wachsen abwärts in Richtung niedrigerer Adressen, wenn Frames gepusht werden
-
Jeder neue Frame wird bei
cfp - 1(niedrigere Adresse) alloziert -
Die
rb_control_frame_tStruktur selbst bewegt sich abwärts
Wertestack (SP)
-
Beginnt bei
ec->vm_stack(niedrige Adressen) -
Wächst aufwärts in Richtung höherer Adressen, wenn Werte gepusht werden
-
cfp->spjedes Frames zeigt auf die Spitze seines Wertestacks
Stack Überlauf
Wenn rekursive Aufrufe zu viele Frames pushen, wächst CFP abwärts, bis es mit SP, das aufwärts wächst, kollidiert. Die VM erkennt dies mit CHECK_VM_STACK_OVERFLOW0, was const rb_control_frame_struct *bound = (void *)&sp[margin]; berechnet und einen Fehler auslöst, wenn cfp <= &bound[1].
Verständnis der einzelnen Frame Wertestacks
Jeder Frame hat seinen eigenen Teil des gesamten VM Stacks, genannt sein "VM Wertestack" oder einfach "Wertestack". Dieser Speicher wird vorab alloziert, wenn der Frame erstellt wird, wobei die Größe bestimmt wird durch
-
local_size- Speicher für lokale Variablen -
stack_max- maximale Tiefe für temporäre Werte während der Ausführung
Der Wertestack des Frames wächst aufwärts von seiner Basis (wo self/Argumente/Lokale leben) in Richtung cfp->sp (die aktuelle Spitze der temporären Werte).
Visualisierung, wie Frames in den VM Stack passen
Die linke Seite zeigt den gesamten VM Stack mit CFP-Metadaten, getrennt von Frame-Werten. Die rechte Seite zoomt in den Wertebereich eines Frames hinein und enthüllt seine interne Struktur.
Overall VM Stack (ec->vm_stack): Zooming into Frame 2's value stack:
High addr (vm_stack + vm_stack_size) High addr (cfp->sp)
↓ ┌
[CFP 1 metadata] │ [Temporaries]
[CFP 2 metadata] ─────────┐ │ [Env: Flags/Block/CME] ← cfp->ep
[CFP 3 metadata] │ │ [Locals]
──────────────── │ ┌─┤ [Arguments]
(unused space) │ │ │ [self]
──────────────── │ │ └
[Frame 3 values] │ │ Low addr (frame base)
[Frame 2 values] <────────┴───────┘
[Frame 1 values]
↑
Low addr (vm_stack)
Untersuchung des Wertestacks eines einzelnen Frames
Lassen Sie uns nun ein konkretes Ruby-Programm durchgehen, um zu sehen, wie der Wertestack eines einzelnen Frames intern strukturiert ist.
def foo(x, y) z = x.casecmp(y) end foo(:one, :two)
Zuerst, nachdem die Argumente ausgewertet wurden und kurz vor dem send an foo
┌────────────┐
putself │ :two │
putobject :one 0x2 ├────────────┤
putobject :two │ :one │
► send <:foo, argc:2> 0x1 ├────────────┤
leave │ self │
0x0 └────────────┘
Die put* Anweisungen haben 3 Elemente auf den Stack gepusht. Es ist nun Zeit, einen neuen Kontrollframe für foo hinzuzufügen. Die folgende Darstellung zeigt die Form des Stacks nach einer Anweisung in foo.
cfp->sp=0x8 at this point.
0x8 ┌────────────┐◄──Stack space for temporaries
│ :one │ live above the environment.
0x7 ├────────────┤
getlocal x@0 │ < flags > │ foo's rb_control_frame_t
► getlocal y@1 0x6 ├────────────┤◄──has cfp->ep=0x6
send <:casecmp, argc:1> │ <no block> │
dup 0x5 ├────────────┤ The flags, block, and CME triple
setlocal z@2 │ <CME: foo> │ (VM_ENV_DATA_SIZE) form an
leave 0x4 ├────────────┤ environment. They can be used to
│ z (nil) │ figure out what local variables
0x3 ├────────────┤ are below them.
│ :two │
0x2 ├────────────┤ Notice how the arguments, now
│ :one │ locals, never moved. This layout
0x1 ├────────────┤ allows for argument transfer
│ self │ without copying.
0x0 └────────────┘
Da lokale Variablen eine niedrigere Adresse als cfp->ep haben, macht es Sinn, dass getlocal in insns.def val = *(vm_get_ep(GET_EP(), level) - idx); hat. Beim Zugriff auf Variablen im unmittelbaren Geltungsbereich, wo level=0 ist, ist es im Wesentlichen val = cfp->ep[-idx];.
Beachten Sie, dass dieser EP-relative Index eine andere Basis hat als der Index, der nach "@" in Disassemblierungslisten kommt. Der "@" Index ist relativ zum 0. lokalen Wert (in diesem Fall x).
Fragen & Antworten
F: Es scheint, dass der Empfänger immer einen Offset relativ zu EP hat, wie lokale Variablen. Könnten wir nicht EP verwenden, um darauf zuzugreifen, anstatt cfp->self zu verwenden?
A: Nicht alle Aufrufe legen den self im Aufgerufenen auf den Stack. Zwei Beispiele sind Proc#call, wobei der Empfänger das Proc-Objekt ist, aber self innerhalb des Aufgerufenen Proc#receiver ist, und yield, wobei der Empfänger nicht vor den Argumenten auf den Stack gelegt wird.
F: Warum gibt es cfp->ep, wenn es so aussieht, als ob alles unter cfp->sp ist?
A: Im Beispiel zeigt cfp->ep auf den Stack, aber es kann auch auf den GC Heap zeigen. Blöcke können ihre Umgebung erfassen und in den Heap evakuieren.