Login
Glint tutorial
Login

One entry per merged slice, in the order merged. Each entry names the slice, the check-in that merged it (linked to its diff), its verdict and what to run to see it. The check-in comment carries the full claim, passage, demonstration and size.

1.1 Project skeleton (33781682, annotate and merge)

Glint is a Crystal shard whose build driver answers --version. Run shards build && bin/glint-build --version, then crystal spec.

P.1 The shell grammar as data (ea1dde23, annotate and merge)

A lexer and one Cleave::GlyphParser rule reduce a call with literal arguments, such as i2c.read 0x48, 0, to one statement glyph. Run crystal spec spec/shell/grammar_spec.cr.

T.1 Inventory (e145887d, resubmit, then annotate and merge)

qemu/inventory.yml lists the sources and generated files of reduced qemu-system-aarch64 -M virt and qemu-system-x86_64 -M q35 under the build profile in qemu/PIN.md. Run QEMU_SRC=/path/to/qemu-11.1.2 crystal spec spec/qemu_inventory_spec.cr.

1.2 First EFI images (16482866, annotate and merge)

glint-build all links BOOTAA64.EFI and BOOTX64.EFI from a Crystal efi_main that returns EFI_SUCCESS. Run shards build && bin/glint-build all, then crystal spec spec/efi_image_spec.cr.

P.2 Crystal is the oracle for syntax (49071ac7, merge)

Each shell example is parsed by Crystal::Parser and must be the same call. Run crystal spec spec/shell/crystal_oracle_spec.cr.

1.3 A console through a fake firmware (2bf938c8, annotate and merge)

efi_main prints "Hello from Glint" through ConOut. The specs run the same source against a fake system table on Linux. Run crystal spec spec/console_spec.cr, and bin/glint-build all for the images.

1.4 Calling C and being called back (77437d6b and f93fcb1c, annotate and merge)

c/hello.c calls back into Crystal's glint_print. The specs check struct layout, a variadic call and the Microsoft x64 convention. The code arrived by mistake in 77437d6b (see its amended comment). f93fcb1c is its review. Run crystal spec spec/console_spec.cr spec/c_interop_spec.cr.

1.5 Finding services by GUID (d25c6f53, merge)

Protocol.locate(EFI::TextOutput, table) finds the console by its GUID through LocateProtocol, and returns a result the caller checks. Run crystal spec spec/console_spec.cr.

1.6 Reading files (23059965, annotate and merge)

EfiFile opens, reads and closes files through the simple file system protocol, found with Protocol.locate. The fake serves spec/fixtures/esp/. Run crystal spec spec/efi_file_spec.cr.

P.3 Reading PCRE2's bytecode (fd888a6d, annotate and merge)

At build time the :call rule is compiled with PCRE2 10.44, built from pinned source, and its bytecode becomes a Crystal constant. Set GLINT_PCRE2_DIR to keep the PCRE2 cache and build out of the tree, then run crystal spec spec/shell/bytecode_spec.cr.

1.7 Reading the keyboard (9fba4922, annotate and merge)

Console.read_line reads keys from ConIn, found by GUID, echoing through ConOut and handling Backspace and Enter. The fake is fed scripted keys. Run crystal spec spec/keyboard_spec.cr.

P.4 A small matcher (7888c9e5, annotate and merge)

A backtracking interpreter with a fixed stack runs the :call rule's bytecode without allocation, and agrees with PCRE2 on 6004 cases. Run crystal spec spec/shell/matcher_spec.cr.

T.2 hxtool and feature_to_c (8255e69b, resubmit, then merge)

bin/hxtool and bin/feature-to-c generate QEMU's option tables and GDB register tables. Run QEMU_SRC=/path/to/qemu-11.1.2 crystal spec spec/qemu.

1.8a One lexer, and the rule's bytecode for the images (635f9618, annotate and merge)

One stdlib-free lexer serves both the host grammar and the images, and the :call bytecode is generated at build time as a StaticArray. Run crystal spec spec/shell/lexer_spec.cr spec/shell/bytecode_spec.cr.

1.8b The shell (ef821c54, merge)

efi_main runs a shell with help, echo, ls and cat. Phase 1's demo is the scripted session in spec/shell_spec.cr: run crystal spec spec/shell_spec.cr, and bin/glint-build all for the images.

T.3a decodetree, reading and checking (3397be4f, resubmit, then merge)

bin/decodetree --output-null reads .decode files and rejects invalid ones with a kind and a line. Run QEMU_SRC=/path/to/qemu-11.1.2 crystal spec spec/qemu/decodetree_spec.cr.

T.5 tracetool, nop backend (c370b9da, annotate and merge)

bin/tracetool turns QEMU's trace-events into headers whose trace calls compile to nothing. Run QEMU_SRC=/path/to/qemu-11.1.2 crystal spec spec/qemu/tracetool_spec.cr.

T.3b decodetree, generating C (ab776aea, resubmit, then merge)

bin/decodetree --static-decode=disas_a64 a64.decode writes the C decoder that QEMU's Arm translator includes. Run QEMU_SRC=/path/to/qemu-11.1.2 crystal spec spec/qemu/decodetree_gen_spec.cr.

T.4 minikconf (9fb77510, merge)

bin/minikconf selects each reduced binary's devices from QEMU's Kconfig tree, using cleave's parser. Run QEMU_SRC=/path/to/qemu-11.1.2 crystal spec spec/qemu/minikconf_spec.cr.

T.6a1 QAPI schema syntax (c0865bd4, annotate and merge)

The QAPI parser reads QEMU's schema syntax with lines and doc blocks. Run QEMU_SRC=/path/to/qemu-11.1.2 crystal spec spec/qemu/qapi_parser_spec.cr.

T.6a2 QAPI schema checks (3b9106e3, merge)

bin/qapi-gen --check builds a checked model of a schema. All 208 of QEMU's tests/qapi-schema cases behave as their .err says. Run QEMU_SRC=/path/to/qemu-11.1.2 crystal spec spec/qemu/qapi_spec.cr.

T.6b QAPI types and visitors (0d9132a7, resubmit, then merge)

bin/qapi-gen -o DIR qapi/qapi-schema.json writes QEMU's QAPI types and visitors. Run QEMU_SRC=/path/to/qemu-11.1.2 crystal spec spec/qemu/qapi_types_spec.cr.

T.6c QAPI commands, events and introspection (07bd33c4, annotate and merge)

bin/qapi-gen now writes every QAPI output the reduced binaries include, 424 files each. Run QEMU_SRC=/path/to/qemu-11.1.2 crystal spec spec/qemu/qapi_commands_spec.cr.

T.7a libfdt (25b5f976, annotate and merge)

tools/qemu/libfdt_build.cr builds libfdt at QEMU's pinned dtc revision, cross-checked with cleave's FDT reader. Run crystal spec spec/qemu/libfdt_spec.cr.

T.7b GLib core (2233e0e7, merge)

tools/qemu/glib_build.cr builds GLib 2.88.4's glib and gmodule from Crystal build data, and ten of GLib's own test programs pass. Run crystal spec spec/qemu/glib_spec.cr.

T.6d keymap-gen (571b40d7, merge)

bin/keymap-gen writes QEMU's 16 key-code tables from keymaps.csv. Run QEMU_SRC=/path/to/qemu-11.1.2 crystal spec spec/qemu/keymap_spec.cr.

T.6e block-coroutine-wrapper (9cc70e56, resubmit, then merge)

bin/block-coroutine-wrapper writes block/block-gen.c's 56 wrappers from QEMU's marked declarations. Run QEMU_SRC=/path/to/qemu-11.1.2 crystal spec spec/qemu/block_coroutine_spec.cr.

T.6f module_block and two shell scripts (e9af9098, merge)

bin/small-generators writes module_block.h, config-poison.h and qemu-version.h. Run QEMU_SRC=/path/to/qemu-11.1.2 crystal spec spec/qemu/small_generators_spec.cr.

T.8a Reduced qemu-system-aarch64: every object compiles (a9a7c087, resubmit, then merge)

bin/glint-qemu aarch64 --compile runs every Glint generator and compiles QEMU's 1,073 objects for this x86_64 host, TCG only. Run QEMU_SRC=/path/to/qemu-11.1.2 crystal spec spec/qemu/qemu_compile_spec.cr (about eight minutes).

T.8b Reduced qemu-system-aarch64: link, unit tests, smoke run (ea008ccf, resubmit, then merge)

bin/glint-qemu aarch64 links the reduced binary from BQ2 and Glint-built libraries only. 74 of QEMU's unit-test programs pass, and it answers QMP. Run QEMU_SRC=/path/to/qemu-11.1.2 crystal spec spec/qemu/qemu_link_spec.cr.

T.9 Reduced qemu-system-x86_64 (fece1ef1, merge)

bin/glint-qemu x86_64 builds the reduced q35 emulator with TCG and KVM, from the same parameterised build as aarch64; EPYC exposes SVM. Run QEMU_SRC=/path/to/qemu-11.1.2 crystal spec spec/qemu/qemu_x86_64_spec.cr.

T.10a U-Boot for aarch64 virt (c5da019c, annotate and merge)

bin/glint-uboot aarch64 builds mainline U-Boot 2026.07 from cleave's Kconfig and Makefile-derived data, and it boots under Glint's own QEMU to bootefi hello. Run UBOOT_SRC=/path/to/u-boot-2026.07 QEMU_SRC=/path/to/qemu-11.1.2 crystal spec spec/uboot/uboot_aarch64_spec.cr.

T.10b U-Boot for x86_64 q35 (ddb1cd2e, annotate and merge)

bin/glint-uboot x86_64 builds U-Boot 2026.07's SPL, U-Boot proper and its DTB, and assembles u-boot.rom with cleave's binman. It boots under Glint's own QEMU to bootefi hello. Run UBOOT_SRC=/path/to/u-boot-2026.07 QEMU_SRC=/path/to/qemu-11.1.2 crystal spec spec/uboot/uboot_x86_64_spec.cr.

T.10c Option ROMs from source (765b5782, annotate and merge)

bin/glint-qemu x86_64 also builds kvmvapic.bin from QEMU's source, so the boot tests load no prebuilt firmware. Run UBOOT_SRC=... QEMU_SRC=... crystal spec spec/uboot.

T.11 glint-run (ee3aec1c, annotate and merge)

bin/glint-run aarch64 --expect "Hello from C via Glint" (or x86_64) boots Glint's image under Glint's own QEMU and U-Boot, through U-Boot's standard boot, and passes when the line appears. This is Track T's demo. Run UBOOT_SRC=... QEMU_SRC=... crystal spec spec/run.

Z.1 EFI images as ELF with a PE header (8b4357fa, merge)

bin/glint-build all now builds Glint as LP64 ELF and adds its PE header in boot/crt0.S; UEFI calls go through one Microsoft-ABI boundary on x86_64. Run crystal spec spec/efi_image_spec.cr spec/elf_dialect_spec.cr, and bin/glint-run x86_64 --expect "Hello from C via Glint".

Z.2 Checking the Phase 6 drivers first (1e8246d1, annotate and merge)

third_party/zephyr/PIN.md pins Zephyr v4.4.2, which tools/zephyr/source.cr finds through ZEPHYR_SRC. The PL011, NS16550, virtio and NVMe drivers compile for both LP64 targets, and every 64-to-32 narrowing is recorded. From the repository root, run ZEPHYR_SRC=/path/to/zephyr crystal spec spec/zephyr/width_spec.cr.

Z.3 Reading Zephyr's bindings, and the build devicetrees (26a1c624, merge)

tools/zephyr/bindings.cr reads Zephyr's binding YAML. compat/zephyr/dts/ holds the build devicetrees: QEMU virt's recorded DTB, with its UART relabelled arm,sbsa-uart, and the PC's COM1. Run ZEPHYR_SRC=… crystal spec spec/zephyr/bindings_spec.cr, and crystal run tools/zephyr/build_dt.cr -- qemu-virt pc-com1 to write both trees.

Z.4 Node and property macros (4183503f, annotate and merge)

tools/zephyr/devicetree_header.cr writes Zephyr's per-node and per-property devicetree macros from cleave's tree and Zephyr's bindings. Zephyr's own devicetree.h is the test: ZEPHYR_SRC=… crystal spec spec/zephyr/devicetree_header_spec.cr compiles _Static_asserts through it.

Z.5 Ordinals, instances and iteration (aab5992c, merge)

The generated devicetree header gains ordinals, instance numbers and the DT_FOREACH families, which is enough for Zephyr's PL011 (SBSA mode) and NS16550 drivers to compile against it with -Werror. Run ZEPHYR_SRC=… crystal spec spec/zephyr/devicetree_header_spec.cr; it checks the compiled object defines the SBSA UART's device.

Z.6 autoconf.h from a checked table (5c8819ca, merge)

compat/zephyr/config/ holds Zephyr's configuration as a short table per architecture, and tools/zephyr/autoconf.cr writes autoconf.h from it. The table is checked against Zephyr's own Kconfig files, read by cleave's Zephyr dialect, including closure under select and one listed override (x86_64's MMU). Run ZEPHYR_SRC=… crystal spec spec/zephyr/autoconf_spec.cr.

T.12 Builds that survive a moved source tree (116d0e26, merge)

Glint::Symlink.ensure replaces a stale build symlink instead of failing, so the QEMU and U-Boot builds survive a moved source tree. The specs now remove everything they create under TMPDIR. Run crystal spec spec/symlink_spec.cr.

Z.7 Syscall stubs (4eb32a10, merge)

tools/zephyr/syscalls.cr writes Zephyr's zephyr/syscalls/*.h headers: with user mode off, each stub calls its z_impl_ function directly. Both UART drivers now compile from generated headers alone. Run ZEPHYR_SRC=… crystal spec spec/zephyr/syscalls_spec.cr.

Z.8 The minimal shim runtime (36aeae9c, annotate and merge)

Zephyr's PL011 and NS16550 drivers are now linked into Glint's images, with a generated device table and a small Crystal runtime (compat/zephyr/). Nothing initialises them at boot yet. bin/glint-build now needs ZEPHYR_SRC. Run ZEPHYR_SRC=… crystal spec spec/zephyr/device_runtime_spec.cr to watch the init runner record a success and an -EIO failure.

Z.9 Zephyr's UART as a console provider (710533c9, annotate and merge)

compat/zephyr/console.cr puts a Zephyr UART behind Glint's text console protocols, speaking UTF-8 both ways. Phase 1's keyboard and shell specs pass unchanged through it: ZEPHYR_SRC=… crystal spec spec/zephyr/console_spec.cr runs them in a child process over a fake Zephyr UART. Track Z ends here; its console first carries a real boot in slice 2.6, after the exit.

T.13 A suite independent of order (dfd919cc, merge)

tools/qemu/deps.cr builds any missing QEMU dependency archive, so each QEMU spec passes on its own, in any order. Run crystal run tools/qemu/fresh_deps.cr with QEMU_SRC set to prove it from an empty deps directory.

2.2 Where am I (e4c041cf, merge)

Every boot now says where it was entered: Entered at EL2 on aarch64, or Entered at ring 0, SVM available on x86_64. It also turns off UEFI's five-minute watchdog. Run bin/glint-run aarch64 --expect "Entered at EL2", and --no-virtualization for the other answer.

2.3 The memory map (d07fa084, annotate and merge)

Every boot prints the firmware's memory map: a summary of usable and free memory, then one line per descriptor with its UEFI type. Run bin/glint-run aarch64 --expect "Memory map:". The recordings in spec/fixtures/memory_map/ are Glint's own boot lines, re-recorded with crystal run tools/record_memory_map.cr -- aarch64 when QEMU or U-Boot moves.

T.14 Boots that test the current build (16101670, annotate and merge)

bin/glint-build rebuilds an image only when one of its inputs is newer, and bin/glint-run calls it before every boot, so a boot never tests a stale image. bin/glint-build --check aarch64 says stale or up to date.

2.4a Finding the hardware: the devicetree (5c56082b, resubmit twice, then merge)

On aarch64 every boot now finds its console in the devicetree the firmware passes, by following /chosen/stdout-path, and prints Console: PL011 at 0x9000000 (devicetree). boot/fdt.cr reads the tree without allocating, and spec/fdt_spec.cr holds it to Cleave::Fdt. Run bin/glint-run aarch64 --expect "Console: PL011". The resubmissions are a lesson in their own right: image code runs without Crystal's start-up (PLAN, Build constraints).

2.4b Finding the hardware: ACPI (8d77c8eb, merge)

On x86_64 every boot now finds COM1 in the ACPI tables firmware passes, with no AML interpreter: boot/acpi.cr checks the tables' checksums, finds EISAID("PNP0501") in the DSDT and reads the port from its _CRS. Run bin/glint-run x86_64 --expect "Console: 16550 at port 0x3f8 (ACPI DSDT)".

2.5 Native mode's system table (4259f1d0, merge)

boot/native_table.cr builds the system table Glint will run on after it leaves boot services: the firmware's runtime services and configuration tables, Zephyr's UART as the console, and every boot-service entry filled (LocateProtocol for the console, EFI_UNSUPPORTED for the rest). Run crystal spec spec/native_table_spec.cr to watch Phase 1's shell run through it.

Z.10 Uninitialised data the loader zero-fills (1bf86292, merge)

.bss is now a section of its own, which the EFI loader zero-fills, so the images no longer store their zeros: each is 17 KiB smaller. Run llvm-objdump -h -p build/aarch64/EFI/BOOT/BOOTAA64.EFI and compare .data's raw size with SizeOfUninitializedData.

P.4b The matcher's bytecode without start-up (e9b57a84, merge)

The shell now answers typed commands on real firmware. Its bytecode had been a constant that only Crystal's start-up fills, which firmware never runs. Run bin/glint-run aarch64 --type help --expect "help echo ls cat", and llvm-nm -C build/aarch64/glint.so | grep const_init to see that nothing is left for start-up to do.

2.6 Choosing the mode and leaving boot services (c4abcc1a, resubmit for the rebase, then merge)

Glint can now leave boot services. A boot is hosted by default; the shell's native command checks that the firmware's console matches the class the image was built for, calls ExitBootServices, moves onto Glint's own stack and carries on through Zephyr's UART driver. Run bin/glint-run aarch64 --type native --type help --expect "Mode: native": every line after "Left boot services" comes from Zephyr's PL011 driver, compiled unmodified.

T.15 A build tool that is never stale (d0d2a777, merge)

bin/glint-build checks itself against its own sources on start, and rebuilds and re-runs itself if any is newer, so a build never runs with an old copy of its own rules. bin/glint-build --check aarch64 says tool stale when it would.

2.7a Panic, and the Arm vectors (9d653553, merge)

In native mode on aarch64, Glint installs its own exception vectors and reports any unexpected exception: class, ESR, ELR, registers. The shell's fault command prints the address of a udf, then executes it: run bin/glint-run aarch64 --type native --type fault --expect "Panic: undefined instruction" and compare the report's ELR with that address.

2.7b The x86 vectors (28fc7e1f, merge)

In native mode on x86_64, Glint loads a GDT and an IDT of its own at the exit and reports any of the 32 exceptions. Run bin/glint-run x86_64 --type native --type fault --expect "Panic: invalid opcode" and compare RIP with the address fault printed.

2.8 The threat model (a138f6a6, merge)

docs/threat-model.md states what Glint assumes about guests, itself, firmware, linked Zephyr code, ESP files and DMA. Each assumption names the slices that enforce or test it. crystal spec spec/threat_model_spec.cr checks that every named slice exists.

2.9a Loading before the exit (4cdb9424, merge)

Before leaving boot services, Glint copies every file in the ESP's root into pages of its own, so they outlive the firmware's file system. Run bin/glint-run aarch64 --type native --expect "Preloaded".

2.9b Serving the loaded files (e10038a8, merge)

After the exit, Glint serves the files 2.9a loaded through the same file protocol the firmware offered, so ls and cat keep working. Run bin/glint-run aarch64 --type native --type 'cat "hello.txt"' --expect "Hello from the ESP!".

2.10 Native by default (f50ce027, annotate and merge)

Every boot now loads its files, leaves boot services and runs native, with hosted mode as the fallback; glint-mode.txt on the ESP, written by bin/glint-run --mode hosted, asks for hosted. crystal spec spec/run/phase2_spec.cr is the phase demo: a native boot whose fault report matches the address fault printed, a hosted boot that leaves and reads a preloaded file, and a native boot without virtualization.

Phase 2 demo: on both architectures under QEMU and U-Boot, Glint boots, reports where it was entered, its memory map and its console, preloads its files, leaves boot services by default, keeps its console through Zephyr's UART driver, and panics cleanly on a deliberate fault at the address it printed.

2.7c Overflow is a panic (a75d8191, merge)

An arithmetic overflow in the image used to hang it without a word; now it panics with the address of the code that overflowed. Run bin/glint-run aarch64 --type 'fault "overflow"' --expect "Panic: arithmetic overflow".

3.1 Page-frame allocator (016aba12, merge)

After the exit Glint hands out pages itself, through the native table's AllocatePages and FreePages. Until Glint runs on its own page tables it withholds everything the map calls usable, since the firmware's tables live there, and serves a 32-page pool from its own image. Run bin/glint-run aarch64 --expect "Pages: 32 free".

3.2 Page-table builder (6afe227d, merge)

Glint can now build page tables of its own. One builder walks four levels of 9-bit indexes, as xv6's walk does, and uses 2 MiB blocks wherever a range allows them and 4 KiB pages elsewhere. Small entry formats say how Arm (at EL2 or EL1) and x86_64 spell each entry. Nothing loads these tables yet, so a boot looks just as it did at 3.1. Run crystal spec spec/page_table_spec.cr to see each format's entries checked bit by bit.

3.2a Glint's own tables on aarch64 (3b8e6b89, merge)

On aarch64, Glint now decides what every address means. Right after leaving boot services it maps all of memory, its own image, the firmware's runtime services and tables, and its UART's registers, each at its own address, and switches the CPU onto those tables. Because nothing moves, every pointer Glint holds still works. The firmware's old tables no longer matter, so 3.1's allocator can hand out all of memory. Run bin/glint-run aarch64 --expect "Page tables: TTBR0_EL2".

3.2b Glint's own tables on x86_64 (df5ec1bb, merge)

x86_64 now runs on Glint's own tables too. The layout is the one 3.2a built for aarch64, so only the switch differs: one write to CR3, x86's register for the root of the page tables. The log shows where the firmware's tables were (0x80000) and where Glint's are now. Run bin/glint-run x86_64 --expect "Page tables: CR3 0x80000".

3.3 Guard pages and exception stacks (0a2f94fa, merge)

A stack that grows too far used to run into whatever memory lay below it, silently on Arm, or reset the whole machine on x86. Now the page below each of Glint's stacks is left out of the page tables, so overflowing it faults at once. The fault is reported from a separate exception stack, which on x86 is reached through the TSS. The new overflow command shows this: it prints the guard page's range, then recurses until it hits that page. Run bin/glint-run x86_64 --type overflow --expect "Stack overflow: guard page".

3.3b Permissions (b783b291, merge)

Each kind of memory now gets only the access it needs. Glint's code can be run but not written. Its data and all free memory can be written but not run. The files it preloaded and the firmware's tables can only be read. So a bug that writes over code, or jumps into data, faults at once instead of doing damage quietly. Run bin/glint-run aarch64 --type 'fault "write"' --expect "Panic: data abort": the store to Glint's own code is refused, at the address it printed.

3.4 Kernel heap (1248ad32, merge)

Until now every buffer in Glint's image was a fixed array, and no Crystal object could be created with new, because nothing supplied the memory. The heap does that. It hands out small blocks in a few sizes, reuses the ones that are freed, and takes whole pages for anything bigger. It works the same whether the firmware or Glint owns memory. Run bin/glint-run x86_64 --mode hosted --expect "Heap: ready".

3.5 A guest address space (86dd5dbc, merge)

A guest will think it owns physical memory starting where its machine expects, at 0x40000000 on Arm and at 0 on a PC. Glint makes that true with a second set of page tables, built by the same builder as its own, which the hardware applies after the guest's own tables. Only the guest's RAM, zeroed first, goes into them, so anything else the guest touches will trap to Glint. Nothing runs a guest yet. Run bin/glint-run aarch64 --type guest_space --expect "Guest space: 16 MiB": the printed host address is the RAM base plus 0x123000.