Skip to content

Stack Frame Layout

opencode-agent[bot] edited this page May 9, 2026 · 1 revision

Stack Frame Layout

Documents the stack frame structure used by JNode's x86 JIT compilers (L1 and L2).

Overview

JNode uses a frame-pointer-based (EBP/RBP) stack frame layout for both L1 and L2 JIT compilers. The layout supports method arguments, local variables, operand stack slots, and exception handling. The same underlying structure is used by both compilers, implemented in X86StackFrame.java.

Key Components

File Purpose
org/jnode/vm/x86/compiler/l2/X86StackFrame.java Core stack frame generation (header/trailer, EBP offsets, exception handlers)
org/jnode/vm/x86/compiler/X86CompilerHelper.java Defines BP (frame pointer), SP (stack pointer), STATICS register, and SLOTSIZE
org/jnode/vm/x86/compiler/l2/GenericX86CodeGenerator.java L2 code generator using EBP-relative addressing for locals/stack
org/jnode/vm/x86/compiler/l2/X86CodeGenerator.java L2 entry point, constructs GenericX86CodeGenerator with X86StackFrame

Stack Frame Layout

High addresses
+------------------+
| Arg N            |  <-- positive EBP offset (callee's args)
+------------------+
| ...              |
+------------------+
| Arg 1            |
+------------------+  <-- EBP + EbpFrameRefOffset (saved EBP)
| saved EBP         |  EbpFrameRefOffset = slotSize (4 or 8)
+------------------+
| method ID        |  (pushed by prologue)
+------------------+
| local 0          |  <-- EBP - slotSize (first local after args)
+------------------+
| local 1          |
+------------------+
| ...              |
+------------------+
| local M          |  <-- EBP - (M * slotSize)
+------------------+  <-- ESP (low water mark during execution)
| operand stack    |  grows downward via PUSH/POP
+------------------+
Low addresses

Register Roles

Register Role
EBP/RBP Frame pointer — base for accessing locals and args
ESP/RSP Stack pointer — top of operand stack
STATICS (EDI/RDI) Pointer to statics table
AAX (EAX/RAX) Scratch / return value

X86CompilerHelper initializes these registers based on 32/64-bit mode:

// 32-bit
AAX = EAX; ABX = EBX; SP = ESP; BP = EBP; STATICS = EDI; ADDRSIZE = 4; SLOTSIZE = 4;

// 64-bit
AAX = RAX; ABX = RBX; SP = RSP; BP = RBP; STATICS = RDI; ADDRSIZE = 8; SLOTSIZE = 8;

Prologue (emitTrailer)

The method prologue (written by X86StackFrame.emitTrailer) performs:

  1. Stack overflow test — compares ESP/RSP against VmProcessor.stackEnd via FS prefix (32-bit) or direct comparison (64-bit)
  2. Optional statics load — for methods with @LoadStatics pragma
  3. Stack alignment test — checks 16-byte alignment (conditionally enabled in 64-bit)
  4. Class initialization check — emits test-and-jump for uninitialized classes
  5. Save registerssaveRegisters() currently empty (reserved space)
  6. Push saved EBPpush ebp
  7. Push method IDpush cm.getCompiledCodeId()
  8. Copy SP to BPmov ebp, esp
  9. Allocate locals — pushes zero-filled slots for non-argument locals
saveRegisters();
os.writePUSH(abp);          // save caller's EBP
os.writePUSH(cm.getCompiledCodeId()); // method identifier
os.writeMOV(size, abp, asp);

// Create and clear all local variables
final int noLocalVars = maxLocals - argSlotCount;
if (noLocalVars > 0) {
    os.writeXOR(aax, aax);   // aax = 0
    for (int i = 0; i < noLocalVars; i++) {
        os.writePUSH(aax);   // push zero
    }
}

Epilogue (emitTrailer footer)

The method epilogue (written in emitTrailer at footerLabel) performs:

  1. Monitor exit — for synchronized methods
  2. Restore SP from BP: lea esp, [ebp + EbpFrameRefOffset]
  3. Pop saved EBP: pop ebp
  4. Restore registers
  5. Return: ret [argSlotCount * slotSize]
os.setObjectRef(footerLabel);
emitSynchronizationCode(typeSizeInfo, entryPoints.getMonitorExitMethod());
os.writeLEA(asp, abp, EbpFrameRefOffset);
os.writePOP(abp);
restoreRegisters();
if (argSlotCount > 0) {
    os.writeRET(argSlotCount * slotSize);
} else {
    os.writeRET();
}

EBP Offset Calculation

X86StackFrame.getEbpOffset maps Java local/arg indices to stack offsets:

public final short getEbpOffset(TypeSizeInfo typeSizeInfo, int index) {
    final int noArgs = method.getArgSlotCount();
    final int stackSlot = index;
    if (stackSlot < noArgs) {
        // Argument: positive offset (above saved EBP)
        return toShort(((noArgs - stackSlot + 1) * slotSize) + EbpFrameRefOffset + SAVED_REGISTERSPACE);
    } else {
        // Local variable: negative offset (below saved EBP)
        return toShort((stackSlot - noArgs + 1) * -slotSize);
    }
}

Example (32-bit, slotSize=4, noArgs=2):

Java Index Type EBP Offset
0 arg +16
1 arg +12
2 local -4
3 local -8

Operand Stack Representation

The operand stack is not pre-allocated — it grows downward from locals via PUSH/POP instructions. L2 compiler tracks operand stack slots using the IR's addressing modes:

  • TOPS — Top-of-Stack (pop to register or push from register)
  • STACK — EBP-relative stack slot for spilled values

L1 compiler uses a simpler stack-based model where bytecode aload/astore map directly to push/pop instructions.

When generating exception handlers, the stack is cleared by adjusting ESP to point just above the locals:

final int ofs = Math.max(0, noLocalVars) * slotSize;
os.writeLEA(asp, abp, -ofs);
os.writePUSH(aax); // push exception object

Exception Handler Table

Each method's bytecode contains an exception table. X86StackFrame.emitTrailer converts this to native code:

for (int i = 0; i < count; i++) {
    VmInterpretedExceptionHandler eh = bc.getExceptionHandler(i);
    Label handlerLabel = helper.genLabel("$$ex_handler" + i);
    // ... clear stack, push exception, jump to handler
    ceh[i].setStartPc(os.getObjectRef(getInstrLabel(eh.getStartPC())));
    ceh[i].setEndPc(os.getObjectRef(getInstrLabel(eh.getEndPC())));
    ceh[i].setHandler(handlerRef);
}

Synchronized Methods

Synchronized methods emit monitor-enter/exit around the method body:

private void emitSynchronizationCode(TypeSizeInfo typeSizeInfo, VmMethod monitorMethod) {
    if (method.isSynchronized()) {
        if (method.isStatic()) {
            // Get declaring class via statics table
            final int typeOfs = helper.getSharedStaticsOffset(method.getDeclaringClass());
            os.writePUSH(helper.STATICS, typeOfs);
        } else {
            // Push 'this' reference
            os.writePUSH(helper.BP, getEbpOffset(typeSizeInfo, 0));
        }
        helper.invokeJavaMethod(monitorMethod);
    }
}

Gotchas

  • SAVED_REGISTERSPACE = 0 * 4 — no callee-saved registers are currently saved, but the space calculation exists for future use
  • Wide locals (long/double) occupy two consecutive stack slots; getWideEbpOffset returns the first slot offset and the second slot is at offset - slotSize
  • In 64-bit mode, the argument slot count is multiplied by 2 in jump table indexing to account for 8-byte slots
  • Stack alignment test is conditionally disabled (false && os.isCode64()) — may need enabling for strict ABI compliance
  • Default exception handler jumps to VM_ATHROW_NOTRACE without a ret — the return address on stack becomes the exception's context

Related Pages

Clone this wiki locally