From 82e7ce35f6e5d6af5ceef9ff37db62896a976852 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 13 Nov 2026 13:48:20 +0900 Subject: [PATCH 1/2] 0.9.0: main and its initialisers, the environment, time and the board's exit; x86_64-uefi through openkal-uefi-platform efi_main turns off the watchdog, records the image's tables and load options, runs the program's static initialisers from the image's .ctors and .CRT sections, and calls main(argc, argv, envp). The interfaces are abort, stream, memory, env and time: GetTime for the wall clock, the processor's counter measured against Stall for the monotonic clock, Stall for a sleep. kal_exit calls the board's exit (okuefi_board_exit, weak) and then Exit with the status. The specification is declared as ^0.25.0 (openkal clause 8). The target row, its compile and link conventions and its runner are the platform package's; examples/hello names it. CI builds the example for x86_64-uefi, checks the image's headers, and boots it under QEMU with the edk2 firmware, reading the status from the debug-exit device. --- .github/workflows/ci.yml | 95 +++++++++++++++----------- .gitignore | 8 +-- README.md | 110 +++++++++++------------------- examples/hello/mcpp.toml | 30 +++----- examples/hello/src/main.cpp | 38 ++++++++--- mcpp.toml | 7 +- src/env.cpp | 38 +++++++++++ src/kal.cpp | 33 +++++---- src/start.cpp | 132 ++++++++++++++++++++++++++++++++++++ src/state.h | 18 +++++ src/time.cpp | 80 ++++++++++++++++++++++ src/uefi.h | 54 ++++++++++++++- src/version.cpp | 9 ++- 13 files changed, 483 insertions(+), 169 deletions(-) create mode 100644 src/env.cpp create mode 100644 src/start.cpp create mode 100644 src/state.h create mode 100644 src/time.cpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd436dc..227eaee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,8 +22,8 @@ jobs: # The specification is taken from the branch of the same name where one # exists, so a change spanning both repositories is reviewed as a whole. OPENKAL_BRANCH: ${{ github.head_ref || github.ref_name }} - MCPP_VERSION: 2026.8.27.1 - XLINGS_VERSION: v2026.8.17.2 + MCPP_VERSION: 2026.11.12.1 + XLINGS_VERSION: v2026.10.26.1 XLINGS_NON_INTERACTIVE: '1' steps: - uses: actions/checkout@v4 @@ -202,6 +202,18 @@ jobs: } subst mcpp.toml .spec subst examples/hello/mcpp.toml ../../.spec + # THE PLATFORM, AS THE SPECIFICATION IS: its repository's branch of the same name, else its + # default branch, named by path in the example (the example states the released version). + - name: Point at the platform's working tree + run: | + set -euo pipefail + git clone --quiet https://github.com/mcpplibs/openkal-uefi-platform .platform + if git -C .platform rev-parse --verify --quiet "origin/$OPENKAL_BRANCH" > /dev/null; then + git -C .platform checkout --quiet "origin/$OPENKAL_BRANCH" + fi + f=examples/hello/mcpp.toml + sed "s|^platform = .*$|platform = { path = \"../../.platform\" }|" "$f" > "$f.next" && mv "$f.next" "$f" + grep -q 'path = "../../.platform"' "$f" || { echo "::error::$f was not substituted"; exit 1; } - name: Install OVMF and qemu run: sudo apt-get update && sudo apt-get install -y --no-install-recommends ovmf qemu-system-x86 @@ -209,46 +221,32 @@ jobs: run: | set -euo pipefail cd examples/hello - mcpp build - EXE=$(find target -name "hello.exe" | head -1) + mcpp build --target x86_64-uefi + EFI=$(find target -name "hello.efi" | head -1) + RO=$(ls -d "$HOME"/.mcpp/registry/data/xpkgs/xim-x-llvm/*/bin/llvm-readobj | sort -V | tail -1) + # Subsystem 10 (EFI application) is what firmware loads; no import table, since firmware + # satisfies no DLL. + "$RO" --file-headers "$EFI" | grep -q "IMAGE_SUBSYSTEM_EFI_APPLICATION" + if "$RO" --coff-imports "$EFI" | grep -qi "Name:"; then echo "::error::an import table"; exit 1; fi - # THE TOOLCHAIN THAT BUILT THE IMAGE, NOT LLVM. - # - # This target resolves to mingw-cross-gcc, so on a machine that has - # never built anything else there is no LLVM to borrow `llvm-readobj` - # from — the previous version of this step globbed for one and died on - # `ls: cannot access .../xim-x-llvm/*/bin/llvm-readobj`, AFTER the - # build had succeeded. The verification tool has to come from the same - # place as the compiler, or the check is only available on machines - # that happen to have another one. - OD=$(ls -d "$HOME"/.mcpp/registry/data/xpkgs/xim-x-mingw-cross-gcc/*/bin/x86_64-w64-mingw32-objdump | head -1) - test -x "$OD" - - # Subsystem 10 is what makes firmware load it. A Windows console - # subsystem would build, link, and never be executed by UEFI. - # Measured spelling from this objdump: `Subsystem 0000000a (EFI application)`. - "$OD" -p "$EXE" | grep -qE 'Subsystem[[:space:]]+0000000a' - - # And no import table: a DLL dependency cannot be satisfied by firmware. - if "$OD" -p "$EXE" | grep -qi "DLL Name"; then - "$OD" -p "$EXE" | grep -i -A2 "DLL Name"; echo "unexpected DLL import"; exit 1 - fi - - - name: It boots under OVMF and prints + # THE STATUS COMES BACK. The platform's board exit writes it to QEMU's debug-exit port, which + # ends the emulator with (status << 1) | 1: the example ends with 42 when every check held. + - name: It boots under OVMF, runs to its end and returns its status run: | set -euo pipefail cd examples/hello - EXE=$(find target -name "hello.exe" | head -1) - mkdir -p esp/EFI/BOOT && cp "$EXE" esp/EFI/BOOT/BOOTX64.EFI - cp /usr/share/OVMF/OVMF_VARS_4M.fd vars.fd - timeout 90 qemu-system-x86_64 -machine q35 -m 256 -display none \ - -serial file:uefi.log \ + EFI=$(find target -name "hello.efi" | head -1) + mkdir -p esp/EFI/BOOT && cp "$EFI" esp/EFI/BOOT/BOOTX64.EFI + set +e + timeout 120 qemu-system-x86_64 -machine q35 -m 256 -nographic -no-reboot \ -drive if=pflash,format=raw,readonly=on,file=/usr/share/OVMF/OVMF_CODE_4M.fd \ - -drive if=pflash,format=raw,file=vars.fd \ - -drive format=raw,file=fat:rw:esp -net none || true - cat uefi.log | tr -d "\\000" | head -40 - grep -q "hello from openkal over UEFI" uefi.log - grep -q "pool ok" uefi.log + -drive if=virtio,format=raw,readonly=on,file=fat:esp \ + -device isa-debug-exit,iobase=0xf4,iosize=0x04 -net none < /dev/null > uefi.log 2>&1 + status=$? + set -e + tr -d '\000' < uefi.log | tail -20 + grep -q "all held" uefi.log + [ "$status" -eq 85 ] || { echo "::error::QEMU ended with $status, not 85 (42 << 1 | 1)"; exit 1; } # --------------------------------------------------------------------------- # The cross-compilation is performed FROM three systems, not only from Linux. @@ -288,8 +286,8 @@ jobs: # The specification is taken from the branch of the same name where one # exists, so a change spanning both repositories is reviewed as a whole. OPENKAL_BRANCH: ${{ github.head_ref || github.ref_name }} - MCPP_VERSION: 2026.8.27.1 - XLINGS_VERSION: v2026.8.17.2 + MCPP_VERSION: 2026.11.12.1 + XLINGS_VERSION: v2026.10.26.1 XLINGS_NON_INTERACTIVE: '1' steps: - uses: actions/checkout@v4 @@ -477,13 +475,28 @@ jobs: || { echo "::error::$1 was not substituted"; exit 1; } } subst mcpp.toml .spec + subst examples/hello/mcpp.toml ../../.spec + # THE PLATFORM, AS THE SPECIFICATION IS: its repository's branch of the same name, else its + # default branch, named by path in the example (the example states the released version). + - name: Point at the platform's working tree + run: | + set -euo pipefail + git clone --quiet https://github.com/mcpplibs/openkal-uefi-platform .platform + if git -C .platform rev-parse --verify --quiet "origin/$OPENKAL_BRANCH" > /dev/null; then + git -C .platform checkout --quiet "origin/$OPENKAL_BRANCH" + fi + f=examples/hello/mcpp.toml + sed "s|^platform = .*$|platform = { path = \"../../.platform\" }|" "$f" > "$f.next" && mv "$f.next" "$f" + grep -q 'path = "../../.platform"' "$f" || { echo "::error::$f was not substituted"; exit 1; } - name: The EFI application cross-builds run: | + cd examples/hello # TWICE, AND THE FIRST IS ALLOWED TO FAIL — every row of this # matrix is a machine that has never targeted this triple, which is # precisely where mcpp's lazy install of the target C library shows. # The first build compiles sources needing its headers before they # exist; the second has them. A single build here reported a # portability failure that was really a cold-machine one. - mcpp build || true - mcpp build + mcpp build --target x86_64-uefi || true + mcpp build --target x86_64-uefi + test -n "$(find target -name hello.efi | head -1)" diff --git a/.gitignore b/.gitignore index 73453ed..6ca15ea 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,8 @@ -target/ -.mcpp/ -compile_commands.json -mcpp.lock # A working tree of the specification placed beside the sources. No trailing +compile_commands.json +.mcpp/ +mcpp.lock # slash: the pattern must match a symbolic link to one as well as a directory. .spec +target/ diff --git a/README.md b/README.md index 7658df0..8e592b3 100644 --- a/README.md +++ b/README.md @@ -3,92 +3,62 @@ An implementation of [openkal][kal] on UEFI Boot Services, for applications the firmware loads before an operating system exists. -```toml -[build] -target = "x86_64-windows-gnu" -ldflags = ["-nostdlib", "-Wl,--subsystem,10", "-Wl,-e,efi_main"] +A program names the target and nothing else; the row `x86_64-uefi`, its compile +and link conventions and its runner are the platform package's +([openkal-uefi-platform][plat]), which selects this implementation: -[dependencies] -openkal = "0.14.1" -openkal-uefi = "0.8.1" +```toml +[target.x86_64-uefi] +platform = "openkal-uefi-platform@0.1.0" ``` -## The target is `x86_64-windows-gnu`, and that is not a workaround - -A UEFI application is PE/COFF with subsystem 10, entered through the Microsoft -x64 calling convention. Both are properties this toolchain already has, so -firmware function pointers are called directly rather than through per-call -annotations. - -The first analysis of this backend concluded that mcpp would need a new "PE -freestanding" target before a UEFI application could exist at all. **Measured: -it does not.** The three link flags above produce -`IMAGE_SUBSYSTEM_EFI_APPLICATION (0xA)` with no DLL imports, which is what -firmware loads. +```sh +mcpp build --target x86_64-uefi # hello.efi: PE32+, subsystem EFI application +mcpp run --target x86_64-uefi # booted under QEMU with the edk2 firmware +``` -## Why UEFI fits this specification +## What a program receives -openkal divides its interfaces by the **kind of resource** rather than by which -part of a standard library becomes available, and Boot Services divide the same -way. This backend is mostly forwarding: +The firmware calls `efi_main`; this implementation records its tables, turns off +the boot manager's watchdog, runs the program's static initialisers (read from +the image's own `.ctors` and `.CRT` sections, there being no C runtime) and calls +`main(argc, argv, envp)`. The arguments are the image's load options split at +spaces; firmware that gave none gives one empty name. | openkal | UEFI | |---|---| -| `stream` | `SIMPLE_TEXT_OUTPUT_PROTOCOL` | -| `memory` | `AllocatePool` / `FreePool` | -| `abort` | `Exit` | - -`process` and `task` are absent because UEFI has no process model. An -application is the only thing running, and an interface provided in part would -be worse than one provided not at all — `import openkal.process;` does not -resolve, which is the honest answer rather than a set of calls that always fail. - -The five interfaces version 0.8 adds are absent by the same rule. UEFI has no -process model to copy an address space within, so `space` is not provided; no -second context to bound a wait against, so `timeout` is not; and while Boot -Services do expose a network stack, this backend forwards only the protocols -listed above, so `net` and `datagram` are not. `terminal` is absent because -`SIMPLE_TEXT_OUTPUT_PROTOCOL` has no mode to read or set. +| `stream` | `SIMPLE_TEXT_OUTPUT_PROTOCOL` (UCS-2, `\n` as CR LF); input reports its end | +| `memory` | `AllocatePool` / `FreePool`, over-allocated for a stricter alignment | +| `env` | the load options; no variables, image, loader or system name (absent) | +| `time` | `GetTime` for the wall clock; the processor's counter, measured against `Stall`, for the monotonic one; `Stall` for a sleep | +| `abort` | `Exit` with the status as a warning code, so a starter reads the number | -Clause 6.1 makes each absence a link-time absence, so a program requiring one of -them is refused when it is built rather than when it runs. +`process` waits for `fs` over the firmware's file system protocol, since a +program is started by name through a directory; the specification's property for +an environment with one task (`KAL_PROCESS_PROP_RUNS_TO_END`, openkal 0.25.1 +clause 7.19) is what it will claim. The rest have no firmware service beneath them. -## The three places this is not forwarding +## The board's exit -**Text.** openkal streams carry bytes; UEFI's console takes UCS-2 and treats a -bare line feed as a cursor movement that does not return the carriage. Every -write widens and translates, through a fixed-size buffer rather than an -allocation — a diagnostic path that allocates is one that can fail while -reporting a failure. - -**Alignment.** `AllocatePool` has no alignment parameter and guarantees eight -bytes. A stricter request is satisfied by over-allocating and storing the -original pointer immediately before the aligned address, which is what a C -library does where the platform lacks `aligned_alloc`. - -**Input.** `kal_stream_read` reports end of input rather than pretending. -UEFI's console input is a key-stroke protocol with a wait event, not a byte -stream; presenting it as one would give a reader something that appears to work -and silently loses every key that is not a plain character. +A status leaves an application through `Exit`, to whoever started it. A board on +which it must leave the machine --- an emulator running it as a test --- defines +`okuefi_board_exit(int)` in its platform package, and `kal_exit` calls it first; +the definition here does nothing. openkal-uefi-platform's writes the status to +QEMU's debug-exit port, only when the processor names QEMU as its hypervisor. ## The table offsets are checked, not trusted -These structures are ABI. A field written one slot out reads a neighbouring -function pointer — which calls something that exists, with the wrong arguments, -and the failure appears far from the cause. Every member this backend reaches is -pinned by a `static_assert` on its offset, so a mistake is a compile error. +These structures are ABI. A field one slot out reads a neighbouring function +pointer, which calls something that exists with the wrong arguments. Every member +this implementation reaches is pinned by a `static_assert` on its offset. ## Verified -`examples/hello` boots under OVMF in QEMU as `EFI/BOOT/BOOTX64.EFI`: - -``` -BdsDxe: starting Boot0001 "UEFI QEMU HARDDISK QM00001 " … -hello from openkal over UEFI -pool ok -``` - -`kal_exit` returns control to the firmware, which proceeds to its own boot -manager — the status reaches the loader rather than the program spinning. +`examples/hello`, built with mcpp 2026.11.12.1 and booted by dirun as +`EFI/BOOT/BOOTX64.EFI` under QEMU's q35 with the edk2 firmware (0.45 s, KVM): +a static initialiser ran, the arguments are counted, an over-aligned allocation, +a 20 ms sleep measured as such, the wall clock reads the present; status 42 +returned through the board's exit. [kal]: https://github.com/mcpplibs/openkal +[plat]: https://github.com/mcpplibs/openkal-uefi-platform diff --git a/examples/hello/mcpp.toml b/examples/hello/mcpp.toml index 705c56a..7c8727f 100644 --- a/examples/hello/mcpp.toml +++ b/examples/hello/mcpp.toml @@ -1,25 +1,15 @@ [package] name = "hello" -version = "0.1.0" +version = "0.2.0" -[build] -target = "x86_64-windows-gnu" -# The three that make this an application firmware will load, rather than a -# Windows one. `-nostdlib` removes the C runtime and the default import -# libraries; `--subsystem,10` is EFI_APPLICATION; the entry point is the one -# UEFI calls, not `mainCRTStartup`. -ldflags = ["-nostdlib", "-Wl,--subsystem,10", "-Wl,-e,efi_main"] +[targets.hello] +kind = "bin" +main = "src/main.cpp" [dependencies] -# THE FORM MUST MATCH THE ONE THE IMPLEMENTATION USES, not merely the -# version. mcpp refuses a graph in which one package reaches a dependency by -# version and another by git: -# -# dependency 'mcpplibs.openkal' is requested as both a version dep -# (by 'hello') and a git dep (by 'mcpplibs.openkal-uefi@path'). -# -# That is the right refusal --- two forms can name two different trees --- and -# it means an example inside a repository follows that repository's own -# declaration while a change is in flight. -openkal = "0.9.0" -openkal-uefi = { path = "../.." } +openkal = "^0.25.0" +# The implementation under change; the platform names openkal-uefi by version. +openkal-uefi = { path = "../..", override = true } + +[target.x86_64-uefi] +platform = "openkal-uefi-platform@0.1.0" diff --git a/examples/hello/src/main.cpp b/examples/hello/src/main.cpp index 2f34428..ca5ccd8 100644 --- a/examples/hello/src/main.cpp +++ b/examples/hello/src/main.cpp @@ -1,11 +1,10 @@ -// A UEFI application written against openkal, with no UEFI type in sight. -// -// `kal_main` and not `main`: nothing here supplies a C runtime, and the -// backend's `efi_main` is what firmware calls. That indirection is the point — -// the same source compiles against any openkal implementation. +// A UEFI application written against openkal, with no UEFI type in sight: a static initialiser, +// the arguments, the two clocks, an over-aligned allocation, and a status the board carries out. import openkal.stream; import openkal.abort; import openkal.memory; +import openkal.env; +import openkal.time; namespace { void say(const char* s) { @@ -13,14 +12,31 @@ void say(const char* s) { while (s[n]) ++n; kal_stream_write(kal_stdout(), s, n); } +int failures = 0; +void check(bool ok, const char* what) { + say(ok ? " ok " : " FAIL "); + say(what); + say("\n"); + if (!ok) ++failures; +} +struct initialised { + int value = 7; + initialised() { value = 42; } +} g_initialised; } // namespace -extern "C" [[noreturn]] void kal_main() { +int main(int argc, char**, char**) { say("hello from openkal over UEFI\n"); - - void* p = kal_alloc(4096, 64); // over-aligned on purpose - say(p ? "pool ok\n" : "pool failed\n"); + check(g_initialised.value == 42, "a static initialiser ran before main"); + check(argc >= 1 && kal_env_arg_count() == static_cast(argc), "the arguments are counted alike"); + void* p = kal_alloc(4096, 64); + check(p != nullptr && (reinterpret_cast(p) & 63) == 0, "an over-aligned allocation"); kal_free(p, 4096, 64); - - kal_exit(p ? 0 : 1); + const kal_duration t0 = kal_time_monotonic(); + kal_time_sleep(20u * 1000u * 1000u); + const kal_duration t1 = kal_time_monotonic(); + check(t1 - t0 >= 19u * 1000u * 1000u && t1 - t0 < 2000u * 1000u * 1000u, "a sleep of 20 ms is measured as such"); + check(kal_time_wall() > 1700000000ull * 1000000000ull, "the wall clock reads the present"); + say(failures ? "FAILED\n" : "all held\n"); + return failures ? 1 : 42; } diff --git a/mcpp.toml b/mcpp.toml index 0eca6b4..3b5a2a5 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -15,7 +15,7 @@ [package] namespace = "mcpplibs" name = "openkal-uefi" -version = "0.8.1" +version = "0.9.0" description = "An implementation of openkal on UEFI Boot Services, for applications the firmware loads before an operating system exists" license = "Apache-2.0" @@ -28,11 +28,14 @@ license = "Apache-2.0" # package answers to; several packages answer to it and the engine knows none # of them by name. provides = ["mcpp:kernel-abi=openkal"] +# The interfaces this implementation provides, which `kal_interfaces()' answers too (src/version.cpp). +provides-interfaces = ["openkal.abort", "openkal.env", "openkal.memory", "openkal.stream", "openkal.time"] authors = ["mcpplibs"] repo = "https://github.com/mcpplibs/openkal-uefi" [dependencies] -openkal = "0.14.1" +# Every patch release of the specification's minor version (openkal clause 8). +openkal = "^0.25.0" [build] flags = [ diff --git a/src/env.cpp b/src/env.cpp new file mode 100644 index 0000000..a52a80d --- /dev/null +++ b/src/env.cpp @@ -0,0 +1,38 @@ +// openkal.env on Boot Services: the arguments are the image's options (start.cpp); the firmware +// keeps no environment of variables a program inherits, and names no image, loader or system a +// program could be told, so each of those is absent (clause 7.7). +#include + +#include "state.h" + +namespace { +kal_uintptr length(const char* s) { kal_uintptr n = 0; while (s[n]) ++n; return n; } +} // namespace + +extern "C" { + +kal_uintptr kal_env_arg_count(void) { + return static_cast(okuefi::self().argc); +} + +kal_intptr kal_env_arg(kal_uintptr index, char* out, kal_uintptr cap) { + const okuefi::state& s = okuefi::self(); + if (index >= static_cast(s.argc)) return -kal_err_not_found; + const char* a = s.argv[index]; + const kal_uintptr n = length(a); + if (out && cap) { + const kal_uintptr k = n < cap ? n : cap; + for (kal_uintptr i = 0; i < k; ++i) out[i] = a[i]; + } + return static_cast(n); +} + +kal_uintptr kal_env_var_count(void) { return 0; } +kal_intptr kal_env_var(const char*, kal_uintptr, char*, kal_uintptr) { return -kal_err_not_found; } +kal_intptr kal_env_var_at(kal_uintptr, char*, kal_uintptr) { return -kal_err_not_found; } +kal_intptr kal_env_image(char*, kal_uintptr) { return -kal_err_not_found; } +kal_intptr kal_env_loader(char*, kal_uintptr) { return -kal_err_not_found; } +kal_intptr kal_env_system(char*, kal_uintptr) { return -kal_err_not_found; } +int kal_env_resources(void) { return KAL_RESOURCES_BALANCED; } + +} // extern "C" diff --git a/src/kal.cpp b/src/kal.cpp index 1cf0fe1..ba6554e 100644 --- a/src/kal.cpp +++ b/src/kal.cpp @@ -30,6 +30,7 @@ #include #include "uefi.h" +#include "state.h" namespace { @@ -37,8 +38,9 @@ constexpr kal_uintptr kStdin = 0; constexpr kal_uintptr kStdout = 1; constexpr kal_uintptr kStderr = 2; -efi_handle g_image = nullptr; -efi_system_table* g_st = nullptr; +// The firmware's tables, recorded by efi_main (start.cpp). +#define g_st (okuefi::self().st) +#define g_image (okuefi::self().image) // A line at a time is enough to keep the trap count low without making this a // large object; UEFI's own console is not fast, and the buffer is a translation @@ -78,15 +80,12 @@ extern "C" { // `kal_main` and not `main`: there is no C runtime to call one, and naming // it `main` would invite a toolchain to attach startup code that does not exist // in this arrangement. -[[noreturn]] void kal_main(); +// THE BOARD'S EXIT. A status leaves an application through Exit, to whoever started it --- a +// shell, the boot manager --- and a board on which a program's status must leave the machine (an +// emulator that runs it as a test) defines this function in its platform package to carry it +// there before Exit is called. The definition here is the default, and does nothing. +[[gnu::weak]] void okuefi_board_exit(int) {} -efi_status efi_main(efi_handle image, efi_system_table* st) { - g_image = image; - g_st = st; - kal_main(); -} - -// ── openkal.abort ─────────────────────────────────────────────────────────── KAL_NORETURN void kal_abort(const char* msg, kal_uintptr len) { if (g_st && msg && len) write_to(g_st->std_err ? g_st->std_err : g_st->con_out, @@ -100,10 +99,11 @@ KAL_NORETURN void kal_abort(const char* msg, kal_uintptr len) { } KAL_NORETURN void kal_exit(int code) { + okuefi_board_exit(code); + // The status itself, as a warning code (the high bit clear), so that the program that started + // this one reads the number and not only success or failure. if (g_st) - g_st->boot_services->exit(g_image, - code == 0 ? EFI_SUCCESS : EFI_ABORTED, - 0, nullptr); + g_st->boot_services->exit(g_image, static_cast(code & 0xff), 0, nullptr); for (;;) {} } @@ -161,6 +161,13 @@ kal_uintptr kal_stream_props(kal_stream s) { // caller that rounds to it is never wrong. kal_uintptr kal_memory_granularity(void) { return 4096; } +// The pool's size is the firmware's and not stated by Boot Services; nothing is reported. +int kal_memory_info(kal_memory_status* out) { + if (out == nullptr || out->self_size < 2 * sizeof(kal_u32)) return kal_err_invalid; + out->present = 0; + return kal_ok; +} + void* kal_alloc(kal_uintptr size, kal_uintptr align) { if (!g_st) return nullptr; if (size == 0) size = 1; diff --git a/src/start.cpp b/src/start.cpp new file mode 100644 index 0000000..69230e6 --- /dev/null +++ b/src/start.cpp @@ -0,0 +1,132 @@ +// What the firmware calls, and what a program receives (openkal clause 7.13). +// +// A UEFI application is entered at `efi_main(image, system table)` in the Microsoft x64 +// convention, which the `x86_64-uefi` row's compiler uses by default. The program is a C or C++ +// program like any other: its static initialisers run, then `main(argc, argv, envp)`, and its +// status is the status of `kal_exit`. +#include + +#include "uefi.h" +#include "state.h" + +extern "C" int main(int, char**, char**); + +namespace { + +// THE STATIC INITIALISERS are found in this image's own section table. A compiler for this target +// places each translation unit's in `.ctors' (the GNU list, measured with clang 23 for +// x86_64-unknown-uefi), another COFF compiler in `.CRT$XCU' (merged into `.CRT'); the linker +// states the bounds of neither here, and nothing else runs them, there being no C runtime. So the +// sections are read from the PE header the firmware loaded, and each entry that is a function +// (neither 0 nor the list's -1 sentinel) is called, in the order the linker placed them. +using initialiser = void (*)(); + +void run_section(const unsigned char* base, const char* name) { // name: eight bytes, padded with NUL + const auto u32 = [](const unsigned char* p) { + return static_cast(p[0]) | static_cast(p[1]) << 8 | + static_cast(p[2]) << 16 | static_cast(p[3]) << 24; + }; + const auto u16 = [](const unsigned char* p) { return static_cast(p[0]) | static_cast(p[1]) << 8; }; + if (base[0] != 'M' || base[1] != 'Z') return; + const unsigned char* nt = base + u32(base + 0x3c); + if (nt[0] != 'P' || nt[1] != 'E') return; + const unsigned sections = u16(nt + 6); + const unsigned optional = u16(nt + 20); + const unsigned char* sh = nt + 24 + optional; + for (unsigned i = 0; i < sections; ++i, sh += 40) { + bool same = true; + for (int k = 0; k < 8; ++k) + if (sh[k] != static_cast(name[k])) { same = false; break; } + if (!same) continue; + const unsigned size = u32(sh + 8); // VirtualSize + const unsigned rva = u32(sh + 12); + auto* p = reinterpret_cast(base + rva); + for (unsigned k = 0; k < size / sizeof(initialiser); ++k) { + const auto v = reinterpret_cast(p[k]); + if (v != 0 && v != ~0ull) p[k](); + } + } +} + +void run_initialisers(const void* image_base) { + if (!image_base) return; + const auto* base = static_cast(image_base); + run_section(base, ".ctors\0"); + run_section(base, ".CRT\0\0\0\0"); +} + +// The options the image was started with, as the arguments: UCS-2 text split at spaces, each +// word UTF-8. Options that are not text (a boot option's binary data) are none. +bool printable(const efi_char16* w, efi_uintn n) { + for (efi_uintn i = 0; i < n; ++i) + if (w[i] != 0 && (w[i] < 0x20 || w[i] == 0x7f)) return false; + return true; +} + +void record_arguments(const efi_loaded_image_protocol* li) { + okuefi::state& s = okuefi::self(); + if (!li) return; + const auto* w = static_cast(li->load_options); + efi_uintn n = li->load_options_size / sizeof(efi_char16); + if (!w || n == 0 || (li->load_options_size & 1u) || !printable(w, n)) return; + while (n && w[n - 1] == 0) --n; + // At most three bytes per UCS-2 unit, one terminator per word. + char* text = nullptr; + if (s.st->boot_services->allocate_pool(EfiLoaderData, n * 3 + n + 1, + reinterpret_cast(&text)) != EFI_SUCCESS) + return; + char** argv = nullptr; + if (s.st->boot_services->allocate_pool(EfiLoaderData, (n / 2 + 2) * sizeof(char*), + reinterpret_cast(&argv)) != EFI_SUCCESS) + return; + int argc = 0; + efi_uintn at = 0; + char* o = text; + while (at < n) { + while (at < n && w[at] == u' ') ++at; + if (at >= n) break; + argv[argc++] = o; + for (; at < n && w[at] != u' '; ++at) { + const unsigned c = w[at]; + if (c < 0x80) *o++ = static_cast(c); + else if (c < 0x800) { *o++ = static_cast(0xC0 | (c >> 6)); *o++ = static_cast(0x80 | (c & 0x3F)); } + else { *o++ = static_cast(0xE0 | (c >> 12)); *o++ = static_cast(0x80 | ((c >> 6) & 0x3F)); + *o++ = static_cast(0x80 | (c & 0x3F)); } + } + *o++ = '\0'; + } + argv[argc] = nullptr; + s.argc = argc; + s.argv = argv; +} + +} // namespace + +namespace okuefi { +state& self() { + static state s{}; + return s; +} +} // namespace okuefi + +extern "C" efi_status efi_main(efi_handle image, efi_system_table* st) { + okuefi::state& s = okuefi::self(); + s.image = image; + s.st = st; + // THE FIRMWARE RESETS THE MACHINE five minutes after it started an application unless told + // otherwise (the boot manager arms a watchdog); a program is not a boot loader. + st->boot_services->set_watchdog_timer(0, 0, 0, nullptr); + okuefi::calibrate_clock(); + efi_loaded_image_protocol* li = nullptr; + if (st->boot_services->handle_protocol(image, &kLoadedImageProtocol, reinterpret_cast(&li)) != EFI_SUCCESS) + li = nullptr; + record_arguments(li); + run_initialisers(li ? li->image_base : nullptr); + // Position zero is the name the program was started by; firmware that gave none gives an empty + // name rather than none (openkal.env). + static char name[] = ""; + static char* unnamed[] = {name, nullptr}; + static char* no_environment[] = {nullptr}; + if (s.argc == 0) { s.argc = 1; s.argv = unnamed; } + kal_exit(main(s.argc, s.argv, no_environment)); +} diff --git a/src/state.h b/src/state.h new file mode 100644 index 0000000..b75a74c --- /dev/null +++ b/src/state.h @@ -0,0 +1,18 @@ +// The firmware's handles, kept for the operations that need them. +#pragma once +#include "uefi.h" + +namespace okuefi { + +struct state { + efi_handle image; + efi_system_table* st; + int argc; + char** argv; + unsigned long long tsc_per_ms; // the processor's counter, measured against Stall +}; + +state& self(); +void calibrate_clock(); + +} // namespace okuefi diff --git a/src/time.cpp b/src/time.cpp new file mode 100644 index 0000000..8cbc258 --- /dev/null +++ b/src/time.cpp @@ -0,0 +1,80 @@ +// openkal.time on Boot Services. +// +// The wall clock is the firmware's (GetTime, a runtime service), whose resolution is the +// firmware's too --- a second on many. The monotonic clock is the processor's counter, measured +// against Stall when the program starts: Boot Services offer a monotonic COUNT +// (GetNextMonotonicCount) but no rate, and an event timer whose callbacks a single-task +// environment runs only while it waits. A sleep is Stall, which waits the microseconds it is told. +#include + +#include "state.h" + +namespace { + +unsigned long long counter() { +#if defined(__x86_64__) + unsigned lo, hi; + __asm__ __volatile__("rdtsc" : "=a"(lo), "=d"(hi)); + return (static_cast(hi) << 32) | lo; +#else +#error "openkal-uefi measures time with the processor's counter: x86_64" +#endif +} + +// Days from 1970-01-01 to a civil date (proleptic Gregorian). +long long days_from_civil(long long y, unsigned m, unsigned d) { + y -= m <= 2; + const long long era = (y >= 0 ? y : y - 399) / 400; + const unsigned yoe = static_cast(y - era * 400); + const unsigned doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; + const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + return era * 146097 + static_cast(doe) - 719468; +} + +} // namespace + +namespace okuefi { +void calibrate_clock() { + state& s = self(); + const unsigned long long a = counter(); + s.st->boot_services->stall(10000); // ten milliseconds + const unsigned long long b = counter(); + s.tsc_per_ms = (b - a) / 10; + if (s.tsc_per_ms == 0) s.tsc_per_ms = 1; +} +} // namespace okuefi + +extern "C" { + +kal_duration kal_time_monotonic(void) { + const okuefi::state& s = okuefi::self(); + const unsigned long long t = counter(); + return static_cast(t / s.tsc_per_ms * 1000000ull + (t % s.tsc_per_ms) * 1000000ull / s.tsc_per_ms); +} + +kal_duration kal_time_monotonic_granularity(void) { return 1000; } + +kal_duration kal_time_wall(void) { + const okuefi::state& s = okuefi::self(); + efi_time t{}; + if (!s.st || !s.st->runtime_services || s.st->runtime_services->get_time(&t, nullptr) != EFI_SUCCESS) + return 0; + long long secs = days_from_civil(t.year, t.month, t.day) * 86400 + t.hour * 3600 + t.minute * 60 + t.second; + if (t.time_zone != 2047) secs += static_cast(t.time_zone) * 60; // to UTC + return secs < 0 ? 0 : static_cast(secs) * 1000000000ull + t.nanosecond; +} + +void kal_time_sleep(kal_duration ns) { + const okuefi::state& s = okuefi::self(); + if (!s.st) return; + kal_duration us = (ns + 999) / 1000; + while (us) { + const kal_duration step = us > 1000000 ? 1000000 : us; + s.st->boot_services->stall(static_cast(step)); + us -= step; + } +} + +kal_uintptr kal_time_props(void) { return KAL_TIME_PROP_WALL_AVAILABLE | KAL_TIME_PROP_SLEEP_PRECISE; } + +} // extern "C" diff --git a/src/uefi.h b/src/uefi.h index f826fe7..d8ade89 100644 --- a/src/uefi.h +++ b/src/uefi.h @@ -25,6 +25,9 @@ #define OPENKAL_UEFI_H using efi_status = unsigned long long; +using efi_u8 = unsigned char; +using efi_u16 = unsigned short; +using efi_u32 = unsigned int; using efi_uintn = unsigned long long; using efi_handle = void*; using efi_char16 = char16_t; @@ -84,7 +87,7 @@ struct efi_boot_services { void* install_protocol_interface; /* 128 */ void* reinstall_protocol_interface; /* 136 */ void* uninstall_protocol_interface; /* 144 */ - void* handle_protocol; /* 152 */ + efi_status (*handle_protocol)(efi_handle, const struct efi_guid*, void**); /* 152 */ void* reserved; /* 160 */ void* register_protocol_notify; /* 168 */ void* locate_handle; /* 176 */ @@ -97,7 +100,9 @@ struct efi_boot_services { void* unload_image; /* 224 */ void* exit_boot_services; /* 232 */ void* get_next_monotonic_count; /* 240 */ - void* stall; /* 248 */ + efi_status (*stall)(efi_uintn microseconds); /* 248 */ + efi_status (*set_watchdog_timer)(efi_uintn seconds, unsigned long long code, + efi_uintn data_size, efi_char16* data); /* 256 */ }; static_assert(__builtin_offsetof(efi_boot_services, allocate_pool) == 64, @@ -106,6 +111,49 @@ static_assert(__builtin_offsetof(efi_boot_services, free_pool) == 72); static_assert(__builtin_offsetof(efi_boot_services, exit) == 216, "EFI_BOOT_SERVICES.Exit follows StartImage"); static_assert(__builtin_offsetof(efi_boot_services, exit_boot_services) == 232); +static_assert(__builtin_offsetof(efi_boot_services, handle_protocol) == 152); +static_assert(__builtin_offsetof(efi_boot_services, stall) == 248); +static_assert(__builtin_offsetof(efi_boot_services, set_watchdog_timer) == 256); + +struct efi_guid { efi_u32 a; efi_u16 b, c; efi_u8 d[8]; }; + +/* EFI_TIME: the wall clock as the firmware keeps it, in its own time zone. */ +struct efi_time { + efi_u16 year; + efi_u8 month, day, hour, minute, second, pad1; + efi_u32 nanosecond; + short time_zone; /* minutes from UTC; 2047: unspecified (local) */ + efi_u8 daylight, pad2; +}; +static_assert(sizeof(efi_time) == 16); + +/* As far as GetTime, the first service after the header. */ +struct efi_runtime_services { + efi_table_header hdr; /* 0 */ + efi_status (*get_time)(efi_time*, void* capabilities); /* 24 */ +}; +static_assert(__builtin_offsetof(efi_runtime_services, get_time) == 24); + +/* EFI_LOADED_IMAGE_PROTOCOL: what the firmware recorded about this image, and + * among it the options it was started with (UCS-2 text when a shell or a boot + * option gives a command line). */ +struct efi_loaded_image_protocol { + efi_u32 revision; + efi_handle parent_handle; + void* system_table; + efi_handle device_handle; + void* file_path; + void* reserved; + efi_u32 load_options_size; + void* load_options; + void* image_base; + unsigned long long image_size; +}; +static_assert(__builtin_offsetof(efi_loaded_image_protocol, load_options_size) == 48); +static_assert(__builtin_offsetof(efi_loaded_image_protocol, load_options) == 56); + +inline constexpr efi_guid kLoadedImageProtocol = { + 0x5B1B31A1, 0x9562, 0x11D2, {0x8E, 0x3F, 0x00, 0xA0, 0xC9, 0x69, 0x72, 0x3B}}; struct efi_system_table { efi_table_header hdr; /* 0 */ @@ -118,7 +166,7 @@ struct efi_system_table { efi_simple_text_output_protocol* con_out;/* 64 */ efi_handle standard_error_handle; /* 72 */ efi_simple_text_output_protocol* std_err;/* 80 */ - void* runtime_services; /* 88 */ + efi_runtime_services* runtime_services; /* 88 */ efi_boot_services* boot_services; /* 96 */ }; diff --git a/src/version.cpp b/src/version.cpp index 92f70c4..d48f0c8 100644 --- a/src/version.cpp +++ b/src/version.cpp @@ -6,12 +6,11 @@ extern "C" { kal_u64 kal_version(void) { return KAL_VERSION; } -// Firmware supplies a console and a page allocator and nothing else this -// specification has an interface for. `openkal.env' is absent because a program -// started by firmware receives no arguments this implementation can report, and -// reporting none would be a claim rather than an absence. +// Firmware supplies a console, a pool, a clock and the options an image was started with. +// `openkal.process' waits for `openkal.fs' (a program is named through a directory handle); +// the rest have no firmware service beneath them here. kal_u64 kal_interfaces(void) { - return KAL_IFACE_ABORT | KAL_IFACE_STREAM | KAL_IFACE_MEMORY; + return KAL_IFACE_ABORT | KAL_IFACE_STREAM | KAL_IFACE_MEMORY | KAL_IFACE_ENV | KAL_IFACE_TIME; } } From ebbe2d09758615f9129b4f1a4467147feb029607 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 13 Nov 2026 14:29:04 +0900 Subject: [PATCH 2/2] ci: the macOS cross-build on macos-15 (macos-14 is no longer offered) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 227eaee..fa175b0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -278,7 +278,7 @@ jobs: strategy: fail-fast: false matrix: - os: [macos-14, windows-2022] + os: [macos-15, windows-2022] defaults: run: shell: bash