boot2

Playing with the boostrap
git clone https://git.ryansepassi.com/git/boot2.git
Log | Files | Refs

commit 4706fe9e699582868f1caf3d64e3a68bd040afea
parent 04e5c663480809c351cc3e9668c4b72900aa4d89
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Mon, 20 Apr 2026 12:52:32 -0700

Add P1 portable pseudo-ISA, port hello.M1 to three arches

hello.M1 is now written in P1 mnemonics and assembles unchanged for
aarch64, amd64, and riscv64; the per-arch p1_<arch>.M1 defs file is
the only source that varies. Spike subset only — LI and SYSCALL —
which is all the hello demo needs; full P1 ISA per P1.md is deferred.

Makefile gains an ARCH variable (default aarch64) and a run-all
target; foreign-arch hello binaries run via podman --platform plus
binfmt/qemu-user.

Diffstat:
AELF-amd64.hex2 | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
AELF-riscv64.hex2 | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MMakefile | 95++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Mhello.M1 | 41+++++++++++++++++++++++------------------
Ap1_aarch64.M1 | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ap1_amd64.M1 | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Ap1_riscv64.M1 | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 411 insertions(+), 48 deletions(-)

diff --git a/ELF-amd64.hex2 b/ELF-amd64.hex2 @@ -0,0 +1,74 @@ +### Copyright (C) 2016 Jeremiah Orians +### Copyright (C) 2017 Jan Nieuwenhuizen <janneke@gnu.org> +### This file is part of M2-Planet. +### +### M2-Planet 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. +### +### M2-Planet 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 M2-Planet. If not, see <http://www.gnu.org/licenses/>. + +### stage0's hex2 format +### !<label> 1 byte relative +### $<label> 2 byte address +### @<label> 2 byte relative +### &<label> 4 byte address +### %<label> 4 byte relative + +### if you wish to use this header, you need to add :ELF_end to the end of your +### M1 or hex2 files. + +## ELF Header + +:ELF_base +7F 45 4C 46 # e_ident[EI_MAG0-3] ELF's magic number + +02 # e_ident[EI_CLASS] Indicating 64 bit +01 # e_ident[EI_DATA] Indicating little endianness +01 # e_ident[EI_VERSION] Indicating original elf + +03 # e_ident[EI_OSABI] Set at 3 because FreeBSD is strict +00 # e_ident[EI_ABIVERSION] See above + +00 00 00 00 00 00 00 # e_ident[EI_PAD] + +02 00 # e_type Indicating Executable +3E 00 # e_machine Indicating AMD64 +01 00 00 00 # e_version Indicating original elf + +&_start 00 00 00 00 # e_entry Address of the entry point +%ELF_program_headers>ELF_base 00 00 00 00 # e_phoff Address of program header table +00 00 00 00 00 00 00 00 # e_shoff Address of section header table + +00 00 00 00 # e_flags + +40 00 # e_ehsize Indicating our 64 Byte header + +38 00 # e_phentsize size of a program header table +01 00 # e_phnum number of entries in program table + +00 00 # e_shentsize size of a section header table +00 00 # e_shnum number of entries in section table + +00 00 # e_shstrndx index of the section names + + +:ELF_program_headers +:ELF_program_header__text +01 00 00 00 # ph_type: PT-LOAD = 1 +07 00 00 00 # ph_flags: PF-X|PF-W|PF-R = 7 +00 00 00 00 00 00 00 00 # ph_offset +&ELF_base 00 00 00 00 # ph_vaddr +&ELF_base 00 00 00 00 # ph_physaddr +%ELF_end>ELF_base 00 00 00 00 # ph_filesz +%ELF_end>ELF_base 00 00 00 00 # ph_memsz +01 00 00 00 00 00 00 00 # ph_align + +:ELF_text diff --git a/ELF-riscv64.hex2 b/ELF-riscv64.hex2 @@ -0,0 +1,74 @@ +### Copyright (C) 2016 Jeremiah Orians +### Copyright (C) 2017 Jan Nieuwenhuizen <janneke@gnu.org> +### This file is part of M2-Planet. +### +### M2-Planet 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. +### +### M2-Planet 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 M2-Planet. If not, see <http://www.gnu.org/licenses/>. + +### stage0's hex2 format +### !<label> 1 byte relative +### $<label> 2 byte address +### @<label> 2 byte relative +### &<label> 4 byte address +### %<label> 4 byte relative + +### if you wish to use this header, you need to add :ELF_end to the end of your +### M1 or hex2 files. + +## ELF Header + +:ELF_base +7F 45 4C 46 # e_ident[EI_MAG0-3] ELF's magic number + +02 # e_ident[EI_CLASS] Indicating 64 bit +01 # e_ident[EI_DATA] Indicating little endianness +01 # e_ident[EI_VERSION] Indicating original elf + +03 # e_ident[EI_OSABI] Set at 3 because FreeBSD is strict +00 # e_ident[EI_ABIVERSION] See above + +00 00 00 00 00 00 00 # e_ident[EI_PAD] + +02 00 # e_type Indicating Executable +F3 00 # e_machine Indicating RISC-V +01 00 00 00 # e_version Indicating original elf + +&_start 00 00 00 00 # e_entry Address of the entry point +%ELF_program_headers>ELF_base 00 00 00 00 # e_phoff Address of program header table +00 00 00 00 00 00 00 00 # e_shoff Address of section header table + +00 00 00 00 # e_flags + +40 00 # e_ehsize Indicating our 64 Byte header + +38 00 # e_phentsize size of a program header table +01 00 # e_phnum number of entries in program table + +00 00 # e_shentsize size of a section header table +00 00 # e_shnum number of entries in section table + +00 00 # e_shstrndx index of the section names + + +:ELF_program_headers +:ELF_program_header__text +01 00 00 00 # ph_type: PT-LOAD = 1 +07 00 00 00 # ph_flags: PF-X|PF-W|PF-R = 7 +00 00 00 00 00 00 00 00 # ph_offset +&ELF_base 00 00 00 00 # ph_vaddr +&ELF_base 00 00 00 00 # ph_physaddr +%ELF_end>ELF_base 00 00 00 00 # ph_filesz +%ELF_end>ELF_base 00 00 00 00 # ph_memsz +01 00 00 00 00 00 00 00 # ph_align + +:ELF_text diff --git a/Makefile b/Makefile @@ -1,32 +1,62 @@ -# lispcc — alternative bootstrap path: aarch64 Lisp-in-M1 → C compiler in -# Lisp → tcc-boot. See PLAN.md. +# lispcc — P1 portable pseudo-ISA demo. +# +# hello.M1 is written in P1 mnemonics and assembles unchanged for all +# three targets. The backing defs file (p1_<arch>.M1) is the only per- +# arch source. See ../P1.md. # # Two-image setup: -# - lispcc-builder (alpine + gcc): builds M1 and hex2 statically -# - alpine:latest (pristine): runs M1, hex2, and the assembled output +# - lispcc-builder (alpine + gcc, host-arch): builds M1 and hex2 +# statically. These tools run natively regardless of ARCH. +# - alpine:latest (pristine, per-arch): runs the assembled hello under +# the target's Linux ABI. Non-native targets go through podman's +# binfmt + qemu-user path. # # Usage: -# make image Build the builder image (one-time, idempotent) -# make Build toolchain + assemble hello.M1 → build/hello -# make run Run build/hello in pristine alpine -# make clean Remove build/ artifacts +# make image Build the builder image (one-time, idempotent) +# make Build + assemble for ARCH (default aarch64) +# make ARCH=amd64 Same, targeting amd64 +# make ARCH=riscv64 Same, targeting riscv64 +# make run Run the assembled hello for $(ARCH) under alpine +# make run-all Build + run on all three arches +# make clean Remove build/ artifacts # --- Configuration --------------------------------------------------------- +ARCH ?= aarch64 + +# Map P1 ARCH -> Linux-platform tag for the runtime container. +PLATFORM_aarch64 := linux/arm64 +PLATFORM_amd64 := linux/amd64 +PLATFORM_riscv64 := linux/riscv64 +PLATFORM := $(PLATFORM_$(ARCH)) +ifeq ($(PLATFORM),) + $(error ARCH '$(ARCH)' not supported — use aarch64, amd64, or riscv64) +endif + HOST_ROOT := $(abspath $(CURDIR)/..) TOOLCHAIN_SRC := /work/live-bootstrap/seed/stage0-posix/mescc-tools BUILDER_IMAGE := lispcc-builder:latest RUNTIME_IMAGE := docker.io/library/alpine:latest -# Builder: alpine + gcc, mounts the bootstrap-explore parent dir at /work -# so we can reach upstream mescc-tools C source. +OUT_DIR := build/$(ARCH) + +# Builder: alpine + gcc at host arch, mounts the bootstrap-explore parent +# dir at /work so we can reach upstream mescc-tools C source. PODMAN_BUILD := podman run --rm \ -v $(HOST_ROOT):/work \ -w /work/lispcc \ $(BUILDER_IMAGE) -# Runtime: pristine alpine, mounts only this project dir at /work. -PODMAN_RUN := podman run --rm \ +# Native alpine — used to run the static M1/hex2 binaries (which are +# built for host arch and don't care about P1 target arch). +PODMAN_RUN_NATIVE := podman run --rm \ + -v $(CURDIR):/work \ + -w /work \ + $(RUNTIME_IMAGE) + +# Target alpine — used to run the generated hello binary on its own +# Linux ABI. Foreign arches transparently use binfmt + qemu-user. +PODMAN_RUN_TARGET := podman run --rm --platform $(PLATFORM) \ -v $(CURDIR):/work \ -w /work \ $(RUNTIME_IMAGE) @@ -36,17 +66,17 @@ CFLAGS := -D_GNU_SOURCE -std=c99 -ggdb -fno-common -static # --- Targets --------------------------------------------------------------- -.PHONY: all image toolchain run clean +.PHONY: all image toolchain run run-all clean -all: build/hello +all: $(OUT_DIR)/hello image: podman build -t $(BUILDER_IMAGE) . toolchain: build/M1 build/hex2 -build: - mkdir -p build +build $(OUT_DIR): + mkdir -p $@ build/M1: | build $(PODMAN_BUILD) gcc $(CFLAGS) \ @@ -63,23 +93,28 @@ build/hex2: | build $(TOOLCHAIN_SRC)/M2libc/bootstrappable.c \ -o build/hex2 -build/hello.hex2: hello.M1 aarch64_defs.M1 build/M1 - $(PODMAN_RUN) ./build/M1 \ - -f aarch64_defs.M1 \ +$(OUT_DIR)/hello.hex2: hello.M1 p1_$(ARCH).M1 build/M1 | $(OUT_DIR) + $(PODMAN_RUN_NATIVE) ./build/M1 \ + -f p1_$(ARCH).M1 \ -f hello.M1 \ - --little-endian --architecture aarch64 \ - -o build/hello.hex2 - -build/hello: build/hello.hex2 ELF-aarch64.hex2 build/hex2 - $(PODMAN_RUN) ./build/hex2 \ - -f ELF-aarch64.hex2 \ - -f build/hello.hex2 \ - --little-endian --architecture aarch64 \ + --little-endian --architecture $(ARCH) \ + -o $(OUT_DIR)/hello.hex2 + +$(OUT_DIR)/hello: $(OUT_DIR)/hello.hex2 ELF-$(ARCH).hex2 build/hex2 + $(PODMAN_RUN_NATIVE) ./build/hex2 \ + -f ELF-$(ARCH).hex2 \ + -f $(OUT_DIR)/hello.hex2 \ + --little-endian --architecture $(ARCH) \ --base-address 0x400000 \ - -o build/hello + -o $(OUT_DIR)/hello + +run: $(OUT_DIR)/hello + $(PODMAN_RUN_TARGET) ./$(OUT_DIR)/hello -run: build/hello - $(PODMAN_RUN) ./build/hello +run-all: + $(MAKE) --no-print-directory ARCH=aarch64 run + $(MAKE) --no-print-directory ARCH=amd64 run + $(MAKE) --no-print-directory ARCH=riscv64 run clean: rm -rf build/ diff --git a/hello.M1 b/hello.M1 @@ -1,24 +1,29 @@ -## aarch64 Linux hello world in M1 +## P1 "hello, world" — portable pseudo-ISA source. +## +## Same program on every P1 target; only the backing defs file swaps +## (see p1_aarch64.M1, and eventually p1_amd64.M1 / p1_riscv64.M1). +## +## Linux calling convention on P1: +## SYSCALL num -> r0, args -> r1..r6, result -> r0 :_start - ;; write(1, msg, 14) - SET_X0_TO_1 ; x0 = 1 (stdout) + ## write(fd=1, buf=&msg, count=14) + P1_LI_R0 + SYS_WRITE # r0 = syscall number (write) + P1_LI_R1 + '01000000' # r1 = fd (stdout) + P1_LI_R2 + &msg # r2 = buf + P1_LI_R3 + '0E000000' # r3 = count (14) + P1_SYSCALL - LOAD_W1_AHEAD ; ldr w1, [pc+8] - SKIP_32_DATA ; b +8 (over inline data) - &msg ; <-- 4-byte msg address loaded into w1/x1 - - LOAD_W2_AHEAD ; ldr w2, [pc+8] - SKIP_32_DATA - '0E000000' ; <-- length 14, little-endian, loaded into w2/x2 - - SET_X8_TO_SYS_WRITE ; x8 = 64 - SYSCALL ; svc 0 - - ;; exit(0) - SET_X0_TO_0 ; x0 = 0 - SET_X8_TO_SYS_EXIT ; x8 = 93 - SYSCALL ; svc 0 + ## exit(0) + P1_LI_R0 + SYS_EXIT # r0 = syscall number (exit) + P1_LI_R1 + '00000000' # r1 = status + P1_SYSCALL :msg "Hello, world! diff --git a/p1_aarch64.M1 b/p1_aarch64.M1 @@ -0,0 +1,61 @@ +## P1 pseudo-ISA — aarch64 backing defs (v0.1 spike) +## +## Implements the subset needed for the hello-world demo: LI, SYSCALL. +## See ../P1.md for the full ISA and register mapping. +## +## Register mapping (P1 → aarch64): +## r0 → x0 , r1 → x1 , r2 → x2 , r3 → x3 +## r4 → x4 , r5 → x5 , r6 → x19, r7 → x20 +## +## LI rD, <4-byte-literal> — load a 4-byte little-endian literal into rD +## (zero-extended into the 64-bit register). Usage in source: +## +## P1_LI_R1 +## &some_label # or '0E000000' +## +## Expansion is "ldr w<D>, [pc+8] ; b +8" (8 bytes). The caller emits +## the 4 inline literal bytes that follow. Addresses that fit in 32 bits +## (true for our ELF-at-0x400000 binaries) round-trip through the W reg +## because aarch64 LDR-W zero-extends into X. +## +## The 4-byte form is deliberate for the spike: it pairs with hex2's +## `&label` sigil unchanged. A proper 64-bit LI via a PC-relative LDR X +## literal is left for the aarch64 hex2_word extensions described in +## P1.md §"What needs to be added". + +DEFINE P1_LI_R0 4000001802000014 +DEFINE P1_LI_R1 4100001802000014 +DEFINE P1_LI_R2 4200001802000014 +DEFINE P1_LI_R3 4300001802000014 +DEFINE P1_LI_R4 4400001802000014 +DEFINE P1_LI_R5 4500001802000014 +DEFINE P1_LI_R6 5300001802000014 +DEFINE P1_LI_R7 5400001802000014 + +## SYSCALL — num in r0, args r1..r6, result in r0. +## +## aarch64 Linux wants num in x8 and args in x0..x5. P1's mapping puts +## args one register higher than the native ABI, so SYSCALL shuffles +## x0..x19 down into x0..x5 and moves the num into x8, then svc #0. +## +## Unconditional (every P1 ISA expansion is unoptimized). Inputs not +## used by a given syscall are shuffled through harmlessly — x0..x5 are +## caller-saved on the aarch64 platform ABI, and the kernel only reads +## the registers the specific syscall cares about. +## +## Expansion: +## mov x8, x0 ; P1 r0 (num) -> native num reg +## mov x0, x1 ; P1 r1 -> native arg1 +## mov x1, x2 ; P1 r2 -> native arg2 +## mov x2, x3 ; P1 r3 -> native arg3 +## mov x3, x4 ; P1 r4 -> native arg4 +## mov x4, x5 ; P1 r5 -> native arg5 +## mov x5, x19 ; P1 r6 (x19) -> native arg6 +## svc #0 +DEFINE P1_SYSCALL e80300aae00301aae10302aae20303aae30304aae40305aae50313aa010000d4 + + +## Linux syscall numbers (aarch64 uses the generic table). +## Emitted as 4-byte little-endian immediates, to be consumed by P1_LI_R*. +DEFINE SYS_WRITE 40000000 +DEFINE SYS_EXIT 5D000000 diff --git a/p1_amd64.M1 b/p1_amd64.M1 @@ -0,0 +1,51 @@ +## P1 pseudo-ISA — amd64 backing defs (v0.1 spike) +## +## Implements the subset needed for the hello-world demo: LI, SYSCALL. +## See ../P1.md for the full ISA and register mapping. +## +## Register mapping (P1 → amd64): +## r0 → rax , r1 → rdi , r2 → rsi , r3 → rdx +## r4 → r10 , r5 → r8 , r6 → rbx , r7 → r12 +## +## LI rD, <4-byte-literal> — zero-extended load into rD. +## Expands to "mov r<D>d, imm32" (5 or 6 bytes). Because x86-64 +## zero-extends 32-bit mov-to-GPR into the 64-bit register, the +## 4-byte literal that immediately follows in source order ends up +## as the full 64-bit value. +## +## Usage: +## P1_LI_R1 +## &some_label # or '0E000000' + +DEFINE P1_LI_R0 B8 # mov eax, imm32 +DEFINE P1_LI_R1 BF # mov edi, imm32 +DEFINE P1_LI_R2 BE # mov esi, imm32 +DEFINE P1_LI_R3 BA # mov edx, imm32 +DEFINE P1_LI_R4 41BA # mov r10d, imm32 +DEFINE P1_LI_R5 41B8 # mov r8d, imm32 +DEFINE P1_LI_R6 BB # mov ebx, imm32 +DEFINE P1_LI_R7 41BC # mov r12d, imm32 + +## SYSCALL — num in r0, args r1..r6, result in r0. +## +## amd64 Linux syscall ABI: num in rax, args in rdi,rsi,rdx,r10,r8,r9. +## P1's mapping already places num (r0→rax) and args r1..r5 (→rdi,rsi, +## rdx,r10,r8) in their native slots. The only mismatch is arg 6: +## P1 r6 = rbx, native arg6 = r9. So SYSCALL shuffles that one register +## and then traps. +## +## Expansion: +## mov r9, rbx ; 49 89 D9 +## syscall ; 0F 05 +## +## The shuffle runs unconditionally — syscalls that ignore arg 6 don't +## care that r9 got overwritten with rbx (both are caller-saved on the +## platform ABI; the kernel reads only the registers the specific +## syscall cares about). +DEFINE P1_SYSCALL 4989D90F05 + + +## Linux syscall numbers (amd64-specific table). +## Emitted as 4-byte little-endian immediates, consumed by P1_LI_R*. +DEFINE SYS_WRITE 01000000 +DEFINE SYS_EXIT 3C000000 diff --git a/p1_riscv64.M1 b/p1_riscv64.M1 @@ -0,0 +1,63 @@ +## P1 pseudo-ISA — riscv64 backing defs (v0.1 spike) +## +## Implements the subset needed for the hello-world demo: LI, SYSCALL. +## See ../P1.md for the full ISA and register mapping. +## +## Register mapping (P1 → RISC-V): +## r0 → a0 (x10) , r1 → a1 (x11) , r2 → a2 (x12) , r3 → a3 (x13) +## r4 → a4 (x14) , r5 → a5 (x15) , r6 → s1 (x9) , r7 → s2 (x18) +## +## LI rD, <4-byte-literal> — zero-extended load into rD. +## RISC-V lacks a single "load imm32" form, so the expansion uses the +## same PC-relative-inline-data trick as aarch64, in three words: +## +## auipc rD, 0 ; rD = pc_of_auipc +## lwu rD, 12(rD) ; rD = *(u32*)(pc_of_auipc + 12) [zero-ext to 64] +## jal x0, +8 ; skip past the 4-byte data slot +## <4-byte literal> +## +## The LWU offset is 12 because the literal lives 12 bytes past the +## auipc: auipc(4) + lwu(4) + jal(4) = 12. +## +## Hex2 `&label` emits the 4-byte absolute address in little-endian, +## matching how lwu reads it. +## +## Usage: +## P1_LI_R1 +## &some_label # or '0E000000' + +## Each DEFINE below is three little-endian 32-bit words concatenated: +## [auipc rD,0] [lwu rD,12(rD)] [jal x0,+8]. +DEFINE P1_LI_R0 170500000365C5006F008000 +DEFINE P1_LI_R1 9705000083E5C5006F008000 +DEFINE P1_LI_R2 170600000366C6006F008000 +DEFINE P1_LI_R3 9706000083E6C6006F008000 +DEFINE P1_LI_R4 170700000367C7006F008000 +DEFINE P1_LI_R5 9707000083E7C7006F008000 +DEFINE P1_LI_R6 9704000083E4C4006F008000 +DEFINE P1_LI_R7 170900000369C9006F008000 + +## SYSCALL — num in r0, args r1..r6, result in r0. +## +## riscv64 Linux syscall ABI: num in a7, args in a0..a5. P1's mapping +## puts args one register higher than native, so SYSCALL shuffles +## a0..s1 down into a0..a5, moves num (a0) into a7, and traps. +## +## Expansion: +## mv a7, a0 ; P1 r0 (num) -> native num reg +## mv a0, a1 ; P1 r1 -> native arg1 +## mv a1, a2 ; P1 r2 -> native arg2 +## mv a2, a3 ; P1 r3 -> native arg3 +## mv a3, a4 ; P1 r4 -> native arg4 +## mv a4, a5 ; P1 r5 -> native arg5 +## mv a5, s1 ; P1 r6 -> native arg6 +## ecall +## +## Unconditional — unused shuffles read caller-saved scratch, and the +## kernel reads only the regs each syscall cares about. +DEFINE P1_SYSCALL 9308050013850500930506001386060093060700138707009307040073000000 + + +## Linux syscall numbers (riscv64 uses the generic table — same as aarch64). +DEFINE SYS_WRITE 40000000 +DEFINE SYS_EXIT 5D000000