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)

Wertestack (SP)

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

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.