diff --git a/Makefile b/Makefile index a50667d..15f2cd6 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ EMULATOR = ./amach ASFLAGS = -march=rv32imac -mabi=ilp32 -nostdlib -s -static -fno-pic -fno-pie -mno-relax LDFLAGS = -Wl,-Ttext=0x100b0 -Wl,--build-id=none -all: rv32imac_tests.bin +all: rv32imac_tests.bin rv32imac_syscall_tests.bin rv32imac_tests.elf: rv32imac_tests.s $(CC) $(ASFLAGS) $(LDFLAGS) rv32imac_tests.s -o rv32imac_tests.elf @@ -13,15 +13,25 @@ rv32imac_tests.elf: rv32imac_tests.s rv32imac_tests.bin: rv32imac_tests.elf $(OBJCOPY) -O binary rv32imac_tests.elf rv32imac_tests.bin -run: rv32imac_tests.bin +rv32imac_syscall_tests.elf: rv32imac_syscall_tests.s + $(CC) $(ASFLAGS) $(LDFLAGS) rv32imac_syscall_tests.s -o rv32imac_syscall_tests.elf + +rv32imac_syscall_tests.bin: rv32imac_syscall_tests.elf + $(OBJCOPY) -O binary rv32imac_syscall_tests.elf rv32imac_syscall_tests.bin + +run: rv32imac_tests.bin rv32imac_syscall_tests.bin + @echo "Running CPU instruction tests..." $(EMULATOR) rv32imac_tests.bin + @echo "Running syscall tests..." + ln -sf test_output_new.txt test_link.txt + $(EMULATOR) rv32imac_syscall_tests.bin @echo "Testing standard ebreak..." @echo "115 0 16 0" | awk -f amach.awk 2>&1 | grep -q "Fatal: EBREAK" @echo "Testing compressed c.ebreak..." @echo "2 144" | awk -f amach.awk 2>&1 | grep -q "Fatal: EBREAK" - @echo "All EBREAK tests PASSED!" + @echo "All tests PASSED!" clean: - rm -f rv32imac_tests.elf rv32imac_tests.bin + rm -rf rv32imac_tests.elf rv32imac_tests.bin rv32imac_syscall_tests.elf rv32imac_syscall_tests.bin test_dir test_link.txt test_output.txt test_output_new.txt .PHONY: all run clean diff --git a/README.md b/README.md index 172a61b..62c4227 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ A-machine is a simple and straightforward implementation of the RV32I specificat - [amach.awk](amach.awk), the emulator core itself that supports .dec files, - and [amach](amach) shell script which is a two-liner that uses POSIX `od` to convert .bin files into .dec files on the fly and then passes them to the emulator core. -Additionally, A-machine is shipped with a reference test suite that assumes that you have a full RISC-V toolchain installed under the `riscv64-linux-gnu-` prefix (adjustable in the `Makefile`). Just run `make run` to build and run all tests. In case you don't have any toolchain available, a file called `rv32imac_tests.bin` is also included into the repo. +Additionally, A-machine is shipped with a reference test suite that assumes that you have a full RISC-V toolchain installed under the `riscv64-linux-gnu-` prefix (adjustable in the `Makefile`). Just run `make run` to build and run all tests. In case you don't have any toolchain available, two files called `rv32imac_tests.bin` and `rv32imac_syscall_tests.bin` are also included into the repo. The project is to be considered highly experimental and not production-ready in any way. @@ -32,12 +32,19 @@ POSIXLY_CORRECT=1 awk -f amach.awk [-v LVA=...] -- path/to/program.dec ## Support -Currently, A-machine only supports the **unprivileged** RV32IMAC instruction set with a few additional `ecall` syscalls: 63 (sys_read, for fd 0 only), 64 (sys_write, for fd 1 and 2 only) and 93 (sys_exit). For completeness, `fence` instruction is also supported but yields a no-op, and both `ebreak` and `c.ebreak` are supported to halt execution with a debug message. +Currently, A-machine only supports the **unprivileged** RV32IMAC instruction set with an extended subset of POSIX/Linux-compatible `ecall` system calls (using standard `asm-generic` numbers): + +- **Process Control**: `sys_exit` (93), `sys_exit_group` (94). +- **File & Directory I/O**: `sys_openat` (56), `sys_close` (57), `sys_read` (63), `sys_write` (64), `sys_lseek` (62). Multiple file descriptors (with full path resolution, creation/truncation flags, and offset tracking) are supported for files and directories. +- **Directory Operations**: `sys_mkdirat` (34), `sys_unlinkat` (35) (including directory removal), `sys_renameat` (38) / `sys_renameat2` (276), `sys_getdents64` (61) (for listing directory contents). +- **File Metadata & Links**: `sys_newfstatat` (79), `sys_fstat` (80), `sys_readlinkat` (78). +- **System Time & Entropy**: `sys_clock_gettime` (113), `sys_clock_gettime64` (403), `sys_gettimeofday` (169), and `sys_getrandom` (278). + +For completeness, `fence` instruction is also supported but yields a no-op, and both `ebreak` and `c.ebreak` are supported to halt execution with a debug message. ## Plans (from higher to lower priority) - Implement a simple RV32IMAC disassembler in POSIX AWK. -- Introduce more popular Linux-compatible syscalls (that make sense to implement in AWK but don't increase the overall project complexity). - Test for RV32EMC compatibility (that the code built for the "embedded" variant runs here). - Implement a privileged instruction set minimum (Zicsr), interrupts and traps. - If time permits, implement a simple RV32IMAC assembler in POSIX AWK, with full pseudo-instruction support. diff --git a/amach.awk b/amach.awk index fa3a712..f554170 100644 --- a/amach.awk +++ b/amach.awk @@ -9,7 +9,12 @@ # Created by Luxferre in 2026, released into the public domain # fatal error reporting function -function trapout(msg) { +function trapout(msg, fd) { + for(fd in FD_PATH) { + if(FD_DIRTY[fd]) { + write_fd_to_file(fd) + } + } cmd = "cat 1>&2" printf("Fatal: %s\n", msg) | cmd close(cmd) @@ -80,23 +85,476 @@ function write_mem(addr, val, bytes, i) { function read_word(addr) {return read_mem(addr, 4, 1)} function write_word(addr, val) {write_mem(addr, val, 4)} +# Escapes single quotes for shell command safety +function qquote(str) { + gsub(/'/, "'\\''", str) + return "'" str "'" +} + +# Converts hex string to decimal +function hex_to_dec(hex, dec, i, len, c, val) { + dec = 0 + len = length(hex) + for (i = 1; i <= len; i++) { + c = tolower(substr(hex, i, 1)) + if (c ~ /[0-9]/) val = int(c) + else if (c == "a") val = 10 + else if (c == "b") val = 11 + else if (c == "c") val = 12 + else if (c == "d") val = 13 + else if (c == "e") val = 14 + else if (c == "f") val = 15 + else continue + dec = dec * 16 + val + } + return dec +} + +# Reads a null-terminated string from memory +function read_str(addr, c, s) { + s = "" + addr = uint32(addr) + while ((c = MEM[addr++]) != 0) { + s = s sprintf("%c", c) + } + return s +} + +# Checks if path exists using POSIX test -e or test -L +function path_exists(path) { + return (system("test -e " qquote(path)) == 0 || system("test -L " qquote(path)) == 0) +} + +# Checks if path is a directory using POSIX test -d +function is_dir(path) { + return (system("test -d " qquote(path)) == 0) +} + +# Resolves relative path against dfd directory +function resolve_path(dfd, pathname, path) { + if (substr(pathname, 1, 1) == "/") { + return pathname + } + if (dfd == -100) { # AT_FDCWD + return pathname + } + if (dfd in FD_PATH) { + return FD_PATH[dfd] "/" pathname + } + return pathname +} + +# Populate timespec structure +# TV_SEC: 64-bit for clock_gettime64, 32-bit for clock_gettime +function get_system_time(time_arr, cmd, line) { + cmd = "date +%s%N" + if ((cmd | getline line) > 0) { + if (length(line) > 9 && line ~ /^[0-9]+$/) { + time_arr["sec"] = int(substr(line, 1, length(line) - 9)) + time_arr["nsec"] = int(substr(line, length(line) - 8)) + close(cmd) + return + } + } + close(cmd) + + # Fallback if %N is not supported + cmd = "date +%s" + if ((cmd | getline line) > 0) { + time_arr["sec"] = int(line) + } else { + time_arr["sec"] = 0 + } + close(cmd) + + if (time_arr["sec"] == LAST_SEC) { + VIRT_NSEC += 500000 + if (VIRT_NSEC >= 1000000000) { + VIRT_NSEC = 999999999 + } + } else { + LAST_SEC = time_arr["sec"] + VIRT_NSEC = 0 + } + time_arr["nsec"] = VIRT_NSEC +} + +# Fills stat structure in VM memory +function fill_stat_struct(path, addr, follow, cmd, line, parts, opt, mode_hex, mode_dec, size, atime, mtime, ctime, i) { + opt = follow ? "-L " : "" + cmd = "stat " opt "-c \"%f %s %X %Y %Z\" " qquote(path) + if ((cmd | getline line) > 0) { + split(line, parts) + mode_hex = parts[1] + mode_dec = hex_to_dec(mode_hex) + size = int(parts[2]) + atime = int(parts[3]) + mtime = int(parts[4]) + ctime = int(parts[5]) + + close(cmd) + + for (i = 0; i < 104; i++) { + MEM[addr + i] = 0 + } + + write_mem(addr + 0, 1, 8) + write_mem(addr + 8, 1, 8) + write_mem(addr + 16, mode_dec, 4) + write_mem(addr + 20, 1, 4) + write_mem(addr + 24, 1000, 4) + write_mem(addr + 28, 1000, 4) + write_mem(addr + 32, 0, 8) + write_mem(addr + 48, size, 8) + write_mem(addr + 56, 4096, 4) + write_mem(addr + 64, int((size + 511) / 512), 8) + write_mem(addr + 72, atime, 4) + write_mem(addr + 80, mtime, 4) + write_mem(addr + 88, ctime, 4) + + return 0 + } else { + close(cmd) + return -2 + } +} + +# Writes file descriptor cached data back to file +function write_fd_to_file(fd, path, chunk, i, size) { + path = FD_PATH[fd] + size = FD_SIZE[fd] + printf("") > path + close(path) + chunk = "" + for (i = 0; i < size; i++) { + chunk = chunk sprintf("%c", FD_DATA[fd, i]) + if (length(chunk) >= 1024) { + printf("%s", chunk) >> path + chunk = "" + } + } + if (length(chunk) > 0) { + printf("%s", chunk) >> path + } + close(path) +} + +function amach_exit(code, fd) { + for (fd in FD_PATH) { + if (FD_DIRTY[fd]) { + write_fd_to_file(fd) + } + } + exit(code) +} + # syscall emulation -function handle_ecall(callnum, a0, a1, a2, a3, a4, a5, i, l, buf) { - if(callnum == 63) { # sys_read - if(a0 == 0) { - getline buf < "/dev/tty" - l = length(buf); if(l > a2) l = a2 - for(i=0; i path + close(path) + } + } else { + if (!path_exists(path)) { + setreg(10, -2) # -ENOENT + return + } + } + + fd = NEXT_FD++ + FD_PATH[fd] = path + FD_FLAGS[fd] = flags + FD_OFFSET[fd] = 0 + FD_SIZE[fd] = 0 + FD_DIRTY[fd] = 0 + + if (is_dir(path)) { + cmd = "ls -a1 " qquote(path) + count = 0 + while ((cmd | getline line) > 0) { + if (line != "") { + FD_DIRENTS[fd, count++] = line + } + } + close(cmd) + FD_DIRENT_COUNT[fd] = count + } else { + access_mode = bw_and(flags, 3) + is_read = (access_mode == 0 || access_mode == 2) + is_write = (access_mode == 1 || access_mode == 2) + do_trunc = (is_write && bw_and(flags, 512)) # O_TRUNC (0x200) + + if (is_read && !do_trunc) { + cmd = "od -An -v -tu1 " qquote(path) + size = 0 + while ((cmd | getline line) > 0) { + split(line, arr) + for (i = 1; i in arr; i++) { + FD_DATA[fd, size++] = arr[i] + } + } + close(cmd) + FD_SIZE[fd] = size + } else if (do_trunc) { + printf("") > path + close(path) + } + } + setreg(10, fd) + + } else if (callnum == 57) { # sys_close + fd = a0 + if (fd >= 3 && (fd in FD_PATH)) { + if (FD_DIRTY[fd]) { + write_fd_to_file(fd) + } + delete FD_PATH[fd] + delete FD_FLAGS[fd] + delete FD_OFFSET[fd] + delete FD_SIZE[fd] + delete FD_DIRTY[fd] + if (fd in FD_DIRENT_COUNT) { + for (i = 0; i < FD_DIRENT_COUNT[fd]; i++) { + delete FD_DIRENTS[fd, i] + } + delete FD_DIRENT_COUNT[fd] + } else { + for (i = 0; i < FD_SIZE[fd]; i++) { + delete FD_DATA[fd, i] + } + } + setreg(10, 0) + } else { + setreg(10, -9) # -EBADF + } + + } else if (callnum == 61) { # sys_getdents64 + fd = a0 + if (!(fd in FD_DIRENT_COUNT)) { + setreg(10, -9) # -EBADF + } else { + bytes_written = 0 + idx = FD_OFFSET[fd] + count_limit = a2 + buf_addr = a1 + while (idx < FD_DIRENT_COUNT[fd]) { + name = FD_DIRENTS[fd, idx] + reclen = int((20 + length(name) + 7) / 8) * 8 + if (bytes_written + reclen > count_limit) { + if (bytes_written == 0) { + setreg(10, -22) # -EINVAL + return + } + break + } + write_mem(buf_addr + bytes_written, 1, 8) + write_mem(buf_addr + bytes_written + 8, idx + 1, 8) + write_mem(buf_addr + bytes_written + 16, reclen, 2) + full_path = FD_PATH[fd] "/" name + dtype = is_dir(full_path) ? 4 : 8 + write_mem(buf_addr + bytes_written + 18, dtype, 1) + for (i = 0; i < length(name); i++) { + MEM[buf_addr + bytes_written + 19 + i] = ord(substr(name, i + 1, 1)) + } + MEM[buf_addr + bytes_written + 19 + length(name)] = 0 + for (i = 19 + length(name) + 1; i < reclen; i++) { + MEM[buf_addr + bytes_written + i] = 0 + } + bytes_written += reclen + idx++ + } + FD_OFFSET[fd] = idx + setreg(10, bytes_written) + } + + } else if (callnum == 62) { # sys_lseek + fd = a0 + if (fd >= 3 && (fd in FD_PATH) && !(fd in FD_DIRENT_COUNT)) { + idx = FD_OFFSET[fd] + size = FD_SIZE[fd] + if (a2 == 0) idx = a1 # SEEK_SET + else if (a2 == 1) idx = idx + a1 # SEEK_CUR + else if (a2 == 2) idx = size + a1 # SEEK_END + else { + setreg(10, -22) # -EINVAL + return + } + if (idx < 0) { + setreg(10, -22) # -EINVAL + } else { + FD_OFFSET[fd] = idx + setreg(10, idx) + } + } else { + setreg(10, -9) # -EBADF + } + + } else if (callnum == 63) { # sys_read + fd = a0 + if (fd == 0) { + getline buf < "/dev/tty" + l = length(buf); if (l > a2) l = a2 + for (i = 0; i < l; i++) MEM[uint32(a1 + i)] = ord(substr(buf, i + 1, 1)) + setreg(10, l) + } else if (fd >= 3 && (fd in FD_PATH) && !(fd in FD_DIRENT_COUNT)) { + idx = FD_OFFSET[fd] + size = FD_SIZE[fd] + count = a2 + if (idx >= size) { + setreg(10, 0) + } else { + if (idx + count > size) count = size - idx + for (i = 0; i < count; i++) { + MEM[uint32(a1 + i)] = FD_DATA[fd, idx + i] + } + FD_OFFSET[fd] = idx + count + setreg(10, count) + } + } else { + setreg(10, -9) # -EBADF + } + + } else if (callnum == 64) { # sys_write + fd = a0 + if (fd == 1 || fd == 2) { + for (i = 0; i < a2; i++) printf("%c", MEM[uint32(a1 + i)]) + setreg(10, a2) + } else if (fd >= 3 && (fd in FD_PATH) && !(fd in FD_DIRENT_COUNT)) { + idx = FD_OFFSET[fd] + count = a2 + if (bw_and(FD_FLAGS[fd], 1024)) { # O_APPEND (0x400) + idx = FD_SIZE[fd] + } + for (i = 0; i < count; i++) { + FD_DATA[fd, idx + i] = MEM[uint32(a1 + i)] + } + FD_OFFSET[fd] = idx + count + if (FD_OFFSET[fd] > FD_SIZE[fd]) { + FD_SIZE[fd] = FD_OFFSET[fd] + } + FD_DIRTY[fd] = 1 + setreg(10, count) + } else { + setreg(10, -9) # -EBADF + } + + } else if (callnum == 78) { # sys_readlinkat + path = resolve_path(a0, read_str(a1)) + cmd = "readlink " qquote(path) + if ((cmd | getline line) > 0) { + close(cmd) + l = length(line) + if (l > a3) l = a3 + for (i = 0; i < l; i++) { + MEM[uint32(a2 + i)] = ord(substr(line, i + 1, 1)) + } + setreg(10, l) + } else { + close(cmd) + setreg(10, -22) # -EINVAL + } + + } else if (callnum == 79) { # sys_newfstatat + pathname = read_str(a1) + flags = a3 + if (pathname == "" && bw_and(flags, 4096)) { # AT_EMPTY_PATH + if (a0 == -100) path = "." + else if (a0 in FD_PATH) path = FD_PATH[a0] + else { + setreg(10, -9) # -EBADF + return + } + } else { + path = resolve_path(a0, pathname) + } + follow = (bw_and(flags, 256) == 0) # follow link if AT_SYMLINK_NOFOLLOW (0x100) is NOT set + status = fill_stat_struct(path, a2, follow) + setreg(10, status) + + } else if (callnum == 80) { # sys_fstat + fd = a0 + if (fd == 0) path = "/dev/stdin" + else if (fd == 1) path = "/dev/stdout" + else if (fd == 2) path = "/dev/stderr" + else if (fd >= 3 && (fd in FD_PATH)) path = FD_PATH[fd] + else { + setreg(10, -9) # -EBADF + return + } + status = fill_stat_struct(path, a1, 1) + setreg(10, status) + + } else if (callnum == 93 || callnum == 94) { # sys_exit / sys_exit_group + amach_exit(a0) + + } else if (callnum == 113 || callnum == 403) { # sys_clock_gettime / sys_clock_gettime64 + get_system_time(now_time) + if (callnum == 113) { + write_mem(a1, now_time["sec"], 4) + write_mem(a1 + 4, now_time["nsec"], 4) + } else { + write_mem(a1, now_time["sec"], 8) + write_mem(a1 + 8, now_time["nsec"], 4) + } + setreg(10, 0) + + } else if (callnum == 169) { # sys_gettimeofday + get_system_time(now_time) + write_mem(a0, now_time["sec"], 4) + write_mem(a0 + 4, int(now_time["nsec"] / 1000), 4) + setreg(10, 0) + + } else if (callnum == 278) { # sys_getrandom + buf_addr = a0 + buflen = a1 + for (i = 0; i < buflen; i++) { + MEM[uint32(buf_addr + i)] = int(rand() * 256) + } + setreg(10, buflen) + + } else { + trapout(sprintf("Unimplemented environment call %d at 0x%X", callnum, pc - 4)) + } } # instruction type executors @@ -384,6 +842,10 @@ BEGIN { RES_ADDR = -1 # reserved address for atomic ops LVA = int(LVA) pc = LVA + NEXT_FD = 3 + LAST_SEC = -1 + VIRT_NSEC = 0 + srand() } # Decimal memory collection section @@ -399,4 +861,9 @@ END { instr += 65536 * MEM[pc++] + 16777216 * MEM[pc++] amach_exec(instr) # decode and execute } + for(fd in FD_PATH) { + if(FD_DIRTY[fd]) { + write_fd_to_file(fd) + } + } } diff --git a/rv32imac_syscall_tests.bin b/rv32imac_syscall_tests.bin new file mode 100755 index 0000000..2488bbb Binary files /dev/null and b/rv32imac_syscall_tests.bin differ