diff --git a/examples/typephp-os/Makefile b/examples/typephp-os/Makefile index d94e78d1..451bb621 100644 --- a/examples/typephp-os/Makefile +++ b/examples/typephp-os/Makefile @@ -6,9 +6,16 @@ PAYLOAD_BIN := $(BUILD)/kernel64.bin PAYLOAD_OBJECT := $(OBJECT_DIR)/kernel64-payload.o KERNEL := $(BUILD)/typephp-os.elf DISK := $(BUILD)/typephp-os.img +USER_SH_ELF := $(BUILD)/sh.elf +USER_LS_ELF := $(BUILD)/ls.elf +USER_CD_ELF := $(BUILD)/cd.elf +USER_IMAGES := $(OBJECT_DIR)/sh-image.o $(OBJECT_DIR)/ls-image.o $(OBJECT_DIR)/cd-image.o BOOTFLAGS := -m32 -O2 -ffreestanding -fno-pic -fno-pie \ -fno-stack-protector -Wall -Wextra +USER_CFLAGS := -m64 -O2 -ffreestanding -fno-builtin -fno-pic -fno-pie \ + -fno-stack-protector -mno-red-zone -Wall -Wextra -Iuser +USER_LDFLAGS := -m elf_x86_64 -nostdlib -static -T user/linker.ld .PHONY: all payload objects disk run test clean @@ -19,7 +26,7 @@ payload: objects # Only the 32-bit bootstrap requires flags incompatible with the 64-bit Nano # payload. Ordinary .c/.cc/.S files stay in project.yml sources. -objects: $(BOOTSTRAP_OBJECT) +objects: $(BOOTSTRAP_OBJECT) $(USER_IMAGES) disk: $(DISK) @@ -36,6 +43,42 @@ $(OBJECT_DIR): $(BOOTSTRAP_OBJECT): freestanding/boot.S | $(OBJECT_DIR) gcc $(BOOTFLAGS) -c $< -o $@ +$(OBJECT_DIR)/sh.o: user/sh.c user/syscall.h | $(OBJECT_DIR) + gcc $(USER_CFLAGS) -c $< -o $@ + +$(OBJECT_DIR)/ls.o: user/ls.c user/syscall.h | $(OBJECT_DIR) + gcc $(USER_CFLAGS) -c $< -o $@ + +$(OBJECT_DIR)/cd.o: user/cd.c user/syscall.h | $(OBJECT_DIR) + gcc $(USER_CFLAGS) -c $< -o $@ + +$(USER_SH_ELF): $(OBJECT_DIR)/sh.o user/linker.ld | $(BUILD) + ld $(USER_LDFLAGS) --defsym USER_BASE=0x2000000 -o $@ $< + +$(USER_LS_ELF): $(OBJECT_DIR)/ls.o user/linker.ld | $(BUILD) + ld $(USER_LDFLAGS) --defsym USER_BASE=0x2100000 -o $@ $< + +$(USER_CD_ELF): $(OBJECT_DIR)/cd.o user/linker.ld | $(BUILD) + ld $(USER_LDFLAGS) --defsym USER_BASE=0x2200000 -o $@ $< + +$(OBJECT_DIR)/sh-image.o: $(USER_SH_ELF) | $(OBJECT_DIR) + objcopy -I binary -O elf64-x86-64 -B i386:x86-64 \ + --rename-section .data=.rodata.user_elf,alloc,load,readonly,data,contents \ + --redefine-sym _binary_build_sh_elf_start=typephp_user_sh_elf_start \ + --redefine-sym _binary_build_sh_elf_end=typephp_user_sh_elf_end $< $@ + +$(OBJECT_DIR)/ls-image.o: $(USER_LS_ELF) | $(OBJECT_DIR) + objcopy -I binary -O elf64-x86-64 -B i386:x86-64 \ + --rename-section .data=.rodata.user_elf,alloc,load,readonly,data,contents \ + --redefine-sym _binary_build_ls_elf_start=typephp_user_ls_elf_start \ + --redefine-sym _binary_build_ls_elf_end=typephp_user_ls_elf_end $< $@ + +$(OBJECT_DIR)/cd-image.o: $(USER_CD_ELF) | $(OBJECT_DIR) + objcopy -I binary -O elf64-x86-64 -B i386:x86-64 \ + --rename-section .data=.rodata.user_elf,alloc,load,readonly,data,contents \ + --redefine-sym _binary_build_cd_elf_start=typephp_user_cd_elf_start \ + --redefine-sym _binary_build_cd_elf_end=typephp_user_cd_elf_end $< $@ + $(KERNEL): payload objcopy -O binary $(PAYLOAD_ELF) $(PAYLOAD_BIN) objcopy -I binary -O elf32-i386 -B i386 \ diff --git a/examples/typephp-os/README.md b/examples/typephp-os/README.md index 88b545d9..a763c468 100644 --- a/examples/typephp-os/README.md +++ b/examples/typephp-os/README.md @@ -3,8 +3,11 @@ This is a long-running experimental x86_64 TypePHP operating system. It boots under QEMU and runs an ordinary TypePHP Nano program without linking hosted PHP, libc, or libstdc++. TypePHP implements the startup self-check, a custom -Zend class, the prime-number demo, and the resident application loop. Small C -and assembly layers provide the machine bootstrap and current kernel services. +Zend class, the prime-number demo, and filesystem services. Small C and +assembly layers provide the machine bootstrap, current kernel services, and +the temporary freestanding userspace programs. +The first userspace programs (`sh`, `ls`, and `cd`) are deliberately written +in freestanding C until tpc can target this small userspace ABI. The architectural rules and staged plan are maintained in [ROADMAP.md](ROADMAP.md). @@ -53,12 +56,14 @@ make make run ``` -The build produces two useful files: +The build produces these useful files: - `build/kernel64.elf`: the 64-bit TypePHP + ordinary Nano payload produced by tpc; - `build/typephp-os.elf`: the final Multiboot kernel accepted by QEMU; - `build/typephp-os.img`: a persistent 32 MiB FAT16 disk image. +- `build/sh.elf`, `build/ls.elf`, and `build/cd.elf`: independent ELF64 Ring-3 + programs built without libc. Run the automated serial-output smoke test with: @@ -66,12 +71,14 @@ Run the automated serial-output smoke test with: make test ``` -The Makefile compiles only the 32-bit Multiboot bootstrap externally because -its `-m32` ABI cannot participate in the 64-bit payload link. All ordinary +The Makefile compiles the 32-bit Multiboot bootstrap externally because its +`-m32` ABI cannot participate in the 64-bit payload link. It also builds the +three deliberately small freestanding C userspace ELF files. All ordinary 64-bit `.c`, `.cc`, and `.S` files remain in `project.yml` and use tpc's generic `c-flags`, `cxx-flags`, and `asm-flags`. Generic same-ABI prebuilt objects can -be supplied with `objects`; the architecture-changing bootstrap is instead -combined during the final packaging link. +be supplied with `objects`; this is how read-only copies of the user ELF files +are embedded for the kernel ELF loader. The architecture-changing bootstrap is +instead combined during the final packaging link. After tpc emits `kernel64.elf`, the Makefile uses `objcopy` to turn the payload into a raw binary and then an ELF32 data object. GNU ld combines that object @@ -88,7 +95,7 @@ qemu-system-x86_64 -m 128M \ -display none -serial stdio -monitor none -no-reboot -no-shutdown ``` -Press `Ctrl+A`, then `X`, to leave headless QEMU. +Press `Ctrl+C` to leave headless QEMU. ## Current capabilities @@ -123,5 +130,34 @@ chunks to a future physical page allocator remains later work. The first filesystem milestone deliberately supports only the FAT16 root directory and DOS 8.3 names. Nested path traversal, long filenames, timestamps, permissions, and a general block-device layer remain future work. -Network sockets, dynamic module loading, `include`/`require`/`eval`, and -external process execution remain unavailable. +Network sockets, dynamic module loading, `include`/`require`/`eval`, and PHP +APIs that execute host commands remain unavailable. + +## Single-task userspace + +After the TypePHP self-check, the kernel validates and loads `sh.elf`, installs +a 64-bit TSS and an IDT gate, and enters Ring 3 with `iretq`. The user pages at +32-36 MiB are marked user-accessible while kernel pages remain supervisor-only. +An `int 0x80` boundary currently provides synchronous `read`, `write`, `exec`, +`exit`, `getcwd`, `chdir`, and directory-list operations. Standard input and +output are backed by QEMU's COM1 serial console. + +The shell synchronously executes the separate `ls.elf` and `cd.elf` images. +Only one user context runs at a time: while a command is active, the kernel +keeps the shell register frame and restores it when the command calls `exit`. +Because this is a deliberately single-task model, the working directory is a +session-global property; a successful `cd` therefore remains visible after +control returns to the shell. + +Available commands are: + +```text +ls +cd DOCS +ls +cd .. +``` + +The current FAT16 limitation still applies: a root child such as `/DOCS` is a +valid working directory, but nested directory storage is not implemented, so +its listing currently contains only `.` and `..`. diff --git a/examples/typephp-os/ROADMAP.md b/examples/typephp-os/ROADMAP.md index 02eddc32..bdc2b356 100644 --- a/examples/typephp-os/ROADMAP.md +++ b/examples/typephp-os/ROADMAP.md @@ -41,11 +41,15 @@ behavior. directory/stat operations, and PHP's local file stream API. The current implementation is intentionally limited to root-level DOS 8.3 names; nested directories and long filenames are next. -6. **Native Class memory.** Exercise Wren GC through Zend MM and verify tracing +6. **Single-task userspace — first slice complete.** ELF64 validation/loading, + supervisor/user page separation, GDT/TSS, Ring-3 entry, synchronous + `int 0x80` system calls, COM1 standard I/O, saved parent context, and + independent freestanding C `sh`, `ls`, and `cd` programs. +7. **Native Class memory.** Exercise Wren GC through Zend MM and verify tracing of PHPX fields under sustained allocation. -7. **Kernel services.** Interrupt-driven timer, keyboard, physical-page +8. **Kernel services.** Interrupt-driven timer, keyboard, physical-page reclamation, and a capability-oriented native API. -8. **Packaging and CI.** Automate the two-stage ELF32/ELF64 build and QEMU boot +9. **Packaging and CI.** Automate the two-stage ELF32/ELF64 build and QEMU boot smoke test in GitHub Actions. Later architectures may provide different bootstraps and host ABI adapters. diff --git a/examples/typephp-os/freestanding/abi/typephp_os_abi.h b/examples/typephp-os/freestanding/abi/typephp_os_abi.h index 2a6cba14..390e5957 100644 --- a/examples/typephp-os/freestanding/abi/typephp_os_abi.h +++ b/examples/typephp-os/freestanding/abi/typephp_os_abi.h @@ -31,6 +31,13 @@ void typephp_os_panic(const char *message); /* Generic PHPX no-exception policy hook. */ void phpx_no_exception_abort(const char *message); +/* Minimal process/console/filesystem contracts used by the Ring-3 syscall + * boundary. The filesystem implementations continue to live in TypePHP. */ +void typephp_os_process_start(void); +long typephp_os_fs_path_type(const char *path); +long typephp_os_fs_list(const char *path, char *buffer, size_t capacity); +long typephp_os_console_read(void *buffer, size_t size); + #ifdef __cplusplus } #endif diff --git a/examples/typephp-os/freestanding/boot.S b/examples/typephp-os/freestanding/boot.S index bac5f374..f266379f 100644 --- a/examples/typephp-os/freestanding/boot.S +++ b/examples/typephp-os/freestanding/boot.S @@ -36,11 +36,13 @@ _start: # Identity-map the first GiB with 2 MiB pages. mov $pdp_table, %eax - or $0x3, %eax + # Upper paging levels must permit user traversal. Individual PDEs still + # decide whether a 2 MiB page is supervisor-only or visible to Ring 3. + or $0x7, %eax mov %eax, pml4_table mov $pd_table, %eax - or $0x3, %eax + or $0x7, %eax mov %eax, pdp_table mov $pd_table, %edi @@ -52,6 +54,11 @@ map_page: add $8, %edi loop map_page + # Reserve 32-36 MiB for the single user process image and stack. The + # remaining identity-mapped pages stay supervisor-only. + orl $0x4, pd_table + (16 * 8) + orl $0x4, pd_table + (17 * 8) + # Enable PAE and prepare the x87/SSE state before entering long mode. mov %cr0, %ecx and $0xfffffffb, %ecx diff --git a/examples/typephp-os/freestanding/bridge.cc b/examples/typephp-os/freestanding/bridge.cc index 9ce3c6ef..79cebfc7 100644 --- a/examples/typephp-os/freestanding/bridge.cc +++ b/examples/typephp-os/freestanding/bridge.cc @@ -19,6 +19,7 @@ extern "C" int typephp_os_disk_available(); extern "C" int typephp_os_disk_read_sector(uint32_t lba, unsigned char *data); extern "C" int typephp_os_disk_write_sector(uint32_t lba, const unsigned char *data); extern "C" int typephp_os_disk_flush(); +extern "C" void typephp_os_process_start(); extern "C" ZEND_NORETURN void phpx_no_exception_abort(const char *fallback) { @@ -138,3 +139,8 @@ php::Bool php_kernel_disk_flush() { return typephp_os_disk_flush() != 0; } + +void php_kernel_process_start() +{ + typephp_os_process_start(); +} diff --git a/examples/typephp-os/freestanding/filesystem_abi.cc b/examples/typephp-os/freestanding/filesystem_abi.cc index e1ee5527..bd86e780 100644 --- a/examples/typephp-os/freestanding/filesystem_abi.cc +++ b/examples/typephp-os/freestanding/filesystem_abi.cc @@ -155,6 +155,30 @@ php::Bool php_kernel_fs_install(php::Object filesystem) return installed_filesystem.isObject(); } +extern "C" long typephp_os_fs_path_type(const char *path) +{ + if (path == nullptr) { + return -EFAULT; + } + return static_cast(fs_type(php::Str(path))); +} + +extern "C" long typephp_os_fs_list(const char *path, char *buffer, size_t capacity) +{ + if (path == nullptr || (buffer == nullptr && capacity != 0)) { + return -EFAULT; + } + if (fs_type(php::Str(path)) != 2) { + return -ENOTDIR; + } + php::Str entries = fs_entries(php::Str(path)); + const size_t size = entries.length() < capacity ? entries.length() : capacity; + if (size != 0) { + std::memcpy(buffer, entries.data(), size); + } + return static_cast(size); +} + extern "C" int open(const char *path, int flags, ...) { if (path == nullptr) { @@ -179,7 +203,7 @@ extern "C" ssize_t read(int fd, void *buffer, size_t count) return -1; } if (fd == STDIN_FILENO) { - return 0; + return static_cast(typephp_os_console_read(buffer, count)); } const php::Int size = fs_fd_size(fd); if (size < 0) { diff --git a/examples/typephp-os/freestanding/kernel.c b/examples/typephp-os/freestanding/kernel.c index e73f9f6d..805609ca 100644 --- a/examples/typephp-os/freestanding/kernel.c +++ b/examples/typephp-os/freestanding/kernel.c @@ -44,6 +44,22 @@ static void serial_put(uint8_t value) outb(COM1, value); } +long typephp_os_console_read(void *buffer, unsigned long size) +{ + uint8_t *output = (uint8_t *) buffer; + if (output == 0 || size == 0) { + return 0; + } + /* COM1 is the standard input device for the headless QEMU target. Keep + * this synchronous: the process model intentionally has one foreground + * task and no scheduler yet. */ + while ((inb(COM1 + 5) & 0x01u) == 0) { + __asm__ volatile("pause"); + } + output[0] = inb(COM1); + return 1; +} + static void vga_scroll(void) { if (row < VGA_HEIGHT) { diff --git a/examples/typephp-os/freestanding/memory.c b/examples/typephp-os/freestanding/memory.c index 44dca15f..764583b2 100644 --- a/examples/typephp-os/freestanding/memory.c +++ b/examples/typephp-os/freestanding/memory.c @@ -9,7 +9,10 @@ enum { static const uint64_t CHUNK_SIZE = 2ul * 1024ul * 1024ul; /* Keep the complete TypePHP + PHP Nano payload away from the page allocator. */ -static const uint64_t KERNEL_RESERVED_END = 16ul * 1024ul * 1024ul; +/* Keep both the kernel payload and the fixed 32-36 MiB Ring-3 area away from + * the Zend arena. This is deliberately simple until a page-frame allocator + * owns user address-space placement. */ +static const uint64_t KERNEL_RESERVED_END = 40ul * 1024ul * 1024ul; static const uint64_t IDENTITY_MAP_END = 1024ul * 1024ul * 1024ul; typedef struct __attribute__((packed)) { diff --git a/examples/typephp-os/freestanding/process-entry.S b/examples/typephp-os/freestanding/process-entry.S new file mode 100644 index 00000000..d344550e --- /dev/null +++ b/examples/typephp-os/freestanding/process-entry.S @@ -0,0 +1,64 @@ +.section .text +.code64 + +.global typephp_os_syscall_entry +.type typephp_os_syscall_entry, @function +.extern typephp_os_syscall_dispatch +typephp_os_syscall_entry: + # Save every general-purpose register. The saved RAX slot is overwritten + # with the syscall result before the frame is restored. + push %rbx + push %rcx + push %rdx + push %rsi + push %rdi + push %rbp + push %r8 + push %r9 + push %r10 + push %r11 + push %r12 + push %r13 + push %r14 + push %r15 + push %rax + + mov %rsp, %rdi + call typephp_os_syscall_dispatch + mov %rax, (%rsp) + + pop %rax + pop %r15 + pop %r14 + pop %r13 + pop %r12 + pop %r11 + pop %r10 + pop %r9 + pop %r8 + pop %rbp + pop %rdi + pop %rsi + pop %rdx + pop %rcx + pop %rbx + iretq +.size typephp_os_syscall_entry, . - typephp_os_syscall_entry + +.global typephp_os_enter_user +.type typephp_os_enter_user, @function +typephp_os_enter_user: + # RDI = ELF entry, RSI = top of the user stack. Interrupts remain masked; + # int 0x80 is synchronous and the first process manager has no scheduler. + mov $0x1b, %ax + mov %ax, %ds + mov %ax, %es + pushq $0x1b + push %rsi + pushq $0x2 + pushq $0x23 + push %rdi + iretq +.size typephp_os_enter_user, . - typephp_os_enter_user + +.section .note.GNU-stack, "", @progbits diff --git a/examples/typephp-os/freestanding/process.c b/examples/typephp-os/freestanding/process.c new file mode 100644 index 00000000..8a12ac9f --- /dev/null +++ b/examples/typephp-os/freestanding/process.c @@ -0,0 +1,425 @@ +/* + +----------------------------------------------------------------------+ + | TypePHP OS | + +----------------------------------------------------------------------+ + | Single foreground process, ELF64 loader and int 0x80 syscall core. | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ +*/ + +#include "typephp_os_abi.h" + +#include +#include +#include + +enum { + USER_BEGIN = 32u * 1024u * 1024u, + USER_END = 36u * 1024u * 1024u, + USER_STACK = USER_END - 16u, + USER_CODE_SELECTOR = 0x23, + USER_DATA_SELECTOR = 0x1b, + KERNEL_CODE_SELECTOR = 0x08, + TSS_SELECTOR = 0x28, + IDT_SYSCALL = 0x80, + SYS_READ = 0, + SYS_WRITE = 1, + SYS_EXEC = 59, + SYS_EXIT = 60, + SYS_GETCWD = 79, + SYS_CHDIR = 80, + SYS_READDIR = 217, +}; + +enum { + ELF_PT_LOAD = 1, + ELF_ET_EXEC = 2, + ELF_MACHINE_X86_64 = 62, +}; + +typedef struct __attribute__((packed)) { + unsigned char ident[16]; + uint16_t type; + uint16_t machine; + uint32_t version; + uint64_t entry; + uint64_t phoff; + uint64_t shoff; + uint32_t flags; + uint16_t ehsize; + uint16_t phentsize; + uint16_t phnum; + uint16_t shentsize; + uint16_t shnum; + uint16_t shstrndx; +} elf64_header; + +typedef struct __attribute__((packed)) { + uint32_t type; + uint32_t flags; + uint64_t offset; + uint64_t vaddr; + uint64_t paddr; + uint64_t filesz; + uint64_t memsz; + uint64_t align; +} elf64_program_header; + +typedef struct __attribute__((packed)) { + uint16_t limit; + uint64_t base; +} descriptor_pointer; + +typedef struct __attribute__((packed)) { + uint16_t offset_low; + uint16_t selector; + uint8_t ist; + uint8_t attributes; + uint16_t offset_middle; + uint32_t offset_high; + uint32_t reserved; +} idt_gate; + +typedef struct __attribute__((packed)) { + uint32_t reserved0; + uint64_t rsp0; + uint64_t rsp1; + uint64_t rsp2; + uint64_t reserved1; + uint64_t ist[7]; + uint64_t reserved2; + uint16_t reserved3; + uint16_t iomap_base; +} task_state_segment; + +typedef struct { + uint64_t rax; + uint64_t r15; + uint64_t r14; + uint64_t r13; + uint64_t r12; + uint64_t r11; + uint64_t r10; + uint64_t r9; + uint64_t r8; + uint64_t rbp; + uint64_t rdi; + uint64_t rsi; + uint64_t rdx; + uint64_t rcx; + uint64_t rbx; + uint64_t rip; + uint64_t cs; + uint64_t rflags; + uint64_t rsp; + uint64_t ss; +} syscall_frame; + +typedef struct { + uint64_t pid; + int running; + char cwd[16]; + syscall_frame parent_frame; + int parent_waiting; +} process_state; + +extern const unsigned char typephp_user_sh_elf_start[]; +extern const unsigned char typephp_user_sh_elf_end[]; +extern const unsigned char typephp_user_ls_elf_start[]; +extern const unsigned char typephp_user_ls_elf_end[]; +extern const unsigned char typephp_user_cd_elf_start[]; +extern const unsigned char typephp_user_cd_elf_end[]; +extern void typephp_os_syscall_entry(void); +extern void typephp_os_enter_user(uint64_t entry, uint64_t stack); + +static uint64_t gdt[7] __attribute__((aligned(16))); +static idt_gate idt[256] __attribute__((aligned(16))); +static task_state_segment tss; +static unsigned char syscall_stack[64u * 1024u] __attribute__((aligned(16))); +static process_state foreground_process = {.pid = 1, .cwd = "/"}; + +static void panic(const char *message) +{ + typephp_os_write("Process panic: ", sizeof("Process panic: ") - 1); + typephp_os_panic(message); +} + +static int range_inside(uint64_t address, uint64_t size, uint64_t begin, uint64_t end) +{ + return address >= begin && size <= end - begin && address <= end - size; +} + +static int user_buffer(const void *pointer, size_t size) +{ + return range_inside((uint64_t) (uintptr_t) pointer, size, USER_BEGIN, USER_END); +} + +static size_t bounded_user_string(const char *string, size_t maximum) +{ + size_t length = 0; + if (!user_buffer(string, 1)) { + return (size_t) -1; + } + while (length < maximum && user_buffer(string + length, 1) && string[length] != '\0') { + ++length; + } + return length < maximum && user_buffer(string + length, 1) ? length : (size_t) -1; +} + +static void install_tss_descriptor(uint64_t base, uint32_t limit) +{ + gdt[5] = ((uint64_t) (limit & 0xffffu)) + | ((base & 0xffffffu) << 16u) + | (UINT64_C(0x89) << 40u) + | ((uint64_t) ((limit >> 16u) & 0x0fu) << 48u) + | (((base >> 24u) & 0xffu) << 56u); + gdt[6] = base >> 32u; +} + +static void install_descriptor_tables(void) +{ + descriptor_pointer pointer; + uintptr_t handler = (uintptr_t) typephp_os_syscall_entry; + + memset(gdt, 0, sizeof(gdt)); + gdt[1] = UINT64_C(0x00af9a000000ffff); + gdt[2] = UINT64_C(0x00cf92000000ffff); + gdt[3] = UINT64_C(0x00cff2000000ffff); + gdt[4] = UINT64_C(0x00affa000000ffff); + memset(&tss, 0, sizeof(tss)); + tss.rsp0 = (uint64_t) (uintptr_t) (syscall_stack + sizeof(syscall_stack)); + tss.iomap_base = sizeof(tss); + install_tss_descriptor((uint64_t) (uintptr_t) &tss, sizeof(tss) - 1u); + + pointer.limit = sizeof(gdt) - 1u; + pointer.base = (uint64_t) (uintptr_t) gdt; + __asm__ volatile("lgdt %0" : : "m"(pointer) : "memory"); + __asm__ volatile( + "mov %[data], %%ax\n" + "mov %%ax, %%ds\n" + "mov %%ax, %%es\n" + "mov %%ax, %%ss\n" + "ltr %[tss]\n" + : + : [data] "i"(0x10), [tss] "r"((uint16_t) TSS_SELECTOR) + : "rax", "memory"); + + memset(idt, 0, sizeof(idt)); + idt[IDT_SYSCALL].offset_low = handler & 0xffffu; + idt[IDT_SYSCALL].selector = KERNEL_CODE_SELECTOR; + idt[IDT_SYSCALL].attributes = 0xee; /* present, DPL 3, interrupt gate */ + idt[IDT_SYSCALL].offset_middle = (handler >> 16u) & 0xffffu; + idt[IDT_SYSCALL].offset_high = handler >> 32u; + pointer.limit = sizeof(idt) - 1u; + pointer.base = (uint64_t) (uintptr_t) idt; + __asm__ volatile("lidt %0" : : "m"(pointer) : "memory"); +} + +static uint64_t load_user_elf(const unsigned char *image, const unsigned char *image_end) +{ + const size_t image_size = (size_t) (image_end - image); + const elf64_header *header; + + if (image_size < sizeof(elf64_header)) { + panic("truncated user ELF\n"); + } + header = (const elf64_header *) image; + if (header->ident[0] != 0x7f || header->ident[1] != 'E' + || header->ident[2] != 'L' || header->ident[3] != 'F' + || header->ident[4] != 2 || header->ident[5] != 1 + || header->type != ELF_ET_EXEC || header->machine != ELF_MACHINE_X86_64 + || header->phentsize != sizeof(elf64_program_header)) { + panic("invalid user ELF64 header\n"); + } + if (!range_inside(header->phoff, + (uint64_t) header->phnum * sizeof(elf64_program_header), 0, image_size)) { + panic("invalid user ELF program table\n"); + } + + for (uint16_t index = 0; index < header->phnum; ++index) { + const elf64_program_header *segment = (const elf64_program_header *) + (image + header->phoff + (uint64_t) index * sizeof(*segment)); + if (segment->type != ELF_PT_LOAD) { + continue; + } + if (segment->filesz > segment->memsz + || !range_inside(segment->offset, segment->filesz, 0, image_size) + || !range_inside(segment->vaddr, segment->memsz, USER_BEGIN, USER_END)) { + panic("invalid user ELF load segment\n"); + } + memcpy((void *) (uintptr_t) segment->vaddr, image + segment->offset, segment->filesz); + memset((void *) (uintptr_t) (segment->vaddr + segment->filesz), + 0, segment->memsz - segment->filesz); + } + if (!range_inside(header->entry, 1, USER_BEGIN, USER_END)) { + panic("invalid user ELF entry\n"); + } + return header->entry; +} + +static int command_image( + const char *name, + const unsigned char **image, + const unsigned char **image_end) +{ + if (strcmp(name, "ls") == 0) { + *image = typephp_user_ls_elf_start; + *image_end = typephp_user_ls_elf_end; + return 1; + } + if (strcmp(name, "cd") == 0) { + *image = typephp_user_cd_elf_start; + *image_end = typephp_user_cd_elf_end; + return 1; + } + return 0; +} + +static long syscall_getcwd(char *buffer, size_t size) +{ + size_t length = strlen(foreground_process.cwd) + 1u; + if (size < length || !user_buffer(buffer, size)) { + return -1; + } + memcpy(buffer, foreground_process.cwd, length); + return (long) length; +} + +static long syscall_chdir(const char *path) +{ + char normalized[16]; + size_t length = bounded_user_string(path, sizeof(normalized)); + if (length == (size_t) -1 || length == 0) { + return -1; + } + if ((length == 1 && path[0] == '/') + || (length == 2 && path[0] == '.' && path[1] == '.')) { + normalized[0] = '/'; + normalized[1] = '\0'; + } else if (length == 1 && path[0] == '.') { + return 0; + } else { + size_t source = path[0] == '/' ? 1u : 0u; + size_t name_length = length - source; + if (name_length == 0 || name_length > 12u) { + return -1; + } + normalized[0] = '/'; + memcpy(normalized + 1, path + source, name_length); + normalized[name_length + 1u] = '\0'; + } + if (typephp_os_fs_path_type(normalized) != 2) { + return -1; + } + memcpy(foreground_process.cwd, normalized, strlen(normalized) + 1u); + return 0; +} + +static long syscall_readdir(const char *path, char *buffer, size_t capacity) +{ + char resolved[16]; + const char *directory = foreground_process.cwd; + if (path != 0) { + size_t length = bounded_user_string(path, sizeof(resolved)); + if (length == (size_t) -1) { + return -1; + } + if (length != 0) { + memcpy(resolved, path, length + 1u); + directory = resolved; + } + } + if (!user_buffer(buffer, capacity)) { + return -1; + } + return typephp_os_fs_list(directory, buffer, capacity); +} + +static long syscall_exec(syscall_frame *frame, const char *name, const char *argument) +{ + const unsigned char *image; + const unsigned char *image_end; + char command[8]; + size_t name_length; + + if (foreground_process.parent_waiting) { + return -1; + } + name_length = bounded_user_string(name, sizeof(command)); + if (name_length == (size_t) -1 || name_length == 0) { + return -1; + } + memcpy(command, name, name_length + 1u); + if (!command_image(command, &image, &image_end)) { + return -1; + } + if (argument != 0 && bounded_user_string(argument, 64) == (size_t) -1) { + return -1; + } + + memcpy(&foreground_process.parent_frame, frame, sizeof(*frame)); + foreground_process.parent_waiting = 1; + foreground_process.pid = 2; + frame->rip = load_user_elf(image, image_end); + frame->rsp = 35u * 1024u * 1024u - 16u; + frame->rdi = (uint64_t) (uintptr_t) argument; + frame->rsi = 0; + frame->rdx = 0; + return 0; +} + +static long syscall_exit(syscall_frame *frame, long status) +{ + if (!foreground_process.parent_waiting) { + foreground_process.running = 0; + typephp_os_panic("shell process exited\n"); + } + memcpy(frame, &foreground_process.parent_frame, sizeof(*frame)); + foreground_process.parent_waiting = 0; + foreground_process.pid = 1; + return status; +} + +long typephp_os_syscall_dispatch(syscall_frame *frame) +{ + switch (frame->rax) { + case SYS_READ: + if (frame->rdi != 0 || !user_buffer((void *) frame->rsi, frame->rdx)) { + return -1; + } + return typephp_os_console_read((void *) frame->rsi, frame->rdx); + case SYS_WRITE: + if ((frame->rdi != 1 && frame->rdi != 2) + || !user_buffer((void *) frame->rsi, frame->rdx)) { + return -1; + } + typephp_os_write((const char *) frame->rsi, frame->rdx); + return (long) frame->rdx; + case SYS_EXEC: + return syscall_exec(frame, (const char *) frame->rdi, (const char *) frame->rsi); + case SYS_GETCWD: + return syscall_getcwd((char *) frame->rdi, frame->rsi); + case SYS_CHDIR: + return syscall_chdir((const char *) frame->rdi); + case SYS_READDIR: + return syscall_readdir((const char *) frame->rdi, + (char *) frame->rsi, frame->rdx); + case SYS_EXIT: + return syscall_exit(frame, (long) frame->rdi); + default: + return -1; + } +} + +void typephp_os_process_start(void) +{ + uint64_t entry; + memset((void *) (uintptr_t) USER_BEGIN, 0, USER_END - USER_BEGIN); + entry = load_user_elf(typephp_user_sh_elf_start, typephp_user_sh_elf_end); + install_descriptor_tables(); + foreground_process.running = 1; + typephp_os_write("Process 1: sh.elf (Ring 3)\n", + sizeof("Process 1: sh.elf (Ring 3)\n") - 1); + typephp_os_enter_user(entry, USER_STACK); + panic("Ring-3 entry returned\n"); +} diff --git a/examples/typephp-os/project.yml b/examples/typephp-os/project.yml index 75c4a080..95f44c61 100644 --- a/examples/typephp-os/project.yml +++ b/examples/typephp-os/project.yml @@ -3,8 +3,10 @@ mode: bin sources: - src - freestanding/kernel64-entry.S + - freestanding/process-entry.S - freestanding/kernel.c - freestanding/memory.c + - freestanding/process.c - freestanding/time.c - freestanding/ata.c - freestanding/zend-allocator.c @@ -14,6 +16,10 @@ sources: - freestanding/abi/cxx.cpp - freestanding/abi/unimplemented.S - thirdparty/openlibm/src +objects: + - build/objects/sh-image.o + - build/objects/ls-image.o + - build/objects/cd-image.o build-dir: build/generated output: build/kernel64.elf cxx-std: c++17 diff --git a/examples/typephp-os/src/filesystem.php b/examples/typephp-os/src/filesystem.php index 3d3155c7..29109d56 100644 --- a/examples/typephp-os/src/filesystem.php +++ b/examples/typephp-os/src/filesystem.php @@ -287,9 +287,15 @@ final class KernelFileSystem public function entries(string $path): string { - if (!$this->initialize() || ($path !== '/' && $path !== '' && $path !== '.')) { + if (!$this->initialize() || $this->pathType($path) !== 2) { return ''; } + if ($path !== '/' && $path !== '' && $path !== '.') { + /* The first FAT16 slice has root directory entries but no nested + * traversal yet. A root child directory is nevertheless a valid + * working directory and contains its logical dot entries. */ + return ".\n..\n"; + } $volume = $this->volume->toObject(Fat16Volume::class); return ".\n..\n" . $volume->rootEntryNames(); } diff --git a/examples/typephp-os/src/hardware.stub.php b/examples/typephp-os/src/hardware.stub.php index 671c7fe3..8031fc24 100644 --- a/examples/typephp-os/src/hardware.stub.php +++ b/examples/typephp-os/src/hardware.stub.php @@ -32,3 +32,7 @@ function kernel_disk_write_sector(int $lba, string $data): bool { return false; #[NativeFunction] function kernel_disk_flush(): bool { return false; } + +/** Load the embedded ELF shell and transfer control to its Ring-3 entry. */ +#[NativeFunction] +function kernel_process_start(): void {} diff --git a/examples/typephp-os/src/kernel.php b/examples/typephp-os/src/kernel.php index be2498c8..edf075f8 100644 --- a/examples/typephp-os/src/kernel.php +++ b/examples/typephp-os/src/kernel.php @@ -176,11 +176,7 @@ function main(): void runMathSelfCheck(); runFilesystemSelfCheck(); runPrimeDemo(100); - $greeting = new KernelGreeting(); - while (true) { - sleep(2); - kernel_write($greeting->render(date('Y-m-d H:i:s')), 15); - kernel_put_char(10, 15); - } + echo $greeting->render(date('Y-m-d H:i:s')), "\n"; + kernel_process_start(); } diff --git a/examples/typephp-os/tools/test-qemu.sh b/examples/typephp-os/tools/test-qemu.sh index 177bb626..11643582 100755 --- a/examples/typephp-os/tools/test-qemu.sh +++ b/examples/typephp-os/tools/test-qemu.sh @@ -15,7 +15,8 @@ if timeout 8 qemu-system-x86_64 \ -no-reboot \ -no-shutdown \ -device isa-debug-exit,iobase=0xf4,iosize=0x04 \ - >"${log}" 2>&1; then + >"${log}" 2>&1 \ + <<< $'\nls\ncd DOCS\nls\ncd ..\nls\n'; then status=0 else status=$? @@ -42,4 +43,10 @@ grep -q "Calculate primes: 0-100" "${log}" grep -q "Prime count: 25" "${log}" grep -q "Prime list: 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97" "${log}" grep -Eq '^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2} Hello TypePHP-OS![[:space:]]*$' "${log}" +grep -q "Process 1: sh.elf (Ring 3)" "${log}" +grep -q "TypePHP-OS user shell" "${log}" +grep -q "Ring 3 confirmed" "${log}" +grep -Fq 'typephp-os:/$ ' "${log}" +grep -Fq 'typephp-os:/DOCS$ ' "${log}" +grep -q '^HELLO.TXT' "${log}" cat "${log}" diff --git a/examples/typephp-os/user/cd.c b/examples/typephp-os/user/cd.c new file mode 100644 index 00000000..55ff0af8 --- /dev/null +++ b/examples/typephp-os/user/cd.c @@ -0,0 +1,11 @@ +#include "syscall.h" + +void _start(const char *argument) +{ + const char *path = argument != 0 && argument[0] != '\0' ? argument : "/"; + if (typephp_syscall(TYPEPHP_SYS_CHDIR, (long) path, 0, 0) != 0) { + typephp_write("cd: no such directory\n"); + typephp_exit(1); + } + typephp_exit(0); +} diff --git a/examples/typephp-os/user/linker.ld b/examples/typephp-os/user/linker.ld new file mode 100644 index 00000000..7c15bfd4 --- /dev/null +++ b/examples/typephp-os/user/linker.ld @@ -0,0 +1,15 @@ +OUTPUT_FORMAT(elf64-x86-64) +OUTPUT_ARCH(i386:x86-64) +ENTRY(_start) + +SECTIONS +{ + . = USER_BASE + SIZEOF_HEADERS; + + .text : { *(.text .text.*) } + .rodata : { *(.rodata .rodata.*) } + .data : { *(.data .data.*) } + .bss (NOLOAD) : { *(COMMON) *(.bss .bss.*) } + + /DISCARD/ : { *(.comment) *(.eh_frame*) *(.note*) } +} diff --git a/examples/typephp-os/user/ls.c b/examples/typephp-os/user/ls.c new file mode 100644 index 00000000..af1ba941 --- /dev/null +++ b/examples/typephp-os/user/ls.c @@ -0,0 +1,15 @@ +#include "syscall.h" + +void _start(const char *argument) +{ + char entries[512]; + const char *path = argument != 0 && argument[0] != '\0' ? argument : 0; + long length = typephp_syscall( + TYPEPHP_SYS_READDIR, (long) path, (long) entries, sizeof(entries)); + if (length < 0) { + typephp_write("ls: cannot read directory\n"); + typephp_exit(1); + } + typephp_write_bytes(entries, (size_t) length); + typephp_exit(0); +} diff --git a/examples/typephp-os/user/sh.c b/examples/typephp-os/user/sh.c new file mode 100644 index 00000000..0760ecb7 --- /dev/null +++ b/examples/typephp-os/user/sh.c @@ -0,0 +1,92 @@ +#include "syscall.h" + +static int string_equal(const char *left, const char *right) +{ + while (*left != '\0' && *left == *right) { + ++left; + ++right; + } + return *left == *right; +} + +static size_t read_line(char *buffer, size_t capacity) +{ + size_t length = 0; + while (capacity > 1) { + char character = 0; + if (typephp_syscall(TYPEPHP_SYS_READ, 0, (long) &character, 1) != 1) { + continue; + } + if (character == '\r' || character == '\n') { + typephp_write("\n"); + break; + } + if (character == 8 || character == 127) { + if (length != 0) { + --length; + typephp_write("\b \b"); + } + continue; + } + if (character >= 32 && character < 127) { + buffer[length++] = character; + --capacity; + typephp_write_bytes(&character, 1); + } + } + buffer[length] = '\0'; + return length; +} + +static void show_prompt(void) +{ + char cwd[16]; + typephp_write("typephp-os:"); + if (typephp_syscall(TYPEPHP_SYS_GETCWD, (long) cwd, sizeof(cwd), 0) > 0) { + typephp_write(cwd); + } else { + typephp_write("?"); + } + typephp_write("$ "); +} + +void _start(void) +{ + char line[80]; + unsigned short code_selector; + __asm__ volatile("mov %%cs, %0" : "=r"(code_selector)); + if ((code_selector & 3u) != 3u) { + typephp_write("sh: Ring-3 transition failed\n"); + typephp_exit(1); + } + typephp_write("TypePHP-OS user shell\n"); + typephp_write("Ring 3 confirmed\n"); + typephp_write("Commands: ls, cd \n"); + for (;;) { + char *argument; + show_prompt(); + if (read_line(line, sizeof(line)) == 0) { + continue; + } + argument = line; + while (*argument != '\0' && *argument != ' ') { + ++argument; + } + if (*argument != '\0') { + *argument++ = '\0'; + while (*argument == ' ') { + ++argument; + } + } + if (string_equal(line, "ls") || string_equal(line, "cd")) { + if (typephp_syscall(TYPEPHP_SYS_EXEC, + (long) line, (long) argument, 0) < 0) { + typephp_write("sh: unable to execute command\n"); + } + } else { + typephp_write("sh: command not found: "); + typephp_write(line); + typephp_write("\n"); + } + } +} diff --git a/examples/typephp-os/user/syscall.h b/examples/typephp-os/user/syscall.h new file mode 100644 index 00000000..90d60a27 --- /dev/null +++ b/examples/typephp-os/user/syscall.h @@ -0,0 +1,57 @@ +#ifndef TYPEPHP_OS_USER_SYSCALL_H +#define TYPEPHP_OS_USER_SYSCALL_H + +typedef unsigned long size_t; + +enum { + TYPEPHP_SYS_READ = 0, + TYPEPHP_SYS_WRITE = 1, + TYPEPHP_SYS_EXEC = 59, + TYPEPHP_SYS_EXIT = 60, + TYPEPHP_SYS_GETCWD = 79, + TYPEPHP_SYS_CHDIR = 80, + TYPEPHP_SYS_READDIR = 217, +}; + +static inline long typephp_syscall( + long number, long first, long second, long third) +{ + register long rax __asm__("rax") = number; + register long rdi __asm__("rdi") = first; + register long rsi __asm__("rsi") = second; + register long rdx __asm__("rdx") = third; + __asm__ volatile("int $0x80" + : "+a"(rax) + : "D"(rdi), "S"(rsi), "d"(rdx) + : "rcx", "r11", "memory"); + return rax; +} + +static inline size_t typephp_strlen(const char *value) +{ + size_t length = 0; + while (value[length] != '\0') { + ++length; + } + return length; +} + +static inline void typephp_write_bytes(const char *value, size_t length) +{ + (void) typephp_syscall(TYPEPHP_SYS_WRITE, 1, (long) value, (long) length); +} + +static inline void typephp_write(const char *value) +{ + typephp_write_bytes(value, typephp_strlen(value)); +} + +static inline void typephp_exit(long status) +{ + (void) typephp_syscall(TYPEPHP_SYS_EXIT, status, 0, 0); + for (;;) { + __asm__ volatile("pause"); + } +} + +#endif