From e27b8258bd6bda84646f309687ed61d882aa8f60 Mon Sep 17 00:00:00 2001 From: octalide Date: Fri, 26 Jun 2026 22:02:35 -0400 Subject: [PATCH 1/3] feat(os): riscv64-linux syscall stubs + os module Add the riscv64-linux OS layer mirroring the aarch64 one: - riscv64 arms in shared.mach for the syscall numbers, stat_t, O_DIRECTORY, the syscall0..6 inline-asm bodies (ecall), and the clone/spawn/trap asm. - src/system/os/linux/riscv64.mach: page_size + thread_spawn + the fwd re-exports. - riscv64 dispatch in os/linux.mach and the riscv64 panic write/trap. riscv64 and aarch64 share the asm-generic syscall table; only the inline-asm dialect changes. The one divergence: riscv lacks __ARCH_WANT_RENAMEAT, so rename uses renameat2 (276) instead of renameat (38), confirmed under qemu. Refs #306 --- src/system/os/linux.mach | 3 + src/system/os/linux/riscv64.mach | 314 +++++++++++++++++++++++++++++++ src/system/os/linux/shared.mach | 234 ++++++++++++++++++++++- src/system/panic.mach | 12 ++ 4 files changed, 555 insertions(+), 8 deletions(-) create mode 100644 src/system/os/linux/riscv64.mach diff --git a/src/system/os/linux.mach b/src/system/os/linux.mach index f126566..3eaca37 100644 --- a/src/system/os/linux.mach +++ b/src/system/os/linux.mach @@ -14,6 +14,9 @@ $if ($mach.build.arch == $mach.arch.x86_64) { $or ($mach.build.arch == $mach.arch.aarch64) { use impl: std.system.os.linux.aarch64; } +$or ($mach.build.arch == $mach.arch.riscv64) { + use impl: std.system.os.linux.riscv64; +} $or { $error("std.system.os.linux: unsupported architecture"); } diff --git a/src/system/os/linux/riscv64.mach b/src/system/os/linux/riscv64.mach new file mode 100644 index 0000000..b7bfdf2 --- /dev/null +++ b/src/system/os/linux/riscv64.mach @@ -0,0 +1,314 @@ +# std.system.os.linux.riscv64: riscv64-specific linux OS primitives. +# +# provides page_size and thread_spawn which require arch-specific assembly. +# everything else is in shared.mach. + +use std.types.size.usize; + +use shared: std.system.os.linux.shared; + +# imported unaliased so the clone-flag constants bind under their bare names +# into this module's comptime context; `THREAD_CLONE_FLAGS` then folds at +# compile time (a module-qualified `shared.CLONE_*` member path is not +# comptime-evaluable, so the global would otherwise have no constant value). +use std.system.os.linux.shared.CLONE_VM; +use std.system.os.linux.shared.CLONE_FS; +use std.system.os.linux.shared.CLONE_FILES; +use std.system.os.linux.shared.CLONE_SIGHAND; +use std.system.os.linux.shared.CLONE_THREAD; +use std.system.os.linux.shared.CLONE_SYSVSEM; + +$if ($mach.build.os != $mach.os.linux) { + $error("std.system.os.linux.riscv64: requires linux target"); +} + +$if ($mach.build.arch != $mach.arch.riscv64) { + $error("std.system.os.linux.riscv64: requires riscv64 target"); +} + +# page_size: return the system page size. +# --- +# NOTE: riscv64 linux uses 4 KiB base pages (Sv39/Sv48/Sv57 all share the 4 KiB +# leaf), which is what qemu-user presents. the robust fix is to query AT_PAGESZ +# from the auxiliary vector at startup; tracked for follow-up once the runtime +# aux-vector plumbing lands. +# --- +# ret: page size in bytes +pub fun page_size() usize { + ret 4096; +} + +val THREAD_CLONE_FLAGS: usize = CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD | CLONE_SYSVSEM; + +# entered by a direct branch from the child side of clone (see thread_spawn); +# the symbol is pinned so the inline-asm branch can target it. at entry sp points +# at the argument block on the child stack; the prologue pins s0 at the entry sp +# (`addi s0, sp, total`), so the four args sit at [s0+0..s0+24]. +#[symbol("_std_thread_trampoline")] +fun thread_trampoline() { + var fn_addr: usize = 0; + var done_addr: usize = 0; + var stack_base: usize = 0; + var stack_size: usize = 0; + asm riscv64 { + ld a0, 0(s0) + sd a0, {fn_addr} + ld a0, 8(s0) + sd a0, {done_addr} + ld a0, 16(s0) + sd a0, {stack_base} + ld a0, 24(s0) + sd a0, {stack_size} + } + val fn_ptr: fun() = fn_addr::fun(); + fn_ptr(); + val done: *i64 = done_addr::*i64; + @done = 1; + shared.syscall6(shared.SYS_FUTEX, done_addr, shared.FUTEX_WAKE, 1, 0, 0, 0); + asm riscv64 { + # munmap stack then exit + ld a0, {stack_base} + ld a1, {stack_size} + li a7, 215 # SYS_munmap + ecall + li a0, 0 + li a7, 93 # SYS_exit + ecall + } + asm riscv64 { ebreak } +} + +# thread_spawn: create a new thread sharing the current address space. +# +# uses clone with shared VM, FS, files, and signals. the child's stack +# carries the function pointer and done_ptr, so no globals are needed. +# the child calls the entry function, sets *done_ptr to 1, wakes one +# futex waiter, and exits. +# --- +# f: function to execute in the new thread +# done_ptr: pointer to a completion flag (set to 1 when thread exits) +# stack_base: base address of the pre-allocated stack region +# stack_size: size of the stack in bytes +# ret: thread ID on success, or negative errno on failure +pub fun thread_spawn(f: fun(), done_ptr: *i64, stack_base: usize, stack_size: usize) i64 { + val stack_top: usize = stack_base + stack_size; + # 48 keeps the child sp 16-byte aligned (riscv64 lp64) while leaving four + # 8-byte argument slots at +0..+24. + val child_sp: usize = stack_top - 48; + @(child_sp::*usize) = f::usize; + @((child_sp + 8)::*usize) = done_ptr::usize; + @((child_sp + 16)::*usize) = stack_base; + @((child_sp + 24)::*usize) = stack_size; + + var flags: usize = THREAD_CLONE_FLAGS; + var stack_ptr: usize = child_sp; + var result: i64 = 0; + + # parent/child discrimination must stay in registers: under CLONE_VM the + # child's frame pointer still addresses the parent frame, so a shared stack + # local would race (both sides writing the same slot — see issue #195). a + # conditional branch reaches only a numeric local label on riscv64, not a + # symbol, so the parent (a0 != 0) skips via bnez to local label 1, and the + # child (a0 == 0) falls through to a `tail` to the trampoline symbol (no + # return). clone arg order on riscv64 is (flags, stack, ptid, tls, ctid); + # all but flags/stack are zero here. + asm riscv64 { fence } + asm riscv64 { + ld a0, {flags} + ld a1, {stack_ptr} + li a2, 0 + li a3, 0 + li a4, 0 + li a7, 220 # SYS_clone + ecall + bnez a0, 1f # parent (tid != 0): skip the child dispatch + tail _std_thread_trampoline # child (a0 == 0): jump to trampoline (no return) + 1: + sd a0, {result} # parent: tid, or negative errno on failure + } + + ret result; +} + +# kernel ABI types +fwd shared.stat_t; +fwd shared.timespec; +fwd shared.dirent64; + +# path +fwd shared.separator; + +# constants +fwd shared.STDIN_FD; +fwd shared.STDOUT_FD; +fwd shared.STDERR_FD; + +fwd shared.O_RDONLY; +fwd shared.O_WRONLY; +fwd shared.O_RDWR; +fwd shared.O_CREAT; +fwd shared.O_EXCL; +fwd shared.O_TRUNC; +fwd shared.O_APPEND; +fwd shared.O_DIRECTORY; + +fwd shared.AT_FDCWD; +fwd shared.AT_SYMLINK_NOFOLLOW; +fwd shared.AT_REMOVEDIR; + +fwd shared.S_IFMT; +fwd shared.S_IFDIR; +fwd shared.S_IFREG; +fwd shared.S_IFLNK; + +fwd shared.WNOHANG; +fwd shared.WUNTRACED; +fwd shared.WCONTINUED; + +fwd shared.CLOCK_REALTIME; +fwd shared.CLOCK_MONOTONIC; + +fwd shared.EPERM; +fwd shared.ENOENT; +fwd shared.ESRCH; +fwd shared.EINTR; +fwd shared.EIO; +fwd shared.ENXIO; +fwd shared.E2BIG; +fwd shared.EBADF; +fwd shared.ECHILD; +fwd shared.EAGAIN; +fwd shared.ENOMEM; +fwd shared.EACCES; +fwd shared.EFAULT; +fwd shared.EBUSY; +fwd shared.EEXIST; +fwd shared.ENODEV; +fwd shared.ENOTDIR; +fwd shared.EISDIR; +fwd shared.EINVAL; +fwd shared.ENFILE; +fwd shared.EMFILE; +fwd shared.ETXTBSY; +fwd shared.ENOSPC; +fwd shared.EROFS; +fwd shared.EPIPE; +fwd shared.ENOTEMPTY; +fwd shared.ENOTSUP; + +fwd shared.EINTR_MAX_RETRIES; + +# feature detection +fwd shared.has_numa; +fwd shared.has_huge_pages; + +# syscall wrappers +fwd shared.syscall0; +fwd shared.syscall1; +fwd shared.syscall2; +fwd shared.syscall3; +fwd shared.syscall4; +fwd shared.syscall5; +fwd shared.syscall6; + +# memory +fwd shared.allocate; +fwd shared.deallocate; +fwd shared.reallocate; +fwd shared.heap_region_base; +fwd shared.heap_region_extend; +fwd shared.protect; +fwd shared.lock; +fwd shared.unlock; +fwd shared.advise; +fwd shared.allocate_huge; +fwd shared.numa_node_count; +fwd shared.numa_current_node; +fwd shared.numa_allocate; +fwd shared.numa_move; +fwd shared.map_file; +fwd shared.sync_file; + +# environment +fwd shared._envp; +fwd shared.environ; + +# filesystem +fwd shared.read; +fwd shared.write; +fwd shared.open; +fwd shared.close; +fwd shared.stat; +fwd shared.stat_path; +fwd shared.unlink; +fwd shared.rename; +fwd shared.symlink; +fwd shared.make_dir; +fwd shared.read_dir; +fwd shared.access; +fwd shared.seek; + +# process +fwd shared.terminate; +fwd shared.abort; +fwd shared.spawn; +fwd shared.spawn_redirected; +fwd shared.getpid; +fwd shared.pipe; +fwd shared.sleep; +fwd shared.getcwd; +fwd shared.getenv; +fwd shared.fork; +fwd shared.vfork; +fwd shared.exec; +fwd shared.wait; +fwd shared.wait_pid; +fwd shared.has_exited; +fwd shared.exit_code; +fwd shared.was_signaled; +fwd shared.term_signal; +fwd shared.was_stopped; +fwd shared.stop_signal; +fwd shared.was_continued; + +# sockets +fwd shared.AF_INET; +fwd shared.SOCK_STREAM; +fwd shared.SOCK_DGRAM; +fwd shared.SOL_SOCKET; +fwd shared.SO_REUSEADDR; +fwd shared.SHUT_RD; +fwd shared.SHUT_WR; +fwd shared.SHUT_RDWR; +fwd shared.SOCKADDR_IN_SIZE; +fwd shared.sock_addr_init; +fwd shared.sock_addr_read; +fwd shared.sock_send; +fwd shared.sock_recv; +fwd shared.sock_close; +fwd shared.sock_create; +fwd shared.sock_bind; +fwd shared.sock_listen; +fwd shared.sock_accept; +fwd shared.sock_connect; +fwd shared.sock_sendto; +fwd shared.sock_recvfrom; +fwd shared.sock_shutdown; +fwd shared.sock_setopt; + +# dns +fwd shared.DNS_HOSTS_PATH; +fwd shared.dns_nameserver; + +# random +fwd shared.random_fill; + +# threads +fwd shared.thread_wait; +fwd shared.thread_wake; + +# time +fwd shared.clock_gettime; + +# errno +fwd shared.error_message; diff --git a/src/system/os/linux/shared.mach b/src/system/os/linux/shared.mach index 426b5e8..36720b7 100644 --- a/src/system/os/linux/shared.mach +++ b/src/system/os/linux/shared.mach @@ -18,9 +18,10 @@ $if ($mach.build.os != $mach.os.linux) { } # syscall numbers are architecture-specific: x86_64 has its own table, while -# aarch64 uses the generic asm-generic/unistd table. the aarch64 table also -# lacks fork/vfork/symlink/dup2 entirely — clone, symlinkat, and dup3 stand in -# (see the fork, vfork, symlink, and spawn_redirected wrappers below). +# aarch64 and riscv64 both use the generic asm-generic/unistd table. those tables +# lack fork/vfork/symlink/dup2 entirely — clone, symlinkat, and dup3 stand in +# (see the fork, vfork, symlink, and spawn_redirected wrappers below). riscv64 also +# lacks the legacy renameat, so its table carries renameat2 instead. $if ($mach.build.arch == $mach.arch.x86_64) { val SYS_READ: usize = 0; val SYS_WRITE: usize = 1; @@ -123,6 +124,58 @@ $or ($mach.build.arch == $mach.arch.aarch64) { val SYS_CLOSE_RANGE: usize = 436; val SYS_DUP3: usize = 24; } +$or ($mach.build.arch == $mach.arch.riscv64) { + val SYS_READ: usize = 63; + val SYS_WRITE: usize = 64; + val SYS_CLOSE: usize = 57; + val SYS_FSTAT: usize = 80; + val SYS_LSEEK: usize = 62; + val SYS_MMAP: usize = 222; + val SYS_MPROTECT: usize = 226; + val SYS_MUNMAP: usize = 215; + val SYS_BRK: usize = 214; + val SYS_MREMAP: usize = 216; + val SYS_MSYNC: usize = 227; + val SYS_MADVISE: usize = 233; + val SYS_NANOSLEEP: usize = 101; + val SYS_GETPID: usize = 172; + val SYS_CLONE: usize = 220; + val SYS_EXECVE: usize = 221; + val SYS_EXIT: usize = 93; + val SYS_EXIT_GROUP: usize = 94; + val SYS_WAIT4: usize = 260; + val SYS_PIPE2: usize = 59; + val SYS_GETCWD: usize = 17; + val SYS_MLOCK: usize = 228; + val SYS_MUNLOCK: usize = 229; + val SYS_GETDENTS64: usize = 61; + val SYS_CLOCK_GETTIME: usize = 113; + val SYS_MBIND: usize = 235; + val SYS_GET_MEMPOLICY: usize = 236; + val SYS_OPENAT: usize = 56; + val SYS_MKDIRAT: usize = 34; + val SYS_NEWFSTATAT: usize = 79; + val SYS_UNLINKAT: usize = 35; + # riscv lacks __ARCH_WANT_RENAMEAT, so the legacy renameat (38) is unimplemented + # (ENOSYS); renameat2 (276) stands in (see the rename wrapper below). + val SYS_RENAMEAT2: usize = 276; + val SYS_FACCESSAT: usize = 48; + val SYS_SYMLINKAT: usize = 36; + pub val SYS_FUTEX: usize = 98; + val SYS_SENDTO: usize = 206; + val SYS_RECVFROM: usize = 207; + val SYS_SHUTDOWN: usize = 210; + val SYS_BIND: usize = 200; + val SYS_LISTEN: usize = 201; + val SYS_SETSOCKOPT: usize = 208; + val SYS_SOCKET: usize = 198; + val SYS_CONNECT: usize = 203; + val SYS_ACCEPT: usize = 202; + val SYS_GETRANDOM: usize = 278; + val SYS_GETCPU: usize = 168; + val SYS_CLOSE_RANGE: usize = 436; + val SYS_DUP3: usize = 24; +} $or { $error("std.system.os.linux.shared: unsupported architecture"); } @@ -187,7 +240,7 @@ use os_shared: std.system.os.shared; # kernel ABI types # stat_t: linux stat structure for file metadata. the kernel struct differs -# by architecture: x86_64 has its own layout, while aarch64 uses the +# by architecture: x86_64 has its own layout, while aarch64 and riscv64 share the # asm-generic layout (st_mode/st_nlink order swapped, narrower nlink/blksize). # the field names are identical so consumers are arch-agnostic. $if ($mach.build.arch == $mach.arch.x86_64) { @@ -238,6 +291,30 @@ $or ($mach.build.arch == $mach.arch.aarch64) { _unused5: u32; } } +$or ($mach.build.arch == $mach.arch.riscv64) { + pub rec stat_t { + st_dev: u64; + st_ino: u64; + st_mode: u32; + st_nlink: u32; + st_uid: u32; + st_gid: u32; + st_rdev: u64; + _pad1: u64; + st_size: i64; + st_blksize: i32; + _pad2: i32; + st_blocks: i64; + st_atime: i64; + st_atime_nsec: i64; + st_mtime: i64; + st_mtime_nsec: i64; + st_ctime: i64; + st_ctime_nsec: i64; + _unused4: u32; + _unused5: u32; + } +} # timespec: time specification with seconds and nanoseconds. pub rec timespec { @@ -270,14 +347,18 @@ pub val O_EXCL: i32 = 128; pub val O_TRUNC: i32 = 512; pub val O_APPEND: i32 = 1024; -# O_DIRECTORY differs by arch: x86_64 = 0x10000, aarch64 (asm-generic) = 0x4000. -# 0x10000 means O_DIRECT on aarch64, so the value must be arch-gated. +# O_DIRECTORY differs by arch: x86_64 = 0x10000, asm-generic (aarch64, riscv64) = +# 0x4000. 0x10000 means O_DIRECT on the asm-generic arches, so the value must be +# arch-gated. $if ($mach.build.arch == $mach.arch.x86_64) { pub val O_DIRECTORY: i32 = 0x10000; } $or ($mach.build.arch == $mach.arch.aarch64) { pub val O_DIRECTORY: i32 = 0x4000; } +$or ($mach.build.arch == $mach.arch.riscv64) { +pub val O_DIRECTORY: i32 = 0x4000; +} pub val AT_FDCWD: i32 = -100; pub val AT_SYMLINK_NOFOLLOW: i32 = 0x0100; @@ -347,6 +428,13 @@ pub fun syscall0(n: usize) i64 { str x0, {out} } } + $or ($mach.build.arch == $mach.arch.riscv64) { + asm riscv64 { + ld a7, {n} + ecall + sd a0, {out} + } + } ret out; } @@ -373,6 +461,14 @@ pub fun syscall1(n: usize, a0: usize) i64 { str x0, {out} } } + $or ($mach.build.arch == $mach.arch.riscv64) { + asm riscv64 { + ld a7, {n} + ld a0, {a0} + ecall + sd a0, {out} + } + } ret out; } @@ -402,6 +498,15 @@ pub fun syscall2(n: usize, a0: usize, a1: usize) i64 { str x0, {out} } } + $or ($mach.build.arch == $mach.arch.riscv64) { + asm riscv64 { + ld a7, {n} + ld a0, {a0} + ld a1, {a1} + ecall + sd a0, {out} + } + } ret out; } @@ -434,6 +539,16 @@ pub fun syscall3(n: usize, a0: usize, a1: usize, a2: usize) i64 { str x0, {out} } } + $or ($mach.build.arch == $mach.arch.riscv64) { + asm riscv64 { + ld a7, {n} + ld a0, {a0} + ld a1, {a1} + ld a2, {a2} + ecall + sd a0, {out} + } + } ret out; } @@ -469,6 +584,17 @@ pub fun syscall4(n: usize, a0: usize, a1: usize, a2: usize, a3: usize) i64 { str x0, {out} } } + $or ($mach.build.arch == $mach.arch.riscv64) { + asm riscv64 { + ld a7, {n} + ld a0, {a0} + ld a1, {a1} + ld a2, {a2} + ld a3, {a3} + ecall + sd a0, {out} + } + } ret out; } @@ -507,6 +633,18 @@ pub fun syscall5(n: usize, a0: usize, a1: usize, a2: usize, a3: usize, a4: usize str x0, {out} } } + $or ($mach.build.arch == $mach.arch.riscv64) { + asm riscv64 { + ld a7, {n} + ld a0, {a0} + ld a1, {a1} + ld a2, {a2} + ld a3, {a3} + ld a4, {a4} + ecall + sd a0, {out} + } + } ret out; } @@ -548,6 +686,19 @@ pub fun syscall6(n: usize, a0: usize, a1: usize, a2: usize, a3: usize, a4: usize str x0, {out} } } + $or ($mach.build.arch == $mach.arch.riscv64) { + asm riscv64 { + ld a7, {n} + ld a0, {a0} + ld a1, {a1} + ld a2, {a2} + ld a3, {a3} + ld a4, {a4} + ld a5, {a5} + ecall + sd a0, {out} + } + } ret out; } @@ -926,7 +1077,13 @@ pub fun unlink(dirfd: i32, path: *u8, flags: i32) i64 { # newpath: destination path # ret: 0 on success, or negative errno pub fun rename(olddir: i32, oldpath: *u8, newdir: i32, newpath: *u8) i64 { - ret syscall4(SYS_RENAMEAT, olddir::isize::usize, oldpath::usize, newdir::isize::usize, newpath::usize); + $if ($mach.build.arch == $mach.arch.riscv64) { + # riscv has no legacy renameat; renameat2 with flags 0 is equivalent. + ret syscall5(SYS_RENAMEAT2, olddir::isize::usize, oldpath::usize, newdir::isize::usize, newpath::usize, 0); + } + $or { + ret syscall4(SYS_RENAMEAT, olddir::isize::usize, oldpath::usize, newdir::isize::usize, newpath::usize); + } } # symlink: create a symbolic link at linkpath pointing to target. @@ -945,6 +1102,10 @@ pub fun symlink(target: *u8, linkpath: *u8) i64 { # aarch64 has no symlink syscall; symlinkat against AT_FDCWD is equivalent. ret syscall3(SYS_SYMLINKAT, target::usize, AT_FDCWD::isize::usize, linkpath::usize); } + $or ($mach.build.arch == $mach.arch.riscv64) { + # riscv has no symlink syscall; symlinkat against AT_FDCWD is equivalent. + ret syscall3(SYS_SYMLINKAT, target::usize, AT_FDCWD::isize::usize, linkpath::usize); + } } # mkdirat: create a directory relative to a directory fd. @@ -1015,6 +1176,9 @@ pub fun terminate(code: i64) { $or ($mach.build.arch == $mach.arch.aarch64) { asm aarch64 { brk 0 } } + $or ($mach.build.arch == $mach.arch.riscv64) { + asm riscv64 { ebreak } + } } # abort: abort the process immediately with exit code 255. @@ -1034,6 +1198,11 @@ pub fun fork() i64 { # (child runs on a copy-on-write image of the parent stack) is fork. ret syscall5(SYS_CLONE, SIGCHLD, 0, 0, 0, 0); } + $or ($mach.build.arch == $mach.arch.riscv64) { + # riscv has no fork syscall; clone with SIGCHLD and no child stack + # (child runs on a copy-on-write image of the parent stack) is fork. + ret syscall5(SYS_CLONE, SIGCHLD, 0, 0, 0, 0); + } } # vfork: create child process sharing memory with parent. @@ -1050,6 +1219,13 @@ pub fun vfork() i64 { # the x86_64 vfork: the child must only exec or _exit. ret syscall5(SYS_CLONE, CLONE_VM | CLONE_VFORK | SIGCHLD, 0, 0, 0, 0); } + $or ($mach.build.arch == $mach.arch.riscv64) { + # riscv has no vfork syscall; clone with CLONE_VM|CLONE_VFORK|SIGCHLD + # and no child stack matches vfork semantics (parent suspended, child + # shares the address space until exec/_exit). same caller contract as + # the x86_64 vfork: the child must only exec or _exit. + ret syscall5(SYS_CLONE, CLONE_VM | CLONE_VFORK | SIGCHLD, 0, 0, 0, 0); + } } # execve: replace the current process image with a new program. @@ -1131,13 +1307,16 @@ val SPAWN_CLONE_FLAGS: usize = CLONE_VM | CLONE_VFORK | SIGCHLD; # child-stack bias: the trampoline is entered with sp = stack_top - bias and # reads its single ctx-pointer argument from that slot after the standard # prologue. x86_64 keeps sp ≡ 8 (mod 16) to mirror a post-call frame; aarch64 -# keeps sp 16-byte aligned. +# and riscv64 keep sp 16-byte aligned. $if ($mach.build.arch == $mach.arch.x86_64) { val SPAWN_SP_BIAS: usize = 24; } $or ($mach.build.arch == $mach.arch.aarch64) { val SPAWN_SP_BIAS: usize = 16; } +$or ($mach.build.arch == $mach.arch.riscv64) { + val SPAWN_SP_BIAS: usize = 16; +} # spawn_ctx: arguments carried across the clone boundary to the trampoline. # lives in the parent's frame; CLONE_VFORK keeps the parent suspended (and this @@ -1174,6 +1353,13 @@ fun spawn_trampoline() { str x0, {ctx_addr} } } + $or ($mach.build.arch == $mach.arch.riscv64) { + # riscv64 s0 is pinned at the entry sp, so the ctx slot sits at 0(s0). + asm riscv64 { + ld a0, 0(s0) + sd a0, {ctx_addr} + } + } val ctx: *spawn_ctx = ctx_addr::*spawn_ctx; # aarch64 has no dup2 syscall; dup3 with flags 0 is equivalent (it differs @@ -1200,6 +1386,19 @@ fun spawn_trampoline() { syscall1(SYS_EXIT_GROUP, 126); } } + $or ($mach.build.arch == $mach.arch.riscv64) { + # riscv has no dup2 syscall; dup3 with flags 0 is equivalent (it differs + # only in rejecting oldfd == newfd, which redirect fds never are). + if (ctx.stdin_fd != -1 && syscall3(SYS_DUP3, ctx.stdin_fd::isize::usize, STDIN_FD::isize::usize, 0) < 0) { + syscall1(SYS_EXIT_GROUP, 126); + } + if (ctx.stdout_fd != -1 && syscall3(SYS_DUP3, ctx.stdout_fd::isize::usize, STDOUT_FD::isize::usize, 0) < 0) { + syscall1(SYS_EXIT_GROUP, 126); + } + if (ctx.stderr_fd != -1 && syscall3(SYS_DUP3, ctx.stderr_fd::isize::usize, STDERR_FD::isize::usize, 0) < 0) { + syscall1(SYS_EXIT_GROUP, 126); + } + } close_inherited_fds(); exec(ctx.pathname, ctx.argv, ctx.envp); syscall1(SYS_EXIT_GROUP, 127); @@ -1211,6 +1410,9 @@ fun spawn_trampoline() { $or ($mach.build.arch == $mach.arch.aarch64) { asm aarch64 { brk 0 } } + $or ($mach.build.arch == $mach.arch.riscv64) { + asm riscv64 { ebreak } + } } # spawn: create a child process running the given program. @@ -1304,6 +1506,22 @@ pub fun spawn_redirected(pathname: *u8, argv: **u8, envp: **u8, stdin_fd: i32, s str x0, {result} # parent: pid, or negative errno on failure } } + $or ($mach.build.arch == $mach.arch.riscv64) { + asm riscv64 { fence } + asm riscv64 { + ld a0, {flags} + ld a1, {stack_ptr} + li a2, 0 + li a3, 0 + li a4, 0 + li a7, 220 # SYS_clone + ecall + bnez a0, 1f # parent (pid != 0): skip the child dispatch + tail _std_spawn_trampoline # child (a0 == 0): jump to trampoline (no return) + 1: + sd a0, {result} # parent: pid, or negative errno on failure + } + } deallocate(stack_base, SPAWN_STACK_SIZE); ret result; diff --git a/src/system/panic.mach b/src/system/panic.mach index 2ed6360..43782b3 100644 --- a/src/system/panic.mach +++ b/src/system/panic.mach @@ -32,6 +32,15 @@ pub fun panic(msg: *u8) { svc 0 } } + $or ($mach.build.arch == $mach.arch.riscv64) { + asm riscv64 { + li a7, 64 # SYS_write (linux riscv64) + li a0, 2 # fd = stderr + ld a1, {msg} + ld a2, {len} + ecall + } + } $or { $error("std.system.panic: unsupported linux architecture"); } @@ -66,6 +75,9 @@ pub fun panic(msg: *u8) { $or ($mach.build.arch == $mach.arch.aarch64) { asm aarch64 { brk 0 } } + $or ($mach.build.arch == $mach.arch.riscv64) { + asm riscv64 { ebreak } + } $or { $error("std.system.panic: unsupported architecture"); } From 0d7e03b76c595b328c9174d5891ebd9731e42b5b Mon Sep 17 00:00:00 2001 From: octalide Date: Fri, 26 Jun 2026 22:02:35 -0400 Subject: [PATCH 2/3] feat(runtime): riscv64-linux _start entry Add src/runtime/linux/riscv64.mach (_start, _rt_init, the argc/argv/envp globals) and the riscv64 dispatch in runtime/linux.mach. _start is a naked inline-asm entry that reads the kernel-supplied sp, publishes argc/argv/envp, calls _rt_init then main, and exits via exit_group. Refs #306 --- src/runtime/linux.mach | 3 ++ src/runtime/linux/riscv64.mach | 76 ++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 src/runtime/linux/riscv64.mach diff --git a/src/runtime/linux.mach b/src/runtime/linux.mach index 6b1621b..b7882a2 100644 --- a/src/runtime/linux.mach +++ b/src/runtime/linux.mach @@ -4,6 +4,9 @@ $if ($mach.build.arch == $mach.arch.x86_64) { $or ($mach.build.arch == $mach.arch.aarch64) { use std.runtime.linux.aarch64; } +$or ($mach.build.arch == $mach.arch.riscv64) { + use std.runtime.linux.riscv64; +} $or { $error("std.runtime.linux: unsupported architecture"); } diff --git a/src/runtime/linux/riscv64.mach b/src/runtime/linux/riscv64.mach new file mode 100644 index 0000000..e2feb2b --- /dev/null +++ b/src/runtime/linux/riscv64.mach @@ -0,0 +1,76 @@ +# std.runtime.linux.riscv64: riscv64 (lp64) linux entrypoint +# --- +# provides `_start`, the ELF entry point for riscv64 linux executables. +# +# on entry, the kernel places the following on the stack (same layout as +# every LP64 linux triple — 8-byte slots): +# [sp] = argc +# [sp+8] = argv[0] +# [sp+16] = argv[1] +# ... +# argv + (argc+1)*8 = envp[0] +# +# this entrypoint mirrors the x86_64 / aarch64 ones: +# 1. extracts argc, argv, and envp from the initial stack +# 2. stores them into the runtime globals +# 3. calls _rt_init to publish envp into the OS layer +# 4. calls user-supplied `main(argc, argv)` (a0 = argc, a1 = argv) +# 5. exits via SYS_exit_group with main's return code (terminates all +# threads; SYS_exit would leave non-main threads alive) +# +# user code must define: +# #[symbol("main")] +# fun main(argc: i64, argv: **u8) i64 { ... } +# +# entry frame: `_start` is a naked inline-asm function (no locals, no calls +# outside the asm block), so the backend emits no prologue and the first +# instruction reads the raw kernel-supplied sp (argc at [sp]). sp is 16-byte +# aligned on kernel entry (the riscv lp64 process-startup invariant), so no +# realignment is needed before the calls. + +use os: std.system.os.linux.riscv64; + +#[symbol("_rt_argc")] +var _rt_argc: i64 = 0; +#[symbol("_rt_argv")] +var _rt_argv: u64 = 0; +#[symbol("_rt_envp")] +var _rt_envp: u64 = 0; + +#[symbol("_rt_init")] +fun _rt_init() { + os._envp = _rt_envp; +} + +#[symbol("_start")] +pub fun _start() { + asm riscv64 { + mv t0, sp # t0 = initial stack pointer (argc at [t0]) + ld a0, 0(t0) # argc -> a0 (1st argument to main) + addi a1, t0, 8 # argv -> a1 (2nd argument to main) + + # envp = argv + (argc + 1) * 8 + addi t1, a0, 1 + slli t1, t1, 3 + add t1, a1, t1 # t1 = envp + + la t2, _rt_argc + sd a0, 0(t2) + la t2, _rt_argv + sd a1, 0(t2) + la t2, _rt_envp + sd t1, 0(t2) + + call _rt_init + + la t2, _rt_argc + ld a0, 0(t2) + la t2, _rt_argv + ld a1, 0(t2) + call main + + # exit_group with main's return value (already in a0) + li a7, 94 # SYS_exit_group + ecall + } +} From 35b92111d346c43ab793c8e58b8ece40b34fd4bf Mon Sep 17 00:00:00 2001 From: octalide Date: Sat, 27 Jun 2026 00:59:31 -0400 Subject: [PATCH 3/3] ci: add cross-riscv64 qemu lane for the riscv64-linux runtime Add a test/riscv64 fixture (a tiny std consumer that reaches main through _start and exercises a write syscall) plus verify.sh, and a cross-riscv64 CI job that cross-compiles it for linux-riscv64 with the released mach and runs it under qemu-riscv64 asserting the exit code. Mirrors mach's cross riscv64-linux lane and exercises the riscv64 runtime end to end. --- .github/workflows/ci.yml | 24 +++++++++++++++++++++++ .gitignore | 1 + test/riscv64/mach.toml | 23 ++++++++++++++++++++++ test/riscv64/src/main.mach | 11 +++++++++++ test/riscv64/verify.sh | 39 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 98 insertions(+) create mode 100644 test/riscv64/mach.toml create mode 100644 test/riscv64/src/main.mach create mode 100755 test/riscv64/verify.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4f13b9..e0dd4db 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,3 +29,27 @@ jobs: - name: Test run: mach test . + + cross-riscv64: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + submodules: true + + - name: Install mach and qemu + env: + GH_TOKEN: ${{ github.token }} + run: | + sudo apt-get update + sudo apt-get install -y qemu-user + mkdir -p /tmp/machdl + gh release download --repo octalide/mach \ + --pattern 'mach-*-x86_64-linux.tar.gz' --dir /tmp/machdl --clobber + tar -xzf /tmp/machdl/mach-*-x86_64-linux.tar.gz -C /tmp/machdl + cp /tmp/machdl/mach /usr/local/bin/mach + chmod +x /usr/local/bin/mach + mach info + + - name: Cross-compile and run the riscv64 runtime smoke test + run: bash test/riscv64/verify.sh diff --git a/.gitignore b/.gitignore index 589a4a9..c2e32f8 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ out cmach tmp output +dep diff --git a/test/riscv64/mach.toml b/test/riscv64/mach.toml new file mode 100644 index 0000000..d21f135 --- /dev/null +++ b/test/riscv64/mach.toml @@ -0,0 +1,23 @@ +[project] +id = "rvprobe" +name = "rvprobe" +version = "0.0.0" +src = "src" +dep = "dep" +target = "linux-riscv64" +out = "out/{target}/{name}{ext}" +obj = "out/{target}/obj" + +[target.linux-riscv64] +isa = "riscv64" +os = "linux" +abi = "lp64" + +[profile.debug] +opt = 0 + +[bin.rvprobe] +entry = "main.mach" + +[deps.std] +path = "dep/std" diff --git a/test/riscv64/src/main.mach b/test/riscv64/src/main.mach new file mode 100644 index 0000000..40cc695 --- /dev/null +++ b/test/riscv64/src/main.mach @@ -0,0 +1,11 @@ +# riscv64-linux runtime smoke test: reaches `main` through `_start`, exercises a +# write syscall, and returns a known exit code that verify.sh asserts under qemu. +use std.runtime; +use os: std.system.os.linux; + +#[symbol("main")] +fun main(argc: i64, argv: **u8) i64 { + var c: u8 = 'k'::u8; + os.write(1, ?c, 1); + ret 42; +} diff --git a/test/riscv64/verify.sh b/test/riscv64/verify.sh new file mode 100755 index 0000000..5f3f317 --- /dev/null +++ b/test/riscv64/verify.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# cross-compile the riscv64-linux runtime smoke test against this checkout's std +# and run it under qemu-riscv64, asserting its exit code. proves the riscv64 +# runtime (_start + the syscall stubs) links and runs end to end. +# +# usage: verify.sh [path-to-mach] (defaults to `mach` on PATH) +# +# requires: qemu-riscv64 (or qemu-riscv64-static). +set -euo pipefail + +# the exit code main returns; the qemu run asserts it. +expect_code=42 + +mach="${1:-mach}" +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$here" + +fail() { echo "FAIL: $1" >&2; exit 1; } + +# vendor this checkout's std as the path dependency (dep/std -> repo root). +mkdir -p dep +ln -sfn "$(cd ../.. && pwd)" dep/std + +echo "cross-compiling the riscv64 runtime smoke test with $mach" +rm -rf out +"$mach" build . --target linux-riscv64 --profile debug +exe="$(find out -name rvprobe -type f -print -quit)" +[ -n "$exe" ] || fail "no rvprobe binary produced" + +echo "running $exe under qemu-riscv64" +qemu="$(command -v qemu-riscv64 || command -v qemu-riscv64-static || true)" +[ -n "$qemu" ] || fail "qemu-riscv64 not found" +set +e +"$qemu" "$exe" +code=$? +set -e +[ "$code" -eq "$expect_code" ] || fail "exit code $code, expected $expect_code" + +echo "OK: riscv64-linux runtime reaches main and exits $expect_code"