Skip to content

Bootstrap & Self-Hosting ​

Every yogc-produced binary starts with a short stub of machine code that the compiler prepends before any user function. This stub — called the bootstrap — performs the minimum hardware initialisation needed to reach the user's entry point with a valid stack. It is entirely generated by the compiler; you never write it yourself.

This page documents the bootstrap sequence for each supported target, explains how the forward-reference patch mechanism works inside the emitter, and outlines the long-term self-hosting goal.


What Is the Bootstrap? ​

When you write a Yog program, you define a named entry-point function (main, setup, loop). The hardware, however, starts executing at a fixed physical address. The bootstrap is the glue between those two things:

  1. It is emitted first by emitProgramPrologue() in the backend.
  2. It sets up an initial stack pointer (or relies on the bootloader to have done so).
  3. It calls your function(s).
  4. After the final user function returns, it halts the CPU permanently.

The user's compiled functions follow immediately after the bootstrap in the same flat binary.


ARM64 Bootstrap (Raspberry Pi 3 / QEMU raspi3b) ​

Output file: kernel8.img
Load address: 0x80000 (the GPU bootloader copies the image here and jumps to offset 0)
Entry point: function main(): void

The Problem: Multiple CPU Cores ​

The BCM2837 SoC starts all four Cortex-A53 cores simultaneously at boot. Only core 0 should run the kernel; cores 1–3 must be halted immediately or they will execute random memory as instructions.

Assembly Sequence ​

mrs  x0, mpidr_el1       // read Multiprocessor Affinity Register
and  x0, x0, #3          // isolate bits [1:0] — the core ID (0..3)
cbz  x0, .core0          // core 0 → jump to setup; others fall through

.halt:
  wfe                    // Wait For Event — suspend the core
  b    .halt             // loop back if spuriously woken

.core0:
  movz x0, #0x0008, lsl #16   // x0 = 0x00080000 — top of stack
  mov  sp, x0                  // SP = 0x80000 (stack grows downward)
  bl   main                    // call user's main()

.hang:
  b    .hang             // main() returned — halt core 0 forever

Step-by-Step ​

StepInstructionPurpose
1MRS X0, MPIDR_EL1Read the core ID register
2AND X0, X0, #3Mask off bits [1:0] to get core number
3CBZ X0, .core0If core 0, skip the halt sequence
4WFE + B .haltCores 1–3 sleep in an infinite event-wait loop
5MOVZ X0, #8, LSL #16Build the value 0x80000 in X0
6MOV SP, X0Set the stack pointer to 0x80000
7BL mainCall the user's main()
8B .hangInfinite loop after main() returns

Why 0x80000 as the Stack Top? ​

The image is loaded at 0x80000. By pointing the stack pointer at exactly that address and letting it grow downward (ARM64 full-descending stack convention), the stack uses the memory below the load address. The region 0x0000–0x7FFFF (512 KB) is available for stack use in Phase 1.

Source Reference ​

yogc/src/backends/arm64.js — emitBootstrap(e) (lines 144–160)


ESP32 Bootstrap (Xtensa LX6) ​

Output file: app.bin
Load address: 0x40080000 (IRAM — loaded by OpenOCD or Espressif QEMU)
Entry points: function setup(): void + function loop(): void

The ROM Bootloader Does the Hard Work ​

Unlike the RPi3, the ESP32 ROM bootloader runs before your code and performs significant initialisation:

  • Starts PRO CPU (core 0) only; APP CPU (core 1) is held in reset.
  • Loads the binary from flash into IRAM at 0x40080000.
  • Initialises UART0 at 115200 baud.
  • Jumps to offset 0 of the loaded binary.

This means the bootstrap is dramatically simpler — no core ID check, no stack setup.

Arduino-Style Entry Contract ​

Yog uses the Arduino / ESP-IDF pattern: setup() runs once at boot, then loop() is called repeatedly forever. This avoids requiring an explicit while (true) in user code and matches the mental model of embedded developers.

Bootstrap Layout (4 instructions, 12 bytes) ​

byte  0:  CALL0 setup      // call setup() once
byte  3:  CALL0 loop       // [loop_top] call loop()
byte  6:  J loop_top       // jump back to byte 3 — repeat forever
byte  9:  J -4             // [hang] unreachable self-loop (safety net)

All instructions are 24-bit (3 bytes) little-endian. Functions compiled by yogc are 4-byte aligned (the Xtensa backend inserts NOP padding before each label), so the CALL0 targets are always valid.

J Offset Calculation ​

J on Xtensa encodes a byte offset relative to PC + 4:

  • J loop_top is at byte 6. Target is byte 3.
    offset = 3 − (6 + 4) = −7 → encoded as (−7) & 0x3FFFF.

Source Reference ​

yogc/src/backends/xtensa.js — emitBootstrap(e) (lines 243–258)


x86_64 Bootstrap (Linux ELF64) ​

Output file: app.elf
Format: Standard ELF64 executable (64-byte header + 56-byte PT_LOAD phdr + code)
Load address: 0x400078 (code virtual address after ELF + phdr headers at 0x400000)
Entry point: function main(): void

What the Linux Kernel Does ​

The kernel's ELF loader maps the PT_LOAD segment, sets up the process stack (with argc, argv, environment on it), and jumps to e_entry. There are no secondary cores to park and no UART to initialise — stdout (fd 1) is already open.

_start Stub ​

asm
_start:
  call  main       ; invoke user's main(); PUSH RBP inside main re-aligns RSP
  mov   edi, 0     ; exit code = 0
  mov   eax, 60    ; SYS_exit
  syscall          ; _exit(0)

The _start stub is 17 bytes total:

EncodingInstructionSize
E8 [rel32]CALL main5 bytes
BF 00 00 00 00MOV EDI, 05 bytes
B8 3C 00 00 00MOV EAX, 605 bytes
0F 05SYSCALL2 bytes

Stack Alignment ​

The System V AMD64 ABI requires RSP to be 16-byte aligned immediately before any CALL. The kernel delivers RSP already aligned. CALL main pushes an 8-byte return address (misaligning by 8); the PUSH RBP at the start of every compiled function pushes another 8 bytes, restoring 16-byte alignment for any nested calls inside main.

Source Reference ​

yogc/src/backends/x86_64.js — emitBootstrap(e) (lines 227–233)


Forward-Reference Patches ​

When the bootstrap is emitted, the user's functions have not been compiled yet — their addresses are unknown. The emitter uses a placeholder-and-patch mechanism to resolve these forward references.

The Emitter Class (yogc/src/emitter.js) ​

js
class Emitter {
  constructor(wordSize = 4) {
    this.buf     = [];   // instruction words (one entry = one instruction slot)
    this.labels  = {};   // name → instruction index
    this.patches = [];   // { idx, labelName, type, ...extra }
  }

  // Append one instruction word, return its index
  emit(word) { ... }

  // Emit a zero placeholder and record a patch for later resolution
  placeholder(labelName, type, extra = {}) {
    const idx = this.emit(0);
    this.patches.push({ idx, labelName, type, ...extra });
    return idx;
  }

  // Define a label at the current position
  label(name) {
    this.labels[name] = this.buf.length;
  }
}

How Patches Work (ARM64 Example) ​

  1. During bootstrap emission (emitBootstrap):
    e.placeholder('main', 'bl') — emits a zero word at some index i and records { idx: i, labelName: 'main', type: 'bl' }.

  2. During function emission (emitFunction):
    e.label('main') — records labels['main'] = j (the index of the first instruction of main).

  3. During serialisation (resolveAndSerialize):
    For each patch, look up labels[patch.labelName], compute the signed word offset target − patch.idx, and encode the correct instruction:

js
for (const p of e.patches) {
  const target = e.labels[p.labelName];   // instruction index of the target
  const offset = target - p.idx;          // signed word offset

  switch (p.type) {
    case 'bl':    e.buf[p.idx] = BL(offset);            break;
    case 'b':     e.buf[p.idx] = B(offset);             break;
    case 'cbz_x': e.buf[p.idx] = CBZ_X(p.rt, offset);  break;
  }
}

Patch Types by Backend ​

BackendPatch typeInstructionOffset unit
ARM64blBL rel264-byte words
ARM64bB rel264-byte words
ARM64cbz_xCBZ Xt, rel194-byte words
Xtensacall0CALL0 offset184-byte words (×4 scale)
XtensajJ offset18bytes
x86_64rel32CALL/JMP rel32bytes (RIP-relative)
x86_64rip_rel32LEA RSI, [RIP+disp32]bytes (RIP-relative)

Xtensa Literal Pool (L32R) ​

The Xtensa backend also has a second patch type for 32-bit constants. Since Xtensa cannot encode arbitrary 32-bit immediates inline, constants are stored in a literal pool appended at the end of the binary. L32R instructions address this pool with a PC-relative 16-bit word offset.

The XtensaEmitter subclass adds:

  • addLiteral(value) — adds a value to the pool, returns its index (deduplicates).
  • emitL32R(reg, litIdx) — emits a placeholder L32R and records an l32rPatch.
  • At serialise time, the pool start byte offset is computed, and each L32R is patched with the correct wordOffset = (litByteAddr − l32rBase) >> 2.

Self-Hosting (Phase 10) ​

The long-term goal of the Yog project is for yogc to compile itself. This is called self-hosting: the compiler is written in .yog source and compiled by a previous-stage yogc.

From VISION.md (Phase 10):

  • yogc compiled from .yog source by a previous-stage yogc
  • make.yog build script orchestrates kernel + userland
  • The OS rebuilds itself from source on Yog hardware

The Bootstrap and Self-Hosting ​

Self-hosting does not change the bootstrap mechanism. Each yogc-produced binary — including the compiler binary itself — will still begin with the same three-instruction boot stub. What changes is:

  • The host running yogc is itself a Yog system (no Node.js required).
  • The compiler reads .yog source files from the RAM FS / FAT32 volume.
  • The build pipeline becomes:
# Phase 1 (today)
kernel.yog  →  [yogc on Node.js]  →  kernel8.img  →  QEMU

# Phase 10 (self-hosting)
yogc.yog  →  [yogc on Yog/ARM64]  →  yogc.bin

Incremental Path ​

Each phase's tools are designed to be usable from within the OS by the next phase. The bootstrap is stable throughout — the instruction sequence in emitBootstrap is not expected to change after Phase 1 unless hardware targets change.

The current status is Phase 1 (proof-of-concept). Self-hosting is tracked as Phase 10 in VISION.md.

Yog — union of JS syntax and silicon.