diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 900de8f..cda4011 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,27 +1,60 @@ name: CI +# What this workflow asserts. +# +# conformance the suite in the specification package runs against this +# implementation and every observation holds +# additions this package's own tests, which examine the operations +# version 0.5 added +# surface the exported names are exactly the fifty-one the +# specification lists, in both forms +# independence the objects reference no C library symbol +# +# The last is the property version 0.5 exists for, and it is asserted against +# the objects rather than against the source, because a source can reach a C +# library through a macro. +# +# Two compiler families, because an implementation is a contract's evidence and +# a contract that holds only under the compiler its author used is a description +# of that compiler. + on: push: branches: [main] pull_request: workflow_dispatch: +env: + MCPP_VERSION: 2026.8.19.4 + XLINGS_VERSION: v2026.8.17.2 + XLINGS_NON_INTERACTIVE: '1' + jobs: - build: - name: conformance + conformance: + name: conformance, surface, and independence (${{ matrix.toolchain }}) runs-on: ubuntu-24.04 - timeout-minutes: 30 - env: - # A version verified to build this package, not a measured minimum. The - # package uses modules, exported extern "C" declarations and ordinary - # dependencies, none of which is recent; the pin exists for reproducibility - # rather than because an older mcpp is known to fail. - MCPP_VERSION: 2026.8.19.4 - XLINGS_VERSION: v2026.8.17.2 - XLINGS_NON_INTERACTIVE: '1' + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + toolchain: ['gcc@16.1.0', 'llvm@22.1.8'] steps: - uses: actions/checkout@v4 + # The specification is checked out at the branch under test where it has + # one, so that this run asserts what it is for: that the specification as + # written there and this implementation as written here agree today. + - name: The specification + run: | + git clone --quiet https://github.com/mcpplibs/openkal.git .spec + branch='${{ github.head_ref || github.ref_name }}' + if git -C .spec rev-parse --verify --quiet "origin/$branch" > /dev/null; then + git -C .spec checkout --quiet "origin/$branch" + echo "the specification is at $branch" + else + echo "the specification has no $branch; its default branch is used" + fi + - name: Install xlings run: | curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh \ @@ -35,82 +68,108 @@ jobs: mcpp --version mcpp self config --mirror GLOBAL - # The version of the specification this implementation is written against - # is stated once, in the manifest, and read from there. Stating it a second - # time in this file is what allowed the two to drift: the manifest moved to - # 0.3.0 and this file compared against the 0.2.0 surface, so four names - # this implementation is required to export were reported as unspecified. - - name: Read the specification version from the manifest + # The compiler family and version for this row. mcpp keeps its toolchains + # in a sandbox of its own, so this selects rather than installs into the + # system, and `mcpp test' and `mcpp run' have no flag for it --- which is + # why it is set once here rather than passed to each command. + - name: Select the toolchain + run: | + spec='${{ matrix.toolchain }}' + case "$spec" in + msvc*) mcpp toolchain default msvc ;; + *) mcpp toolchain install "${spec%@*}" "${spec#*@}" + mcpp toolchain default "$spec" ;; + esac + mcpp toolchain list + + - name: Every interface, every kind of examination run: | - v="$(sed -n 's/^openkal[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' mcpp.toml | head -1)" - test -n "$v" || { echo "the manifest does not name openkal" >&2; exit 1; } - echo "OPENKAL_VERSION=$v" >> "$GITHUB_ENV" - echo "written against openkal $v" + bash .spec/tools/run-conformance.sh openkal-linux . full - - name: Conformance + - name: Point at the specification's working tree run: | - mcpp test 2>&1 | tee conformance.log + # run-conformance.sh has already done this; it is repeated because a + # reader of this file should not have to know that. + sed 's|^openkal = .*$|openkal = { path = ".spec" }|' mcpp.toml > mcpp.toml.next + mv mcpp.toml.next mcpp.toml + - name: This package's own tests + run: | + mcpp test 2>&1 | tee tests.log # A suite that discovered nothing reports success, so every suite is # asserted to have run. The list is derived from the files present # rather than written out here: a hand-written list names the suites - # that existed when it was written, and a suite added afterwards - # escapes the assertion silently. This assertion named two of five. + # that existed when it was written, and one added afterwards escapes + # the assertion silently. missing=0 for f in tests/*.cpp; do name="$(basename "$f" .cpp)" - if ! grep -q "^$name \.\.\. ok" conformance.log; then - echo "suite did not run, or did not pass: $name" >&2 - missing=1 - fi + grep -q "^$name \.\.\. ok" tests.log || { echo "did not run or did not pass: $name" >&2; missing=1; } done test "$missing" -eq 0 - - name: The specification, at the version the manifest names - uses: actions/checkout@v4 - with: - repository: mcpplibs/openkal - ref: ${{ env.OPENKAL_VERSION }} - path: .spec - - - name: The exported surface matches the specification + - name: The exported surface is complete and contains nothing else run: | - # Clause 9.3. The list and the checker come from the specification - # itself rather than from a copy kept here, so that the comparison has - # one source. --complete because this implementation claims every - # interface: a name it fails to export is a failure, not an interface - # it declines to provide. + rm -rf target && mcpp build bash .spec/tools/check-surface.sh --complete .spec/SURFACE.txt \ - $(find target -name '*.o' | tr '\n' ' ') - - # The program is taken from the specification rather than copied here. A - # copy in each implementation is a copy that can diverge, and the value of - # the program is precisely that every implementation runs the same one. - # - # The manifest is written here because naming the implementation is the - # manifest's job and not the program's, which is the arrangement the - # program exists to demonstrate. - - name: The portable program runs + $(find target -path '*/obj/*' -name '*.o' ! -name '*.m.o' ! -name 'conformance*' | tr '\n' ' ') + + - name: The C++ declarations are complete run: | - # Not `sed -i`: BSD sed requires a backup suffix after -i and reads - # the next word as one, so the GNU form fails on macOS with - # `invalid command code m`. This form is the same on both. - sed 's|^openkal = ".*"$|openkal = { path = ".spec" }|' mcpp.toml > mcpp.toml.next - mv mcpp.toml.next mcpp.toml - cat > .spec/examples/portable/mcpp.toml <<'TOML' - [package] - name = "portable" - version = "0.1.0" - - [dependencies] - openkal = { path = "../.." } - openkal-linux = { path = "../../.." } - TOML - cd .spec/examples/portable - mcpp run 2>&1 | tee run.log - # Both directions: that the program reported, and that nothing it - # observed failed to hold. Asserting only the first would pass for a - # program that printed its failures. - grep -q 'openkal: the portable program, above eight interfaces' run.log - grep -q 'openkal: observations that did not hold: 0' run.log - ! grep -q 'NOT HELD' run.log + # The specification's own tool examines the C form. The module form is + # examined here, where a build of the modules exists: the test names + # every entity SURFACE.txt lists, so a name the modules do not export + # fails to compile and the diagnostic names it. + list="$(grep -vE '^[[:space:]]*(#|$)' .spec/SURFACE.txt | sort -u)" + { + for m in types abort stream memory env time fs process task; do + echo "import openkal.$m;" + done + echo 'const void *const surface[] = {' + while read -r n; do [ -n "$n" ] && echo " (const void *)&$n,"; done <<< "$list" + echo '};' + echo 'int main() { return surface[0] == 0; }' + } > tests/conformance_declarations.cpp + mcpp test conformance_declarations 2>&1 | tee decl.log + grep -q '^conformance_declarations \.\.\. ok' decl.log + rm -f tests/conformance_declarations.cpp + + # The property this version exists for. An implementation beneath a + # program that supplies its own runtime must not reach for that runtime's + # names, or its calls resolve to the program's and recur without bound. + - name: The objects reference no C library symbol + run: | + rm -rf target && mcpp build --features standalone + objs=$(find target -path '*/obj/*' -name '*.o' ! -name '*.m.o' ! -name 'conformance*') + test -n "$objs" || { echo "no objects were found; the check would pass vacuously" >&2; exit 1; } + + # The permitted set. memcpy, memmove, memset and memcmp are the four a + # freestanding translation unit is permitted to require: a compiler + # emits calls to them from ordinary loops, and they compute rather + # than call, so none of them can re-enter this implementation. + # __libc_start_main and main are the hand-over, and are undefined here + # by construction. + permitted='^(memcpy|memmove|memset|memcmp|__libc_start_main|main|_GLOBAL_OFFSET_TABLE_|kal_[a-z_]+|__init_array_start|__init_array_end|__preinit_array_start|__preinit_array_end|_ZN3okl.*)$' + bad=0 + for s in $(nm --undefined-only $objs | awk '{print $2}' | sort -u); do + [ -n "$s" ] || continue + if ! printf '%s\n' "$s" | grep -qE "$permitted"; then + echo "the implementation references a symbol it must not: $s" >&2 + bad=1 + fi + done + test "$bad" -eq 0 + echo "the implementation references no C library symbol" + + # A checker is only useful if it fails when it should. + - name: The independence check detects a dependence + run: | + printf 'extern "C" int puts(const char*);\nextern "C" void okl_probe(void) { puts("x"); }\n' > src/probe.cpp + rm -rf target && mcpp build --features standalone + objs=$(find target -path '*/obj/*' -name '*.o' ! -name '*.m.o') + if ! nm --undefined-only $objs | awk '{print $2}' | grep -qx puts; then + echo "the probe did not produce the reference it was written to produce" >&2 + rm -f src/probe.cpp; exit 1 + fi + rm -f src/probe.cpp + echo "a dependence upon a C library is visible to the check" diff --git a/README.md b/README.md index 1fe7abf..18848ed 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,148 @@ # openkal-linux The reference implementation of [openkal](https://github.com/mcpplibs/openkal) -for Linux. It is a usable backend, and it is maintained as the worked example -that other implementations follow. - -## Interfaces provided - -| Module | Notes | -| --- | --- | -| `openkal.abort` | terminates through `_exit`, so that no exit handler runs | -| `openkal.stream` | descriptors 0, 1 and 2; writes are completed or reported | -| `openkal.memory` | built upon the C library allocator, as clause 7.3 requires | - -The package declares no module. The interface belongs to the specification -package, which this package imports in order to define what it declares. - -## Use +for Linux, written on the kernel's own system-call interface. ```toml [dependencies] -openkal = "0.3.0" +openkal = "0.5.0" [target.'cfg(os = "linux")'.dependencies] -openkal-linux = "0.3.0" +openkal-linux = "0.5.0" ``` -## Points of interest for other implementations +## Why it does not use a C library -The implementation is short, and the following aspects of it are the ones the -specification expects to be reproduced. +Version 0.4 of this implementation was written upon the C library of the host, +and that is a correct implementation of openkal. It is not a correct +implementation for every program above openkal, and the case where it fails is +the case the specification cares most about. -**The package exports no module.** An implementation contributes definitions. -The interface is the specification's, and an implementation that exported one -would place a name the consumer relies upon outside the specification's -control. +A C library ported onto openkal defines `write`, `malloc` and `open`. So does +the host's. Two definitions of one name cannot both be reached from one program, +so an implementation that calls the host's calls the ported one instead — and +the ported one calls openkal, which calls the implementation, which calls it +again. The recursion is unbounded and neither side reads as though anything is +wrong. -**Interruption is retried, not reported.** A caller cannot distinguish an -interrupted call from a genuine failure without knowledge of the platform. An -implementation that reports it produces short transfers on any system that -delivers signals, and such a defect is unlikely to appear during testing. - -**Errors are translated through a table.** The platform's error values are -mapped onto the closed set the specification defines. Translation preserves the -naturalness requirement of clause 7.1; reconstructing a foreign namespace would -not. - -**Allocation is built upon the C library allocator.** Clause 7.3 requires this. -The C library's formatted output is coupled to its own allocator, so an -implementation that introduced a second one would place two claimants on one -region of memory. - -**Positioning is absent, and its absence confirms the decomposition.** On Linux, -whether a stream can be repositioned is a property of the individual descriptor: -the same implementation succeeds for a regular file and fails for a pipe. Had -`openkal.stream` offered positioning, this implementation could neither claim it -honestly nor withhold it usefully. Clause 6.3 places positioning in -`openkal.fs`, whose resource is a descriptor, and writing this implementation is -what confirms that clause independently of the reasoning that produced it. +The remedy is not a linking arrangement. It is that an implementation which +sits beneath a C library must not depend upon one. This implementation contains +the kernel's calling convention and no reference to any C library symbol, and +CI asserts that by examining the undefined symbols of the objects it produces. + +A second consequence is less obvious and equally binding. A structure passed to +the kernel has the kernel's layout, which is not the layout of the same-named +structure in any particular C library: musl's `struct stat` and glibc's differ, +and an implementation compiled against one and linked with the other would read +the wrong fields. The kernel's own layouts are therefore declared in `src/sys.h`. + +The specification says an implementation may be built upon a C library, beneath +one, or without one. This is the first implementation that demonstrates the +second and third. + +## Interfaces provided + +All eight. `tools/check-surface.sh --complete` in the specification package +compares the exported names against `SURFACE.txt`. ## Conformance -`mcpp test` runs the suite. It verifies both halves of the claim: that the -operations provided behave as specified, and that the operation not provided is -absent, which is asserted at compile time. +The suite lives in the specification package and is the same suite every +implementation runs. That it is the same suite is the point: a conformance suite +that differed between implementations would be testing implementations rather +than the specification. + +```bash +git clone https://github.com/mcpplibs/openkal .spec +bash .spec/tools/run-conformance.sh openkal-linux . full +``` + +This package's own `tests/` are additional, and examine the operations version +0.5 added rather than repeating what the shared suite already observes. + +## The `standalone` feature + +Whether this implementation is the whole of the program's environment. + +openkal says nothing about what else a program contains, and there are two +arrangements. Ordinarily a program already carries a runtime of its own; that +runtime has already received control from the environment and already creates +execution contexts, and this implementation borrows both. That is the default. + +Sometimes there is no such runtime — because the program supplies one itself, or +because it has none. Then nothing in the program has received control and +nothing creates contexts, and this implementation does both: + +| | | +| --- | --- | +| the program entry | `_start`, which finds what the kernel left on the stack, establishes the thread pointer, and hands control to whatever the program calls its beginning | +| thread-local storage | a block per execution context, built from the program's own `PT_TLS` segment, installed through the register the processor reserves | +| execution contexts | `clone` directly | + +The feature is a statement about the program, not a smaller or faster variant of +this implementation, and the consumer that knows which arrangement holds is the +one that declares it. `openkal-musl` declares it, because a program above a C +library carries no other runtime by construction. + +## Points of interest for other implementations + +**Allocation is not built upon a C library's allocator.** Clause 7.3 requires +that where the environment already provides an allocator, this one be built upon +it. The environment here is the kernel, and the kernel provides mappings rather +than an allocator, so `src/memory.cpp` supplies one. The hazard the clause names +is two claimants upon one region on a system whose heap grows by extending a +single region — that is, `brk`. Nothing here uses `brk`, so a program containing +both this allocator and a C library's has two allocators drawing on disjoint +mappings, and neither can shorten the other's. + +**The working directory is reported under its own name.** `kal_fs_preopen(0)` +reports the absolute path rather than `"."`. A C library above openkal must both +resolve an absolute path and report one, and a name of `"."` leaves it able to +do only the first — which version 0.4 did, and which is why `getcwd` could not +have worked above it. + +**Interruption is retried, not reported.** A caller cannot distinguish an +interrupted call from a genuine failure without knowledge of the platform, and +an implementation that reports it produces short transfers on any system that +delivers signals — a defect unlikely to appear during testing. + +**Errors are translated through a table.** The kernel's values are mapped onto +the closed set the specification defines. Translation preserves the naturalness +requirement of clause 7.1; reconstructing a foreign namespace would not. + +**Positioning is absent from `openkal.stream`, and its absence is confirmed +here.** Whether a stream can be repositioned is a property of the individual +descriptor: the same implementation succeeds for a regular file and fails for a +pipe. Had `openkal.stream` offered positioning, this implementation could +neither claim it honestly nor withhold it usefully. + +**The compiler is not permitted to reach for a runtime the program may supply.** +The implementation is compiled without exceptions, without run-time type +information and without the stack protector, and the flags are attached to this package's own sources rather +than to the whole build: a module interface records the dialect it was compiled +under, so a translation unit compiled without exceptions cannot import one +compiled with them, and the conformance tests are consumers rather than parts of +the implementation. For the same reason the implementation includes the +specification's C headers rather than importing its modules. + +The four names the compiler may emit calls to — `memcpy`, `memmove`, `memset` +and `memcmp` — are the exception a freestanding implementation is permitted to +require, and they cannot re-enter this implementation because they compute +rather than call. + +## Architectures + +`x86_64` and `aarch64`. Adding one is `src/sys.h`: a calling convention, a table +of system-call numbers, the kernel's `struct stat`, and the two thread-pointer +conventions in `src/tls.h`. + +## Verification + +`mcpp test` runs six suites. Five cover the interfaces; the sixth covers the +operations version 0.5 added, and each of its assertions is written so that it +can fail — the three conditions `kal_fs_open` exists to express are observed by +their effect rather than by a return value, because a truncation that did not +happen leaves a longer file while every call reports success. ## License diff --git a/mcpp.toml b/mcpp.toml index 8d665f1..00c28e3 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,15 +1,56 @@ [package] namespace = "mcpplibs" name = "openkal-linux" -version = "0.4.0" -description = "The reference implementation of openkal for Linux. It is both a usable backend and the worked example other implementations follow." +version = "0.5.0" +description = "The reference implementation of openkal for Linux, written on the kernel's own system-call interface so that it can be placed beneath a C library as well as above one." license = "Apache-2.0" authors = ["mcpplibs"] repo = "https://github.com/mcpplibs/openkal-linux" [dependencies] -openkal = "0.4.0" +openkal = "0.5.0" # The package contributes definitions and no modules. The interface it # implements is declared by the specification package, which this package # imports and does not re-export. + +[build] +# No exception may propagate out of a C entry point, and no C library is +# available to unwind through in the configuration this implementation is +# written for. Neither facility is used, so neither is emitted. +# +# The stack protector is disabled for the same reason it is disabled in every +# C library's own lowest layer: the check reads a value from thread-local +# storage that this implementation is itself responsible for establishing, so +# a function that runs before that point cannot perform it. +# +# The flags are attached to this package's own sources rather than to the whole +# build. A module interface records the dialect it was compiled under, so a +# translation unit compiled without exceptions cannot import one compiled with +# them; the conformance tests import the specification's modules, and they are +# consumers rather than parts of the implementation. +flags = [ + { glob = "src/**", cxxflags = ["-fno-exceptions", "-fno-rtti", + "-fno-stack-protector", + "-fno-asynchronous-unwind-tables"] }, +] + +[features] +default = [] + +# Whether this implementation is the whole of the program's environment. +# +# openkal says nothing about what else a program contains, and there are two +# arrangements. Ordinarily a program already carries a runtime of its own, that +# runtime has already received control from the environment and already creates +# execution contexts, and this implementation borrows both. That is the default. +# +# Sometimes there is no such runtime --- because the program supplies one +# itself, or because it has none. Then nothing in the program has received +# control and nothing creates contexts, and this implementation does both. That +# is what `standalone' selects. +# +# It is a statement about the program, not a smaller or faster variant of the +# implementation, and the consumer that knows which arrangement holds is the +# one that declares it. +standalone = { defines = ["OKL_STANDALONE"] } diff --git a/src/abort.cpp b/src/abort.cpp index 07d1e33..f4e23bf 100644 --- a/src/abort.cpp +++ b/src/abort.cpp @@ -1,25 +1,26 @@ -#include -#include -import openkal.abort; +#include "sys.h" +#include extern "C" { [[noreturn]] void kal_abort(const char* msg, kal_uintptr len) { - if (msg != nullptr && len != 0) { - kal_uintptr done = 0; - while (done < len) { - const auto r = ::write(2, msg + done, len - done); - if (r <= 0) break; - done += static_cast(r); - } - } - ::abort(); + if (msg != nullptr && len != 0) okl::write_all(2, msg, len); + // The kernel's own means of stopping a program whose state has been + // declared impossible. A C library's abort() raises a signal so that a + // handler and a core dump follow; this implementation is beneath any C + // library and raises the same signal directly. + const okl_long tid = okl::sys(okl::nr_gettid); + const okl_long pid = okl::sys(okl::nr_getpid); + okl::sys(okl::nr_tgkill, pid, tid, 6 /* SIGABRT */); + for (;;) okl::sys(okl::nr_exit_group, 127); } -// _exit rather than exit. The specification requires immediate termination, -// and exit would run registered handlers and static destructors first. The -// difference is not observable in a small program and becomes observable in a -// large one, which is why it is stated rather than left to judgement. -[[noreturn]] void kal_exit(int code) { ::_exit(code); } +// Termination is immediate. The specification requires it, and the difference +// from a C library's exit --- which runs registered handlers and static +// destructors first --- is not observable in a small program and is observable +// in a large one, which is why it is stated rather than left to judgement. +[[noreturn]] void kal_exit(int code) { + for (;;) okl::sys(okl::nr_exit_group, code); +} } diff --git a/src/env.cpp b/src/env.cpp index 829c66b..09a64c5 100644 --- a/src/env.cpp +++ b/src/env.cpp @@ -1,45 +1,63 @@ -#include -import openkal.env; +#include "sys.h" +#include -namespace { +namespace okl { -int g_argc = 0; -char** g_argv = nullptr; -char** g_envp = nullptr; +// The vectors the kernel places on the stack at inception. They are recorded +// by whichever of the two entrances the program has: this implementation's own +// program entry, when the program is linked without a C library, and otherwise +// an initialiser, which every environment that starts a hosted program calls +// with the same three arguments. +// +// Reading the pseudo-file the kernel provides was the alternative. It requires +// a file system before the program has one, and reports the arguments as they +// were at inception rather than as the program received them. +int g_argc = 0; +char** g_argv = nullptr; +char** g_envp = nullptr; +okl_ulong* g_auxv = nullptr; -// The environment supplies these to a constructor. The alternative, reading the -// pseudo-file the kernel provides, would require a file system before the -// program has one and would report the arguments as they were at inception -// rather than as the program received them. -[[gnu::constructor]] void capture(int argc, char** argv, char** envp) { +void record(int argc, char** argv, char** envp) { g_argc = argc; g_argv = argv; g_envp = envp; + if (envp) { + char** e = envp; + while (*e) ++e; + g_auxv = reinterpret_cast(e + 1); + } } -kal_uintptr length(const char* s) { - kal_uintptr n = 0; while (s && s[n] != '\0') ++n; return n; +okl_ulong auxval(okl_ulong key) { + for (okl_ulong* a = g_auxv; a && a[0]; a += 2) if (a[0] == key) return a[1]; + return 0; } +} // namespace okl + +namespace { +[[gnu::constructor(101)]] void capture(int argc, char** argv, char** envp) { + if (okl::g_argv == nullptr) okl::record(argc, argv, envp); +} } // namespace extern "C" { -kal_uintptr kal_env_arg_count(void) { return static_cast(g_argc); } +kal_uintptr kal_env_arg_count(void) { return static_cast(okl::g_argc); } const char* kal_env_arg(kal_uintptr index, kal_uintptr* len) { - if (index >= static_cast(g_argc)) { if (len) *len = 0; return nullptr; } - const char* s = g_argv[index]; - if (len) *len = length(s); + if (index >= static_cast(okl::g_argc)) { if (len) *len = 0; return nullptr; } + const char* s = okl::g_argv[index]; + if (len) *len = okl::length(s); return s; } const char* kal_env_var(const char* name, kal_uintptr name_len, kal_uintptr* value_len) { - for (char** e = g_envp; e && *e; ++e) { + for (char** e = okl::g_envp; e && *e; ++e) { const char* entry = *e; kal_uintptr i = 0; while (i < name_len && entry[i] != '\0' && entry[i] == name[i]) ++i; if (i == name_len && entry[i] == '=') { const char* v = entry + name_len + 1; - if (value_len) *value_len = length(v); + if (value_len) *value_len = okl::length(v); return v; } } @@ -48,20 +66,20 @@ const char* kal_env_var(const char* name, kal_uintptr name_len, kal_uintptr* val } kal_uintptr kal_env_var_count(void) { - kal_uintptr n = 0; for (char** e = g_envp; e && *e; ++e) ++n; return n; + kal_uintptr n = 0; for (char** e = okl::g_envp; e && *e; ++e) ++n; return n; } const char* kal_env_var_at(kal_uintptr index, kal_uintptr* name_len, const char** value, kal_uintptr* value_len) { kal_uintptr n = 0; - for (char** e = g_envp; e && *e; ++e, ++n) { + for (char** e = okl::g_envp; e && *e; ++e, ++n) { if (n != index) continue; const char* entry = *e; kal_uintptr i = 0; while (entry[i] != '\0' && entry[i] != '=') ++i; if (name_len) *name_len = i; const char* v = entry[i] == '=' ? entry + i + 1 : entry + i; if (value) *value = v; - if (value_len) *value_len = length(v); + if (value_len) *value_len = okl::length(v); return entry; } return nullptr; diff --git a/src/fs.cpp b/src/fs.cpp index eeb9fd9..e7bcd49 100644 --- a/src/fs.cpp +++ b/src/fs.cpp @@ -1,89 +1,88 @@ -#include // renameat -#include -#include -#include -#include -#include -#include +#include "sys.h" #include "handle.h" -import openkal.fs; -import openkal.types; +#include +#include namespace { -int translate(int e) { - switch (e) { - case EBADF: case EINVAL: case EFAULT: case ENOENT: case ENOTDIR: return kal_err_invalid; - case EAGAIN: return kal_err_again; - case ENOMEM: return kal_err_no_memory; - case ENOSPC: case EFBIG: case EDQUOT: return kal_err_no_space; - case EACCES: case EPERM: case EROFS: return kal_err_permission; - case ENOSYS: case ENOTSUP: return kal_err_not_supported; - default: return kal_err_io; - } -} - -// A name is a single component or a sequence separated by a forward slash. It -// shall not begin with a separator and shall not contain a component that -// ascends: a program able to ascend from the directory it was given would not -// be confined by having been given it. -bool acceptable(const char* name, kal_uintptr len) { - if (len == 0 || name[0] == '/') return false; - kal_uintptr start = 0; - for (kal_uintptr i = 0; i <= len; ++i) { - if (i == len || name[i] == '/') { - const kal_uintptr n = i - start; - if (n == 0) return false; - if (n == 2 && name[start] == '.' && name[start + 1] == '.') return false; - start = i + 1; - } - } - return true; -} - -// The operations take a counted name; the system calls take a terminated one. -// A bounded copy is the whole of the adaptation, and it is not a compatibility -// layer: it converts a representation and reconstructs no namespace. -struct terminated { - char buf[4096]; - bool ok; - terminated(const char* s, kal_uintptr n) : ok(n < sizeof(buf)) { - if (ok) { for (kal_uintptr i = 0; i < n; ++i) buf[i] = s[i]; buf[n] = '\0'; } - } -}; - -} // namespace - -extern "C" { // The directories this implementation supplies. A hosted system does not // confine an ordinary program, so it supplies both the working directory and -// the whole file system; an implementation that confined a program would supply -// fewer, and the program would not be able to tell the difference except by +// the whole file system; an implementation that confined a program would +// supply fewer, and the program could not tell the difference except by // finding that a directory it wanted was absent. -namespace { +// +// The working directory is reported under the name the environment knows it +// by, which is its absolute path rather than ".". A C library above openkal +// must both resolve an absolute path and report one, and a name of "." leaves +// it able to do only the first. +struct preopen { const char* name; kal_uintptr len; okl_uptr handle; }; -struct preopen { const char* name; kal_uintptr len; const char* path; uintptr_t handle; }; +char g_cwd[4096]; preopen* table(kal_uintptr* count) { - static preopen t[] = { - { ".", 1, ".", 0 }, - { "/", 1, "/", 0 }, - }; + static preopen t[2]; static bool opened = false; if (!opened) { - for (auto& e : t) { - const int fd = ::open(e.path, O_RDONLY | O_DIRECTORY | O_CLOEXEC); - e.handle = okl::pack(fd); - } opened = true; + + const okl_long n = okl::sys(okl::nr_getcwd, reinterpret_cast(g_cwd), + static_cast(sizeof g_cwd)); + // getcwd reports the length including the terminator, and reports a + // failure when the directory has been removed. A program whose working + // directory no longer has a name still has the directory, so the + // handle is opened either way and only the name falls back. + okl_uptr cwd_len = 0; + if (!okl::failed(n) && n > 1) cwd_len = static_cast(n) - 1; + else { g_cwd[0] = '.'; g_cwd[1] = '\0'; cwd_len = 1; } + + const okl_long fd0 = okl::sys(okl::nr_openat, okl::at_fdcwd, + reinterpret_cast("."), + okl::o_rdonly | okl::o_directory | okl::o_cloexec, 0); + const okl_long fd1 = okl::sys(okl::nr_openat, okl::at_fdcwd, + reinterpret_cast("/"), + okl::o_rdonly | okl::o_directory | okl::o_cloexec, 0); + t[0] = { g_cwd, cwd_len, okl::failed(fd0) ? 0u : okl::pack(static_cast(fd0)) }; + t[1] = { "/", 1, okl::failed(fd1) ? 0u : okl::pack(static_cast(fd1)) }; } - if (count) *count = sizeof(t) / sizeof(t[0]); + if (count) *count = 2; return t; } +int kind_of(okl_u32 mode) { + switch (mode & okl::s_ifmt) { + case okl::s_ifreg: return kal_node_file; + case okl::s_ifdir: return kal_node_directory; + case okl::s_iflnk: return kal_node_link; + default: return kal_node_other; + } +} + +void fill_info(const okl::kstat& st, kal_node_info* out) { + *out = kal_node_info{ + static_cast(st.size), + static_cast(st.mtime_sec) * 1000000000u + + static_cast(st.mtime_nsec), + kind_of(st.mode), + (st.mode & 0200u) != 0 ? 1 : 0, + }; +} + +// Enumeration reads the kernel's own directory records. A C library's +// directory stream is not used, for the reason src/sys.h gives: the C library +// in a program above this one may be the one this implementation would be +// calling. +struct listing { + int fd; + okl_uptr used; + okl_uptr pos; + char buf[8192]; +}; + } // namespace +extern "C" { + kal_uintptr kal_fs_preopen_count(void) { kal_uintptr n = 0; table(&n); return n; } @@ -101,29 +100,56 @@ int kal_fs_preopen(kal_uintptr index, kal_dir* out, const char** name, kal_uintp int kal_fs_open_dir(kal_dir base, const char* name, kal_uintptr len, kal_dir* out) { const int b = okl::unpack(base.h); - if (b < 0 || !acceptable(name, len)) return kal_err_invalid; - terminated t(name, len); if (!t.ok) return kal_err_invalid; - const int fd = ::openat(b, t.buf, O_RDONLY | O_DIRECTORY | O_CLOEXEC); - if (fd < 0) return translate(errno); - *out = kal_dir{ okl::pack(fd) }; + if (b < 0 || out == nullptr || !okl::acceptable(name, len)) return kal_err_invalid; + okl::terminated t(name, len); if (!t.ok) return kal_err_invalid; + const okl_long fd = okl::sys(okl::nr_openat, b, reinterpret_cast(t.buf), + okl::o_rdonly | okl::o_directory | okl::o_cloexec, 0); + if (okl::failed(fd)) return okl::translate(fd); + *out = kal_dir{ okl::pack(static_cast(fd)) }; return kal_ok; } -int kal_fs_open_file(kal_dir base, const char* name, kal_uintptr len, - int write, int create, kal_file* out) { +int kal_fs_open(kal_dir base, const char* name, kal_uintptr len, + kal_uintptr flags, kal_file* out) { const int b = okl::unpack(base.h); - if (b < 0 || !acceptable(name, len)) return kal_err_invalid; - terminated t(name, len); if (!t.ok) return kal_err_invalid; - int flags = (write ? O_RDWR : O_RDONLY) | O_CLOEXEC; - if (create) flags |= O_CREAT | O_TRUNC; - const int fd = ::openat(b, t.buf, flags, 0666); - if (fd < 0) return translate(errno); - *out = kal_file{ okl::pack(fd) }; + if (b < 0 || out == nullptr || !okl::acceptable(name, len)) return kal_err_invalid; + okl::terminated t(name, len); if (!t.ok) return kal_err_invalid; + + const bool r = (flags & KAL_OPEN_READ) != 0; + const bool w = (flags & KAL_OPEN_WRITE) != 0; + okl_long f = w ? (r ? okl::o_rdwr : okl::o_wronly) : okl::o_rdonly; + f |= okl::o_cloexec; + if (flags & KAL_OPEN_CREATE) f |= okl::o_creat; + if (flags & KAL_OPEN_EXCLUSIVE) f |= okl::o_excl; + if (flags & KAL_OPEN_TRUNCATE) f |= okl::o_trunc; + if (flags & KAL_OPEN_APPEND) f |= okl::o_append; + + const okl_long fd = okl::sys(okl::nr_openat, b, reinterpret_cast(t.buf), f, 0666); + if (okl::failed(fd)) return okl::translate(fd); + *out = kal_file{ okl::pack(static_cast(fd)) }; return kal_ok; } -void kal_fs_close_dir (kal_dir d) { const int fd = okl::unpack(d.h); if (fd >= 0) { okl::retire(d.h); ::close(fd); } } -void kal_fs_close_file(kal_file f) { const int fd = okl::unpack(f.h); if (fd >= 0) { okl::retire(f.h); ::close(fd); } } +// The form the earlier version specified, defined in terms of the one above, +// which is what the specification records that an implementation ordinarily +// does. +int kal_fs_open_file(kal_dir base, const char* name, kal_uintptr len, + int write, int create, kal_file* out) { + kal_uintptr flags = KAL_OPEN_READ; + if (write) flags |= KAL_OPEN_WRITE; + if (create) flags |= KAL_OPEN_WRITE | KAL_OPEN_CREATE | KAL_OPEN_TRUNCATE; + return kal_fs_open(base, name, len, flags, out); +} + +void kal_fs_close_dir(kal_dir d) { + const int fd = okl::unpack(d.h); + if (fd >= 0) { okl::retire(d.h); okl::sys(okl::nr_close, fd); } +} + +void kal_fs_close_file(kal_file f) { + const int fd = okl::unpack(f.h); + if (fd >= 0) { okl::retire(f.h); okl::sys(okl::nr_close, fd); } +} // A file's stream is the file. The descriptor is what openkal.stream's handle // holds on this implementation, so no conversion is required and none is @@ -134,78 +160,124 @@ kal_uintptr kal_fs_stream(kal_file f) { return fd < 0 ? 0u : static_cast(fd); } -int kal_fs_seek(kal_file f, __INT64_TYPE__ offset, int whence, __UINT64_TYPE__* result) { +int kal_fs_seek(kal_file f, kal_i64 offset, int whence, kal_u64* result) { const int fd = okl::unpack(f.h); if (fd < 0) return kal_err_invalid; - int w = SEEK_SET; - if (whence == kal::fs::seek_current) w = SEEK_CUR; - else if (whence == kal::fs::seek_end) w = SEEK_END; - const off_t r = ::lseek(fd, static_cast(offset), w); - if (r < 0) return translate(errno); - if (result) *result = static_cast<__UINT64_TYPE__>(r); + int w = 0; + if (whence == KAL_SEEK_CURRENT) w = 1; + else if (whence == KAL_SEEK_END) w = 2; + const okl_long r = okl::sys(okl::nr_lseek, fd, static_cast(offset), w); + if (okl::failed(r)) return okl::translate(r); + if (result) *result = static_cast(r); return kal_ok; } +int kal_fs_truncate(kal_file f, kal_u64 size) { + const int fd = okl::unpack(f.h); + if (fd < 0) return kal_err_invalid; + const okl_long r = okl::sys(okl::nr_ftruncate, fd, static_cast(size)); + return okl::failed(r) ? okl::translate(r) : kal_ok; +} + int kal_fs_info(kal_dir base, const char* name, kal_uintptr len, kal_node_info* out) { const int b = okl::unpack(base.h); - if (b < 0 || !acceptable(name, len) || out == nullptr) return kal_err_invalid; - terminated t(name, len); if (!t.ok) return kal_err_invalid; - struct stat st{}; - if (::fstatat(b, t.buf, &st, AT_SYMLINK_NOFOLLOW) != 0) { - if (errno == ENOENT) { *out = kal_node_info{ 0, 0, kal_node_absent, 0 }; return kal_ok; } - return translate(errno); + if (b < 0 || out == nullptr || !okl::acceptable(name, len)) return kal_err_invalid; + okl::terminated t(name, len); if (!t.ok) return kal_err_invalid; + okl::kstat st{}; + const okl_long r = okl::sys(okl::nr_newfstatat, b, reinterpret_cast(t.buf), + reinterpret_cast(&st), okl::at_symlink_nofollow); + if (okl::failed(r)) { + // Clause 7.7: enquiry about a name that does not exist is answered, + // not refused. A component of the name that is not a directory is the + // same answer, because the name still refers to nothing. + if (r == -okl::e_noent || r == -okl::e_notdir) { + *out = kal_node_info{ 0, 0, kal_node_absent, 0 }; + return kal_ok; + } + return okl::translate(r); } - int kind = kal_node_other; - if (S_ISREG(st.st_mode)) kind = kal_node_file; - else if (S_ISDIR(st.st_mode)) kind = kal_node_directory; - else if (S_ISLNK(st.st_mode)) kind = kal_node_link; - *out = kal_node_info{ - static_cast(st.st_size), - static_cast<__UINT64_TYPE__>(st.st_mtim.tv_sec) * 1000000000u - + static_cast<__UINT64_TYPE__>(st.st_mtim.tv_nsec), - kind, - (st.st_mode & S_IWUSR) != 0 ? 1 : 0, - }; + fill_info(st, out); + return kal_ok; +} + +int kal_fs_file_info(kal_file f, kal_node_info* out) { + const int fd = okl::unpack(f.h); + if (fd < 0 || out == nullptr) return kal_err_invalid; + okl::kstat st{}; + const okl_long r = okl::sys(okl::nr_fstat, fd, reinterpret_cast(&st)); + if (okl::failed(r)) return okl::translate(r); + fill_info(st, out); return kal_ok; } +int kal_fs_set_modified(kal_file f, kal_u64 modified_ns) { + const int fd = okl::unpack(f.h); + if (fd < 0) return kal_err_invalid; + // The kernel's operation takes two times and this interface names one, so + // the other is given the value that means "leave it alone". Setting the + // access time to the value openkal did not ask about would be an effect the + // caller did not request and could not have predicted. + constexpr okl_i64 utime_omit = 0x3ffffffe; + okl::ktimespec times[2]; + times[0].sec = 0; times[0].nsec = utime_omit; + times[1].sec = static_cast(modified_ns / 1000000000u); + times[1].nsec = static_cast(modified_ns % 1000000000u); + // A null name and a descriptor is this kernel's spelling of "the open file + // itself" rather than "a name beneath it". + const okl_long r = okl::sys(okl::nr_utimensat, fd, 0, + reinterpret_cast(times), 0); + return okl::failed(r) ? okl::translate(r) : kal_ok; +} + int kal_fs_mkdir(kal_dir base, const char* name, kal_uintptr len) { const int b = okl::unpack(base.h); - if (b < 0 || !acceptable(name, len)) return kal_err_invalid; - terminated t(name, len); if (!t.ok) return kal_err_invalid; - return ::mkdirat(b, t.buf, 0777) == 0 ? kal_ok : translate(errno); + if (b < 0 || !okl::acceptable(name, len)) return kal_err_invalid; + okl::terminated t(name, len); if (!t.ok) return kal_err_invalid; + const okl_long r = okl::sys(okl::nr_mkdirat, b, reinterpret_cast(t.buf), 0777); + return okl::failed(r) ? okl::translate(r) : kal_ok; } int kal_fs_remove(kal_dir base, const char* name, kal_uintptr len) { const int b = okl::unpack(base.h); - if (b < 0 || !acceptable(name, len)) return kal_err_invalid; - terminated t(name, len); if (!t.ok) return kal_err_invalid; - if (::unlinkat(b, t.buf, 0) == 0) return kal_ok; - if (errno == EISDIR || errno == EPERM) { - if (::unlinkat(b, t.buf, AT_REMOVEDIR) == 0) return kal_ok; + if (b < 0 || !okl::acceptable(name, len)) return kal_err_invalid; + okl::terminated t(name, len); if (!t.ok) return kal_err_invalid; + okl_long r = okl::sys(okl::nr_unlinkat, b, reinterpret_cast(t.buf), 0); + if (!okl::failed(r)) return kal_ok; + // One operation removes a name, and the kernel distinguishes two kinds of + // name where this interface does not. + if (r == -okl::e_isdir || r == -okl::e_perm) { + const okl_long d = okl::sys(okl::nr_unlinkat, b, reinterpret_cast(t.buf), + okl::at_removedir); + if (!okl::failed(d)) return kal_ok; + r = d; } - return translate(errno); + return okl::translate(r); } int kal_fs_rename(kal_dir from, const char* a, kal_uintptr alen, kal_dir to, const char* b, kal_uintptr blen) { const int f = okl::unpack(from.h), t2 = okl::unpack(to.h); - if (f < 0 || t2 < 0 || !acceptable(a, alen) || !acceptable(b, blen)) return kal_err_invalid; - terminated ta(a, alen), tb(b, blen); + if (f < 0 || t2 < 0 || !okl::acceptable(a, alen) || !okl::acceptable(b, blen)) return kal_err_invalid; + okl::terminated ta(a, alen), tb(b, blen); if (!ta.ok || !tb.ok) return kal_err_invalid; - return ::renameat(f, ta.buf, t2, tb.buf) == 0 ? kal_ok : translate(errno); + const okl_long r = okl::sys(okl::nr_renameat, f, reinterpret_cast(ta.buf), + t2, reinterpret_cast(tb.buf)); + return okl::failed(r) ? okl::translate(r) : kal_ok; } -// Enumeration holds a directory stream. The iterator word carries its address, -// which is the implementation's own and is never interpreted by a caller. +// Enumeration holds a descriptor of its own, obtained by opening the directory +// through itself. A duplicate would share the file offset with the handle the +// caller holds, so two enumerations of one directory would consume each +// other's entries. int kal_fs_list_begin(kal_dir d, kal_uintptr* iter) { const int fd = okl::unpack(d.h); if (fd < 0 || iter == nullptr) return kal_err_invalid; - const int dup = ::dup(fd); - if (dup < 0) return translate(errno); - DIR* s = ::fdopendir(dup); - if (s == nullptr) { ::close(dup); return translate(errno); } - ::rewinddir(s); + const okl_long own = okl::sys(okl::nr_openat, fd, reinterpret_cast("."), + okl::o_rdonly | okl::o_directory | okl::o_cloexec, 0); + if (okl::failed(own)) return okl::translate(own); + auto* s = static_cast(kal_alloc(sizeof(listing), alignof(listing))); + if (s == nullptr) { okl::sys(okl::nr_close, own); return kal_err_no_memory; } + s->fd = static_cast(own); s->used = 0; s->pos = 0; *iter = reinterpret_cast(s); return kal_ok; } @@ -213,32 +285,41 @@ int kal_fs_list_begin(kal_dir d, kal_uintptr* iter) { int kal_fs_list_next(kal_dir, kal_uintptr* iter, const char** name, kal_uintptr* len, int* kind) { if (iter == nullptr || *iter == 0) return kal_err_invalid; - DIR* s = reinterpret_cast(*iter); + auto* s = reinterpret_cast(*iter); for (;;) { - errno = 0; - dirent* e = ::readdir(s); - if (e == nullptr) { - ::closedir(s); *iter = 0; - if (name) *name = nullptr; - if (len) *len = 0; - return errno == 0 ? kal_ok : translate(errno); + if (s->pos >= s->used) { + const okl_long r = okl::sys(okl::nr_getdents64, s->fd, + reinterpret_cast(s->buf), + static_cast(sizeof s->buf)); + if (okl::interrupted(r)) continue; + if (okl::failed(r) || r == 0) { + okl::sys(okl::nr_close, s->fd); + kal_free(s, sizeof(listing), alignof(listing)); + *iter = 0; + if (name) *name = nullptr; + if (len) *len = 0; + return okl::failed(r) ? okl::translate(r) : kal_ok; + } + s->used = static_cast(r); + s->pos = 0; } + auto* e = reinterpret_cast(s->buf + s->pos); + s->pos += e->reclen; // The two entries that name the directory and its parent are omitted. // They exist to support ascent, which this interface does not offer. - if (e->d_name[0] == '.' && (e->d_name[1] == '\0' - || (e->d_name[1] == '.' && e->d_name[2] == '\0'))) continue; - kal_uintptr n = 0; while (e->d_name[n] != '\0') ++n; - if (name) *name = e->d_name; - if (len) *len = n; - if (kind) *kind = e->d_type == DT_DIR ? kal_node_directory - : e->d_type == DT_REG ? kal_node_file - : e->d_type == DT_LNK ? kal_node_link : kal_node_other; + if (e->name[0] == '.' && (e->name[1] == '\0' + || (e->name[1] == '.' && e->name[2] == '\0'))) continue; + if (name) *name = e->name; + if (len) *len = okl::length(e->name); + if (kind) *kind = e->type == okl::dt_dir ? kal_node_directory + : e->type == okl::dt_reg ? kal_node_file + : e->type == okl::dt_lnk ? kal_node_link : kal_node_other; return kal_ok; } } const kal_uintptr kal_fs_props = - kal::fs::prop_case_sensitive | kal::fs::prop_modified_time - | kal::fs::prop_atomic_rename; + KAL_FS_PROP_CASE_SENSITIVE | KAL_FS_PROP_MODIFIED_TIME + | KAL_FS_PROP_ATOMIC_RENAME; } diff --git a/src/handle.h b/src/handle.h index 4799315..2ab36b8 100644 --- a/src/handle.h +++ b/src/handle.h @@ -6,7 +6,7 @@ // arithmetically from the word, and the array holds only generations, so the // implementation retains the property clause 7.1 requires. #pragma once -#include +#include "sys.h" namespace okl { @@ -17,21 +17,21 @@ inline unsigned* generations() { return g; } -inline uintptr_t pack(int fd) { +inline okl_uptr pack(int fd) { if (fd < 0 || fd >= kMaxDescriptor) return 0; - return (static_cast(generations()[fd]) << 32) - | (static_cast(fd) + 1u); + return (static_cast(generations()[fd]) << 32) + | (static_cast(fd) + 1u); } // Returns the descriptor, or -1 if the word does not name a live one. -inline int unpack(uintptr_t h) { +inline int unpack(okl_uptr h) { const int fd = static_cast(h & 0xffffffffu) - 1; if (fd < 0 || fd >= kMaxDescriptor) return -1; if (static_cast(h >> 32) != generations()[fd]) return -1; return fd; } -inline void retire(uintptr_t h) { +inline void retire(okl_uptr h) { const int fd = unpack(h); if (fd >= 0) ++generations()[fd]; } diff --git a/src/memory.cpp b/src/memory.cpp index 88c30e9..f3eacad 100644 --- a/src/memory.cpp +++ b/src/memory.cpp @@ -1,24 +1,146 @@ -#include -#include -import openkal.memory; +#include "sys.h" +#include + +// Clause 7.3 requires that where the environment already provides an +// allocator, this one be built upon it and not beside it. The environment of +// this implementation is the kernel, and the kernel provides mappings rather +// than an allocator, so an allocator is what this file supplies. +// +// The hazard the clause names is nevertheless worth checking against rather +// than dismissing. It is two independent claimants upon one region, on a +// system whose heap grows by extending a single region --- that is, `brk'. No +// allocation here uses `brk'. A program that contains both this allocator and +// a C library's therefore has two allocators drawing on disjoint mappings, and +// neither can shorten the other's. +// +// Version 0.4 called the host C library's `malloc'. That is correct for a +// program above a C library and wrong for a program that is one: the ported +// library defines `malloc' too, so the call resolves to it, and it in turn +// calls this implementation. See src/sys.h. + +namespace { + + +constexpr okl_uptr kPage = 4096; +constexpr okl_uptr kMinBlock = 16; +constexpr okl_uptr kMaxSmall = 32768; +constexpr okl_uptr kChunk = 1u << 20; +constexpr int kClasses = 12; // 16, 32, ... 32768 + +// Contention is rare --- the allocations of a C library above this one are +// served by that library's own allocator, and this one sees page-granular +// requests --- so a lock that spins is adequate and needs no suspension +// primitive, which would make openkal.memory depend upon openkal.task. +struct spin { + volatile int held = 0; + void lock() { + while (__atomic_exchange_n(&held, 1, __ATOMIC_ACQUIRE)) okl::sys(okl::nr_sched_yield); + } + void unlock() { __atomic_store_n(&held, 0, __ATOMIC_RELEASE); } +}; + +spin g_lock; +void* g_free[kClasses]; + +int class_of(okl_uptr n) { + okl_uptr s = kMinBlock; int c = 0; + while (s < n) { s <<= 1; ++c; } + return c; +} +okl_uptr size_of(int c) { return kMinBlock << c; } + +void* map(okl_uptr bytes) { + const okl_long r = okl::sys(okl::nr_mmap, 0, static_cast(bytes), + okl::prot_read | okl::prot_write, + okl::map_private | okl::map_anonymous, -1, 0); + if (okl::failed(r)) return nullptr; + return reinterpret_cast(r); +} + +void unmap(void* p, okl_uptr bytes) { + okl::sys(okl::nr_munmap, reinterpret_cast(p), static_cast(bytes)); +} + +okl_uptr round_up(okl_uptr n, okl_uptr to) { return (n + to - 1) & ~(to - 1); } + +// A chunk is page-aligned and a block size is a power of two no larger than a +// chunk, so a block at a multiple of its size is aligned to its size. That is +// why an alignment request is satisfied by choosing a larger class rather than +// by padding: no metadata is needed, and none is kept. +bool refill(int c) { + const okl_uptr bs = size_of(c); + auto* base = static_cast(map(kChunk)); + if (!base) return false; + for (okl_uptr off = kChunk; off >= bs; off -= bs) { + auto* b = base + off - bs; + *reinterpret_cast(b) = g_free[c]; + g_free[c] = b; + } + return true; +} + +} // namespace extern "C" { -// Built upon the C library's allocator rather than beside it, as the -// specification requires. The alternative places two claimants on one region -// of memory, and the C library's formatted output already draws on its own -// allocator, so the second claimant would not be idle. void* kal_alloc(kal_uintptr size, kal_uintptr align) { if (size == 0) return nullptr; - if (align <= alignof(::max_align_t)) return ::malloc(size); - // aligned_alloc requires the size to be a multiple of the alignment. - const kal_uintptr rounded = (size + align - 1) / align * align; - return ::aligned_alloc(align, rounded); + if (align < kMinBlock) align = kMinBlock; + + okl_uptr want = size < align ? align : size; + if (want <= kMaxSmall) { + const int c = class_of(want); + g_lock.lock(); + if (!g_free[c] && !refill(c)) { g_lock.unlock(); return nullptr; } + void* b = g_free[c]; + g_free[c] = *reinterpret_cast(b); + g_lock.unlock(); + return b; + } + + const okl_uptr bytes = round_up(size, kPage); + if (align <= kPage) return map(bytes); + + // An alignment wider than a page is satisfied by mapping more and + // recording, immediately before the region returned, what must be + // released. The record is reachable because the returned address is at + // least sixteen bytes above the mapping's base by construction. + const okl_uptr total = bytes + align; + auto* base = static_cast(map(total)); + if (!base) return nullptr; + auto addr = reinterpret_cast(base) + 16; + addr = (addr + align - 1) & ~(align - 1); + auto* user = reinterpret_cast(addr); + reinterpret_cast(user)[-1] = total; + reinterpret_cast(user)[-2] = reinterpret_cast(base); + return user; } -// The size and alignment are discarded. They are carried by the interface for -// the benefit of implementations whose allocators require them; on a platform -// whose allocator records its own metadata the parameters cost nothing. -void kal_free(void* p, kal_uintptr, kal_uintptr) { ::free(p); } +// The size and alignment are those passed to the allocation. The interface +// carries them so that an implementation need keep no record of its own, and +// this one keeps none: the class is recomputed from the size, and only the +// over-aligned case --- where the returned address is not the mapping's --- +// records anything at all. +void kal_free(void* p, kal_uintptr size, kal_uintptr align) { + if (!p || size == 0) return; + if (align < kMinBlock) align = kMinBlock; + + const okl_uptr want = size < align ? align : size; + if (want <= kMaxSmall) { + const int c = class_of(want); + g_lock.lock(); + *reinterpret_cast(p) = g_free[c]; + g_free[c] = p; + g_lock.unlock(); + return; + } + + if (align <= kPage) { unmap(p, round_up(size, kPage)); return; } + + auto* user = static_cast(p); + const okl_uptr total = reinterpret_cast(user)[-1]; + auto* base = reinterpret_cast(reinterpret_cast(user)[-2]); + unmap(base, total); +} } diff --git a/src/process.cpp b/src/process.cpp index cb4deac..aeb2b73 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -1,55 +1,49 @@ -#include -#include -#include -#include -#include -#include -#include +#include "sys.h" #include "handle.h" -import openkal.process; -import openkal.types; - -extern "C" char** environ; +#include +#include namespace { -int translate(int e) { - switch (e) { - case EBADF: case EINVAL: case ENOENT: return kal_err_invalid; - case ENOMEM: return kal_err_no_memory; - case EACCES: case EPERM: return kal_err_permission; - case ENOSYS: case ENOTSUP: return kal_err_not_supported; - default: return kal_err_io; - } -} -// A bounded copy of a counted string, as in the file system implementation. -struct terminated { - char buf[4096]; - bool ok; - terminated(const char* s, kal_uintptr n) : ok(n < sizeof(buf)) { - if (ok) { for (kal_uintptr i = 0; i < n; ++i) buf[i] = s[i]; buf[n] = '\0'; } - } -}; +constexpr okl_uptr kMaxEntries = 512; -// The counted arrays the interface takes are converted to the terminated arrays -// the environment takes. The conversion is bounded and is released on return. +// The counted arrays the interface takes become the terminated arrays the +// kernel takes. Every allocation happens before the program is duplicated, so +// that the duplicate performs nothing but two system calls: a duplicate of a +// program that has more than one execution context may hold a lock no context +// in it will release. struct vector { - char* storage[256]; - char* slots[257]; - kal_uintptr used = 0; + char** slots = nullptr; + char* bytes = nullptr; + okl_uptr slots_bytes = 0; + okl_uptr bytes_bytes = 0; bool ok = true; - bool add(const char* s, kal_uintptr n) { - if (used >= 256 || n >= 4096) { ok = false; return false; } - char* p = static_cast(::malloc(n + 1)); - if (p == nullptr) { ok = false; return false; } - for (kal_uintptr i = 0; i < n; ++i) p[i] = s[i]; - p[n] = '\0'; - storage[used] = p; slots[used] = p; ++used; slots[used] = nullptr; + bool build(const char** items, const kal_uintptr* lens, kal_uintptr n) { + if (n > kMaxEntries) { ok = false; return false; } + okl_uptr total = 0; + for (kal_uintptr i = 0; i < n; ++i) total += lens[i] + 1; + slots_bytes = (n + 1) * sizeof(char*); + bytes_bytes = total == 0 ? 1 : total; + slots = static_cast(kal_alloc(slots_bytes, alignof(char*))); + bytes = static_cast(kal_alloc(bytes_bytes, 1)); + if (!slots || !bytes) { ok = false; return false; } + okl_uptr at = 0; + for (kal_uintptr i = 0; i < n; ++i) { + okl::copy(bytes + at, items[i], lens[i]); + bytes[at + lens[i]] = '\0'; + slots[i] = bytes + at; + at += lens[i] + 1; + } + slots[n] = nullptr; return true; } - ~vector() { for (kal_uintptr i = 0; i < used; ++i) ::free(storage[i]); } + + ~vector() { + if (slots) kal_free(slots, slots_bytes, alignof(char*)); + if (bytes) kal_free(bytes, bytes_bytes, 1); + } }; } // namespace @@ -64,37 +58,46 @@ int kal_process_spawn(kal_dir base, kal_process* out) { const int b = okl::unpack(base.h); if (b < 0 || out == nullptr) return kal_err_invalid; - terminated p(path, path_len); + if (!okl::acceptable(path, path_len)) return kal_err_invalid; + okl::terminated p(path, path_len); if (!p.ok) return kal_err_invalid; - vector args, envs; // The vector is passed unaltered. Clause 7.6: argv[0] is the name the - // started program observes as its own, and it is the caller's to choose — + // started program observes as its own, and it is the caller's to choose --- // the started program reads it through kal_env_arg(0), so a caller that did // not supply it could not predict what the program would read. - for (kal_uintptr i = 0; i < argc; ++i) args.add(argv[i], argv_lens[i]); - for (kal_uintptr i = 0; i < envc; ++i) envs.add(envp[i], envp_lens[i]); - if (!args.ok || !envs.ok) return kal_err_no_memory; - - // The started program's working directory is the directory supplied here. - // The environment offers this as an attribute of the spawn rather than as a - // change to the caller, which is what makes it usable from several contexts. - posix_spawn_file_actions_t actions; - if (posix_spawn_file_actions_init(&actions) != 0) return kal_err_io; - posix_spawn_file_actions_addfchdir_np(&actions, b); - if (streams != nullptr) { - if (streams->in != 0) posix_spawn_file_actions_adddup2(&actions, static_cast(streams->in), 0); - if (streams->out != 0) posix_spawn_file_actions_adddup2(&actions, static_cast(streams->out), 1); - if (streams->err != 0) posix_spawn_file_actions_adddup2(&actions, static_cast(streams->err), 2); + vector args, envs; + if (!args.build(argv, argv_lens, argc)) return kal_err_no_memory; + if (!envs.build(envp, envp_lens, envc)) return kal_err_no_memory; + + const okl_long in = streams ? static_cast(streams->in) : 0; + const okl_long ou = streams ? static_cast(streams->out) : 0; + const okl_long er = streams ? static_cast(streams->err) : 0; + + // The image is duplicated and then replaced. openkal has no operation that + // duplicates the calling image, and this is why: the duplicate is not a + // resource the caller receives, it exists for the length of two system + // calls, and no environment without it could be asked to reproduce it. + const okl_long child = okl::sys(okl::nr_clone, 17 /* SIGCHLD */, 0, 0, 0, 0); + if (okl::failed(child)) return okl::translate(child); + + if (child == 0) { + if (in != 0) okl::sys(okl::nr_dup3, in, 0, 0); + if (ou != 0) okl::sys(okl::nr_dup3, ou, 1, 0); + if (er != 0) okl::sys(okl::nr_dup3, er, 2, 0); + // The started program's working directory is the directory supplied + // here, expressed by naming the program relative to it. There is no + // operation that changes a working directory afterwards, because a + // working directory that can be changed is shared mutable state + // between execution contexts. + okl::sys(okl::nr_execveat, b, reinterpret_cast(p.buf), + reinterpret_cast(args.slots), + reinterpret_cast(envs.slots), 0); + okl::sys(okl::nr_exit_group, 127); + for (;;) { } } - pid_t pid = 0; - const int rc = posix_spawn(&pid, p.buf, &actions, nullptr, - args.slots, envc == 0 ? environ : envs.slots); - posix_spawn_file_actions_destroy(&actions); - if (rc != 0) return translate(rc); - - *out = kal_process{ static_cast(pid) }; + *out = kal_process{ static_cast(child) }; return kal_ok; } @@ -102,15 +105,21 @@ int kal_process_wait(kal_process h, int* status, int* terminated_by_environment) if (h.h == 0) return kal_err_invalid; int st = 0; for (;;) { - const pid_t r = ::waitpid(static_cast(h.h), &st, 0); - if (r < 0) { if (errno == EINTR) continue; return translate(errno); } + const okl_long r = okl::sys(okl::nr_wait4, static_cast(h.h), + reinterpret_cast(&st), 0, 0); + if (okl::interrupted(r)) continue; + if (okl::failed(r)) return okl::translate(r); break; } - if (WIFEXITED(st)) { - if (status) *status = WEXITSTATUS(st); + // The encoding is the kernel's: the low seven bits name the signal that + // ended the program and are zero when it ended by returning, in which case + // the next eight bits are what it returned. + const int signalled = st & 0x7f; + if (signalled == 0) { + if (status) *status = (st >> 8) & 0xff; if (terminated_by_environment) *terminated_by_environment = 0; } else { - if (status) *status = WIFSIGNALED(st) ? WTERMSIG(st) : -1; + if (status) *status = signalled; if (terminated_by_environment) *terminated_by_environment = 1; } return kal_ok; @@ -118,7 +127,8 @@ int kal_process_wait(kal_process h, int* status, int* terminated_by_environment) int kal_process_terminate(kal_process h) { if (h.h == 0) return kal_err_invalid; - return ::kill(static_cast(h.h), SIGTERM) == 0 ? kal_ok : translate(errno); + const okl_long r = okl::sys(okl::nr_kill, static_cast(h.h), 15 /* SIGTERM */); + return okl::failed(r) ? okl::translate(r) : kal_ok; } // Releasing the handle does not affect the program. A program that has not been @@ -126,7 +136,7 @@ int kal_process_terminate(kal_process h) { void kal_process_close(kal_process) { } const kal_uintptr kal_process_props = - kal::process::prop_terminate | kal::process::prop_stream_passing - | kal::process::prop_exit_status; + KAL_PROCESS_PROP_TERMINATE | KAL_PROCESS_PROP_STREAM_PASSING + | KAL_PROCESS_PROP_EXIT_STATUS; } diff --git a/src/start.cpp b/src/start.cpp new file mode 100644 index 0000000..1e1758d --- /dev/null +++ b/src/start.cpp @@ -0,0 +1,121 @@ +// Program startup, for a program that carries no runtime of its own. +// +// Something must receive control from the kernel, find the arguments the kernel +// left on the stack, establish the thread pointer, and call the program. Where +// a program already carries a runtime, that runtime's first object does it and +// this file is not reached: the definition below is used only when the linker +// has an undefined `_start' to satisfy, which happens exactly when no other +// object provides one. +// +// It belongs to the implementation rather than to whatever sits above, and the +// reason is visible in what it does. Every step is a fact about this kernel --- +// the layout of the stack at inception, the program headers the kernel reports, +// the instruction that sets the thread pointer. A consumer that contained these +// steps would contain a copy of them per environment, which is what depending +// on openkal was meant to remove. +// +// Control is handed on through `__libc_start_main', which is the name the +// arrangement already has a name for. The symbol is weak: a program written +// directly against openkal has none, and then this file runs the initialisers +// and calls `main' itself. +#ifdef OKL_STANDALONE + +#include "sys.h" +#include "tls.h" +#include +#include + +namespace okl { +void record(int argc, char** argv, char** envp); +okl_ulong auxval(okl_ulong key); +} + +extern "C" { + +int main(int, char**, char**); + +[[gnu::weak]] int __libc_start_main(int (*)(int, char**, char**), int, char**, + void (*)(), void (*)(), void (*)()); + +[[noreturn]] void __okl_start_c(long* sp); + +} + +namespace { + +// The initialiser arrays the linker builds. Each is weak: a program with no +// initialisers has neither symbol, and the loop then runs zero times rather +// than failing to link. +using initialiser = void (*)(int, char**, char**); +extern "C" { +[[gnu::weak]] extern initialiser __preinit_array_start[]; +[[gnu::weak]] extern initialiser __preinit_array_end[]; +[[gnu::weak]] extern initialiser __init_array_start[]; +[[gnu::weak]] extern initialiser __init_array_end[]; +} + +void run_initialisers(int argc, char** argv, char** envp) { + for (initialiser* p = __preinit_array_start; p != __preinit_array_end; ++p) (*p)(argc, argv, envp); + for (initialiser* p = __init_array_start; p != __init_array_end; ++p) (*p)(argc, argv, envp); +} + +void* allocate(okl_uptr n, okl_uptr a) { return kal_alloc(n, a); } + +} // namespace + +#if defined(__x86_64__) +__asm__( +".text\n" +".globl _start\n" +".type _start,@function\n" +"_start:\n" +" xor %ebp,%ebp\n" // the outermost frame has no caller +" mov %rsp,%rdi\n" // the kernel left everything here +" and $-16,%rsp\n" +" call __okl_start_c\n" +" hlt\n" +".size _start,.-_start\n" +); +#elif defined(__aarch64__) +__asm__( +".text\n" +".globl _start\n" +".type _start,%function\n" +"_start:\n" +" mov x29,#0\n" +" mov x30,#0\n" +" mov x0,sp\n" +" and x1,x0,#-16\n" +" mov sp,x1\n" +" bl __okl_start_c\n" +" brk #0\n" +".size _start,.-_start\n" +); +#endif + +extern "C" [[noreturn]] void __okl_start_c(long* sp) { + const int argc = static_cast(sp[0]); + char** argv = reinterpret_cast(sp + 1); + char** envp = argv + argc + 1; + okl::record(argc, argv, envp); + + // The thread pointer must exist before anything that has thread-local + // state runs, which includes the C library's own initialisation. + okl::describe_tls(okl::auxval(3 /* AT_PHDR */), + okl::auxval(4 /* AT_PHENT */), + okl::auxval(5 /* AT_PHNUM */)); + const okl::tls_block b = okl::make_tls(allocate); + if (b.tp != nullptr) okl::set_thread_pointer(b.tp); + + if (__libc_start_main != nullptr) { + __libc_start_main(main, argc, argv, nullptr, nullptr, nullptr); + // A C library's hand-over does not return. Reaching here means one + // did, and continuing would run the program a second time. + kal_exit(127); + } + + run_initialisers(argc, argv, envp); + kal_exit(main(argc, argv, envp)); +} + +#endif // OKL_STANDALONE diff --git a/src/stream.cpp b/src/stream.cpp index 73de218..a5d5bd8 100644 --- a/src/stream.cpp +++ b/src/stream.cpp @@ -1,26 +1,5 @@ -#include -#include -import openkal.stream; - -namespace { - -// Platform error values are translated, not forwarded. The translation is a -// table; it does not reconstruct a foreign namespace, and the distinction is -// what keeps the implementation free of a compatibility layer. -int translate(int e) { - switch (e) { - case EBADF: case EINVAL: case EFAULT: return kal_err_invalid; - case EAGAIN: return kal_err_again; - case ENOMEM: return kal_err_no_memory; - case ENOSPC: case EFBIG: return kal_err_no_space; - case EACCES: case EPERM: return kal_err_permission; - case EPIPE: case ECONNRESET: return kal_err_closed; - case ENOTSUP: return kal_err_not_supported; - default: return kal_err_io; - } -} - -} // namespace +#include "sys.h" +#include extern "C" { @@ -32,16 +11,16 @@ kal_io_result kal_stream_write(kal_stream s, const void* buf, kal_uintptr len) { const auto* p = static_cast(buf); kal_uintptr done = 0; while (done < len) { - const auto r = ::write(static_cast(s.h), p + done, len - done); - if (r < 0) { - // An interrupted call is retried rather than reported. A caller - // cannot distinguish this condition from a genuine failure without - // knowledge of the platform, and an implementation that reports it - // produces short writes on any system that delivers signals --- - // a failure mode that a test suite is unlikely to reproduce. - if (errno == EINTR) continue; - return { done, translate(errno) }; - } + const okl_long r = okl::sys(okl::nr_write, static_cast(s.h), + reinterpret_cast(p + done), + static_cast(len - done)); + // An interrupted call is retried rather than reported. Clause 7.5: a + // caller cannot distinguish this condition from a genuine failure + // without knowledge of the environment, and an implementation that + // reports it produces short writes on any system that delivers + // signals --- a failure a test suite is unlikely to reproduce. + if (okl::interrupted(r)) continue; + if (okl::failed(r)) return { done, okl::translate(r) }; if (r == 0) break; done += static_cast(r); } @@ -50,22 +29,40 @@ kal_io_result kal_stream_write(kal_stream s, const void* buf, kal_uintptr len) { kal_io_result kal_stream_read(kal_stream s, void* buf, kal_uintptr len) { for (;;) { - const auto r = ::read(static_cast(s.h), buf, len); - if (r < 0) { - if (errno == EINTR) continue; - return { 0, translate(errno) }; - } - // A short read is reported as it occurred. Unlike a short write, it - // carries information the caller requires: a result of zero denotes - // end of input. + const okl_long r = okl::sys(okl::nr_read, static_cast(s.h), + reinterpret_cast(buf), + static_cast(len)); + if (okl::interrupted(r)) continue; + if (okl::failed(r)) return { 0, okl::translate(r) }; + // A short read is reported as it occurred. Unlike a short write it + // carries information the caller requires: zero denotes end of input. return { static_cast(r), kal_ok }; } } -int kal_stream_flush(kal_stream) { - // Descriptors are unbuffered at this level, so the operation has nothing - // to commit and reports success. - return kal_ok; +int kal_stream_flush(kal_stream s) { + // Nothing is buffered at this level, so there is nothing to commit for a + // stream that is not a file. For one that is, the durability the caller + // asked for is the kernel's to provide, and a failure to provide it is + // reported rather than concealed. A descriptor that cannot be synchronised + // --- a terminal, a pipe --- reports that, and reporting it as a failure + // would make every caller distinguish it from a real one. + const okl_long r = okl::sys(okl::nr_fsync, static_cast(s.h)); + if (!okl::failed(r)) return kal_ok; + if (r == -okl::e_inval || r == -okl::e_notty || r == -okl::e_badf) return kal_ok; + return okl::translate(r); +} + +kal_uintptr kal_stream_props(kal_stream s) { + // The enquiry the kernel offers is an attempt to read a terminal's + // settings: it succeeds for a terminal and reports ENOTTY otherwise. This + // is the same test every C library performs, and it is performed here so + // that the library above need not know which environment it is upon. + unsigned char termios[64] = { 0 }; + const okl_long r = okl::sys(okl::nr_ioctl, static_cast(s.h), + 0x5401 /* TCGETS */, + reinterpret_cast(termios)); + return okl::failed(r) ? kal_uintptr{0} : KAL_STREAM_PROP_INTERACTIVE; } } diff --git a/src/sys.h b/src/sys.h new file mode 100644 index 0000000..9aed92d --- /dev/null +++ b/src/sys.h @@ -0,0 +1,354 @@ +// The system call interface of the Linux kernel, and nothing else. +// +// openkal is a contract and says nothing about what else a program contains. +// That silence is the reason for this file. +// +// An implementation is selected by the program, and the program may itself +// supply the facilities the implementation would otherwise borrow. If it does, +// and the names agree, there is one definition of each name in the program: +// the implementation's calls resolve to the program's, and the program's +// resolve back to the implementation. The recursion is unbounded and it is not +// visible in either side's source. +// +// Version 0.4 of this implementation borrowed the host's C library. That is a +// correct implementation of openkal for a program that borrows nothing, and it +// is wrong for a program that supplies its own --- which the specification +// permits and clause 1 names first among the consumers it expects. The +// specification says an implementation may be built upon a C library, beneath +// one, or without one; only an implementation that borrows nothing can be the +// second and third. +// +// This implementation therefore contains the kernel's calling convention and no +// reference to a facility any program might also define. The property is +// asserted by CI, which examines the undefined symbols of the produced objects. +// +// A second consequence is less obvious and equally binding. A structure passed +// to the kernel has the kernel's layout, which is not the layout of the +// same-named structure in any particular runtime: two C libraries' `struct +// stat' differ, and an implementation compiled against one and linked into a +// program carrying the other would read the wrong fields. The kernel's own +// layouts are therefore declared here. +#pragma once + +using okl_long = long; +using okl_ulong = unsigned long; +using okl_u64 = unsigned long long; +using okl_i64 = long long; +using okl_u32 = unsigned; +using okl_uptr = __UINTPTR_TYPE__; + +namespace okl { + +// --- the calling convention ------------------------------------------------ + +#if defined(__x86_64__) + +inline okl_long sys(okl_long n) { + okl_ulong r; + __asm__ __volatile__("syscall" : "=a"(r) : "a"(n) : "rcx", "r11", "memory"); + return static_cast(r); +} +inline okl_long sys(okl_long n, okl_long a) { + okl_ulong r; + __asm__ __volatile__("syscall" : "=a"(r) : "a"(n), "D"(a) : "rcx", "r11", "memory"); + return static_cast(r); +} +inline okl_long sys(okl_long n, okl_long a, okl_long b) { + okl_ulong r; + __asm__ __volatile__("syscall" : "=a"(r) : "a"(n), "D"(a), "S"(b) : "rcx", "r11", "memory"); + return static_cast(r); +} +inline okl_long sys(okl_long n, okl_long a, okl_long b, okl_long c) { + okl_ulong r; + __asm__ __volatile__("syscall" : "=a"(r) : "a"(n), "D"(a), "S"(b), "d"(c) + : "rcx", "r11", "memory"); + return static_cast(r); +} +inline okl_long sys(okl_long n, okl_long a, okl_long b, okl_long c, okl_long d) { + okl_ulong r; register okl_long r10 __asm__("r10") = d; + __asm__ __volatile__("syscall" : "=a"(r) : "a"(n), "D"(a), "S"(b), "d"(c), "r"(r10) + : "rcx", "r11", "memory"); + return static_cast(r); +} +inline okl_long sys(okl_long n, okl_long a, okl_long b, okl_long c, okl_long d, okl_long e) { + okl_ulong r; register okl_long r10 __asm__("r10") = d; register okl_long r8 __asm__("r8") = e; + __asm__ __volatile__("syscall" : "=a"(r) : "a"(n), "D"(a), "S"(b), "d"(c), "r"(r10), "r"(r8) + : "rcx", "r11", "memory"); + return static_cast(r); +} +inline okl_long sys(okl_long n, okl_long a, okl_long b, okl_long c, okl_long d, okl_long e, okl_long f) { + okl_ulong r; register okl_long r10 __asm__("r10") = d; register okl_long r8 __asm__("r8") = e; + register okl_long r9 __asm__("r9") = f; + __asm__ __volatile__("syscall" : "=a"(r) + : "a"(n), "D"(a), "S"(b), "d"(c), "r"(r10), "r"(r8), "r"(r9) + : "rcx", "r11", "memory"); + return static_cast(r); +} + +enum : okl_long { + nr_read = 0, nr_write = 1, nr_close = 3, nr_fstat = 5, nr_lseek = 8, + nr_mmap = 9, nr_mprotect = 10, nr_munmap = 11, nr_ioctl = 16, + nr_readv = 19, nr_writev = 20, nr_sched_yield = 24, nr_nanosleep = 35, + nr_getpid = 39, nr_clone = 56, nr_execve = 59, nr_exit = 60, nr_wait4 = 61, + nr_kill = 62, nr_ftruncate = 77, nr_getcwd = 79, nr_fsync = 74, + nr_arch_prctl = 158, nr_gettid = 186, nr_futex = 202, + nr_getdents64 = 217, nr_set_tid_address = 218, nr_clock_gettime = 228, + nr_clock_getres = 229, nr_exit_group = 231, nr_tgkill = 234, + nr_openat = 257, nr_mkdirat = 258, nr_newfstatat = 262, nr_unlinkat = 263, + nr_renameat = 264, nr_readlinkat = 267, nr_dup3 = 292, nr_execveat = 322, + nr_dup2 = 33, nr_utimensat = 280, +}; + +#elif defined(__aarch64__) + +#define OKL_SVC(...) \ + register okl_long x8 __asm__("x8") = n; \ + register okl_long x0 __asm__("x0") = a0; \ + __VA_ARGS__ \ + __asm__ __volatile__("svc 0" : "+r"(x0) : OKL_IN : "memory", "cc"); \ + return x0; + +inline okl_long sys(okl_long n) { + register okl_long x8 __asm__("x8") = n; register okl_long x0 __asm__("x0"); + __asm__ __volatile__("svc 0" : "=r"(x0) : "r"(x8) : "memory", "cc"); + return x0; +} +inline okl_long sys(okl_long n, okl_long a) { + register okl_long x8 __asm__("x8") = n; register okl_long x0 __asm__("x0") = a; + __asm__ __volatile__("svc 0" : "+r"(x0) : "r"(x8) : "memory", "cc"); + return x0; +} +inline okl_long sys(okl_long n, okl_long a, okl_long b) { + register okl_long x8 __asm__("x8") = n; register okl_long x0 __asm__("x0") = a; + register okl_long x1 __asm__("x1") = b; + __asm__ __volatile__("svc 0" : "+r"(x0) : "r"(x8), "r"(x1) : "memory", "cc"); + return x0; +} +inline okl_long sys(okl_long n, okl_long a, okl_long b, okl_long c) { + register okl_long x8 __asm__("x8") = n; register okl_long x0 __asm__("x0") = a; + register okl_long x1 __asm__("x1") = b; register okl_long x2 __asm__("x2") = c; + __asm__ __volatile__("svc 0" : "+r"(x0) : "r"(x8), "r"(x1), "r"(x2) : "memory", "cc"); + return x0; +} +inline okl_long sys(okl_long n, okl_long a, okl_long b, okl_long c, okl_long d) { + register okl_long x8 __asm__("x8") = n; register okl_long x0 __asm__("x0") = a; + register okl_long x1 __asm__("x1") = b; register okl_long x2 __asm__("x2") = c; + register okl_long x3 __asm__("x3") = d; + __asm__ __volatile__("svc 0" : "+r"(x0) : "r"(x8), "r"(x1), "r"(x2), "r"(x3) + : "memory", "cc"); + return x0; +} +inline okl_long sys(okl_long n, okl_long a, okl_long b, okl_long c, okl_long d, okl_long e) { + register okl_long x8 __asm__("x8") = n; register okl_long x0 __asm__("x0") = a; + register okl_long x1 __asm__("x1") = b; register okl_long x2 __asm__("x2") = c; + register okl_long x3 __asm__("x3") = d; register okl_long x4 __asm__("x4") = e; + __asm__ __volatile__("svc 0" : "+r"(x0) : "r"(x8), "r"(x1), "r"(x2), "r"(x3), "r"(x4) + : "memory", "cc"); + return x0; +} +inline okl_long sys(okl_long n, okl_long a, okl_long b, okl_long c, okl_long d, + okl_long e, okl_long f) { + register okl_long x8 __asm__("x8") = n; register okl_long x0 __asm__("x0") = a; + register okl_long x1 __asm__("x1") = b; register okl_long x2 __asm__("x2") = c; + register okl_long x3 __asm__("x3") = d; register okl_long x4 __asm__("x4") = e; + register okl_long x5 __asm__("x5") = f; + __asm__ __volatile__("svc 0" : "+r"(x0) + : "r"(x8), "r"(x1), "r"(x2), "r"(x3), "r"(x4), "r"(x5) + : "memory", "cc"); + return x0; +} + +enum : okl_long { + nr_getcwd = 17, nr_ioctl = 29, nr_mkdirat = 34, nr_unlinkat = 35, + nr_ftruncate = 46, nr_openat = 56, nr_close = 57, nr_getdents64 = 61, + nr_lseek = 62, nr_read = 63, nr_write = 64, nr_readv = 65, nr_writev = 66, + nr_readlinkat = 78, nr_newfstatat = 79, nr_fstat = 80, nr_fsync = 82, + nr_exit = 93, nr_exit_group = 94, nr_set_tid_address = 96, nr_futex = 98, + nr_nanosleep = 101, nr_clock_gettime = 113, nr_clock_getres = 114, + nr_sched_yield = 124, nr_kill = 129, nr_tgkill = 131, nr_gettid = 178, + nr_getpid = 172, nr_mmap = 222, nr_munmap = 215, nr_mprotect = 226, + nr_clone = 220, nr_execve = 221, nr_wait4 = 260, nr_renameat = 38, + nr_dup3 = 24, nr_execveat = 281, nr_dup2 = -1, + nr_arch_prctl = -1, nr_utimensat = 88, +}; + +#else +#error "openkal-linux supports x86_64 and aarch64" +#endif + +// --- error values, as the kernel returns them ------------------------------ +// +// A failing system call returns the error negated, which is why the values +// appear here rather than being taken from a header: the header belongs to a +// C library and this implementation has none. +enum : int { + e_perm = 1, e_noent = 2, e_intr = 4, e_io = 5, e_badf = 9, e_child = 10, + e_again = 11, e_nomem = 12, e_acces = 13, e_fault = 14, e_busy = 16, + e_exist = 17, e_xdev = 18, e_nodev = 19, e_notdir = 20, e_isdir = 21, + e_inval = 22, e_nfile = 23, e_mfile = 24, e_notty = 25, e_fbig = 27, + e_nospc = 28, e_spipe = 29, e_rofs = 30, e_pipe = 32, e_range = 34, + e_nametoolong = 36, e_nosys = 38, e_notempty = 39, e_loop = 40, + e_timedout = 110, e_connreset = 104, e_dquot = 122, +}; + +// --- constants the kernel defines ------------------------------------------ +enum : okl_long { + o_rdonly = 0, o_wronly = 1, o_rdwr = 2, + o_creat = 0100, o_excl = 0200, o_trunc = 01000, o_append = 02000, + o_directory = 0200000, o_cloexec = 02000000, o_nofollow = 0400000, + at_fdcwd = -100, at_removedir = 0x200, at_symlink_nofollow = 0x100, + prot_read = 1, prot_write = 2, prot_none = 0, + map_private = 2, map_anonymous = 0x20, map_stack = 0x20000, + clock_monotonic = 1, clock_realtime = 0, + futex_wait = 0, futex_wake = 1, futex_private = 128, +}; + +// --- translation ----------------------------------------------------------- +// +// The kernel's values are mapped onto the closed set the specification +// defines. A table preserves the naturalness clause 7.1 requires; +// reconstructing a foreign namespace would not. +inline int translate(okl_long r) { + const int e = static_cast(-r); + switch (e) { + case e_badf: case e_inval: case e_fault: case e_nametoolong: + case e_loop: case e_spipe: return 1; // kal_err_invalid + case e_again: return 2; // kal_err_again + case e_nomem: return 4; // kal_err_no_memory + case e_nospc: case e_fbig: case e_dquot: return 5; // kal_err_no_space + case e_acces: case e_perm: case e_rofs: return 6; // kal_err_permission + case e_nosys: return 7; // kal_err_not_supported + case e_pipe: case e_connreset: return 8; // kal_err_closed + case e_noent: case e_child: case e_nodev: return 9; // kal_err_not_found + case e_exist: case e_busy: return 10; // kal_err_exists + case e_notempty: return 11; // kal_err_not_empty + case e_isdir: return 12; // kal_err_is_directory + case e_notdir: return 13; // kal_err_not_directory + default: return 3; // kal_err_io + } +} + +inline bool failed(okl_long r) { + return r < 0 && r > -4096; +} + +// A call the kernel interrupts is retried rather than reported. Clause 7.5: a +// caller cannot distinguish an interrupted call from a genuine failure without +// knowledge of the environment, and an implementation that reports it produces +// short transfers on any system that delivers asynchronous notifications. +inline bool interrupted(okl_long r) { return r == -e_intr; } + +// --- the kernel's structure layouts ---------------------------------------- + +struct kstat { + okl_u64 dev; + okl_u64 ino; +#if defined(__x86_64__) + okl_u64 nlink; + okl_u32 mode; + okl_u32 uid; + okl_u32 gid; + okl_u32 pad0; + okl_u64 rdev; + okl_i64 size; + okl_i64 blksize; + okl_i64 blocks; +#else + okl_u64 rdev; + okl_u64 pad1; + okl_i64 size; + okl_u32 blksize; + okl_u32 pad2; + okl_i64 blocks; + okl_u32 nlink; + okl_u32 mode; + okl_u32 uid; + okl_u32 gid; + okl_u32 pad0; +#endif + okl_i64 atime_sec, atime_nsec; + okl_i64 mtime_sec, mtime_nsec; + okl_i64 ctime_sec, ctime_nsec; + okl_i64 unused[3]; +}; + +enum : okl_u32 { + s_ifmt = 0170000, s_ifreg = 0100000, s_ifdir = 0040000, s_iflnk = 0120000, +}; + +struct ktimespec { okl_i64 sec; okl_i64 nsec; }; + +struct kdirent64 { + okl_u64 ino; + okl_i64 off; + unsigned short reclen; + unsigned char type; + char name[]; +}; + +enum : unsigned char { dt_dir = 4, dt_reg = 8, dt_lnk = 10 }; + +// --- operations used by more than one interface ---------------------------- + +inline okl_long write_all(int fd, const void* p, okl_uptr n) { + const auto* b = static_cast(p); + okl_uptr done = 0; + while (done < n) { + const okl_long r = sys(nr_write, fd, reinterpret_cast(b + done), + static_cast(n - done)); + if (interrupted(r)) continue; + if (failed(r)) return r; + if (r == 0) break; + done += static_cast(r); + } + return static_cast(done); +} + +inline okl_uptr length(const char* s) { + okl_uptr n = 0; while (s && s[n]) ++n; return n; +} + +inline void copy(void* d, const void* s, okl_uptr n) { + auto* a = static_cast(d); + const auto* b = static_cast(s); + for (okl_uptr i = 0; i < n; ++i) a[i] = b[i]; +} + +inline void fill(void* d, unsigned char v, okl_uptr n) { + auto* a = static_cast(d); + for (okl_uptr i = 0; i < n; ++i) a[i] = v; +} + +// A name is a single component or a sequence separated by a forward slash. It +// shall not begin with a separator and shall not contain a component that +// ascends: a program able to ascend from the directory it was given would not +// be confined by having been given it. +// +// The rule belongs here rather than in the file system implementation because +// openkal.process names a program the same way, and a program started by a +// name that ascends is exactly the escape the rule exists to prevent. +inline bool acceptable(const char* name, okl_uptr len) { + if (name == nullptr || len == 0 || name[0] == '/') return false; + okl_uptr start = 0; + for (okl_uptr i = 0; i <= len; ++i) { + if (i == len || name[i] == '/') { + const okl_uptr n = i - start; + if (n == 0) return false; + if (n == 2 && name[start] == '.' && name[start + 1] == '.') return false; + start = i + 1; + } + } + return true; +} + +// A counted name becomes a terminated one for the kernel. The conversion is a +// change of representation, not a namespace being reconstructed. +struct terminated { + char buf[4096]; + bool ok; + terminated(const char* s, okl_uptr n) : ok(n < sizeof buf) { + if (ok) { copy(buf, s, n); buf[n] = '\0'; } + } +}; + +} // namespace okl diff --git a/src/task.cpp b/src/task.cpp index b4e7aad..fb3e820 100644 --- a/src/task.cpp +++ b/src/task.cpp @@ -1,101 +1,267 @@ +#include "sys.h" +#include "tls.h" +#include +#include + +// Execution contexts come from one of two places, and which one is a property +// of the program rather than of this implementation. +// +// A program that already carries a runtime has that runtime's threads, and a +// context obtained from them may call anything the program can call. This is +// the ordinary arrangement and it is the default. +// +// A program that carries no such runtime --- because it supplies one itself, or +// because it has none --- has nothing to obtain threads from, and this +// implementation creates them. The feature named `standalone' selects that. It +// is not an optimisation: in that arrangement the choice is between creating +// contexts here and not providing openkal.task at all. + +#ifdef OKL_STANDALONE + +// The child of a clone begins on a stack of its own with no return address, so +// the transfer cannot be written in C. The sequence is the one every C library +// uses, and it is short enough to read: the arguments are moved into the +// positions the system call takes, the function and its argument are placed on +// the child's stack, and the child calls the one with the other and then ends. +#if defined(__x86_64__) +__asm__( +".text\n" +".globl __okl_clone\n" +".hidden __okl_clone\n" +".type __okl_clone,@function\n" +"__okl_clone:\n" // rdi=fn rsi=stack rdx=flags rcx=arg r8=ptid r9=tls, 8(%rsp)=ctid +" mov $56,%eax\n" // SYS_clone +" mov %rdi,%r11\n" +" mov %rdx,%rdi\n" // flags +" mov %r8,%rdx\n" // ptid +" mov %r9,%r8\n" // tls +" mov 8(%rsp),%r10\n" // ctid +" mov %r11,%r9\n" // fn kept out of the way +" and $-16,%rsi\n" +" sub $8,%rsi\n" +" mov %rcx,(%rsi)\n" // arg, for the child to pop +" syscall\n" +" test %eax,%eax\n" +" jnz 1f\n" +" xor %ebp,%ebp\n" +" pop %rdi\n" +" call *%r9\n" +" mov %eax,%edi\n" +" mov $60,%eax\n" // SYS_exit, this context only +" syscall\n" +" hlt\n" +"1:ret\n" +".size __okl_clone,.-__okl_clone\n" +); +#elif defined(__aarch64__) +__asm__( +".text\n" +".globl __okl_clone\n" +".hidden __okl_clone\n" +".type __okl_clone,%function\n" +"__okl_clone:\n" // x0=fn x1=stack x2=flags x3=arg x4=ptid x5=tls x6=ctid +" and x1,x1,#-16\n" +" stp x0,x3,[x1,#-16]!\n" +" uxtw x0,w2\n" +" mov x2,x4\n" +" mov x3,x5\n" +" mov x4,x6\n" +" mov x8,#220\n" // SYS_clone +" svc #0\n" +" cbz x0,1f\n" +" ret\n" +"1:ldp x1,x0,[sp],#16\n" +" blr x1\n" +" mov x8,#93\n" // SYS_exit, this context only +" svc #0\n" +".size __okl_clone,.-__okl_clone\n" +); +#endif + +extern "C" okl_long __okl_clone(int (*fn)(void*), void* stack, int flags, void* arg, + int* ptid, void* tls, int* ctid); + +#else #include #include -#include -#include -#include -#include -#include -#include -import openkal.task; -import openkal.types; +#endif namespace { -int translate(int e) { - switch (e) { - case EINVAL: case ESRCH: case EFAULT: return kal_err_invalid; - case EAGAIN: return kal_err_again; - case ENOMEM: return kal_err_no_memory; - case EPERM: return kal_err_permission; - case ENOSYS: return kal_err_not_supported; - default: return kal_err_io; - } + +constexpr okl_uptr kStack = 256u * 1024u; + +struct context { + void (*entry)(void*); + void* arg; + void* stack; + okl_uptr stack_bytes; + okl::tls_block tls; + volatile int tid; // the kernel clears this when the context ends +#ifndef OKL_STANDALONE + unsigned long thread; +#endif +}; + +#ifdef OKL_STANDALONE + +void* alloc_bridge(okl_uptr n, okl_uptr a) { return kal_alloc(n, a); } + +int run(void* p) { + auto* c = static_cast(p); + c->entry(c->arg); + return 0; } -struct trampoline { void (*entry)(void*); void* arg; }; +#else void* run(void* p) { - trampoline* t = static_cast(p); - void (*entry)(void*) = t->entry; - void* arg = t->arg; - ::free(t); - entry(arg); + auto* c = static_cast(p); + c->entry(c->arg); return nullptr; } +int translate_posix(int e) { + switch (e) { + case okl::e_inval: case okl::e_fault: return kal_err_invalid; + case okl::e_again: return kal_err_again; + case okl::e_nomem: return kal_err_no_memory; + case okl::e_perm: return kal_err_permission; + default: return kal_err_io; + } +} + +#endif + } // namespace extern "C" { int kal_task_start(void (*entry)(void*), void* arg, kal_task* out) { if (entry == nullptr || out == nullptr) return kal_err_invalid; - trampoline* t = static_cast(::malloc(sizeof(trampoline))); - if (t == nullptr) return kal_err_no_memory; - t->entry = entry; t->arg = arg; + auto* c = static_cast(kal_alloc(sizeof(context), alignof(context))); + if (c == nullptr) return kal_err_no_memory; + okl::fill(c, 0, sizeof(context)); + c->entry = entry; c->arg = arg; + +#ifdef OKL_STANDALONE + c->stack_bytes = kStack; + c->stack = kal_alloc(kStack, 16); + c->tls = okl::make_tls(alloc_bridge); + if (c->stack == nullptr || c->tls.tp == nullptr) { + if (c->stack) kal_free(c->stack, kStack, 16); + if (c->tls.base) kal_free(c->tls.base, c->tls.bytes, c->tls.align); + kal_free(c, sizeof(context), alignof(context)); + return kal_err_no_memory; + } + // The context is a thread of this process: it shares the address space, + // the descriptors and the file system view, it is reaped without a wait, + // and the kernel clears `tid' and wakes anything suspended upon it when + // the context ends --- which is what kal_task_join waits for. + constexpr int flags = 0x00000100 // CLONE_VM + | 0x00000200 // CLONE_FS + | 0x00000400 // CLONE_FILES + | 0x00000800 // CLONE_SIGHAND + | 0x00010000 // CLONE_THREAD + | 0x00040000 // CLONE_SYSVSEM + | 0x00080000 // CLONE_SETTLS + | 0x00100000 // CLONE_PARENT_SETTID + | 0x00200000; // CLONE_CHILD_CLEARTID + auto* top = static_cast(c->stack) + kStack; + const okl_long r = __okl_clone(run, top, flags, c, + const_cast(&c->tid), c->tls.tp, + const_cast(&c->tid)); + if (okl::failed(r)) { + kal_free(c->stack, kStack, 16); + kal_free(c->tls.base, c->tls.bytes, c->tls.align); + kal_free(c, sizeof(context), alignof(context)); + return okl::translate(r); + } +#else pthread_t id{}; - const int rc = ::pthread_create(&id, nullptr, run, t); - if (rc != 0) { ::free(t); return translate(rc); } - *out = kal_task{ static_cast(id) }; + const int rc = ::pthread_create(&id, nullptr, run, c); + if (rc != 0) { kal_free(c, sizeof(context), alignof(context)); return translate_posix(rc); } + c->thread = static_cast(id); +#endif + + *out = kal_task{ reinterpret_cast(c) }; return kal_ok; } int kal_task_join(kal_task h) { - const int rc = ::pthread_join(static_cast(h.h), nullptr); - return rc == 0 ? kal_ok : translate(rc); + auto* c = reinterpret_cast(h.h); + if (c == nullptr) return kal_err_invalid; +#ifdef OKL_STANDALONE + // The kernel clears the word and wakes those suspended upon it after the + // context has left user space, so releasing its stack afterwards is safe: + // nothing in it can still be executing. + for (;;) { + const int t = __atomic_load_n(&c->tid, __ATOMIC_ACQUIRE); + if (t == 0) break; + okl::sys(okl::nr_futex, reinterpret_cast(&c->tid), + okl::futex_wait, t, 0, 0, 0); + } + kal_free(c->stack, c->stack_bytes, 16); + kal_free(c->tls.base, c->tls.bytes, c->tls.align); +#else + const int rc = ::pthread_join(static_cast(c->thread), nullptr); + if (rc != 0) return translate_posix(rc); +#endif + kal_free(c, sizeof(context), alignof(context)); + return kal_ok; } -void kal_task_yield(void) { ::sched_yield(); } +void kal_task_yield(void) { okl::sys(okl::nr_sched_yield); } kal_uintptr kal_task_current(void) { - return static_cast(::pthread_self()); + // The identity is the kernel's, read once per context. It is unique among + // contexts running at the same moment and may be reused after one ends, + // which is what the specification says of it. + static thread_local int cached = 0; + if (cached == 0) cached = static_cast(okl::sys(okl::nr_gettid)); + return static_cast(cached); } // The primitive. It is the operation a caller cannot construct: the comparison // and the suspension occur without an intervening opportunity for the value to // change unobserved, and only the environment can arrange that. -int kal_task_wait(const __UINT32_TYPE__* word, __UINT32_TYPE__ expected, - __UINT64_TYPE__ timeout_ns) { - timespec ts{}; - timespec* tp = nullptr; +int kal_task_wait(const kal_u32* word, kal_u32 expected, + kal_u64 timeout_ns) { + okl::ktimespec ts{}; + okl_long tp = 0; if (timeout_ns != 0) { - ts.tv_sec = static_cast(timeout_ns / 1000000000u); - ts.tv_nsec = static_cast(timeout_ns % 1000000000u); - tp = &ts; + ts.sec = static_cast(timeout_ns / 1000000000u); + ts.nsec = static_cast(timeout_ns % 1000000000u); + tp = reinterpret_cast(&ts); } for (;;) { - const long r = ::syscall(SYS_futex, const_cast<__UINT32_TYPE__*>(word), - FUTEX_WAIT_PRIVATE, expected, tp, nullptr, 0); - if (r == 0) return kal_ok; + const okl_long r = okl::sys(okl::nr_futex, reinterpret_cast(word), + okl::futex_wait | okl::futex_private, + static_cast(expected), tp, 0, 0); + if (!okl::failed(r)) return kal_ok; // The value had already changed, which is a successful outcome: the // caller's condition no longer holds and it should re-examine it. - if (errno == EAGAIN) return kal_ok; - if (errno == EINTR) continue; - if (errno == ETIMEDOUT) return kal_err_again; - return translate(errno); + if (r == -okl::e_again) return kal_ok; + if (okl::interrupted(r)) continue; + if (r == -okl::e_timedout) return kal_err_again; + return okl::translate(r); } } -int kal_task_wake(const __UINT32_TYPE__* word, kal_uintptr count, kal_uintptr* woken) { - const long r = ::syscall(SYS_futex, const_cast<__UINT32_TYPE__*>(word), - FUTEX_WAKE_PRIVATE, static_cast(count), - nullptr, nullptr, 0); - if (r < 0) return translate(errno); +int kal_task_wake(const kal_u32* word, kal_uintptr count, kal_uintptr* woken) { + const okl_long r = okl::sys(okl::nr_futex, reinterpret_cast(word), + okl::futex_wake | okl::futex_private, + static_cast(count), 0, 0, 0); + if (okl::failed(r)) return okl::translate(r); if (woken) *woken = static_cast(r); return kal_ok; } +// The thread-local position is reported in both configurations, and it is true +// in both for different reasons: the C library's threads establish the +// convention, and so does the block this implementation builds. Clause 7.10. const kal_uintptr kal_task_props = - kal::task::prop_preemptive | kal::task::prop_parallel - | kal::task::prop_wait_timeout; + KAL_TASK_PROP_PREEMPTIVE | KAL_TASK_PROP_PARALLEL + | KAL_TASK_PROP_WAIT_TIMEOUT | KAL_TASK_PROP_THREAD_LOCAL; } diff --git a/src/time.cpp b/src/time.cpp index 3229c5f..fd224f3 100644 --- a/src/time.cpp +++ b/src/time.cpp @@ -1,43 +1,53 @@ -#include -#include -import openkal.time; +#include "sys.h" +#include + +namespace { +kal_duration nanoseconds(const okl::ktimespec& t) { + return static_cast(t.sec) * 1000000000u + + static_cast(t.nsec); +} +} // namespace extern "C" { kal_duration kal_time_monotonic(void) { - timespec ts{}; - clock_gettime(CLOCK_MONOTONIC, &ts); - return static_cast(ts.tv_sec) * 1000000000u - + static_cast(ts.tv_nsec); + okl::ktimespec t{}; + okl::sys(okl::nr_clock_gettime, okl::clock_monotonic, reinterpret_cast(&t)); + return nanoseconds(t); } kal_duration kal_time_wall(void) { - timespec ts{}; - clock_gettime(CLOCK_REALTIME, &ts); - return static_cast(ts.tv_sec) * 1000000000u - + static_cast(ts.tv_nsec); + okl::ktimespec t{}; + okl::sys(okl::nr_clock_gettime, okl::clock_realtime, reinterpret_cast(&t)); + return nanoseconds(t); } kal_duration kal_time_monotonic_granularity(void) { - timespec ts{}; - if (clock_getres(CLOCK_MONOTONIC, &ts) != 0) return 1; - const auto ns = static_cast(ts.tv_sec) * 1000000000u - + static_cast(ts.tv_nsec); + okl::ktimespec t{}; + if (okl::failed(okl::sys(okl::nr_clock_getres, okl::clock_monotonic, + reinterpret_cast(&t)))) return 1; + const kal_duration ns = nanoseconds(t); return ns == 0 ? 1 : ns; } void kal_time_sleep(kal_duration ns) { - timespec req{ static_cast(ns / 1000000000u), - static_cast(ns % 1000000000u) }; + okl::ktimespec req{ static_cast(ns / 1000000000u), + static_cast(ns % 1000000000u) }; // The specification requires that the call not return early, so an - // interruption resumes the remainder rather than reporting it. - while (nanosleep(&req, &req) != 0 && errno == EINTR) { } + // interruption resumes the remainder rather than reporting it. The kernel + // writes what is left into the same structure, which is why it is not + // const. + for (;;) { + const okl_long r = okl::sys(okl::nr_nanosleep, reinterpret_cast(&req), + reinterpret_cast(&req)); + if (!okl::interrupted(r)) return; + } } -// CLOCK_MONOTONIC on this system does not advance while the machine is +// The monotonic clock of this kernel does not advance while the machine is // suspended, which the corresponding property records. const kal_uintptr kal_time_props = - kal::time::prop_wall_available | kal::time::prop_monotonic_suspends - | kal::time::prop_sleep_precise; + KAL_TIME_PROP_WALL_AVAILABLE | KAL_TIME_PROP_MONOTONIC_SUSPENDS + | KAL_TIME_PROP_SLEEP_PRECISE; } diff --git a/src/tls.h b/src/tls.h new file mode 100644 index 0000000..fd4a867 --- /dev/null +++ b/src/tls.h @@ -0,0 +1,108 @@ +// Thread-local storage for a program that has no C library to establish it. +// +// openkal reports, through kal_task_props, whether a context started by +// kal_task_start observes the thread-local storage of the toolchain that +// compiled the program. This implementation reports that it does, and this +// file is what makes the report true when the program is linked without a C +// library: the register that names the current context's storage is set by +// whoever creates the context, and in that configuration that is this +// implementation. +// +// The layout is the processor's, not the specification's. It is described by +// the psABI of each architecture and is reproduced here rather than obtained +// from a C library, for the reason src/sys.h gives. +#pragma once +#include "sys.h" + +namespace okl { + +// The program's own thread-local segment, as the loader described it. +struct tls_image { + const unsigned char* data; // the initialised part + okl_uptr filesz; + okl_uptr memsz; + okl_uptr align; + bool known; +}; + +inline tls_image& image() { static tls_image i{}; return i; } + +inline okl_uptr round_up(okl_uptr n, okl_uptr to) { return (n + to - 1) & ~(to - 1); } + +// Reads the program's own headers, which the kernel reports at inception. The +// loader has already applied any bias, so the addresses are the ones the +// program will use. +inline void describe_tls(okl_ulong phdr, okl_ulong phent, okl_ulong phnum) { + struct elf_phdr { + okl_u32 type, flags; + okl_u64 offset, vaddr, paddr, filesz, memsz, align; + }; + constexpr okl_u32 pt_tls = 7; + auto& im = image(); + im.known = true; + if (phdr == 0 || phnum == 0) return; + for (okl_ulong i = 0; i < phnum; ++i) { + const auto* p = reinterpret_cast(phdr + i * phent); + if (p->type != pt_tls) continue; + im.data = reinterpret_cast(p->vaddr); + im.filesz = static_cast(p->filesz); + im.memsz = static_cast(p->memsz); + im.align = p->align < 16 ? 16 : static_cast(p->align); + return; + } +} + +// How large a region one context's storage occupies, and where within it the +// thread pointer goes. Two conventions exist and both are represented, because +// the two architectures this implementation supports use one each. +struct tls_block { void* base; okl_uptr bytes; okl_uptr align; void* tp; }; + +inline tls_block make_tls(void* (*alloc)(okl_uptr, okl_uptr)) { + const auto& im = image(); + const okl_uptr align = im.align ? im.align : 16; + tls_block b{}; + b.align = align; + +#if defined(__x86_64__) + // Variant II: the storage lies below the thread pointer, and the word the + // thread pointer addresses holds the thread pointer itself, which is how a + // program obtains it without an instruction that reads the register. + const okl_uptr size = round_up(im.memsz, align); + b.bytes = size + 64; + b.base = alloc(b.bytes, align); + if (!b.base) return b; + auto* base = static_cast(b.base); + fill(base, 0, b.bytes); + // The linker measured every offset backwards from the thread pointer, so + // the initialised image sits at the start of the region and the thread + // pointer at its end. + if (im.data && im.filesz) copy(base, im.data, im.filesz); + b.tp = base + size; + // The word the thread pointer addresses holds the thread pointer itself. + // A program obtains it with one load and no instruction that reads the + // segment register, which is why the convention exists. + *reinterpret_cast(b.tp) = b.tp; +#elif defined(__aarch64__) + // Variant I: the storage lies above the thread pointer, after a gap of two + // words reserved by the procedure call standard. + const okl_uptr gap = round_up(16, align); + b.bytes = gap + round_up(im.memsz, align) + 64; + b.base = alloc(b.bytes, align); + if (!b.base) return b; + auto* base = static_cast(b.base); + fill(base, 0, b.bytes); + if (im.data && im.filesz) copy(base + gap, im.data, im.filesz); + b.tp = base; +#endif + return b; +} + +inline void set_thread_pointer(void* tp) { +#if defined(__x86_64__) + sys(nr_arch_prctl, 0x1002 /* ARCH_SET_FS */, reinterpret_cast(tp)); +#elif defined(__aarch64__) + __asm__ __volatile__("msr tpidr_el0, %0" :: "r"(tp)); +#endif +} + +} // namespace okl diff --git a/tests/conformance_additions.cpp b/tests/conformance_additions.cpp new file mode 100644 index 0000000..76d8234 --- /dev/null +++ b/tests/conformance_additions.cpp @@ -0,0 +1,150 @@ +// The operations version 0.5 added, and the declarations the modules export. +// +// Each assertion is written so that it can fail. The three conditions +// kal_fs_open exists to express are exactly the three that an implementation +// could appear to satisfy while satisfying nothing: a truncation that did not +// happen leaves a longer file, an exclusion that did not happen succeeds, and +// an append that did not happen overwrites. Each is therefore observed by its +// effect rather than by its return value alone. +#include +#include +import openkal.fs; +import openkal.stream; +import openkal.types; +import openkal.task; +import openkal.time; +import openkal.env; +import openkal.abort; +import openkal.memory; +import openkal.process; + +namespace { + +int failures = 0; + +void check(bool held, const char* what) { + if (!held) { std::printf("FAIL: %s\n", what); ++failures; } +} + +kal_dir here() { return kal::fs::working(); } + +// Writes the given bytes to a name, replacing whatever was there. +bool put(const char* name, const char* text) { + kal_file f{}; + const auto flags = kal::fs::open::write | kal::fs::open::create + | kal::fs::open::truncate; + if (kal::fs::open_file(here(), name, std::strlen(name), flags, &f) != kal_ok) return false; + const auto r = kal_stream_write(kal_stream{kal_fs_stream(f)}, text, std::strlen(text)); + kal_fs_close_file(f); + return r.e == kal_ok; +} + +// Reads a whole file into the buffer and reports its length, or -1. +long get(const char* name, char* buf, kal_uintptr cap) { + kal_file f{}; + if (kal::fs::open_file(here(), name, std::strlen(name), kal::fs::open::read, &f) != kal_ok) + return -1; + const auto r = kal_stream_read(kal_stream{kal_fs_stream(f)}, buf, cap); + kal_fs_close_file(f); + return r.e == kal_ok ? static_cast(r.n) : -1; +} + +} // namespace + +int main() { + const char* name = "okl-additions.tmp"; + const kal_uintptr n = std::strlen(name); + char buf[256]; + + // --- truncation on opening ---------------------------------------------- + check(put(name, "0123456789"), "a file is written"); + check(put(name, "abc"), "the same file is rewritten shorter"); + long len = get(name, buf, sizeof buf); + // Without truncation the file would still hold "abc3456789", which is the + // silent wrongness clause 7.8 describes: every call reported success. + check(len == 3, "opening with truncate discarded what lay beyond"); + + // --- exclusion ---------------------------------------------------------- + kal_file f{}; + const auto excl = kal::fs::open::write | kal::fs::open::create + | kal::fs::open::exclusive; + check(kal::fs::open_file(here(), name, n, excl, &f) == kal_err_exists, + "creating a name that exists is refused, and says which condition held"); + kal_fs_remove(here(), name, n); + check(kal::fs::open_file(here(), name, n, excl, &f) == kal_ok, + "creating a name that does not exist succeeds"); + kal_fs_close_file(f); + + // --- appending ---------------------------------------------------------- + check(put(name, "one"), "a file is written before appending"); + { + kal_file a{}; + const auto flags = kal::fs::open::write | kal::fs::open::append; + check(kal::fs::open_file(here(), name, n, flags, &a) == kal_ok, "a file opens for appending"); + // Positioning at the start and then writing must still append. An + // implementation that ignored the flag would produce "two" and report + // success for every call. + kal_u64 at = 0; + kal_fs_seek(a, 0, kal::fs::seek_set, &at); + kal_stream_write(kal_stream{kal_fs_stream(a)}, "two", 3); + kal_fs_close_file(a); + } + len = get(name, buf, sizeof buf); + check(len == 6 && std::memcmp(buf, "onetwo", 6) == 0, + "a write to a file opened for appending went to the end"); + + // --- the length of an open file ---------------------------------------- + { + kal_file t{}; + check(kal::fs::open_file(here(), name, n, kal::fs::open::write, &t) == kal_ok, + "a file opens for setting its length"); + check(kal_fs_truncate(t, 2) == kal_ok, "the length is set"); + kal_node_info info{}; + check(kal_fs_file_info(t, &info) == kal_ok, "an open file is enquired about"); + check(info.size == 2, "the enquiry reports the length that was set"); + check(info.kind == kal_node_file, "the enquiry reports what the handle refers to"); + check(kal_fs_truncate(t, 9) == kal_ok, "the length is extended"); + check(kal_fs_file_info(t, &info) == kal_ok && info.size == 9, + "extending reports the larger length"); + kal_fs_close_file(t); + } + + // --- absence is an answer, and is distinguishable ------------------------ + kal_fs_remove(here(), name, n); + kal_node_info gone{}; + check(kal_fs_info(here(), name, n, &gone) == kal_ok && gone.kind == kal_node_absent, + "enquiry about a name that does not exist is answered"); + check(kal::fs::open_file(here(), name, n, kal::fs::open::read, &f) == kal_err_not_found, + "opening a name that does not exist reports that it does not exist"); + + // --- a stream reports what a C library must know before writing --------- + // The value is not asserted: whether the test runs on a terminal is not a + // property of the implementation. What is asserted is that the enquiry + // answers for every standard stream and that the answer is confined to the + // positions the specification assigns. + const kal_stream standard[3] = { kal_stdin(), kal_stdout(), kal_stderr() }; + for (const kal_stream s : standard) { + const auto p = kal::properties(s); + check((p.bits & ~kal::stream_prop::interactive.bits) == 0, + "a stream reports no position the specification has not assigned"); + } + + // --- the property a C library cannot be ported without ------------------- + check(kal::task::has(kal::task::thread_local_storage), + "a started context observes thread-local storage"); + + // --- the capability words are of distinct types --------------------------- + // The following would compile in version 0.4 and answer a question nobody + // asked, because every capability word was a kal_uintptr: + // + // kal::time::has(kal::fs::links) + // + // It is now a diagnostic. A test cannot assert that something does not + // compile, so what is asserted here is the property that makes it so. + static_assert(!__is_same(kal::time::props, kal::fs::props)); + static_assert(!__is_same(kal::task::props, kal::process::props)); + static_assert(!__is_same(kal::fs::props, kal::fs::open_flags)); + + std::printf("openkal-linux: the operations version 0.5 added\n"); + return failures == 0 ? 0 : 1; +} diff --git a/tests/conformance_env_time.cpp b/tests/conformance_env_time.cpp index 68ed75f..9c53263 100644 --- a/tests/conformance_env_time.cpp +++ b/tests/conformance_env_time.cpp @@ -43,7 +43,7 @@ int main() { // The wall source is claimed by this implementation, so it must report a // time after the specification was written rather than zero. - check(kal::time::has(kal::time::prop_wall_available), "the wall source is claimed"); + check(kal::time::has(kal::time::wall_available), "the wall source is claimed"); check(kal::time::wall() > 1700000000ull * 1000000000ull, "the wall source reports a plausible time"); diff --git a/tests/conformance_fs.cpp b/tests/conformance_fs.cpp index 4255d02..ff2731a 100644 --- a/tests/conformance_fs.cpp +++ b/tests/conformance_fs.cpp @@ -40,7 +40,7 @@ int main() { const char payload[] = "conformance"; check(kal::write(s, payload, sizeof(payload) - 1).e == kal_ok, "the file is written"); - __UINT64_TYPE__ pos = 0; + kal_u64 pos = 0; check(kal_fs_seek(f, 0, kal::fs::seek_set, &pos) == kal_ok && pos == 0, "the file is repositioned"); char back[32] = {}; diff --git a/tests/conformance_process_task.cpp b/tests/conformance_process_task.cpp index 0bbaca8..ee867c8 100644 --- a/tests/conformance_process_task.cpp +++ b/tests/conformance_process_task.cpp @@ -18,16 +18,16 @@ void check(bool ok, const char* what) { // Shared between contexts. The word is what openkal.task suspends upon, and it // is ordinary memory: the interface adds no object of its own. -volatile __UINT32_TYPE__ g_word = 0; +volatile kal_u32 g_word = 0; int g_ran = 0; void worker(void* arg) { g_ran = 1; *static_cast(arg) = 42; - __atomic_store_n(reinterpret_cast<__UINT32_TYPE__*>(const_cast<__UINT32_TYPE__*>(&g_word)), + __atomic_store_n(reinterpret_cast(const_cast(&g_word)), 1u, __ATOMIC_SEQ_CST); kal_uintptr woken = 0; - kal_task_wake(const_cast(&g_word), 1, &woken); + kal_task_wake(const_cast(&g_word), 1, &woken); } } @@ -134,9 +134,9 @@ int main() { // Suspension upon the word, which the worker changes and then wakes. The // loop re-examines the condition after waking, because waking is permitted // to be spurious and the specification says so. - while (__atomic_load_n(reinterpret_cast<__UINT32_TYPE__*>( - const_cast<__UINT32_TYPE__*>(&g_word)), __ATOMIC_SEQ_CST) == 0u) { - kal_task_wait(const_cast(&g_word), 0u, + while (__atomic_load_n(reinterpret_cast( + const_cast(&g_word)), __ATOMIC_SEQ_CST) == 0u) { + kal_task_wait(const_cast(&g_word), 0u, 1000ull * 1000 * 1000); } check(kal_task_join(t) == kal_ok, "the context is joined"); @@ -144,7 +144,7 @@ int main() { // A wait whose expected value does not match returns rather than suspending, // which is what makes the primitive usable without losing a wake. - __UINT32_TYPE__ other = 5; + kal_u32 other = 5; check(kal_task_wait(&other, 6u, 1000ull * 1000) == kal_ok, "a wait on a value that already differs returns");