diff --git a/README-CN.md b/README-CN.md index 730707a8..627052ed 100644 --- a/README-CN.md +++ b/README-CN.md @@ -304,6 +304,12 @@ sources: - path: src/windows if: PHP_OS_FAMILY == "Windows" +# 由项目自身的原生构建流程预编译。 +objects: + - native/build/startup.o + - path: native/build/platform.obj + if: PHP_OS_FAMILY == "Windows" + ignore: - src/experimental @@ -327,6 +333,13 @@ ext-deps: `PHP_VERSION`、`PHP_VERSION_ID` 和 `PHP_OS_FAMILY`。命令行参数优先于 YAML 中的同名配置。原生链接依赖应写入 `link-libs`;`ext-deps` 会生成 `ZEND_MOD_REQUIRED`,缺少所需 PHP 扩展时由 Zend 拒绝加载模块。 +通用的 `objects` 列表会把已有 `.o`/`.obj` 文件直接加入链接步骤。TypePHP +不会重新编译这些文件;原生编译器、目标架构、编译参数和增量构建均由项目负责。 +使用目标通用参数的原生文件仍应放入 `sources`;仅当某个编译单元需要不同参数且 +生成的对象与最终目标 ABI 兼容时,才应预编译后放入 `objects`。例如 `-m32` +生成的对象不能直接链接到 64 位目标,必须由项目在 tpc 产出 ELF 后另行封装。 +项目级 `cxx-flags`、`c-flags`、`asm-flags` 和 `ld-flags` 分别应用于 +C++、C、汇编和链接命令。 构建目录保存生成的 C++、依赖对象和预编译头缓存。复用同一个构建目录可以显著加快 增量构建;仅在确实需要重编 PHPX 公共对象时使用 `--force`。 diff --git a/README.md b/README.md index 67ab134d..8a33e4a6 100644 --- a/README.md +++ b/README.md @@ -336,6 +336,12 @@ sources: - path: src/windows if: PHP_OS_FAMILY == "Windows" +# Precompiled by the project's external native build. +objects: + - native/build/startup.o + - path: native/build/platform.obj + if: PHP_OS_FAMILY == "Windows" + ignore: - src/experimental @@ -360,6 +366,15 @@ directory; conditional entries support `PHP_VERSION`, `PHP_VERSION_ID`, and `PHP_OS_FAMILY`. CLI arguments override their YAML counterparts. Native linker dependencies belong in `link-libs`; `ext-deps` writes `ZEND_MOD_REQUIRED` entries so Zend can reject loading when a required PHP extension is missing. +The generic `objects` list adds existing `.o`/`.obj` files directly to the +link step. TypePHP never recompiles these files; the project owns their native +compiler, architecture, flags, and incremental build. Keep native files that +use the common target options in `sources`; use `objects` for separately built +translation units that remain ABI-compatible with the final target. A `-m32` +object cannot be linked into a 64-bit target and requires a project-owned +post-link packaging step after tpc emits its ELF. +Project-wide `cxx-flags`, `c-flags`, `asm-flags`, and `ld-flags` are applied to +C++, C, assembler, and link commands respectively. The build directory contains generated C++, dependency objects, and the precompiled-header cache. Reusing it makes incremental builds much faster; diff --git a/docs/en/COMPILER_CLI.md b/docs/en/COMPILER_CLI.md index d175f070..a71df34f 100644 --- a/docs/en/COMPILER_CLI.md +++ b/docs/en/COMPILER_CLI.md @@ -117,6 +117,28 @@ Corresponding long options: When a `project.yml` is passed, command-line arguments take precedence over same-named settings in the YAML. For the project file format, see the user documentation and the project configuration parser in the code. +### Precompiled object files + +A project can add object files produced by an external native toolchain as +generic link inputs: + +```yaml +objects: + - build/startup.o + - path: build/platform.obj + if: PHP_OS_FAMILY == "Windows" +``` + +Paths are resolved relative to `project.yml` and accept the same conditions as +`sources`. `tpc` only loads and links `.o`/`.obj` files; it does not recompile +their C, C++, or assembly sources. Native sources using the project's common +options should remain in `sources`; `objects` is intended for separately built +translation units that remain ABI-compatible with the final target. Objects +built with `-m32` cannot be linked into a 64-bit target and need a separate +project-owned packaging step after tpc emits its ELF. +Use `cxx-flags`, `c-flags`, `asm-flags`, and `ld-flags` for project-wide C++, +C, assembler, and linker options. + ### PHP Extension Dependencies When a program depends on other PHP extensions, the required modules can be written into the Zend module dependency table: diff --git a/docs/zh-cn/COMPILER_CLI.md b/docs/zh-cn/COMPILER_CLI.md index 04fa874b..d52f0998 100644 --- a/docs/zh-cn/COMPILER_CLI.md +++ b/docs/zh-cn/COMPILER_CLI.md @@ -117,6 +117,25 @@ TypePHP 和 PHPX 的最低运行时版本均为 PHP 8.4。`--php-version` 与实 传入 `project.yml` 时,命令行参数优先于 YAML 中的同名配置。项目文件格式参见用户文档及代码中的项目配置解析器。 +### 预编译对象文件 + +项目可以把外部工具链生成的对象文件作为通用链接输入: + +```yaml +objects: + - build/startup.o + - path: build/platform.obj + if: PHP_OS_FAMILY == "Windows" +``` + +路径相对于 `project.yml` 解析,并支持与 `sources` 相同的条件表达式。 +`tpc` 只负责装载并链接 `.o`/`.obj`,不会重新编译其对应的 C、C++ 或汇编源码。 +使用项目通用编译参数的原生源码应继续放在 `sources`;`objects` 主要用于单独编译 +且与最终目标 ABI 兼容的编译单元。`-m32` 对象不能直接链接到 64 位目标,需要在 +tpc 产出 ELF 后由项目自己的构建流程另行封装。 +使用 `cxx-flags`、`c-flags`、`asm-flags` 和 `ld-flags` 分别设置项目级 +C++、C、汇编和链接参数。 + ### PHP 扩展依赖 程序依赖其他 PHP 扩展时,可以将必需模块写入 Zend 模块依赖表: diff --git a/examples/typephp-os/.gitignore b/examples/typephp-os/.gitignore new file mode 100644 index 00000000..84c048a7 --- /dev/null +++ b/examples/typephp-os/.gitignore @@ -0,0 +1 @@ +/build/ diff --git a/examples/typephp-os/Makefile b/examples/typephp-os/Makefile new file mode 100644 index 00000000..3ad4cf8c --- /dev/null +++ b/examples/typephp-os/Makefile @@ -0,0 +1,45 @@ +BUILD := build +OBJECT_DIR := $(BUILD)/objects +BOOTSTRAP_OBJECT := $(OBJECT_DIR)/bootstrap.o +PAYLOAD_ELF := $(BUILD)/kernel64.elf +PAYLOAD_BIN := $(BUILD)/kernel64.bin +PAYLOAD_OBJECT := $(OBJECT_DIR)/kernel64-payload.o +KERNEL := $(BUILD)/typephp-os.elf + +BOOTFLAGS := -m32 -O2 -ffreestanding -fno-pic -fno-pie \ + -fno-stack-protector -Wall -Wextra + +.PHONY: all payload objects run test clean + +all: $(KERNEL) + +payload: objects + cd ../.. && ./bin/tpc.php --nano examples/typephp-os/project.yml + +# 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) + +$(OBJECT_DIR): + mkdir -p $@ + +$(BOOTSTRAP_OBJECT): freestanding/boot.S | $(OBJECT_DIR) + gcc $(BOOTFLAGS) -c $< -o $@ + +$(KERNEL): payload + objcopy -O binary $(PAYLOAD_ELF) $(PAYLOAD_BIN) + objcopy -I binary -O elf32-i386 -B i386 \ + --rename-section .data=.payload,alloc,load,readonly,data,contents \ + $(PAYLOAD_BIN) $(PAYLOAD_OBJECT) + ld -m elf_i386 -T freestanding/linker.ld -o $@ \ + $(BOOTSTRAP_OBJECT) $(PAYLOAD_OBJECT) + +run: all + qemu-system-x86_64 -m 128M -kernel $(KERNEL) \ + -display none -serial stdio -monitor none -no-reboot -no-shutdown + +test: all + bash tools/test-qemu.sh $(KERNEL) $(BUILD)/qemu.log + +clean: + rm -rf $(BUILD) diff --git a/examples/typephp-os/README.md b/examples/typephp-os/README.md new file mode 100644 index 00000000..3864fc0f --- /dev/null +++ b/examples/typephp-os/README.md @@ -0,0 +1,118 @@ +# 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 the resident application loop. Small C +and assembly layers provide the machine bootstrap and current kernel services. + +The architectural rules and staged plan are maintained in +[ROADMAP.md](ROADMAP.md). + +## Architecture + +There is no kernel-specific tpc mode or reduced PHP Nano profile. The 64-bit +payload is compiled with the normal command: + +```shell +./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 `freestanding/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. Filesystem APIs are the next subsystem to +replace with real implementations; sockets are outside the current scope. + +Cross-project portability uses general feature switches only: + +- `PHP_NANO_NO_LIBC` asks the embedding host to provide Nano's clock, sleep, + and entropy hooks; +- `PHPX_NO_EXCEPTION` routes PHPX exception propagation to the host abort hook; +- `PHPX_NO_RTTI` selects PHPX's non-RTTI checked cast policy; +- `TYPEPHP_NO_MAIN` lets 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, and `qemu-system-x86_64`. + +From this directory, build and boot with: + +```shell +make +make run +``` + +The build produces two 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. + +Run the automated serial-output smoke test with: + +```shell +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 +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. + +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: + +```shell +qemu-system-x86_64 -m 128M \ + -kernel examples/typephp-os/build/typephp-os.elf \ + -display none -serial stdio -monitor none -no-reboot -no-shutdown +``` + +Press `Ctrl+A`, then `X`, 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, selects mapped usable RAM, +and installs the remaining arena behind the project-owned `posix_memalign()`. +Upstream `zend_alloc` then obtains and subdivides aligned 2 MiB chunks. C++ +global `new` and `delete`, including `std::vector` allocations, 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 typed + `std::vector`; +- built-in date handling and `sleep()` through Zend Bridge; +- a TypePHP prime calculation for 0–100; +- a resident loop that prints the RTC-derived UTC time and + `Hello TypePHP-OS!` every two seconds. + +The 64-bit payload currently occupies about 7 MiB, so the first 16 MiB is +reserved before physical memory is handed to Zend MM. Returning entire Zend +chunks to a future physical page allocator remains later work. + +Filesystem ABI entries already link as explicit panic stubs. The next +milestone will replace them with an in-kernel filesystem and make PHP's local +file stream operations real. Network sockets, dynamic module loading, +`include`/`require`/`eval`, and external process execution remain unavailable. diff --git a/examples/typephp-os/ROADMAP.md b/examples/typephp-os/ROADMAP.md new file mode 100644 index 00000000..ddb4f9a1 --- /dev/null +++ b/examples/typephp-os/ROADMAP.md @@ -0,0 +1,49 @@ +# TypePHP OS roadmap + +This directory is a long-running experiment. Every milestone must remain +bootable in QEMU and must not change ordinary TypePHP, PHPX, or PHP Nano +behavior. + +## Architecture rules + +- The project is an ordinary `tpc --nano` consumer. tpc has no kernel option, + and PHP Nano has no kernel source profile. +- PHP and Composer remain build-time tools; the kernel never links `libphp`. +- Keep copied php-src C/H files unchanged and compile the complete Nano source + manifest. TypePHP OS owns the required libc/POSIX and C++ ABI boundary. +- Missing ABI operations must be linkable panic stubs. Replace each stub with + a real kernel service before exposing the corresponding PHP capability. +- Cross-project changes must use generally useful portability contracts such + as `PHP_NANO_NO_LIBC`, `PHPX_NO_EXCEPTION`, and `PHPX_NO_RTTI`; product-specific + conditionals are not allowed in PHP Nano or PHPX. +- Ordinary same-ABI source files and flags belong in `project.yml`. Translation + units that require incompatible per-file options are built by Make and may + be linked through tpc's generic `objects` facility when their ABI matches. + Architecture packaging and ELF/binary conversion happen after tpc emits the + 64-bit ELF. +- Every completed milestone must pass the serial-output QEMU smoke test and + leave the 64-bit payload with no undefined symbols. + +## Milestones + +1. **Boot and scalar AOT — complete.** Multiboot v1 bootstrap, x86_64 long + mode, VGA/COM1 output, TypePHP scalars, functions, and control flow. +2. **Memory and Zend containers — complete.** Physical arena, TypePHP OS ABI + layer, original `zend_alloc`, `zend_gc`, `zend_string`, `zend_hash`, zval + destruction, and generated TypePHP arrays/strings through PHPX. +3. **Zend object model — complete for the current demo.** Original object + store, class entries, inheritance, interfaces, handlers, PHP exception + classes, generated user classes, construction, properties, and destruction. +4. **Time service — complete.** CMOS wall clock, monotonic host hook, built-in + date formatting, and the standard extension's `sleep()` path. +5. **Filesystem — next.** Physical block device, a small filesystem, file + descriptors, directory/stat operations, and PHP's local file stream API. +6. **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 + reclamation, and a capability-oriented native API. +8. **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. +Generated TypePHP code and PHPX values remain 64-bit on every target. diff --git a/examples/typephp-os/freestanding/abi/cxx.cpp b/examples/typephp-os/freestanding/abi/cxx.cpp new file mode 100644 index 00000000..9cd56b3d --- /dev/null +++ b/examples/typephp-os/freestanding/abi/cxx.cpp @@ -0,0 +1,203 @@ +/* + +----------------------------------------------------------------------+ + | TypePHP OS | + +----------------------------------------------------------------------+ + | Freestanding C++ allocation ABI backed by Zend MM. | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ +*/ + +extern "C" { +#include +#include "typephp_os_abi.h" +} + +#include +#include +#include + +extern "C" { +void *__dso_handle = &__dso_handle; + +int __cxa_atexit(void (*destructor)(void *), void *argument, void *dso) +{ + (void) destructor; + (void) argument; + (void) dso; + return 0; +} +} + +namespace { + +void *zend_allocate(std::size_t size) +{ + void *memory = emalloc(size == 0 ? 1 : size); + if (memory == nullptr) { + typephp_os_panic("C++ allocation failed"); + } + return memory; +} + +void *zend_allocate_aligned(std::size_t size, std::size_t alignment) +{ + if (alignment <= alignof(std::max_align_t)) { + return zend_allocate(size); + } + const std::size_t extra = alignment - 1 + sizeof(void *); + if (size > static_cast(-1) - extra) { + typephp_os_panic("C++ aligned allocation overflow"); + } + void *base = zend_allocate(size + extra); + const auto start = reinterpret_cast(base) + sizeof(void *); + const auto aligned = (start + alignment - 1) & ~(static_cast(alignment) - 1); + void **result = reinterpret_cast(aligned); + result[-1] = base; + return result; +} + +void zend_free_aligned(void *memory, std::size_t alignment) noexcept +{ + if (memory == nullptr) { + return; + } + if (alignment <= alignof(std::max_align_t)) { + efree(memory); + return; + } + efree(reinterpret_cast(memory)[-1]); +} + +} // namespace + +void *operator new(std::size_t size) +{ + return zend_allocate(size); +} + +void *operator new[](std::size_t size) +{ + return zend_allocate(size); +} + +void *operator new(std::size_t size, const std::nothrow_t &) noexcept +{ + return zend_allocate(size); +} + +void *operator new[](std::size_t size, const std::nothrow_t &) noexcept +{ + return zend_allocate(size); +} + +void operator delete(void *memory) noexcept +{ + if (memory != nullptr) { + efree(memory); + } +} + +void operator delete[](void *memory) noexcept +{ + if (memory != nullptr) { + efree(memory); + } +} + +void operator delete(void *memory, std::size_t) noexcept +{ + ::operator delete(memory); +} + +void operator delete[](void *memory, std::size_t) noexcept +{ + ::operator delete[](memory); +} + +void operator delete(void *memory, const std::nothrow_t &) noexcept +{ + ::operator delete(memory); +} + +void operator delete[](void *memory, const std::nothrow_t &) noexcept +{ + ::operator delete[](memory); +} + +void *operator new(std::size_t size, std::align_val_t alignment) +{ + return zend_allocate_aligned(size, static_cast(alignment)); +} + +void *operator new[](std::size_t size, std::align_val_t alignment) +{ + return zend_allocate_aligned(size, static_cast(alignment)); +} + +void *operator new( + std::size_t size, std::align_val_t alignment, const std::nothrow_t &) noexcept +{ + return zend_allocate_aligned(size, static_cast(alignment)); +} + +void *operator new[]( + std::size_t size, std::align_val_t alignment, const std::nothrow_t &) noexcept +{ + return zend_allocate_aligned(size, static_cast(alignment)); +} + +void operator delete(void *memory, std::align_val_t alignment) noexcept +{ + zend_free_aligned(memory, static_cast(alignment)); +} + +void operator delete[](void *memory, std::align_val_t alignment) noexcept +{ + zend_free_aligned(memory, static_cast(alignment)); +} + +void operator delete(void *memory, std::size_t, std::align_val_t alignment) noexcept +{ + zend_free_aligned(memory, static_cast(alignment)); +} + +void operator delete[](void *memory, std::size_t, std::align_val_t alignment) noexcept +{ + zend_free_aligned(memory, static_cast(alignment)); +} + +void operator delete( + void *memory, std::align_val_t alignment, const std::nothrow_t &) noexcept +{ + zend_free_aligned(memory, static_cast(alignment)); +} + +void operator delete[]( + void *memory, std::align_val_t alignment, const std::nothrow_t &) noexcept +{ + zend_free_aligned(memory, static_cast(alignment)); +} + +/* libstdc++'s header-only containers call these ABI hooks on impossible or + * allocation-failure paths. The kernel deliberately does not link the + * hosted libstdc++ runtime, so terminate through its panic boundary. */ +namespace std { + +const nothrow_t nothrow{}; + +[[noreturn]] void __throw_length_error(const char *) +{ + typephp_os_panic("C++ container length error"); +} + +[[noreturn]] void __throw_bad_alloc() +{ + typephp_os_panic("C++ allocation failed"); +} + +[[noreturn]] void __throw_bad_array_new_length() +{ + typephp_os_panic("C++ array allocation failed"); +} + +} // namespace std diff --git a/examples/typephp-os/freestanding/abi/libc.c b/examples/typephp-os/freestanding/abi/libc.c new file mode 100644 index 00000000..84462567 --- /dev/null +++ b/examples/typephp-os/freestanding/abi/libc.c @@ -0,0 +1,834 @@ +/* + +----------------------------------------------------------------------+ + | TypePHP OS | + +----------------------------------------------------------------------+ + | Small freestanding C/POSIX compatibility layer for Zend bootstrap. | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + + This is not a replacement for zend_alloc. It only supplies the host calls + used by zend_alloc to acquire its aligned 2 MiB chunks, plus the C memory + primitives emitted by C/C++ compilers. User allocations continue through + emalloc/efree and therefore retain the original Zend allocator semantics. +*/ + +#include "typephp_os_abi.h" + +#include +#include +#include +#include +#include +#include + +typedef struct typephp_os_block { + size_t size; +} typephp_os_block; + +static uintptr_t arena_cursor; +static uintptr_t arena_end; +static int typephp_os_errno; + +/* TypePHP OS has no hosted stdio object. These opaque values only satisfy + * upstream error paths; bytes are forwarded through the host write hook. */ +void *stdin = (void *) 0; +void *stdout = (void *) 1; +void *stderr = (void *) 2; + +size_t strlen(const char *string); +void *malloc(size_t size); +int posix_memalign(void **result, size_t alignment, size_t size); +int vsnprintf(char *buffer, size_t size, const char *format, va_list args); + +int *__errno_location(void) +{ + return &typephp_os_errno; +} + +void __assert_fail( + const char *assertion, + const char *file, + unsigned int line, + const char *function) +{ + (void) file; + (void) line; + (void) function; + typephp_os_panic(assertion); +} + +static uintptr_t align_up(uintptr_t value, size_t alignment) +{ + return (value + alignment - 1u) & ~((uintptr_t) alignment - 1u); +} + +static int is_power_of_two(size_t value) +{ + return value != 0 && (value & (value - 1u)) == 0; +} + +void typephp_os_memory_init(void *address, size_t size) +{ + const uintptr_t begin = (uintptr_t) address; + if (size > UINTPTR_MAX - begin) { + arena_cursor = 0; + arena_end = 0; + return; + } + arena_cursor = begin; + arena_end = begin + size; +} + +size_t typephp_os_memory_available(void) +{ + return arena_end >= arena_cursor ? (size_t) (arena_end - arena_cursor) : 0; +} + +__attribute__((weak)) void typephp_os_write(const char *data, size_t size) +{ + (void) data; + (void) size; +} + +__attribute__((weak, noreturn)) void typephp_os_panic(const char *message) +{ + typephp_os_write(message, strlen(message)); + for (;;) { + __asm__ volatile("cli; hlt"); + } +} + +void phpx_no_exception_abort(const char *message) +{ + typephp_os_panic(message); +} + +int php_nano_host_random_bytes(void *bytes, size_t size) +{ + static uint64_t state = UINT64_C(0x9e3779b97f4a7c15); + unsigned char *output = (unsigned char *) bytes; + while (size-- != 0) { + state ^= state << 13; + state ^= state >> 7; + state ^= state << 17; + *output++ = (unsigned char) state; + } + return 0; +} + +uint64_t php_nano_host_random_seed(void) +{ + uint64_t seed = 0; + (void) php_nano_host_random_bytes(&seed, sizeof(seed)); + return seed; +} + +void *memset(void *destination, int value, size_t size) +{ + unsigned char *output = (unsigned char *) destination; + while (size-- != 0) { + *output++ = (unsigned char) value; + } + return destination; +} + +void *memcpy(void *destination, const void *source, size_t size) +{ + unsigned char *output = (unsigned char *) destination; + const unsigned char *input = (const unsigned char *) source; + while (size-- != 0) { + *output++ = *input++; + } + return destination; +} + +void *memmove(void *destination, const void *source, size_t size) +{ + unsigned char *output = (unsigned char *) destination; + const unsigned char *input = (const unsigned char *) source; + if (output <= input || output >= input + size) { + return memcpy(destination, source, size); + } + output += size; + input += size; + while (size-- != 0) { + *--output = *--input; + } + return destination; +} + +int memcmp(const void *left, const void *right, size_t size) +{ + const unsigned char *a = (const unsigned char *) left; + const unsigned char *b = (const unsigned char *) right; + while (size-- != 0) { + if (*a != *b) { + return *a < *b ? -1 : 1; + } + ++a; + ++b; + } + return 0; +} + +size_t strlen(const char *string) +{ + const char *end = string; + while (*end != '\0') { + ++end; + } + return (size_t) (end - string); +} + +char *strdup(const char *string) +{ + const size_t size = strlen(string) + 1; + char *copy = (char *) malloc(size); + return copy ? (char *) memcpy(copy, string, size) : 0; +} + +int strcmp(const char *left, const char *right) +{ + while (*left != '\0' && *left == *right) { + ++left; + ++right; + } + return (unsigned char) *left - (unsigned char) *right; +} + +int strncmp(const char *left, const char *right, size_t size) +{ + while (size-- != 0) { + const unsigned char a = (unsigned char) *left++; + const unsigned char b = (unsigned char) *right++; + if (a != b) { + return (int) a - (int) b; + } + if (a == 0) { + return 0; + } + } + return 0; +} + +void *memchr(const void *memory, int character, size_t size) +{ + const unsigned char *cursor = (const unsigned char *) memory; + const unsigned char value = (unsigned char) character; + while (size-- != 0) { + if (*cursor == value) { + return (void *) cursor; + } + ++cursor; + } + return 0; +} + +char *strchr(const char *string, int character) +{ + do { + if (*string == (char) character) { + return (char *) string; + } + } while (*string++ != '\0'); + return 0; +} + +char *strrchr(const char *string, int character) +{ + const char *match = 0; + do { + if (*string == (char) character) { + match = string; + } + } while (*string++ != '\0'); + return (char *) match; +} + +char *strstr(const char *haystack, const char *needle) +{ + const size_t needle_size = strlen(needle); + if (needle_size == 0) { + return (char *) haystack; + } + while (*haystack != '\0') { + if (*haystack == *needle && strncmp(haystack, needle, needle_size) == 0) { + return (char *) haystack; + } + ++haystack; + } + return 0; +} + +static unsigned char ascii_lower(unsigned char character) +{ + return character >= 'A' && character <= 'Z' + ? (unsigned char) (character + ('a' - 'A')) + : character; +} + +int strcasecmp(const char *left, const char *right) +{ + while (*left != '\0' && ascii_lower((unsigned char) *left) == ascii_lower((unsigned char) *right)) { + ++left; + ++right; + } + return (int) ascii_lower((unsigned char) *left) - (int) ascii_lower((unsigned char) *right); +} + +int strncasecmp(const char *left, const char *right, size_t size) +{ + while (size-- != 0) { + const unsigned char a = ascii_lower((unsigned char) *left++); + const unsigned char b = ascii_lower((unsigned char) *right++); + if (a != b) { + return (int) a - (int) b; + } + if (a == 0) { + return 0; + } + } + return 0; +} + +int isascii(int character) +{ + return (character & ~0x7f) == 0; +} + +int isdigit(int character) +{ + return character >= '0' && character <= '9'; +} + +int islower(int character) +{ + return character >= 'a' && character <= 'z'; +} + +int isupper(int character) +{ + return character >= 'A' && character <= 'Z'; +} + +int isalpha(int character) +{ + return islower(character) || isupper(character); +} + +int isalnum(int character) +{ + return isalpha(character) || isdigit(character); +} + +int isspace(int character) +{ + return character == ' ' || (character >= '\t' && character <= '\r'); +} + +int isblank(int character) +{ + return character == ' ' || character == '\t'; +} + +int iscntrl(int character) +{ + return (character >= 0 && character < 0x20) || character == 0x7f; +} + +int isgraph(int character) +{ + return character > 0x20 && character < 0x7f; +} + +int isprint(int character) +{ + return character >= 0x20 && character < 0x7f; +} + +int ispunct(int character) +{ + return isgraph(character) && !isalnum(character); +} + +size_t __ctype_get_mb_cur_max(void) +{ + return 1; +} + +int isxdigit(int character) +{ + return isdigit(character) + || (character >= 'a' && character <= 'f') + || (character >= 'A' && character <= 'F'); +} + +int tolower(int character) +{ + return isupper(character) ? character + ('a' - 'A') : character; +} + +int toupper(int character) +{ + return islower(character) ? character - ('a' - 'A') : character; +} + +struct lconv *localeconv(void) +{ + static struct lconv value = { + .decimal_point = ".", + .thousands_sep = "", + .grouping = "", + }; + return &value; +} + +char *setlocale(int category, const char *locale) +{ + static char c_locale[] = "C"; + (void) category; + if (locale == 0 || strcmp(locale, "C") == 0 || strcmp(locale, "POSIX") == 0) { + return c_locale; + } + return 0; +} + +char *getcwd(char *buffer, size_t size) +{ + if (buffer == 0 || size < 2) { + typephp_os_errno = 34; /* ERANGE */ + return 0; + } + buffer[0] = '/'; + buffer[1] = '\0'; + return buffer; +} + +char *getenv(const char *name) +{ + /* The kernel process starts with an intentionally empty environment. */ + (void) name; + return 0; +} + +int strcoll(const char *left, const char *right) +{ + return strcmp(left, right); +} + +char *strcat(char *destination, const char *source) +{ + char *result = destination; + destination += strlen(destination); + while ((*destination++ = *source++) != '\0') { + } + return result; +} + +char *strpbrk(const char *string, const char *characters) +{ + for (; *string != '\0'; ++string) { + if (strchr(characters, *string) != 0) { + return (char *) string; + } + } + return 0; +} + +static int digit_value(int character) +{ + if (character >= '0' && character <= '9') { + return character - '0'; + } + if (character >= 'a' && character <= 'z') { + return character - 'a' + 10; + } + if (character >= 'A' && character <= 'Z') { + return character - 'A' + 10; + } + return -1; +} + +unsigned long long strtoull(const char *string, char **end, int base) +{ + while (isspace((unsigned char) *string)) { + ++string; + } + if (*string == '+') { + ++string; + } + if ((base == 0 || base == 16) && string[0] == '0' + && (string[1] == 'x' || string[1] == 'X')) { + base = 16; + string += 2; + } else if (base == 0) { + base = string[0] == '0' ? 8 : 10; + } + const char *cursor = string; + unsigned long long value = 0; + int digit; + while ((digit = digit_value((unsigned char) *cursor)) >= 0 && digit < base) { + value = value * (unsigned int) base + (unsigned int) digit; + ++cursor; + } + if (end != 0) { + *end = (char *) cursor; + } + return value; +} + +long long strtoll(const char *string, char **end, int base) +{ + while (isspace((unsigned char) *string)) { + ++string; + } + const int negative = *string == '-'; + if (*string == '-' || *string == '+') { + ++string; + } + char *parsed_end; + const unsigned long long value = strtoull(string, &parsed_end, base); + if (end != 0) { + *end = parsed_end; + } + return negative ? -(long long) value : (long long) value; +} + +long strtol(const char *string, char **end, int base) +{ + return (long) strtoll(string, end, base); +} + +int atoi(const char *string) +{ + return (int) strtol(string, 0, 10); +} + +long long atoll(const char *string) +{ + return strtoll(string, 0, 10); +} + +intmax_t imaxabs(intmax_t value) +{ + return value < 0 ? -value : value; +} + +double strtod(const char *string, char **end) +{ + while (isspace((unsigned char) *string)) { + ++string; + } + const int negative = *string == '-'; + if (*string == '-' || *string == '+') { + ++string; + } + double value = 0.0; + while (isdigit((unsigned char) *string)) { + value = value * 10.0 + (double) (*string++ - '0'); + } + if (*string == '.') { + double place = 0.1; + ++string; + while (isdigit((unsigned char) *string)) { + value += (double) (*string++ - '0') * place; + place *= 0.1; + } + } + if (end != 0) { + *end = (char *) string; + } + return negative ? -value : value; +} + +void qsort(void *base, size_t count, size_t width, int (*compare)(const void *, const void *)) +{ + unsigned char *bytes = (unsigned char *) base; + for (size_t index = 1; index < count; ++index) { + size_t current = index; + while (current != 0 + && compare(bytes + (current - 1) * width, bytes + current * width) > 0) { + for (size_t byte = 0; byte < width; ++byte) { + unsigned char value = bytes[(current - 1) * width + byte]; + bytes[(current - 1) * width + byte] = bytes[current * width + byte]; + bytes[current * width + byte] = value; + } + --current; + } + } +} + +int fflush(void *stream) +{ + (void) stream; + return 0; +} + +int setvbuf(void *stream, char *buffer, int mode, size_t size) +{ + (void) stream; + (void) buffer; + (void) mode; + (void) size; + return 0; +} + +int vprintf(const char *format, va_list args) +{ + char buffer[1024]; + const int written = vsnprintf(buffer, sizeof(buffer), format, args); + if (written > 0) { + typephp_os_write(buffer, (size_t) written < sizeof(buffer) + ? (size_t) written : sizeof(buffer) - 1); + } + return written; +} + +int printf(const char *format, ...) +{ + va_list args; + va_start(args, format); + const int written = vprintf(format, args); + va_end(args); + return written; +} + +int _setjmp(jmp_buf environment) +{ + (void) environment; + return 0; +} + +double pow(double base, double exponent) +{ + long power = (long) exponent; + if ((double) power != exponent) { + return 0.0; + } + double result = 1.0; + unsigned long magnitude = power < 0 ? (unsigned long) (-power) : (unsigned long) power; + while (magnitude != 0) { + if ((magnitude & 1u) != 0) { + result *= base; + } + base *= base; + magnitude >>= 1u; + } + return power < 0 ? 1.0 / result : result; +} + +double fmod(double value, double divisor) +{ + if (divisor == 0.0) { + return 0.0 / 0.0; + } + double quotient = value / divisor; + if (quotient >= 0.0) { + quotient = (double) (unsigned long) quotient; + } else { + quotient = (double) (long) quotient; + } + return value - quotient * divisor; +} + +double ceil(double value) +{ + long integral = (long) value; + return value > (double) integral ? (double) integral + 1.0 : (double) integral; +} + +double floor(double value) +{ + long integral = (long) value; + return value < (double) integral ? (double) integral - 1.0 : (double) integral; +} + +double trunc(double value) +{ + return (double) (long) value; +} + +double round(double value) +{ + return value < 0.0 ? ceil(value - 0.5) : floor(value + 0.5); +} + +double fabs(double value) +{ + return value < 0.0 ? -value : value; +} + +int abs(int value) +{ + return value < 0 ? -value : value; +} + +long long llabs(long long value) +{ + return value < 0 ? -value : value; +} + +int ap_php_vsnprintf(char *buffer, size_t size, const char *format, va_list args); + +int vsnprintf(char *buffer, size_t size, const char *format, va_list args) +{ + return ap_php_vsnprintf(buffer, size, format, args); +} + +int snprintf(char *buffer, size_t size, const char *format, ...) +{ + va_list args; + va_start(args, format); + const int result = ap_php_vsnprintf(buffer, size, format, args); + va_end(args); + return result; +} + +int __snprintf_chk(char *buffer, size_t size, int flag, size_t buffer_size, const char *format, ...) +{ + (void) flag; + (void) buffer_size; + va_list args; + va_start(args, format); + const int result = ap_php_vsnprintf(buffer, size, format, args); + va_end(args); + return result; +} + +__attribute__((noreturn)) void __longjmp_chk(void *environment, int value) +{ + (void) environment; + (void) value; + typephp_os_panic("zend_bailout"); +} + +__attribute__((noreturn)) void longjmp(jmp_buf environment, int value) +{ + (void) environment; + (void) value; + typephp_os_panic("zend_bailout"); +} + +int fputs(const char *string, void *stream) +{ + (void) stream; + typephp_os_write(string, strlen(string)); + return 0; +} + +int fputc(int character, void *stream) +{ + (void) stream; + const char byte = (char) character; + typephp_os_write(&byte, 1); + return (unsigned char) byte; +} + +size_t fwrite(const void *data, size_t size, size_t count, void *stream) +{ + (void) stream; + if (size != 0 && count > SIZE_MAX / size) { + return 0; + } + typephp_os_write((const char *) data, size * count); + return count; +} + +int fprintf(void *stream, const char *format, ...) +{ + (void) stream; + /* Zend's allocator only uses this on fatal paths. Keep the implementation + * allocation-free; the complete formatter belongs to the standard layer. */ + typephp_os_write(format, strlen(format)); + return (int) strlen(format); +} + +int __fprintf_chk(void *stream, int flag, const char *format, ...) +{ + (void) flag; + return fprintf(stream, format); +} + +void *malloc(size_t size) +{ + void *result = 0; + if (posix_memalign(&result, 16, size) != 0) { + return 0; + } + return result; +} + +void free(void *pointer) +{ + /* The bootstrap arena is monotonic. Zend's own allocations are reclaimed + * by zend_mm; backing chunks remain reserved until the kernel exits. */ + (void) pointer; +} + +void *calloc(size_t count, size_t size) +{ + if (count != 0 && size > SIZE_MAX / count) { + return 0; + } + const size_t bytes = count * size; + void *result = malloc(bytes); + if (result != 0) { + memset(result, 0, bytes); + } + return result; +} + +void *realloc(void *pointer, size_t size) +{ + if (pointer == 0) { + return malloc(size); + } + if (size == 0) { + return 0; + } + typephp_os_block *old = (typephp_os_block *) pointer - 1; + void *replacement = malloc(size); + if (replacement != 0) { + memcpy(replacement, pointer, old->size < size ? old->size : size); + } + return replacement; +} + +int posix_memalign(void **result, size_t alignment, size_t size) +{ + if (result == 0 || !is_power_of_two(alignment) || alignment < sizeof(void *)) { + return 22; /* EINVAL */ + } + if (arena_cursor == 0 || size > SIZE_MAX - sizeof(typephp_os_block)) { + *result = 0; + return 12; /* ENOMEM */ + } + + const uintptr_t payload = align_up( + arena_cursor + sizeof(typephp_os_block), alignment); + if (payload > arena_end || size > arena_end - payload) { + *result = 0; + return 12; + } + typephp_os_block *block = (typephp_os_block *) payload - 1; + block->size = size; + arena_cursor = payload + size; + *result = (void *) payload; + return 0; +} + +__attribute__((noreturn)) void abort(void) +{ + typephp_os_panic("abort"); +} + +__attribute__((noreturn)) void exit(int status) +{ + (void) status; + typephp_os_panic("exit"); +} + +__attribute__((noreturn)) void _Exit(int status) +{ + exit(status); +} diff --git a/examples/typephp-os/freestanding/abi/typephp_os_abi.h b/examples/typephp-os/freestanding/abi/typephp_os_abi.h new file mode 100644 index 00000000..2a6cba14 --- /dev/null +++ b/examples/typephp-os/freestanding/abi/typephp_os_abi.h @@ -0,0 +1,38 @@ +/* + +----------------------------------------------------------------------+ + | TypePHP OS | + +----------------------------------------------------------------------+ + | Freestanding host contract owned by the TypePHP OS experiment. | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ +*/ + +#ifndef TYPEPHP_OS_ABI_H +#define TYPEPHP_OS_ABI_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Install the contiguous physical-memory arena used by the libc/POSIX shim. + * The caller owns page-table setup and must keep the whole region mapped. */ +void typephp_os_memory_init(void *address, size_t size); +size_t typephp_os_memory_available(void); + +/* C++17 global new/delete are supplied by TypePHP OS and allocate + * exclusively through Zend MM (emalloc/efree). */ + +/* A platform may override these weak hooks for diagnostics and shutdown. */ +void typephp_os_write(const char *data, size_t size); +void typephp_os_panic(const char *message); + +/* Generic PHPX no-exception policy hook. */ +void phpx_no_exception_abort(const char *message); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/examples/typephp-os/freestanding/abi/unimplemented.S b/examples/typephp-os/freestanding/abi/unimplemented.S new file mode 100644 index 00000000..8094fef0 --- /dev/null +++ b/examples/typephp-os/freestanding/abi/unimplemented.S @@ -0,0 +1,103 @@ +.section .text +.code64 +.extern typephp_os_panic + +/* ABI coverage comes before subsystem implementation. Every placeholder is a + * real exported symbol so the complete php-nano/PHPX source set links, while + * any accidental runtime use fails loudly instead of returning fake data. */ +.macro TYPEPHP_OS_UNIMPLEMENTED symbol + .global \symbol + .type \symbol, @function +\symbol: + lea .Lmessage\@(%rip), %rdi + jmp typephp_os_panic + .pushsection .rodata +.Lmessage\@: + .asciz "unimplemented libc/POSIX ABI: \symbol" + .popsection +.endm + +TYPEPHP_OS_UNIMPLEMENTED access +TYPEPHP_OS_UNIMPLEMENTED chdir +TYPEPHP_OS_UNIMPLEMENTED chmod +TYPEPHP_OS_UNIMPLEMENTED chown +TYPEPHP_OS_UNIMPLEMENTED close +TYPEPHP_OS_UNIMPLEMENTED closedir +TYPEPHP_OS_UNIMPLEMENTED fcntl +TYPEPHP_OS_UNIMPLEMENTED fdopen +TYPEPHP_OS_UNIMPLEMENTED feof +TYPEPHP_OS_UNIMPLEMENTED fileno +TYPEPHP_OS_UNIMPLEMENTED flock +TYPEPHP_OS_UNIMPLEMENTED fopen +TYPEPHP_OS_UNIMPLEMENTED fclose +TYPEPHP_OS_UNIMPLEMENTED fread +TYPEPHP_OS_UNIMPLEMENTED fseek +TYPEPHP_OS_UNIMPLEMENTED fstat +TYPEPHP_OS_UNIMPLEMENTED fsync +TYPEPHP_OS_UNIMPLEMENTED ftell +TYPEPHP_OS_UNIMPLEMENTED ftruncate +TYPEPHP_OS_UNIMPLEMENTED getgid +TYPEPHP_OS_UNIMPLEMENTED getgrnam +TYPEPHP_OS_UNIMPLEMENTED getgroups +TYPEPHP_OS_UNIMPLEMENTED getpwnam +TYPEPHP_OS_UNIMPLEMENTED getpwnam_r +TYPEPHP_OS_UNIMPLEMENTED getpwuid_r +TYPEPHP_OS_UNIMPLEMENTED getuid +TYPEPHP_OS_UNIMPLEMENTED gnu_dev_major +TYPEPHP_OS_UNIMPLEMENTED gnu_dev_minor +TYPEPHP_OS_UNIMPLEMENTED isatty +TYPEPHP_OS_UNIMPLEMENTED lseek +TYPEPHP_OS_UNIMPLEMENTED lstat +TYPEPHP_OS_UNIMPLEMENTED mkdir +TYPEPHP_OS_UNIMPLEMENTED mkstemp +TYPEPHP_OS_UNIMPLEMENTED open +TYPEPHP_OS_UNIMPLEMENTED opendir +TYPEPHP_OS_UNIMPLEMENTED read +TYPEPHP_OS_UNIMPLEMENTED readdir +TYPEPHP_OS_UNIMPLEMENTED readlink +TYPEPHP_OS_UNIMPLEMENTED rename +TYPEPHP_OS_UNIMPLEMENTED rewind +TYPEPHP_OS_UNIMPLEMENTED rewinddir +TYPEPHP_OS_UNIMPLEMENTED rmdir +TYPEPHP_OS_UNIMPLEMENTED stat +TYPEPHP_OS_UNIMPLEMENTED umask +TYPEPHP_OS_UNIMPLEMENTED unlink +TYPEPHP_OS_UNIMPLEMENTED utime +TYPEPHP_OS_UNIMPLEMENTED write + +TYPEPHP_OS_UNIMPLEMENTED acos +TYPEPHP_OS_UNIMPLEMENTED acosh +TYPEPHP_OS_UNIMPLEMENTED asin +TYPEPHP_OS_UNIMPLEMENTED asinh +TYPEPHP_OS_UNIMPLEMENTED atan +TYPEPHP_OS_UNIMPLEMENTED atan2 +TYPEPHP_OS_UNIMPLEMENTED atanh +TYPEPHP_OS_UNIMPLEMENTED copysign +TYPEPHP_OS_UNIMPLEMENTED cos +TYPEPHP_OS_UNIMPLEMENTED cosh +TYPEPHP_OS_UNIMPLEMENTED exp +TYPEPHP_OS_UNIMPLEMENTED expm1 +TYPEPHP_OS_UNIMPLEMENTED hypot +TYPEPHP_OS_UNIMPLEMENTED log +TYPEPHP_OS_UNIMPLEMENTED log10 +TYPEPHP_OS_UNIMPLEMENTED log1p +TYPEPHP_OS_UNIMPLEMENTED log2 +TYPEPHP_OS_UNIMPLEMENTED nextafter +TYPEPHP_OS_UNIMPLEMENTED sin +TYPEPHP_OS_UNIMPLEMENTED sinh +TYPEPHP_OS_UNIMPLEMENTED sqrt +TYPEPHP_OS_UNIMPLEMENTED tan +TYPEPHP_OS_UNIMPLEMENTED tanh + +TYPEPHP_OS_UNIMPLEMENTED mblen +TYPEPHP_OS_UNIMPLEMENTED strerror +TYPEPHP_OS_UNIMPLEMENTED strftime + +TYPEPHP_OS_UNIMPLEMENTED _Unwind_Resume +TYPEPHP_OS_UNIMPLEMENTED __cxa_begin_catch +TYPEPHP_OS_UNIMPLEMENTED __cxa_end_catch +TYPEPHP_OS_UNIMPLEMENTED __cxa_rethrow +TYPEPHP_OS_UNIMPLEMENTED __gxx_personality_v0 +TYPEPHP_OS_UNIMPLEMENTED _ZNSt15__exception_ptr13exception_ptr10_M_releaseEv + +.section .note.GNU-stack, "", @progbits diff --git a/examples/typephp-os/freestanding/boot.S b/examples/typephp-os/freestanding/boot.S new file mode 100644 index 00000000..bac5f374 --- /dev/null +++ b/examples/typephp-os/freestanding/boot.S @@ -0,0 +1,118 @@ +.set MULTIBOOT_MAGIC, 0x1badb002 +.set MULTIBOOT_FLAGS, 0x00000003 +.set MULTIBOOT_CHECKSUM, -(MULTIBOOT_MAGIC + MULTIBOOT_FLAGS) + +.section .multiboot, "a" +.align 4 +.long MULTIBOOT_MAGIC +.long MULTIBOOT_FLAGS +.long MULTIBOOT_CHECKSUM + +.section .bss +.align 4096 +pml4_table: +.skip 4096 +pdp_table: +.skip 4096 +pd_table: +.skip 4096 + +.align 16 +stack_bottom: +.skip 16384 +stack_top: + +.section .text +.code32 +.global _start +.type _start, @function +.extern kernel_entry +_start: + cli + mov $stack_top, %esp + + # Preserve the Multiboot registers while page tables and MSRs are set up. + mov %eax, %ebp + + # Identity-map the first GiB with 2 MiB pages. + mov $pdp_table, %eax + or $0x3, %eax + mov %eax, pml4_table + + mov $pd_table, %eax + or $0x3, %eax + mov %eax, pdp_table + + mov $pd_table, %edi + mov $0x83, %eax + mov $512, %ecx +map_page: + mov %eax, (%edi) + add $0x200000, %eax + add $8, %edi + loop map_page + + # Enable PAE and prepare the x87/SSE state before entering long mode. + mov %cr0, %ecx + and $0xfffffffb, %ecx + or $0x2, %ecx + mov %ecx, %cr0 + mov %cr4, %ecx + or $0x620, %ecx + mov %ecx, %cr4 + fninit + + mov $pml4_table, %eax + mov %eax, %cr3 + + # Set EFER.LME, then enable paging. Protected mode is already active. + mov $0xc0000080, %ecx + rdmsr + or $0x100, %eax + wrmsr + + mov %cr0, %eax + or $0x80000000, %eax + mov %eax, %cr0 + + lgdt gdt64_pointer + + # The 64-bit System V ABI receives Multiboot magic/info in RDI/RSI. + mov %ebp, %edi + mov %ebx, %esi + ljmp $0x08, $long_mode_start + +.code64 +long_mode_start: + mov $0x10, %ax + mov %ax, %ds + mov %ax, %es + mov %ax, %ss + xor %ax, %ax + mov %ax, %fs + mov %ax, %gs + and $-16, %rsp + + # The embedded ELF64 payload is linked and loaded at exactly 2 MiB. + movabs $0x200000, %rax + call *%rax + +halt_forever: + cli + hlt + jmp halt_forever +.size _start, . - _start + +.section .rodata +.align 8 +gdt64: + .quad 0x0000000000000000 + .quad 0x00af9a000000ffff + .quad 0x00cf92000000ffff +gdt64_end: + +gdt64_pointer: + .word gdt64_end - gdt64 - 1 + .long gdt64 + +.section .note.GNU-stack, "", @progbits diff --git a/examples/typephp-os/freestanding/bridge.cc b/examples/typephp-os/freestanding/bridge.cc new file mode 100644 index 00000000..a9d28981 --- /dev/null +++ b/examples/typephp-os/freestanding/bridge.cc @@ -0,0 +1,84 @@ +#include +#include +#include +#include + +void php_main(); + +extern "C" { +#include +} + +extern "C" void kernel_clear_c(int color); +extern "C" void kernel_put_char_c(int ascii, int color); +extern "C" void kernel_halt_c(); +extern "C" unsigned long physical_memory_megabytes(); +extern "C" unsigned long physical_chunk_smoke_test(); + +static void exception_model_smoke_test() +{ + php::enableDebugInfo(true); + php::pushDebugFrame("kernel-smoke.php", 42, "Kernel::smoke"); + zend_throw_exception_ex(zend_ce_type_error, 0, "%s", "kernel exception"); + if (EG(exception) == nullptr || EG(exception)->ce != zend_ce_type_error) { + typephp_os_panic("Zend TypeError construction failed"); + } + + php::augmentException(); + zval value; + zval *file = zend_read_property_ex( + zend_get_exception_base(EG(exception)), EG(exception), ZSTR_KNOWN(ZEND_STR_FILE), true, &value); + if (Z_TYPE_P(file) != IS_STRING || !zend_string_equals_literal(Z_STR_P(file), "kernel-smoke.php")) { + typephp_os_panic("PHPX exception augmentation failed"); + } + + zend_clear_exception(); + php::popDebugFrame(); + php::enableDebugInfo(false); +} + +extern "C" void typephp_kernel_main() +{ + if (php_nano_startup_composer_extensions() != SUCCESS) { + typephp_os_panic("Unable to start Nano extensions"); + } + exception_model_smoke_test(); + php_main(); +} + +void php_kernel_clear(php::Int color) +{ + kernel_clear_c(static_cast(color)); +} + +void php_kernel_put_char(php::Int ascii, php::Int color) +{ + kernel_put_char_c(static_cast(ascii), static_cast(color)); +} + +void php_kernel_write(php::Str text, php::Int color) +{ + for (size_t i = 0; i < text.length(); ++i) { + kernel_put_char_c(static_cast(text.data()[i]), static_cast(color)); + } +} + +php::Int php_kernel_word_bits() +{ + return static_cast(sizeof(php::Int) * 8); +} + +php::Int php_kernel_memory_megabytes() +{ + return static_cast(physical_memory_megabytes()); +} + +php::Int php_kernel_chunk_smoke_test() +{ + return static_cast(physical_chunk_smoke_test()); +} + +void php_kernel_halt() +{ + kernel_halt_c(); +} diff --git a/examples/typephp-os/freestanding/kernel.c b/examples/typephp-os/freestanding/kernel.c new file mode 100644 index 00000000..e73f9f6d --- /dev/null +++ b/examples/typephp-os/freestanding/kernel.c @@ -0,0 +1,135 @@ +typedef unsigned char uint8_t; +typedef unsigned short uint16_t; +typedef unsigned int uint32_t; +typedef unsigned long uint64_t; + +enum { + MULTIBOOT_BOOTLOADER_MAGIC = 0x2badb002u, + VGA_WIDTH = 80, + VGA_HEIGHT = 25, + COM1 = 0x3f8, +}; + +static volatile uint16_t *const vga = (volatile uint16_t *) 0xb8000; +static uint32_t row; +static uint32_t column; + +static inline void outb(uint16_t port, uint8_t value) +{ + __asm__ volatile("outb %0, %1" : : "a"(value), "Nd"(port)); +} + +static inline uint8_t inb(uint16_t port) +{ + uint8_t value; + __asm__ volatile("inb %1, %0" : "=a"(value) : "Nd"(port)); + return value; +} + +static void serial_init(void) +{ + outb(COM1 + 1, 0x00); + outb(COM1 + 3, 0x80); + outb(COM1 + 0, 0x03); + outb(COM1 + 1, 0x00); + outb(COM1 + 3, 0x03); + outb(COM1 + 2, 0xc7); + outb(COM1 + 4, 0x0b); +} + +static void serial_put(uint8_t value) +{ + while ((inb(COM1 + 5) & 0x20u) == 0) { + } + outb(COM1, value); +} + +static void vga_scroll(void) +{ + if (row < VGA_HEIGHT) { + return; + } + for (uint32_t y = 1; y < VGA_HEIGHT; y++) { + for (uint32_t x = 0; x < VGA_WIDTH; x++) { + vga[(y - 1) * VGA_WIDTH + x] = vga[y * VGA_WIDTH + x]; + } + } + for (uint32_t x = 0; x < VGA_WIDTH; x++) { + vga[(VGA_HEIGHT - 1) * VGA_WIDTH + x] = (uint16_t) (0x0720u); + } + row = VGA_HEIGHT - 1; +} + +void kernel_clear_c(int color) +{ + const uint16_t blank = (uint16_t) ((((uint16_t) color) << 8u) | ' '); + for (uint32_t i = 0; i < VGA_WIDTH * VGA_HEIGHT; i++) { + vga[i] = blank; + } + row = 0; + column = 0; +} + +void kernel_put_char_c(int ascii, int color) +{ + const uint8_t character = (uint8_t) ascii; + if (character == '\n') { + serial_put('\r'); + serial_put('\n'); + column = 0; + row++; + vga_scroll(); + return; + } + if (character == '\t') { + do { + kernel_put_char_c(' ', color); + } while ((column & 7u) != 0); + return; + } + + serial_put(character); + vga[row * VGA_WIDTH + column] = + (uint16_t) ((((uint16_t) color) << 8u) | character); + column++; + if (column == VGA_WIDTH) { + column = 0; + row++; + vga_scroll(); + } +} + +void typephp_os_write(const char *data, unsigned long size) +{ + for (unsigned long index = 0; index < size; ++index) { + kernel_put_char_c((unsigned char) data[index], 0x0f); + } +} + +void kernel_halt_c(void) +{ + // QEMU's isa-debug-exit device turns this into process exit status 33. + outb(0xf4, 0x10); + for (;;) { + __asm__ volatile("cli; hlt"); + } +} + +extern void typephp_kernel_main(void); +extern void physical_memory_init(uint64_t multiboot_info_address); + +void kernel_entry(uint64_t magic, uint64_t multiboot_info) +{ + (void) multiboot_info; + serial_init(); + if (magic != MULTIBOOT_BOOTLOADER_MAGIC) { + const char *message = "Invalid Multiboot boot magic\n"; + while (*message != '\0') { + serial_put((uint8_t) *message++); + } + kernel_halt_c(); + } + physical_memory_init(multiboot_info); + typephp_kernel_main(); + kernel_halt_c(); +} diff --git a/examples/typephp-os/freestanding/kernel64-entry.S b/examples/typephp-os/freestanding/kernel64-entry.S new file mode 100644 index 00000000..2977db18 --- /dev/null +++ b/examples/typephp-os/freestanding/kernel64-entry.S @@ -0,0 +1,31 @@ +.section .text.boot, "ax" +.code64 +.global kernel64_entry +.type kernel64_entry, @function +.extern kernel_entry +.extern __bss_start +.extern __bss_end + +kernel64_entry: + cld + mov %rdi, %r12 + mov %rsi, %r13 + + # The raw payload does not carry ELF NOBITS data, so clear it explicitly. + lea __bss_start(%rip), %rdi + lea __bss_end(%rip), %rcx + sub %rdi, %rcx + xor %eax, %eax + rep stosb + + mov %r12, %rdi + mov %r13, %rsi + call kernel_entry + +halt64: + cli + hlt + jmp halt64 +.size kernel64_entry, . - kernel64_entry + +.section .note.GNU-stack, "", @progbits diff --git a/examples/typephp-os/freestanding/kernel64.ld b/examples/typephp-os/freestanding/kernel64.ld new file mode 100644 index 00000000..eb345a82 --- /dev/null +++ b/examples/typephp-os/freestanding/kernel64.ld @@ -0,0 +1,41 @@ +OUTPUT_FORMAT(elf64-x86-64) +OUTPUT_ARCH(i386:x86-64) +ENTRY(kernel64_entry) + +SECTIONS +{ + . = 2M; + + .text ALIGN(4K) : + { + KEEP(*(.text.boot)) + *(.text .text.*) + } + + .rodata ALIGN(4K) : + { + *(.rodata .rodata.*) + } + + .data ALIGN(4K) : + { + *(.data .data.*) + } + + .bss ALIGN(4K) (NOLOAD) : + { + __bss_start = .; + *(COMMON) + *(.bss .bss.*) + __bss_end = .; + } + + ASSERT(__bss_end <= 16M, "64-bit kernel exceeds its reserved boot memory") + + /DISCARD/ : + { + *(.comment) + *(.eh_frame*) + *(.note*) + } +} diff --git a/examples/typephp-os/freestanding/linker.ld b/examples/typephp-os/freestanding/linker.ld new file mode 100644 index 00000000..45e638c4 --- /dev/null +++ b/examples/typephp-os/freestanding/linker.ld @@ -0,0 +1,49 @@ +OUTPUT_FORMAT(elf32-i386) +OUTPUT_ARCH(i386) +ENTRY(_start) + +SECTIONS +{ + . = 1M; + + .multiboot ALIGN(4) : + { + KEEP(*(.multiboot)) + } + + .text ALIGN(4K) : + { + *(.text .text.*) + } + + .rodata ALIGN(4K) : + { + *(.rodata .rodata.*) + } + + .data ALIGN(4K) : + { + *(.data .data.*) + } + + .bss ALIGN(4K) (NOLOAD) : + { + *(COMMON) + *(.bss .bss.*) + } + + ASSERT(. < 2M, "32-bit bootstrap overlaps the 64-bit payload") + + . = 2M; + .payload ALIGN(4K) : + { + KEEP(*(.payload)) + } + + /DISCARD/ : + { + *(.comment) + *(.eh_frame*) + *(.note*) + } +} diff --git a/examples/typephp-os/freestanding/memory.c b/examples/typephp-os/freestanding/memory.c new file mode 100644 index 00000000..44dca15f --- /dev/null +++ b/examples/typephp-os/freestanding/memory.c @@ -0,0 +1,95 @@ +typedef unsigned int uint32_t; +typedef unsigned long uint64_t; + +extern void typephp_os_memory_init(void *address, unsigned long size); + +enum { + MULTIBOOT_INFO_MEMORY_MAP = 1u << 6, +}; + +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; +static const uint64_t IDENTITY_MAP_END = 1024ul * 1024ul * 1024ul; + +typedef struct __attribute__((packed)) { + uint32_t size; + uint64_t address; + uint64_t length; + uint32_t type; +} multiboot_memory_entry; + +typedef struct __attribute__((packed)) { + uint32_t flags; + uint32_t memory_lower; + uint32_t memory_upper; + uint32_t boot_device; + uint32_t command_line; + uint32_t modules_count; + uint32_t modules_address; + uint32_t symbols[4]; + uint32_t memory_map_length; + uint32_t memory_map_address; +} multiboot_info; + +static uint64_t usable_memory_bytes; +static uint64_t chunk_cursor; +static uint64_t chunk_end; + +static uint64_t align_up(uint64_t value, uint64_t alignment) +{ + return (value + alignment - 1) & ~(alignment - 1); +} + +void physical_memory_init(uint64_t multiboot_info_address) +{ + const multiboot_info *info = + (const multiboot_info *) (unsigned long) multiboot_info_address; + usable_memory_bytes = 0; + chunk_cursor = 0; + chunk_end = 0; + + if ((info->flags & MULTIBOOT_INFO_MEMORY_MAP) == 0) { + return; + } + + uint64_t position = info->memory_map_address; + const uint64_t end = position + info->memory_map_length; + while (position + sizeof(multiboot_memory_entry) <= end) { + const multiboot_memory_entry *entry = + (const multiboot_memory_entry *) (unsigned long) position; + if (entry->size < sizeof(multiboot_memory_entry) - sizeof(uint32_t)) { + break; + } + + if (entry->type == 1) { + usable_memory_bytes += entry->length; + uint64_t region_end = entry->address + entry->length; + if (region_end > IDENTITY_MAP_END) { + region_end = IDENTITY_MAP_END; + } + uint64_t region_start = entry->address; + if (region_start < KERNEL_RESERVED_END) { + region_start = KERNEL_RESERVED_END; + } + region_start = align_up(region_start, CHUNK_SIZE); + if (region_start + CHUNK_SIZE <= region_end + && region_end - region_start > chunk_end - chunk_cursor) { + chunk_cursor = region_start; + chunk_end = region_end; + } + } + position += (uint64_t) entry->size + sizeof(entry->size); + } + + if (chunk_cursor != 0 && chunk_end > chunk_cursor) { + typephp_os_memory_init( + (void *) (unsigned long) chunk_cursor, + (unsigned long) (chunk_end - chunk_cursor)); + } +} + +uint64_t physical_memory_megabytes(void) +{ + return usable_memory_bytes >> 20; +} diff --git a/examples/typephp-os/freestanding/time.c b/examples/typephp-os/freestanding/time.c new file mode 100644 index 00000000..88d81e02 --- /dev/null +++ b/examples/typephp-os/freestanding/time.c @@ -0,0 +1,215 @@ +#include + +enum { + CMOS_ADDRESS = 0x70, + CMOS_DATA = 0x71, +}; + +typedef struct kernel_datetime { + uint16_t year; + uint8_t month; + uint8_t day; + uint8_t hour; + uint8_t minute; + uint8_t second; +} kernel_datetime; + +static inline void outb(uint16_t port, uint8_t value) +{ + __asm__ volatile("outb %0, %1" : : "a"(value), "Nd"(port)); +} + +static inline uint8_t inb(uint16_t port) +{ + uint8_t value; + __asm__ volatile("inb %1, %0" : "=a"(value) : "Nd"(port)); + return value; +} + +static uint8_t cmos_read(uint8_t index) +{ + /* Keep NMI disabled while selecting a CMOS register. The kernel does not + * install an IDT yet, so accepting an NMI here would be unsafe. */ + outb(CMOS_ADDRESS, (uint8_t) (index | 0x80u)); + return inb(CMOS_DATA); +} + +static int rtc_update_in_progress(void) +{ + return (cmos_read(0x0a) & 0x80u) != 0; +} + +static uint8_t bcd_to_binary(uint8_t value) +{ + return (uint8_t) ((value & 0x0fu) + ((value >> 4u) * 10u)); +} + +static void rtc_snapshot(kernel_datetime *value) +{ + uint8_t second; + uint8_t minute; + uint8_t hour; + uint8_t day; + uint8_t month; + uint8_t year; + uint8_t century; + uint8_t status_b; + uint8_t previous_second; + uint8_t previous_minute; + uint8_t previous_hour; + uint8_t previous_day; + uint8_t previous_month; + uint8_t previous_year; + uint8_t previous_century; + + do { + while (rtc_update_in_progress()) { + __asm__ volatile("pause"); + } + previous_second = cmos_read(0x00); + previous_minute = cmos_read(0x02); + previous_hour = cmos_read(0x04); + previous_day = cmos_read(0x07); + previous_month = cmos_read(0x08); + previous_year = cmos_read(0x09); + previous_century = cmos_read(0x32); + + while (rtc_update_in_progress()) { + __asm__ volatile("pause"); + } + second = cmos_read(0x00); + minute = cmos_read(0x02); + hour = cmos_read(0x04); + day = cmos_read(0x07); + month = cmos_read(0x08); + year = cmos_read(0x09); + century = cmos_read(0x32); + } while (second != previous_second || minute != previous_minute + || hour != previous_hour || day != previous_day + || month != previous_month || year != previous_year + || century != previous_century); + + status_b = cmos_read(0x0b); + if ((status_b & 0x04u) == 0) { + second = bcd_to_binary(second); + minute = bcd_to_binary(minute); + day = bcd_to_binary(day); + month = bcd_to_binary(month); + year = bcd_to_binary(year); + century = bcd_to_binary(century); + hour = (uint8_t) ((hour & 0x80u) | bcd_to_binary((uint8_t) (hour & 0x7fu))); + } + if ((status_b & 0x02u) == 0 && (hour & 0x80u) != 0) { + hour = (uint8_t) (((hour & 0x7fu) + 12u) % 24u); + } else { + hour &= 0x7fu; + } + + value->year = century >= 19 && century <= 99 + ? (uint16_t) (century * 100u + year) + : (uint16_t) (2000u + year); + value->month = month; + value->day = day; + value->hour = hour; + value->minute = minute; + value->second = second; +} + +static int is_leap_year(uint64_t year) +{ + return (year % 4u == 0 && year % 100u != 0) || year % 400u == 0; +} + +static uint64_t rtc_seconds(const kernel_datetime *value) +{ + static const uint16_t days_before_month[] = { + 0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, + }; + uint64_t year = value->year; + uint64_t previous_year = year - 1u; + uint64_t days = 365u * (year - 1970u) + + previous_year / 4u - 1969u / 4u + - previous_year / 100u + 1969u / 100u + + previous_year / 400u - 1969u / 400u; + days += days_before_month[value->month]; + if (value->month > 2 && is_leap_year(year)) { + ++days; + } + days += value->day - 1u; + return (((days * 24u + value->hour) * 60u + value->minute) * 60u) + + value->second; +} + +void kernel_datetime_c(kernel_datetime *value) +{ + rtc_snapshot(value); +} + +void kernel_sleep_c(uint64_t seconds) +{ + kernel_datetime now; + rtc_snapshot(&now); + const uint64_t deadline = rtc_seconds(&now) + seconds; + do { + __asm__ volatile("pause"); + rtc_snapshot(&now); + } while (rtc_seconds(&now) < deadline); +} + +long typephp_os_time_seconds(void) +{ + kernel_datetime now; + rtc_snapshot(&now); + return (long) rtc_seconds(&now); +} + +void php_nano_host_system_time(int64_t *seconds, int32_t *microseconds) +{ + *seconds = (int64_t) typephp_os_time_seconds(); + *microseconds = 0; +} + +uint64_t php_nano_host_monotonic_nanoseconds(void) +{ + return (uint64_t) typephp_os_time_seconds() * UINT64_C(1000000000); +} + +void php_nano_host_sleep(uint64_t seconds, uint32_t nanoseconds) +{ + if (nanoseconds != 0) { + /* RTC resolution is currently one second. Round sub-second sleeps up + * until the timer interrupt implementation is available. */ + ++seconds; + } + kernel_sleep_c(seconds); +} + +unsigned int sleep(unsigned int seconds) +{ + kernel_sleep_c(seconds); + return 0; +} + +time_t time(time_t *result) +{ + const time_t value = (time_t) typephp_os_time_seconds(); + if (result != 0) { + *result = value; + } + return value; +} + +int clock_gettime(clockid_t clock_id, struct timespec *value) +{ + (void) clock_id; + value->tv_sec = (time_t) typephp_os_time_seconds(); + value->tv_nsec = 0; + return 0; +} + +int nanosleep(const struct timespec *duration, struct timespec *remaining) +{ + (void) remaining; + php_nano_host_sleep((uint64_t) duration->tv_sec, (uint32_t) duration->tv_nsec); + return 0; +} diff --git a/examples/typephp-os/freestanding/zend-allocator.c b/examples/typephp-os/freestanding/zend-allocator.c new file mode 100644 index 00000000..0b5f4632 --- /dev/null +++ b/examples/typephp-os/freestanding/zend-allocator.c @@ -0,0 +1,57 @@ +#include +#include + +unsigned long physical_chunk_smoke_test(void) +{ + + unsigned char *small = (unsigned char *) emalloc(37); + unsigned char *large = (unsigned char *) emalloc(96 * 1024); + if (small == 0 || large == 0 || small == large) { + return 0; + } + + for (unsigned long index = 0; index < 37; ++index) { + small[index] = (unsigned char) (index + 1); + } + for (unsigned long index = 0; index < 37; ++index) { + if (small[index] != (unsigned char) (index + 1)) { + return 0; + } + } + + efree(large); + efree(small); + + if (!gc_enabled()) { + return 0; + } + zend_gc_status status; + zend_gc_get_status(&status); + /* A newly enabled collector has an allocated, empty root buffer. */ + if (status.runs != 0 || status.collected != 0 || status.num_roots != 0) { + return 0; + } + + zend_string *key = zend_string_init("answer", sizeof("answer") - 1, 0); + if (key == 0 || ZSTR_LEN(key) != sizeof("answer") - 1 + || memcmp(ZSTR_VAL(key), "answer", sizeof("answer")) != 0) { + return 0; + } + + HashTable table; + zend_hash_init(&table, 4, 0, 0, 0); + zval value; + ZVAL_STRING(&value, "forty-two"); + if (zend_hash_add(&table, key, &value) == 0) { + return 0; + } + zval *found = zend_hash_find(&table, key); + if (found == 0 || Z_TYPE_P(found) != IS_STRING + || !zend_string_equals_literal(Z_STR_P(found), "forty-two")) { + return 0; + } + zend_hash_destroy(&table); + zend_string_release(key); + + return zend_memory_usage(1) >= 2ul * 1024ul * 1024ul ? 2 : 0; +} diff --git a/examples/typephp-os/project.yml b/examples/typephp-os/project.yml new file mode 100644 index 00000000..40645e7d --- /dev/null +++ b/examples/typephp-os/project.yml @@ -0,0 +1,57 @@ +name: typephp_os +mode: bin +sources: + - src + - freestanding/kernel64-entry.S + - freestanding/kernel.c + - freestanding/memory.c + - freestanding/time.c + - freestanding/zend-allocator.c + - freestanding/bridge.cc + - freestanding/abi/libc.c + - freestanding/abi/cxx.cpp + - freestanding/abi/unimplemented.S +build-dir: build/generated +output: build/kernel64.elf +cxx-std: c++17 +include-paths: + - freestanding/abi +defines: + - TYPEPHP_NO_MAIN=1 + - PHPX_NO_EXCEPTION=1 + - PHPX_NO_RTTI=1 + - PHP_NANO_NO_LIBC=1 + - __NO_CTYPE=1 + - ZEND_MM_ERROR=0 + - ZEND_MM_CUSTOM=0 +cxx-flags: + - -ffreestanding + - -fno-builtin + - -fno-stack-protector + - -fno-pic + - -fno-pie + - -m64 + - -mno-red-zone + - -mcmodel=small + - -fno-rtti + - -fno-threadsafe-statics +c-flags: + - -ffreestanding + - -fno-builtin + - -fno-stack-protector + - -fno-pic + - -fno-pie + - -m64 + - -mno-red-zone + - -mcmodel=small +asm-flags: + - -m64 + - -mno-red-zone + - -mcmodel=small +ld-flags: + - -nostdlib + - -no-pie + - -Wl,-m,elf_x86_64 + - -Wl,--build-id=none + - -Wl,-T,examples/typephp-os/freestanding/kernel64.ld + - -lgcc diff --git a/examples/typephp-os/src/hardware.stub.php b/examples/typephp-os/src/hardware.stub.php new file mode 100644 index 00000000..b83772b8 --- /dev/null +++ b/examples/typephp-os/src/hardware.stub.php @@ -0,0 +1,22 @@ + 0) { + $digit = $number % 10; + $digits[] = $digit; + $number = (int) (($number - $digit) / 10); + } + + for ($index = count($digits) - 1; $index >= 0; $index--) { + kernel_put_char(48 + $digits[$index], $color); + } +} + +function isPrime(int $number): bool +{ + if ($number < 2) { + return false; + } + for ($divisor = 2; $divisor * $divisor <= $number; $divisor++) { + if ($number % $divisor === 0) { + return false; + } + } + return true; +} + +function runFeatureSelfCheck(): void +{ + kernel_clear(0); + writeLine('TypePHP OS POC', 10); + + kernel_write('Int bits: ', 15); + writeNumber(kernel_word_bits(), 15); + kernel_put_char(10, 15); + + kernel_write('RAM MiB: ', 15); + writeNumber(kernel_memory_megabytes(), 15); + kernel_put_char(10, 15); + + kernel_write('Zend MiB: ', 15); + writeNumber(kernel_chunk_smoke_test(), 15); + kernel_put_char(10, 15); + + $checks = ['Zend string/array: OK', 'Kernel is!']; + foreach ($checks as $check) { + writeLine($check, 10); + } +} + +function runPrimeDemo(int $limit): void +{ + $primes = std::vector(Type::Int); + for ($candidate = 0; $candidate <= $limit; $candidate++) { + if (isPrime($candidate)) { + $primes[] = $candidate; + } + } + + kernel_write('Calculate primes: 0-', 11); + writeNumber($limit, 11); + kernel_put_char(10, 11); + + kernel_write('Prime count: ', 15); + writeNumber(count($primes), 15); + kernel_put_char(10, 15); + + kernel_write('Prime list: ', 15); + for ($index = 0; $index < count($primes); $index++) { + if ($index !== 0) { + kernel_write(', ', 15); + } + writeNumber($primes[$index], 15); + } + kernel_put_char(10, 15); +} + +function main(): void +{ + runFeatureSelfCheck(); + 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); + } +} diff --git a/examples/typephp-os/tools/test-qemu.sh b/examples/typephp-os/tools/test-qemu.sh new file mode 100755 index 00000000..775a4795 --- /dev/null +++ b/examples/typephp-os/tools/test-qemu.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +set -euo pipefail + +kernel=${1:?kernel ELF is required} +log=${2:?log path is required} + +if timeout 8 qemu-system-x86_64 \ + -kernel "${kernel}" \ + -display none \ + -serial stdio \ + -monitor none \ + -no-reboot \ + -no-shutdown \ + -device isa-debug-exit,iobase=0xf4,iosize=0x04 \ + >"${log}" 2>&1; then + status=0 +else + status=$? +fi + +if [[ ${status} -ne 124 ]]; then + cat "${log}" + echo "QEMU exited with ${status}; expected the resident kernel loop to reach the timeout" >&2 + exit 1 +fi + +grep -q "TypePHP OS POC" "${log}" +grep -q "Int bits: 64" "${log}" +grep -Eq '^RAM MiB: [1-9][0-9]*' "${log}" +grep -q "Zend MiB: 2" "${log}" +grep -q "Zend string/array: OK" "${log}" +grep -q "Kernel is!" "${log}" +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}" +cat "${log}" diff --git a/phpunit/src/Backend/BackendTest.php b/phpunit/src/Backend/BackendTest.php index 882cc304..f0dba82d 100644 --- a/phpunit/src/Backend/BackendTest.php +++ b/phpunit/src/Backend/BackendTest.php @@ -102,6 +102,7 @@ class BackendTest extends TestCase 'user_defines' => ['FEATURE_X=1'], 'lto' => true, 'is_zts' => true, + 'cflags' => '/experimental:c11atomics', ]); $this->assertStringContainsString('/TC', $cmd); @@ -111,6 +112,7 @@ class BackendTest extends TestCase $this->assertStringContainsString('/DFEATURE_X=1', $cmd); $this->assertStringContainsString('/GL', $cmd); $this->assertStringContainsString('/DZTS', $cmd); + $this->assertStringContainsString('/experimental:c11atomics', $cmd); $this->assertStringNotContainsString('/EHsc', $cmd); $this->assertStringNotContainsString('/std:', $cmd); } @@ -214,6 +216,7 @@ class BackendTest extends TestCase 'march' => 'native', 'target_platform' => 'aarch64-linux-gnu', 'build_mode' => 'ext', + 'cflags' => '-ffreestanding -fno-builtin', ]); $this->assertStringContainsString('-fsanitize=address', $cmd); @@ -224,6 +227,19 @@ class BackendTest extends TestCase $this->assertStringContainsString('-march=native', $cmd); $this->assertStringContainsString('--target=aarch64-linux-gnu', $cmd); $this->assertStringContainsString('-fPIC', $cmd); + $this->assertStringContainsString('-ffreestanding -fno-builtin', $cmd); + } + + public function testGccBuildAssemblerCommandUsesNativeFlags(): void + { + $compiler = new Gcc(new Linux()); + + $cmd = $compiler->buildNativeCompileCommand('entry.S', 'entry.o', [ + 'nativeflags' => '-m64 -mno-red-zone', + ], 'assembler'); + + $this->assertStringContainsString('-x assembler', $cmd); + $this->assertStringContainsString('-m64 -mno-red-zone', $cmd); } public function testGccBuildLinkCommandIncludesPlatformPathsOptionsAndLibraries(): void diff --git a/phpunit/src/CompilerBaseApiTest.php b/phpunit/src/CompilerBaseApiTest.php index 81332633..b5087351 100644 --- a/phpunit/src/CompilerBaseApiTest.php +++ b/phpunit/src/CompilerBaseApiTest.php @@ -776,6 +776,37 @@ YAML, 'myproject.yml', 'nested/config'); $this->assertSame([$projectDir . '/libs'], $this->compiler->getLinkPaths()); $this->assertSame($projectDir . '/bin', $this->getPropertyValue('outputDir')); $this->assertSame('my_app', $this->getPropertyValue('targetName')); + $this->assertSame($projectDir . '/bin/my-app', $this->invokeMethod('getTargetFileName')); + } + + public function testParseProjectYamlAddsPrecompiledObjectsAsGenericLinkInputs(): void + { + $projectFile = $this->createProjectFile(<<<'YAML' +sources: + - main.php +objects: + - build/startup.o + - path: build/platform.obj + if: PHP_OS_FAMILY == "Linux" + - path: build/windows.obj + if: PHP_OS_FAMILY == "Windows" +cxx-flags: [-fno-exceptions, -fno-rtti] +c-flags: -ffreestanding -fno-builtin +asm-flags: + - -m64 + - -mno-red-zone +YAML, 'objects.yml', 'native-objects'); + + $this->invokeMethod('parseProjectYaml', $projectFile); + + $projectDir = dirname($projectFile); + $this->assertSame([ + $projectDir . '/build/startup.o', + $projectDir . '/build/platform.obj', + ], $this->compiler->getProjectObjectFiles()); + $this->assertSame('-fno-exceptions -fno-rtti', $this->getPropertyValue('cxxFlags')); + $this->assertSame('-ffreestanding -fno-builtin', $this->getPropertyValue('cFlags')); + $this->assertSame('-m64 -mno-red-zone', $this->getPropertyValue('asmFlags')); } public function testCliOutputOverridesYamlOutputOnlyWhenCommandLineArgumentsAreApplied(): void diff --git a/phpunit/src/GeneratedCodeIndentationTest.php b/phpunit/src/GeneratedCodeIndentationTest.php index 0723a7ed..379931d7 100644 --- a/phpunit/src/GeneratedCodeIndentationTest.php +++ b/phpunit/src/GeneratedCodeIndentationTest.php @@ -30,7 +30,11 @@ class GeneratedCodeIndentationTest extends \PHPUnit\Framework\TestCase $code, ); $this->assertStringContainsString( - "\ttry {\n\t\tphp::checkCallArgCount(1, 1, false);", + "\tPHPX_TRY {\n\t\tphp::checkCallArgCount(1, 1, false);", + $code, + ); + $this->assertStringContainsString( + "\t} PHPX_CATCH(zend_object *, typephp_wrapper_exception) {", $code, ); $this->assertStringNotContainsString("\ntry {", $code); diff --git a/src/Backend/GccLikeBackend.php b/src/Backend/GccLikeBackend.php index 484064c2..7fdde5d5 100644 --- a/src/Backend/GccLikeBackend.php +++ b/src/Backend/GccLikeBackend.php @@ -106,6 +106,10 @@ abstract class GccLikeBackend extends CompilerBackend . ' -mllvm -wasm-use-legacy-eh=false'; } + if (!$includeCppStd && !empty($config['cflags'])) { + $cmd .= ' ' . $config['cflags']; + } + if (!empty($config['target_platform'])) { $cmd .= ' --target=' . $config['target_platform']; } @@ -247,6 +251,9 @@ abstract class GccLikeBackend extends CompilerBackend } $cmd .= $this->buildCompileOptions($options); + if (!empty($options['nativeflags'])) { + $cmd .= ' ' . $options['nativeflags']; + } return $cmd; } diff --git a/src/Backend/Msvc.php b/src/Backend/Msvc.php index 3e70af5d..6504dda5 100644 --- a/src/Backend/Msvc.php +++ b/src/Backend/Msvc.php @@ -106,6 +106,8 @@ class Msvc extends CompilerBackend if (!empty($config['forced_include'])) { $cmd .= ' /FI' . escapeshellarg($config['forced_include']); } + } elseif (!empty($config['cflags'])) { + $cmd .= ' ' . $config['cflags']; } $cmd .= ' /nologo'; diff --git a/src/Build/NativeCommandOptionsTrait.php b/src/Build/NativeCommandOptionsTrait.php index ef1b362e..760326b8 100644 --- a/src/Build/NativeCommandOptionsTrait.php +++ b/src/Build/NativeCommandOptionsTrait.php @@ -104,7 +104,9 @@ trait NativeCommandOptionsTrait protected function getCCompileCommandOptions(): CompileOptions { $options = $this->getCommonCompileCommandOptions(); - $options = $options->with('suppressed_warnings', ['4244', '4146']); + $options = $options + ->with('cflags', $this->cFlags) + ->with('suppressed_warnings', ['4244', '4146']); return $this->isNanoMode() ? $options->with('c_std', 'c11') : $options; } @@ -122,6 +124,10 @@ trait NativeCommandOptionsTrait if ($language === 'objective-c++') { $options = $options->with('cpp_std', $this->cxxStd)->with('cxxflags', $this->cxxFlags); + } elseif ($language === 'assembler') { + $options = $options->with('nativeflags', $this->asmFlags); + } elseif ($language === 'objective-c') { + $options = $options->with('nativeflags', $this->cFlags); } return $options; diff --git a/src/Build/SourcePipelineTrait.php b/src/Build/SourcePipelineTrait.php index 0450f4b1..18da9161 100644 --- a/src/Build/SourcePipelineTrait.php +++ b/src/Build/SourcePipelineTrait.php @@ -128,6 +128,7 @@ trait SourcePipelineTrait // Apply command-line arguments after all configuration is loaded (so they // take the highest precedence) $this->applyCommandLineArguments(); + $this->validateProjectObjectFiles(); // The generated public import stub is an output artifact, not an input // of the library that produced it. Exclude a previous build's copy when diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 26d7d3a6..34459685 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -500,12 +500,16 @@ class CompilerBase implements PropertyAccessContext protected int $maxJob = 4; protected string $buildMode = self::BUILD_MODE_BIN; protected string $cxxFlags = ''; + protected string $cFlags = ''; + protected string $asmFlags = ''; protected string $cxxStd = 'c++17'; protected string $march = ''; // --march: target CPU instruction set (e.g. native, x86-64-v3) protected string $targetPlatform = ''; // --target-platform: cross-compilation target triple (e.g. aarch64-linux-gnu) protected string $ldflags = ''; protected array $linkLibs = []; // --link-lib / -l: user-specified libraries to link protected array $linkPaths = []; // --link-path / -L: user-specified library search paths + /** @var list Precompiled project object files linked into the target. */ + protected array $projectObjectFiles = []; /** @var list Required PHP modules recorded in zend_module_entry.deps. */ protected array $extensionDependencies = []; protected bool $debug = false; diff --git a/src/Config/ProjectYamlLoader.php b/src/Config/ProjectYamlLoader.php index 69563d57..2652e000 100644 --- a/src/Config/ProjectYamlLoader.php +++ b/src/Config/ProjectYamlLoader.php @@ -31,20 +31,32 @@ final class ProjectYamlLoader /** @return array{0: string, 1: string|null} */ public function parseSourceEntry(mixed $entry): array + { + return $this->parsePathEntry($entry, 'sources'); + } + + /** @return array{0: string, 1: string|null} */ + public function parseObjectEntry(mixed $entry): array + { + return $this->parsePathEntry($entry, 'objects'); + } + + /** @return array{0: string, 1: string|null} */ + private function parsePathEntry(mixed $entry, string $key): array { if (is_string($entry)) { return [$entry, null]; } if (!is_array($entry)) { - ($this->error)('Each `sources` entry must be a string or map'); + ($this->error)("Each `{$key}` entry must be a string or map"); } $path = $entry['path'] ?? $entry['source'] ?? $entry['file'] ?? null; if (!is_string($path) || trim($path) === '') { - ($this->error)('Conditional `sources` entries must include a non-empty `path`'); + ($this->error)("Conditional `{$key}` entries must include a non-empty `path`"); } $condition = $entry['if'] ?? $entry['when'] ?? null; if ($condition !== null && !is_string($condition)) { - ($this->error)('Source condition must be a string'); + ($this->error)(ucfirst(rtrim($key, 's')) . ' condition must be a string'); } return [$path, $condition]; } diff --git a/src/Translator.php b/src/Translator.php index a4a8e516..39a76b17 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -84,6 +84,8 @@ class Translator extends Preprocessor public const string APP_NAME = 'TypePHP Compiler (AOT)'; protected bool $hasExplicitOutput = false; + /** Exact output stem; generated C/C++ identifiers still use targetName. */ + protected ?string $explicitOutputBasename = null; protected ?string $explicitOutputExtension = null; protected array $sourceDirs = []; private ?ProjectYamlLoader $projectYamlLoader = null; @@ -870,12 +872,13 @@ class Translator extends Preprocessor } $this->hasExplicitOutput = true; + $this->explicitOutputBasename = $path; $this->setTargetName($path); } protected function getTargetFileName(): string { - $targetFile = $this->targetName; + $targetFile = $this->explicitOutputBasename ?? $this->targetName; if ($this->isBuildModeLib() && !$this->isWindows() && !$this->hasExplicitOutput) { $targetFile = 'lib' . $targetFile; } @@ -3056,6 +3059,24 @@ CODE; $cfg = $this->getProjectYamlLoader()->load($path); $projectDir = dirname($path); + $objects = $cfg['objects'] ?? []; + if (!is_array($objects)) { + $this->error('`objects` must be an array'); + } + foreach ($objects as $entry) { + [$object, $condition] = $this->getProjectYamlLoader()->parseObjectEntry($entry); + if ($condition !== null && !$this->evaluateProjectYamlCondition($condition)) { + continue; + } + $object = $this->resolvePath($object, $projectDir, 'Object path'); + if (!in_array(strtolower(pathinfo($object, PATHINFO_EXTENSION)), ['o', 'obj'], true)) { + $this->error("Project object must use the .o or .obj extension: {$object}"); + } + $object = realpath($object) ?: $object; + $this->projectObjectFiles[] = $object; + } + $this->projectObjectFiles = array_values(array_unique($this->projectObjectFiles)); + if (array_key_exists('php-version', $cfg) && !$this->climate->arguments->defined('php-version')) { $this->setPhpVersion((string) $cfg['php-version']); } @@ -3135,6 +3156,21 @@ CODE; } } + // Read C and assembler flags independently. This keeps project-level + // native builds generic while allowing each language its own ABI flags. + $cFlags = $cfg['c-flags'] ?? null; + if (!empty($cFlags)) { + $this->cFlags = is_array($cFlags) + ? implode(' ', $cFlags) + : str_replace("\n", ' ', (string) $cFlags); + } + $asmFlags = $cfg['asm-flags'] ?? null; + if (!empty($asmFlags)) { + $this->asmFlags = is_array($asmFlags) + ? implode(' ', $asmFlags) + : str_replace("\n", ' ', (string) $asmFlags); + } + // Read cxx-std $cxxStd = $cfg['cxx-std'] ?? null; if (!empty($cxxStd)) { @@ -3337,6 +3373,27 @@ CODE; return $this->filterIgnoredFiles($list); } + /** @return list */ + public function getProjectObjectFiles(): array + { + return $this->projectObjectFiles; + } + + protected function validateProjectObjectFiles(): void + { + if ($this->isDryRun()) { + return; + } + foreach ($this->projectObjectFiles as $object) { + if (!is_file($object)) { + $this->error( + "Precompiled project object not found: {$object}\n" + . 'Compile project C/C++/assembly sources before invoking tpc.' + ); + } + } + } + /** * @return array{0: string, 1: string|null} */ @@ -5055,7 +5112,7 @@ CODE; // returns with EG(exception) set. Convert back to normal Zend exception // propagation at the outermost wrapper. $this->indentLevel++; - $cppCode = $this->getIndent() . 'try {' . PHP_EOL; + $cppCode = $this->getIndent() . 'PHPX_TRY {' . PHP_EOL; $this->indentLevel++; $argCountCheck = $this->genParameterCountCheck( @@ -5177,7 +5234,8 @@ CODE; } } $this->indentLevel--; - $cppCode .= $this->getIndent() . '} catch (zend_object *) {' . PHP_EOL; + $cppCode .= $this->getIndent() + . '} PHPX_CATCH(zend_object *, typephp_wrapper_exception) {' . PHP_EOL; $this->indentLevel++; $cppCode .= $this->getIndent() . '/* EG(exception) is already set; return control to ZendVM for frame cleanup. */' . PHP_EOL; $this->indentLevel--; diff --git a/src/compiler.php b/src/compiler.php index 09fc76d8..5fe149ff 100644 --- a/src/compiler.php +++ b/src/compiler.php @@ -111,7 +111,10 @@ function main(int $argc, array $argv): void } // Compile all C++ source files. - $objectFiles = $translator->compile($sourceFiles); + $objectFiles = [ + ...$translator->compile($sourceFiles), + ...$translator->getProjectObjectFiles(), + ]; // Link all object files to produce the executable. $binaryFile = $translator->build($objectFiles); // If --run / -r was specified, execute immediately after compilation.