P59 made the kernel boot. P60 is about getting Linux to actually hand control to a userspace program — proving the M-mode → S-mode → U-mode transition and the syscall path all work end to end.
Status: PARTIAL. The kernel reaches
Run /init as init processwith our binary as PID 1, environment variables set, ready to exec. We have not yet captured the userspaceprintfoutput before the testbench timeout — that lands on the follow-up page once a long-enough run completes.
Same chip RTL as P59. The work is entirely in the kernel/userspace stack on top of it.
What’s new
userspace/hello.c— a freestanding RV32 program. No libc._startis the entry point. Calls Linux’sSYS_write(64) andSYS_exit(93) directly viaecall. Builds with-march=rv32ima -mabi=ilp32 -static -nostdlib. Comes out to ~1.9 KB of ELF.userspace/Makefile— buildshello, copies it torootfs/init, and emitsinitramfs.cpio(newc format).- The kernel was rebuilt with
CONFIG_BLK_DEV_INITRD=y,CONFIG_INITRAMFS_SOURCE="/abs/path/to/initramfs.cpio",CONFIG_INITRAMFS_COMPRESSION_GZIP=y,CONFIG_BINFMT_ELF=y,CONFIG_DEVTMPFS=y,CONFIG_DEVTMPFS_MOUNT=y,CONFIG_PROC_FS=y. The cpio embeds at link time, so the resultingImageis self-contained.
Why no libc
Bringing up an RV32 libc (musl or glibc) would mean either
multilib in our Nix toolchain or a separate musl cross-build.
hello.c sidesteps that entirely:
_startis our entry, so no crt0 needed.write()andexit()are inlinedecallinstructions with the syscall ABI ina7/a0–a2.- A hand-rolled
slen()instead ofstrlen(gcc helpfully optimised our naive loop into astrlencall against libc —-fno-builtinfixes that).
Result: a 1.9 KB ELF that does literally only what we want.
Why /init = our hello
Linux mounts the initramfs at /, then execs /init as PID 1.
We don’t have a shell, BusyBox, or login — we just point /init
at our binary directly. When it exit()s, the kernel panics
with “Attempted to kill init!” (because PID 1 is supposed to
stay alive); that panic message also goes via SBI putchar to our
UART, which is fine for a test demo. We see our hello AND we see
the panic, both proving the kernel is fully alive.
Boot path
chip reset
-> stage-0 firmware (M-mode)
-> mret -> kernel _start (S-mode, satp = our identity PT)
-> setup_vm + relocate_enable_mmu
(satp = trampoline -> fault -> stvec redirect -> virtual)
(satp = early_pg_dir)
-> start_kernel
-> all the init... (memory zones, slub, irq, clocksource)
-> rest_init -> kernel_init -> ramdisk_execute_command "/init"
-> load_elf_binary("/init")
-> hello._start prints via SBI putchar
-> hello.exit
-> kernel panic "Attempted to kill init!"
What we have captured
The kernel reaches Run /init and prints the exec preamble:
Run /init as init process
with arguments:
/init
earlyprintk
with environment:
HOME=/
TERM=linux
That is the kernel telling us, through SBI putchar, that it has
loaded our ELF, is about to call start_thread, and is handing
control to U-mode. All the plumbing — initramfs unpack, ELF
parse, exec, env var setup — completed cleanly. The userspace
write/exit themselves happen a few hundred thousand cycles
later, which is where our 200M-cycle testbench timeout was
hitting. Bumped to 500M for the next run.
Files
userspace/hello.c— the userspace binaryuserspace/Makefile— build hello + cpioapp/,runtime/,boot/,src/,test/— same as P59, unchangedtest/Makefile— adds the kernel-with-initramfs path
How to reproduce
# 1. Build hello + initramfs.
cd projects/60_userspace_hello/userspace
make
# emits: hello (binary), rootfs/, initramfs.cpio
# 2. Configure kernel for initramfs and rebuild.
cd /path/to/linux-6.12.85
CPIO=/abs/path/to/initramfs.cpio
./scripts/config --enable BLK_DEV_INITRD \
--set-str INITRAMFS_SOURCE "$CPIO" \
--enable BINFMT_ELF \
--enable DEVTMPFS --enable DEVTMPFS_MOUNT \
--enable PROC_FS
make ARCH=riscv olddefconfig
make ARCH=riscv -j$(nproc) Image
# 3. Build chip + run sim.
cd projects/60_userspace_hello/test
make run KERNEL_IMAGE=/path/to/Image
Harden
NOT RUN. Software-only project; same chip as P59.
What just happened?
The chip is now hosting a real Linux userspace ABI. The kernel
parsed our cpio, allocated pages, built ELF auxv, set up the
user stack, switched to U-mode, and was about to start running
our binary’s _start. Every privilege transition the
RV32-Linux model defines is now exercised at least once on
this chip.
Closing the partial — getting the userspace write actually
captured — is a wall-clock-time problem, not a chip problem.
The follow-up will land on a P61-equipped
run because the TLB ~halves the cycle count to reach /init
exec.