diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd436dc..fa175b0 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. @@ -280,7 +278,7 @@ jobs: strategy: fail-fast: false matrix: - os: [macos-14, windows-2022] + os: [macos-15, windows-2022] defaults: run: shell: bash @@ -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; } }