Skip to content

Build BootImageBuilder Internals

opencode-agent[bot] edited this page May 10, 2026 · 3 revisions

Build - BootImageBuilder Internals

The BootImageBuilder (JNode Linker) combines precompiled Java classes, native kernel code, and bootstrap data into a single bootable executable image.

Overview

The BootImageBuilder (org.jnode.build.x86.BootImageBuilder) is JNode's equivalent of a static linker. It takes the nano-kernel (pre-assembled x86 code from kernel.asm, vm.asm), pre-compiled Java classes, and bootstrap data structures, and combines them into a single binary that can be booted by GRUB. The resulting image follows the Multiboot specification and is loaded at LOAD_ADDR (1MB / 0x100000).

The build process operates in two modes (controlled by enableJNasm):

  • ELF mode (default): Copies a pre-assembled native kernel ELF file
  • JNasm mode: Assembles .asm source files at build time using JNode's custom assembler

Key Components

Class / File Role
builder/src/builder/org/jnode/build/x86/BootImageBuilder.java x86-specific builder - combines kernel + classes + bootstrap data
builder/src/builder/org/jnode/build/AbstractBootImageBuilder.java Base class - class loading, compilation, emission orchestration
builder/src/builder/org/jnode/build/BuildObjectResolver.java Object resolution during build - resolves object references to image offsets
core/src/core/org/jnode/vm/classmgr/ObjectLayout.java Object header size constants (HEADER_SLOTS=2, OBJECT_ALIGN=8)
core/src/core/org/jnode/vm/classmgr/VmType.java Class representation with initializeForBootImage(), loadFromBootClassArray()
core/src/core/org/jnode/boot/Main.java Entry point vmMain() called by boot image
core/src/native/x86/kernel.asm Nano-kernel source with Multiboot header
core/src/native/x86/vm.asm VM support routines (context switch, exception handling)

How It Works

Binary Layout

The boot image follows this memory layout (low to high addresses):

+------------------+ <- LOAD_ADDR (0x100000 / 1MB)
| Multiboot Header | Magic: 0x1BADB002, flags, checksums, header fields
+------------------+ (must fit within first 8KB)
| Native Kernel    | <- Copied from ELF (kernel.asm, vm.asm, mm32/64.asm)
| (vm.asm, etc.)   | Contains entry point, context switch, paging setup
+------------------+
| JUMP_MAIN_OFFSET | <- Exact offset: (HEADER_SLOTS+1) * SLOT_SIZE
| Bootstrap Code   | Intro code: JMP instruction to Main.vmMain
+------------------+
| VmSystemObject   | Initial stack: low address, high address, then zeros
| (Stack Object)   | Grows downward; stack ptr = high address
+------------------+ <- bootHeapStart (after page alignment)
| Boot Heap        | Pre-allocated objects, class data, compiled code
+------------------+ <- bootHeapEnd, imageEnd
| Page Alignment   | Aligned to 4KB boundary
+------------------+

Key offsets computed by BootImageBuilder:

  • JUMP_MAIN_OFFSET32 = ObjectLayout.objectAlign((HEADER_SLOTS + 1) * VmX86Architecture32.SLOT_SIZE) = 12 bytes on 32-bit
  • JUMP_MAIN_OFFSET64 = same formula for 64-bit = 16 bytes
  • The VmMethodCode object starts at JUMP_MAIN_OFFSET, with its JMP instruction at the exact offset of a normal object header (2 slots = 8/16 bytes), followed by 4/8 bytes for the JMP = 12/24 bytes total
  • INITIALIZE_METHOD_OFFSET = 8 (offset within the VmMethodCode object where native code begins)

Multiboot Header patching (lines 619-640):

  • The header is embedded at the start of kernel.asm
  • patchHeader() fills in MB_LOAD_END_ADDR and MB_BSS_END_ADDR with the final image size
  • This allows GRUB to know where the image ends and zero out BSS

Class Pre-Resolution

Classes are pre-loaded and pre-resolved during build time using a multi-pass compilation loop:

  1. VmType.initializeForBootImage(clsMgr) (VmType.java:347-417):

    • Creates the 21 primitive/array/system classes (Object, Class, Cloneable, Serializable, boolean, byte, ..., Object[])
    • Links all of them (resolves their vtables)
    • Called once at AbstractBootImageBuilder.java:482
  2. compileClasses() loop (AbstractBootImageBuilder.java:217-268):

    • Iterates until no new classes are loaded and no new methods are compiled
    • For each loaded class:
      • vmClass.link() - Resolves constant pool entries to method/field offsets
      • If !vmClass.isCpRefsResolved() and high optimization level: vmClass.resolveCpRefs() - Fully resolves class hierarchy
      • vmClass.compileBootstrap(compiler, os, optLevel) - Compiles all methods to native x86 code
  3. prepareAfterBootstrap() (VmSystemClassLoader.java:240):

    • Finalizes the boot class array passed to loadFromBootClassArray()
    • Returns a VmType[] of all loaded classes
  4. emitStaticInitializerCalls() (BootImageBuilder.java:526-565):

    • Emits a VmMethodCode object containing a call to VmType.loadFromBootClassArray(bootClasses)
    • This sets the static VmType fields (BooleanClass, ObjectClass, etc.) at boot time
// From BootImageBuilder.java:536-548
VmMethod lfbcaMethod = vmClassClass.getMethod("loadFromBootClassArray", "([Lorg/jnode/vm/classmgr/VmType;)V");
os.writeMOV_Const(aax, bootClasses);
os.writePUSH(aax);
os.writeMOV_Const(aax, lfbcaMethod);
os.writeMOV(abx.getSize(), abx, aax, nativeCodeField.getOffset());
os.writeCALL(abx);

Native Kernel Entry-Pointing

The kernel entry sequence transfers control from GRUB to Main.vmMain():

GRUB (multiboot)
  -> kernel.asm (Multiboot entry point)
  -> _start (arch-specific init: paging, GDT, IDT)
  -> _$$introCode (in boot image, at JUMP_MAIN_OFFSET)
     JMP to bootstrap code (sets up VM and thread state)
  -> _$$clInitCaller (calls loadFromBootClassArray)
  -> _$$Initial_call_to_clInitCaller (invokes clInitCaller method)
  -> initCallMain (calls Main.vmMain)
  -> Main.vmMain() (Java entry point)

Setup sequence in initImageHeader() (BootImageBuilder.java:263-313):

  1. initVm() (lines 480-497): Sets up the STATICS register (EDI in 32-bit, RDI in 64-bit) to point to the VmSharedStatics table, and initializes VmUtils.VM_INSTANCE to the VmImpl object
  2. initMain() (lines 507-524): Sets Main.pluginRegistry to the PluginRegistry object built at compile time
  3. initVmThread() (lines 416-441): Sets up the initial VmThread object with its stack pointer from _$$initialStackPtr
  4. initCallMain() (lines 395-407): Calls Main.vmMain() by:
    • Loading the VmMethod for Main.vmMain into a register
    • Accessing the nativeCode field of the VmMethod to get the compiled code address
    • CALLing that address

The STATICS register (EDI/RDI) is the root of all static field access. All static fields are stored in the VmSharedStatics table at fixed offsets, and the JIT compiler generates mov [EDI + offset], value for static stores.

Object Emission

Objects are emitted in a carefully orchestrated order (AbstractBootImageBuilder.java:546-610):

  1. First pass: Emit all objects except blocked ones (VM, processor, class manager, statics tables)
  2. VM emission: Emit VmImpl (twice, intentionally — line 555-556)
  3. Compiled methods list: Emit CompiledMethods (twice — lines 561-565)
  4. Boot class array: Emit the VmType[] (twice — lines 569-573)
  5. Class manager: Emit VmSystemClassLoader (twice — lines 577-582)
  6. Statics tables: Emit VmSharedStatics and VmIsolatedStatics (twice — lines 585-590)
  7. Remaining objects: All others (line 594)
  8. Static initializer calls: Emit the _$$clInitCaller method object
  9. Alignment: Page-align the end of the image, mark _$$image_end and _$$bootHeapEnd

Each object in the image consists of:

  • Object header (2 slots): Flags word (offset -2) and TIB pointer (offset -1)
  • Instance fields: Follow the header, aligned to 8 bytes
  • Compiled code: Stored in separate VmMethodCode objects, linked via the nativeCode field

Jump Table Initialization

The x86 jump table (X86JumpTable.TABLE_LENGTH entries) is initialized by both initializeStatics() and copyKernel()/compileKernel():

  • On 32-bit: Entry i is at statics index i (4-byte address per entry)
  • On 64-bit: Entry i is at statics index i * 2 (each entry is 8 bytes)
  • Labels like VmProcessor_reschedule, VmThread_runThread are linked from native code to Java method VmMethod objects

Gotchas & Non-Obvious Behavior

  • JUMP_MAIN_OFFSET must equal object header size: The bootstrap jump MUST be placed at exactly the object header offset. This is verified at lines 268-274 — if the offset doesn't match, the build fails with a message to "set to Object headersize".

  • Emit phase runs twice for critical objects: Each critical object (VM, compiled methods list, boot classes, class manager, statics) is emitted twice to handle cross-references. The second pass resolves any references created during the first pass.

  • Statics indices are architecture-dependent: Jump table entries use index i on 32-bit, i * 2 on 64-bit (lines 719-730). This is because reference size differs (4 vs 8 bytes).

  • Multiboot header must be within first 8KB: GRUB searches only the first 8KB of the image for the Multiboot magic. The header is embedded at the start of kernel.asm.

  • enableJNasm controls kernel source: If true, kernel is assembled at build time from .asm files using JNasm. If false, a pre-built ELF is copied (line 488-491 in AbstractBootImageBuilder).

  • Blocked objects prevent premature emission: The blockedObjects set delays critical objects until all references are ready. VM, shared statics, class manager, and isolated statics tables are all blocked initially.

  • INITIAL_OBJREFS_CAPACITY pre-allocation: The assembler pre-allocates space for 750,000 object references. If exceeded, a warning is logged to increase this constant for faster builds (line 664).

Related Pages

Clone this wiki locally