From 9dbdb15abf8ffb9dbdd972d6bbbcea9a3591e2a3 Mon Sep 17 00:00:00 2001 From: Bradley Morgan Date: Sat, 3 Oct 2026 20:02:29 +0000 Subject: tashaboot: arm64 bootloader A small arm64 bootloader. No board code, no device tree porting, the architecture manual is the whole story: exception vectors in the fixed 16 slot layout (Table D1-7), ESR_ELx decoded by exception class (D1-2172), EL entry and eret chains per the programmers model (D1-2146), cache maintenance by set/way over the CLIDR_EL1 levels, semihosting for console and file io per DUI 0203, and the A64 boot protocol from Documentation/arch/arm64/booting.rst. The loader boots a stock mainline Image end to end on the qemu virt machine. Boot receipt with 7.3-rc3 (42MB Image): tashaboot 0.1 loaded 43450368 bytes at 40200000, entry 40200000 jumping [ 0.000000] Booting Linux on physical CPU 0x0000000000 [0x411fd070] [ 0.000000] Linux version 7.3.0-rc3 [ 0.000000] Machine model: linux,dummy-virt [ 0.000000] earlycon: pl11 MMIO32:0x0000000009000000 ... ---[ end Kernel panic - not syncing: VFS: Unable to mount root fs ]--- The panic is the expected end state, no root filesystem is handed over yet. The boot chain, state per stage, start to payload: +-----------+-----+--------------+----------------------------------+ | stage | EL | state | work | +-----------+-----+--------------+----------------------------------+ | firmware | any | MMU maybe on | x0 = dtb, jump in | +-----------+-----+--------------+----------------------------------+ | tashaboot | 3-2 | | SCR_EL3.NS = 1, eret to EL2 | +-----------+-----+--------------+----------------------------------+ | | 2 | virt scrub | HCR/CNTHCTL/CPTR/HSTR, CNTFRQ, | | | | | VBAR_EL2, MMU off, tlbi alle2 | +-----------+-----+--------------+----------------------------------+ | | 2 | | load Image over semihosting, | | | | | validate header, place per | | | | | booting.rst | +-----------+-----+--------------+----------------------------------+ | | 2 | caches clean | flush dcache, inval icache, | | | | | args ride x20/x21, regs last | +-----------+-----+--------------+----------------------------------+ | payload | 2 | fresh start | x0 = dtb, x1-x3 = 0, DAIF | | | | | masked, br to image entry | +-----------+-----+--------------+----------------------------------+ Two handoff bugs the kernel caught, both AAPCS clobbers in the final jump. Cache maintenance was called after the register setup, x0-x18 are caller saved, so tb_flush_dcache_all() wiped the dtb pointer and the kernel spun in setup_machine_fdt() with an invalid device tree blob. The flush helpers also clobbered x1 (u-boot's void call convention left mov x1, x0 in cache.S) which handed the kernel a wild x0. The arguments ride in x20/x21 across the cache calls now, callee saved, and the register setup is the last thing before the branch. What is missing on purpose: no SMP bringup (secondary cores park), no PSCI, no initrd or root filesystem handoff, single serial console. Those come next. Signed-off-by: Bradley Morgan --- common/console.c | 74 +++++++++++++++++++++++++++++++++++++++++++++++++++++ common/image.c | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ common/load.c | 57 +++++++++++++++++++++++++++++++++++++++++ common/main.c | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 283 insertions(+) create mode 100644 common/console.c create mode 100644 common/image.c create mode 100644 common/load.c create mode 100644 common/main.c (limited to 'common') diff --git a/common/console.c b/common/console.c new file mode 100644 index 0000000..a7108ba --- /dev/null +++ b/common/console.c @@ -0,0 +1,74 @@ +/* + * console.c - the console dprintf writes to. semihosting SYS_WRITE0, + * the firmware service on the qemu dev path, the arm64 stand-in for + * the bios teletype the osdev loaders use. on real hardware this is + * the one file that changes. + * + * Copyright (c) 2026 Bradley Morgan + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files + * (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include + +/* + * lk's _dprintf sink. printf buffers a line here then hands it to the + * host, semihosting wants zero terminated strings not counts. + */ +#define TB_CONSOLE_MAX 256 + +static char console_buf[TB_CONSOLE_MAX]; +static size_t console_len; + +static void console_flush(void) +{ + if (console_len == 0) + return; + console_buf[console_len] = '\0'; + smh_write0(console_buf); + console_len = 0; +} + +void _putchar(char c) +{ + if (console_len >= TB_CONSOLE_MAX - 1) + console_flush(); + if (c == '\n') { + /* the host terminal wants cr lf, not lf alone */ + console_buf[console_len++] = '\r'; + } + console_buf[console_len++] = c; + if (c == '\n') + console_flush(); +} + +int tb_console_init(void) +{ + /* + * the probe is one harmless call: SYS_GET_ERRNO with no file + * handle open. a host answers, bare metal ignores the trap. + */ + if (!smh_probe()) + return -1; + console_len = 0; + return 0; +} diff --git a/common/image.c b/common/image.c new file mode 100644 index 0000000..640eab4 --- /dev/null +++ b/common/image.c @@ -0,0 +1,77 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * image.c - arm64 kernel Image validation and placement. + * + * The relocation rules are the ones from the kernel boot protocol, + * including the pre a2c1d73b94ed quirks, same math u-boot's + * booti_setup() runs. + * + * Copyright (C) 2026 Bradley Morgan + */ + +#include +#include +#include +#include + +#define LINUX_ARM64_IMAGE_MAGIC 0x644d5241 /* "ARM\x64" */ + +#define SZ_16M 0x01000000 +#define SZ_2M 0x00200000 + +/* the 64 byte header from Documentation/arch/arm64/booting.rst */ +struct Image_header { + uint32 code0; /* executable */ + uint32 code1; /* unused */ + uint64 text_offset; /* load offset, LE */ + uint64 image_size; /* size, LE */ + uint64 flags; /* bit 3: relocatable */ + uint64 res1; + uint64 res2; + uint64 res3; + uint32 magic; /* "ARM\x64" */ + uint32 res4; +}; + +int tb_image_setup(uintptr_t image, struct tb_image *img) +{ + const struct Image_header *ih = (const struct Image_header *)image; + uint64_t image_size, text_offset; + + if (le32_to_cpu(ih->magic) != LINUX_ARM64_IMAGE_MAGIC) + return -1; + + if (le64_to_cpu(ih->image_size) == 0) { + /* ancient image, no size field, assume the old defaults */ + image_size = SZ_16M; + text_offset = 0x80000; + } else { + image_size = le64_to_cpu(ih->image_size); + text_offset = le64_to_cpu(ih->text_offset); + } + + /* + * flag bit 3 says the image can live anywhere, honour where it + * already is. otherwise the base must be 2MB aligned, the + * physical offset from there is text_offset. + */ + if (le64_to_cpu(ih->flags) & (1ULL << 3)) { + uintptr_t base = image - text_offset; + + img->load = ((base + SZ_2M - 1) & ~(uintptr_t)(SZ_2M - 1)) + + text_offset; + } else { + /* + * no relocate flag: the image must sit text_offset from a + * 2MB aligned base. it is already staged at the fixed + * address, treat its own position as the answer. + */ + img->load = image; + } + + /* the whole image, header included, lives at load, entry is code0 */ + img->ep = img->load; + img->size = image_size; + + return 0; +} diff --git a/common/load.c b/common/load.c new file mode 100644 index 0000000..cb3b41d --- /dev/null +++ b/common/load.c @@ -0,0 +1,57 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * load.c - pull the payload into RAM. semihosting is the whole story + * for now, the bios INT 13h of this loader: the host serves the file, + * we read it at the fixed address and let the image header place it. + * + * Copyright (C) 2026 Bradley Morgan + */ + +#include +#include +#include +#include +#include +#include + +int tb_load_semihosting(const char *fname, uintptr_t load_addr, + struct tb_image *img) +{ + long fd, len, ret; + + fd = smh_open(fname, MODE_READ | MODE_BINARY); + if (fd < 0) + return fd; + + len = smh_flen(fd); + if (len < 0) { + smh_close(fd); + return len; + } + + /* header first so placement is known before the big copy */ + ret = smh_read(fd, (void *)load_addr, 64); + if (ret != 64) { + smh_close(fd); + return -3; + } + + if (tb_image_setup(load_addr, img)) { + smh_close(fd); + return -4; + } + + if (img->load != load_addr) { + /* the image wants to sit elsewhere, copy the header there */ + memmove((void *)img->load, (void *)load_addr, 64); + } + + ret = smh_read(fd, (void *)(img->load + 64), len - 64); + if (ret != len - 64) { + smh_close(fd); + return -5; + } + + smh_close(fd); + return 0; +} diff --git a/common/main.c b/common/main.c new file mode 100644 index 0000000..fb388cb --- /dev/null +++ b/common/main.c @@ -0,0 +1,75 @@ +/* + * main.c - the C entry. console up first, then load the payload and + * jump. called from start.S with x0 = whatever the firmware passed. + * + * the osdev model, arm64: the firmware services do the work, the + * kernel goes at a fixed known address, whatever x0 we were handed + * goes straight through to the payload. + * + * Copyright (c) 2026 Bradley Morgan + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files + * (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include +#include + +#define TB_VERSION "0.1" + +extern int tb_console_init(void); + +/* + * fixed load address, the osdev way. past the bootloader at the + * bottom of RAM, the image header decides its final resting place. + */ +#define TB_LOAD_ADDR 0x40200000 + +/* the file semihosting serves as the payload */ +#define TB_BOOTFILE "Image" + +extern void __NO_RETURN tb_boot_linux(uintptr_t ep, uintptr_t fw_arg); + +void tashaboot_main(uintptr_t fw_arg) +{ + struct tb_image img; + int ret; + + if (tb_console_init()) + return; + + dprintf(ALWAYS, "tashaboot " TB_VERSION "\n"); + + ret = tb_load_semihosting(TB_BOOTFILE, TB_LOAD_ADDR, &img); + if (ret) { + dprintf(ALWAYS, "load failed (%d), halting\n", ret); + platform_halt(); + } + + dprintf(ALWAYS, "loaded %llu bytes at %lx, entry %lx\n", + (unsigned long long)img.size, img.load, img.ep); + dprintf(ALWAYS, "jumping\n"); + + tb_boot_linux(img.ep, fw_arg); +} -- cgit v1.2.3