diff --git a/.github/workflows/test-x86-lilo-qemu.yml b/.github/workflows/test-x86-lilo-qemu.yml new file mode 100644 index 0000000000..3ad3f79aa7 --- /dev/null +++ b/.github/workflows/test-x86-lilo-qemu.yml @@ -0,0 +1,41 @@ +name: X86 LILO (legacy BIOS/MBR) QEMU test + +on: + pull_request: + branches: [ '*' ] +jobs: + lilo_qemu_test: + runs-on: ubuntu-latest + container: + image: ghcr.io/wolfssl/wolfboot-ci-x86:v1.29 + timeout-minutes: 20 + + steps: + - uses: actions/checkout@v4 + with: + submodules: true + + - name: Trust workspace + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + - name: install req + run: | + apt-get update -o Acquire::Retries=3 + apt-get install --no-install-recommends -y -q -o Acquire::Retries=3 dosfstools fdisk cpio + # A stock distribution bzImage to boot, without installing it. The + # kernel itself ships in linux-binary-, under /usr/lib/modules. + mkdir -p /tmp/kernel && chmod 777 /tmp/kernel && cd /tmp/kernel + abi=$(apt-cache depends linux-image-amd64 | awk '/Depends: linux-image-/ {sub("linux-image-", "", $2); print $2; exit}') + apt-get download linux-binary-$abi + dpkg-deb -x linux-binary-${abi}_*.deb . + cp usr/lib/modules/$abi/vmlinuz /tmp/vmlinuz + + - name: build + run: | + cp config/examples/x86_64_lilo.config .config + make keytools + make + + - name: run test + run: | + python3 tools/lilo/test/run-qemu.py -k /tmp/vmlinuz diff --git a/.gitignore b/.gitignore index 178e591d48..3bd45e8fdc 100644 --- a/.gitignore +++ b/.gitignore @@ -667,3 +667,7 @@ tools/unit-tests/stm32wb_write_extract.h tools/unit-tests/t10xx_flash_status_extract.h tools/unit-tests/ti_hercules_erase_extract.h tools/unit-tests/versal_host_write.c + +# x86_64_lilo +tools/lilo/lilo +tools/lilo/test/out diff --git a/Makefile b/Makefile index b8a0336af0..6de2d7cea1 100644 --- a/Makefile +++ b/Makefile @@ -442,6 +442,10 @@ ifeq ($(TARGET),aarch64_efi) MAIN_TARGET:=wolfboot.efi endif +ifeq ($(TARGET),x86_64_lilo) + MAIN_TARGET:=wolfboot.bin +endif + ifeq ($(FSP), 1) MAIN_TARGET:=wolfboot_stage1.bin endif diff --git a/arch.mk b/arch.mk index 8042ff8901..9a3af7fcbb 100644 --- a/arch.mk +++ b/arch.mk @@ -2476,6 +2476,29 @@ ifneq ($(CROSS_COMPILE_PATH),) endif +ifeq ($(TARGET),x86_64_lilo) + # Legacy BIOS/MBR boot, LILO style: no EFI and no FSP. wolfBoot runs in + # flat 32-bit protected mode and drops back to real mode for BIOS disk, + # keyboard and video services. tools/lilo installs it on a disk. + USE_GCC_HEADLESS=0 + DISK_FS?=fat32 + LD := ld + CFLAGS += -m32 -ffreestanding -fno-stack-protector -fno-PIC -fno-pie \ + -mno-mmx -mno-sse -mno-red-zone -fno-asynchronous-unwind-tables \ + -ffunction-sections -fdata-sections -U_FORTIFY_SOURCE -Os + # gnu11: keeps the host glibc headers from turning memchr & co. into + # C23 _Generic macros that collide with src/string.c + CFLAGS += -std=gnu11 + CFLAGS += -DWOLFBOOT_LILO -DDEBUG_UART -DWOLFBOOT_NO_LOAD_ADDRESS + LDFLAGS = -m elf_i386 --gc-sections --entry=_mbr_start -T $(LSCRIPT) \ + -Map=wolfboot.map + LD_START_GROUP = + LD_END_GROUP = $(shell $(CC) -m32 -print-libgcc-file-name) + OBJS += src/boot_x86_bios_mbr.o src/boot_x86_bios_start.o src/x86/bios.o + OBJS += src/disk.o src/gpt.o + UPDATE_OBJS := src/update_lilo.o +endif + ifeq ($(TARGET),x86_64_efi) USE_GCC_HEADLESS=0 GNU_EFI_LIB_PATH?=/usr/lib diff --git a/config/examples/x86_64_lilo.config b/config/examples/x86_64_lilo.config new file mode 100644 index 0000000000..6160cb6826 --- /dev/null +++ b/config/examples/x86_64_lilo.config @@ -0,0 +1,11 @@ +ARCH=x86_64 +TARGET=x86_64_lilo +SIGN=NONE +HASH=SHA256 +DEBUG=0 +SPMATH=0 +FORCE_32BIT=1 +DISK_FS=fat32 +WOLFBOOT_NO_PARTITIONS=1 +# required for keytools +WOLFBOOT_SECTOR_SIZE?=0x1000 diff --git a/docs/Targets.md b/docs/Targets.md index b458dd969e..20215e8026 100644 --- a/docs/Targets.md +++ b/docs/Targets.md @@ -46,6 +46,7 @@ This README describes configuration of supported targets. * [NXP T10xx PPC (T1024 / T1040)](#nxp-qoriq-t10xx-ppc-t1024--t1040) * [NXP T2080 PPC](#nxp-qoriq-t2080-ppc) * [Qemu x86-64 UEFI](#qemu-x86-64-uefi) +* [x86-64 legacy BIOS/MBR, LILO style (x86_64_lilo)](#x86-64-legacy-biosmbr-lilo-style-x86_64_lilo) * [NVIDIA Jetson Orin (aarch64_efi)](#nvidia-jetson-orin-aarch64_efi) * [NVIDIA Jetson Orin (NVIDIA Tegra234) BL33 firmware](#nvidia-jetson-orin-nvidia-tegra234-bl33-firmware) * [Raspberry Pi pico 2 (rp2350)](#raspberry-pi-pico-rp2350) @@ -8189,6 +8190,139 @@ To pass an authenticated Linux kernel command line, sign the kernel with `--cmdl +## x86-64 legacy BIOS/MBR, LILO style (x86_64_lilo) + +The third x86-64 port, next to [UEFI](#qemu-x86-64-uefi) and +[Intel FSP](#intel-x86_64-with-intel-fsp-support): no EFI stage and no FSP. +wolfBoot boots from the MBR of a legacy BIOS machine and is installed, the +way LILO was, by a `lilo` tool that reads `/etc/lilo.conf`. + +### Boot flow + +1. The BIOS loads the MBR (`src/boot_x86_bios_mbr.S`) at `0x7C00`. It reads + wolfBoot from the sectors between the MBR and the first partition + (INT 13h extensions, LBA) to `0x7E00`. +2. wolfBoot (`src/boot_x86_bios_start.S`) enables A20 and switches to flat + 32-bit protected mode. Disk, keyboard, video and memory-map services are + BIOS calls, made by dropping back to real mode for each one + (`src/x86/bios.c`), so wolfBoot needs no disk driver of its own: it + reads the disk the BIOS booted from (tested on QEMU with IDE, AHCI on + q35, and virtio-blk). +3. It reads the boot map written by `lilo` and, LILO-style, shows a `boot:` + prompt over the configured labels (`src/update_lilo.c`). +4. The selected kernel is read from a FAT32 partition with wolfBoot's own + FAT32 reader, staged in RAM and checked with the regular wolfBoot image + verification (with `SIGN=NONE`, the SHA-256 integrity check). +5. The kernel is started through the Linux real-mode boot protocol: setup at + `0x90000`, protected-mode kernel at `0x100000`, initrd high in RAM. The + kernel's own setup code then queries the BIOS (E820, EDD, video), as it + did under LILO. A 64-bit (x86_64) kernel boots this way like any other. + `other=` entries chain-load a partition boot sector instead. + +The progress letters are LILO's: `L` (MBR running), `LI` (wolfBoot loaded), +`LIL` (wolfBoot running), `LILO` (map loaded). A failure in the MBR prints +the BIOS error code after the letters (`E0`: not installed, `E1`: no LBA +support in the BIOS). + +As with LILO the MBR code is BIOS-only, and the disk must use MBR (not GPT) +partitioning. Unlike LILO, the map names files instead of listing their +sectors, so a kernel can be replaced in the FAT32 partition without running +`lilo` again; run it again when `lilo.conf` changes. + +Disk layout written by `lilo` (all of it in front of the first partition, +which usually starts at sector 2048): + +| LBA | Content | +|-----------|----------------------------------------------------------| +| 0 | MBR boot code and descriptor; disk signature and partition table preserved | +| 1 - 33 | boot map (`include/lilo_map.h`) | +| 34 - | wolfBoot (about 40 sectors) | + +### Configuration and build + +An example configuration is in +[config/examples/x86_64_lilo.config](../config/examples/x86_64_lilo.config). +The build needs a 32-bit capable gcc (`gcc-multilib` on Debian/Ubuntu). + +``` +cp config/examples/x86_64_lilo.config .config +make keytools +make +make -C tools/lilo +``` + +`wolfboot.bin` is the MBR sector followed by wolfBoot. Kernels are wolfBoot +images: + +``` +tools/keytools/sign --no-sign --sha256 bzImage 1 +``` + +### Installing + +`tools/lilo/lilo` takes the usual `lilo` options (`-C`, `-b`, `-i`, `-D`, +`-s`, `-t`, `-q`, `-u`, `-v`) and the usual `lilo.conf` keywords; see +[tools/lilo/README.md](../tools/lilo/README.md). For example: + +``` +boot=/dev/sda +install=/boot/wolfboot.bin +prompt +timeout=50 +default=linux +serial=0,115200n8 + +image=/boot/vmlinuz_v1_signed.bin + label=linux + initrd=/boot/initrd.img + root=/dev/sda2 + read-only + append="console=ttyS0,115200" + +other=/dev/sda3 + label=dos +``` + +`/boot` is taken to be where the FAT32 partition is mounted, so +`/boot/vmlinuz_v1_signed.bin` is `/vmlinuz_v1_signed.bin` on the partition +(see `fat-mount=` to change it). `lilo` checks every image with wolfBoot's +own partition and FAT32 code before writing anything. + +### Running on QEMU + +[tools/lilo/test/run-qemu.py](../tools/lilo/test/run-qemu.py) builds a disk +image (MBR, a FAT32 partition and a partition with a test boot sector), +signs a kernel (by default the newest `/boot/vmlinuz-*`), installs wolfBoot +with `lilo`, and boots it on `qemu-system-x86_64` with the stock SeaBIOS: +default boot after the timeout, a label and extra parameters typed at the +prompt, recovery from an unknown label and from a corrupted image, and +`other=` chain loading. + +``` +python3 tools/lilo/test/run-qemu.py [-k bzImage] [-a kvm|tcg] +``` + +``` +LILO +wolfBoot: 639 KB low, 510 MB extended memory +... +boot: linux +Loading linux +Boot partition: 0xF00000 (sz 14546880, ver 0x1, type 0xFF01) +.............. +Verified /vmlinuz-wolfboot-v1.bin, version 1 +Loading initrd. +Booting kernel: BOOT_IMAGE=linux ro console=ttyS0,115200 panic=-1 +[ 0.000000] Linux version 7.2.11+deb14-amd64 ... +``` + +`SIGN=NONE` authenticates nothing: anyone who can write the disk can replace +the kernel. The image check only catches corruption. The port uses the +generic image verification, so a signing configuration (with a keystore) +builds the same way, but the MBR code and the map are not authenticated by +anything in a legacy BIOS boot. + + ## NVIDIA Jetson Orin (aarch64_efi) The `aarch64_efi` target builds wolfBoot as an AArch64 UEFI application (`wolfboot.efi`), the direct counterpart of the [Qemu x86-64 UEFI](#qemu-x86-64-uefi) target. It uses only UEFI Boot Services (no SoC-specific registers), so the same binary runs on any AArch64 UEFI platform. It has been validated on the NVIDIA Jetson Orin Nano Developer Kit (Tegra234), where the on-module UEFI firmware (edk2-nvidia) launches it after the NVIDIA-signed early boot chain (BootROM -> MB1 -> MB2 -> UEFI). wolfBoot reads the next-stage image from the EFI Simple File System, authenticates it with wolfCrypt, and boots it via the UEFI `LoadImage`/`StartImage` services (an AArch64 Linux `Image` is itself a PE/COFF EFI-stub application). diff --git a/hal/x86_64_lilo.c b/hal/x86_64_lilo.c new file mode 100644 index 0000000000..4d23bec1cd --- /dev/null +++ b/hal/x86_64_lilo.c @@ -0,0 +1,255 @@ +/* x86_64_lilo.c + * + * HAL for the x86_64_lilo target: legacy BIOS/MBR boot, no EFI, no FSP. + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#include +#include + +#include "hal.h" +#include "disk.h" +#include "printf.h" +#include "x86/bios.h" + +/* Console: BIOS teletype plus an 8250 UART driven directly. Output always + * goes to both; serial input is accepted only once the map enables it, as + * a floating UART reads back as a stream of 0xFF. */ + +#define UART_THR 0 +#define UART_RBR 0 +#define UART_DLL 0 +#define UART_DLH 1 +#define UART_IER 1 +#define UART_FCR 2 +#define UART_LCR 3 +#define UART_MCR 4 +#define UART_LSR 5 +#define UART_SCR 7 +#define UART_LSR_DR 0x01 +#define UART_LSR_THRE 0x20 + +static inline void io_write8(uint16_t port, uint8_t v) +{ + __asm__ volatile ("outb %0, %1" : : "a"(v), "Nd"(port)); +} + +static inline uint8_t io_read8(uint16_t port) +{ + uint8_t v; + __asm__ volatile ("inb %1, %0" : "=a"(v) : "Nd"(port)); + return v; +} + +static const uint16_t uart_ports[4] = { 0x3F8, 0x2F8, 0x3E8, 0x2E8 }; +static uint16_t uart_base; +static int uart_input; +static char last_out; + +static int serial_setup(uint16_t base, uint32_t baud) +{ + uint16_t div; + + io_write8(base + UART_SCR, 0x5A); + if (io_read8(base + UART_SCR) != 0x5A) + return -1; + if (baud == 0 || baud > 115200) + baud = 115200; + div = (uint16_t)(115200 / baud); + io_write8(base + UART_IER, 0x00); + io_write8(base + UART_LCR, 0x80); + io_write8(base + UART_DLL, (uint8_t)div); + io_write8(base + UART_DLH, (uint8_t)(div >> 8)); + io_write8(base + UART_LCR, 0x03); /* 8N1 */ + io_write8(base + UART_FCR, 0x07); + io_write8(base + UART_MCR, 0x03); + return 0; +} + +static void serial_putc(char c) +{ + int spin = 100000; + + if (uart_base == 0) + return; + while ((io_read8(uart_base + UART_LSR) & UART_LSR_THRE) == 0 && --spin) + ; + io_write8(uart_base + UART_THR, (uint8_t)c); +} + +static void console_putc(char c) +{ + if (c == '\n' && last_out != '\r') { + serial_putc('\r'); + bios_putc('\r'); + } + serial_putc(c); + bios_putc(c); + last_out = c; +} + +void uart_init(void) +{ + if (serial_setup(uart_ports[0], 115200) == 0) + uart_base = uart_ports[0]; +} + +void uart_write(const char *buf, unsigned int len) +{ + while (len--) + console_putc(*buf++); +} + +void hal_console_serial(int port, uint32_t baud) +{ + if (port < 0 || port > 3) + return; + if (serial_setup(uart_ports[port], baud) == 0) { + uart_base = uart_ports[port]; + uart_input = 1; + } +} + +int hal_console_getc(void) +{ + uint8_t lsr; + int c = bios_getkey(); + + if (c >= 0) + return c; + if (uart_input && uart_base != 0) { + lsr = io_read8(uart_base + UART_LSR); + if (lsr != 0xFF && (lsr & UART_LSR_DR)) + return io_read8(uart_base + UART_RBR); + } + return -1; +} + +void hal_init(void) +{ + uart_init(); + /* Third LILO letter: the second stage is running. */ + console_putc('L'); +} + +void hal_prepare_boot(void) +{ +} + +/* Disk: drive 0 is the BIOS boot drive. Every transfer goes through a low + * memory bounce buffer, since the BIOS only reaches the first megabyte; + * aligning it to its size keeps any transfer off a 64 KB boundary. */ +#define BOUNCE_SECTORS 64 +static uint8_t bounce[BOUNCE_SECTORS * 512] + __attribute__((aligned(BOUNCE_SECTORS * 512))); + +int disk_init(int drv) +{ + return (drv == 0) ? 0 : -1; +} + +int disk_read(int drv, uint64_t start, uint32_t count, uint8_t *buf) +{ + uint32_t done = 0; + + if (drv != 0) + return -1; + while (done < count) { + uint64_t off = start + done; + uint32_t skip = (uint32_t)(off & 511U); + uint32_t left = count - done; + uint32_t n = (skip + left + 511U) >> 9; + uint32_t chunk; + + if (n > BOUNCE_SECTORS) + n = BOUNCE_SECTORS; + if (bios_disk_read(bios_boot_drive, off >> 9, (uint16_t)n, + bounce) != 0) { + wolfBoot_printf("BIOS disk read error at LBA %u\n", + (unsigned int)(off >> 9)); + return -1; + } + chunk = n * 512U - skip; + if (chunk > left) + chunk = left; + memcpy(buf + done, bounce + skip, chunk); + done += chunk; + } + return 0; +} + +/* Read-only: nothing on the boot path writes to the disk. */ +int disk_write(int drv, uint64_t start, uint32_t count, const uint8_t *buf) +{ + (void)drv; + (void)start; + (void)count; + (void)buf; + return -1; +} + +void disk_close(int drv) +{ + (void)drv; +} + +/* No flash: images are read from the disk into RAM. */ +int hal_flash_write(uintptr_t address, const uint8_t *data, int len) +{ + (void)address; + (void)data; + (void)len; + return -1; +} + +int hal_flash_erase(uintptr_t address, int len) +{ + (void)address; + (void)len; + return -1; +} + +void hal_flash_unlock(void) +{ +} + +void hal_flash_lock(void) +{ +} + +void *hal_get_primary_address(void) +{ + return NULL; +} + +void *hal_get_update_address(void) +{ + return NULL; +} + +void *hal_get_dts_address(void) +{ + return NULL; +} + +void *hal_get_dts_update_address(void) +{ + return NULL; +} diff --git a/hal/x86_64_lilo.ld b/hal/x86_64_lilo.ld new file mode 100644 index 0000000000..04122e6f59 --- /dev/null +++ b/hal/x86_64_lilo.ld @@ -0,0 +1,71 @@ +/* Legacy BIOS/MBR boot. wolfboot.bin is the MBR sector (0x7C00) followed by + * the stage the MBR loads at 0x7E00. Everything must stay below the Linux + * real-mode setup area at 0x90000. */ + +OUTPUT_FORMAT("elf32-i386") +OUTPUT_ARCH(i386) +ENTRY(_mbr_start) + +STAGE_BASE = 0x7E00; +STAGE_LIMIT = 0x90000; +STACK_SIZE = 0x4000; + +SECTIONS +{ + .mbr 0x7C00 : + { + KEEP(*(.mbr)) + } + + .text16 STAGE_BASE : + { + _start_wolfboot = .; + KEEP(*(.text16)) + KEEP(*(.data16)) + } + ASSERT(. <= 0x10000, "real-mode code and data must stay below 64 KB") + + .text : + { + _start_text = .; + *(.text*) + } + + .rodata : + { + *(.rodata*) + } + + .data : + { + _start_data = .; + *(.data*) + _end_data = .; + } + _stage_load_end = .; + + .bss (NOLOAD) : ALIGN(16) + { + _start_bss = .; + __bss_start__ = .; + *(.bss*) + *(COMMON) + . = ALIGN(16); + _end_bss = .; + __bss_end__ = .; + . += STACK_SIZE; + _stack_top = .; + } + _end_wb = .; + _end = .; + + _stage_sectors = (_stage_load_end - STAGE_BASE + 511) / 512; + ASSERT(_end_wb <= STAGE_LIMIT, "wolfBoot overlaps the Linux real-mode area") + + /DISCARD/ : + { + *(.note*) + *(.comment) + *(.eh_frame*) + } +} diff --git a/include/lilo_map.h b/include/lilo_map.h new file mode 100644 index 0000000000..73868b10de --- /dev/null +++ b/include/lilo_map.h @@ -0,0 +1,114 @@ +/* lilo_map.h + * + * On-disk format shared by tools/lilo and the x86_64_lilo target. + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* Disk layout written by tools/lilo, all of it in front of the first + * partition (the "MBR gap"): + * + * LBA 0 MBR: boot code + lilo_mbr_desc + partition table + * LBA map_lba map: lilo_map_hdr sector, then the entries + * LBA stage_lba wolfBoot (wolfboot.bin minus its first sector) + * + * Unlike the original LILO map, entries name files on a FAT32 partition + * instead of listing their sectors, so replacing a kernel file does not + * invalidate the map. All fields are little-endian. + */ + +#ifndef WOLFBOOT_LILO_MAP_H +#define WOLFBOOT_LILO_MAP_H + +#include + +#define LILO_SECTOR_SIZE 512 + +/* Boot descriptor inside the MBR, between the code and the disk signature. */ +#define LILO_MBR_DESC_OFFSET 0x190 +#define LILO_MBR_DESC_MAGIC 0x4C49574CUL /* "LWIL" */ +#define LILO_MBR_SIG_OFFSET 0x1B8 /* NT disk signature */ +#define LILO_MBR_PTABLE_OFFSET 0x1BE + +struct lilo_mbr_desc { + uint32_t magic; + uint16_t stage_sectors; /* set by the linker */ + uint16_t map_sectors; /* the rest is set by tools/lilo */ + uint64_t stage_lba; + uint64_t map_lba; +} __attribute__((packed)); + +#define LILO_MAP_MAGIC 0x50414D4CUL /* "LMAP" */ +#define LILO_MAP_VERSION 1 +#define LILO_MAP_MAX_ENTRIES 16 +#define LILO_LABEL_LEN 16 /* LILO's limit: 15 characters */ +#define LILO_PATH_LEN 128 /* WOLFBOOT_FS_MAX_PATH */ +#define LILO_CMDLINE_LEN 704 +#define LILO_ENTRY_SIZE 1024 +#define LILO_MAP_SECTORS \ + (1 + (LILO_MAP_MAX_ENTRIES * LILO_ENTRY_SIZE) / LILO_SECTOR_SIZE) + +/* lilo_map_hdr.flags */ +#define LILO_MAP_PROMPT 0x0001 +#define LILO_MAP_SERIAL 0x0002 + +/* lilo_map_hdr.timeout: no timeout, wait at the prompt forever */ +#define LILO_TIMEOUT_NONE 0xFFFF + +/* lilo_map_entry.type */ +#define LILO_ENTRY_LINUX 1 +#define LILO_ENTRY_OTHER 2 /* chain-load a partition boot sector */ + +/* lilo_map_entry.vga, as in the kernel boot protocol vid_mode field */ +#define LILO_VGA_DEFAULT 0 /* leave the kernel's own default */ +#define LILO_VGA_NORMAL 0xFFFF +#define LILO_VGA_EXTENDED 0xFFFE +#define LILO_VGA_ASK 0xFFFD + +struct lilo_map_hdr { + uint32_t magic; + uint16_t version; + uint16_t n_entries; + uint16_t default_entry; + uint16_t flags; + uint16_t timeout; /* tenths of a second at the prompt */ + uint16_t delay; /* tenths of a second to wait for a shift key */ + uint16_t serial_port; /* 0..3: COM1..COM4 */ + uint16_t entry_size; + uint32_t serial_baud; + uint32_t crc32; /* over the header (crc32 = 0) and the entries */ + uint8_t reserved[LILO_SECTOR_SIZE - 28]; +} __attribute__((packed)); + +struct lilo_map_entry { + char label[LILO_LABEL_LEN]; + char alias[LILO_LABEL_LEN]; + uint8_t type; + uint8_t part; /* LINUX: FAT32 partition index (0-based) + * OTHER: MBR partition slot (1..4) */ + uint16_t vga; + uint32_t reserved0; + char kernel[LILO_PATH_LEN]; + char initrd[LILO_PATH_LEN]; + char cmdline[LILO_CMDLINE_LEN]; + uint8_t reserved1[LILO_ENTRY_SIZE - (2 * LILO_LABEL_LEN) - 8 - + (2 * LILO_PATH_LEN) - LILO_CMDLINE_LEN]; +} __attribute__((packed)); + +#endif /* WOLFBOOT_LILO_MAP_H */ diff --git a/include/x86/bios.h b/include/x86/bios.h new file mode 100644 index 0000000000..2d2be7ee87 --- /dev/null +++ b/include/x86/bios.h @@ -0,0 +1,88 @@ +/* bios.h + * + * Real-mode BIOS services for the x86_64_lilo target (legacy MBR boot). + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#ifndef WOLFBOOT_X86_BIOS_H +#define WOLFBOOT_X86_BIOS_H + +#include + +/* Register image exchanged with a real-mode interrupt. The layout is shared + * with src/boot_x86_bios_start.S. */ +struct bios_regs { + uint32_t eax; + uint32_t ebx; + uint32_t ecx; + uint32_t edx; + uint32_t esi; + uint32_t edi; + uint32_t ebp; + uint16_t ds; + uint16_t es; + uint16_t flags; + uint16_t pad; +} __attribute__((packed)); + +#define BIOS_FLAG_CF 0x0001 +#define BIOS_FLAG_ZF 0x0040 + +/* Final real-mode handoff (Linux real-mode setup, chain-loaded boot sector). + * Layout shared with src/boot_x86_bios_start.S. */ +struct bios_jump { + uint16_t cs; + uint16_t ip; + uint16_t ds; /* also loaded into es, fs, gs */ + uint16_t ss; + uint16_t sp; + uint16_t dx; + uint16_t si; + uint16_t pad; +} __attribute__((packed)); + +/* Every buffer handed to the BIOS must be addressable from real mode. */ +#define BIOS_LOWMEM_LIMIT 0x100000UL +#define BIOS_SEG(addr) ((uint16_t)(((uintptr_t)(addr)) >> 4)) +#define BIOS_OFF(addr) ((uint16_t)(((uintptr_t)(addr)) & 0xF)) + +/* Drive number the BIOS booted from (DL at MBR entry). */ +extern uint8_t bios_boot_drive; + +/* Copy of the MBR boot descriptor, taken before 0x7C00 can be reused. */ +extern uint8_t bios_mbr_desc[]; + +void bios_int(uint8_t intno, struct bios_regs *regs); +void __attribute__((noreturn)) bios_realmode_jump(const struct bios_jump *j); + +/* src/x86/bios.c */ +int bios_disk_read(uint8_t drive, uint64_t lba, uint16_t count, void *buf); +void bios_putc(char c); +int bios_getkey(void); +int bios_shift_pressed(void); +uint32_t bios_ticks(void); +uint32_t bios_lowmem_top(void); +uint32_t bios_mem_top(void); + +/* hal/x86_64_lilo.c */ +void hal_console_serial(int port, uint32_t baud); +int hal_console_getc(void); + +#endif /* WOLFBOOT_X86_BIOS_H */ diff --git a/options.mk b/options.mk index b1de7043d9..612381fa4a 100644 --- a/options.mk +++ b/options.mk @@ -1042,8 +1042,8 @@ ifneq ($(DISK_FS),) $(error DISK_FS must be one of: fat32, ext4, both) endif ifeq ($(WOLFBOOT_TARGET_BUILD),1) - ifeq (,$(findstring WOLFBOOT_UPDATE_DISK,$(CFLAGS))) - $(error DISK_FS requires a disk-boot target (DISK_SDCARD=1, DISK_EMMC=1, or an x86 FSP/AHCI target)) + ifeq (,$(findstring WOLFBOOT_UPDATE_DISK,$(CFLAGS))$(findstring WOLFBOOT_LILO,$(CFLAGS))) + $(error DISK_FS requires a disk-boot target (DISK_SDCARD=1, DISK_EMMC=1, an x86 FSP/AHCI target or x86_64_lilo)) endif endif CFLAGS+=-D"WOLFBOOT_DISK_FS=1" diff --git a/src/boot_x86_bios_mbr.S b/src/boot_x86_bios_mbr.S new file mode 100644 index 0000000000..6f41b5f23c --- /dev/null +++ b/src/boot_x86_bios_mbr.S @@ -0,0 +1,164 @@ +/* boot_x86_bios_mbr.S + * + * MBR boot sector for the x86_64_lilo target. + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* Loaded by the BIOS at 0x7C00 with the boot drive in DL. Reads + * desc_stage_sectors sectors from desc_stage_lba (both patched in by + * tools/lilo) to 0x7E00 through the INT 13h extensions and jumps there. + * + * Progress follows LILO: "L" once running, "LI" once the stage is loaded. + * On failure the BIOS status (or E0: not installed, E1: no LBA support) is + * printed after the letters and the machine halts. */ + +#define STAGE_SEG 0x07E0 +#define MAX_XFER 64 /* sectors per INT 13h call: 32 KB */ + + .code16 + .section .mbr, "ax" + .globl _mbr_start +_mbr_start: + cli + xorw %ax, %ax + movw %ax, %ds + movw %ax, %es + movw %ax, %ss + movw $0x7C00, %sp + ljmp $0, $1f +1: + sti + cld + movb %dl, boot_drive + movb $'L', %al + call putc + + movb $0x41, %ah + movw $0x55AA, %bx + movb boot_drive, %dl + int $0x13 + movb $0xE1, %ah + jc fail + cmpw $0xAA55, %bx + jne fail + testb $1, %cl + jz fail + + movl desc_stage_lba, %eax + movl %eax, dap_lba + movl desc_stage_lba + 4, %edx + movl %edx, dap_lba + 4 + orl %edx, %eax + movb $0xE0, %ah + jz fail + movw $STAGE_SEG, dap_seg + movw desc_stage_sectors, %di + +load_loop: + testw %di, %di + jz loaded + movw %di, %ax + cmpw $MAX_XFER, %ax + jbe 2f + movw $MAX_XFER, %ax +2: + movw %ax, dap_count + movw $dap, %si + movb boot_drive, %dl + movb $0x42, %ah + int $0x13 + jc fail + movzwl dap_count, %eax + subw %ax, %di + addl %eax, dap_lba + adcl $0, dap_lba + 4 + shlw $5, %ax + addw %ax, dap_seg + jmp load_loop + +loaded: + movb $'I', %al + call putc + movb boot_drive, %dl + ljmp $0, $_stage_entry + +/* AH = error code */ +fail: + movb %ah, %bl + movb $' ', %al + call putc + movb %bl, %al + shrb $4, %al + call putnibble + movb %bl, %al + call putnibble +3: + hlt + jmp 3b + +putnibble: + andb $0x0F, %al + addb $'0', %al + cmpb $'9', %al + jbe putc + addb $7, %al +/* Echoed to COM1 as well, so a headless machine shows the same letters. */ +putc: + pushw %bx + pushw %dx + movw $0x3F8, %dx + outb %al, %dx + movb $0x0E, %ah + movw $0x0007, %bx + int $0x10 + popw %dx + popw %bx + ret + +boot_drive: + .byte 0 + + .p2align 2 +dap: + .byte 16, 0 +dap_count: + .word 0 +dap_off: + .word 0 +dap_seg: + .word 0 +dap_lba: + .quad 0 + +/* struct lilo_mbr_desc, see include/lilo_map.h */ + .org 0x190 +desc_magic: + .long 0x4C49574C +desc_stage_sectors: + .word _stage_sectors +desc_map_sectors: + .word 0 +desc_stage_lba: + .quad 0 +desc_map_lba: + .quad 0 + + .org 0x1FE + .byte 0x55, 0xAA diff --git a/src/boot_x86_bios_start.S b/src/boot_x86_bios_start.S new file mode 100644 index 0000000000..c8c0f96b96 --- /dev/null +++ b/src/boot_x86_bios_start.S @@ -0,0 +1,291 @@ +/* boot_x86_bios_start.S + * + * Real-mode entry, protected-mode switch and BIOS call thunk for the + * x86_64_lilo target. + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* wolfBoot runs in flat 32-bit protected mode with interrupts off. Each BIOS + * service is reached by dropping back to real mode (bios_int), and the final + * handoff to a Linux real-mode setup or a chain-loaded boot sector is a one + * way trip to real mode (bios_realmode_jump). The real-mode pieces live in + * .text16/.data16, which the linker keeps below 64 KB so they are reachable + * with zero segments. */ + +#define CODE32_SEL 0x08 +#define DATA32_SEL 0x10 +#define CODE16_SEL 0x18 +#define DATA16_SEL 0x20 + +/* Real-mode stack: below the MBR, which is no longer needed */ +#define RM_STACK_TOP 0x7000 + +/* struct bios_regs, see include/x86/bios.h */ +#define R_EAX 0 +#define R_EBX 4 +#define R_ECX 8 +#define R_EDX 12 +#define R_ESI 16 +#define R_EDI 20 +#define R_EBP 24 +#define R_DS 28 +#define R_ES 30 +#define R_FLAGS 32 +#define BIOS_REGS_SIZE 36 + +/* struct bios_jump */ +#define J_CS 0 +#define J_IP 2 +#define J_DS 4 +#define J_SS 6 +#define J_SP 8 +#define J_DX 10 +#define J_SI 12 +#define BIOS_JUMP_SIZE 16 + +#define MBR_DESC (0x7C00 + 0x190) +#define MBR_DESC_SIZE 24 + + .code16 + .section .text16, "ax" + .globl _stage_entry +_stage_entry: + cli + xorw %ax, %ax + movw %ax, %ds + movw %ax, %es + movw %ax, %ss + movw $RM_STACK_TOP, %sp + cld + movb %dl, bios_boot_drive + movw $MBR_DESC, %si + movw $bios_mbr_desc, %di + movw $MBR_DESC_SIZE, %cx + rep movsb + + /* A20: ask the BIOS, then the fast A20 gate for good measure */ + movw $0x2401, %ax + int $0x15 + inb $0x92, %al + orb $0x02, %al + andb $0xFE, %al + outb %al, $0x92 + + cli + lgdtl gdt_desc + movl %cr0, %eax + orl $1, %eax + movl %eax, %cr0 + ljmpl $CODE32_SEL, $pm_entry + +/* Second half of bios_int, entered from 16-bit protected mode */ +rm_thunk16: + movw $DATA16_SEL, %ax + movw %ax, %ds + movw %ax, %es + movw %ax, %fs + movw %ax, %gs + movw %ax, %ss + movl %cr0, %eax + andl $0xFFFFFFFE, %eax + movl %eax, %cr0 + ljmp $0, $rm_thunk_real +rm_thunk_real: + xorw %ax, %ax + movw %ax, %ds + movw %ax, %ss + movw $RM_STACK_TOP, %sp + lidtl rm_idt_desc + movl rm_regs + R_EBX, %ebx + movl rm_regs + R_ECX, %ecx + movl rm_regs + R_EDX, %edx + movl rm_regs + R_ESI, %esi + movl rm_regs + R_EDI, %edi + movl rm_regs + R_EBP, %ebp + movw rm_regs + R_ES, %es + movw %ax, %fs + movw %ax, %gs + pushw rm_regs + R_DS + movl rm_regs + R_EAX, %eax + popw %ds + sti + pushfw + lcallw *%cs:rm_vector + cli + movl %eax, %cs:rm_regs + R_EAX + movl %ebx, %cs:rm_regs + R_EBX + movl %ecx, %cs:rm_regs + R_ECX + movl %edx, %cs:rm_regs + R_EDX + movl %esi, %cs:rm_regs + R_ESI + movl %edi, %cs:rm_regs + R_EDI + movl %ebp, %cs:rm_regs + R_EBP + movw %ds, %cs:rm_regs + R_DS + movw %es, %cs:rm_regs + R_ES + pushfw + popw %cs:rm_regs + R_FLAGS + xorw %ax, %ax + movw %ax, %ds + lgdtl gdt_desc + movl %cr0, %eax + orl $1, %eax + movl %eax, %cr0 + ljmpl $CODE32_SEL, $pm_return + +/* Second half of bios_realmode_jump */ +rmj_thunk16: + movw $DATA16_SEL, %ax + movw %ax, %ds + movw %ax, %es + movw %ax, %fs + movw %ax, %gs + movw %ax, %ss + movl %cr0, %eax + andl $0xFFFFFFFE, %eax + movl %eax, %cr0 + ljmp $0, $rmj_real +rmj_real: + xorw %ax, %ax + movw %ax, %ds + movw %ax, %ss + movw $RM_STACK_TOP, %sp + lidtl rm_idt_desc + movw rm_jump + J_DX, %dx + movw rm_jump + J_SI, %si + movw rm_jump + J_CS, %cx + movw rm_jump + J_IP, %di + movw rm_jump + J_DS, %bx + movw rm_jump + J_SP, %bp + movw rm_jump + J_SS, %ax + movw %ax, %ss + movw %bp, %sp + pushw %cx + pushw %di + movw %bx, %ds + movw %bx, %es + movw %bx, %fs + movw %bx, %gs + xorl %eax, %eax + xorl %ebx, %ebx + xorl %ecx, %ecx + xorl %edi, %edi + xorl %ebp, %ebp + lretw + + .code32 + .text +pm_entry: + movw $DATA32_SEL, %ax + movw %ax, %ds + movw %ax, %es + movw %ax, %fs + movw %ax, %gs + movw %ax, %ss + movl $_stack_top, %esp + movl $_start_bss, %edi + movl $_end_bss, %ecx + subl %edi, %ecx + xorl %eax, %eax + rep stosb + call main +1: + hlt + jmp 1b + +/* void bios_int(uint8_t intno, struct bios_regs *regs) */ + .globl bios_int +bios_int: + pushl %ebp + pushl %ebx + pushl %esi + pushl %edi + movzbl 20(%esp), %eax + movl (,%eax,4), %eax + movl %eax, rm_vector + movl 24(%esp), %esi + movl $rm_regs, %edi + movl $BIOS_REGS_SIZE, %ecx + cld + rep movsb + movl %esp, saved_esp + ljmp $CODE16_SEL, $rm_thunk16 +pm_return: + movw $DATA32_SEL, %ax + movw %ax, %ds + movw %ax, %es + movw %ax, %fs + movw %ax, %gs + movw %ax, %ss + movl saved_esp, %esp + movl 24(%esp), %edi + movl $rm_regs, %esi + movl $BIOS_REGS_SIZE, %ecx + cld + rep movsb + popl %edi + popl %esi + popl %ebx + popl %ebp + ret + +/* void bios_realmode_jump(const struct bios_jump *j) */ + .globl bios_realmode_jump +bios_realmode_jump: + cli + movl 4(%esp), %esi + movl $rm_jump, %edi + movl $BIOS_JUMP_SIZE, %ecx + cld + rep movsb + ljmp $CODE16_SEL, $rmj_thunk16 + + .section .data16, "aw" + .p2align 3 +gdt: + .quad 0 + .quad 0x00CF9A000000FFFF /* 0x08: code, 32-bit, flat */ + .quad 0x00CF92000000FFFF /* 0x10: data, 32-bit, flat */ + .quad 0x00009A000000FFFF /* 0x18: code, 16-bit, 64 KB */ + .quad 0x000092000000FFFF /* 0x20: data, 16-bit, 64 KB */ +gdt_end: + +gdt_desc: + .word gdt_end - gdt - 1 + .long gdt + +rm_idt_desc: + .word 0x3FF + .long 0 + + .p2align 2 +rm_vector: + .long 0 +saved_esp: + .long 0 +rm_regs: + .space BIOS_REGS_SIZE +rm_jump: + .space BIOS_JUMP_SIZE + + .globl bios_boot_drive +bios_boot_drive: + .byte 0x80 + .globl bios_mbr_desc +bios_mbr_desc: + .space MBR_DESC_SIZE diff --git a/src/update_lilo.c b/src/update_lilo.c new file mode 100644 index 0000000000..1ecec1b373 --- /dev/null +++ b/src/update_lilo.c @@ -0,0 +1,584 @@ +/* update_lilo.c + * + * LILO-style boot for the x86_64_lilo target: a "boot:" prompt over the + * entries that tools/lilo wrote to the MBR gap, then a Linux real-mode boot + * or a chain-loaded partition boot sector. + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#ifdef WOLFBOOT_LILO + +#include +#include + +#include "image.h" +#include "loader.h" +#include "hal.h" +#include "printf.h" +#include "disk.h" +#include "disk_fs.h" +#include "gpt.h" +#include "lilo_map.h" +#include "wolfboot/wolfboot.h" +#include "x86/bios.h" +#include "x86/linux_loader.h" + +#define BOOT_DISK 0 + +/* Linux real-mode layout, as in the boot protocol's own example: setup at + * 0x90000, heap up to 0x9E000, command line right after. */ +#define RM_SEG 0x9000 +#define RM_BASE 0x90000UL +#define RM_HEAP_END 0xE000 +#define RM_CMDLINE (RM_BASE + RM_HEAP_END) +#define RM_CMDLINE_MAX 0x800 +#define RM_TOP (RM_CMDLINE + RM_CMDLINE_MAX) + +/* Protected-mode kernel. The signed image is first staged above the area + * the kernel is copied to, so the copy never overlaps its source. */ +#define KERNEL_HIGH 0x100000UL +#define STAGE_ALIGN 0x100000UL + +#define LINUX_HDR_MAGIC 0x53726448UL /* "HdrS" */ +#define LOADED_HIGH 0x01 +#define CAN_USE_HEAP 0x80 + +/* Chain loading: MBR copy and boot sector, where an MBR would put them */ +#define CHAIN_MBR 0x600UL +#define CHAIN_BOOT 0x7C00UL + +#define BIOS_TICKS_PER_DAY 0x1800B0UL +#define PROMPT_MAX 256 +#define LOAD_CHUNK 0x100000UL + +static uint8_t map_buf[LILO_MAP_SECTORS * LILO_SECTOR_SIZE]; +static const struct lilo_map_hdr *map; +static const struct lilo_map_entry *entries; +static uint32_t mem_top; +static uint32_t lowmem_top; +static struct fs_volume vol; +static struct fs_file file; + +static inline void *phys(uintptr_t addr) +{ + void *p = (void *)addr; + __asm__ ("" : "+r"(p)); + return p; +} + +static void put_str(const char *s) +{ + uart_write(s, (unsigned int)strlen(s)); +} + +static void put_chr(char c) +{ + uart_write(&c, 1); +} + +static int map_load(void) +{ + const struct lilo_mbr_desc *d = (const struct lilo_mbr_desc *)bios_mbr_desc; + struct lilo_map_hdr *h = (struct lilo_map_hdr *)map_buf; + struct lilo_map_entry *e; + struct gpt_crc32_ctx crc; + uint32_t want, len, i; + + if (d->magic != LILO_MBR_DESC_MAGIC || d->map_sectors == 0 || + d->map_sectors > LILO_MAP_SECTORS) { + return -1; + } + len = (uint32_t)d->map_sectors * LILO_SECTOR_SIZE; + if (disk_read(BOOT_DISK, d->map_lba * LILO_SECTOR_SIZE, len, map_buf) + != 0) { + return -1; + } + if (h->magic != LILO_MAP_MAGIC || h->version != LILO_MAP_VERSION || + h->entry_size != LILO_ENTRY_SIZE || h->n_entries == 0 || + h->n_entries > LILO_MAP_MAX_ENTRIES || + h->default_entry >= h->n_entries || + LILO_SECTOR_SIZE + (uint32_t)h->n_entries * LILO_ENTRY_SIZE > len) { + return -1; + } + len = LILO_SECTOR_SIZE + (uint32_t)h->n_entries * LILO_ENTRY_SIZE; + want = h->crc32; + h->crc32 = 0; + gpt_crc32_init(&crc); + gpt_crc32_update(&crc, map_buf, len); + h->crc32 = want; + if (gpt_crc32_final(&crc) != want) + return -1; + + /* The map is unauthenticated: never trust its strings to be bounded. */ + e = (struct lilo_map_entry *)(map_buf + LILO_SECTOR_SIZE); + for (i = 0; i < h->n_entries; i++) { + e[i].label[LILO_LABEL_LEN - 1] = '\0'; + e[i].alias[LILO_LABEL_LEN - 1] = '\0'; + e[i].kernel[LILO_PATH_LEN - 1] = '\0'; + e[i].initrd[LILO_PATH_LEN - 1] = '\0'; + e[i].cmdline[LILO_CMDLINE_LEN - 1] = '\0'; + } + map = h; + entries = e; + return 0; +} + +static char to_lower(char c) +{ + return (c >= 'A' && c <= 'Z') ? (char)(c + ('a' - 'A')) : c; +} + +static int name_eq(const char *name, const char *tok, uint32_t len) +{ + uint32_t i; + + if (name[0] == '\0') + return 0; + for (i = 0; i < len; i++) { + if (name[i] == '\0' || to_lower(name[i]) != to_lower(tok[i])) + return 0; + } + return name[len] == '\0'; +} + +static int find_entry(const char *tok, uint32_t len) +{ + uint32_t i; + + for (i = 0; i < map->n_entries; i++) { + if (name_eq(entries[i].label, tok, len) || + name_eq(entries[i].alias, tok, len)) { + return (int)i; + } + } + return -1; +} + +static void list_entries(void) +{ + uint32_t i, col; + + for (i = 0; i < map->n_entries; i++) { + put_str(entries[i].label); + col = (uint32_t)strlen(entries[i].label); + if (i == map->default_entry) { + put_chr('*'); + col++; + } + if ((i % 4) == 3 || i + 1 == map->n_entries) { + put_str("\n"); + continue; + } + while (col++ < 18) + put_chr(' '); + } +} + +static uint32_t ticks_since(uint32_t start) +{ + uint32_t now = bios_ticks(); + + if (now < start) + now += BIOS_TICKS_PER_DAY; /* midnight */ + return now - start; +} + +static uint32_t tenths_to_ticks(uint32_t tenths) +{ + return (tenths * 182U + 99U) / 100U; +} + +/* Returns the line length, or -1 if the timeout expired before the first + * key. Like LILO, any key cancels the timeout. */ +static int read_line(char *buf, uint32_t max, int has_timeout, + uint32_t timeout) +{ + uint32_t n = 0, i; + uint32_t start = bios_ticks(); + int c; + + for (;;) { + c = hal_console_getc(); + if (c < 0) { + if (has_timeout && ticks_since(start) >= timeout) + return -1; + continue; + } + has_timeout = 0; + if (c == '\r' || c == '\n') { + put_str("\n"); + buf[n] = '\0'; + return (int)n; + } + if (c == '\b' || c == 0x7F) { + if (n > 0) { + n--; + put_str("\b \b"); + } + continue; + } + if (c == 0x15) { /* ^U */ + while (n > 0) { + n--; + put_str("\b \b"); + } + continue; + } + if (c == '\t' || (c == '?' && n == 0)) { + put_str("\n"); + list_entries(); + put_str("boot: "); + for (i = 0; i < n; i++) + put_chr(buf[i]); + continue; + } + if (c < 0x20 || c > 0x7E || n + 1 >= max) + continue; + buf[n++] = (char)c; + put_chr((char)c); + } +} + +/* LILO without "prompt": a shift key (or any input) during the delay + * brings the prompt up instead of booting the default. */ +static int wait_shift(uint32_t tenths) +{ + uint32_t start = bios_ticks(); + uint32_t limit = tenths_to_ticks(tenths); + + do { + if (bios_shift_pressed()) + return 1; + if (hal_console_getc() >= 0) + return 1; + } while (ticks_since(start) < limit); + return 0; +} + +static int read_file(uint64_t off, uint32_t len, uint8_t *dst, int dots) +{ + uint32_t done = 0; + uint32_t chunk; + + while (done < len) { + chunk = len - done; + if (chunk > LOAD_CHUNK) + chunk = LOAD_CHUNK; + if (fs_read(&file, off + done, chunk, dst + done) != (int)chunk) + return -1; + done += chunk; + if (dots) + put_chr('.'); + } + return 0; +} + +static int boot_linux(const struct lilo_map_entry *e, const char *extra) +{ + struct wolfBoot_image img; + struct setup_header sh; + struct setup_header *rsh; + struct bios_jump j; + uint8_t *stage; + uint8_t *kernel; + char *cmd; + uint64_t fsize, limit, low; + uint32_t setup_size, pm_size, cmd_max, n, xlen, x; + uint32_t initrd_addr = 0, initrd_size = 0; + + if (fs_mount(&vol, BOOT_DISK, e->part) != WOLFBOOT_FS_OK || + vol.type != FS_TYPE_FAT32) { + wolfBoot_printf("\nPartition %d is not FAT32\n", e->part); + return -1; + } + if (mem_top <= KERNEL_HIGH + STAGE_ALIGN || + fs_open(&vol, &file, e->kernel, mem_top - KERNEL_HIGH) + != WOLFBOOT_FS_OK) { + wolfBoot_printf("\nCannot open %s\n", e->kernel); + return -1; + } + fsize = fs_size(&file); + if (fsize < (uint64_t)IMAGE_HEADER_SIZE + 0x400) { + wolfBoot_printf("\n%s: too small\n", e->kernel); + return -1; + } + stage = phys(KERNEL_HIGH + + (((uint32_t)fsize + STAGE_ALIGN - 1) & ~(STAGE_ALIGN - 1))); + if ((uint64_t)(uintptr_t)stage + fsize > mem_top) { + wolfBoot_printf("\n%s: does not fit in memory\n", e->kernel); + return -1; + } + + /* The header bounds what is read next, so read and parse it first. */ + if (read_file(0, IMAGE_HEADER_SIZE, stage, 0) != 0) { + wolfBoot_printf("\n%s: read error\n", e->kernel); + return -1; + } + memset(&img, 0, sizeof(img)); + put_str("\n"); + if (wolfBoot_open_image_address(&img, stage) < 0) { + wolfBoot_printf("%s: not a wolfBoot image\n", e->kernel); + return -1; + } + if ((uint64_t)img.fw_size + IMAGE_HEADER_SIZE > fsize || + img.fw_size < 0x1F1 + sizeof(sh)) { + wolfBoot_printf("%s: truncated image\n", e->kernel); + return -1; + } + if (read_file(IMAGE_HEADER_SIZE, img.fw_size, stage + IMAGE_HEADER_SIZE, + 1) != 0) { + wolfBoot_printf("\n%s: read error\n", e->kernel); + return -1; + } + put_str("\n"); + if (wolfBoot_verify_integrity(&img) < 0) { + wolfBoot_printf("%s: integrity check failed\n", e->kernel); + return -1; + } + if (wolfBoot_verify_authenticity(&img) < 0) { + wolfBoot_printf("%s: authenticity check failed\n", e->kernel); + return -1; + } + wolfBoot_printf("Verified %s, version %u\n", e->kernel, + (unsigned int)wolfBoot_get_blob_version(stage)); + + kernel = stage + IMAGE_HEADER_SIZE; + memcpy(&sh, kernel + 0x1F1, sizeof(sh)); + if (sh.header != LINUX_HDR_MAGIC || sh.version < 0x0204 || + (sh.loadflags & LOADED_HIGH) == 0) { + wolfBoot_printf("%s: not a bzImage (boot protocol 2.04+)\n", + e->kernel); + return -1; + } + setup_size = ((sh.setup_sects != 0) ? sh.setup_sects : 4U) + 1U; + setup_size *= 512U; + if (setup_size >= img.fw_size || setup_size > RM_HEAP_END - 0x200) { + wolfBoot_printf("%s: bad setup size\n", e->kernel); + return -1; + } + pm_size = img.fw_size - setup_size; + /* Where the kernel decompresses itself must exist. */ + if (sh.version >= 0x020A) { + low = (sh.pref_address > KERNEL_HIGH) ? sh.pref_address : KERNEL_HIGH; + if (low + sh.init_size > mem_top) { + wolfBoot_printf("%s: needs more memory\n", e->kernel); + return -1; + } + } + + cmd_max = RM_CMDLINE_MAX; + if (sh.version >= 0x0206 && sh.cmdline_size + 1U < cmd_max) + cmd_max = sh.cmdline_size + 1U; + n = (uint32_t)strlen(e->cmdline); + xlen = (uint32_t)strlen(extra); + if (n + 1U + xlen + 1U > cmd_max) { + wolfBoot_printf("Command line too long\n"); + return -1; + } + + /* Both copies leave the staging area: the source sits above + * KERNEL_HIGH + fsize, past the end of either destination. */ + memcpy(phys(RM_BASE), kernel, setup_size); + memcpy(phys(KERNEL_HIGH), kernel + setup_size, pm_size); + + if (e->initrd[0] != '\0') { + if (fs_open(&vol, &file, e->initrd, mem_top) != WOLFBOOT_FS_OK || + fs_size(&file) == 0) { + wolfBoot_printf("Cannot open %s\n", e->initrd); + return -1; + } + fsize = fs_size(&file); + limit = mem_top; + x = (sh.version >= 0x0203) ? sh.initrd_addr_max : 0x37FFFFFFU; + if ((uint64_t)x + 1U < limit) + limit = (uint64_t)x + 1U; + low = KERNEL_HIGH + pm_size; + if (sh.version >= 0x020A && + (uint64_t)sh.pref_address + sh.init_size > low) { + low = (uint64_t)sh.pref_address + sh.init_size; + } + if (fsize > limit || ((limit - fsize) & ~0xFFFULL) < low) { + wolfBoot_printf("%s: does not fit in memory\n", e->initrd); + return -1; + } + initrd_addr = (uint32_t)((limit - fsize) & ~0xFFFULL); + initrd_size = (uint32_t)fsize; + put_str("Loading initrd"); + if (read_file(0, initrd_size, phys(initrd_addr), 1) != 0) { + wolfBoot_printf("\n%s: read error\n", e->initrd); + return -1; + } + put_str("\n"); + } + + cmd = phys(RM_CMDLINE); + memcpy(cmd, e->cmdline, n); + if (xlen > 0) { + if (n > 0) + cmd[n++] = ' '; + memcpy(cmd + n, extra, xlen); + n += xlen; + } + cmd[n] = '\0'; + + rsh = phys(RM_BASE + 0x1F1); + rsh->type_of_loader = 0xFF; + rsh->loadflags |= CAN_USE_HEAP; + rsh->heap_end_ptr = RM_HEAP_END - 0x200; + rsh->cmd_line_ptr = RM_CMDLINE; + rsh->ramdisk_image = initrd_addr; + rsh->ramdisk_size = initrd_size; + if (e->vga != LILO_VGA_DEFAULT) + rsh->vid_mode = e->vga; + + wolfBoot_printf("Booting kernel: %s\n", cmd); + hal_prepare_boot(); + memset(&j, 0, sizeof(j)); + j.cs = RM_SEG + 0x20; + j.ip = 0; + j.ds = RM_SEG; + j.ss = RM_SEG; + j.sp = RM_HEAP_END; + bios_realmode_jump(&j); +} + +static int boot_other(const struct lilo_map_entry *e) +{ + static uint8_t mbr[LILO_SECTOR_SIZE]; + static uint8_t bs[LILO_SECTOR_SIZE]; + struct bios_jump j; + uint32_t pte_off, lba; + + put_str("\n"); + if (e->part < 1 || e->part > 4 || + disk_read(BOOT_DISK, 0, LILO_SECTOR_SIZE, mbr) != 0) { + wolfBoot_printf("Cannot read the partition table\n"); + return -1; + } + pte_off = LILO_MBR_PTABLE_OFFSET + 16U * (e->part - 1U); + lba = gpt_le32(mbr + pte_off + 8); + if (mbr[pte_off + 4] == 0 || lba == 0) { + wolfBoot_printf("Partition %d is empty\n", e->part); + return -1; + } + if (disk_read(BOOT_DISK, (uint64_t)lba * LILO_SECTOR_SIZE, + LILO_SECTOR_SIZE, bs) != 0) { + wolfBoot_printf("Cannot read partition %d\n", e->part); + return -1; + } + if (bs[510] != 0x55 || bs[511] != 0xAA) { + wolfBoot_printf("Partition %d has no boot sector\n", e->part); + return -1; + } + /* DS:SI points at the partition entry, marked active, as an MBR would + * leave it for the boot sector. */ + mbr[pte_off] = 0x80; + memcpy(phys(CHAIN_MBR), mbr, LILO_SECTOR_SIZE); + memcpy(phys(CHAIN_BOOT), bs, LILO_SECTOR_SIZE); + wolfBoot_printf("Chain loading partition %d\n", e->part); + hal_prepare_boot(); + memset(&j, 0, sizeof(j)); + j.cs = 0; + j.ip = CHAIN_BOOT; + j.ds = 0; + j.ss = 0; + j.sp = CHAIN_BOOT; + j.dx = bios_boot_drive; + j.si = (uint16_t)(CHAIN_MBR + pte_off); + bios_realmode_jump(&j); +} + +void RAMFUNCTION wolfBoot_start(void) +{ + static char line[PROMPT_MAX]; + const struct lilo_map_entry *e; + const char *extra; + uint32_t len; + int prompt, first = 1, idx, n; + + if (map_load() != 0) { + wolfBoot_printf("\nNo valid boot map: run tools/lilo\n"); + wolfBoot_panic(); + } + put_str("O\n"); /* LILO: map loaded */ + if (map->flags & LILO_MAP_SERIAL) + hal_console_serial(map->serial_port, map->serial_baud); + + mem_top = bios_mem_top(); + lowmem_top = bios_lowmem_top(); + wolfBoot_printf("wolfBoot: %u KB low, %u MB extended memory\n", + (unsigned int)(lowmem_top / 1024U), + (unsigned int)((mem_top - KERNEL_HIGH) >> 20)); + if (lowmem_top < RM_TOP) { + wolfBoot_printf("Not enough low memory for the kernel setup\n"); + wolfBoot_panic(); + } + if (disk_open(BOOT_DISK) < 0) { + wolfBoot_printf("Cannot read the boot disk\n"); + wolfBoot_panic(); + } + + prompt = (map->flags & LILO_MAP_PROMPT) || wait_shift(map->delay); + for (;;) { + idx = map->default_entry; + extra = ""; + if (prompt) { + put_str("boot: "); + n = read_line(line, sizeof(line), + first && map->timeout != LILO_TIMEOUT_NONE, + tenths_to_ticks(map->timeout)); + first = 0; + if (n < 0) { + put_str(entries[idx].label); + put_str("\n"); + } + else { + char *tok = line; + while (*tok == ' ') + tok++; + len = 0; + while (tok[len] != '\0' && tok[len] != ' ') + len++; + extra = tok + len; + while (*extra == ' ') + extra++; + if (len > 0) { + idx = find_entry(tok, len); + if (idx < 0) { + put_str("No such image. [Tab] shows a list.\n"); + continue; + } + } + } + } + e = &entries[idx]; + wolfBoot_printf("Loading %s", e->label); + if (e->type == LILO_ENTRY_LINUX) + (void)boot_linux(e, extra); + else if (e->type == LILO_ENTRY_OTHER) + (void)boot_other(e); + else + wolfBoot_printf("\nUnknown entry type %d\n", e->type); + /* Only back here on failure: fall back to the prompt. */ + prompt = 1; + } +} + +#endif /* WOLFBOOT_LILO */ diff --git a/src/x86/bios.c b/src/x86/bios.c new file mode 100644 index 0000000000..e00bbfc580 --- /dev/null +++ b/src/x86/bios.c @@ -0,0 +1,181 @@ +/* bios.c + * + * BIOS services for the x86_64_lilo target: disk, keyboard, video, memory. + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#include +#include + +#include "x86/bios.h" + +/* BIOS Data Area */ +#define BDA_KBD_FLAGS 0x417 +#define BDA_TICKS 0x46C + +/* Keep GCC from treating fixed low addresses as null-page accesses. */ +static inline volatile void *phys(uintptr_t addr) +{ + volatile void *p = (volatile void *)addr; + __asm__ ("" : "+r"(p)); + return p; +} + +int bios_disk_read(uint8_t drive, uint64_t lba, uint16_t count, void *buf) +{ + struct bios_dap { + uint8_t size; + uint8_t zero; + uint16_t count; + uint16_t off; + uint16_t seg; + uint64_t lba; + } __attribute__((packed)); + static struct bios_dap dap; + struct bios_regs r; + int tries; + + if (((uintptr_t)buf + (uint32_t)count * 512U) > BIOS_LOWMEM_LIMIT) + return -1; + /* Floppy-era BIOSes reset the drive and retry; so does LILO. */ + for (tries = 0; tries < 3; tries++) { + dap.size = sizeof(dap); + dap.zero = 0; + dap.count = count; + dap.off = BIOS_OFF(buf); + dap.seg = BIOS_SEG(buf); + dap.lba = lba; + memset(&r, 0, sizeof(r)); + r.eax = 0x4200; + r.edx = drive; + r.esi = BIOS_OFF(&dap); + r.ds = BIOS_SEG(&dap); + bios_int(0x13, &r); + if ((r.flags & BIOS_FLAG_CF) == 0 && dap.count == count) + return 0; + memset(&r, 0, sizeof(r)); + r.edx = drive; + bios_int(0x13, &r); + } + return -1; +} + +void bios_putc(char c) +{ + struct bios_regs r; + + memset(&r, 0, sizeof(r)); + r.eax = 0x0E00 | (uint8_t)c; + r.ebx = 0x0007; + bios_int(0x10, &r); +} + +int bios_getkey(void) +{ + struct bios_regs r; + + memset(&r, 0, sizeof(r)); + r.eax = 0x0100; + bios_int(0x16, &r); + if (r.flags & BIOS_FLAG_ZF) + return -1; + memset(&r, 0, sizeof(r)); + bios_int(0x16, &r); + return (int)(r.eax & 0xFF); +} + +/* Shift, Ctrl, Alt, Scroll Lock or Caps Lock: what LILO checks for. */ +int bios_shift_pressed(void) +{ + return (*(volatile uint8_t *)phys(BDA_KBD_FLAGS) & 0x5F) != 0; +} + +uint32_t bios_ticks(void) +{ + return *(volatile uint32_t *)phys(BDA_TICKS); +} + +/* Low memory in bytes (INT 12h), the ceiling for real-mode payloads. */ +uint32_t bios_lowmem_top(void) +{ + struct bios_regs r; + + memset(&r, 0, sizeof(r)); + bios_int(0x12, &r); + return (r.eax & 0xFFFF) * 1024U; +} + +/* End of the usable RAM range that starts at 1 MB, capped below 4 GB. */ +uint32_t bios_mem_top(void) +{ + struct e820_entry { + uint64_t base; + uint64_t len; + uint32_t type; + uint32_t ext; + } __attribute__((packed)); + static struct e820_entry e; + struct bios_regs r; + uint32_t cont = 0; + uint64_t top = 0; + int i; + + for (i = 0; i < 128; i++) { + memset(&r, 0, sizeof(r)); + memset(&e, 0, sizeof(e)); + e.ext = 1; + r.eax = 0xE820; + r.edx = 0x534D4150; /* "SMAP" */ + r.ecx = sizeof(e); + r.ebx = cont; + r.edi = BIOS_OFF(&e); + r.es = BIOS_SEG(&e); + bios_int(0x15, &r); + if ((r.flags & BIOS_FLAG_CF) || r.eax != 0x534D4150) + break; + if (e.type == 1 && (r.ecx < 24 || (e.ext & 1)) && + e.base <= 0x100000ULL && e.base + e.len > 0x100000ULL) + top = e.base + e.len; + cont = r.ebx; + if (cont == 0) + break; + } + if (top == 0) { + /* No E820: E801 reports KB up to 16 MB and 64 KB blocks above */ + memset(&r, 0, sizeof(r)); + r.eax = 0xE801; + bios_int(0x15, &r); + if ((r.flags & BIOS_FLAG_CF) == 0) { + uint32_t kb_low = r.eax & 0xFFFF; + uint32_t blk_high = r.ebx & 0xFFFF; + if (kb_low == 0 && blk_high == 0) { + kb_low = r.ecx & 0xFFFF; + blk_high = r.edx & 0xFFFF; + } + if (kb_low >= 15 * 1024 && blk_high != 0) + top = 0x1000000ULL + (uint64_t)blk_high * 0x10000ULL; + else + top = 0x100000ULL + (uint64_t)kb_low * 1024ULL; + } + } + if (top > 0xFFFFF000ULL) + top = 0xFFFFF000ULL; + return (uint32_t)top; +} diff --git a/tools/lilo/Makefile b/tools/lilo/Makefile new file mode 100644 index 0000000000..34926da392 --- /dev/null +++ b/tools/lilo/Makefile @@ -0,0 +1,21 @@ +# wolfBoot lilo: installs the x86_64_lilo target on a legacy MBR disk. +# +# make build lilo +# make clean + +CC ?= gcc +CFLAGS += -I. -I../../src -I../../include -Wall -Wextra -O2 -g +CFLAGS += -D_FILE_OFFSET_BITS=64 +CFLAGS += -DWOLFBOOT_DISK_FS -DWOLFBOOT_FAT32 -DWOLFBOOT_NO_PRINTF +CFLAGS += -DWOLFSSL_USER_SETTINGS + +all: lilo + +lilo: lilo.c ../../include/lilo_map.h ../../src/fat32.c ../../src/disk_fs.c \ + ../../src/disk.c ../../src/gpt.c + $(CC) -o $@ lilo.c $(CFLAGS) + +clean: + rm -f lilo + +.PHONY: all clean diff --git a/tools/lilo/README.md b/tools/lilo/README.md new file mode 100644 index 0000000000..0ea84ea632 --- /dev/null +++ b/tools/lilo/README.md @@ -0,0 +1,94 @@ +# lilo for wolfBoot (x86_64_lilo) + +Installs the `x86_64_lilo` wolfBoot target on a legacy BIOS/MBR disk from a +LILO configuration file. See +[docs/Targets.md](../../docs/Targets.md#x86-64-legacy-biosmbr-lilo-style-x86_64_lilo) +for the boot flow and the QEMU test. + +``` +make -C tools/lilo +tools/lilo/lilo [-C config] [-b boot_device] [-i wolfboot.bin] [-D label] + [-s save_file] [-t] [-v] +tools/lilo/lilo -q [-C config] [-b boot_device] [-v] +tools/lilo/lilo -u save_file [-b boot_device] [-t] +``` + +| Option | Meaning | +|------------|-----------------------------------------------------------------| +| `-C file` | configuration file, default `/etc/lilo.conf` | +| `-b dev` | boot device or disk image, overrides `boot=` | +| `-i file` | wolfBoot binary, overrides `install=` (default `/boot/wolfboot.bin`) | +| `-D label` | default image, overrides `default=` | +| `-s file` | save the sectors about to be overwritten | +| `-t` | test: check everything, write nothing | +| `-q` | list the images in the installed map (`-v` for details) | +| `-u file` | restore the boot code saved with `-s`, keeping the partition table | +| `-v` | verbose | +| `-V` | version | + +`lilo` writes the MBR boot code (keeping the disk signature and the partition +table), the map at LBA 1 and wolfBoot at LBA 34. All of it has to fit in front +of the first partition. GPT disks are refused. + +## lilo.conf + +The syntax is lilo.conf(5): `keyword=value` or a bare `keyword`, values with +blanks in double quotes, `#` comments. Options before the first `image=` or +`other=` are global. + +Global options: + +| Keyword | Meaning | +|------------------|-----------------------------------------------------------| +| `boot=` | whole-disk device or image the MBR is written to | +| `install=` | wolfBoot binary (`wolfboot.bin` of an `x86_64_lilo` build) | +| `default=` | label booted by default (otherwise the first image) | +| `prompt` | always show the `boot:` prompt | +| `timeout=` | tenths of a second at the prompt before booting the default; none means wait | +| `delay=` | without `prompt`: tenths of a second to wait for Shift, Ctrl, Alt, Caps or Scroll Lock (or a serial character) to bring the prompt up | +| `serial=` | `port[,bps[N8]]`: port 0-3 (COM1-COM4), also accept input from it | +| `append=`, `addappend=`, `root=`, `read-only`, `read-write`, `vga=` | defaults for the images | +| `fat-partition=` | (wolfBoot) MBR slot 1-4 holding the FAT32 boot partition; default: the first FAT32 partition | +| `fat-mount=` | (wolfBoot) where that partition is mounted, default `/boot`. Image paths under it are looked up relative to the partition root | + +Per image (`image=`, a wolfBoot image of a bzImage on the FAT32 +partition): + +| Keyword | Meaning | +|--------------|----------------------------------------------------------------| +| `label=` | name typed at the prompt, at most 15 characters (default: the file name) | +| `alias=` | second name | +| `initrd=` | initial ramdisk on the same partition | +| `append=` | kernel parameters, replacing the global `append=` | +| `addappend=` | kernel parameters added after `append=` | +| `literal=` | the only kernel parameters, ignoring `root=`, `ro`/`rw` and `append=` | +| `root=`, `read-only`, `read-write`, `vga=` | as in LILO: `vga=normal\|extended\|ask\|` | +| `optional` | skip the image, with a warning, if its files are missing | + +The command line is built as LILO did: `BOOT_IMAGE=