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:
- It is emitted first by
emitProgramPrologue()in the backend. - It sets up an initial stack pointer (or relies on the bootloader to have done so).
- It calls your function(s).
- 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 foreverStep-by-Step
| Step | Instruction | Purpose |
|---|---|---|
| 1 | MRS X0, MPIDR_EL1 | Read the core ID register |
| 2 | AND X0, X0, #3 | Mask off bits [1:0] to get core number |
| 3 | CBZ X0, .core0 | If core 0, skip the halt sequence |
| 4 | WFE + B .halt | Cores 1–3 sleep in an infinite event-wait loop |
| 5 | MOVZ X0, #8, LSL #16 | Build the value 0x80000 in X0 |
| 6 | MOV SP, X0 | Set the stack pointer to 0x80000 |
| 7 | BL main | Call the user's main() |
| 8 | B .hang | Infinite 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_topis 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
_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:
| Encoding | Instruction | Size |
|---|---|---|
E8 [rel32] | CALL main | 5 bytes |
BF 00 00 00 00 | MOV EDI, 0 | 5 bytes |
B8 3C 00 00 00 | MOV EAX, 60 | 5 bytes |
0F 05 | SYSCALL | 2 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)
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)
During bootstrap emission (
emitBootstrap):e.placeholder('main', 'bl')— emits a zero word at some indexiand records{ idx: i, labelName: 'main', type: 'bl' }.During function emission (
emitFunction):e.label('main')— recordslabels['main'] = j(the index of the first instruction ofmain).During serialisation (
resolveAndSerialize):
For each patch, look uplabels[patch.labelName], compute the signed word offsettarget − patch.idx, and encode the correct instruction:
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
| Backend | Patch type | Instruction | Offset unit |
|---|---|---|---|
| ARM64 | bl | BL rel26 | 4-byte words |
| ARM64 | b | B rel26 | 4-byte words |
| ARM64 | cbz_x | CBZ Xt, rel19 | 4-byte words |
| Xtensa | call0 | CALL0 offset18 | 4-byte words (×4 scale) |
| Xtensa | j | J offset18 | bytes |
| x86_64 | rel32 | CALL/JMP rel32 | bytes (RIP-relative) |
| x86_64 | rip_rel32 | LEA 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 placeholderL32Rand records anl32rPatch.- At serialise time, the pool start byte offset is computed, and each
L32Ris patched with the correctwordOffset = (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):
yogccompiled from.yogsource by a previous-stageyogcmake.yogbuild 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
yogcis itself a Yog system (no Node.js required). - The compiler reads
.yogsource 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.binIncremental 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.