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

FilePurpose
org/jnode/vm/x86/compiler/l2/X86StackFrame.javaCore stack frame generation (header/trailer, EBP offsets, exception handlers)
org/jnode/vm/x86/compiler/X86CompilerHelper.javaDefines BP (frame pointer), SP (stack pointer), STATICS register, and SLOTSIZE
org/jnode/vm/x86/compiler/l2/GenericX86CodeGenerator.javaL2 code generator using EBP-relative addressing for locals/stack
org/jnode/vm/x86/compiler/l2/X86CodeGenerator.javaL2 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

RegisterRole
EBP/RBPFrame pointer — base for accessing locals and args
ESP/RSPStack 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-bitAAX = EAX; ABX = EBX; SP = ESP; BP = EBP; STATICS = EDI; ADDRSIZE = 4; SLOTSIZE = 4;
// 64-bitAAX = 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 EBPos.writePUSH(cm.getCompiledCodeId()); // method identifieros.writeMOV(size, abp, asp);
// Create and clear all local variablesfinalintnoLocalVars = maxLocals - argSlotCount;
if (noLocalVars > 0) {
os.writeXOR(aax, aax); // aax = 0for (inti = 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:

publicfinalshortgetEbpOffset(TypeSizeInfotypeSizeInfo, intindex) {
finalintnoArgs = method.getArgSlotCount();
finalintstackSlot = index;
if (stackSlot < noArgs) {
// Argument: positive offset (above saved EBP)returntoShort(((noArgs - stackSlot + 1) * slotSize) + EbpFrameRefOffset + SAVED_REGISTERSPACE);
} else {
// Local variable: negative offset (below saved EBP)returntoShort((stackSlot - noArgs + 1) * -slotSize);
}
}

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

Java IndexTypeEBP Offset
0arg+16
1arg+12
2local-4
3local-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:

finalintofs = 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 (inti = 0; i < count; i++) {
VmInterpretedExceptionHandlereh = bc.getExceptionHandler(i);
LabelhandlerLabel = helper.genLabel("$$ex_handler" + i);
// ... clear stack, push exception, jump to handlerceh[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:

privatevoidemitSynchronizationCode(TypeSizeInfotypeSizeInfo, VmMethodmonitorMethod) {
if (method.isSynchronized()) {
if (method.isStatic()) {
// Get declaring class via statics tablefinalinttypeOfs = helper.getSharedStaticsOffset(method.getDeclaringClass());
os.writePUSH(helper.STATICS, typeOfs);
} else {
// Push 'this' referenceos.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