feat(kernel): implement single-task userspace with ELF64 loader and syscalls

- Add user-accessible page mapping (32-36 MiB) in boot.S with proper permission bits
- Implement ELF64 loader and process management in new process.c module
- Add syscall infrastructure including int 0x80 handler in process-entry.S
- Create userspace programs (sh, ls, cd) with dedicated linker script
- Integrate filesystem path type checking and directory listing functions
- Reserve memory regions for user processes and update kernel memory layout
- Add console input/output support for userspace applications
- Update kernel main loop to start user process after initialization
- Modify Makefile to build and embed userspace ELF images as kernel objects
- Document new userspace capabilities and limitations in README.md
master
韩天峰 4 weeks ago
parent 515132b6b8
commit 0cfa577db1
  1. 45
      examples/typephp-os/Makefile
  2. 56
      examples/typephp-os/README.md
  3. 10
      examples/typephp-os/ROADMAP.md
  4. 7
      examples/typephp-os/freestanding/abi/typephp_os_abi.h
  5. 11
      examples/typephp-os/freestanding/boot.S
  6. 6
      examples/typephp-os/freestanding/bridge.cc
  7. 26
      examples/typephp-os/freestanding/filesystem_abi.cc
  8. 16
      examples/typephp-os/freestanding/kernel.c
  9. 5
      examples/typephp-os/freestanding/memory.c
  10. 64
      examples/typephp-os/freestanding/process-entry.S
  11. 425
      examples/typephp-os/freestanding/process.c
  12. 6
      examples/typephp-os/project.yml
  13. 8
      examples/typephp-os/src/filesystem.php
  14. 4
      examples/typephp-os/src/hardware.stub.php
  15. 8
      examples/typephp-os/src/kernel.php
  16. 9
      examples/typephp-os/tools/test-qemu.sh
  17. 11
      examples/typephp-os/user/cd.c
  18. 15
      examples/typephp-os/user/linker.ld
  19. 15
      examples/typephp-os/user/ls.c
  20. 92
      examples/typephp-os/user/sh.c
  21. 57
      examples/typephp-os/user/syscall.h

@ -6,9 +6,16 @@ PAYLOAD_BIN := $(BUILD)/kernel64.bin
PAYLOAD_OBJECT := $(OBJECT_DIR)/kernel64-payload.o PAYLOAD_OBJECT := $(OBJECT_DIR)/kernel64-payload.o
KERNEL := $(BUILD)/typephp-os.elf KERNEL := $(BUILD)/typephp-os.elf
DISK := $(BUILD)/typephp-os.img 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 \ BOOTFLAGS := -m32 -O2 -ffreestanding -fno-pic -fno-pie \
-fno-stack-protector -Wall -Wextra -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 .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 # Only the 32-bit bootstrap requires flags incompatible with the 64-bit Nano
# payload. Ordinary .c/.cc/.S files stay in project.yml sources. # payload. Ordinary .c/.cc/.S files stay in project.yml sources.
objects: $(BOOTSTRAP_OBJECT) objects: $(BOOTSTRAP_OBJECT) $(USER_IMAGES)
disk: $(DISK) disk: $(DISK)
@ -36,6 +43,42 @@ $(OBJECT_DIR):
$(BOOTSTRAP_OBJECT): freestanding/boot.S | $(OBJECT_DIR) $(BOOTSTRAP_OBJECT): freestanding/boot.S | $(OBJECT_DIR)
gcc $(BOOTFLAGS) -c $< -o $@ 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 $(KERNEL): payload
objcopy -O binary $(PAYLOAD_ELF) $(PAYLOAD_BIN) objcopy -O binary $(PAYLOAD_ELF) $(PAYLOAD_BIN)
objcopy -I binary -O elf32-i386 -B i386 \ objcopy -I binary -O elf32-i386 -B i386 \

@ -3,8 +3,11 @@
This is a long-running experimental x86_64 TypePHP operating system. It boots 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 under QEMU and runs an ordinary TypePHP Nano program without linking hosted
PHP, libc, or libstdc++. TypePHP implements the startup self-check, a custom 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 Zend class, the prime-number demo, and filesystem services. Small C and
and assembly layers provide the machine bootstrap and current kernel services. 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 The architectural rules and staged plan are maintained in
[ROADMAP.md](ROADMAP.md). [ROADMAP.md](ROADMAP.md).
@ -53,12 +56,14 @@ make
make run 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 - `build/kernel64.elf`: the 64-bit TypePHP + ordinary Nano payload produced by
tpc; tpc;
- `build/typephp-os.elf`: the final Multiboot kernel accepted by QEMU; - `build/typephp-os.elf`: the final Multiboot kernel accepted by QEMU;
- `build/typephp-os.img`: a persistent 32 MiB FAT16 disk image. - `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: Run the automated serial-output smoke test with:
@ -66,12 +71,14 @@ Run the automated serial-output smoke test with:
make test make test
``` ```
The Makefile compiles only the 32-bit Multiboot bootstrap externally because The Makefile compiles the 32-bit Multiboot bootstrap externally because its
its `-m32` ABI cannot participate in the 64-bit payload link. All ordinary `-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 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 `c-flags`, `cxx-flags`, and `asm-flags`. Generic same-ABI prebuilt objects can
be supplied with `objects`; the architecture-changing bootstrap is instead be supplied with `objects`; this is how read-only copies of the user ELF files
combined during the final packaging link. 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 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 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 -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 ## 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 The first filesystem milestone deliberately supports only the FAT16 root
directory and DOS 8.3 names. Nested path traversal, long filenames, directory and DOS 8.3 names. Nested path traversal, long filenames,
timestamps, permissions, and a general block-device layer remain future work. timestamps, permissions, and a general block-device layer remain future work.
Network sockets, dynamic module loading, `include`/`require`/`eval`, and Network sockets, dynamic module loading, `include`/`require`/`eval`, and PHP
external process execution remain unavailable. 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 `..`.

@ -41,11 +41,15 @@ behavior.
directory/stat operations, and PHP's local file stream API. The current directory/stat operations, and PHP's local file stream API. The current
implementation is intentionally limited to root-level DOS 8.3 names; implementation is intentionally limited to root-level DOS 8.3 names;
nested directories and long filenames are next. 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. 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. 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. smoke test in GitHub Actions.
Later architectures may provide different bootstraps and host ABI adapters. Later architectures may provide different bootstraps and host ABI adapters.

@ -31,6 +31,13 @@ void typephp_os_panic(const char *message);
/* Generic PHPX no-exception policy hook. */ /* Generic PHPX no-exception policy hook. */
void phpx_no_exception_abort(const char *message); 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 #ifdef __cplusplus
} }
#endif #endif

@ -36,11 +36,13 @@ _start:
# Identity-map the first GiB with 2 MiB pages. # Identity-map the first GiB with 2 MiB pages.
mov $pdp_table, %eax 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 %eax, pml4_table
mov $pd_table, %eax mov $pd_table, %eax
or $0x3, %eax or $0x7, %eax
mov %eax, pdp_table mov %eax, pdp_table
mov $pd_table, %edi mov $pd_table, %edi
@ -52,6 +54,11 @@ map_page:
add $8, %edi add $8, %edi
loop map_page 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. # Enable PAE and prepare the x87/SSE state before entering long mode.
mov %cr0, %ecx mov %cr0, %ecx
and $0xfffffffb, %ecx and $0xfffffffb, %ecx

@ -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_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_write_sector(uint32_t lba, const unsigned char *data);
extern "C" int typephp_os_disk_flush(); 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) 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; return typephp_os_disk_flush() != 0;
} }
void php_kernel_process_start()
{
typephp_os_process_start();
}

@ -155,6 +155,30 @@ php::Bool php_kernel_fs_install(php::Object filesystem)
return installed_filesystem.isObject(); return installed_filesystem.isObject();
} }
extern "C" long typephp_os_fs_path_type(const char *path)
{
if (path == nullptr) {
return -EFAULT;
}
return static_cast<long>(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<long>(size);
}
extern "C" int open(const char *path, int flags, ...) extern "C" int open(const char *path, int flags, ...)
{ {
if (path == nullptr) { if (path == nullptr) {
@ -179,7 +203,7 @@ extern "C" ssize_t read(int fd, void *buffer, size_t count)
return -1; return -1;
} }
if (fd == STDIN_FILENO) { if (fd == STDIN_FILENO) {
return 0; return static_cast<ssize_t>(typephp_os_console_read(buffer, count));
} }
const php::Int size = fs_fd_size(fd); const php::Int size = fs_fd_size(fd);
if (size < 0) { if (size < 0) {

@ -44,6 +44,22 @@ static void serial_put(uint8_t value)
outb(COM1, 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) static void vga_scroll(void)
{ {
if (row < VGA_HEIGHT) { if (row < VGA_HEIGHT) {

@ -9,7 +9,10 @@ enum {
static const uint64_t CHUNK_SIZE = 2ul * 1024ul * 1024ul; static const uint64_t CHUNK_SIZE = 2ul * 1024ul * 1024ul;
/* Keep the complete TypePHP + PHP Nano payload away from the page allocator. */ /* 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; static const uint64_t IDENTITY_MAP_END = 1024ul * 1024ul * 1024ul;
typedef struct __attribute__((packed)) { typedef struct __attribute__((packed)) {

@ -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

@ -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 <stddef.h>
#include <stdint.h>
#include <string.h>
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");
}

@ -3,8 +3,10 @@ mode: bin
sources: sources:
- src - src
- freestanding/kernel64-entry.S - freestanding/kernel64-entry.S
- freestanding/process-entry.S
- freestanding/kernel.c - freestanding/kernel.c
- freestanding/memory.c - freestanding/memory.c
- freestanding/process.c
- freestanding/time.c - freestanding/time.c
- freestanding/ata.c - freestanding/ata.c
- freestanding/zend-allocator.c - freestanding/zend-allocator.c
@ -14,6 +16,10 @@ sources:
- freestanding/abi/cxx.cpp - freestanding/abi/cxx.cpp
- freestanding/abi/unimplemented.S - freestanding/abi/unimplemented.S
- thirdparty/openlibm/src - thirdparty/openlibm/src
objects:
- build/objects/sh-image.o
- build/objects/ls-image.o
- build/objects/cd-image.o
build-dir: build/generated build-dir: build/generated
output: build/kernel64.elf output: build/kernel64.elf
cxx-std: c++17 cxx-std: c++17

@ -287,9 +287,15 @@ final class KernelFileSystem
public function entries(string $path): string public function entries(string $path): string
{ {
if (!$this->initialize() || ($path !== '/' && $path !== '' && $path !== '.')) { if (!$this->initialize() || $this->pathType($path) !== 2) {
return ''; 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); $volume = $this->volume->toObject(Fat16Volume::class);
return ".\n..\n" . $volume->rootEntryNames(); return ".\n..\n" . $volume->rootEntryNames();
} }

@ -32,3 +32,7 @@ function kernel_disk_write_sector(int $lba, string $data): bool { return false;
#[NativeFunction] #[NativeFunction]
function kernel_disk_flush(): bool { return false; } 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 {}

@ -176,11 +176,7 @@ function main(): void
runMathSelfCheck(); runMathSelfCheck();
runFilesystemSelfCheck(); runFilesystemSelfCheck();
runPrimeDemo(100); runPrimeDemo(100);
$greeting = new KernelGreeting(); $greeting = new KernelGreeting();
while (true) { echo $greeting->render(date('Y-m-d H:i:s')), "\n";
sleep(2); kernel_process_start();
kernel_write($greeting->render(date('Y-m-d H:i:s')), 15);
kernel_put_char(10, 15);
}
} }

@ -15,7 +15,8 @@ if timeout 8 qemu-system-x86_64 \
-no-reboot \ -no-reboot \
-no-shutdown \ -no-shutdown \
-device isa-debug-exit,iobase=0xf4,iosize=0x04 \ -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 status=0
else else
status=$? status=$?
@ -42,4 +43,10 @@ grep -q "Calculate primes: 0-100" "${log}"
grep -q "Prime count: 25" "${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 -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 -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}" cat "${log}"

@ -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);
}

@ -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*) }
}

@ -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);
}

@ -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 <directory>\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");
}
}
}

@ -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
Loading…
Cancel
Save