16 KiB
TypePHP freestanding OS experiment
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 filesystem services. Small C and
assembly layers provide the machine bootstrap, current kernel services, and
the temporary freestanding userspace programs.
The userspace shell and commands are deliberately written in freestanding C
until tpc can target this small userspace ABI. They already use standard
main(), libc-shaped APIs, and a Linux-style process-entry stack.
The architectural rules and staged plan are maintained in ROADMAP.md.
Architecture
The source tree is split by execution role:
boot/ Make-built startup assembly and linker scripts
kernel/*.php TypePHP kernel implementation
kernel/core/ Native kernel implementation compiled by tpc
kernel/core/abi/ Freestanding C/POSIX and C++ compatibility boundary
user/ Ring-3 commands, TypePHP projects, headers, and runtime
Additional TypePHP kernel files stay directly under kernel/ until a concrete
subsystem boundary justifies another directory.
There is no kernel-specific tpc mode or reduced PHP Nano profile. The 64-bit payload is compiled with the normal command:
./bin/tpc.php --nano examples/typephp-os/project.yml
This composes the complete php-nano and PHPX source manifests. The TypePHP OS
project owns the freestanding boundary under kernel/core/abi: implemented
C/POSIX and C++ ABI functions live there, while APIs required for linking but
not implemented by the kernel are exported as panic stubs. Consequently an
unsupported operation fails immediately with its ABI symbol instead of
silently returning fabricated data. Sockets are outside the current scope.
On a fresh checkout, run examples/typephp-os/tools/fetch-thirdparty.sh before
invoking tpc directly; make payload performs both steps automatically.
Cross-project portability uses general feature switches only:
PHP_NANO_NO_LIBCasks the embedding host to provide Nano's clock, sleep, and entropy hooks;PHPX_NO_EXCEPTIONroutes PHPX exception propagation to the host abort hook;PHPX_NO_RTTIselects PHPX's non-RTTI checked cast policy;TYPEPHP_NO_MAINlets an embedding host provide the process/kernel entry.
None of these switches refers to TypePHP OS or to a kernel build. Ordinary Nano and PHPX builds preserve their hosted defaults.
The payload uses PHP's original Zend allocator, GC, strings, HashTables, objects, classes, exceptions, and built-in extension registration. Generated projects and built-ins are registered through their ordinary MINIT paths; the only excluded capability is dynamic PHP execution through ZendVM.
Build and run
Required host tools are TypePHP's PHP/Composer dependencies, GCC/G++, GNU
binutils, GNU make, dosfstools, mtools, and qemu-system-x86_64.
Third-party source is not committed. make invokes
tools/fetch-thirdparty.sh, which downloads fixed
archives or explicit source manifests and verifies their SHA-256 checksums according to
THIRDPARTY.md.
From this directory, build and boot with:
make
make run
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 containing the independently built shell and command executables;build/sh.elfand the command ELF files: independent ELF64 Ring-3 programs linked with the shared TypePHP-OS runtime rather than a hosted libc.build/tnhello.elf: a full TypePHP + PHPX + PHP Nano Ring-3 executable generated by tpc fromuser/nano/hello.php.build/free.elf: the tpc-generated TypePHP memory-reporting command.- matching
build/*.elf.debugfiles: host-side symbols removed from runtime ELF files; keep these beside the binaries when debugging with GDB. build/libtypephp-os.a: the sole reusable userspace platform archive linked by both C programs and every tpc-generated TypePHP program. Static archive member selection keeps small C commands from pulling in the PHP Nano host.build/libcompiler-rt-builtins.a: selected upstream LLVM 128-bit integer compiler helpers linked into freestanding kernel and userspace programs.
Run the automated serial-output smoke test with:
make test
To build and audit the restricted hosted Toybox applet set, run
make thirdparty-smoke. This is an integration audit, not part of the boot
image.
The Makefile compiles the startup sources under boot/ directly. Its 256 KiB
early kernel stack is kept separate from the adjacent identity-map tables. The 32-bit
Multiboot bootstrap cannot participate in the 64-bit payload link; the 64-bit
entry object is injected into tpc's final link through the generic objects
setting. The Makefile also builds the
deliberately small freestanding C userspace ELF files and installs them into
the FAT16 image with mcopy. All ordinary
kernel .php, .c, .cc, and .S files live under kernel/, remain in
project.yml, and use tpc's generic
c-flags, cxx-flags, and asm-flags. The user executables are not linked
into the kernel payload. The architecture-changing bootstrap is 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
with the 32-bit bootstrap. The bootstrap is loaded through Multiboot v1,
identity-maps the first GiB, enters x86_64 long mode, and transfers control to
the payload linked at 2 MiB.
To invoke QEMU manually:
qemu-system-x86_64 -m 512M \
-kernel examples/typephp-os/build/typephp-os.elf \
-drive file=examples/typephp-os/build/typephp-os.img,format=raw,if=ide,index=0 \
-display none -serial stdio -monitor none -no-reboot -no-shutdown
Press Ctrl+C to leave headless QEMU.
Current capabilities
The bootstrap passes the Multiboot memory map to the 64-bit kernel. The
physical layer reserves the complete kernel image and divides the selected
usable region into two pools. Upstream zend_alloc obtains aligned 2 MiB
chunks from one pool through the project-owned posix_memalign(); the other
pool is a recyclable 4 KiB physical-page allocator used by page tables, ELF
images, stacks, brk(), and mmap(). C++ global new and delete, including
std::vector allocations, continue to use Zend MM.
The QEMU smoke test currently verifies:
- 64-bit TypePHP scalar and control-flow execution;
- real Zend allocation, GC, string, array, object, class, and exception data;
- PHPX
Variant,Str,Array, custom classes, and a typedstd::vector<int>; - OpenLibm implementations of the ordinary double-precision math ABI;
- LLVM compiler-rt signed and unsigned 128-bit integer helpers, exercised by a real Ring-3 command that requires the helper symbols at link time;
- RTC-derived UTC time exposed to userspace through
time(); - Linux-compatible
uname(2)shared by theunamecommand and PHP'sphp_uname()implementation; - ATA PIO sector I/O, a 32 MiB direct-mapped read/write-through cache, and a TypePHP FAT16 implementation with DOS 8.3 files, nested traversal and mutation, including automatic directory-chain growth;
- PHP's unchanged plain file stream and
php_stat()paths, includingfile_put_contents(),file_get_contents(),is_dir(),mkdir(), andscandir(), forwarded through the POSIX ABI to TypePHP; - normal PHP output through multi-argument
echoandPHPWRITE; - a TypePHP prime calculation for 0–100;
- disk-backed ELF64 loading, Linux-style
argc/argvstartup, and libc-shaped userspace calls, including a complete PHP Nano program generated by tpc; - an independently compiled TypePHP
freecommand reporting usable RAM, kernel Zend-arena usage, physical-page usage, the kernel reservation, and block-cache storage through a small private memory-information ABI; - a separate x86-64 address space for the resident shell and every command,
4 KiB user mappings, a guarded 64 KiB stack, NX/WP enforcement, and ELF
RX/RWsegment permissions; - Linux-numbered
brk, anonymous privatemmap,mprotect, andmunmapcalls backed by recyclable physical pages; - Linux-compatible file metadata and persistence calls (
stat,lstat,fstat,newfstatat,access,faccessat,fsync,fdatasync,truncate, andftruncate), exercised from both C and PHP Nano; - Linux-compatible directory descriptors and
getdents64, exposed to C and PHP Nano throughopendir,readdir,rewinddir, andclosedir; - the first
fcntlsubset (F_GETFD,F_SETFD,F_GETFL,F_SETFL), including trackedFD_CLOEXEC/O_NONBLOCKstate and effectiveO_APPEND; - single-task PID/TID and root UID/GID queries plus
gettimeofday,clock_gettime,clock_getres, andexit_group; - dynamic command discovery and safe rejection of malformed ELF files;
- recovery from invalid opcodes, cross-address-space reads, and writes to read-only mappings without losing the resident shell;
- user-mode file creation, reading, writing, seeking, closing, removal, and nested directory creation/removal through the TypePHP FAT16 implementation.
The loadable 64-bit payload is followed by a large zero-filled block cache in kernel BSS. The first 64 MiB is kept away from both allocators for the kernel payload, cache, bootstrap state, and early host arena. Userspace has a separate 0x40000000–0x50000000 virtual region. Entire Zend chunks are not yet returned to the physical-page pool.
The current filesystem deliberately supports only DOS 8.3 names. Reading,
directory enumeration, stat, executable loading, and file/directory mutation
traverse nested FAT16 directory chains. Full directories grow by linking a new
cluster. The current ATA cache is synchronous and write-through. Its 65,536
direct-mapped sector entries cover the complete current 32 MiB disk image, so
repeated Nano ELF loads no longer return to ATA PIO. It is still a block cache,
not a general virtual-filesystem page cache, and every Nano process still
performs its own runtime initialization. Rename
currently stays within one parent directory; cross-directory rename,
replacement semantics, long filenames, timestamps, permissions, and a general
block-device layer remain future work.
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 opens /BIN/SH.ELF from FAT16,
validates and loads its PT_LOAD segments, installs a 64-bit TSS, IDT and
SYSCALL MSRs, and enters Ring 3 with iretq. The resident shell and each transient
command receive an independent CR3. Their 1–1.25 GiB virtual window is composed
from recyclable 4 KiB pages; the shared identity-mapped kernel remains
supervisor-only. The CPU has write protection and no-execute enabled, and the
loader applies the final ELF PF_W and PF_X permissions after copying each
segment.
The native x86-64 SYSCALL boundary provides synchronous read, write, close,
lseek, openat, exit/exit_group, getcwd, chdir, mkdir, rmdir,
unlink, file stat/access/persistence/truncation families, fixed identity
queries, time, gettimeofday, clock_gettime, clock_getres, brk,
anonymous private mmap, mprotect, munmap, getdents64, the initial
fcntl flag operations, fixed-console ioctl(TIOCGWINSZ), and private
spawn and same-directory rename operations. The former private directory-list
syscall has been removed; ls and PHP Nano use the standard directory ABI. Standard
input and output are backed by QEMU's COM1 serial console. Syscall numbers are
shared by the kernel and userspace through typephp_os_syscall.h.
Entry immediately switches from the untrusted user RSP to a dedicated kernel
stack. Return uses iretq, allowing synchronous spawn/exit to replace the
complete saved user context.
Userspace programs now expose standard C main(argc, argv) functions. A shared
crt0.S consumes a Linux-style initial stack containing argc, argv, an
empty environment, and a terminating auxiliary-vector entry. The normative
userspace compatibility rules are recorded in user/README.md:
the long-term target is GCC/glibc-compatible userspace, and Linux syscall
numbers are reserved for genuinely compatible semantics.
The kernel, PHP Nano/PHPX sources, shared userspace runtime, bootstrap, and
small C commands are release-built with -O2. Because the host compiler may
enable glibc fortification by default, freestanding builds explicitly disable
_FORTIFY_SOURCE; TypePHP-OS supplies and audits its own libc/POSIX boundary.
Runtime ELF files are stripped before installation into FAT16, while adjacent
.debug files retain their host-side symbols for GDB. Hardware access, CPU
context entry, and the syscall trampoline remain C or assembly. Filesystem
policy, FAT16, startup checks, demonstrations, and the free command's
calculations and presentation are implemented in TypePHP.
The shell synchronously executes separate command ELF files from FAT16.
Entering name requests /BIN/<name>.ELF; the kernel validates the ELF header,
program table, file bounds, target address range, and entry point before
loading it. There is no compiled-in command whitelist, so a compatible command
can be installed in the disk image without relinking the kernel. Commands may
reuse the same virtual addresses, but never the shell's mappings: every launch
creates and later destroys an independent address space and stack. A page-count
invariant detects leaked command pages on both normal exit and faults. 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. fork, clone, execve, wait, pipes,
threads, scheduling signals, and job control are intentional non-goals. The
private synchronous shell launch syscall must not be exposed as POSIX
execve() semantics.
TNHELLO.ELF demonstrates the intended TypePHP userspace path. It is built by
the ordinary tpc Nano pipeline, links the complete php-nano and PHPX manifests,
links the reusable libtypephp-os.a platform archive, and uses the same ELF
loader and Ring-3 syscall boundary as the C commands.
Its startup code converts the Linux-style initial stack into argc and
argv, and php_uname() calls the standard extension's unchanged info.c
implementation through a userspace uname() ABI that reports TypePHP-OS.
Run it from the shell with:
tnhello alpha beta
The current user ABI intentionally implements only the calls needed by the
demonstration. Missing libc/POSIX operations are linkable panic stubs and
terminate the command with an explicit unsupported TypePHP-OS user ABI
message; they do not silently emulate success.
Available commands are:
ls
date
uname
uname -a
pwd
echo Hello TypePHP
cat HELLO.TXT
write NOTE.TXT Hello from Ring 3
cat NOTE.TXT
touch EMPTY.TXT
mkdir TMP
rm NOTE.TXT
rmdir TMP
mv OLD.TXT NEW.TXT
systest
cd DOCS
pwd
ls
cd ..
The development-only fault, vmfault, and wrfault commands exercise an
invalid opcode, an attempted read from the shell's virtual address, and an
attempted write to a read-only mmap() page. The kernel reports each Ring-3
exception, destroys the faulty address space, and restores the shell. The
memtest command exercises sbrk(), anonymous mapping, protection changes,
and unmapping. systest covers file metadata, persistence/truncation, fixed
single-task identities, and wall/monotonic clock ABIs. User exceptions for
divide errors, breakpoints, bounds, invalid
opcodes, invalid TSS/segments, stack faults, general-protection faults, and
page faults have IDT entries. A fault raised in Ring 0 still causes an
immediate kernel panic.
Executables now live in the conventional /BIN directory and both ls /BIN
and cd /BIN use TypePHP's FAT16 cluster-chain traversal. The same path
resolution is used by cat, write, touch, mkdir, rm, and rmdir, so
nested directory trees are mutable from userspace.